././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0264585 photutils-1.3.0/0000755000214200020070000000000000000000000012176 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/.bandit.yaml0000644000214200020070000000007300000000000014401 0ustar00lbradleyexclude_dirs: - photutils/*test* - photutils/**/*test* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9576511 photutils-1.3.0/.circleci/0000755000214200020070000000000000000000000014031 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/.circleci/config.yml0000644000214200020070000000055500000000000016026 0ustar00lbradleyversion: 2 jobs: build: docker: - image: quay.io/pypa/manylinux1_i686 steps: - checkout - run: name: Install dependencies for Python 3.7 command: /opt/python/cp37-cp37m/bin/pip install tox - run: name: Run tests for Python 3.7 command: /opt/python/cp37-cp37m/bin/python -m tox -e py37-test ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640123871.946704 photutils-1.3.0/.github/0000755000214200020070000000000000000000000013536 5ustar00lbradley././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9581943 photutils-1.3.0/.github/workflows/0000755000214200020070000000000000000000000015573 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638927624.0 photutils-1.3.0/.github/workflows/ci_tests.yml0000644000214200020070000000657300000000000020146 0ustar00lbradleyname: CI Tests on: push: pull_request: schedule: # run every Monday at 6am UTC - cron: '0 6 * * 1' env: TOXARGS: '-v' jobs: ci-tests: name: ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest python: '3.7' tox_env: 'py37-test-alldeps' - os: ubuntu-latest python: '3.8' tox_env: 'py38-test-alldeps' toxposargs: --remote-data=any - os: ubuntu-latest python: '3.9' tox_env: 'py39-test-alldeps-cov' - os: macos-latest python: '3.9' tox_env: 'py39-test-alldeps' - os: windows-latest python: '3.9' tox_env: 'py39-test-alldeps' - os: ubuntu-latest python: '3.9' tox_env: 'py39-test' - os: ubuntu-latest python: '3.9' tox_env: 'codestyle' - os: ubuntu-latest python: '3.9' tox_env: 'bandit' - os: ubuntu-latest python: '3.7' tox_env: 'py37-test-alldeps-astropylts-numpy118' steps: - name: Check out repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install base dependencies run: | python -m pip install --upgrade pip python -m pip install tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }} - name: Upload coverage to codecov if: "contains(matrix.tox_env, '-cov')" uses: codecov/codecov-action@v2 with: file: ./coverage.xml allowed_failures: name: (Allowed Failure) ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest python: '3.9' tox_env: 'py39-test-alldeps-devdeps' steps: - name: Check out repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install base dependencies run: | python -m pip install --upgrade pip python -m pip install tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests run: tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }} - name: Upload coverage to codecov if: "contains(matrix.tox_env, '-cov')" uses: codecov/codecov-action@v2 with: file: ./coverage.xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638927247.0 photutils-1.3.0/.github/workflows/cron_tests.yml0000644000214200020070000000270400000000000020504 0ustar00lbradleyname: Cron Tests on: schedule: # run at 6am UTC on Tue-Fri (complete tests are run every Monday) - cron: '0 6 * * 2-5' env: TOXARGS: '-v' jobs: cron-test: name: ${{ matrix.os }}, ${{ matrix.tox_env }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest python: '3.9' tox_env: 'linkcheck' - os: ubuntu-latest python: '3.9' tox_env: 'py39-test-alldeps-devdeps' steps: - name: Check out repository uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install base dependencies run: | python -m pip install --upgrade pip python -m pip install tox - name: Print Python, pip, setuptools, and tox versions run: | python -c "import sys; print(f'Python {sys.version}')" python -c "import pip; print(f'pip {pip.__version__}')" python -c "import setuptools; print(f'setuptools {setuptools.__version__}')" python -c "import tox; print(f'tox {tox.__version__}')" - name: Run tests if: "! matrix.use_remote_data" run: tox -e ${{ matrix.tox_env }} -- ${{ matrix.toxposargs }} - name: Upload coverage to codecov if: "contains(matrix.tox_env, '-cov')" uses: codecov/codecov-action@v2 with: file: ./coverage.xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/.gitignore0000644000214200020070000000130100000000000014161 0ustar00lbradley# Compiled files *.py[cod] *.a *.o *.so __pycache__ # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. *.c # Other generated files */version.py */cython_version.py MANIFEST htmlcov .coverage .ipynb_checkpoints .pytest_cache # Sphinx docs/_build docs/api # Packages/installer info *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg distribute-*.tar.gz pip-wheel-metadata # Other .cache .tox .*.sw[op] *~ # Eclipse editor project files .project .pydevproject .settings # PyCharm editor project files .idea # Visual Studio Code project files .vscode # Mac OSX .DS_Store ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768753.0 photutils-1.3.0/.lgtm.yml0000644000214200020070000000007100000000000013740 0ustar00lbradleyextraction: python: python_setup: version: 3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/.pep8speaks.yml0000644000214200020070000000100100000000000015052 0ustar00lbradleyscanner: linter: flake8 flake8: max-line-length: 100 ignore: - E741 # l and b are valid variable names for the galactic frame - E226 # Don't force "missing whitespace around arithmetic operator" - E402 # .conf has to be set in the __init__.py modules imports - W503 # line break before binary operator - W504 # we've been perpetually annoyed by W504 "line break after binary operator", since there's often no real alternative exclude: - _astropy_init.py - version.py ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/.readthedocs.yml0000644000214200020070000000037100000000000015265 0ustar00lbradleyversion: 2 build: image: latest sphinx: builder: html configuration: docs/conf.py fail_on_warning: true python: version: 3.8 install: - method: pip path: . extra_requirements: - docs - all formats: [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640121750.0 photutils-1.3.0/CHANGES.rst0000644000214200020070000015245300000000000014012 0ustar00lbradley1.3.0 (2021-12-21) ------------------ General ^^^^^^^ - The metadata in output tables now contains version information for all dependencies. [#1274] New Features ^^^^^^^^^^^^ - ``photutils.centroid`` - Extra keyword arguments can be input to ``centroid_sources`` that are then passed on to the ``centroid_func`` if supported. [#1276,#1278] - ``photutils.segmentation`` - Added ``copy`` method to ``SourceCatalog``. [#1264] - Added ``kron_photometry`` method to ``SourceCatalog``. [#1264] - Added ``add_extra_property``, ``remove_extra_property``, ``remove_extra_properties``, and ``rename_extra_property`` methods and ``extra_properties`` attribute to ``SourceCatalog``. [#1264, #1268] - Added ``name`` and ``overwrite`` keywords to ``SourceCatalog`` ``circular_photometry`` and ``fluxfrac_radius`` methods. [#1264] - ``SourceCatalog`` ``fluxfrac_radius`` was improved for cases where the source flux doesn't monotonically increase with increasing radius. [#1264] - Added ``meta`` and ``properties`` attributes to ``SourceCatalog``. [#1268] - The ``SourceCatalog`` output table (using ``to_table``) ``meta`` dictionary now includes a field for the date/time. [#1268] - Added ``SourceCatalog`` ``make_kron_apertures`` method. [#1268] - Added ``SourceCatalog`` ``plot_circular_apertures`` and ``plot_kron_apertures`` methods. [#1268] Bug Fixes ^^^^^^^^^ - ``photutils.segmentation`` - If ``detection_catalog`` is input to ``SourceCatalog`` then the detection centroids are used to calculate the ``circular_aperture``, ``circular_photometry``, and ``fluxfrac_radius``. [#1264] - Units are applied to ``SourceCatalog`` ``circular_photometry`` output if the input data has units. [#1264] - ``SourceCatalog`` ``circular_photometry`` returns scalar values if catalog is scalar. [#1264] - ``SourceCatalog`` ``fluxfrac_radius`` returns a ``Quantity`` with pixel units. [#1264] - Fixed a bug where the ``SourceCatalog`` ``detection_catalog`` was not indexed/sliced when ``SourceCatalog`` was indexed/sliced. [#1268] - ``SourceCatalog`` ``circular_photometry`` now returns NaN for completely-masked sources. [#1268] - ``SourceCatalog`` ``kron_flux`` is always NaN for sources where ``kron_radius`` is NaN. [#1268] - ``SourceCatalog`` ``fluxfrac_radius`` now returns NaN if ``kron_flux`` is zero. [#1268] API Changes ^^^^^^^^^^^ - ``photutils.centroids`` - A ``ValueError`` is now raised in ``centroid_sources`` if the input ``xpos`` or ``ypos`` is outside of the input ``data``. [#1276] - A ``ValueError`` is now raised in ``centroid_quadratic`` if the input ``xpeak`` or ``ypeak`` is outside of the input ``data``. [#1276] - NaNs are now returned from ``centroid_sources`` where the centroid failed. This is usually due to a ``box_size`` that is too small when using a fitting-based centroid function. [#1276] - ``photutils.segmentation`` - Renamed the ``SourceCatalog`` ``circular_aperture`` method to ``make_circular_apertures``. The old name is deprecated. [#1268] - The ``SourceCatalog`` ``kron_params`` keyword must have a minimum circular radius that is greater than zero. The default value is now 1.0. [#1268] - ``detect_sources`` now uses ``astropy.convolution.convolve``, which allows for masking pixels. [#1269] 1.2.0 (2021-09-23) ------------------ General ^^^^^^^ - The minimum required scipy version is 1.6.0 [#1239] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added a ``mask`` keyword to the ``area_overlap`` method. [#1241] - ``photutils.background`` - Improved the performance of ``Background2D`` by up to 10-50% when the optional ``bottleneck`` package is installed. [#1232] - Added a ``masked`` keyword to the background classes ``MeanBackground``, ``MedianBackground``, ``ModeEstimatorBackground``, ``MMMBackground``, ``SExtractorBackground``, ``BiweightLocationBackground``, ``StdBackgroundRMS``, ``MADStdBackgroundRMS``, and ``BiweightScaleBackgroundRMS``. [#1232] - Enable all background classes to work with ``Quantity`` inputs. [#1233] - Added a ``markersize`` keyword to the ``Background2D`` method ``plot_meshes``. [#1234] - Added ``__repr__`` methods to all background classes. [#1236] - Added a ``grid_mode`` keyword to ``BkgZoomInterpolator``. [#1239] - ``photutils.detection`` - Added a ``xycoords`` keyword to ``DAOStarFinder`` and ``IRAFStarFinder``. [#1248] - ``photutils.psf`` - Enabled the reuse of an output table from ``BasicPSFPhotometry`` and its subclasses as an initial guess for another photometry run. [#1251] - Added the ability to skip the ``group_maker`` step by inputing an initial guess table with a ``group_id`` column. [#1251] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Fixed a bug when converting between pixel and sky apertures with a ``gwcs`` object. [#1221] - ``photutils.background`` - Fixed an issue where ``Background2D`` could fail when using the ``'pad'`` edge method. [#1227] - ``photutils.detection`` - Fixed the ``DAOStarFinder`` import deprecation message. [#1195] - ``photutils.morphology`` - Fixed an issue in ``data_properties`` where a scalar background input would raise an error. [#1198] - ``photutils.psf`` - Fixed an issue in ``prepare_psf_model`` when ``xname`` or ``yname`` was ``None`` where the model offsets were applied in the wrong direction, resulting in the initial photometry guesses not being improved by the fit. [#1199] - ``photutils.segmentation`` - Fixed an issue in ``SourceCatalog`` where the user-input ``mask`` was ignored when ``apermask_method='correct'`` for Kron-related calculations. [#1210] - Fixed an issue in ``SourceCatalog`` where the ``segment`` array could incorrectly have units. [#1220] - ``photutils.utils`` - Fixed an issue in ``ShepardIDWInterpolator`` to allow its initialization with scalar data values and coordinate arrays having more than one dimension. [#1226] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - The ``ApertureMask.get_values()`` function now returns an empty array if there is no overlap with the data. [#1212] - Removed the deprecated ``BoundingBox.slices`` and ``PixelAperture.bounding_boxes`` attributes. [#1215] - ``photutils.background`` - Invalid data values (i.e., NaN or inf) are now automatically masked in ``Background2D``. [#1232] - The background classes ``MeanBackground``, ``MedianBackground``, ``ModeEstimatorBackground``, ``MMMBackground``, ``SExtractorBackground``, ``BiweightLocationBackground``, ``StdBackgroundRMS``, ``MADStdBackgroundRMS``, and ``BiweightScaleBackgroundRMS`` now return by default a ``numpy.ndarray`` with ``np.nan`` values representing masked pixels instead of a masked array. A masked array can be returned by setting ``masked=True``. [#1232] - Deprecated the ``Background2D`` attributes ``background_mesh_ma`` and ``background_rms_mesh_ma``. They have been renamed to ``background_mesh_masked`` and ``background_rms_mesh_masked``. [#1232] - By default, ``BkgZoomInterpolator`` now uses ``grid_mode=True``. For zooming 2D images, this keyword should be set to True, which makes the interpolator's behavior consistent with ``scipy.ndimage.map_coordinates``, ``skimage.transform.resize``, and ``OpenCV (cv2.resize)``. If backwards-compatiblity is needed with older Photutils' versions, set ``grid_mode=False``. [#1239] - ``photutils.centroid`` - Deprecated the ``gaussian1d_moments`` and ``centroid_epsf`` functions. [#1240] - ``photutils.datasets`` - Removed the deprecated ``random_state`` keyword in the ``apply_poisson_noise``, ``make_noise_image``, ``make_random_models_table``, and ``make_random_gaussians_table`` functions. [#1244] - ``make_random_models_table`` and ``make_random_gaussians_table`` now return an astropy ``QTable`` with version metadata. [#1247] - ``photutils.detection`` - ``DAOStarFinder``, ``IRAFStarFinder``, and ``find_peaks`` now return an astropy ``QTable`` with version metadata. [#1247] - The ``StarFinder`` ``label`` column was renamed to ``id`` for consistency with the other star finder classes. [#1254] - ``photutils.isophote`` - The ``Isophote`` ``to_table`` method nows return an astropy ``QTable`` with version metadata. [#1247] - ``photutils.psf`` - ``BasicPSFPhotometry``, ``IterativelySubtractedPSFPhotometry``, and ``DAOPhotPSFPhotometry`` now return an astropy ``QTable`` with version metadata. [#1247] - ``photutils.segmentation`` - Deprecated the ``filter_kernel`` keyword in the ``detect_sources``, ``deblend_sources``, and ``make_source_mask`` functions. It has been renamed to simply ``kernel`` for consistency with ``SourceCatalog``. [#1242] - Removed the deprecated ``random_state`` keyword in the ``make_cmap`` method. [#1244] - The ``SourceCatalog`` ``to_table`` method nows return an astropy ``QTable`` with version metadata. [#1247] - ``photutils.utils`` - Removed the deprecated ``check_random_state`` function. [#1244] - Removed the deprecated ``random_state`` keyword in the ``make_random_cmap`` function. [#1244] 1.1.0 (2021-03-20) ------------------ General ^^^^^^^ - The minimum required python version is 3.7. [#1120] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - The ``PixelAperture.plot()`` method now returns a list of ``matplotlib.patches.Patch`` objects. [#923] - Added an ``area_overlap`` method for ``PixelAperture`` objects that gives the overlapping area of the aperture on the data. [#874] - Added a ``get_overlap_slices`` method and a ``center`` attribute to ``BoundingBox``. [#1157] - Added a ``get_values`` method to ``ApertureMask`` that returns a 1D array of mask-weighted values. [#1158, #1161] - Added ``get_overlap_slices`` method to ``ApertureMask``. [#1165] - ``photutils.background`` - The ``Background2D`` class now accepts astropy ``NDData``, ``CCDData``, and ``Quantity`` objects as data inputs. [#1140] - ``photutils.detection`` - Added a ``StarFinder`` class to detect stars with a user-defined kernel. [#1182] - ``photutils.isophote`` - Added the ability to specify the output columns in the ``IsophoteList`` ``to_table`` method. [#1117] - ``photutils.psf`` - The ``EPSFStars`` class is now usable with multiprocessing. [#1152] - Slicing ``EPSFStars`` now returns an ``EPSFStars`` instance. [#1185] - ``photutils.segmentation`` - Added a modified, significantly faster, ``SourceCatalog`` class. [#1170, #1188, #1191] - Added ``circular_aperture`` and ``circular_phometry`` methods to the ``SourceCatalog`` class. [#1188] - Added ``fwhm`` property to the ``SourceCatalog`` class. [#1191] - Added ``fluxfrac_radius`` method to the ``SourceCatalog`` class. [#1192] - Added a ``bbox`` attribute to ``SegmentationImage``. [#1187] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Slicing a scalar ``Aperture`` object now raises an informative error message. [#1154] - Fixed an issue where ``ApertureMask.multiply`` ``fill_value`` was not applied to pixels outside of the aperture mask, but within the aperture bounding box. [#1158] - Fixed an issue where ``ApertureMask.cutout`` would raise an error if ``fill_value`` was non-finite and the input array was integer type. [#1158] - Fixed an issue where ``RectangularAnnulus`` with a non-default ``h_in`` would give an incorrect ``ApertureMask``. [#1160] - ``photutils.isophote`` - Fix computation of gradient relative error when gradient=0. [#1180] - ``photutils.psf`` - Fixed a bug in ``EPSFBuild`` where a warning was raised if the input ``smoothing_kernel`` was an ``numpy.ndarray``. [#1146] - Fixed a bug that caused photometry to fail on an ``EPSFmodel`` with multiple stars in a group. [#1135] - Added a fallback ``aperture_radius`` for PSF models without a FWHM or sigma attribute, raising a warning. [#740] - ``photutils.segmentation`` - Fixed ``SourceProperties`` ``local_background`` to work with Quantity data inputs. [#1162] - Fixed ``SourceProperties`` ``local_background`` for sources near the image edges. [#1162] - Fixed ``SourceProperties`` ``kron_radius`` for sources that are completely masked. [#1164] - Fixed ``SourceProperties`` Kron properties for sources near the image edges. [#1167] - Fixed ``SourceProperties`` Kron mask correction. [#1167] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - Deprecated the ``BoundingBox`` ``slices`` attribute. Use the ``get_overlap_slices`` method instead. [#1157] - ``photutils.centroid`` - Removed the deprecated ``fit_2dgaussian`` function and ``GaussianConst2D`` class. [#1147] - Importing tools from the centroids subpackage without including the subpackage name is deprecated. [#1190] - ``photutils.detection`` - Importing the ``DAOStarFinder``, ``IRAFStarFinder``, and ``StarFinderBase`` classes from the deprecated ``findstars.py`` module is now deprecated. These classes can be imported using ``from photutils.detection import ``. [#1173] - Importing the ``find_peaks`` function from the deprecated ``core.py`` module is now deprecated. This function can be imported using ``from photutils.detection import find_peaks``. [#1173] - ``photutils.morphology`` - Importing tools from the morphology subpackage without including the subpackage name is deprecated. [#1190] - ``photutils.segmentation`` - Deprecated the ``"mask_all"`` option in the ``SourceProperties`` ``kron_params`` keyword. [#1167] - Deprecated ``source_properties``, ``SourceProperties``, and ``LegacySourceCatalog``. Use the new ``SourceCatalog`` function instead. [#1170] - The ``detect_threshold`` function was moved to the ``segmentation`` subpackage. [#1171] - Removed the ability to slice ``SegmentationImage``. Instead slice the ``segments`` attribute. [#1187] 1.0.2 (2021-01-20) ------------------ General ^^^^^^^ - ``photutils.background`` - Improved the performance of ``Background2D`` (e.g., by a factor of ~4 with 2048x2048 input arrays when using the default interpolator). [#1103, #1108] Bug Fixes ^^^^^^^^^ - ``photutils.background`` - Fixed a bug with ``Background2D`` where using ``BkgIDWInterpolator`` would give incorrect results. [#1104] - ``photutils.isophote`` - Corrected calculations of upper harmonics and their errors [#1089] - Fixed bug that caused an infinite loop when the sample extracted from an image has zero length. [#1129] - Fixed a bug where the default ``fixed_parameters`` in ``EllipseSample.update()`` were not defined. [#1139] - ``photutils.psf`` - Fixed a bug where very incorrect PSF-fitting uncertainties could be returned when the astropy fitter did not return fit uncertainties. [#1143] - Changed the default ``recentering_func`` in ``EPSFBuilder``, to avoid convergence issues. [#1144] - ``photutils.segmentation`` - Fixed an issue where negative Kron radius values could be returned, which would cause an error when calculating Kron fluxes. [#1132] - Fixed an issue where an error was raised with ``SegmentationImage.remove_border_labels()`` with ``relabel=True`` when no segments remain. [#1133] 1.0.1 (2020-09-24) ------------------ Bug Fixes ^^^^^^^^^ - ``photutils.psf`` - Fixed checks on ``oversampling`` factors. [#1086] 1.0.0 (2020-09-22) ------------------ General ^^^^^^^ - The minimum required python version is 3.6. [#952] - The minimum required astropy version is 4.0. [#1081] - The minimum required numpy version is 1.17. [#1079] - Removed ``astropy-helpers`` and updated the package infrastructure as described in Astropy APE 17. [#915] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added ``b_in`` as an optional ellipse annulus keyword. [#1070] - Added ``h_in`` as an optional rectangle annulus keyword. [#1070] - ``photutils.background`` - Added ``coverage_mask`` and ``fill_value`` keyword options to ``Background2D``. [#1061] - ``photutils.centroids`` - Added quadratic centroid estimator function (``centroid_quadratic``). [#1067] - ``photutils.psf`` - Added the ability to use odd oversampling factors in ``EPSFBuilder``. [#1076] - ``photutils.segmentation`` - Added Kron radius, flux, flux error, and aperture to ``SourceProperties``. [#1068] - Added local background to ``SourceProperties``. [#1075] Bug Fixes ^^^^^^^^^ - ``photutils.isophote`` - Fixed a typo in the calculation of the ``b4`` higher-order harmonic coefficient in ``build_ellipse_model``. [#1052] - Fixed a bug where ``build_ellipse_model`` falls into an infinite loop when the pixel to fit is outside of the image. [#1039] - Fixed a bug where ``build_ellipse_model`` falls into an infinite loop under certain image/parameters input combinations. [#1056] - ``photutils.psf`` - Fixed a bug in ``subtract_psf`` caused by using a fill_value of np.nan with an integer input array. [#1062] - ``photutils.segmentation`` - Fixed a bug where ``source_properties`` would fail with unitless ``gwcs.wcs.WCS`` objects. [#1020] - ``photutils.utils`` - The ``effective_gain`` parameter in ``calc_total_error`` can now be zero (or contain zero values). [#1019] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - Aperture pixel positions can no longer be shaped as 2xN. [#953] - Removed the deprecated ``units`` keyword in ``aperture_photometry`` and ``PixelAperture.do_photometry``. [#953] - ``PrimaryHDU``, ``ImageHDU``, and ``HDUList`` can no longer be input to ``aperture_photometry``. [#953] - Removed the deprecated the Aperture ``mask_area`` method. [#953] - Removed the deprecated Aperture plot keywords ``ax`` and ``indices``. [#953] - ``photutils.background`` - Removed the deprecated ``ax`` keyword in ``Background2D.plot_meshes``. [#953] - ``Background2D`` keyword options can not be input as positional arguments. [#1061] - ``photutils.centroids`` - ``centroid_1dg``, ``centroid_2dg``, ``gaussian1d_moments``, ``fit_2dgaussian``, and ``GaussianConst2D`` have been moved to a new ``photutils.centroids.gaussian`` module. [#1064] - Deprecated ``fit_2dgaussian`` and ``GaussianConst2D``. [#1064] - ``photutils.datasets`` - Removed the deprecated ``type`` keyword in ``make_noise_image``. [#953] - Renamed the ``random_state`` keyword (deprecated) to ``seed`` in ``apply_poisson_noise``, ``make_noise_image``, ``make_random_models_table``, and ``make_random_gaussians_table`` functions. [#1080] - ``photutils.detection`` - Removed the deprecated ``snr`` keyword in ``detect_threshold``. [#953] - ``photutils.psf`` - Added ``flux_residual_sigclip`` as an input parameter, allowing for custom sigma clipping options in ``EPSFBuilder``. [#984] - Added ``extra_output_cols`` as a parameter to ``BasicPSFPhotometry``, ``IterativelySubtractedPSFPhotometry`` and ``DAOPhotPSFPhotometry``. [#745] - ``photutils.segmentation`` - Removed the deprecated ``SegmentationImage`` methods ``cmap`` and ``relabel``. [#953] - Removed the deprecated ``SourceProperties`` ``values`` and ``coords`` attributes. [#953] - Removed the deprecated ``xmin/ymin`` and ``xmax/ymax`` properties. [#953] - Removed the deprecated ``snr`` and ``mask_value`` keywords in ``make_source_mask``. [#953] - Renamed the ``random_state`` keyword (deprecated) to ``seed`` in the ``make_cmap`` method. [#1080] - ``photutils.utils`` - Removed the deprecated ``random_cmap``, ``mask_to_mirrored_num``, ``get_version_info``, ``filter_data``, and ``std_blocksum`` functions. [#953] - Removed the deprecated ``wcs_helpers`` functions ``pixel_scale_angle_at_skycoord``, ``assert_angle_or_pixel``, ``assert_angle``, and ``pixel_to_icrs_coords``. [#953] - Deprecated the ``check_random_state`` function. [#1080] - Renamed the ``random_state`` keyword (deprecated) to ``seed`` in the ``make_random_cmap`` function. [#1080] 0.7.2 (2019-12-09) ------------------ Bug Fixes ^^^^^^^^^ - ``photutils.isophote`` - Fixed computation of upper harmonics ``a3``, ``b3``, ``a4``, and ``b4`` in the ellipse fitting algorithm. [#1008] - ``photutils.psf`` - Fix to algorithm in ``EPSFBuilder``, causing issues where ePSFs failed to build. [#974] - Fix to ``IterativelySubtractedPSFPhotometry`` where an error could be thrown when a ``Finder`` was passed which did not return ``None`` if no sources were found. [#986] - Fix to ``centroid_epsf`` where the wrong oversampling factor was used along the y axis. [#1002] 0.7.1 (2019-10-09) ------------------ Bug Fixes ^^^^^^^^^ - ``photutils.psf`` - Fix to ``IterativelySubtractedPSFPhotometry`` where the residual image was not initialized when ``bkg_estimator`` was not supplied. [#942] - ``photutils.segmentation`` - Fixed a labeling bug in ``deblend_sources``. [#961] - Fixed an issue in ``source_properties`` when the input ``data`` is a ``Quantity`` array. [#963] 0.7 (2019-08-14) ---------------- General ^^^^^^^ - Any WCS object that supports the `astropy shared interface for WCS `_ is now supported. [#899] - Added a new ``photutils.__citation__`` and ``photutils.__bibtex__`` attributes which give a citation for photutils in bibtex format. [#926] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added parameter validation for all aperture classes. [#846] - Added ``from_float``, ``as_artist``, ``union`` and ``intersection`` methods to ``BoundingBox`` class. [#851] - Added ``shape`` and ``isscalar`` properties to Aperture objects. [#852] - Significantly improved the performance (~10-20 times faster) of aperture photometry, especially when using ``errors`` and ``Quantity`` inputs with many aperture positions. [#861] - ``aperture_photometry`` now supports ``NDData`` with ``StdDevUncertainty`` to input errors. [#866] - The ``mode`` keyword in the ``to_sky`` and ``to_pixel`` aperture methods was removed to implement the shared WCS interface. All WCS transforms now include distortions (if present). [#899] - ``photutils.datasets`` - Added ``make_gwcs`` function to create an example ``gwcs.wcs.WCS`` object. [#871] - ``photutils.isophote`` - Significantly improved the performance (~5 times faster) of ellipse fitting. [#826] - Added the ability to individually fix the ellipse-fitting parameters. [#922] - ``photutils.psf`` - Added new centroiding function ``centroid_epsf``. [#816] - ``photutils.segmentation`` - Significantly improved the performance of relabeling in segmentation images (e.g., ``remove_labels``, ``keep_labels``). [#810] - Added new ``background_area`` attribute to ``SegmentationImage``. [#825] - Added new ``data_ma`` attribute to ``Segment``. [#825] - Added new ``SegmentationImage`` methods: ``find_index``, ``find_indices``, ``find_areas``, ``check_label``, ``keep_label``, ``remove_label``, and ``reassign_labels``. [#825] - Added ``__repr__`` and ``__str__`` methods to ``SegmentationImage``. [#825] - Added ``slices``, ``indices``, and ``filtered_data_cutout_ma`` attributes to ``SourceProperties``. [#858] - Added ``__repr__`` and ``__str__`` methods to ``SourceProperties`` and ``SourceCatalog``. [#858] - Significantly improved the performance of calculating the ``background_at_centroid`` property in ``SourceCatalog``. [#863] - The default output table columns (source properties) are defined in a publicly-accessible variable called ``photutils.segmentation.properties.DEFAULT_COLUMNS``. [#863] - Added the ``gini`` source property representing the Gini coefficient. [#864] - Cached (lazy) properties can now be reset in ``SegmentationImage`` subclasses. [#916] - Significantly improved the performance of ``deblend_sources``. It is ~40-50% faster for large images (e.g., 4k x 4k) with a few thousand of sources. [#924] - ``photutils.utils`` - Added ``NoDetectionsWarning`` class. [#836] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Fixed an issue where the ``ApertureMask.cutout`` method would drop the data units when ``copy=True``. [#842] - Fixed a corner-case issue where aperture photometry would return NaN for non-finite data values outside the aperture but within the aperture bounding box. [#843] - Fixed an issue where the ``celestial_center`` column (for sky apertures) would be a length-1 array containing a ``SkyCoord`` object instead of a length-1 ``SkyCoord`` object. [#844] - ``photutils.isophote`` - Fixed an issue where the linear fitting mode was not working. [#912] - Fixed the radial gradient computation [#934]. - ``photutils.psf`` - Fixed a bug in the ``EPSFStar`` ``register_epsf`` and ``compute_residual_image`` computations. [#885] - A ValueError is raised if ``aperture_radius`` is not input and cannot be determined from the input ``psf_model``. [#903] - Fixed normalization of ePSF model, now correctly normalizing on undersampled pixel grid. [#817] - ``photutils.segmentation`` - Fixed an issue where ``deblend_sources`` could fail for sources with labels that are a power of 2 and greater than 255. [#806] - ``SourceProperties`` and ``source_properties`` will no longer raise an exception if a source is completely masked. [#822] - Fixed an issue in ``SourceProperties`` and ``source_properties`` where inf values in the data array were not automatically masked. [#822] - ``error`` and ``background`` arrays are now always masked identically to the input ``data``. [#822] - Fixed the ``perimeter`` property to take into account the source mask. [#822] - Fixed the ``background_at_centroid`` source property to use bilinear interpolation. [#822] - Fixed ``SegmentationImage`` ``outline_segments`` to include outlines along the image boundaries. [#825] - Fixed ``SegmentationImage.is_consecutive`` to return ``True`` only if the labels are consecutive and start with label=1. [#886] - Fixed a bug in ``deblend_sources`` where sources could be deblended too much when ``connectivity=8``. [#890] - Fixed a bug in ``deblend_sources`` where the ``contrast`` parameter had little effect if the original segment contained three or more sources. [#890] - ``photutils.utils`` - Fixed a bug in ``filter_data`` where units were dropped for data ``Quantity`` objects. [#872] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - Deprecated inputting aperture pixel positions shaped as 2xN. [#847] - Renamed the ``celestial_center`` column to ``sky_center`` in the ``aperture_photometry`` output table. [#848] - Aperture objects defined with a single (x, y) position (input as 1D) are now considered scalar objects, which can be checked with the new ``isscalar`` Aperture property. [#852] - Non-scalar Aperture objects can now be indexed, sliced, and iterated. [#852] - Scalar Aperture objects now return scalar ``positions`` and ``bounding_boxes`` properties and its ``to_mask`` method returns an ``ApertureMask`` object instead of a length-1 list containing an ``ApertureMask``. [#852] - Deprecated the Aperture ``mask_area`` method. [#853] - Aperture ``area`` is now an attribute instead of a method. [#854] - The Aperture plot keyword ``ax`` was deprecated and renamed to ``axes``. [#854] - Deprecated the ``units`` keyword in ``aperture_photometry`` and the ``PixelAperture.do_photometry`` method. [#866, #861] - Deprecated ``PrimaryHDU``, ``ImageHDU``, and ``HDUList`` inputs to ``aperture_photometry``. [#867] - The ``aperture_photometry`` function moved to a new ``photutils.aperture.photometry`` module. [#876] - Renamed the ``bounding_boxes`` attribute for pixel-based apertures to ``bbox`` for consistency. [#896] - Deprecated the ``BoundingBox`` ``as_patch`` method (instead use ``as_artist``). [#851] - ``photutils.background`` - The ``Background2D`` ``plot_meshes`` keyword ``ax`` was deprecated and renamed to ``axes``. [#854] - ``photutils.datasets`` - The ``make_noise_image`` ``type`` keyword was deprecated and renamed to ``distribution``. [#877] - ``photutils.detection`` - Removed deprecated ``subpixel`` keyword for ``find_peaks``. [#835] - ``DAOStarFinder``, ``IRAFStarFinder``, and ``find_peaks`` now return ``None`` if no source/peaks are found. Also, for this case a ``NoDetectionsWarning`` is issued. [#836] - Renamed the ``snr`` (deprecated) keyword to ``nsigma`` in ``detect_threshold``. [#917] - ``photutils.isophote`` - Isophote central values and intensity gradients are now returned to the output table. [#892] - The ``EllipseSample`` ``update`` method now needs to know the fix/fit state of each individual parameter. This can be passed to it via a ``Geometry`` instance, e.g., ``update(geometry.fix)``. [#922] - ``photutils.psf`` - ``FittableImageModel`` and subclasses now allow for different ``oversampling`` factors to be specified in the x and y directions. [#834] - Removed ``pixel_scale`` keyword from ``EPSFStar``, ``EPSFBuilder``, and ``EPSFModel``. [#815] - Added ``oversampling`` keyword to ``centroid_com``. [#816] - Removed deprecated ``Star``, ``Stars``, and ``LinkedStar`` classes. [#894] - Removed ``recentering_boxsize`` and ``center_accuracy`` keywords and added ``norm_radius`` and ``shift_value`` keywords in ``EPSFBuilder``. [#817] - Added ``norm_radius`` and ``shift_value`` keywords to ``EPSFModel``. [#817] - ``photutils.segmentation`` - Removed deprecated ``SegmentationImage`` attributes ``data_masked``, ``max``, and ``is_sequential`` and methods ``area`` and ``relabel_sequential``. [#825] - Renamed ``SegmentationImage`` methods ``cmap`` (deprecated) to ``make_cmap`` and ``relabel`` (deprecated) to ``reassign_label``. The new ``reassign_label`` method gains a ``relabel`` keyword. [#825] - The ``SegmentationImage`` ``segments`` and ``slices`` attributes now have the same length as ``labels`` (no ``None`` placeholders). [#825] - ``detect_sources`` now returns ``None`` if no sources are found. Also, for this case a ``NoDetectionsWarning`` is issued. [#836] - The ``SegmentationImage`` input ``data`` array must contain at least one non-zero pixel and must not contain any non-finite values. [#836] - A ``ValueError`` is raised if an empty list is input into ``SourceCatalog`` or no valid sources are defined in ``source_properties``. [#836] - Deprecated the ``values`` and ``coords`` attributes in ``SourceProperties``. [#858] - Deprecated the unused ``mask_value`` keyword in ``make_source_mask``. [#858] - The ``bbox`` property now returns a ``BoundingBox`` instance. [#863] - The ``xmin/ymin`` and ``xmax/ymax`` properties have been deprecated with the replacements having a ``bbox_`` prefix (e.g., ``bbox_xmin``). [#863] - The ``orientation`` property is now returned as a ``Quantity`` instance in units of degrees. [#863] - Renamed the ``snr`` (deprecated) keyword to ``nsigma`` in ``make_source_mask``. [#917] - ``photutils.utils`` - Renamed ``random_cmap`` to ``make_random_cmap``. [#825] - Removed deprecated ``cutout_footprint`` function. [#835] - Deprecated the ``wcs_helpers`` functions ``pixel_scale_angle_at_skycoord``, ``assert_angle_or_pixel``, ``assert_angle``, and ``pixel_to_icrs_coords``. [#846] - Removed deprecated ``interpolate_masked_data`` function. [#895] - Deprecated the ``mask_to_mirrored_num`` function. [#895] - Deprecated the ``get_version_info``, ``filter_data``, and ``std_blocksum`` functions. [#918] 0.6 (2018-12-11) ---------------- General ^^^^^^^ - Versions of Numpy <1.11 are no longer supported. [#783] New Features ^^^^^^^^^^^^ - ``photutils.detection`` - ``DAOStarFinder`` and ``IRAFStarFinder`` gain two new parameters: ``brightest`` to keep the top ``brightest`` (based on the flux) objects in the returned catalog (after all other filtering has been applied) and ``peakmax`` to exclude sources with peak pixel values larger or equal to ``peakmax``. [#750] - Added a ``mask`` keyword to ``DAOStarFinder`` and ``IRAFStarFinder`` that can be used to mask regions of the input image. [#759] - ``photutils.psf`` - The ``Star``, ``Stars``, and ``LinkedStars`` classes are now deprecated and have been renamed ``EPSFStar``, ``EPSFStars``, and ``LinkedEPSFStars``, respectively. [#727] - Added a ``GriddedPSFModel`` class for spatially-dependent PSFs. [#772] - The ``pixel_scale`` keyword in ``EPSFStar``, ``EPSFBuilder`` and ``EPSFModel`` is now deprecated. Use the ``oversampling`` keyword instead. [#780] API Changes ^^^^^^^^^^^ - ``photutils.detection`` - The ``find_peaks`` function now returns an empty ``astropy.table.Table`` instead of an empty list if the input data is an array of constant values. [#709] - The ``find_peaks`` function will no longer issue a RuntimeWarning if the input data contains NaNs. [#712] - If no sources/peaks are found, ``DAOStarFinder``, ``IRAFStarFinder``, and ``find_peaks`` now will return an empty table with column names and types. [#758, #762] - ``photutils.psf`` - The ``photutils.psf.funcs.py`` module was renamed ``photutils.psf.utils.py``. The ``prepare_psf_model`` and ``get_grouped_psf_model`` functions were also moved to this new ``utils.py`` module. [#777] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - If a single aperture is input as a list into the ``aperture_photometry`` function, then the output columns will be called ``aperture_sum_0`` and ``aperture_sum_err_0`` (if errors are used). Previously these column names did not have the trailing "_0". [#779] - ``photutils.segmentation`` - Fixed a bug in the computation of ``sky_bbox_ul``, ``sky_bbox_lr``, ``sky_bbox_ur`` in the ``SourceCatalog``. [#716] Other Changes and Additions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Updated background and detection functions that call ``astropy.stats.SigmaClip`` or ``astropy.stats.sigma_clipped_stats`` to support both their ``iters`` (for astropy < 3.1) and ``maxiters`` keywords. [#726] 0.5 (2018-08-06) ---------------- General ^^^^^^^ - Versions of Python <3.5 are no longer supported. [#702, #703] - Versions of Numpy <1.10 are no longer supported. [#697, #703] - Versions of Pytest <3.1 are no longer supported. [#702] - ``pytest-astropy`` is now required to run the test suite. [#702, #703] - The documentation build now uses the Sphinx configuration from ``sphinx-astropy`` rather than from ``astropy-helpers``. [#702] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added ``plot`` and ``to_aperture`` methods to ``BoundingBox``. [#662] - Added default theta value for elliptical and rectangular apertures. [#674] - ``photutils.centroid`` - Added a ``centroid_sources`` function to calculate centroid of many sources in a single image. [#656] - An n-dimensional array can now be input into the ``centroid_com`` function. [#685] - ``photutils.datasets`` - Added a ``load_simulated_hst_star_image`` function to load a simulated HST WFC3/IR F160W image of stars. [#695] - ``photutils.detection`` - Added a ``centroid_func`` keyword to ``find_peaks``. The ``subpixels`` keyword is now deprecated. [#656] - The ``find_peaks`` function now returns ``SkyCoord`` objects in the table instead of separate RA and Dec. columns. [#656] - The ``find_peaks`` function now returns an empty Table and issues a warning when no peaks are found. [#668] - ``photutils.psf`` - Added tools to build and fit an effective PSF (``EPSFBuilder`` and ``EPSFFitter``). [#695] - Added ``extract_stars`` function to extract cutouts of stars used to build an ePSF. [#695] - Added ``EPSFModel`` class to hold a fittable ePSF model. [#695] - ``photutils.segmentation`` - Added a ``mask`` keyword to the ``detect_sources`` function. [#621] - Renamed ``SegmentationImage`` ``max`` attribute to ``max_label``. ``max`` is deprecated. [#662] - Added a ``Segment`` class to hold the cutout image and properties of single labeled region (source segment). [#662] - Deprecated the ``SegmentationImage`` ``area`` method. Instead, use the ``areas`` attribute. [#662] - Renamed ``SegmentationImage`` ``data_masked`` attribute to ``data_ma``. ``data_masked`` is deprecated. [#662] - Renamed ``SegmentationImage`` ``is_sequential`` attribute to ``is_consecutive``. ``is_sequential`` is deprecated. [#662] - Renamed ``SegmentationImage`` ``relabel_sequential`` attribute to ``relabel_consecutive``. ``relabel_sequential`` is deprecated. [#662] - Added a ``missing_labels`` property to ``SegmentationImage``. [#662] - Added a ``check_labels`` method to ``SegmentationImage``. The ``check_label`` method is deprecated. [#662] - ``photutils.utils`` - Deprecated the ``cutout_footprint`` function. [#656] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Fixed a bug where quantity inputs to the aperture classes would sometimes fail. [#693] - ``photutils.detection`` - Fixed an issue in ``detect_sources`` where in some cases sources with a size less than ``npixels`` could be returned. [#663] - Fixed an issue in ``DAOStarFinder`` where in some cases a few too many sources could be returned. [#671] - ``photutils.isophote`` - Fixed a bug where isophote fitting would fail when the initial center was not specified for an image with an elongated aspect ratio. [#673] - ``photutils.segmentation`` - Fixed ``deblend_sources`` when other sources are in the neighborhood. [#617] - Fixed ``source_properties`` to handle the case where the data contain one or more NaNs. [#658] - Fixed an issue with ``deblend_sources`` where sources were not deblended where the data contain one or more NaNs. [#658] - Fixed the ``SegmentationImage`` ``areas`` attribute to not include the zero (background) label. [#662] Other Changes and Additions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - ``photutils.isophote`` - Corrected the units for isophote ``sarea`` in the documentation. [#657] 0.4 (2017-10-30) ---------------- General ^^^^^^^ - Dropped python 3.3 support. [#542] - Dropped numpy 1.8 support. Minimal required version is now numpy 1.9. [#542] - Dropped support for astropy 1.x versions. Minimal required version is now astropy 2.0. [#575] - Dropped scipy 0.15 support. Minimal required version is now scipy 0.16. [#576] - Explicitly require six as dependency. [#601] New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added ``BoundingBox`` class, used when defining apertures. [#481] - Apertures now have ``__repr__`` and ``__str__`` defined. [#493] - Improved plotting of annulus apertures using Bezier curves. [#494] - Rectangular apertures now use the true minimal bounding box. [#507] - Elliptical apertures now use the true minimal bounding box. [#508] - Added a ``to_sky`` method for pixel apertures. [#512] - ``photutils.background`` - Mesh rejection now also applies to pixels that are masked during sigma clipping. [#544] - ``photutils.datasets`` - Added new ``make_wcs`` and ``make_imagehdu`` functions. [#527] - Added new ``show_progress`` keyword to the ``load_*`` functions. [#590] - ``photutils.isophote`` - Added a new ``photutils.isophote`` subpackage to provide tools to fit elliptical isophotes to a galaxy image. [#532, #603] - ``photutils.segmentation`` - Added a ``cmap`` method to ``SegmentationImage`` to generate a random matplotlib colormap. [#513] - Added ``sky_centroid`` and ``sky_centroid_icrs`` source properties. [#592] - Added new source properties representing the sky coordinates of the bounding box corner vertices (``sky_bbox_ll``, ``sky_bbox_ul`` ``sky_bbox_lr``, and ``sky_bbox_ur``). [#592] - Added new ``SourceCatalog`` class to hold the list of ``SourceProperties``. [#608] - The ``properties_table`` function is now deprecated. Use the ``SourceCatalog.to_table()`` method instead. [#608] - ``photutils.psf`` - Uncertainties on fitted parameters are added to the final table. [#516] - Fitted results of any free parameter are added to the final table. [#471] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - The ``ApertureMask`` ``apply()`` method has been renamed to ``multiply()``. [#481]. - The ``ApertureMask`` input parameter was renamed from ``mask`` to ``data``. [#548] - Removed the ``pixelwise_errors`` keyword from ``aperture_photometry``. [#489] - ``photutils.background`` - The ``Background2D`` keywords ``exclude_mesh_method`` and ``exclude_mesh_percentile`` were removed in favor of a single keyword called ``exclude_percentile``. [#544] - Renamed ``BiweightMidvarianceBackgroundRMS`` to ``BiweightScaleBackgroundRMS``. [#547] - Removed the ``SigmaClip`` class. ``astropy.stats.SigmaClip`` is a direct replacement. [#569] - ``photutils.datasets`` - The ``make_poission_noise`` function was renamed to ``apply_poisson_noise``. [#527] - The ``make_random_gaussians`` function was renamed to ``make_random_gaussians_table``. The parameter ranges must now be input as a dictionary. [#527] - The ``make_gaussian_sources`` function was renamed to ``make_gaussian_sources_image``. [#527] - The ``make_random_models`` function was renamed to ``make_random_models_table``. [#527] - The ``make_model_sources`` function was renamed to ``make_model_sources_image``. [#527] - The ``unit``, ``hdu``, ``wcs``, and ``wcsheader`` keywords in ``photutils.datasets`` functions were removed. [#527] - ``'photutils-datasets'`` was added as an optional ``location`` in the ``get_path`` function. This option is used as a fallback in case the ``'remote'`` location (astropy data server) fails. [#589] - ``photutils.detection`` - The ``daofind`` and ``irafstarfinder`` functions were removed. [#588] - ``photutils.psf`` - ``IterativelySubtractedPSFPhotometry`` issues a "no sources detected" warning only on the first iteration, if applicable. [#566] - ``photutils.segmentation`` - The ``'icrs_centroid'``, ``'ra_icrs_centroid'``, and ``'dec_icrs_centroid'`` source properties are deprecated and are no longer default columns returned by ``properties_table``. [#592] - The ``properties_table`` function now returns a ``QTable``. [#592] - ``photutils.utils`` - The ``background_color`` keyword was removed from the ``random_cmap`` function. [#528] - Deprecated unused ``interpolate_masked_data()``. [#526, #611] Bug Fixes ^^^^^^^^^ - ``photutils.segmentation`` - Fixed ``deblend_sources`` so that it correctly deblends multiple sources. [#572] - Fixed a bug in calculation of the ``sky_centroid_icrs`` (and deprecated ``icrs_centroid``) property where the incorrect pixel origin was being passed. [#592] - ``photutils.utils`` - Added a check that ``data`` and ``bkg_error`` have the same units in ``calc_total_error``. [#537] 0.3.2 (2017-03-31) ------------------ General ^^^^^^^ - Fixed file permissions in the released source distribution. 0.3.1 (2017-03-02) ------------------ General ^^^^^^^ - Dropped numpy 1.7 support. Minimal required version is now numpy 1.8. [#327] - ``photutils.datasets`` - The ``load_*`` functions that use remote data now retrieve the data from ``data.astropy.org`` (the astropy data repository). [#472] Bug Fixes ^^^^^^^^^ - ``photutils.background`` - Fixed issue with ``Background2D`` with ``edge_method='pad'`` that occurred when unequal padding needed to be applied to each axis. [#498] - Fixed issue with ``Background2D`` that occurred when zero padding needed to apply along only one axis. [#500] - ``photutils.geometry`` - Fixed a bug in ``circular_overlap_grid`` affecting 32-bit machines that could cause errors circular aperture photometry. [#475] - ``photutils.psf`` - Fixed a bug in how ``FittableImageModel`` represents its center. [#460] - Fix bug which modified user's input table when doing forced photometry. [#485] 0.3 (2016-11-06) ---------------- New Features ^^^^^^^^^^^^ - ``photutils.aperture`` - Added new ``origin`` keyword to aperture ``plot`` methods. [#395] - Added new ``id`` column to ``aperture_photometry`` output table. [#446] - Added ``__len__`` method for aperture classes. [#446] - Added new ``to_mask`` method to ``PixelAperture`` classes. [#453] - Added new ``ApertureMask`` class to generate masks from apertures. [#453] - Added new ``mask_area()`` method to ``PixelAperture`` classes. [#453] - The ``aperture_photometry()`` function now accepts a list of aperture objects. [#454] - ``photutils.background`` - Added new ``MeanBackground``, ``MedianBackground``, ``MMMBackground``, ``SExtractorBackground``, ``BiweightLocationBackground``, ``StdBackgroundRMS``, ``MADStdBackgroundRMS``, and ``BiweightMidvarianceBackgroundRMS`` classes. [#370] - Added ``axis`` keyword to new background classes. [#392] - Added new ``removed_masked``, ``meshpix_threshold``, and ``edge_method`` keywords for the 2D background classes. [#355] - Added new ``std_blocksum`` function. [#355] - Added new ``SigmaClip`` class. [#423] - Added new ``BkgZoomInterpolator`` and ``BkgIDWInterpolator`` classes. [#437] - ``photutils.datasets`` - Added ``load_irac_psf`` function. [#403] - ``photutils.detection`` - Added new ``make_source_mask`` convenience function. [#355] - Added ``filter_data`` function. [#398] - Added ``DAOStarFinder`` and ``IRAFStarFinder`` as oop interfaces for ``daofind`` and ``irafstarfinder``, respectively, which are now deprecated. [#379] - ``photutils.psf`` - Added ``BasicPSFPhotometry``, ``IterativelySubtractedPSFPhotometry``, and ``DAOPhotPSFPhotometry`` classes to perform PSF photometry in crowded fields. [#427] - Added ``DAOGroup`` and ``DBSCANGroup`` classes for grouping overlapping sources. [#369] - ``photutils.psf_match`` - Added ``create_matching_kernel`` and ``resize_psf`` functions. Also, added ``CosineBellWindow``, ``HanningWindow``, ``SplitCosineBellWindow``, ``TopHatWindow``, and ``TukeyWindow`` classes. [#403] - ``photutils.segmentation`` - Created new ``photutils.segmentation`` subpackage. [#442] - Added ``copy`` and ``area`` methods and an ``areas`` property to ``SegmentationImage``. [#331] API Changes ^^^^^^^^^^^ - ``photutils.aperture`` - Removed the ``effective_gain`` keyword from ``aperture_photometry``. Users must now input the total error, which can be calculated using the ``calc_total_error`` function. [#368] - ``aperture_photometry`` now outputs a ``QTable``. [#446] - Renamed ``source_id`` keyword to ``indices`` in the aperture ``plot()`` method. [#453] - Added ``mask`` and ``unit`` keywords to aperture ``do_photometry()`` methods. [#453] - ``photutils.background`` - For the background classes, the ``filter_shape`` keyword was renamed to ``filter_size``. The ``background_low_res`` and ``background_rms_low_res`` class attributes were renamed to ``background_mesh`` and ``background_rms_mesh``, respectively. [#355, #437] - The ``Background2D`` ``method`` and ``backfunc`` keywords have been removed. In its place one can input callable objects via the ``sigma_clip``, ``bkg_estimator``, and ``bkgrms_estimator`` keywords. [#437] - The interpolator to be used by the ``Background2D`` class can be input as a callable object via the new ``interpolator`` keyword. [#437] - ``photutils.centroids`` - Created ``photutils.centroids`` subpackage, which contains the ``centroid_com``, ``centroid_1dg``, and ``centroid_2dg`` functions. These functions now return a two-element numpy ndarray. [#428] - ``photutils.detection`` - Changed finding algorithm implementations (``daofind`` and ``starfind``) from functional to object-oriented style. Deprecated old style. [#379] - ``photutils.morphology`` - Created ``photutils.morphology`` subpackage. [#428] - Removed ``marginalize_data2d`` function. [#428] - Moved ``cutout_footprint`` from ``photutils.morphology`` to ``photutils.utils``. [#428] - Added a function to calculate the Gini coefficient (``gini``). [#343] - ``photutils.psf`` - Removed the ``effective_gain`` keyword from ``psf_photometry``. Users must now input the total error, which can be calculated using the ``calc_total_error`` function. [#368] - ``photutils.segmentation`` - Removed the ``effective_gain`` keyword from ``SourceProperties`` and ``source_properties``. Users must now input the total error, which can be calculated using the ``calc_total_error`` function. [#368] - ``photutils.utils`` - Renamed ``calculate_total_error`` to ``calc_total_error``. [#368] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Fixed a bug in ``aperture_photometry`` so that single-row output tables do not return a multidimensional column. [#446] - ``photutils.centroids`` - Fixed a bug in ``centroid_1dg`` and ``centroid_2dg`` that occurred when the input data contained invalid (NaN or inf) values. [#428] - ``photutils.segmentation`` - Fixed a bug in ``SourceProperties`` where ``error`` and ``background`` units were sometimes dropped. [#441] 0.2.2 (2016-07-06) ------------------ General ^^^^^^^ - Dropped numpy 1.6 support. Minimal required version is now numpy 1.7. [#327] - Fixed configparser for Python 3.5. [#366, #384] Bug Fixes ^^^^^^^^^ - ``photutils.detection`` - Fixed an issue to update segmentation image slices after deblending. [#340] - Fixed source deblending to pass the pixel connectivity to the watershed algorithm. [#347] - SegmentationImage properties are now cached instead of recalculated, which significantly improves performance. [#361] - ``photutils.utils`` - Fixed a bug in ``pixel_to_icrs_coords`` where the incorrect pixel origin was being passed. [#348] 0.2.1 (2016-01-15) ------------------ Bug Fixes ^^^^^^^^^ - ``photutils.background`` - Added more robust version checking of Astropy. [#318] - ``photutils.detection`` - Added more robust version checking of Astropy. [#318] - ``photutils.segmentation`` - Fixed issue where ``SegmentationImage`` slices were not being updated. [#317] - Added more robust version checking of scikit-image. [#318] 0.2 (2015-12-31) ---------------- General ^^^^^^^ - Photutils has the following requirements: - Python 2.7 or 3.3 or later - Numpy 1.6 or later - Astropy v1.0 or later New Features ^^^^^^^^^^^^ - ``photutils.detection`` - ``find_peaks`` now returns an Astropy Table containing the (x, y) positions and peak values. [#240] - ``find_peaks`` has new ``mask``, ``error``, ``wcs`` and ``subpixel`` precision options. [#244] - ``detect_sources`` will now issue a warning if the filter kernel is not normalized to 1. [#298] - Added new ``deblend_sources`` function, an experimental source deblender. [#314] - ``photutils.morphology`` - Added new ``GaussianConst2D`` (2D Gaussian plus a constant) model. [#244] - Added new ``marginalize_data2d`` function. [#244] - Added new ``cutout_footprint`` function. [#244] - ``photutils.segmentation`` - Added new ``SegmentationImage`` class. [#306] - Added new ``check_label``, ``keep_labels``, and ``outline_segments`` methods for modifying ``SegmentationImage``. [#306] - ``photutils.utils`` - Added new ``random_cmap`` function to generate a colormap comprised of random colors. [#299] - Added new ``ShepardIDWInterpolator`` class to perform Inverse Distance Weighted (IDW) interpolation. [#307] - The ``interpolate_masked_data`` function can now interpolate higher-dimensional data. [#310] API Changes ^^^^^^^^^^^ - ``photutils.segmentation`` - The ``relabel_sequential``, ``relabel_segments``, ``remove_segments``, ``remove_border_segments``, and ``remove_masked_segments`` functions are now ``SegmentationImage`` methods (with slightly different names). [#306] - The ``SegmentProperties`` class has been renamed to ``SourceProperties``. Likewise, the ``segment_properties`` function has been renamed to ``source_properties``. [#306] - The ``segment_sum`` and ``segment_sum_err`` attributes have been renamed to ``source_sum`` and ``source_sum_err``, respectively. [#306] - The ``background_atcentroid`` attribute has been renamed to ``background_at_centroid``. [#306] Bug Fixes ^^^^^^^^^ - ``photutils.aperture`` - Fixed an issue where ``np.nan`` or ``np.inf`` were not properly masked. [#267] - ``photutils.geometry`` - ``overlap_area_triangle_unit_circle`` handles correctly a corner case in some i386 systems where the area of the aperture was not computed correctly. [#242] - ``rectangular_overlap_grid`` and ``elliptical_overlap_grid`` fixes to normalization of subsampled pixels. [#265] - ``overlap_area_triangle_unit_circle`` handles correctly the case where a line segment intersects at a triangle vertex. [#277] Other Changes and Additions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Updated astropy-helpers to v1.1. [#302] 0.1 (2014-12-22) ---------------- Photutils 0.1 was released on December 22, 2014. It requires Astropy version 0.4 or later. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/CITATION.rst0000644000214200020070000000011200000000000014134 0ustar00lbradleySee https://github.com/astropy/photutils/blob/main/photutils/CITATION.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/CODE_OF_CONDUCT.rst0000644000214200020070000000025700000000000015211 0ustar00lbradleyPhotutils is an `Astropy `_ affiliated package. We follow the `Astropy Community Code of Conduct `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/CONTRIBUTING.rst0000644000214200020070000001362400000000000014645 0ustar00lbradleyContributing to Photutils ========================= Reporting Issues ---------------- When opening an issue to report a problem, please try to provide a minimal code example that reproduces the issue. Also, please include details of the operating system and the Python, Numpy, Astropy, and Photutils versions you are using. Contributing code ----------------- So you're interested in contributing code to Photutils? Excellent! Most contributions to Photutils are done via pull requests from GitHub users' forks of the `Photutils repository `_. If you're new to this style of development, you'll want to read over the `Astropy development workflow `_. Once you open a pull request (which should be opened against the ``main`` branch, not against any other branch), please make sure that you include the following: - **Code**: the code you are adding, which should follow as much as possible the `Astropy coding guidelines `_. - **Tests**: these are either tests to ensure code that previously failed now works (regression tests) or tests that cover as much as possible of the new functionality to make sure it doesn't break in the future. The tests are also used to ensure consistent results on all platforms, since we run these tests on many platforms/configurations. For more information about how to write tests, see the `Astropy testing guidelines `_. - **Documentation**: if you are adding new functionality, be sure to include a description in the main documentation (in ``docs/``). For more information, please see the detailed `Astropy documentation guidelines `_. - **Changelog entry**: if you are fixing a bug or adding new functionality, you should add an entry to the ``CHANGES.rst`` file that includes the PR number and if possible the issue number (if you are opening a pull request you may not know this yet, but you can add it once the pull request is open). If you're not sure where to put the changelog entry, wait until a maintainer has reviewed your PR and assigned it to a milestone. You do not need to include a changelog entry for fixes to bugs introduced in the developer version and therefore are not present in the stable releases. In general, you do not need to include a changelog entry for minor documentation or test updates. Only user-visible changes (new features/API changes, fixed issues) need to be mentioned. If in doubt, ask the core maintainer reviewing your changes. Other Tips ---------- - To prevent the automated tests from running you can add ``[ci skip]`` to your commit message. This is useful if your PR is a work in progress and you are not yet ready for the tests to run. For example: $ git commit -m "WIP widget [ci skip]" - If you already made the commit without including this string, you can edit your existing commit message by running: $ git commit --amend - To skip only the testing on Travis CI use ``[skip travis]``. - When contributing trivial documentation fixes (e.g., fixes to typos, spelling, grammar) that do not contain any special markup and are not associated with code changes, please include the string ``[docs only]`` in your commit message. $ git commit -m "Fixed typo [docs only]" Checklist for Contributed Code ------------------------------ A pull request for a new feature will be reviewed to see if it meets the following requirements. For any pull request, a Photutils maintainer can help to make sure that the pull request meets the requirements for inclusion in the package. **Scientific Quality** (when applicable) * Is the submission relevant to this package? * Are references included to the original source for the algorithm? * Does the code perform as expected? * Has the code been tested against previously existing implementations? **Code Quality** * Are the `Astropy coding guidelines `_ followed? * Are there dependencies other than the Astropy core, the Python Standard Library, and Numpy? - Is the package importable even if the C-extensions are not built? - Are additional dependencies handled appropriately? - Do functions and classes that require additional dependencies raise an `ImportError` if they are not present? **Testing** * Are the `Astropy testing guidelines `_ followed? * Are the inputs to the functions and classes sufficiently tested? * Are there tests for any exceptions raised? * Are there tests for the expected performance? * Are the sources for the tests documented? * Are the tests that require an `optional dependency `_ marked as such? * Does "``tox -e test``" run without failures? **Documentation** * Are the `Astropy documentation guidelines `_ followed? * Is there a `docstring `_ in the functions and classes describing: - What the code does? - The format of the inputs of the function or class? - The format of the outputs of the function or class? - References to the original algorithms? - Any exceptions which are raised? - An example of running the code? * Is there any information needed to be added to the docs to describe the function or class? * Does the documentation build without errors or warnings? * If applicable, has an entry been added into the changelog? **License** * Is the photutils license included at the top of the file? * Are there any conflicts with this code and existing codes? ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/LICENSE.rst0000644000214200020070000000273400000000000014020 0ustar00lbradleyCopyright (c) 2011-2021, Photutils 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 Photutils 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/MANIFEST.in0000644000214200020070000000044600000000000013740 0ustar00lbradleyinclude CHANGES.rst include CITATION.rst include photutils/CITATION.rst include LICENSE.rst include README.rst include pyproject.toml include setup.cfg recursive-include photutils *.pyx *.pxd *.c recursive-include docs * prune build prune docs/_build prune docs/api global-exclude *.pyc *.o ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0266516 photutils-1.3.0/PKG-INFO0000644000214200020070000001047200000000000013277 0ustar00lbradleyMetadata-Version: 2.1 Name: photutils Version: 1.3.0 Summary: An Astropy package for source detection and photometry Home-page: https://github.com/astropy/photutils Author: Photutils Developers Author-email: photutils.team@gmail.com License: BSD 3-Clause Keywords: astronomy,astrophysics,photometry,aperture,psf,source detection,background,segmentation,centroids,isophote,morphology Platform: UNKNOWN Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Cython Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Scientific/Engineering :: Astronomy Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: all Provides-Extra: test Provides-Extra: docs License-File: LICENSE.rst ========= Photutils ========= |PyPI Version| |Conda Version| |PyPI Downloads| |Astropy| |CI Status| |CircleCI Status| |Codecov Status| |Latest RTD Status| |LGTM Grade| Photutils is an `Astropy`_ package for detection and photometry of astronomical sources. Please see the `online documentation `_ for `installation instructions `_ and usage information. Citing Photutils ---------------- |Zenodo| If you use Photutils for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment:: This research made use of Photutils, an Astropy package for detection and photometry of astronomical sources (Bradley et al. 20XX). where (Bradley et al. 20XX) is a citation to the `Zenodo record `_ of the Photutils version that was used. We also encourage citations in the main text wherever appropriate. Please see the `CITATION `_ file for details and an example BibTeX entry. License ------- Photutils is licensed under a 3-clause BSD license. Please see the `LICENSE `_ file for details. .. |PyPI Version| image:: https://img.shields.io/pypi/v/photutils.svg?logo=pypi&logoColor=white&label=PyPI :target: https://pypi.org/project/photutils/ :alt: PyPI Latest Release .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/photutils :target: https://anaconda.org/conda-forge/photutils :alt: Conda Latest Release .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/photutils?label=PyPI%20Downloads :target: https://pypistats.org/packages/photutils :alt: PyPI Downloads .. |Astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: https://www.astropy.org/ :alt: Powered by Astropy .. |Zenodo| image:: https://zenodo.org/badge/2640766.svg :target: https://zenodo.org/badge/latestdoi/2640766 :alt: Zenodo Latest DOI .. |CI Status| image:: https://github.com/astropy/photutils/workflows/CI%20Tests/badge.svg# :target: https://github.com/astropy/photutils/actions :alt: CI Status .. |Codecov Status| image:: https://img.shields.io/codecov/c/github/astropy/photutils?logo=codecov :target: https://codecov.io/gh/astropy/photutils :alt: Coverage Status .. |CircleCI Status| image:: https://img.shields.io/circleci/build/github/astropy/photutils/main?logo=circleci&label=CircleCI :target: https://circleci.com/gh/astropy/photutils :alt: CircleCI Status .. |Stable RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=stable :target: https://photutils.readthedocs.io/en/stable/ :alt: Stable Documentation Status .. |Latest RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=latest :target: https://photutils.readthedocs.io/en/latest/ :alt: Latest Documentation Status .. |LGTM Grade| image:: https://img.shields.io/lgtm/grade/python/g/astropy/photutils.svg?logo=lgtm&logoWidth=18 :target: https://lgtm.com/projects/g/astropy/photutils/context:python :alt: LGTM Grade .. _Astropy: https://www.astropy.org/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/README.rst0000644000214200020070000000662700000000000013700 0ustar00lbradley========= Photutils ========= |PyPI Version| |Conda Version| |PyPI Downloads| |Astropy| |CI Status| |CircleCI Status| |Codecov Status| |Latest RTD Status| |LGTM Grade| Photutils is an `Astropy`_ package for detection and photometry of astronomical sources. Please see the `online documentation `_ for `installation instructions `_ and usage information. Citing Photutils ---------------- |Zenodo| If you use Photutils for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment:: This research made use of Photutils, an Astropy package for detection and photometry of astronomical sources (Bradley et al. 20XX). where (Bradley et al. 20XX) is a citation to the `Zenodo record `_ of the Photutils version that was used. We also encourage citations in the main text wherever appropriate. Please see the `CITATION `_ file for details and an example BibTeX entry. License ------- Photutils is licensed under a 3-clause BSD license. Please see the `LICENSE `_ file for details. .. |PyPI Version| image:: https://img.shields.io/pypi/v/photutils.svg?logo=pypi&logoColor=white&label=PyPI :target: https://pypi.org/project/photutils/ :alt: PyPI Latest Release .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/photutils :target: https://anaconda.org/conda-forge/photutils :alt: Conda Latest Release .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/photutils?label=PyPI%20Downloads :target: https://pypistats.org/packages/photutils :alt: PyPI Downloads .. |Astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: https://www.astropy.org/ :alt: Powered by Astropy .. |Zenodo| image:: https://zenodo.org/badge/2640766.svg :target: https://zenodo.org/badge/latestdoi/2640766 :alt: Zenodo Latest DOI .. |CI Status| image:: https://github.com/astropy/photutils/workflows/CI%20Tests/badge.svg# :target: https://github.com/astropy/photutils/actions :alt: CI Status .. |Codecov Status| image:: https://img.shields.io/codecov/c/github/astropy/photutils?logo=codecov :target: https://codecov.io/gh/astropy/photutils :alt: Coverage Status .. |CircleCI Status| image:: https://img.shields.io/circleci/build/github/astropy/photutils/main?logo=circleci&label=CircleCI :target: https://circleci.com/gh/astropy/photutils :alt: CircleCI Status .. |Stable RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=stable :target: https://photutils.readthedocs.io/en/stable/ :alt: Stable Documentation Status .. |Latest RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=latest :target: https://photutils.readthedocs.io/en/latest/ :alt: Latest Documentation Status .. |LGTM Grade| image:: https://img.shields.io/lgtm/grade/python/g/astropy/photutils.svg?logo=lgtm&logoWidth=18 :target: https://lgtm.com/projects/g/astropy/photutils/context:python :alt: LGTM Grade .. _Astropy: https://www.astropy.org/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/codecov.yml0000644000214200020070000000001700000000000014341 0ustar00lbradleycomment: false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9667408 photutils-1.3.0/docs/0000755000214200020070000000000000000000000013126 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1561470461.0 photutils-1.3.0/docs/Makefile0000644000214200020070000001074500000000000014575 0ustar00lbradley# 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." ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1640123871.96902 photutils-1.3.0/docs/_static/0000755000214200020070000000000000000000000014554 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1418856802.0 photutils-1.3.0/docs/_static/favicon.ico0000644000214200020070000003535600000000000016711 0ustar00lbradley h6  ¨ž00 ¨%F(  ¨o1‘xm²t,G®p(¦m.ª¤l,£¢j*z h)1’\ ¨o/©q4.­p*É¡k1ÿšj9þ¥k*ÿ¢j)ÿŸh)ÿf'üšc&»—a%4‘[¦o35«o*ô–lFÿtfýŽjNübOüœe)üœd&ü™c'þ—a$ÿ–_!ÿ”]s“Y¡m6©o-Ìžg,ÿ‘ymúͪƒÿÑ­‚ÿ¢ƒiÿƒ]>ÿbÿ–^ÿ•^üŽ\%û[$ÿY{ŽX¤l-Z¥l*ÿ—e0üŽp\ÿÓ±‡ÿؽžÿ±“vÿ€]Aÿ’a+ÿƒePÿZÿuWDþmTJüˆUÿ‰TI¡i* žg*ÿ¢g#üƒ`Eÿ‹iNÿ¡}Zÿ‹{{ÿŠVÿ†eJÿƒ€”ÿ}R)ÿ†hQÿ“sTüqP6ÿˆQÍOGWf(½že$ÿ^.ýŽ^-ÿ…[4ÿ}Y<ÿŠVÿ“ZÿŠXÿƒT#ÿ…TÿsT?ÿ¡xGÿs\Qý~KÿNTœd%µ’]%ÿ„uuýngÿ“Zÿ‘[ÿ‚fUÿ\;ÿŠTÿ‰Uÿ‚QÿpUEÿÁ£ÿƒoeürEÿ~K¢Ÿd‰U/ÿº´³ü’‚|ÿˆQÿ}U2ÿ™’šÿnhÿƒOÿƒOÿ‚JÿlQ@ÿƯ•ÿxgýhAÿ{H ÌŸ`>ƒX/þ„|„þ|_Jÿ‹Uÿ‰TÿzT3ÿ}R&ÿ‚MÿzW6ÿr\OÿfI6ÿždÿ}[:ýeC&ÿwDÓY»yQ-ÿ†Sü‰TÿzO#ÿzN!ÿ€NÿxGÿvwÿykkÿg?ÿz`Mÿš|]ýgI0ÿq=¸‰U7ŠUüˆSþpM0ýzbSÿ„l[ÿjQDÿgI5ÿnEÿsB ÿuC ÿaG8ÿubZücA#ÿq<q…Q€Qg„PÿkK2ü`Gû¼—iÿ¦‚ÿdJÿeG2ÿtC ÿqA ÿj?ûU:+ÿh< Ün;ƒP~La}JùeG2ÿpZOþ–~jü˜yYü\HDük?üm> ýk<ÿk;üh:E€NyH)zF ¯h?úeF-ÿdJ8ÿd=ÿl=þh;ÿf9Óc9 :h:M}G H*s<sn9 m<ªg:‘f9Nb5g:øÿàÀ€€€€Ààðøÿ( @ ªq0©o.«q1©p/&¨o.>§n-G¦m-A¥l,-¤l,›d&›d&ªp/ªp/¬s1ªq0S©p/©¨o/Þ¨n.ø§n-ÿ¦m-ÿ¥l,ÿ¤k+û¢j+ç¡i*¾ h)|Ÿg(*—a$—`$ªp/©o/«q0Yªp0ߨo1ÿªo,þªn)ÿ§m+ÿ£l.ÿ¤k,þ£j+þ¢i*ÿ¡i*ÿ h)ÿŸg(ÿf(ýœe'½›d&A˜a$”^"ªp/ªq0 ªp/©©p0ÿªo,þ¯q&ü l6û—j?ýŸk3þªl!ÿ£j*ÿ i+ÿ h)ÿŸg(þžf(üœe'û›d&þ›d&ÿ™c%ÿ˜b$°—`$“]!ªp/ªp/ ªp/Ĩo0ÿªn*ú£m2ým]fÿncrÿragÿj_pÿsclÿ h)ÿ¡g&ÿf)ÿe'ÿœe'ÿ›d&ÿšc%ÿ™b%ü˜a$ü—`$ÿ•_#ð”^"S[ ¥m-©o/±¨o.ÿ¨n,ù¥n1ÿaZsÿ¬œ–ÿ³|=ÿ«o)ÿ«r0ÿ}[Dÿe_xÿŸf%ÿœe'ÿ›d&ÿšc&ÿ™b%ÿ˜a$ÿ—`$ÿ–`#ÿ”_"û“^"ý’]!ÿ‘\ yŽY¨n.¨n.q§n.ÿ¤m/ú®n$ÿq]_ÿ™„{ÿÙ²€ÿ¹’gÿÊ­Žÿʯ’ÿËœ`ÿvZMÿv`]ÿ¢dÿ˜b'ÿ˜b$ÿ—a$ÿ–`#ÿ•_"ÿ”^"ÿ’]"ÿ“\ý’\û[ ÿZŒX§n-¦m-ë¥l,ÿ¤k,þ¤m.ÿfXdÿÅ¡xÿ¸bÿѶ—ÿɪˆÿ¶_ÿË®ÿ¬„ZÿbUcÿœc"ÿ—a%ÿ•`%ÿ–`"ÿ“^#ÿ“]!ÿ’]!ÿ“\ÿ[$ÿ‹Z$þ‘YûŒXÿŒWjŠW¥l,u¥l,ÿ£k+û£j)ÿžj2ÿiW\ÿÄžsÿ»•lÿÕ¾£ÿøôðÿØÃ«ÿ®‚Qÿ¾yÿcQYÿ˜b&ÿ•_$ÿ™_ÿ’_'ÿ–^ÿ’\ÿ‘[ÿ‹Z%ÿ\WpÿXWvÿ€W1þŽWþ‰Vü‰U<‰U¤k+Ç£j+ÿ¡i*ý h+ÿ¤h$ÿg]oÿk7ÿÓ¸šÿÀžyÿÈ©‡ÿ»™sÿ»“eÿ¯”{ÿ_Q]ÿ›aÿ—_ÿvXDÿ`d‰ÿgYbÿŽYÿ•ZÿmXSÿoRBÿ…OÿTTvÿŠUüˆTÿ‡SуQ‡T¢j+$¢j*ô¡i*ÿ h)þg*ÿ¤gÿbOÿhZfÿ©jÿ¾–fÿ³‰XÿµˆRÿìÕ·ÿc\rÿ[>ÿ˜^ÿ‘^%ÿab„ÿãÆ ÿ‚˜ÿuVAÿ™[ÿ`QXÿŒmQÿ³ƒFÿgPHÿhSOÿSû„Rÿ„Qx„R¡i*L h)ÿŸg)þžf(ÿe'ÿ™d*ÿŸdÿr__ÿe[lÿV-ÿ‡V#ÿ™}hÿjj‡ÿoWNÿ™^ÿ\#ÿ”\ÿgW[ÿ|}™ÿdazÿ„V%ÿ‘Wÿ`Uaÿ¦‹rÿ°Š_ÿ£YÿQI`ÿ†SþƒPÿ‚Pç€NŸg)gžf(ÿf'üœe'ÿšd'ÿ¡dÿžcÿbÿ„_@ÿk]fÿh\jÿfV[ÿ€W2ÿ—^ÿ[!ÿZÿŽZÿŽXÿwU;ÿ…U#ÿ‹VÿŒUÿ_TbÿW-ÿzAÿ¶_ÿcWcÿpN3ÿ†Oû€NÿMif'qœe'ÿ›d&üšc&ÿc ÿv\Rÿv\Qÿ˜`!ÿ™_ÿ›_ÿ—^ÿ˜^ÿ•]ÿ”]ÿZÿYÿŒXÿ‹WÿXÿŠVÿ†TÿŒTÿ^R^ÿyO&ÿ£zKÿ¯‰[ÿ…naÿYHKÿ‡O ü}Mÿ}L¾›d&i›d&ÿ˜b'üŸcÿo[ZÿrciÿlamÿxZIÿ˜^ÿ\$ÿ\"ÿ‘[ÿŽ[#ÿ}]Eÿ‹Y#ÿŽXÿŠWÿ‰VÿˆUÿ‡Tÿ…Sÿ‹Sÿ^MRÿ_Dÿ·–nÿ¸›{ÿŸ^ÿPGYÿNþ|Kÿ{JòzI"™c%O˜b%ÿšb!þŽ_/ÿ`ZrÿDz“ÿ¢ƒÿcRXÿ˜^ÿŽ["ÿZÿ‰X$ÿUOgÿwoÿTOiÿ‚T%ÿŠUÿ‡Tÿ†Sÿ…RÿƒQÿ‰QÿaKEÿzcVÿ­†WÿÒ²ÿ°cÿSJYÿvK ÿ|JýyIÿyHU˜a$*–`%øœaÿuWFþswÿçãÚÿ¨œ—ÿbPUÿ•\ÿŒY"ÿ–\ÿnZXÿ€ÿûá·ÿ—†€ÿgW\ÿWÿ„Rÿ„RÿƒQÿPÿ†OÿjK6ÿkZ\ÿ¹–kÿáÙÒÿµ‘eÿ]NRÿjI/ÿ}H üwGÿwG˜b%“_%Õ›`ÿkSMý’…ƒÿëèàÿ”‡„ÿePOÿ•[ÿŠX!ÿXÿvW@ÿfbyÿÄ«ÿjezÿnSCÿŠSÿƒQÿ‚PÿPÿ„PÿƒNÿsL&ÿ[Q`ÿ¶”jÿÌ»ªÿª„VÿgRJÿ_G=ÿ|FüuFÿuEš‘]%•™^ÿiRNû~zÿÒ¾žÿgatÿxU8ÿXÿŠVÿ‰UÿŒUÿiOCÿ[XsÿeNFÿ‡Qÿ‚PÿOÿƒOÿƒM ÿlF%ÿxKÿLÿOI^ÿ¥†dÿ±•vÿ²‘hÿfH2ÿYHJÿ{EüsDÿsC£Ž\$?”\ÿ|X:ýb[pÿze\ÿ[SfÿWÿ‰Vÿ‰Uÿ‡Tÿ…SÿŠSÿŒVÿ‰Qÿ‚Pÿ€OÿƒOÿwJÿRQoÿqtŽÿQSvÿJ ÿQCLÿwkÿvKÿ‡Y"ÿfC$ÿVJSÿxCüqCÿqB ™‡TY Å‘YÿpUHübRYÿŠWÿŠUÿˆTÿ†Sÿ†RÿˆRÿ‡Rÿ„Qÿ€OÿNÿ€Nÿ|KÿUUsÿÀ»¹ÿµ°®ÿNMiÿHÿaD2ÿg^lÿ¦Rÿm9ÿxdÿRHTÿvAüoAÿo@ {ŒXXM‹WÿWüWÿˆUÿ‡Tÿ…Sÿ‰RÿƒRÿtO.ÿmG%ÿvKÿƒOÿ„M ÿLÿoH%ÿV]…ÿ€“ÿMNpÿrEÿzG ÿuHÿFB]ÿž|Yÿ©‰bÿ´«¨ÿJ=FÿvBþm@ ÿn? L‰U‰V«‡Uÿ†Tû†Sÿ„RÿˆQÿqQ9ÿPMjÿ_S]ÿyv‹ÿd`vÿMEXÿ]LMÿxLÿ‚IÿiG+ÿ_@+ÿyF ÿxF ÿsEÿzDÿTFLÿ]F>ÿ§|Eÿ‰xqÿN?Eþs?ÿk> çl= ‡TˆU†Så…Rÿ„Rý„QÿPÿQNjÿ€Rÿ³Œ]ÿ¡vAÿ™l8ÿ‡_3ÿiRGÿFC_ÿ^JFÿ€Gÿ{H ÿuFÿtDÿsDÿsC ÿpBÿCFlÿj6ÿQ>CÿXB:üp=ÿj=  …R…RC„Qü‚Pþ‚Oý€OÿQLdÿ‡Vÿª‹iÿ€S!ÿ¾¨ÿµ™yÿ°Œ_ÿžvGÿRFPÿRIWÿxEÿsDÿrC ÿqB ÿpAÿr@ÿcA'ÿDGlÿIE^ÿj= ýj<ÿi< 8„Q‚OYNÿNý„M ügNBÿOGZÿšj0ÿ±’mÿıÿßÔÈÿ½§ÿ›ySÿ¦zBÿNBMÿ\G@ÿvBÿpBÿpA ÿo@ ÿm? ÿo=ÿh>ÿi< új;ÿh;›i;‚OMT~Lø|KÿJû_LJþIE^ÿ|W1ÿ uAÿ¦Uÿ¤‚[ÿl:ÿ«ŒgÿxIÿJE\ÿsAÿn@ ÿn? ÿm> ÿl= ÿj= ÿj;úi;ÿg:Ôh9f9€N|K4{JÜyIÿ~GükJ.ûJF`þNBNÿlQ?ÿ£Œvÿ»¦‘ÿŽkDÿRACÿQEQÿr?ÿl? ÿl> ÿk= ÿj<ýi;úg:ÿf9àf9#g9~LzI xH“vGüyEÿyEþfG.ûUHQüVRiþXWpþE?TÿRDIÿn? ÿl> þk= þj<üi;ûh:ýg9ÿf9Æe8f9|KyHvE,sD§rC÷uBÿvAþq>ÿl<ÿq@ÿr=þk= ÿj< ÿi;ÿh:þg:ÿf9ìe8sc6m@ f9yIyHpAqB oAlm@³m@ ám? øk= ÿi= ÿi<ÿh;úg:ãf9´f8ce7f9f9vEuEk= k= 'j<=i;Fh;@g:+e9 h:f9e8ÿÿÿÿÿ€?ÿþÿøÿðÿààÀÀ€€€€€€€€€€ÀÀààðøüþÿÿ€ÿàÿüÿÿÿÿÿ(0` $«q0ªp/ªo.¶v.Ÿg)Ÿg)žf(«q0ªq0©o.ªp/ªp/U©o/ˆ¨o.­¨n.ŧn-Ѧm-Ô¦m-Ï¥l,Á¤k,¨£k+ƒ¢j+S¢i* ¨q6œe'œd'ªq0ªq/«r1ªq0gªp/Īp/ù©o/ÿ¨o.ÿ§n.ÿ§m-ÿ¦m-ÿ¥l,ÿ¤l,ÿ¤k,ÿ£k+ÿ¢j+ÿ¢i*ÿ¡i*ú h)ÍŸg)~Ÿg('™c%™b%ªp/¬r1«q0tªq0éªp/ÿ©o/þ¨o.ÿ§n.ü§m.û¦m-ü¥l,ü¥l,ý¤k,ý£k+ý¢j+ü¢i*û¡i*û h)ü h)ÿŸg)ÿžg(ÿžf(ûe'·œd'B—a$–`#ªp/«q09ªq0תp/ÿ©o/þ¨o/ü¥n1ü§n-þ©n*ÿ§m+ÿ£l.ÿ£k.ÿ£k+ÿ£j+ÿ¢j*ÿ¡i*ÿ¡i*ÿ h)ÿŸg)ÿžg(þžf(üf'ûœe'þœd'ÿ›d&ÿšc&¿™b%4—`#”_"©p/ªq0mªp/ý©p/ÿ©o.û¦n1ý¬o'ÿ±p ÿ¥m.ÿœk8ÿŸk2ÿ«l"ÿ¬lÿ¡j,ÿ¡i+ÿ¡i*ÿ h)ÿŸg)ÿŸg(ÿžf(ÿf(ÿœe'ÿœd'ÿ›d&ÿšc&üšc%ü™b%ÿ˜b$þ—a$’–`# “^!©o/ªp/ƒªp/ÿ©o/û¨o.ü¥n0ÿ±o ÿ“kHÿ\^…ÿT^“ÿW_ÿU^’ÿU^‘ÿxcaÿ¨j ÿ¢i(ÿŸh*ÿŸg(ÿžf(ÿf(ÿe'ÿœe'ÿ›d&ÿ›d&ÿšc%ÿ™b%ÿ˜b%ÿ˜a$ü—a$ü–`#ÿ•_#Ú”_"4’]!©o.©p/x©o/ÿ¨o.ú¨n.þ¥m0ÿ°n ÿsepÿHV•ÿŠo`ÿ¢m2ÿ¦l(ÿ¢h'ÿ’hAÿ^`†ÿP\“ÿ g(ÿ g&ÿf(ÿe'ÿœe'ÿ›d&ÿ›d&ÿšc&ÿ™b%ÿ™b%ÿ˜a$ÿ—a$ÿ–`#ÿ–`#þ•_#û”^"ÿ”^"ú“]!_\ Z©o.©o/P©o.ÿ¨n.ü§n-þ¤m0ÿ¯n ÿxfkÿLS†ÿÍ iÿëÓ³ÿ›^ÿœc"ÿ£m1ÿž`ÿ©b ÿubeÿM[•ÿ£gÿ›e(ÿœd'ÿ›d&ÿšc&ÿ™c%ÿ™b%ÿ˜b$ÿ—a$ÿ—`$ÿ–`#ÿ•_#ÿ”_"ÿ”^"ÿ“^!ü’]!ü’\ ÿ‘\ |Z¨o.¨o.¨o.æ§n.ÿ¦m-ý¥m-ÿ¨m)ÿ›k:ÿGX™ÿ³~?ÿîâÕÿ¢n3ÿ·Œ[ÿæ×ÆÿäÔÃÿãÓÁÿ²Œcÿ§_ÿ`_ÿm^hÿ¥eÿ™c(ÿšc%ÿ™b%ÿ˜b%ÿ˜a$ÿ—`$ÿ–`#ÿ•_#ÿ•_"ÿ”^"ÿ“^!ÿ“]!ÿ’\!ÿ‘\ þ[ û[ÿZƒX§n.§n.›§m-ÿ¦m-û¥l,ÿ£k/ÿ®mÿndvÿmVUÿݹŒÿÁ {ÿ¨u:ÿíâÖÿ­}Gÿœc#ÿ¯Nÿìâ×ÿº‘bÿŽZ$ÿO\”ÿc!ÿ˜b&ÿ˜b%ÿ˜a$ÿ—a$ÿ–`#ÿ–`#ÿ•_"ÿ”^"ÿ“^!ÿ“]!ÿ’] ÿ\"ÿ‘\ÿ[ÿŽZ!þZûŽYÿYwŒW§m-§m-0¦m-û¦m-ÿ¥l,ÿ¤k,ÿ¢k.ÿªk!ÿ\`Šÿ‰]4ÿáɬÿ¬|GÿͰÿá{ÿÁžvÿìáÔÿª{Fÿ¥s;ÿäØÊÿŸcÿTZ‡ÿŒa7ÿ›b ÿ—a$ÿ–`#ÿ–`#ÿ”_$ÿ“_$ÿ”^"ÿ“]!ÿ’]!ÿ’\ ÿ\!ÿ”[ÿZ ÿŽZ ÿ”ZÿŒY þXüŒXÿ‹WXŠV¦m-¦m-™¥l,ÿ¤k,û£k+ÿ£j+ÿ¡j,ÿ§j#ÿZ_ŒÿŒ[)ÿÜħÿ³ˆYÿº“gÿþþýÿÿÿÿÿ÷óîÿãÔÃÿ”ZÿÛʸÿ²~AÿTTxÿ†`@ÿ›aÿ•`$ÿ•_#ÿ“^#ÿ›aÿ›`ÿ‘\!ÿ‘\!ÿ‘\ ÿ[!ÿ“[ÿ‚Y4ÿDU”ÿBT–ÿsWFÿ“Xÿ‰Wþ‹WÿŠVò‰U,‰U¥l,¤l,ê¤k+ÿ£k+þ¢j+ÿ¢i*ÿŸi,ÿ¨iÿ[_ˆÿ†[6ÿ½‘]ÿäÖÅÿ–\ÿȪˆÿÈ©‡ÿ´‹]ÿáоÿ‘VÿÛÉ·ÿ°~EÿNRÿ‹`5ÿ˜`ÿ”_#ÿ”^"ÿ˜^ÿz_Qÿy_Rÿ–]ÿ‘\ÿ[ ÿŽZ!ÿ–ZÿHUŒÿuWDÿŒYÿ;SŸÿzV7ÿVÿˆVü‰UÿˆTƃO¤k+P£k+ÿ¢j+ü¢i*ÿ¡i*ÿ h)ÿžg+ÿ©hÿl_lÿmamÿ¢^ ÿ˱–ÿçÙÊÿº”iÿ¼˜oÿëàÓÿ®ƒSÿše+ÿçÖÁÿa6ÿKWŽÿš`ÿ“^$ÿ’]"ÿ˜^ÿNNsÿWa”ÿXb“ÿKMtÿ”[ÿY ÿYÿƒX.ÿLU‡ÿVÿŒQ ÿ}W6ÿ@S•ÿŽUÿ‡Uÿ‡Tû‡Sÿ†Sy†S¢j+¢j*ÿ¡i*û¡i*ÿ h)ÿŸg)ÿžg)ÿŸf&ÿ˜e.ÿEYŸÿ›e)ÿ˜[ÿ¯‡[ÿзœÿзœÿ£r<ÿ¡q<ÿøøûÿÌ`ÿMNuÿo]^ÿœ_ÿ‘]%ÿ™_ÿ}_KÿWb”ÿà™ÿâÄ›ÿYb’ÿx]Mÿ”[ÿ’YÿrWHÿSQpÿ—\ÿ—oFÿŽNÿRTwÿcSWÿT ÿ…Sþ…Rÿ…Rñ„Q"…R¡i*Á¡i*ÿ h)üŸg)ÿŸg(ÿžf(ÿf'ÿ›e)ÿ¤eÿx_WÿGY›ÿe%ÿž[ ÿ’UÿTÿ”[ÿѱ‰ÿêÍ¥ÿe_uÿNV†ÿ›_ÿ‘]#ÿ‘\"ÿ—^ÿ~^GÿQ^•ÿع‘ÿÚ»“ÿR^“ÿy[Hÿ‘Zÿ’XÿgUVÿ\SfÿË©|ÿÉ´ÿ³eÿvJÿCT‘ÿŠSÿ„Rÿ„QûƒQÿƒP˜ƒQ¡i*  h)àŸh)ÿŸg(ýžf(ÿf(ÿe'ÿœe'ÿ›d&ÿ˜c)ÿ£dÿu^YÿCXŸÿp^`ÿb7ÿ”`'ÿšn@ÿ…k[ÿCIyÿRW€ÿ˜^ÿ’\ ÿ‘\ ÿ[ ÿŽZ!ÿ•[ÿRNjÿP^–ÿQ^–ÿOLlÿ‘XÿˆV ÿ“Xÿ\LSÿwuÿÁšjÿu;ÿư™ÿ£q3ÿEJuÿnRBÿ‰Qÿ‚Pþ‚Pÿ‚OõO%OŸh)Ÿg)ñžf(ÿžf(þe'ÿœe'ÿ›d&ÿšd'ÿ˜c(ÿ˜b(ÿ–b(ÿ bÿŽ`1ÿ^ZuÿNXŒÿNXŒÿKU‰ÿRVÿz]Mÿ›^ÿ‘\ ÿ\ ÿ[ÿZÿŽZÿŽZÿ“Yÿ|\Cÿ{[Dÿ‘WÿŠWÿˆVÿ‘V ÿ_R]ÿbWeÿ’XÿNÿ•j;ÿȨ€ÿbH<ÿPRvÿ‹Pÿ€OÿOû€Nÿ€N†€Nžf((žf(øf'ÿœe'þœd'ÿ›d&ÿ™c'ÿ›c#ÿ¢cÿ¡cÿ™a!ÿ”`'ÿ˜`ÿŸ`ÿ™_ÿ’^#ÿ•^ÿœ^ÿ˜]ÿ\#ÿ\!ÿZÿZÿŽYÿYÿXÿŠWÿ‘Yÿ‘YÿˆUÿ‰Vÿ‡UÿT ÿbSZÿ[Q`ÿR ÿ€Nÿw?ÿ¿¤†ÿd7ÿ?KÿPÿNÿ€NýMÿ~MÜ|Kf'*œe'úœd'ÿ›d&þšc&ÿ™c'ÿœc ÿ”a,ÿY[€ÿWZ‚ÿ‘_+ÿ˜`ÿ”_$ÿ’^%ÿ’^#ÿ“] ÿ‘\!ÿ\#ÿ["ÿŽ["ÿŽ\$ÿY ÿŽYÿXÿŒXÿ‹Wÿ‹Wÿ‰Vÿ‰VÿˆUÿˆTÿ†TÿS ÿfRPÿXSkÿ‡Jÿ°’qÿ¶˜wÿ¡}Wÿ²ˆUÿCDhÿmP>ÿ„M ÿ~Mÿ~Lý}Lÿ}KCœd'$›d&ö›d&ÿšc%þ™b%ÿ™b#ÿ—a&ÿCWœÿrZRÿnZZÿKWÿš_ÿ’^#ÿ“]!ÿ’]!ÿ‘\ ÿ‘\ÿ[!ÿ‘Zÿ™]ÿœd ÿ”XÿŒXÿ‹Wÿ‹WÿŠVÿ‰Vÿ‰UÿˆTÿ‡Tÿ†Sÿ…Sÿ‹RÿlRCÿNNoÿ“\ÿ¼¤‰ÿ²“qÿ²•vÿ¹”dÿYHJÿWPdÿ†Lÿ|Lÿ}Kû|Kÿ{J‰›d&šc&ë™b%ÿ™b%þ–a'ÿ¡bÿXY|ÿl^fÿ¼™iÿ²†NÿPT€ÿ€\@ÿ—]ÿ‘\!ÿ‘\ ÿ[ÿZ!ÿ’Zÿ†Y+ÿWY~ÿT`”ÿdV^ÿXÿŠWÿŠVÿ‰UÿˆUÿ‡Tÿ‡Sÿ†Sÿ…Rÿ„RÿˆQÿtQ3ÿEJuÿšh-ÿ°’rÿ–l>ÿϽ©ÿ¸—qÿuS7ÿFMzÿƒK ÿ{Kÿ{Jü{JÿzIÛd&™b%טb$ÿ—a$ý™a!ÿŽ`1ÿGS‹ÿª†[ÿÇÎÙÿÁ­‘ÿaYnÿnYVÿ™]ÿ[!ÿ[ÿŽZÿYÿˆX%ÿ8KŽÿm[[ÿ¤ŒxÿACjÿUSqÿU ÿ†TÿˆTÿ‡Tÿ†Sÿ…Rÿ…Rÿ„QÿƒQÿ„Pÿ}P"ÿ@J|ÿ•i5ÿ¯mÿ» ‚ÿêâÙÿ´—wÿ‹`0ÿ>I|ÿzKÿ{JÿzIþyIÿyHéxG˜a$µ—a$ÿ•`%ü`ÿq[Xÿ`YpÿÄ­ŽÿÝâëÿȵšÿc[oÿkXXÿ˜\ÿŽZ!ÿŽZÿŒZ!ÿ™]ÿa[pÿmeuÿѨpÿìâ×ÿ¯BÿK]›ÿ‚Y2ÿ‹Vÿ†Tÿ†Sÿ…Rÿ„QÿƒQÿƒPÿ‚PÿOÿƒOÿAK}ÿ‡a:ÿ³“nÿů–ÿòíçÿ±•vÿšl4ÿ@EpÿmK0ÿ}I ÿxHÿxHÿwGþwG4–`#ƒ–`#ÿ“_%ûž`ÿ^WnÿuddÿÑŲÿêïöÿÄ®ÿYWsÿrXMÿ•[ÿY ÿYÿ‹Y"ÿš`ÿ_]{ÿ…xzÿâÈ¥ÿõöùÿÈ¡oÿXd—ÿ]@ÿŒWÿ…Sÿ…Rÿ„QÿƒPÿ‚Pÿ‚OÿOÿNÿ‡MÿIMtÿqVCÿ¸–mÿí•ÿïèâÿ¨Šiÿ£v>ÿGB[ÿ_LIÿ~GÿwGÿwGývFÿvFQ•_#G”_"ÿ’^$ýœ_ÿVUvÿ€kaÿÒÉ»ÿÝáæÿ²•rÿLQ~ÿY4ÿYÿŒXÿŒXÿŠWÿUÿxT6ÿ4Fˆÿ‰_5ÿáwÿU>;ÿCPˆÿ‰Q ÿƒQÿ„QÿƒQÿ‚Pÿ‚OÿOÿNÿ~Nÿ}Mÿ†LÿXN]ÿXJSÿ¹”eÿ»£‰ÿÒÁ®ÿ{Uÿ¥zEÿP@DÿSL^ÿFÿuFÿvFýuEÿtEd”^"“^!è‘]$ÿ›^þVTtÿ~g]ÿÈÁ¶ÿÉÆÃÿŽpVÿFQ†ÿ“Zÿ‹Xÿ‹Wÿ‹WÿŠVÿˆVÿUÿmTJÿEW˜ÿPbŸÿHRƒÿ‚S!ÿ†RÿƒQÿƒPÿ‚PÿOÿNÿ„NÿˆNÿ‡MÿLÿK ÿjN<ÿBCfÿ²‹[ÿ®’tÿ«kÿ¦‡dÿœq<ÿZ@3ÿJKmÿ}EÿtEÿtEütDÿsDm‘\ ’\ ¢\#ÿš\üaWgÿhX^ÿ¸™nÿ´ZÿPS|ÿhVXÿ“Xÿ‰WÿŠVÿ‰Vÿ‰UÿˆTÿ†TÿŒSÿŠVÿ‰^1ÿŠP ÿ…Qÿ‚Pÿ‚Pÿ‚OÿOÿNÿˆNÿpL+ÿKGbÿBJxÿjL7ÿ€J ÿyLÿ8BuÿzQÿ¤ƒ^ÿ«Œjÿȵ ÿ|I ÿdE,ÿCJvÿ{DÿsDÿsDürC ÿrC i[\ D[ ÿ’Zý„Y0ÿ>S›ÿ„X-ÿ]TgÿHSˆÿ‘Wÿ‰VÿŠVÿ‰UÿˆUÿ‡Tÿ‡Sÿ†Sÿ„Rÿ…Rÿ‡TÿPÿ‚Pÿ‚OÿOÿ€NÿNÿ†M ÿQI]ÿ?U›ÿ‚™ÿ|qtÿ9J‰ÿ{JÿIÿCIsÿpVEÿ¿¢ÿr@ ÿzKÿwCÿrQ3ÿ?IzÿxDÿrCÿrC ýqB ÿqB YZÇY ÿ“YýsWGÿDT‘ÿVUsÿŒWÿ‹Vÿ‰UÿˆUÿˆTÿ‡Sÿ†Sÿ…Rÿ„RÿƒQÿƒQÿƒQÿ‚OÿOÿ€Nÿ€Nÿ~Mÿ†MÿRK`ÿQf¦ÿϱÿìܾÿƒ|ƒÿ@GuÿI ÿ~Hÿ[LQÿE?UÿÀžsÿŠd:ÿn<ÿzIÿ‡jOÿ=I~ÿvBÿqBÿqB ÿpA ÿo@ ?YYVXÿ‹XüXÿ‘Wÿ’WÿŠVÿˆUÿˆTÿ‡Tÿ†Sÿ…SÿƒRÿ…Qÿ‡Qÿ†Pÿ„PÿOÿOÿNÿMÿ~Mÿ€LÿtL%ÿ=NŒÿÁ¤yÿÖ˹ÿ€“ÿ2K–ÿsIÿzH ÿxGÿrHÿ5C|ÿ‘h8ÿ»¤‹ÿ]%ÿìÿ¯Ÿ“ÿ5>pÿuCÿpAþo@ ÿo@ ðn@ ‰V‹WÁ‹Wÿ‰VüˆVÿ‡UÿˆTÿ‡Tÿ†Sÿ†Sÿ„Rÿ†QÿŒQ ÿPÿvR2ÿtP1ÿwM$ÿ€Nÿ‡Mÿ†Mÿ~Lÿ{Lÿ~KÿuK ÿ;N‘ÿ^bƒÿFW“ÿ ÿl> ‘ˆUˆUŠˆTÿ‡Sû†Sÿ…Rÿ…Rÿ„Qÿ„Qÿ‚Pÿÿk> ýl> ÿk= D†SŠU†SdžSÿ…Rü„QÿƒQÿ‚Pÿ‰P ÿdOLÿRQnÿ‡DÿŸ|Wÿ»¢‡ÿ~Oÿ{IÿšrEÿ¡vCÿ“_ÿsA ÿLCSÿ7O™ÿaI>ÿFÿtGÿvFÿuEÿuEÿtDÿsCÿrC ÿqCÿtAÿfB#ÿ1J–ÿqBÿ~>ÿ?EpÿUB?ÿq=ýj= ÿj<Ôj< …R…R#„Qé„QÿƒPý‚PÿOÿ†O ÿgOCÿNQvÿ~@ÿªmÿžyRÿj1ÿ¼£‡ÿ°”sÿ¢\ÿ¯”wÿ»¢…ÿ›k.ÿi; ÿ9M‘ÿQI\ÿ}EÿsEÿtDÿsDÿsC ÿrC ÿqB ÿqA ÿnAÿu?ÿTCFÿ4H‹ÿJDYÿ6G‡ÿm=ÿj= ûj<ÿi<ki<„QƒQ=‚P÷‚OÿOý€NÿNÿ‚Mÿ;O“ÿqJ&ÿ‡Rÿȶ£ÿ‡Y&ÿ°“rÿÝÐÂÿØÉ¹ÿ¿§ÿ”nCÿº¤Œÿ¯lÿv;ÿÿ]@-ÿED`ÿg=ÿl<ýi< ÿi;Ôi;h;ƒPOJ€Nù€NÿMü~Mÿ‚L ÿlL4ÿ6OœÿuDÿŽ[#ÿ¯šÿ©‹hÿ¾¦‹ÿÖȸÿÞÒÅÿ̹¥ÿn:ÿ¶‚ÿ¡€Zÿo9ÿ5L•ÿjDÿtBÿqB ÿpA ÿo@ ÿo@ ÿn? ÿm? ÿm> ÿk> ÿn<ÿr;ÿk<ÿi;üh;ÿh:Jh:OMDMó~Lÿ}Lû{KÿƒJÿeK=ÿ5OžÿdD.ÿFÿ§„[ÿ±—|ÿ«oÿ­‘qÿ²—yÿsC ÿo>ÿº¤Žÿw@ÿSGPÿLG\ÿx@ÿnAÿo@ ÿn? ÿn? ÿm> ÿl> ÿk= ÿk<ÿi< ÿh< ÿh;úh:ÿg:Œh:€N~L/}KÞ|Kÿ|JûyJý€IÿoI%ÿ:MÿGKrÿg<ÿ}Dÿ\!ÿŠ_.ÿˆ`2ÿvIÿœ|Yÿ´…ÿv9ÿXFGÿGFdÿw?ÿm@ÿn? ÿm> ÿl> ÿl= ÿk= ÿj<ÿi<ÿi;ÿh:úg:ÿg9²i9i;~M|K{J°zIÿyIýwHû{G þ}FÿWIOÿ;NÿAHuÿdSSÿŠlNÿÙɶÿæ×Âÿµ•mÿwFÿ^A-ÿ1J–ÿdA!ÿp?ÿm? ÿl> ÿl= ÿk= ÿj<ÿj<ÿi;ÿh:ýg:úg9ÿf9¹e8 f9}L{JyHcxGîwGÿvFýuFû|EþwD ÿ^F;ÿEHlÿ;I‚ÿ9F}ÿIWŽÿ8@pÿ7E~ÿ:HÿaA)ÿq>ÿl> ÿl> ÿk= ÿj<ÿj<ÿi;ÿh;þh:ûg:ýf9ÿf8Ÿf7i=f9|KzIwGvF˜uEùuEÿsDþsDüxCüzBþtBÿkBÿb>ÿdC&ÿjAÿt>ÿp>ÿk> ÿl= ÿk= ÿj<ÿi;ÿi;ýh:ûg:ýf9ÿf8ðe8bf9f9yIvFtE%tD–sC ïrC ÿpBþoBÿoA ýq@ ûr@ûq?ün>ýk> ýk= ýk=üj<üi<ûi;ûh:þg:ÿf9ÿf9÷e8™e8f9f8wGwGqB qC qB dpA ¸o@ ño@ ÿn? ÿm? ÿl> ÿl= ÿk= ÿj<ÿi<ÿi;ÿh:ÿg:ÿg9þf9Öf8€e8f9f8uEtDn@ n? Cm> tl> k= ºj<Ëj<Òi;Ñi;Çh:³g:‘g9bf9+d8f8e8rC qB qB g:f9e8ÿÿÿÿÿÿÿÿÿÿÿøÿÿÿàÿÿÿ€ÿÿÿüÿüÿøÿðÿàÿàÀ?ÀÀ€€€€€€€€€€€ÀÀÀààððøøüþÿÿ€ÿÀÿàÿð?ÿüÿþÿÿÿ€ÿÿÿðÿÿÿÿÿÿÿÿÿÿÿÿ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1464275319.0 photutils-1.3.0/docs/_static/photutils.css0000644000214200020070000000040100000000000017314 0ustar00lbradley @import url("bootstrap-astropy.css"); div.topbar a.brand { background: transparent url("photutils_logo-32x32.png") no-repeat 8px 3px; background-image: url("photutils_logo.svg"), none; background-size: 32px 32px; } #logotext1 { color: #e8f2fc; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1418856802.0 photutils-1.3.0/docs/_static/photutils_banner-475x120.png0000644000214200020070000005045300000000000021601 0ustar00lbradley‰PNG  IHDRÛxº…XÀtEXtSoftwareAdobe ImageReadyqÉe<PÍIDATxÚì]`UúÿfÓ„E^Xè+MITä°&6ÄFâY°œ³œå¼$÷·ßiÀV’xŠŠ%ÁÆY *EQ U9–* B =;ÿ÷½}3ûfvfwf[xLvgwöÍ›yoÞïý¾÷"Dˆ!B"*±âKâø[ÒAüïª_:×%îˆ!B„ V¤£í‚“ϼ=h*y›F´S|ïÄ}Y–uGËʶ«¾+ÇOÉÞò¾’¼© û Ë_¬]Jˆ!B„U`Û.ý'ÂtŠ(¸‚œÎ!'ÿVùW‹¾ú2è"ðR.'à+±!B„9òÀ¶}ÆLd¨ÓÈÛtÀz©E°ôè*{¤˜òZÖ¸B¯!B„Ø9ò5€U¦Ý`m€=+AuAÈLð¨ˆÃ–a)‡ªæÙà}I¨›£,‰“ò³•¾@\uŸä‹;&DH«[Y¶m³Ú¬T‡³ÿŠë¬Ù¤)fàº+mIRAN’$XJì¿–›jàŽÄZPÆ·žc-”ãùwÀ•<Ÿ+ÓZ˜ ¬¸¥"²_7úFÝYt£*ж#”“M€­!BÂ*޶Vá”sîJM9ûNZÒ²[H@-ÑÌóE7Â!X*€éK‰"qûÊ5åp“(mìX+ehêBË!“)ì  ‹ÀëÝPˆ!BŽli3Ì6eâ]hÜ”GÙ‰$1†*)ŠZ/ÐÉvªì%¥F Õ˵ǖ=û¤˜™d?™.a¹ù¢; "¤­Šœ Ùà]ª©Ê LÜ•6¶'ÞN^ȲUQYt:Еչ C¤jÙZ9²Ête™®œGÕœ9tËE·"DHÚ" ¶ügÅpsÄÝñH«U#;Ïý[ªóÜ{Š"-&[:¨°&é̺ STõmDTËËÑ(Ÿ¹‰{ë£ZÆw8#\,TËB„iƒŒ6Ûà«löÖ¶ÎI÷:“þ–O°h  l-@yaK’´@æK)‚ ë=Ì|=7XàVAw&ù³ŠnšèžB„i2!ÈïŽ*iUjäNº×e‹dÊòŒÖa¨)jXÆý©–;Ju0"æw³†Çì%ûõpFÌÃó¯i>Ö’íë¦^ðAã¨r'èÖs#©ZVÊ¡û©¤d¹¹+^*ÝTˆ!B؆²“ïs2ã§™Ôh‰âŽ?ã'ÿ ;Üñ;\û+œO¶á޽ô˜µîc ˜v%`ÚæÖŸ•JñøAAûxÒWð|ògðzÃ`¸§öLº‘[Ï•¹µfïÚ28ÑU(nôM#W¼˜+ºª#Y'å/†®YuŸäKâNÙÑ’K*‹ˆ[FÌËöóÖ¶ÇL¾°"tá‘yÐÆ?èzÊkl„`yUì:¸%îè+UÁ6¹#|Ôt¼Þé|˜¿³TÊ Ð±}"ŒØ†ìç“×¾Ý9ï.—®£ÅØ8P-ë‚ØMt—Àq›aò¡K Pw±mD¥ ˆaný¬B®!Ë…™pp…¡#YRÅ-ˆˆðþåårpê ·8Bà.ÀÖ6О÷@> ¥±ž¡Ò}IXI9%˜ý̳ßÇQ ÷Æ-¥@[Eõ£æãanc&¬!Lße \uzOض{?̽{ Ü$õüU‡j጑ǑïÀšM;U Ãr^o6 „Û½‹Ú¿ Cªs Š°a-C Ì"lªåìø17áN®Hv D€­Ö&huLwIX´[;Òùü¿£µm)A¡toä'/ ¤¬0Ëí먂¿Å"È®¥,öÖ†ó(“E T0ïªI§À׫…oVo‚ñ#¶=`8Û×?]IAö‚qÃàÖKϤìö–'Þ€JÀ XbYç¾ –µžHú n®™¨c¨Ñr¢û8kÄû–%º­!BZ!à pmM`ÛåüÓŒ,¦À!Y]‡Õ‚.‚ìô˜•„i&Âmdç7÷Æ"VVuH9ó?ý^ïoÖÀ^}»u¢À;ÿ“•ô'¾ú a·d¿ æÞ3®úÇ< È!à>R?OÄIÛĪEÐeÌÞ€-g†[DØ­P) 9b$qR~º¸ áÕÐJ$ª®?].x0›Çb#NÉÔ=G2uóAc§% E0=ö{x¾ùTH«»…ío9>¾¬ )ÿ#»¥ ̹ }½z3\•WLYîðãzù¸çlsw¤ë¸¡ûçz™»nN¨sÉõë*„*å"Ñu…bA„ÏþÑÆl½ð3 a+TªçEå€-Žo‰ùŽý –ºû„†ëa›œâ»žËÊp¶K„ázÀ°=¡#y‚ë³-[OÁÖÈUUÍøÝã†ÃÚM»4uQP°£Ô@˜n|p¡ j™îÍK–¿ Ô6Bû"D€­´y4”—ÄÀCæÔŠ5®Ì?iÃ-JðlÌ05f <Ñ|<ÞtŸÈNU-wl—çÓ³ÆS EAðÄ ?{ô–‹`né7pßÜ÷ ýs˜”AÞPïlÅ]¨u„~,Š{sEò*D7"Ø—!G9Øv½(¿ˆI6‡Ž`tééLX®êàÕØ·a˜´®mº >n>A)AKÙé™ãè†ò1a¯¿ö9|³f3NPmêÄ“aÎ]WP†‹L–B:|\ÇEKeü³%VAõªø ðaÓqÞ¥Sh5©üJ àŽ"€+,”…´eé(n¶!­‡Ñ2+bH Y®t ½¸ñZX'wÕ?¡œ?f(<û×KéûçË–Âó¥K¡ê°`Ç }»9©Ae¥x=êaP?ãû1Âz€é1\V!\'Æ·ÔM­AWè kÌÂ×?W’LËHÅü¸äa0%¤-‹P# `4Ð^\àÍ!˪ñ“UÐ}Öñh›ÐB7Ž{ÖsŸÍ½¦žsÙeðÄë_@%Ùñ`§gŽ¥êd3©:T/ßà5X"uA¶‹|F~ x -áÄrXëî ¯7ƒp§ò3fa ¸³ÆN/©_ö|¹èÎañFÈ!r¦ÿˆ\Bްí–ùÏ"‚Ù\¤'  «³˜,ý W:VÃ_š/‚õrwàƒY Úxá£×CŽ×>ô:|¼lƒÊrÿóàÕ~ëUu¸®ùç«ô#J=2ýB¸jâÉp5Z\ßåUÔ÷Å-£1•Ç×\繄֛ʯ(aÜôQõKŸo•êdæÚ›”\” ®;¯&[YÝ'ù­^%N®'•¼d’m$xœøŒU²kQ@»œ\Oy+©o:«oÇ&¬¾•l[ÝBu  D€­} ý?£5\‹år¹Rlò fÑjᙘ÷á÷éð¦<’‹å)ãÂhh/¾÷X·ù7Z&6)êd3ŵÜÇ^ûÂk05°' +ËÔ½è¶Ä 83n'ý^ðpÍ©Ø2ÌHwKqýÒ¹®05§ÓãXÂe» ëOpÀÏd ›Ó˜.›xApk‹©l1ƒ]OE„ëêýWÚÁ1›”WÎ& ¡‚n8|Ãí0à%! ¼ú‰f\#– Äà|)ÃHdsõã&5úöO#Ç/¶Y½«‘¡ôe[½†Y¼wÓô-=)Àßás<Ç?r–ƨ°-ÚLÏÅ z(v˜?™î•R¼(†jHòYÏ½çª xó‹U°l­‹oîàZ,2Ý¥k·hز‚K㇤ `9=v%<ÿ%Ú[ë'³ŸNågtý%'»Þlÿ8¥zx­~0=âöÄÕpaÜf]ue àV2'EÓXj‚n g} ,®XV:)3«%Y.9?j òÂP¨‹Iy‘\¦®/‚ðÆNgíÐj&?Q+ÍÖr¾Tý3˜ó,±ÙWÚ “U&8é!<Ïø¬’²fÀ ¨U Øö¸äazb/CU¢Ûݾd‚Їl‹äA@ÂrútuÒí…÷Wø°åE+~"@|,yævxã‹ ˜VRëcØb@w7bU5¯%`|ÛSïÀŸýjV.ïí#ñ_PVûxã8xŒlzëgSЕeŸD fë¹f`92æwXÔþ=ØêN1ÕSið üñkõƒà¿)¥ð@òwðpíiØrvâø[ ê¾™ãŠâ ¡°©"á.*'Š*ÁJvM8ˆ•Bx-eiLpRö¨pƒ)³1ÚH‰2ùɈ4;"$ l¶Ð)(¯ вä‘fðÜÏ#e";Î! [1°íqé#JR§¯ZØèú³(Т¬‡î\Œ`(î:Û÷T‚Þ?wûž*ȸcÜ|Ñ7¼?^*Y.2^´VþxùOêz-ç0KËïØÏÅ}¡®i¸„fò‚(ø&Jàüs•º‡¢ZÆP˜?vò¡K š%SÀÃ1{Ñsui„áVב,¯?¶LÕ‡ÑÊ›ÆT¬¥Ù¬-¨†uE‰áV°kZ ‘ ¶Êô°h "8Ñ1›,¬ŠòäGHàÉa¹ŸöJÓkw¢ä:’nÚ"€E†ZfÏ`Ï›S÷\/&ß瘩ÝC[ÉÃhSõja­ñ“¤2I¿Á,€‰¬î¥k]ÔÐéÊsFÁ‹ï/÷aËÂi‘6º”ì-Y[gXv„:ø[ì74©†¼¨ñ|x9†j=a}(ªeÌ(ÔÏQíIã‡@« fñl}ÜŸô-\¿…2ÝlYa·ÑR÷­ŠÒyö‚g¨Ù>®…ÎC\ Ñ÷SÅÉÀmy!;ö¡ `HgýCH¶…uÔ­2ÑÕOVr¬î~c:A$4y:m’bXh(!%"èyÙ£äDR¦&¬¾¤†äg $>%.Ï€¤°„;ÀÃ^'K ËyñýðÐ ‚›ƒ•$É´bû$'@½—€lEÂ\˜³nk¼.l¸š¦ê“Ô2ì'JPÀ%90/ÇóçŒØpuü˜^{®šP-™‹ŸÐ8.ˆß &É ØåѺ8£Är¢-NÆ #-ÙQºža`µÁm… pE0 !mIôö$ŽVVº•Ìhn§-Èòg,4Øö¼ì1ò I…> ¢$; »àÄ2è7IË Ëù×›KàÅ<€ûÃË3áî©0nx*ÛúÓ }g5µáê‡1ޑɮJ˜76ûBó©0ªþVxÓ=@W—ð€.— Èh@þ<Ÿô)|Ø8nþ² á÷gÆî ”HÝ­¬“»˜š¦<5/éÌëHl¦hg†01¨àÚ£œ :A±êP®Aˆ(³ÚtÝÇYŒ©†¤Y Û(ÚåþŽ ZLÆvª÷Öªs9u)Ç$Ý|ŒÖseøL†Ïa|ïëdž?×0 ë¹þUË÷Ç/§ëµ÷Ö§ûÍ4â¸Þ°vË>z,ª›qm׿®œ–tÆm©µ_?çja€¥¦ñuŸä» ÀBñM·Yn¡?UM„Ï»|×±ðPýœj³<¼ÅA­‘:ÌJ{€IàÆRgØpe½8˱&ŸO°Ñ lÔ­\@Œ?«ò@àht–Øöºâñ|2¶§ù‚¥Ð5 fÁ¯çJ°z'þø?ø¾—¶À¿å xOÅ÷ö½•ðà+Ÿè¢P©sjè4VrÁy1©JÕÆKå~4"Õî0¯ÅjÀ’£ÿ`ö×sÑúùÖøaNÃI4_®d²ž‹Á7.?ùu'-»_ÌAr|Š•T~عò[ Sã@žh=Ã(cÁìø`¦"P›Óˆ ÈæM¸=Ÿ©uí€Õ„`Àìû¬“ºçhº…kÉ`Ïà }¢Ó¯‘ïóM&ùVÁÖ¬ !B,>k¼”D»¶Á¶÷O ÈæÉÏPuÆOf Ð"Ë}KËȳ~,†ÙR)ÞE°”ì¯'@¼”Ž|Š=€>r%ô‘*aì†aÒnÕªy©œ ÿrO€Ý'ÀvÙ©‚1‡ú† ÕË t¯Žó„™œÓx’¦.˜yèüqà_·càµO¾ƒ ÇPM¼Ö4 gÄ›z[‰Buq €-¸YvŒ˜”ÑØÆ&€L‹»µ4qà®%‡‹ÜdEl¯yr¡/ôzÐE×$ð¾Y\dÙ£Äx.¤Kš¦§uƒ­¢¾òF\ò’e–ktÑXj†| ü2`2ücɽÁ×»$ß(UR"uÚFõM9 –‹@ ªE/•°~¼c;(á™ÖÊ]¡JN t5®BdÿÖø`~ãPO™¬Œá{Á9p·íÙ÷M›Dý…?\º–²\½U€ÐiÉgþ%µæ«g¢Õ©f‘:(—#¸l°BdSÎ[&S㉠,†s# ¶`/ÀF™ åÚ£ƒ‰€uKó´Ð6bGZܶÀØöžò¯luV­€%ˆèzØòv¹¼H öE£!¦Zïû`RùéÁr˜c/Mï×WªŽÀíÏ7J˜òñ4‘¼Fµlƒ-_»ž”] sšNVkôéÞ >~òVê 0<æwvÉ’®…xëge_š¥~²¿(6;à©ks ´œ,ŒÐŒÛŽ•ùl?kÌ–µ`ÝR9n Ò¥" Z¥è€-¹ÑÕ ˜{ŠèjG|I‡§^p ‡«P° «­‹Aã¥Eò‰Ðбœ›Ö¦Œ?zN‡‡ÜWt¾¾?þ_ê‰:E˜¦ÐE¶Œiû0ö²â*tË%gPC¨[þõT³ô(ãG¤¯#Èw’Xüº I-Ô¡B; ©ks…A=©p†é6Ž ™õ³û0Û¦¶Aˆ¶¶3äÌ誖-mß©O:=³jIÃHMƒYØb¹áÝÀl8òÜ¢í“âàøaÔ€®>¢\2î˜4v8¼}Üõpcò p^Üfx?~¾p-úçÞÿ# žACB"­iŸ ÷];æ¾÷5¬Ý´K-?G2ʇËÖy¯A³`F Û¦ÀÖæš_k¾6;`m‰ 2²UæXV h_,Æt!md"ÏRa4+`‘ÙJ3ÉÀíÔ”¸~XnXA±åð©–¹O„)ÒjWÃo•µàFPLiÇõégŽ™cO€äácáö”› oÌ!¸uººpuó¢>œó ‹½ìùî–¬ñôõ±×>ó‰B¥¨‘1¾3Oñ×è'‚”æÛ¥ÏHocE¹Õ™eîQ¶6YíÂp]mWê(DHÔ„E‰Ò-Ù„ÝE«Á¶ïUO9ɘ=Ã,úàZQÏP-²ÜÄ„¸A“Öãší?jÞ÷ÀZ×>¨:\ 1±1ÐýØÎ0ü¸Þ0é”ÐkäÉpK» ¯£šF¦ …jxÌ^ªFƹ ã¾êÜS`né7Pù(T¸n{þ]ÏSÕ22^gû$-pKF ËÝ+ûƒykW[°#”gd*³hN€DG!­Ur &¸«Xé–[ Y-HN/fø] ÔAnÓ§° ñyºå7½½å¾ê\ÓõÜð©–c¡¬çzÞUA\×|œëÞ·î}>­ØËÖo‡-;öÂÁC‡!6†€î1)pêñÝÁÑo0<“A“ô“ª¹¾ ‹éûÖº»Â6èH?¼`ìPšÍˆæÕ5 ý8ÿÓï)ð~³f“~*bûÙ‡å¶5°ÝjãØ£i`·ÜŽÈŽ´5õ"$ÊìVIÚPi0Ž`Æž"–Õ§eÀ–ŒØÓTàT­Ê ÞjaAÃøsó×°ÞÑ V8Â$÷zXÞð\Þü}dUË ëÉÐøÉÿz®—宓»Ã_ÜÃ%?ÀÍ¿ý¾ú~#|üÝ/°jãVعgÔÖÕCûÄXH=¶ÌKÔ ScÖø€%_´`~£y¸zï¦VûѲõ°]Mý§7ƒb,÷0§¢ ´ Û¯=vT°GS\Þ–1;à-˜­¶¸.ƒ¯³É¶Š1ÝìpPùõ³íwõ¬lPÒçñ¸Q/5CŠ\ã Œ¥0ö\ÊtŸjz‹¶ æ_¿Ú0ùçò¡ããT¬ñ›Êϯ¯—å¾%„íÍN(·à´}[ ïÐe°`Ï8¡WGèÑ)b ‹Þ[U MÍnXä>ÆIÛ||k•`ÃòRw?zÎŒ5f\óÏÿLå§`§Ç ìæÏmkLC$& lËÅ­"Ä?à ňg…`ìÛF\Ë-$ÇáZïìP[ Y­lrž÷§Ë¿Âh÷¯0%á6¨–’!†|ŒZ\7é c'Ñcž$€»Ãq ,—³0]¿¡4É °Œ‰q@\L y×hÜöAÃFžÕ|<-¿ój^„®0çXx-aÄ’ ®ªi¨;D`XH¾n<;EV‹VÈërþØ¡”±~´lO³ A÷›Õ›i9Øèk«þ @þ\oÀ!B„Â.ÍcKÀc$cà–tÍ‚1²\dÂèWì/^P`›zÍìTZÅðÉt/oZ Pu3ãЧSôp&¡ú&ؼ÷0Ì®™cÜ›àÉÆ·`lÂý|ÁÒ ©º úÁD¡"ûtsB%…lÙ?èʺL>Û¡d¹¯ƒ±°î–—ÀÓÍÿªº$XÝèqC{èq/¸Og_†z¾ƒ1›8 «ýxÙz5‚TJûD¸jâÉôØùŸþà laPŽG ¯ %F³ò\£,ÔzB„b º¨ *g)ø”¬WNÍR!c»ÅäµÄnÖ X?¬v†Ì€MG=èŽnþæÅI­z MÜú› ‡ë›aŦýðý–J¸Sž ‹êŸ¤jeT/›w8TËÝi’â=`¤”?¶¬É•cÈ–õ,7ËÝ&:+m…¾l‚ó±ûDxÃ=’†p4b¨˜uh˜´wŸáeºlo{êz,æà}ÿ‰©±ʰ=à¶'ß6NäÀ@ùË*ı\‘oTˆ!Büƒ.2W´VÎÅõZðøŒ›iQØ.‚mUÐõ£F–2%l2ŸÆŽ®/ÊÐA®…Þò~Âl{C)Ð…€ï¼Ø3!…€óåÍ+!(ÿ\ £Í>¾ …Ý„AÏì} îû¹ äàOò†¨„~ôõÖ[?cö ­4Ÿ®Ræøá`Ýæß`ûžJê?;=s<_¶ÖnÞ /÷¬á' «·8F¡ ï â-K’äC’À×]¨õ‹&î:ŠÆ«×š*†K!BÂÃvÉ–O6dG˜ËŒÁb(ãÅ܆ Á– ÐÓŒýju Ë@×k;&ÇBÚÄÄHNN‚”vIp\÷пk2ÔÇ·ƒwbO…Iîu¾Á,ÐÕ H5qÂ$ñï4¿Ÿ6? WÈ?Bs—~3pŒ…-0O~>s?K€ø·ÐAW_ýÝ PÆ8i+³Bö|‚Ìv-[T'wl—/”-SÝ|0!Áø‘4A1” DÕr¢ß$ºÙ ¤ì¸* °ئ‹6r”oÙÐe¨¿ è¦\3?Ûtc¿Zt½Xã€øXÄÆÆ@bB´KN&[tII‚]ÛCJb|3Îm^GÝ„¼˜|* ¤ñvó‹ÐÀe17Á蘿AÙIwÃú³îƒswÀé1wÓcßu¿Lט¡š³e C !ô£“Lp½ÖÃl=eášìÒµ.úÁåž«ÏV*2^,ñg˜’óê³ðÃrÛ–Ø ¶(Nb$Dˆ—í"èÇ@­\‘™®ØœöúÕ¦ñ€cÌB2À@]oâã!))‰²ÛöÉ Ð³S]Çý.Öx´¼ Âúñ•¦Wi@scgÀ ‡‡NÙF èNÜÇÀeŽa»Ô æ¹_o±T~Ø¥òz¹;-§o7'e²ë6ïVY.ÊÔ‰'⣧.}U,ѼõT~ZÐm3b•EU†1Ø~[–Œ¬e¹¼D¯"¤5‚n9c¹F©ûfZe¶é¾¡A›ó´ã–š™å/†2Dv‹€‹¯Û'À±) P“LÝ„†ºw‚¿ÐV@w˜¼ ÆÈ›!7æ ¨¦4˜ßï >0 ç1ÐçX'ý¨ZJ‚?;®¥ìÕÌÑMåçy‡*dŒFU%%Òý¾]=Çë¶üÃö¤À‹‚¯ãF 0KyaµAäÏm3ƒadvR´mÁ/ì´ãÅal“T`+’9š·Œ£Q¦ïó[L8.Iª[Ëe‚já¦fjÜÐìv{7.–mBBÔÎ`=ÃAî ŒCC*õ»á¼S!¥Çù†óOQËDf»Àq2L!`ÝT~°¤`‹þ¸¬ŒqÃû3VK´«¶=¨ëÆËÁ€Ãj*?ßµå6!v@báÑô`3Æh'‘{¸\¾ìL€«r4nŽîc§Ñscäú“Ήī×Ç3ÈëqcâÞë›Cum446CssuŠ! 7.¦b¡Sr$°E¦‰VÉ<Ȝϫ‘{Nb| Ä8ô€nÎöôøŒÝÛ`O—S û¤“à´A½aˆTh°ÅmÉê-dsAüú-Ðû—· ëíL¿ß_] MÍôýẓ`,ÔúQuñ±î*4TÚMýp=åH4L#u,¡Åq°•ôȬd¶)ª4uñ f¡úç¶!Àe *[ ì~¥Ìâ=Röâ0œs†cK"ÝGZhé UÀŠ?€‹A1*t ‘~™íq9s„E¥ú°7¦»ÃÑ»wBc³ön„ÚúFhhh„úúhjj¢Çà:nÌøØø–0Û!TÌ3K_–‹1Ž‘·#mùí€Ç@×Nía`ÏcÈ÷±rlw3¤ MíªZ^†¦vƒSNì }ºv„Ž„ãoñ}Â(;&{Ô·íãÉyÈvÕ2Æ2ƀ롻Êt‡õïËÖmSKa TÕ2?éQŒ¬üe2a¹KÚHŸÍ³q¬+BiìZ»Øaóy¡²[òûl@6‰è¹Ø ñÙL Àl¥4Fg=¬SÌBe€ŒR}{<Œnúþã> öj€ªšF¨«¯‡ÚÚZp'$–ë¦Ç£µ2%§\Ju³x˸¿®±‰‹Å@Þ¯¶#Ü0ù$øóyžp‡Ë7l‡ü’/aýqR~órhzÀg?lò–©¾ø·¬Bųî@A1†V‹²”®£Ëvj­¡õ‚Öʼ´H ý8̱—~¾ª‘õÁ,¼uñ(|ã$GABÔ3m²ÚÙGãÓLÀ¬ŒÜ+—ÅÁ?•M`rƒllÓÂ(´I¹‰Vz44hq*딀¸lLÜH??BZ…œh:t¬*I’ÀËý6æx8½ùp`Ùw°öT×AM]=Ô°EÀmlj·ÛíeÅ\9æÇúua_÷œÂå]ÐÐowü^ù¯.V‹/ @»Áµ—þ]„®pÿŸ8††5•_à`„ÅÂnØNÍêØa©°nËnkn®tÝ '¬ƒY`?s‹cS¦[…ŽVDçô u%ÆUÁ¯ø(~¨ l;“±Ó`€v± ¥MìØ´(ÞgW ¦a†4… 䈇v°—úI:£%%ð½è~{]ƒ=§q ¨i‚M{j`ÿÁ:8|¸†nõx›šš¡¡Éí±VÖŒß`’©Õò'Ža°ÁÑ^n,Ö÷Wk\x«aýÖßU¬*hþ€¾_à8lçÏåá^gü¤e©Æªå>R%[ p©>\gØÛöVª®@ë™eºŽ=°Nîf1˜…wÀj銳]LéB;ªKÆhí ê”A†Wy´>°äÚ‹Áž/k‘Àå€Ö¸Û&6UÏ©¤~3[lÓÃ}´‹!zjjÁ #'iú5rª @wGLgø,n$\Úø-Ô5ɰåZØþÚš:ÊnQ¥\ߨë›àÊÐQ5Ž ‚åê@÷θ©Ð[> æÒWüð«5[aÍfê6Eªƒ§šÀåîï!?öB8(%é"Q™³Âfµ< öÀ2T!¸áþ²uÚöØNÀדрªª¦N½š –‚Y¨•¨¬üä1W;„[è¦ùc³d+%oKm8 ÏϵmÕ0îâmâd ¼Å&ÐVÀ µMìh_ .3¶ Uô¶‡³9 d,ð¨°s!TôQ«õÄê˜f*pAí=ã6[Ó”µÖ¸ÊJççl¯yz6ÿ»«Ž…õ»A—ñÐ|¹mÕ5ÂÞêÆnjL…ªgœe¶Ï¯ç²7ÔgwJü­ðRCü·á)Êvã×ÿí÷õ€ü¦/àòæïé¡o;NVáÑ›Ê/ø„õV×sÑ¿·JÄY]kÙò:¶¦Œ‚»lÝV¸{ªÇ5ˆÏl€îC˜¾O3!ð“lž[Ïm #"']Ta"8V°­ŠuÈ´fò¹G3«åØ ®Ý"ÃͶñ3df«Øš/‚$cBÌ-' —´ÐæùpÑJº„õ­JÖ§R•k!ß Ñ` ï¿–œ†¡øÂ‘DÜh+#Á:MÖžÃr mIXB\ªÊŠÄº;+?3ÐR¶dŒNÕæ•U†o™a-oŒCGtx7~4ÜQ÷‹© ý`šUÈæz.þÃ`ÁÅ[ÖÀ§áz®«å>l ¤Ì–n\—G˜¬jü4 ;U!ÿ÷ÛŸa8Z7yËnõxýkµuQÙ«õõTåGÀÃBAE¨MA*£6Ü“Ÿ‚0_G8Ö'gÔ©(À%¿Ég@›ÊõëH°,½Ì6Ð6MÖÏF}ŠÚ–„2qâ€V/ËÌÚÔáÅÿn>F Ëïÿ­Ý4Øéè Ï~5í QuÔ `pó¸£îc(NÈÿ BÝý‡jáõÇ•‚Hågå]t9ÒN$<ïÑ÷kx‚W$Á]WN @»ªjêaìðT ¼Ô?˜•9Yú–É©`%•Ÿ®~û=­A¸,‚³òŒ£,á€]ÀÅ{Ô?ŠZŒ‚­2qÈ c‘©¡J±õ=#–‚€‹ )=À`œŠ2ÙÐè,O,IyD×m žÏlVÿTêW#6Ih5`èž\®‰HÉг…µ‘å¾BŽÍÄûg´~5p:)P×ben UÌ»ô(©TÕ ×tø+¼v°^?TïÅ¡–ʧ7ý²ë¿¤ïŸN8¼K¿ÞÐt3~òœÖâz.·FÙHÀ}oåaŽêÉêÚ'+<Àš°çwáXÏE[j‰¬«Ë²õ[á®)gÂ?æ%Âì¿\D¿yð•OéQ¸vKÕÉìcž÷KÕ~ ® °ýdaü,f!Îs¹`´–ï?Þ£Qh&G 8hçD2«ª¥É5P0 S‘80‡hd°žEV4ìË6(ÕÊz#@” l0O3™@f1Œ–ä°ú:uõß –suïÈêÎˬô°2ðjÚRR…­W`žY mXF~SÎ&<úç#•[…lïÇj®ÏWrí8Òà>jÆ+÷)V«‡ôððÇþâ$óVËÕR2\”ò×Kê—ÑWd»Å gÁÓ‰ç{ÏdoÙty°ÔF³’LÀòp]£—Ìše8AWÖ˜>i&ÚøÆžß¼µx ÜxÁé°ñµ»©Ô%þªkêèõÖþþÊ'^V ?ÓõÚÊz-·Ùè–EyÀŸEÊrÖqÓC(ª’±'áâc¿ rI”„¡ ôíƒÛ¬hL|8À-„Ð-t'@Ö‚ÉšÃ@Õh"cÇpcZF²"àË«0tR\n0òi·bd®8ÛÁÈlØ:9-A¹U­Á\Ôy`l$˜Ê¶L›u, åÔÄj0‚±ZÕÅLjåZÝâij(ÀªÀ¦à’E¶ìµ8¶Çr×7A»„ØlÙ:è¶OŒ‡C˜¨@ò^£Or3–+ór¯Õrõáz8çΗ`XÿnÔõ§Š©½'>RÚ%ÂòuÛÔ{0VrÁ"yŠÛú$Jâ}¢òâú㣇*Z`°§j_æ¯9ì­›á,²$Zƒú®VÎ`½¦Að–Ç.6ÐG»=à–û­Öu ê…Œu§Úüy[Ì`ÏMê?ŠÕ?Ýæ}„¬7Z…›ôƒ´ Ê£Rf£2!8WÄJ®]-Ý#uéqÐMóÒ=3¼˜Æ±IYEX$ïQ²&ž±öçrepÇêÊ‘õuãÀ²]bœ‡Ýrqe],d ^iN¨­]»„8OV “k4,‡ã÷˰z@žã|]]4?R7ëö‹(Ÿý×é~¹VJOA¶<Á e(,W}?k߇ÿÒú¬[%“2ÒØ€Ÿfò€—3µÍôP~a¡ÒËSXŒÓD½éb «¼µ$z`ë®™\ýSý¨ùúG $˜U±¿~­¨–œË¢¬2¶SÿtÀR꾄ս¢•Ô9ŸÕ9U×gCVɳû}l$+?ÍB+³«Z5Â_o0 Ó•%-Xš2T£€ ñϵ–m¯ç$NðÏ–ý³ÜDÛúÆ ÖsQªYÂx½ŠÚ(•ßäÓO€¿õ•w_ú‰º ý«uãË0f¹ èJ%­áa¶H(Þ²m ÌÀËÚhýqp›­(r 6Û¯Ûbý™š6¿-߇œ$#Ð尜ղxËfIëµ<\›Ø[FM}“íÐf®B•Ô}ýsu•Mµ©«Ð”Œ‘T…Œë¹J]Æ’ Ô"Z ©üt~¾û>ügT¤0&*"Dˆ#Rb9`¨ô·®õ\#¦Ë§òÃŽhÁŒ 7o§¯;¤Î°Óq >µ"æxO*9ø(TælYËt{vN-»+M¬Ÿ ؼOl Õg @‹ë¹ø›¾Òø“üäÀUÀ‡m ˜´ÞS·–`µipt&s"Dˆ{`ûÓ 9ƒ§±q=< ë?ô£²zËûáŽÚàœÆ ®ü⧘>`ñýt{7ÿÙ ;<àK>7n4ŧC5$…º&ÆOÇ÷î K×oókýìOµì\sW¡±CúÂØ¡ýàÒ·^WÁüOÀTÈÒ`f„¬1~Ò…~ä&’T)Ýéç„"¤uƒ­<€te?®B Ð`¹p>px\Ò°œº=“t0å‰ŸÌ gD…l÷Ò†o!§a1ÝŠâ3`vÂdKë¹Fî9¸8–‹`žÛô œÛ¼z/Üwà_îóbΠ¯v\…ð `±â¹épYþ|XŽÀ­Ý;§œA?_¾~«ºž;Ž Á0ôöÏrË~_Xò "DHk}н -誫£¾ë¹Šië¹ç4¬†Å•P•1FŸJïø0'œ ;c:›–Ó§Kú‚‡’.ƒ þ Åhp?<ü8 qïM¢GˆB… é—Ö?Dö•Ø3á‡ëæÃKÝ®¡©ßj˜ëÉ$d)ô#—†ð÷*º MíúT~7ž*ŒÚþQô¹ú›að %Ûé$îzü§òãÖs D—"DˆV¶¨†”$}°|°`üdt‘ÉÎ94—ÆT¾¸ãß¡4a¬Îˆ tûNÔz›½phÙ‰çÁ…íï£ûókž&€»3¤Ð/ÕσÒ109ñN˜w& š|),ízMrP; žl|Ƹ7yËÑÞ<^?À݉°ÖíXOÑ€åÐþÝàÎ+΀—?Z \{Ô2n”—ѤË¥þvóçï]˜ç]Zˆ!BZ?³]Â@"ºYõËá±CÅðLò…poûl šÞrÀÔjùÛŸwÑ­º¶^¿7æ“íÜ“ÀGg¸ªÝ ª~ž_3†4ï *ÞòeÍ+éÚñM ×ÃAR§¡ýºB»Äx¸`ô‰ôw³âÎ…Žp}ó×`%Á®¹v„ZºÿÔ;K¡÷±á†ó<€;´Wx'ÿ*ØNï“o/Uãw„:ø“¼^vŒµ”´žgòä¥Dtg!B„i¢_³eÌH5¸ñì)á®çòyo}×sOkÜH€¶ž%@ûlÒ…>q’­øçîÜw²\…íâ~ѧ«ážO“áù𠏶»—‚°‘űYèÇÑÍ¿ÂWÉ£ Ó€Á0ºwg¸þO£< þ%¸ñ¼“aÓo`óoûaïÃê±mJ]%d£˜%_Â'+1CûÀÛåkaAù:Ð[?_.ÿ@Xíxja-ÙKXŸ+º²!B„´°]?纊¡·¾ZI†p§ž¡Úg¹Zн½æ}úûû;\ïëºÂîiƒzðëL@ÎxT}¼q—Æ=ç¡7—’ãzªÇÜ‘y*\ýø.8H@ꞤkáƒC¥߻q§ûøÕÖ54«Œ´¦¾þø¹¾ûy'ô«OÑKÞ†ô•ýéºðŒKN‡ËÎBYé?_[Ÿ~¿ ®oZ3¥$xiÑ~Â6z™m®ûS C]±a;ݼ—âeÜ—»€>òXs²—¹ðÏe [¼»ôA—èÊB„ÒFÀ–üådÏ”#4b¹vA·§ûw¸®ös¸mµ#Y•-#hN;{\wÎ0@ùËE'CuM”|¾ž^ø=¨ûÁ3ä}uM=<0uU'_:þDx÷›ðSlx/~4ÜQÿ1¼K^­†~ü,v\߸„ª“Wì;f¿÷-ÛyŸTÀ§?l†¨%ßŸÆ W ¾¯®WµŒ¾Â( Û¥N~]…Rä:(hþ²Úx¬ªD˜Ê¯R–$Áj…"¤•‹Ãà³%À;yßj ŒQy ¼¤nìŒé¥Iã|ÊLXlÙ?.Û/:‰-ª‰K—ýž}ÿGúºóƒôsÝ÷ó/ƒ ¡HŠ(þl-¼·ôPòÙ»½øTµ\Lç×ÛýœÛ¸Ú0ô£>ñ=eбÇÓèT/Ô½L­šwì;Hѯܸ“úÞþ»þ ¸³â&Y ý¸Üq-w(ì è*tƒ{)Ý*æ0L|/qÚ¾€‚Ýï= üj…"¤­1[ Ã2c"]ŽYy˜!cº:#óõ\/sˬ[ _$Œâ’Í{ÊÜçxõÎó YØ÷ÀªÂÊÍwÜ?e æ¹·O‚kžø@]ÏEÐ6q]»Ò· ]¿Eã¨ocO€‰MkhÒzÓT~:–;=éx¡öeø¨æ ¼ÇÁŽg7ÂÌëáÔÚïè÷W&ÞNËö2Kÿ¡78zÂh÷&øÄ1Ì4ñýPyüµù3ȹÈÃ†ÍØ²/Ë­øí½DÎW!B„i‹ÌvÝs׸ÈK…¾Ì ‚`¹½›ÿ€^Íû 4q<ðÜ2¥]<<{ë9hÞþdý_)”.ÿÅÐ?·”0Økÿõ!ýè´{Àé'öT™º}‡kº€ÖɽTÿÜÏ ÈžC˜m c¦+é2àº-Z'‡2³Vžý`aÄ[U°D Ì€zåÆßT°DŸX…¡z%ØÝf.,”Öø‰ã÷6X®ÿ\Эرàa},Dˆ!mTbý}¹ö™©ÅÃïx#dHµAJ50ò¬6nŒï½ª÷ÑDñ™E¯¤G[òÿgfŒ„k¹ƒúv¦ªâiO."Œ÷8í„îtÝöÛ»á;¨*Þ“Šj¹tÙ/P]Û¨&ÜäÉo‹VÉæñ–e¿ë¹õÚ‘3Ä …Êîz®™®.ÉA%¹±v*¤Åä /t’—4²U~ðÁGصe“—T`­ýúŽä¶8ªÁ–aZùS$ƒ¤ ì7˜5Üû9®Ÿ‡6ü_$žì±fk²¢sÔr$([þ+dŽ9î¿ü4˜öÔ)!#ÓÕà ³~~4û ÊjQå¬O)ÓXAC7‚.˜…ov"Ðd²jµ.ÐÕ'JÐ%' €œ±ý­»…µ¯–܃Ç:ýˆ˜ø1ÐZÌ®M }Z!ÚBH‹€í𧧏ãdüO“%›q’É߃Ždø’€lfÍ7ôYùË^ú»Sï¦úÚ¢<÷Ñj8;­/œJ@ø™[΂ûK¾¡@ª·Ü›üæ´Ö(÷E݇øç4¬‚ÏãG¸Ð˜±Üp®ìë*¤€®¬³’Ìr³¶½qWE ÎØ·°ÝYd&œÅÁ¬ˆ¼d³]œ…w 㬥‚”yÄNBȵÊ&_U2@(!×_,†° %›õ%­,ÑÆBZl2à ½8Øä_& xz5ÿAc$ïÚˆî ¶×f †ÇÞYI¿kÿaxìí•ððuãàì‘}áó‡/ƒRÂvWþo7MF€V̰È(”Ñ.ø>¯Øæ%Øä_VÝRz®’¤sü‚¥mÐ5p⸲O&ŸUË9ÛÞøkyˆí›É^]ì}4“drïd`É$GY˜fõÀfõåGÁ3Z Û Ô%ÒÉ=Fî©`7ÁÉö:»LÚZe“sã³–NÎ/!Ø|ò’‡c¹¦¨–ÀvÍÓW–˜ñæ,‚3­³PA—¼_˜|&ÜZ] ·|èt¤9­¢™áÚ³CÙŠMêš­çý¸c)¸bð %/h<õ(ZT1KººÜ^ó|AXí.G Ðù]™s6YÏžj63Úi hK°“‘ΖuVXn†ŽÇÅ@ƒ¥±#¤½òMþb6g ö”89)ÚXHka¶êì+“`A*AÊ,˜…&@ùîqç50ûB(#À»2a¬üu|¹f;œ5¢çž ,eî>2ÞKù°Û>p"²Y€Q02²Ü/“ÝAUÏÞ5c…¡Þvø‹ùºŽwÛ fYÕ² Е ÐÎh™ ™à,ry |+,þ>]aÅäAwÙ<ýÅìgKØ"ç8_:ÇÄËÆÁ©§ñ,—|®¼§*ev͸©Æ#œL…žY³Iª–Œ¥ÁWÎTëZ!•EwÀf׊Ïc)»×ÅmŸÊß í¦LÂ9!³[£6 p¼º´ ¶ìsÀ~ô½ ÔÆ!Þ³Pžm¿m6—w”ºµWϰ÷‹u£ÏD¸ú•-õÀÈo¥Ët¶¥‰8dݾ:(qFU³ÿx N­ÿ &u›EóÚvHŠƒ¢ ƒí»xþåÅrÊj•£› <;‡¹t><Ûîbx6ù"mÊl’ü R‹Ù!ƒÝ÷LV?ýu™¶»g38¦§Lvr”AF.ÝïKÙyñ¾Íâú€­º°~PÈM¸LÛ™;G!xí”ö›mÄ î›^4×fr_ÊÙuV˜”™ËŽ)å®»“PØmcýgì|ú{†ŸgÓê5)êã ÛØÄ W_Ý8“ÎÞ;õÏ›ócésiÒo\ìø2Ýy I¤Ò_X{”±q«”+3‹Ý·tvoË ê1“Õ£À¬ÿ9ì à«gO)Gëä`ƒYü½ÓtØs,ÌÛ÷0¤¸kàPm#äÌþŒlÏÎíáÝû. ~¶š\®\9š9‚¤ Ú?¨y;¼zà ø.~a¸#%ú@ÙKNp(¦™O¾ŸÀž‘¾žì¾d°ørVŸÅ¬õ7¸-†÷#\ml YººW°û¥Y²±{MÊ=%ßå±ý|“Irª“Ã2·²ºúéçý¹öÁߌduœ¦ƒð»~ìÞf2ðS¤” xf^ÆîC!+«œ•cØæãF…®~DÙýÈ&¯¹ºçEY²+÷§–wÕ$r%R!£R|äd†º1>.ïö(}ÿÎÞûᬺ¨ÛÎŒ—Àãïþ`›-ßvh!xœ2ÚiÇü 9Úùa¨’Žû†[ôV;–k1•ùCºp-7x¡,ä™26ãËô3ã\hÐY\ÜÃmE…œ©?7zžõ2bxJ}ËCd*ëtŽ-óê ö°Îæ®Õ‡ ê“\ŽUb·xýeV×yq‚¡ÛJb”rþž€K9ÇöyÉãîS¥~½0@²Ù@Va,6ë’fÄ฾˜©زٹ&%úY ÉãØœQýqK5Ñ~¸ ‹;mÌI…þsî~9ÃxMfl6žÌ2Yþ©dÀ”j2ÎèûØj?cÐVýuq¶Åzµ>+Ÿí4“ó’“g „›TÁ˜æW[Lo\=kJåÈ™ 2h‡d§7.ƒd‰åîŠ=þÜõïpKÕ»ÔhjeÂ`˜›r ¬Œ VÙrfí7pëÁRH‘kà¹ö™tó2I_÷/C ‚åFÀ?—W†“–-¯Þ ãE…¬g°eÜ ±8BçUU㜧RÇ|ùsç€wýt›—°‡(”{Sn2k5šð¿ÉãT‚vÏ—ªpíæê€u†Ñ gaðçôb³Ù97áQfñéþT”vݱ¸52e-ËÊä+P]\è–ëuEÕï£Z5Qç:uÇ¥ºõÃ|ƒ?£þá ²ÏÚjã(_“?õv…‰ö ’=Çé¬M]k”:TùQë+ÇÙ:¿«Y¬­fè&ÓØWv°õî p¥Å4œvóÞ$ ô‰N×Á—ɧPÐ÷ûÄ1ð‚ïÏq}éú®RΠ†­pbãV8µág8›°áî(Ks:dQõ1˜Ö0Ë®5ëç‚-¯þ%?w»ñç4oU%lW.Ö©™Ì¬ªJf*ÇJN-•Æ6Tæ„Á7 ðf«R¥c»•hËø‰ ÙÇ2ä»2¾d±Ýl ˜ÉM¬–0¶^jÀðƒàëÚ8+”º0™Í&…ì·ÊÄň‘¥s»Ì0?;vîKZ¸ÎI?Ö]“Ý2#ñ Oà&*ya“ˆbÆØ©%§BHbC99Ü ÆpÉ(9ƒI6ÿ}ÂÂr‡@Ϧßᚃ‹à”úŸàšCÿ5='1,†~ÜÉùÑŒfЕý†~TtÓ~Y.æ£ÍÚôêmåì às꽊Ùhí4Ô‡ÚicðÓ0k¨eŒ5Í`/–‡FýÃáž`À°ÓÀP »çÉ©Âx)â®5"çk¬X…â¹ôkJ>ªCýg6ÀËÍPÚ ×nuÆQ¶ê¢0 ææ’§<+™J¯Ì@ûöˆh6ïK›ˆK¡k*·p‘ºO ÏŠà„üL g0­œÑ²YøÁVÜ´Ü‹˜û†èdΠH g¾Éæ‘Õ"ÓU\|ÛÁ}ØsŒ§ÛÅÃ`Um4Çí„4 fÁù ›øç†¨Z.'edm*¾5b>™àUš­¥éŒ ÂÉny Í2ðiÅz‹?fÍÔ79äØÕ3§ª\7)уÄ4ÝÃlGÒÁ»æ«°Û Âf+9C©i¹Ð€ÙLiÕ8ï)®Ó¥ÛPUR eà˜ËÚ<T¹‚­ ëÊàUþí»¸þ‰ˆhî‹¢j] mGÂ}MJ¤ù™´§El]Üs5°e}¿¼FhÓX_XG8*PQxEÁ¢ Iâolhyo1ÁJÂzé–8vÅk»}]ŒÜsôTawª$Ÿæ͈$Ðr3@ç[È1ÜH0j0bŒl0Vú‡j¤…Æ Ì²Õ õF?Æ^`æJÝ02O!WźØÃcy"BÀ[IÕh £†RL…¬SEMü¨cŽB£{hbXR¡ `sX+ ±.Š&ƒ®y2㸠“6t× '?·¬€SOÕ?,¬Ïµ2 öš\F}µA1k³™eæ³ïÊè•Ò‹bð™mCUîâ44¡²['»NËjòØp]9ÜÊ´Ü·p #‘³eÀ,9UW!¿ Õ[ކjÙo926‚”ûkñôh °Rià Œ­‘RK½0EI嘭?·‘…ÜŒ7“û]&3«çE(ƒ;`ÅÜ€‘­XO’W;±N VqÅLš­ÒŒ}²`<¨2 Í“ ,Šà"É^J•Á€õ‡lvMN]?@«×‹Áëg©ø!¦‚×÷vT€ÉKs³ÈÔ©“mÕ…Ñ—Òè•ëÚ7‹µasç˜ÍÊMUž«æ‚6ÓØdl1Wv:7AÍjKI0B¸¦rÖfE¬8ÁëÒÇk6úq}{×ÎY¼&\?Åò±-fk©%«Æz”è–8–°ºå±1P±)â~r÷n¶•ß9Ây* /¯$̤ ó°ò‰Ö-&›·Ì‚d˦u‘B`º†l)ë×¢é¸Ehu*ä@3­2ŽQ„[…¼ÄÂyéCÉüó”àŠÿ)à"Ã`FcÀvÓl<(XF0V¡—ƒÎgÏ€! ÿ,Z/ÆW6¸g˜¹q › ‘1JãÈÅÚb1x£óµi¼¿³8vªø[V‚u‹é\v|çºe«.L‹ L´ò 6LW) œÂ(v?Ó•¼‘Ȇ8󔲕Éਖbp æš””ƒé\;frm ÐHÿHOHX3¸IR‡BðÆe×÷³bÖ•{¬†o¶nòP"fGX.†OæRáÊÚhA†~ŒX9ºï Ë0ÙXIþÎþå•›òAH0ì8•c´|=Ó9•¯+ÈsZŽéŠá¥2¦ñ¨„qsYQ ³ðŒ“c³¢pÓ8•¬ËÆïB¾ŸÁÖ…±„l£šõ‹B6Șí{Ý£Qv >k¶®)P;¶†Ô—vêŽ8Ê\xÆ\Áè‚-ºùžÙƒì”u±‡[t–a'ÙtqÖ8ë/ß(½ƒm¿ÅYt‰—Ÿ£l’…ñ›³ÙĽÀ_Üc!B¢ØoW1À¶Ìà‘®TEáåù•TìQçr8/…fD¥)4±«F³ “jŸdûÍ@+Äh©ß°Ú€’éÇPKQõ•‹Û$¤m>cѶr!G5)0a¹¨¾Àu‚t­jÙCmªeßL>.ò§„¼›µñ¥À f«?ÖÉ‚Z`GV›eb©,Ô„È^‹XÅ?\‰—‹’#rº iÁ>ª`W*Û|bŠ·*°Õî4¯ÕrÀ2„r8ÀÅ äç¯,E€­¿cóÙƒé"[®`µ–³lϘ ‰N¤¸jÍ>RÖK…´y°EYhu¶ÅÁ–ÝTÏŒVžF0.5Ú k±Œ ‹…²Ÿ^ȼ!B„±-Rk©HZî‚t‚qèF‘ ªUj8@7¨2h,WRFÙOÏg €"Dˆ!GØò2ræ\|FÚ> ¾:½v–[IÙ+W(ß0÷ºrÑ-„"Dȶ¾àûV*e»2à~2e¾²Ç¯Ê:è–³Ö*™îË®õÏ]+˜«!B„`kUFÜñFšìaÁ®µÏL *Dˆ!B„"Dˆ!Gƒü¿F®ÕTBIˆIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1561470461.0 photutils-1.3.0/docs/_static/photutils_banner.pdf0000644000214200020070000006065300000000000020641 0ustar00lbradley%PDF-1.5 %µí®û 3 0 obj << /Length 4 0 R /Filter /FlateDecode >> stream xœå}Ë®$IrÝ>¾" rÂßî[‚€ZŒ´ÐBЂH©Y nIh΂óù²sŽ™GܼÅ⨵$=uÍ3Âææöv‹ßß”Ö3ÏõHé|–•?©¬gÏé1ÛsŽa`~®Rµ?Ky¤³>KœŸg¯×c´çèùq|ËÏ|¦GéÏtæÇ·ô\«>~ìx¸åf«Ï6u<Ïšù™ÒxÌþl³>Êùgá,f;ìÑæÓ*Ík5Ÿ«Ùï¥>WÊ÷Y¿ß²’ÿöøßÇùÌ#ŸÖõiÿûvAÏUû˜ÓzLϳÍñxý8þôçãŸþòøÓ_¾·Ç_¾>Òÿû/ÿé¡_›ò9Ï´ÿzœ°ÿþùøïÿÃú:ÿó¨ÿüøÝžæ8ø‡ˆKm劾k{f{qg-ÓæVÏgoù±Ê3¯ñ(ÓPRËV ~«ÝræçœÅWË3ÛŠ³?×ÙG¶5ð<ףæf8ÍxüÄŸ5M cÿoÏõ‘s=O[ïã¿>b…ÿòOØ”56åT:ÛmàÛ“jTÛ·Òµù¨¶›Ãµ.SÂäl%6¼íL¯ùxõÌABè¶öãD¿Ã L­aäŽõ® ¦ (MÂDB#'Ѐ-3Û€ teÓ>5®M‘ó­špdz÷ùßãóO ”·µþví³ÑG)˨þ_­íì¿>~µ§¶]FÞs~pëm ±l¹6Ðß0€‘”ÍÉHmj€aLÁàldÂzÜoL›EýÛ0²é†›8Ï®uh$þŒ$Øk‰pþo§ñW›TŸgê8Z‰7æ– Ïöʶìk£²ci„Q:—œçnØCv`rµc‡SZö·w’eYÕg­ mõÌ:ã€LqvqD“팽:2Àõ,´m Ë%ÀìÔœ³ž‹:ØÉfuV £Æl†'ˆ5Ùæžg¦ó×ܧƒß÷üït2àVlDpM;ãÆY+–“6øâ2sÀÇ#ÛÙ)ûeA ¸ÒŸ¶X¶^šéY×îÒ¡—xDƒÏÇß¼fŠ“zŸ÷Å®¸öšÏÖméÙÉ¿Ã{{¹³ ¶Ù±œÆz ÎÆÀl6XØ=ãjÆ/—í&˜ %–½šŒes³ý8}ýw0ÂjÔ?‰+7vMYÈŽm}3?F›5¡TMÌØ‹í48ɰ7æÛ›ßFá: «¶G†mÈOcéݤŸÍüD;ì¹fÄ4AqU›5Hï`‹1[!Ø^o†¾Ö(ÐÑ}çãŸàŸA‡ƒUw ;#¶vÌ}%6†êÙ6w ä\l-ù”¬²¹ʺwoÈÉYK]ZjNz~¯Ý~¦qˆ(Þ±õ‡x/Tm°GdݨÚNv>Ÿà1ã4È~µÆ~mãBHÛžƒìûýõ?̾ ‰ÃD‘u”H¡¶ß¡ ÜÕàe#^ã°o;GÉ6ÍvÉÿÀ®ùkqi6¨NÆ…ìÜþöm[­'tЧLÑ×ÁRöþ}Í4lY¿dßë|dCMËÚ¢e:d–@3'ÁN¦oƒ¬NêQî ÷48C0ñl=wò0CEªE-ƨ{€oö*„„Zè%Ö»€fH;x"ŒðÐ`ÒÕ é?›Ð°1Á9å›qç^fÿÄ·M–ŒFÅÔÿø.2¼ }¨ú.‚˜ ËdäÚ›MÓÒ:N+ ðRènxˆ¿TÉϯ:ä¿H¡õL¯B 3YæãúÏ>'¾šŸgÌÃû¶†¯\½÷õ·±u°˜b´Z«àvx(Ü»ôF0ˆl²uã¯ùP¼ŒK7PÇÂ&?FyV¨Ô¦,Û¡·®ÊԣˠIþ¿$Æ~mñ“ÒÐð!?%µS¡ð¨Ð)ŽwÍÓ€œ³‘²Mú{iYL5>™³& …ÀxK§–Æ £€²*´{°QÄÅfd ¬·†Ÿƒ‡àÉçM¤Ýv Y &7N) ÅŒ#›{)އn‡Èp¸š˜z“üy¡šú§•ÿ¤ ÷Žª?ÌÙ±%cÑÖ„>l– -QSc!¿?¨ÏäBÓ#åä Ó0B“„Üý­‹?ÌÜÙ3H~ÒªÃdL\OZ)} ŽÝ)YØPþÇûF¾YCÔàr©XžýEC ÿJ! ÁÈ>ÑSÞc°¡¥òÿÄåAHÀ«QaJ#ÎÈZÁÂA–yŠÕUiÍÐ ›|i§±#ÓN ”〞¦×›†oÕU;-äH^¤L;ö‰–0´ÿ¾?‚ynLlRƒÃI¶†²JæÏ0zì´7šòo¦úÿVѱD04ÖD@ X`¥Æ¯V£Éoë;×™ÑK~t·=̾¯F9öϬõÀÔZ‘‚؆ÔH˜7Ö°örü5¬ËJ©ÑÚï‹",Aã1Ó¨ÚD¯‘µ@<6Xº‰Ò’úãmæ:Èo‹qR(u¤NRø¤5(–¼cå› í eÏpQ@Ùjp-UãSߌ@²1(›`5ÍâçZ‚}3 kjéÃOz}8˜Õýäóh°]5%vñm“&®—w 5âIåB#óW“þšVÇ{ìI!_Ö„üþøÓ_:=N¿ÿ?Þ2ղԲŜ зA 8~PÔ!ì„Oh«m‡€±/:&Œžý3änö·ý³±ùEþào/´ç½÷ýû÷ã}üß~Eì¿Ö¹»u`ÿê0Úø¦tˆA–‘s,I5 è‚‚ºSÎýj@ì÷ ¬ëÞßl^Fï×!3Æ|Xoð)ù«Ÿæû’Ö_@ld’ïð¯¦Tî­4¶¨¶ò¶™C}À¯–q<†fTí7lÞ˜ôTãbú2žß§TžÃ;¸0Ýfå[p›²C ~ÿ~¼Oáë~þbæ“Ö>¸í‡Ã+Ñö›F5™¾MðTŸŠC´’áò|ôÏèU‡¼ã׆}Žþ*¸ñ™¢cA`S>®ÿsÔ«ŸgŒ}|_ƒXTÊe I+¬Ì¢­rýñßF& €è"‡l CÊo±\Ó¨³A²°aß =ìTñ×Ã@³Bl2ƒšž ‚޳\ésÓ«¦»“µGǀȲc\X–šÒà‹éñy¾dNo+Iÿ‡^þŸ¿ƒžÀ^ºJƒï[’ªþ/ÿ‹¡…ß¶÷¦à¬Å²6KmÝxÎ-®ð×Tê=ÁwoTZ5B£kñ›‰ÔœÚm´ç•4„~=•éOÚšfy­ü6H»9Ñ«N´-qÖ‚=5ÜÃL66äaQ¾h²RŠ735è0ÛǶ¼~m`Ð s‚iY ioMòK-8‹³70q5Lj0°¾* «îq Ó}ÖiÜ!›Mt7wô'¸FW$jᓹ=Ó91#È,v)ØÞ0&²Ú8nôíAªÐl·æR%W¾2“B*ù™àx¯¨]Ó”6!Ý×R¨7¤I«l 8j†ÜÊ…èJSv©)™mªûeêG{ö\3=ð\$†Œyh£æÏ£Áæ]ô3ù_¦‡½'¸ìL.j¢þ4ÙmêX „«¤œY¹t_,|!Ð- ±G²ÃÔŒé@§J…n,“qTÔ Ú í•)I 'SÆ«)ãf<0ö Ú#ÜjðLÄöuwî^¦ Êú’ €c²¸e¼+ö­tøg9ÓÑ%ƒTmŒI­Ùž„7¶#˜4Ø4ex%;GCD¦C}æ`…ú©Á'vÕžª4½TJëâÄÁ) æCü>2¹öRýV8°q/tö"âðo@:¬N_¶­ðÑùzà¤E› ãÑh4»€|GíoG>W”²­•jlN™':Z>®³¹Î‘½…lÀE…6sdcœ0$à‚±©gãf œúbç3›²ãþîÖéJ$!ºtP¶÷ÍF=âDç<è‘¿}6!¥3îOëB, gèÊ;"c<¤e¯ÔD7Žâa#Ó3)LȰxáãj€ª¬•hË&~Þ[p®ëœä\¡•Rõi¶†¥0²b ò°ã&ÄÙë1wc§«bóÂó´aŽ`ŽÅÏpK#vRŸcMÙ6vì·d„mÒœ/Æöl•^LFQ 7[ÿ+Û›ZßÙÈÁçµ¹ÉY»Yq†÷œF?™µÁ•ì2˜ù‘a1´tc÷¦¯ÐÕ!ƒµÎ‹vFã7ê¤ÐK¤dã¡P3w¶å4­¯1¦|1l˜UŒÇeJÆÉþ´l•¼X¨m,7ÔÑ`ÍÝöù“½xBf" †¶hvзáu5Øâ )éuH•dÜÞ»gÈkéq ßäÚs£x{îðJR‹¯ìz-þ€L:=ÆÎ1Økã2…qEq±âhh‡âwß¿xÛ7øêÝI Fw ÑäŽMB×ìÄbu¢ÀXºèFŒÓ¯wlò¼~&0VNÇ(ó:Ñ`¢¼É‚š â‹}0M¢cƒº“kØ=pßcè&SÕú l9jU^Iü>³Œm*(&~õ…¶öøP'LˆÙ¸ Éf¥¶sÄëÆæêŠîÁWJ×ãr‹‡K“C~Ž=yd˾ºx@‹ß¯;r®îy>ºcöðɽ¡þ§»ñÛQ’Q#F0Ô b¡yEÃÇnab-qô·òkz7BK½»°g¬)+¡ç'-×kŒ£ä¯-ÇmxëK41æwDƒkA%7òh€Ë8„YŠ9Ft¨œ Ž{C)†ÍFMî‚b²¡ò­ ÅÇF€n:·aç« Èô3xÌs dBÇUH­ ²ª!ê–:˜¦± e¦´5LͯJ©ìÈ2ÁH‡ŽALoo•³˜l¤Z$ª¼ðÛ—´ÄèÔ€K‚jÒ™8…¸_ÇíäV°1ÎèENÚH|¡ ¿D56J*‚œ£„³Ú–?¨f’ž`P£Dÿ /Ðq ÀwôøF|o~³ÚJ Y#õÈêój»åc· ei] 8äHh€~°ƒÐËËH3ã–ÅÖFj|¼ÃðË4Wñ0Ø{°w ŸÃ´‘ø=l‚Å«2`{]6\1CDÏTuËžj9Òl’ëàGÑp[ÞìÒ3¾´le¤›©«Ÿ[΋È?®v,f¤@#*0|ÅDkFV…vÝ„+i)Œ— G}_ŒëÑA\œëZE¦Í±0òBÒÙu”^9D&2o2UTH«œ÷¶ãL#ö(ð€ÜiYûWÊøí¨§{`lr¬²©3£ác7à­Ò™m…¸Þš7ô¢eÁ]ÞUÙ°@@_áë;ƒ`"_[4òq{kÏ%:âTåÞÐé׈£KÍè´"΃´$#èH/¶ uÊö~€Ð’@×­˜t4CÒ£q$HÝ5eÒ䯪ð鈪—÷^¥ˆ Ä5 ì2'¦š3Ã2ƒÑ,äípš"K1ŸâJ“ š=ml CÜÎYLnA²Är*¶ñk`Þæª_ð5­»åcÅp$?ˆõ§ƒ9¸*ücÈ/i›`±¦DýbñA85WðJ€«æLTBðÚp­*1t1€5 qTDç»2$Sjö!ìYq " é¬EŽwC*@Ø|HÁ²ã Œ$“Ë?s7dÌ™È7ÚP‡©Øò—s`G£(Ð…x–§½lèÚ-µÐWÁì7ÐC©<±˜ež¹Z*„g14{ Pgp.&’7l‡lÄ+jYŒr{f3!ßùª1Š5Lš1Œ<°qÍÒà¹ðu8Œè;n0ÿñêa1/÷6ÒxaakËöY·†½ÝâK.6*|¶0‹ã†O÷ÝmtÞî›aã.Û~°¨#7Æâ-›ÕÇK7Óôk?FàÀ°ÒèX-Ì{N<ñÑò±[:×÷†“‘ÇJcâØ,2­Ü8”‚Ü6Ç€I‘ÊfP©Î âq†Vhù` }¼Q©jQ^£¿F¯8ÊäÎ#¤Õ8äÇ¡qäÇú1‡ÝAQtÅJ0΃X™˜ Ϥ«V{þ›)@)jÛ¹eåØàV+¾ÿç¶ðu\ܶÁ1|UÈ.Db ¬ÃžƒmW3i‘hÙ3L¬M¦çkp1UøÌùlBÄ`eFñ´ç—‚ ûw%ãB[Ã5‹|À“ yÍømC^eQrøå© O82¡r8¸Å¯«aRè€qârX2{ƒ?–É ÏË5I-Æ_½)5Éc «…»oQ»Å|‘#°ƒ¬…%ƒ'×7@+¦KBfZ´ÕªdãAc'v æ¸ôøÉ”!3Áé.‘L3ÍÄP ËÜÀ%¡5©pdˆ-æJLd÷6¤‹AÇ‚/ÛÖÌ¢ ¯v]ʱàFC\['ˆüTÄMšm$6´+¯_i»H¢?AèXpK1Ñ_à¶þvC¡šKÊB˜ •ý3㣇º0¦ù&1¬B™Ë6yhŒðÑä³éNBö¤f¶ˆá FB9D©’´—²˜z’ç‘K€3ÿ\ ”¬’¹y• kÇ”áOÈË´;' ‰æªU£ï$D… ¼ $V%!à\BŽ"„gÍ!üD¢HìBJÈ̼›à¦`îø?ˆyakŠ3Ã+_夣#ʆi#P.øE *hêØ "+$ìGYTý"ÓS™f fD÷3Mo,C؇謷p¤ yÌ^܈„­ë´º*Cät+à¢É”yŽ“pê ÙÖrøBiP™Œ.g(ãæ†³}7œL8pq5€Ïçp:(F‚Ùœ…&ÊE$¶$2I¸’»¨pbœj©¼fÑB ¦-²NFû²µ-)lÍ7«ó3ùq@1쇛 ‚Ó…„VÐ&ÒìEÚ‹äÒÈ0¦ÓŠ Sq¤YÈxwv„§•P ëç³9NêÚt Ÿ#F!€ç;œî@‡ ¬dOqDsKÄÞ1ÜB ¥R‹ SÓÙ§|‰§Ø%E:!.0ÚZèRÈÈ¥AÛ6Ú¦ÓKžÝõí9ÀÎPž5&2Ç>#%8‹=éþòÔà[®£ñ%ä7•yÀ[’|ÿ*ë~CžB²_ÿ@þ¨_%É'Õølƒ}@£ yDÆMýïP,K&Ë̸²Q f0èÜü®î¼1‰ºS¥½ " v¥2åI½[Cõ†ãC—æJºÞ8©ÊïþLwî$÷ŠùäÅí×lÂp¦ùrìù<ºò3³TÞÌA`ˆa;ÙÆË!Fá°Bûh ®&iš‰×xeYLktºÌ‘`*ÅÉÕJ¼?uãY¿›Á Miôc*„‰Þi;#Çfc°“`M™0à`èùuoȦÜ0E£„yˆÑñáFp!_fh8¾&µ?N®“‹NŸ=ò"æ /“ø@Ŧ1'–×Z¯Žx¦’ì›6ä… XP…Èäe(÷`ÕíŸê«o`‰h‡‡SnÀWŒ;àpD ­¦H\ÙÉÂ"d.ê@!n œB\áö@#Ãáä‘8„ðjÒ•Á gtöº ­7€05ít@UÂl –æˆD˜;| 8¸×†œо³)iƒ7Þ ³pìßóÀŠ™qbÆy_ã=¿få%g¦æT17Þý˜I‡ygÉï<Ì™nžõØxš)šÝ sãê¯=@ö–äóƒWÍffîd  '¬g^ ç´Ìëg˜)\+Cr€Ê¢Ôœ"È®¢,mÜÁ^e®7°ŸÅéÂàâÆÛ'Y³IŠ¡Îp=/)ðÊá2@LP#COǕϪô@¢ÈH´x±ä›d¥:„äÇgPBâŠçí€_ŸŸ<Ú$Ïéée‹÷Êh¤ †¬=ÉÈêÜR†“âó™§¢ì ®2í7í¶µé!‘[¬€t±Á¿!Æ5½Ì“Ô99ÃÙFBƒYŒ€)H¨ :wfö˸%Cêôî-û×5¼²© ‚&Ëk‹kÁæÁ·—ƒ–Áô‘&©Éœ´‰Ra¼có4rq'ÅE ®9ÚÉQr«§C!Í×am?•±x éÊa!1‘§, –²ŸóÙ8[R®G0áЇ|2©yUƒï_ˆÉäfÍæ™2˳ÁóôW}]ùÃ{©’ƒÈÂ*ò¢6òÁì=Òâ—†xe!øÅxÇ~úšÀúyMu7 N€?µ£iôø ä…•>?=àh!ý,ÝL Ûêq0¤¤Ë½˜]b”¨)© ~ çbÏè-QÒ™¯§JŸ¢ÀHdÆØÄü®åù J¤ôù 6ð|É—²i€ÙJëF#°æ†Ã#—Ô58 :)P‹,ÎÅÎÉžÓ )UÅ&à“‚D¡ :]¸›c)yw„ÞŽ»%Ki—•)ŽLJÑã¨ó!ßô8¯ÛG |'7£ÀUyDEF€fÝR\mú€vü]0$ñ­a°l JàAM ò Û=Ekô½'ÔL(ümòvò’¨(Ëp-!«vË:$šöˆ5ÈìÀ ˜ÒË™2º™é@ãTt<”O¦ûº5O@±/ŸÀ!5µéý“±NÀ8a?Šœòú†ƒ¾èwÞ©8úŠ…gV­y+´˜àö%wL? ä™áš‚€˜ñ<#!¡ ú.ÝÔ·}C.¥.Ògà.C­AöáÉ ;g°?ÎKk¡&Ÿºñ-}RЬB…/# ­')1LŠ9u±zQÛ˜ -£ìôK aÎMÔw žBT;«j·_ …ý;b°)ûõtÒßµµûñÓøI>Õ˜âŠ}\³7¹]˜ŒËKbåZ<3¡þ^È¡y8±k9f ó¯ ž;²]Êsôž”ÎúÚ£#ƒJML†ÕìÁ¦fããJ ,Öî!.Ç åÚìBΩðV¡PÜ:O Üo{ã GlÝ~]{{uï{ã;eÄìœr4ûÃ)M:Wçtkwº¼#² ܳÇã3Í3ù ³Ù{ñã±á©$G¤Po~òhô|ÒÅZ‘J…å䡼ù„Ó¸(:mk7ÓtUðu‚Süyfe#¤ÅkxìœVù¸É(ņë ÞÍÌì9vïà)±Íô‚ÀßöÔ JC#&ðµ—vì´ôýºP³{wÄiôÖ˜›Àãñ5í żVŸý–H4L²ë¦m£öÁ; y¢ÌpX‡ùøÔ€Œw¸fx‹ XͽúBŽ èS"…y\Sžs%ÉÚ`¬·‹“¡cøÝW‚^ Jæ 7˜_Çõ@§FËþ˜TŽˆIUYèÊǶnPå`%û"[sPHõîõÈ”Ÿ%] Ì`àmÿá·0Mê.Ž˜‡.ÖãØ±‚ZgêÅVOÑÀbgJ±ÃB¨'Uš0]ÓÖ¡?1'ZpètÕŠ©Ð +7¦ l^L‰)“ýµ>…÷$&$ˆCçý/ضEK.¼M8•6©-šèm)É]R°{°¥*_ æ@; ÐP&uv%þdZ‡YhF–=—’xq¡^Cþæb¹RŸèÃÀz=œ8 Š1ªñºjLrèÌÏ‚n»ºï¹C§ÆtHšä]ÖÓð[?¤'Ùf̤ñ5iÛoð¢6ÐÒl§ Z@÷â`k<ÿ¿ƒ©I$Ó¹JºU9Ù\`¯½Õ*B6’»M‰ µˆB+~p›ªHŠ:å8FpÝžÍ!|FñkæõÓ¬,ÞTRO’„LVÍ4¦m:S—L‹û8~F?¦N•S9ºOÖ'è7 éËö©L Fð÷lÎ|´Â%÷ ÄW1,®@ZÉŠ'û¯EAHZ+€èWC “~µ\*#5dý… «ä$å"†ÀÅŠ¥;+JÓÅ1û+,cp㘘+“Ê®bÆi”4&÷+JTwáœY;ñ !ÈȽ†›AÐÙ šá‚J“SÉÓGº™7lZ÷]ôý©žßm¼.Í(&»èJ?öb<èZ\×ÑõL5°x¸Qñ #¿1VGÞÈöiÀŸt师,Ìq×åzÂæ1+’\n%ÊÛL‰Oä;£~^\Áå¢Ûþ•";SCœ>Œ(fúe<À L3~F¬ or'¤žJÂþLÆÿ^l0£¨HÚ1¬¼Œ@wµp[EÊÔ€« tÑ-ÖÎÊÍ3ór;)3s‡—xG MXm‹þªŒŒ0•“+ë1!23‡¦±&Ú©Ë9n¸ÿ ôvr€³ÉFëºÀsH‹–ª}‰×•±»»Ïq'+†Ïb±š¿4ªbúIålxÏcÒwמåÊ£‡·‹áž%D]"?æ}­Àì)í10/Ï»ª†ÝàVá±wï2Ñ Š¬`¾#q?++yo¢š:'-s=Q¯©Å‚ ½“… ,€‡4ø.7HüúólèÌLÒë,·»Gîn×ñ"jf¹JÞñùJ—"Ã`Ð1ûéŽÔ5|=?]³1s$Û¥ŠþÇn˜ºœÄ|2LƒQ‚•â s_÷™à,'#ιËGŸ˜¦€ $Ôâ"‚yk tBÚ–*ÜByí¼L´h>eÞ-Ô½ƒŒîÓýqM*_V?€']îÀå'ç¥"ЏÍ2Ì–pa@v<Â>!š„Z×¥÷íWÁëem<`4^1¢paOG%0Ý )Ó•q¼ý ¼(ÕZXâ|‚*o•Äóœ‰Ú:o]Ûƒ„ Lž;x=8Ù¾ÕXÍ£SqêÜ…Ú¿/]8Do8¤¸~+CÎn4xÚà ËIGn x:©ÞG´ÄqãM¢Y>Á̲҅^50õj˜ L_Ü.J˜JR˜¤Uë2GöH&Ü™`(@À¥fxsgç¨&?–[òðŽ$Ö`ò¡íYyÁDZ ÛÏçG·œÖ…v*Îr5Вﺎ–HGe ÔªD\áP$ݦÅÐX+sÃZe¡±ò´Úõ{'á“,4<^Ø< GLVYæÜûɉÔý¼¯ˆÕª&ånX¬oÆ‚{׃-Sœ £ ?ÿ]µ8®[”#Ô²&78£—×¥¨O Åî> éûXär™Ì"êôëéÈ*ºzEŒdâ¨w}¼íÄO7ÇX Ó=W„Hm¿¼ GIºäþº˜.E&åÖ#B°­l÷"k°A«ç£¡µÐß3£|3ì`†Ãx•ßÊÛ™ sÀôÀ >¸ÑãÜN¤¹ÞŽõt·püÌ»P—Í”*uVãz g« =8#¢Ò››ü‘Åí¹Dz|Ò«§ªsl©ò#Åë†âyÈ—Àð],zI¤MUm&Ëå䙜5âuÁr±XdRÆï¨&YMèd¨.òŽ—ha÷JÞ.§²ê)ÔÞüÎÄ{SÁt »ƒnè“lu+F¾‚šh¨ …ƒnòAŠhcÛSÐk7IÉéñ6ȸ§Ýû@pøN•)–3 >ÏÂ+H@Î5Íßä³gÑæ³™ÁN)VÇŒØa] kAÞ9lÜ‘‡W–•ƒd$Ï]Ø«ô»GFq¹ äMJr”Ã)£Œn‰Pø8­mf‹Ò×ç÷«X}}JmV 8Æ.iƒ:U†×SÄD¶Wu¦ˆ>rç0×’73Gä¹”·gþÇþÝpSÇå³Jºzg ޶GÇÆ±FõÃïé›û²Ž—ëÌ%QÞÅ+ʸ²KÈl(^´õ›÷¨Jû+"ÃzçN¿ñÖ¯í¬ð˼gùôÉ' kf–ŸŸ·”7H‘1w°EÒb‡Q3^yßdyñÌ(ÖpÁåt&jo˜…ÿŽH9ñ9tš Ä£š÷Mÿ¼[ÙòzË[×F׊ƒm´œ§¤‡k£™ÕFZ¡Öé_¡ŽxnåÇ‘:zkà]W€ŠEAXpcë£h¡ŽI}ç˜>ZN•üÛ )n @æÒºB uĶBŠ›Î¬i-…´°:i0¿ÂÐ@½˜#”ddñ'飉­¢!—k2ªÕ"u” lÓÍ—ê^ê(ÒœÏê(;È)ÔQô^ûŽgœŽgRŽÎÉfÏÅ)™K“³Zb‹˜ºÔSàu*¨¢4dAô¨—6ZNe¤º6 °Ë…6ºÁÐF½áØ„ÇZþ“‡òɆœ>7xq*8Ã1aj9RFyG½_º(7KÊggáã ˆ;Õ#r~øÞ¿ÆçsÏxÇ’­Ž"RœÛVGã|P=oˆ7U‘´=Ò¥ñÜ Žbtr ©£œÌœ—>ІūdÒG9ÿš‚bu§ôMqD4x(H¿£ük¨£€Ì0¹©£Ä¥|½ƒ›/³×ÕÑ8¡[Ý [ÅœÕQ ÐÚÖGÉJ¿ôQž±ŒB‘t}0݈¡bõÈbw}`QÕK裓~=Y¼P´õÑkê£o[ñÓÝAI„¾K`äÇãÖ`Ê ÁÉ{1*‡;R,N¯\€kÐûò+ót½µÊ»÷‰VÇ%r•JÝ¿*{eÑó‚IUºç~`ñÑÅ~Ö3õ5ýO“¥_„¡3ãNp.á©;à1DÕˆÖÕŒ³_þI»†@ROÖ0»+ÅÏÎT ö&Á‚¼g€ ¬¿!ÂÉ…`ŠjøqCöVÚ\chú¼ü¨)à/Rp{¡éƒðAêé–7s ^–8þ<YEë&·Ëöê:L¶;n?Ó>›»¹Äêgˆæ ºDƒ „xx$H8Œ ¿®H‡k™ºçY®u_•É:x¾õxÖãÈg? -/i0$¼ 6U(¹¨ °@öˆ´H€¬E×T}Yϳ|fã <®f«2°Ie¼=øQ?Ho0ž!o Å^–;+qÑÀÌÚÊ|5ÇÁ©æ)‚K—h÷‰á}€'å"êʱ3ÊÐɈN‰èûÉû¯œ÷H©«"qkÓc€Añºií yL2Dõõg÷UwnRµ“¤r†“ <ˆGˆ”¿6ªC¸(êŸê˜zV«r¢, BÆ ¼´;í±3^r%bÈýpyXɲ,ùçV•è¨}2íä×7™'1Øã^põCüjî€ÒãV–¢„/à**Š”ô5¢qJgpS(#w*9`— ¸5Ïã[(ÈKxW£‘¹®ÓÎÄJŸËz*ù@((Ä„`ø­Ôï³xî[~§>GSY‚ËÔ• *ZÁÅw±7nïì×M¾x‡ýÞ½K‘¾J`rÇ^sß ñxfR¤¯[¬á†–øV ng ­šÍ í-\%Ú•&{Öt‘ø¶«º¾¿w½Kõ’øD2?¥"SÖ³¾<·Ý3ÑU©äwfàÜ8”<–§ÎtÓµXÏt,»F¹-x£.³?w&>ÀGËS½¥.<¸g=¶ƒÅ»xãPþß’= Ìe4n‡Þ¼Ç¨üÅÌÿÙY!&ðT cfI\÷Eƒ #¸”¸zŠ”+€¸ŠïÉÏ/V*;«.ÿÛ(½2RøK6Å釬¤\TÅ´GÄg î|}ů¶‚Nš|8èHßGRâZÉž¦©†Èúî¹0)YâLþJ*^yÐt‘ÊéHõä Zß0‡Õw·øL¡{«o{¸¤HìGª ¾É>VñO4°$©· ¯— ´*س¼œœEŽ¥y>øÀTBX©þöTíÔh]YûÊ&Ãl"MÔsuO’&Ò_\, ZÄÙ=Íò¦WÞ›¼©…‰UÛ< õNåç¢îãJà%õëR`y8¦g 1?<ð¦ºÒVCÏÁr®Ã(oêÛÙä FÑ É|?»A¹]@ ¿!ÍÝi‘8/t!Ëñ`FV%‚\¥ŠŸ™[çÐÕt|ãg«ÂËOä1ÚS=¥·0mQAêÓÑéWÈz%žw#°«or¡8Êf´ré‰^~²+Ó‹åÌÃ3NýîI•Èrã¹ÉQsü]ÆððET]"mü"Ï, W !=PH_î2ªté4Æ"p+\7ÀU4lÝã]7Ž[‹\©×;ï ÛãW‹égø¸¿‘â6Ç[ƒBsW·‰y§~Ü[”ÃÙö‹WUK•ã¦l§IÕ§yp᤭^ŒLÐP qéà‹ Š—ÿ®²²äÛuc%Tä-ƒšïŠ|æUßš¥|¬rù"go¸óà}¯±ýHž¾‰;Á*ÄB¥¥\P„JûýrÓãÂS=šÊîñþ~„JÑ@œ3TŠ"}nSbšpߪ›Må+%âL9E7q‡œ£~9¥[÷ì“xÝ–"‹ºˆfÄð3üáœÚŠd˜¹9%–†ë­ÉBÅ‹ïoÄ ‡®Qo(Ħè"±*ð¸Y<ŒÎ_w{0zßQ§Ý£R‡Ï.‚V1ûKdùâ<è+÷˜X`F\þpTεCrÙŽùKŠøÎìß¹oÇõz w›Kßø=¾¨bÏÎ]²1û#Èj/OTç«¢¼°ãDØs 'jÇg‚Òô±ï3 ör~J¾„ÔÀìyÑtë-ëÎŽG{¹¼V"ÚËZ”=_Á^àƒoìXÇö\Y )Õ »*:¡hoáGXw0wïF{KÛ©¥h8 ¿Q¯×{¼ÝIÕD°Y³* ÀÙNLüÞ„¯N×céóŽz§¼T/Ü-Çê¡¿÷Ïa-O½áùèqî}@ö}2G„ýc¶;î«ñ¤X«§.®œƒ@–'%.#(b/¯þc¯÷ø¢„=;QÊmö¤¤ÛòTMx/^”xÃ(uãNtˆõ½ýŠôM t–£ÚU8P¯6›TD&Òaª 1w}3ýS?¬›f»j»N© -*wU³ŸNZÖ/6 I³ßj² ¥Í~=àuX­ lÕ¨æ¯×-|cË(ø\†Ç=Uâ #Æ[ƒCṡëú8oC pˆï¾IÔe”ÂOxŒ¥Áƒ‚±R'èõaä¸Cé ‘œ"H¼Ò„"Xí [ÇÑF]Ñ÷÷³½ô{á;ǹð›9óÊq.^OËÊTÞ¸ç8£20¿7½sœËÔG1=Ç ê¤zŽsÁá|¥8&,D†3 ºüCØP‡çyF†sÁêU#Ã`öê<ü°÷6:v‹2œ÷ë^‰;º g?2œcrIWcîžßÌ•õùÍ\·{Àt•rá9ðöZw«Ê_ÞXüæ2U¦pç7GÃÞ[mÜqß]Ô0Sª­r}ËÔçj½S*9Ò›1ßSÕw”ÞŒ”:Ь6,׿·¼%jB³øÖ¦¤TBR¢á¨Œß^×oÝ7~bøÆ•O ú[ ƒFõµ=CÒW–†F÷u'÷M^’ Úà†>•˜E¬:¼U‹Ý ƒ&^w½0zß*»~„N“s?f¿M‚Xœ› ±ò¨…,ÌÜ.†êÜ" ̺Á˜ßMìL\ Û¯Ÿrcíî}ãc|§Š˜ˆ&НMT±:§¹X»“ä…'Ù@žúظýLñ*Ø­›·C0£ËÞb‚ïš “CkÇzŒpWë¹¾¶BçˆräÛ¢AÙ •ׯ¶Epæy³hXK~l‹¦ú'¦ÝbÙÛ «¥—Ë¢Áö ÔÛHO¾w^Yåm^#¿^s«JöØöLet¯nÚ½ðÔî¨wÂËÛž¸Ò6i¼Naéßñª+çÑõVÞ}äÈXöi¹æÓÞ¦A,ËM‡X´Ži½‡€’ì „ºÙ²9O˜5±!ûwm×±_ç†^fMlxŒîässr¹æ.rÚf“[˜5AŽfœ\sNÌŽVAÇ/éßä‚ßÃvÿßVð× ùw`ͪഠ|T@åЦÙ·fOÁøÒp½3"mò­Ã÷wöD¼,q®{ƒræðí‚“õ UW•Îh]wñmÚ !©šýf¬|ò•vÒº\ô5÷ðF á¨ºí¬Ã¶øsSòwâ5^4x+¬!ܲîÁ)Ÿ"<8¹å,šscÀ ¢3îb+{éC®­+û ÌmPÕ¢šu*87Ô´qçÜÙCŠñ{Uú“2½*=ï3Ôê²jR"Õ[<•ÊÎ:Ë Ïž£ÀÇ‹ YŒ×ñ+ öˆX?Ñ÷= }”‚ŸSSbf çrnp¸»øøþ•JA¸óÆSŒn‡àÍúþKÈe^m±w ;:hkÙJ)ôqe¶ßÔèQk«3%…¾}×·×Ý?‹¯/¸¾A}į‡:S”=p©3¥í"þÎ/²×ë=й:S†ò~}ø©â£>5e3ÞfŽòë¸V¶Âò¦:ƒÏAçË? ¸ÔK©§âªŽT7–„Èõ×C"{ï—ÄöÑÃ?ë³Ûßg©3¾¸P|åûÓ=òÝ?ë¸Ûúˆc6ˆÞ1 ß™ý;÷í¸^×Î^êŒoü?…v¥Ù‘hn“'M{uNs¡ÎD¶T÷™…Ì÷©_JAÙUy¨4øÂC¥ ^Ž›Êáx •ÄÑK0Ÿ­Ñø¶xû¶_/Ê\¹ºßuÁ5¼ˆbOND£ÉNdc\‹#É]KIÞp#’ݸKÛš"fÓgcê'gÀäB-qAXî¾Ê/ÔßT ·ÐáYç½5GwñlÀÌ•'¤"Ɇº'¤Ö©ð´E¶+ª-“€ûŠJ‚Nàü¸öEàŒœìì°:²ª<îAð³±5ÒÃ*JE«ÒÃzEéa¾ß€ÄRg7=U€ß ÁEJÙ³R9ükžÐÝMn¡ {†XÀž"F0E†ûRpQbÀ£WÎUFZæNõjAm}è–g¤ˆ³EYZ•¿ûGd]&Û¸ës(—0¯ŸÓSñ]~¼mxáàHC§'ˆ³Òƒ!f׺2İ<ž4Ï«£ÄŒRýõÙ"CŒÛ¦ë,þµ2T„¯QlOÕUÃq>¯»·/®´Ž±óê—ìͱ«Øç¥9öŠ,Üú$àéaNß·ô0€s×HSTéaq~vzXœ¯HÛçÑuÇÏÇS_ )b‚‘ï´”Vù!úéaÜÝ8wepð³‚JéJÚéÕwÏ”„€RrÐàUöâw¯FõK‘Î[‚ K^WDĈO©!´C@‡©\ bD¯ÌC&ˆ>¥D!A f™ÙH#þ•uɯF%ˆÕÞ#?w'ˆ}A¾06Å–¶çdèë‚—Ç"¶ïdL5¶.‚[Úä|ÛU‚bëó+¼ß˜.p¾4\žeOCðå7qØÝ&óô´}æ ¯`W!´°4ž»MP¡Û«p€Ò£¾÷&]Ö^¸ÿÜXuÙ½&*­póšÌ9 %ø—¾Âm‚µ•p›øã«ÊDs·Éô{bî6™ÛqA· ÆÓ53w› ’ƒê#Aµ;ø§}°ù§ë½ïâP¿ŸQÃP UŸ¡p$®ÆŠK÷t› åA†ÛßLHw· 꾫’ Ý&(”_‰+ºMPìÀ3bW<ž®ò*Aá6 0Ü&ß¿R¨-¢¡µÞ´ã©šè7zÚå¿•¶€†¢Tí« žÇ€¯1›S.ˆ©O4î<|¥KáwZ ËS¾\º®…@â;eÙ?vß1cµ }rkùg"‹?ß¾ƒ¸—âéêKÅvãGÑ”oá\feiCJb¨^IÜ“ö¬ºJb@ qœR̕ŀ…/%€øGÚV8q6§®¹~nFüRY Ѱ/ߨ›âõ¾u¶›ø¤3^:yYÎvs1Ëž М?{/4¿Ê×Tç .¾Tb·UqI ðh~ï&dŽÒΤåDŠß‡n4¬} PNÄä_„dzøIå$;jMùS2‚x1¯cêª>/æwW‡xåBr€Ô8§ÌÕï êC÷/Gå‡&—®£ðë¢M_0ó’ÅÉg©ºbçÕ¹ÊÏ•í A%€~ûT¾ ƹ·|°%>¹‚Ï…¡~y!U ÝU1¿9bBøˆã)5AóÅ'ÕxM#îmð‹zº.¨ã3fõîGgL±Ó½YµÐç•)Ë8CL)I“ˆ5%æ¥T\NÞ-9ˆ#~E‰Q^ŽÖ§É–îã-qClïDWÁ’C{òjÒñrò@Ã(ú„$Â:ªKéGQ¥”Ç;SúU·q â¶éëDÓ/×èÇØýV~(lJ·_úžöäç#ÈßÈoÇŸÿ í¼Å­ endstream endobj 4 0 obj 15625 endobj 2 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> /s6 6 0 R /s8 8 0 R /s10 10 0 R /s12 12 0 R >> /Shading << /sh5 5 0 R >> /XObject << /x7 7 0 R /x9 9 0 R /x11 11 0 R /x13 13 0 R /x14 14 0 R /x15 15 0 R >> >> endobj 16 0 obj << /Type /Page /Parent 1 0 R /MediaBox [ 0 0 512 132.915924 ] /Contents 3 0 R /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources 2 0 R >> endobj 17 0 obj << /Type /XObject /Length 47 /Filter /FlateDecode /Subtype /Form /BBox [ 41 52.915924 73 85.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 0.19 /ca 0.19 >> >> >> >> stream xœ3P0¢týD…ôb.CS#=KCSK#c#cc…¢T…4.¯Ù˜ endstream endobj 7 0 obj << /Type /XObject /Length 48 /Filter /FlateDecode /Subtype /Form /BBox [ 41 52.915924 73 85.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /XObject << /x18 18 0 R >> >> >> stream xœ+ä2T0B©k g`ab`nj©œË¥Ÿh ^¬ _ah¡à’ÏȽ & endstream endobj 19 0 obj << /Type /Mask /S /Alpha /G 17 0 R >> endobj 6 0 obj << /Type /ExtGState /SMask 19 0 R /ca 1 /CA 1 /AIS false >> endobj 20 0 obj << /Type /XObject /Length 47 /Filter /FlateDecode /Subtype /Form /BBox [ 46 57.915924 68 80.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 0.595 /ca 0.595 >> >> >> >> stream xœ3P0¢týD…ôb.3Ss=KCSK####c…¢T…4.°  endstream endobj 9 0 obj << /Type /XObject /Length 48 /Filter /FlateDecode /Subtype /Form /BBox [ 46 57.915924 68 80.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /XObject << /x21 21 0 R >> >> >> stream xœ+ä2T0B©k g`ab`nj©œË¥Ÿh ^¬ _ad¨à’Ïȼô endstream endobj 22 0 obj << /Type /Mask /S /Alpha /G 20 0 R >> endobj 8 0 obj << /Type /ExtGState /SMask 22 0 R /ca 1 /CA 1 /AIS false >> endobj 23 0 obj << /Type /XObject /Length 47 /Filter /FlateDecode /Subtype /Form /BBox [ 65 31.915924 91 56.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 0.19 /ca 0.19 >> >> >> >> stream xœ3P0¢týD…ôb.3ScC=KCSK##3#S…¢T…4.°JŸ endstream endobj 11 0 obj << /Type /XObject /Length 48 /Filter /FlateDecode /Subtype /Form /BBox [ 65 31.915924 91 56.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /XObject << /x24 24 0 R >> >> >> stream xœ+ä2T0B©k g`ab`nj©œË¥Ÿh ^¬ _ad¢à’ÏȽ # endstream endobj 25 0 obj << /Type /Mask /S /Alpha /G 23 0 R >> endobj 10 0 obj << /Type /ExtGState /SMask 25 0 R /ca 1 /CA 1 /AIS false >> endobj 26 0 obj << /Type /XObject /Length 47 /Filter /FlateDecode /Subtype /Form /BBox [ 69 35.915924 87 52.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 0.595 /ca 0.595 >> >> >> >> stream xœ3P0¢týD…ôb.3KcS=KCSK#C Cs…¢T…4.±© endstream endobj 13 0 obj << /Type /XObject /Length 48 /Filter /FlateDecode /Subtype /Form /BBox [ 69 35.915924 87 52.915924 ] /Group << /Type /Group /S /Transparency /I true /CS /DeviceRGB >> /Resources << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /XObject << /x27 27 0 R >> >> >> stream xœ+ä2T0B©k g`ab`nj©œË¥Ÿh ^¬ _ad®à’ÏȽ & endstream endobj 28 0 obj << /Type /Mask /S /Alpha /G 26 0 R >> endobj 12 0 obj << /Type /ExtGState /SMask 28 0 R /ca 1 /CA 1 /AIS false >> endobj 29 0 obj << /FunctionType 2 /Domain [ 0 1 ] /C0 [ 0.188235 0.443137 0.670588 ] /C1 [ 0.0196078 0.219608 0.396078 ] /N 1 >> endobj 5 0 obj << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [ 121.590797 450.859406 367.959412 24.1366 ] /Domain [ 0 1 ] /Extend [ true true ] /Function 29 0 R >> endobj 30 0 obj << /Length 31 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 19 /Height 18 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream xœc`À1DXyÄØ9Q„™$=’œ5XBæ«.ž?;+Þ„&Äî¾ýûïßïnÏŽ–`‚kœùñï¿¿>ï)Ôƒ©ã=ôùï¿ÿÿ¾]o2ေHä\ýúÿÿÿߟvËB-aTÉ;õý/Pì÷…"I˜qÊé›>ýüÿïß·ÍœP1ž€ §?}ÿóóRªÜOšÑ“.^º¹6^áf6-ó¬Âp5d¯1ròJ ³(8»‘E endstream endobj 31 0 obj 185 endobj 14 0 obj << /Length 32 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 19 /Height 18 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 /SMask 30 0 R >> stream xœÁ ‚@EýÉþÇ/h´h%´›y:øP0ÑPH©Ål¢o°ˆ#há].÷<žeMËŠ¬ó2ì(Ð0ð’/r(Dä!#1ˆÒ¼,$r:hd»<«Î][J€™f„bÙ)ukdì;ºFDV÷çý$£­>ȺWêzLÁ5sdE{éš½Œ6Yr!U!ãYƒ³åˆ)Fœ˜KÞ^Â(§cëÓ$~³‰ù›xi endstream endobj 32 0 obj 180 endobj 33 0 obj << /Length 34 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 19 /Height 33 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 >> stream xœc`ÀјعؘPDXøä´äùX5ñGåÆ˜‰"‰±©æ¬Û³¶ÎN˜nÇü{¯ªµà‡Y¢œwèÓŸ/¶eª°ÁôôÞúùÿßó]xaB¦+^ÿýÿïß÷óù2P—°ZoÿôïÿÿÿžÌ²å˜Æé¾ ,ô÷ÞD)ˆ2nÏ­ŸABÿ~\kÑ€¸ËiÛW°Ðß7+MX!Ž0Ùúÿÿë>°a¬¦‹_þ }?ž&ö›aßÝ_`¡Ÿ—kTÁ†±¨–_þ ù÷û~:X£h̰Ðÿ¿/gjC¬ätÙúá/HèÏó¹æì`!vãI÷~Ýÿl®5ÄJyÇ¿@„æ™AƒQÀmÐãÿÿýy>SŒÌJuW¾ÿû÷ÿ÷ Vhà ù-}øóß¿¯Ç •`À¡™·ãÅŸ¯Ö‹Â⎉߲îàƒGg[Œ8q+dW½|ÍD_qx$"jí"ωãâr¢ì¨©€‰™•…yª»_ endstream endobj 34 0 obj 381 endobj 15 0 obj << /Length 35 0 R /Filter /FlateDecode /Type /XObject /Subtype /Image /Width 19 /Height 33 /ColorSpace /DeviceGray /Interpolate true /BitsPerComponent 8 /SMask 33 0 R >> stream xœ’IRÂ@…¹¤÷á î²bßU¡çt:&LA4&R(Pê "&a¥ÿò«×ÿð^×jÿ®œ( ~F̶q`ž" ¢œè$œ1‚¯@Eê3!uÎÈ«N„ê¦m:W+Ù^$Ý^àšR+ÔáV0Ež›‡ NÏâБèðPéîI˜B-eœ~¼\eÉ +x‰ —þ4]¿fOÃJ¹þôq»{ºíKÞ*ÛwtoºÚ}®ï#›“b ‹³í׿aäpX43¡æŽ–o›lìjô¢0 P;Lž·ïéÄÒ"½iúò¼»“]"á çiš 4Ê-šÜêÇÉ|9‚•'å-¢;Ñä&¾¶x§º1Ó ‡¡'Tþ0*-¯çw­ì©5ÐÞXË’ ³« ʸ.‚¿ÉåïSÂðè~‘ ¦¤Ý> stream xœ]± €0 {Oñ;ÁN2#ÐÀþ„H ¡¯^§¿ßIQsÌèFÁ|R¯¬^ØÌÙÔkÕ±ÂKa‘³hë–>œ mM/Nr+ó·ÎÆ[³¿|ù½O4П@ ö endstream endobj 37 0 obj 101 endobj 36 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> >> endobj 21 0 obj << /Length 39 0 R /Filter /FlateDecode /Type /XObject /Subtype /Form /BBox [ 46 58 68 81 ] /Resources 38 0 R >> stream xœ]» Ã@ C{MÁ hù>’nŒŒ&va¶÷ŒK‚°"yÈŒžsÁôT,—cJÖh†Õ¨‘`A׌ Õ©%££ô™3jûšòf®´Ù‡Ê½~Vxý¿}ÉCnåcØ endstream endobj 39 0 obj 102 endobj 38 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> >> endobj 24 0 obj << /Length 41 0 R /Filter /FlateDecode /Type /XObject /Subtype /Form /BBox [ 65 32 91 57 ] /Resources 40 0 R >> stream xœeŽ;€0 C÷œÂ'é'M{ ŽÀB`î/Q¨òYϲ³“í£b˜õ¤¤\BDŒœD±ÁŒMÂí};+,³“‚à9¶Øãե΋°Ï z z@YœïšØòÃß/¿féÃ"= endstream endobj 41 0 obj 105 endobj 40 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> >> endobj 27 0 obj << /Length 43 0 R /Filter /FlateDecode /Type /XObject /Subtype /Form /BBox [ 69 36 87 53 ] /Resources 42 0 R >> stream xœeŽ1Ã0 w½‚/`­J¶ägô ]šíäÿ@ÒÂ…‡Bq AqÅç¶—{Á²KëÌRáÎvÊ´8Ù˜¡x!’Z:¬ÑÊàôœ~6º:dÌ@¥¹Ï‚ze×/?ÿù7á!79’Î"o endstream endobj 43 0 obj 106 endobj 42 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> >> endobj 1 0 obj << /Type /Pages /Kids [ 16 0 R ] /Count 1 >> endobj 44 0 obj << /Creator (cairo 1.14.0 (http://cairographics.org)) /Producer (cairo 1.14.0 (http://cairographics.org)) >> endobj 45 0 obj << /Type /Catalog /Pages 1 0 R >> endobj xref 0 46 0000000000 65535 f 0000023749 00000 n 0000015741 00000 n 0000000015 00000 n 0000015717 00000 n 0000020004 00000 n 0000017040 00000 n 0000016587 00000 n 0000017951 00000 n 0000017498 00000 n 0000018861 00000 n 0000018407 00000 n 0000019774 00000 n 0000019320 00000 n 0000020650 00000 n 0000021702 00000 n 0000015996 00000 n 0000016218 00000 n 0000022307 00000 n 0000016980 00000 n 0000017127 00000 n 0000022665 00000 n 0000017891 00000 n 0000018038 00000 n 0000023024 00000 n 0000018801 00000 n 0000018949 00000 n 0000023386 00000 n 0000019714 00000 n 0000019862 00000 n 0000020228 00000 n 0000020627 00000 n 0000021061 00000 n 0000021084 00000 n 0000021679 00000 n 0000022284 00000 n 0000022592 00000 n 0000022569 00000 n 0000022951 00000 n 0000022928 00000 n 0000023313 00000 n 0000023290 00000 n 0000023676 00000 n 0000023653 00000 n 0000023815 00000 n 0000023943 00000 n trailer << /Size 46 /Root 45 0 R /Info 44 0 R >> startxref 23996 %%EOF ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1561470461.0 photutils-1.3.0/docs/_static/photutils_banner.svg0000644000214200020070000012633000000000000020662 0ustar00lbradley photutils logoimage/svg+xmlphotutils logoLarry Bradley ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1561470461.0 photutils-1.3.0/docs/_static/photutils_banner_original.svg0000644000214200020070000003743600000000000022556 0ustar00lbradley photutils logoimage/svg+xmlphotutils logoLarry Bradleyphot utils An Astropy Package for Photometry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1418856802.0 photutils-1.3.0/docs/_static/photutils_logo-32x32.png0000644000214200020070000000407100000000000021116 0ustar00lbradley‰PNG  IHDR szzôtEXtSoftwareAdobe ImageReadyqÉe<ÛIDATxÚ¤W PT×þÎaaewwQpäµ ò¨ƒ©±¶¤˜„Ñ’¦Ð´3u™Nf´“Nb§í$ÓÎ$mÆ´i’JÓv:MšJ§6ŽŽŒB4JRÌ¡€°°,( ¸¼–ǽýï¹û¸ ¤ÑÊðï½÷<þÇ÷?Îùîá/jçadXeY¢/²¬ýØç.ÿÖ~'<ØÝ ¹¿¶Œ„|‹Hyn„ìTÂ7f§G=}ürþÊëö{VÀøÀ3ÕÄô9­PU ¤úŽÀ{-®%EÜw­@ìÞcVâð±)Ó2/ë‚[Ž€qU$Ü“1~q!ù3•¡7=w‘-ZþaÿKx\ù³Õ`ìÏŒ± EW#ób¿®)|)_ªÀ‡j¾­Í£ ý·nã‹:'º¤5ª]ŒÑ¿úï@$M 3\_ø¨ãsˆÿÚs'Iãj¿ÙlÙ¸…FnGIÎ&¸§<ÂúÓM­(ÍMÅ¥ëÝ„J7Nϧ.±^Ò¸Kâ†]s¼&Э$<ᡫ–ÌHð̘DkÊ^”oJÄW‹2‰¶Âvc,wÃ-¬o•Ö‘±\$Õ<™ƒib†A6ÑûIúÈ_щûr„;*PôTÀœx[PaÆ©K6Ü_Ž÷.·ãüG]ðÎ/ÂûïsÂ=ÍR’r¨ðC%›Ã£ú.äêF`“â•ÉDe[ßBsKI¼PYzID¯BL…‘ÉŠ*¦ÕQÈIKÂ÷_>%"Ü,ßF ëƒ ëÐ(Yˆ¯o­õïUè±ðœðZ‘Ç‘6…¾ÅeÝÓôSÉ•/öÒ†K#8KÂŒ‰oó:ª=ÑF8zz) "ÑD‚…Ÿ¡‰xM”ò~ôIä¡4/ §ó .Î'Óeò˜üây‘ãøŠÜ¯2k—“Ð&¯W… û‹eªë¸,üü¢ñCŒ7ap`œË `âs°ìÞ [·¹›7 ‘¾ÕìàÊö2%bRõ²‰qþ4çY¸‰Jùcšç" ÄýÄ©™ãDýºì<øMl~ð ª"ÚĸÉ'lbzŽa7”ªÊ7ðf)"có¾r€T:`Ä,òùë P:qí<9˜ÏœeHKŽÃØ„'˜ãDÍ3±ˆ¶Áz6 ׈ŸÈ ØÍ{ ´ö AŽÆë7 ÷ŒÂ£ï¬@€´Q­'_;,%8\± ±ñkEdsŸ?ÅÌŽ‡×Œ¢}ËúÝT:ŽÚbð§Å¼à>¢òâl‚?׸o,ˆO«~Ußêƒúð0¤lHÂdæ.d“BèiÞŒq)¶=\%2ÂÂÆU8y!Ó(ìGçgO–cÂãEn˜ «q§²‡q+÷Yà [ ÓX¦<3H4EÃÀfã(}Ìñ&ìKÆÞÊJeY°÷@%*¶¬FjRÅD„ˆ •T^FÊýI…ª’>”aá“@¬¹m*ó§½¢@‘û ÎÜ‹›î™€%Þ ƒcSxut¾Ýùs¼ÓÛ‚¶†³xƒ—*gŒH·`T3(‡ £IN…¡µO~}‡@Ì(uSj‹R¢Ñ§ lƒ2øZt^¸ ƒäAO˜ÚyeÇÉð2 ÐD\ÔíG.ìõ×…Œ²W/ň½yF¸ÃĽjuUôûе>Vèjø,HŒÊN4uñªŸ‚i#¨Õî ñ£ßMÚ TàWçÚìð÷Ð7î±÷~ÚÀù²<çè· CgãJjîY´a‡ÔMÏV‘S³ó¡u€öÄ›V•¨–î3׺`I0Á⺎÷ÙV­±õE5C ‹2¶c± {>Á¹+:Œ¹0?ø8jbû±è F?Ñ—¥6¼çÁw¤FŸ1*RÅÙ)ÈN]ׄP\Ë›è¸8Žia‹z|ùý§ÁÉ .±áTd ÓE$;G§ñë_€³êqŠæ8Ì„¯‚‰î% X¿q†h=Î^ÑÁ5> ãŒm}#4v&¨¨)1á‹‘º[õÏÚÅ-ë{¸Mƒ&íA²o¦çbËp¬²Ÿ:FÅx 1û´D ¶ëqV—+Î\À£ÑhjëÇYž-ÎŒ­„Ò8ôTõbñÄÂ%ʘºi>èÝÚ÷„ÂG2 «öe=ÈÛq®uÞÄöoGù}iÂÂSt!Ógw7œÞxjð¯Çü‡ ÎW$~OªUCs0`†Uz\ëº%”8ï™ÃÕÿ â/lÄßš:‘ì;Tü¨OÏRV¹(ãï†eâF$Õ ¼}´N{g핃 u|IJ6·t¢¢h³PBñeEqºˆæê©éÊžIÆÒ”¤®zŠ68þòLÍÒ˜.x;eµ´ºŒTÞ¨ p!z;ªœÿÂäh":œ³¨¿rCX#Mcß| ÞŠÚ­Zå³N^v|€ŠÀ#«yíç6&yGÞ¶Òã 2ùo´»fš… *,Šp…é9}~èÝ+4%Bñ~¼ç­ÃÏßqgd­}ǪÞZeëŠMäÏh>¤ŽHiÍ触ûäS wÝ’&zQZ±•„.·^ZÒÊÇ;ßønÝ=7§¤ˆrG¤žgërÈV+mW=Ñß;^«©¿›f÷Ž›SŠ“Pb9öÖÙÿßÿ¿ æ(HU¿Ž’ŠIEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638932391.0 photutils-1.3.0/docs/_static/photutils_logo.svg0000644000214200020070000003625200000000000020360 0ustar00lbradley photutils logoimage/svg+xmlphotutils logoLarry Bradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/aperture.rst0000644000214200020070000007450300000000000015520 0ustar00lbradleyAperture Photometry (`photutils.aperture`) ========================================== Introduction ------------ In Photutils, the :func:`~photutils.aperture.aperture_photometry` function is the main tool to perform aperture photometry on an astronomical image for a given set of apertures. Photutils provides several apertures defined in pixel or sky coordinates. The aperture classes that are defined in pixel coordinates are: * `~photutils.aperture.CircularAperture` * `~photutils.aperture.CircularAnnulus` * `~photutils.aperture.EllipticalAperture` * `~photutils.aperture.EllipticalAnnulus` * `~photutils.aperture.RectangularAperture` * `~photutils.aperture.RectangularAnnulus` Each of these classes has a corresponding variant defined in sky coordinates: * `~photutils.aperture.SkyCircularAperture` * `~photutils.aperture.SkyCircularAnnulus` * `~photutils.aperture.SkyEllipticalAperture` * `~photutils.aperture.SkyEllipticalAnnulus` * `~photutils.aperture.SkyRectangularAperture` * `~photutils.aperture.SkyRectangularAnnulus` To perform aperture photometry with sky-based apertures, one will need to specify a WCS transformation. Users can also create their own custom apertures (see :ref:`custom-apertures`). .. _creating-aperture-objects: Creating Aperture Objects ------------------------- The first step in performing aperture photometry is to create an aperture object. An aperture object is defined by a position (or a list of positions) and parameters that define its size and possibly, orientation (e.g., an elliptical aperture). We start with an example of creating a circular aperture in pixel coordinates using the :class:`~photutils.aperture.CircularAperture` class:: >>> from photutils.aperture import CircularAperture >>> positions = [(30., 30.), (40., 40.)] >>> aperture = CircularAperture(positions, r=3.) The positions should be either a single tuple of ``(x, y)``, a list of ``(x, y)`` tuples, or an array with shape ``Nx2``, where ``N`` is the number of positions. The above example defines two circular apertures located at pixel coordinates ``(30, 30)`` and ``(40, 40)`` with a radius of 3 pixels. Creating an aperture object in sky coordinates is similar. One first uses the :class:`~astropy.coordinates.SkyCoord` class to define sky coordinates and then the :class:`~photutils.aperture.SkyCircularAperture` class to define the aperture object:: >>> from astropy import units as u >>> from astropy.coordinates import SkyCoord >>> from photutils.aperture import SkyCircularAperture >>> positions = SkyCoord(l=[1.2, 2.3] * u.deg, b=[0.1, 0.2] * u.deg, ... frame='galactic') >>> aperture = SkyCircularAperture(positions, r=4. * u.arcsec) .. note:: Sky apertures are not defined completely in sky coordinates. They simply use sky coordinates to define the central position, and the remaining parameters are converted to pixels using the pixel scale of the image at the central position. Projection distortions are not taken into account. If the apertures were defined completely in sky coordinates, their shapes would not be preserved when converting to pixel coordinates. Converting Between Pixel and Sky Apertures ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The pixel apertures can be converted to sky apertures, and vice versa. To accomplish this, use the :meth:`~photutils.aperture.PixelAperture.to_sky` method for pixel apertures, e.g.,: .. doctest-skip:: >>> aperture = CircularAperture((10, 20), r=4.) >>> sky_aperture = aperture.to_sky(wcs) and the :meth:`~photutils.aperture.SkyAperture.to_pixel` method for sky apertures, e.g.,: .. doctest-skip:: >>> position = SkyCoord(1.2, 0.1, unit='deg', frame='icrs') >>> aperture = SkyCircularAperture(position, r=4. * u.arcsec) >>> pix_aperture = aperture.to_pixel(wcs) Performing Aperture Photometry ------------------------------ After the aperture object is created, we can then perform the photometry using the :func:`~photutils.aperture.aperture_photometry` function. We start by defining the aperture (at two positions) as described above:: >>> positions = [(30., 30.), (40., 40.)] >>> aperture = CircularAperture(positions, r=3.) We then call the :func:`~photutils.aperture.aperture_photometry` function with the data and the apertures:: >>> import numpy as np >>> from photutils.aperture import aperture_photometry >>> data = np.ones((100, 100)) >>> phot_table = aperture_photometry(data, aperture) >>> phot_table['aperture_sum'].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum pix pix --- ------- ------- ------------ 1 30.0 30.0 28.274334 2 40.0 40.0 28.274334 This function returns the results of the photometry in an Astropy `~astropy.table.QTable`. In this example, the table has four columns, named ``'id'``, ``'xcenter'``, ``'ycenter'``, and ``'aperture_sum'``. Since all the data values are 1.0, the aperture sums are equal to the area of a circle with a radius of 3:: >>> print(np.pi * 3. ** 2) # doctest: +FLOAT_CMP 28.2743338823 Aperture and Pixel Overlap -------------------------- The overlap of the aperture with the data pixels can be handled in different ways. For the default method (``method='exact'``), the exact intersection of the aperture with each pixel is calculated. The other options, ``'center'`` and ``'subpixel'``, are faster, but with the expense of less precision. For ``'center'``, a pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. For ``'subpixel'``, pixels are divided into a number of subpixels, which are in or out of the aperture based on their centers. For this method, the number of subpixels needs to be set with the ``subpixels`` keyword. This example uses the ``'subpixel'`` method where pixels are resampled by a factor of 5 (``subpixels=5``) in each dimension:: >>> phot_table = aperture_photometry(data, aperture, method='subpixel', ... subpixels=5) >>> print(phot_table) # doctest: +SKIP id xcenter ycenter aperture_sum pix pix --- ------- ------- ------------ 1 30.0 30.0 27.96 2 40.0 40.0 27.96 Note that the results differ from the true value of 28.274333 (see above). For the ``'subpixel'`` method, the default value is ``subpixels=5``, meaning that each pixel is equally divided into 25 smaller pixels (this is the method and subsampling factor used in `SourceExtractor `_). The precision can be increased by increasing ``subpixels``, but note that computation time will be increased. Multiple Apertures at Each Position ----------------------------------- While the `~photutils.aperture.Aperture` objects support multiple positions, they must have a fixed size and shape (e.g., radius and orientation). To perform photometry in multiple apertures at each position, one may input a list of aperture objects to the :func:`~photutils.aperture.aperture_photometry` function. In this case, the apertures must all have identical position(s). Suppose that we wish to use three circular apertures, with radii of 3, 4, and 5 pixels, on each source:: >>> radii = [3., 4., 5.] >>> apertures = [CircularAperture(positions, r=r) for r in radii] >>> phot_table = aperture_photometry(data, apertures) >>> for col in phot_table.colnames: ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum_0 aperture_sum_1 aperture_sum_2 pix pix --- ------- ------- -------------- -------------- -------------- 1 30 30 28.274334 50.265482 78.539816 2 40 40 28.274334 50.265482 78.539816 For multiple apertures, the output table column names are appended with the ``positions`` index. Other apertures have multiple parameters specifying the aperture size and orientation. For example, for elliptical apertures, one must specify ``a``, ``b``, and ``theta``:: >>> from photutils.aperture import EllipticalAperture >>> a = 5. >>> b = 3. >>> theta = np.pi / 4. >>> apertures = EllipticalAperture(positions, a, b, theta) >>> phot_table = aperture_photometry(data, apertures) >>> for col in phot_table.colnames: ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum pix pix --- ------- ------- ------------ 1 30 30 47.12389 2 40 40 47.12389 Again, for multiple apertures one should input a list of aperture objects, each with identical positions:: >>> a = [5., 6., 7.] >>> b = [3., 4., 5.] >>> theta = np.pi / 4. >>> apertures = [EllipticalAperture(positions, a=ai, b=bi, theta=theta) ... for (ai, bi) in zip(a, b)] >>> phot_table = aperture_photometry(data, apertures) >>> for col in phot_table.colnames: ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum_0 aperture_sum_1 aperture_sum_2 pix pix --- ------- ------- -------------- -------------- -------------- 1 30 30 47.12389 75.398224 109.95574 2 40 40 47.12389 75.398224 109.95574 Background Subtraction ---------------------- Global Background Subtraction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :func:`~photutils.aperture.aperture_photometry` assumes that the data have been background-subtracted. If ``bkg`` is a float value or an array representing the background of the data (determined by `~photutils.background.Background2D` or an external function), simply subtract the background:: >>> phot_table = aperture_photometry(data - bkg, aperture) # doctest: +SKIP Local Background Subtraction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ One often wants to estimate the local background around each source using a nearby aperture or annulus aperture surrounding each source. The simplest method for doing so would be to perform photometry in an annulus aperture to define the mean background level. Alternatively, one can use aperture masks to directly access the pixel values in an aperture (e.g., an annulus), and thus apply more advanced statistics (e.g., a sigma-clipped median within the annulus). We show examples of both below. Simple mean within a circular annulus """"""""""""""""""""""""""""""""""""" For this example we perform the photometry in a circular aperture with a radius of 3 pixels. The local background level around each source is estimated as the mean value within a circular annulus of inner radius 6 pixels and outer radius 8 pixels. We start by defining the apertures:: >>> from photutils.aperture import CircularAnnulus >>> aperture = CircularAperture(positions, r=3) >>> annulus_aperture = CircularAnnulus(positions, r_in=6., r_out=8.) We then perform the photometry in both apertures:: >>> apers = [aperture, annulus_aperture] >>> phot_table = aperture_photometry(data, apers) >>> for col in phot_table.colnames: ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum_0 aperture_sum_1 pix pix --- ------- ------- -------------- -------------- 1 30 30 28.274334 87.964594 2 40 40 28.274334 87.964594 The ``aperture_sum_0`` column refers to the first aperture in the list of input apertures (i.e., the circular aperture) and the ``aperture_sum_1`` column refers to the second aperture (i.e., the circular annulus). Note that we cannot simply subtract the aperture sums because the apertures have different areas. To calculate the mean local background within the circular annulus aperture, we need to divide its sum by its area. The mean value can be calculated by using the :meth:`~photutils.aperture.CircularAnnulus.area` attribute:: >>> bkg_mean = phot_table['aperture_sum_1'] / annulus_aperture.area The total background within the circular aperture is then the mean local background times the circular aperture area:: >>> bkg_sum = bkg_mean * aperture.area >>> final_sum = phot_table['aperture_sum_0'] - bkg_sum >>> phot_table['residual_aperture_sum'] = final_sum >>> phot_table['residual_aperture_sum'].info.format = '%.8g' # for consistent table output >>> print(phot_table['residual_aperture_sum']) # doctest: +SKIP residual_aperture_sum --------------------- -7.1054274e-15 -7.1054274e-15 The result here should be zero because all the data values are 1.0 (the tiny difference from 0.0 is due to numerical precision). Sigma-clipped median within a circular annulus """""""""""""""""""""""""""""""""""""""""""""" For this example we perform the photometry in a circular aperture with a radius of 5 pixels. The local background level around each source is estimated as the sigma-clipped median value within a circular annulus of inner radius 10 pixels and outer radius 15 pixels. We start by defining an example image and an aperture for three sources:: >>> from photutils.datasets import make_100gaussians_image >>> from photutils.aperture import CircularAperture, CircularAnnulus >>> data = make_100gaussians_image() >>> positions = [(145.1, 168.3), (84.5, 224.1), (48.3, 200.3)] >>> aperture = CircularAperture(positions, r=5) >>> annulus_aperture = CircularAnnulus(positions, r_in=10, r_out=15) Let's plot the circular apertures (white) and circular annulus apertures (red) on the image: .. plot:: from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.aperture import CircularAperture, CircularAnnulus from photutils.datasets import make_100gaussians_image data = make_100gaussians_image() positions = [(145.1, 168.3), (84.5, 224.1), (48.3, 200.3)] aperture = CircularAperture(positions, r=5) annulus_aperture = CircularAnnulus(positions, r_in=10, r_out=15) norm = simple_norm(data, 'sqrt', percent=99) plt.imshow(data, norm=norm, interpolation='nearest') plt.xlim(0, 170) plt.ylim(130, 250) ap_patches = aperture.plot(color='white', lw=2, label='Photometry aperture') ann_patches = annulus_aperture.plot(color='red', lw=2, label='Background annulus') handles = (ap_patches[0], ann_patches[0]) plt.legend(loc=(0.17, 0.05), facecolor='#458989', labelcolor='white', handles=handles, prop={'weight': 'bold', 'size': 11}) We can use aperture masks to directly access the pixel values in any aperture. Let's do that for the annulus aperture:: >>> annulus_masks = annulus_aperture.to_mask(method='center') The result is a list of `~photutils.aperture.ApertureMask` objects, one for each aperture position. The values in these aperture masks are either 0 or 1 because we specified ``method='center'``. Alternatively, one could use the "exact" (``method='exact'``) mask, but it produces partial-pixel masks (i.e., values between 0 and 1) and thus one would need to use statistical functions that can handle partial-pixel weights. That introduces unnecessary complexity when the aperture is simply being used to estimate the local background. Whole pixels are fine, assuming you have a sufficient number of them on which to apply your statistical estimator. Let's focus on just the first annulus. Let's plot its aperture mask: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> plt.imshow(annulus_masks[0], interpolation='nearest') >>> plt.colorbar() .. plot:: import matplotlib.pyplot as plt from photutils.aperture import CircularAperture, CircularAnnulus positions = [(145.1, 168.3), (84.5, 224.1), (48.3, 200.3)] aperture = CircularAperture(positions, r=5) annulus_aperture = CircularAnnulus(positions, r_in=10, r_out=15) annulus_masks = annulus_aperture.to_mask(method='center') plt.imshow(annulus_masks[0], interpolation='nearest') plt.colorbar() We can now use the :meth:`photutils.aperture.ApertureMask.multiply` method to get the values of the aperture mask multiplied to the data. Since the mask values are 0 or 1, the result is simply the data values within the annulus aperture:: >>> annulus_data = annulus_masks[0].multiply(data) Let's plot the annulus data: .. plot:: import matplotlib.pyplot as plt from photutils.aperture import CircularAperture, CircularAnnulus from photutils.datasets import make_100gaussians_image positions = [(145.1, 168.3), (84.5, 224.1), (48.3, 200.3)] aperture = CircularAperture(positions, r=5) annulus_aperture = CircularAnnulus(positions, r_in=10, r_out=15) annulus_masks = annulus_aperture.to_mask(method='center') data = make_100gaussians_image() annulus_data = annulus_masks[0].multiply(data) plt.imshow(annulus_data, interpolation='nearest') plt.colorbar() From this 2D array, you can extract a 1D array of data values (e.g., if you don't care about their spatial positions, which is probably the most common case):: >>> mask = annulus_masks[0].data >>> annulus_data_1d = annulus_data[mask > 0] >>> annulus_data_1d.shape (394,) You can then use your favorite statistical estimator on this 1D array to estimate the background level. Let's calculate the sigma-clipped median:: >>> from astropy.stats import sigma_clipped_stats >>> _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d) >>> print(median_sigclip) # doctest: +FLOAT_CMP 4.848212997882959 The total background within the circular aperture is then the local background level times the circular aperture area:: >>> background = median_sigclip * aperture.area >>> print(background) # doctest: +FLOAT_CMP 380.7777584296913 Above was a very pedagogical description of the underlying methods for local background subtraction for a single source. However, it's quite straightforward to do this for all the sources in just a few lines of code. For this example, we'll again use the sigma-clipped median of the pixels in the background annuli for the background estimates of each source:: >>> from astropy.stats import sigma_clipped_stats >>> from photutils.aperture import aperture_photometry >>> from photutils.aperture import CircularAperture, CircularAnnulus >>> from photutils.datasets import make_100gaussians_image >>> >>> data = make_100gaussians_image() >>> positions = [(145.1, 168.3), (84.5, 224.1), (48.3, 200.3)] >>> aperture = CircularAperture(positions, r=5) >>> annulus_aperture = CircularAnnulus(positions, r_in=10, r_out=15) >>> annulus_masks = annulus_aperture.to_mask(method='center') >>> >>> bkg_median = [] >>> for mask in annulus_masks: ... annulus_data = mask.multiply(data) ... annulus_data_1d = annulus_data[mask.data > 0] ... _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d) ... bkg_median.append(median_sigclip) >>> bkg_median = np.array(bkg_median) >>> phot = aperture_photometry(data, aperture) >>> phot['annulus_median'] = bkg_median >>> phot['aper_bkg'] = bkg_median * aperture.area >>> phot['aper_sum_bkgsub'] = phot['aperture_sum'] - phot['aper_bkg'] >>> for col in phot.colnames: ... phot[col].info.format = '%.8g' # for consistent table output >>> print(phot) id xcenter ycenter aperture_sum annulus_median aper_bkg aper_sum_bkgsub pix pix --- ------- ------- ------------ -------------- --------- --------------- 1 145.1 168.3 1131.5794 4.848213 380.77776 750.80166 2 84.5 224.1 746.16064 5.0884354 399.64478 346.51586 3 48.3 200.3 1250.2186 4.8060599 377.46706 872.7515 .. _error_estimation: Error Estimation ---------------- If and only if the ``error`` keyword is input to :func:`~photutils.aperture.aperture_photometry`, the returned table will include a ``'aperture_sum_err'`` column in addition to ``'aperture_sum'``. ``'aperture_sum_err'`` provides the propagated uncertainty associated with ``'aperture_sum'``. For example, suppose we have previously calculated the error on each pixel's value and saved it in the array ``error``:: >>> positions = [(30., 30.), (40., 40.)] >>> aperture = CircularAperture(positions, r=3.) >>> data = np.ones((100, 100)) >>> error = 0.1 * data >>> phot_table = aperture_photometry(data, aperture, error=error) >>> for col in phot_table.colnames: ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) id xcenter ycenter aperture_sum aperture_sum_err pix pix --- ------- ------- ------------ ---------------- 1 30 30 28.274334 0.53173616 2 40 40 28.274334 0.53173616 ``'aperture_sum_err'`` values are given by: .. math:: \Delta F = \sqrt{\sum_{i \in A} \sigma_{\mathrm{tot}, i}^2} where :math:`A` are the non-masked pixels in the aperture, and :math:`\sigma_{\mathrm{tot}, i}` is the input ``error`` array. In the example above, it is assumed that the ``error`` keyword specifies the *total* error -- either it includes Poisson noise due to individual sources or such noise is irrelevant. However, it is often the case that one has calculated a smooth "background-only error" array, which by design doesn't include increased noise on bright pixels. To include Poisson noise from the sources, we can use the :func:`~photutils.utils.calc_total_error` function. Let's assume we have a background-only image called ``bkg_error``. If our data are in units of electrons/s, we would use the exposure time as the effective gain:: >>> from photutils.utils import calc_total_error >>> effective_gain = 500 # seconds >>> error = calc_total_error(data, bkg_error, effective_gain) # doctest: +SKIP >>> phot_table = aperture_photometry(data - bkg, aperture, error=error) # doctest: +SKIP Pixel Masking ------------- Pixels can be ignored/excluded (e.g., bad pixels) from the aperture photometry by providing an image mask via the ``mask`` keyword:: >>> data = np.ones((5, 5)) >>> aperture = CircularAperture((2, 2), 2.) >>> mask = np.zeros(data.shape, dtype=bool) >>> data[2, 2] = 100. # bad pixel >>> mask[2, 2] = True >>> t1 = aperture_photometry(data, aperture, mask=mask) >>> t1['aperture_sum'].info.format = '%.8g' # for consistent table output >>> print(t1['aperture_sum']) aperture_sum ------------ 11.566371 The result is very different if a ``mask`` image is not provided:: >>> t2 = aperture_photometry(data, aperture) >>> t2['aperture_sum'].info.format = '%.8g' # for consistent table output >>> print(t2['aperture_sum']) aperture_sum ------------ 111.56637 Aperture Photometry Using Sky Coordinates ----------------------------------------- As mentioned in :ref:`creating-aperture-objects`, performing photometry using apertures defined in sky coordinates simply requires defining a "sky" aperture at positions defined by a :class:`~astropy.coordinates.SkyCoord` object. Here we show an example of photometry on real data using a `~photutils.aperture.SkyCircularAperture`. We start by loading a Spitzer 4.5 micron image of a region of the Galactic plane:: >>> import astropy.units as u >>> from astropy.wcs import WCS >>> from photutils.datasets import load_spitzer_image, load_spitzer_catalog >>> hdu = load_spitzer_image() # doctest: +REMOTE_DATA >>> data = u.Quantity(hdu.data, unit=hdu.header['BUNIT']) # doctest: +REMOTE_DATA >>> wcs = WCS(hdu.header) # doctest: +REMOTE_DATA >>> catalog = load_spitzer_catalog() # doctest: +REMOTE_DATA The catalog contains (among other things) the Galactic coordinates of the sources in the image as well as the PSF-fitted fluxes from the official Spitzer data reduction. We define the apertures positions based on the existing catalog positions:: >>> positions = SkyCoord(catalog['l'], catalog['b'], frame='galactic') # doctest: +REMOTE_DATA >>> aperture = SkyCircularAperture(positions, r=4.8 * u.arcsec) # doctest: +REMOTE_DATA Now perform the photometry in these apertures on the ``data``. The ``wcs`` object contains the WCS transformation of the image obtained from the FITS header. It includes the coordinate frame of the image and the projection from sky to pixel coordinates. The `~photutils.aperture.aperture_photometry` function uses the WCS information to automatically convert the apertures defined in sky coordinates into pixel coordinates:: >>> phot_table = aperture_photometry(data, aperture, wcs=wcs) # doctest: +REMOTE_DATA The Spitzer catalog also contains the official fluxes for the sources, so we can compare to our fluxes. Because the Spitzer catalog fluxes are in units of mJy and the data are in units of MJy/sr, we need to convert units before comparing the results. The image data have a pixel scale of 1.2 arcsec/pixel. >>> import astropy.units as u >>> factor = (1.2 * u.arcsec) ** 2 / u.pixel >>> fluxes_catalog = catalog['f4_5'] # doctest: +REMOTE_DATA >>> converted_aperture_sum = (phot_table['aperture_sum'] * ... factor).to(u.mJy / u.pixel) # doctest: +REMOTE_DATA Finally, we can plot the comparison of the photometry: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> plt.scatter(fluxes_catalog, converted_aperture_sum.value) >>> plt.xlabel('Spitzer catalog PSF-fit fluxes ') >>> plt.ylabel('Aperture photometry fluxes') .. plot:: from astropy import units as u from astropy.coordinates import SkyCoord from astropy.wcs import WCS import matplotlib.pyplot as plt from photutils.aperture import aperture_photometry, SkyCircularAperture from photutils.datasets import load_spitzer_image, load_spitzer_catalog # Load dataset hdu = load_spitzer_image() data = u.Quantity(hdu.data, unit=hdu.header['BUNIT']) wcs = WCS(hdu.header) catalog = load_spitzer_catalog() # Set up apertures positions = SkyCoord(catalog['l'], catalog['b'], frame='galactic') aperture = SkyCircularAperture(positions, r=4.8 * u.arcsec) phot_table = aperture_photometry(data, aperture, wcs=wcs) # Convert to correct units factor = (1.2 * u.arcsec) ** 2 / u.pixel fluxes_catalog = catalog['f4_5'] converted_aperture_sum = (phot_table['aperture_sum'] * factor).to(u.mJy / u.pixel) # Plot plt.scatter(fluxes_catalog, converted_aperture_sum.value) plt.xlabel('Spitzer catalog PSF-fit fluxes ') plt.ylabel('Aperture photometry fluxes') plt.plot([40, 100, 450], [40, 100, 450], color='black', lw=2) Despite using different methods, the two catalogs are in good agreement. The aperture photometry fluxes are based on a circular aperture with a radius of 4.8 arcsec. The Spitzer catalog fluxes were computed using PSF photometry. Therefore, differences are expected between the two measurements. Aperture Masks -------------- All `~photutils.aperture.PixelAperture` objects have a :meth:`~photutils.aperture.PixelAperture.to_mask` method that returns a list of `~photutils.aperture.ApertureMask` objects, one for each aperture position. The `~photutils.aperture.ApertureMask` object contains a cutout of the aperture mask and a `~photutils.aperture.BoundingBox` object that provides the bounding box where the mask is to be applied. It also provides a :meth:`~photutils.aperture.ApertureMask.to_image` method to obtain an image of the mask in a 2D array of the given shape, a :meth:`~photutils.aperture.ApertureMask.cutout` method to create a cutout from the input data over the mask bounding box, and an :meth:`~photutils.aperture.ApertureMask.multiply` method to multiply the aperture mask with the input data to create a mask-weighted data cutout. All of these methods properly handle the cases of partial or no overlap of the aperture mask with the data. Let's start by creating an aperture object:: >>> from photutils.aperture import CircularAperture >>> positions = [(30., 30.), (40., 40.)] >>> aperture = CircularAperture(positions, r=3.) Now let's create a list of `~photutils.aperture.ApertureMask` objects using the :meth:`~photutils.aperture.PixelAperture.to_mask` method:: >>> masks = aperture.to_mask(method='center') We can now create an image with of the first aperture mask at its position:: >>> mask = masks[0] >>> image = mask.to_image(shape=((200, 200))) We can also create a cutout from a data image over the mask domain:: >>> data_cutout = mask.cutout(data) We can also create a mask-weighted cutout from the data. Here the circular aperture mask has been multiplied with the data:: >>> data_cutout_aper = mask.multiply(data) .. _custom-apertures: Defining Your Own Custom Apertures ---------------------------------- The :func:`~photutils.aperture.aperture_photometry` function can perform aperture photometry in arbitrary apertures. This function accepts any `~photutils.aperture.Aperture`-derived objects, such as `~photutils.aperture.CircularAperture`. This makes it simple to extend functionality: a new type of aperture photometry simply requires the definition of a new `~photutils.aperture.Aperture` subclass. All `~photutils.aperture.PixelAperture` subclasses must define a ``bounding_boxes`` property and ``to_mask()`` and ``plot()`` methods. They may also optionally define an ``area`` property. All `~photutils.aperture.SkyAperture` subclasses must only implement a ``to_pixel()`` method. * ``bounding_boxes``: The minimal bounding box for the aperture. If the aperture is scalar, then a single `~photutils.aperture.BoundingBox` is returned. Otherwise, a list of `~photutils.aperture.BoundingBox` is returned. * ``area``: An optional property defining the exact analytical area (in pixels**2) of the aperture. * ``to_mask()``: Return a mask for the aperture. If the aperture is scalar, then a single `~photutils.aperture.ApertureMask` is returned. Otherwise, a list of `~photutils.aperture.ApertureMask` is returned. * ``plot()``: A method to plot the aperture on a `matplotlib.axes.Axes` instance. Reference/API ------------- .. automodapi:: photutils.aperture :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/background.rst0000644000214200020070000004755400000000000016016 0ustar00lbradleyBackground Estimation (`photutils.background`) ============================================== Introduction ------------ To accurately measure the photometry and morphological properties of astronomical sources, one requires an accurate estimate of the background, which can be from both the sky and the detector. Similarly, having an accurate estimate of the background noise is important for determining the significance of source detections and for estimating photometric errors. Unfortunately, accurate background and background noise estimation is a difficult task. Further, because astronomical images can cover a wide variety of scenes, there is not a single background estimation method that will always be applicable. Photutils provides tools for estimating the background and background noise in your data, but they will likely require some tweaking to optimize the background estimate for your data. Scalar Background and Noise Estimation -------------------------------------- Simple Statistics ^^^^^^^^^^^^^^^^^ If the background level and noise are relatively constant across an image, the simplest way to estimate these values is to derive scalar quantities using simple approximations. Of course, when computing the image statistics one must take into account the astronomical sources present in the images, which add a positive tail to the distribution of pixel intensities. For example, one may consider using the image median as the background level and the image standard deviation as the 1-sigma background noise, but the resulting values are obviously biased by the presence of real sources. A slightly better method involves using statistics that are robust against the presence of outliers, such as the biweight location for the background level and biweight scale or `median absolute deviation (MAD) `__ for the background noise estimation. However, for most astronomical scenes these methods will also be biased by the presence of astronomical sources in the image. As an example, we load a synthetic image comprised of 100 sources with a Gaussian-distributed background whose mean is 5 and standard deviation is 2:: >>> from photutils.datasets import make_100gaussians_image >>> data = make_100gaussians_image() Let's plot the image: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> from astropy.visualization import SqrtStretch >>> from astropy.visualization.mpl_normalize import ImageNormalize >>> norm = ImageNormalize(stretch=SqrtStretch()) >>> plt.imshow(data, norm=norm, origin='lower', cmap='Greys_r', ... interpolation='nearest') .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image data = make_100gaussians_image() norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data, norm=norm, origin='lower', cmap='Greys_r', interpolation='nearest') The image median and biweight location are both larger than the true background level of 5:: >>> import numpy as np >>> from astropy.stats import biweight_location >>> print(np.median(data)) # doctest: +FLOAT_CMP 5.225529518399048 >>> print(biweight_location(data)) # doctest: +FLOAT_CMP 5.186759755495727 Similarly, using the median absolute deviation to estimate the background noise level gives a value that is larger than the true value of 2:: >>> from astropy.stats import mad_std >>> print(mad_std(data)) # doctest: +FLOAT_CMP 2.1443760096598914 Sigma Clipping Sources ^^^^^^^^^^^^^^^^^^^^^^ The most widely used technique to remove the sources from the image statistics is called sigma clipping. Briefly, pixels that are above or below a specified sigma level from the median are discarded and the statistics are recalculated. The procedure is typically repeated over a number of iterations or until convergence is reached. This method provides a better estimate of the background and background noise levels:: >>> from astropy.stats import sigma_clipped_stats >>> mean, median, std = sigma_clipped_stats(data, sigma=3.0) >>> print((mean, median, std)) # doctest: +FLOAT_CMP (5.199138651621793, 5.155587433358291, 2.094275212132969) Masking Sources ^^^^^^^^^^^^^^^ An even better procedure is to exclude the sources in the image by masking them. Of course, this technique requires one to :ref:`identify the sources in the data `, which in turn depends on the background and background noise. Therefore, this method for estimating the background and background RMS requires an iterative procedure. Photutils provides a convenience function, :func:`~photutils.segmentation.make_source_mask`, for creating source masks. It uses sigma-clipped statistics as the first estimate of the background and noise levels for the source detection. Sources are then identified using image segmentation. Finally, the source masks are dilated to mask more extended regions around the detected sources. Here we use an aggressive 2-sigma detection threshold to maximize the source detections and dilate using a 11x11 box: .. doctest-requires:: scipy >>> from photutils.segmentation import make_source_mask >>> mask = make_source_mask(data, nsigma=2, npixels=5, dilate_size=11) >>> mean, median, std = sigma_clipped_stats(data, sigma=3.0, mask=mask) >>> print((mean, median, std)) # doctest: +FLOAT_CMP (5.001013475475569, 5.000584905604376, 1.970887100626572) Of course, the source detection and masking procedure can be iterated further. Even with one iteration we are within 0.02% of the true background and 1.5% of the true background RMS. .. _scipy: https://www.scipy.org/ 2D Background and Noise Estimation ---------------------------------- If the background or the background noise varies across the image, then you will generally want to generate a 2D image of the background and background RMS (or compute these values locally). This can be accomplished by applying the above techniques to subregions of the image. A common procedure is to use sigma-clipped statistics in each mesh of a grid that covers the input data to create a low-resolution background image. The final background or background RMS image can then be generated by interpolating the low-resolution image. Photutils provides the :class:`~photutils.background.Background2D` class to estimate the 2D background and background noise in an astronomical image. :class:`~photutils.background.Background2D` requires the size of the box (``box_size``) in which to estimate the background. Selecting the box size requires some care by the user. The box size should generally be larger than the typical size of sources in the image, but small enough to encapsulate any background variations. For best results, the box size should also be chosen so that the data are covered by an integer number of boxes in both dimensions. If that is not the case, the ``edge_method`` keyword determines whether to pad or crop the image such that there is an integer multiple of the ``box_size`` in both dimensions. The background level in each of the meshes is calculated using the function or callable object (e.g., class instance) input via ``bkg_estimator`` keyword. Photutils provides a several background classes that can be used: * `~photutils.background.MeanBackground` * `~photutils.background.MedianBackground` * `~photutils.background.ModeEstimatorBackground` * `~photutils.background.MMMBackground` * `~photutils.background.SExtractorBackground` * `~photutils.background.BiweightLocationBackground` The default is a `~photutils.background.SExtractorBackground` instance. For this method, the background in each mesh is calculated as ``(2.5 * median) - (1.5 * mean)``. However, if ``(mean - median) / std > 0.3`` then the ``median`` is used instead. Likewise, the background RMS level in each mesh is calculated using the function or callable object input via the ``bkgrms_estimator`` keyword. Photutils provides the following classes for this purpose: * `~photutils.background.StdBackgroundRMS` * `~photutils.background.MADStdBackgroundRMS` * `~photutils.background.BiweightScaleBackgroundRMS` For even more flexibility, users may input a custom function or callable object to the ``bkg_estimator`` and/or ``bkgrms_estimator`` keywords. By default, the ``bkg_estimator`` and ``bkgrms_estimator`` are applied to sigma clipped data. Sigma clipping is defined by inputting a :class:`astropy.stats.SigmaClip` object to the ``sigma_clip`` keyword. The default is to perform sigma clipping with ``sigma=3`` and ``maxiters=10``. Sigma clipping can be turned off by setting ``sigma_clip=None``. After the background level has been determined in each of the boxes, the low-resolution background image can be median filtered, with a window of size of ``filter_size``, to suppress local under- or overestimations (e.g., due to bright galaxies in a particular box). Likewise, the median filter can be applied only to those boxes where the background level is above a specified threshold (``filter_threshold``). The low-resolution background and background RMS images are resized to the original data size using the function or callable object input via the ``interpolator`` keyword. Photutils provides two interpolator classes: :class:`~photutils.background.BkgZoomInterpolator` (default), which performs spline interpolation, and :class:`~photutils.background.BkgIDWInterpolator`, which uses inverse-distance weighted (IDW) interpolation. For this example, we will create a test image by adding a strong background gradient to the image defined above:: >>> ny, nx = data.shape >>> y, x = np.mgrid[:ny, :nx] >>> gradient = x * y / 5000. >>> data2 = data + gradient >>> plt.imshow(data2, norm=norm, origin='lower', cmap='Greys_r', ... interpolation='nearest') # doctest: +SKIP .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.datasets import make_100gaussians_image data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data2, norm=norm, origin='lower', cmap='Greys_r', interpolation='nearest') We start by creating a `~photutils.background.Background2D` object using a box size of 50x50 and a 3x3 median filter. We will estimate the background level in each mesh as the sigma-clipped median using an instance of :class:`~photutils.background.MedianBackground`. .. doctest-requires:: scipy >>> from astropy.stats import SigmaClip >>> from photutils.background import Background2D, MedianBackground >>> sigma_clip = SigmaClip(sigma=3.) >>> bkg_estimator = MedianBackground() >>> bkg = Background2D(data2, (50, 50), filter_size=(3, 3), ... sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) The 2D background and background RMS images are retrieved using the ``background`` and ``background_rms`` attributes, respectively, on the returned object. The low-resolution versions of these images are stored in the ``background_mesh`` and ``background_rms_mesh`` attributes, respectively. The global median value of the low-resolution background and background RMS image can be accessed with the ``background_median`` and ``background_rms_median`` attributes, respectively: .. doctest-requires:: scipy >>> print(bkg.background_median) # doctest: +FLOAT_CMP 10.821997862561792 >>> print(bkg.background_rms_median) # doctest: +FLOAT_CMP 2.298820539683762 Let's plot the background image: .. doctest-skip:: >>> plt.imshow(bkg.background, origin='lower', cmap='Greys_r', ... interpolation='nearest') .. plot:: from astropy.stats import SigmaClip import matplotlib.pyplot as plt import numpy as np from photutils.background import Background2D, MedianBackground from photutils.datasets import make_100gaussians_image data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient sigma_clip = SigmaClip(sigma=3.) bkg_estimator = MedianBackground() bkg = Background2D(data2, (50, 50), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) plt.imshow(bkg.background, origin='lower', cmap='Greys_r', interpolation='nearest') and the background-subtracted image: .. doctest-skip:: >>> plt.imshow(data2 - bkg.background, norm=norm, origin='lower', ... cmap='Greys_r', interpolation='nearest') .. plot:: from astropy.stats import SigmaClip from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.background import Background2D, MedianBackground from photutils.datasets import make_100gaussians_image data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient sigma_clip = SigmaClip(sigma=3.) bkg_estimator = MedianBackground() bkg = Background2D(data2, (50, 50), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data2 - bkg.background, norm=norm, origin='lower', cmap='Greys_r', interpolation='nearest') Masking ^^^^^^^ Masks can also be input into `~photutils.background.Background2D`. The ``mask`` keyword can be used to mask sources or bad pixels in the image prior to estimating the background levels. Additionally, the ``coverage_mask`` keyword can be used to mask blank regions without data coverage (e.g., from a rotated image or an image from a mosaic). Otherwise, the data values in the regions without coverage (usually zeros or NaNs) will adversely affect the background statistics. Unlike ``mask``, ``coverage_mask`` is applied to the output background and background RMS maps. The ``fill_value`` keyword defines the value assigned in the output background and background RMS maps where the input ``coverage_mask`` is `True`. Let's create a rotated image that has blank areas and plot it (NOTE: this example requires `scipy`_): .. doctest-requires:: scipy >>> from scipy.ndimage import rotate >>> data3 = rotate(data2, -45.) >>> norm = ImageNormalize(stretch=SqrtStretch()) # doctest: +SKIP >>> plt.imshow(data3, origin='lower', cmap='Greys_r', norm=norm, ... interpolation='nearest') # doctest: +SKIP .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.datasets import make_100gaussians_image from scipy.ndimage.interpolation import rotate data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient data3 = rotate(data2, -45.) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data3, origin='lower', cmap='Greys_r', norm=norm, interpolation='nearest') Now we create a coverage mask and input it into `~photutils.background.Background2D` to exclude the regions where we have no data. For this example, we set the ``fill_value`` to 0.0. For real data, one can usually create a coverage mask from a weight or noise image. In this example we also use a smaller box size to help capture the strong gradient in the background: .. doctest-requires:: scipy >>> coverage_mask = (data3 == 0) >>> bkg3 = Background2D(data3, (25, 25), filter_size=(3, 3), ... coverage_mask=coverage_mask, fill_value=0.0) Note that the ``coverage_mask`` is applied to the output background image (values assigned to ``fill_value``): .. doctest-requires:: scipy >>> norm = ImageNormalize(stretch=SqrtStretch()) # doctest: +SKIP >>> plt.imshow(bkg3.background, origin='lower', cmap='Greys_r', norm=norm, ... interpolation='nearest') # doctest: +SKIP .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.background import Background2D from photutils.datasets import make_100gaussians_image from scipy.ndimage.interpolation import rotate data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient data3 = rotate(data2, -45.) coverage_mask = (data3 == 0) bkg3 = Background2D(data3, (25, 25), filter_size=(3, 3), coverage_mask=coverage_mask, fill_value=0.0) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(bkg3.background, origin='lower', cmap='Greys_r', norm=norm, interpolation='nearest') Finally, let's subtract the background from the image and plot it: .. doctest-skip:: >>> norm = ImageNormalize(stretch=SqrtStretch()) >>> plt.imshow(data3 - bkg3.background, origin='lower', cmap='Greys_r', ... norm=norm, interpolation='nearest') .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.background import Background2D from photutils.datasets import make_100gaussians_image from scipy.ndimage.interpolation import rotate data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient data3 = rotate(data2, -45.) coverage_mask = (data3 == 0) bkg3 = Background2D(data3, (25, 25), filter_size=(3, 3), coverage_mask=coverage_mask, fill_value=0.0) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data3 - bkg3.background, origin='lower', cmap='Greys_r', norm=norm, interpolation='nearest') If there is any small residual background still present in the image, the background subtraction can be improved by masking the sources and/or through further iterations. Plotting Meshes ^^^^^^^^^^^^^^^ Finally, the meshes that were used in generating the 2D background can be plotted on the original image using the :meth:`~photutils.background.Background2D.plot_meshes` method: .. doctest-skip:: >>> plt.imshow(data3, origin='lower', cmap='Greys_r', norm=norm, ... interpolation='nearest') >>> bkg3.plot_meshes(outlines=True, color='#1f77b4') .. plot:: from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.background import Background2D from photutils.datasets import make_100gaussians_image from scipy.ndimage.interpolation import rotate data = make_100gaussians_image() ny, nx = data.shape y, x = np.mgrid[:ny, :nx] gradient = x * y / 5000. data2 = data + gradient data3 = rotate(data2, -45.) coverage_mask = (data3 == 0) bkg3 = Background2D(data3, (25, 25), filter_size=(3, 3), coverage_mask=coverage_mask, fill_value=0.0) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data3, origin='lower', cmap='Greys_r', norm=norm, interpolation='nearest') bkg3.plot_meshes(outlines=True, color='#17becf') The meshes extended beyond the original image on the top and right because :class:`~photutils.background.Background2D`'s default ``edge_method`` is ``'pad'``. Reference/API ------------- .. automodapi:: photutils.background :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/centroids.rst0000644000214200020070000001304200000000000015652 0ustar00lbradleyCentroids (`photutils.centroids`) ================================= Introduction ------------ `photutils.centroids` provides several functions to calculate the centroid of a single source: * :func:`~photutils.centroids.centroid_com`: Calculates the object "center of mass" from 2D image moments. * :func:`~photutils.centroids.centroid_quadratic`: Calculates the centroid by fitting a 2D quadratic polynomial to the data. * :func:`~photutils.centroids.centroid_1dg`: Calculates the centroid by fitting 1D Gaussians to the marginal ``x`` and ``y`` distributions of the data. * :func:`~photutils.centroids.centroid_2dg`: Calculates the centroid by fitting a 2D Gaussian to the 2D distribution of the data. Masks can be input into each of these functions to mask bad pixels. Error arrays can be input into the two Gaussian fitting methods to weight the fits. To calculate the centroids of many sources in an image, use the :func:`~photutils.centroids.centroid_sources` function. This function can be used with any of the above centroiding functions or a custom user-defined centroiding function. Getting Started --------------- Let's extract a single object from a synthetic dataset and find its centroid with each of these methods. For this simple example we will not subtract the background from the data (but in practice, one should subtract the background):: >>> from photutils.datasets import make_4gaussians_image >>> from photutils.centroids import centroid_com, centroid_quadratic >>> from photutils.centroids import centroid_1dg, centroid_2dg >>> data = make_4gaussians_image()[43:79, 76:104] >>> x1, y1 = centroid_com(data) >>> print((x1, y1)) # doctest: +FLOAT_CMP (13.93157998341213, 17.051234441067088) >>> x2, y2 = centroid_quadratic(data) >>> print((x2, y2)) # doctest: +FLOAT_CMP (13.948284438186919, 16.98788199435759) .. doctest-requires:: scipy >>> x3, y3 = centroid_1dg(data) >>> print((x3, y3)) # doctest: +FLOAT_CMP (14.040352707371396, 16.962306463644801) .. doctest-requires:: scipy >>> x4, y4 = centroid_2dg(data) >>> print((x4, y4)) # doctest: +FLOAT_CMP (14.002212073733611, 16.996134592982017) Now let's plot the results. Because the centroids are all very similar, we also include an inset plot zoomed in near the centroid: .. plot:: :include-source: import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes from mpl_toolkits.axes_grid1.inset_locator import mark_inset from photutils.centroids import centroid_com, centroid_quadratic from photutils.centroids import centroid_1dg, centroid_2dg from photutils.datasets import make_4gaussians_image data = make_4gaussians_image()[43:79, 76:104] # extract single object xycen1 = centroid_com(data) xycen2 = centroid_quadratic(data) xycen3 = centroid_1dg(data) xycen4 = centroid_2dg(data) xycens = [xycen1, xycen2, xycen3, xycen4] fig, ax = plt.subplots(1, 1, figsize=(4, 5)) ax.imshow(data, origin='lower', interpolation='nearest') marker = '+' ms, mew = 15, 2. colors = ('white', 'black', 'red', 'blue') for xycen, color in zip(xycens, colors): plt.plot(*xycen, color=color, marker=marker, ms=ms, mew=mew) ax2 = zoomed_inset_axes(ax, zoom=6, loc=9) ax2.imshow(data, vmin=190, vmax=220, origin='lower', interpolation='nearest') ms, mew = 30, 2. for xycen, color in zip(xycens, colors): ax2.plot(*xycen, color=color, marker=marker, ms=ms, mew=mew) ax2.set_xlim(13, 15) ax2.set_ylim(16, 18) mark_inset(ax, ax2, loc1=3, loc2=4, fc='none', ec='0.5') ax2.axes.get_xaxis().set_visible(False) ax2.axes.get_yaxis().set_visible(False) ax.set_xlim(0, data.shape[1] - 1) ax.set_ylim(0, data.shape[0] - 1) Centroiding several sources in an image --------------------------------------- The :func:`~photutils.centroids.centroid_sources` function can be used to calculate the centroids of many sources in a single image given initial guesses for their positions. This function can be used with any of the above centroiding functions or a custom user-defined centroiding function. Here is a simple example using :func:`~photutils.centroids.centroid_com`. A cutout image is made centered at each initial position of size ``box_size``. A centroid is then calculated within the cutout image for each source: .. doctest-requires:: scipy >>> from photutils.centroids import centroid_sources >>> data = make_4gaussians_image() >>> x_init = (25, 91, 151, 160) >>> y_init = (40, 61, 24, 71) >>> x, y = centroid_sources(data, x_init, y_init, box_size=21, ... centroid_func=centroid_com) >>> print(x) # doctest: +FLOAT_CMP [ 24.98911515 90.43056554 150.20332399 159.87234831] >>> print(y) # doctest: +FLOAT_CMP [40.08504359 60.56869612 24.74216925 70.32723054] Let's plot the results: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.centroids import centroid_sources, centroid_com from photutils.datasets import make_4gaussians_image data = make_4gaussians_image() x_init = (25, 91, 151, 160) y_init = (40, 61, 24, 71) x, y = centroid_sources(data, x_init, y_init, box_size=21, centroid_func=centroid_com) plt.figure(figsize=(8, 4)) plt.imshow(data, origin='lower', interpolation='nearest') plt.scatter(x, y, marker='+', s=80, color='red') plt.tight_layout() Reference/API ------------- .. automodapi:: photutils.centroids :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1464275319.0 photutils-1.3.0/docs/changelog.rst0000644000214200020070000000011300000000000015602 0ustar00lbradley.. _changelog: ********* Changelog ********* .. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/docs/citation.rst0000644000214200020070000000010000000000000015461 0ustar00lbradley.. _photutils_citation: .. include:: ../photutils/CITATION.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638214186.0 photutils-1.3.0/docs/conf.py0000644000214200020070000001467100000000000014436 0ustar00lbradley# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst # # 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. from configparser import ConfigParser from datetime import datetime import os import sys try: from sphinx_astropy.conf.v1 import * # noqa except ImportError: print('ERROR: the documentation requires the sphinx-astropy package to ' 'be installed') sys.exit(1) # Get configuration information from setup.cfg 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.7' # Extend astropy intersphinx_mapping with packages we use here intersphinx_mapping['skimage'] = ('https://scikit-image.org/docs/stable/', None) # noqa intersphinx_mapping['gwcs'] = ('https://gwcs.readthedocs.io/en/latest/', None) # noqa # Exclude astropy intersphinx_mapping for unused packages del intersphinx_mapping['h5py'] # noqa # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns.append('_templates') # noqa # Exclude template PSF block specification documentation exclude_patterns.append('psf_spec/*') # noqa plot_formats = ['png', 'hires.png', 'pdf', 'svg'] # This is added to the end of RST files - a good place to put # substitutions to be used globally. rst_epilog = """ .. _Astropy: https://www.astropy.org/ """ # -- Project information ------------------------------------------------------ project = setup_cfg['name'] author = setup_cfg['author'] copyright = f'2011-{datetime.utcnow().year}, {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__(project) package = sys.modules[project] # 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 -------------------------------------------------- # 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. # 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_theme = None # Customized theme options html_theme_options = { 'logotext1': 'phot', # white, semi-bold 'logotext2': 'utils', # orange, light 'logotext3': '' # white, light } # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # 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 = os.path.join('_static', 'favicon.ico') # A "Last built" timestamp is inserted at every page bottom, using the # given strftime format. Set to '' to omit this timestamp. # html_last_updated_fmt = '%d %b %Y' # The name for this set of Sphinx documents. If None, it defaults to # " v". html_title = f'{project} {release}' # Output file base name for HTML help builder. htmlhelp_basename = project + 'doc' # Static files to copy after template files html_static_path = ['_static'] html_style = 'photutils.css' # -- 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')] latex_logo = '_static/photutils_banner.pdf' # -- 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)] # -- Resolving issue number to links in changelog ----------------------------- github_project = setup_cfg['github_project'] github_issues_url = f'https://github.com/{github_project}/issues/' # -- 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: nitpick_filename = 'nitpick-exceptions.txt' if os.path.isfile(nitpick_filename): for line in open(nitpick_filename): if line.strip() == "" or line.startswith("#"): continue dtype, target = line.split(None, 1) target = target.strip() nitpick_ignore.append((dtype, target)) # -- Options for linkcheck output --------------------------------------------- linkcheck_retry = 5 linkcheck_ignore = ['http://data.astropy.org', r'https://iraf.net/*', r'https://github\.com/astropy/photutils/(?:issues|pull)/\d+'] linkcheck_timeout = 180 linkcheck_anchors = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/contributing.rst0000644000214200020070000000302000000000000016362 0ustar00lbradleyReporting Issues and Contributing ================================= Reporting Issues ---------------- If you have found a bug in Photutils, please report it by creating a new issue on the `Photutils GitHub issue tracker `_. That requires creating a `free Github account `_ if you do not have one. Please include an example that demonstrates the issue and will allow the developers to reproduce and fix the problem. You may be also asked to provide information about your operating system and a full Python stack trace. The developers will walk you through obtaining a stack trace if it is necessary. Contributing ------------ Like the `Astropy`_ project, Photutils is made both by and for its users. We accept contributions at all levels, spanning the gamut from fixing a typo in the documentation to developing a major new feature. We welcome contributors who will abide by the `Python Software Foundation Code of Conduct `_. Photutils follows the same workflow and coding guidelines as `Astropy`_. The following pages will help you get started with contributing fixes, code, or documentation (no git or GitHub experience necessary): * `How to make a code contribution `_ * `Coding Guidelines `_ * `Developer Documentation `_ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/datasets.rst0000644000214200020070000000355400000000000015477 0ustar00lbradley.. _datasets: Datasets (`photutils.datasets`) =============================== Introduction ------------ `photutils.datasets` gives easy access to load or make a few example datasets. The datasets are mostly images, but they also include PSF models and a source catalog. These datasets are useful for the Photutils documentation, tests, and benchmarks, but also for users that would like to try out or implement new methods for Photutils. Functions that start with ``load_*`` load data files from disk. Very small data files are bundled in the Photutils code repository and are guaranteed to be available. Mid-sized data files are currently available from the `astropy-data`_ repository and loaded into the Astropy cache on the user's machine on first load. Functions that start with ``make_*`` generate simple simulated data (e.g., Gaussian sources on a flat background with Poisson or Gaussian noise). Note that there are other tools like `skymaker`_ that can simulate much more realistic astronomical images. Getting Started --------------- Let's load an example image of M67 with :func:`~photutils.datasets.load_star_image`:: >>> from photutils.datasets import load_star_image >>> hdu = load_star_image() # doctest: +REMOTE_DATA >>> print(hdu.data.shape) # doctest: +REMOTE_DATA (1059, 1059) ``hdu`` is a FITS `~astropy.io.fits.ImageHDU` object and ``hdu.data`` is a `~numpy.ndarray` object. Let's plot the image: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.datasets import load_star_image hdu = load_star_image() plt.imshow(hdu.data, origin='lower', interpolation='nearest') plt.tight_layout() plt.show() Reference/API ------------- .. automodapi:: photutils.datasets :no-heading: .. _astropy-data: https://github.com/astropy/astropy-data/ .. _skymaker: https://github.com/astromatic/skymaker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/detection.rst0000644000214200020070000002672100000000000015646 0ustar00lbradley.. _source_detection: Source Detection (`photutils.detection`) ======================================== Introduction ------------ One generally needs to identify astronomical sources in their data before they can perform photometry or morphological measurements. Photutils provides two functions designed specifically to detect point-like (stellar) sources in an astronomical image. Photutils also provides a function to identify local peaks in an image that are above a specified threshold value. For general-use source detection and extraction of both point-like and extended sources, please see :ref:`Image Segmentation `. Detecting Stars --------------- Photutils includes two widely-used tools that are used to detect stars in an image, `DAOFIND`_ and IRAF's `starfind`_. :class:`~photutils.detection.DAOStarFinder` is a class that provides an implementation of the `DAOFIND`_ algorithm (`Stetson 1987, PASP 99, 191 `_). It searches images for local density maxima that have a peak amplitude greater than a specified threshold (the threshold is applied to a convolved image) and have a size and shape similar to a defined 2D Gaussian kernel. :class:`~photutils.detection.DAOStarFinder` also provides an estimate of the objects' roundness and sharpness, whose lower and upper bounds can be specified. :class:`~photutils.detection.IRAFStarFinder` is a class that implements IRAF's `starfind`_ algorithm. It is fundamentally similar to :class:`~photutils.detection.DAOStarFinder`, but :class:`~photutils.detection.DAOStarFinder` can use an elliptical Gaussian kernel. One other difference in :class:`~photutils.detection.IRAFStarFinder` is that it calculates the objects' centroid, roundness, and sharpness using image moments. As an example, let's load an image from the bundled datasets and select a subset of the image. We will estimate the background and background noise using sigma-clipped statistics:: >>> from astropy.stats import sigma_clipped_stats >>> from photutils.datasets import load_star_image >>> hdu = load_star_image() # doctest: +REMOTE_DATA >>> data = hdu.data[0:401, 0:401] # doctest: +REMOTE_DATA >>> mean, median, std = sigma_clipped_stats(data, sigma=3.0) # doctest: +REMOTE_DATA >>> print((mean, median, std)) # doctest: +REMOTE_DATA, +FLOAT_CMP (3668.09661145823, 3649.0, 204.41388592022315) Now we will subtract the background and use an instance of :class:`~photutils.detection.DAOStarFinder` to find the stars in the image that have FWHMs of around 3 pixels and have peaks approximately 5-sigma above the background. Running this class on the data yields an astropy `~astropy.table.Table` containing the results of the star finder: .. doctest-requires:: scipy >>> from photutils.detection import DAOStarFinder >>> daofind = DAOStarFinder(fwhm=3.0, threshold=5.*std) # doctest: +REMOTE_DATA >>> sources = daofind(data - median) # doctest: +REMOTE_DATA >>> for col in sources.colnames: # doctest: +REMOTE_DATA ... sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) # doctest: +REMOTE_DATA id xcentroid ycentroid sharpness ... sky peak flux mag --- --------- --------- ---------- ... --- ---- --------- ------------ 1 144.24757 6.3797904 0.58156257 ... 0 6903 5.6976747 -1.8892441 2 208.66907 6.8205805 0.48348966 ... 0 7896 6.7186388 -2.0682032 3 216.92614 6.5775933 0.69359525 ... 0 2195 1.6662764 -0.55436758 4 351.62519 8.5459013 0.48577834 ... 0 6977 5.8970385 -1.9265849 5 377.51991 12.065501 0.52038488 ... 0 1260 1.1178252 -0.12093477 ... ... ... ... ... ... ... ... ... 282 267.90091 398.61991 0.27117231 ... 0 9299 5.4379278 -1.8385836 283 271.46959 398.91242 0.36738752 ... 0 8028 5.0693475 -1.7623802 284 299.05003 398.78469 0.25895667 ... 0 9072 5.5584641 -1.862387 285 299.99359 398.76661 0.29412474 ... 0 9253 5.3233471 -1.815462 286 360.44533 399.52381 0.37315624 ... 0 8079 6.9203438 -2.1003192 Length = 286 rows Let's plot the image and mark the location of detected sources: .. doctest-skip:: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> from astropy.visualization import SqrtStretch >>> from astropy.visualization.mpl_normalize import ImageNormalize >>> from photutils.aperture import CircularAperture >>> positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) >>> apertures = CircularAperture(positions, r=4.) >>> norm = ImageNormalize(stretch=SqrtStretch()) >>> plt.imshow(data, cmap='Greys', origin='lower', norm=norm, ... interpolation='nearest') >>> apertures.plot(color='blue', lw=1.5, alpha=0.5) .. plot:: from astropy.stats import sigma_clipped_stats from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.aperture import CircularAperture from photutils.datasets import load_star_image from photutils.detection import DAOStarFinder hdu = load_star_image() data = hdu.data[0:401, 0:401] mean, median, std = sigma_clipped_stats(data, sigma=3.0) daofind = DAOStarFinder(fwhm=3.0, threshold=5. * std) sources = daofind(data - median) positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) apertures = CircularAperture(positions, r=4.) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data, cmap='Greys', origin='lower', norm=norm, interpolation='nearest') apertures.plot(color='blue', lw=1.5, alpha=0.5) Masking Regions ^^^^^^^^^^^^^^^ Regions of the input image can be masked by using the ``mask`` keyword with the :class:`~photutils.detection.DAOStarFinder` or :class:`~photutils.detection.IRAFStarFinder` instance. This simple examples uses :class:`~photutils.detection.DAOStarFinder` and masks two rectangular regions. No sources will be detected in the masked regions: .. doctest-skip:: >>> from photutils.detection import DAOStarFinder >>> daofind = DAOStarFinder(fwhm=3.0, threshold=5. * std) >>> mask = np.zeros(data.shape, dtype=bool) >>> mask[50:151, 50:351] = True >>> mask[250:351, 150:351] = True >>> sources = daofind(data - median, mask=mask) >>> for col in sources.colnames: >>> sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) .. plot:: from astropy.stats import sigma_clipped_stats from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt import numpy as np from photutils.aperture import CircularAperture, RectangularAperture from photutils.datasets import load_star_image from photutils.detection import DAOStarFinder hdu = load_star_image() data = hdu.data[0:401, 0:401] mean, median, std = sigma_clipped_stats(data, sigma=3.0) daofind = DAOStarFinder(fwhm=3.0, threshold=5. * std) mask = np.zeros(data.shape, dtype=bool) mask[50:151, 50:351] = True mask[250:351, 150:351] = True sources = daofind(data - median, mask=mask) positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) apertures = CircularAperture(positions, r=4.) norm = ImageNormalize(stretch=SqrtStretch()) plt.imshow(data, cmap='Greys', origin='lower', norm=norm, interpolation='nearest') plt.title('Star finder with a mask to exclude regions') apertures.plot(color='blue', lw=1.5, alpha=0.5) rect1 = RectangularAperture((200, 100), 300, 100, theta=0.) rect2 = RectangularAperture((250, 300), 200, 100, theta=0.) rect1.plot(color='salmon', ls='dashed') rect2.plot(color='salmon', ls='dashed') Local Peak Detection -------------------- Photutils also includes a :func:`~photutils.detection.find_peaks` function to find local peaks in an image that are above a specified threshold value. Peaks are the local maxima above a specified threshold that are separated by a specified minimum number of pixels. By default, the returned pixel coordinates are always integer-valued (i.e., no centroiding is performed, only the peak pixel is identified). However, a centroiding function can be input via the ``centroid_func`` keyword to :func:`~photutils.detection.find_peaks` to compute centroid coordinates with subpixel precision. :func:`~photutils.detection.find_peaks` supports a number of additional options, including searching for peaks only within a specified footprint. Please see the :func:`~photutils.detection.find_peaks` documentation for more options. As a simple example, let's find the local peaks in an image that are 5 sigma above the background and a separated by at least 5 pixels: .. doctest-requires:: scipy >>> from astropy.stats import sigma_clipped_stats >>> from photutils.datasets import make_100gaussians_image >>> from photutils.detection import find_peaks >>> data = make_100gaussians_image() >>> mean, median, std = sigma_clipped_stats(data, sigma=3.0) >>> threshold = median + (5. * std) >>> tbl = find_peaks(data, threshold, box_size=11) >>> tbl['peak_value'].info.format = '%.8g' # for consistent table output >>> print(tbl[:10]) # print only the first 10 peaks x_peak y_peak peak_value ------ ------ ---------- 233 0 27.477852 493 6 20.404769 207 11 24.075798 258 12 17.395025 366 12 18.729726 289 22 35.853276 380 29 19.261986 442 31 30.239994 359 36 19.771626 471 38 25.45583 And let's plot the location of the detected peaks in the image: .. doctest-skip:: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> from astropy.visualization import simple_norm >>> from astropy.visualization.mpl_normalize import ImageNormalize >>> from photutils.aperture import CircularAperture >>> positions = np.transpose((tbl['x_peak'], tbl['y_peak'])) >>> apertures = CircularAperture(positions, r=5.) >>> norm = simple_norm(data, 'sqrt', percent=99.9) >>> plt.imshow(data, cmap='Greys_r', origin='lower', norm=norm, ... interpolation='nearest') >>> apertures.plot(color='#0547f9', lw=1.5) >>> plt.xlim(0, data.shape[1] - 1) >>> plt.ylim(0, data.shape[0] - 1) .. plot:: from astropy.stats import sigma_clipped_stats from astropy.visualization import simple_norm import matplotlib.pyplot as plt import numpy as np from photutils.aperture import CircularAperture from photutils.datasets import make_100gaussians_image from photutils.detection import find_peaks data = make_100gaussians_image() mean, median, std = sigma_clipped_stats(data, sigma=3.0) threshold = median + (5.0 * std) tbl = find_peaks(data, threshold, box_size=11) positions = np.transpose((tbl['x_peak'], tbl['y_peak'])) apertures = CircularAperture(positions, r=5.) norm = simple_norm(data, 'sqrt', percent=99.9) plt.imshow(data, cmap='Greys_r', origin='lower', norm=norm, interpolation='nearest') apertures.plot(color='#0547f9', lw=1.5) plt.xlim(0, data.shape[1] - 1) plt.ylim(0, data.shape[0] - 1) Reference/API ------------- .. automodapi:: photutils.detection :no-heading: .. _DAOFIND: https://iraf.net/irafhelp.php?val=daofind .. _starfind: https://iraf.net/irafhelp.php?val=starfind ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640123871.969308 photutils-1.3.0/docs/dev/0000755000214200020070000000000000000000000013704 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/docs/dev/releasing.rst0000644000214200020070000001521400000000000016412 0ustar00lbradley.. doctest-skip-all **************************** Package Release Instructions **************************** This document outlines the steps for releasing Photutils to PyPI. This process currently requires admin-level access to the Photutils GitHub repository, as it relies on the ability to commit to main directly. It also requires a PyPI account with admin-level access for Photutils. These instructions assume the name of the git remote for the repo is called ``upstream``. #. Check out the branch that you are going to release. This will usually be the ``main`` branch, unless you are making a bugfix release. For a bugfix release, check out the ``A.B.x`` branch. Use ``git cherry-pick `` (or ``git cherry-pick -m1 `` for merge commits) to backport fixes to the bugfix branch. Also, be sure to push all changes to the upstream repo so that CI can run on the bugfix branch. #. Ensure that CI tests are passing for the branch you are going to release. Also, ensure that Read the Docs builds are passing. #. Locally run the tests using ``tox`` to thoroughly test the code in isolated environments:: tox -e test-alldeps -- --remote-data=any tox -e build_docs tox -e linkcheck #. Update the ``CHANGES.rst`` file to make sure that all the changes are listed and update the release date, which should currently be set to ``unreleased``, to the current date in ``yyyy-mm-dd`` format. Then commit the changes:: git add CHANGES.rst git commit -m'Finalizing changelog for version ' #. Remove any untracked files (WARNING: this will permanently remove any files that have not been previously committed, so make sure that you don't need to keep any of these files):: git clean -dfx #. Update the package version number to the version you’re about to release by creating an annotated git tag (optionally signing with the ``-s`` option):: git tag -a -m'' git show # show the tag git tag # show all tags #. Check out the release commit:: git checkout #. Generate the source distribution tar file by first making sure the `build `_ package is installed and up to date:: pip install build --upgrade then creating the source distribution with:: python -m build --sdist . #. Run tests on the generated source distribution by going inside the ``dist`` directory, expanding the tar file, going inside the expanded directory, and running the tests with:: cd dist tar xvfz .tar.gz cd tox -e test-alldeps -- --remote-data=any tox -e build_docs Optionally, install and test the source distribution in a virtual environment:: pip install -e '.[all,test]' pytest --remote-data=any or:: pip install '../.tar.gz[all,test]' cd python >>> import photutils >>> photutils.__version__ >>> photutils.test(remote_data=True) #. Go back to the package root directory and remove the generated files with:: git clean -dfx #. Make sure the source distribution doesn't inherit limited permissions from your default umask:: umask 0022 chmod -R a+Xr . #. Generate the source distribution and upload it to PyPI:: python -m build --sdist . twine check dist/* twine upload dist/* Check that the entry on PyPI (https://pypi.org/project/photutils/) is correct, and that the tarfile is present. #. Go back to the main branch:: git checkout main #. Push the released tag to the upstream repo:: git push upstream #. Update ``CHANGES.rst``. After releasing a minor (bugfix) version, update its release date. After releasing a major version, add a new section to ``CHANGES.rst`` for the next ``x.y.z`` version, with a single entry ``No changes yet``, e.g.,:: x.y.z (unreleased) ------------------ - No changes yet Then commit the changes and push to the upstream repo:: git add CHANGES.rst git commit -m'Add version to the changelog' git push upstream main #. After releasing a major version, tag this new commit with the development version of the next major version and push the tag to the upstream repo. This is needed if the latest package release is the first bugfix release tagged on a bugfix branch (not the main branch):: git tag -a -m'' git push upstream #. Create a GitHub Release (https://github.com/astropy/photutils/releases) by clicking on "Draft a new release", select the tag of the released version, add a release title with the released version, and add the following description:: See the [changelog](https://photutils.readthedocs.io/en/stable/changelog.html) for release notes. Then click "Publish release". This step will trigger an automatic update of the package on Zenodo (see below). #. Close the GitHub Milestone (https://github.com/astropy/photutils/milestones) for the released version and, if needed, open a new Milestone for the next release. #. Go to Read the Docs (https://readthedocs.org/projects/photutils/versions/) and check that the "stable" docs correspond to the new released version. Deactivate any older released versions (i.e., uncheck "Active"). #. Check that Zenodo is updated with the released version (https://doi.org/10.5281/zenodo.596036). Zenodo is already configured to automatically update with a new published GitHub Release (see above). #. After the release, the conda-forge bot (``regro-cf-autotick-bot``) will automatically create a pull request on https://github.com/conda-forge/photutils-feedstock. The ``meta.yaml`` recipe may need to be edited with updated dependencies. Modify (if necessary), review, and merge the PR to create the conda-forge package (https://anaconda.org/conda-forge/photutils). The Astropy conda channel (https://anaconda.org/astropy/photutils) will automatically mirror the package from conda-forge. #. Build wheels and upload them to PyPI. The Photutils wheels are currently built using https://github.com/larrybradley/photutils-wheel-forge. Once the wheels have been built, they are uploaded as artifacts in Azure Pipelines. Download the wheels from Azure Pipelines and upload them to PyPI:: python get_wheels.py twine check wheelhouse/*.whl twine upload wheelhouse/*.whl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/epsf.rst0000644000214200020070000003077200000000000014626 0ustar00lbradley.. _build-epsf: Building an effective Point Spread Function (ePSF) ================================================== The ePSF -------- The instrumental PSF is a combination of many factors that are generally difficult to model. `Anderson and King (2000; PASP 112, 1360) `_ showed that accurate stellar photometry and astrometry can be derived by modeling the net PSF, which they call the effective PSF (ePSF). The ePSF is an empirical model describing what fraction of a star's light will land in a particular pixel. The constructed ePSF is typically oversampled with respect to the detector pixels. Building an ePSF ---------------- Photutils provides tools for building an ePSF following the prescription of `Anderson and King (2000; PASP 112, 1360) `_ and subsequent enhancements detailed mainly in `Anderson (2016), ISR WFC3 2016-12 `_. The process involves iterating between the ePSF itself and the stars used to build it. To begin, we must first define a sample of stars used to build the ePSF. Ideally these stars should be bright (high S/N) and isolated to prevent contamination from nearby stars. One may use the star-finding tools in Photutils (e.g., :class:`~photutils.detection.DAOStarFinder` or :class:`~photutils.detection.IRAFStarFinder`) to identify an initial sample of stars. However, the step of creating a good sample of stars will also likely require visual inspection and manual selection to ensure stars are sufficiently isolated and of good quality (e.g., no cosmic rays, detector artifacts, etc.). Let's start by loading a simulated HST/WFC3 image in the F160W band:: >>> from photutils.datasets import load_simulated_hst_star_image >>> hdu = load_simulated_hst_star_image() # doctest: +REMOTE_DATA >>> data = hdu.data # doctest: +REMOTE_DATA The simulated image does not contain any background or noise, so let's add those to the image:: >>> from photutils.datasets import make_noise_image >>> data += make_noise_image(data.shape, distribution='gaussian', ... mean=10., stddev=5., seed=123) # doctest: +REMOTE_DATA Let's show the image: .. plot:: :include-source: from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.datasets import load_simulated_hst_star_image from photutils.datasets import make_noise_image hdu = load_simulated_hst_star_image() data = hdu.data data += make_noise_image(data.shape, distribution='gaussian', mean=10., stddev=5., seed=123) norm = simple_norm(data, 'sqrt', percent=99.) plt.imshow(data, norm=norm, origin='lower', cmap='viridis') For this example we'll use the :func:`~photutils.detection.find_peaks` function to identify the stars and their initial positions. We will not use the centroiding option in :func:`~photutils.detection.find_peaks` to simulate the effect of having imperfect initial guesses for the positions of the stars. Here we set the detection threshold value to 500.0 to select only the brightest stars: .. doctest-requires:: scipy >>> from photutils.detection import find_peaks >>> peaks_tbl = find_peaks(data, threshold=500.) # doctest: +REMOTE_DATA >>> peaks_tbl['peak_value'].info.format = '%.8g' # for consistent table output # doctest: +REMOTE_DATA >>> print(peaks_tbl) # doctest: +REMOTE_DATA x_peak y_peak peak_value ------ ------ ---------- 849 2 1076.7026 182 4 1709.5671 324 4 3006.0086 100 9 1142.9915 824 9 1302.8604 ... ... ... 751 992 801.23834 114 994 1595.2804 299 994 648.18539 207 998 2810.6503 691 999 2611.0464 Length = 431 rows Note that the stars are sufficiently separated in the simulated image that we do not need to exclude any stars due to crowding. In practice this step will require some manual inspection and selection. Next, we need to extract cutouts of the stars using the :func:`~photutils.psf.extract_stars` function. This function requires a table of star positions either in pixel or sky coordinates. For this example we are using the pixel coordinates, which need to be in table columns called simply ``x`` and ``y``. We plan to extract 25 x 25 pixel cutouts of our selected stars, so let's explicitly exclude stars that are too close to the image boundaries (because they cannot be extracted): .. doctest-requires:: scipy >>> size = 25 >>> hsize = (size - 1) / 2 >>> x = peaks_tbl['x_peak'] # doctest: +REMOTE_DATA >>> y = peaks_tbl['y_peak'] # doctest: +REMOTE_DATA >>> mask = ((x > hsize) & (x < (data.shape[1] -1 - hsize)) & ... (y > hsize) & (y < (data.shape[0] -1 - hsize))) # doctest: +REMOTE_DATA Now let's create the table of good star positions: .. doctest-requires:: scipy >>> from astropy.table import Table >>> stars_tbl = Table() >>> stars_tbl['x'] = x[mask] # doctest: +REMOTE_DATA >>> stars_tbl['y'] = y[mask] # doctest: +REMOTE_DATA The star cutouts from which we build the ePSF must have the background subtracted. Here we'll use the sigma-clipped median value as the background level. If the background in the image varies across the image, one should use more sophisticated methods (e.g., `~photutils.background.Background2D`). Let's subtract the background from the image:: >>> from astropy.stats import sigma_clipped_stats >>> mean_val, median_val, std_val = sigma_clipped_stats(data, sigma=2.) # doctest: +REMOTE_DATA >>> data -= median_val # doctest: +REMOTE_DATA The :func:`~photutils.psf.extract_stars` function requires the input data as an `~astropy.nddata.NDData` object. An `~astropy.nddata.NDData` object is easy to create from our data array:: >>> from astropy.nddata import NDData >>> nddata = NDData(data=data) # doctest: +REMOTE_DATA We are now ready to create our star cutouts using the :func:`~photutils.psf.extract_stars` function. For this simple example we are extracting stars from a single image using a single catalog. The :func:`~photutils.psf.extract_stars` can also extract stars from multiple images using a separate catalog for each image or a single catalog. When using a single catalog, the star positions must be in sky coordinates (as `~astropy.coordinates.SkyCoord` objects) and the `~astropy.nddata.NDData` objects must contain valid `~astropy.wcs.WCS` objects. In the case of using multiple images (i.e., dithered images) and a single catalog, the same physical star will be "linked" across images, meaning it will be constrained to have the same sky coordinate in each input image. Let's extract the 25 x 25 pixel cutouts of our selected stars: .. doctest-requires:: scipy >>> from photutils.psf import extract_stars >>> stars = extract_stars(nddata, stars_tbl, size=25) # doctest: +REMOTE_DATA The function returns a `~photutils.psf.EPSFStars` object containing the cutouts of our selected stars. The function extracted 403 stars, from which we'll build our ePSF. Let's show the first 25 of them: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> from astropy.visualization import simple_norm >>> nrows = 5 >>> ncols = 5 >>> fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(20, 20), ... squeeze=True) >>> ax = ax.ravel() >>> for i in range(nrows * ncols): ... norm = simple_norm(stars[i], 'log', percent=99.) ... ax[i].imshow(stars[i], norm=norm, origin='lower', cmap='viridis') .. plot:: from astropy.nddata import NDData from astropy.stats import sigma_clipped_stats from astropy.table import Table from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.datasets import load_simulated_hst_star_image from photutils.datasets import make_noise_image from photutils.detection import find_peaks from photutils.psf import extract_stars hdu = load_simulated_hst_star_image() data = hdu.data data += make_noise_image(data.shape, distribution='gaussian', mean=10., stddev=5., seed=123) peaks_tbl = find_peaks(data, threshold=500.) size = 25 hsize = (size - 1) / 2 x = peaks_tbl['x_peak'] y = peaks_tbl['y_peak'] mask = ((x > hsize) & (x < (data.shape[1] - 1 - hsize)) & (y > hsize) & (y < (data.shape[0] - 1 - hsize))) stars_tbl = Table() stars_tbl['x'] = x[mask] stars_tbl['y'] = y[mask] mean_val, median_val, std_val = sigma_clipped_stats(data, sigma=2.) data -= median_val nddata = NDData(data=data) stars = extract_stars(nddata, stars_tbl, size=25) nrows = 5 ncols = 5 fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(20, 20), squeeze=True) ax = ax.ravel() for i in range(nrows * ncols): norm = simple_norm(stars[i], 'log', percent=99.) ax[i].imshow(stars[i], norm=norm, origin='lower', cmap='viridis') With the star cutouts in hand, we are ready to construct the ePSF with the :class:`~photutils.psf.EPSFBuilder` class. We'll create an ePSF with an oversampling factor of 4.0. Here we limit the maximum number of iterations to 3 (to limit it's run time), but in practice one should use about 10 or more iterations. The :class:`~photutils.psf.EPSFBuilder` class has many other options to control the ePSF build process, including changing the centering function, the smoothing kernel, and the centering accuracy. Please see the :class:`~photutils.psf.EPSFBuilder` documentation for further details. We first initialize an :class:`~photutils.psf.EPSFBuilder` instance with our desired parameters and then input the cutouts of our selected stars to the instance: .. doctest-requires:: scipy >>> from photutils.psf import EPSFBuilder >>> epsf_builder = EPSFBuilder(oversampling=4, maxiters=3, ... progress_bar=False) # doctest: +REMOTE_DATA >>> epsf, fitted_stars = epsf_builder(stars) # doctest: +REMOTE_DATA The returned values are the ePSF, as an :class:`~photutils.psf.EPSFModel` object, and our input stars fitted with the constructed ePSF, as a new :class:`~photutils.psf.EPSFStars` object with fitted star positions and fluxes. Finally, let's show the constructed ePSF: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> from astropy.visualization import simple_norm >>> norm = simple_norm(epsf.data, 'log', percent=99.) >>> plt.imshow(epsf.data, norm=norm, origin='lower', cmap='viridis') >>> plt.colorbar() .. plot:: from astropy.nddata import NDData from astropy.stats import sigma_clipped_stats from astropy.table import Table from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.datasets import load_simulated_hst_star_image from photutils.datasets import make_noise_image from photutils.detection import find_peaks from photutils.psf import extract_stars, EPSFBuilder hdu = load_simulated_hst_star_image() data = hdu.data data += make_noise_image(data.shape, distribution='gaussian', mean=10., stddev=5., seed=123) peaks_tbl = find_peaks(data, threshold=500.) size = 25 hsize = (size - 1) / 2 x = peaks_tbl['x_peak'] y = peaks_tbl['y_peak'] mask = ((x > hsize) & (x < (data.shape[1] - 1 - hsize)) & (y > hsize) & (y < (data.shape[0] - 1 - hsize))) stars_tbl = Table() stars_tbl['x'] = x[mask] stars_tbl['y'] = y[mask] mean_val, median_val, std_val = sigma_clipped_stats(data, sigma=2.) data -= median_val nddata = NDData(data=data) stars = extract_stars(nddata, stars_tbl, size=25) epsf_builder = EPSFBuilder(oversampling=4, maxiters=3, progress_bar=False) epsf, fitted_stars = epsf_builder(stars) norm = simple_norm(epsf.data, 'log', percent=99.) plt.imshow(epsf.data, norm=norm, origin='lower', cmap='viridis') plt.colorbar() The :class:`~photutils.psf.EPSFModel` object is a subclass of :class:`~photutils.psf.FittableImageModel`, thus it can be used as a PSF model for the :ref:`PSF-fitting machinery in Photutils ` (i.e., `~photutils.psf.BasicPSFPhotometry`, `~photutils.psf.IterativelySubtractedPSFPhotometry`, or `~photutils.psf.DAOPhotPSFPhotometry`). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/docs/geometry.rst0000644000214200020070000000050400000000000015512 0ustar00lbradleyGeometry Functions (`photutils.geometry`) ========================================= Introduction ------------ The `photutils.geometry` package contains low-level geometry functions used mainly by `~photutils.aperture.aperture_photometry`. Reference/API ------------- .. automodapi:: photutils.geometry :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/getting_started.rst0000644000214200020070000001251700000000000017055 0ustar00lbradleyGetting Started with Photutils ============================== The following example uses Photutils to find sources in an astronomical image and then perform circular aperture photometry on them. We start by loading an image from the bundled datasets and selecting a subset of the image:: >>> import numpy as np >>> from photutils.datasets import load_star_image >>> hdu = load_star_image() # doctest: +REMOTE_DATA >>> image = hdu.data[500:700, 500:700].astype(float) # doctest: +REMOTE_DATA We then subtract a rough estimate of the background, calculated using the image median:: >>> image -= np.median(image) # doctest: +REMOTE_DATA In the remainder of this example, we assume that the data is background-subtracted. Photutils supports several source detection algorithms. For this example, we use :class:`~photutils.detection.DAOStarFinder` to detect the stars in the image. We set the detection threshold at the 3-sigma noise level, estimated using the median absolute deviation (`~astropy.stats.mad_std`) of the image. The parameters of the detected sources are returned as an Astropy `~astropy.table.Table`: .. doctest-requires:: scipy >>> from photutils.detection import DAOStarFinder >>> from astropy.stats import mad_std >>> bkg_sigma = mad_std(image) # doctest: +REMOTE_DATA >>> daofind = DAOStarFinder(fwhm=4., threshold=3. * bkg_sigma) # doctest: +REMOTE_DATA >>> sources = daofind(image) # doctest: +REMOTE_DATA >>> for col in sources.colnames: # doctest: +REMOTE_DATA ... sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) # doctest: +REMOTE_DATA id xcentroid ycentroid sharpness ... sky peak flux mag --- --------- ---------- ---------- ... --- ---- --------- ----------- 1 182.83866 0.16767019 0.85099873 ... 0 3824 2.8028346 -1.1189937 2 189.20431 0.26081353 0.7400477 ... 0 4913 3.8729185 -1.4700959 3 5.7946491 2.6125424 0.39589731 ... 0 7752 4.1029107 -1.5327302 4 36.847063 1.3220228 0.29594528 ... 0 8739 7.4315818 -2.1777032 5 3.2565602 5.418952 0.35985495 ... 0 6935 3.8126298 -1.4530616 ... ... ... ... ... ... ... ... ... 147 197.24864 186.16647 0.31211532 ... 0 8302 7.5814629 -2.1993825 148 124.31327 188.30523 0.5362742 ... 0 6702 6.6358543 -2.0547421 149 24.257207 194.71494 0.44169546 ... 0 8342 3.2671037 -1.2854073 150 116.45 195.05923 0.67080547 ... 0 3299 2.8775221 -1.1475467 151 18.958086 196.34207 0.56502139 ... 0 3854 2.3835296 -0.94305138 152 111.52575 195.73192 0.45827852 ... 0 8109 7.9278607 -2.24789 Length = 152 rows Using the source locations (i.e., the ``xcentroid`` and ``ycentroid`` columns), we now define circular apertures centered at these positions with a radius of 4 pixels and compute the sum of the pixel values within the apertures. The :func:`~photutils.aperture.aperture_photometry` function returns an Astropy `~astropy.table.QTable` with the results of the photometry: .. doctest-requires:: scipy >>> from photutils.aperture import aperture_photometry, CircularAperture >>> positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) # doctest: +REMOTE_DATA >>> apertures = CircularAperture(positions, r=4.) # doctest: +REMOTE_DATA >>> phot_table = aperture_photometry(image, apertures) # doctest: +REMOTE_DATA >>> for col in phot_table.colnames: # doctest: +REMOTE_DATA ... phot_table[col].info.format = '%.8g' # for consistent table output >>> print(phot_table) # doctest: +REMOTE_DATA id xcenter ycenter aperture_sum pix pix --- --------- ---------- ------------ 1 182.83866 0.16767019 18121.759 2 189.20431 0.26081353 29836.515 3 5.7946491 2.6125424 331979.82 4 36.847063 1.3220228 183705.09 5 3.2565602 5.418952 349468.98 ... ... ... ... 148 124.31327 188.30523 45084.874 149 24.257207 194.71494 355778.01 150 116.45 195.05923 31232.912 151 18.958086 196.34207 162076.26 152 111.52575 195.73192 82795.715 Length = 152 rows The sum of the pixel values within the apertures are given in the ``aperture_sum`` column. Finally, we plot the image and the defined apertures: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> plt.imshow(image, cmap='gray_r', origin='lower') >>> apertures.plot(color='blue', lw=1.5, alpha=0.5) .. plot:: from astropy.stats import mad_std import matplotlib.pyplot as plt import numpy as np from photutils.aperture import aperture_photometry, CircularAperture from photutils.datasets import load_star_image from photutils.detection import DAOStarFinder hdu = load_star_image() image = hdu.data[500:700, 500:700].astype(float) image -= np.median(image) bkg_sigma = mad_std(image) daofind = DAOStarFinder(fwhm=4., threshold=3. * bkg_sigma) sources = daofind(image) positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) apertures = CircularAperture(positions, r=4.) phot_table = aperture_photometry(image, apertures) brightest_source_id = phot_table['aperture_sum'].argmax() plt.imshow(image, cmap='gray_r', origin='lower') apertures.plot(color='blue', lw=1.5, alpha=0.5) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/grouping.rst0000644000214200020070000002337000000000000015517 0ustar00lbradleyGrouping Algorithms =================== Introduction ------------ In Point Spread Function (PSF) photometry, a grouping algorithm is used to separate stars into optimum groups. The stars in each group are defined as those close enough together such that they need to be fit simultaneously, i.e., their profiles overlap. Photoutils currently provides two classes to group stars: * :class:`~photutils.psf.DAOGroup`: An implementation of the `DAOPHOT `_ GROUP algorithm. * :class:`~photutils.psf.DBSCANGroup`: Grouping is based on the `Density-Based Spatial Clustering of Applications with Noise (DBSCAN) `_ algorithm. DAOPHOT GROUP ------------- Stetson, in his seminal paper (`Stetson 1987, PASP 99, 191 `_), provided a simple and powerful grouping algorithm to decide whether the profile of a given star extends into the fitting region of any other star. Stetson defines this in terms of a "critical separation" parameter, which is defined as the minimal distance that any two stars must be separated by in order to be in different groups. Stetson gives intuitive reasoning to suggest that the critical separation may be defined as a multiple of the stellar full width at half maximum (FWHM). Photutils provides an implementation of the DAOPHOT GROUP algorithm in the :class:`~photutils.psf.DAOGroup` class. Let's take a look at a simple example. First, let's make some Gaussian sources using `~photutils.datasets.make_random_gaussians_table` and `~photutils.datasets.make_gaussian_sources_image`. The former will return a `~astropy.table.Table` containing parameters for 2D Gaussian sources and the latter will make an actual image using that table. .. plot:: :include-source: import matplotlib.pyplot as plt import numpy as np from photutils.datasets import (make_random_gaussians_table, make_gaussian_sources_image) n_sources = 350 sigma_psf = 2.0 params = {'flux': [500, 5000], 'x_mean': [6, 250], 'y_mean': [6, 250], 'x_stddev': [sigma_psf, sigma_psf], 'y_stddev': [sigma_psf, sigma_psf], 'theta': [0, np.pi]} starlist = make_random_gaussians_table(n_sources, params, seed=123) shape = (256, 256) sim_image = make_gaussian_sources_image(shape, starlist) plt.imshow(sim_image, origin='lower', interpolation='nearest', cmap='viridis') plt.show() ``starlist`` is an astropy `~astropy.table.Table` of parameters defining the position and shape of the stars. Next, we need to rename the table columns of the centroid positions so that they agree with the names that `~photutils.psf.DAOGroup` expect. Here we rename ``x_mean`` to ``x_0`` and ``y_mean`` to ``y_0``: .. doctest-skip:: >>> starlist['x_mean'].name = 'x_0' >>> starlist['y_mean'].name = 'y_0' Now, let's find the stellar groups. We start by creating a `~photutils.psf.DAOGroup` object. Here we set its ``crit_separation`` parameter ``2.5 * fwhm``, where the stellar ``fwhm`` was defined above when we created the stars as 2D Gaussians. In general one will need to measure the FWHM of the stellar profiles. .. doctest-skip:: >>> from astropy.stats import gaussian_sigma_to_fwhm >>> from photutils.psf.groupstars import DAOGroup >>> fwhm = sigma_psf * gaussian_sigma_to_fwhm >>> daogroup = DAOGroup(crit_separation=2.5 * fwhm) ``daogroup`` is a `~photutils.psf.DAOGroup` instance that can be used as a calling function that receives as input a table of stars (e.g., ``starlist``): .. doctest-skip:: >>> star_groups = daogroup(starlist) The ``star_groups`` output is copy of the input ``starlist`` table, but with an extra column called ``group_id``. This column contains integers that represent the group assigned to each source. Here the grouping algorithm separated the 350 stars into 92 distinct groups: .. doctest-skip:: >>> print(max(star_groups['group_id'])) 92 One can use the ``group_by`` functionality from `~astropy.table.Table` to create groups according to ``group_id``: .. doctest-skip:: >>> star_groups = star_groups.group_by('group_id') >>> print(star_groups) flux x_0 y_0 ... amplitude id group_id ------------- ------------- ------------- ... ------------- --- -------- 1361.83752671 182.958386152 178.708228379 ... 54.1857935158 1 1 4282.41965053 179.998944123 171.437757021 ... 170.392063944 183 1 555.831417775 181.611905957 185.16181342 ... 22.1158294162 222 1 3299.48946968 243.60449392 85.8926967927 ... 131.282514695 2 2 2469.77482553 136.657577889 109.771746713 ... 98.2692179518 3 3 ... ... ... ... ... ... ... 818.132804377 117.787387455 92.4349134636 ... 32.5524699806 313 88 3979.57421702 154.85279495 18.3148180315 ... 158.34222701 318 89 3622.30997136 97.0901736699 50.3565997421 ... 144.127134338 323 90 765.47561385 144.952825542 7.57086675812 ... 30.4573069401 330 91 1508.68165551 54.0404934991 232.693833605 ... 60.0285357567 349 92 Length = 350 rows Finally, let's plot a circular aperture around each star, where stars in the same group have the same aperture color: .. doctest-skip:: >>> import numpy as np >>> from photutils.aperture import CircularAperture >>> from photutils.utils import make_random_cmap >>> plt.imshow(sim_image, origin='lower', interpolation='nearest', ... cmap='Greys_r') >>> cmap = make_random_cmap(seed=123) >>> for i, group in enumerate(star_groups.groups): >>> xypos = np.transpose([group['x_0'], group['y_0']]) >>> ap = CircularAperture(xypos, r=fwhm) >>> ap.plot(color=cmap.colors[i]) >>> plt.show() .. plot:: from astropy.stats import gaussian_sigma_to_fwhm from matplotlib import rcParams import matplotlib.pyplot as plt import numpy as np from photutils.aperture import CircularAperture from photutils.datasets import (make_random_gaussians_table, make_gaussian_sources_image) from photutils.psf.groupstars import DAOGroup from photutils.utils import make_random_cmap rcParams['image.aspect'] = 1 # to get images with square pixels rcParams['figure.figsize'] = (7, 7) n_sources = 350 sigma_psf = 2.0 params = {'flux': [500, 5000], 'x_mean': [6, 250], 'y_mean': [6, 250], 'x_stddev': [sigma_psf, sigma_psf], 'y_stddev': [sigma_psf, sigma_psf], 'theta': [0, np.pi]} starlist = make_random_gaussians_table(n_sources, params, seed=123) shape = (256, 256) sim_image = make_gaussian_sources_image(shape, starlist) starlist['x_mean'].name = 'x_0' starlist['y_mean'].name = 'y_0' fwhm = sigma_psf * gaussian_sigma_to_fwhm daogroup = DAOGroup(crit_separation=2.5 * fwhm) star_groups = daogroup(starlist) star_groups = star_groups.group_by('group_id') plt.imshow(sim_image, origin='lower', interpolation='nearest', cmap='Greys_r') cmap = make_random_cmap(seed=123) for i, group in enumerate(star_groups.groups): xypos = np.transpose([group['x_0'], group['y_0']]) ap = CircularAperture(xypos, r=fwhm) ap.plot(color=cmap.colors[i]) DBSCANGroup ----------- Photutils also provides a :class:`~photutils.psf.DBSCANGroup` class to group stars based on the `Density-Based Spatial Clustering of Applications with Noise (DBSCAN) `_ algorithm. :class:`~photutils.psf.DBSCANGroup` provides a more general algorithm than :class:`~photutils.psf.DAOGroup`. Here's a simple example using :class:`~photutils.psf.DBSCANGroup` with ``min_samples=1`` and ``metric=euclidean``. With these parameters, the result is identical to the `~photutils.psf.DAOGroup` algorithm. Note that `scikit-learn `_ must be installed to use :class:`~photutils.psf.DBSCANGroup`. .. plot:: from astropy.stats import gaussian_sigma_to_fwhm from matplotlib import rcParams import matplotlib.pyplot as plt import numpy as np from photutils.aperture import CircularAperture from photutils.datasets import (make_random_gaussians_table, make_gaussian_sources_image) from photutils.psf.groupstars import DBSCANGroup from photutils.utils import make_random_cmap rcParams['image.aspect'] = 1 # to get images with square pixels rcParams['figure.figsize'] = (7, 7) n_sources = 350 sigma_psf = 2.0 params = {'flux': [500, 5000], 'x_mean': [6, 250], 'y_mean': [6, 250], 'x_stddev': [sigma_psf, sigma_psf], 'y_stddev': [sigma_psf, sigma_psf], 'theta': [0, np.pi]} starlist = make_random_gaussians_table(n_sources, params, seed=123) shape = (256, 256) sim_image = make_gaussian_sources_image(shape, starlist) starlist['x_mean'].name = 'x_0' starlist['y_mean'].name = 'y_0' fwhm = sigma_psf * gaussian_sigma_to_fwhm group = DBSCANGroup(crit_separation=2.5 * fwhm) star_groups = group(starlist) star_groups = star_groups.group_by('group_id') plt.imshow(sim_image, origin='lower', interpolation='nearest', cmap='Greys_r') cmap = make_random_cmap(seed=123) for i, group in enumerate(star_groups.groups): xypos = np.transpose([group['x_0'], group['y_0']]) ap = CircularAperture(xypos, r=fwhm) ap.plot(color=cmap.colors[i]) Reference/API ------------- .. automodapi:: photutils.psf.groupstars :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638214186.0 photutils-1.3.0/docs/index.rst0000644000214200020070000000407200000000000014772 0ustar00lbradley .. the "raw" directive below is used to hide the title in favor of just the logo being visible .. raw:: html .. |br| raw:: html
********* Photutils ********* .. raw:: html .. only:: latex .. image:: _static/photutils_banner.pdf **Photutils** is an `affiliated package `_ of `Astropy`_ that primarily provides tools for detecting and performing photometry of astronomical sources. It is an open source Python package and is licensed under a :ref:`3-clause BSD license `. |br| .. Important:: If you use Photutils for a project that leads to a publication, whether directly or as a dependency of another package, please include an :doc:`acknowledgment and/or citation `. |br| Getting Started =============== .. toctree:: :maxdepth: 1 install.rst whats_new/index.rst overview.rst pixel_conventions.rst getting_started.rst contributing.rst citation.rst license.rst changelog User Documentation ================== .. toctree:: :maxdepth: 1 background.rst detection.rst grouping.rst aperture.rst psf.rst epsf.rst psf_matching.rst segmentation.rst centroids.rst morphology.rst isophote.rst geometry.rst datasets.rst utils.rst Developer Documentation ======================= .. toctree:: :maxdepth: 1 dev/releasing.rst .. toctree:: :hidden: test_function.rst |br| .. note:: Like much astronomy software, Photutils is an evolving package. The developers make an effort to maintain backwards compatibility, but at times the API may change if there is a benefit to doing so. If there are specific areas you think API stability is important, please let us know as part of the development process. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638927624.0 photutils-1.3.0/docs/install.rst0000644000214200020070000001166100000000000015333 0ustar00lbradley************ Installation ************ Requirements ============ Photutils has the following strict requirements: * `Python `_ 3.7 or later * `Numpy `_ 1.17 or later * `Astropy`_ 4.0 or later Photutils also optionally depends on other packages for some features: * `Scipy `_ 1.6.0 or later: To power a variety of features in several modules (strongly recommended). * `matplotlib `_ 2.2 or later: To power a variety of plotting features (e.g., plotting apertures). * `scikit-image `_ 0.14.2 or later: Used in `~photutils.segmentation.deblend_sources` for deblending segmented sources. * `scikit-learn `_ 0.19 or later: Used in `~photutils.psf.DBSCANGroup` to create star groups. * `gwcs `_ 0.12 or later: Used in `~photutils.datasets.make_gwcs` to create a simple celestial gwcs object. * `bottleneck `_: Improves the performance of sigma clipping and other functionality that may require computing statistics on arrays with NaN values. Photutils depends on `pytest-astropy `_ (0.4 or later) to run the test suite. Installing the latest released version ====================================== The latest released (stable) version of Photutils can be installed either with `pip`_ or `conda`_. Using pip --------- To install Photutils with `pip`_, run:: pip install photutils If you want to make sure that none of your existing dependencies get upgraded, instead you can do:: pip install photutils --no-deps Note that you may need a C compiler (e.g., ``gcc`` or ``clang``) to be installed for the installation to succeed. If you get a ``PermissionError``, this means that you do not have the required administrative access to install new packages to your Python installation. In this case you may consider using the ``--user`` option to install the package into your home directory. You can read more about how to do this in the `pip documentation `_. Do **not** install Photutils or other third-party packages using ``sudo`` unless you are fully aware of the risks. Using conda ----------- Photutils can be installed with `conda`_ if you have installed `Anaconda `_ or `Miniconda `_. To install Photutils using the `conda-forge Anaconda channel `_, run:: conda install -c conda-forge photutils Installing the latest development version from Source ===================================================== Prerequisites ------------- You will need `Cython `_ (0.28 or later), a compiler suite, and the development headers for Python and Numpy in order to build Photutils from the source distribution. On Linux, using the package manager for your distribution will usually be the easiest route. On MacOS X you will need the `XCode`_ command-line tools, which can be installed using:: xcode-select --install Follow the onscreen instructions to install the command-line tools required. Note that you do not need to install the full `XCode`_ distribution (assuming you are using MacOS X 10.9 or later). Building and installing manually -------------------------------- Photutils is being developed on `GitHub`_. The latest development version of the Photutils source code can be retrieved using git:: git clone https://github.com/astropy/photutils.git Then to build and install Photutils, run:: cd photutils pip install .[all] If you wish to install the package in "editable" mode, instead include the "-e" option:: pip install -e .[all] Building and installing using pip --------------------------------- Alternatively, `pip`_ can be used to retrieve, build, and install the latest development version from `GitHub`_:: pip install git+https://github.com/astropy/photutils.git Again, if you want to make sure that none of your existing dependencies get upgraded, instead you can do:: pip install git+https://github.com/astropy/photutils.git --no-deps Testing an installed Photutils ============================== The easiest way to test your installed version of Photutils is running correctly is to use the :func:`photutils.test` function: .. doctest-skip:: >>> import photutils >>> photutils.test() Note that this may not work if you start Python from within the Photutils source distribution directory. The tests should run and report any failures, which you can report to the `Photutils issue tracker `_. .. _pip: https://pip.pypa.io/en/latest/ .. _conda: https://docs.conda.io/en/latest/ .. _GitHub: https://github.com/astropy/photutils .. _Xcode: https://developer.apple.com/xcode/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/isophote.rst0000644000214200020070000002454300000000000015522 0ustar00lbradleyElliptical Isophote Analysis (`photutils.isophote`) =================================================== Introduction ------------ The `~photutils.isophote` package provides tools to fit elliptical isophotes to a galaxy image. The isophotes in the image are measured using an iterative method described by `Jedrzejewski (1987; MNRAS 226, 747) `_. See the documentation of the :class:`~photutils.isophote.Ellipse` class for details about the algorithm. Please also see the :ref:`isophote-faq`. Getting Started --------------- For this example, let's create a simple simulated galaxy image:: >>> from astropy.modeling.models import Gaussian2D >>> import numpy as np >>> from photutils.datasets import make_noise_image >>> g = Gaussian2D(100., 75, 75, 20, 12, theta=40. * np.pi / 180.) >>> ny = nx = 150 >>> y, x = np.mgrid[0:ny, 0:nx] >>> noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., ... stddev=2., seed=1234) >>> data = g(x, y) + noise .. plot:: from astropy.modeling.models import Gaussian2D import matplotlib.pyplot as plt import numpy as np from photutils.datasets import make_noise_image g = Gaussian2D(100., 75, 75, 20, 12, theta=40. * np.pi / 180.) ny = nx = 150 y, x = np.mgrid[0:ny, 0:nx] noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., stddev=2., seed=1234) data = g(x, y) + noise plt.imshow(data, origin='lower') We must provide the elliptical isophote fitter with an initial ellipse to be fitted. This ellipse geometry is defined with the `~photutils.isophote.EllipseGeometry` class. Here we'll define an initial ellipse whose position angle is offset from the data:: >>> from photutils.isophote import EllipseGeometry >>> geometry = EllipseGeometry(x0=75, y0=75, sma=20, eps=0.5, ... pa=20. * np.pi / 180.) Let's show this initial ellipse guess: .. doctest-skip:: >>> import matplotlib.pyplot as plt >>> from photutils.aperture import EllipticalAperture >>> aper = EllipticalAperture((geometry.x0, geometry.y0), geometry.sma, ... geometry.sma * (1 - geometry.eps), ... geometry.pa) >>> plt.imshow(data, origin='lower') >>> aper.plot(color='white') .. plot:: from astropy.modeling.models import Gaussian2D import matplotlib.pyplot as plt import numpy as np from photutils.aperture import EllipticalAperture from photutils.datasets import make_noise_image from photutils.isophote import EllipseGeometry g = Gaussian2D(100., 75, 75, 20, 12, theta=40. * np.pi / 180.) ny = nx = 150 y, x = np.mgrid[0:ny, 0:nx] noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., stddev=2., seed=1234) data = g(x, y) + noise geometry = EllipseGeometry(x0=75, y0=75, sma=20, eps=0.5, pa=20. * np.pi / 180.) aper = EllipticalAperture((geometry.x0, geometry.y0), geometry.sma, geometry.sma * (1 - geometry.eps), geometry.pa) plt.imshow(data, origin='lower') aper.plot(color='white') Next, we create an instance of the `~photutils.isophote.Ellipse` class, inputting the data to be fitted and the initial ellipse geometry object:: >>> from photutils.isophote import Ellipse >>> ellipse = Ellipse(data, geometry) To perform the elliptical isophote fit, we run the :meth:`~photutils.isophote.Ellipse.fit_image` method: .. doctest-requires:: scipy >>> isolist = ellipse.fit_image() The result is a list of isophotes as an `~photutils.isophote.IsophoteList` object, whose attributes are the fit values for each `~photutils.isophote.Isophote` sorted by the semimajor axis length. Let's print the fit position angles (radians): .. doctest-requires:: scipy >>> print(isolist.pa) # doctest: +SKIP [ 0. 0.16838914 0.18453378 0.20310945 0.22534975 0.25007781 0.28377499 0.32494582 0.38589202 0.40480013 0.39527698 0.38448771 0.40207495 0.40207495 0.28201524 0.28201524 0.19889817 0.1364335 0.1364335 0.13405719 0.17848892 0.25687327 0.35750355 0.64882699 0.72489435 0.91472008 0.94219702 0.87393299 0.82572916 0.7886367 0.75523282 0.7125274 0.70481612 0.7120097 0.71250791 0.69707669 0.7004807 0.70709823 0.69808124 0.68621341 0.69437566 0.70548293 0.70427021 0.69978326 0.70410887 0.69532744 0.69440413 0.70062534 0.68614488 0.7177538 0.7177538 0.7029571 0.7029571 0.7029571 ] We can also show the isophote values as a table, which is again sorted by the semimajor axis length (``sma``): .. doctest-requires:: scipy >>> print(isolist.to_table()) # doctest: +SKIP sma intens intens_err ... flag niter stop_code ... -------------- --------------- --------------- ... ---- ----- --------- 0.0 102.237692914 0.0 ... 0 0 0 0.534697261283 101.212218041 0.0280377938856 ... 0 10 0 0.588166987411 101.095404456 0.027821598428 ... 0 10 0 0.646983686152 100.971770355 0.0272405762608 ... 0 10 0 0.711682054767 100.842254551 0.0262991125932 ... 0 10 0 ... ... ... ... ... ... ... 51.874849202 3.44800874483 0.0881592058138 ... 0 50 2 57.0623341222 1.64031530995 0.0913122295433 ... 0 50 2 62.7685675344 0.692631010404 0.0786846787635 ... 0 32 0 69.0454242879 0.294659388337 0.0681758007533 ... 0 8 5 75.9499667166 0.0534892334515 0.0692483210903 ... 0 2 5 Length = 54 rows Let's plot the ellipticity, position angle, and the center x and y position as a function of the semimajor axis length: .. plot:: import matplotlib.pyplot as plt import numpy as np from astropy.modeling.models import Gaussian2D from photutils.datasets import make_noise_image from photutils.isophote import EllipseGeometry, Ellipse g = Gaussian2D(100., 75, 75, 20, 12, theta=40. * np.pi / 180.) ny = nx = 150 y, x = np.mgrid[0:ny, 0:nx] noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., stddev=2., seed=1234) data = g(x, y) + noise geometry = EllipseGeometry(x0=75, y0=75, sma=20, eps=0.5, pa=20. * np.pi / 180.) ellipse = Ellipse(data, geometry) isolist = ellipse.fit_image() plt.figure(figsize=(8, 8)) plt.subplots_adjust(hspace=0.35, wspace=0.35) plt.subplot(2, 2, 1) plt.errorbar(isolist.sma, isolist.eps, yerr=isolist.ellip_err, fmt='o', markersize=4) plt.xlabel('Semimajor Axis Length (pix)') plt.ylabel('Ellipticity') plt.subplot(2, 2, 2) plt.errorbar(isolist.sma, isolist.pa / np.pi * 180., yerr=isolist.pa_err / np.pi * 80., fmt='o', markersize=4) plt.xlabel('Semimajor Axis Length (pix)') plt.ylabel('PA (deg)') plt.subplot(2, 2, 3) plt.errorbar(isolist.sma, isolist.x0, yerr=isolist.x0_err, fmt='o', markersize=4) plt.xlabel('Semimajor Axis Length (pix)') plt.ylabel('x0') plt.subplot(2, 2, 4) plt.errorbar(isolist.sma, isolist.y0, yerr=isolist.y0_err, fmt='o', markersize=4) plt.xlabel('Semimajor Axis Length (pix)') plt.ylabel('y0') We can build an elliptical model image from the `~photutils.isophote.IsophoteList` object using the :func:`~photutils.isophote.build_ellipse_model` function ( NOTE: this function requires `scipy `_): .. doctest-requires:: scipy >>> from photutils.isophote import build_ellipse_model >>> model_image = build_ellipse_model(data.shape, isolist) >>> residual = data - model_image Finally, let's plot the original data, overplotted with some of the isophotes, the elliptical model image, and the residual image: .. plot:: import matplotlib.pyplot as plt import numpy as np from astropy.modeling.models import Gaussian2D from photutils.datasets import make_noise_image from photutils.isophote import EllipseGeometry, Ellipse from photutils.isophote import build_ellipse_model g = Gaussian2D(100., 75, 75, 20, 12, theta=40. * np.pi / 180.) ny = nx = 150 y, x = np.mgrid[0:ny, 0:nx] noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., stddev=2., seed=1234) data = g(x, y) + noise geometry = EllipseGeometry(x0=75, y0=75, sma=20, eps=0.5, pa=20. * np.pi / 180.) ellipse = Ellipse(data, geometry) isolist = ellipse.fit_image() model_image = build_ellipse_model(data.shape, isolist) residual = data - model_image fig, (ax1, ax2, ax3) = plt.subplots(figsize=(14, 5), nrows=1, ncols=3) fig.subplots_adjust(left=0.04, right=0.98, bottom=0.02, top=0.98) ax1.imshow(data, origin='lower') ax1.set_title('Data') smas = np.linspace(10, 50, 5) for sma in smas: iso = isolist.get_closest(sma) x, y, = iso.sampled_coordinates() ax1.plot(x, y, color='white') ax2.imshow(model_image, origin='lower') ax2.set_title('Ellipse Model') ax3.imshow(residual, origin='lower') ax3.set_title('Residual') Additional Example Notebooks (online) ------------------------------------- Additional example notebooks showing examples with real data and advanced usage are available online: * `Basic example of the Ellipse fitting tool `_ * `Running Ellipse with sigma-clipping `_ * `Building an image model from results obtained by Ellipse fitting `_ * `Advanced Ellipse example: multi-band photometry and masked arrays `_ Reference/API ------------- .. automodapi:: photutils.isophote :no-heading: .. toctree:: :hidden: isophote_faq.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/docs/isophote_faq.rst0000644000214200020070000002250200000000000016342 0ustar00lbradley.. _isophote-faq: Isophote Frequently Asked Questions ----------------------------------- .. _harmonic_ampl: 1. What are the basic equations relating harmonic amplitudes to geometrical parameter updates? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The basic elliptical isophote fitting algorithm, as described in `Jedrzejewski (1987; MNRAS 226, 747) `_, computes corrections for the current ellipse's geometrical parameters by essentially "projecting" the fitted harmonic amplitudes onto the image plane: .. math:: {\delta}_{X0} = \frac {-B_{1}} {I'} .. math:: {\delta}_{Y0} = \frac {-A_{1} (1 - {\epsilon})} {I'} .. math:: {\delta}_{\epsilon} = \frac {-2 B_{2} (1 - {\epsilon})} {I' a_0} .. math:: {\delta}_{\Theta} = \frac {2 A_{2} (1 - {\epsilon})} {I' a_0 [(1 - {\epsilon}) ^ 2 - 1 ]} where :math:`\epsilon` is the ellipticity, :math:`\Theta` is the position angle, :math:`A_i` and :math:`B_i` are the harmonic coefficients, and :math:`I'` is the derivative of the intensity along the major axis direction evaluated at a semimajor axis length of :math:`a_0`. 2. Why use "ellipticity" instead of the canonical ellipse eccentricity? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The main reason is that ellipticity, defined as .. math:: \epsilon = 1 - \frac{b}{a} better relates with the visual "flattening" of an ellipse. By looking at a flattened circle it is easy to guess its ellipticity, as say 0.1. The same ellipse has an eccentricity of 0.44, which is not obvious from visual inspection. The quantities relate as .. math:: Ecc = \sqrt{1 - (1 - {\epsilon})^2} 3. How is the radial gradient estimated? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The radial intensity gradient is the most critical quantity computed by the fitting algorithm. As can be seen from the above formulae, small :math:`I'` values lead to large values for the correction terms. Thus, :math:`I'` errors may lead to large fluctuations in these terms, when :math:`I'` itself is small. This usually happens at the fainter, outer regions of galaxy images. `Busko (1996; ASPC 101, 139) `_ found by numerical experiments that the precision to which a given ellipse can be fitted is related to the relative error in the local radial gradient. Because of the gradient's critical role, the algorithm has a number of features to allow its estimation even under difficult conditions. The default gradient computation, the one used by the algorithm when it first starts to fit a new isophote, is based on the extraction of two intensity samples: #1 at the current ellipse position, and #2 at a similar ellipse with a 10% larger semimajor axis. If the gradient so estimated is not meaningful, the algorithm extracts another #2 sample, this time using a 20% larger radius. In this context, a meaningful gradient means "shallower", but still close to within a factor 3 from the previous isophote's gradient estimate. If still no meaningful gradient can be measured, the algorithm uses the value measured at the last fitted isophote, but decreased (in absolute value) by a factor 0.8. This factor is roughly what is expected from semimajor-axis geometrical-sampling steps of 10 - 20% and a deVaucouleurs law or an exponential disk in its inner region (r <~ 5 req). When using the last isophote's gradient as estimator for the current one, the current gradient error cannot be computed and is set to `None`. As a last resort, if no previous gradient estimate is available, the algorithm just guesses the current value by setting it to be (minus) 10% of the mean intensity at sample #1. This case usually happens only at the first isophote fitted by the algorithm. The use of approximate gradient estimators may seem in contradiction with the fact that isophote fitting errors depend on gradient error, as well as with the fact that the algorithm itself is so sensitive to the gradient value. The rationale behind the use of approximate estimators, however, is based on the fact that the gradient value is used only to compute increments, not the ellipse parameters themselves. Approximate estimators are useful along the first steps in the iteration sequence, in particular when local image contamination (stars, defects, etc.) might make it difficult to find the correct path towards the solution. However, if the gradient is still not well determined at convergence, the subsequent error computations, and the algorithm's behavior from that point on, will take the fact into account properly. For instance, the 3rd and 4th harmonic amplitude errors depend on the gradient relative error, and if this is not computable at the current isophote, the algorithm uses a reasonable estimate (80% of the value at the last successful isophote) in order to generate sensible estimates for those harmonic errors. 4. How are the errors estimated? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Most parameters computed directly at each isophote have their errors computed by standard error propagation. Errors in the ellipse geometry parameters, on the other hand, cannot be estimated in the same way, since these parameters are not computed directly but result from a number of updates from a starting guess value. An error analysis based on numerical experiments (`Busko 1996; ASPC 101, 139 `_) showed that the best error estimators for these geometrical parameters can be found by simply "projecting" the harmonic amplitude errors that come from the least-squares covariance matrix by the same formulae in :ref:`Question 1 ` above used to "project" the associated parameter updates. In other words, errors for the ellipse center, ellipticity, and position angle are computed by the same formulae as in :ref:`Question 1 `, but replacing the least-squares amplitudes by their errors. This is empirical and difficult to justify in terms of any theoretical error analysis, but it produces sensible error estimators in practice. 5. How is the image sampled? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When sampling is done using elliptical sectors (mean or median modes), the algorithm described in `Jedrzejewski (1987; MNRAS 226, 747) `_ uses an elaborate, high-precision scheme to take into account partial pixels that lie along elliptical sector boundaries. In the current implementation of the `~photutils.isophote.Ellipse` algorithm, this method was not implemented. Instead, pixels at sector boundaries are either fully included or discarded, depending on the precise position of their centers in relation to the elliptical geometric locus corresponding to the current ellipse. This design decision is based on two arguments: (i) it would be difficult to include partial pixels in median computation, and (ii) speed. Even when the chosen integration mode is not bilinear, the sampling algorithm resorts to it in case the number of sampled pixels inside any given sector is less than 5. It was found that bilinear mode gives smoother samples in those cases. Tests performed with artificial images showed that cosmic rays and defective pixels can be very effectively removed from the fit by a combination of median sampling and sigma-clipping. 6. How reliable are the fluxes computed by the `~photutils.isophote.Ellipse` algorithm? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The integrated fluxes and areas computed by `~photutils.isophote.Ellipse` were checked against results produced by the IRAF ``noao.digiphot.apphot`` tasks ``phot`` and ``polyphot``, using artificial images. Quantities computed by `~photutils.isophote.Ellipse` match the reference ones within < 0.1% in all tested cases. 7. How does the object centerer work? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `~photutils.isophote.EllipseGeometry` class has a :meth:`~photutils.isophote.EllipseGeometry.find_center` method that runs an "object locator" around the input object coordinates. This routine performs a scan over a 10x10 pixel window centered on the input object coordinates. At each scan position, it extracts two concentric, roughly circular samples with radii 4 and 8 pixels. It then computes a signal-to-noise-like criterion using the intensity averages and standard deviations at each annulus: .. math:: c = \frac{f_{1} - f_{2}}{{\sqrt{\sigma_{1}^{2} + \sigma_{2}^{2}}}} and locates the pixel inside the scanned window where this criterion is a maximum. If the criterion so computed exceeds a given threshold, it assumes that a suitable object was detected at that position. The default threshold value is set to 0.1. This value and the annuli and window sizes currently used were found by trial and error using a number of both artificial and real galaxy images. It was found that very flattened galaxy images (ellipticity ~ 0.7) cannot be detected by such a simple algorithm. By increasing the threshold value the object locator becomes stricter, in the sense that it will not detect faint objects. To turn off the object locator, set the threshold to a value >> 1 in `~photutils.isophote.Ellipse`. This will prevent it from modifying whatever values for the center coordinates were given to the `~photutils.isophote.Ellipse` algorithm. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/docs/license.rst0000644000214200020070000000017200000000000015302 0ustar00lbradley.. _photutils_license: License ======= Photutils is licensed under a 3-clause BSD license: .. include:: ../LICENSE.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/docs/make.bat0000644000214200020070000001070500000000000014536 0ustar00lbradley@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%\* del /q /s api del /q /s generated 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/morphology.rst0000644000214200020070000000750200000000000016063 0ustar00lbradleyMorphological Properties (`photutils.morphology`) ================================================= Introduction ------------ The :func:`~photutils.morphology.data_properties` function can be used to calculate the morphological properties of a single source in a cutout image. `~photutils.morphology.data_properties` returns a `~photutils.segmentation.SourceCatalog` object. Please see `~photutils.segmentation.SourceCatalog` for the list of the many properties that are calculated. Even more properties are likely to be added in the future. If you have a segmentation image, the :class:`~photutils.segmentation.SourceCatalog` class can be used to calculate the properties for all (or a specified subset) of the segmented sources. Please see :ref:`Source Photometry and Properties from Image Segmentation ` for more details. Getting Started --------------- Let's extract a single object from a synthetic dataset and find calculate its morphological properties. For this example, we will subtract the background using simple sigma-clipped statistics. First, we create the source image and subtract its background:: >>> from photutils.datasets import make_4gaussians_image >>> from astropy.stats import sigma_clipped_stats >>> data = make_4gaussians_image()[43:79, 76:104] >>> mean, median, std = sigma_clipped_stats(data, sigma=3.0) >>> data -= median # subtract background Then, calculate its properties: .. doctest-requires:: scipy >>> from photutils.morphology import data_properties >>> cat = data_properties(data) >>> columns = ['label', 'xcentroid', 'ycentroid', 'semimajor_sigma', ... 'semiminor_sigma', 'orientation'] >>> tbl = cat.to_table(columns=columns) >>> tbl['xcentroid'].info.format = '.10f' # optional format >>> tbl['ycentroid'].info.format = '.10f' >>> tbl['semiminor_sigma'].info.format = '.10f' >>> tbl['orientation'].info.format = '.10f' >>> print(tbl) label xcentroid ycentroid ... semiminor_sigma orientation ... pix deg ----- ------------- ------------- ... --------------- ------------- 1 14.0225090502 16.9901801466 ... 3.6977761870 60.1283048753 Now let's use the measured morphological properties to define an approximate isophotal ellipse for the source: .. doctest-skip:: >>> import astropy.units as u >>> from photutils.aperture import EllipticalAperture >>> position = (cat.xcentroid, cat.ycentroid) >>> r = 3.0 # approximate isophotal extent >>> a = cat.semimajor_sigma.value * r >>> b = cat.semiminor_sigma.value * r >>> theta = cat.orientation.to(u.rad).value >>> apertures = EllipticalAperture(position, a, b, theta=theta) >>> plt.imshow(data, origin='lower', cmap='viridis', ... interpolation='nearest') >>> apertures.plot(color='#d62728') .. plot:: import astropy.units as u import matplotlib.pyplot as plt from photutils.aperture import EllipticalAperture from photutils.morphology import data_properties from photutils.datasets import make_4gaussians_image data = make_4gaussians_image()[43:79, 76:104] # extract single object cat = data_properties(data) columns = ['label', 'xcentroid', 'ycentroid', 'semimajor_sigma', 'semiminor_sigma', 'orientation'] tbl = cat.to_table(columns=columns) r = 2.5 # approximate isophotal extent position = (cat.xcentroid, cat.ycentroid) a = cat.semimajor_sigma.value * r b = cat.semiminor_sigma.value * r theta = cat.orientation.to(u.rad).value apertures = EllipticalAperture(position, a, b, theta=theta) plt.imshow(data, origin='lower', cmap='viridis', interpolation='nearest') apertures.plot(color='#d62728') Reference/API ------------- .. automodapi:: photutils.morphology :no-heading: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/docs/overview.rst0000644000214200020070000000162700000000000015534 0ustar00lbradleyOverview ======== Introduction ------------ Photutils contains functions for: * performing aperture photometry * performing PSF-fitting photometry * detecting and extracting point-like sources (e.g., stars) in astronomical images * detecting and extracting extended sources using image segmentation in astronomical images * centroiding sources * estimating the background and background RMS in astronomical images * building an effective Point Spread Function (ePSF) * matching PSF kernels * estimating morphological parameters of detected sources The code and issue tracker are available at the following links: * Code: https://github.com/astropy/photutils * Issue Tracker: https://github.com/astropy/photutils/issues Contributors ------------ For the complete list of contributors please see the `Photutils contributors page on Github `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638392192.0 photutils-1.3.0/docs/pixel_conventions.rst0000644000214200020070000000214500000000000017430 0ustar00lbradleyPixel Coordinate Conventions ---------------------------- In Photutils, integer pixel coordinates fall at the center of pixels and they are 0-indexed, matching the Python 0-based indexing. That means the first pixel is considered pixel ``0``, but pixel coordinate ``0`` is the *center* of that pixel. Hence, the first pixel spans pixel values ``-0.5`` to ``0.5``. For a 2-dimensional array, ``(x, y) = (0, 0)`` corresponds to the *center* of the bottom, leftmost array element. That means the first pixel spans the ``x`` and ``y`` pixel values from ``-0.5`` to ``0.5``. Note that this differs from the IRAF, `FITS WCS `_, `ds9`_, and `SourceExtractor`_ conventions, in which the center of the bottom, leftmost array element is ``(x, y) = (1, 1)``. Following Python indexing, the ``x`` (column) coordinate corresponds to the second (fast) array index and the ``y`` (row) coordinate corresponds to the first (slow) index. ``image[y, x]`` gives the value at pixel coordinates ``(x, y)``. .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ .. _ds9: http://ds9.si.edu/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf.rst0000644000214200020070000007324700000000000014465 0ustar00lbradley.. _psf_photometry: PSF Photometry (`photutils.psf`) ================================ The `photutils.psf` module contains tools for model-fitting photometry, often called "PSF photometry". .. _psf-terminology: Terminology ----------- Different astronomy sub-fields use the terms "PSF", "PRF", or related terms somewhat differently, especially when colloquial usage is taken into account. This package aims to be at the very least internally consistent, following the definitions described here. For this module we take Point Spread Function (PSF), or instrumental Point Spread Function (iPSF) to be the infinite resolution and infinite signal-to-noise flux distribution from a point source on the detector, after passing through optics, dust, atmosphere, etc. By contrast, the function describing the responsivity variations across individual *pixels* is the Pixel Response Function (sometimes called "PRF", but that acronym is not used here for reasons that will soon be apparent). The convolution of the PSF and pixel response function, when discretized onto the detector (i.e., a rectilinear CCD grid), is the effective PSF (ePSF) or Point Response Function (PRF). (This latter terminology is the definition used by `Spitzer `_. In many cases the PSF/ePSF/PRF distinction is unimportant, and the ePSF/PRF are simply called the "PSF", but the distinction can be critical when dealing carefully with undersampled data or detectors with significant intra-pixel sensitivity variations. For a more detailed description of this formalism, see `Anderson & King 2000 `_. All this said, in colloquial usage "PSF photometry" sometimes refers to the more general task of model-fitting photometry (with the effects of the PSF either implicitly or explicitly included in the models), regardless of exactly what kind of model is actually being fit. For brevity (e.g., ``photutils.psf``), we use "PSF photometry" in this way, as a shorthand for the general approach. Building an effective PSF (ePSF) -------------------------------- Please see :ref:`build-epsf` for documentation on how to build an ePSF. PSF Photometry -------------- Photutils provides a modular set of tools to perform PSF photometry for different science cases. These are implemented as separate classes to do sub-tasks of PSF photometry. It also provides high-level classes that connect these pieces together. In particular, it contains an implementation of the DAOPHOT algorithm (`~photutils.psf.DAOPhotPSFPhotometry`) proposed by `Stetson in his seminal paper `_ for crowded-field stellar photometry. The DAOPHOT algorithm consists in applying the loop FIND, GROUP, NSTAR, SUBTRACT, FIND until no more stars are detected or a given number of iterations is reached. Basically, `~photutils.psf.DAOPhotPSFPhotometry` works as follows. The first step is to estimate the sky background. For this task, photutils provides several classes to compute scalar and 2D backgrounds, see `~photutils.background` for details. The next step is to find an initial estimate of the positions of potential sources. This can be accomplished by using source detection algorithms, which are implemented in `~photutils.detection`. After finding sources, one would apply a clustering algorithm in order to label the sources according to groups. Usually, those groups are formed by a distance criterion, which is the case of the grouping algorithm proposed by Stetson. In `~photutils.psf.DAOGroup`, we provide an implementation of that algorithm. In addition, `~photutils.psf.DBSCANGroup` can also be used to group sources with more complex distance criteria. The reason behind the construction of groups is illustrated as follows: imagine that one would like to fit 300 stars and the model for each star has three parameters to be fitted. If one constructs a single model to fit the 300 stars simultaneously, then the optimization algorithm will have to search for the solution in a 900 dimensional space, which is computationally expensive and error-prone. Reducing the stars in groups effectively reduces the dimension of the parameter space, which facilitates the optimization process. Provided that the groups are available, the next step is to fit the sources simultaneously for each group. This task can be done using an astropy fitter, for instance, `~astropy.modeling.fitting.LevMarLSQFitter`. After sources are fitted, they are subtracted from the given image and, after fitting all sources, the residual image is analyzed by the finding routine again in order to check if there exist any source which has not been detected previously. This process goes on until no more sources are identified by the finding routine. .. note:: It is important to note the conventions on the column names of the input/output astropy Tables which are passed along to the source detection and photometry objects. For instance, all source detection objects should output a table with columns named as ``xcentroid`` and ``ycentroid`` (check `~photutils.detection`). On the other hand, `~photutils.psf.DAOGroup` expects columns named as ``x_0`` and ``y_0``, which represents the initial guesses on the sources' centroids. Finally, the output of the fitting process shows columns named as ``x_fit``, ``y_fit``, ``flux_fit`` for the optimum values and ``x_0``, ``y_0``, ``flux_0`` for the initial guesses. Although this convention implies that the columns have to be renamed along the process, it has the advantage of clarity so that one can keep track and easily differentiate where input/outputs came from. High-Level Structure ^^^^^^^^^^^^^^^^^^^^ Photutils provides three classes to perform PSF Photometry: `~photutils.psf.BasicPSFPhotometry`, `~photutils.psf.IterativelySubtractedPSFPhotometry`, and `~photutils.psf.DAOPhotPSFPhotometry`. Together these provide the core workflow to make photometric measurements given an appropriate PSF (or other) model. `~photutils.psf.BasicPSFPhotometry` implements the minimum tools for model-fitting photometry. At its core, this involves finding sources in an image, grouping overlapping sources into a single model, fitting the model to the sources, and subtracting the models from the image. In DAOPHOT parlance, this is essentially running the "FIND, GROUP, NSTAR, SUBTRACT" once. Because it is only a single cycle of that sequence, this class should be used when the degree of crowdedness of the field is not very high, for instance, when most stars are separated by a distance no less than one FWHM and their brightness are relatively uniform. It is critical to understand, though, that `~photutils.psf.BasicPSFPhotometry` does not actually contain the functionality to *do* all these steps - that is provided by other objects (or can be user-written) functions. Rather, it provides the framework and data structures in which these operations run. Because of this, `~photutils.psf.BasicPSFPhotometry` is particularly useful for build more complex workflows, as all the stages can be turned on or off or replaced with different implementations as the user desires. `~photutils.psf.IterativelySubtractedPSFPhotometry` is similar to `~photutils.psf.BasicPSFPhotometry`, but it adds a parameter called ``n_iters`` which is the number of iterations for which the loop "FIND, GROUP, NSTAR, SUBTRACT, FIND..." will be performed. This class enables photometry in a scenario where there exists significant overlap between stars that are of quite different brightness. For instance, the detection algorithm may not be able to detect a faint and bright star very close together in the first iteration, but they will be detected in the next iteration after the brighter stars have been fit and subtracted. Like `~photutils.psf.BasicPSFPhotometry`, it does not include implementations of the stages of this process, but it provides the structure in which those stages run. `~photutils.psf.DAOPhotPSFPhotometry` is a special case of `~photutils.psf.IterativelySubtractedPSFPhotometry`. Unlike `~photutils.psf.IterativelySubtractedPSFPhotometry` and `~photutils.psf.BasicPSFPhotometry`, the class includes specific implementations of the stages of the photometric measurements, tuned to reproduce the algorithms used for the DAOPHOT code. Specifically, the ``finder``, ``group_maker``, ``bkg_estimator`` attributes are set to the `~photutils.detection.DAOStarFinder`, `~photutils.psf.DAOGroup`, and `~photutils.background.MMMBackground`, respectively. Therefore, users need to input the parameters of those classes to set up a `~photutils.psf.DAOPhotPSFPhotometry` object, rather than providing objects to do these stages (which is what the other classes require). Those classes and all the classes they *use* for the steps in the photometry process can always be replaced by user-supplied functions if you wish to customize any stage of the photometry process. This makes the machinery very flexible, while still providing a "batteries included" approach with a default implementation that's suitable for many use cases. Basic Usage ^^^^^^^^^^^ The basic usage of, e.g., `~photutils.psf.IterativelySubtractedPSFPhotometry` is as follows: .. doctest-skip:: >>> # create an IterativelySubtractedPSFPhotometry object >>> from photutils.psf import IterativelySubtractedPSFPhotometry >>> my_photometry = IterativelySubtractedPSFPhotometry( ... finder=my_finder, group_maker=my_group_maker, ... bkg_estimator=my_bkg_estimator, psf_model=my_psf_model, ... fitter=my_fitter, niters=3, fitshape=(7, 7)) >>> # get photometry results >>> photometry_results = my_photometry(image=my_image) >>> # get residual image >>> residual_image = my_photometry.get_residual_image() Where ``my_finder``, ``my_group_maker``, and ``my_bkg_estimator`` may be any suitable class or callable function. This approach allows one to customize every part of the photometry process provided that their input/output are compatible with the input/output expected by `~photutils.psf.IterativelySubtractedPSFPhotometry`. `photutils.psf` provides all the necessary classes to reproduce the DAOPHOT algorithm, but any individual part of that algorithm can be swapped for a user-defined function. See the API documentation for precise details on what these classes or functions should look like. Performing PSF Photometry ^^^^^^^^^^^^^^^^^^^^^^^^^ Let's take a look at a simple example with simulated stars whose PSF is assumed to be Gaussian. First let's create an image with four overlapping stars:: >>> import numpy as np >>> from astropy.table import Table >>> from photutils.datasets import (make_noise_image, ... make_gaussian_sources_image) >>> sigma_psf = 2.0 >>> sources = Table() >>> sources['flux'] = [700, 800, 700, 800] >>> sources['x_mean'] = [12, 17, 12, 17] >>> sources['y_mean'] = [15, 15, 20, 20] >>> sources['x_stddev'] = sigma_psf * np.ones(4) >>> sources['y_stddev'] = sources['x_stddev'] >>> sources['theta'] = [0, 0, 0, 0] >>> sources['id'] = [1, 2, 3, 4] >>> tshape = (32, 32) >>> image = (make_gaussian_sources_image(tshape, sources) + ... make_noise_image(tshape, distribution='poisson', mean=6., ... seed=123) + ... make_noise_image(tshape, distribution='gaussian', mean=0., ... stddev=2., seed=123)) .. doctest-requires:: matplotlib >>> from matplotlib import rcParams >>> rcParams['font.size'] = 13 >>> import matplotlib.pyplot as plt >>> plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', ... origin='lower') # doctest: +SKIP >>> plt.title('Simulated data') # doctest: +SKIP >>> plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) # doctest: +SKIP .. plot:: from astropy.table import Table from matplotlib import rcParams import matplotlib.pyplot as plt import numpy as np from photutils.datasets import (make_noise_image, make_gaussian_sources_image) sigma_psf = 2.0 sources = Table() sources['flux'] = [700, 800, 700, 800] sources['x_mean'] = [12, 17, 12, 17] sources['y_mean'] = [15, 15, 20, 20] sources['x_stddev'] = sigma_psf * np.ones(4) sources['y_stddev'] = sources['x_stddev'] sources['theta'] = [0, 0, 0, 0] sources['id'] = [1, 2, 3, 4] tshape = (32, 32) image = (make_gaussian_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=123) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=123)) rcParams['font.size'] = 13 plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') plt.title('Simulated data') plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) Then let's import the required classes to set up a `~photutils.psf.IterativelySubtractedPSFPhotometry` object:: >>> from photutils.detection import IRAFStarFinder >>> from photutils.psf import IntegratedGaussianPRF, DAOGroup >>> from photutils.background import MMMBackground, MADStdBackgroundRMS >>> from astropy.modeling.fitting import LevMarLSQFitter >>> from astropy.stats import gaussian_sigma_to_fwhm Let's then instantiate and use the objects: .. doctest-requires:: scipy >>> bkgrms = MADStdBackgroundRMS() >>> std = bkgrms(image) >>> iraffind = IRAFStarFinder(threshold=3.5*std, ... fwhm=sigma_psf * gaussian_sigma_to_fwhm, ... minsep_fwhm=0.01, roundhi=5.0, roundlo=-5.0, ... sharplo=0.0, sharphi=2.0) >>> daogroup = DAOGroup(2.0 * sigma_psf * gaussian_sigma_to_fwhm) >>> mmm_bkg = MMMBackground() >>> fitter = LevMarLSQFitter() >>> psf_model = IntegratedGaussianPRF(sigma=sigma_psf) >>> from photutils.psf import IterativelySubtractedPSFPhotometry >>> photometry = IterativelySubtractedPSFPhotometry(finder=iraffind, ... group_maker=daogroup, ... bkg_estimator=mmm_bkg, ... psf_model=psf_model, ... fitter=LevMarLSQFitter(), ... niters=1, fitshape=(11, 11)) >>> result_tab = photometry(image=image) >>> residual_image = photometry.get_residual_image() Note that the parameters values for the finder class, i.e., `~photutils.detection.IRAFStarFinder`, are completely chosen in an arbitrary manner and optimum values do vary according to the data. As mentioned before, the way to actually do the photometry is by using ``photometry`` as a function-like call. It's worth noting that ``image`` does not need to be background subtracted. The subtraction is done during the photometry process with the attribute ``bkg`` that was used to set up ``photometry``. Now, let's compare the simulated and the residual images: .. doctest-skip:: >>> plt.subplot(1, 2, 1) >>> plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') >>> plt.title('Simulated data') >>> plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) >>> plt.subplot(1, 2, 2) >>> plt.imshow(residual_image, cmap='viridis', aspect=1, ... interpolation='nearest', origin='lower') >>> plt.title('Residual Image') >>> plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) >>> plt.show() .. plot:: from astropy.modeling.fitting import LevMarLSQFitter from astropy.stats import gaussian_sigma_to_fwhm from astropy.table import Table from matplotlib import rcParams import matplotlib.pyplot as plt import numpy as np from photutils.background import MMMBackground, MADStdBackgroundRMS from photutils.datasets import (make_noise_image, make_gaussian_sources_image) from photutils.detection import IRAFStarFinder from photutils.psf import IntegratedGaussianPRF, DAOGroup from photutils.psf import IterativelySubtractedPSFPhotometry sigma_psf = 2.0 sources = Table() sources['flux'] = [700, 800, 700, 800] sources['x_mean'] = [12, 17, 12, 17] sources['y_mean'] = [15, 15, 20, 20] sources['x_stddev'] = sigma_psf * np.ones(4) sources['y_stddev'] = sources['x_stddev'] sources['theta'] = [0, 0, 0, 0] sources['id'] = [1, 2, 3, 4] tshape = (32, 32) image = (make_gaussian_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=123) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=123)) bkgrms = MADStdBackgroundRMS() std = bkgrms(image) iraffind = IRAFStarFinder(threshold=3.5 * std, fwhm=sigma_psf * gaussian_sigma_to_fwhm, minsep_fwhm=0.01, roundhi=5.0, roundlo=-5.0, sharplo=0.0, sharphi=2.0) daogroup = DAOGroup(2.0 * sigma_psf * gaussian_sigma_to_fwhm) mmm_bkg = MMMBackground() psf_model = IntegratedGaussianPRF(sigma=sigma_psf) fitter = LevMarLSQFitter() photometry = IterativelySubtractedPSFPhotometry(finder=iraffind, group_maker=daogroup, bkg_estimator=mmm_bkg, psf_model=psf_model, fitter=LevMarLSQFitter(), niters=1, fitshape=(11, 11)) result_tab = photometry(image=image) residual_image = photometry.get_residual_image() rcParams['font.size'] = 13 plt.subplot(1, 2, 1) plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') plt.title('Simulated data') plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) plt.subplot(1, 2, 2) plt.imshow(residual_image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') plt.title('Residual Image') plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) plt.show() Performing PSF Photometry with Fixed Centroids ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In case that the centroids positions of the stars are known a priori, then they can be held fixed during the fitting process and the optimizer will only consider flux as a variable. To do that, one has to set the ``fixed`` attribute for the centroid parameters in ``psf`` as ``True``. Consider the previous example after the line ``psf_model = IntegratedGaussianPRF(sigma=sigma_psf)``: .. doctest-skip:: >>> psf_model.x_0.fixed = True >>> psf_model.y_0.fixed = True >>> pos = Table(names=['x_0', 'y_0'], data=[sources['x_mean'], ... sources['y_mean']]) .. doctest-skip:: >>> photometry = BasicPSFPhotometry(group_maker=daogroup, ... bkg_estimator=mmm_bkg, ... psf_model=psf_model, ... fitter=LevMarLSQFitter(), ... fitshape=(11, 11)) >>> result_tab = photometry(image=image, init_guesses=pos) >>> residual_image = photometry.get_residual_image() .. doctest-skip:: >>> plt.subplot(1, 2, 1) >>> plt.imshow(image, cmap='viridis', aspect=1, ... interpolation='nearest', origin='lower') >>> plt.title('Simulated data') >>> plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) >>> plt.subplot(1, 2, 2) >>> plt.imshow(residual_image, cmap='viridis', aspect=1, ... interpolation='nearest', origin='lower') >>> plt.title('Residual Image') >>> plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) .. plot:: from astropy.modeling.fitting import LevMarLSQFitter from astropy.stats import gaussian_sigma_to_fwhm from astropy.table import Table from matplotlib import rcParams import matplotlib.pyplot as plt import numpy as np from photutils.background import MMMBackground, MADStdBackgroundRMS from photutils.datasets import (make_noise_image, make_gaussian_sources_image) from photutils.psf import BasicPSFPhotometry from photutils.psf import IntegratedGaussianPRF, DAOGroup sigma_psf = 2.0 sources = Table() sources['flux'] = [700, 800, 700, 800] sources['x_mean'] = [12, 17, 12, 17] sources['y_mean'] = [15, 15, 20, 20] sources['x_stddev'] = sigma_psf * np.ones(4) sources['y_stddev'] = sources['x_stddev'] sources['theta'] = [0, 0, 0, 0] sources['id'] = [1, 2, 3, 4] tshape = (32, 32) image = (make_gaussian_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=123) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=123)) bkgrms = MADStdBackgroundRMS() std = bkgrms(image) daogroup = DAOGroup(2.0 * sigma_psf * gaussian_sigma_to_fwhm) mmm_bkg = MMMBackground() psf_model = IntegratedGaussianPRF(sigma=sigma_psf) psf_model.x_0.fixed = True psf_model.y_0.fixed = True pos = Table(names=['x_0', 'y_0'], data=[sources['x_mean'], sources['y_mean']]) fitter = LevMarLSQFitter() photometry = BasicPSFPhotometry(group_maker=daogroup, bkg_estimator=mmm_bkg, psf_model=psf_model, fitter=LevMarLSQFitter(), fitshape=(11, 11)) result_tab = photometry(image=image, init_guesses=pos) residual_image = photometry.get_residual_image() rcParams['font.size'] = 13 plt.subplot(1, 2, 1) plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') plt.title('Simulated data') plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) plt.subplot(1, 2, 2) plt.imshow(residual_image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower') plt.title('Residual Image') plt.colorbar(orientation='horizontal', fraction=0.046, pad=0.04) plt.show() Fitting additional parameters ----------------------------- The PSF photometry classes can also be used to fit more model parameters than just the flux and center positions. While a more realistic use case might be fitting sky backgrounds, or shape parameters of galaxies, here we use the ``sigma`` parameter in `~photutils.psf.IntegratedGaussianPRF` as the simplest possible example of this feature. (For actual PSF photometry of stars you would *not* want to do this, because the shape of the PSF should be set by bright stars or an optical model and held fixed when fitting.) First, let us instantiate a PSF model object: .. doctest-skip:: >>> gaussian_prf = IntegratedGaussianPRF() The attribute ``fixed`` for the ``sigma`` parameter is set to ``True`` by default, i.e., ``sigma`` is not considered during the fitting process. Let's first change this behavior: .. doctest-skip:: >>> gaussian_prf.sigma.fixed = False In addition, we need to indicate the initial guess which will be used in during the fitting process. By the default, the initial guess is taken as the default value of ``sigma``, but we can change that by doing: .. doctest-skip:: >>> gaussian_prf.sigma.value = 2.05 Now, let's create a simulated image which has a brighter star and one overlapping fainter companion so that the detection algorithm won't be able to identify it, and hence we should use `~photutils.psf.IterativelySubtractedPSFPhotometry` to measure the fainter star as well. Also, note that both of the stars have ``sigma=2.0``. .. plot:: :include-source: import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LogNorm from photutils.datasets import (make_noise_image, make_gaussian_sources_image) from astropy.table import Table sources = Table() sources['flux'] = [10000, 1000] sources['x_mean'] = [18, 9] sources['y_mean'] = [17, 21] sources['x_stddev'] = [2] * 2 sources['y_stddev'] = sources['x_stddev'] sources['theta'] = [0] * 2 tshape = (32, 32) image = (make_gaussian_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=123) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=123)) vmin, vmax = np.percentile(image, [5, 95]) plt.imshow(image, cmap='viridis', aspect=1, interpolation='nearest', origin='lower', norm=LogNorm(vmin=vmin, vmax=vmax)) Let's instantiate the necessary objects in order to use an `~photutils.psf.IterativelySubtractedPSFPhotometry` to perform photometry: .. doctest-requires:: scipy >>> daogroup = DAOGroup(crit_separation=8) >>> mmm_bkg = MMMBackground() >>> iraffind = IRAFStarFinder(threshold=2.5 * mmm_bkg(image), fwhm=4.5) >>> fitter = LevMarLSQFitter() >>> gaussian_prf = IntegratedGaussianPRF(sigma=2.05) >>> gaussian_prf.sigma.fixed = False >>> itr_phot_obj = IterativelySubtractedPSFPhotometry(finder=iraffind, ... group_maker=daogroup, ... bkg_estimator=mmm_bkg, ... psf_model=gaussian_prf, ... fitter=fitter, ... fitshape=(11, 11), ... niters=2) Now, let's use the callable ``itr_phot_obj`` to perform photometry: .. doctest-requires:: scipy >>> phot_results = itr_phot_obj(image) >>> phot_results['id', 'group_id', 'iter_detected', 'x_0', 'y_0', 'flux_0'] #doctest: +SKIP id group_id iter_detected x_0 y_0 flux_0 --- -------- ------------- ------------- ------------- ------------- 1 1 1 18.0045935148 17.0060558543 9437.07321281 1 1 2 9.06141447183 21.0680052846 977.163727416 >>> phot_results['sigma_0', 'sigma_fit', 'x_fit', 'y_fit', 'flux_fit'] #doctest: +SKIP sigma_0 sigma_fit x_fit y_fit flux_fit ------- ------------- ------------- ------------- ------------- 2.05 1.98092026939 17.9995106906 17.0039419384 10016.4470148 2.05 1.98516037471 9.12116345703 21.0599164498 1036.79115883 We can see that ``sigma_0`` (the initial guess for ``sigma``) was assigned to the value we used when creating the PSF model. Let's take a look at the residual image:: >>> plt.imshow(itr_phot_obj.get_residual_image(), cmap='viridis', ... aspect=1, interpolation='nearest', origin='lower') #doctest: +SKIP .. plot:: from photutils.datasets import (make_noise_image, make_gaussian_sources_image) import matplotlib.pyplot as plt from photutils.psf import IterativelySubtractedPSFPhotometry from astropy.table import Table from photutils.background import MMMBackground from photutils.psf import IntegratedGaussianPRF, DAOGroup from photutils.detection import IRAFStarFinder from astropy.modeling.fitting import LevMarLSQFitter sources = Table() sources['flux'] = [10000, 1000] sources['x_mean'] = [18, 9] sources['y_mean'] = [17, 21] sources['x_stddev'] = [2] * 2 sources['y_stddev'] = sources['x_stddev'] sources['theta'] = [0] * 2 tshape = (32, 32) image = (make_gaussian_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=123) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=123)) daogroup = DAOGroup(crit_separation=8) mmm_bkg = MMMBackground() psf_model = IntegratedGaussianPRF(sigma=2.05) iraffind = IRAFStarFinder(threshold=2.5 * mmm_bkg(image), fwhm=4.5) fitter = LevMarLSQFitter() psf_model.sigma.fixed = False itr_phot_obj = IterativelySubtractedPSFPhotometry( finder=iraffind, group_maker=daogroup, bkg_estimator=mmm_bkg, psf_model=psf_model, fitter=fitter, fitshape=(11, 11), niters=2) phot_results_itr = itr_phot_obj(image) plt.imshow(itr_phot_obj.get_residual_image(), cmap='viridis', aspect=1, interpolation='nearest', origin='lower') References ---------- `Spitzer PSF vs. PRF `_ `The Kepler Pixel Response Function `_ `Stetson, Astronomical Society of the Pacific, Publications, (ISSN 0004-6280), vol. 99, March 1987, p. 191-222. `_ `Anderson & King, Astronomical Society of the Pacific, Publications, Volume 112, Issue 776, pp. 1360-1382, Nov 2000 `_ Reference/API ------------- .. automodapi:: photutils.psf :no-heading: .. automodapi:: photutils.psf.sandbox ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_matching.rst0000644000214200020070000002205200000000000016323 0ustar00lbradley.. _psf_matching: PSF Matching (`photutils.psf.matching`) ======================================= Introduction ------------ This subpackage contains tools to generate kernels for matching point spread functions (PSFs). Matching PSFs ------------- Photutils provides a function called :func:`~photutils.psf.matching.create_matching_kernel` that generates a matching kernel between two PSFs using the ratio of Fourier transforms (see e.g., `Gordon et al. 2008`_; `Aniano et al. 2011`_). For this first simple example, let's assume our source and target PSFs are noiseless 2D Gaussians. The "high-resolution" PSF will be a Gaussian with :math:`\sigma=3`. The "low-resolution" PSF will be a Gaussian with :math:`\sigma=5`:: >>> import numpy as np >>> from astropy.modeling.models import Gaussian2D >>> y, x = np.mgrid[0:51, 0:51] >>> gm1 = Gaussian2D(100, 25, 25, 3, 3) >>> gm2 = Gaussian2D(100, 25, 25, 5, 5) >>> g1 = gm1(x, y) >>> g2 = gm2(x, y) >>> g1 /= g1.sum() >>> g2 /= g2.sum() For these 2D Gaussians, the matching kernel should be a 2D Gaussian with :math:`\sigma=4` (``sqrt(5**2 - 3**2)``). Let's create the matching kernel using a Fourier ratio method. Note that the input source and target PSFs must have the same shape and pixel scale:: >>> from photutils.psf import create_matching_kernel >>> kernel = create_matching_kernel(g1, g2) Let's plot the result: .. plot:: :include-source: import numpy as np from astropy.modeling.models import Gaussian2D from photutils.psf import create_matching_kernel import matplotlib.pyplot as plt y, x = np.mgrid[0:51, 0:51] gm1 = Gaussian2D(100, 25, 25, 3, 3) gm2 = Gaussian2D(100, 25, 25, 5, 5) g1 = gm1(x, y) g2 = gm2(x, y) g1 /= g1.sum() g2 /= g2.sum() kernel = create_matching_kernel(g1, g2) plt.imshow(kernel, cmap='Greys_r', origin='lower') plt.colorbar() We quickly observe that the result is not as expected. This is because of high-frequency noise in the Fourier transforms (even though these are noiseless PSFs, there is floating-point noise in the ratios). Using the Fourier ratio method, one must filter the high-frequency noise from the Fourier ratios. This is performed by inputing a `window function `_, which may be a function or a callable object. In general, the user will need to exercise some care when defining a window function. For more information, please see `Aniano et al. 2011`_. Photutils provides the following window classes: * `~photutils.psf.matching.HanningWindow` * `~photutils.psf.matching.TukeyWindow` * `~photutils.psf.matching.CosineBellWindow` * `~photutils.psf.matching.SplitCosineBellWindow` * `~photutils.psf.matching.TopHatWindow` Here are plots of 1D cuts across the center of the 2D window functions: .. plot:: :include-source: from photutils.psf import (HanningWindow, TukeyWindow, CosineBellWindow, SplitCosineBellWindow, TopHatWindow) import matplotlib.pyplot as plt w1 = HanningWindow() w2 = TukeyWindow(alpha=0.5) w3 = CosineBellWindow(alpha=0.5) w4 = SplitCosineBellWindow(alpha=0.4, beta=0.3) w5 = TopHatWindow(beta=0.4) shape = (101, 101) y0 = (shape[0] - 1) // 2 plt.figure() plt.subplots_adjust(wspace=0.4, hspace=0.4) plt.subplot(2, 3, 1) plt.plot(w1(shape)[y0, :]) plt.title('Hanning') plt.xlabel('x') plt.ylim((0, 1.1)) plt.subplot(2, 3, 2) plt.plot(w2(shape)[y0, :]) plt.title('Tukey') plt.xlabel('x') plt.ylim((0, 1.1)) plt.subplot(2, 3, 3) plt.plot(w3(shape)[y0, :]) plt.title('Cosine Bell') plt.xlabel('x') plt.ylim((0, 1.1)) plt.subplot(2, 3, 4) plt.plot(w4(shape)[y0, :]) plt.title('Split Cosine Bell') plt.xlabel('x') plt.ylim((0, 1.1)) plt.subplot(2, 3, 5) plt.plot(w5(shape)[y0, :], label='Top Hat') plt.title('Top Hat') plt.xlabel('x') plt.ylim((0, 1.1)) However, the user may input any function or callable object to generate a custom window function. In this example, because these are noiseless PSFs, we will use a `~photutils.psf.matching.TopHatWindow` object as the low-pass filter:: >>> from photutils.psf import TopHatWindow >>> window = TopHatWindow(0.35) >>> kernel = create_matching_kernel(g1, g2, window=window) Note that the output matching kernel from :func:`~photutils.psf.matching.create_matching_kernel` is always normalized such that the kernel array sums to 1:: >>> print(kernel.sum()) # doctest: +FLOAT_CMP 1.0 Let's display the new matching kernel: .. plot:: :include-source: import numpy as np from astropy.modeling.models import Gaussian2D from photutils.psf import create_matching_kernel, TopHatWindow import matplotlib.pyplot as plt y, x = np.mgrid[0:51, 0:51] gm1 = Gaussian2D(100, 25, 25, 3, 3) gm2 = Gaussian2D(100, 25, 25, 5, 5) g1 = gm1(x, y) g2 = gm2(x, y) g1 /= g1.sum() g2 /= g2.sum() window = TopHatWindow(0.35) kernel = create_matching_kernel(g1, g2, window=window) plt.imshow(kernel, cmap='Greys_r', origin='lower') plt.colorbar() As desired, the result is indeed a 2D Gaussian with a :math:`\sigma=4`. Here we will show 1D cuts across the center of the kernel images: .. plot:: :include-source: import numpy as np from astropy.modeling.models import Gaussian2D from photutils.psf import create_matching_kernel, TopHatWindow import matplotlib.pyplot as plt y, x = np.mgrid[0:51, 0:51] gm1 = Gaussian2D(100, 25, 25, 3, 3) gm2 = Gaussian2D(100, 25, 25, 5, 5) gm3 = Gaussian2D(100, 25, 25, 4, 4) g1 = gm1(x, y) g2 = gm2(x, y) g3 = gm3(x, y) g1 /= g1.sum() g2 /= g2.sum() g3 /= g3.sum() window = TopHatWindow(0.35) kernel = create_matching_kernel(g1, g2, window=window) kernel /= kernel.sum() plt.plot(kernel[25, :], label='Matching kernel') plt.plot(g3[25, :], label='$\\sigma=4$ Gaussian') plt.xlabel('x') plt.ylabel('Flux') plt.legend() plt.ylim((0.0, 0.011)) Matching IRAC PSFs ------------------ For this example, let's generate a matching kernel to go from the Spitzer/IRAC channel 1 (3.6 microns) PSF to the channel 4 (8.0 microns) PSF. We load the PSFs using the :func:`~photutils.datasets.load_irac_psf` convenience function:: >>> from photutils.datasets import load_irac_psf >>> ch1_hdu = load_irac_psf(channel=1) # doctest: +REMOTE_DATA >>> ch4_hdu = load_irac_psf(channel=4) # doctest: +REMOTE_DATA >>> ch1 = ch1_hdu.data # doctest: +REMOTE_DATA >>> ch4 = ch4_hdu.data # doctest: +REMOTE_DATA Let's display the images: .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.visualization import LogStretch from astropy.visualization.mpl_normalize import ImageNormalize from photutils.datasets import load_irac_psf ch1_hdu = load_irac_psf(channel=1) ch4_hdu = load_irac_psf(channel=4) ch1 = ch1_hdu.data ch4 = ch4_hdu.data norm = ImageNormalize(stretch=LogStretch()) plt.figure(figsize=(9, 4)) plt.subplot(1, 2, 1) plt.imshow(ch1, norm=norm, cmap='viridis', origin='lower') plt.title('IRAC channel 1 PSF') plt.subplot(1, 2, 2) plt.imshow(ch4, norm=norm, cmap='viridis', origin='lower') plt.title('IRAC channel 4 PSF') For this example, we will use the :class:`~photutils.psf.matching.CosineBellWindow` for the low-pass window. Note that these Spitzer/IRAC channel 1 and 4 PSFs have the same shape and pixel scale. If that is not the case, one can use the :func:`~photutils.psf.matching.resize_psf` convenience function to resize a PSF image. Typically, one would interpolate the lower-resolution PSF to the same size as the higher-resolution PSF. .. doctest-skip:: >>> from photutils.psf import CosineBellWindow, create_matching_kernel >>> window = CosineBellWindow(alpha=0.35) >>> kernel = create_matching_kernel(ch1, ch4, window=window) Let's display the matching kernel result: .. plot:: :include-source: import matplotlib.pyplot as plt from astropy.visualization import LogStretch from astropy.visualization.mpl_normalize import ImageNormalize from photutils.datasets import load_irac_psf from photutils.psf import CosineBellWindow, create_matching_kernel ch1_hdu = load_irac_psf(channel=1) ch4_hdu = load_irac_psf(channel=4) ch1 = ch1_hdu.data ch4 = ch4_hdu.data norm = ImageNormalize(stretch=LogStretch()) window = CosineBellWindow(alpha=0.35) kernel = create_matching_kernel(ch1, ch4, window=window) plt.imshow(kernel, norm=norm, cmap='viridis', origin='lower') plt.colorbar() plt.title('Matching kernel') The Spitzer/IRAC channel 1 image could then be convolved with this matching kernel to produce an image with the same resolution as the channel 4 image. Reference/API ------------- .. automodapi:: photutils.psf.matching :no-heading: .. _Gordon et al. 2008: https://ui.adsabs.harvard.edu/abs/2008ApJ...682..336G/abstract .. _Aniano et al. 2011: https://ui.adsabs.harvard.edu/abs/2011PASP..123.1218A/abstract ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9728599 photutils-1.3.0/docs/psf_spec/0000755000214200020070000000000000000000000014730 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/background_estimator.rst0000644000214200020070000000415200000000000021672 0ustar00lbradleyBackgroundEstimator =================== EJT: Existing code documented at https://photutils.readthedocs.io/en/stable/api/photutils.background.Back groundBase.html - while the ``__call__`` function has no docstring, the ``calc_background`` function is the actual block API. I'm providing this as an *example* block because it is heavily used in other parts of `photutils` and therefore probably should not be changed much unless absolutely necessary. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- data : array_like or `~numpy.ma.MaskedArray` The array for which to calculate the background value. axis : int or `None`, optional The array axis along which the background is calculated. If `None`, then the entire array is used. Returns ------- result : float or `~numpy.ma.MaskedArray` The calculated background value. If ``axis`` is `None` then a scalar will be returned, otherwise a `~numpy.ma.MaskedArray` will be returned. Methods ------- This block requires no methods beyond ``__call__()``. Example Usage ------------- A variety of implementations of this block already exist in ``photutils``. A canononical example is the mode estimation algorithm ``3 * median - 2 * mean``. This can be done on an array called ``image_data`` by using the block like so:: from photutils.background import ModeEstimatorBackground bkg_estimator = ModeEstimatorBackground() bkg_value = bkg_estimator(image_data) The median/mean parameter values can be adjusted as keyword arguments to the estimator object if desired:: tweaked_bkg_estimator = ModeEstimatorBackground(median_factor=3.2, mean_factor=1.8) new_bkg_value = tweaked_bkg_estimator(image_data) The estimator will also accept a sigma clipping object that automatically does sigma clipping before the background is subtracted, like so:: from astropy.stats import SigmaClip clipped_bkg_estimator = ModeEstimatorBackground(sigma_clip=SigmaClip(sigma=3.)) clipped_bkg_value = clipped_bkg_estimator(image_data) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1599103037.0 photutils-1.3.0/docs/psf_spec/block_diagram.png0000644000214200020070000163221700000000000020230 0ustar00lbradley‰PNG  IHDR°ÔÒgAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs  šœYiTXtXML:com.adobe.xmp 1 LÂ'Y@IDATxt½bÞ8²k;I÷yÿ'žÎÅZ@Irï¹´%‘U(Ô)JŸã$¿þúýÿþü‡ö럜èþöúëWFœþùõŸ?þüçׯ¢ü´Èb“ÜŸð ’J.‘ýúÏ?ªø ·tŒ«£tPðýk–ð ùo”áA‚^)•Ô^ia¼Ø7Ç¢ÑÅ>ú_ä÷áªij[ЩAéuAF¿‘L/dàL« ë*½èÓ§”WSk, €­7F²§¶AüùõÍ ãº V4âýEßü‚1úh½!Þ€rùM|ñÿƒØ ú²N×F\à%’âwò+–´ôó-ÏD” Ä‚ œÂõç¿â›BÀ€ÒÌ! ¬­2ɦz”ʉ‹Ö<b%³ð©«9ÞúÁ–Ößcº´Ùmal"ë:ª”›ƒ\³!Bj„ÊõÍ5­á·xAÁ¦• Û©sk†œ¾-DÎuÖµŒ%a€wYãÖÊŸ?Ü—eþ@m~‰;þÍë?ÿ cîPS-–µkƒ‹$kbÌÕTþŸýâŸq% •M>s"wé˜÷´?ò`4Þ ˜NH4aE±ÆÑfL¾ù¢ÿÖ½¨_‰ãÏïæçzJ°bS˜x—°~ ÝÖSÆ©Dm´#VZôŸÜ+qjèBï|xë‚8‰Lâþ‚æáÝG‚µ<ÁËeÆ1ŒPü2eH ˆkx‰µë„Xr´&Œ……ƒì1.c:kJƒ5ê¸Ø›¨ ÒˆácÔyF#¸Öž>qC:ÿò“ë-ÄäÄAiÝ/‰è¢j¿{{dámk1!Ã|0¥ã»W¡/oÎôyæ¾*"îâŸ/ô>ëÀÙ‡ž˜ý•>÷¬VÓŒÿ’ JÝ:Ô®AÞœD\P:±+×ßr0î-¾Ì÷Öfçÿ¹K¤bì‰ >ˆ…øÿ›®‚¯¡k'ñjÜ$øÎŸu_Î7ôÄ‘ÎkI}ZGç‹Ù¸r”«kàÓOn­x0ÆW+ÎFk]êGa¿¹\}W0÷>ò„a 0HØzfع#žã; «>¼Ñ´¥×ïÔ vß>Hæ+vÚô“®s3[bNŸºÔ‚ sµ¦kεòŸ>KÛ‹ÏWà½[qÑgj×Ac¦>ÄT_®iÌãƒL]v–”uƒƒSGÚu ›l†û¨F˜Ãgvä›qêV*Jݹ_ȱUxçáæ¤>¢‡â3¿%ˆÕÝšÂÕŠyÏàùŠïã{Þ§ã‡-à R6œ?M¤AHšÓ›”Ù‰5ièÿGMbµsLä|«¦€ô3°W5²>°¢ë 1Ñ·…5}­`ÌwS¢ëô¼¯Fƒ/Žœ±õ&r”~Æy«1?à'E4VMrŠŠÈAÜú8þµ‹7ââV~b@2‡FÝg "FJ(ù’ß“‡ xÁ¢ïÙ¾Lóh‡ ¾fé4ÖNëÅÙ¼Ž!ÁÕ.6ÖÞM!X\Ñ4†ô¬yÔh\?òú°' Låç,Rš’#)µUÃMæòx”%„G I’±*¼$wõ‹Ý~}“M“ç¤ wjLå/ulnÍÌÚn3ý1gÆ¥ÜãÈúòeÀ8æ„åô€‹GÓ‡oìâëØxÄu\"A‹ë¼´–Ì•Ïè6mÁ5ZÂ{í+«¿èðÛ‹¨c¢¬RÕƒs„M³ÀŒºŸ›ºÔ§‹0:4r:ùãP#É–¯‚›Od[–óÅ{…qåþÔ#÷/msE.Øøðå«.ØÈüÒc·Eebëòå”Çç§µRàËØ8 ` üàÂYÅv¼vá¤.ÃSs49ýb¯2_¢¥åœ1×çÞgxmë°µÂ0x_piÜëa3úÅÃuzmÜ¢qìÁ)µÍ…¨©²Uxæ90b6/°C Y:¬që=Lú¨âÉ/®š*-—Æp`Ç•n¼ÞË?2_®ÕŒ7Á ~t‘•?~£ßÑ'×IVr†—'¶€Í'ð‘YzÊZ4OQÁþ]­¡ë ½Ù´ð­u³”uتøœ€§O!ƒ‹ú¹pÊ>ž+Ÿóúð'ÖŸMʉê)ö}\™¤É‹©¯q¡ðøcŠ/{>CÒ×ÁïXf¥·H‘Á]U?|`ÞþgŒìƒxƆ´óšþ‚ÇÚC ,è¬Ò‡ÄÛÜ_õ¤¥ï…ëéÞKuò‡ûbôÆq+¡kI¥AQ³è<”bu¯å«¸Q’qzÎaä©AŸpÍçn½1ìý5|+’AÖ‘ó·5à 7xãÒ˜(8ðÌþM™3v¨ªä_¡ž8](±Y ÖLä—hÞ—¸iÒÌ+]íMf}µœˆ—|á9›*Ÿü´F×}~d½Dzk z£È®ò!̵cm¢R¤ë|k¡µâÚ§ý ¿ûTÈæ ñcö6jmE""û̇þ޸ȯsî°›sˆÛ𯸦ÁÇÑÌ8ÞW|^DMÌb¶Öy†8»r™×;u%Ä€ËÇaœœ0, -8Ã'zÂñ¸ ™Ù'X7ΨèQ’¢87É"¯%×܆ô àç°ØôÃeaË w{œYŽprpÞÍÊ8ßb«bð4ä|…÷Á‰"s3xïÛÜào|‚Â×htOÌíä\^%@çy¯5¡¨¾•n}Àü¦˜ª2âš·ÚRøËWúµ=fF´,‡M•z„ÐRl¾µ½ùŒ|˜m5È¢õ†­_ ±‰QĆ„½ÈcXü$æ¦ëc˜¼¬½…⩟ènaLj/€OMàh3· \ØÖñ0øæÆ!Z»‹Oâœp:Çoø‘½v]c‹3Ö_£Ç ¼1Ï®78˜Ê];°,¾~ÅÒyiœägúØCq*ìÍù¼ždãiïøõùj´vÈéä‘ dÜü¢[6é“…MÉñæ_ œr}Æè1ºµ ´DtÂ{­[èR£Õý°]Tß9äI/ ^XÚÐOqõtLLób‹þî3‚º¹cÙöe,¸¼Ýý‡Œm¦Ç.’‹ï h‘4Šœñ#Ååœ5¹Ö¨14NühqJlÇuC ŽRnlç…ü•µ"ÐJçz\©¢^Ó]k}î~Õ¢“·ü“U„FÉýY’ È8‚šóÕØŽÝ9t@èxRbx˜ûS=l#æ!?§%/u™óÌ01ä‰0{3 ¢`Þ=Oˆ\F÷¸¥³uR.çéäOŒ]ðvçgÔW³›ÃÎ?ÑŒñ<ë*6õÔ@æ5Ò[Ó‹ …÷DÈí¼Ô[˜œáãÛ9ø`Œ\¿à|[m.LT͹‡óŸJâ‡qÚùӿŠÚÏŸd®†/žJ!¦Û7¡.ÿƒè³â¼ ìÚbãk“2ÜÚc^|V Âèò€ªû½6S0ÏŠ+ý…–0„Ň!QOœ½º>èú‚:Y•Ã&ÞÓYŒ26~H~Ö^˜»ø#Çnd>©E¾ñìQ¿¬Ù"1~ è3¬(—2ˆ3òŠLhu¼o¸?.Š€†Ö½+tòawä1Z{Þûpz†áèOã'X<äÒ¹AÞ¡é;ÏqŸÐ×Àã^,ûÄx^–æu÷Ö BÔºV¬Aq÷§²®®©`Šm.˜®ˆVöî3:bp¿ÑÇ/¡¯n¸æ@†Yçù€ö믿þvš •öa ‚4ø˜$A•°¬€t±g$dP’¼Ÿ¤°C#…ØË" ƒX^0)x8á*Sôð|. 9ѧ=>pã΋…ÔL9Ú¨¿»VY8»ábÿPÙð]^}(Ÿ2þŸ  IRP{/®þ>]µ™óÙ¢ÎMª¿œLMôi˃HÛS…À·¡à/€b o„ %³S%H7Æxb£û¯ÊÀõXìlW/zÈ%ø6˜Î#bÖ ¹—•`Õ,›ÚØðkèÔ¯gN +õà¦ÌªQžqŠÏ¹UÙ5Q¶ž±mÄœ#ã…¬ÝÑ—¥¾À©ÑjÊÀ|c26°B¥WãŒ0F˜ë§>FnÐ[wÞØ5†ÙqAøzxú½ÿ:ü'óêž6®S•{ª›-ƒoeŽ¥ IëÞõ°±—ó!yr ÷7õñ×MHþÖyúÂðÕ{ ›yÆ©)_è±h^ô>säkÈñÔ¬–ÏUˆòý6¸`ëõ~z¥¯põÞ`À_LñÑ%üþ0 yý“Ó‹‹;7fùØÿˆp¾q’3º6|G·5/vºF“:ñkT€þ˜ÚHL`DÛ}ñFøÆÁ ×1Ç­ã°¹ø§@ÍùtšCÒØ{?hÕ÷Ô|sÖµ£“ƒüPD¿côÇÛ5ñO°•1Æ?6œÞXZÕáàáaÆõ 8ßsÆT0‡D@÷:ðÍÝBÜã¯:³ˆ¼_@Às\KDw|_ßy.òlª‘f±· ¨d­SŸ§¤L†ôùqÆ2Ïñ=C}¾úâ  û#Hð×°»[µæ#?@®ðd6l}&à398EÄÚÅöYÚ¹7ÂÈñ×û#£®ÁÕ4ÏN~]¯˜Ø¦Y,á]ŽÝ7(óƒ5Ã7RÀ~×Ý×'ÜøçÐpý\^ƒh™ÕâÜ'ÿÄþÛbás½s‰=Ïï›@­ÝããÿÚ¾ñÀy•O¤É½žÏ?רC‘n™\…#¯bꂾ¬x,MWøí“ÇÃXu.ˆkŽÅÆؘd¾¹ þܯÏÂ{Ù¥•¾²ú„-¾S½"ye˜b×ýâÖ’C£ëØ8Q²·?kqælÚïZ®-ÌícúõHÙß\OÎ<¦Ä!5¡öÒ¨øèƒ[óùÈ:¯€Dû'õ{K êè3DÒÆ{Nc®<ýÌEg4(ÈhÎ#PßFÀŠ:Æfˆ‹á¬qÀb‘â:í`œG G@]ÇQ;Ut#~4và;ˆîÈï]ª©Ñý{F!èbáÜÀàäë‰Ø.Îre«#ëë³üïBÍĶc°sKX2U%Æþd¨?=̵êĶ#Œˆ5?³ Bq‰Ï¡«œ4ŠŽk‚ñ…ÆÛIÿ!6î<4y^àçñuôIþÑ¥ªø„㸠}¶øa ÒïËÖi{­mW’7dâýã‡(ƒ5³/øóõÇ_V ¬¾HJ®ËHôXÆÄB~¨ÑÑöò¿1Ò® rŠM'0¨‚æâLïx¤™ºµš%cë¡!wN-QM¢ ®œ#4ÿ“_x(o]¹×ÅCö…¸æœÑI{P=MH|3ÏzXÌÓþðýžåh/ø›îÔEä©;x *JNõž‘…¼¹Æ ¿[ ÷k‡T»œû“$K¬Î€Bî7»ôäâºPÅÌ·Gäϯ‰RoýC‰WâèÜ%{%Çr!:¯Mlz8ÆiË%pÄRúÕäœ5à™›û¯IìëóâºGÿ~ˆÆ9½úò#iy©WÆÁ­þgY>³±ˆú–f_ŠÕÕsù·¾?ï³³ƒ}¡­±’ÄóaEÆDLk}è5{òV·DZë¡QJùd×*Ë ˜véÌd3 \Õ)¸_A¤Í^ºÖjúä!f8„‡Ÿ‚‹µØš¶ÜÂ8Fά´ŒýF>c!ñ!É1Gè8'Éj W[õÚ(¨1ç†'iû¡@—£?”ù£@”—8éÞ5ÝWï%\C|ýUšsnÛç‡ ©ùW{1×LjŽ0fÅržÕSo>ê5Õbx‰6XinÝ !0„p€k'o‚[É/Ÿks‹÷ èôëŸAðÎÕ#£q¼º»ŸZD¨ |7ã{n½¼e&xç*}×–ñØk8%„4yqMô,‚ËÇ­âKv¯EºA]àÊ¢êÔC:| röei¾¹36Ž¥Y3‚¦²Ûšéì˜Î³ã_×àEW£h\ÓGؾ™Es ÖO¿gÿ^Õ¥bäæ .®BéwwK‡%fCˆ(˜ËJù¡He*4Îý½ÍÃóƒ–¾çà»¶w¿¸Ø³9~ýõ÷ÿ³$ù±`R·$BÄÀæT€di<ʰ#Ù4Æ_}í‘þ“/’ë„5€³À(ö’å4¥\¨òÕ?ÂÂÇù!ÖoË(ßÚP·)¯®¢M Þ! }LùïWp)‹È?"®.à^G½ñ³PÂÝ~¸ÈßÄp vŸúþüW¯½Ïô¨Wë!›+ñqø—È"îϽá6KÆ"n~¼B”Ný:?ä E¹IbA?¹Ö?løBd‹ô3R—Z¹À2 Ž|uÝž\ö¹}u¿¿÷Ç´ˆÛ.ËcçèÉû‚âš·¬,õ“=wcƒ,öGUbË×ýt«ó…ÍÝ™äðö‹£–äïæ£cû¾Ú"üžo®ÅU†}u]×VHfŸ™ËJ1ÛyJ'-ã89«RgD<˜ª ¬E)^¢häýØ%–Æ|s6†G~qq­=ðž`­ñr‘7ñÏD,±àĘª¸°ñØ%˜ëõ«·ÏËeü{oµýïß|XÀ3ñs1šù¼Üê•Ýý»¯`¶Ð´ç”˜åÄܶXsaox^g‰¥E JëîžðØF›Zé&°'^ç¸&µOìÆ¨”·¾¼K¬#ø[o\u߸s.©!-—JœŸšÆÜºS¿« îÇ!qq ªþG—Ë”û‹¯pC{ÖÔ§ëo÷ùbª=^‚ln¾Ïד¤ŠÃ>\ºcÔƒlôKïÙ›Ìì˜3?MN“—SãeA©:§úAM›[ι ‹¦ŠÔ@Â`×$ŠÎ_ûè½}¸\ãÍ€Ø9·ÁCìüÅ]î-tĶÆþÎÚ ;»|qçãÕ±|ôÌ\ÏHÂëý䲸£»/aWÛ®ãÖ¼ëk4?ÖþœŸèú, D G-æépï6f„Ì-WâFs¨ål;rÏö%.Êþþ‹Ê +sÉXˆæ$ÿžÿÌ»5®~û—UÉoLÇ=‹ýu ò4’rÒížA¶<#™"oìîÍAðU²êˆÓA†Ø×ø"YÁX“H¨€5Ÿ—<ïI£ãBÎ`YÇÆ¦¬ñiç:?ÿ#ô‚ÏXâvãÎ-s½<û`Ö¢"ZïM:E}ZãÌÍÿŠ@Îܯš9 ar-á+Rþâ5yÅÇkßšYkãD >pða[›\¶–„ä´ªGÁŸzqnÝbc øcŽñxñÌÖçMí9ÿâ_g‚1 Of^X qÏ¢‡¼þ´©´ú42c)^ZƒåÝ· ±\÷@ëšý|`ÙÇs‹Œrö 3Зˉ Q¢r€á_/Ú#(ñ[»NA”Ê"§Kü› â:‘hÚÙeaêéÙh¦CH`Èá"Èû›Ú=¿Ó×$FϦ œè±M<ò#¤…$€šÀ‹¼5oŇIž vú•sÊ-¸´õWƒ”.]6B«ÉÚÐ?ú´¸«o⃻ip<Òu"-NÀq=<,d8‡ÃU8_Å"gùâÿ6kQÖ¥<ü@ßÞ•¦¤òÕ!ÜmõšƒQŽ|˯ÈÁ ¦wñ¢jAb G®nöŒ9ž9Ôb[—6ƒÔ¯“4]ó‚¶îáI4À\NDdÝáú%둇òþÊ px‘àž’Q6ê!Ï37Ìe8pESÙný Ma]LQ®â*ÝŠ=`ìÇI»1²†®MùÁá™ë¼ì¹>À”r ÏEmNÍIÖ õ ×nî†sAЩG7úØ|Ã;|F OƬwC.¼uÎ-ZâiGúì[ðœ£ô/ð=8଱§Ï I® #‚5æžnuªµIoØæ茗3ãóu?ÔÀüCÉ) 3º·vX!¨ÿ\kã›A0]³?ÿ¹sõ'kÆÖŸ„ÿ²5i=ÐÙM|Ö¨b9ë1œ·†1E†§Ìs Ûƒ Œ¹œì}Ì4ØÊÀ¦i{Ý›Æø©çÎåõÕø'þ0pÀ‡ª«{¹%¨­.hmÎ/÷ª10/å©öP5¡hY±À‚³C¼­C}RÕ‘ä»z´Œoývè³N-‹ÉyLåÁ4j+_Ð÷U$¶Ê¼ùT^vúi»Jƒë¥ 8ºþL÷8ØXçVÁíw°½­ÑáêòÒln1Ýß8@ppÝÜ¡BT¹f€_ïS+(Zš–;…T‘¸ùKèðé¡éb±ñ¥Öزç}xdÌe2/L!Ò'9ïüÎSxà´ØDY#5.Öw}#ŸeŒxOìªe c¤À~zêäWw¶Å¡¯ ò5Ìc`-´) Úæ\pß\ŒÄNí^¾²ƒ.öô¬ˆXÆÎ)yÇg³šoÌñNK’‘ÿ‚¢uùĤ<§p±O½s2?úŒ>ìÚ3N'"ãÉžŒöâ„ë4Y>6Ùjì‹#/xùâÓ÷o  1H¥ÕwmÃuSUíÃë-ä=À´Í©iÄ—R ÉûADz-u“ô(¢wÂè>Àôaè¡ #L!ד!ÏQ׎ÔkÔA£ŒžE,¨Áü¯ò€h Þ …?ù7úÄáÆíC–ú#ËÁ¯ç„¿‘äì†úJtNÎòr*’« 2"kû¼T”èi\•²hÍ£òI3 63Zÿ•Gƒ½¸J±rÃdXÑôÝ4ð‰oU9ñ®F*]ôü‘å= eM¡„aÃ]ü‰‡4ØîQs”:[s9"£É Z ÏΙó)@hc~ðµ`ÈKµ¼ôÅqÚ ·ú<ÿá] K)ÚØ—–È5ޱ}g ñõ±—@¡­9f æzº)/²þU9Ê:0qk@pt€w˜<#ë·2p0•­(à æ<†²Ôÿc1ü^£³0•G¯g°»£œüS·ÈÏF‘úPÈš:]}hÔŸ=¬¾ÔÌ~,w›Ë ຘbë¹óòðá'ˆ*:ïÑzæ??šìJj¦=ëg~˜3–î8ˆ¡{uíå|ó™AÌ_‹`Zá‘3jéð4ª]g²õ¸ÖG‘X¬9bÒæ {·÷²Aaƒ-ñøZ?—ÌSºÚwüî<˜0ÄÌ¿÷%l¥­Ác݈Øú(IÝDFì矘Ÿæ`<ÆE ßËq´\ùAUs8¢d¸ ñ™„xˆzPàÕçÄøêê¾B0Óê§{¥?Ö#¨'ˆÔP{™añ «Ç³  ãEP»œáã½â âTÑ$ˆÛÓ¡ûÞ÷T‚xE}“G·Â5û! ƒÃÓêæ"Šhï:hÍï°šÖ¾5Ë>Ò,LZçuM†ú'¶Y\ ˆ…óÓ ¨mýÉ9cŒ2ïr~lŠÏkÙ2€؃¼h4j-uhœ EDbtø•ãMÂÉÍú¨‘œƒED4 æØ»#¸ûPƒ†-Ã{ TæºçÞ oye Žq9êmƒ£ø©YûÚTrçp…´£˜§sÉœ$–oÆ —ÿ³V < ¹‚‘ãáÝŠ1&i ’S¾ÃGBkÎaÀ>óå?¹¬ù±rÛp3ŽÇŸf˜ÚàóOÿþû¯Ô6#ULZ^Õd†ßeÊ@¹Ïm[·TÇ&ƒaÈÑD°LŸ$éD`UþÖ NŽøÁ•B$ ÓÊ)~ÂgTUÓœð›ËõÁ¢ÂÖ¯é ó­3W|ÝOHu%+ŠlÊòBÇ !¾¹®¡l °¦‰@]-9;TÙøÈµ9†‹Zùqíë `î(×ÇCÒ—0¢«Iì#{>Í»hBa+ú¾xß“WoDXôô¯7ø:]ü§K÷¡(ÝŸ-úXì \¸­a.ûK.è%ÁJbÄ…ý\‚çËøpf‹ópú .ŸŸ« %ÈÌ%~›#]ð`±Úš‘œªèd' »v"Èã_?yvc2?H¨ck2þúñùŽ‹{yðBêŸÊ`¨‹l#‹S '€Ë¿øOŒ±BEHÆ ƒ²—|çúõ‚~ñC='ëž üÌ)9†g|3Ïĸ—k©‰DÑu~\¯0%g/>Àʹ­^*('~i®Š`‰k2/Ø;ræóxr½?"¢ M*W jÜqFý®2òÜÐn°¨»'E3mØ*àXž¸7—ñËg®©ø1ÙÅ”^²kô™·´pí=ÁË5µéÈb~ ãÆÌùCã\¾óA-çGÕaÊõ#Dûa©×_:ôY'šªˆ¬<–è(ï^,‚ÅОdÁ»JÌ+©4ˆºÖÁ4ìzïó¼¹X«³O­Yle– +L7WÄÍßGë‡>“¢7·Dãßthµm¿çBs=ôÍ%ßÆÖ—à§æL’æ O1Ïd¸¹DuAÐ?ÜÍfüÚ÷ÝÜ£¿á€áòÔjü.§èyecìÿã2Æ`Qðf -\(é=u`ýЊ¿:ûÞÉwxt]±}×e™Ñ›ßbÌ„ëó5GA$Ä,¸ó–.¡4 uÒ,õóÕT#bE–1ä‹ob÷R|_Æ'dÕwøW¨`è^'ˆ æúà¹eôøÑ¡' ê@Ÿu›Ëê­.rUÆ„.Ü8'\ e DÞèë Pl­ü´àøbÞÍ>ŽŸqp(ÏVºœ¾¿iç¾ êøµÎpn­Õd^ºXñ@¦óÁyÿ™/sôÞqè{^¬ƒ1öhë«sH†±Ì;zqµT‘¨âºÖ½®‰ôïGFA½Î=œ<ÛÎáà’Vþ‰Õ{Œ<8àl×ú/7c’wý¦Ÿ«pñÓZýùÏß}ÖÁ„¢’‡ #"n&§Å+IKÓI´6ÌÅè‡ßú1×à ¸ nÊ$A·"-9 áÖ»…¥â*äÆ>L'Üù®Rã5úžôŨ(º™Èdƒˆ9„Ó¼RõÍûâB¨¿èú2 c0pÐ"oêãÚ¦àæ:Ìiê87Ÿ‚ñ.¨’=*É·!ÎÌqófxþÅ!OÄQtS@¸ "|o2Œê„óK'íì^-ö!´8‘Z<‘ú&ÔÑãŠÁÓ£µã >_]§¬Åæ"÷—°®á£ÝƒRΚ#4yš³žïDqÂç:“¼,u+¨öà|C; 8¬²?ëýµ»0(ν`ž`©Ozé/§ÒƒO“:‡pÒ÷윪ЀÓl‚Q”a-*ïYM°w…®5Á]È«GzÛÀðJ©:¯ÑKÈÉŽ¦·V oTé/üjšÜF;‘e|¦ôÖEYPŽw!á‰%Añ@7>ãÙ“TMµ_•Iƒ4F„‡ÙAáé(’ :»Í™üá)fv‹+‰¼ÂFlÝ[í›”® aÈ’/h£¦£Oñÿ-z|èÂ>uL§Pα®™×VÅPôyªrb ÏùbȟЕSƒÇ÷å|µ?¿A^» ¤ãtŠ9uŽörÔDŒÛÅ"vq„À´¹#ƒ/šÔÔ~†Wï»h¯u/YíνáÜ[ú>¢øç›Ó^Ä‚”3`ëh‚W&É뾀͢ùYü(îeâ?yœ-…dng*3Y×OcAÈPŽ½è—¯qM½ú’ܵ6š*!Y;å0—?Ÿá½//ÀY³ÿêË$O|r}ò¢ïFRëw×Ú³úØÅ÷û+x¼à©IìRJFé+‹M¢ïÇ,vÈT!¤#‚žØãétFʼ1WðÕ¶â%¼9bܹ•U82¬Îöí!Æõx–2|>û sã*ÊãÆ¦É„Å×=[,ö9/‚Ê·/îšS7¹¨0>Už¬vab«y÷Ɖ¢,•4L4]sœŸ$è#äÄÁ3~þÓ³Ôhœ_BCœbJÇÚD<ÿÌ%M¤ë¸cϩں<üMÆ|—ºÝ_Ï|óŒ²ï?Rí‡ÁþŸúkÓU£»³!¯æŠÞ_ œ°w=¦C,@9^mïÆ Å1Z®Â'ú&É@$éâÂ%w’N9}šÜŒÇw¡uñÔþ€}œ!èñØ"¹*Z°w±é.p“΀±ÜÁÁâX$J¸A> J…$OcŸcrJÄUÙ76ˆËtóî¸xûû©EÃîbÔ,–Ïž >qp:_Ÿž  ¸8>6±o­Î2œÚAŽÐÑ/g—5M½äF/”v4¢*8ƒ•“:Ó—³q¾@pƒ¬ÃÚ0þü¥VÌú"',TÌG­¨:ýénŠ+%®÷…EñðÔgc “>ä®ñ‚™]=¶6mùa7æ`[ËXYïØ†Ây$×îK¹³sŸ+}Ý:i•%?Ɉ-ðo#[ô½ÏŠ«0|´3Ì(A#{ëÙ"ê9ñ³úò\KlÉŸË’SKþî­yˆÎ{àLñs®Å‚Go(( üYóì[5Ë™ÚäKS„`ÇI,ïZ¥}š?‰µóîÛ2<¼Â<¹Lµ“ÜÞ‡j}×ÎãoFddþÚBˆˆú²€_ýŸXSÛüâŸxU—\“ ×Ìd­Á¸Í}<ËX} g±gžrÈ8Ö€ÁÍ·Ü¥.gôÌ´=Ï(ý“HŸuôášV®Ç¨šp 1îÿ9@,Ý?síž.UpÌ5}öœðû,;øütÞyÜzw"'~ ;r‰ˆ*Ûa`>ÍYaÀAs`eL™aã­œüä¬Ä·•½¾©u¿ZÕpòzßÈk¨9Eš?½ê/Ò?û,6‰É_£¡ÆGa5†Øè¶ÜÇŠµPìuК|cCký+8h÷E4|ñý¶ãQÝj­iÙÁv-×ôî½ËÝqƒ-k>NâŸÿ˜É†O0RŽWZ°œ‘SçúQÓ¤ÓE×Ö}5ñ":0×`Šj.ž#x,媺îÉ×%å.˜HV-ÔÑ»G…ƒ5KÄú…«þ=+dŽQ>Þ7o ºç>{dðÆ&±v?L¿ êáò^‡&8ËœÞÏ÷™ã"]"`޼ÚÐïâ¬ݧ‘{ g€üô7%åîà`^˜ £{¶sËü¶vU®ŽZ†ƒ®ý±øwuǽÇn}Ÿ4ÜÓ]GŒÒÿû¯¿ÌÆê$” ¸ 9Q1+h74^Ò 4rð¬›ôï?ðP§ Ü`jjBÚ‘(7×Z8˜`ì f3t*b®…é"(§â;•zT¦/!МFüˆG§æÒ'À|ÓGùü±;þúÓ¯>ìЧ‰ãd'&8¹xÕ)ÑX&qµ+*84ß`Ì™+qQƒÖèñ£ÑñSK0qFlЙ¶]žð¶råìÿ\Éùæζ–%xEèënù‚§G†KôU’u<._øŸÜËýV§KlÏ }Zó"¾Îƒrsrû0…Óº1ìEŒ-íÆTcµÇóQ6¦~ s|¹.#"þôM†ùb ¬Ù@vt2®ÿjj¨Â ‚b{òØij`qotÑ£2ŽüØ=ýß@œkDô¹æŸë<¶Þíë¤ yöeÀ|ç}}fصŸp¥« s±Põ§.þl6±±IÐ|Ñ sþ Þ\87.h9“#÷ }¾R·>Èɉ>ÄÈ›O_&æô©iÕá9ì­ÇÖ¦«¯ëÈwO‡§ïÙ0> ñoDÚ8A¸_ J·Éô‚Z€ç×~Cý’¡Yö‚‘DFì:æO(†(69>“$þ†pÝ!Lm"b«<ìd 7phüê“ ÷WØ` vù¨$†®q¨ ‡—ø±¶úƒBÊ|çpµöµ™­rXÁ±.ƒçz¾t\D¨‚AjYæs ÔN{p¢bó®‘“ dÔC˜§â ㉅5‡Ç̯¹ψ0­U¢6\™ÒgÀÓöììwW[÷ÎÕõ€uj¬ˆJâ«©úÕèG~Ì;Üá‹ÛÖ§1—šà8h—ËÆäo¨]Ý®³Kïѱ'Q­ÃÂ=†éç[†dEGÞp8&¨A Pváº\:™±¥èý|µ¬ylŽèºÂ£µNÄ)yÆhÔ?eP®*'e—O ÆKžÈÖ䉭æp!ïóÙºøG 9¶¯}þe®N¢1±gT¾_Í»Ž€"fÝaÙ{P`„³·v³Ãÿjyëûbû]§ÌKãJvøiçùG™ï›:‹ŸüKŽ«_‰ËÇ.ï¾ 9|ìññ™¾at²FX‚s?`Ì^ˆ¾ç~ÈŸ\|à¨mO'£*y¶¼ï°Ìü½gÏæZv†®6—c÷¯0zoÛ4ÈÓêµdA­«j<Ñ@΄ühA!ñ_É9È/ªK8âš·¸ËË$0176ž‘r´/¹£[#Ñ·­>7€ñ ¬åØ_³9¹¢€ÖûFÔ&îs£ˆ=ß¡¥ûzó×(ÂùãŸôÜÆ[dôU}x.rÆŸ0ä ]¤÷ÂôÉÝÎ| °…àÉ»ñë2²Þ¼ŒÞ ~茡¡Ðå&hm¦0žÄåGïÆÕ 8f³ Ûâó=~ëÿ™â]RD܇pùc¬ËÝ$´ý .¬¹ ›‹µãwô.VB4LnŠÅë_y!ç»Æ$>r8ž‰ÀœÍè+&æÞßJ&ËΛããÖ9y7»ÿ“VÝU¼°ž5禳ÊnÓ2XB–7œ·>tð,‡æVœž>µ÷AE¯7¸Ð­Ì¸ÀÒ¹C…8-°û¶óŒ¾ÄЭµƒ½7 Wv—Xô7—©èâ:ë ZéræÂfý©‹1sa:!x6R=¼Ùå‡N%œØ¾\oÿêЈ¶Õy%œ/ÙãèG‹‚^ݧ(3µ*íŸëpY!Íè_K/‰¢XÆä„þ‹ªð掼~6«_ÌnA{ÈñC.½7ë£&ø‚!à<ähÙkŽ,$d †aåë‰7…‡§ë$,çDAýC®2úR’Q#ˆÒÏæÄpæç·ia€–¶¼o ôœ+«Å#ërqzEáwãañd¨ S;JP?ò{cÛ´p`ƒ¾upW^{U€œ+¡V4Q˜“Iƒv=Ç­´X"Ê®†Ì‡F«'ö‘“ ÚbWLµß>Ar;2y¢y[-N–Qêà‹Ù¿¨Z‘ØM®…5k<Ä„ªuüò¸_±9‘/­%ÑÚ|ÈQ²»~캖©ÓìάL/òÜ"cûé¿ùh@ðSƒÎ¹¡ÆæîµôÆÉ¥AôCï3»sŒ”Õ3pîâ·ËªSÏÔÞ{ŽÎ‡æ³Õ]ù´v¯Œ\Çfdv¹Œ ISî³¢ý*Û7òXeWž_lŽçáÒ´GLäý|HÒv™ó°k¶ª©Iddþ ] VF‰MÁ± _^0|–`ÁZ7´³«¿sùnl¯‡Å´ýî ¡³y¨*F«³J0èbÁï© ÇéýÅŸ<- t1¸Oi,p:Ù"¹E’.ÉûˆüSTGZ·@×NÒŒ èãzo¼Sû"¨"~†£À‚#Ç õS 8þO“ ¸>\úI ß½I$ÛËwù"Q<Óc‚?¤ÐÃGHl¢r×'ÂóCI'B9 ló £tkÆâ¨È¤äIÝ‘ÖÉiÕÝ óÝZv~RA`_î F3—N#¤œ¯[ÈÆWʰ»Ú§fqlÃU7#ú­Á7ëÆ©EEVcˆœH­9Å£‡Œ®±fÍe^º©‡¦õäªóXaÇŸÀ¤ak¸õS. UÐ_0|J·¯Ą‡k¢ñEMsBs¶)W’¾\sŒŒ|evS­ÓÁ‰~aòYîSD×OôÜJûÃÌ´;­ ãyêzú"#ahlgúèŸ\¿ûÂûDHt¬ÇáhÒgͨ²LZKpmT“g–ƒÑåTéÆ RƒÖ8óÀó'- hÆ”+$ÈòMWí ò»‡)ϺjpE§Ü{=WFcâ¯;$¬Ct¬ îô¼Ý»^Ç‡Žµ0­^Åé`÷`ú½on­é(|± öÁo/¨7<¦©$¸té¿­+;ùpfáðŸ rïT~¸¯ $PÅ—ÝþeDdîÑ}ãÒ{õSsÐ;¨(ò%7ßÍ—GPm½Gü‰ZŠ~¨ÓrýîU®Kçýò¹ß•-{íâ{jrøM½:‰ª©.sáþ_M‡¸Ñ1bN!éüK|÷Ɇ%Ó·‘3íbЇÅPÔÉ‹ê rŽ·-žc-.×øt]Ý©*ÙñÕý÷üÄ“ÉPÍ51çP… ù—ï—Úú4¦Æ·ÂhãÚ›}k ¦8âò'¬„æ=ú_ç‡øµÄƾÚs#º®FÆë‹î÷á6^– õa«_=g=ê×_x 7¥AÑL¼´b°¼=²$ßý]íØáçÌy/pÏ$^î©ãÏ50Fþ‰{üö¹ùY_øä~Ã~ °ePjN·®"£Vƒë»ŒµÌ©qÇ—}HÐÐ0z 5Pc=+×çâ÷•Øšƒâ ¦° ¥&TUòáªÂ£OCöƯ‚œÅ'N7úŽÏºqPÏÁð@ýrÅomo*…·Y\.åìgÍ’Å^Šá£Ÿ¯>ª…ã7âdžƒ:€úòGcmªo$Ïx¹gÐcÛð“‡TЭÝt1%rÜÉ 61§ÿý-è¬@ˆ¼‡½·F9ÁòŸ†‰ºs8þúëoc¡àÎÅHÏÀHQ”H‹¦¬ü9Ç&x*¸hsÚ2äæ³è`úg•],ý̦=ÖrÊý¸› ±ê¤`Òø ¦Úsé6:³ï2ÀfCÌÌ€B?¤ær‹ >üW ôÆKåëMûØ:Ý ¬±½ö¼tû¯Ü¬”0Ò1ñ'†ÜOLÄ'cs)º^Ûçœ1ްå%S§àÃ7Ðñ;lPoœxˆŒz‘ëÄ’Œ.‡cÂ'.´h•à8Fb‚'¢ÈhÁ”Ѫ5ï@ÚÄÔTõZ¼h,ÍËßaëÏ4 óÿV“Ú\LŹl‰¼~ †ï®S¨NJÄ“/éÑ©ߨúëpÑ‹ÐÓ“ Ãú°gíÇ"itñ=1¼a‘ÆÙˆ”´Çùâcθ—iÚd‘»«>ý¹Ÿ ¾˜_Èr: rÙ«…ôœâ›=cÆ‹Àˆ›’o|òP‘6'޹"K\}ˆê!'äñ‘K:pZ› ‘hï¿(uñïÅñMD`ï÷Z;‘ò:ƒËJMqÍW„ý=ÓŒ§ÅϹ >0P#¹´¯†5Öâçá,ùhA6©PNl±¥B†ó\°§u Fo§†ð3šHpö‘1¾õ[Ô´Í™†ö+‘tàËÎ †@÷ÁbÉÖcÏ4îœ5Gþ¯¶˜}f,®—ÿðøJ I ŸÌà/·6?–5¢&'üÞ¬S8±^:Žê¹Pož*"Éñ©]»Èž(è#¦¦´œéîêB,Udã·àâ,ö´gÓvAvŠè™dâaœcÜôè#æt)ˆpÍsòŠ,ç‡-øÿÙÊG­KjZÇÓõD­Ъt~6'—×ãóp\ñÏ9±«ß0쟣éÙ+iÕ9DÓ¥T™û©M­Xj²µòÒ•ÇÐŽsJ\3ûeíþ4eƒíе¹¹7šðÕ¼Øàº>[ÓÚ _€·nç'ý2|'5Âb5Õ#OjÈY# º¤(šx4ËÅõ]Oš¬Nð ~üaSÛï’Stm/<Ѥ¶`ËCL‡òÀEžÇÛîƒAM[Ìý!M-3J>˜ÀÛõÕ´ÖGm0ðÃý8 NùOWg¯>F­eyÌà.Áæå˧r§‹mœˆÏkÈ9.œ{UÈòÍh…ЧZ(òíiÂüXB kÞ Â‚êÊ…ŸŠ;rˆî>ÐȤQ ¾'8}7ò 5sáà­ Ö™ø#ÿp‰tùë ‚i¶—38D£iÐrœÝá#‰¸_ôeáÜaÝ HºÚKC˜Ib¬œ¿<)AÝÆã d¤gݰ±Ö×ê4ý(£[6òC%±sÓµžõ×s´æðŒÂÒ~¯D_Xº6÷‚sJv1‘úá޶þq9À“|»á †ï‹b<·Yœ^êQÖüEý²ã ^V7Ü™1AØ5ŽÓ‰‹M^ùãÈ1wHõ`NSäÒ‡ÖE0?óÙºépö¬ØÐ<ø9OxI¿î ±YaiÎßù׌‡º€BŠüœ‘P–h‡SŽ®åÑ¦Ž° ‡v@r¹Ëù¼üÓx7dY]RN?¤(©¸pï:Ž^výDE}¦*–Ê@åìŽê„»ìÓ¥–®7grÎMW¡ƒ—#g'¿üœ B®¨9„hÜzÕBÐý·¯_h],zV­ÊÞ¯}mjŠ.ÖŸóy}y&(¾c¢ÐF‚úïËSúªºÓã%õ¯Oý­OâOçKÌ‹¦]K~òI<áåƒdé#ÁWäÒ9ë—õXls‰í-¼¹«‡9šd­[®p¸ ÖZÀWöˆóOjÖ€ÒxX½7EíÂTß]?]3ÈórE*TÌIWØÍyTW7b‚;ö¬õÖ¡¦Íø#‡‡{ö!×°±É…ŸãéµÊÝWÆoíJcU5ªýÁuü€˜¯ñ-¨¢1[’,’Ô¢Œã=s±œjU&†ûP’ÍZU_ãÖ PbÐüî_ìãç-ˆf¾L# ¬Þ:?µ2æ‰B².ø×]|!H>lu“þjÚXÅù‹Þ5C\pŇx‘ÄwóÑ;>"óßG¹È@ô…¸=ÄîOÖ\Û»4ìýa.~qùp¾þ‹Ðdd?ó“üÓh dì8ÅyâøÖ[¹ç—ùw<“ ÜŸcÓ—Ÿè±¹vÝÙÔ7øpù¨€U0÷Mp}Ÿ/åÀ>Zü§ws†Ôû^]Ÿoèýç)c¨ý OÚ¶ßQŸ¯ÃF[¸o£øG,Á¥ës ¹Rb%O£p6Ô_ÐN7¨e…g—QæÞçÝ"Fï4^‡«~ŽGÁsº2Üóµõè=y”zŽŸç—\o¯{Xè€ÊÁºù‘À7îBÈ›}°0òfnˆïbĆ>|ë¹ô¶þ|?¢ßšÕ2}¿Zÿ(måN÷‰+~S{£Â§Ï|lÂws¦¶±¼û±à#^-Z½Âä‡;\ðÓ¬¶H<ôM¼ÑGÔ5I—€IãVÙÚ7ty:IW\×S,á Ó«CŒ+Ï×kµeÔ{ïÆõ™(¢aVD¸~~úÛô™'l©Cðéƒáhÿ¯<Áµrì;éë¨öÔÆ{ÿ3>jF[:W§ ‘<Ñ´hÕ×*ç í¿fŽï×ë¡hÙž™Á òÀ©cÑ# ©yÖL”Ô6ájÃ”Ç -” ƒ¬òµ˜.nr€ðЫ˜ŒùÕçwOñÛŠÓé12GôÕܧñâ¹y´zX4ÍŽ:ÁåV›¸V¸.A‡ëêÃï7Wl88Ö˜â¶ÌŽž«Â¿)À‰:_áÅ‘«Ôž°ËØÄ‰•Q!‡B[nÍæskø¡ÀÊX×/ÔÇ}që„Ñ1¸äÑñé¯U1\,̆¬Y»õûy ¬a Hj=y ŠPNlÓÀb„X›Hw-xöË«¡•ßûf./mcøú3’Ç…õµ ÙhhÇçF´zµJO ¹}ºnxøli—Ðmßæq œHÒ°£7úp…¬_V+úÖGÏÑk›ÿê¾-#ƒmÜj#0~}˜ã Á@aŠèžŸpÙ'îiòsÕ3Ѭßz·”‘U +q‡=ŠÍšzçñläNnÖ£ÖØP ØZ'IsBFMxº ˜X¢_IŒú/ȵÚRzÜÞW7Vb*v»‡ƒ0Xc2 s”îªcÙ ­!Œ@ç²V;çòû”w˜sªs@XË¥Ln< ‰Ç—®×Òs^zî¾÷y—OÍë‰RWå®=ÿ †¹ØX¤-'¨"¾#bcäOŸ™WUEv¾šT_®âaϬ§ç/†W7²Ã ²Ý-+òBWúÅX~bç[!Dî;ÇbfON5ï‚éî¤+ŒÓYÍ:²YróKe¢Ðpw?"Ǭè=Nÿ‡Ì=nk5ü%µL»’–Yå®§øvž‡}­W¿Á'GNê ÒiÄoôEGƒL%§¶¹%ÁëÊáàìru}b^T+üå±t7¾8|Èâ2i¿¶éï&?S¼®àÑ}òÓãCr%Þ4´ÚYW¥X¡mv-UðvƆ¹Øp–*‚KOeÄùÒ¬>_@²#ÎÕ«yO„iÈÓ¹u¯ íúlì_ÍJôŽŒ&§þ®F}´t<½û /3Ö(¾-”ÆœkbjM¢¸vü(ŽBŒ÷æI-ÀׯûRÉå88/ 9ž›=Ö< !ñ'6š÷ãcÙ±¾ÿúýÿBP‚ ¡Å­ºû2ß@dMß„åøWÃnþ£)Ÿ›^°n,˜ôl‚È– éf*sîfWdc޳£<ã >ùòÅ9Pmèâv ê'gY\áéoNdœ>¡²àØhü!ŠFù"kË•îåÅÍ,¿QĤP«R[ ;Æ.Zü½ƒaÁíï8Ç.ÿf|N9 "Öð¾#®&¢'N^"’×¥?±Í%ý¦Ýè‰Î(á×®¹´¦õWúÞe̙ذ"OüYL÷²ãï>ï“®!€xsvî«}~²çC8#@IDAT‘á“_^­#Ü…… ›tž²×V·¨Ú âÓO<ˆ¬“~ýû䈕ó5KÑ7&¿ÓÐ'Ö®;å‰}âL}ðÃZ¤nzg]¤ÇÜ÷/Lwüå0T9µÉúp§,â4s30¦ýé8Ø|œßÈÀË©Œnùõ`þ‘¥Ú¾&9‡¡!‰t‘™gdƈÑâýFîOV‰Æ‡)جч‹N¹1ÃGÍ'®ìœùýãÊÓ‘‹eø‘„Òrѽ;Â2eÜ<˜gbJÌqx¿Ã9ÓÿE7•VŸ˜àkí{OæM<ñé´q6’`MðdXºdë:5i0‰1w/uý |6‹ùèÅ'FÔ¬÷ØçuA}°|p°‡]?=0Æñÿho³ÐAø¸Fæ± §–S5 ·e­€ÅïâW‚m$‰½6Ä S0ñ‡ç®ˆDp¥9öÅú,&Nâ'0°‹5ŠÙq"ÇîZ{—ó# ]ŸïØÖâ‰ëYë}ÙÓa×pZÓóÒ˜ÁôæD?‡¢ÃFdtã;]茼$"0£s–ˆ±X®­¯rœ°ÞÊC꼤%×bŸ“ÎÛƒŠJ¸.ÿÌ_ꮺÆL<Ú¤zæåØêsþ‰ïs{,, »eæ 4Ò4Ì ‚C£cTI¤‡µÃ 2ÁWЈ,Â1@ ®†'ý½¿£Pþù;%Wçšã¹$Žóÿ\È÷øÄ¢ Ï ›'u“ÄsúÍÈ€D'ïòH3y¡ÔAø&uq˜ýÉÙ¨ÃËðµ5N¡íBÚZiP½y/aun§ÎeíŸn#ÖÙ³ÖÀí¼® N×ëŽÞŽÇ^mr)KGäxòÒúASÓTxÉó¾GüDedŒÕa“øàMØ÷Ì ]k>}¨uþïw„×̈k¼âI%1ómLˆSû‘Ïü™£÷Ì85„\‰rÆ‘érk]Þhžo‡É.bÑ¯á ›/gcÓiPujL`3ÆGªÜ'o§qSÍYë» ÂKî\iåS¶êצڞññôÖ1)ÍJ=8¬‰“–Ÿ.ó"C¬ŽwBÙ úJ룆âµò‘uÝmsMç›Êß| @#»¡fX\¼hïü^nì 7>R夨Az  Œ.ç¬ñK& ¾o==ùDº÷Ýô‚x¸Õ™Ý€ {ôÃ9cÇ1£‚Ÿ¯·½1tÍSŸ®™/ ü{ïD#Ù±P“eÒ"}|ô3eÌ$}‚ià+ÁC*ÆSÔï|ÓN]³Ã½/â…˜CPYä¢x|æ¾ËÅ=+ñƒ‘›Ón©MeWæ{1D»‰hž€¡GžïqTú=S¿Äª‘@ß¾ –øûò v‰(¦‹pþ"0÷[e¹ÞühB®p¬=2î™Ú|U’*0ßM#j­±VõA…š ën®'U•1wüâšL±”D D~Ç*>í“ãÇÙ _×{”Xy‡D¼ñVëy\<§¬»÷)õAÁQ¬?±_yͨ7k/°žè¤U‹ãïküé¦ÍРV?ê4_ðA¯Ã /|þŸRú`5tfgì—l÷XX,xËíþdÛ—Û¶~sŸC§TÏ€¨':dÝ›¨+su±T?ûag‡Þ‘Q>jª4†ºK_QÏÚ9:æ†piE\°ÐŽûqXöÀØÃÕøzd Ãw9ïåïýž+ñ¯üñdÿñÈ“ Áë‡ÌÚ m7-S 6Ò`øTD0㑱öö®0Òd!"Ò,“þ)ä9FÚËŠ~O ¢/Ƣȓhà\eøµ‡ú#Ê1dDü~šûÞ8ÄA êžÀº€ð-ùbKÿbƒ+¸³“nÃ8k¯ÿ'Ï V[&¥5Ážƒø¸¤¹òaJ :äiDOkNoÿjDlܾüäŸ2ÕjW6ªÄ( šÐô;ýèë@žj‘ñ¢€ÙÙÌn¬'ÕÕ¿Nþt vÐÞMyQÉIîÑ[çG9 Mä^L]ÿr@Þ@jTÛ'GðÌ5ô‚„¾ÓˆÚ€Ë!)¸Ê+‚€UÄù]?®«à&1ƒ¸Fýèç1‘ŸîöE ã*ʉ{[Áé^HÓ7bÉð°¨l',ÎiÊZô~øÆG¤”5ŽOò‡¥µÊ½#ˆ¿¹þ¢Mç½/’W¾Ø ø°ÐY€5ˆMþy\𑺦áôñÉ:/÷“û äZ€æÙ:·†Äa”º©Ìعi=à¯~‘ã7íûnŒ 9GšìyôWBbè™üÈe2gý¨*²þ¨H¿@òu+FªžrÆi4¹ø Í+9Óț濃þä2aUÿ:K&ssæ…ñúþ%oWÈš__´5׺-BbÁ”5k¿8lÌ82—³úàž–¬·Ÿ«oñžØ,¦/7à`Õ=³cd{™iýÀ¹òâÏ€*Èyåš112_ÖŒ½å’ÝHY»å¯lªØ¦VÆmxÎãí¿Þ ùKÔMqÉ4~ZbÀ"Y`K׬¸â=(ƒMxj¦íY 嘂KLƒ‡›÷&üÚIŸ½~xîŠÕ5üØ0çô“ÕÙcêԃŠ7‚»^º‡{s$=%ö÷råp75N œ¬-êJ}¥ô©;¿¥@ž•·‚«ñ¿~{Ï”‡²j¡£Pµ¨@¿¸°ed £‹§„xf‹ÍóŠP9§²¼÷*ãÕT¼‹GRl8ˆƒ^t:II©ûz\´³ÉãüÊ;/J²vyhA½?m÷êúÕ§õ„?ð&\ä¦Ù† Ô( }†‰…ÂÆòZc,kädIñ•Ã:?1`\çÖ`yÞz»¬Em.¾2ãÉéö鋽?üÈÆnŽ0dµ~å1àÄæ.àÕà›mônîÔ  ;ÚÖÊrÀGó ²n"Âo¥g#OÄ~@‹/Aø¥Åæ™3Ì!;£×C¹Á;OfŒŸe‚I ®cº_\ãÏÉ9ˆ~â ÓÏ‘ +‘a.¿Úœ&ˆÌȉgzM@håYMÀ²Ž¸ ;[@ók àçµsÄírçsñò£±öœŽGN'Sùb¯ûõÕWZ®pxŸصÜÔµ§°œn03)r©í%sœ]›¢µå^'—ÆC Ϻ 4/!¶‘Õ5ò¡5t@\59sâÒûPZ{©ðN#޵ëÈ[çËé]HyoÉó%6¾@.Fs×é ÀñÄãfuð£.îÅ$ù"Nß}ÆÆòüdn5üÔÊt‘>ž§È«x³fLg iPz×d¸au üëLš×ÞXñ”Xu¬{©†³.Óܯˆ¯z´}öVï3S7aÖ±Ë[?2`f²Ô5 ¥ovÅ€}~ݼ“ˆ(­<íküHKíá½HZÛo7¡¦ä9ØÅi=~ØÃñùÎ8 ¢1ë°®Ò=~øbÑLýÃöŽ×ÚîŸÿü] §Õ®7¢~›’=ÜÜ„e¯x¡Ð~s2gj Ô( €OÔlID.zè¸q¾-Ò/#3¹›„ ˜(âá‚RUN7F¬ûzêñ7ŠaUz ôÓçÌÕA°EI—gÍ|Æ 0ÆwómA܉îG͠Ѷ{A¼¥t~¬é ª»O qÝsp–Mb±EÂEÉ‹BôÍM½vÁÎ qœë'ÃlœðIŽsÁµÖ‡i¬Á mLô;¶ÅMFB7^C6–¨X7¬.lZ—BXÓöS]KÔŰ¹ºVV=:ŸÔ™Í ³æŠ¬å­yÖ_ëî„Õ±aÜ\—8¿É4þ°m=Ò‘£kŠ<Á|ñ¿Þø°f.ÖlÄ·æšOyê‹ØÇI]Óú«ôõW û— ÀŸTÆøE˜+; ^ûá§ñ,Šþäkœþ“mláö ¬¯äiÍË}®ÚéÅÈë,ˆÍOt☺B¢OttWÓD[ìàV?á΂f¬^åÞÍú»WÒW£"åSîˆÕÔYzÉÿѦÓù&«œ_ia7ä"Ê£I9›ó±¾WµÏzp¡5ÐKÖ ¤è\Ó›WbF½\žHZŸÃtÝ”+þ ÅxírZÄ]|ò"1G©˜Ö.þÄŠ_ôgÏ<ç#œQ§ÎIGœ#=?Ž"{Uï˜ØØ_ñMë¹¼¤Ò±Œt優9wŸFK\]{1rO‚Í6,þ/JÂn #±Ïžq¹ê+×øk U~û¬iRh fàÜb÷/2| &8z <á¿5UuË–Ñ8”Ÿ–VUæ2ÍLr²FÌB—/W_kÕ|Õ6JòÓN®(Ï¡ C"˜«As_…/k¢ˆÃÕ¸ÏÈöïlãþ’ºt[×QCOÃìõÓësFƒŒê—ç‰Ù÷QT#žëçQ<¤ý‘ ^“ÿðëº7ùmàÔ†B8ÿjq”Ö"¦2ÂWE[Ÿ:`údG1ô_IÈøç 2ÙÇ—®¤ˆŠÂ#€3ð-r·þQ‹•Alà\pœ,ÝÿÓþõ1RÇI·‹c ÷'it¶XÙ8³L–~­™(ªÄO)û“ÍÜhtÈÏ:TÊy>ä«Ô®lÇ:© /ÈøÜÐÅ?¹‹s‹ú6¤Þ6oG §}S% x#cN¹Ðå:ûÔø@æ!H ®‡'>l0ÆæßŸ¦#/qtz”ëòI2F¥ âD⿎„TMXƒåÁ@»¬¸3:Pæ -öGnv‘³~‹ícDѵ_¯TämðàƒÇÄÛ‹`R 'kŸ¸|q{˜,Ã?Ú@?5 DOžÈh‰gNn-t."€1 ‚§*ç.óê9E»º€Sr>uÿ_Ð}U—¶”áÆŠ½áäž#ùüàÃ[äÀúŒ>3Ì8Z`Ázß±Kt¿hTØ 2Gù%ÊøpQÈ_H°·ªÈ™„äzÅ3%‘-1Ö0üj>rFØ>/‹¿û€ÊœæxøVák Kò“,]üßHNÝq§°*ûŸµmò”(}ce™)Œˆ>‚à°ïÉ%yj.ã±L(œ€iÒ5I*Œž¯î‰ðéê1Ó¶Æí6¸õdžoó@ŒO®œâ/Cn}ÚAš Ú)ªÍ¹6rÈÜY­œ3û­5êaº»/D…ûÐp;ZàqÌ—qì>5nÀߦM쟎˜hì9·Š)ý°<‹à·æÈ8 =Ž ##ïZÌ18¤Þ߉–˜å©üù€ar”KÌr’M_Ô‰QçÖz9Žo9ãI0Ò¡óK5ëÉï`‘}ŸA ƒqy˜ãÅ‘M’pó+ÒÌ!ªèˆý±1È©spÌ1ÄššÃe¼`èc@‰ÁØ[¿®9ÁÅ{Qløál“ƺ|Ƶl€`–Öõ~ýá‡np-È ,üÁaßýþÏß± a·èhPÖEo^´sKÐgÑ¡@vyC‡®í·×âZ’`( øm6ÄÆ›ø±¿¢CKaZ|¨ 9þ±¹D¡ãz/$6NLù83Q2¶JS`]hë-R²_Òqº6uoh„Fç _…¢Î£6åi‹OÒÚaOsLÈQ{¨ôíö%ô^ÆÌ3êÎo¸L&Á_‹eÅ6ðžG‹+×x5Œ'‚v‚Žœ R§‹XI68ÇÚEoa›MKGœ%¿Hƒb$†;ù•¢®]s˜žF5ôãw¸L&ÐìídQžLˆ“Öº¥“¡æ•z~ÑøÀ0¤èØÍ5t×Í¿‹çösÖs~­Æf ï5W.^—hÇš`)˃pu¢2zоéÇfPólhrÁN½sá¤þÀ#Ø* 0{^¼"±1?.Ëqzăh[gQ ‹BŸéò‚‰,¶¬gĽÇÁ%F}”ðÅ%~“,ó·K8nܯ֧|ö¿V÷÷ZŒÙhúÀ€áöÐXó«]ãˆÖà@o ³½ò–kâÓæì³—ÀýƒÅ‚ØñÓL*‡ÞN®¼(0Ú}™«ÜÂU¤ç`&Ì?öÔ#ÍçÖK,©ê¨ÞŸ ëÑ}ë÷À'ާ6.¡&Ÿ¡ŠÒ s>¾Ÿê4cHŒ }œŽÑâ°Çý–uÒ}nÜ@iç*)Àûî=‡‰L7©“†˜^‚ÎO‡}iL>.i°·]àa8»>:—ÅÕÙSlQ âÔuàý@œ‘è1Ø€>ðôoÐû 4 c~}’…Eê±Àd9ªÕäb"Ñç{±Vš³÷bÖL6R"Ö¨{ÙuŒ‚Ñ öÈ×— E…³FÞuþ„MˆîÖU.P<±¢L¸®ã Z“«ÇÅTYbg=´…¸ù^²L‰äðÛöžÛ &à dpŒI’tÚí úãkÀÀa`nÏòƒ»}Šúú°‡D–${ŒòúÛ·H8ˆðWN{í9eâß§7ˆ»œé&X>‰™L>Þú ω§Hø!¯‹…GùзßÞ'”P …­ ¨tV2º[ð‘êWGòÂù/ÞßD%_Çñ‘¥à„O¾.Æ!]>çvШ‡ƒ©ŠjØú°G$º6øöwÙ’c¿2é/½Œ/ÿØÃ¡;=cÅÍôÆL·¯™ Tþ :,.¥Ú»p#‘Uºg$ÞÅ_b²V÷âÆ6õ^ñ>˜‹=vø]#øÌ*©›‘Y¦Øâ=FÈTXóês.yt[!!·[/eÅ®ù÷Ã4LñZ:i­ÛùDžÚJûFwçgîSïQ*ÒÚ@Þ­$<-?bãë|O½÷@ÀÄu÷ƒó{öÑùbé8}˜(ÂÄpƒGþ$6âEÓZ霨h‰›7<!¢‹ŒGׂôÚ6‚s9vü•·ç§¢³cÌÁóGƒ»Ê©]ý ÛºŸÛï<»Ï(ÏÉ8bEþÐ5,ãµm‹~|ÐåøÑNpûoÆÍ%X9K÷Ù¶XqLtËBì r çPúŸ )áci€®+ã)®Ú‹¶Æ]ßäVÊê<ÇwëPoý‰‘†MuK@™ªNLg-Œá»ÿ#¡¼Gl]ë¶”à8Ü=àoŽ]'l\©†0û!Àèõè ‹;*xÈ•€lî•@0v.Ò+\ì4(gPt㔞1þ¸ .¼è ƒ´ÅÀ§Ø1ÜÅrÒ/Ø‹é(BͰ¿12ÓŒíÙßÕØà|ñ™Øô[ƒòp'ÏôQ)¯2cI"ÄÆ0r –¾¦6ëÚG“F¼üÒ—²b¹+^²Ìésïi˜akŠi³þÜGÐa~‰c¿œ :'Zr?Èq1¢(ÄÌ Vœ½?ɯ\RŒAFÔø3´áƒ†ž>6½–¤á$ЩZ†òÅADÇ8‘kã]ð™îS7 ˆÔ"¶¯î ˆ9šñ«";ÚäÆÄ³»»·ªSùѨ*sz[ë[+Gùu‡ïZh5èÑÊˆÑ ¶°ÒäjÂÿ€N[5ÒK`7¸ÁÚ.èÌuIysü‰klìô°9{º‘eX vèsôÆÉ52¾ïȯ [uãÙÚKŒV ™œåp.#ó‡ró÷çXÿ¹¤y‰ ­Çoä“!wŽÿþý>Óâðcàäk3!˜G6fWbÞv^$ŽÝ®ÎT†Ù`oº£c(bKbõ1IE.ÖFcüâ‡# Yƹß$šM¡ŠZB:[㌯Bså&aЃxÿ?ºÞíWÃí*ïû¼½÷ö>ú„mðÙM¥JA(H ‰ ¥ ¹Ná!¥ÿø*½HE*5B\ $ª† ¤´8HH©+„`m0ŠÏ1>nö¡Ïï÷Œ1ßïÛvæZïûÎ9Æ3žq˜ó=|ßúÖZz:òº˜ô’.‹n&.òJ*ž¸·.Ïž‹M^éÊ žĘºœÛ"4øðà`r=‘@6pô§œÔz0b!ë¸y熹õ>¶`ŽUàÅW‚5ñŽ,:zÃMP20xú®öÎyʱ ËgeˆåVƒ¾‡‹_58ñ;þ,Öˆãé·×ø³˜‘ÃÖ&&<\8k»|‚³Óß:•Èrœ¼ =[|ä jb'gjÙL¢A¯ÿ>|oŽã¯6ØÊCÌS7/L•ï; ÷`sC0s¬ÏøòEä›´]œ—ëoãè¥8˜píœnÎü*Äö¯ ™mNç{d‚IåxRµàÛ;]ñÁ'ZÆ]CÅï~§×^Øh}ÁGí7$\»&mXàô۹ªWj xã¡5t×M»¾®(À]ç²å‰“Æ»k³ãïKƒÚFg”x$LÄÍ-+;ã^cÒ1öŒ½1` d}bƒ=›õLF¢Ë–Rü^7jtÚ®_f¥¶àùv3ž´z=?úâÆé—§Ü ?8 n8C${”Ç?”úKˆuZ·îg@\Ës!é…ïØf¨­Y¨¡ þ"É­Ýäü×_•œ˜ùŠÜºÈÙhê[ZüH˘ü‘QÛ½VmP-W_ød¡òtNÖ°溴–ðãp Éq®@ˆk ETvúpË3r}±¾édË7ô|9#g#&CKìÉ—•aÜ3çÄ8h;¬ ÌbG½-ÜpÉ7ëìœT`øßs¯µ¤æQ˜_í+y„å§ŸF,v¨q:4<ŽB˜2æ¼Â˜ÁEܹJHÄŒ7.zF­zPLCwX‹¬¦“5äf–0OŒfF±i^ø}äg~ ¤êî±hí4¬ ô1.¹à8)ÖÃ`Ï‹-=„Ú˜UŦckIn†2Î5Å^ýfÜ‹RrŒImEÝ;Iõ#CvÔ¤Ó[ÜÌõòËeÀ 5±ä´ñV¢©9ÙÑïCôU$ä^˜"â?m²x¤„ˆï;B­Ø)Ï*ÉSwób¦ðU¾{I~k$OðD`Lw` “áTt'‰fMr½àð¡ênÊ•¶6â+túDÛ‚‹“0À44Õrì ×ô’üu<¢´¢8ÐÀs+{b`Ü/ñìÒŽÛÏ`1MÓncw0s¶  hçóä†ÌàRþ!ùmŽÕ—ÉýåøN¸]”´p¥ë9’.Ìò#fÛˆ»:Ž]g]½€ÔäE°«(µ³öKe¼áÛq:ž àqn[%ÇíªzžVµ5 ¸Ì@}4wöÓ§¾¼ª‡x³½Ádmðà—½¸»£œá 'K§YvúŎᥑSÍN]Á²í¼É¥ò6¿WôPoâÒ×€Ù-Íÿg s<ªºr£W›sþ'F$„ªó’Ä>5Š%lRÁqÿKëMlôõ5ô/ šh}¸¿Î‰æy‡2¯puÁÃ7ñò¦ÚÄÝ:ââò¿¦”¬oaî&só3ÙEýÿ.zì í3Nk®Þ'Zb;ʘÁÁ ŠÙ8¤M¹]èà8Ìé3d}C¦ÊHÈàz®F0²Zõª˜»õn¼¢ïhj²|õ!Ê:lÔ÷ŒV]ŽG¢æŒ­É'þò°§‘S‰ÆŽÐ9n~Ä{Åå4êÉœâˆoŒñUBHÛ¶Çœ{>ñ¢`ÙcߪìYm×Ô«ŽÆW9Ûa^W“/ëXgæuB$Ìá¶;õ^Æa#›œˆ†n6V"K!sÒg†ZWºñT_MЈGÅš§ù%à§J;Œ(:ø€¼hñã7Tùàë›ða¿‚ZÞ%Ÿòô‰£4ß½Hÿ\“˜L™è7ð§9Ñå†ä½i—±'/åËÖ3;ÝØgÿ:­cR6“=Ea€BeDàÒw8:4OÌ(vùô„CÄÜ:v9% }öqH¸êë:¡­B„Í»q”vNbóGÁ<“Ë™ ý±5¿Øpq Žcb‹Ïè¥p/àN!Æ!v¾Ä;iüÒm?î…üþ}ɉ¯îô!ÁëÜËÄÎ@ÏˆŽ w©î5©£q‡Èš:õ5;kå>'Ë0…÷'=É’´‘X,ÀÌT.tÍ ú1R†Msi èª\Èà ˜‡úµÄ¹p/²4D®u8I¼¼(ȱ71ÌÐyCF>¬F¢zB·­&q˜,˜›Ú˜ˆu,ï¼5Ü@–!&Ö@ª÷HjÔºµF1(}7N‘c8Nù°W)xô?dAýíkÁNEPÙŠø®õ§8598À03ñÿÆ7æ#@«%ì°lré¤ÅÈâF'†d§…¨~"!øð¸vpb‹ ¦­ïóq êªk‡ løÕó(–Z‡€n}ïþèªÄ¬*ÃÞ<6 Ìd(yµÔüÌ>RôæÃ9H<©#Ä!Èøk¡@,öG?_Å4Éæ\{÷S‘†0ù׉ ÒÀ°H³˜{ÌÁºp ¿çôÑ= `ט/#ÆÁ#ï}1´¥Ê±‚‚$¼†ç87#-‡v3¾âjÍ䪛Ø_­œ;.ÉP­0Ç5Dƒ·^ìfÆÏ‘îôz„-b+R ý€zC[ƒ+¿!2ç‘zß·ÒŽßQ c]q Ù~ŽÖ rùÀ†K=ý´S„‘GÙµ²>‰~gƒ¹FIš"çÂÂÍéÜGÎZ!ä´—ŽN ļÀ4™†G¥yÕCçI‹Qæ'X†KUÇ,HÚ b• U™Rˆ®|NÀê7ÎkþÊÑ_¤Çã(8¶<ÌÓîb—Ãh«Ú½5‡Ž|¡Ù8 ¯ˆ{ßµS1‡°5`–ˆ·ö%¿Ë!* 7.õì˜Ç‰U¶ûù.ÓËXAöziƉçJ7c $mXÌK¹ÙxØaÓ,rŒ%“]ÔË™>*Û=&ý1}`ÈÏTIC×AGÄ·ïã6º †›Ð4^Öt£ô^C»Få¯aí0¾š20Ç÷¥÷Á[ÓÄDŽw~Ê` õ%ì‚\š Jâܘ5M޹Ο˜ë‹ü°qí/±¶ìÀ°Ge‡Gn‘µ¾½&\7,Y[õ[ê0±ˆgs6SÞô@ï!Xýê(ýä˜XxQ2ABL˜’EÊ"ì¹iHq5.Ü«y@¼`« ¾Ž­›špùLX v´×%ÅX ¾½q b‡¥ûï~9$¸:€iõÑþš™PvÓ¨ÁÃ0±eŒ=KØht«0Ï|“³dù<çäGm˜trg¯µhžÆC}¸ p,QX›Yd樿jˆÍx8wÍ9Ä!Œä2§Ò©@5M’,‚pð›2|ÁD”9ŸØ0áDïP^‚D¬h­ÖË~ÆU•‹º°‘¿ _†‘ÉÝ[Ù[ ãÜ.ÞW87²’â›­µÀ¦}:õkpÖáð¾LQØôÒðáDGãmcŽ¡*ÇÄœÍ<ÃÉÈ®u¡ÎÄ×x„3æåhŽ3>7 ÆÔòP¥ùÒöi„ *0'n¼^ÍÑ 8Ï2Ù†»¡~i¢Q"±ÓÜù‰õö"æƒk,ÛÁÒ/’y± ÐuÇœ]$D®«k} Müc„ÞyhiyÈꃖ7ÞÈ R^ó²D–èœÉC+µ:ô­ ñT€¼ ëüÃ1Tv;½9¦g`‘9!õ‹Öþ4 ¿—¼7ê÷a¦9eT†μµñØKD°þ‘Ñþñ¯'â߸¢oŸè¶ÚÆÛ¹#_>úŸœ.Có ¬%°³.ìcWW}8 ß/rØùí ¾¶»÷Üñ:G-øJ‹Áu+@ˆÃ"&FxuÄ#ê=J‚;iDî%A%€‰+&`¶®íñ†ÂÆØÖ¤õStCR"ùÌgLŠ#¶­G êóáñN…l «7„ˆŒQ12ç eê˜!Ö3’l™4Ú>m¹9f3¦ÆV¿#—›,zᤤW‹œ€Þ{ȹJLÎûhïgØÅűz†ÎI:n<_\g²_Œøde7Cç$=ƒ,o QZ‰úSõè'¾=o¬ÖÉÑÙl;âކíù¤Ì#èæ³woÌñ×ýÖÐz_gí\^šÇ5îŠŶe½ŽðëN6bñ¡ó@àÜZ3/n9L=JFŠ‚iŸ1¨urš´ŒMÄx¡2ì`ŽùÊy£H,ðšLGÒèrÔ¶ëÐXˆ(Xæ®o~‰Ò¼ó9dä§mvt­ ºŽ+Ìð4t´ÁWæ×ÉàI"Ìr놓;NàË*«ÆùŒ¦›õ•¡ÅXŸ…4AbîyÜzÕyä§ÑOrΧ-+ý’tOÁÐa ïr2D™B2l›ž†¸pŒøBÕ0v½Î” –…p•µŸ'%ì{R£È&'¨³^¸€_$ éŒï9è—‡=9D8]˨ËŠ÷FŠéßǪmŠ—úî[lóî¥võ j¢®¿uwqURW¹ébCøÑ”#ãtè—?ubL.ʺÃÞZ¦yQ½HÎ\SsqÃ06ØWÜjh­ä¹ìnVbÈfCƒ[È©¢9êÂ6+ccÇ֓ÓÛ5Л–ÿÕ—“Ç…]$kBöÔÝÅíÚAccÁK=öá”À®qÓßõ†.ÍdsT©DÙZ É 0¤Ä¹‚5žúŠý¶+éqmÖ¹†ÑaòK¾óï\6üQÛÔ¬ÀGÊ3‚ä{ùÀÚâP\*(uí¬gäjYýÖ·ˆÄFL\T;×3gË»u ÁŠüÅù×ø+axdvØtT` Œ©ë-ˆËí¹‚s[cë ‚Ånuaí"Ž‚kN€,nˆå"oš3§ƒrɱÜÄ\>lhu ‘£‘Éh¿a¤»Üг[ †É!Ã>èVÓÚ°ŸÚ‚ÑG}bƒ~•3*ž(Ûàl_”iäZ×w™8?±l& ­Q'¸ó€° ×p>ÊPCòºé\bñZ,и˜y§þéÑν„¢DN¯ù9ð£Šé9·õ®ƒAö^þÊò¨èH…[니1_þíðŒøÿ50¨³6`2Ž©P'"·Ê ¶®Îq†2[›oêk ý©"Xô±s!ìšœu¤ß‡0"ò܃CÈÌÒ`c&÷nµnΆ9q2?ØY d‡(ýɃ5dÀ¢¨ø T ugéõÏûÓW_”ÚQÃt°ñͧƒû*øÉ…¼{Oì¾ÌeÀdµÀŸëŒã°·6Å!ç±çþ÷£¯P¥Q»6pc7qF mƒfÀ?k%ÁkÚØM*ÊÕ/§)-ìpÔÓ#‡Šú§.9G†Z‹™m1ÈÆ7çƒör>K|Õ²~©i<¾áF~Ðä@B—ó cè†-±!J³žíŠ ~D¸>å98øç™ÀÉÇ08m¬€}}%YéÓ'jž•Ô+$ˆÀK`‹ë\ªvMOb×vŽúÁÎÅ89$Q™KRjMê£xúiS“ G,:=¯w.'ƒ8E¥¾“cbu*Ž.!LŠ^FƵ“·füYÔãG¬;´ (=cZyµ{9ZPeºZôbœä\`ÙQ«êtvÑ0ÜÜ vUp¥a[ Î-åàî¹ðž¯ÁÖ˜…Ô¯d<[[ßQgbWNah>®‡p9Õ9öÂ3¾#å ïÝÚ¿¸P ÉÂvñć„åæ©:~ï!¨mÄÏ¢ë¸Ò.Xúõ¹žËÛDv·0Àñ…±òW..òEÓ¥‘ë'Rß]ôP¿.â‚-G¾5 $ûËw؃àm¹³x‡(bÖ¸m0ƒÐ^bŠLbâŽì¤™9ZEiט>Ö—<«C˜¤³ $n§·F{ÁÌV¬j¸GÚ5;ƒ=ø ’AÀ ÒÀ0[_,­AÆŠ³óâÓ~k¡¶|Ôbc ƒÊîä°øPž j»Ï:÷ÜMª‘ì½¾”p•~ˆUj#ìµ-²}C²öóðÖ4 lÙäàÁ i÷WŒÇÚ ñЇ& p€p|ªËøaÌ¢ý²eVõÑÏŽ{ üaæN{~ôЂ0y`Áez}à_þg54àÙx&ÚÂûãŒ:*€=8d>kÑx.›0M6ëþþÔ[:Á<,”y¡eÖEÒ'Ög­¦C,Èy€"çl=ÐQgòÇ 1;«ëvSç®æ8»© × †àrݡ澣%oH¢Û‡Ã¦TbÂ_{”ƈ8'0ÅŸQɆ}EÉÑù5Áô!a«Âãò¢ðZ.üžù€0­5_Ô+[ù¢öõƒ¦±—õ‰;¢§&mõC¿Y©6À*\ÒAÃjñ1âð/Iq*©}|2¿qC–Pžû¡H8’a6æ{ç øÇ6ù†Lú(ûGGvÕäè‰-µ¤^§ð‰3Î7¥(Ty}¹Õ3Npé´(ñEЉ#‡ô:jüpî6¦x¹dáp´1-)ë¹~Øa¹ÖV.2•åۮǙgxΖ.-¢^_é0‘Y; ¢g¿¶eÜwÌP¤N‰Ã#»Ötûãœù›1=º4Á 2'»FUdWç3º·ÅhÇéû=õ oíÖ~gMh’5F Ïý6XM׫â:—EGrZŸo9+Ã;òºªß´álÜ‘‰Åwªë¡¾;{@0¾fDß8F.0ÇÇæsˆõÛÀ6mNî}÷ßšíÚ#¸´Bê£ç%$\w9V 8^ ¯*Ú&Ò…kÈ àÄ['=©·¿GŠQ' oëqú‡'ãøF=Ó<lLÈ©!‹}ÍžØs‚ñKâ€E]go*]Ðdn|r.fúSl´p”´¾±áB¨·"Ê5¼°-j8„ÐGž©qLü”.{,É죡ÏFÍ9R÷ôéÒ×oŒéçxf¦NN»³$Ñ%2ÆãýªcDfH‚ph,âmQ¸TGÕª17ö.Rò…¦GhÌ]“Aú­I:ºq§·Æqr¦®¯½þ¯!‰Â/„à—Dú@E§ï2woi0xÃË>5.Æ1ÉÆ›tš7üW‹²ÁŽˆ DÒ0ªªqú‰oûØ`êì-Gö¼]¨ËVrÏÖZ€Ý|S¿Ô¸ua´uö¦©-2lÍ—®µ'ÞT}浇ÆÖZf0ÝïzwX8 %·fåï˾À#ÿÚ],3÷Ô%&Šè×þMÔ0Ûèù§B§ôcÊ5Ìn_¸LuÂMÍ`¢ÅýûI#ܣʵ s‘·~‹9££4 9Ì­„rAzd€qDc~° —sU?G-&óLLœiöÔ‚©5ö±t^i´‡")½mãÈà>þ±¡Æhòºw&ž¼VGŒ½. a#VoÐÁÔÓð%¡þ¥ ƒ]äÎî.¾A]úZDÜ6=n³Ä'0ÌsþðŸ¶Y§ãEwó<¢ £r@ž=ß»Ž0‡¾x4¨(‘¥ï1|WT¦Þ{Í´BQö©†ó!SÖ¯éú£|*f‡ðçCC 6ß½=Ö¥=k;@ócáуk ïõÅ~„Ê©a±s„Û{ZkÌúR \|%’i0퉈²l3wC¬/uB.®ñ«õä Ô×®ÀÚ­¹ÁsZøâ“Ô½¦ZœfTÚô ³;×ññáóWôüïá£/®¹z®ÃËyÑ‹Þá±jð†gý;À\„ÀÎnø;?t’‡cwÓÒgâfP•j°ÐËÔÿs¢•ãcT ½ûkPÆ'–òvæ|†²2¾®õ…|¸üÝL0ÌQ|k2ö{Îü#Ÿá …³ñõ:Μâk)«ƒú1n#t¯é -,ü‚<6ZF·FÄ3<Ö ÄĈ¼æÁ¤+,ZìÄÔÖŒYMدq?}ýr‚“¬5î‚ÕFGvROåݾÇkG0W—\4$Ѿ‡äjyâTbŸØkð‰á=.L9,\v¥#] rd‚Y”ѾœÈ7êr1F1ª‘åp.àĆõô-ãN@³V —q×ýóQ+f±ñµ¡„”S­F0ÐZƒuè¾Sª®I·»ðO¨éwÎVYgH¢ûµhޏÞu›ÁÔ\>õò¯ Æð5þÜ!aØn¹ FCãxê€ 7_ŠlL‚ŠìüɦS·6ÔN&FÊê+2‹°žsIJ¨¬Õ¢‘ÙÏ/~s±à!º€ÇÃε˜Øî|˳òÅàldÔÚ‹TEﺼxV«?ÃÝøà .5òZ3´uÔ€›åÑÑÈ2ê;Ç5ؘv®v™'ÍÇ|éb×… ,/ŽíËû¸PÖèH"÷{ŽT²çðæ[ ÖK{äæs’Ãr4wÙðQq»¾4Ò/‰q-3Ä⪪Ñy€7±n_7Z1¢Ž‚@Œpqû¨q0[rb{ÕóZ+‹ 4Ò™q¸àE>«äŽoÏs‘ã;â/·Û@IDAT©_)&®@Œa6Ìë„k7ᢑ¡+Œ=sÖhû1– Õöè­ŒPÖ®ü•šUªÚ,ñÙJ*¾v.[ ެ/gBjÝQ¥gØ`éÜùCætD¼k LQÕ»§Žò0Š–6›gœ˜§–`Ëà^ÜØ©—±»åh oÆêÊ3´Æh¥ãÿ¬GìðÁ…ÎØí;Ðë¢' ¼üÉðÇºŠ gkµg’t"‘¼œ£tçMþ§Z`]ûÁµÉ¿ƒ`â= :;ëgÂsÙ8ÁUïÿd8kY÷m©Ûë·«f¤á|·ƒe>±¡¥ïuY±ÅO\¨ÉœëÆ'Xyjwø- Ê<»»†ÎBÛk9˜¤—%ˆnçþîú#(gÚÔ—K¦(†YsIñƒÑ]n—·™/¼‘Še˜<e$öïäv™#¶4ýDˆ\ÝYM–‰kyô×úZÌ«d¥H—â›<äj‹\@ã¡ËÛ½æ ^k^íÆTGÅàû¼AµÏT{ ЯN6È9J ¹[ê7¿BÆ[œ A„ €K"%Ýûc·j#Dðy¥uNX+„Á“ÃI£å6ÝMášG}³,\ÆÁ{c€ꡇ}¿Ä ÒKì`áÖdº-ú±Ü`]¤Á9«8ÂþŒSÈ=|=èá+ž£a|±ë jã_"ƒÁþQ&ÅÙ=ž¨em~\„Ù†ßc¼ÎVÂ“ÂÆË<šHsŽ'!všRw:m¤Hô»?òø7OЉbÚý$™2üøMéÖ]:ü(•::S7/Úá ù ‰VzdÄcø»j½Ñ¶ÖYüož'#âvºÓê!6™»»Ç§“WϼmdaÙ˜G¶±ôßþø"~·HâcÏ=e¦¶Ìຆ Íðwvæb|̱4.×Jâržª©¯‰¼[­ALZ;/NWj­ulZâh\M!™2'·õÄ™N³MkfÌ…¨b¦î[Í*mìSëäb ûà€ÔÒ/Kc»|GŸAë´7+VøÄgö6æ“®-]sªÌ³ÑnübKÍÉ£qüEçº~gVlz=™‹6àÎ5×Ô­qãĸ¼Ë>Fò·(H˜#}í\÷„Ý2•‡Q6_d cœ–µµzâ»Ô3k’ë+æÚ`Ç<ј‹öà¹j"ÆñlS2ýÝòS[ÍbŽ€âá°D€Èƒ˜Oý0ª!é;ñÑs7kF“¬MÏÑȵ#‡¶Z³G2k˜¸Cԟȧðä8~]‘Õ[zmåf=”Ÿšéf,Qn10ÈfŽÓªùXúGÅÞwe 5‘‘kÃb‘O\Û‡‡zà‹Ù¦x¦=¯ëylºÍžóH”&.ªñÖu·óEý»!ÀB‘ã†îÆÕÏXGb­P¤ zSÌÓù„g#êšQ€>YŸž':*·vøž€›»~öJ„õD˜ãÔYžÆÕغV»Î¨øã5¢öÄÿ$í¹þþ‚E~æOÄÁèãˆ#!FzâáÙ6 ³`;Ñ3ö¼ÜsSA ¥`WsËÜà”ù¢²O-Z“òca9“t×ÌÎqiÑb½uØly—³^†)Œæ§õæë:ΰ“JåCsŽ• 1ÂAðõÔt_l¿QÁ–Ÿâ-wþ‚%¦µZ?Ôã¾!_]]aá:&3ÃdÅO(zuÞ¢Eçï­ªãì4iÏuÊPÐÀ\ü˜išãæ¶Ïœ’Ani‚ÒIýy=Jþoxã“ù?‡Š…ÎE“ Æ)†U='}…˜ÔF¡"î|w.‚©ÀžB-Âpƒ·Õ·¤Ë'*vW(QZ1t9éÏ;€€sð?Ãõ·®ãø2ø¢qáp¼PU06¸-w¡æ;t0µ¢È…@Té§Ž,+ç4ãfÇ8}ñu‡O/R¾öDnŽÃåIúÎLDÖ ²6ù!ì½nú°‡fðf‘x‰’w÷;Swð“Û=±ê.¥§MòÚŒyÂGÛÕQƒšŽU # ”­¶=Ž¥vV?f`ËÍE¿ë ä(ègs­kÜqöbæøq‡%öXCð 4hÅ1êÌÙ«Yyj7j×%\K»ç/þ13Ïß]/³>12H­u-"$c!¦Dd¤ÞÁƒ+ÅD_rº+]¢ Ž‘\=kXêÓ*õæ6ªHKLjb/ÇñÅ ºöØ<64‰ שÑ(ÁÇÁ®¥`EgL,‘aqèʦ2Š^ëdÔaäá_Xõ^sfÃÌCo £½xñÛÛ úðcæ¾÷°­’!0£Øð&s¯={¡ÞóâXß9è4^Êw^°Äaa Æÿ˜j°¹e`žƒ6”â8-CÄ-“=F#$?@ðOGôâ¢P̘‡Dê’>o–å`®ÙMˆ¬ P”h#ÛH­ÿ]^Ò$væ ºïn®[¥ÁžAac@Óí9¢P°t°©»:Á”ê’/ú’¤ÈÇtiÌ‘+MyôÓš'¦µ—wòÅÃhêÌ84'´aßÂwG 4¯Y_‡žó2ç¶a·Ö Zñµal´Ï8kùÎñë˜5gXMÕc}öÞ&|Ñ­éIHH«q?ÉU›³ CKõuÙƒÙÊÑ›ëh㇙ë@ä¶ØÞ5-¡³æ†m|ºXÏ4¢iõ‡%šj]4ôc‡Åz«:Ø|ƒ„sdG¾a s ³mø© Ýè5ÛA1>ÇNÍŽ__ G/1ä¢`/V„27»ñax\€Z”·³nlc€JlÇ&G—©Á1Ïx|“ ñêŠU:휨“ ¾p=å jÀZ”·¡­œÅÑxt‚E Â1 ŽHFóì ¬Rã^96ÔÏw¹Y×¹±ÍþyÑE¢¯Kn•61¤dÇ; íc|µ,’ÇXUº€èIƒÊ^ñÐ0éY¾YŒáì¿4hèÓÔò>>ËmNYOòÄ ˀðè6Iø‰j̃Ā¿ŽLºÁáŸ9 +bØ B8?´Tá¬q§[¦ËËZl®Tˆ"˜ 1ê sŒ«£¾õ}©ŽMìz‡+Í2:ßà û€†;>—FÛ¨¥1 éräHt´b“Z•oĬ\KÕ‚1VÝjß}YïåÀš·H À·Ä—fÅç¿ì5‰Î¥'|`CFÄlõÕzµi[HGÆ W¹ƒ‘ss¬?0˜`rä ”v;×"èÒŽOcºVI#Áž¶¾X_Õð#YcWÔ¾¹?>5;ûµ¾×ì…2¸Ápà¼,#_ËF‚ ³:Zé Iyvm6‰I4­vÇš¹ð¡ÕVÙØ`2äJ744iaŠhYÓá{îEÕWR’Aä"¸ôáÖ }§aâ‰|ù÷‡¾è¦!)_ö6âdr̃ˆÒµww. ã%sÈÜtŽ&½f1¦Á»vŽ”µÖèÃSu±z '÷—é‹"_y &¶ó QsÙfì›@ìL1ˆ|±q|f,nW‹dælšÙMøÆúÞ0£ãÈŒF_®Ë06û˜ó öDL$Ò¨ Y‘7Òì¨5ÇûmüÔ Ô 9M¢b4[#…»:ØÚ¾Cîù¥ÜÍ+#ë` ZÌcBq¯<§.ƒÅŠË@ãkltpõšÉYvjÁ5‘&V×%ƒ$;ßÎi ‚ñ¸œðîVV¥ý¢të? ÇËI ˜QÖÁ=êœ ©ùØô'µ·¦£‚Æ«;œÃ[n Ônµ;‡µhE6Ê16!bÙ~uŠÎNó55‡m×KŸUÙÆ¼ÐËnEï ¡÷ÌD N,@䔸"'Tk5|^ó'îNëØÛrA3|ÈÓ¬¥²ø¾ÿOÀ˜»½ñH=AОˆŠ)@xÁ¯*nf´ }ÐÌèÃ.b‚üHQ]h¨›Qtæ¤}£Ñpl·Ï‘wAcÃ+[â{[‹à$ÿZ~äÆ4o±›¶ÄÉ…‡~x¹™ ¬±FFì;Ié‚Ûp‰„ÄЉ]Ã;»µDŒtØ;'1#¹ì` gƃ}dv#_5ÝQÃ^ Ï.Tm5A)Æi!ëÒèbÉp Öµ1ýŸ’(]}êo=¸øÐȽ€I7Þ3ö„VÕh47† nA¶–D†Ýä 8ÑÎܵ•c—Üüù~â ¢ÈĘ(/ª›ÃßV$/À0Úµ~çeßÉRD4±]¹³zijóª/è¨9ØÊ(Õ8kșۡv!Œ^¼ý5€tâðÈʉ.ßðàͯ™›•¸æ hmá+gcB>7=?1¥]:ÓîãEôú±‹!¤°÷waꇸ}¡&Oô9EzÃ+öz™:—^Š›y3‡)×!בMޤ¬ë£+!#]“q„ƒÜv0©þ¨/~Á5úÌ…¨uWÆuZ/9‡Qé D’|àôlŒ¾«6t˜Ì5øáz8ï:ÌõAgËCd>þG/„v#7îµ# ƒ6úèžn Ë2JŠÚÜ<ß2ÎÈû — k‡ [ŒÛzMJÐHWϱqëÓpÙÉqúÂ# ¨¹UÓÚ0v¨¶PD‡:¿Ðä4;±¬—eœñøâ@Ì^¶ÌWˆ 5hó¥?˜a›q픥Z‹-»¾0¥È'>ÙÂMÏú ¼LS:ð*§¾CÖ˜½9?d‘)F4ú4 à 6\áй~FVÓ±…Øß;•¢µÃ£k$j#Ÿõ]pd“{Æ©1òk b|€COƒŒ˜°¨N5<‘{šÑÍ×¢ÊM,‘êãŒâV^”!n‹Ý t 'v®«a±Gæ$¤O0¶Öú^¶Ž±ie¼°µEÜnö\iñ/ bgàô”C…BðªXë1ÒÝÔàðJ.¦¡ã`üFпùOâ¨N:ùà F1í¹þì‡Q¦Ck@ýØ"q1ž\M¬˜ž_õÜk}Ÿo¨«x¯%ÑO½Gá,œ9LIš_î×^ƒÔò,ÂùPæ`"‚,võ@œÈуàK;øé×h¸ ŽŒƒ ”ø˜äO~eÛœéÇG|Eß%ì0{ߟ)¥qF"€i´Y‰(¯JÕ£]zG†c6 “sD¾âbÙcG°µG_äÔÂ"C¯â¿ûXáÓ Î…”>܆¡=±„#r]œ~Ç× 0x×íœéòà£Øú”mu&z-ö£ƒb[sLbC¿Jb].t´(ÊI8'¶úˆz8€íÖº•‡ZŽË\¬ê ÌGÇÈÃGÂ^¨Uu¢}×Iµ»åÄ‚™cŽ•3y›ÓÔüD*¨|·[×RÀG+Ï‚èpÆ;¦»H‚] OYN…ì¹qc—¡ô¦ÌÚFèkð5¢×½“ã ÿ[1¤äZ¸‡p3dú¸âÜ,WÕp’Ï.ƒ}èÛüúÎ^øh(£ë½Ü#¹K!Ñ:—h“1QU‚´òî#'¡4ê2]FÍѺÕ†fG×ZÓÁW«$]óÑbì%§=r®õ vh¨YãÁWl(ßaÓ]¾¡ÁjŽÂÐhšF=1­|«Ìqì¼*j1ËkÃùWǹ ,  ®Ø;Œ ¸6ËJ÷ÈZoE î9ܽŽã5ðŒŒ‘ñe߯w|"¤­ Wc§*|¥Í‚Ëu}}q­K”ã£+U+[®]5õ5€-ý|ãšÚ›»´àÀ0§Å0>ÍóL¢ã0Íè3>>Õé@P´³‚ïò–ÿ¡H\È£õ@'”Š2ÿ"âñà®\’G«W›0œ9†#6züÔwјvMï±f<Œ?yœ¸—§žº'6¾cálÞà©{¸h!ß>ëq«ŸÎÄ pE0¤W$ÞOn.•e´@u‰ÇoòD!yöú—1:¼¸2’†Ük S(§ Ÿ˜ˆm5KÜœöäoÜä)v9 aWOåBUDðyO ¸ˆ(‚-×Ö½ø *v©WKjœqT­€÷/Ú9@ÇšcáVº14äm›£ÞøP³Ëap’'xCƪƒŸ€ä’¶v,\ß=QÄØ•­†±ˆT™XxÓ÷ä• í}cކÝð9ŠŽoÜO0 Ùhõ]~cP0xQ ‡Žkih» ¥>j¢[–LÇî ¥:°ÙLMá²›øˆõY|Ø)À¢Í“1¹rÈí‡N6ý%.ÄÆ–>?Ê—95í }1è;ž¼2bÛZ›€ui)‚Íwãn, HÓg€`Ÿàæ{*lr#5d€ÜÙaÑ\,bÛ5Q™êì`ä¢í«t/0Ø7þ2u Þ‡;ê±ïxDæÿbˆŸë½1™‰ô…‡\Ùá9÷äLˆyäˆ×ž3—ÏÆ¹1Hss,=Œi¾{Á\ 0J_èq¯xÒ&~|÷ߘ³s“27^L”Žx#EÌ:†~Úõ ë  šì §í{ëèZ[t½€E†)IQZ.Z5Ê"¶Î4þëMñè®›…•õÅ[¹Ç†&žÓ"Sœ$=SPdÓkÝ.ò­A¤ýxdÀØŒÐ̨¼ùbmACÛsTàónç¶ÐÑ™›ý^Àº–3÷ícäÿp’®€½뙺ƒM3ÆË®Âˆ#›&CüžeH³í9V$, ×¥y`[dK*)(°ÎÜt~b³Àè×›×åc„׉…¼‘¢Öb õÈ®5eãÕ;ÿ½5Õ9ÁØ‹<œÎNrôO‰ÆsÓ/|¥˜1ÔqtD ¾z8ùâH;5Ùu]b ÂdB·f‡ÖX5Ž Ò¼SPOãYâ%`!écúÉwÏ>–ÌöÁÃd©ï0茽׈K¾Yc)Èu×µrm~S!B7¾á¨k=A„Ä}!êØŸ9Âgr¯ 9¹T-ŒX7pµëì,hó7þ0ùe†?s, Â&¦uBœ‘g(3uåAá1î5r¬Ïí‚°=ŸvVàVwĉžÝfŽ®zUÌ&qÁ¯Ÿê°¶æ+õ}UHð ‚:5=ÉkTÇÈ”¯.çT#@ã©ç†~ì ¨ ëtþlï82ž>×GPÖ3¹ñý®M}îý Çhügb .Œ 4î ‡Ù÷¼¬ÏÎÃ:縸v:‡]Û(½v Äó+0ù©ç4¯ûžf×õ™$žàœC­í$^ŽÙÕ®d¼ Ë'@;MÐgsím?Š}>àˆ9ç‘縎ð“|}Ó6`C¹†¶'Ÿ|21d„À¨èèëTl¡} £݄粋9M–éàÇÞ³Oß„‘® VÝÐõ"Žzo¦LLäE;ôxW1„“6)Ãw¯É[k£-O y¨ ¢Œ­iø¨Ûê£=†ø+ÅEsš"È°ËæÅ.Gc¬M!˜äAq6þNÊÌi¢·ðаÇ~n¸Úác} \}AV5¶4älÍ›>4“V̾pÑOÖzÁn&†J>hçž6Ò×Ü=9¾6ŽˆÐ\|Á#S;âÀóUUpÄàˆØWcK«sn¶ßHÉ)‰’‹¡d —ÒQ‰.¼{îšë¬¿».i ¨1™(ì0×&Õ`xÓ–xé³:ŽÆŠ}›=jrDtàÊ‘:ê k£"tHÀµ’à2Âw¿ÕßiEK5ËSÂAs¯ÐåðÕÊ=@KÁèÄÅ€H`Ù-y+í>]GÕƒ]Yص/ÿ^«e\\²FèPî <ÒµÖÄDìˆZfÆ•]9Ek ¯—7O錪½:Ù=6´Ä{8ˆ=òŒ×¢¿x˨>zþaG½XóÈ÷£zó×VFŠx®gà9¥M¸É¡kX\Gšû@´YÏ>oýU€ë\8\¬v8\évô!ãC> Ì&ÄEHÇŒ9ru•*D|𓨴>_rÄ<;°v¼F¦_žT×8v½Æ&`¾dd‡ÓlÆ­1±m¬Ë½–ßÚnLAœø$bxv®q‚›8Hwï³ô·uàê‡ö5P0÷r<ñ´Swž­Õ†ÛÜ¢@ÇõÖûÀ±¼ÖkÃæ¡zìÍ!¯ãŒAcÛ{‹1QØ+²tYï­o-ÇÇ},µÅ¿KâžqYÃÊŒW† sÓ"}.$ë5ƒal$[`·Á‡>v[;oœ\PÊÃlêbiÕDfP±5ag*ÝØÁgN9À±ã&M¾œO^4¤Ÿ kéÙe#¯GdÌ7>Á¦åÝäÁÉL¾jÝXê Mk1&Ua5ªb›8{9ÇÚã•x‘5‚X °^@;ÒSq9£ëÅ _äêµ6Ô¬/9Áq¾Â½s ˜¶ÇËçÊîã-–\Û«åŽ#DN­Cƒ*Ú »â±Cšm\¥›Yd4ÖÏ6lW~ç0ê–52;ØèM¾!žü ”qíÓ]ßÊpšl“ÇÅ]ÖœIF–kœ.ñ=b;æ²ÕÛ‡ûˆZGtɳªôPÀÀC[*‡«ÎtquÇXÃàÀGÉaLƒóGý¬-Ø­åà¬/ýZr$ ,ðÑZŒþ F/‡Û ŠÁÒuìÐUÑ´s·ë³vF•n#=ªøt•K£k¤©~ÇÏL€K6óSáQtˆ­Þê‹ó´_7$qž¦ät´[srÊ×Ä8¤wÆwü]ÌåÓ¯F2NeZ³ðù‹žÄHߨ挱ò|¨¹p¥O\D€Õ51äà<Þå,´ÔLO;æ¤ý7¹Ðí<,b²<}¦Jƒ‘“Ñ"zÿ‘K1»dç>Ð3W$2ðj!Cäb„ŽUê aO÷ZWÄ ]òë oùºÔl‡x£Þd±ÛÌfŒš¨(ÒCÛqýÖë`\=Ä¡:Šä¾‘Ô7ûá3€¢Ú‰gã`x‘›8Ã4òc25F…¿iWò@<ñ4ºìi8V6ú¢¼Ö‰#ô]kåŸý™ÿŒ`7êÏ—Ãq‡µ†²5ìš‚byÓ§¾qÍ`ÚK`µˆ,kß{.’½fÆþpÆBºìŒŒ8ÈäÛ8ÃþÆ7ÎOTLt!f¸S¤ÃjDMgØ´_м\Ü¿¹j•1+QŽ{ÁEvß\í x•Fó†ŽÝâ+ßøveŒÜø"C76NÐlLŽ ™["„´Sì¥C1­uê~êREíò¶õó¡ÇZdÌ„¹˜ðà Õ\ƒ³æ‰…^Å:F¡!R6õ0Den‘žœ‡ [j: ܺÁmb‰¸qð‘›üÏÔW_ÖŠÿRÈEt]k žrZüË`GÛïÚ݇r”Ôƒ|ù ƒ˜][=©O ú;†é&†iÞÃØv$À›'fÙ_cÂJP6Ñ8˜<[hyºCNÛXáËZ×}tùîI‰R8鵟®£Æ_.þ<€£¨ìsñÀðÕWòNf,XÌ ¡u¾º—( fx ‘)Äȶ‘qvÞ~‚¹'Ÿh´…ùÏ€9⤮(û`(Ûp0’Bìéè%¶\#Ï÷9W´Ð÷H< 4b¤%‡pœÿ£q WŸÅÜﱯ(½3+R“[¥µ*Þõ™XZ ,Ì4åÃ&µË˜ª¾úJÿηÿáS;ðÔ§::¹9]¬²OnÔ*xcÃf[Á­­â;Ýb¾ç‘اþÑ¿šé3—=·ñ¹ÆÄÚªØ^,ÍôƒhoüOجÌÎñ}ì¥^DŽã£äŠåÕèWÖ‡°ôƒó% $jÖ ëC\¿â‹¨hX۷̑ӹC{ù°Ï¼°¶­ñB&ëúãô`]p|sŽŽWz¦ ÖuÊ‘ Í4AôWž<"ÓDÈ`‘ c-Јƒ¦4Çf³Ô\ë©‹•í½œç3!õ×{ærH8œÕŸ}êjÿuЮ%êÿSÖgb_ÃÈxÁpQ ¿æ–\øò•øAë?>ü%aâì<ÐkÃ?½ž[} üë1ëbBòùAùêRA®i¹Vñ§ÔмÐÁBl`ïü4>=ÛRùSÏøòa¯|›Ëåço9.wˆè²£ŽV‡<­FOACˆ›u”´"ˆHý‚”šûßZË3xûŸN8œ_à™OB”)¥©1.é–qâG<ˆkå¾ÏqT6š)ÎÐVÕqý FžÈ_ã%‹¤õϼàŸõáy,Ak:ÁiO ÆØ•TöÔŽã(!Û~oHò7Atwzl5ãÇ}‹ÂxF–$y®£y/1öR„½ò©îÅ-êpo]s>á+ë¡ç|ã§÷ÿÆg}¡›ëÓuH=ßÈ”¦pÇuÀˆ¢“hú m‘M<FŸ…¸c\²í›^”–d8±ñ¢ÀÄȱXÀ$·lœ0è’x º”ÍÏ´ë \s×~ƒª~"ÑÇyh Up„ß±Ù§WÛžX6ŒZx{‡©·î ŒùN9vcŸ1Ë"©%ËäE§ ûü72 ˆ-1Ò'›Lé!Or_yôÈ 3$.ð^|èÈÃË‹/¾x{áÍ/X˯|å+·¯ýë·7æÄ«{*„Ó:nm2´=´B":Z;øÇ·šø× L«_ðúˆ2ø2#cÎX‹éo=ã¨;úÉØŸQliÕ êX{P47—tDŽ‚Æ±}"(§‘¨eÌ£_Ì×ÚÅ™·îE•oé7¾ÁÃój^4?ùÔS·ïû¾ï»å/wݾóíïܾðÅ/+ù€1ª<Ô=ý¦7Ýžî9ÇßøÆ7nßþö·ûbÁĈ©¼,m®˜%!|ûãÔõP뤑‘µ4ä†Ò–ÁÌ©u”<­\/bÞHƒõ¦¦ ÕÙ±¼‘Îd ?ç¡s0 Ó¯Ÿ]Cׯ8—Ü]¬t;çèi¹è& /¢€pcodq£–¥hÜXLÒKÔ!{%óõ¶·¼ýöü‹Ï§ö\£šý—¾ô··¯|åË·|ô20WBè[ W¶¡Oü4]çF\pZ µgëu¸ó0ãÈÅY;P]O _xñ…ÛS‰ãå—¿sûú×¾ª®z,˜‹ý Hç¥Dx„;º9´@+G½^Ån Ð-ªÝïÅ“ ±ÇnÁ©_DFOþö‘§T}â̱b„¢6í!! ԬܳÒA"£€ ‘!¢ŒctX&™2-Bte<æñŠšÀ$B¯/ðP#̺àð €Ìe] S  4ýË{ÖÁQ(há³ØÐt™†ä:ˆ™x d žŽ„O¦qÆ (}•e߆lòL¯£Áé£Zjp_Ë®}®'£ÇrŠk.Æzš { ëÉ.¢;7Ô#ùZjjŒŠ·À±ÉÞȱU‚£FdÎÓå`üh© auò¦4e³ÎøÔÀ§U/JY¾ÆÐŸŒöþŠHØK74H;W¢²79¶P°?6rÆo[!ì‘gg­áq’С .®=ñ̺5/tKÍ$Dê£+(µ!€KÙÖÓ*ä; fÜØ0Æ_±Rv€B~дÖÐn%ÆÍ˜œö ƒC\«[õŸÚ„5b4´¡nÚ Mrt:•÷žÈ" ¾*§ö‰žGrÌld“g cOÛ‘ß”ìšÑ·11á'›ßaƒ Ç'Ÿx#ñFIÀW-ã, ň \û Φ,”+J±/ÄÏ\ôÊ…¿yç³ÒGƒÃîÚQúM¯W…ïŠ&*V‹Û4Šd·eZq¯ƒÑŒó¾ LÆ›¶5К¸‘wûÂy€.æv‡«±ÖÏÆAl“!\ùêÅ/¾áSœ'd=¡Ö¾|Œ` -º±0&ø°ß—(‚£Ý›.q?õÔÓ·o}ë›UÍžËW¾óí«o8ÊIþõ­å'`Lúæw½^­cÙP·Œ³^\Z>\®]TÑ‘ÎÆïD¬/XŠõ…|\ÐàÐy“?ýc¸è——X؈ý}…ÃC#Ÿ3†ÿê艿ñC¸­÷r,_Ǭ)íÂ}¡!{$Èxã¡Ò(ïîÉ'ŸòÅçPc͈ÈCÝ·¿õíƒâa“߉ìyVVx‹'F§ÄŸ~¶ò¿nªÊ9 *pÔäÔ°.[C8#_tða¡XjO<iÛŒ©a'<ºÖœ}ÃÃ7\¬;b`ndŒ_×´èî0ç¡ALüŸP¶£ahç9äOú4P ‚Øx±öÞ÷¾ïöÅ/|ñöÍo½„ø¡½ð ·¯}ík¾óZOÒ6bÖ /2Ì=ÿM¸qƒ`}ƽq·äPkôÈ:*%±×ùÚ™\į°(¦õ§ ¦¾Ú¥úÚ5>>A1Eá€Ö™¥·Xd£Ȩ²[¼Hä —a[â ƒòbÓ''.¨qŸ‡çLÖ®©½—”ÿ,d)6‡Õ'8/Žzñ;Š`¸ðÈÁ®“(y†.’¥Ú=9’¤6 ÈDÎõ2ÂF¼ í)0‹[y«n<%¯ŒÚöÏdn’dµH{U~ÙµÀàê_ ŠŽW¨tê‘7&8°1’t½Ä]²™À++ÉFŸƒvÈX ,,RZ¸Y$¸p>8â-dÈq¨ƒ‹Çqî/ÄÁ©ANhkC¹‘£Éây’w™¿åø­o}ëíÙgŸ½}úÓŸéÇtnýuIÃ5'ð4þýíÊèɤ_i‚‹:T¹¸ó Ù¬„’k\ùÉ`Û”1 á÷¨YúõÛ9m">8Ó yÿU=ëMADXË`)÷A uuàØ²§^ιC$Ù°%ß¾À†öF‰zìéqáCþjâî|çoÅ @ÀXLÏ-â¸åÚþØ9Û»½ãïÈ;É_½½ôÒKyì»ÉpñpùÕ¯~¥$³Ó›žŸ”OÖ™uš ë7¬C׬ï~¬e)A'zr3/v÷XÕY‹°äm×5gq‹†€žÚÉ«:1¹NDJÝJâXœ ®/QðP‘ù:0øÎðw *Bn ‘cLJÀà†°} ÄüJ>¢õþ÷¿ÿö©O} '·Ÿú©ŸºýÄOüÄíÍo~óíoÿöË·|ä#·ÿð>r{þ…o/}㥸Âãló®°†ú"àä>Üæ†Òx°«oD{ѧ¯©ªäo²–2†Íô¸äÇß˯ôã}ˆ™‡§žz2ñ¿\¯+®ýÔúªÇÌÝgF"‡èð¥3vÔsTè2v³Ó,k.Xk®M‚ô8·‰pdˆë¬°»}_ä >Á]q] ì‰'TÄ’ ëÙëzãlT y8_äZÛ½–flŒÁÃ?½†ÈT’(z_mJ׋³®ï뼫o<˜s —~òS~k‘/èâ<I]Ñ͵ʼ‰‹s#1q‡¢ùA9#:ÙæÚK—é¢u2¾ÏçNo@1€ÂF‡X§ïÚ`P%"¸°ëbô{MµÔí @éÁ›KE]Æ,wãœ'þYØpSÓ{Ïå‰D]FĶ5Á Cvá.={¶Ö'š3¶ vÚ®Ö¤³ê=%öÔVýÑC^·uåJJu#$Tg•8>=Ä^¤ÆÁ/J§±VýXTdžçÍÌL¨ ,Íß?s+±F†~´ó،ᩴvŠÓ¢LÑ›9Ôƒ½ÖíØBNÃØLX=äÈ9E y¡ë<Á6¸;ß]ír?>¯/¼iÞŸÓG™«È³<Kbk”™¹È7d±‰K—†œkdä(~t×9+[J¡…æuÄ ×B‡RflÎÁJ€ MlÃn4€«*hè"º.zØ5X1!Ií)[’\W°²¸Ñ(?ìZô¢$íQç:›FéME.Ä‚ )páçĘˉ/fà®XOá\`Éñxdb¦fÑ6žv똭 ÔÕÂõ,k1x:%X:§ÿkEy wO@ÆLO&Ø2ÈCAÞ½üÀ?àÃÿ¯ýÚ¯Ý>ó™ÏÜ>úÑÞ~ögVÌ{ßó^߉»š;‚rÀÃí5/ŪHwls¸_¬75ÚmòÄǺÄ7ÐI¹DÍÆÐGüJ@¼˜ùøÛûÞÿ¾ÛÛßþ¶¼CÛ‡ kºœÀ¡Ùqœ¹†u%megžN(óulM ïžC$±¿z{×;ßáÃ"Gƒ\& _ÍÍøyØçáÿŸÿóÿùögög™“Oß~ó73ÿ_‘œÞâÍÿûßõ.þù—Ù9ûƒ?øƒÛÿò/þE~’óÒíû¿ÿ]·XÐx¨Ù‡‹îÂÞ@Чl».W?ó-ÙÊ{´ŽØd¸ç6ƒºAZnötïëŽ7<¨³¿=Àp>Ž+Œ gòÚUçÕåPÔ5Ã>6§öÕ²z^_Œg…ÍqóEº<üðƒôáÿü'ÿäöû¿ÿû·_ÿõ_¿ýê¯þêíñoþð¯Þþé?ýŸtœ¿:¿_CF¦yM\ÈÈ„ØøI/Öˆ*ß½F¡ßf3OÃ8ÂqƬ¥çžεÄÃÿoÿöoßþøÿøö[¿õ[·|àý·ï|çÛ··½-çŸ‡Þ álŸxB çz'&]{Œ”u5kÉóßÚ'±…êþ(ÄPìÉ­ì­hòwÏ`š„ÆPU™²Ñ¤‡áø´^¼˜Ì«ajÚ9V IL›!m;®¤ V#šö¡«9Ž“‰lZ·?Ä—ÁD;ú;*ÀÙöÊ@óFB®gÌë»ßûžlï¾=÷Ü3žëýL{²•§lÃ7›óç‘aíŒÞEÐrb»†9Rb¸ºV–´µÄÕñKÇA= ›Ð#¿ð °oŠÔ7ûÔt|`­­®ˆ„Ü|3‡G_,doŽØ ïÈëåÓyÁûîxw~Z÷ÞÛ‹o~Ñ:êõ3ÜÐ'öžsXGÀ|‡ieô¥F:Ië®B´Õµöi¶b9bcdðBïZíz5?íõ’ÞÁA;¶p£‹ÐùÈÄ\oˆ¥NÖä"a˜Å€œ†MíZßs@]ˆöõªÁ‘·Æ€±M]š¨"äo;×éØ|èM_êÍ\]pÞ9’[uÜ×Öä ¡v²«%²n;—Èù©²ýý !qx=Ö¦LÄnËtÑï¹ÛùãYcA°ÇßÄ ÔÆÀºrCÏÆa´õƒÐtžCΡûø×‡tì€ë³ÞHòÚíÉñSBrÞLÓ)öC²zSŒa/zü80FÔ(3®ÓšAâ,j숽ïEî§:ÉPˆèR¶È üz×®¾ºÆx¼kØRÖYÎb»Ÿ ?¯Òõ÷që#²r6òpòð®-ªS— 7e¦(ž\}WÁÅ|9Ç^’«ß³~ÆT ¡Z/Š<ÓÓžÐôý'‹Ãž~p[kYñw-bclõù|ù&õwÿî“Çï÷Á†wšiÏ<û¦’X cG<‘ÖkeúF¶QÉ{ò+{Þ™(æÚ÷GcÃE¨8`–vêÎ œ 0œ9¾’9{úé~”éÓŸþO mÏä£L/'s4\%‡ÁÆPÆÛùtøÁ…vìØ8©°iéØ×ºÀ¡ŒD{‚»ùnëg?÷ÙÂgÿôÓÏøyl~/F.€/'™_|óí3ŸýìíüƒÿîöÃ?üâŸ~ú©“Ëwò±¬gžyfXn·ŸüÉŸ¼ýØýXb}ÍPO¿)x/lWüüÉzÅ_+Øß\ «¶û|™çµÍ:gþÍdó?6;Ñžˆ8›àÖ/׌Œºê°ÏHÇSwk½ˆë¤>ÛO|‘e«&pÞeŠœŒ\,’–¿ë4üÌ3¶ùš3¼þSpNóž3Ì—Q‰Ã”ÇÓz,†wÎßÿ¾÷Ýþê¯þêöÓ?ýÓ·ÿãßþÛÛ‡>ô!”§ï_ÿu2ðÊË÷ï¾S[üáº4^‡Œ2d-Ø!fãÊ8­!öä (‡fU}™+ù¾é™§o_ýZuüd‚ÆoûÛýÉÒgžy¶n&úcòQ:ç ªdø ?õŠÖœÈÏ»l æÀ0¶ý\j‚‡›w²)ƒ­s,½XÊ@†"µ`d$ Êî&šV„;Çx17Мt 9a–Q:µÀ7²l‚Ðõ]Á¾sEÈÎ"A‚v­@P.½–žH±SÐ×úŒkÃ>ý`jÛØ_}™‡ÿg}áÿ“¿çò;@/å÷ü­½ jØ—>®5éçá¼i<Ĭ1[­Ç_zo;×àÉwC®‰yMWœ"m£—9‰ñúFN-ˆ¡>‡H'(yÌidÔ,ZïÇH°íæ³KúoÊuó¥o¾tûOw÷…çž{ÁŸ¨J5Ñ—C¬‡0gs-!J3µ§YQsbp»›7šÂp4î`¯{ Œ4rÈÞ„†Ë8Œ(š9Rài>'e`ö0²ÝÍ cjÜókéÕgz““² ½9lkò0„x;‚rÅÿªE­oU<¤v0U·æƒ>óo|ÍÞs.ôÉÒΗ‰#‡âY­?…LߩѨq1¹Í'º·&ë 4øå÷QǪn±šŸXȵÇÊÌs&ÏœS¾àñM^zQ¾^J¶£qâ>¶Ø3DióF§;®\Åkò†Û“3J‹¡=ˆ¦&O`‡ÊÞÍÁ©Iåι¿ŒkÉ(œ9\ôI6c˜þêG^3Ù"™›‹6æòpdb6NúØrÒ¯sº ıs˜qäxèÅM’ØF²˜"ËSuô|M\J$vô£——»7Œò³iÅÐÛ8ûb§õ…6Ï(„¦ÃBVÕ\wë;´ª vX–«žO®uàžï$½õ­o»}ⓟPöÁü$€öÉO~òöþÎïØçmqßü\É+>2ÜõÓr€lê&Xõ&Œ]mÑWÎÞ^öXƒ`OGçéÌX!£‰ ä\ ŸÉÃÿKù=†w¾ó·ô?ü#©ùsŸÿÜíw’ 'Æó¹›sÆh(IÄw,ñIÝ'NÄ)iˆÅ¿“„È“9qTÊõˆ_>Ëÿ­o}ëöÏ~æŸÝÞù®wæ£<_¹ýþÿó‘Ûg?÷™Û³ù¸Î·òNìuâfÝ3Ÿi?øƒ?è‘ÝŸþéŸÚϻߓ‡ÎOÞÞ•wÿ?óÙÏT–w´hÈ÷w×>7雹¬#£Šœó·s1Ñ/îzó­®cd11÷Þþ:·í\`-ÆÊ(ŠFư5Á×i!1=oîØÕ¶%f>‹½,VB=aé¼ìaÝ•¥<š+h.\GÌÁ›n—ÝZà8¢¯‰•Õ7° Wíçí9o>õ7ãøÃþðíCúPÖÖk·þ½ýÞïýž/¢9OþýÿýïÅ|ñKÿÙßpη…FR^¯_yî#nÁé5®„JM;›Tµç›7ˆŒ™!µ6va·o~³¿ãÑ¿.Eë»pgŒu=0†¹x¦^\SЬjgi×ùµ”%Þù†óHJα4¼Õ-, ŠÚƒúlT‰ :A+Ÿx@‘HWRøqr㳞ç¸\~ÖXb‰¼É˼t8ŠÑ‰ z×"|òÝ^€ÆÅˆòÕŽ¾7¿žs’Škm%Ö¯¥^;ùæÝ׿þÛþèÞ~è‡~HðŸüÉŸÜØž{öykïÓ—Ó˜ž 1mþ½žqk=êÛü +5qy I?'¬iåÅWK¿ç ãðsðEC+Ôk(ÂÔ ót=G"i‘¶¡‹L¸]‘7~êG^‚X~õödÞ<ááÿïüÿ:o”ü¨o}üã¿ýáþaúù=7?×½öâ˜Ç5baAcðüÃ#­óÚ(ð—Q¯³®‹M’XÜ )?úÖß'ëùdŽr§þÇl¬G4©RZߌÄk.GU.¸ôùp4hèÒÚoÊéŸèâçœ8 ËÅXJÃŽc?Z Žsä\O2>øã¾ׄ %5Ð)3NÇ?ì  +%sš5ÔxÅn×â!pÑCNNµ›Îàœ·ôñÚh£ƒØ³ŠqGíú ÂmC™¥0N÷†'ýšéª8‹¶.r£9A&(NŸ Ä („XöxÛ¶Ä#£Xé¾–;YßíZ+ø(`ÆŠÂN.= —u=p× eBOô'ö a-}ÑØV–l§xåw²z!C³>ÓK×p-G¢Ue*éY·±é ‘ú«áôñ¼\u~ö§îHt–cÀäÓ‹;ŠõŸð½v{áùço_ÊÃÉÏüÌÏøqŸÿüçoÏ4¿ãï¼}ùË_ÉE-^ð;Ø×CæA%¦m2Û^4ÅE2b`ÓM‡Q¶;*â¼Çீ€ÌO cá#ïÏnŸøÄ'nÿý?ü‡·ó¿ÿßíüwÿ׿óÀ{ÞóîÛg?û¹Ø§Múºóg>.£GN õ2¾2âºìsU.gÎÓÚ]]ÄÏ[ßòæÛ—þöKx½ý«ÿõ_Ý~äG~äöGôGÞÀ‘ñ—Y^úÂç}qÂG6øìø_þå_ ÊÇ4>à‘þç³ïŸ ïSù A´üÓÿØ Ñÿüç¿pûØÇ>Ž·ôAÀX¸±¥näœq󙜑ªkS²ä&6ªØð2;:çîªE“-§Ô`âÇivÛFеûÊ9–ï’dìâF5:©±žÐ”Ô,16f|‘\@œ•¥ÝHBĦ„w`ϱ•÷òײ^¼ýçÌë/ýÒ/Ý~üÇ\íüÿ£Ÿÿwp·{öÙçnßxé›sA®¯ª×[Gûð¬7×uK¤1)Û‰-]§D7îá²^éÇHôžƒÁùçI9ÞÉ–¹™–Ÿ‡¦Þ¼öºú§.¢9SõT*½ׯýÄÖŸ!‰Ÿ!QOÿá3ã ¥vƂѧ͘yT|§§^+gÏÚ쌷hÄÈV[4®òØÊ}ëœô—æmÐh¦a™Z%>îïhZzäÁ8|Ç îûY•Á”KŸhg‚;†±5uì6T8óÔËü¥‹¯ú%.09gKÔ¸¾;oºüͧþÆãýʯüÊí-oyËí7~ã7n?÷s?—{Ü÷ݾøÅ/槪}AÜuÂ|Ã:ùzœT’·oú…_¿ø Ò:¦³ñ«ÍÀkëØ[  %w»p•¬ru bÆz¥Ÿºó‘½ô|VÐë!PÖy¾2ˆà¤Ûë±ÀË6½6j¹346æ>¶šÕ¶Ñe/÷ƈ´šò!ÇlåÅ‹ÀŽæüsuv=6°"ã‚z¢_t…<ø+`ôdì¼rÓ¾×9€ ùJ<篆ÙyØù1–ØòÍšl.ðÝÖ@Ž+vôù¤YÌS{Àრ>{‘uV‘"aÔ¹¤6´=ê‹u¥ÎÚ<‰ Bš°# |—>¼ùÎI°n8–H”á´—½ÄŒæõ†ã)âÓxKRøF¡×öÁŽ[o(#{P c‘ÒX8œ˜˜vÖGT_aâ+1 ‹{væ6ñä$ëÀr_ÅŒºp¸ÁÒº¨À›5˜ŒÒ_jëILøNþcI<Fȸ qB^g¼Ó×?YÚX¢l ¶<AoªUÁÝè"G<,šàÍÕŰÈç_x^ã¿ÿ÷â¼øøÇ?¡ì­¹P~!ÙÄ?ah#&v‡‘ÛK¬(ãPwvg"'¦Ø/Ó‡„Øèv§ úA"O+©"Zômû€óßþ½¿ç;äH¿üÿ³vð~UÞrÓIHBK ÷¦@hR„PÀÅ‚i²l¸»Š¢ ¬‹eEWÐË.MWÖe¥*¼*RL@zPLrÈMi$$ôÿûûþΜçÿ¿7ôãûNò¿Ï<3gΜ9sfæÌÌ™yt#nèÐa2·Y(E;ç(„ò(ø­¼&NÁÇuªŠV¦îÄE$²#ÀÉ?©ê¥[Êæ–¶5epÒÛOªvÚi'ÅUõJìØ1ÛUkÙ¶ïsàÚ°1b³¥Ýu×ùPv n¹ågN»\WBI›ïE®ª8Ð&DbŽ‚®ƒÁËWèÊPù£öƒVÞTÄ(³«_2Ä{ʲ H[§Œ¤Q€ÿAØ>REªIÁEµ†pþÈE2.ÒcíA0‡À³I‚Š)Óü@IDATÛx‘d!BQ§IÑ~bºà+t¹N,é‹ÂÈC9loKN¼|³Né枊íY °Ÿ©Bº…ÏKa`‹Â ƒt@žIîè£vûÀdîG?ú‘ÿW®´r¶tɲjýº¸–Õ‘æ+>ú‰  ¦¬3è‰üZûd“HÈÅ€%2+A—Aª~V¨Å2sõ /™È¥‡ŸàÂ_ê¼ ž¤ (?]qIÒ±—6ñꨤƒNi þ»`΂áÍý•6¯Âd•Nžûð}ô1²÷Џ=æG °–)äÐ…£1h)¯Š|‹Ÿ‡ëÚÐü¡À¤§üÀe=Ðpà^Ýêé§Ÿ6ðÈQ#«¥Ë–ˆ‡ÜêC§ÜGtn¨ô—½ª&_þò—©Ÿ ^«V­R}’rÖ¤I“êÝ€gžyÆpýtØmƒx üîć'8òôê8 d㎿´Ι |½ 3˜ÌuttT-”9з¿ýmûÙiÂ<‹ï8ø¦pE¶Šÿ e¯þ$ 3E¦È‹k`ƒ6d„záüG¶«àŠX”Ý­Ké-ÏdÂÀ“zsQ¸#˜`d i™Vhhó›Ëí)x÷Iyn´éD$é ¬øç6Jy/åv å7.À•–¸ÌùsÙõ׸·qòݬ—“fY—•W–LjÁ/¾¨_ØÒˆ³}µ‹ 7mÞ`yr9ÌQoš T¸7»;ÞæøPªY(B~—Õד60@;ƒpÞÂDßD¾ÈRÈdäÓV+_¤ƒá÷äù ª²î£ßŒté'`GI#>]ñcžüÈŠþEi)Wd´gÀk¹*æ<–+ÊNFñ—Ó¹Àåë2(Mp“<€·~P À«Þ=ˆeù7¨Â‘ ú1Ú«Óê/“¡úúHÊÒ"ë¦Þ#ÇØÆxÇ‚VŽ{ðŠp+IÒZÓAP”E‰ .è2/A(âL3<€ÿ¥ž,Ž ˜Lšàc†(oÑŸ‹,?ˆ³^É T}$n— HYäK¶ô™†›âè_)'åߟ]nS$~…tEG­‘þ½Ò9ŠÙ ºã~È/õÎmÉø¢Ì u]'b5M)kÀµ)?H0öÀÑPÂ]¯DÌgæª.aNR’;2ÿ8ß:V(¡‡Há• Ç»èP^ÈtB?Çê­”IuN!öÃ+ÁB‹ó1޼”†pØÏ“0„ó*ðG$7@ñP‰Šp¹X3 ^¹¿ªùV'Àüë2 mQyb²ï ˜Í\ L¡CI"7:+ Á!û‰qæz¦€ÇW•ÊuD|8<°óf€ >PˆÈÏ%'AiøÁ´_ø F¡õ*ÎHmS!#ÎÜóéúC%%l‰«áyç'ˆR!5ã Ò?òi:uk„+¬û =TxBá ¥-5.Ãe#„th¯;@÷ò9LålÓŠñújÍÚ¸G<:ÀJ«¾£e¿?Ò™1ˆ€ß+»Z§þ–èÀ)´Ó(ÉßüUœe‘ÕýP™&¬|1ÌS&Nì0®õþÀƒÙ²9D0¤`ΧàÔÃ¶ì˜ ¡HŵT$S„~¹€ˆ,)6DM™²Þnìv^%‡wuçEG%ºYe}añR)³ëT)É _a¢yªÎ ¥pÛÑ£*ìýq\ˈcåü ™ÛàP¦XÚȄLj#ñºtjÀ¸#ÎuÁÅÏSyº: ÒÇC¼ÙÅü§ !/Ô#,C¹©ªÉ“§¸ŽPàfÏ., DÔÿ)?žè´9ä;¸šÐ>Á“»õºžua×BÕëj ^}«þúÙ¦Ò˜zûãÓqNÀš”*}õÏxTh/(l(¾ãvg^Ào~ Vœ`pàÌ׿Ò3ƒ~”èlû*aÐ ÒB?üÁOˆ]áa„D27LÊ8pðn©^Ryàh¸õ¹7n¼8#k”ƒ:ᇼ,]¶L°ZSYQ³q»S¦mÀÙù"·T‘«-6i!“¬ZÖÈ  ñk„âþʺ—cBf,BDSñüW'Èäl¨Ìæø(Û¡‡fœ”wžÎÍนi£””š¤Q×N§´Y¿òÖ>Úü°0úµ†•E&Ûo·}QXb"”uoV¬xQ•–+Ú‡ë z] ®¥:¯W÷ Еütç¦wÂà;ü¿ÓÎÕ Õ´l§ž0IfhCËdƶšo ¤¨”…΢Þò7róW®3fôØjÄÈžõ@>€·D¦mÜ€…|úŒ•°0þ¨ª!Ú.yÊ‹ÎÝhB;hÐ߆…C3&v(/”‡t+Õ¿åŽZ,˜€,h¹Qëq×…p¨ó´Ü œr’ÊκB”†d, ômë¯Cã;—v·Iý‘äž~Ž2 ˆvǤ²±IûÁ}ùVS"|È·i'¦¦§é'Ž:鯳P,~ x$·8Žó+µËHœûBI 2F»$ “Ï¥êGÝö-Kô‘Œ¸6ËbÔ+Ò< Jå¯IÝòƒ´êÙ#µe8ÒFÙü×éÅŠëj[cl®ãöʕȊÌ~Šs,L²Y ð‚…r€ôŸ~úßW<£,ŽQúàIÐF“úZúÚ­ÒCWëäÌc«Â‚J ÈP #O$ï›Ë5μE½‘F1…QÐB’ú Äg“Àªj;]ÈÁn.‹\)¼Tm¬Mô– „µÔ‹ÊÈÄ– íè/4YÚ,äƒ|õUùYƒ_Ȳ’( ‹0é‡R“¨hÊä"šô/–ßAºPC}ýOÈo,⬕~bùUžŒ1‘Mê"”ë@¼*’ìp>þŒ+ÞØýû¬Žø`êZ#Î'|Õ?ÍòÖ¹­òVêÌõg”+<gI­’YŽ =€ÒßboÆòÄé)>&TL2ÁMœòô³ÀÊ¡ŽËPžá ™8"êOxK¶~+°䥖Ì (T @ØñÔOd&¨£@¦_ .ZdV½J¡ä¼æ ;WBEi r"ÈD ¦ IÈ›ÿ^YL,%ÅŒ¤B0¬X*Æ'P½7ñ !ÆeÌÚˆSN¼ëieÕµ­·’=šO0Í  "…ðC®p®àw¬¢Ë"\Q`É% ^ë-“û-ÊuÆÆ)ÎÛÄt|ì åŸ[z8¸ûÌ3’ÐŒO¸|²¢¿L ÓJ™"0[·ràÒ"Ì¢^åÛFƒîóÏ?ïÃQàÅÿóŸßjÿ(­P£tvÎëô{o&MšT-\¸Ð“è†/%ƒxº¼…´á7¢Œ/EO Î)é¸÷†8uu-0_ŒT<‡ŸÙ a7ÏÍ9(p»OÝÝ×¼‹é¶ÛoS\L¤XEòWrE'0_E?a¼•l>ÔD9¼ÓÒJˆå¢.-O”A Å6új2‡q)÷J™ùŸÍ¥Ô††¡»ßeûýr¢Î ü}àœ×0r`íZÝÏ?eaÅEƒQ2çÍVæ#q«]àöDF<‘?n…dåÇ×ýØ~P·6 Ê;]¨½½]üoÔ¦Bîå²?¸–USäÓõj™Åu kÌÚ¯ÂÜ]ßz%B3e›¢&`ä‰_:Ηp𮣣CIÚªÎιµÕs[}y;™MÍ·©“7µ7÷IÆOÛ$[ - †¤o”"È¡pÚÌìÙ¶¢ºâÀDwÇ!ÛWóÅwp¡Láaå“IufsÄD¶pÏ=÷œŸLÌ9ܽ~ƒlþ½bqjç<à›x’ƒ ”Ξ"{´ – í%;¯¨¬óIØ«ãã”)»ºLL-{ÆD{^÷UäÒ›k ~-oº¡mYÉW}´··;ñü2Áé a#u(rÓÕµH}Á+¢Eã +%r´™ðÁNV:c%—v‹Â2G;ŽË–/5loè›Æé O×Â.µ‡u.§š˜\ð’zRóQíyjoïð®ã_ËM™2Å n»È:3^9× ËÔÒ,‡£J°W{ ˆÛÂUD§@Y7n¼V°ûI®æõªŽ¾Emû:LÉ@?p©\ƒuݧwHŒ¸pÔŽúßF\dv£ð„œ‰fƒ’KZÆ]vYwÜ‘+D‡úÌQψ#ÃtRå9r„ûòà«Ö,,ÑßÁ Ï×kÖ3 ð¥”Ò?qÕ,íô…%KªUŒG¢ÓíÖHôÇ<ÖCiãøú97U±PÂöjŽ3Rô)ôÜž¦œ ¯êOxÀ…°èÃG.q„¥K?õÂ5ªL,6­bÂ%ƒ¾ööv/F¬Óù…’=7j@¸žý„Ýä‹?Œò›µ(ÄÂWŽÂó5úêv—ÆøAzù¼›}Ô˦ /Yùz¢z™[-ј’n;bqÅÛ}ŠÊK_ØNœ8É»ÆišiZŸð‹‰Èüyó”³j‰6*ÚCªÁ‰‹¾Ü„ª­2—ü²«ü×È/‹v]]]Îlaâ‰r«ÔÂIÑÓïE%‡Ã Gù M™qA›1™f`¦ €Tø ëþGï®SÁ*o¸.Û2!‘<‰ÿª–—èipò(ùø]ù;/ñ³Gñ gÊÏø‚Ï–ÿB?ËCŸH* š¹˜àô O]®BÞsq4ò‡¸MK­ˆë—ô¹±’2‰Ì’9@ÈOZyM€^¢//`Ç‹3¦ÕM$L8=ëBxþ/9™$D† q ¸æ€ ¼îl‰2Äi‚kæA qÞ~rf)ì…Á‘ÜéŒG0%7c­#ŠÇ<.éO³¡2#è=*:J¹fhâ¿ÿ((Ù„b:PõFßùèX} ££ŽqÞ¼N+šÄ¼óÔwV{îµ§^>•q×\sÕ9N¼£náJÌ03!£fa‡HÉÄMŸ>Í«-ø;;;yT{ì±§LUž²ÿ¤“NªöÞ{ïjW)/¿òr5gîœjÑÂE¶y¦“ÚN‡¬Ø±xI¤òì_e@>ÍEŠI–Êš‘m†ðM8s…#Ý{ÞsZÅà7yò$wàd@~衇¥,ßo°ŽŽ+® ¢£GA¡#deÅŠþ¸Ï¾ûÔ6ñÆÑiòÃÐÙ—t.€¯ÓYey‰PV¦„bÅÚ•cV™`¶Bù; üÒ1hA ;3/®\á_Æ¥¢Î„‹[bpYG”‡ý?gXMa%:Ý0…y"-bm^zòÉ'{nÁ‚>lÁ¾,’C¨ Ó!VÓ’ûï¿¿¯e@àÇê#|€¶o¸¡¦‹ˆ°87AÉ•¹j;sXây¿:_Ë”Âðº²%Ì37õS:Ùª,²BY‡ê>s”“¤ 9xãß(ùÛÃ4£X/Z´È4ýâ¿ðDšÙ ÀL*” !+y@2‚YJS{{».vªpgœq†ïî'øÈ þ§žzZõòÃ0p³2¼aS¬~3ÈŽ”RÄä —J /~ÿæ7Q§üSìÂ>Lçk6É«É#Pú¨›¢<Ä£,Ž=FãAµ\‹}öÙÇ7Bqx‘zæ@#íçVÝnŤÇD9^&lQ …)~ßúORÏàš T›%ÿÃ$G£õ øƒÛÿ×ûàú8Ý=ÒL»@Ù`âßzë­ÞMäâ€AZ%dBLÝЄ,€Eµ£ìh_L¶–ieåwæ™gª˜XutLt»&_dïÉ'Ÿ¬îºë.÷L6«Ïba$pǪpàEYÙT Öª?7mÍŸ?Ïx“–'ú•çž}ÎJöO~ò“z7Žï9P”4;*) gT‡M–u†Rä^Î}DÀ†&S&´ ï|B}XüÐC•LvhR°“åiÞ<ʹ¨ºöÚk¬ð­óç?'“ÂØ…*›õÕR$ùŽ.ðÓ÷ã¸\ ݈mPâWzqƒEê©Õe»È°ÓN;M‚ÌŸýìf7LD‘ù”{`£ÿ,2­ Ç4ËÊ $L˜·v‰”~~é<¹Ð.C?õqãŽ0¸ï⬾*ù•‹_´WäöºQåèTŸN{ýýï_ó‚þ`¥p¾¤Ý0êÙ$ÿák^žÑÁh‹&17ÏaeL¼Íì´¨Àw1ð#ƒ­Žë¤1Ñ AA ±@N¬„j€™‹7ô©s[3Fh²ËŽ'J âÂâ·¿ýuõ²c©—Ÿ©^†x ¾ÓŽXp`,Ê…®$fažÑ_Ðò{ðÁÍ3ÊWêâUê›êþ“^I´…ËäI¥Ó/ëƒt\vpètÉoG»Ù úmÚ):ã6ù}Vrâë DÊ:QŒ§P¼£Á/V(Ð$(6&‘Ž-4¥:5ŽðJëvZðF1²MWäâ/ù Êùʹ}úÓ CX]Ó-p„ËA°òM–|¥|ú™è=ÓP K$¤«cÉ:ã@\é)ç™x"Tr"M@’s`"ЉãvÐg8)O wC NCÛß )—òóž?½·õk¨ãQ|ÿ×Þ7ðñ+am¤4¢â'-?à‰Ë'þÈ×°¯¶òÐÓ¬pð$_ðök+ùÔù–ü§YCf'%àø•ò>è1.ÓAZh/tóÞß|€ü¦´ŽÓ³Ÿð)Ž_¤ zœwÁ©•u§üå7ÍàÎ_¤5/ûé#è<5ÎRNø¢šjhôó£ýhCW»5t«KãÒK/ÍoŒ=ºñï|§¡ ¡NFrÞtÚmHáh¨Sküà?¨ÓŒ7Îþ~ýÚú©mömLœy}ýë_—®¤îXNëtÿôOÿÔÐjuC+¶ÍLäÓ€ÒyPC©jüë¿þ«áØØfø6ö?(;Ü{ï½ Ç¼ñÃþÐðRÔý”‚kú)³®rtu Ut#_Y¿Ôg‘ååœ6mšë‚ôԗ믽ÝO}œ«ñÐC5î»ï>ÑöxC+<¦‹2ÍšuoCWÐ5ôÁµÆ/ùKËiwÞy§UÞÐuž ]ýÙ¸þúëkº¤0GEFtF ¡AÓx³ÞvÙe‚xõ N)d.~ ò_ýêWæ«&?5ŸðHùlh¥ª¡‰@ãúÿ¹¾±çž{:/êŒÞ¥;/hk´ÚtáùÓ"Û’k·á>ýƒmŒ9Êøþë¿~Ü¢e¼å-oq´iØÐ‡ª]]]–¯$.eAJ·yöáØi4¡i :ÜþèS²Íõ·lÓn¥hÕø¯¼ò*óS;Ýd|t€·!Ű1sæÌº·;¶¡oJ8ýöÛïàç™gžayzðÁ‡ü\¼ø“ /}ôQ‡=ðÀƒ®wêŸrm¿ÝnîWú¤ѧ ‡À™Ï;Ñv«†>úfÜšL5n¹å–†vT~ÐA¹aŒ‘2çºÍþfêÔ©†¡þçþ‡q~ðƒtØð¡ÛHVÔÇx¬D–áIi Ó¹¢†" K𫝾F8þ³‘í}ÔÈm£o(<Ô¢žÖÚí…ÿÐüýïßõÓÛØ ‹Rj=6è&$çƒ,Œ=Ö~íŒ5²ÿÊW¾â~–qàÉ?<é~¾‘ž16À3øB‘wö³Œmô«_ùêWųè´˜âþÅ2¡vFÙS¢Úí0Žƒ9¤ñ-É6é/¸à³Óm` 64‰ò;í•:Géû4‘q8õ¢?zìÏzÑ­EŽ7n|cà ÷Zû‹üãµ¼çX“òA;Ò¤©ñë_ÿºA{¦ŒZ°kl§±ÏþSçîT®jÿ¯ò}±u¬ZVRÊ6†Þ§~Ÿ`¬+©ý‘-SÐC˜âëñưès¥="%¬N'™°.‰|gʨÂé‹lè‰%Îý”âk™Ž¶Œ|…Þ ½Ä'LÈ@À7qB'ýˆå£<›8#<ûäÎ~ÒzÚ(å__çôFš(¿aß̳™ôAsàÖì•‚&ñ<ùeXë»Â s-à†. \¯né¥4Iqfƒ %Š4ü€GùŽ<Ìôâ¯qªã@± RûóTº`yž %ñ‚ AA9#¾(ø—ž\£Œb–ýÁÀf™•gVP¬ O9ŒÜÂa^Öú7@ÆG^ÐR 3ðÝÒF…žÂKà‹°ÛnÑñ^uÕUÙ~ߺü[n¨_ûÚ×Z¬Ã0”@hä=‚V6¹hìÐ7hÐÐFþ7ßü3'ÇÙgŸeø;ï¼³:.òA1ou `©X£Pk•¤® ó¦”k—Ò™Ðᠰ˼¤ &4äÑsÀF9ûìg£¥Ó££Õj•éŸlÖ»áê©Hd$Ê6ðÉcílX ~Ö¬YŽÓ–°;q­¨nSÖ£^y'½V~ýÔ¡ÏDÝ ¾ˆÛ}÷P¬TþGçO:”}ôË~”—¬O”@âwA±×¿ìþýd°ÆQ7ŸúÔ§6iÒ$?ÓŽQí‡>”žV—²Ós" J…>åôÁkÚœø!YGÞ£MÐ)"ßÑñĤ ešv¥Ÿ:ïQ£št0iÄiE¾¦M+û.ƒ#ô‡úGÎ…žŽpä‡ò!ÓÚ=¢«º¢ º/ŠAg„ †—­“BðŸ2£\ô”5Â/U;GGL ²^n¼ñÆžäl¥'@N´g\Ù÷„LÁÃèGTÈ«u0×—z·jgÉ꬧ÓÊoãMÇg<ÙÎú·1è£ @LvÉ£9èÔ ú™Ì—Å ”h­Kj\È ­N+ æuÓS™Eñ@‘!/}¸®!sã¤Îþ-û!`fΜيڼÌ> 7Üúž‡q“6&´QÉ¢3¤ÉV(ÿLŠzÒ²{Nbdj¢ è{Œ{¼”v-À™¹Îºê«E˘eM~'´ƬSW¤ü—þމ“˜VGÛ†”µ§²F_—)íê4dJROâè+ZýÿzºìSRööØ}Oú?ü;ý½§7´ÂÝ3©$ˆ×j¯á¾ô¥/Õ0?ýéO†Ìè|Eôîr ¥ ö×0¤±ÓŽ1±$M:p›¶í5Úr×^N¨‡žåŒÅ‰5½Ž ðQ_ß6^pÓ× kðŽwú–ž®gý¯Û‹Y¤e’€ƒOÙ¯ç˜I¿çqŸòã/í¸½ôgŸ}v-ƒ,¢o¬&§LÆÅœ]]]΃ñ˜w¿û=u;uDùC9ˆG®¿[Ûã²Ôê^­¿ ]\wÝuÆeš´ÂÓú:“ô4!׋Qÿoò+ZÝ>› .ù Óюܮܪý¸-ÝÌaÈc‹h*ïÍ×ìë Œp¹P]À+Ç¥m‡ë(êÉí—÷z¼ZL“èËÖ †0è)Oç Ξ´D|”‡¾!û›e†ÎÄ—c©q½Sï&òŠ2„NÜ ‹2 ôædÉx˜Ë ^ã6NìäêéTÓ]"ýžÈ”ÎØ © P|Úãz‹42*Èõ[Gƒdû­êÔ“´å‰c›íöÿó*)ÙÕé§Ÿî0l#¸ÿêÁ‡´¶Ÿ 8âð#ªÃ?Ì~ùòç-·Þ¢/ÊN«^”i ¦ kÖ`“ÎöèPm¥.4>­ù©•bÜ¡zyÍËê:±‹3ª<·§OŸnü˜gi2à²>'3œ j'd}̵Sè¯\Sïo~ó›%/7UoûÛÕW”ùŸÎÄè)8ÇÈìó¨/~ñ‹Õ¹çžkÓh„î™3g¹}`‹Œr˜qšÌá³”Y›P"ëä¾O¼®Mßt†£Õy\Ñ‚dÃ[ߦC½òÿmÆ‚9´H™Õýüº?2O¹Ÿxâ ›~@;íáàƒ2/0)C´ójó¨ï~/¾.½xÑó&ÓAÌihïî'ˆ@®»tI@Ø»÷S_fB´m²O…#íDÙ‹_¬vAã04¶ùÈjš%|¶oàROpÙ °JÙò2ÄfdKšÖôÑ8ÜʘÂX‡ùÈG>ò‘êâ‹/®ûxˆìÓgr1c m™z;\cçªè—Þõ®w¹]œzê©ê79Ô;¨6ùÔ¬û LW1¯;à€ ;{öõ|3¡¿ø6Àç¿B7Úâq¤.€<”»YöˆAç©‚Ê ½c÷4uZ%QL˜×ȧù[·zÙsϽªK.ù—º_ׂÇ׋ÆW푙ŋC&ZÇSúL}èGùn ý5åÛ{ï½ÜŽè¯gÞýîw»Ÿ{ÛÛÞ&SÓu2ËÝÆæeRDmF7~ü8Éïs’ßÓþ ù-ù=x+ù]*ó¾ïI~µp%¤gÅÎG†£%Ÿ2Ì<2Ki[rèT“_ù)’—f·…“DNàõâ1ʼV”â2ã¥>“yL“³2L$Žw¢ Jy6ñj­P±8ž¢Ó¯!Ž4N’’w8ñĨò]O†ï‚<¡q*ÆSƒGR"‹³·–?ð+"”Y­ëDæ1ˆ™DÌfʬɳž2+I¿f±½Ð>f=eÆQÃ6g(žÙxõ¿ÌŽ˜™xœÕzà'¯œÝµ†1kÊÙu¤gG ¶_˜=œÌŒ7qåÌ-éhÁé™­àÊJ|à"]Àêd½gO±%;ÌÚ‚&h6-äkú3¯&ßš+A‘6àÊ,¬žõQÒ6ÓQÃÛ­Yª*WJskXÂæ…o~ó›ˆÃ«þÔ¬¶º¯j “4¹"„?WHdÛëíAàY•eå'›^­HÇJó«å‡ÙMº»ï¾ÛyÈîÐ+̇^ó$œœÆe‡aÒ¤XÝ÷ Þõœu+ lKé3î'ž>/\¸°q衇9LƒªV3tË;wXm¥%ýl‰³%›»lGãtø¬6IÉELSH7N¦]]±zôdÙE œÝ|¡úí[M˜Œ‹?ðOÊS¯4$-¬žæ*3;$ ϪŸ:Ì™6C;WÑf%ÏðL?¿+¹NÓ )ìõŠ'»1¹2& ¤¡·ç%—\Òm^¦É ;OÒ>ÍÝ4ɬù£´qùå—¿&þéÓmH ©y•«dííÞòO“‚¤ >¥“ÒÙ+îa2• üÞ É¶ïv2Eÿ¥+]k“˜ÖÕJÊ÷ÿø½âM¾øÏ_´ùMÒÁ®q˜Gi"ãú½m¬ðÞ} vªbE3x×ÞÞîôŸùÌg,{à•-vãóŸÿÂkÒnú%Vcq¬¦žxâ‰N“u&Nœè0ú#)¥†eGSŠÛkâÿ¬Ú&fF8)¦5_²­Ž+ÿ:WP—‘v0’W½=1ƒ’Âd¼üÁì8LŒà}X˜³–~Þïø©?ý´Ê–ãRôåý¼#¥CçÆÃÊo:Lïz£¡5ŒŽ\Æ\'ã¢>cW&ÃZå/w›2ŽçP™Çé«áÆ1uênyó:M ã ;L8ê ”w—ÝL¯ jx‚ÓþË¿\R×UšÅÀ ñ†ñKãeŽcgÙ]\ïä.%õf–î#5“_¶áÃ;¼Þ¥€Z©û餫ç“>$ù¥É^½K ~`#ZÓœ}ÖÙußvÝuÍ]ƒ„ѵÈZ¥º#L aæf‘=w̲¾kYp_WÕ;gÊÔF ZN™ø0ÇÄ’'ïàÌ>‡¾ùížáXͿ馛ë]©¤oÄðâyÿÚ4yIGÛþä'?Ù­¼™.Ÿì,±”.wRáåAÞÙÙIüÿ’_v>ØU€vëln?©ã… E;ß:U¶3ÂHO¯ŠËßO»lÍ4¥=º ŒQÝ߯ÁáqªŒOÓšG3M—WÑK¾Î¿Ð¦r4ËB~`cLéäµ¢i’à-<>•êÆrlÙ‰‹ceSp|HmÆ„‚OZÎ'Œ1Êþ4ÓѪeÝé¢l7iÒdý&™¯ÙÙꀣãˆÏ ‹iÓ§;<ív'M % ÛW[¶¤cR‡™.ïœ%È OˆtøÞ{¿®1±cR·:Õj¼Ÿ A;>ÆË úo|Ãi’/nS’éܾÉ1òrßl¯´¿˜iwÁ8~üã/õ˜´“´òƒ'“'O±¤\cn•&W_üϵ¬"§Z-t:ä³Oi_ªkpåàˆâý1aÈæ_ð Yc…¶VÑEŸ¯M‚ü’®ÑšTáǾ\;ö'ä9[A¼Ê+­Ôè†+Û5c} ý|*¿ÒæsRŽ\§9ƒtN¼ —ògû§¶Nš‰âyBs¶_&nçwžÃÓž:i¶û`ªáÒ¤at1N«æuý_~yœ1@ž§¨n²Â;Òêb€ºßâ¬@º+¯¼Òø;:˜@µ“˜8k÷²–ƒ<ßÄBÄ„ ¥îÕ¨~h'izÁ¹§Tör¢OàgGiÿ*Ó­„ɶFyèé#]çJƒ¼ìóº}Lß¾ûì[+ž,€hgÓá´s­Ÿ ?ãuý½·ßݧ—>\á´Æ+ò¢¼ôÖ·ÆB 8»õ’wú úVÒðƒò¤žLµÓÝ8äiÏñ8ÇÕžò;X&äßo¿ýšò+ÜôwI~ÑSúÓ”X Ý0ô·Ô…¥àe.òšÇ‹öú’ÒÒÞ ¾&,icc9´.§0Ázl¢SO~F>u_ ¬û夅öžz¬òjÕ ^~ôå¤<Ü·L„Îñ¥ÏwY€'-?&<)_À×r¥ükšÐI)‡Ò¤NâE%Ñ<+qÄ; Çtµ¼£vŸdÁM0‰šÄõó5ÞYÄ…3[àÍ”îÌH\ÍIEx)“•~pNôx¶Vç¡p˜Daô³™!¤ æÆ*[øoašÊDãlV6L‹_Àeš|F9“þš&ò%¯’Ÿ…É~ʵuÚ­*‚²˜~`¡ú”—„ \4®‰£‘¢Ø |¥ÓÝÌ÷½ïÃh»ßðtp̬9@ïMÛM:ÛT(Pþ´•n#°(Þ<[WˆÉO§ùN'’Jpf ÇÏ ÏSN9¥ž ,XÐ¥Ž´Ýá˜U?J(éàHË¥?oþx ÃRi«]qº´ã§CÃÞ ¥|Wup¤v\Lb¾æ0WhºÍ«£P­ikÞ°„£±†Üfc³ÌÕÊlìŠ`sŸŠ«h¤¥ó·,¨^“Ϭ^f=4Ðq²“ ¹ Åaœ6pô†ƒj…EŒ0™ÐhbÁ€«F<[W¿ó+ºÅM*gXõNÇ®éð°+Ư-x+9øùåàríµ?Ìd^-$.êŽiÑM[ -"Ó)û´~ù®§`ØÙHE$•æTÒr`?tQÿø‘g°2¹ãÐT¬8Tî—·ýÒñ ^tÒyhíõR&»ºbwdáÂ…µ2’“äÁm§ÈsN®Ž?þøzÐ&]Ú£8B‡îK÷î^ XñÎoô˜Ñ!kÙ™â•òs[OÞˆVàsÂ…?wü6lØØàP>a ¶” ¶ }pËï¬zO‘òM+×éP ‹‰Pï M÷Íq†Ï•Xþ•+W‹àÒuõ„>@»ÍºMY; (ŠO:¸4 ÛDÒÁWæ;µ Ð7¤bÅâ†Ì–Œ#'Öô›ÔgGÀ;_ì’-\»{ìJ{lLf9órÛzF#M¦âƒ2´‡Lõï߯ž\Ð~ÓåÄ¥‰ü=Þe¹EÆÕi.‚©p(ø\Œ`b“rž+À»H©fl®)QŸ9±$]ºßÞq‡aY¸Ñ-@ö3á"=—<¤ûÚ×.uXN– ˜œÜ±Òœ€LsÙe±#F¾Lº}ð[—8p€–vÝûàú‚—3Lš}¤ûJ¯˜öSßö×Lšã;ƒLxpЙ»FYÊ’òŸ}DÆM›6½^ˆaRÃaXàe¨§x+y…Êç 7Üà0&ú¾…ëS–-cë«M˜™TõiÑPºPà‚ç¹ñj€!C×cr»u½\fº˜ä »È=ÊåÑj!À8]Ûøò—¾ì4ôº]È~àáze¦ºeá?;céòòˆl'¯)¿â)mYøùõ*¿ÚÅ!ŽÔCt…Ç µ·)á =ˆ'¿P€sḺ¤©Gµèl9ö8]¶Åćn…_z•Æ&ÿh«)«¤qzê-àòiæi%Ü0àÊ1-ð'}^øªÇ¾Œ6%Ócfâï9F²c¼‰±³™¿p¸ME98¸ ÍŽ÷B{¼ÿH›´F82zfÒ¤÷¨/U‹íŸeÕ*¯ "¼¸è—üRÌ‹ì—àÈ®(­˜¨_Œ–ø ¨ꉋ4 '¯ˆO@ò•“ §“ù/êx2„)OZ)ø•&l¸@€ÕÓÓ%^ùƒ ¿_„+îEq”pÍü”Î顺)[àr0‰ƒÀx[÷°Àᅧ'xyçn\óˆ(уý­Vnm ˜“m‰ñã¸ßýª«®´-ùO<é+ ¹;XÆÁ’V:4mXì&q7üô†Úv™û¶ÕÉ;\7›øMìzqêHýä”óJ—ìâã¬wk`Ó•h|D&lú58×öé¸ÂãøèWÚá6êŽéd#ý¬®“RU½õ­ou8ö«ŸùÌöSFlU7ËÀ²ó1±ÌC®+7øº3€¹ P"§ÓÄÅOlF¹CŸ²§Ý½” Çi ¨¯ˆ'õNß>½#îô3OóýÔøóš: ˜ÕšÕk%ZºJNõ„„©€®7}P9*­òùlnq2Óò;åÍ›9s ¡[üööŽš_œÝÀ©T]ë£/1~”A«ÄŽ;üˆÃk›aÎ"à4¸Œdtõe|@ƒ‡ã°û”¢f¿?‘)œVƒüÔŠ»ìŽóúÀWl_ØN#Cê C&:;u ëâlœ×ñ%ÿ£ô4±äMiwæV”ÁíC9s¥í0Ùœjj:¤TøI`Óû‰ó>á÷ÝD§¶ÀMCÊšVôŘ>¾.oùòmÇ 0gÒñ#Ü+¶uík»pާ˦[aœVØ”×"_g'ÅW¼§¬š6ëãqСγnwóæÏw]J‰õµ“6l2Žä+ò†{Ç;Þa[müRfyøìÈš—òŒDH#\p-9'oTíqᜂ&'xe«þt¥­|_ï7gÎ\Ë WfÛ ßC~–-[b{sÒ\{íµ¾Š?çZtóìÙçé|ÃèšoÄusîÌ b¸Š·Ç{êúÀh#´1ýÁ }t̼S½kb¦fÁëŽö™|ÔsÊIž­Y¶ty5kn ¬L—%RÒW„ÌR&ý2: OíÖÉ6{‘mò¹V÷Áï÷õŠ´ïñ²Sxd.h‡Gé´óc¯JG õ3º [²OµFxpØåC»V_e?>Øa|€,ƽ6³PN‘‡&*“òufFädõhÉôŒ-8®J†oÐK{‡çnêo²q&(Ï{ð¥n<”‚mŠQò> sãE»Óý~þñêŸ8Ïm„|çÌ™í:\«ó›6Åx×G·Ìý´Q±tî+5®%%Yöš¤üÐn¬Ï¯| 'øÌ~Ý–äkS¤äZh£Úa\ÐÔWªþàmæÞ{géŒÚL’Ú®}ÆŒcìçÃg:UˆêÎ3ƒùèºõxߌ L.ò”nÒDù3“,?![;£þ·Ž‹ñW‡nuívîh+uŽŒïÄ4t†f¸¾€ûÜç>W÷?ñxõÙÏ}ÖýÏ3ÏÌöÇÝyÎWHTÝD±FWtsžÇ˳O~ÃAopy!û©uõ*¿„·ôÔ/ø«]¶,¾KÓ”ßuµü/[¶T:¢ò‹qÁâäT>Cž8 ™õdZÌ\ørfb‹¾~¢\Ô¢ª‰\\£öáѧěBœ„˜üÉg<ä¯6À™Qñ™@^CžSæK»‡& ² ÁÁ4!é58Gë õ&|NÆI€hv<ÁÄ”p¿ZW,ù¹_¬"²O1OÁ­ÿ“ëòšwÉ_½HdHF —ˆ$>âÌHŒ)…Ùñ&1*r9Lb8@\@ÙSÒeP¦/]‡^[i(錫‰#˜.Ì” ጓÌüÊŸÈ%…¹†‘'Ȉênk*žxpè‡ ®ÆM°ÒE%K” , ìÀ#.ð8­B:*“ À6º3|±”"œVLýäÊï‡u¨²0‚GfÒG5©1tºäÓj³Ó>úØ£ÐxAXR`xò.m9N«K~2!]¤ýÛo¿e¡˜{ …X ÌÓ.< Ú9èvuÅÄCÛÔRÜãà- ïÿj…ö¶Ûn«nׇ¹PLQø466Ӧޅgð`Èg^ç<¥cc2“N«iR¢†i¢ñ¬ƒÚÛÛýä0ó¬Y1(p`±…p}cAʈŠŽVVìO\€„®fâ‹IšV¶ÄÓ8Ô©B§å@oÞÍâˆãpbúuŽÂat¾üãŸì§ƒÙ¢=•~Íþ‹|UUGG‡óÈ¡A"×­_gÅìÙ约•+?Áû+Ç­ZµZé@0ŒŽ>¿M=ñr¤UUÃnN&"þ²di±”~Ü5×\[Ý}÷=V4³^y2á“EaàI“I„ÑQêU†˜Ëi CÖüä 8Šd:ÿÞ÷ûû*í,TÒÝùµ,iÈÈôq{ËXÊ/“5”Ld!•`Ía`öÉS&WËï_.þ¼&¬-®ÔåÃiÕßOò¿êÊ«¬œÊÃað^“×ÀÁUWR*$ ´%Ú똱cü¨œˆ“(ùÏmé(ºYWóæuÖmƒ×I¹ùæ›Ü®eÞæò/kR‹Ηj™¼/_ÿ«ê4Í¡ÄWtqÀúõ|+-|^0}ªû·oúp¸Ìt,$\€¼«¼–icâøoæÓŸò^”ÉN²“¾ÇÝüL¼©¿ìcQ´¹£ZÖ®}Ùùã£\›t˜ƒÐôô‘2ÓQŸ©oŠèRÚ3‡¢™èww¯V±­PFôeû!ö!]“©Põ´&œô/ͯý*Âãê«ã§ì!á1Ž¥´› %Yk~ά۟†ï“Ì-¨´ãYMš4ɱŒI?ýéÿÚÏÄe?¾$œã9ɯ>Åääe)8´”a\*aø¹9E¦"•šòjÇ$"YVYY]{VÊ2¿t²ÿô„ ™;Hj Á( Ð#ÕV’Å ò€“w½²LÝ\xÁ…gPÂÍÊGÖ9¨áý yf½S^”!­øŽJ@IDAT« ƒ“øsÅ9¹ÒVzõýïÿ úà? ‰FLÀP|P°@Ï—WQ´H¿I+þ÷ßÿ{2µC^d¾ã›1PÄX5Æí±Çîþ2&íóšÈâÀƒƒf~îW C\ ù3¥– YÞä‘ +Ð÷κ×xPY-fðîÀ!#v<õ¶V“QøÀ )7δ:äaùŠe*gܺÔ×£¢ÅÇñøHÓ7Þà_ÂËNÂ(øø |€m…àÓ-ÒÄ7mÚ!uЬEH&W½´Fma{OØ)d ù|ßûÞç Ö׿~©q𸣴Z.yC[µRñ ì¶äŽ p¬X2ÑfrŒcwèÔSO1O¸ñDç€,÷9A×Õ¤ú*ôö. }‹«µã·qã:ßœb$úÃmHôaærí dœ£—âA_‹L†Œ7wŒ™LÆÁú¨ÜG?ú÷Aííí¾Aé§œâ]VPàà­¶ø-”ƒYmÙ¸¥zèáæ.uËG³h¯î‚Ž:ÞDò–áùŒ[€ØéÅqk®¡þ(ÊúZy®àelQ´O}«Þ±4©y%o3KçÑúÇíS;p¸ÇÌðŽ~v¯ïºë.M¦G”-tðÂû -xÍ.N?}Œr¡ûD™VVüÒé›#1¡r?Ø$¤Wš„>òPjû›ð‰Ï¼i¾Ô¾ì« ­é¯£·öuk» M¾Óÿr;Ž ¿Ç¬Rçô#Zú &Á¸Z˜øÝ=¿³Ÿñc»Û-äS;Eb]ôW¹ÊÿÑGíú3;Øìôáî¸ãÉïGkùå6¥“O>õ5äw½w+¶liÓÍ[Ý妽¨GêÃÑÍÌwÆræñ¥E5èºãŽßÚÏ„— =+Ù¹²=_Ên—]Æëk‘sħ6›FïÉ'Ÿòê2ñÔë5×\£/ÖþF«l?­dë/LJã꼉'Ù쇄íu­" 5“A”tL¢Ý! ù‹´3œ²TÓžü‚¼ôÕnP(íí,+¤MtõâK/V»íº›¿Ò‰Ùqg%-xR&Ôg•ö!{ T!_¹ûiji£V£4Àá0 Ð!iËò{öÙgyåö¿~ø£jñ ±Óf`ý ³²>Þa3j¬¾&;ÈægË´òÌŠ.ñСC¬pfÜ8)ÅrLÒšÊÔÑŠJ°&Ð/X˜ÄøAyhýš¤i¢…ÓÙœº 2‰¾ë&z¼^wûƒRÎ N¿½íÏj1×:zåÓô$[“Ö=RÏÛÊ|ˆ‰´l•¥ˆl©¿Þš:ý\L¾\Ǩ‹T–C­\yY:VÜ^ye`G;è'?ùo]Q| ËÍùâ‹¿PxâßUì8üïÿÞ ¯0WËôLb¶1VýÌ+ÐŽŒòÝ+_d2Ò§ºì²oj§éT¯ÒV0=|æOÏT÷üîO~‘û… »­Ÿ²7÷s½&6ôÁ,¼¨I6«ñRÅ”UŽi€©?¤Nù$.ê%X6ºÕMPÙa›9s¦•YÌÅp‡rpu¿v¼fÍšå+buÖ Z¨>¡Õ‘Žò°² ÏX̱iÛyE]G䫇+Uj²67ã‘AË¡À©ã_ÿê7NHÛyLØÖgú[óè.3Ä8ĠȺä·bÕš¬‡ŸxBܸqãõöög¿ÍnšwÝÀåþŸÛymñBÇKÚbÕ[eÿMœ1›ñÄ …©Å™¾–w¼JÕwø&hk8tèWÆÄ€Qú@ÔLÒ‹/ê ¥Dª¨¬ú{‘r*¢˜`¦˜.ûþ#<²î/˜0ݬ¯6ãæÍëLÐ×|¦n Yç˜Ã¤—Ýs]–°•ü²ëpÿý÷Z~Ct±ÂVòK?jùÕõÓèôEÔc–±.)V°Pr`U{›Íiâ]YÔ‹Bü΃¶m®Éð¶¾ ÀÈzÁï|ˆr !Æ튼rL&F0&øp[W±hh’! ^4^úüú~è´Îh¸(›Ç@É®$+¹Å£dNEë @`•ÔÊÇéô<JNñæ·âù@j3NE¢Ò™Ñà’@ü‘i§†iÄdGfÆ %g9OL(°]!ÀTË€GÇ)ÇŸgl ª`{—/‘i–Óaà2 \¼he†‚.âÁWh'ôå“<›øä.Š W“è8E’@V^å¤JH.Ì™H“4¨'÷â\t¾‘‚·ÚÑXÒwomãçJöþð¤aè$BPyÁ$Ê¡WWO½ÓqÜŸ÷¯c7ž+ô *¾§ì,Q®ÙjÏÕ&ëµ…rႹP&:¦¡2]X²t YÔJ4ƒŠ¾´ë0`ùä:îÀô“UC>»¯h7€ÕUŠŽ:в˜ÿZâ1#aEÇ 0åÞ[EéÃì…eN±]¦lÙ¸µ¾+[z}^ƒÃá‡^+¾<òÈÃVpYuu>®Wg#V›Ãzán¯½dR&¹"‰Í9'í‰}öØ©¨/xnÃvÐVõóº£]U.'¾Š§9!cCR ×յЫðÔwX3ùÛ´)L=PØqóæùªx(zÙ%À…â»—ïhçîÌFq»øâ‹mc˪øo{§”•;eV1°ª½½£š??ðbç TEÍ ³D!RzXñpYÀ¹´U›U¹C‡ FÁŽf•;MK;IÝ„üÖúG1úÏD"¹ÏŽBÿþ!ãÈþavÖÕÕeeþç?¿Õf1úЇL;r¶¿&¼Ü5Ι ̱yô‘êš«¯Ö=ÕÏO‡Ê½D¼eË–ªcÒA™1×buìE};`‚&2L¦pÈ¢n²…=BQlyv”[г_‹:ÊöŽ,å. õßu‹•¿àw`‰¿p*9ZáÒäŸo  逶å)LÊJŸÛŸ˜”*½õé;0×b7rîÜ9uœnÈÒÖÿ~6³˜4i¢Û0(ïð…_:äг–,ÜGŠŸ tY¹Ð!ÄjbGGõ±ܦ?”Ÿ69mÚôêœsÎQý̵]:²‰Y"¦UË—Ó¶b"K=ÛX%mh"¶ÆmvT0áºæš«=áf×Ývõïä“ßaÛiÌý¨ú#](Pïté¾¾•²R€5R,e^C(C©;{! x̸¦w”QÆ&^•5«µc!§K<áž1c†ß‘LáøžwýÿùϳÍgG0ãÊó[Lª6H¤.ÜŸ«ˆ•EòBœ•Ÿ­þˆI˜|«ÁìA±üóìgЉ1´™"Æ™|k¦Oy̘ %9Ó*Uò×@Íôé£lÔ„7~|L¦ñçb LÐ æ`¸tV®"Ôý ý;a²‘§o¤Mq‹þÂmƒÊQš×rPÍ?“+ø‚½[Ðl^~w¯n‰{y)UXd'(/¦®¸Œw®ÊØúËgörìÁ„9öXhþâ_ê@·y¬¤­2ñÄ1þ±€¸ú¯ßø‡ЂÓl·UÆOä7ûQôÆü¥ÒçÍ\•Ãå*å l°¾¡^“¯ãDL2<vÜêÀŒŒgÊeö¿Á¿cœ®ÝV¤W.ÂCVx/l¥A¶È5øJ~ ‹Ð”Þô«  TïàѿГ[hih—$u9— ‚õKì%Ã:¿ SñNO¬h39à\ ˆô oMar&‚” Òñ+"‚ W_I蘨xî›±S­¹3RH&4èˆ Æ9‚9ôJ%»ˆf’pP`Ç7Ó%±M|ÂXh3™hºE êJzJ~öBN𮌠´î˜:£tÍö© ’€½ákºˆˆw%vÖäåpþèg >ãoA帘x¡½cƒ™Ê«ê8R]/i? |ƒfçÆjºÀ­(Ÿu%Ûb ̸cßxl=0/•BÃ`‡Ã|‚Ɖ«¾”2\áj~K°ôæÿìðÂEhªˆ-Yò‚ì6'×+¡(Ñ¿þõ¯œBV`¦NÝ£ŽG1ž5ë^Ç£¨á¨{êÏܦLéÌ&ø‰ùCtÞð$ÞP²C™](Þ—¼ŠUUííÆÀ ǃûîã>(é§ÝP?Lü´˜äP^tQÕ9~u®&i˜½1Q¦žá¿#<Â;‹LØfÿy¶Ï‹0iEùGAÆ,îÁ}º`Ǻ Ž5Rõ¶¯m˜¡~„¶ÏGá0Éþ SKLõÕß@ÎCyéã]‡ÊQþäjSú¡„ß@MØ×ÙDUVä ÓG&ÝÈ. õÍå üpðäÓŸþ´wìn— õþëG×í)2ÒÄN}nÃÆ~îŸð·ŠGòÚŠ¥p±CFŸ“«Æ­ bÂÖ8( c2Å"P“:ξÞþÐ?Ó3Pzþ’¿Òš0=oW­Xˆ‚>\šRBýi¸f"Â7:ø¯ÿc|bx0hCãf`À!uj-jɶ׿ÐJ™¡]^À)QM× «’ˆ²Õ× ×ŽüÙ~ã!ÖøÃHT÷R®‘x@¿“ý*8þV—cüg‡z¥vªic(ð!¿ßõeL8hËäÏ!m~8vÌÎ?ÿ|™Cþ±ÒÍl•nr8; |P ³Òèÿ¨Ÿç"Æ.ˆYèz ž7~„s¼^s"Ü𠿉_œŒzÉô¯H¨àBZ]ïTJÝ7„$Ëùx@(º@ÚÃËîäe¿')‚T{jæ 0OI]ú™<Á†ÔF<òt$ÊâH‰x<“ä Ìd ¼xð¢Ãžv.íF *!B 0¢©(€8"Œ5Þ>˜®·¦sb%txÀA– ©§“ƒ[ÿŒ½®°ð‹|å1ŒÒQ‘8OFôJïÉ„€H8âȉÜUáÈ™ t3HN êç8Âõ#%‚×qÂcR1Ì`D (b…ˆú92«1è0“M‘OAï4Š#584†Ë¯òr(,kù ”:fV;œ•T­~ˆ\©‚&\®&ò•ÐÜIXT¾$‰Í)[Ýi{œŠ$‡vÒ†’Á/¨eÊ&J…Ô_‡•u— À›Ž{S­D³ZÛIø±Ç{ì1õV%JßÌ™¿Ó--œsX)\¬Ð¨$úÏ ”3 L8e„Í#»iÁmÙæ*ÉGŒ.Åcq…‚’ «Â¬ãÂŽš]XÝÌE;W-çK ÁÅ!¸+>»°ùT]‹÷Æ •ò±È°y ¦îìw:Êøb±£Lþbß™öüµ<*þ‘õ£CeІΜqà›Á’ÕûT8sâ†Òu§Vîq˜ ÷ð¥š:Þq}Õxñ"xsÅ*.+’(ýÈõˆÒ…rïø¢UrV'¿õ­oIiù®ëóùçµã¢Æ@Ÿ­î<‹¬¹+ü¢úà^”)rÒÆ©G‚úðæÎãwQy^«ƒ¢ÁE£T`rýÁv»çíGØLÿY+P8dƒ‰'寄ƒ&@ú •~ŸÓÙ–¯úËÒL¦hìÖpF€ß±Ç« À­ÕYg¥°—5yió'ÊÈV|È?“¿©õ$0‹+´a²›´[ÆÝ¯¨lù$A"“:ê~­„;pµs/¬7˜,üf/ì‰ ¥å¶ ò“XH Gtûá‹Ï€ò²ú•¯|ÅÊr&ß|EY¦¿À\ŽÅÚ²}Õ•WkQ`¢¾Z~»“d[c¥—6ÚêH»ì¼¯’ ¿ÓO?CöÈGY!F6Q0¨Úmy?Dç ¸QŒó6W\qE+ºÚÏG¹qh½òÄô†¯¿÷ô÷úKÂÔ=‹#L©/d€“ú*][ê//]»›5ҿѳhQœ5"ù¹ç~ÈX.ºèB™J½¡êèh¯8è©;ðÝQ^Ú%²N|ˉú"ì—´+}‹—?ì à|öAu¤?n¥&A¬€¯^-Y”Ë]Ç•ZÉCÄyV…ø~LrÜað†Ü"[HD³ï#¦§KyEþBdÑ Šsò‡3¨ç“ôd‹CþÓ5ñBàG‚š8{A86äg•–ÜãV¯P4Ù…a\dG ©–¢efÝž¥(¯Þ]‰wd ,ýU–<Ñ{òJ‘(c7l%Ï‹ü3Þ\­óˆP]CTã»GiNXÓòšTD¤éP§ÎbÃSO=åÀµê;Å8›O¡ü3¦­òeí~ýUŸ¿è³þRüÄI“|öŽöÄ`”ä˜;Þ'¼ù-º¼«:û¬÷Yùggm¡ô}+CyÚå‹ê¥n¡špê5ê:…Ãʾ‹Íâ1} É'¾ËÑC6êhÆLþLg§ó8¦W Zå.ôåaœŸYB£é6½…Önõža¦P‰©kÉ®}‘™G9x'"òЉâH8PÊÏøÐ>þŠú„×[«s¹‰<bqýÈD&._­?%ƒ$3pDtΦ^gGzyëA+ÃU‘Þ°ÆHF‚B‡|ÑÐñAxà ÑÁ?Â30_­ÉL‡ˆRØP¶J‡áp¥6¹¥<¼ð_4—G9JE*¢»;gÀ~+ê\6åbûèt¼ÚæÃÑHõ¥DûsBÀÕØÓZ`$‰dÃòdãÍU¥©ZµË݆T„P®XiÅu´kÓ;ŧ{Òü•I9Äuñü*ÿ’8 7òàòÐÊ·ÓàÖê37±`tˆ> ‚ŒË}”ôE‹› ’¬Š1!À¥2óÈ#±Ë7JÐI§:2E¢‘ê2dV…—ëÜîõÎ+|Ü`‚c6œÊÌŽ Ž E7o·Xí‰^à$N_‚U'&()8&Øï‡ ~Ú@æ¥ãÅ1I„”¦ ìˆVøÏíMmm¥ü/Ö Ñ6IÒé:Üy£Ó±"íù>û¼Î“ŽIêÐÉ›ô(!\KIé.uójÁÝŒ¤®9t;1côG+ °”;ê|œì¡sE{_­ ³ƒKY¢÷M›7hðÒjwôMŠ-mUx-u*:¸˜H0 1©euǤòÖr¨lõë nß¾²ß—mø ™s0qììì¬ôͧÁVxÆŒ£mš†Ò\utLôdžê|ó€çˆ:HÏî’Ê×_ƒßƱ:™‡´™œÎ;ÌŽYø‡$ÃðZÐVÐñ´ÛlÏÔ5“ÇœBhàÒŸ;áƒÝ„€8²P Ñ^ƨm1I/¦,ìŒaÓNº¿Æ±ºÇjÝ1Ç[}ï{ß«w™P20Ùš93lÚsbÚÎ:¿”uNÛÂí6uwµÁ.cð'RBç°a»»ÝüèG?ÔªáqlQ†Ùõk—òIòM¿Ã“ÃΛ4YùÉõ×W“;:¼ ÑŠ—ü‘øùð£ye=è˜Zaò8Uôpf»gv˜ ÐO_¢­ôM”Js¿H¿íÅ• ÷oúKz&eô“ÏÉ^[_Õ5ž]ÚÛ«=´ú='N² ´Ð_¢P1)¹à‚ÏTßÐíDÎf±‚ <²¿rÕ‹6e¢¼ÈNßÁ¨Fh§ƒ~XxàÝ@ñó øÒµ°Ë°üáæ'n„£ïê«~Ã-ù²Ìô&8ÝÀ÷øV‚ø§²†,·Ê~d-Ò´«m#¯bm­Ã€ ÙŽþ&2{mz« âè“Ø9«3Kðn£¸b†Ð J¾ñØê/¼ª!ÒÛ jë@èd&N8E,$i¸ÔW74°Tð'f ”¥»S F%/dkÁMAúâv÷$ã‡x|T‡AÙ† ªK-VUC$“(ñsçΩ>ÿùÏרÙ`a‡~tò$µÕŽvïF0n¿Y“€;ïºCüÏ©xà~/Æ,Ñ"@šì…²+T¥¸®cø§2¢ÁÁ¤‘g€à ¿åDú¢cO<)êqˆ÷:az,9† „Jå(°à׋3вgQAKÕ@?0äLl:½)1èb1.Ôn² 'ÈfRE¾Qâ€hÂF»qÿí?¤þºC´ÑÁG*èvÙM“ÂyšqWeäm!k“Í“ ˜ OÈ1_ÈT?Ãyö ƒåÇ#ßã5ÂôWiƒ(`Jœ™D« ì˜ ñÎß`~©‚€/ КƄ£Pçäþíúo¡s~ñî%Ž{§Á® Ìzú["HJÞyàë²-Ñ© §§SYˆ9p¢ÄÀåàÏ :•;`â– L È¿ðAá™}6$`7iåG_3ôa Aƒ†kpÙÓô±Jœ7¿¬ZùR5rT(ýØC³†›£Ã£8îÈ_¬XÓ/>“3BFG޲Zìšà¸I#W·éô¶”ÃgÛHJ—J+¹(R!]Ö>‡<É“ü˜€$OôaíŠìbpìíÿû¿¯³Ÿ-ßœ€À·œlä*9¼Û¤o ôï7ÐöèÜ´ø…0ÙÀT‡W¯qX5éŠ: "ñ³JžÛèo{Û[êmWb·–¼,;T*f›m8À¶Ô+ɘà–• «Ò º”yáJêg\î€7' ÜÖ„ËÉ\«Œ´N¼–.Y¦ÜeN¦-ÜT¶‘˜%Kk0¸joï°’üœxøÔSOúgÄúƒÒÑEŸ÷ALvPF°QfÀŽË¶ÛŽ)æ:1°[&œ8k…šeÈ>fMê›qÚÛÛÅÛàgòÑÉý‡vÞ‚KhXÙšN‹M&L~òÀöáÏ<óÇjÌ豨Vë G›¥AVQV­\m³&vA˜ltiƒ2ÅÍ0üÒñ] ìÎ'MšhÙ)Aá½æšk*&óæuJûi›ûE'ÉIgrr2ÊÛ ©ܧ5• ÝýFô ^¡,Š7â°ë¶í¶TÎ’‡Ñ’‘åÞ1Ñw$+Ñ£!‡0˜‡»SƒÙÎF¥‰uÊQ£G±Ý|pÝÀ-š¡àe׊ï_àäSY8ЗC»ÝŒ”HÀbŠ\2Yâ¦&2,\ÐG¼ >Wæ;ãwŽ›¦²ïËôñ”„Š9§i•:Ø5À ‰çjÉýž~ªêÔŽ}&y8òÜC„Ow´›7f‹cZÿ˜KÕÛO>Éøé—÷fï¦ÍÕ®ßsÜ £V‰1Ÿ¤I× X7÷tï»[ñþ-þ å‡É9ýh+-]Ú¡{^‹?,Æp¶e“úÇ~z219õ]§š^ÚA«{»d” Ñe÷o´”ß/õ«4˜V°â^üQ»4³uƇ¼Ï×Î}éM7ܨúZ«¼GÙŽºƒ)²{àKüª7UXŒyȤ}VçÐú·Õõ• 3­2㥖ء'åä€K˜ '0±Æ Uû@~h{!ô3ŠPV‘ùò9ÜÏ‚é)óҥ˵K§q_¼$ú3v¯öÂØÅãý ù¥ãû ïÿßû†:Æ4&ÊŸ6mš¯ñ¦ “=¸´eö·EŸ«òPmúPY­Ž¤*)½Àȩ̂ñºCɱ„²NÝÙéy`*Ï;äÒXD&BZÚ’ëŬ3r³soŽ€G2Æáp¼€;ÊCx´eh‚?ò[w޵D™p𥾬Y°ÊÙBr‰ êgðC&@ Ì´0VÄ)nç õ|dBœ™C«‚Ÿ3¯H…ãël(täjþ“ïà+”Ö  pf¼|æ™3Î'Ò qJeô@;DèÈè XJþdó*8ÁŸð.ÿ:Ñ@¸p9üQ¡x`[è:,ò-áÆC°œÉ ¯þŠó&ƒ‡|ä¡\”òì4œGâsúfyM ü®æŒöÄŠû…~ºÚyÂÎN…íùwþÖö±] ©³ˆN#Ï`^2gN˜2 ™²BcîªìC‡÷*!¡¹SÍý7„BÅ5˜y+ON.€UÓ›7=É'ó(0È]|ô%äïØcßX+úyG: üÒ¥Ë<˜iòäIVDP@òV–Ø/º2Ìç¸ãŽ—r+è(Ú(‘(fE׸èA!Ò, ÞIèGá•4òÅ6‡²»R“+æ<(®¸¼~’wv[úöÕª7mN¦NÇDŽ3ˆä™>íîtLè¸í¤¿V¶}W¿BY1d F‘˜ÛÙi8}bÚÅÕ’(¹\šn›á#µüR…]?+Ùúš¤ÏœÄŠ_œ]€¾ÓN{O…Í7.hVÚwßñ}¶vAÔÚv„Í Þõ®wËìgƒÒ6>þ±[ùg0ÇD™C‘5Îò¬]£ûúÕ䢱Ñÿ³ú“E’óÝtVe£4ø­IÈ—±ûm_…2×|‚½a„ßhgE¸ç³àEYκLÙBaˆCèÄD9Ì…‡ž¨þ½cÏr.Ê„œ#aò(ÙÔŠ<Êí°Iww Hc³?3©¦Œ¯å˜@ù™ú×u +·i—ùÉ?þøü¡ ¢9gpÅßÅk¥c°Ê‚Ée‡ÉFÚ3úKžÊ‹¢äê0x€WOðç­Ð Ì ßÞ{ïUó—[JpLÖÉ} +&S¦Lvõ–W¯²(ÿçžûa}1õA ¿ä’KÔé®´â; ¿Ìt*¢ÜœgXüüRO\vÛuªq¢T£xàIV¤q±… Ä ¦W¼¥Ø¡RvúŽ\<ˆ=Ýl$ó׳æÍF¼§¢BÚ5j(ú 7%úÅà²Ñ+ó|”écû˜mLÁÑÙÙiuüžèÑfµ«‰ ?zo¼ñ&Ý->ƶàì^°ÂLJiµ±«ëy™M=_­Ð€ÆÍ$‡s˜R¥KcÕ3­PØ0ÍH…&obGbÍjÝÒ¤ª…gnË(èQÕBI„ÿ‡N‰g¯H~p>úˆ•oüL>^ÿúýñÊF|¨2 èúÜ©BÄ |˜—¤ü¼ó§úœ;i2ÇÁݦ¥­¶Ë;'­|PÝVYýçúL”*>@¸`A—¯Ð}~ñ›±kÌç5©F¦r=®÷B'uÃä^ó§þ‹xváè³â_GÁ ÿ¡/â™ y¿&Nò¬ñnlÁ»Þ+ôëÄÉ“~+Úñ_ õ/•å/ÄsгYÆuòðGù Ù‡v&z/ãF+^Ldá <ê®YðƒüÜ ãÆ«Ž;á8WÝ*™1ÙÀl©9ÞÄj¼ô'7¤¯Œ’òA¸Ú2‰ücÒÞߊùî»ï©~il&¯Ÿ‰‡Ú+ã޶4nœn¼[±´LÄR•Qx#h#LSi]20F7ÙàŽ;þ›ïq‹çDž·ÀáDޝîEé?œS7àV:éûz:t Bi™æ¯ ,¨O\NfðSGéºÓ醿[C—t@­_¿Î(~ø¡º¿à&VØqL\׬}I;Ñë=V¬ÕÎ< ALl×êù²;֪ͲØÈŽ3;¼|ðñÊ+¯ªÎ8ãL+ÿðž]óµº ‹‹#¨>ìµ÷Þ¯ó¸¼í¨Ñª«—•~‰ìúÙ¶ٲ喧‰]æË/¿¼^„cG–se¸˜È¢gáš\h;¨ƒ:&˜¿…ÃD¸‚;‘\Œ#rðŠJž\5×Ϫ–\Ÿ—þ ·z1$äçŸ",sÖ5®‰¥X•C&ÞéB6ã ³V :2ÖePeXtTÑX×tSnÆU—‰pIe3%‘R×Ç&Iƒ'rpvÅÛÌ›È?<ººÎP‡hÌ,Äd@aH 5„åžѤwc, ÊäJó gPèÁ_~àDÒoJCå $\²PH ·ó 7F„ALj:˜FÞE Jƒ1J§ÿÉ$žê KéðƤ'€iakÅß‚€LL ì ”˜rC¢`žF4(L7ÉÕ‰nÖJ©fÔó:ç9qÚ¦ó‚¢ÁGRp( m}°Þ‚O%OÂÛôõS)bÂê!á›ßüfµ—”I·ñq¯¡CôÑ¡ç—YñEI7cäò ]·ø˜¾nÜÞÞaÅ›~ _3ÄaþÃa>\Š•G‚Åä'e sTð—/ß2AØÌ²NÊÍ`Éd„C¸(f8ñåÎ|üýïp8ålHYæ’bEk„vÇ¡¨ç H¶)G.ÉCbÚWõ— ÷”)»ž]Œ4í±,)4v^T9ÇÕü`š¯éâ°‰NEpŠl§8ààäîf»˜Pµœ% üq¯>7Ǭó­MÀ1¢^p ÷ߟý±Š+yUçJG>¡}guöíZ1ß%v'Šý{tT¢ZüÌ{û rgd¹_øJ¤Z‚å×¹Š/ô)ÔŠãxJÍÉÐYgå ”[~v‹éšj¬ˆ±•Ä5t¢ô2ÊÈÐËÚó~¾kís@¼yž—x_üá:ç·÷Úk}ûêk¯µ¶±›nª#'{žéoè\~Õgd-Zx;Ï+éèB@Uâ*ݬ‡ôÞB;€6vÃæäÄÂÄô·Vvúög-ºÎA}G9­Ú‹¼IhsÊ•S÷tô‡oN:©6ÁçhP7zþŠÓШŽvØtÖ?æ{'$éèšG•ÍoAxzUÙ±NË2ßK°µßøpYFjxhK?õžÿüÞá 'Ë(—æ¿é?ÃÿsZ‡é´i!EÓiê¯rüp¼ßÆs—eºžÕQy~½ŽÔôÓäÿÿbuëú’$±ß#Ùœ™ê—qR–®%ÆcÝíÄK? ¢/Ou ›3Ý©*lÏ$ÈÏßüi[:¿øÍú‚7Nò°b ½jõJ&ì®ÁoDz®ö[ Þëå{dWØÞþŒmŽv€nL!î~Õ³ÚŒ¶J+6fìo.®xA•OóÓ¯©|I?$ý*&Þ¶2,õ½x%ð2ÉÓù‚(óxOå KJ Ò5‘ï”aù¥ºr€Eéâä¨òñ˜éThèô·:…^‹µ¨Ñé”úá”úkX¼T3A.Õ™ìŒ2pVéø½+LSH)u>v¿ÏcÉ>RŽò ¦P†ZDe1!s$Y+ R¹iò`åh a**dQ( HT»‹×äªA ‘á;¥O×_J1H£g.²oPKiû #DG¦Œ( £"ÈLÙX‰ÄÉßJø,Û±cõÔ§îG'¾:ÊVÖvÆžýìí™þòÉfG*_ã­„ï½oå°1…'>ñ YÓ}øá‡¯~õ«SèíØ|ücþ&›nL!½+ë\ ØcÏ=xå·qâ–,Yœ»;ûÝ8˜ÊÕLšLÔufmùÚƒYs+gO•ÃsµuvÌtvtÎÄ»VQg§ï÷ÿò¥Ù ¶¦‘Xî]ÖÄBÇÓB<…ÄJfÙ²k‡ãŽûØøa2oj'ixÆv:t­ÃvÀ°¯¡ÖÚ÷Óˆ<>Ͻ IÒ½ ¹:Ug:.½ãc²øÀ™mõ°Aõë sÙ±ûãwg™ÐelÊ;†Ù¥š-ß%gœqšÞq™Å|–øÜÍŒË>ûìÛdwÚi§U_ M^@õwƒÕ >”¢ë§?Ù1ö\~]5¼_Y³éÏ:ðYãlºkóuY™ý¥ûªãPqõú}Ýì½pÖÐ|S+UgHýBr_Ö²ï¾S3À—_^éºå[±ü…Ͱägt<'ÝAß":—­¼çŽäátXÐÑ’<3ká• ŽLµÓl}£sFÙ©­XqÓ°ÃŽ“äéÕ‰Kç¤åñ{ï[•ÍÖÎTÚÀ| ŸnÖwÞyÃ_±ArCÖ-;ko§Cç:}Oå_gØŽÁIüë:ȰÓ×:xè[é|ïà`ÉÙ°vØÔñ¢yÒIÿÜÛ9[ßtsP ëƒÊÎŽéäÿàƒÌ&çÉLÖ1©o¬»ZºX¤R¬|&üþ°=:¿%`ºë,›Ç×´íŸôÄÀiƒÈœ²ïÒ•Õ¼aYo|“õ¶·ýÅøÌ%Eg0s¿Ýv ”—ƒÝ/æø)g9Öõ¥<úû Ù´s¹ÏÈëÏ·MÊ~iä2%–×'ï¹W6µŽe¨u¨«®*ÛÿÎÿرÅ!yÊ»›Ä }կˆt.¹äÒÀÚqwBEgÞpi¥mµf÷3IÒÄöÇæðvA7}éSMz‘×°•:¥³î=Ês±‚,C§Çý²êdÙúhÁé ¼:;ù‡DàÛ–”|€ ÏÍÆâ•³~î4¡g†t” 0¤Ã‰ ^O¯Q¼§`¦Ã@$Ø« ϶¡×èB«´!4D…÷üSq¡Pru™ïN³÷©ÍoŠg´N|é2dçUGV‰ÀX™ÙïÂh#ÊðÝoˆ„ò`10¡ 0¶âãiê(A /M•Èy‰çs€ðë™+  >K„VÔÄw­›f ‡ŽªŠÊØhÙtÑ âýy'®x…R”B'yvÚ gO¸ˆeŸ¢!]3^òØ¥þ*ŠJ‹à>0):âÊ‘5àlå˜|E³/¹(*CÖ<»fý{ßû·,°ÒLG9]«¹›Œ/Yš£(_ËòÿÁ’¾éê+_ùêp—O&&&i ¯£"ömAuÒ÷¤“á1tºþñß2¸Ì'¯ËÒômÀÀfýy|Ô‰·º‰í'r·ÃðÿQGÕÕ+ʵ³áÇlöùÏ.3øÎ(Ó‘GÉfäÍ2]vÙ53ŒV.fg§Á¾ì²ªt>ÀI¯ýëÒ ¹YýcŽyGø©KֲϜ]ÙƒP—;ôú¾ÖÞ™*—mÜAÇÕs´ëUyHŒ6ði—]jÖ}½õç w˜ÈQ“YC‰æn¤µ£çììk^ó^™¾5›6´Óçùº-ÙdÚgÈlu3êÆLËUtžÊd'ϯÌÞ|ó­ä‰Ú|íµWç5n_&ãZû¾¶¼/£ê3Ð{Ñ îºöt3ϸ·Ãuº›˜ØasÐ099™S"\óä'ï•åZ•ïª\£ž.CÙ{¯½ùÔûKÓ`›®}ã¸ç·ûå\7bÞu÷4Ê;æ$gù\FðÃþˆÍ`'3Ó³Q|–?O!R]_Žb=bœ³ioas¨›œÝ_àœÚ]5€2ÙÐ8èÒüñÙü«ßÿ;ÞñN½)7‚ú·ÜoÐ;°Úè9¼=óƒPæ­zSPçô›ßÌwn\½øâêÄä#O†úéÏ~ÆÉP?¡³?A/n£ä9y¦sðÒ÷£d RŸ LlÕu±3Ì,ï–£ÜSéXW(ÃLˇmlËIRßÎʵw_Êäî™zñEæ-€yº/Q;眳ófÕIïï{ßûòAµ=XßmÝîà8ùZsyäö?ó™ÏPçÕ›è)å[®£­q©‰hݾð…áàçœ 0OÙ:‘ÍÉ/e÷¢EW¦~°Y°U‡È·\áä@?™ËNëßÿýûCóVŽåõ «ƒÕ¸„Úæ–ë¼ç±oÊo>¸<òæ›oîc¢Ì<®óp N½èE‡„ö%ÔY»ïþ0·¤ìhCÂÖ9.›;è93ÁñÒà÷:o, ½ïÕÍ0%NDœþh­QYIQæXpÃõ71Q± Gv}8ꨣRÇi‹¿üo9|ösŸÍÿÝvÝ-ß¡¨%°Öó,3¥¬Z¦­ç:èà,—² ó-cÿ’ð¶Û-HYðÁš„tÑ ^ð‚Lîxbœ{Ô<©Ë· ¶wÖU¯Uš8h’þäääøáIóÑT=éÄ íýÓS£il_›ÛgJ¿£Ù¿*¯ÓëÒêw•mí§•sæ­ÒBqíÜiŒò^õý€Ñ£¼pbJHí€1žÿ†„_ŽÊ ŽD½T¯àPö@ajºüòêyK2=_(m:ï’ò'rHä‘¿2 Ù¡¼J\ˆŠ[ºˆ+t‰—ëÌBTHÃE*e:ÁH¬J˜˜ZE4<ÂçĽ^̦…Y1pÊž@SƒH'MŽq½YPä2 ­0åqTeÆ1$™ÈÖÅj8Ò/´¦wè©d&„ƒ†’›ðúœ]”ž#J;«R‘Xó‹(|2EáéÏ)&=ú £Œ:®²äO1ËÞŽÌFÚù±P^vÙåéœÚqv'ýÑG;³ñÃŽð+÷ãŽ;n8ôÐCY>SN;o~óѬ‘äô*Bgî}M_f›™i×ùjÿŽÊÓÕÌ=§ÿà¯ÔV®’ÚÁ»Knn¸í:A‡­,ÈÝÎù‰'žÿªUkÒ1vÃéÂ…yj%Ò;Ùžµý‰O|¹ÞœY× =ìâæ"âó1gœåøà?˜ÐN;î”5÷3éü{JFŸ•œœ©8+¯³cÙݼ¹¦£b§Ew-k™³¼‚Ù&gLþâ/ÞÆ Èÿìà¿rÏ{Þ3üùŸÿy;Ï?ï³CK–,¬³âË—ßD½²NSwÝu×tÒô÷ Ùh×]m̽-ém˜ƒƒþÚ¼/-Y¸p‡aiÖ²²ô¢½í˜˜œÐðWoÿë៿µsÒ@YÞß÷¾¿£Ñý¢ÑuÔ‡w¾óã›OåøøÇ?žNÂòýšñLðšP¡@Xê2å[+7£?ý©Ï²4à¨Ìúeíï°ÔÊŽÏÏ^uÄCH´7‹ãò‚ïϲ¼ÚÎøEõGBú]ØcÂiŸ(¯Þ«mC,²Æ f³l²rIË™gž™NáÃ?|¸” ¶'´==D‘êé£ùh&M|rM_Æ9ë¹ý³`_?üÑùß—³Ôzú…/üƒ Š_üâ?býÀ¦¸ãÿùóEñ;u³©nûí¼3›Ýëüæ2¥§K˜t}Ò)\|³æÄ‘îsŸû|¾œîÞ¿jïdÄk_ûZ½&þá—CyQ–Ü:™ã?ý¤:a­üÝÊæC\úV«,OZs7¨õ¶ûíÿíí÷¾ýÏ©/¬ó|ãþÞ÷üíðµ¯u$ÿpÏQG5¼ý¯Þ>ž¤vÞy?aYëgéφÃòW0`Z'“…¶ÃŸc@qøáœã¢]ÂùÞ÷¾'õ÷×¾öÈô¯»îÚ°3ûRæËøÚºõ‘Î%…iÇP+“¡ôÒïò9ý(žS‡ª7oê )8é̳õ÷šM.Î~›Ö«{¿J":†µfÍ\שpY*« VuêË_ò9¡bONg߸è´^*a=õ*D>£$r~ü™æl ¶KV÷­iÁM¹öWç,q?EÇÊý¾ûîa†s:¥t@w}ðŠ›n¦s;‡ ýO³–ðÀŸ•ŠÐ·{1}…ZçnGáÁTb®ß~ÚÓž:VnT}?GÛ9 °Ã·èª%ÈÀ²#FNn\³ºf˜·å˜Áîüz¨3Ίhgˆ}{²Î¬õÚg Cø¾TlýÍÁ1ÇüuÖÌŸ}öÙYÖãàÀ·#{³YØ™góu'œpBö=ø‘;¾çŸAÂIq ȺððµÎµñ½ñq-¥ ƒ†Üd“ÍØˆu×ðõo|- otΪô†biëP‹ï&PÏg¾nÙëKPÜp G$êú²¬›x;ãÆnׄ»qëM|aUç«k×ÑŸrÊÒ‘»…Ž´'Aè<©ÈoJk4?@IDAT(h÷¾ÁûG?úQfߌwÅš5«2£ï³ÎWÇvþu΂õåJÙsa&}Ü íÒz7q÷À¢E‹²9×4u ûÒ—þQf¬\þrñÅ—äÍŒéê@Ø¥Hæ‡ÞqpæßÎÿÇ>vÜ09¹ù«3»grnã¯ûä'?Åëêgf™”yÀY>Î>ël:פ|˜¾.Ûwß}rªütæ7{î3qùŒEÓÌÂb³6ê{ú`7ÙºöÕ"êK«ª‚sðO˜eǼa™š*ÿ~!óúë¯&'?;ÿ§9‚Ó¯z[î;¿ü†ÓO?ƒt½&j;.;ï¼3Ë㞎nû&¯C˜þtì YøÆÀš Ž£³s®+»µ`t]°:_sÍÒtrÞñŽwdÖÓŽØùçŸOú~Ò Dªþ™XØ“Ë|#å~#;†–;7Y»þßÍþ²šÎ»qûÝí1dóÁ jÖôëÆ4#÷ް ~'ÁSê|ðÿðÑá%/~ ùòô¬÷6½ýšýÞ{ïå"}¹žõËè^ùÊW„~ç&´þ9«½GL¾õ­o¶¤\zØ¡mÆýEi#|ÃøÓŸþ$HñçÎÇÛʽ2‰Po\¢wˆûÂð©O}jè$›ÆÉ£–ŸØÞ­ývèßö¶¿Ìñµ›nº mñöj•Á=+9I²‹x#òo|“Þ’·ÑN²9¹sòɧwmøóùýY&ã~'']¬KŽÿâ—†7yDêõ^ÏwûöµÛ)ŸØ9á힇vɤcKu÷mçõ¼é´<ÿ‚ŸQ_¼{ø oËý‚uù§>õI>|÷ªLà¸ÏÔôKÜ~‘Þ¶lŸ}ž2¾AV~Ûc{×|é·œ¤IG›ðw¿ëoxù¹,}œ˜˜`ïã¼ý=t8ïÇç K9FVXmëD¥}‘žyÀàÄÎ¥” .Óm³Ív´a7Ð&¦6$E¬L žç¬Ç õ¹åÀVOæ47¼&ÿà/è*ÍæÌÃÂzI?’gîR*ºÚÙÊ:};òBÛç‘»“ÖÕåOÅNˆ˜’.§G…ctÉnlÁ°ŧD‘ƒÕÔĵ´l3Då’2á¿¶ï2QÒnÒ/é"¿pàU>IÔ4:!×.ÆîÌÞÏhDÉ0D”‡Aí³Ý1P ¬ÞÀk$ÅÒ•`Š‚Í܉—Ÿòárþà [Nþㇿôù•<D< V~i³¨žÆY:Üõ*w—OrÂI3á ¹ž¾®)”Ô-hÜN3¯™Œ ¯‚îâ4fHZRñ©9õ釯çç2ò¾þ†šÎèëÿí̸|ƃ}ô£ÿ@‡h‡thœyx›µòÊ]|¥gÃÛÝ/~ñølTÉÙ÷.ƒùå/o óÆGŒèôô¥üܼjÏ/tžOÇcîúu ¦&ŠìJ­JíÉNGïк¾½o»êªEaï’÷®Õô»rùò•ÙÇàºÌ8 •è>t¼üÙ‰ÖÞÒv¶¿Ó÷-…ë¼=öýTdÿ6LLLf¦¨Ò ìw/¯kçÍ«õ†v÷¢AÐù5×ýöÛ/v‘žM[nµy6šM°1léÕK†Ï~æ³Ã»Þý®TÎêbåÌv·­vÕ¾º[$}þ _ ±ø‹q9³Ξèê­Š§±†´ ’ªó˜ètîývùÏŽ³¯¬L.Ü~ø%otvètÎÿüõ¦@¿Í<*góȾûþ^:ßÂ9íÛ•™'êòzvêžÃ7 Ž>úèÌòôÙ1_Ýûó¶¶ñ7}­¦ö¿Ðýë^' ã­Yús=i¶ã¬öƒa\‹/Œo\®ºê €Íb+¢û™5·¨s¯ˆK~tÎàû6k·Ýv¥yU^-;¸òç ·ËÕBâØY×Îï|çÏŒø$Ák®¾¶ò°9tï4o¼ñ— ·ÏÛÓÜY-ÓUÞ¾ ð˶½aSoÓ·§•iþ:Ïï}Ï{±é÷2{xõ5×f [{&¬Ð…\/o}©™ùÞÓH|+efN½èVøñWâNúo½Ó'<ؾKÐ8Èõ+·–‰w¾ó,Ú1ƒ2O6ñU¼ËÁjpáò´õÃÅ‹6:õßO:ø „¹DÀ2!]Çy5SVà.й̨oÒîñæ©Í6Û2ƒ ß¾îõ¯ ß’ù›žF–™3gQ®g0@y'6ßÁ¦ÄnëàwžÇ¤l`Óa_!i£Ì¥™È¹ù&ÞnÇÇîá­âŸpbÌ?¦Lo̾£?ü×0±ñÂö¦ÌýµÇ‰;¼ýù7¿ù͵þëå§ò^á¬\u÷0g5KÚfÏ{Õa,=Z™¯8;€wpîÏ™hó¹òšïÌ¿Ý9 õyfêÙÞtÓ-9Aè6ö"Üù…‘¯K9u–/–pÂlo±e9“ü¬'¤óÇ|xn¸Aê}¿Ví$–§ ½œI5y;pðgy¨¥Ê¾¯ïaÈÃÉ–?ýÓ?KýúÖ¿ø¯õ,¢`YêÞÔ]ôZx³ëÀC—2I¬®â­CøÙNº˜NN‚-aÙoÕ_ˆMÆú‚e Ö.^Í©AÚËúnzj˜ƒõ#^ÿ†á’K/N½ißÁ¶Ä^‰éè8·³>=õGÃ[Þò_†|àïc3ëT—ÊöD×ëÓéõÁ•W^ÎòÍžÔG7´rªŽmZú¶Õ‡Ž^v¦Ç~“ :MÑL`ÿ.¨-Øh”›zJg¥lW³ù·çUaÚ®ìH8hÖ¿Ê ÉïÒ–AFì-›œ- X…Ç*\õ7ÚÒ-|"m…a„ðlZË#8Ü•!l e "^â‰êçëˆÕgüå­½¤Sòí>É4Ò—vø«Cו»èá"~ú[3ÿ¦ h ?æ×¬4Ó‰.c4xo×XQÖæâ•ƒ 4>k ùè¦ŽÑ §‰Ié‚3-\Ô2^¤ó1Χ4b‘„ qpÌÙX g¼\z¸2–H±Ÿ§ ÈpŸG úyêt’ú&¬KÈà/Ãå]p%ƒÙ]?®áìÛ‹™Myõ«_•ŠÄsû]?é ù—¾ôOtÄ6ü¨–„?_T+¡¥K—拨Gq$Ëq¾‘5énøq²3P$ƒýjvëo»`[ô{Í»WÿÎlïYgŸ½ÙcE^ 檦%76pÃø¾Þt  oü ÒW\Naš5¬æìæžÈ®½ôsôžZsç]w¦âõû~Œ«ë`çS=l¬Üí€:³ðQŽû“?ysÖ#Úá»öšë׊©X¤ºx—Þ¸áÖmštÇté¶±ôÙÏ~.35ë¯?y8 ížUÌ–lšŽžu±r´òRŽn[µwçä“OÎ,Ñ?þã§é1ëÄ,Šè9çœË«Í±cgV¯Ê[ 'ÖžišÜ=œüýÓŒ<+Œ2IÛŸËU\ßzæ™g0s;ü¹ÞZ›:ðùú׿–޵3®³´³çÒ›=X‡=¹pat>çœs²¬gCf›î¾{%2Ù`{ööÝù¸Ñ=Ü¿úÕ¯fƒ°okºÍ»ŽÞ•Ëüã̼3Z~ì#ù0šÏ`€¸%rrðè×|YfÄ à¶lˆ} ƒ,xísê©§ÆFsx“å‰"–ßZhWO):úMGÎÛ¹÷«½§œrJ6¼^Çú¿½0—ü`>˜ž¤íÒ/—ó|úÓÿ˜Æß}æ…«—^ O¹`+SûKÙ"Ÿº9ÜÎç)§œLvC Òï¶ïyM;Ùéw†ÌÁŧ?ýéá G‘üæÉ!W_³,ƒÅÞÙ37à®\y'a9]gì\7¿;¨¾‹cñzƒ^¶u‹%®»ªcRŸõY-ë‚ÔÀ¤.™'''ø¸Ø©Ìü—Í”æq;êà[“ž?µ§ƒÐÔŸýìpį³tÖœ!\›NBÊ.WËoqtûï¿ÊÚE]ÌFç“È,SheßPiKëšOüfÊã&oÂw=êë´ÓÓHZèL#ëªÃ?œ7!WOg–ØS¾”¯§ÏïîU Ø Ð¹ ÿÝK¿œå¶9lÝhþtpËß|³xÉ%qúÖwóvs.y¼ØóD/[Nê¸9þc>ñæ7m6É›*óÑr&´œ²ói§×c)]Zë‰5;é7L¬§òñAàî=rÙêôúA>ÖÕÊ·|¹‡0œ™Áò'X®èÀ×É3÷ X_ýïÓ݉~8ø±žÞÁ/äæóVÑvB—A_ôPêU¡Õ”½ÔkNaV5|ù%$5l¡ëv© Ö.Xxèi`cW[‚mǃAû~ x©2òT“ÐÕŸ•‡PU×ëŸrW4ªÿXñ]µÄ—p )«7Û‹þ„¯Ó†˜&ãd‹Bzëxøãozö"<>•ža¯™kÏVߢÅ=•@ÃP@ S±\) ÁÀFθDwÚÀ—aÅCØÆDÏÔ«Ò;á©Ç裒‰#S4åóå ª!@'ž{&¬Ñ*Ê’)ybR-D럆'‚.ÂÇôCÜžÐ&BfìBKPi¶6]œÎ¢DwaTEƖƆ–·‚ŒÀÜÿfeÒñ¸2›\ßÍŒ´œ…k£÷ÜóÉÙT# 7Ølxå¡4ìö¸]ÓɱÃlƒ{9Ú·Ýí°pÇá::?.ÇȇVzb‘Ž÷³j}6»zVðt·Ñ†›Ð¸³L¡ˆÑ¨Ah—¦ •Èü çÑQ¹¥EÖÍ#Ïì¼Ú±ª¼c8ù3xZÉt&%¿ýŽ[ƒp衇eÆ[Ý­¬ÜGàŒK Î:ëÌÀ8k @ž?n‘•%ÄL¥oåÃ6ÚŽæMÃÄÄŽÌz»vy2g|ÿâ ‡ÓN=m8—c,]fe'Îãµá O^2­Î<ó,:Ìßf³Îô^§gq³3¹=þ~YoΙ™r–ÂeE~¿aåÊ»ztî~16ëÏéà™gÍgÚÌSu\jd‡Îå_º7Ñßm·]3à4ÿÝxãò¬ÝwæÇ͸Ý-\¸zÞ’Š3È©äÈ·! Ðúsù@ ߘî6Üp£áÎÛ Âfqgµ¶ä8¼eË®ayÐX;ÿ¬4>Î ž@ããæA—ÙèæmÔ^{=™Ùîi\7H'ØFä¢ /æmÄIÙxãÍhˆggmª’8©G¯„Õßüi¹Üf›­)KæÀÚå““Ék6‚‹/N¾ru饵Wd“M7æÐñYFce>–^U Ð¥àØ)vÀt·Aòýô5àŠn²9Óï̆‰m2ãcjõNL\¯wݰ8A>ô-UwžÑ½pr!ƒ¿‰ =Eìü;XÔmÁIM\ÎWŸýÈ\äçÙôïL #k(SnítìL9 ߣEÝë³9ƒä«› /|á!™õµÐÓèÊ+H£‹/ʦ~é9PréÃË}eÞ¼X?Uý9Åíw¾Çž¬¯-Sß|Ò·¿Ãdz.ákðm3?åØR`;~ïý÷d‡eZ·ÓŽ»0Ëü\N˜Ì›FËŒÇI[¦Ž?þ‹Ùf›ms:šyrº[w¾¹Â<ùÃF“ý~É~îúó8n“aéÒÅAٛ㪟Ê9ò“““´{[¥l:QääÕÙgŸÃ`âœÀí°ÃNòk®ã»žvC^w MæOß%<\æä²ÙMóV|Ûm¶çT¹£òVÔÎëÅt¾ÏdùÏgœ²cgÕ¢ãÀȯE¯;{7„׫^õêagÊÂÄd•GÛ6—0~áódzĶÚgç/Yø~QokVaO^s±šez”ºB9d€3‹ým P‡Q`tQ噪1œ•öíÍý¿R_ø÷½¶gNY_,¡¾¸‚ Å/ù„Pq`㇯cù¡“„é›Xɇ µG˜yêàªqi ˆóçoBßä‡]xc½ívud°)K—.Í’É^ ;19ɶ«S·¤®yë¡6£~•†9ÅßPEI_ ùÃØ±ÞlòU'=œ÷jÕ+¶‹„CÄv:µ/úÚÉûvFRW–JeÁ><Žw3–>ÙO¸ü?©/$e_áF>äåa¾)IgTê­~ŠÑdÌ¿Ý{­6Ý‚Ã|á¸BÿG·óλf´íŸK#û2© A3º…×T»•§n¦!·òæ¹ £^Ná:!àrÚNµPÔ(b¥ þiN@¥~´_b?U¬ú‡N§G :ÂzW®p¹Š§€*ÐëüŠ÷ªlyê²4>ÒqI¸mMMœ—m ¬ÛIšº.H÷ÙV©ƒñæ*o-—Јê&úñ`ÝÐ ™QÐŒ9N_ à6¸­0²JJÕ©*Æó—k„ǯì8ášT>5¿X6b•pÕ ¶Çƒ#’.Œªƒ”°„WáÍóÂV·gŒ¦¡©dò’n#¬º‘Æç—ç@íà:8!^=¨ œUôäU«îN?íôljòµþÞpd–,Øz[Fèձ݊N®ËXî§#b%¦˜ù°³³šË9FÂÎÚB=¿&]øõ•”ûY¦Qñú G©[r&-A®Ìj…ŒÆ±‹Tª‚~N–n­µXKÝòhH<òÒ&΀Úá©N…ˆÙ˜ïÔ)™ 1âãk]N.YÍIVÄÅ_z•z'+ Õäã†=î´ÏzœÖr3KŠàÒ;É.Jî°Ó+Ÿ¸Æî>NÎÙpC:™ hÔ,ë½ —¯ò¸´çzÖ.ª³³dvŠ­¬íôù†%d*f°^Ù™GÜmÊÒ›ù¬%µ3n¼4½{6»3?y£mó“z˜.Égvü5`hª»²×ÚLgtÒ´aвÍÖ€ó˜ 7lN'î6'Ûh»‘5i@Þ½:¯ClŽSv_§DHÌ 4vÃâ•wýº¦¸ÊäëqéÆ!‡oµ¶Ûn»l&}éKÿhpé”3Ãçž{n6g³8¯¶ïºûvÕ¹ €7c}&ºÔúÞn{;Ü71¸¼ë®ÛZ^ÐFðQx\™§=`A匌Ƞ³íò¬Í·ØY=r°Ò@úÊ®þ¦¯{ÖÜ»š°u³l‹˜ÑŒÄfO`åÀT<Á™¡Sˆ&©Øù*bLÖâå[vUvý8.úǺ3¡v¶D:ÐtsÝfô™o,÷=ŸÕ€ÛM·’~76²1×RáE¤òŒüçgGfËÔÌë95ŠÀØNy +>¥Ò†s™)tÉœ"rIè#³KÝ<—ÝÍþ.ªNуÃÑoùS¾/²QÒ“T ­ÇòEóc–vy,Kú[MÝ©sLKËÂw9˜á ÎÞÏ€¥•5 `>¶|[fœ§CKYۚɗçxt¦åßüfÙ0ïÞÊQš·ò¶,}Ú' |ƒ'Ìwøõh6™frC̘ææÅ ómÀ :ô[3Q5›Ù÷Ê{b«î².uyä ×-§wÖ¿Õ[Ö¡ÒT·ÜŠ.Þ´3.>·Þ÷T—~¦½¸Ÿ*wýõ˱‰}ë\ ´þ&pbÁ/á:Á!Õj¿Š—o÷è¸vÞ\¯cÜ\ëDŽû¹\åÛÑ .RÁ(«ËCrE€ßÂY¾);½N‹>¶[ê¤Ñ e›Èä¥ì[MžtÒ9<#e<Êbj²IW\þ¢þŠ(>Õ¼”a (yŠN¬~­ãÿËI$λhå,¿%=Ãÿ¶ÛnÖ™9/ŸdÔußûÞ)Á²“él±ëýn cL,Ïì—Ž¤›N©,rü)at`tU ›Ì )¿éÓ%³_0óA ¦Oþ”œtçÓÖ•yKÿ¤V„!ghOøeW¾ü›žÆÅš!±ù  •,úÛøš¡k+‘ t_µÚ‘uÝ£òÍàíEßPI€A~³*CS€ƒžxó¡[g– MU¤Bj/ mÒ×Mè&¼˜È<“™ªÛo¿‹4¸UÈTXF´¡¶&c·“2z¤ª:j+óä!ó%ù„%È´‚†pëàgÑ8i"7¦Ù©ó ³CåšÍ)I¤Œ˜h)VºE.ljCm¨ic¾Ð¶>WþÃâ+ƒñŒa6o"Vð[¼ûÈ' ÜKÛm®î•,sD†4¸§Ó­ ÐT*#=}‰Å$ Û¹ëW?¿+ sso?5gñâ% ›Ï,ÒŠ‹á7'K”–²ŒM.6à™1£Qñ-ƒtz^ejù53ñÅ[žþpˆ©øê·öŒÙ™_Ê«éèG­Íì¬Ø¹°±Á¥/}ЃÉDm®ìZÔ¹òoy,¬m“ö@rDôÉ|Ïö©8Ó£‘E†¼YиBl'2"?NiܼâìåѶ–;aä9â×0ë§è>´òÒy„#áv`º>PâAýW1(fô™¼V¥S= n£Dy¶ú…ã%K—ä9ù˜òä‰/÷1ÐÓy6¹ËæV­v`þ±Cˆ þÌagÇÅŽŒùKYîÿ†À¤wã‰ÙóÖÏû#:LXƒmRÉŒ1æ“G„þÿhy²ž`i¤6H9 åã_^„ë\ ìDoÁLg÷ØdMgPñ¾¥¬úÄåƒòï&[÷µNwCêeò3Ÿ9Q`™¨2lþs/{È„µPà@-¾Ê[I´Ê¢wi¥½`FW_"·n¶<½êü='ì’/ 5kíÙ¬»¿ƒ´K=H MÐù^ƒî:í£ŒÊnÞªöÂoëX×T™¬Ž§öV.x³¯™Ûˆi}«]Lon)ËømÈÀ©| œ —:YÜÂÛØª/hÏl£­§(‹–×*çÔè! Ó[Y%$)}õŒ—ú3è§îY :¶ \´ŒèO^ _8¬o-h3é¯+zåW)S”MŠW"#û¿ŠçY£t} ERÃFøÜ1EÃ/Ýј`ÄßÒ2tà \Œ2-¶ø…ª´îÅÆ…þQ IxR·ªa±5B¹á-ºú±&‘_*~.nø…PÑÓ5»‡‡Ò_/~1É÷Ê:½­é dœýü¢'/§!åšè.HnÊPÈEX qïÚ(Ôg'¨F‚±Eò0@…©â ÙèhŒ†ßíЄ!”Bb`N *yj”Fr—"rOUXF\ ¯¢+Oðª*¦Œ"}ÉS†”Ÿ?p âL£Ò0 °¦Gƒ¢ˆÀ·9¢"™ b•HøŠ~‹¥,·X ^qèK²¬F ×ÓÝyç­ÌsªŠè€I¡rí³:Ða&N]}À–W~$± °5k#¥VèÐ+(ö·±“ ?þ‹2ÔUjZIçÁt°ÁÐ8é©‚ÆL¥h”ñU©éWNÉVöGÒî1UAZI˜ÆVLž "NU`eÉTæ¢è‚+]ÓKzEWŸÿqÀTƒá,©±÷zû'ß°ø X6A:Tɬ\ï³ñÒŒ0R餀¥™/M3p%Y¸ëbîFàOaD·Ò‰â¦žüU &í%o5«òSy¿W*$µ½²`|tÕ±E“ŒrñpÉxö—ïZ4 òÍL;q&£Ìžßé…BDðaó‰¡ákZã[ X bLKµPÿkñ0:˜:O6Ò–6Ø‹]•°Y4âé|nãD÷‘gmÊ›­°­F¸Ò€ØÊ›o½¬7´á ÐŒÐÐBßà$ê·ää¡2 e $å ?Õðþ8:-¾A½‹åOzÂø¶ÃïqÆüφ³Îýq6Eº¬c:®Jvòqí Áe3šÓÓȺ“ŸDãÔ¿Ó2Þ8ߨE`ü:ùÎïì)U?ÿÅ…9õÅe]Yj… ]‡Õ¼5|âãwÏRÑË®¸"ûzzœt§LêôreÀ¦BqÖÚÐ|MЉ2Ïh á4ßñœ¼Ç ˜ž³+íÛSâ–U ‚-?а2!/6¡G,3Y"¼âˆZe§dRN+½¢Jd#ÐI›/`9¡K"*ß<ülŠtñÅüÇÏùðŸ'…1`oòÖ½2.Ý»S³ØŠï À&ù¥\gõu`à1”ËX“Y{äP;ìÆy¤tnåˆÛ;Ûƒÿm³`AÑ¥óïPq=Nׄõ¿8{sŠÕæà‡þ²†ÝU»&T>ˆBÔzÀL‚ü­?–¨¼OO%¼2DòO"ä<&¯ðh[^®å;SL°”ÊçÂV]c8„×õþCp NÆäý¦‡ø„wVÂsíß«çäå&gX௺TzÖFA¸9Ÿ›-ºPa\°Q!x–`›,ÁQöð*R £ø5¾FHç]Ú…´üK{¤þú36MP Å!uJ¹ Ѫ;Cc&L›°ªÁ"C?È¡]ðʳ¸Å_^qtrkF­êφ²ÚÀ4VŠÞÆ1»Q‘7DR‡fß@;8‹~ÒÂ…}`rŒ&8´F¹­¿œˆdbÍÁ®ê9°íu¼Õ?“@ñ7®ò…°2Tˆ†ç³4Á+¯öi‘¥`ƒrhq­ %* O>1.LlsÊ–Nˆ… ׸AŸúD `¥iÈÔ_Iahã!á»HÆ”ØøD—ˆ@!Õ¬Žðà3 ®b´ÄH!·ÍÊ?Ó„uù㬨JŠï½„7&rD–w±§=Òá,”Š{º>‘ƒ(e)'Œ¶­§*Pâ r ¯–×â7¯„K§«¬ÄT+A1’°ð*Z=ÈËY±éo¼`êõlË#¡ÈERù‰h ty+a2ÿݰ¦\FÆ ¾=—MŠ„éá¢acº®.¥O )7!AhaikÏržxøÙRÝK¸6ä”Nü‘£a¥4*‹yF¸‘Q<$Øó_ðÜabbB/‚¯É}bb!yü.ÞÂÊ·IaùÆ¥ì 'õŠn™ÊçæÑ.z5ÖÚÑ4h „‘DÏ!ÉżÝ*Ud÷í[‰ FøZ†*´ŒÒÆTƒñe oôRiãçíœy1Æ€¡ò“zU(ô:SÈH?W}6<%›°¥÷ÎØh+¯˜ISxà"«Ê-}žFÑÀ4XÀ*[`¥Xº×Û qÅœî¤+ Â¸Õ GdJ¥hìQb;ƿݟRù!¼[o=ì³÷“3Ã~+ç—;“îÚê[ˆÛarbxê¾OIœoO®zÒŸ@g~íáÇ?ùÙp‡›3ÛÛƒ‡Êû 7]»¾ÖðŽªõô;è:ž¯…Ð5?k îßÚ˜ÿOê¾tØ7Vð•l-nçßÁ‚xûì½Ç6nŸ £Îþ{jÛjäü%û[o5ìÇ—³7ŒÁ€ÿÍ6Ý$T¯¸rQ6_:øx¨Œ‚}mkø)k¹^NÌCþš#Í-ïÉc°ù§ri² UþÌ•':zž±}ì•@óš„ )ØN£—杖 .d#XÒ†tŠnòiªQäÿQ×¢`ˆî–è•¶³ “¸Hhœ¿f«†®8‚ŽõŽŸõP:©%`«Ûq!¢Xa”½áBtÓ*‹Ævú½Ž4¥ToGy0R§–Ò[/Ò[h.ß eJÝŠU‰Uõ@Kcà“fÐ ©àkéšG/y úG.žÓŸ è†–À£ú†£ÜáÒx7¿¤tE[Fa’礛œý·½Àƒh`úW¶¬ºW€’öú©»éU{ÂsHÊHBÐá9*”8îxªMÁ—z”»$åÉ_&@­Ëå¡;¢4ÅÏ%÷qâÚ¾ònÑ+HI„&±ENüNC¾:e-aͧ:e‰`yf Pz5‚[Ú³4dnø(Ò-(Òa4”áüÌ©Âòƒ&¢—¡L E×Ti,Íž„…§—+ [Ù€ ±¢&¿ ë¸-¾Ó M±í‹C!V‡<ñl|ËHÆJZYºË,‚xÉTõ†wn!±V6Ó&‚¨º‡ÞªàÊhtåÊ[†Í6YÀ±a·1ûsûp)úÁNh}6Y6¦Œ­RÈÔKÙ¦90KGE>ѱ ÔG Ÿ´©.™4ŠŽ1à !}•VØéÁACⓊcÖ2Ê ±UqYxÃS˜"Ü -z範¦SG`C•Ó8%©üBöò‘g;¨•z<È!4M‡Ã¸ €?¶1Œÿ²½á:˜5oè({·³þ \øü$äq %àS®ÕIš\¼•Üî1É“ Sxi`„1.eÖÆzª•ÎÎͲe˲$ÈYV]ffÙðíþŒ²¥jð'32‡퟈F;˜ .¡à©6Áwº]Ô;hÉh!Ù°¹Ì´áTq!Ö*ÊÐ:ÜJäQ ÜòU¥¿Ï•ÆæIÓÛ†¢§Y*ðЖwÓ-™d‡+o:@£–^(šÈ%‹OÚ©¤æÚô !Yãªa€F7[xˆ…ƒ¶A¡5å-æÀ¸´LÀN ož§äªr‡ äRl€—a=H€ÇÖ™´Mhôñ(^ÌGž¢µߟxö³žÉÎ5Ã|>ªç¹æôœá;|ˆè’K/ÏG}ÙK9¦t³ÌÜ?óÏNüÖ·‡{ØØüÔßÛ'ñûqÆü©|LÊÁ‚õYÓí‰0vØ÷ÚsaÏ=žD=½’ÓO8†ÍÎçþøÇùË–í‚§~úðï§Ÿ™ï·¼äE/và;$ÚÄuö¹Ã™Lè˜_ÿ¸Ý†ž¹?kä+ìû”½µG@_›Í”¯älõ­9™Æ%B<óÃ)?øÑðÃSOvçÛî_¹Ö²MÒ«:â73ªIZسÞ÷„ìwóˆ@É ÅlLÓ K®¬ÌSÙÀ|̳¹Ù¬a^ÆŸúú¯W½É{-ß™«RwVxó7î–Щ >ÌÓæÃÖü®…aãª<ÞŒ/¾Á×_`Ÿ¸¢)ß‚->²Ñ•½xð9 V™éð†WݦÄ-®!‡—8ÝÅ_m\‚ÏøÃOy£y±#BÕh¤%@N¡¬M¬ã℞»ºÃOQºS~!À1í 4ZI¡Ã³t©ô’®?\ÒAôËV>—R¨«,_3¢’²Èg„Ž:µ…µ­ôQ–’­@ÞÄП}b☸J÷)ˆ ýü¢¤®4FW›+¦¿´$:íøyœR“>]ÑT.aT€i° µÔÓH`‚o¾3ÌGä”,.ñ’´m„Ïj;|k—‹iYª!ŠTy)õÓp]â“Ôy,†R8;øºžY3ëUøeT L:1Á¬çŒ¦TN§r-.ìÅixf¼Æ"<àVÉÄ]¦ª4U)Py“10g¬Þø@'•Oè- ÄþÀ+}Gµfvy§Ò‘f$è‰0È'ä­ WXDàRùÍL$Mù"o 'L%!¥&ÏÝwùmqîŽ:êh^ïÍ×€_Æ«ß94ëåsæÎJ‰£*¡yRv{ÄI‡šA&²Ä„žY!ð@ć'vèã/êãßC]³”¤££Ké´[F13Ú!&%í+–ñ"ø,õâI0VqÕ6EÏ{$ J| <ÆW¥G¸öUèz‹-äYüê^üF¾ÈÔÁ¥kÒIK̼I~„~“øŒ:J†Jß’;:[:gZç¹Â:ÏÌ($ÏÈHžÜµG0ñ…(Tšž]À¤³ú%\œ¢þð|ß­(D2`TG®iÌDÇW¼Ÿ\š<üïöi eÛná)í¤Û!{f¨dMtéV§¯¿ÎS"ÖNüæ·†çü>µÏðŽcÞE¾žÍ›€ëXúÆl½f“–¸Ú!D`f®b¸…­—Q(pˆÄ#¸ò±Ä OšÒ*yòdH\Uø6Ò“ŒñøG0ÓÑò+a¢§ yP›´´ñQ*sð€K^•±åÍr ކ_ÈyÝî‚ß—yRW9¹ò´ü™È-=øU¾Pæ6k+âÒ9 ¼ü:µò‡ä埾²3·8íR4J?ý%hÇÃR˜SKSÍAÀ£ü³3¬~Îä;;nݰ¯ãÄá#ÿ븜?þ><÷ó‹/dpà à¸r8öýÌÒgþÏûéùhÕ/.¼ˆÁwÊøØJ:΄«Ÿ§½ì¸ÃB:ô ùŠë/†¿ùÛcó-ç=÷àañ’%ÃÛþúœ‰~Õðd ¿¸äòáÏ;8ðÿò¯ÿ{xßûÿçðÓó/žÆþ]õKî/xþóò•Ôÿ¯ '|å«õ;çœ÷Óáå/}É0Þç>ý™á/ÿªè>uß}†9ÚY\ß ÿ’ãV]¶ä² G˦ÓÓJ•”ÇÓó]…UŽLý>-­Mñ 3¿˜§+ŸhÐdm Ùò±¾´³– Ë™ù˲Ò~A7Œ) Éd@z˜<ÀãQòS¥%¬xž‚3OW^¯²¤ ’HùP†‚®,®, ªE<ð“gØWt.”‘Í)gŠ{“qÔHÄ,ͽv<î{Agl{ˆ¯vÀÚÈ:!Vxö²-#ƒì%•c“«,“‰«AAÕ“¥¥RŒP™0)ÛÇŠíòJ_-¤­ Æ«NÝ›€>ñg«*,ö; EõÕÏM_ÕË%w… ‰d>ð±sØ7dU'ð^ÌSçBS:¡ÍÁõED«páµ!òEp¡~yÓŒò—L5‰"ñøO»ƒìÏ?\¥tÑ×Zt@HìÌÁº´¿`)OéPòûvÁg ›+`¥~³m£U+1à”ÁC„„—IéóPœ—#ãXR¢B"t *fþÊ0ÄÅÓq+ާ˜Ê{%Ä%Á³‘_Ѿ\$‚tW\4p& E·(šf’ ¼Nãj,*1]âHó ŽtÔØ,÷hZhUáÉÌèÝJªºÓ¥¶A_¿úlš{)Y"aÐÄT⨠!:m[Ý å±¯3‹#+WpfîŠ[ì4•›5s.øDvý•A>QM”©¸ˆÑ TuÒ{zDäkКÀsáGG@ƒªô¾0ºX\{É·ƸŽÀ\RüÚW=uÊ´clJº$¹Ú 2…ò)XÑ´iÙµqo¶–¦´ÕQèãŠÐ§ˆ*ÎØrÒ.hÂë3&¾›…Ã_Ö]»¢›Ä•ñ ú'zñšIcÑUXb‹RñMž ¦¶ ^¥f ¸ºGTèTžíù¥çEx…9°ü‡mÐäןµyIÙàQö`ãeåŽi¬ ¸È›´“p/zûl€zvÈ¢Qb³¢;3{özY§þ„8—9œÉïñ¡ÕQ !BÕŸ[)Ý@å=Ó$‘ë+œ²‹h<#Ð(·åR±"±õjç!iŸ¤ ¸^Ú€«”*·&D1ˆM'^ÝSî$íàQoYê‡_H 'ž¶%‹€TÌ\¹5‰eÜD¨Ê— G"ÚÆ›´à/=É.ºizh¸¶®¹GŠOx"¼.,¸qÊUþ†Ñ¹©CòÇHçò˨²‡:K·hgy •›3ÊÕ¦õ(º¦.kþïÊW¿oà+⟠ã¼ÉÆ›d-ý9?>o8`ÿg ÍŸÇ™íe&Þ¯…ßÎRO|ÒoRl8,œØ.oìÈßÂR›Í6߃tèŠPº¹ºm«‹|„âŽP [B‘_I¬ˆ’÷“»à‘ed+.Â#êÌŸ•×0-c/Aøñj8”±’•p1œ·f[¨”4âðçbc¾‘]9}õK؆P;`eQÒ}à<ðâç@Øë|x@Æn?£F{7ÚÞ,kVßÇòŠØ<fké|ùã䙽¬A+À±A—‰{ýGºnGcµO TúšÄœ!‚vQŽ@„ኚ|S0£Ü!ÒâxCixaJG<(y“B!L¥õ¨³Ü½”×àA'e;¶2Z7)È×?]—+"T™S/E&´äg˜ùüÔ½g2·–@IDATJdÙ”^Õ =WR£ô[C§Œ‚Ž7b5kÐäÈ]È.áFi¾kºD:m4âMá´L4ÎN§£:e ¢÷(]]¿ËtœÝ¿è¢‹y4sØ‚e>vÜ7£Óî¹ó«lžÄlü˘]Ý¿&nûéùçÿüﲩv| ý¦›V ›0Hp¯Uu¬Isö§ÜÃ××ywÃïU‹Û°6ÓMù€!ñóÎ;/øs‰›Ë€.byÛÿxø­Î=ïgÓ8±Ç= Ê¡=×á €Â;ƒeF{ì¾kNñY¸í‚XÂÈÞ{í™ÀäÄÄðpݼì2 K/»ŒAÌí” _Ôé?}öÌw¿¡AI×Ê3¤©=š–졚ÌhÇf¦c‹«$5O^øÉF4¸düæ“Þ1¬¶Á0ò3.ÝÐ| jŒ–åð fã!õÎ\z7‘§^våÑõ!¸å§!âñKþYàåN–ýOð¡/5(ouR›XÅ7¬‰Wš,ÛŒüÄZFuˆ¬ÕÂWUÙ*Ü|o½í÷¶  kyS†Â3.ŽöÃä‘qÈWèx‹pi5å-Zò°þ0Æú¢;‰…WBÑ£è QqÆüÓÄDWàé#hkIJ^ò©xÖÓ°üé' ­‚Á›v‘»{ãÔ[>ÀTJ­I„ ·‘‘:’~YÓµBàޙܓ¬ÒU*½ôqÂ/\d`ÛÜÓ£d0Ö8y ¡Fʧ¿B€ç!jµÐh.8 Eˆë‘§äª¤ïü̃ê'`é*™jÏKŽ~U†’CÒòlÄ£°¸æó©GoË9œˆµÐH5W}f%"A‚¹†ƒVˆ˜Ê>Ò÷Y¤©Y!;Ò:¯ËO¥´cæè‘¡s ~ Š €¹‹”ÏQVÍuö8ã/¹c"„2NÍð+@Ë_™WðS¨’6€vŒÓ¸=³A:•ÁÄ( —©È~«­ ×¾âÁ‰Ù²_._‘éä|ïè/O±t1C,d+N~þÉGºÂWAï&)í|ú¦d‰Üy&Hg†Ñ¾õ„Ìf¾¢¨œbF<±TàõWwÜL)pÒ0Ôó¥»\ÉÈ‘-)P\Ò)Ö –i’ˆü©<µQQòVy¬Â§ò O¤ 7ΨÄ~®5R¹+ŸÜñ&¯pãå ¶åš *.-6p•O„±£LzTM6©*W1V®¬¥¼e0!:GIʼn§g*¯VÀ4Ä&…!æ+ÃB)•ºøþL3Ãø…žaÊn~0Ì8…Av[¥ÀÈ£ê ÓÅÆ}å=«Úb15¯:ëhS¹ [Kkê rÈe3IB?4õû³à±C*MÊ`B0J­@B&ü†’8lÝ|íÚô i³=ÎÈ¡£'ED±|{ÀrƒK£Axl™€?U7gÞQŠvÓk¹Ícü%CòOƒK®Hç£× B›·JŸº‹L(¶­Š½^®›UšÊ‘]u j•éê„-‰ÀPàŸŽ†é­íuá,dRFòWPZ< Ò7HYkY RY†ÂG"’CD×é»^}ŽI¶~Õ¢EÙwâf]?–¸ÓN;p’ÚÒaWî³×]wxï±¶§ÃýŒýž:<ë€grêÏ¥ìØ þ¥W_“€ åÕ–v¾oç” wÚ1:ÝÈ©=nÒu  †nôUGÏî·s'=Àítúoåmœ$tÑ¥WÏ?ø9Ù;àÉ>Ò\ÌéæÌžÍ2¢ëófBÙO;ýŒáéû=m¸òÊ«†?xÉ‘Ãk_õâo<àéÃ5×.¶Ûv›L ]Îö88°½hIöÕT3Où‘Ï¼ÙÆ¯«ºÕ2Òêb³º¤sƒ«|Ôê #1/˜òçckÇ,£Ä'_Y&B³½Y0Ÿ„fÕ£ù޼Æ|&aó54„%Ü[#˜pòŸò&ž|SV}¨LÕ_¡ByIÑ©«&½Ô ’µ®C÷БY‘Og¯º[Ñ67Ô‘%¹!¬ÑŒ-ÑײX’gCÊÚ-”i à¿éb9Óuº”u“1‘Ÿíc€;ï†LÉZõª2Š'õ´’øxŽÌa˸8Ý)³a¸Ð’K{ŽŒ½@š‚åj®ûÁ›d¯a0iY¿t}ÌKMýä¯<ÅsÊg~’Qð«tÇ¯ìæµüïª?©ÓK}e€’Wé(ö”nÒò¹xˆZ \”§èÇ’áIˆtDL\ù³1ñY0B©[AZwÙd$!1âáfÊÚÊ~ƒ®ì“|h=W¾R>bå@8„rÓhAòI'›p˜ x¨Ü¨»’€(=õ u!’€É=qVÍØ(xð¤m!n cX¾XXÆ.ÊJ‰ {+ŸTOù ìKèèÍ/6Ì‘+Ôz„þ6çi>ž¸óço9*Çy~ñ„¯ Oc“ï«{å°'÷œøýÓò5ìUlþ½å4Û2 ÆM7ß üœØfmö[å îvø,Ø:o–³ßçí·Û6uõ2–òÜ îÄöÛ¥CåU‹²FkN!Ú}׆«|¼ü%¿?<~÷Ç gžuöpvt)ÒS8ýçZ:÷óxsð 6üΟ?øÙÿ÷ Ûm·{öͦC_ñÒáÅl&ö#K»0ñ¨è«¯Y–%I†iÛßÄŽÓÓÁN¤¿œŠãÝ´ÔacsCòmÒØühª|˜ºÕ6#ùÄ0Àæ¹—!ËMÊ®y;xpž…†ßh§-‘™5äk;ƒU>à!?âÂïõ G×e$Æb«\®6#e€0ƒ”¸Ê<,§Y¬!ÚÛRi |zsá‘rë i*š‰ë”–~ˆék?Ÿùõ`žŒ õ¦s‚Z¨vK(BikùczùÚ)vL(iD)õ(žyÔðŒ] ލS—ª£BØ´j¨‘1@:Ü­ƒ Sü™À«\“´ob$ͺÞ•y:þ<Ũ·Ÿ•hÂLñ’°Ü ·ŽÞ~¬§a=ïÅ|q­o£ õ'ÓÛXéšÖ=Ö{kSGžÂ›™Ôš¦ È‘Mtàò×è•Ùpæ9¸ÔCe‡êo2ÂÄx#·„KH|H@ÉÃÈ¢«Ä ï‡ßÌ Jâ ZÝëå®åCÃ}ËAÄÄk ½Áe”QììäµHE¦fSE±Ëü¦®Sè>ÉZ3o®V jpåg¢FmœçÈ|äT€újéZ}» Ah)¥q47ú¤Â2¬BŠ 6ØVNâ…s4¢&sFW9‘ÀŽ(N ¾ T×nÏ‘h Â.—Ìš]·Ðl‰!­. ̺Î!a–S”lòÐ’ÊÉ¥œaVˆÀUÃ]ÒdPa‚S:gÙMQ­ˆPPòâêO+È!QÑųå‘àÄ@DF !u—~VC,{ÝùÅßôóM¸¢haÊæ ’òSXˆø¯ ³4å sÖãÁtêI§À'ØØx"±0h·Ä"¼iQÀÂHC>òm±’J‡¯%duæ¸äüÈX¸–•¤_h¨Âç_¤Jƒ–¼µÿ ôª¼‡„Óð“ÿ‚~:zJO[$¯H[Mz˜rä¡x«Š¿†ïݼîd¹ÍšÓÀ MtÊ]è芪ôŠošé”É„XÆË²*Ûë‹*› ê5ó_”’çÍw¡Uù%$=äl¹/^ OI«œ3z&AAµ@ÒÈ ËTç_½L;* ¨š|eœ<ˆOÚ¦3+‰S¢Õ©LÁM*º~5¤‡©“ð\cëó¸Ï‰7ݺ,¯_vm²_1Jf7Eë"_|„GþN¿xc‰†#¬rˆ/¬·”¦â†äÉÑŽâ¹ZZ„ºµÎ~–t4¹»vÊ"½ù{ûìãegÍš5¼ûoÏ›uÖ™5|ãÄoqÜæÃ%—]Îô=‡£Þðúá°W¾|˜7oîp*§ê,f#ºÒ_Æ›e¼1p;F•°0ùçö;ïÎ×y¿íö;Ói÷ «‹—,áw¥ã?›Ù|ãWrêÕigœ=ö–7g€1oÞ¼áô3ÎNg?ÀœÙs† ~þsö%쟂Vó•ì{YªtþdÒç?õDh(—6*9ÃÐÔn|¹á*Ï s—¼ÎÒ¡«Yhô¡mLiFÿJŽ@4¤ÃK\øÇ!äE„¸Âïe®þÇÜ5®Ê¹JÕí§<§nRÏN³hyµî/(쥼‚›fm¶Ñ/i/¸Du¥ê0‚¼âͰ_AüˆQßm¡eôã Wø&C`«ÑVBF…T>‚É]Z&oñÜ +° \ˆÐËŒüð–ö-i0ÅÃ0CÔ¯¼š·n¦X<ôvºúÓB£á‘šI €‹ØE¹R&g®½~Ës2iÆÁ %d=K¤\ ìK²Ž|6˜v˜0C1,‚„øÚ_ƒ'K€è-Ö ´FŠŒfŽÊU‰”:Õ¡1Á¢¡äèJƒ?e(•M>M]é§q*ÔWÿ†°yZ…H}šþe`‰v‘”vЋ—‰°Ò©ARóK·¢O¹|’”üø®Ø<•bôµ%áµCs*é¬'ÁÚ¤d‘He†Ø¢XD'±R‰¨"`I¡ä®Oâ{ë™ú÷x(½Bn$W¸i+â§ð‹±VêNe¾`J —ɳӗtMOe—¿Îøâ^~å–»ñW“4yµK clá]dlϋتƒn1Fí®7O°0U}V¼¥“:TÈQ¹Z…ÿáe¯|EuV™­n(ÆY<¢sÑ’«‡?{ó‘Yó‘ãþŸáß^äóL?ĵ喛³ÿŽÌšï½ç“X׿v–þ,»þÆa‹-6c“íªÈæ·æ³ÎÞ€©iá+—]µhq-7¢³î üê:ÿ™ø×fýó,äp)’v±c.½Ÿü¤¬ÕwðqÅUµwÀ8—=õÿ[±ÁwéÒ«‡ËYò³ƒåZ¹rÇ{Îæ4£'&u.øù…Ãõ,;r#±'þ¬"~öìu©òÍ+=¡ c åWg>ãÔS‡k®^Ä[‘°ÑjlO3‹y ¼{ž«‰2ƒŒ47×xZ])¸Uí ¬ –š‡«½€Œ–2#³@Š\tC€2—¶°˜VzA¬š‚’g-—Aa¦@ ŸòmžT6ËBsÒS¨Ô † _7=J çèÕ¡ä7VØ‚ nîæ¡À¨«ÑE¬“œâ£Ïr%Xl+°ÿ‘Uùä* ü!ëáW:¾ÁEácg.NZ*cpKGíá~…ðÐ>Ú °ªlC‹“ßò˜ê ÐÀ†9¢5嬗¬2¸kð‡f3€µ¤b03„^ú†ƒ¯¿·ÎSÖé8<>ÄW Th“Mœ¤‹ù(ùCÝ€U¾Ð/p) çµø®ß|,¬v¨4 DòˆZðZÔrð,»jåÕ¢ª¦ÅAìÊ-ÒM¸²šåø«Ao¥‹ÁŠ=AÓ% À“–aM†Ø\[Ôt{‡üÕ?BJaH÷´}Æ©GàóÐö˜økbâ3a+Ó,ù«‰X ±h–¿ Ê4^ ¦1ãS\¦ ·2ðÀ&ºøóH<åÔZå!°`ñQޏ…uɧ ŹQ%­¦JeȪµÌ§ì¡l"—Üe½¦«ÁÒ–x¥â(†<£qPšZ°:‘›KÀ„$t‰0=$ŠPB+†tÝÍ-Ò“6 Wz-‹@1+ÒVXIwåƒ?‰–D° _ÿE—0azl#‚Ü %H“¿³¥QM®àó,lÓ5é É^é†ñV¶*[•ªé£]ET7Ä ¡FÕÑù \ì7pR6 ¥‰Ÿ0u®2¥U=¬Ù+65r/¬üd|áçi,š<µoåÀÉà <¼I3¼@ñKÓpèç&y&¿é4E²e‚&Ki<-Pº”õ¥Xƒ±¢U49¢ƒòG8 ±› JE¹DQ¿ÈÕf‚ˆ_‹EǑ߼Ѹ„Ø Z‚MðÑÓÁ¹iPü*¿‹§] m28>BÞËÀá-'u¶?YÓÜX2{—¶¨çð¬ŠÏU8㽕§–• …³ÕE¬~Ó+ä­í¼Ï§óì†Ük¯½v¸ùÖ;†S~xZ–ôlIç~S޵SíÒ7ÿË¿’úæ„û5Ýÿ—½ûùpÍïó ß¬µfcÏ~Æ3vlÏ”:S*•6å€Rå ÍIi%þªÐ"8¨@ª8BœpVú9q’PDJØ© V(H= (1¸&‰dìÄ73íÙ¬5\×õ¹ïßû®å‰³3ÒÄ]÷û¾ÏsßßÍ绹7Ïý<¿Íû6ߣï¦ÜïéÿØG?ÚŸß­súæ[oí-E¼=È·¹9ö­7®ü ÿ¿âô‡6Ì7?~=§7ÿ×ÿøÿÎç¾Å€?Ó‡ýÇ_o2þÊOýôã?ùKÿéã3Ÿúdoÿ“$µù÷ˆ¿ðÿÏ=…ÿüç>Óæ_]søa6ÿ–òiåäÛêïGq<¶6·Ž ¡:Æ2äü·O%9Ƨçør<5h}"K»±sô7a7NïÜйœ¨•Æ[µck#UedÜ8!ÙÛ 8«¤ïùt6¢FµÑ_o ̽ Ë"@²Îç‘°{{6‹Súk<±t\έ¯ÇsåÍ¢ ××ÍÐñÐ;p6œæ°xþ16ÀjSzóV"¥«{7øâçpÍw Q¬ ²…"Jc»ê¡É€#A:þè]ŸøWLò3f²”²ýu¤3«ÔôFUÿà§«/¢r® /›Ò5àÁ\XWÏŠýv0i&ÁÁoƒX_Ãsq;q^¾±©¥®ÊB‚­aߘ<_;Š “ŸøŸ‹µS¦¦l£ï,úJÑ8™ITîõÓšù†Ø^lÒ…´…}ÀâJàÍ}•Ϫƻ£Ùt¡ø´7Š÷₾~K1âXõi7ß„Ÿ‡ë_ Nãy±xÆ $Î`W %÷ÝB•Øq¸mR·¯ÒMk´õåx’—]+âY†åq¹£–ùu8OeŠ7³‡|ý‰‚pØx/½q ¦¯ 8úó”Üú¢¼kæêª2MT©©ÎÅWŸ]Ûö‰Jý>Þâ÷mž´‚·óümžÒÿôö3/þØô @nâ}bîSyãwíæý‹¼ßß(}Bï ůßü6›î?÷¿ý Ý,ô Ewô­0ÿØGÛüÛÝx·2B6ʯOþØöGþòÏÂ<뇟¸Å›‡Ïðõ£?ÄgÜøûa^L¥‹þ›|†A?-Þ \Ýl–Ïßç<^ÇÌŒøŒ£ÞD½Â¹‡l¯ú¯¯¿4GŠøW匔ëC¹k¨DåÜ :&á&­ñë˜Ö Çí^‰q ÚrŽìšäšczx~NÎü;?]KüÕú°i@÷ú(R—ŒË[c¶ÏüÈ–|6MÍù¾âS< \ý?v_h›kÚ4nÏKŸŽ¬îüÔƒÖLÖ^1ôjù‘N=ÿõÀºœ]yÅ0¨g¾k« y°5 ‘è¥Û¬Þƒõâc³Šü—ôb¤rõ­ö£=µŸcd¼ù#LU”ÙõCꮳÚF½ð±1bzºc|Z2Þ’öTP•UõÆåx_†ÊH<Ó¡‰ çtˆst_?%é“ Q Ü–Jz3ƒŽ!\IfOŸKB }B¥\;žŽË=d 4‘°‡M:º›Ð²¢³»©@AüyrÔçpM® Ÿ©'/&e樃ùL¼ ³í)9•RM~ö§_.–I4/NÝê´OÌâ¬~쨣ñë“?Nf}Õ†‘98ÏtTgØñ5’1Wnú/e›/Ъ2òol»I-SˆuÓ~]­üÉC&…)-TŽˆö? ï,•/æýðiÖ1PN̳ ¶‚°4ìÍp†Ô3×þtgjÛ¾.D3¿Õ%NÈQ9;œ'îÇYð%3ø‘F‹†Q€eLc€gCù~‘g+|«O{bh ]¢ûAqð\2‘áÇ _å)£ÏWÜ ý%—,¸‚‡«h³œ¨{¦xiÔ6q+kŒïòa«I «·q³¼ï„õ²EE ý¶¾¿q yê9£¿ .Ïò†­žô•#Rc‘Ù{庿~“­‘¾Ýb*ŸQ›‘ß"˜ýäè[½°ÑŸ_J³ÐÓçºëøPŸö@€zþxæ¯âù4¶¸Ê”ó²ˆ6Æ’I Îlÿû8ŸZ1‰©iü.]bÅI#VõIC´'rÒDR<[°£Ó¬\]ù š;cõ ü.fÒõCA!û´—¯òûM¦p{ ®±hƒ~…ÚÂì­ßaT”¸E-Ê!ñj; ÇmëÉüu³Ûçðï9×.È?æ™­3›ú7x‚ÿöãϳy÷ýû¾·ß·ýè¾£Ãß;ÞfÓïÁÊrkØw‹ê·}Чóß«¨ûº,_‡¢ú»Û¿ó6ÿÛ¤Ù/ÈTïÄÛ‡¾EžÆÔÅwãù^~¾Ëæ»4~ŸŒ•Öoæ˜7mÏBºN\?Ïð¦Mpišé7S>udì{ÌÉ×~¢/75Ïc&ðZ'^)ÆšIï:ëܲu’‘Ì&vù˜Ìø7ÑÓÏMpæ„ÒþÕÒ^2³'çR–#ƒ)![çÂ*((+ OtëÎ-Nü;eùƵ¢¹h®¯¹ìJ§4ï_Ÿ.ÉJTd¤¯î ®¿³ÕñZú«RŽ醑7BèÖ¾lB¡Íÿå}>KÑv×%#Ê.QáÃ\Ô9A–*Jqòú—D¸Ki@ŠtQ£®T¿àoîíh”Ö`üò2qëûƹyyíÚz>t9¾èɤT6¾:%\s`Y‰µµr9X²ô‡·º»¢ˆi_4j!ӞΠõ¦,ÆVÞ€ÈGèEˆ‰pÞÍá0¥mÍUwå»Úv/Áùz¯oÉŠôð´~âCõ­],ÁØASx=NJ¨›˜ñDÁÄBpP [RO;Éy|7 Œmznç…«‘CØSïäŽrœN H}ö¯°ÕŽ\&©­³ä+¹—v³ƒÅ= Ȳ“@rBÔ‰¶2ÿ¦ÂÄÀ,™ùíE޶±©'û ìL!é4èGXtæ×Òø%®ªÁ<+Ëwà…•ÿ (b߀*c¿Ñ¯–¢waHÞ§U „ß­Î|e?³¡xgjÆ‘WÚ©sõã_ßÉ«š`¦Ü „›S'~ú-¾ÌšÚ¥ï“§-Ïóv(£' €Ü£0md}¹^?ÁBïe,yibO9«MÑÓÇõüÑÆÌ¬akÙs­Ä!ˆÜìž1±E%áù¹~|‹}FÈ3ŽãCãÁMT~²À!¬–ã0U±Ž…³'òHéGòÆO]ÅÜ»OIÌ.<°|z¹!hÙs!)“Q‘¿Û ×Í!µ+)5Ža&á™r^Ø+jN»sýc¶µq‹|Ôt¼¾:t†ãýæ{ŸIJ’Ãl‹’vë•-tPØÈô}åªÞib :ñƒ°Õ)?ðÊõIdó_ÉúÊM ‚Ž;h^‰tÝ’¸,µí·óämê!“¬öø18ÚõMh2•Z|Å==i,þ¯& ºÎáå3Ä£®›á”;HMMÖ<%šÃÒg#™”‹à™{7…ÆÇê€ÈÚ“mÚ3,Ìï_ÑQþüNKõ%$¿åUîù4‹ê½4â]&žBGõ½‚ïæ?m\ò{ÅéÔ;®È˹‘ôÒü]µßK÷w ÿãð[?½Æì€g<1“V½^)G7Æ]¯r§O{£~p^ ÊŒ;qàu „W·yàÏëøx‹R²²ÓÑÍ;Wžy£!Ÿ’0Ág )oõÄm¯àØ÷gwàÈ…|âÛ͆bžå‹ä[wh/QGML×fÎÀì£=û.*áÊP²óÌ·#êc|d±Õµ1YÚPÖ"OòÑÍuÖÒū̽&à‡¹IY|}ЗSR~nµVÖœÍ_ÅŠD|ÊbÁí }ˆm0úï« ÛgÊ.o5 /]!рɧ²ÿöêN½ˆhâbŠ¯Æƒ‡[?íyt„œ)8~ýöÀ6Áøò6}ÓõC”dõŒRÓúbb¹ª–Œqún€()!OÃê¢öš"­„í¼aòÞnBÌÈSÐÝ©T3p·Ïò 9™gΤKsØ9 Põ¾‚$[¯µÝ[¼d‚¥OæeûA|a\¾5Å48˜ë.Þ—¦Ó…Ï€®–ËPŽ ÍÛUN“]J‹0²Ðä®E=n +91á¿L,7= ðs«·åCEÔ{QÖíëiy’ëäCÌùk‘¿*‘é˜E¶“#X†ŽÙ0Æ38ëÄT:Ç‹æ€~1ÒÝø,ƒ}²eg#âÀ(X êðNÆ5°_ǪԵ?/<šwu¬§€=³WÆêxó¢Ü~Œg‹â ?Œ$8 ÙÂb.Ë‹OÒÄ*‹Ôf)„ÆÇÅN >ý¨ëÆE¥ l©U“þ<¾ˆ9 g9¾ù2&f)›õ-$Ó«?fL Ô¤7wú¢Œv9úW.Z•µ'¢NN:º.}†ÄºR‚(V¡æ³æ2» ‰É[Õ–êX7—Û»²µ‡« Ì›-ŒÚ7Œ9FŸÙ³ü•CýB« èMÒÆBØУéFU+‹‰ú €S’[¬µ}ªý1 Ù^¢7vlâõq–ÌŸ õŠy^^WFׯ|W;ÁçydP,u—ýT©eÌ<¾•Fl«øè¯bRÖ"óĦ{ÇòÔ‘o¬¾ªUBh*çƒ1¤±¹Ô:Í1ýÁÉÎÑÙ*Æ£s óMŒx´Ó'/AI¯l” GOßòÿŒÓ]¬r/õÇ¢ŒGüÕ//0£–þ'¯çÞðÊß$|Ïo¾I†ä˜Ò¤uËÓï°G¯Ÿ¦v÷k>ÌÕûÖ–æË‘ûý>}àíy®yûG*ÿÈŠÿHÖ¾•îï«@›+xYZÖ¯‘wTl>+²ë{#Æ´ábX¶q4)E¶®mrƒ"œóA–&!‰]ïJç·} x›vóC~û‚5Ñ[;³Ô㣠Vh)/ ZkÍ'¶6„àl“y5Îüºñ0[s]›¯¢â<>æ‡N‚ÑNI,q F<÷¡{£´¯mÜà ýS^9dšëè–ðÁÍ£ì™ä‡?=ýpMÂ£× d«‡ªFõ“>#ÖRܲ´>X裚â¢ï,l1-fZÂÙÒ&¹í¿Žß §ªlµ´Æ¶ŸRøT>ö/àêŸÐz…‰£kg"Šá“öª²h"ïæ9Soÿžÿ“fë”ÊG¯ý¡¸„ðvó(†ãU?°˜Ëضncú–7]Ë ðrFŽœ†(Û#j§ä.§Òù[¿øn‹$=„-G{UÑQßÑ7b7>ʆ‹Êþ£7¿ó¡Ò–e¾)U¨åêUælò®Iã¤ÏÓöé¶(ÆYÂëDÕIJb´ƒÀ\P "7èN$“ÊŸÀp)nR“/áP"{–LYYÉ©’¯·/E<Ñ^OX¹ú $móÍ“›e³ß‰>Ê,«ÆA'§§’Âú+$1PWv“¦²´õ) åh龪vfõW²±$®:"¹‘¶ñÝ *®änμI[Žœƒ7@õ—©£Ž/úŠü‚ÉþÞƒH?Ÿ—÷ÞOýBŽEkpï_6x|Þ´r,ËnJÑéÔ!ã[P ÖÏ+è”ù·ºGG̵Eÿ¶/³7}òƒ.üýqƒn‹ƒMdü•˜ýúÆš6SŸ“ΪПØá¬_7ÁÅÄ#Oðû–„$C.q- .ú¡à{©Ý8ŸV‚ĵü¥ç< ¬/3Éa~6¶Ào¹ÅïŒêT^©Ù¦+ý-6ØÄìÍln]ÚÍyAS+ABk—PºäN*¹Z0ìã£<}ø[|ùzæòûf ¥øÃ—á¬í1âÀ4<ý…xK±yÎ@Mˆ“mý Äüˆmÿ•XÊæ,´ƒ÷nÐóY4Ê“9ToX õÀh:Íš5¶ Ëú¥¹­=ô删w+Ò4M ˆ[£6¿Ä™Ùüš„2dA÷Ô°ŸÂ!bˆ]ÈgIyB¤1Bqä—ö®Œ üµ®3¿ø}ª}¥ÍvîW=fÉœòö•mh´L&|IÚ5ý¬ë£¾ü¾ÍÙgÎØÑÏ"+IwóºÒ1)WÑÖß †£·kŒ‹sçJ7¹‡º•ÞYëzá˜DÏáØ|p>3¹>ìz-ãeí›9uøaL÷Åu mEh ê^Á [þdšSUמö NásÐýkž{ + ­8Ç‹Ìd·X†d~2oL4Â9ÊbÍ!e/¿¶“ÿÇSü‚ãµ°g(Õ¤ù •8›ÃVàún›C†—,_Ü#çvr=ãIqªæt×oj,·^ÃkT‡>²¢Œ¿ú`]}sw÷’ Ñîɸ™5ËEPgëÛö¤æz¨,”¥<>˜Ÿ«»ü+%MÇ“¢mó¼¼L ¡•÷ØÕÖ(C3·'ÉðpE奄– dºø ·z*6R¢¥cÖÕe žœÖ¥!ß|ªoŸI³¡æØ˜¥ñûŒ"—!³cßjunÂ@Ë»M/B×JcÄ„³¢Ã¾%ÐñDQ×Mÿîç´¤"ëÎg¿ðÅ6¤£›ãtþÇ©@¦JHR߯QÞ<\’†±Ù¤Üµc’΀Je> äf/héÙñ&…ZI—ã`Øß;uú䎥ò‘žËA‹n1.‰ÐÔ5oGÉC ÛŒÙìb?ø×`ñL®|v‹ Í Ä»‰º¿Ù¶²F öÜÄk·Mƒ©ŽHEû£¶¶Ä–ec²®5ù‰ÄµïÄqðCW¿@ôWøwbÐÒ^tòÙ&Tõ ¿äEŒkkã+8 Ï–Ñ'ƒ]4ù±/ñ#"ú'Y¥³1žC˜2'æÒþñÈʇ HÊYö61q͇v´§-Žˆ×(°4`ñ»’¸¸3Þõ É|Ïœ¨ÇnÉÄ:QhkasQ«â«+Mw5ÊßÓOo°dJžÿóvý¾ns²¥xcˆ…MÑlhJCsù¢*k}?Š(0ßk £xW<—ÆY@é­¡ò)CÊQ1(m®•ïãLÈ£k\1•÷­—[îžÚA<$®;®t¯XœSà¢ðÄéÖmï8¶N®ßÉØßŠ›ÈÅu}VÆñ¶y¦Ÿਂ-±[ŒÜ$Ö)n«ð8å‚Ü ¹‹‘L„1¤dù:ô8Õ ýÈ1¨Mtƒµ³Ì„TÚK½CÀo®àDPK6reÞëVM€\o•è®r‹Š8ê.r2-%#Í:ÀïWîåσWúGÛKãgïJ g¶ SŠº$c ½7¼ *«éì¢t%»Þ±>}Dá|¬r&ž´mJ©(݈ѶO±'¶\Rý“¿Ò/™S1: ݟǤÊËÄ·‰ä‰e#Å~=2Ï `‚ø.v³ºqA~Mžù”¦¿hRüR̉9ÂŽ°uÝ'''þù+-dÎHÊs·ªÍi';QŽ.OÎ= ظ'þþg…ºÇÏi~Ú9(’5 jÚ¯ZsRz÷®½ãzš—’OÜ7øf†·ý¯‰â†¥äì§SœÆ 9ºvO¾®â«û^YCÁ¶yôU±O ÓX9§må×'ZZ -3~la²½˜çÓ©'k=ÔÎZ¿É‚Ñ ·c¤CþóÑÛi_ÓQz,Ù}ßy´k{øËS’¨;  Ÿ$wÉÐ0Åùºžj6TožÀ[ºcr6Ä’çANz¥«¾Ñ)1'õô;m~[ŸTs\{&÷öåz ?–QcÔö5­8ÞüEÀ›yÕÃ2O$ïÞ,§T›è¬Ó»ØÛ×JÞ*”£RbVOªÞ(²=Íõ_´ƒÃê/ûä ©y@UZù¾ø`ïÐ|¿» œÌâ ´ƒŸZÒÖÎC¢$iZ^Í!ý+‹§3¯_®Š~›¼çêr¬B `ä¢,_¨ø{ЧÅéæ²ý_âߨ±´NŸîYƒOWEu=·%¢o÷©nü‹#eŸ¶^ÝöT=.WÑE´o†q=ÎbúE½ ¸ë—ïCPöÅÇ»g]ÎfcIKf Ɖ¶ôÝý¯}m^#y¤Ø “‰ž.íÖdùÈk¹ºúöÕqE9ËâWO,N‚yàO¿M-;ë#0úìJ’ŽAú›Þš=­Á½pÐæ¾¹ÏP S¹•ñv¼Vm¡qEˆ[½ž˜´3ºþCÇïÄôAÀâàŒÏK½\}PÀš ížÎ7´BÜÕB m\ÇföiG>Šƒ)œ Wù” ]qvQ™œNàyeŒèKê×¼½Èôn{7)Ž»Ùó¦Ná½R _êkÃbN¤ûJ°oyÆø3LÑÃ]~ÔzÆiίàMÜã)’Ë»ãó˜;¹-l«“ŸŠRwÌZµ®<çwxˆ ü ǃãÜnêoîž’nvå‘UDßqã oo††ù¾ùºÔlð5˜Jot,ŠŹN±Oãë§ïäë—‹ï³ ‹¬ïS£œ1ÏÇmÞ1®ž¼åI|säŒm½ n«]?iÛÙåÌO·*a­Á'Б÷¬ÿâØ§RÕGûœàIµ¡T¹’uDbÅA®ØÖo)÷Ê~æú­6¨hK0m[ÕF„wëb¡9/>ëµþQæ»ÚóÏNÞ'e—mäuZÛ”ÆDcÁ^d6Ðrû 6”ëóDß?|Ÿf`cÓyG¯2ÖIKÎÍ;ï'Žž­ƒ…ÆÓæÉ>p©®ãV}kœ*¶¤n¼6Öš_{ȽSZmÌ5ŸW6UCYØÍ-jêKðºßZ«=Æâønrl ß¹í|–,u Åmó[7¾9³uSÞæ Ž)a^TZ "fmç¸Ç Dư¹p+™ìÑ3nÝ•æ|½¥=Ë&¯ù'{•*Óy€À) iÌë ¹Þ*&˘@éú#šW0•Tšë s7g3¤’®KŠÙ¤ámŒ4ž×õrîü/©pÅÕŸ£ rÒbÚ £­›ò—O½Û«ÌÊRM§UÍ+‡T}NwSðÇgû(ðsšßÆÈ":"³¶õ̼¯»¿’k_ÕÇæ:ðWµŽMs™ïbQ‡Ûú}–ãŸÇ×c}±ÇâpŠàš…ÖmýVçìéØTvq¨§ýù­OÛ“H/›aÖgæÍhÓSázà°Cº~XÑèÜ Gt0¶áMÙp¾Û¶Ê/þŠkíèæ€äæo}cÞ†"ož»wK½M¸ÜÛv~5vô yüPݧIšS°Ø«ëŸ˜JÀM@ ´­ŽÕ_xÐsF„õGÌŒ{k˜æÆ¾,&+X ‡uÌ,—É`1 å/H9' ßJ9”>?†d.ÏjÎÆj5¡M.'¯ß® Ç|îüÍ%ŒC25ã NÏžÍòqCðü ßV;Z³½–W_q¹s‡šö”Ü"cd©‡¤†fhG’£ORÕr|èy#ßù~ÁÕ· è¸kpæÀ|ȼ£‚†1{’\Þ©ÁS¥·>Äwï•ÿ\ûÛü“­̇ÖBeß/ß·ØšÌ?Xã?ݯS}|èñÿ§À~ï€gƇV·­ö¿sÑn¯ë7ÈŠ¯q ÕaãqO­m¡Ó‰Q¨¼˜òãŒvF¬’üxtþB}Æm<´·—8¾*˱å¸Éa=}¯¯dN{^=ê÷Å:KnËä¯v^j{²±âš¯3/ðCWZÃz…¥‹XçØ<—s×Sè˽çcñ‰¥W²]_ÄríR®¼]¿œ¿‘¯‹åT®‘ˆ>I ñ§ÃÒ½–S³§”JgºéOÎÝ\òDw”–޶»a ß­;á ;›33[ʪ£­Öȼ<'YüèŽ õ¤‚Å¢Õå;í3bÊWÏä[‹ŽjãÆºüÆÂå{­ÒÞˆ¡.ölÏ‘WiÊ‹n9OÁüSºÖù`(GN?P·'{bÏÜì•Uc×#²8ï[lGWQÖŽõ—`i¦…¸rꥈ=èµ'/ Ÿ€Ø…àt€&ÈïÕÍÒèÀ5¨œyMT“NëàΩ%¸⡌N–3ÎýÞ’*t› M@Ù±Ó ¤ú¨êLÏï^.”+b{¾wÿ¸4M´óÏëSÊ•u“èv>'|,R‘èж‘õ¢•v‰Ö—ywÔ28s¿’Ç! Íi§ÔA³-¯h‰ï>µ—NéGbŽ@µáAÏ%çßÀÊÿæ‡^lù;£OŽúÄÏn€ôœ$K¼kG3]Ê`ôŠjó.Œã9Wn\[Þx@_|”«•]œ"f“GÇ¢þ+ÿ’f1¥Æá(GÖoÊMÌÆzóH[Â;_HZéjî„^»^¤B«˜£BAÏR˜!Ú¹·Þk¢°¹¤O²À‹>ŸN}–z£…I’j.Ÿ4³§Ï™ãŒ¦qðcŽ´×z§èB&¶ä-Wò>ø!þ¹ïjû•_ù•Ç'?õ†äQ8VÿI:™S»jcüýË?HÞ_ûÚW_ÿÚ/?>ò‘Ïò? k&ÁA&öÔÒñõB«ê0i $ ‰³`çԆ罒çˆP‹º{•î©Ñ ßëÆÞ$¸¥ÌVAMHéK¨­'ã}/ðRFù„HÛ)å>%pXSÎÝÐÓÖÖ:T¹½N~d‡^#8óYhœ3Éqq 15t½W˜Ðô‘´ U)ÅãSídÊ9þÊÕ¥ f‡û]ìØ¸½…i¶[ ÕÇX8AÍf}éTwÀ@†• ‡ <ßï´|óë Ç®ô ùœÂúè(wO‹Õ÷é¼9 Ô?À5Ò*¯æ÷"œú¢R´ûòXHº’¶<$1¹ØSYœNÒÊ ´Xx1oµ,¿ÿúZ•ƒ2²pè;HÍ]0o™]©OO›¶+9;Æç~Ý,ìð¦>O?·°¹l×ëêR'¿v3GizÞX„oÌʨíë—ö•µ¬;îtYßr¡oëgåöjÊóí,Ê#2„”ª‹³›v=—«˜8pZ´Àq\—;|±ï\D5Ç›·)©JÙ[[ô£Ìúpí‹R–º`Q»±¯OŒMÿnŸ%žïÚ”·[8“Ëó%ô žã×úW{ºçßÀA¨û¤¼Ïv_ßæDJtê•—Úõ&0¦Žcq™4’ƾ£Ï|ÔÖem —TåS?–íÎJ97áðïgD_úµrrù‹~}“%Ÿ½.kœŽ,ýàg96­”“âµ¾5YÜìѶ4ÍÙQxúuv`˜‹¢bœôy(HGä8Fôí·ýLËG?÷×ÿÖãó?ü#ü7ÝïŸ[þÉ&‡güÝíC~ž®ü{ÏO÷à¼Wü{Ûï¥Ë·è¶ÿ=XýÂ-ËëtòîãßÿÝÒ/­«wÏ/œÕ.ýž/ÿ¶ÿAÏê)[!(ÿ ±¯Ú|‰ÿ¬ì»ƒ½6¬ãOÉâexŽGÅzôs¼rË‘¯>OâŽFù²¶×ƒëúÁט àKãO¯2ËÓÖa×M‰ ¯ü‚6\收ʽ±khz#^ß$Éuh\x**×™*çãÞнy¦è¡)¿ §¨Ñ†·>@îYŒ§ðb$ƒëÈ®q¶•Q¬y͘V&áß(Zä˜ÓGó—:ªÒç"QˆS*(™bcG)s±q¤†%MÌÃAÏ=ž¹C,ž!¾0Ž>ðæ[ŸZV^ çW‰³)Ù]†³*Öq‚ªµÐn“À 3`dÝG»Žtm cIc“eѦƒöɧˆÀ:Î\:þø2MFõ6ÇüÛäx–SRèÞås3ÙáÀ»d“A¹“N4ãŽæi0µ³~x¹u…®\}aÃI¡¢¿Ã›m½´Vמã+y¸' Xûô¹ñ+gT ÎÐÊ’\ Ò f-›èØæs_Z¤7½4–Eû±Z²ÏÕ»ê½j›¥‡› Ž@™‹s|ëzÙ% ÇÃñ<çRîf¥'&:²Šúlÿ‰•!êòuÎI&ÝæêÑiNÆ–?NZqj“±Z~Ði –§ùÝEÉ RRØBe›díÝÃëéµöÏ&Ô.sxµQÕ¶êå-/¦8~v๔‰0ßÎEðú 5K³«”7 ÐôihäÅÉä93æg>KØ ÛäkÇö0Ÿ:œ›žS_ÇÉߨï=ù⇭ž'5‹õ6ÜI-P7wÊ–Ï^øï»]”ß mfIVk$v ûZNUoÀq–D}ülÂÛˆp,)ÃØ8;È…‹o͈èתö§“ÌÓQm*è1p‡#p–Qpûû®1 ³õP7ûÒËÃKÜÒ_Jè`õÿZÖ ¥³ªœmøóíxƒ|ñ&¯>=A%¾óü¥]?ßñ\–e¸u|“·’üÎ×_ø§~âñÏþÿK}äÃm*BÌØ‘ú=EÒÓô{x6ÿ~ü«råÞ{¾üà³n Bq³jÕöï売׮õ÷–ïÅû^´÷êýã´_ãW?qõ‚hÞä˾õ­o>þÚÏüÌãoýÜÏ<>üáÏôô±ŸùPÐDðqp’s¦ÏÆšk¨lG4â­Ÿ6ÏZ–ìQغ#NœÎ5Ü&c¤kɪ`:÷kúîÁ9ˆÞêœÃˆ-7zïõ¾ƒY¾uÿ\t˜4ó¬æÀÑkP"mÞöª¼>‰Ž1åqüÙOÚCåT¸†´Zdιâüפ¶sKýÍ[ãèšÁõ¢ë%XZAKa˜V(úròQ³œ*©XÒºy¦æ°ä,_óÿ ò°ùH®'Íø7´Õ('Þ CÃv|Ã9ý¹5"núb†¯]¯¥ù£m­ø*‚ç<š£Éݶ1¸/9¾ëâÒ‚`K¦Æ©øvßòë”?I¹Ó?"'!doVj — hñ[?]_»2¥€ýæqf]ãØ’¥v0B-œ{kÏ}·FÚÖúbÏ÷£™Oâ‡3o¨_ÐÃé¬/Rn§fßýîä´«¼õÁOr³·n¨3D(¹s,/Èd Çœ¤Ù">IÛžsÐŒdÛÉ*’/ÔQ4q‘HÜ’>L6ZÈ>Ÿ#&Ì,¸aÏõQÁQ_¸½ Mâêd²­ŸÊCÇmÜRÀ%Ò{KÁßqsƹÔ#ôò¡dô}/ùËU L~Â-åž‘ªjZ9¥ÜÒ~ò\ Âô¬u£³8¼ ¾iÑV^EÖ:‹ ­6`êðÛ@D¬Øµ!('sÓ‡{hè«å)×F ‹þíɯñjwÉOK5äæñ²Òv úÉ— !©µŸ›Ë…íã]¸‰IeÎîÃ@ëí»XâˆyP¦Êøb”KáðF:?bdýèù”´'<ór´fÙ¬JZ*ƒ?ð¬¸^ɵ¾<+pÄɲò,FzØŽC!Œ šà¤b£ÜºjÜÉ¡é#¥ÐËøé¬eKôOýÄ‘ð‹!Ý\ [iÎAýRßÑ©:÷UÖ0¥ƒ•÷ùð:yظ´ÝÃÙ¡f)÷V³%渳 ­drÆ?\?Å«¤”<É´ë[]ä¦ c0È×úÙ~gáç µù.°Ÿª`‰`wz^×Î#Û“LzBJÅðàú wàçWI.§ñTW¬[O›?ŽmfÀ‡çÜnc^ë/QæëÑXùˆYÿÁËE¨ÐAÛKñÖm;ÊŒæÅ¸ðE‚Àüì¹ùɾêh/á¡ÐàGy@0Çz†î,hë°¤„ƒÒo}ëëÏýð>þ™Ÿü#üÇÞ!Å¿|Ëi.¿¿‹ùüøÈãÿöŸþÔãßûý;/~ñ‹ÿñÿäúøì§?Á¼?cþû;„ßÓ;ãûÎw|µæñøïûÿ[óç_ú[íñá}æñ¶og¡#½cö—ãBi{ÛâŠX_‡Î©ñ|‡-Ç|]ÎúÆÑÆÀó‰cjàâ:dhïLuwìÕÎè©m¼ÞÍóÄgËëè.9´S9cÝyploÝA«ñž6‡wãÛ~yÈ!¿Üw^*~ä9umƒÖ+g>Übþ,U``¾‡WÞ9ÚÜ0fD…2Tk\åý9Æ0yçêõž¶Î:4ÙQ›?œ+Ù[œ›´2ªAþ4wì§£ÓQ\o©dÒ>žl7j`VçÙÕØ­Cëͯ(áœ"ÂC[°LB·ÔÃ8ùÚWI/ÛåáÑ…ŽN±&öɧ.Ø:ô­}PŠ_÷f|}xrûʧY"ÖÖâ^ =¾—–cìè‡}á?#¶=u‰#ü®UÔ¥ZœuÚЇ¼IŽšmÖxg$ä§:ØKÛòSÖgðúËéTVÄKŒµØµù©éŠÖÚXæP³hXñTh9á˜Áýã êOþä1u‡r¬ãŽúÒX<Ôñù¡ÜEVSå–ÈÌãSp,všúɪ¤W<ëòà(kÀ µ™†w{ƒ\IœK,]ôÌ)1(ò­ê-ņð¢×θÞ|”1ƒ‚6¯¡”»I‹!ûhØLN»¤»"Ü´ºDAâgOY]œ•Ú$Jû-À‚û[©R Añs{}aÁ{[_Q(ƒVé«Ýn¡WÎáÃ2GT6ÄR¨^¬ãõ-Æ{KIº9ýôâ’œ˜’•ù±Q8?R¡¿Û<2?´®VëZ0Â(XiÖÉ1&W{˜­ˆœB³Gg*c ]ΛæðØÅÝ9j?gÕºˆæçl)½±#Š¡ÞI+-¹ÆºöM¯>/2rÅèLÕô„©4{ål› ÑõÀÒø(_Î?m ib+çúæ”Ο¿ô‡•Ä;oü94Svˆ§,dGm¤ïÃþÄã+_þ2¿úøì}:ùÏ}îs}˜˜²ÉÔ˃pê™ sð“5áäoíJ¯}ªñº.á¶ïy7ߟzTˆãm>û©O~ìñµ/ýÕÇ—éËÿÊ¿ôÇ?øÁ<þòÿë¯<~ü}þñ­o?ÿ~þ ªM‹²¯ºü×¼×më·\{¶¯¼õïE¿ö.oòÓ~À÷+_ùÊãmn¾öÕ¯s3ðM6ÿ?ôøÎ;ß)®­?׆Ú8p í:Íx ËëçuÞ!ì4.Ì•ë|Ɖuç[n"^oMdž6V!Ék}9㵼燶îÐkÜ?“‘¢¾&ø)å˜ÛƳ¹=ýI!xýhÜÛ8ëë‘ì;ÖÑïZ”:ý6Þ¥iñ¨€ŸÅF½÷Âóöª¾}hhŘƒ®5¬ã=ÿ©Ž.” ô¡ÕçÆÀ•ªÁ#WNÔSž X~Š®gV(Æâ:{CÏ5&‚\ºë#Ñ÷@ÐÊRüÚâÞú¬•ðgÍ@„l=×Mì†Ç®g©sOÈJçÚ«~kÆf?9Îô¥Å^<þøU®Ð3¤Tòô‰(”,]“ÏÃK²DÎ(j±|EÈŸ2^+¬ ¨ãQfý)ÿÄzûw¼Å1|°U)UGÕü·ÿ¹¹e¨j¶t&æLxüJ_Ië™TÎ뚎Q7®gÎ ŸñK-DeÞ:XÒhûž3+ü°¥u›`íbfãt†£†£³ºciˆñêÛ#ĪZë–µ¸îëXüü ­/úZÇÅÛt…Bo›/þ|ÉÒ€úÇ1c—-(&½¢ÎÚ’]4ô3[AˆÎ kª"ö$—OüÂWo ÎÖ ¢êÞ;+fÙà|¦°yv‘›Ï‹ê”>\,§MgµÖ•×ñ½·o<±Ÿ1d_IµÔsùg³i¿ièL.|ñdrÆ#×£o‹h‹‡$cáOêùO])4iãIyTE´•¼f'â©Ý|øÏ$gPÛ ÷„™³¨lpÔçmWù¤_§î7-×ï Q‹ñYäéún¬3 ¯rYUÓ?íwñ±N<ÛL›7äôÝñH¹ëæò£,ô6HXß¾¶x]]\)Ðñ…Cú²¨c$!dÕ§Þïh(õ^vå_ü#rr¡\9G á*ϱêâLOc³;÷i¨ë²vòKÝ1élúL[þÐÇØd¾ýøU>ìàþÕ¯ü]ο¯Ë‡>ø©üû_ý/ÿ×?ý§ÿëö'ÿðã?ùÿ/o}ýë¯þÆ—¿¯}ÿsîɽù?>ôÁ=¾ÝWC²oíÀ¦ŒcøŒ¯ûmÜh9r”UZÂ1ÞþAY¸ ÇE³ù‰£#ùa{FöÌ…m6½Áp0 êX^ãKóp*Ϻ>^ºšc$—<GH}E¢é•Oú)I['Öü;êkŸf_ÝÜ:®‚oÉ›—âš ×½µÞº°ÄÙ÷ J´£ “«b2åÉßsNFÙ-ƒ2fÒÒº¤¯%k¾jS„ß“®¼†lŸ¤/[‹´øFo7‚þµ?¶²ßÖÃpô6ž¾-æHìz¤{‚±d ´®£úfÜ[,QõÁ^YBØ'ä⫇ v燖¯5ö:¦†=«†ÂB>±«o\¦¨±Ä)ŸaØ}Ö3’¡ãŸ°^|=Åë£9 ?™CÙPcöWé†%9üÕu L÷Îæ œ;ñÃÑó yü­«î+‹/ké—!e£ú­H‹?c£©dÅ9™å“ž=c×xß"L„Ü¢(¢z.¢áTž$tÝÉíT¦làbÁ‡¥TɉI½nHꬃ/û@↞\2˜P23›& Ì9?iǪ¢ý9®ºÐ–Qva’®~Où³Ÿ‡´%Ž{ö.dë0•d#ÏÞVOâÏ…›ËÄ=è¯êœþàûÎÊ+¤’1a5)Ö¡œ8œ;w1,óžô$õïó}ê ´Ý„v&}O§zâ'6ëûp+4ÅãÏw76Ì]Ì£C¡a¨¾:²â‡ã+[ö9Ò;Ÿî‹çÑ¢?êŠÐÛ¸Â8fØÕ¤žïooƒùòiH ýòCÜ/‹ÙñF‘ÓAîßïÿlæö]Öó]H½{Æ`˸¯÷öK7a>ÍÐÖð|¿o#k²¡Øwy_tó(ē듋ð™_mT^68§(æ°_ˆäûdz·ƒ[ÌõƒS'üÆ+•:ð%ã6Dþ®ßÏŸ<Í|ƒV™S·ŸJý˜?G,ÍzX\ËÍOwMãç s«ù„ß›#~ÅÁeaϸã¤z¼ö†R "6?ÐÛ~ %lÂvB.ÛúäßJ½üæhÙsË'UûJ{QVýר¡}‹UøÓG#¾zÆÕµ "ÓÇÈrêg<¤·4?¯¿VÔ|І›sóä/˜…Cã~›³îdóèíU!XD$*û¯«®-?­ ’µ¸.{’à_KŸ1¡Iû~¡€ë]s(ÁòŠŒÞ;–Þúà[¼Üì[€Èê›@â+õÜøÔ¢aµõ]ã+«:v@rŒ\–u£m,G]);›_5nƒvô”½ýU—ˆ¡4 ÷Ï|öŸzüµ¿þÿyüÆo|ýñ/ü‹ÿåøŸýÜg¿ó­·y;“cX’‡k‡èKÚÖ».¼´_ò«#}ƒkUÙ¾Ð\|¬8ÇùUÎ&mT$ªÜü·²¹E„Ç—ušã [ WaPñÕ¦ïòŠÀ[ŽõõCÂòlÅ„[×vÙ¸*~ìoc…= d t߯ŒB.Çú¥¸Ôÿ`6ew…7 G¹¬qôÄËOCgóH¿ÂÑ*ºÂâ›uùv¹wÅ ™º¯zqR–Ÿb?8ÏñÝ 9êK 2'✵>Ýë¬c œH·´|r]??ðǹÝFÆ•5.¥UçÇÍ¡¾ì«ˆí Ö%è\VŠ)AžOÚ»9aÔ“ýŠFùC‡7€Ì8®A.-ošû^™ôøª°¿í…DÑKgGäÎ…]Ûÿãþˆuì¢mÓÖ®O[c•Y¿ § ©§æ`kLv„Ÿ­ÕȘ3èRýY]$~ZÿÄ¢8…‚w²’•¯ ƒšVSÛYÍèhVë†øÖÇÜØW¯ úébc‘–ß¾[À$Kã/_¨vófßá}¡£sCÿáK×AÎÞx¤š£Ä± /¢’§n4ð«ÁtéȦ¯º¢”Ä.Ä⨿§‹9+‡•hú»Å[BòGg¶ÕW^äå eOØgý2bóa“¿»L‰Ã0_ÚbWä©„½_ {•6)ÒLä;ùåšuûaÞMž5Ðdk85/†w“ƒˆðù\Ïå×Å^ó¾áãSŸþÈãç"þ,ǯë÷õóÙ~ ¾ª ã`ÍèÕ—m\GL^Iz¥v«ë¨µ”±œÓ; eO©€˜§É饼&Z¿ÅD½jg÷Ò.ý¾Måë1žÄ ÈùøñŠ;yá½·~‘fÍ5Nvª£¿D=)ü}/ÍXįïeˆœ¡käè¾–;2&{óh¤Ë¿XèÆ¾¹<&§‹@ìSÞ5.ý5ÞSÊ9E?¿«\ ÏóÐÒy­ ~“†³¾Øg­ äÐÝáÆpmÐwåäâN¯\È·\µA‰‡Ÿ-êÏØ.ïØ¹rϼ]ºàï­K³Ìh\–§kîxm½¦]Ì{†—mÛ/4k¶[ï"Ÿ5êø=™ÄfJ—zhŸ±Y%rí(<ݤbÝù}IÉ{ä’Aì¹&Mç·ßæàS_ú¥¿Íך~åñOÿÓ?!ƒÏ3üÀã‡?ÿi¦ùQúÜÒ– ´xÊäõáÌÉ ~ž9‘ê{0ĪÙƒï¸Û5æÓîñIù§ý¹ôìƒ9ÿøö=hús\^Éîæ||ýžPõ«r5¡=±®|õˆ¼Œ‹é¿¦<Ã}u¸&"Ù89ý]>óÎíá©tÔsþßré·}=¹Ž-í÷*GÇØß•÷E*Äò"_û–¨«– K»¶à'Bû™ç#þŒåØ}b [µ6°A½¶sôŸsÙöµg=M+ï)x|ý7¾ñøõ_ÿ-¾Yʘܽ8P©ÿŒÜ6Ýâð2‡öz´íO…#“ÍK‹–rÞ,?Ã5Wà>oЄ@¯+Ây°cºã‡ùg™°†”YŸÛr¶aú2òÜ×Ím÷fð“´ù¸±Ð˜:{á@‘ë¡`ù‘¬7úl¬'–»Á¯Þ hpÙ©¿fÊ'£ i÷f¤<¦sn²¼1×6rÚ·¼µŽñ©¾”=M+²‰1òx(¾m=.²&E‡MÉ †&6ßöŽ~×iê+ß¶2²ÙQcöíŠ[äÛáۀʮ;"-‰§Í¯£ô`ICÐäÀ+yÅ¡ í¹‘œín¢zjK£E‡»\Šb¼Å!6S„j~VÁÛlßœùôðD‡?Öõ­Mˆx0+‚ZvnpÅ{³g’úÇÜ¡´.êöl?Ó!wÞ¤Ñ*¿{éÍ;}ŸVK@µÙÒ´.&õƒè[ÿsì§?õ±Ç¯þê/<~íWa½_ÞÏÀûx?À3ð¥Ÿ_€ñÿðzü÷þ»ÿõ¯þå?àQ¿ÞûøÇËÀg>ýc¯~õ·xãÈݲŸxîÝ—m7µ½Õö,íWÎFè|2'܇ìf@¶!Gž=Y;*6÷nz§æîÏÝ :ÛmŸã^­½t4÷޽Zƒ²»i?{#ým;¥û­°UVIƒ¾zÂíñ:ŠžtUÙôŒq÷W~³Ý»Þa’<°¿Ò¥W}z•{¶vcpxº ¶‡E_m¥ÝãikðûÉO~âñ…/üøã/þïÿâã7¿ñ›¼ýgoYyöAèqÎ,‡ ¤ôNüµÀ±÷»ô¤Üñ8ÇWúß3ÞËãÀ̧Cêˆbq¸~d\>¯c}—?¯ùÚBÿöûü½`Ê¿Gæ]må2ȉ³vïùB¼«}mªvôÔæûè«KµÃSÿÈ?Ûò-¯pÖ¡ÐRŽûârWT¾uK¢¯á?óóéô0.þ|¥þ.{×Îs¾\ý‹ômÞ¸ÞeþS䨼íwÉË;Œwé_²8È$¦Âo¾ùÖã/ý¥ÿÇãßÿ÷ÿ=þèãßøû'þ1k|÷ JoŸÑCbvÝmêU©}Û݇à^ÝšId“] ûªÄ„ã•€ûVr9¯tŽâñÜn¬5òÆ•{¤Þ¯”ƒÉ*íΪýqƒO”í£z0×ý {¬ù¿Wàl­Èñ+ÍÛ3ò¿÷Þ#ïç!ÛS>W~Öt·ýZ1žHìäCw_¶› ìKý"íäº4+ãmŸ-8dþfÈ·æ%î0èc¨$ ºòæÝT» D[ÇóÍM­rBÊUÆ·ütû1¯êÜà”€ßSÿFï­-M1DL¸Aö=ãÝ1¡ïhÛK0©ßïŒ÷©·úÊ'C½—z2<ßî3rß–¥óYP 4=ßÓoxgó¯”ú{ßÓg´ÒûÓ«–*óÕ<©çfÜí¿²ë(åwÃa[G–&@†v¶½ùw<ňM7CÏn]-­ÙξÞ8Ý÷-ƒ¨9ËïÔ>Ú¾„­½‡þnJçϺþœO|ü#¯þúãñÇÿøükÿÚŸàýò~ÞÏÀûøÏUþÔŸ~íûÏU‡¿ì?t¼9öàcÿÁÇ7¿ù|°{›í‰¶Or¯è^èóÀU#îsÜ©´ñðÜÍœíºÔÝN¬ö;Gô©³ýŠÒbÍâÙû´ybçýlŸ†zÌÍ®/¥=¤{`}ew©ú ò}XÚ~ 2¨éffõ\´0ûÜš¶?R+WWB{l7ìî  <§ü¸9âݳýâ¶÷—V<÷€Ë—µµÐ£ìü DZ&÷8´o°Ø‡Læ®*n,M‚ºlÍóÚ=h™hŠÈ9nk6´éFs–9Z9Xû~TIè듘ëÝÏäBD·ü©ï“ú`¹Ûîa@s×?V¼+•Ïbd@\~´ †‘.Bht@ž½å ûéí.+ñÛòG kIß Ráxm¶}o–wN ×Yœ„ÞPÈ•þÌ«³õ¯Ï$æ@_eªpòù8ŸD¨/ºÁóû·AÒ§#YXèUDÁçÀ<ðGîÿløýÑÿóÿÅŸ}üÂÏÿ<ï‡ýh_é'Êûåý ¼Ÿ÷3ð-^¿üðŸü“òñ‰O|üñçþÜŸ|úÓŸú>Z÷v…øƒ–÷÷ãù'#oðe ßàU±ÿñ/<þÛÿÖŸi¡ç½?ÞÍÅwZô R=mõXö•pb}Gìùbº‡ºúîi$ºoÛ^«íÐÙ ‰¦]QÎÎ ŠXî Ü/¶+Н¬Ôí•y6i´ŸÅ›c§½lXîo_ÄÜË^$°Àl³Ï>ñìÓ„½\ÕÅɘz`´ß’”Ûyêÿ|¾tÏ0oó>{ë{3´h8*“o)¥ÄÙÛ¥æ—(ou7ÄÆ³$œYs°÷ÆãŸMÝÌG1tƒï“wrœ“x NLŒÒÛ·‘ì•+’¬Ø±b,ð6úšn¯Nè£Éô=Wa< R„ᨒMÅB× &0t= ¬Ú™øÝ+ J€+Þ‘NÊ»ºðk“Qè11Ÿ—›z9&§Ãª‹'ä¼ÓwÑ_6€0O y)‹Žê-QP6°åó<,§ú„P>¡£ }w°q¾·GZ¶½x/„y7ŽŽÔõ<Ž“,sk纞o² ‚?´Ï$xƒ÷}‡¯õûþ7ÿ»Çôýe¿_ÞÏÀûx?à3ð‘üÀã_ý¢§›àƒ}?À÷3ð™?ñ¯þ©Ç¿ùoþ·ø–@÷ì5ܰœ½P_Ÿ:¢;~ØõyD÷dÊÅä´}Êýg¬Ûƒ‚wp¶;»÷3n–(wå&FÊ$æG¦+WûÊet·Ï’/®òî‚â-<úÍJ=¹žÔ_¼íÛI¡çVtïrŒá„¡±êîÿ÷ÍQÓÈðœÃ>»³L»ÿÒ+Ý0Ô Í[«òC#DŒí?ѧjê²çsÿŠSzg4ƒÙÛA ãvÐÝdûµ\~âZùØ€ ±ß£ÿŽÊ%Êíe’'Èž—Qôr‹…’ônªâʜַ{w¶Ís·üÓª³iF®§èú3÷ Ë ZOþ5䆖â#ov@Š}ïOºŠ©™AUßþ¥)Xte¨Ú)úP½Ý‘,¯ ÓßU‘Ò£÷Eö]óÆ„ñ=úw|;:bý!\=¼Ä§†õÞ-¼”Y]¬¾<$ô|¸Û|m§½œ!wßÞ¥k §¼TWæù9ì>ØóÅ/~¡æ?÷Ïý |êÿ7‘=>NŠ£í£ó¤ÝÊ¢šÌ3žªWçè×Wþê{~-÷º}eÞc?œ#ÜÆÞE¹Z;¿¦‚C³ñ!dåYYóŠ›kóun–n?Ž-ýꢲ^”[I±9„ñ$¼ØX‡­­pr§yOÆY¹ö®­ÑgíÊ<•¨ˆ·Ó­³"Ñ_ë8FFžÓG±“Ë‘úxõìáÊ£ò®¾¾rï=®ð­¼û|Å£¾Çæk_¬ëC}fV¨^ñ—D¼Â¾öZý L&’óÍyøº\Уïü‹´çSé•Ú»€ÅºwŽK»vä]¾Š¿Kyü qHÔC„#nÝua´'÷ˆ¨³ùïhµí’u$Ÿs K¹å›Cktë ¦´c¤9®ÍƒýªÄˆ©@”Ñ^U#_žtäŸxV,žÏØxx|2—¶ ð'ø‡`ÿïŸú>~ú§æñoüÿúãÏü™ûñgÿìÿìñÏÿóÿ_ŠðÕÞ"yf1ÚW7 òaM. #îxû] ÊÙÅ¢’|y‡®lÕÃWµñ'ñÈ ð´_á<é* tËÕ»mu¨¿"·få‡22.®mм«pû>†‡ ôJ&Þë¶uË{e_ËÀÎi§?ŸŽB ý§}±üsÞÉ~iӤض¨{xéÚ7¯ðëÁéqNî‘VÐyòÉÓÍÅk¹Ãö*Û|ºs/ØaOÄ:¶#Ý1"B¶&5Í–Å»âIlœöQA-W###ØkžõÃO¼O|⣟ú©¿ÌËþñ}èw(…î~%ÝçºxUNÈ-Ú»²Þ6/u×Ë·]H…åoÉÚó‹”¹Õ=5©‡9÷€š<å‡ÉS1èÇÍ©v68Ê®@÷ƒÌàl ”§®ò/R}–Ÿ¤öÿjìŸlONÜýj_Uuµ{0ˆá‰I9ãLóż>³ÀùìçÉY?÷êƒÈêáyÜêú”œøð D°œuS¬ø6Ì&j×+•ú}:Ž›è—§÷÷ûÓ68tsð|4pŠôšÈÍž´븷Jí­KjŠú+®ƒ Ç„Þ …pµ ‘êHš˜îÍ÷_íP·MÅ$ ëw ™‹sDMÿ¼iÈ÷#ºá3 Žjo0‹+”ZåBù؆w¿6U?|‹“Ù6—ŠÎ‡éÛªD§†Bßòc;j¢hÿØ)âñOĦ¿`‘†ØÍV²pÞ×»â2‰Æ‚‚fγ¿Z¢xÂÞ·¿½#ï‘ü;÷×ɯ}£?¾J£Xö;!òçbÖWÞd@uCaæ6(¨S”SAƒ bûWà}EÖ¾s7v¾Ï.2Ò¾}ÈtÔÍ1Ô+1“o¢ä?íói;dó‚¥B}…âŒá7[ù­Ys‚ŸàÞOñ£Àÿbd#G ëXy)øÓÄ µòšÙÌL’£«y±CÉ´NX*hogO §¾D(OdçŒóÇ—5Ï$öˆt}0cÖÆæÓúÞù´4 kîìSrg®Ô›Þf4bD‹'¡`†,6f÷†>{ÚXæ¯$"®Îæ­ù1&~¢£“)!øbÙx&c>h˜§§ip´ =¬¼öóHŽIåä›mÏ#cY17ö9múlÞÊK>’;)ó’Uy"ØõUýnÝèõFy½7¿·-Ò‹§ë0Œëôý>k¥ž(äo†Ê©rûœ“±¨dìÆ©Ð-òÄ„/Ýöo¾Ó6Ñ^½9ùjªÿ‹aó]}d…å»ÌùÆå+­¿\“ylq°!—WslÝ‹cÖ< $\œÙŠ\Óǽ}r†~Ü\?ª¨ŒE¨¡[Ží|KëÂáÁyúç¿ùíXþÏÿ‡ÿÁÿßyüÑ?úGkã7ëñó?ÿ˽ùáâxØæÜ\Ù;YªÒõÇ_%¡óoÒ—ç8{ÆŒ„}¿]ÑÞ«Øþ? M\;³™ôQiÆ‘¾.×·}·øúDc°/‹áN×èêòõñ‰7ì1È*[œ{ø” Í6¿¬ÜqÑæJ9‚²QÕÖ/…è&×Êyõ[¿êù|Î;ä=‹âj‚È Ulí{âÕ>ïRˆ°>#sØZmË[þŸs¡4ž}fй‘µ£gº¦K)šþ?ƾñ8¼Þ7Ÿ²Œ ûÚuð®ì©¡ø»þld¡¼#»‡RÐìG3¢qÌ=ÿsº†‹K$õ<  ža;z}£3ú,Þw?öc±ßaO°< C¿Ã©ÓðŒ·é3vëöcœÏ¸rMyÖË¥6ÍÉú±€ô‰µFÛ7N­€oÕÙ¨g;õùuE‹Æk>HâHæGq:Æ,Ê¥'OÙ²ÈÙuÍ\hÞñBÅØüÕÿ¬ œLXè'"Sߌz]Çt®@"Šj–× ý6¯u¡Ë›õ„+Öw°ë’üìk¤²gŸ F»´…À?Ë¡PtXëø&ÌmtË1úæ°g›vg–g.Z²žhÛ1»¤kÁ¾$ÍÄ€¶ƒÄÉ)ÆA&tg" 2 òͦÙr³ ¾ÅËH_£YÔnnÕ–‹ºlmkI»f×¶å¾1)e,úi×À;“O'ë¤øêšM$‰iþÈWÊü~›¡&=’·ƒÎÆÑ¼šæýC)Ÿ~AÀÿ¡y[9ñV; ¤Bõý ¬$Ð0·ö²7»Ó>ðÈÉ_¹çˆØ»’zþR´Ó+ œ»pÞFàúŒYs)=(ÎÛ!–ÃŒlÒWÑll,.†Å¥]³.6 Pzä + vô¥c1L2H`KëëãýÓ9íiùrbŸÙœ=ir= £ÿæ`#`y”{cËõnˆU¢¥iãîo^7â›1Ýð诱HH­£îÊÑôÆ«höñ¤vÓ¦7w1äÛØñ<µ0Gý£¬Ö.9è0îL™à» Pá"tÇ…±ìÌðí\ÊàÌÝ6~üøP~é‹}Aœ8Vºór¾÷M[ÇŸðYIŽlýg úŒ ŒO³¿ö.Z¨Ÿ0ø¦}ιQgD{å-A§-Ç–Î:rl½E¼Z¼õ V1ßíeѱ Yß?£šVI]+5•Ó{ŠkUwŒè…+²rœ[gì f£u׿¤Ê›y/üv´dé;@ÄÑšö"Ï2ýXèû““âhÅõ ãO›ÃDüàM%U±éÜûò±Xà¨)«þ94™n&Ã"›ØqÌ–CO]`µOLpbAwÜYºqæÜ‰ýA}¾.‰Eâ2"ËúL]ûMkÆ‚t!oŽËÛü×çm¢ÔuTl-ÐÌr‰ ²¡ ΃ßüúo?þÐúÃ_üÅŸ}üÚ¯ÿÚã'ò¨þøíßþæã#þo‰Ôذ\û6ÆtZ_°m¨ˆlt‰¶Ï(€©×)ͮ˭q]¿®v¢ámÙ£§®oЯŸNr²C}¯¨×É—IEš»¬_ëJ¯Ážëоé‡ãÔ›'úF¯í®–"6X~•WÚ†1fG:ºŒ4ÞhHÙunÙÿÐw’˜ªþ ‰ì‡—7È–% ¨Ššƒ$ðžzŠÐ‹Æ6òÛShÃq„§Ää8ÕÌÆcÞC9†×¸µsÍ;ŰŠçØT· »ë’„F×ðáÚÚftñeÒƒøøÐ‡ßä[4Þ~|ýëß"sï<>Øÿo_ÞãÛoñ?:ŠKŸNÞ3SÛ gÆ__¹Ó›‚xÊÎný|曹°ÿËŸ~=/x­4è;ïæ‡ýiv¦#6_8`è½* ÙÉŸ9‚Ù e}¸þÐŽe½¾õìÚîÚ¯·K&dî…±oõSÛn*Wõ~S7ªƒVAe)úŽž0ö7×”}¥µ7äòˆÎ~3àÿÖo}çñ£?úƒÑîçþæãý±±ú/ü¿ñøáþïüò×{—@9iÜxƒì<9>€£ Å\Ì–7»ðöÑü}ÙÜ,çüŸ”7¶(>²$6ý3À5p„×Óu .½§¢¯ÄÒË1Òˆ.'*·´C­&3{:médY8¯¯Ê]·jÐo¦«tÈ`è}zÒk\ÿ¤lî͇þ#½þeäÈ#[ÑGcem)Pleìæ•¬"r©O+¿[w°ö¼ FT‹·Óv¬ZŠKІô!>ãÒZ‡8”ɼr³Ce%’cT<Çeù Õüˆv#ËŸm»t±É1GœÆž.z’vÃ0žòš¹9‡µîòŒAãÎßìË^ÕW&ü2F-{ ³kH¨à£by°³/¸ômÚ7ÿÍ1coÊHè›ø“¥J¾c$]p•²tÍjÿ~}í|C_-óµj}T†ˆ;gÅK?«l¬ltê{²!%šÚ²¥£¡ü´ì,Ÿ/¶”sGÈ s–s!ûdÂÅ`µ\£'H,Ï kÀÐý©ÅËĦ}ÇbÔ§ËOñ Bc&u³u2„É–küÈ2)T®Ã¼Iç¸A½E­‚QÒ ƒ¯PÉÒ1,ØéÙ7=@ïiæ:ªÞ.<½ÂÓdu qŠ BšÜܸ(5æµ"Ĩ”вb;ʰ•³z¸ç€R] õPª^*g¾”I‡ã’ -Xå–ô²f/»AðBc’ÒŒC3´aÉ£nžT p‹Ùxõ«N¬ã«’•ùX>â-§z£µä’°E^Ø¿yî.`ðºÛgRQüÝe¡#×Íáðßä³ ŸûüÇ{Rök¿æôæjñïC]]~7IÙ8€\fÊògį̈>ø·xªK¾³Ù¸O³ÅÐqvÁœ–Ù ?:ÖÌžÝÒM}䇜?óC}üÀ|èñË¿ôu6rŽm¡ ÷ä‚XuëŒ#¡Ä¯è»¹(wv&¿%Z£Q®y|‡MâÿÈÇRûå_ú9RÆcT,‹]â‚À÷F¡L`ÞFÿ³ŸýA¾•ä£üã•ß|üòWþvX_üây|ðC|üò—¿öø›Gÿ+c# [¤ÄÉO=9~«Iuøo?¾óÎwÀþèã#y‹<üfùé‰2ù†äÉh6Õµo6´&a~Œ4¹êg^`?»ÅáüâB˜ ’Œíe`æ~Ò9‡qZGVÜÍTFœn¾H »þXŠ}µÙç˜añt<ç“TúÈl×ß7];‚Ò/åxÀxøÐßzücý7¾öÛlÖ¿‰ÌwyOìgû¯±ßuƒ®cîì[èÞyüÎoÿO”‹—Î}EæÍpÞdúÂ?óø x?û³÷ñø:o¯{|îñÿÌó•zß~|ù¿º›Öƒïðýô?ò#Ÿà¿sìF±¢!«¨~ù—~ýñµ¯þöã'~âóÜT|ˆåèú¯ s ½9ø­o|ëñk¿öhB~ðCo>>÷¹>¾ö¿Elßædkë¾`%y«OI!Ϯ՞4ë ¦}ê¬ýô·v!6ÆÈQóª1®œ-•SÏoq[ ",íÍn1t-ݸI—…ãû fç­c)b>œ*2£mžR/")í†]mó3ÿc=þê_ù©Ç¿ò¯ü×ÿý¿ùøÿÂÀM·ÿ`ëg^§ã~ZÄ,jkÅõñ¬[=ÔHZ;»áQßöÑ=z]g¤é²7Ðý¡‚¿*PoÌËŠ@rΫ=Æ(ûÌÑùèú¢þ+ùl͆¸ ñdòq:T_‰¢¿\殨`7ÆCâ:eZ@§—«xâÛJÖ¼ŸùÒU7—P8:ž•_†ßæ{90==~i§uÁœÒ¬/‡»&¬^<“[ÿˆu‹+yÝ wïðÆÄÿÀ<9ûÖãçá¯'ôCŸý/<~ý×~›úϳá5_þòo°15/xТM?ŠS¿-f=/×hö27ãψ'‡’Ò_#»4)äÂ\ÞÔš6c  øÇ“r“ûí·¿óøüç?öøò/ÿM)Ÿì«Nç·¿ÃÞÆ—ï¸qX¼ÎûãéS~è1еú¿ñé8E¬Xõà»OæÃ_ü¥ŸUùñ™OññlÖòâÂïÐØÜÄçG`8×¾Í[~ìÇ>ùø…¿ó×_ù^Ê—¾´¼äÃ?ÒÍ̯üÊoò^å!`>ÙÜøS~ÔKîUŒå;ôáqô+_ù›¾ùæ·üÖ7ßîý“²>-_òô‰%{Ùǧ>ù¡þoį|åëoòôZÑI Î:‘Î}òµ‹%œŠ~™Ù|’‚¬Å1[Vüõ‚Ù øŽ+õ&Íg)Äó6¶çX|â?ðø þ|çwظx½„oŸ(¿>™îÍõ.ˆÞ\v)ÄòƒïmPQØ\æóÄûÑ}„~û ïÿEÐèÇÏ|áñ6oÿÒ—~¦öïuø±û#|ˆžÍ5ýþƒÜp~–œéKÿßw©¼óøÊãoü¯DûÂ~’›ºo×o=>‡ì/þÒÏñ÷.ñ¿gÃ9ø³?÷ÓOþe|áÇÿì½wØ_EµÇ;Þ IHH§÷*"Vl`ņÄ.*–cÇ‚]±aÁލ  ŠGTA.$„@è Tˆ÷óù®½ß>ç>÷Þç¹÷»ß÷·ËÌj³fÍÌšÙ3³·lwÞ¹´9¤-{ø~ÒEG„câ„ÙéDlÀwAª<¡ÿЇ:ô‡š’uÚv½5ò¹£lw¬q%“xñØZ:#Ú0Þ\-^ü–Ië2'9$uëi$§BKF!a.÷÷‰Ö “³ä[Œ"TÂ=á²ÖÎãÏŽ| äÔ¦«Ö' 8¢„¢Ü/]b§¨µŸ}B;èmoi{Ä nåEùN‚Ÿ8…,=ð<û'A%/K/¸>#¸8‰ŒXÑAàIúÆ’9ÉÁôeù^b'RähÕa9•PéßòÃÑ+w€4…1.'®)v"„„AA6@¢ƒÊNà{åö­X) zŽÐ)¿¼CÏ{勌vS‘*ED” P'âTJ#º*:¦•ÿÈXrÖfOe\J§kXL8•_ĦƒG@¼lã$nzĬ#¢ˆ”ƒ<Äd‰ŽåM˜8¢Ýwÿü¶ã{´ÏágìpMûÄ'>Ö6ž²iF>ï¼ë–`N˜0“ÑR.EfÒá˜6„œC Ù(Tý„; S¼·Q𬠜@ÓªÜr£>‚.Ë¡-‚Âçà™ûqãG´;™Ûë1~ì¬6GñŽ;nl_üâ—ÛŽ;îÔ>öÑ϶k®½¸Mš8·=È*¾Oª¤E "i·Cm)ª9|äÆ_:ÜÞ’+Cµ‘8>÷?0ŸE~ßúÖ7·1cg·Çx±f5zéh‚ÑqC†‘&§‰LÛdlœÿ Úôö³c¿Š¼;0"<…7¶E‹îj]ô×öþ÷ÿW[¹ªáhÍÅ™§ÀGZÖ’Ÿ%½ù Áð‘‹™˜h“7…Svkûâa_n[m½u;è­d´úVÞБ{pº7Í–ó ™pÜV3ª=‘¼€4-YBXÍ4–ñí‘åkp’”½Â,xƒ•éÈàœ+£+}ΜÑ,ÓîB«Ìz]®¸ ŒzŒ³»)g]Jþ„qZÔø Èóà‚¶t)m“ ŒrÛ™+2ª¼y´iþ,9·§š¢W¥O‡vâÄQíÞûokÏzÖ>íxo;í´?´üà»2ƾÔvÚi§Ü§NSWØ _–½å–ÛÚ)§œÖÎ?ÿLÌÉmÂ8§˜¬ÅÙ¾©½åÍoo¯ÙÿUmΜÙmüøqè{I›?AûùÏOlÇû“6mêæÐJ§oùûÁöÌgîU;Ñô GOæ‘™»Aûùq'´ãO8¶yäy31+¶oœeÀ7u5±í@IDAT<²"»ÜüðÇÑiÿG›»é¶m…g?ûy±¡SOý¸ß o;R™‹þupuT¢ó(ŸºÒa ëTæe¹ÖÉ; ×âüOÚh8²y‘vØÐiäÉ tmH£'~lÓeÞC·³7Ù”ÉjDÜ!Gl0•g•jï­El Èðë·àKÐ#Wlo…UGƒk eö¬­Û:uK0î3feõªGÛðƒÛò•k€íä ­"Z µi)söܵ3i'M¤3”€1Ø4GfóŽÊpµkhéÎ ã”MxÂC€7ÖudIO¼uQ}˜!–ð°‘Ut2‘—¶ÆWÛfÉTæÂKeµ]]jœ4\L HxE^TyÒÉ\'yD•xÚ0¨;½-‚¯/Q÷Êe¾c7ðò)zÑòTR+]iˆ‹ ÆFö®nS—Ò'Ny"¶øÔÁj-Ží&›Œg÷¥›ÚK_úÊöõ¯…·“éh]×þþ÷+ƒ¡ƒòîwÌ|å]3eáÞ{ï'-Û÷-'‡¬\5QSnØÄc8ñƒ™ª5†Qm–wâÚŸøDFi‡ È´¨á8‹ o£ÏU«™[®™ §"Mž42Né¾ûîÇéÚI'ýº]}Í¥8Ý3ÛJæQ;µjjÛàÑ òFáŸN¯Rh˜GkhS—†wŠ ÓdòÆÒ<#°ú€'º}Œ²6”)Jv0V®ÀA‡†ùëuâFÕíåùò\ƒ/Ïž^ð‚ÐØ‘ŽÔâ8è¡Î×¶§=õil'ù¦ö›ßœÒÞøÆ×óa‹tæ¾ÿ½¶×¿þµØå’vã7¦³0„NÛ;lß¾ÿýo·m¶Ùº}ìcn;í¸;ìym¯½žÑ^üâµûî»4“š4ÚJn®’ÓÊNùüÓŸÎIØË_þ2:ƒ“YÐzÓŒœ¾RuÍcØßsŸûœöö·¿GÿGíSŸúxàM—exÞ¼Ûò±lpS¨,·ÚŠô³=•8z‚®§iÉÛžÕ¥…¼³°Nµ(=3451t¤•º9Á–_èqODw'!Äp›öÚ5à'´°ÆÚŠtéTÙB ª£ÃEG"ŽŠg£ù©ÇL‘E?©m ÀQë¾ãëj.Ò(‚WtxW=>ÆQ¹@GÛö°ocM[ì5mS¨q2 .fòÎÀ¦³Nd`I¦¦IتjSéÈŽ8`Jr} î 2†›Ð2]Š&þ„?žƒî“ñ /À :¢¾ÂÕaè„núgÅh‚B?4áù¤£‚×…§3b"€Žá¶Ý¨DÆ›þiI£¯.+:òº§<–Q!àJ'v¢/¢<è-¹ ¾‡´êJü@á¹ï¨4 C4Ë^À6¹¿øÚv/]Ñå|s±jõÁzî”?Nm ¤U°æl• ŸåYÖ¤"BW©˜ÐO"•A43V[ñÁgeÍ7<$xNìŸD¨£ ‚­my’²&jd3œÏÐH˜x§¿VfxaÈÛÇÄ«° ¹Ô V4 x‡SL‚+/‘V@‘[¡zã¬4@ÇðÕNNÒ"@Þe4Þsk&å 3s"¦½c ½<•ɉ)q l#+¯ð°òÔøË0 àU Uqð¶– ý’I¹%bè¿ÿ"ÿ2úÒ­DxF¤U«VáÄЈr ^ÎÒÛx…n˜€Ì“v^³Î:í8#¡ÃÛæ;"ùÀ}Ëq‡£8”ÑëeÌe_…sºa[I>x8S&î¾{I›µq}•óîÅСÓù3zX›4y ýšvß=·#·©à8b·øNöîÆÑ7~8SZ['ÏW+ë‘õ„Nj—\r)S&æ+9ÁF'3Bû`d0½ÉA’ª#4köDÕØnŸ¿$ŽÇHÐ$¦3‚éËÚïf<´6lw1½BÝôZ\½Šžp Åy3zdæî¯dÎ÷`’ú'|Fƪñmþ‚šòÅÃ>ǨüFí€ýߨ~ê¯C£?qÄwÚ;ßùŽvôÑ?ko~ó ;£­ÀŸ”EÆÈvÏÒ¶#ߣMÛÒGØ }EäŸãر#Ñߘ²·û—c#ã2Ÿÿ~lϹü:Ö66<ÓgŒ£34y–¢;ßðæŽrVW¢§8€ÀzXAÏŸ{›;wv¤!“Ò&í/ß«}ä£hoxÃë uдw½ë}|IóÍíâ‹/¡óùÔgÝåŠ+®Œƒ~Ö™ç´sέð)ßÒ¥µ¿ðÕíâ¿ð~®CÊÝÎËrgÝyá…·§>uOž-tæ8&o´U{ù~{Ó¹øŽíû{>vsTòÝüïÓ5nùóð#m$v蔸{±…åäWül¸G©‡F ÔfN‡Þþ[ËÂô±c}󲊩uËR÷l4yt[òPkûðjÞ\m׎9æÊÖâ6vü0¦­NGxòBû¼—üBçÌò»†ÎÚÝ‹)?ð±Œ9Ø0w“Ét˜–Ñ’ζuÇðÃȯ È›%ä׆È366³bÅ Êäx:kè=”<ªê™ºyå锤{ï]’:gÓɼ[ƒlKketuÎ9çSg¼µ}ä#¤ðÃØ¬yýà«Úf¬Õxø‘åÈI;[†-¾kë|Æ$ݮɉ®°©ÔÝð´£2uc×qlѰa¤—?Œ”KÒ`FÛ%þ´ÛÄA¯Ä¬x¡¥c½_Š¿÷‘Ã*»ª `d੃©ðÀE7U°½)´V›lk¯ói« öølGd„ŒÊ-¥¨³Ï,·Æ b¯§¶'E¨GýÄ™7ž#ø¡m¸â]{šÑb'{’©Ä,~–g7jÂJâ‚ó̯‰>d%L¦%å*¦p @Þž}ÛÕë'ÝÛæ/$C]hþT(ÂÛÖ »ÎáI}E N^9Ä/úTév@!ùª+„Há'LÅ·K·Ž& B%©JÒG‘À%=¦C)‹}À¸ðèˆ0¶¨J£ø†¹‰Qs¹-ÚàèWË ”Ç|ºÜæ^LíÞ®d"ÇæÕ±zVZ….ªëá€Â\;ù£HɨWìD²ááµìbcD5…{<Þ•¬"ú^<}õ_vûï:, €’f H9ÏÊ•4+¸´,ÇÒÀ0oÔlY˜>€¡†¥ÌËC´NCëä¹TŽ4B¯ž ±Gyš'T|Ç Nj}"4̼GP[Î|øK:xWÓl¤‰Qr à6]‘–ÇN¬¢R »B-<9€ËT ÈW手iíàåÑëÏ çè+éF¶(¾*´BU‰¸u†UdTžGáZž"y`×…KH;I¡¬ –ò‹„2–±1. °]¡æ¹FŠSè…§ÒWÚd s$‘’¨TYÌ“Êã^ƒB¯+Hé%±Ê™¤GÞèÊ…Ʊ¬ª<¬ 5Båìh’Ç>%yyòÙ_/[Á˜NÁEÃ+¿¤»°o¼aQ(8b5ªÇ-·^Ó¦OÛ²-ºë~œý Ìg^ÔîÇ1íÛÞ[ç OÜh4æR¦¹ŒËè·£FÎ`±äµ=x;nVFö–²¸rÉC —Óvßÿ@=N™²e`œÿ›`oGÁãúë¯`”u—ö³Ÿgª©Sg"gñ˜Êô‹Z¬[:y”†~ãÇ´ ®ü„ñ³qHW2E`ÔÞ÷¾÷dôß}_øyóÿÁë,œ»š>qÂ:'Žp²ˆ•‘•Ù‚nþùÖè Ò‰p:í÷×]÷÷gÞy7÷|útòo‹U»QØûî»?õÄÅ_ØU+WÓi{¤ÝxSÍ%/ºu;zf{‡o*:\|÷-Œd¯‹]Œ,îô·ñÆèý ŽS¯ß¶¾~ç—}ùÆaôˆQÈm©ntˆ<—tò<¸dyÅ1c‡áÞÅ›ùó´paÝÄÚ Þ Ü{ŸÓ¾¬Ó6hS§%O?¯ÿê«+/\´[yX¶.•”×u„Œà§#¾²ýú¿OÈo1 Ü›7þpkvœ"ßžþôWÓx{°;Ç8˜£Ûå—_ÜÞðúƒÛß.ýs{ßûß5Ѐ PP꾿¸úöF¾;Úœvd8« ýð‡ßÉÔ¤SO=¹½ì¥/IÀD™R+®¸8×þ4bÄL¦­ÌÚkê ãGæíÒmó®ëA˜ÖèÔãŒé[¥cõpçL/_¾<ŽüÂ…¥× 7ؘ…µf-Ä‚ÛËN{B÷wýš3¶jw-Z_ËÃÝmÙÍ]¦õ€ë]gÍÜ›ZFgt唓°÷/Ó‘EÌ’¼­qZÖPä¾ù–«ï©·åÁƒ¦ñ¨êš&ÎIý iÒ䥅»ß¼1ØnÞT¦j §jÔQ Ö7ÀÄ¡óëÂYë{«ì‚¨sr>ÕtNq”´ãøï]›RN´±M*ÞÏD¤®ç±B¼AîÀ*ží0<[ÞÒ‚ –x-¦5­ÙÂË#œËW¨çþÞ„ ÍUA;‘¼À€Gÿl%9‡·0â"¤ê-¼AI€?a(Lù(§t,p‡ÔAš¿HVÈi»zDâ½ü¦®ÞôjÚ½ú#IžrB=³(ŠopåkÚ„:8œ„HrKèAa$%mŸ 1’yê ’ØEh‡üÍ?Ã‰Ž„Üg÷ÅЭ°ÒÞàPO§ÀôB°ï h‚Q]’¡\JeNó¶“7äNÌSÝEË@k7JÒù^£ §q¤ÎNZò™.éȪ×A“TdÑ¿4r}{çYV‚Dx£¹íGëÍõ"Õ´&N§3,*ýè¼¾ j¡:|Š2Ü)£¼ C´?;þÚšrGŸ‘H¼¢Ú–Թ#–£Ò@™'IqúÐHQm¦üø’¡c~âæmžJ‘¿Á5â_ E „BÀ"3VFˆ#0,u ÛÁ Ÿî«sÃ|«—h‹‚¯ÓjZ:‡3° ˆ<¡Ÿ‚¬Q€“að»¥ÐA—vW‘D“Å¢FAÔ²u„ÅP)Ê£‚ –W®dà!ᦋHîןƓŠÍø uºà^.ý¡XH!¤<ùÃ!² H7Ów€‰È©|­•PÓ›?Å+yC'dH^TOáÝÇ»ÇÚ p#ƒ–Z‹+«‡œâ ÒžÄWºÕ¯:‰B´„ÈCŠÔ•z‚‡ôR0ÃÍ9z“k ê[)ŒFï3G"Ž7¼ñ¥íÅ/za›Â„Gªÿó¥o¾å–öÞ÷þó’·¡ñ-'úG?:ºí²ËÎŒÄ Ãz˜¹ì3*ù^œ±»ÛܹÛáH_×=ô“ÜÏio}ë[Ú7¿ùí8ºçžû猤n4q6Rßݾýíï3}ewF GÄɸ›–ÿ8çBŒâ0ÕâMÍ)S6F^Ý;ûÖ[okÿõ_ïn¿ãÝí »ïÖ>þñ/ÑxßÄ´ˆ£pbîejġȿ9 F¤QÉäÉ#ã¸~îs‡eÎ;Þñ6ænçÇžÔ>ù©CÚæ›o–i5÷㈞qÆÛ—¿ü…6fÔŒ6ši6ëŽÒѾû¾Š7 3ÛI¿8¥Ýƒ1z丌VŽ3ggAÛ~»=ÚÞû<‹·'_Êî/Ž„/¼½œ”‘#F2 ùŽòC8¯:'­sÎ9|±tç¶éÜp‚®j[m3›¯™¾³ýõ¯“ÞSèTÌadô1œpÁáíSŸü£¦w´£ùQðS©`·Í»2S¥ìLŒ7ŽÜêvë-·¶~äómÞ‚«IÏ̶äÁ‡3Çü©O}j:Gù-¬ÅíðÃàmÌøöÉOÂöèl c^ù{u_Î4™ýqgºÎÿÄq›µo÷ómk¦¹ ¥ã®4N©ùú׿ƒ œC‡Å¢¾m¹×N;îÙ>ñÉG¿:¨1þçóÎg½ÉG‘}ÓyÆÿyî¼ó.Â~ÜÎ=÷Læm'‰íÇØÛÎÿÖÞóÔ7møfçPçÿ•¯|m{Ó›_ϨéÑ›6ñìì¶[çS|(½½íC:eÝúŒcÌØ™¬#Û–ãøctzã©Û¶K/ý ÷­m‹-6̈#è˜w¶,ö-ýÃËV’®•”®Mx›rIòsÑ¢Êï q’ï¦ÛÙÁØý Ûàp¯ŒŠR¥v:n¾1ꞺŽÚæ›ÍämÏ£Ø×jÞ†fªÞ¦ FžQÎ6ý™O@¯µ¯|åëmÏ=÷ ¾1Z‡cÛ‰'þŒÿ-xù0ÓÆ§þyå~´7¿å „o<§Jù6е{íµw{Ç;Þ‚ó?šúiè€Üo|ãûˆ}°ýô§?kW^y6õÛöË_}2>ºö…/cÿSHk1Žÿ ºšMGý1ZȵØÅ0Þ Ík¯xÅk±µ‰¤íXFdGd(°›g}>ê¼èbW{@]MDZ'óg <ùeNëÙÔ(7ÀÛn :âÞêÅA“áÚoè'\¨CÀ–\4,$„Ã7¯îV#”Ⴈ¤B6?òÎ —š¢bZ$ŠäÑé÷m<Ø«4Š2†iP…N°­lJ´”JlQ|*ñÕÏ&¬“› ã_â½æ¾ §}ŒÏ¤.ÍÐðÃKP~^ RC…%ÜzçtGà’Iý^f\“0‚º¿rÀ ·0äœøòQ}$Æü|KHžÐÏxçQºÁ­¼!j¸ïÀ„c例S®ìÙï3÷fOò€+VÅYX~ã“PâêPæ’@[§rÕ§ðLºèÀ™¾ѵž.ùe†…Páɶšû]ú*ÄÈä>tÕ“¸rÔ÷’Ÿ0üÊgDgÜ‹T® œwÄ/¡*‹Nÿ˜¾ÞV‚ϳ Æ¥,’1ã”#Gi< WÙ«ò,’œzªð¼’ŸrGfâ½ç'„G¡†+¢É­brî€ ‹èÄ ï½yÉ›¼B\¹fqO”Æ×,¡4‚ÊN¨bøQV‘‹¤Qsøú§r:ÍÐ2Ì@:§É—…Ûî;®^‰Ñ«1@ž\t´r˜¡xù³€*PgHÙËy¤]ÓjÀ±'®‚%Ë)hÈoª,ŇY2â–ê7Æ{ƒ£íxéó‚^•ÁÇŽ’|ú4ª™ÊL! CðO½y5Ó¹Ï/n¿üå¯ÛA½…Qø) ÷tÿ p”þÜœ[íZƒÛoŸÏˆÝ"¾à¹eûÖ·¾ÎÜø÷vôè1lŸ91²(Ï„ “˜ª0.q»î¶+#²/¤c25Ï;ì°Cäðáž{o§ì0îç'ÞŽÈvÛm—ûE‹nÄqÙ¿zÚ¯pPžÌ[€»ãÈê zèGqîÕ–=r#¥–…:úÑóMq¼>óÙOàX½ˆ¥L#±¢2ÜhRééㇾ¿}ð|tË'´#¿û«äÓË^þ’¹ø’óqR¯gG™‡“G;lÿœö2·ûÙ8g±‚¶õÖ›Ñáz‹;÷ ΄‰cxkâH0SÚòvð;ßÖö{å˸/xœjñÛßžJ'ìýqìÍ—eŒTïó¼}Ú¹çJ‡â•¤g!ÓqFáNÄ¡sô}œ¤qÑï=÷<Èhö¶mß}_œéV®µØu×I? ‘pþW¯¹³M²M»à¢SÛó_ð<¦´,MžÝ{ÏÝtìžÆŽ£Û“÷|6ÎÿͼÙ$ÎÿþLy:õ´“Û“Ÿ¼'žÒc˜–ñ¡Èjc«ã3~üF“gœòÑQò˜={ÇvÕ•×´Wü[{»00÷1ì¼éf“±ŸÛg?{£â?l;ÏwÞ¹ˆŽÒÂØéÛÞþ–tŠÒà³+bý½eŠ{KµkUtþ=fÑÙsº’Ç7Ü”çO}ŠN;;]sí¥¼¡¸…¢·¶m±å: ;Ðaz~;øàw ‡]ƒ“òFY½èB;Ï+Ú¥—]ÈbÞËqø/o×òózÝuõ&&ë”'2ÙnRd¯½öò¶ÉÔíÒ‘õ-އë|c·çž{¶ŸŸðÖ‘LÂæï‰ƒú¶·ÔÎ:ëO@-Í:ÔÍqË3Ú‡>ô8övàî½÷Þ¬…°ãý‘ ÌýqŒÇSöìäkûãÇM <Ï «îàmÄÛWÿo¸¦óVæöè{úôMÚ×¾öYç_n[mµ9ë`^Ú9äƒt0?†£=)åm옱íý,Êþò—¿Ö~ýëZg!mŹëfœ6ç[À­·™–rs衟Içb‹-¶€çÂüfÏžÍ@Â7èÄ~SvÈ8&Sˆ|à”´ñã'Ð!î§]Êz Î;ÿd: ÏH§ägžÚ8àUí ‡}:4–òfrìØ¡LÉB§¾^G|éKŸÅ_JüÒLÃë–?AîhegûbÝìæ¶,Ž@{²Þ·µæG/†y<žjô‘ëüà'ªðÞÁ‰ðÔ°ÒðèèdKФYÏ0ByNÏC¸ŽgÜðÈ t*Ò¾ƒ°´r³M RµMám[ UÛ*žÝѯFl-AÊ_xÒ«8ÛqŸÌ©ÊJœéäò@R¯†‘I˜P ‡gÚÇèÆetZ@tصqqФŒó7·¥óW`òÂÏ\Y'Cà“ö‚íDé42º¶iéôAÞKÇ´ÊN°àqêÓX¾—x¦ªp½WŠþgl/O$ ±NöÎo0¨hx®{;YBÊF%hŸ@}|n»S—~åU@Œ«ò¿ò¦jC@‰ kèT‡V KÖžšñR‹ö •)7”çr± Pè+޾¶#rtX¸qÊÑYxgšýE½=ÉGú}š$~ÒÚr$N/}… SGQ´¢wÄ]n‘H©z®´ÝßN>¡ÆÄhhš{¢¹B¹+аRyhK·h,lgOT1=O;}–’w¬_%ÈPVº——T ·’a…Žu^ìxX0ƒ¯€ù1pÌé*¡ÏS¼ê²CNñ¬Ðð¥IÑIJ²¡a¡2U€’ÊÇ#¯n,ð¬I5„ãì—rͦê¹C"R@ Xø™Ö(E! H‘FàS¹TæJ÷I`%…Q:ý&ë*ˆÓ˜xH:Ô®i0(iéG¢3ÓÔñî^Ó b—‰•£²™c•¦¼Þ_*:B•KŠ!%¹Ü™)u Po(^˜T^C†C]>ˆ–ƒÓÕH·lHšz-`Àr82äqâ ¿mßýÞ·pP®Ç©XS·GÂgÎÜšÆö†ö»ßÍbÊím½³ýêäçéÃþxûô§ͨݛÞôú„;ïyҤ߮}ö3_h‡íKLš•8G÷t ?ÿ¹/¶Ã¾øÙ„õ§… ñ&âuí»ßý&‹*š…•ò<ù‰=XœžÕ«Ù>G}süŠŽÆç>ÿéöùÏ)ކóƹÿÁÇÚ>ÿå8;vF<µ¹8 ‡eîð¬YO"¤¦¦ç|üW½ê•q„¾ò•à ÊÑ|qÄWÛNûÇñ•œÏ¿þµo¶/å ÐûìóFGÚ¾ð…Ïàtÿ Úÿd÷šWâtÙÉz#¸ïmýõÜÀ?ùÉ»eDÿ©O}J;óg#ó«¾)otn›wg;éWGñæÄ¹ñ´?sZâ<½èEûÁã˜ö¾«]´ßŸ˜ÿ]ó¸>ÿùOņ6Ýtî¬7't2ú}EûÒ—ÏbÙ×¼f¿õäyòœÓœúáqüñGâlioïÂÞŽO˜§µ·[n}°=÷9/nïy÷;éÈ-Ȩõ07?ûéqtš^‘a瘯;\¬î A£S¶_ÅÜ{_]O9å÷¤y:o¾—€oû¨ö¬gí•NâYD­s}æn×ßxùÀš-¶Ø1óÊW0ÕËòe~pàÞtH¶Á›©*Ú¦¥ÎéKwÜqW;ûì?ðTusú=ÖŸ:çô¸ñc·h¿;õØäÿ‰'þ*0¦ÇNÀ{<§ùFö?•pOßÿþQ픥|䌰—]uÔOÚ>û<·ýøÇG3R~Ь7×]w=Ïw·ÿ=„ßäwê©§³P}—öÄ=ž0û’2zþyazÔ³½9ó̳ÛÅî?úÑ÷é_Mý³6Jóü‹ë•÷sÏ=Ù>È´¾ÛûÞû¡vâ/~6@ëvÞš9Ò‰?m×ýãïtb_•]œ„}ÊS,³ëéXïœþ‡3Ûï~÷ëü,k»ïîù^î ÐßätævÛm7Êá_Úk^õ–vó­5e¤=“Á稓\`=‘Ž·ïg—,Y@~:¶ÿóŸWç:–µtº]ïdM›º;íµ7xÜ“Áii{j†02Þ¼Oû’ºË¶"­jì¡wlêAHë¯=îýËàø¶}þÕ¼éLf =Ã(î"s’¿÷¾õWâ’±ÚÍ@Y \…/Ÿ€!6ñÃ8DZ2$=çÖK§v$ «ut¤ésXC22»VG3ø5ÚnÚÂ_pÜ9ÎnUiVö‚5µ9 ‹HųB¯2’imøŽÑn‡_Ó¥z}K#q\£Ü°ÇŒ5*Nz+ŠdÄ×U Ê[ö¬&*ã—v"ê.%'Aqp8z‰6´ÕÊ+§nùJSd°É¯:WóÝ8AJ·)S†øIš‚Ñ`©$hHº“ êáU´™€ø&Ögî‹u…™Èäs'-).ôÄ6L»sLŽüâÒrôÕôQDŸ «ƒp®ÊД–ªï"xæ^cų…Nwd6Dæïø¨ÒL"åH‡xÐg†è'øFÅUÓš&¤—F0b©àîúp‘°@-|¡g¡©Œ‡¯âz½IÒÂOš±æA±hÓ yE^^rí’ lÉF0¥[ rˆˆÈ¤á¬«XÑ*oz-žÃ$v‡qI71ìì"׃3dãM&&B‡eâØvÛÝâü;Ÿ}ûí·o?øþãüo³ÍNŒ Îjsçlƒ3ýÅ8COúÓÀp´Í·'娞tÒ¯âüo±ùŽm[>z؉¸ä’¿ÅùŸ5s«6gÎö4°[&î²K/k³UÄÝáW#¯%O´Á‚Äê”h§*ÇÅ‚G|ëkÙÍeŸ}ž“gGE×tÎÝÞ„¹ÓËw¾óÄ}뻡ߨ½û=äy#Ž;0‡X§sdãÞtÓM8/¬§Òý:-Ÿwþm‹Í7oÏ{ÞK=½•) c€ZÇtâĉí4œ&-·Ø¹øºÚ·Žø#ŠcYDz0S ŽÁ±›×þüçóé4}…‹óÛ¼ù×ÒA©éFVNë¨ "1u庄¸p©ÓÉ'ÿ:ÎÿöÛíÆ[„]Ûv{JûãËhìÛ–[nÁT©Oeý„ÐŽ’bNŒZÏ)dÎ~ôÊ©XóæÍpþgÏÞ¦Ý6oa`”ÿ8#ÎÿN;=‘)[>‚NÍÉÁÙzëÊ?wmúÖ·¾Gü4:_în»>™|³ó6¸½úÕûå-’SÈúcܸÇËsç]7³ÏÁmíÑdÿ­·Þ=ÏYÏÞÎf.¾öVS©~ç[y[1¸½tßׇ쮻î=h_v(¯ºêê8â½Ã/ŽöìÙ3ÛÕW_“N¯Í믿¾]sõµt$nÏBßãŽ;ž·=ïaäëöë/¥Ã³?[ƒ^Àˆû“Úa‡}Ž)M§ã8ÿÿ›éG¶›o¾[{0‹b{£GŠã{ì±GcƒG¦ï´ïóóú£IçÍ‘yòäM³öf÷ÝwÍ4eQ&×\smûÇ d{˯ÑÑs=€‡vî®PùË…qþ7ßlû¦¾=ÞÉÛ¢eË–‘Žzö›/zñ ÑÅUqþ ·ýv»ãœWü^ÿŽt”¿üUßÔ1|8‹u©«,ëÆÖÛ6?@¤wþÝNx§Ê)wG¢|·qo G]“éb—_þ÷è`3ä{⟚¸K/½<×ßÿþqþwÞy:%Ë 7Ü@™,~|ð[™Ê´bÀùŸ3{Ûlûiœo—3]í ·OÃÔÉØ1cÚþ¯yƒÔ¥ÓYs4?»d¹ŽãyÏß;Îÿ[ìDìxlõÓ¼ÁZœÎ‘ðîDTNJ£œïé…Õ¹ra½ Ô©l`Sß[Û†Ùrâ—?n3@cýOH—vÈêÄ6€ŸeQ:—º†Bç2m‰|¨ŒõHMÄ)<¬ˆp“¶LñáÑx¡ãTÉÛ€õèûh`ÚHœ2§ÀÇö5õ®m²UD®ò‹óÏU\ÛiSŽa¦ÇÖ¬äW ˆ˜!½ø„veGAغB 5Ê×tÇ –r  ¸'Þ¼ÈWtC˜6QZ50ZáÅ9ƒ+ßHƒ:‹‡éó‡M‘~óLNI‡d£>|p.©ƒ½R–låÞÔ˜Wõ+iªgœÑê[ï•Ápô;ú¼çºÖmÆJPð½h% (½Z²U“¼Ä·NÅ,Durð˜4r£HC'2„¸±ê¸KCì"H„wz†Tü {Ó²õ\¼Ä*ŸßÀ_Xƒ…#ãð¬Ý'¤=>mp€ €¸¦³K_â€S}òç?ù‘4š’Òn§™E_eµÄ¦“„tè:¢vtLARhÚ}àÐþÌÿØiø$Í<*Ǿl'¶‰¬¦5:OzJ×ÚPuÂ鎨 5Ôé¹|:iR6Ò+0UþdæêoÕ‰:‰'š@E°VN¢™S h¯ãª0U@¥ËsX”ÂTož£B¢bõE8ÿ]^ð\ªD¥Èped•0¥±¢ <Í‚«… R|@ .FHCŠ.Ý…KŽH ?©º5§Á3ÞuüÄ >Ñ¥ïžffuy¤Oú4òÛÀQõ…‘g*9Ú#‰æ Œê€É•ìUÖÈÂ÷%YéØ'î8Èà®ãTië¤Ñ I¼©©Cç°RVÒÜWœ)D1»#Ï¥%% žFÇU{Å»îΧ~‹Nï£Gε°FðùÏÅÙ^ÙýÄQ„²MáÂÕ¼_ÍZ¡ãT\Ë«÷1ìG¿ž‡á8<ðÀƒí¨#Éó°ly碇ڈa3Aÿx{Æ3tÞ˜Z´ðFœ­k™³=iÞÊÜã’oÑé¿È'¼O(½Ó_˜Á ‹/þÎüæí9Ï~!»ðüÇú:¦×¼¸m¶Ù¦Ù¹¥Çs®±SŽN?ý7mÚ”m™çýóªn[oµ}@ÜÞ9ƳfìУ<îúóŸŸ”<ÝÿýîSç=ï¹ìŽó`ûìç>™gõ=—ÎÍû™4kÖÌöñ}’ÑÉS™S|:ÿø!í¦•xà›qBÿœÿqªÌ낈ևÙÍK?î8õÌ’Qvƹá†Û™³_#ñ?ýé/á÷@ÖlôxýÛ wGê[_vYˆêD.\ðÑ#Ù>t‹öÚbšD½¸êª¿15cI{ oqw?üʇ¢æ6Îåw:ÎÄ sqŠï$} øÎÄ\&·^so¾ƒ¾Kú7Pý"NŸ½­ÐÞ=’§ñmñ¢•YÈúØ£ëìMïu–L;í´龑„.§ãµS»òŠÛy^Än8ÕAø#o5¤?|ذðìO§t¸\gáÏ:c Sœœ¢voºA1lóÛW0­g·vÓÍ—eÊÖsžó’tìþv饩žóìg±0ýGä¯oGþɹGÂÂoVò¡‡Ÿw"#òGòM‰cpú2ðsíŠo<î¿o5gê™h ÒdÅ ëc~ó›ß¶½ð•¡Þa‡ÝƒãË„i÷°[´èÞÜÓ¥çê¶›Ö'©ioç-ÜHÖü9Ïc™víu·µy·.f­Ìæí²ËÏ£³ÆêÌ('k"¿°˜7 ÏLÙøÛßê ×–[îÜ®½f!å~ ¿þú㽘n(à¸: ÷°3˜‡Ûº>ÌŽEvp<fGžØ…*ˆmTƒ >o·Ý¶¼E»6QÛm÷$Ê÷RÄ?LùªŽä"ìÍ)„ý¡N†ÞvÛ­âG-Xöü¶g ž;Í3#hJ8ýèiOÛ›M®cc«é¸îº—þíÒÀ̳r’O4$¦-õ«1>ä©®Õ k(m'ð¹·n6mMœ]Âõ.W™A89b.Nl·m×ʪ@æð›yØNéXؾ¤ã>Î,D¬Fªëëhch‘ŠÈj{f… a±A°ùåØgÍÈLé¨Î­[1ØèD™Ò¢œ,L ´ä.Å ¼ØôÒéÌHÐë'0ùqÆAª)VÀÓZ×&X|þeGk@¢ÍðêÛ;<—šá l9ÐJf<° S"òGZÜEnå7ÞÂÊa*+%Gˆz.xýÛ€;šq#(|¥“´ëq;”+︻ôëç98ëÀbÉkžkKê{éyxíåõ9)1¼‚Ͻ ’&5¢Ž¹(†—äƒáʨ,PÜøðñAØÒMø.ÍúS*ðCVRW&–‚´àtbߥ ÀDòW܃½«cƒã‡*—¡I{^Ζ¯’³àEœ×ÜçFh…οT¹MŠŠGBJŒØ\`‡ÆÐøÝœMCd¾x5¹Ú\¥ÞXäÁ4aŠœ<“/úð¯"æXìSÀMBÔ“ÄD4ñCÀH!KPcˆØj \Ÿ,Ðq%ch8æGñ‚ð—îŒ}ò¿ µRû¨øÒGÌà'8:¥”é@Ÿ{¥P1I”—»Ð!L…ÈKù¼—@½rÈCÞÈ®‘ío€‘±„GyòðÆ{ÿ‰P™¹'\õ'¾þENq”)B42C7Ù¢ÜÒ2Ìä“¶DMaЪ#ÞÁ©—L½ Ú¤i*ªîkÔ¥!‚a¤F&éÊÏg)’fÉoe«Âg¼Âq~½IZíį{ȞĶi@äP<þïqhz6‚Åy“&mD#|ZÒ^²£ªŽ?–ùØs訽óûïºÇÆ}-»Þ0Ua°ÛTe±äÅíe/{ ÎÛþmΜYYtꨴ»Ç¸V@yÈ Qì ^Nlqyü9)@_Õë¶ÐÚ/N<9s°_÷úýÛÙ,–ôxݯÉ4Ÿýô„<{Ò!Úl³¹8-× މ„ ir·!§Ô8ò¹ûÛµÛ︦ÓoiÂQ}GÖ]ê(°Ç•W^Ü6Ût7 Ž–£°ÝvO˜Û½Ù¦;°]é-í;LkZÿø Sqþë½ïÊÈèÕWý#zqäò?fâã§#8’}Í ñN,;³3ZjGi"rþ=#§ë¶º¬ÏÕ ääÔ8ª­Zµ"Ï+pZÙ©‘_¿°ŒßÍíÝïz?#ÈÏ'6f4x $ÇÄù…“í· úc ó»W¬(xC¦¸lH¾:œ¹é«XŒ<’‘ld½árœºêô6¿îÊ»…ÇÙ–jÙ0Ó¹ ØÛ„qmÃvâ„ t4nÉ} Ê–­°KÇ|xÍ…Ñë¯ëÐyÖáÝygGÿý1sƶÙó~v­ºþzG¬GóÖ`6»í\_õä=ŸÃ´C™cþÖ½•iNÆù&ÍÀ;>„§{ðsÕ–v£åW¶½öªQòØ”)»O­é~ÿî¯ï›:ìP ¶Ô»­Í™;+#ã7ÝTzª—ßÏøgÁÔ2;í›o>5OEÝ‹»:‰T³mï’¥:â›s?²ÝÞ<ÜösÈ0m°ïŒóN ]?å){R>öbkàGR_ôN§Î8í©ë_å«òkû»ܯ+ 3¢7 ¸’5!.@OÝÝQq1úܹsý~B¦ó[DÝS-¶ÛnÃ33÷Ó!c+Oþ2u0·=]Æö¿cÇÕ›É_žôëŒö¿éM¯m\pfðÞö¶7¤¾øÝï«>±Ít Q§ÿhÊœ:«ë$Æ|H¢Œ ëëjKÄ0”?׺øT<¡¬ãÓJT94¨4ÁYêpKùš-Ws¾©iuƳ}ª6Gßf)´C0|ÂN¦ühO`‘‘\â È}ÉeZ‹¯"(u' wn‰hHÜçš ËS”Ë6!åI © HúFpw5³µÌ ›eÄYƒPo+ª3í&tL¹á r­û´ÃxŽ™ö‚v„ˆ/¾Dø‹©dÒà@?÷AÈ™âám‚l“½á'!9×S‰¯a;oˆ}þ‡Æ¤Bò“¿í¸ÿú`Jc™m¿ôç8¹nH¸:ßÕI#á!×é®K›t£êiƒÍç-¤æ)ô"Wž8™öðVžžF/‹éÆçŠS®JT„Hþˆy–»øb)¡L˜²Fb"Ï)âfdO¹ãcþFãtœxRséx™—$À4=B Õó¨\Ž^FŒ~Vt¡N拈ږ7ú½LZ°Z±SUy³îí@Ùƒ8%Y%¹l~`D?2«/S[é©ÎaÈ‘N“ YJõ†|ξ–žÇp ±äa9ŒÑC—YÅ1µpA¸7ŽP“h¤”‚‡D*qšaUJ fb5Hà9¹0×¢pîäœøS=Ä%ðÄñ•…¸Òx°ÖÉ|-ÙI”!+-ÿü÷§}Ô³ø¦Ò'MVfÒ—2Ôa˜ .§Ú “†g{ae„QŠŽ ;i<>Ÿ ”0:uÒe€Døo¿¢.wî:Y šjËÐG! çЈ\¦#rv0œÆ¹(2 —o–„Q]Úò,ªz1¿¬T”C¡ÁÅ#¸u[Äwò÷E>±¢­w<­ïuèW¬XÞÎ;ïÏÄÐ åYx¯~~àØñJ×Ö1’·Ó¦fÄÿºöÕ¯~ƒ©6oÌT›nº™×ë÷à@¬lßþæÑíkßü4£v»æcO!ô¯§RrR›‚–·¹swh`DÿÖ[?žùÐ=Ú÷Ø=Îú¹>½Í™½#‡×åHÊzÖYgáÌÔqŸG£V±¶À7-˜¿0dª“QM·ÇYgþ©½ëݳ#ÑY„¨#ÿf: ÃÛÉ¿:%ñîSÿŒ§ï…·gŸy£ß32ÍÇ’è´¥{?Ì>óLABw‡ò~v®9€w.N*²ß£³ã,r¯°=3¤1€JvTYç\LwIž±Å&^€;¢ô,ÿ×£/?ùÚÒyí¿øUœ#f;?¢¿‡˜^ò¾÷}“ŽÎï3ÚºŽhÙ„Ï:9v,j$›}áGfAò‚€þO9Ö ¶ÎÞÎößÛ›»Åœ{îù¡UŸÂ·Ì™gŽ®T¾jÖÿjáEWÆΠcÃW‘ù¦Áö™·¨Þ|`E{ˆu“6ÙžòÔýXãp_»øo—í)u€¥}Ñ_ÏnÏyÎÙYëá””M¦îÐî\Lç‘Dú¦ïÏ6öyr¦°= Òo'/õã-·Xn¬÷¯Ñê¹s¶ÃVãÛkØæuH~«ùðÞÂ;â Ò$Å8útZ×VclTG¿S´r¬Ï·â)»¼ rm‰‡åX¸Ñ£‡âð/*Ëé­—yÕÀ«õ²S;ƒ6ÂCéh›ï}¹©Æ¸ä9œ`ÞõÌK‡î®tƧ3}oÂzõ‹¢lˆü)¹­¶Ú˜´-ê)òÕó‰m›Ü÷à 4¨3½1Ö¢1¥gò¤ÍÛ)¿ýeûÄmm{>©:öÂ<ýéOaÁûBº÷1f1úÏ·'À‹bÒ^p]ÐjX-óWr¶Våž«ž®/ï®ö¤4¨ÛªÝ“¦ñÅF jÑGµlûeûàH¶4eTm@nvÁ? FÃF>Ð €'„)¸Ê¯8Ù‰£-ÕáM½ ¾²£o}†Ês½Ê›4•Ѷ©:uðQ>%ä)+¡“˜špõ(q¶æ5=GJ¶Ä%›3„²Sb<·´ÉžDäšdp mÃtÜx€©8R3ñÜ"‹&Ñ×ÔúM☪â("‡(¦‹Ûä£uK$ªÈý5y*—úrH•ñØÉâè´¬‘¦”ÄS¾rÚ+ï !"eVåAI]‡4^^yªìA‹4«ë’‚€¢éå0¼âòØGçò‰¯hŠ¥LEKÚ¶åíÉŸº¦h ÙñÉÜÙ0»0IXXñŒÊ¹äé¢Tå¤b½7?Ê®ÁIž‡-p±½G·¥ €ø&^= Rå™ü$Jù!µäM¤YvV yÅ÷¨mWM`:¤áá=9}j‹ÖKÒ⇙ï€Á¦ÃIš€}iŠ òŠ2ú8Óª|Å_œÁ:•á¹Ö!KÅÊE\ + åÞ¤ä0¬Ð¨zë‰%×´wH ·ì˜¶B3ÃC<| K¡OäºT—2Ä p¤‘R®òB#±RˆÒr >ñ}¬øÁÒi4Q4$Ç)O>‘A[ÙúizÒ8ã¼áÌ–Ž…éKaqä‚}ÁR‡}áφ!4Š8ƒìHØ ÖeRGcUzõ! ¹Eƒ—TWU®JaAñdx2=DúÔ#ŸÏ“Þ(Pþ›îô{¾ æ”Þa¥({‰”DÔõžs[•´ôALݯ7€ƒLæ›±p슂³œí ÇþŸ1섲E¢ûJ§`×°ÛÌ(¶½’¹é{ežý-l/ú¤'9/üñG9jëËTñ½<ýÕPïû)Jc˜ÎàqöÙç´÷¼ç]í5Ìý%Û2šâ‰'%n,sÍsþ»Ç¸Ê>fã´×Èàú0óQ%+‡C?qx{ýdNøóÓØk¯§g{Îcû1#¨;âÐ]ݾ÷ý¯0¿zçýpJ.ÍVŸ ™º°!öºÃŽ3ÚUW/jcjó—§N6IÉïíÛr8†·‘ÃØªP;XïÐa³ã±å–3Ø‘æٵŽãýˆÚƒ ñµ-ø„ÿÍXEÚÊëñ´Ö·!§åÌ™½¦kÓÉÙwßeúÒk^óÊ:ý°ëÓr¤Ý·=#ø"ï lǯDΚÉVŽtßú–w²µë”öË“~Û‘0ÁëlÎ@ÇÿÝÞ¦ )xwŽ]‹©‡òu×á|7a5Îù8>\vþà´M6f$—ÑnôõïŽ|ÑGØe#«ysõÀ+ùêòcÍb=ôÈm¤c§öõ¯}µ]ÃõŒò¿ùåÓyãp7€ Ø_~(sןÆô© òb&ß~Øy—­Ú§ûöÈÃNŸvÃ^N>~»­Y·û…êM¦—ífªf!Ö¯ÓúóKº«Ùjô~¾-|JwW>« <§Ô“}9¯›W[n¹Y†³£ÁnPc™þŨ¶‡oà/¾çÿö<[Ž-gkøê³Ç-|ãb_ž9ÑvwÒ†ïît~ ÌcÎÜÙL×ZÊN;g±æãµ S6 ùo²ƒÿµûù'kMË·n@IDATnoý¨oRþÓ±)oöÆ&Ò·®°ãñÊWïÝ~pä |'bÜã•Â7T2•q«-7â-—Ûòþ™¹”m?í`Κ5‹­‰Þôéµ[nâíäÔàú¹ä,úIrÔ“õºÎZ­3„u÷„ºKPÙ:pëë!4¤g[tuK{û©œ„êzõw9~v®±¤ÐŠ4‘ËEÉ›pqÓ!†˜õGêfî¥åNwCù0\<`ÊÒZgíR¾ú:5¶[®10ÕÒŠ¿Ý~ô2aÅ% |ý¾’y¦½4!oI]ÒYyFE'ŒÒöê\/ 1‰ºÑ+¢< Áƒ2©ñˆ,Š¦Æ ż=ˆ OBÓÎFñ’Ÿ¸†•^Š®g9‹_´ª¬ÐË ìvèØ˜D¢t@ƒÙ•+ƒzÚE©¨«w¿acyOÉ#m•’‚Ö÷Ë[±qâÒÀCì^ú¢\²y…EˆXVr”]†…‡õJÐ4ÄðA'(GŸæ°!$é%Þq éªÐÞМvDÑ®%c¡ðδxh¤‘6x•rS*Cé$ŸUE»è¨^jù@VÅŠ$—Òª|éµÃ)hÎþA=¢(w€ 00ë?W°NBï(õçE±KÆ3÷báãW™—}H{Ö³žÇüÝå8œCx]þ§l÷¹ë®»¶ý÷sˆ¤rí¸xñ«À{î¹kFÝO9¥œ¿gïb¦ÜEŽŒíè€ûÛuòôN†iŠ­p¾Nëí¡‡Þœ·ýÒ—¾ˆ§ ²§øg?ûIîuþ+­W2õçÅ/zA{éK^èß/X€ ŒZ>òÈ#L º„=øÿ†ã0˜Åе•#ˆ¸«½ßŒ=Îo½í*`¯fJÄ6íùÏ{rOoÿýß5ú?Ž©1νwŠÓ~û½½]Êîà6{Ž_`ÕvJ–9L'ÊT”ûî½78v€Œ.G%ÕØ5Gôö…+Û›ÞòæjOp&EXÁâ]÷<Å~û¶ þr&ÎÉ :“âÔµ+Xoq@¶¼úškBß“öå|÷^ÚJŒO¦…0‡ÚcÇ·~-»6}5ÏO}ê³á…çäyÒ¤É,†¾'÷ž®¼âêöÒ—íËŽAûó1­³@t%Nãí·W?ø¡÷fÚÒ¿ø™åYßf.¼èâöÌgio_!O?²ž½ ÅÞÎŽ½ù¦èÕ¯z#s×ïá›·µ]غÔÃí5]ܺžÜϼŽç>÷YèÆé8刦]õ#Ô~DmõêGÛÃ|‰9»» N¯ršÌÐ!3qÚ¯Š³øDvÛñ¸öºKÛ¶Û<ztvp|ç³S’ÇÔ©S±%æ¼_{KžJW ±A”Ed(çŽLy t˜÷y`Æ7+VX?7ÔnQú3{zº–îÿqôíFW{7Ö]ü¤½ó]ïÈôqìŒl¹Å””Í«¯¾„…ï¯Ævæf­AOSÇѺ`9¼7n.‹×ÏhóçÏo;²æÂãúþÎÚ™]˜v4„yþµŽeçwfAⵡ*§œØ}êÑÄv§Þæ”[ãÏQåÛ5#­ÝÛn»m^Þ 5˜üØóI[‡î¥—ÜÐV­¹£ÝtÓ­ÌíÿåþÅÁLÞÙp ×m)£½^óÀ)50,ùŽ9L‡ò­öZvíÚwßdʘo3~tTuìøà"#¿`[†‘]:i; ÑJ«ƒJq¸mÃ’^$>½¶uÒždyòû1â÷ŽNˆGWÚBÁ^Z‡¢„Bx9ö•øHص“ª×VKÛ1ÊàZÖ«,Î:y,Š\‹ÏºgãL‡r÷ü ó^)ú«a®úûþJPâ|o}˜¤ˆ°>N~¶ã}8·Á1^¼ž¯WŽV{5¾—µÇíaˆÊa¸aý}5L|žžWÄéåëùõtˆJ¼×¾¿÷êa¸4ü™¶ž·ÇŒh£F¦]²“gˆe‡À5M ï“0O`âý sßûÄ|$Õűl,â@3Ö”²XòI»˜ò§\ ­ ÒĶì8zègÈÏ)Å®€Ò‘pÒ2\Úm')øØip€•¬Ð ®>,©'íD×òŒ„À+€ÿ¤YÃö1ÏÐîäóíK}1ã‚Ò—u÷Š’æ…€'2\á‹Xâk¡RäΚéœ#ùS yòSI&°­4 jÉ7Óùq£é&2ôž0Óá«™ªÜàGTÒ©FÅÏÛ GÏåm¤ŠÄÑÕX‚KÚaáÞÄö/,ë”gÓBqBÚ+¿¤[z%Sðetàáé„—|ÕP„¸e`…%º±öÚC z•C/ƒÉ‹B Æc¬•:R¤ò¢wù‹è¯;äÙkΦ1î‹` ×»÷Ë›+º¯0]uõÅL]™•]2.\ÈÂÈ7·½žñFÊÎ`ôúü8ÿo|ãÛY´¹þ<æÀŸÛÑ•Onsz´A¼}á8kýß#áçw&Ó'þçÿ‡?äƒO8N⛯üjþvM)Çíª«.ÞÀ©ã±tÉrFñtïC®ËpêŸÎb¾§àÌ_Ð-·äã`÷¹¨µµ#ptœ´Þ |ÈJçÿk_;Gcæw×bØ ˜Ž_üíG»ÝÇ5_ûúaù@Ô 'ü:àK—>’ë»Þqs¥1MæÝíU¯|}æÛq¸mÞ5ðú %wnï}ß»ÒQøÁ‘? Î1Ç“:=ûÙÏÌóßÿ~aÖ´6w`—GØŽNɯxÅËØÖó}í†ÿ};'ó¶å™ùÚð]w-fËͯ²ûÒ¶A³“`œvÚ:]–PåÆ-2mX[[|×Ý™w}à¯Êó_þò§VÎÿ&ìt:Nï:ƒÝâM ¾÷ý£ã<öÅÏ^Yn¼é õÞÁâ×cÚœ9sØRö´Äyrú‰òœþ‡Ê+çqvØgèô,Ìw öÚk}{;›^ÙÛ¼yó²pU¿8éäì`ãBpôÛn»:ß&8âˆï±s+dZC'@§§;ëž))XÞ‰C§î a>?¼lM>B%Ɖ|pjò¤I,¢=7þqýeèújÞ®ü¯ßš­M§®Í-\è”ʰwm§³Ÿ*áj—è÷uQ€é„(O?§GŠ 4°¡‘ºQ!ûcý{Ãxà÷ëÊûígœ…]ìÆ‡ê¾Ívµ7²¨ù h—ÞÝëÞÐ_üÒ· Ó¤SØ9^öÐmì~3/LO<á—YòÛSNÍó 7^ÁÔ§ÒûÙg“|8öØ—0 èå´îúO‡‚wƒ´ìÍÞò­× ¹`úÑ5 ©wÎb磳ãü»]ëÌ™Ó3zßSvÿo¼‘5³¼ˆ…ëëá†3ˆNÕ⻡þØ…è”ý+³IÁ³Ÿó,ÞÒ]Mù»°m¾Ù,D¤ sªT5mo 3Ÿ£©Q¿ÊO“`û(XôožÄ°¼Ø•ÔHì€<,Î{ŸŒ­v¡Úó’éð3¶p´Ù‚7T t¬`ˆKÛHt|°]7q8uÓøÈWñ†Ú†bšuÃô1”7(C©Û†pBš-+Cyƒ6”7{Æ ËýàØê0¦¬Y§Ç+¯¡–­w(ë%¤zà ÂÛ¢šÂöÏEÛ2 -ëÖÂ_þÊQ?é[N¤5LÙà1Ly AþNV;óòr:šTßL†.t¯G˰aò oî“ÎJ‡iYÄ‘~ÏËúÐ×:º%sô¤Ù¥ý–ÔiUîÄIÇ_‡K:FŽÊ×Ùòë[`àKדּ‰¯FÞZ$±ýŠz“T6‘e—ÿ`Y“öQ‡vÓÆ­«÷CÊkÝ[7z«ë„”Íam†ÆïŸèŒgê›qDèæ3ŒòÛ‡‰å‰˜®÷@Ÿ+ƒ¨ÒQ¢øt=Oe&éœÄ΂Ž<·ÝUxîé°âs3pýd$‰p¶”‹°`‡ÒHÏa*Ì#4âçñ–`¶œü#{—‹5ÈwÜ!òÌ»m~>†eÅ«ìµØ‰´ ŸU«KçÇsB¦i8ûïü$²¯b$uѵ¹sv`Ç3pÖ¾ÆÖ˜΂ū®º2£œÓ¦nÒ¶aÑàÿxÓÞÊ×^·cúÈu™âÑë”=Ìœð¡Cg³«Ë·ÚŒîùä=âŸ{îilgº- N:ÀâÛ¯ãK§_dŸû϶ã~þãöÁ½'k œG=lØFÖwˆÍ|áó_aôòö¯ßƒQÔKx“ð[ø¿'þ¦=Ì‹C5wî\¦fÜp¹ÕhL`>´ 7/¹øÒ¬)xõk^˜Q̵߅•\0û™Ïø½,âÞÇ´1Z½ ‹xÏ<óxè/`W™Ã32îT"§Nxظοõ~Lm:_óýh{.ùõÎw½=__v¡°òo¶Ù¦|ôk!ÓUȾC;æ§Çñ–àà³²·ÿGYã°€øk®¹:v=mÚ´ìærúégðEãe”^G}þ¼ùÈ3´ñÇãò¥g;RÚÉ!úùw8#ÒÿÉÞöCÒ¡8nÛ³¯ý÷ó¦âmo{ k7n§p]œo÷ñ÷ãq¿§ÃñZ¾m0º›&d}{2dHu2}Nùëë›”5C|“ó_ ÞŒi_a‘û\øî—¯ñúm‡GuŸûùw§šË/¿Û{'óÔ7'_néÊÓ²AáÀN­ãò¢£W©¶9˜iO+Ø’µF£³öDâ]=VŽ€”㮸Û9¶ôù& ÿÝaùÖáªNÏ[ìÂöœoÄ®7a·®7óíˆ'Q¶èÜ Æ(ù¶ûô§?3QÛeç'·+®¼û_Ôžü”'3ò~skÔÃÿÞ/ñq®-Ùöt¿ìîcÇßôm¶Ùfyò}¶ þîw¿tZ/ŸÕW ¯ÌÆù¦ÅCÙ«ögÕNeÕ¾Y™š­†7ÝtS:f:ÛM7Ýü†|qx3ÊÚ vWúY¦âí¼ÓžíÊ«þšéxOxÂnÍ©yv`ê¸3üz}X–­ ­Û©íz;/ÇóÃð?ü+•Ž 6ÒV“Qó=²uq*ý’ÙA.òѬ²~·.2å¤uu5!©ëIzÚªª„*þfš#¤€©¡ÊB웟¡ª(bÄet è΄„WÒV›>ØRH7­GÒ;rä`Þ8ÞÔ¶Ý~·¶Ç“^ÁW¯7κ¨L’ýÿü¿®ós ÓènæÍÖOÿ=òPF…­Ñ~8ÿ¾ìÍü]ÏbÈúø :®]ÅR‘IÒÆºk.yÀv**Æ@9±Úļt¶CçuC_E/L?îZ×n»ïýÇX®Ä;MIC«|¥ ‘/é Üò?2¶LšŠux˽Fû½ƒ‰eÁò&à#GdânV—¦™¨ô:5;Ï”wq’&Ű_ä —@` ï×H¨|°JÏÀ:Y•'S•’²ÉU¿0zÒßS ã=§\f@&hò³œ’‚_z¡õ\|ˆsüg’¨ô=®8 ”J£îâËÙ2ÿ8sY”„ù—?ÕSAàXy ÏÆ b Ð?C]ÔÊŒ°2Ž‚ )qÅà$ï {:ú&• HŽ|WÏ“æÐÆ!a¸ò™é(Kdƒ²é·÷iãa˜- éy“{ΰ-¥j2‹=ŒmîRY¤¡\¤ N@ûÈÈ«ŒKÝ•LJç½1Ò,ðpƒFôðä¯\OXþ¸íŽ¢Qt RÒ2êÊsž ^¯¹*wSh扉VÌQ£Ü¥ãnœœóß½µ¯ºúïìâ²1‹5׆’SòÒÓy>|SLF3j£i°EàDFŸï¦âYC>3Óqœü»ßÐæà8;ºÆéµY³7aÄmfÛGÔÆôì³ÿ”¹ðö†§M›ã³ÜYl]¸Ü¿"#sœÍwt±rå0>õ#…émî?‚59²#uo;èƒí[ßþ2€§å«Ä\p!NÇÍ8´«ÒÈϘ13Nðoû›àûEÚ?žqó•ÙY„/Ö>´t ÕÂök¿9f´Ëé¤üð¨oóeÙMÛ#‡–³ Nágüši s¶ žÅ‚: 'ýâ—ìAÿdžŽ³8Ú÷¶¹s7öf:gÐPŽgj óÙgM$wµ§?ãq~ò㟶‹.º€Å¢Sµaw‘e+¡=3˜ /¼štŽc*ÎÆÙfÐ=òÍÑ+ÙýC>Ň´ŽF†mÛ¢;–’“éÇô›ÑÌ™Ÿ™ÆÙż¾ÕxÉK^D¾ÌÓuþùç!#ßT˜1›-?ïá{ûƒ3gx+p¦r?"ûÛ¿ç݇0Ú~pîhô õÒ¸¼µÙ|óMqž6Å6lþå¯8ê·#ûx:GÒñ¸§ŽoU¦NC‡ç>ì3øÄlì`2NÌÜêK.¹¤½ü/a^ô¢¶%_|µ“tä‘ÇBgz;ëìSÚ-7/̇¼fLŸýº'üñ?ÿE¾Ê;tð¬ö(ðË—ÀöÎD/[ ÈsöÙfôþF½kÙªõ"Òü¯öv6ööR¿"Ê;-ã ³·“O>g}oV¦EãÆO‡ëðÃH‡d»í¶êÒçÛMx#°¬ýí’ËqÚ¯ÃÛZæÚc¢)+–AË–äU«œä±,†þQž'°ë[»ú¡3;dK—.£Óò{¾0ýð– × ¶„½;åà^¹Ÿ~Æ9ùâî AìvµŠÚ›‘u™¡~ϵüF€7*2ý…Žñ5LÙZ»v, t­1ªÌZX•Xïè3›/cÆlD9’/q›oÚ§Ý+ÅΚœiù(ל=OBÎ øÅ‘±óÒ)K£GÁ¾oj‡|øèðxìn{Þ–=Œ/aà.ô?ƒ2@ÎɾË[®Žj¿ýݱÐZŽ=ÌæÃmÓ™4ŽÒwçcd_ùêaäçN”»™Â0^ÃÈã‹è(^NGel5?²ü6eò4ê¡åŒêŸKgy£¨È΂çG¹{ÃÛ›;©?þB:†µ©oB½qtÊÕ,¾#²1kf̃%l»ûïÉ75>ƶ¡[ð&j%r.ióo»Û›žo ø…ó;î¸7pW³þakê…¿· þr~t‡ŠÈ·žkV»[Õøö÷+ΣþvFÇb‹Þ×°x[¥sà78(0e#H•6‰ ©:ÝŒ¡.OƒŽîÍ^ÛbÓî oüÿ©ë¿òÿ¿J÷£ó¿Å÷|ÿïÂýŸÅïùõWðF°-óÄ&Röfµ¹›mÑnøÇ-”÷•Ùla-u‚‡¨„þSÙ%ö/+©@|Ù JY½§í´ónLm{!ƒ>óÛñÇÿß`*eåѼ}¡½Äžµ;î¡§»*+ÕËæú5%ÂÄè¨'µo7†‰íåÉr®„nh2¶¬óŸ­ÓÁ“\¿¼ªlu²teÆ2U~¥üü)W13Nò5í§“Ç¨nÍ•~Hô£¼Ñq ôe3aÊ ŠxÝ]ÒC á=ìOáƒÃIZq”oZò„§aêA’;=¸–3 ¼¬¯ö¯À`•Ô8¯È¥+q¡¦äò«SÙ.”%"‰‚Bd«Ô¦\FšPi·51 ƒtcX?×[ÉÀ¼rǃìtÑ`׃ÍÞ˜8Ý Í˜9šÎÅ¿OËFà¨ç{XΜûÑ8nׇפI[áP®Ä‘‰¾Æ2Ïû’86~3`ÓMç3Ge&£k~(ˆî¦7sæXòåêà×iGÙ„Í™½Îÿ2>ÍáâL #Í•¾ Ü8òÿ@î6ž¼u®wß[pÓ§oC>—œ‰Xï4gÎNŒÔ/%ýæ;»ñ¦¥µ»ÖƒØ˜{*ßVóاLÚ†t.g!- xy«2eÊplCøšO¿"·.}x Îü§Œ¢S±~º×aMæM„ß xpÉê6v”vèäuúpßÿÑtF'°óÎÂ…×®C|ÜÝýÉ|oà!œJ_ù¢ÁdÁïÿÞ<½ÿþ›Baʤ­±Á5èáÖ<½i[³Šrl=hÆYF([UAFùw}À¶°ɼé;ï,Û.qfq¹½nsžC<¶Ýy×ÒØÈÝÙ׈MÚ¶Ï\¾|5òZ÷Øa Ö”§.ø6tŪù¡2q–™~„Ùòo`¼õyµš7cÇ Î(!ï(‚3™|{ÅËÚ¹ëþ‰ŽW¬™—¸)3wßý0Ñ‘±Í{î)]$r½“ûêß¾À©2”UÒûà’Ûr¿d:ŽÃ¨ÄÝ~û¿×õÜ9;’oQ.u6vwÐ'm´ ¼Ýà­æÄ‰#Ú½]~´6‰7‰1Íh¥šgšÕ(ì­ìzäÈÍqÌÿIþ²©í¤ÿÑfÍÚ!‹¤uhÆŒÔñ­5'®é¹ð¿²èÿ½ɘ2e«v?å4oNÁ_Åô°m·Î›¸KÛSŸ²œ$8+o|srËÍÄÆ´ ²À ùPuò&ùbfi;æ˜y«É7x2÷ˆ!žÀ&K0“ZÙÆùÔ[l±Úg©x€Jµ*ŽûÀsågÝ.q0ä¢egN±ÍNaí¸Óví¯|1Ï£±ëõ¦†òÿúÿ’|ç^zÉe|¡û8òÌ’­ló¦½ÄA'«ÍmýŽT_XAþc}Ô-›LÍ»kÚë_ëY¾ÇÇÏe»ì}ØäaÇvï=@Ÿ£¡‚ÍP>c§:äúD*‡Ø'mÏC¸òYô‘CжÄQpŽøLú†õ`ñåÝÀI‰Ä‡¬ÒIÛ ¡,k–Á(hŽ‘ø“ò¡\!dïÏ•ßVeSùÐeRzþY"…hª_à Y ¦†‹O¸2p¸¤m}ã|ƒr8@®øi囃+"0…_ô ÌŽ˜ªl|€œÏŠãx–;ˆÓÀkß6ª ¥¶_Vü»m‰d“Þ*)ªàYþ Wž;µ†”€cŒJƒ¡Î¯‰Ì Ë?ðµÄW9b.8îÞIÛ£ð½‡Z€DÙ„%%+E y(²–²@‘`Œ²25@`™&»?ï}ƒÃ‡ÔsOº… L¢ ’ pÓ¢Ú¨¯^¢R%ÔP%I&!¼FYŠ[úG½‰)tè©Ëÿƒ½7þ-- üNߥ÷}¥iº›}§Q‘   ¢h¢‰Ù'™2©dæÌ™8U“JÍ$5SS©©‰3ê2:ÁQ\0ŠŠ( "»ˆ44½oôJ/÷Þ¾·ïí%ŸÏç9çw/M«€S'œßïû=ïyßgžw=ï9ßFÀK-££I"®¶”°ä&±É*ˆ·–,ÆÉ Ú•D¡hùÚlWÛ(Fc¬‡×;—\h_ùFŒáo±ð­'§°åãœóžËªüÞ€á¶:NVÛ/8ÿ xîæNÀ©üBíaG{÷<²ÜxÃ}m‹q?â 7ÜÀž»¬T?á¢3Á;´|áöȲ•ãŽÛ839‘‡^t(Žƒ(Š¢&‹‹.zû¦yÅâ½û»pñÎbPæ^ãC  xý!­ÜanwÞqÛýL(žÙT}‘w±ŸÂ*Éw>°œ}æÓŒÏ`{oKpA•õÇý˜¬\ü¼Œpë­ @(ÈüÚ N $öÕŠq]ž#Ë9Ü 9ûœ3ßr?ös"£Ž»°Ëã/|V´¿À>ás¸ÜΠŽEÝå[_ô:¶´<{ùu~ÜËãiO}*«+÷ô°¨»¹ và û²™¶¸Ö:üàµÁ25`Òp+¼.7aË]îeÛ̓øÅ;—^úîZâÝù÷r—åô9£ÕÏ={×kïÂÚÕ‰›@_Wùä'_Πñ~O_änÇɬÕöß<ä ÙIêAHgœ~ vd«Òöõ¶ãŽs‰…ûîãÁYhÀ¾S߯½÷ø]Èp>aïKy¦ánüþ@4οàôåÞ»²’2Û¤.dRÇÛnîf ~ïr AípÝõNZäuŒ§.gžs?µo¹Ÿ;2'óùAàÏ8ýTV¼žÉ¤]ÝúÁ6 Æ·ðÚGœ<‡øØË’Á÷¡C¬&ŸLñÆïJ|áb‡®µÌüÄÝ“/{~¿âzãòd:rñ9øcöx;HÜÞ]w1ÐÄxÞq…ÿf~ Î»C¶7îKµ^ÚæN%¢î®v¸ï>~]úÈË9á…›€¹ÏÖ—u7Í­n·~Á»k'V§Îc¢vû«ï¸c_m}èÕÞa÷Úâä0ÚÊ_›9Ù¸ƒX;Ž˜pÁVÆÖbÚ-Û޽τçÀ¾‡¨Ÿg00~ü²zëà×^Úe·V²ëEÈ‹YÍ÷7n§£÷!ô/ÞÅo1œº—׈¾8#Vîð· ö,—^B,òƑ믻»®oàPßóXUwËÚmwÜ·œÂÄîAÚž}äG—a7Ýäd7¼Î"Îvç7cê |O9ùTls±{ÉÄ¡ö‡Ûb:8ȩ̈ò°ú‰øã å<ŸÁ`Dz;šd?ßî&&îouÐÕù»nŸ÷Áë›o–ç#Üi9›‰Æñ\ïï­M'ž°«úÖYgòæ§K–+Ùbæ‰Ë.½x}÷d?ݽÐw°c?z:wûü{ü­¿ý7;ÿÄO¼¥ó‡‹!B™cÚÛÚdÃ$©Kä«^ÒaÍ1ÝÓ jlxz5b4ìˈ4bAüµ·!.¥#îô¯´ôþLÏydý XNŒíψ4kê!íÖËݧ¿ŸxñnÊ«_ó2IN¡Ý§ ~,)ùÆñõgïJ;žxÖsž¹¼èůZ>ò¡O°¢}¥~#ÖpqMÐ>žà Ó|Òm#Üqðš?@8&:œ š‡øô’‚ð­´;´7[É‘ò|Û–­^£ÙZ2c™‘I*Žï|88â%«b­„ýÔZ»XDZ7¦g¤8ʤªfŽ]›ÅõL" Ó€N™§NȲq3íê6Væ!*ÌšoDŸUÆí9Ô‡<á; ¼ŽÑÈÙ´¨Ë­Ñ”=õ]ÚËØNÄE\Úc&*0‚Öß„II 3$¢ÙRÎŒÁ)…›Ñh )I0»hd åŒLPÒÁÞ:Ââ j•–Üg¥Úk’„ƒÆ$›òôèÔCé’M)£6is1º #›vÕv„Ú“2ýsö¤QÓ5TÒò!= 0½78a€Ayƒ3-<€Ý ‚u&»Ó0£Ó4¾¿”Q ¨¿ù~—8J°ÛV«â¬±°}™ÒY3³©´t¤ú†å8øU H«wÜÃW¿‚Jã§ÑÈú ‚yø$7JRºhY¡= &bØs^)Å£ ÍÖTFw20°R9@”‚¯œ¼“„ƒ„óÏ=­NÜÖkv˜·Ü²Á‡«Ô'#êIë á>|Â*ƒ1'KÒÚ·Ÿ7ª0È‹(U¸ð*;{ËO=íxV—y§7w$a•Ê[ñ¼ø°Aǃäï‚Ö]wÝO ìyL˜¬ìâívâ®â6©A~Ÿ…ðÍPUXlv'ƒL»»ì©©}ý#¶Í¦(º»ÉóD^SyÞ9§Öp¦±½öš»²ƒjC4wß}8Ùá) š¯ä}ýÿoDzE¯ø¼óŽ;y¨ÏHøÞtÞ‚”ÙÃØØÃdës>(æŠê‰'=§2·•xgæð2ð×Cl10ÞüÑ7ÜËfíYíó“„û’gï^ô@?·€hÜ{y…¡UÛýË×\ýEö‰j§sóÃð•ô÷oô8˜T'H¼[žWÉxwà ÈöÀo6Æs~ó„ïNࡺ/2º—ÜéÜprá–ˆk¯½»&Ê¡: ™­—½= ²z±ÜxÝ}Ü Ø“}Ý‚fgv58x »0HÅV»±¯“€‡•óÎ;º&Ÿ“ã™||IŠzçh7ñæ¶¡ë a;a­IѬÐU.×\{/ƒâÝÅ.`À{U÷#ð;n¹ýNb9{­*¨·ð[  {-!€²‡U[ªí®]]‡õ ƒø0Þa¶ë\{Ý=ÜIÙÍÝ^-züžæ|9pÿÙ_Þð¬Ïï½I-“ImS‹cϸ€X—ák}µÓ•ïõøÝöƇç!9Ê jÛÉ:i Šyk$ô™+0aåMY ÿrHdÓw0‘€·6”µõO{_e¬ðë¸Ö¿Gh¼ÿþ¨&îãê£[æNbà>¾ñ÷w/¶tr¨N!FÏ¥^'wßíö›‡¨Ã¶Ç£—Û›n¿õ@ºàêðÁþLF¦Lh~ ýíû¸;¤½%‰üãðË®»æîå$îð{Þ™µIÆî­Ðw‹–~4¦ö7N0žp‘á±Þë¯_¾…‡³=®¾æV&ñgd_ÛBçÂÉä¡Ã'.?ñã?¹<›Öå—¿€­GïàîÞo/—põVîŒãÖRÊè§ùÏÖún¡¦À2œÌ5“;Ñ‚6¢˜^o¤TwÚPÃ{-h±GùÂìÄøÀÚîc!`] 4»9f½.2ŸÀݘ‡îdõÿ™ü:÷YÕ³ì ¨l¿q|ýZÀç¸üñ»'?å&¿O]~Ürq¡Ú)bÌ©=!QO{?‘hQ$Z"ØÎaœ®EÅ€[½í?m_¤»í÷uëÓЊ*'Ú÷"Ø/(±HÒržÓÈwêbwV*ÕúÇkF»‘<1<ƒk©øG)_[ 2À!p]4iìK¿äµÖ™òD“üê²ÊgFcÒ°ÉôÕG]_4(H*ÒÁ$„@¬øÖÒÕA:j A«XRÛÒã‘Å—“€“ÝÃVrwPìÐKÿm8šžͪ€Ìü×8x1(ËÖXd8¨ôZ¸˜ JJ áÕ/Çb¬iÔçu-]aPT9N;[§ªäs4hö’cdÇd N)F3¡lÑQùHŸ4€ªQ/„þICÐ8pPº…Ô4½”ŒÕÁ'm&2WIÍfwr’ ÈŸ=w l®ßè’-µÝèºUŠ&)Aü#oj˜¤—À­d:˜"(²µµî[~[°lúJ¯a÷‰]h9‹-–,XE=nµyƒÉ'OØc»UŸr(/a¹M"£ïíÐêdÔ•\`|°v?t'vÀúRi嵇·!t/+Åʦ>šÉUÂâŠRWU½5N0ða#¬zÙÁ{›|?ûä z°³"³x§Ë–C Ž…WÏ®N@ž^ ®ÖcwC<¥¨pÙ#%'xn¿)_šÚ?ˤ9é¹#á$ãnVOµå>h¹1`)ÆÃNqż7Šžv?6ö-í/ÿ{ïq-Û£ØöÀj¿ƒÖXƒ+=i9`òEÚ÷2¨qd‰6u¢p<{rõÁ&•úVÜÿýšTÃíbãòݧaðMh¯‡°µ6ßÀøûË2m%ÌÛü0wÆnÌ|×½uÈÕo†ËÕ©SÔ¯µnS —ú×_Te¤þ“0upÄ©à:ðÃÔé.öÛÀùKÑw߇ñ¸¦´­.Â;ÈTí¬ïXr(ómß­''0jŒ/È jlµ³>’ž96É»÷øÃYþ¢ñÔ-í¼ÇÀ‚àüWۉȌ6ÕÛ»ªàŠÎ—Â`£äR_Áz%4±5êþ;|M#õÞúo~áè¤Ow‡Äã¥úÚé ´ðþww[¹[ó}ƒ Y‰¤-ŒÛ.#8rØ\‘5ÊÔe檆 c[J‹J>e"[?‰Ýš£Q'z¦h{æ“#ë fñ®ÂL|ˆ}xh?N´÷ó*RI+f çûð`"øwêì¾­ÃÞÅR¬ž†.=Ë¥2Æ·2Z'µG°\iÀ°ÏÐêM|űD°N£Ì~P?ÙŽ‡]_ƒë/®,.ÖX÷¾q|ý[À:äë²= qWœkS ?føß:V¬áÛyQ,³«Rë÷D©ø3@·õ“ž‘kîÄpôÅtwùrªœª½áªÐ&ßã²mÊÖõ5¾¤\ßµuFIŒI9÷àM^µˆÜ ××#|$(žX)hóŸlƒm°eQ =­¸ãCDM°ª¸”Ïí7«6±´ U&æ˱à.;Q׊㪔ëÃäsŒoDW_HD:~ññÏŸ—]/Ì‹¦iÊv3Ð0¯ µÇ"ž6è3ý*5‰(YwF¶˜r`¯ô×H!­Þ¤;n:î*öª£6+f‹d³l«?j W/ofKÊé§>mùÿàòùGÐe™ã"Þäö(·£èE(ȲTÏ¡`&R·ë¸½äsmyFú å+¶£¡ê–xÚw¦Òuõ0í´þiö:}@¤ µlEÌ=0,Ï–ÆÜ# £AþÑR#L#iž3Ÿ/rEl«UF)›ñÚJ­£v®m/¬'s§P}´¡«ßÌ#uçBXú¼ÕnàÜ#+yå[v¾¹@ˆn ‹imZC.ý¶:­úž¸°ˆ bŒ ‡*L‘ L½BQ¤œ¶a{¸~˜UuÛã¶^=º ˜í”™tmrµÆ®kô.˜8)¬^ /[.A%Fñ”~^B'i©‰d¼š‰kmÕV[ïàÚ†G.Ä‹¾©b†»MbLg Ž0 KfଫʸÇX@f§á 4[w‚:“~|0Ô9…ꆒQ¶éÑ'8i›(c;¡W$«G%E&ùq;²óm¦Õ£lªôÐÁ5 Þœ=‡ìž³ûïÜ;ðNfw ëÛ÷p÷àÈ‘Kx•ñëd±÷<Ô=ÜIð. ·)ä—ü:ÁPç¨n`›i㑚ڧÅS8¼cô{8)LC@ ûÁ|_©:ÍF»óÞi NPhDþ¢æEãXÒÕ=|@°=!î8}£[–Ø~U¶:iÔ7|¾q|}[@Y‡N?ãtR'2!§vA¦H‡ÄLmõÇ?cÊÕ{˧'8F¿G»›“öN þÔ}¢Ç¶Àö£Ð5Ž)¿Ø3ÈH•k¹—ªÕ±ö^¸.,°ZÌy¢–¦<ÛÈä­Ý–zWý‘v4¨-ÕkºMé…±ý»yŽ ìå¤ðS·¬¶?–X¯J÷àqŽB¥‰œpðì\žI:YÉ£æêj[k0˜±å#Ô¹ø¹´©gú‘ÅH+¾icð™ò8æTæFuê#!«²tló@º‘„0«À« ‘] KÇÐâÏÐYCŒ`ÐÓ†œê¤mÜ4èJ‡„èáçË$ÄOØ„ãrÂMø~ YÁ)€1Ÿ2L Ê‹«•n‰¬1ÝC#®·í5RÞä’cL§³Kœ´ÒêD‚zV@À µUX¦èà¥Q¦Žªd: v†$üpO^K|‘É@ÀN6¤ókÁcƒãZâv0¢}8É{¨›P‰UÿÓ©‰—‰ &0C&rº ‚rB«I¨P«Šs=6F>ù G:v6ÚpxÍÖ0s„ _"ùZÉF6K-DBI‚[Lj ã›×"”4˜Å{X:Á;„€‚É7Zå+‰Ò¬&[±„ñZ~&gÀ9¢V<Ó•…t„›_f´TD€9±Aat&j†®iiìØÕé¼6I7¥•>Ub¥zÏ®Çqâ:òxS¿në;Äݯ 8‡±¢ßIzͧIë£d·zž €©ñ³ú°ƒ¯êŒ>.ŠÄâL{dékQ±š*Øü$W6j@(¢"Nð^KM^ÃS)¹ª1%ŒÌ&žôÏ@ó]c²Ù¿ã?-^y:ÕE ­EƒðV…#r˜j‚ £`á:ÀÍ$QSG?C'01œÔ‹Œv:¤24G­4¬íüŒƒÄ§ë“pD3´v#iGæöMîö¦­%ćLë¹g¾ÓièCeõ¥<Œ•´Ñ”¶Ò rÃoö²ä@š\iê×ø$P¦Ü|I7ÿb>YuwjF‹²ígKÞ¥—ž½\{íMl#üb?ÚJ[´¼kæ6@A} Ý–ï¿ß‡áo㹂gñ ŠwÈ/»O&bWåPKÛ¦Iiøx›¯\Ó?Sˆ~³vO7n~#gj#óÜÙÿHY;jngôÚ¶ÓxÑ¿Å2t´©–´k,Ž0F°¤Š%C¿;.~xÁÆ‘óÎ? Ïrø0‹6øÇ×·¬ôkgò\ËîãÇ]áÃls<¡;SFKœùÙ: O­ÜY¬¾=£ŸAågóºþ'޼µ¸2Æ¢õ¶šR²E áeèS('m[)»à9[¬òJ[H'ñFv÷ô×3¾Y¡áá2@2°cÜ¥ƹw Moµ¤qéš'ŠPƸúNÚÜu"Ý¥²lV €8ž6xõ°¿Ó|pæSmâzx§ie×vÈTèp¶â®Ç´uBIsÚS%ˆ<5ߨ&dÒòM•2 ZŽIkDÍ¡ƒdÓª+E±to±j3q €xŽ‚^*R䭱┑c'1šBHZ~àT@ü5×”M‡8Êê,|8Ë*CÓ¸i²§T•W„H§ŠGÖÈ©®ŒÖ$k4V¨ë5wdIËÁr5zu2E±ŠÌpÄ(Í…dÕy$Ü*#eu8+Åf¤`®B%ëxAYÞR*%+*&\+Em-nïÌ…>xHûÃËUm\Û«¯Ò©ÇìñTí*[B°AŒåæéG¤wëX®¸u®âsÌ¡1=ÔõuʲõüÒÄâ |´Óî Ê·úVÍ\AÐn5ñø!½Á·7s5[WlâÈ­ÍÊê¨O 7´)Åx¼–$ÆÐœçQ„Y?æ­r 0üõ‰ñ¤]&ÚH?úžo…XÅ´xUž<åS?2k1„Û±;9a³ú‰ì{>ãŒçòàéCËn>ȶ„áÛ*€¼á3ñ1K)Ïgà,¯þÀÉ6¶Uí4ü‡F«0ASˆÎù eõ§4fj‰„ÙC¹#­c}ÿôמ×Jˆò‹+ùÒ”r+Ǥ½®¸K¸ ‡YÅáè¤çð¥E R*›Ã¨px»ð-¿êŒ– ¥oÃ]Ü$Q%æöII )Wåªñ›²­1_‘Œ “[ À c:5Q$íµ{\ÁצrÚp™/³åW)yIõr›©þ桟øÔ7Ÿù±hô¦ äÞôBžPòJo®½{z¶Ûál^ ɱ´¥>uk$ˆL¶N“\¬ãR,¡Éò:š”M34ý‘›Ä¯­—>ÝC™ûA+€Å…më@[\ôQ”BEÄ·Þé®Êà–¯ú°G=B€ž$sYJsÊéÁ[¤­‰¼òŽ4Ù±¥nÁ𓟠Ýk®¾†ýï<”Îáï:œ|"möí gò\;ϪœÍÃèð É<7³{Ýžg}Ö'´çê5z*—mÊzÊU9y]p%ûUßì öCè=qjaH‘®—dB…ƒ¯l®ýàT|(‹Eä š$<€Ã+» Û=?1ìÖÍ=lÇâÎ7w›Ž!%ùãëØÆ…þòùª‡\-ǯ3fšàÚútãoê›qR„K šÕÏ,?asqcJ\c3qfØÕÖLPƒA Ï:cÛ"Á¢<{'½n›1´„q§]~ Ê·˜O#¹øÛ–@MøjB/Žò 2E›>…¦>R–ÒÐ&IŽ¡¥ Ê3(ù‚¨¤´¯í~-gqj;ä%j|d)¶Dµƒ¼¼€kè÷ì€0kÙÀS_“-0Q¸–ÊÐ’…yë×´c\;¨7å5¶£¤ø Pa¤ %6rê)‚׺Ça‚Y62vZ“²Z6æ n™ê‡<ø ß‘‘FŠY¥p0…ÙøtQ^v¨ÒÛ˜k ÉÔˆ¯òKOÃø_ûq¨‹ØÊ•,zí ‰xÈ<†v ‘œò°\”ˆh`ÓR‘¶”ü3)¬úiõò ”Ѻú3˜jƒ¼t«%:ÕL©H?~#³2(ÙL™< Ë`â)~l%jÅAJñùh¯ (–®TÇwòãM¢^¬vUˆì‰Tc 9Äæ¡M!ížòÑ?¦È Í(S5™ÒꫬšúÄ»FV²È'Mv(uV„ Û‘£Ï`aP ¿Õ2q•GzðN¬É´tò°ÃèÉu²(8ñ “|.lóù¿Œ|†ZõQYmW ]äð•Â’&xÚNY0„7Ži ´—’Éϸä¨MñRhtã–ÝažQðøäŸæ7;^±¼ô%ßÁ/¿s¹à¼góª`ž9Q/èÐ ôÀ³¯¦¥Š³Jni õÍ‚$¬a-Ʋ~¯áSà$Ty«‘¦œlñk $ký¶þÊCÚàζD½7úæ'ýÔÝ«ðΨ>FVÁùkQE_ðß Í(A<3ã_Ù|ãøú´ÀŽÏjô/‡_%ð§AbûáÉ6ßÃúe]3¾œ¹4chñM 96TøYÎá¿Ø3YÌpe¼KäZÕiŒ@“ Œ8ÊAžµØ*£\F¹²Y›¼/ÞVF®«ý€'¸™Q…‡0Äðªéô=¢ÅK ÑWÿ¡¯„±Îw÷ HiX<¬ÇÃS‚A"_¢JU}75ô#®€Ã<{´]U[Š­èâudB\ÊÉS„á?<õÅéºç }V›„²!BžqèÚ‘l†’ßJJ$JHBi䑈2VXþ´”Rñ?À|ƒI²¦4¯/t‡› 5;ƒ.¡‘gxŽóWîÐÜ8r–„bÄ ™š§3*”ÄT,ïÞ‰§Ìƒ²ƒW¾å6ŒvF Ô%Òá%-,"¦nY­&ÍøH®ÒÙtÍŒ|©ÈuŒÊK`lƒ~RªóJX© ÏÇ`R…ä« ª=ä³ÑSeF.ÿº\“xdWóí $Yç¥Å˜(5á€þ €á €Ê_’¬+ð §D &DŒôøl*³‘ݪ!oíÛ¹˜—~dJEZé±Ò‹ð²ð’[-ß™»P3K ãÓs+›,cZ™FÄÑòø˜—üj LÊC&Çð4aùVè„'(ùr°°É°q4† åÏÍKdšɵ J–°a€‹`êàÑA†q£‰3"‘ˆß|Рº„®’µPxmËDZ@IDATþd,RHs £Jűþks}ÍŸÛf‚à0;XãV¨¡¹Åht€¨®$Å0ÎÞRb°"=áõ‹rÈkê¼Xföæo¦uÈì|möƒ„ê5ªóàrò,0•¡·&7ßÈSê³GHu¯åV2ƒL}‹§ ‘ƒ¼ÁG»P7µÍs½[(] Йç—Vy¤ëˆ£Ž•Sl­m±ßù’Œ1áLHô2ž¸ “ÉÇgº„ïƒ.ù£À,™líR¾Õ¾Âƒ§VRëÛ•xîb¬…i¾ùÄ<ívxExòc:-ß Xÿ00s×Èýá ¾ÚUMµ7XÑ!ÙÝ=7O…8”¯öH||aafù:ðÛXœvUºV iGRœä§]ìè; yp+Ì~ÙZZ†Àõ‹”g íˆ}}¨ki¿ñï_ü•è׿áÕMÎåw8p2>iâµv¢:M¾ŒÅ¹:/õ¡9.S(3Dû.½ÒØëx¯±;åêJ!|·v§;›\YîßÔGëm²z ŽÌoëœYÒ¯â Á2(s¡'P—d^íá…èý†ö«ÏWYf¥·ª€Åá/)ÉËε䡟}ÅòµoÀ­νó‘B˜ŒŠîÚ¶³‰<£_®ŽA¨•.·£rH¦¨À2¤¿5ƒ-o    ©’ä`¬(rí%2>4•``‡ÁêÔ¦¼•O IYZ…&ÏFÖT¤DÕŽ&»´:q-y#Æ£:òAó(k†‰­LG„Tv²L'ñ•QîBYŠdïö÷ÆWA:ÖŒõÊ Wc,]A´g?#ïÀò«Qœl¨³ÿê/?"À¿À2|†VÈùf@´]ÕPºÒ‚w²ü–å—|ãp¤m\àxŸjêÐÐq€<j5Yn+}ê:“Oy« ÿO (wòÄ;ÇKI%”~Y¿¤~5Û’×N¬!Ÿj/Ÿå‚h°U)lzÖ!…ô\ˆÑ>~©€«G Ú?r×çf ìnjpµ‰2{&G9Ìöðd^YÖX>ÚXLi%¯˜pYñ#£cEÚ%NhÀ¦ RbÓF+ùÕ›‡æV~ñÑ€Ä gê:6‹?Ù¾Ú¸ ãÁ)V”%¯yéŵyÀ—3ø[uÕ>4ÔÆtÖÓ&Ø\ý´•~Ê^k£¥<ä Ìœ!Ï"W½´ ¼:¯Z×y%)§‚³-}ÈkÝlOhpÉ>â \}R¡l³r+?êd¹A9rÑá£[SaôVÞ9L[.¾z¹·Y_í@îfCÓÒê<%ÃGú\+ƒþŸE(ª¤ÝÚ$ýÁu4ÐæMBb눣ñ­«C‘6-ž†,#Z&å5ÏW µü•{„ ¸¯ºU<ÝX hoóä·Ñ3æL+ç‘™,R™]|Ó6*㿯žaZñP<}’Œâð¡ {m”µk×ñ v{€_¢ð¶<ÚàMv­cÀOKú—¦0ðÀÞ½Oäu²ŸàuŸ÷.—^z©ÂñzØýý~€¯³U'½]\*)ièïc¹¿Q høÐ 9eq}Žã-nMö¥Åþ¤“ ÍÎ0 HRfÓ&AhùÅ6eì"xmó l<ÇÖ}[¿à!\~Tn#Ö|&ƒ ;ú|jþ7ޝk P_lfÒ¦ãùàÏ|¬‹sIèÓb€/p¶z~⧠ͰÉ~^4aMÖÉGGËÅÙ˜Ÿ6 õ£:m¾td$Îp´_Ë„v$Êm絡õÉô¨µê(ôГÃ~%úÖñ1‚|§Ý31ݲôÈöäp$àovæ%a¥æ­Ó6ÊГ5bÓ"ÊeŒë9\…‚±V””·]ø|ÕLAŽQS®…¥Q7a‡Žo£¦7:]Õ´¡Q^ƒÅÆØ™^[ŽäIiÿ¢7‹4Ù4 eRà:ñÐI® ¨È S6p£³xÈèGx«äºjG­†ôʨŸ¨|5à‚¿~nÀ Y­8Ù4¨™Çæõ ª r"6rÍÀ`صRo…Ùô’ˆiÊfpddeE©[Ê7gôI^evø#ó¬nôh4ôBÞ¾F¸¹Ré2æ¢@2“ë*™él1iù‡$‰ äÑNÊF²iªý•U8ý5³||º½ÚÓÏ\ëdI[áveÜ ’·×Ð÷ªh*\NuhÄß80²WY@—„xæIm|bÎÄ€bŒNƒ—¾«íãc!™±å¦«ÄÀ(¿q"Š+£óF‰šPL¶+ä=V¡ÖrOÄ*°Fl<¤­¿†@2ø:ÅÐn%½ÕMGýh™×m³á<ÛP󅙆ݻD4ˉ‡w9[ú0ÀÚãsô“´•0¯•½Åñ8V^{uöØô©‘•=+š).<‚Ä/^«ýÙ¡·‡e6¨v0Qæk¼6eÖù1ŒúY¡õN“6]aÕ;dù“™WÐkš}¯Æ?id,©3m®õOž À)T;f}68›ìê+kð¦rP.ßÊŸ"Z”€–(Üz”âkhHÛE \m¬IÔs"iQuÚÒׇӑÈuPë„_6á+·òðW{§[dR™SÂi¶ŒŒ^Ýù3ˆzÇ®Nî´{àÏñ‡õV²ð¿kQÞ ¯‡.›Voì©¿ºsa?—.Ò"š Reƒ’sYJA¥ÃŸ)Wûl¥ýѲK.=k¹úêeùèG>¾¼ôeߺ¼ô¥o@œ‡yÏÿ§øq³“yU-4 ³k}µŸüôÕ ¼²-T=™ÚsmÔÀ(Énä /ªéeç0)Œ44»ýNöW`íoµÕØÅ^ÇC?ÀÛrÓ”zA^-ÔØï*vsÓßl· ';ÓgÃC¿eGò„þÆñõmëÏzî°¯4F 2f±‡å…N±¼}’q;XñM‰Ôâl­ŸÿFF­•qŸú[ˆÖÓä‹2 ®êS3+³0FQ¦¬ÂPT»T[:ôF/ êøÀO†‘]•­SÕ7ò£LÝmkªõù-_ÅNêMŽ62aÖº©¬Òƒî´Ã1àZRGÀ鿼Nƒ’ï›$­‡â¿ú#ÉT D#‹ý¼m°¶¿ÕsÚ¦t±g&äC*ké[Ê¥±‡6“à Ϥ@TD·}A¡ø× | ÿNkC!óˆH0Z*&q;ܸ ¾òÑ!0 šA9 Ûþ`‰À 4œZcÌIÙ&ÜFð½–†¥µwâÙð×<‘m>y5“98BৈèÔŠ™û(‚—A´1!eû m žä¥·0\|pŒ1 ÉSOù­(£жê,k2¤A ”¦L¹"Ûd‡2áÅC+Ç\C¦8’ð!(Æ^Ë“kós¾$융 Z) v4HÀ”›2aÌçn®â)#לRÕoªãÕNênöÆÛ"l”?eVÃÆf; ̲@x JYÔp&c¤ š„G[(¯ƒ•Ç*“X­F”G®²kƒ›-HÇüê>Uc+¿ô¹ð?ø‘/%¢åé¢cžò&|1—¯­á8´v+ÚП7šX¢OÜ»’*ùq²…*mƒ(ÔÅbPÛ™üBNæ›/ÒŽš†h!FþÐ}4%M$¹MLõ¡¸|EÝ”Rþ$L׿a¸âÛkñýâ‡IkCq¡~Ê\¬tÇwÙÈ2øÓi’” ì,ó¹ÅƒÊwÜV†Ø AFÈ -D0`Ý’æûÅl7KNS}p­ûsM¬¨4ŠH#- l»`žÊgó¼®2ëoË×와¬ìÉW—…°róPŸãø!œ&D«Ÿ‹ÕèKH-4ºÿ#‡œôÜÐPN>08ýô“ü_¹üàþåU¯~y¯}ë[ßÌk|ÿ1€w.ç_ðMüð!?–"knzfdQ°|íJÞðQÞñ¥C¹yømôç¶ ‘—^ÚKxÀ«íÄ•Ö' àÔÛ°LWíPþ:³?¸½|Ú—,„ ÚAþ|ÃFû:¹^DéÂHæ6@j!yݽ%o°#ñÿù—¼ÕÑv{&v•Ò|åê+å,ÄaËcâö±(ôl0GûÐÇ‚úóó@ŸÙîj[§çýY`% à‚slh›Õ¥p;‰øÚŽ"Îæ%ôHØÙ¼3>DîüCÐ^éê·yxZ Z/=á" ¢3íeQæu¤m¦-ª}0fÅÉFS«”£z\æy³¾Š×zY¼h{b\ŽþN†íŽíTú#*ÚZ±4ƒyîT7VÞêƒ$ýørÊtÐE€–yð±ÿURš‰o¾ú·\a„µ Ôa8[«\å’,mÂH/<”l+ÈËË\»Ï 3¨ç—¬•FE½‚ˬH˜,¹rž¾×\&@É£_‘‘k4¸vÅE­F1©Ožúã@´Á/£º±âÓö#%­Q”‰üxšžò†Öæ@Åš”ÁL#ŒÍ>ê}J$ ùœ Ÿ×Á9: H8 ½Ì8õF“t£ ðd–”ëúq0UOU4;Útålã«þýŽjth%mAý žäÑÃNÇiEèU Š)'Ë •¿âù%wÍ»á Üz­í Ö 72y ¥¼ž9)Ò–m®+Æ\oƒS,ŒyˆÂa¥4u,‚Õ>«­„h4d‹‹€³³³LíV‚`'Ó?\¸_*øްr'Ò²»é#ès`’Ñìµ(FB³­oÚv¥”bÐVa¥3[3œ43+Íé¯ñ#ùþÕ â§& ŒŽÑ€±,®q¬ßðçÄ?%“¥yfz$üš ¼8·\ùO~ÓÒ]ñÒ»±+ò8(´”̱‡H6`úBÙpJt¬YdúßIRçÐÌþ,S¿V«E"Ãï¡m]–”.rz‘œ+@¤$1ÑÓz\¹ÆD‡àÉt0šæ's•%€®•[@³ç ÛH?–æÓpgkµÙFB—TSµ%º×&mº¤/¸ÇˆçÅHU6_«ÄDæN/&&®6“9õNM&†ÌŸÎZi|Qi2smÙapÈc¬VÖ.yURÛ*“FVRÙdå© !B_lþÙžÝÐþ‚SPÛ8"èãs­g Ûoøw'ð6œyÛÍ~lÿ~¤Ny£Âyâöh^¿9h+ùoƒÙòFâUp=6úÇÂNÉQ¸ æØüï±xyášò[…Ô<«só>¼<éIOäWOìÏ~÷w?áû<À]‚¤ÅÛ>Òßxö¨2éË÷Xý ·] ³ÑòìqLL”ž-}Sæ·rŒ »w¿œqö ýàÁG¨£úÎA“À3ÕeØ8¯-¤¦2¬iòÖ¸’®õÂA«}Á&ÿ*Û5åöÀý÷ó‹Ò'Ó¯Ò§À_…4_9Oå¾ÿÐAê+?TxÂñTÅǰ¤.ä8tÀ_ßËoIøËÞ7`î·žz˜´¶)BáÛ°ƒX}ŠcGP˜²ûZöŽ_*-†¸Ð¡XZ;/øÀÙ2ÓÆT}¡Äké£,=ã/èpÁÄdްÄW<Ûæ€íóÈ]r²*UÇNÊžhÅ÷& ñN¹oG´o°/èù&mÁúÑØmðfŽ–f—­0üSfŸ9úqò‚ünKHÝæ¯|Øz'Ú‰õÜ ¶€*ù‚ŒÄ€2ƒ‰]à] )y’ÅŒpE˜º©”Òð‡?Ù×.qM3”/â´ê6ìäv(¢Lr¦0áì'ÇêÓ€ì´IÂ)¿õh +%5dX`¢)Aç"=ã!O‚f†ä«2ß‚'•'c쪦ãà "M¤è"°¸6J¬½,³&Ý”Y2Ò2ÑèÈ„÷ŽˆõhÛ Î ƒ"{ ^ÆSOx Ï˜Ä[ü– ‹^8@½5C¨AêÇH¨zÒ"ÃàWOe&Ëj×-].¼ »“qh32%)xüw麉ž²d6å„.nåøbc'ÖLNsŒ<ê¥Ê¯F®–è‘ÚÁðÜ—$ŽÜª…È è'3¹–NP¤«°dG©”M|€÷–ºùÁ»]|+`¤²]~—9+ƒ&šò*wLì•ö5O¢ ,ñ‰ò8 *‚ämÚò¢ AMpÉç;˜E¾zù7ùøç›y@£„¸$רÖ&Æ~²˜+ô XjÆÀ@e@͵>€ ¼¯‡õׇYêÞ–r2aô(¤×ÉENPJ¤ˆïJÊ(TÍ&Aä³Hã/(ïÆ‘îŸWyhžªKEÆù^X.=1€×5Ù11•XK¥ëy¤¦tÕÏe-¿s[ˆû§ûÛã] ‘Ž¡ìmg6Þˆ8¬ðùí]ÂYÃÁ¦ðŸÛ­J m(e¢%€ú)Â4ùV/µ +E8mhy+CÂW/Ã^ÿjqô)x;¸"êcié[;8ü Í—rAª ‡†…½úwµO40\ÛmT{kqûcðaì‘~È•nú fešú›ÀBÜ´€¾!ê䓿¶÷íû,™g2;›WcŽîÖïmƒÇ¨DHZÚ׸Ú’Ô+³ ˆ´‚«,Åç*ŒeÇÈ‚è¡jâ{&{Ö„|ÛˆÞ¾ÄY[v¬>’¼ªÓÏÃ,‰•‡÷Ü{ÿrÉ%Ï[Þö¶µœuÖéËþèÿ¾\ý Ë 7|l9÷ܧ/G<¯ws÷ØY ‘m!ö„P£¤sò¥Û*ì6G7-áà!ÝWþMö”AG¯±¿Ô:ªÓà:rçØ¸ ¯-ýÁ²û÷-_¼ó*JŸ„ü'ñzRàQr"¸êlp¹“ãö!Å›/y l±/’2"Ïl¸‹ì+‡ÄÐo‡ôu3JUŸ­Ž>ÒÀÿéO{êrÝ 7 ÛýLÎ|])0Ú0è.%méoã’¯L~ëåè-M·sýgÐw°+ïNƒgòÐ,O¸èñÙñ–/ÜÊ$™Qߘ:¿âgY.}Ò“xöd¿6Oº%ñd¿‚´¢Ê$÷½$eÌ‚nÅ~­Š.Š´›õ ^&'‡„å[LsI<Òê¨-•T_]>ýQeÄ€…ÒŸVÍ\ÛsìGI[§ÈÍ®ÈÊß=̦w;ÂWþ87\„PÜi'å ½¡dü#ßfˆ* Ö:L*ycJYZ @ºã6¾â'Ñ‘S6–oxò$ís­-ìòÀkùQ²>†86*M8É*í QáöÔ‘n̬ C!'?àæ#1ÊF÷KÒ6Ú}\ë;:ÒŒ=Î’«C”AÞ*?ë±ÉÛ \ÆàKæVòÜ!Ã4òR«ž¥‡ã´ƒ¯Šá©«a 㘇}÷ºŽR¹á£2ê=iJıãG·U„hxU ‚[‰xÐ’g1´ƒ3­$tdOe³c™J¼Vu†A!i±¶-5b#oüaPèk7ªp°<øj,}äP*ˆÞ(ÿ’ðÕñê ?&Ê&³Fz‰l‡es€ ®×àÜÀ[mí€# %ø9“,È$B_òAJIA2J1ª”©/G1`ö‘¹l9 >Åiª(äÕP€?0’šùGh*Òµr äH)×%¹Ü:—¤†B0yªxù¸4q ¤ôRMaÈSº*(Hs¶LDù "ž Ž1z!åM$6¹6…rø02¾d$~Õ Z«^Ðx[õÙ„ëq?ùði0žeæ×Œñ£4!½½Å@V²7*´Áhl¦¬æqÚb\Øl4ô‹ç_Ñ=Šnb´½HG'zÆË毜ÆKYTl?Ä ‚YL®ŒJ¤9^JÓsö@ðl'ãÕ~H5[qªvæ§•A8¶ Y(è„'%Ràʬþ¾«ºà Ir®àZ9”)¡ÂÓNpé˜­Š«¬ØA7+ÞL`AÆŠÕb &6WžÙ¹$ŒŒ©Îeí*W^g“5e{UlJwµm2§•1~–qdwÒê)ým€£6ž£óÀúøSNÙ»Üs+ßËò]oü¾åÒËžÔ 98›CÚ ¼^oéí¼fw:6Ï´ÇÈ6éc¿7Ø îXØ­ìXxÓì£i>Vþ–w ì¸ë¸ï_n½ýöå=ï}/¢·üíÿáïð[§gOc÷K?‹Ï—B}éÕ£q,=–®å^op[zƒùòrcéÀþLXnZ>ôO.·Ü|çrÎÙç.ûöóëÅNü¬Ï´cn‹õ§Êpxا›Û79 ã—Z^Jìðo㊤¬G‡8ÉãweÙàç„IJC‡¸ÃÄùʼÞÏ€ÿ"ÒÏîs–n¼±_'v¥ü pˆç»ï…õ•®G¸[p„0;á„Ê{˜2é}ùa¥§¡L‚ãÄb4ü¸ƒ à÷ú+Þз|‡>pòx4ýž;CåR¯ÀÅË^Š_®_>uåg–'<þñËýFK\ùî¿ÿàr&ñõšo{õòÞ÷½o¹úºë– /¸  çcËüåZìä䳑…ñ%¾GíͯMøjÚlṳ̂Y”È1;¤ŽM¨wô8›ôÊvËÁ¶e+§)°LÒ¶Ä—¥Žëìe,¾ö˜-/¢ „H¹˜ÝÄ´xJ+0òj³¤<'®Õ–ŽØ×ö/-©8Œ””§]œÚiM­ö õ½øzm8‡[´e(}NÐL õ3¡ÍÉóõééa³H¾E5(Wž¤*>´&:¸¦k›Íå_S¾ÁÊ{%*ÓþýøömŒ“†péÊ×k ®ÀýE8´!XçH™’ÎC¼)“«8£ç@:÷š’ÊSö0Ëù‚k$ø5¢qP¾)T9_vºYkêU²Ç-Àä ñ‹ ŒÛ`M! ¾ÕÚ·X¾À¥ª°äÍcG Œ, ´¥÷,{€o` !ø=äÈdë`P)¥6¢-Œ,Vbtóá:S€V• 8ÿ¬ù̇Á÷"»40R•Ü}ç½¾ã•òU®ÁvÞ ¥—ÉiƒfùÔ,a¢±)­T Ìûü!-¿H¨×¤ÉÝ9ÈYÓê½ì£w„Å>ʧ.è¨Í›L)ßT>-™=Èu¦mÅ´Äø˜x\ýM©«“ö®¤ò“?4¥&Oí½U4ŠƒÔà´‹x^4è•×ÄDʤ(ÕØyÈ’t1ïܼNå#p”bÈJ§É1ƒë¡£>â9˧Ԅ‡|Ô »Ì¢-6#ÝVœì7¶18§£Úæ?UÖÈp6ÅA^ .áðÊ”Ný}ð¨˜RI±·‰Žg¢Ë¾e&›âH+¿(—4º£ƒB§ÁÝÒ²=Œ¨ÆÅú®¶Då¸ôä:Ò3Ì’ºåsWGu[Å$³˜€¯²Ù ·Ú",xÑÍÉ1 ÈÈXäXŒG)·M-Î)±°@û'Zu,e€X¦ïB½¡“”‹:%«Š [™‹ r,µß *ãôžñ#k&¶ Ê4r†¯¼¬˜[m¬1Œ+¢©]'&l›¥/–4¥5×µOÜUÇÚ$êIÉKRÑÙ²ø+kºˆGú$Ì­_ê@ùÈ@R*ätÊ.ÿû—ÓN;aù¯~ø{–‹/¾ð‘ " ýGrü90_mù¿MzËßΖ›þ³àwòÅã–sÎ=gù䧯`Åöøå²Ë.ÉŽ_µ¾Å÷ؼcÓÿÇʳlËßÎüÎù‘åì³ÏZžðÄ',OÆS–_ûï,Ÿºâªå¬³/XÞÏÊ¢î\ïÔ§CÅ ô¶º´C*f´…“¼&ŸÈQˆ¯5%-Ànç9çì³´ñî{–ÓN=¥­/–íg+Ì…çŸß ÿN~•ÙÉשlû¹þ†—·Þô‹ÈµðKÌàCƒ_¼½ýλò™4”ù¶;îXN9ùäåŒÓO_n¿ãÎVÝO:ù¤Ýšo; yëÕJ&ì⸠˜¾ƒås°©×šä¤OŒÆwÝE}8m9ý´S‡>x':Ñ€Þ½÷ÞÇÄá¡åqœ_™[z¤yãM776¸õ¶Û¹>;j'-·Ý~GÿÃG¯ìÞq’“™m±Éû•œ¥±=lëlÑX,éHÓNÔÒ^Z{)€v° MZ›rÔæè¡Í%V_/ 2êwû°¸,y–ÃS‹/ývUp)ìÈB×[»Vcdƒ% gF@&Ëh¸±æÔf9®S@&CB]Ë2ñ&OñÉ“š‹ãöc‡Äõ0Oc'Äoû%e’U‹ÛeðEQv³%?,Ë ró²W7fxÅß6Õ1D: áEÉí®<Ûå-ÇŠÐ@Þd“®#ƒ—½&\øœ%/öèJ¹bdv¥1ÖŽÌQ‹A °£Ä…_ vò MœÎ~ØÙN³Òm BùÆC9ä£à$‡´!=xg‹Ž*‘2”K#?ghHý8£ýt˜3I°0“‚¢ò§UxySXÝ´Sm8°6¨é¾ ¶ ôBpň!¹É›«2„©$·¨Ãä™¶XYçîF¾²öÝC;r¯ Ñ.Ð#¹Í.§èÛ]“*pUb àn¦ùÌ€Ë ã’1«4£ M’àxyìAN—ŲxÎôù^û®•Z~ÜMÉn«¼bw¿­ræc”p3”’…k Š|ûR“eAëÙ§º5ä0೤F[¶ò5&ß¹¼ìÉÊXHÍd#LéçпY9lq–¼æ§Œb+ÓÈß _ÊSf}êÕ$z¬xpvÒÖõ¡á§ß‘{d_ÈzÞ&Ö 9¯SÕc`€’Y<’'¿¯¨ÚBÝ7Øá!p±8@£G@«Ÿ4äÀÊÕ-vù'£´½wàCÈÄ:0Þ5PqƒÑ—küè1us`ŠòŒrK…÷yë̪¾”ÆøÚAuS+`”K’J ¶¶r¨.¶GЩ«C§•ÍM|”ÆØ,9Eg¨Œn±/f¶ƒ‘SíÄEkÀC:™¨c§`F6¯:D&TÙ¨x’ìlez X:!ÑžŒî‚ø=í©Oeà~Çò»ï~ÏòÒk1¿ÿþå—~õíËL4|þ;¿ãÛ·"rò)ËÝ÷ܳüÚ¯ÿÆr/“„C‡Xžö”'/¯zù˱ùË>h_Ëjþ&47Ý|3?·wyã^¿<ù²Ky¾ä¤åæ[nY~ý¿• cÅß»7Þ|Ëòø /ìîBý_ŽÚ$þ‹ÏÚÚ;ú¬1ÏÚÖÚÐáþÚ›é§Ýò›V vÓq_~”‹,³NDŒÐˆØÖÖ¦`Ç£wŒi"d»e, Â7éßñ¡-j«³Eàâ\`§×˜vihϸÀ4¹’I§®r[f±<ÌñDÚ6Nmöì—æõÀ”+y[”«¿¶ÙMGgŸÜØPºÓ.Êíò5=vÜ`_f3š^Àë䉖ì€Ñ¢%?‰Úùi«[’ïV.ž5MÒ¡*½rŒElß­ÂòÑ â`¶iŽnsyª·^Zæ¡ró¼Ý±ŽSÒ8§Á‘Lµ¬²t˜1”Ƶ£Z]íE`ÉCYÊ’Û ›ç˜i%v3™%Ð) ,Š0üIc¬à•5±•qh5c„ÿºvUak°«¡ÉÕ”Î'ø-·³W_¸JVè œ,/BÒ‹“a§ÌDCSÛVhp ¾ Ð B¸¦£6ÕŽ„wÙGSdZ|G þ¯×+V62ôånÀ¹ÊY¬x*ìாN†ì'q>Ûy½\c§¸ÑæÔö Ô´òÇ›TvÕf0"]ÐFK¬2(ÛØEúaómã¼2`ÿÎÒ§óÈ$yžlOTe/ÂV¹wq{ó“òÞâ!¸ð$ž„òmÀÛĘafCI¾¬k•.-õÒv6îÏ›ºd3 ÎŠkÌ)ßèàäT¿õf˜l#÷ÙįÈkWß5RÓÃI¦BÜêŹ™?YåŸÛWûyw¦C½³ Wë@„d_••¼ ^¸À$?6qEÄ?J›Ø!nÆì@W’ÚÔ~–ÇQŸJSm¢ü–‡€kÅ–£j–2ßò_|s‹eB%ìèåP|é¡¥|BªÚ Ì.y'›\ÅTV&Gż¸£·iÿ­?úÕ3Þ6^¥×k™hlæ>iäRæ|†²¤ƒØ)a|ŽÏ¦“ {‡¿¸$² Q|ï"Šñg¼3|»Ðd=ÛÎM¸þªÏÈd»v7«ã_tÑò*&î…Û/ýÊò‡øàò2ögŸyf0ÿÑüû ÷o~ñmËo½ó]ËåÏþò¢^Î*ÿ—ç<ûYm«yÚ“/ãWš_²|ôc]~ògÞÒÝ„W¿ò•˧þôsËk^õÊÿ¿ÏVšöÏÿEƒj'n!rð®O7ù­0wq÷á…—?ùæ¾pùìç>·ü«Ÿû×ËYÈòŸýÇ?´ÜÈV£ÿó'ºs_õ²—-ÿÏ{Þ¿|ÿ÷|÷òìg=sùƒ÷`ù©·¼%™¿÷ß½\ÍŸ§\véò&Ò·ÝvÛò3ÿ×Ï.Ÿýìg—oíkáùÈòñ+®\~àûÞÄDãqËÛ~ùW–Ÿy Ï™œqæòò—¼x¹þ¦[šøÜÉãÀý‡ÚÂäñ6Y¿â³}>x~jøÂœƧ kW_ÛÈBí·m9_v”W±t€±_uF`íÏÖι ¢~Þµ—+¢u©q$¢gÈ+oûÄÚž6víÿadKÞŸ<ÓËŸô ܳ@;ºIo⺾Ûö?û‡Ú=øû"3­ãŸííŸ$¹Tz£¢ÃÛM6ÚŠZõ ¥C‰J“C™6W×]ÜR}9ÞHËÑäÙ¼ê%eBde TeñÍ¡ }®mÒÉ‘8ºX{UM£› õVþ½F;eDeC ÓÝên+ Å!Æ¥% ámHÔ§·M–òI½¡ÕH‚fЈ(ÒÐW^—:}{SŽN(Í5"$=·=`JÏ%' ößn5QÊl¤e*²§p<‘…=Ê£ÅÊ]à uK@ìc9ÇŽ¤A±´<”²ðÞÎV›k“­6ùÙ"ýi³BÜ©°’ªhÙJÕ¼XåµaN–•…µ\ÙUR}+³‚òï€N{?ûí}¨ÄuwdÔïàUîØÎë¥åÈ]E K½ä%oª­RžVø{>Æfâ˜ïI›û`©r ߤDØ•Žc; )Ï–éŠd¥žxê hd È Õ¸¬‚#@ö†~|UéìG‡H†V饯@ONÚ¿ÉŸ"$ tL'Š7\å¬Id>²de„´á¯5ƒ¡<£Évð]IhÛ˜˜Àç[u‡bÇ0†+ÒÊ&7¯Ó ~A+H«{V‹àáïokèGežz’´‘Wÿh{eüJ‡fšÕç)WŒ™s&ÝŠ3Ž´¡-Ž*çKmõ w L‡YEz¦Qy€ oë9Æš²J"!¦¬k¾æ5›ècq °.­±¾„³ô²(eß©'À /›\·3E%>¦F.ä "éž)omsôà¹È–ùì¨É(× d[ØB¼ù(ÈÝí±ÍSWr,Ñ~:Aí”Å\ëtÚX|ä¨$wh©90M_‰å!sb€T“7ñü³ÒH߉'ñ¡¥ â¢âZ\øÖ›¬Ó6€c'GPš—']r1ä‰q'EÊŒž–ü»~«å_'}kS@º ½,g°}˜ÕçÝ˃‡Ç÷ÀÄŸ=u€x²N[wŒ ƒÁ J/­&—gþ×p@Çvßm;_ü„åš«¯Yþù¿üÉåd¶õÜðÐò⽈»J‡—ïùÎ7ðv©Ëü/ÿëòŒ§>yù½?øàò¬g>ƒí1,œwNûí¯üÌg–ç?ïyü0ÛIËûþðCË'?{{èyxû‰/¯xñ·,Ï{îs—ß{÷»—+®üÓô¸òO?Ûàþd¶Õ¸­g/Ï?lí­w$Îf Ñélçy÷{Þ³¼ùç~~¹øñ.ß…×\{Íòþ[ÎåŽÄw¾þ;–ûxÀú¿xÓw-Oa…ÿÍ?õÓËŸù\õsÞûûËë¿ýµË…ç·ü{¯zÕòùÏ_µüÿù./ÿæË—Ÿü¹·ò;/Ynçy’×¼òeÈøÄå-?û³Ë½ÜPïnº‰‡µÏZNC—3™t|úÓŸžú¿Ú½¾ð«5÷ŽÏLðïÊv>ÅÓkÝÕ÷½¶šüúÚh®ºyK­gN¸¨CœØÙf*ÿ ¬a”×ÄíDh³Å“mŒ‡p®l{]ëdvrq†¦/P°¼±•p¦åe¦4_àš¸’#K2ž `]ˆõz·ó”³•u»“ýŽ*˜“î‚C@Zæ:¨ŽX -ëÁQ]WX·aJ!©ÚçúRŠ_åQÎÕÊ”[~ämå’etMÓw…L»,|ÙW”=¶½•©Ül°Õe‡øË/ê-ºIð ­"~4½^&¯UjÅ!&Ét ¦¥aÑ–‡Ã|˜;xŸíò“±64LË ù$àš†Ë:½¢I8®_PQ„Q·"ðo²Ìt%­y›mÆ{v´®µú׌ö= ¬$GÁ†À‡Y©É5P2õ'Þ E޵zÛ8Ê0deeËøä Ãapù'Üì·ã€°òèø6[=u¥I¹z"’ÖP7¾¢ïÉKsWv7ÃbWÃùƒ0¸¤”-@($³|(J£F0jR\ó‡¶8Ôg Ä×ûS"ðÚØrÕ[^ʜĊá5ÚDódKà¦Ê“oGž`àdƒˆR†¢x”)î+^+Ø0«Ç'ÚÂÛYÃ_Lø ¼%ùHjÚÅÆ([Àd w²¿Icƒ‘îê­òÍ£F¤Ö®8ÝË{Uß>=2ƒ}îH'{H[oÕWšÅPøÚeµzkmß]äm‚²êݳXSû(orrN2Iv;ÝaiëYÉJÍ‚6ÛY'ä©Ï°‡46{_\{ª&ÇWšÅ[p+åä9ÚH”0ÐF9‹à· ÓäÏ]*™JGß!pü¦íƒB/›)`ur`ÄlÌVwáÊþ×6Œ+ãHú†Aþdð) ÐáCyˆ8ÑR:ð$§oÌq›\¨[þ¢^=Z*1WÍÕ'ù…ËyÂø7òHJ~µ!ÚÑ¥pp’ŸBDƒTñ]®ñEPé Û£ê‹ue3¿ÿáºEœÌ1´4¦2&硈Ô!ß*³ŸÆÚ³çœåô3N+Ïüñ©°ñ±Á*Ã_æÎ_–ÆŸÇÿ/¢ÿ•ÿy´ÿ*Ê´•“µÓØkþŒg=yùÓ+o_Î9ù´&S%&èõ{Cƒ#Z¶bbõ—t¸—Ê_â8Ì[|N浪®®;ˆ?‹É‰Û`žÍÿî{îf¥þðré%—´•ç¶0¹%ÇUû½ì¯w›Ï>ÀCÃwp'à£lrþïÿÈßm‹ÏâË?ù±ŸZþñßÿyVà”ÊÜd¼:Q¸ýöÛxÏ!ÊN%϶ٻi¼5 ú—^ò¤î |ⓟ\^€,OxÂãÃyÇo~d¹gs'åtöð¿ïýïïa^W÷?òñ?Y¾õ›_¸|áÖÛš<(×eÐñµ±øÁ./}áó{vÀÁ™Ï\{ÝõË·|Ó { è·q7Bz¾ÕÈg®¸âSË“yåì&&W_{ír&eò¶©Ú¼¯ÁäúÌEˆicð°í-€àvkbÍ´m#J»ò£ÏÁ£‚—×Ó`e›T¹-¹¶¶3¶õ»íKÌ›ˆ’ÄN¿]ÛµR vñÜ€#´„Y{¦i+lǤ5í’’×ôK7ÙåíR«çs²I¬6Ó¾JþpØîj9s‘ªÑ±ÑsÜÚ?@“Å>DÄB]à Æ,Œ© |m‹x #¥É“W2÷ÍE«Ãw®ÕÅruM|`Œ¥íB^pꢪ\×÷¶Jž# ú? ]Lv¡lR6ÀËI° M)Î=¨*÷ã~’q°"ÿ&¨Œ½eÎLLkк>ïYk ò)Yû Ä¢Ü+™¤g»éhË‹ÙZ@¯Sk §COÞ>x¨ˆÊŒÐw5:å½ /g"ÁÒC,ˆtúk‘ s{Šg®d02`¹l°`c Aul¶ 0ÕCþt¬Ê+)až'†ÐänHÈ[9çtÄc&$´3XÊv·‡tö¸ˆ|ÒM¦,#3{«¿F±c/™¹bçÏA‡tÆá€¢»\g‚@ žâif·5ÌŠéÈENÇÑ+ì™ÀÈGC«·ÈPÙuš:O£ªÆV¾U²´[‰Í@&òˆ¯ÖÈ¡˜Ìä²%öU.3E‘²˜Qæ«3¾QSG{p& ¸–•ͱŒ¾#v¹k`ÌéÇUiD*{’©_Ik/£O)æNùê) ò¢‚ã-dó ÿ£) Ãrxµ/e¿L®TÐpEåàK\%]Óùwt•Nø— „§aË»)úBkËxúŸ³I­ŽüäŽÜó@®7Ê2(¾à*—˜DÃè¨2´/Mô²Þ×fÈ@IL¬_ŠÁ„Ÿºˆ+ ¬jÔhøÕZ=C®ÂûÜéruÄï`ÑdN±pÚ\šŠ‰ÅÏú['À?ÙÈ®—ƱÆÑö2á;¸lD~åäW¡ŠÊüâ.É”H+׃¬$;˜œ¶J¤úê·ncpQ%a¾´/Yé(öÄýWÈÿËýÙg%íQ0Úvêá€ñk=Š{õXëþ×Jç+ųÿv``_çlh¸Š7£Ð±ÕcC’Ƴ,®jKjl_¥C|ðˆÿj¾¥«í|¸×}ý{÷îi½oÀ¹‡Áñ¹ýSدïó7±Þ7âø€¬ïû?÷œ³—}èÃí‹wÅþà¡hYþéþØòìg<}yÊ“/[¾ýu¯]n½íŽå|V᯽îÚå'ÞüÓËe—^²|ñî»Á{æîC†éo­_굇ë»ï¹oy%[ÛÜ{ïÃÆ‡îô^Û‡ú¯v¼y¸ßzë­ Ö÷V'>sõuËúCÿ!ôï]NäÃAVõ¯½þFÆ>»çžûìg6远Wð\ÀG>ú±åm¿úk‹“…{î¹·ÉÆ•l[zá ž—¯ã`',GØfWÛÿ¯öÈ›Ïðq‹iA—ï´ƒÿ»¬›Óª–\¯¦w2Àzé?ˆ'ùt4¦jßl¬\œ0{Å™v×h¢Ra€mŸq'Ôs"sµöU‘Ezö-BÙN5éáÂ’òö5Æå÷ˆ[{Ö#öŠ!yl.Çú«hsU™ôÁÒs ®|.橹=¹"ÍaŠ\øÍ$ÈKù›?Pmì–°êKmúÜuc2’ üíšÙÉþƒ2bS+°pç,QÀÔø¦I4»'á¶€z5zRë²@•sÜS‰ äIC§¨SÌQLu%E5M¦Kˉ—Ð¥|5ÆõÃõ¼.ËÆ\;ŠØ#>í¬[lú“)ä^ b<Å*ÈäœÚÌŸº¨Ÿ­î"¬¤åÑ›±hPáeE–kåéÇ"àQÞ)mŽ—éNjHÓ¨jMHủ.A†*ÇêÇÍØéŒÑ·™ôà½c;9é`õ0Ùø:œk…ðÔ}`äíuAj1N-01TT•ÁOôÔ÷è!„‡agi˜Ì¾qe!T¸lkg+$§äÓnöt'“™Âs®œïéT"2tÕA\¿HË«m`ùÁ2”£lÅȇîq6#H*C&R2bøÁöÒ¨eS)™:ÁÙà"6šé¯ôµ‰ug°ä9×Ê¥Îöûù»DŠU WÌ“—k ¦aP*‰@“ú#]c%Òú"%2W}«wœÜbShý PpàÛî•× >À«ƒ±ä¡˜ù 6öÕó ®Ü‚GÐú,’"’?ÛtÄõ³2Bä,(GnòŒ?Û¡?~åô'Èý“æQ¹‰oר¥Ê-•Ò|5È•¤ %ôÒƒk)Ù`{·d BZ{j£Xx¬&Cᕆ´í’¾‰×ú¥ Ai}¢ ÂóÑ\(‘–è'ÉDM&:JkÃnb .YÉ6-8™hÄ…eäÈ ÊÕÒ²!R[€0Æî´ºH¤¬©;DÆ›™µew7U†ìtZËÔ˰±=®b“–”RØöP{£ä↰¥_/I¨s«rœøˆó˜ËŽ-']ýA'|vÊ w,½ÊEßI„´°Õ q öXZ•~ïm œæAs“Û¬–tÀ’î¢ó%öxT™<Üzr°é/ý³®­Û£¯Å{¿¾6sâæD hWNÌëWc,eÒõµ¯Æ‡qg‡e›ï7º_õþÒsÿ¿Ût|«’oıÍñU›>ðê`Û7ëxGÀë~òÊöæÿϸ%èïú½å©l½¹™i/|ÜùËßúo¸·üüÈßý§Ë/þʯñ{ÒN³e ;»•æWüéò›ì×"o¬úÞ7¾±‡¢ÝSïäÃɨ|õ©¯ñtO¾oîñaàøèÒK.éYßÖãµìjºòÓÅàlGº y?É£üžïþIË»y]¬‹‘>|}š+?ûùžUx#[‰|;Ї?öÇñ>ãŒ3–_ù…·.ø£O,ßÄ7|j³-Jž>(|Ë­·÷ Ò¯yÿ?‚Z/¶z:‹:Ù¶ÄÆ„.íÏëí'õµÿœÇí\pÌ÷£RfnƒFá°õ°ëàlŒØÙzxö°ýi!‚k›XcË·ù\Á{m…¢×ÖÒÇ™³ "­IÏyÚáK½”\}ãΘS®@ ùäÐ~lç]pU´t6!¯•]—(d]½¦ïÀEa1wÔþ’ÞÙjo‰|Ô§±”„3œeœ¢I3yDif샵 ôV[E÷²ÒúEÆ/* Iê m3 $Ô+bbSÖœÙ*ºÌEU¢C m ØWǦˆVM) /-„òŸü8¤ ×5i @ Ÿ¡—`f¿}H•´,nžË$! /ì½Ä#½Ë'9¸l6 }”^åc¶Ð.°&Ø…þ£‡³Ð©$’SBh×–çÌé<¬fe\ ”z°Ñ^®g6À5=t:lH_y+€¶N{(ƒÇH OR ¨MrÌŒ f­œuƪ{ƒWá,¶&YBº"ùz;d÷o­Fé’H®ce(mŽd4 ß&*«ýDèÕ£ŽÔ†5¹Éºm‰‰~´Çs=º‡çr¸¯ÊFÚ³tÈ${½6nܶ•/·tµöÃÀVȤ‹úv—:ãʱ²Å{Õɧ;4ê5¢˜Á–†Sœö ¢øf‰ŽêñŒÔFÇ3Ž6\hÓ“aŒUßt ˆ ÂW‹ ¡¥;Há«W·.ù6Ðê2( C’?m'±!§‚ÂË2nËN`•/3eÞ7{ WC4¸Àø—Wò‰:{†Y6µ=AWÕ3>ø.ÖR@Îöåa'Y’æ ¨%r"‚ü‚+–¾6 ”Uº–‡©u@UG #þÖ(§-_m»’pù’“õGº2‘‚q­-G&c“"?Â)¤ÎÄaB ,_þ¶;<\N¥€eè¬Ñú?²ËÚ±€»¶„R4åU݇ýjœ¼ÚX ñ•ÝGul¢#/ôÔf ÏeR±• »¢ÆüdN©´ü)=¾¦ó^q¡×[.àÓ3JIJ’xÛ¥/¶òl»ö¥4±Ñc@<îeU÷µßöê4¿ý;ïb»ƒûÑÁOpìåmrƒ CHfy¶kùŒA›"¼îµ¯Yþ„-®úúE¡£L¶¿Šãk£¯Aà ]ñ÷ñvßÚòŒ§=}ù½÷¾'ž}ÐYyö±ïûO{ÛTžÉÖ”ßínÈñÇûCN+x¾Ò}´<bnKq xÇbù1$ ºj²ÝØž¹hòKþ±6Ó>µ/Ú ,a3–~•/õH»()×oqô!—Æãg1ª íã§«ý¬›ïŠÊWù]íík?Ï$|ÃŽoÓqkÛ]ðò>~Åg–«®ºzùnÍî³÷m@ÒÿïŸÿ–=£´o͹êšë²ÿÿ×ÿåòöÞ_ÄCÅ×óÃ`ïÿàG³ÇþŸüÐòË?ý㬰ßÓ|>þñ?^Þñ;¿·<îüóðçÕ&Æ«ú/má¹êª«ò¹Ï ¸Ç‡½ó¡ó¼pçw.wß·ùЇ?²|ß÷¾qùÿäk’p!wÞÇÛ‹>öÇW,—]òÄ~‡á¿ÿo~xùô¼ób¬|ŽgîÛw`ùèGÿhyÝë^³üqd?qÁyç÷¬ÂçùÝï,ÜÄóñ\í5Nú*m-¸õÇ8"6 –\ÅHÊIò_ÝÇÅJhÆE­ƒ¥}oö!ÚMû3Ö£Z,Òµ-dÖnTèBÎÜmöRy+‘®íP+÷$f]jEŠQå!éT½‚—ÈÄ·rûï•C ¸$¸Æ\åIˆÐàÁÙìº1SK¼¾gP\Ú‘§¬à«„àµçÚž…1@IDAT1_û mäŸù2YÉj/›F©ï²~®tô‹ˆY¡ÛK(Úqép[É]i뀙lHY3Xˆt³8€Œùâ¬Ì‹ù ˜hõ3‘Ì×l0Ya”½x97Þfpô›¾ÝK¦rm1HfÐÒàŠY'¦äÝT^ … nâñ(êC£BÓ‰ÌCy‰ÐX%GBR$†„yó'ÏNÙ ¼5nÙ Ûàù唡­–+¹Â*¤JNŽå~’ўݴYkù8…¼õ¼FÚ:¢‚PWóüwpߊ²Èfi X5P·Ã{¨ 0 W×Mðªk,a®ˆU,PE’–6H{pƒò ÀòÐTWƒ§ç9VxOŠCslƒV ×ìðáñ=ò4<•ewƒj‚•UõG~®£'‚„øôjÆÑ&bpàJí&n¬\íUôG¥´Çªo• ~’™_¨ÕFåõ¼¾ODè0ø)Ç<”yQ½˜]ËÔ•/LN´P°;eb™ÈÐëfuqP+/éÇP"s¬úIë¸õõ¹12y°ã ÀMc ¡§o‡ReUr¯WòÝk$fGÏ[sꟺú'±lËy| Ü“7Tðab<ÛY&àĺ–+ÿØÌëáJ>y Ô•]Hh%Û ù-8yk Š+,vsp’%×róŠùuŽx¨›^ІIq+³‚PŸòQvèÚá¥OŒ'>“Õ2G4*( ®g[™ŒÌæ·Ê¹Æ×øÇLè0‡$ºL™)—ñÇ5üe£¥7clKrÐØ–©Ì «½mçXåÌ#ñ›O»`ÜŠ2Ì4ƒp3ˆ—¼mWVAVy¨JNTÓu«ïÃcrø%èÆ@£QŽñ³án2+á½ p|#\{7ú‰'œØÀÿ:ÞŒ"o}ï ¸+›x`Sîvp cíÛ(` †«³§ñ~uqˆ?žZ·ŽÜË– a¥!ìaad9@ë¹-uàKë1ñÅdÃmÒ78nµðõŽ®8Ÿxâ Ëçy0Õ˜7ßÓîC°nqeúÓW~¦U|ùܾeǺ!¼o³QVCpvèGž¸>ýÔSy å©l ¹~ùÞô½IöÇòIŠ—DëX;*µÛ_\‘VV÷¾k÷Ë»=Å‰ŠƒaÐNäí]óÅvÊð†1‰)Œ×™iJÝ“…½*‰È3–,ÀÇõGš1ONìçwh–eÑ×p¸ÿß÷õûCXŸâµžþÕÇϯ¾ý×Ûªóœ§?eyË¿~ëòÊ—¾x¹ˆýþŸgýÞ÷½Ù‡-.`ðþ ¿ôËm±ñýþ?ö/ß¼¼ì%ßÚ¶›÷ÿáxøƒ½GÿO>uåòæŸ~Ëò‚ç=Wô–¡O~ú3Ë›¾ë üXÛvvâ\C,݉øÍwþN±¡­÷ãë?÷s7àtb@[˜m;níyÑ žËJþÇÛ®ô|ésüÒ¯¼}ù(Ï\ò¤‹›\üø›jyòŸÀDâSŸúôòËoÿµîj<ûéO]~ãïbâsgw2ŒŸwüæo/ŸfûÏK¿ùËoþö;{eèÅ]ˆ_ãœàçk9ô=>­3ÝFÓ;ÖÞL›QaóÀ˜nš&I`;¦Ùá¼]¯‰tL›e»7RJü›Æ[*cðoA‚ÜŠ(¨ÆrPmb ][E&Ý$q~ ÑkKE.=8;©P 'èAG½wà”IÙ×f×"è PжMem‹%ÜaÝ ®Éß½>kJ±´–}—¼ á쇔W^3ƪu~èÕžBsÄ1•Æ· WRm·-šoÛ« 7vW¾µ'"CIø;cçå,AÅÈCÑU-„+¬äÃ3ƒëlÍÞ æºÄÆ^2 浤,#?Í(’¶kßí€ËàÅÕô¬ðeBOGQ ó{G58[~Jâ@P7;Û €n†h®6µ¢¬6LÑ·}ÙRXàÊŠ?d•+ÈÊO/ñ½D.XΡ\O*£ÜY!B„*C‘|=ÜN"lò«¼È­¼˜]¹Û4mËIvt×.ʆ äY×ÃAÚ¤É/ ÌÿÙ{¨í®ª@ó&ù3B !™ dA$  hQ DK(µÑeõàD/íªUÝVuu[m£ÕÚÚ¶UH—È ˆb¡¢È Å, !„@IC !’~žgßûý_~£’r¿ï}ï½çìyï³Ï¹çž{_A¤¯ý9 <ÁšM<j É:>ms°S;®<‰ñDÚÔ þ‹Syè'eJö‚†HYfT´î?{¼É¦oäVCÑ®S" ËK* *» µ±‰ºÎŒ¼VZ ¥<:¼FTÌpž¨Ph¨ké+·ñ;ògPŠm•”|X60µLRa/9ŽÃ×G]~S,28#›Ñ‡ó™Í§D+Òj™HF¿å@é©{”‘…'Vé®Ú2¸-“G\ƒŒ#ÿôÆÚ#¢ñ¦­¬)fÔ™$7–ØÖ&“ å+ÇÒÈMrTxwÑkt C¡ö¦%{a ,ú rJär¾ÍnåŸâÏ×ÐΉS©›<“Ÿ¸˜Ä¬ÒR?k"~ò…Ò#ž¼8p¦<œ8ŽøIÏ×A(s“4µ-¥ÚW9ÚV›ª! Œõ+i(‡àÔ~’oå”éëÈQ&ú¸)#+ìÄ øÇ¶µ™9…–`Âð-#þu³ú¯• LíàÃçK±b²ç»4K€B]Û@±É䫤óƨÑkë zž‹zåñ"pûh6wÔÝÀ»Ï}=ãi§žºÜÄ€ýïzW³¼¾YÅjrÖ¶70>äÐC–‡yfè7¿õm-Ù0n˜?ö+Ý[R®dæÖ÷ÂûCJ\xaK4¨»ŽúpžÀ›U|¤ƒó·½ó †¬#]ßÚà ×Yé;@—·€*w\Zòd^ùPds­÷ÛÞñ.4¾=ý•ÙK©wyÉço¸¹_¢}ài§uAãàðsW®·ÓxÇãô=uégÔFÔA¼¯ä–yðƒ™ýÿôrÿïÇ/ËòšGbÐuvœ_[>b9öð#¸09¢‡X]ÿÚ×ýÙò`ÞKï€ó:.œþ‚Á®ºÜ€]ýѪ³yõ¥Ë:°|óÛÞVŒx1Ð…>ï &¶KÚëÏÅ Îs†§íÚ?ãÚ¡{‚µö½«×«ùb¿ä®Îþ(—ýÚáÜ ±EêËKX¤ýÁ,ýõ{¯y-=7tqpßãköÿÎ}Ͼ[^|úÊÏ,ÿþE¿™¿½³"œyÆ;ÿ‚,ozËÛçÙ{ôÃÆkAÏl-þÜ%¡½©0Û5W_½¼ñÍoçÍ?÷é‚U}}–À y¹}ôò¯Œ÷äÂô½ï{?ôßAÍ-ÈvNæý\à(ÇU,%ú÷¿ñ’äòÐ »îúÒÏó7ñÖ¢Wÿñë°7?4wô={°yïM{ùÕéO%¯ŸÝõ‰ó?ìK»6«¾æ2wõF¦¥v Çõ€Ý5²_¯ìvÚ÷~ì«7N‚œ\réGZ®ŒÊ¥N^œ8ã|9Ÿ~†åÃy¤¦¾”÷Ð;È~"o¾ï‚ –×3ý=ßý]ÍÊ»\ã„î×;ß_öۿÑO³åi=ê?ÝçÞÇócVß²üêoüwŽlvþîzȡ˵\àhíl`eGfœ¿â¬³–'<þk– ¹è¹žÁþƒøÀåÄûˆ½¸ãÀù)'Ü@ò?¼è7–³¸PùƧ=µçÇ?ù‰ìà’™—1KîMyMŸŒ~Šv¼oÕ÷åæîiSúylgLÌÆƒ?~7¶ žøæàÏAµñäE̶Ý;1ÚÂ;æ¦0 ®È€5ž»6KoNŸºLç§<·f½[$ÎQGÝŸ²ÜLµ9ÿ8kù_ô_RýÆsö·4è?éþ'ŽWÜü±04oüâùa\$ºY¯¾½ÈW‡FŸ//bÜnÞ{sB®LnòîÐB8ßnäg|.§n.=ÒWɀ̊þÝÆvä2jÑÅ\‹}”ÅÔØ8ÏSn)_è÷±çðæd×¶sVÞ J o© `ê”Þq3ùÃþRB3FJy΂¤Üœ§3ÙA KT*?ãF =Ï0”)¶Ä B‚ä3qý¢\LñoqËwælr¨6ˆŸ°ëì¾ÇÓn&Ø?´Tg%)Nt3Òð· ›gâ-¨¦s¬þ#¬Ò$Tòçˆ$òдçŽ0Ž#ÝæM@ÚP€å£ ¢0ßµoäöŒ_ÆÐËà)Àü×½ÛùÜŸŽâ\¹älYaoÇãJV~ ¼ %±„dÐ[I$pê!œC`žøÓÈ%$I‰‰‡~è+í*,.n=—¯@òtm¢Wv‚G å>Áô6ù„›ƒu„ÖŒNùÊ$Úò&pmÛÉ}:9¦‰·Ž,>æeGš.”Ã6"ið“‰3÷€fg“µý E¹õBè˦Æ_ê­ªÄL LÂC9õ§X[#£6`ÔÓXÐúºk=à8ÚCiñ1W¯{Ø×6Öö9å¥S°X¶±e'éÁbb±¯ Ë º/mN£9¶J:Êí‚í0³ôôs/]ÐÚåÊ—QöÐA"Äe*CpI(Λ‰¡~Ã𕲂n¸Hž% 0`ÉcŒB[˜r:Ídμ‘Ä(/BÑ¥0>´è ÓÙéW±Ôá?ú#Ë?û¡^¾•å.¾ký>÷¾wò¼‡%öÑ~ÉË^¶\Æ€ì›xk‹g:°ÿ†§>•™Û»/¿øKÿ÷r³ï¾Må§þõO.7\uÃr¯O¼7t>À ØõÛþÂꕬ£ÿ?þ¯_Zνø’å¹Ï|Æò]ÏùŽåÞ,úïZï5æm@ì~†yúŽoý–åP<ýù_üÅåJfhç"âÇ~è¿e@}ÒrÞû.hyѹçžË ñ7—«ˆ=wžóìog†öhèРý.\ü)gyÿ·þìònò|Ë:~âÇ~´å).ñ}õ/å¢âmù®xü×?ð|f¦o=ø™—lF›‹môöw¼sù,ìWvqåR¤rôô'}mç¾'þ‰,ºÇ‘÷Xþ￈»'pgƒ×F2ˆu‰OÙâ–#Ê[]°Õ¦ŒyZwµÍCF”¯(G ‡>58Û¸µíSÎ!6ŠÙ‰hÕn3nCý‡ìk‡»ð#:”¥[ÎËÈàaüvÁUÍùÀ{¶…È0êonŽÄÚûð+ÓG[]û}çæ7·á½oJ•ùÖüµã&A·… Ž’e­—ò{®ÐŽ3ê£=ÙXŽŒ¹–ÑPn5ãì^²=ƒáM“AïL\fç|X¬çw0†, ؤpù›ù3ÓÚUÚ7YW~£mIÙ¥ozÀåŠÁb'aÝ´!…Q`v»cy4ñ¡øhWqWûŠH*krp1Ë ¹òv‡·s¾Ù]x$6¿)›¯Ï»VІ[òi´´XyÓ‘¥KQ¢Ò_ÓH <¾:ÀíãMIâºÔ×ÖØ$›ÒØ »_wÓ§mý¸GZà 4„¥€ÿ´)+L!’q¼«8r’¶Ð]Ú.¡¤Eò' ¦£¥ÉUZN½ÿgŒuä5¥¢„$'Mg þm䬶 ø¼O›úì±7†_Ýe¸òÕ,¨N’Xé(§Ý›#IàƒôÔ𮄀l@¥܆†œÄSwe1<‡WÁät!td=5+p2¨uâ9ÈÑ^¢Ú©[žýM¾ž'Pц7çÚB_” s¦È R^ÁÔ„=ES§Õ·7DÈÏëâƒcïÔHS|‡Q:X•¶‘ò¾M87dÞIáÛ7hÅ-{À¦z -sNÚq6зVkå˜ö¥ø S•zCÓ‡3o¾™dbcÀÒÕ™ƒÌsj»Ú›~™Xòo¶±£WóÐß:íšo°› ùü^BKy£ob£Îz×ï#»BþÙBä§ÿÆ0#£ZIÞDç€\ m ÕÍ^ÃÛv!+ý-Ïkž* p‰ÃaRÑ#;éB€™-}åÂ+б~Hl~‡—6:@û(ç]¡ZþµüÇ«ìãùê@à8C(×#šÐ!<¾–š¶ÍN<ÛGFKBi?«“E~ÃC:žxqá¹[Ù¶lM](ׂ( ‹W£¹t l) Ûô-á/<™Ø>aÝl+ÕS%maÔÙÜÊ=ÒÌf©H\ŠLñ®<ðq!ÝÚ8ÇÚÕu­ÎjÍ$ÄÄsЧI!ã,Íê#,¦b¶Éô²Z¹•hÛ±¶¦?ùªµ*›ry40Ú«Ò]1’.˜K†ÛHóâE ÂÕrE*FðÃ\Tÿ&ýË·óm¿ñÝÿ|+woüFî–y¾m¬çÛñß·ß »ÿñ{¾ñÚ·•}ñ{t5å{´ÜòªpéÒbÿ*ƒÿs§˜òn3ÐÆ?xÇcØ_Ô×®v5ÛÍE‡Pö—eàŒcÊ<•ž¹NüƒÖI £Ò>(d d¤åˆeyÇq2–=¢hë¤\ÕfouåÇ(æ8ý¥w—8šÞ€©~:"›w#{ yœl† y# <·Á¿{eu©Æ- ¶Ý>ÿ‚÷/?ñoþm?ÐôË/ü)x=}y#¯Ftë Úc,ŒW=ºÞ߇)ïÁLøï¯Çô ÀŠA­ôŸõžÑ|~Âãß]†_zY¯Œ¼‘AïýÛŸY¾æŒÓ‹ÇxÞw7àv°ï~ò´]Оы”h&Þ7³ø.úü¾ïi€þFÞÓóԟѳé§Y2äßà¢à£W|œ •{ñJÈvq!­ßâbç³×\×…ÇóyÝäÝYo ¸Gò6 ßÖóá\ì3Ÿñ =¤übî†x—à½ç‡<\ÜbˆHrÆf飷ڤA˜œ¹ÐÞx1\&;‚ŒÇÝ¡¶ùw#åÛ„ÅwØöËFàN>®¥¾ÈàI‘i'ÈÁÓ÷”§ bÇhÆ´:¤‡âË„­Á7rˆã@ÝñÀÖ¶<^Cpö ¸žcÌŽ †ÆdLׄŒþÒž¾Ê¾Ñ6³AXoìÅ…«Ìlh<›Kõ“¸Ùm=Y™©¡Ní¥3ùy&À,§^Ò6ià}~›HX¯cËFy%BQ݈•$™)_ÝÔìtZ¦T2Žº…Á±uÉâNé¹Cà :m!B 0ààÐíêmÍjŒDpeÐ@ÏA··•äŽÜ6Jàÿ Šu%ÀD6çj€pÀ·GÖ` ™/¸2Tóßf< º±åðFŽ ]<Ç:8¿¤J7$H©zØ/ ú%ØÎF«K½üÚt1 ¦cuVMÛû/ßRë¸Úb,j5<2%Ž–Ù<õBl<…}„@A;xÕXB‚šÞ\v+Í]çÕAGäÞK@:`OUhm*ç„ZþT[( iúʪýFWÙÎ%ÕŠÒ‡?kI*ACñݺð¡zfyÑ ?9Ù¬¼I‹YúŒ‹Œ©%©h q/XÝÍNr^ãå‰~ä¯Ö{í]3ØÊ@àhçd¿ ×/a-óϨ Zý-©ñC -ÒãnÙI×xÛDJ Û[NÕ¦ÊA)n&Êq áÂa¹G ÛEfLVY`%×â]X;·èi]y¦UÇÊÚ…2º'<„aš‚+ÅiWøCÆæ põÃ68S±hC'\¾ÓXÛ•]”‰«LòSyèˆ[¾Ñþð’mÓá]šM„·±ÚÚ]ôÓ–Îä»n݇Uµƒ¯êtFÿí,[9éþ'öí»Ï=où¯¾çy­ÿ÷‡–\‹}ƒ]´¾ÁçrÖ««Çó¾ë;yû[ùeÔ{ÃϼÿƒZþÉsžÓC°×1¸¾˜‡ký¨G-/ÿ¹öv ¯äø^ðü½W¿ºÁ²~ß7ï¶—?—ÆxñU,Ïù¾ç=w9‡ßxè™g,§ž|òò*pîÏ _ãéCŸ^H\ÙàÿQ¼éåËË_ñ;ËCôÀÞÈóIä¼A>*/ÿâ¿ÿ±å?¿å­ÑQï7¼ñMËGX²ä2¢o|úÓºè8õ”“—G>âáËëßð†ån\ðøÃTÞAð]ö_`¶Ó⇂-»„ÊA»¯ôü!ûƒwS¼›qöÙÃ(ÍÖ;a€ÿÁ‹¹3ñ̳–üÞçqÇâŠx¹ôçU¿ÿêPöB£ÁAî~bB_ËØx$Bõ?‚X7ƒün;76Œ/cCÁ(+¿X4DØ«E²Ýu~g±‡þ²}º9¦r%€[ 4­À÷ÝÑ×û$ŠÍ‹>rsÏ$›Ðvm@ÖMNÑåyÝx1g8ö¢Äö<¹¾ÂÀÏ(ºYo`g` ¿ ͶþÛX~‹?ñgE’„¬ø+@'6ÑIýtReQdéte[ëÕø¯±®„3¹¶ò ×… íéÁ¶#%‡:ÓM‘Z~QE]}´µÙ™ö$ ò8.Å#©…•‹l4gÌÔÊ ëkDŸ ‰[\Ф/¡­j1—›;–déLÿ„SH r,QVZ.À~„} fo§ï6£ïNc­”+Œµˆ,;õÊK¡†·Îw–<£måÑ:â#›…>°« SDÊ…‰Ös4‘o÷võ ™¯oÒpLwÖN@HCä24õo¾4¾t­£^c698„u@¸‡ø­­T„jÙˆ@ (7ŽÍ‹)ž! j‚ÆÒÖ /=²_úÕ•ÅJ¼€ïHAÛF—f'À-þæ?ߨ‹ÇÀ¦£2©¯#lh5ð°6Ù¥ _Ng9 öP®tzÖ§«ƒœÊ±!v36|ÎÇ2‹µ¡Äz…ån3Ø{"±„®cN¥±¿š‘òÉFs.m=.mŽa _ùéÕr€ÒC<¬Ï/ Yf± ¬^(:Ç”UýÊÒS/} Mì*Ÿü @´Güì)=,r\é³%ThmxAL;þÒT…Ö82N“A¾ù}øz ]ŒkÎðš?Z¾þÉg/OyËsxõ]¼úòuoüóåŒÓO_ÞôçÑ»ö?Ê«0¯æ` Kã®u÷]þÎ^¿â•¯\ž þ“Ÿôuý ÓåÌ‚»ÄåXÎ.yñõ™gžþ Þ!ôQ¯o™‘Ï8p~éË_ÎLøG¸¨¸W¯åDƒ6]¬|—]~ÅòGü'Ìê?jyÊÙg/Ÿç™…ßýýß_|0ùÁ àß²¾ŠôŒ‡‡pÄᕾ‡±”h.ü17ƒ|—$yÇáRÞPt8ÇÞ9qY’?>åì¿3ÿ¾õÍ\xxáôž÷ž×² ï´œvÚ©½Ãþ·ù*dô7|›Ž‚ÏRƒ¼ˆ5t6ÿº˜/íãfh`h‰…™JÀ³Ÿg™Ë,³x­ð€­‚9¼ëüNes„K˾ðã‹×™â<ÛɃ0pêe rŽ^Äë£ÿ­Ûµí;‹¬FOHκÆBæ$é×ë ÚËf,Ð|h f-’U1êX þNýŽ­|/º¹Ÿ0ÞúuûŸûIó—Ä¡èDŸ¿¨y ?4*}éÀ{{ðÜ~\;4þ¥ßnœ‰>Ž“|Æ ~ÙLd4bœr¦G•éGJ•5QÕÆö1yhÌäêÆÍÉ‘Db€.ž„Va96ãzA†„Š ¤È›stö5üŠ6ý7úr,™ƒµî÷I%Q1zZÙèÓÁ-Ìø œè•DÑB ž2 ^E×r./ j|„ãÎC…§sžºD“.¼4®†nˆƒeXGlP–f§àI8ð­ ”±ß uÄX~Oƒ7ÊU˜f¼¡—!ô2H4›|2Y1ž§Ž¤¬cì!Ï FáäWÝÊß²ô5ú8Xuz^pM„óáhè˜âø'+×-'õ–¥›>²CD‰c^ü+Pöoà,[åS>ÖOÇ$ÁNnIV‘çYvÔÒ¨\ü;Se;ŸpÝY?¸m®v)0¸WÒµ¶/³H ò"Ár|O…†kêfÐ. 2RÙŒçðsf_|xd—à tOktW0O•Ëml‡•4݈lŒE1£<ÆŸ¸]İWw;½ ®ö¹…“I’ƒUaeã?:ÆÜv’” sCÕø[ý& yÉIÞñ’II\é:Jœ8À¯Â¼æ2vÆ× ‘ÂHê´Áƒâ$$èi_qnaö/~ÚüyК»ÄÈdP{Bõj€Í¾qƒADsiÉÕØWÑu|sÊ8iÐë½ÒJcŒáä˜o¥%]O¨n²rà,… ì!/Ê´‹à’õB‹#ŽÏÖQ±Cè@HbÈŠ­EO9†·±6¼Õ¥;KÐÌÅ~Ú2‘d¨> ¼£$è+ [åÚµ3bÛxÖ$ˆ²äSeÆ“ž¯ÎÎuqÆïæÛ½y¤¤´5¯r'ºb3J¤Ü±ÙÀ\¬åÔ7$àz6a=5ãÈl°ÚO:ÅŪúvò¡àÈC¬öAeõá9îõÝü¿ò¢÷êÅ뮽¾u÷.›¹âã_>pñÅ=Dû—uŽ{@•ƒ^çùz.Œ1_«éà÷øÉÿy9éÄ–‡ñ{¾ÚóϘ=wþ»Îyw³ê­§‡ïk_÷ú–ù–›ËY¢ãúx—¿Å’"¾Å<\®s>¯î|ÒøËsÎ釹>Í……“5'rç @ÊÁü;‘Ñ×=:Û$¯íô¼\›÷¼óp_~“À|ßþ—ïB×{2ãÿÌr K„î] ¹tç—~å×zs Êâàß~_òòßînÄSŸ|ör,ký÷ÉIŸÆ_1ú:î$ø~{Ö?ï»ð‹o RO½ý!.rÞÇ›‡îÎr"cõ½ÀøÏoá"çÐ~UW8åöbm`Þpb¦4ª.+¹êÆ/çßÈÎJžÇ|º'~Š7éÚ °QõQƒl-cÈÔ€©A”ì±»±í0Θ‘ž2fEdV'ÉUÎNÝËâ?dËõ¡±ª¤ ´µ(Ûüa-q®Çr(Ës7ïJÈ:~ß5ïCº>T{5K•Ç[xÅ2ž·½ý•#u@-:G²LÈWºž_{mv‡ ß“e;Û@߇s=>—»ûË©|ŸDNïÎô÷Vèùœ„¯ÕÞnÂ)kù ?xáã…ËuüB­›§¼³åÖbÿ‹_Üh<Ê›‡)áÜ~±n…¯é»&&|Œ¤ü·ú}·ïꮯ;¥&ßå<ä›ÜHøà[>ÄîÆçø¿îô§;´2)³mG³¯mñå¤f¿®ŽÐ¦­é'/±õ±öaüB‡Rò΀Æ]}4'/C§ü­\ c$R‚ìÖKO¤6ºƒ/L4ã#a`õ”‰ñ$/¼žÍ’Ùú•‹6”®Ë´¼8ÜÎåXœè×Hñ¥Tu•Æè‚þ jŒ0ßAãEÑ•m ¡¯ÀÊ“Xo0“|ä¨^ÎhR;íJ‚k¼uð’?ÂÂÈqèç'…›ÍD­zF„"/Öä["P&°B)m&¤©2H¶†¯…­*) ½ô íRg*TTN}Wǽù(‚žýÑ×ÒAv åNe‘–0µé©·5ËGÚÔŒ®¢ÉŠ/ËÙl1…V‡8Ï%76²kÐÚ\‘ÕÝr㢈úóœ9®\>â[ç¿6—çÊQLô5Võ.Ê<È%ñ´e°ƒ/2€ÕµÄºÀzåìÒµ°ÒÒþ2‘OòÆYY,‡’{îptFÙV„h‰´Å‡<…eó•vÞ‡In €Éì3v•°V÷ç9 Çä^Î5¯0Á¼0Â_áW|ýMêÔ&¨Q¥ 7°9œÂlu Ør¸nZ[ü™Q’Šö:“—¯Ø/ÛvóØ «¶c¡Û&¼’ŒÌ[{ßãþ§?þ“å\–´œÂƒ­ÂŸÏ{ù/eÙŽkÞô§ë†§*À¨ËžƒãŸÏó«ºïÿÀE­—/~VXeq îàÛu÷¶»Ôà4øG/7ŸãðMBNVív/zéû@í˘¹ÿÀÅê7”WYŒ·ÑõÖtnä÷"ä¿cç¨Ý&Ž×ß%§x®ùÿÛ6I¹4l³üØá ~Ú Ÿíy2õnÓVÅ’u#Hƒ(ÔÏQoŒêïîÀ#£Pê06ç÷=%S7᫽ÝQ‘çn£»¾ïTÐý8Ò ïÏ_w},c}îíÔ2DšôL¯1³é¶úz#d5‡À„ F£Å㨠zå jœ`è±ñœØ*瀑\v.+Cå0¯Â`³‘ÙrcX õ¬©º¢¦›5ö7ì̓éɹr™Þꃂ`è̬;TAòNBäÝí¦¹ò–®4ÝCn[cA;X™`ÄG¶›Ð=CK3D~}Þxû2™úšÕ. 5Fn/l8jr›2¹MÛ¥úõ}±½”{ߘd@UŽ·t“%ƒüCPÒÄ=š"ƒ†SS ¬œi`ÙÄŒ²•W{–¬‘5 eñiÆEÆÐ2Vï)ϙДUƒØç6ì€OÄ?¾Â(WÌ•‰úü¿)£Îuu$žt,`qöyfà4<´uõÒG*¾9ÕAz\Z:ÄdmÝDÁì×ãèJ_YÙ t¥Ì ˆw¤JΫ·CA¸‘¦÷úÃBl®çaÜJQf¸CDYèø­Ã%ýb[‡Y“¼Ù‰ RóÛ±VJõz H‘P)3³ú¥ë&å°®-¼Iªø[ WäÜàWgäì*^ž2”ÕMªÐ­``Y}Y¨ÃkëŒBddæ,dÙ†ÆÖ޲¯¼Ôí%Ç,ƹ6ÑÖÍ2ËúlE2`• 5U4y¨©ÖÀk;8]3|FþÉ–ˆ‘¹§[Ú©*Ï”js>âÛN·¸é‹$†xæ_u/À§Ñu1fÐï±a,†Eû¶]úôÁ±çÏ‹—Ú”X€‰çgîÔ²³¸/ÇO&Å–%'|ü¥8)AXäÁ ö@Ð`M5&AÙ˜‡f³«df xÀÏF« Æ©Ó6þq,KÄJ>Ò±&жV(õ(Yt±°Æ’IS¾kòR«ñ•·•}[ƒXîbÀ¨—4PÄ¡Akr9×òƒDÛváQéZá I_ ·þÒô¬E¤M^÷ÝÝ"ºi/uïÖ#çæepÀœMÔWNÓë þ²%§ Êu¢´ZN¡ÔZËE„þKö¾¶ˆ’'xðñ.K‰kÍGòk™Õ’ž¹jì4²ö óÕôé¡\%:¡˜–ü\èk³ÖÉË­ïñj]èÈ óƒcȫܑ¯†3+-f³Úާ¥^Ê•–Ã_r^Ü w1ÉE‹0ŽÁõåÐp?ñì>›ÊÏMqÍôÛ@IDATÐ$š “G©EA”§d•AxމK¹TD ‚«lÆ”Q/qÔ]zò8ÉÁ»¸Ufkµ…õ‡| wÑĦ°-ÑTuUoÊFÆcã¹ þÒ‰°·ÞVn]ø÷œs®}?’e8n71 îrÕúû¶M iümÛþt¶6•~èèCÁ^ðy§ ü¡›ø¡#ߪãÒ!/ôÃ5×^×—eƒ¿!å›\ûƒ|1rîûwß?Ëú>–ÿrŽ1TÜëæCF0¶ëò½3-ÂyŽË9dcÙÓQ‡-žår÷{±sì=·Yëg³î@‹ñÿͶÑßö·—UÕH§MŸÛ‹þíMg·þ«ì«§â„oÌ—.ÇûÐEgÂñ€åÃlÿzÒv‹ÿ×I€íÙ/k¬Ë§ö«¦€¿î'?„Ì ®í7N"\^–ïD„ËᛣdÌÏwpßöc"u&ܰSïÀ͸lcÔü£ÀÎo·Ðr-[œhö¿ê‡Ü ûÊákðÓ·)³±ï¹.NtÊ8´?±¼I@í:x@ùA.S ÜÜVÞ]ù•S©´»¾Ú¤ÜO_èÄÈFð£Œ}ùC«)ã0Ù°‰äØ-/˜ò÷ß.VKb¤žiì¤í‹øFXä?„Ûw×_#o¸zyôc°œqÆ© @)ÉÉo¬Ã¯©Ù9í@)-wï¶f+Û]¾•ÝÎF+Bû}íÆ³jÿó <-W)v˲¯~;Ú·¿5\Î\+',7^û0n­çîòý7ÜMß—å8G}êSW.¼ïCË_¾óbu÷å¸ã£“ã–Nªù,ÀR`i4m(ÓΆ¦8›×ìômÌaIÖˆJÖ Bµ²Ø¹1Ã%Uƒõ66ËÅÓü6®`àÈ¿º$S*Î)´rENšòò¨†bõêZ„¶Ù{‰fƒèJÛ'u+m€ñäð–æ$ª8 îppµÀØ)½V¹I]ÞÊm€ÛJí nì ½]ú³ót$ÕC_8xQ®_L`I1Auµ^pàv1¾³¸IH¢“X ÎF•@ÌEÑØÔÁ­Ú»opߤ Ž65Ñ _ÄV¿ „äÀ…®víՂŲÊ/‘•§‡ k‚³¢jâ«m³©rZ‚0ÐG†=^DÀ»%m [‰2»cWgÞÅ7Ö S½8_¥š&É_냱xŠ¢Ì—|·;6ÙK¢–66Ví†.eÔÕ‘ÈÐBvYÕƒŽb¤U+¤Ì®/¡V^ìIÄHÿ1˜þ‰friGtR„·Ð¶$> åZ¤8cˆé(`Xü!ç->s„MõÚ$ßÈ:4ô¾b/û<õnð6 âpÂ.ù:ã¥^$wlç*Ô”ŒÉ*¸ðvÎÚ’–ñðÎNö®â*í—.$ö—óK%ZŒ`Çm@61NtÂÈvb´cnñ´#Å Àæ«»yÈrÞ9—/'vÝrܽ^>Ä •<õ¥Šy‡ákup…ÄühÚ&ÊíÆX=®¹ú:fþ?¹\wõÍüXž¿aþ\óJ·|½»ËYò —ZHÛ˜ ´M$}¼ËÏ[n¡Ìüå 0î8ÆR`nᡬ—Ï^åCM×,ß÷ü§.§?ø´ÔRˆÿ²mÕ*© |»ní¨çnŸýë§ö‹û¾=hl÷—Ïòþ¶ß`o{ÒÉ÷_zÖC–‡?ââåÕ¯zËrõg¿ÀÇðÀ—³°¸´ lté"-a0ëú¬Â±cÜ F`VSy5_DÐp'0A Ö 2Ù ”Î FQ>Û‘JækWÛ¦ÀÎ!̈mCuaÛrkàÚüçã ³t‘¯¢Î ƒ2‹áo§â•·•5Œö¦ÓSe¬(%êáqôU×¼GF€|M, ?`4˜2Qà8hÔé^›xkPšIˆ¥:(ŸÔø%IHœÍP¨©“«Wû꥙-—Ó JÕÝ …XVЀ˜IRÀ¨\FXÈÚìÊjv%“Y~Ñ·ë€RNÈ&O“§TÂW}¥¯)B¶-¡©¯:˜· í<Mô™é*•mñ(”›KW»h4;ÏñßœTEÆ^Àm¿H>2©”‚A’¡™Ü+ðº)×(`ÇPWvuNìouÄgÀÌà•Dñ¶þê¤ Ÿ‰ñ‰=%è™cu+?Z ÑääX÷Ô‘YÌ.ÏÖìºÅ9A®ž{ÁÏÆ¹²)G1J¦£â\ÿ’4…–Â]Ô’)Ú£í¹6Õ†Â×HÓoÙr¾sKãT_k\nçñÐgkbQ6i$õÑ Ø°1ºÛGÞ®[:¦ÃíH¢ú×I_éùi–G|ðA½¾óaüêï9ï~w¿ipÌ1¼Ç7ÿ\ÃC½Çßû¸Åׄ¾‡W~Þpà =»0ïOÚÛQÐÛ‘6´Ûõ›ñ„Ç¿¶µ‰UmS¼ €ë8V;óÓä.'«.:ïÊå²K?»{ÜÀC`h} ÍÇc¾ÛAC_NQåž×ÖjêCà ÐÚÃZ·áÕN櫚­z؉´2ØÅÇèƒUíáæ-óæç¿k¹ç½N_îwü=x~…Ü@ኲת֔ß¿t´–Ïn¦RØEL½åïÛ .ȦëV'È´B+9Ë C,v¢DÏê±éµ×ܰ|ê_àô=Ëa‡¼Üh¡Z=G¬ñÇŽOÀ7wÈ<Ÿ{ÈóNƒÅqÛ&¬üìxËTü€#ÿKWzå'ö{(s5¢X¦ï!?ç©Ñ'·–ÐØFùÝüc![îž#GP¼y­²¹¸3^)'Ùɯ>Oj뱩«ìÀ­ÇÄü‹ÚÑ $éËØÍ¡.b'åUVãmP9ìÏÇ~iA}^0Jа®rÉI|õ‚àäÓ‘“³ú'nã¼Þt½L@Ûœ ω›kƒÔÉf†ëÈ×óÃ)Ïÿ§/g>ôÁ%ï™ÍXºk÷ÿ«tÞÁ¼¥â¬‡ÁUùáË/üÜ«{øúð»ù“í4ƒmçµ·7,f`nq¢ÄÓ±7—š%ÔbÉ âß@pöü°ðM|VûÙÙD*Ê+ÚDè,YqÎWe4ˆi[üÚè@™Øl€¶H ŽŸæ×í,V\;PQ&õIR«8²!x^t8qð>„h*À»Ôc8×nn$› Éç^Õ¸Vq’B×ê3!D’và%d¥”2¾1Œ¤•v¾ƒžì£œ(ìEOIG»H›óʬÆ/)”OOE€©Þ½€U.äÓVpé"$ûB+û‰’Õ][‹Û0/úœ4«/=mPþÑ;KrŒulÀm‰-SO!2RϯÜtÎŽ-´“xê^Àpœ ˜ñ¨›zi(Aähä z³À á‰#}è'Ú‰›±åÇ¡]Ë0ØPÉæA#޳HÙX¹ª³ñÖƒÁÌÞ Facýâ{åÙ‘£*ÎÑCušÎ 𛽫›x°¾UôMw°¤ÅiUò²€Í±­¸¢Þ?qã%OÏáÑžã Ü>²/Ô½vnâi¿|´Q;´††î–æÌöAOpŠzã—áë¹|P zˆ>~Ë¿«ÒÑÇ«˜Ž@E!ôaeG+¾<6ím|\ÁëFóÈGð;gôcZ—ò®mò¡}˜ ²ƒùÑ­O,~ÄW,æ7.ýèG—w¾ë.>³<þ«ÛoøN—+ù& ‰ƒ;§þú̸±é¢]ÜŽve¼ª÷Ä©±Ì1ðzݶ¤{ öúZb,%¡ÿÚs o=úÂMËG/ù|“ FÇL6t”3ð‰-ŸOñBלëË›"h¢>…2®)àA¡^Xw0‡Î®wÀƒÔ¶A7Ï;L wᘈ^‡¡t±± æot;23P‚)—†I¤9l§h‰:÷Zcòe%?plÝtQ‡·§J7~å˜ÆlÒÂ~D{ÉÌ6>‰Á—gY7¾Ÿ™ëAϰ±=ŸID‰ˆhÏ»*0²šT”S]½C`©6.Õ”ô¥Ý5»4ð§2Ú»äîÞÖ]3©úÉøëÖ1•ÖùjÒYëoì@G;P§íêd ¿É*ç>ìêð’™"aò'6‡†w.L„ê{à-,=ÐnfN5çp¨Èøµ=t‡¸ük9Ð5vYLpÕ½|8È»_›†'ØÑ×g=p›!W¿N/•ßlŸÊÛòŽi6sÊ^>ŲpžóUtÙ&9¯R” º©§ìÆ eÉlúE°‹WQÕyî|éÓ‰ý)V´ÁH»Ö•NÖJ[¦Ê㧯-–©7¶(_"ƒýH6ÕÙ_.›6ÄžW±öùÿô»ùÕâÓ–«®ºj9íÔS²õõ×_Ç{þ/é5žßÏ/ëÞ—õºöºk—G<œ ~àYÿÍ/?ð½÷î—Œ¯ïü>€Ëƒ¤ygÝ”,ãOÛ–çë|mÌè¾U|}ê Eµc†c¿·öî±o4¦}=éÁ‡~´-_Šm™X^•3`ZÆ@9 Çg“§YÙPðíÙq(¶9HòÚ=܇2AGB6GPn““oÅÞª$à·޽Ûrþg߸¼àÿryòÙ_·üÔ[6˜»39wôQ“†®ZÚDi[ÒåXjhá-b/M±[j\-Pn\aÅR4ET&4¨m%™·äÐAxÛ¶ªl|„ŽB"»Í>Ö)‡Òï]aë´“çbÛÚ0¶Thû6©¶dSzœèêù’ÚôÑ⻉µoËg”ÉÃ~¤žQÁ(1à ÄJ¯€_ýe™69~¸îR¢âÅQŠ]i;ðÕ® ?:Æ)<}%¸yŠJâcr¥Øê;õj͹34YyG¦(ë—µ|ìa›˜²ä^ñ¬s\$wcb]ìÚ>´¡´”%8ÎoqìÀy1;N ¦»'ù­ì{¨«*qäRí{IS.“õhëquàpЏоþš½Ë‡±œrÊý‡äP2ú®íޱržÐ<ठ>ž¸–wd¾‡r“N^°6ðFJ}¦kkF|êL(vÑ¢OP&„~½ØÈ4 È×Ôùo¢©yÒ6ûí¬bq(÷c’…"§ ˆhQ·6ü¡GàÒ)Éß½i²¼¶Ó"L£Q¼4™r@›mÃbœ÷òÊO“VÃM†\¾TéŒm&ÐAh`(ßhÔ™°òAž}†ì¡¤ÑØ&Á¸ÂdOÁdž›»Ê’½¸ÒP~IùÍ–iä6r9@ %À–¾´Öæ²tóר -3Yæõâ%K!²|‰ˆ £zh#Š@¬j‹ <–‡ ñâ­,ò3qYb­rŽtÚe.TdÈ ¬`@ا ³è+’ ÏRZ6 ÅðŒ>¶R>u׿770¦nóA¶'¦ØÇY»#K§H”ÊÔ¨_¶DMêo1øÌ‰ºeÑäžS¾É*²À›eC‰ËWš(ÐÅg ~ƒ_Ñ@“³D 4ûEÉÚ­ô‘Sºüu»ÊbW¤®3¨ =¯]„ï QùècH{ñ#¥ Žºq(ÒÁËÊ–øAÃó±Å*?zŒ/Ø+¸ÑRà%õàIqëèY,¬Ÿë ;Qi·¤|Ylˆéo\òÑË—úÏæÆ¸üʯ½hùÓ?ëò„Ç>jù_ÿõO.}ðƒËË~óÅËË_ñ;]üÄ¿üÉåO^óûË ~üŸ/ÏãGÊþñã¹Í;÷?Ξ}þ–?`3_zgߌ›–m6(ŧúÙùO‡z ýH»0s¼ ãfV !ÿ›æþ™%€³©E«¢Í•OÁ«^byÝ nÍw¤­§Í˜; BfÿNðîúR4/¸©¶£|7R9yÁ87€'žœjòAñ…¶„lÀؾlÖÂ]…Ë?ö¹$xÎs¾uù«¿úëå×~ý¸+ô¤åòË?Ç'†FóõØÁ>^+M¥Št÷5²ê;ÊÕ}Õ6zY„M+Ý Ûä…bÉ,Û¸Ô8Î|ð0®½:ùDœiÓæÉY6Œ,Úgå¯,- Nr©·´{MnÚêô±ù§Dɉ/m˜ü_ótú‰®žû¶.Š8MxÒ7;¡ÆŸ}­}º¹bî`wÿÔCuÕžæ÷.Mtר£züfõJ;›ÂÌ?„ƒ‡¨£ŸEnYÝÄOê«·ö•§ åâö áѪŸö™äW®C£‘SZr•ˆq$Ž9:ìbpØSn¿E$ߦË8H`¤>úÀ¢s`Mç«ðøOœÔVÆÍŠˆLQ¦2lV “omè-mb…n Üðyæût€~îÚîx èßmíO½?ô÷ZÞsΕËýîÎ2 |e2ô Ökê–£pN ùO‡7@ôIýl¤†»k˜—°6.ƒÅ°š€)Œh €RaàíÚ*\Ëbq×À³!Y§õâÕ€@6LÐ’3t•aëÀ½œIB iÌkCQÆWVa¡ü—_L* Æl à¥@Ò‘gð<Ǥ|ôZûž"„î¶ê•Q}ÀÚ k/žcÿI†"…ýM—é‰íÁ*m`»3zKÒ¡ŸnR¤ÚQ³Ï,öQ>aÔ‰}ãUtl¨Þù™rÛ1ôR ;4åòb'*ñ@²úÈ`g¾¤b'€º™8Ô½·+ Ð øŒe¶Ð \íï±ré›Už$^Å‚ƒcG¹i§q;&!¢ (5³l9ôwÞd¤ÜÈhÈÏ”Öæ]ŒØ«h­ú ‚éÖXq¬~*#µCŠÅÆ”…êëCiŠb’Çà+Ž%o²ë eB6%ð>Äè èØ=D› êØ1õI‰@š¶ì’µ$>fê®`0¾ åŽ6»ì†‰Š•^%̉q71)?q)]å¦c'î* Fxuê.†…b§ÐO8)l‘›ËåŒA?-áPÁ;ý¦ìÃŽ¿×±Ë£XúóÚ?ùÓå•ü†å©OxìòÞ .Z.»ì²åÒK/]wöÓ—GRÿî÷¼gùæg<}ùŽoû–ånüâ®Û ÷9~9âð×ñËÆ>¬}¶—/Ü™M œºÉt#Äòñ8Ü8q™ÈÄÇô&‚¾7=ÙæE–ÅÆEñÄQchÒ8¤CƒPÛ†»‰©¢RÁ˜_<>dÍõåxŽËUœow!ýY^Ô=ÑðœG[âÜu¹³‡-…¡ '½´Œqâv.¢UGɽ ÓrÜqü‚ôeoZ~þç™·<¾|û³ÿ5^®½†ßÀ&Û]8ÐÑG\sSRSç™2sž­@My*‰zaÍI ÙX8Ô0ó8ðÓÚµB©Á8óÊup`bºqó™- û†Ê´:[Ü䌶S¶äÑœEݶé·P¯=Ë&Å-#AŽa üÖ­÷ÑÙÆ¶Íê€ ç-7Šl\ˆºÚAÝÜŒ3õ¼®ÅD äGó”6UWá©ÏÈ`nÜ@}¿;oÑ4ö6fÑW “ÙŒ‘mu‚|¶ uSÌð(ôÍÊGq@{µ3'º¢É±¬-`À+›wÚí'=v´“½ô›R§¼"ØßZd>ÍgÄ•}œò¬ ‹Œá'"›¶‘¿47¹•M?§ÊMû8p¹æ†›—St–-˜¬e²j¡»¾î@ ऽ,añsÇ ‚|luªMƒ?\µ]ßšîÅàÖÛÏ&Ï0 „ŒÂàplPÛ¼¬j ïmU# 4aûž=Äø›¦Z…4lƒü™\Z^n¼k]6õvR¨q˳ÁP4T %Õ!º.Ôûk²@A^ÑQndkG=i˜(æIÏÑ.³5Р<Ú  (Cåæá 6P&“îXO@Ò-ce7;”ùXÝ`ZY¤¥Ú6{íWÍž+ÉuÂAI1_ÁX±U¬2f¿ÕàF;8dD/gYÌaýȉ,™’ÇàhþfVù© Æ] ëiI&^ØBXÊc©rHRR±t“«äC °9˜[\“O}:Xn©eaäÎXÀ$/çòmV>>ÐWl0šý0qqâ¹lžˆ¥ï”´UËaØ»HÌMðÚ GÚ«„*.J©—ƒy¯ÅãÚVªãX-ªƒ©‰Ó-q.oKÄëzƒ RòupÃI6äKJ Œ+þ,ÙüQGâ­)ê6^PYåšÙ’2&níu¥¯–ݧZ>ô+£“N¦särí°›1£´ ßÈ8²å?„ìGó0EOmCÊ?Í6ES|øù5V@|h±^Õ¤ƒôv÷¶zúìñ`^†ñ©+¯ZžòÄÇóðîçùuáó–'}Õ£úa°ûŸp|¾—_ç}Ö7z¢#3û'žpB:Úoþù_üwÃÆ-¸èâåÈ#ø%^Êó­~ZpgÕßvf\/ ¦õ*íeî N[¸úEšÌP1•ÚZ…åF¡p'Æ~¨õbØ6gô c?e»-Ï;¶ã8šÜi2Œ]ZºåæHvÄœôøŽŸGT”ÛsδÎŬ¹¾–ÇðAëøI!^tPG}ørÞ{/_N9õk—¯ÿú'-¯yÍkÁ|÷òPfÿ¯`öÿ Ç`rK^íc¼7àƒˆm ÊT ^Èø×ÝÚ.jFCa²’"7•¸ž?zT6€V0i!²¶²Q—×Ì-âYÇ–sC <ŒÇ¹Ó¢îø˜ƒ¸…ÔŒ:ô4·<¥—® p¤¥Ïµƒ†w ·Ad×֠ΣÜKaüÙ a@|‹åW°Ó¦W6Šõ2˜”Õ<áÅ>oàÂqýI ±n²p»3í•×í³W_ÃÀï(&^öòccñFŸ{-ï½è’åG¾ÿ¹½öó5ú†å_¼à‡{.à;žýìåÿêå¢w¿mùéþìòáK>²œrÊɽô£—]ÎóuÜ­¥½íoƒ;“Þ꬜#«$1îômM‘é¥_“{âU¹ã$ö•ÚÈåùŠ#û`”A“ /qͰž ”êÀ`I&JŒ}iä_€œˆÕï~¼+øøÍ=õæçê¬@§øx¨Täy‹å¯Ón”etN6!9¯c)kü‚þ%-öô.‘$nÃNÂõOª£( "¹;Èíø®ýk‹n á¨mPÈÍKBeBWÖŒ&ü9D °M#  œ‚˜Fgc2J ]^Rðpz0Ǧº½æÏ˜#E›Í˜ ¢Óƒôj&j‰ÊÆ[)´3i–DLÊL"ÛŒ4%ɤ?2—h€3œ%ì¹o|˜ÙRÜàIþè©}jü‚óWRƒ®¥;ëG9[£^ ÈB]»A[|²1àPišjèZ\kt¯ÜÚNÆJW;®Uu3q™pìØ4Š2µõÊñùÀ'¤+8Ê©‘ð¹Ý¯*Í,*vÈ–CùPê¥gþ‰ÎC¿zÜGîZÖS¿­CêøÉ²‰"÷$?\:å˜ÂzmÎTæ<¿Q>3<Òæ#½ÀLhê‡d¸øR6œ<§˜:?]B»‹0ÀDé_V[ñ„ߥT˜#ωòª£<-+æ£Ï¹ø5}¦ÞÒñ‘F?YÑÓ=•ñç[9óY² —Ìå˜#çØÀ:1Õ††—€¡ŸŸÎÎs œ °3P–lÆaõͶ)£1ÊGš¾f±=@鉤ê®<RÇäR?rFžGîœb“Ûà bгa6ßðʘ(ì;йp³…®„¥Ÿ \Ü­vUGñÙ9`tTâñ—ÁÇ»çÝï¶|”¥>'0³ÿƒß÷Üå¨{¹|ﳟ¹<ãžÆdÙuË_½å½ñç>Çßgyá¿û¹å‘¼5ïWý7–ozÆ7ôüÀO=u¹æšk– ?|iK7}Žk.†îä6ÐWÄ…}ޱåk< ¢Çqwñ‹_ÕÅXt3vMI}pÆç{VXg@ˆclrè©  ôŠNèy.Cê«8*Þä)Š36«µçúkF\Ra{ˆÅÂYoXÚŽlÓÊßì,5“?w'ŒºS~ oí¹ï½ï¾œûž7,ÿä»p9ë¬3——½ô•Ô,Ë1ÇÜmù‚oâ+‡H}ÅÒ0îí2ìmmÄ »¶Žf"› «mÈÓ2W'¯2#Å^^ŒmÁ•}9Žc§PmŸpÑ'ÑàXE²!ŧ B_›êËì!-êköÀ°è]kè«—äË'©’Ÿ¡â Õ“Êòý7s*4ã¹ö9èœ_Ç;K1’2!ˆ“‡Ê1íÃúP º TdÖþÁRn¿àëDÝŒ©õ0½,³^û[Pî£lË—Þ5ÕúbgLh1·äC}û¾?ÅÔ.œ­ñem‡Yô%Êω„H–dl,5“1F„6ƒŒ„kŒÎÐÊxd_?¶2éq´ñÊvK‚ý¹ÙwC.èÄ•ÍÎ̺Ç^´8ÔWöˆH>úÌ„EU8]Œ ×Q0éÆ ¥• øî’H\ᤧ–, §Ú ò& }´²…ž “2Ñý“?°¹(Z#þÏ [ÙÀ›ð|(P?ÖqcšÙ”´=QúÙKzÖ‘§ÌO÷Xçý‰´pÕV9‚áÜ.‘4@W™5c± Aå…æU¨kâT ‚ɺcåÒ¯Ðö={ÚWðøKG}°‡µEíÃqúȹ}ßEœ±Sj*oìÐÒ"ÄÒˆrµ5o3{];œáÕ|ìão7Ã+:²zaKAy‡òÓ2kYxñ¡Ü¾:ÊÚ—c¹+§KäüSÉüíRO¡6o[Jc›Hꂇs(ñq¿mw¶ó…[¼q¹×1G/oyû»z«Ïžð5Ë™gž±|úÊ+yøó~$ó³Ë“Ÿþ–?ü“×/§žròòO{êò¸¯z,R{–ß}Õï/¯ü£7.ØÃ–÷¿ÿÂ娣î™ï½ãVLmjïìï\úO¢m§n¶7]ç‘ýŠ>¬½:¡k˜çsÞ ÖcÌ<È¾ÄØl³|<êj3nñX€£¤•)è+„mo‡®ù™œíŽªä8y hÌ2ªl›!å\F¶ óº"éH”ô±Ü×~^Í;òݾÿùÏ]>ȃ߿ð ?½œ~úy»Ó ÌÌO¾Ô8˜‹ìéyÙç«Üæ;ÙA:Ý·Q®Ì~)bÈR½rXŽ|Óï‰/ßé"4ç5vÊ–Ÿv@_ÍX` n&œå}•cÆ*?uÉ" ¶ÀD Êú<¸ðæpó•(òí·/ëGýiþæL·)@^ØÄ®Ø×XãŸñ¦•ß”­’Ÿ2Ë•”§*ù.œìÑÞ.O—!ÝÔqnÿ!ãV&ÇöÝLñ£ÅÁfCêëëAnh/pR¬¡P§|]È4à¶ÇÚAþ¡²—}‡Xáqœ”ÀÖO"L°ØT\ó±Â8û /)Nª’.UÀ1ìá0kà/i½#QjîÚî` 芉(œ«Gˆ@Î'iN(õ78¿æÁzg½»5Ý,Q¥¹zœÈ{ ²6ކží': X¾Ée 4Ha³bÛ-Må(Ê ÑßÚúøDHZó±Ñ+¹¾†Ïd£¼KmO‚6Fëd8éÊZÆ¡®Q]Ò“²i\ò°œso9fKlä~—£²¡“NvX]]S£lÎ48‰?4BSþ ØM˱LûÙQ ? 2ÁíÉ€Q†cG{xîŸÿ°Vùô·&ºê{/njàœ;(T™Û,½¾Ž :úR›)ƒ¾´ßð}ÀD}µ¦ó‡I:ÆÊHÅ7Fñ‚«TM¢š€µÐÞO¸–(´htËQùáÛ;ºÙ}‘E»¦Á–†(™ÔÍŒ‚)G·ƒã"ȯÞà*Sr"/N’¥œò€­&Ay¾ô;n–i E7Ñ[l|\Ô°ˆ¿‹  ´×ü N4I"|÷XTÛ ò·-cB¤ly¸PÎ>Ú T‚SæèZž$ù$‘cž'Pw…ú|†çž¹AYxöBxQêCmÚGVë±èJGHø V§øØqͶ—ÈõèÆ¼HYñzF³UASÙ•85šXáœÀÒŽž+·Ær Ùá§mÛ¯§ z«íŽ>GTä>†ÁûÏüŸ¿¼œyúi˱ǵ\ðþ‹ø±¯Ï,Dz4è(êœ,û_þ÷Ÿ_¾â¡§{Ôrîù,W}îšåký°å¥¯x•ÆïGÃ|•sÛŽþ·R–“;Zß]ü9l°¥ß8n¡„øÒfXéZuóyÇ& 1·¬ZÉ1ð7ñg_Ðã¸Hæ­‰ #Çþ¢=ômeÓ´ÊĽ5Á’üÃìl涉/©Ì𠧤R"&×|È´iœÙFˆÕø%Òô“§¥~ ?ºyØrÞyoXþõ¿úéå¾÷=~ùg?øãÀûœäýfÒÁëÀµ~OäøK~µ—²{•¤Ú„¤«QƒÏÚ‰ç¶ä¡N]&-¡ESìE5ÐíÜh-Ï™ÍEú„=$e L]Ø`Ú©ðÃC¶ùÒI˜: ëlçZM†±»tbo@h?ë|±B0²å£zE˜)õ»qÊ”r¦æê wÐ8³,Fƒ¦jö­ö•å1ŽÇÆŠáEŠbÛbK¡g¼)[Q ÌTa é®j5QÎ)À¥ÙÞk®tÊx€ËÃVÙB¶Sé‹(š¸ÅÒóúéKéKFSüîý'mA^EZ…)>ÙZ3þ¦`Ék éê3éºD¼'²J*»}‡£?c6~ÏþÆ‹ ©JèÂLÒ:Ví5D:ݵ¿ƒìÛ­ ìÄ/Ù–ØpPNÖQ8°eøÏ™ô @‚Å>xV 9^ƒ(§{ì¿4ØZÂQà{b,LR0åNÀê´{Ó½ñbïFež¤³2"H=ö‚³`uð ;ép$Ýi”ûdsvÝ™]g aåB$‚`ñh8¿[w²<˜lÛÀÐFRV½œV†„°ló@Òð¯Ñ i[þÙÊཪ‘$к‰èÍÌÌø§Jà&Ž ¿Ì¢˜ "/Uù­æ9•ñe Ô$?ØH[¯mÒPÏÉ8ᤊ²§Zä„óaDZ³Í¦ž)«ã¤Gjð)ãè›Ü3ù˜,:«&¹|«Ì€{¦4ãK¦È³ò<Èa";ÞpÊ¢ú“”Ò2ñ³K<ÒÆ«ÃÌC‘Ž ½©@Ј¢`ŒŠå±Ç<‰ [bïDc7š+ Ò‹@èfh3ÃôÞçL¾ßßÚûÌ!o|Þ?Þäý¸Ï¹ï{ïµ®¾®u­²×^Úҩˤ ¶ÂH˜Ks=­Òpƒzîi-oùª4õ²óRKôÿ‡fÕ÷Sæ³èÃtþ÷9¼L¿gÖóû,–þžTÛØ5v‘&²hÿtáÖI£nÚN¥¦JWûÈ;8À"—|ê7í Ì2`ò,Gih GýþÓïÿ—vÐiÊKC -¢ üxf2GL^ÙÔ4e¤_Á£GQz‰-ádBƒhiÔ£’βçO½ó ;³U¾ªÌüK¢:64ŒIê­Õ;¤ž×¾I8é{jï¤7nŒR(ŸÐ«ô ¿Ö‹´ë€X’J…Ç*)r-.Pµ7?²DÚÔvL,àÔ2ú³wt¼kyʧ¶¯.»…?ŸÊ`iºOVþegD9šòN¿"m„Úyßõ86Ò×°šûOÇ Xºµ,ìÜæØDnG‡Iˆ#4ç© œÖÆ».wÑ÷)];Ñ\Çj'ÖQHØ‘5cÕÐqÚ#@N•ªŠdš×Þ –­ô Ì &ÔÀºv±ÒCšVÓA¯uȬð¯NÊ…5'ºé¸Õ+•/kÈ«0̯=¨T?ë@‚½ÞœÙ h™–£d•/´ùæ:úñ+ï$XYá¨ë UGZÊQeH#'­¤©úp-LÛˆÍ9ŒÅñÞoÈ“¦ù•?·8Éë‘zÇu ¦Œ¤Õ.ƒV> @.’TVÿÒù–f’„t;16Æ‘‰tóÖªƒmb3þy.­Z.$ œ¤¥ËðHâÜ€`3ô#§><åK^™÷”–F…Jò¼Tžœ˜¥,$f¦þÎVaò¤{’²R.e1]~\Z~J?Ž]åì!Ï Ëi¦?H‘&DÊ 8gÇBZ¯׋eä]û|ÒLé‘·›gÍs* Œj˜‡Th)•±Ï"4Ææ$†áÜËúSˆ;Òp¢²tù*‚ÖA6vÉŒ“Û#¥ò3GYÒáF>“Òg€V4HZz^BežbPQ ²Ü•3mrÈë¯æM)â{ÞÉ’kâ'¶Ñ5ç*XqZýÇRk}TV©¤Åæ&۱ˉßÚVÎ8dl—Ë*»Xú+™U¶†Ÿéú·ô¥#¾ôp+•(ÎæKJÒÐI) .„Û&t¦’)Äu†ÀÔ ©üÿóh;²ØsØ;ûðÇaý1ž6~c¡·œu` Aä,u‹ÑkNÈáÏЃs¥x-Sän*\ Šœð `´KO§9œÉ“þ§Iªü=¦Oh…ö©·„íTJ¸©5T`ÏÁˆ{éœê! \ÒO%@ÿ4MM¢4ìfP!(C–q%<„÷Ü_ùµ¤"g«m¬%±£z™Ì‘E8ÓùMÿØI‰¯ïH3,›¦Bdt5‘ç±³²šŸ©UÓñ`"0wrÍ3¨UeC1,dÒ‚I‚È_m•«|©Q:ÖÎÌð0ß™ÙÞtNSˆ€EB  m¨âäàDÏ1­íÖýãT†Oþ‘! e#VÛ˜Ùà™äq@ãŠVFÞrmÝ:ÓAA49—Ží¤@¤“%UGž²8 Ì0øñQ?T ¾šzª‚Àw?ƒ \~äP.c]çÖœê¸æÑf î$8f‘¶uö[ _>T=qÌç/wFr¡¤É Nø #y‘&žGv ¥íLg: éOÛm”Øt옇úÐMõɨvŒÞê$.6Ë(JKWú‰lÞïÍ.BJÞRá<öÓÞÊì=3}QYIÑ߸¶S&˜È±Û º²lÉ´Þ¼™|ëÖí¥cãÖ2gÎmäv+ãÇYV,ÝPFŽRz÷éƒ y·@Lr´çÿ#«òi"åöó\òšC^`*¼8Ïÿ|tÚôŠýßÒiŸ‹^›VÝömw:V®ØPzõêMíÇ&æ`4å¨OKôWý±Í²Jz!¤§‰Å&dñ[k_ˆƧ¡%L6­N$®õ?MJ}³z^ë°‚&¦pbP®Hïeõév N_Ä–¤¾óÌ3(}ú8"žWÞÿ¾*‹/)úÐûËÔ©Góð÷¦ÒË;Äñ‚Xg•1õXZÒTw2¢§1DÚpÒ>ò§Æ%T6dŒ à[å ©ª«8~Ð+m„¤­žÑL®m£  )¹óç516X6»q,KR¤!ˆAÂ%©&Ò.ˆl`ÕÑÃ,c¶òÄ0¦™Ø\ÔdJ4m¶F°ý0Ûœ›¨£½8¯ý³†>°U6®#"0ÄÂôÅÈ‹/Ä&#¿‡ºsèêÑ8Ïs\øgCiõ7ËNÑIJ9ø«žiç%$í-½œUŠßšDzë ”@@žMÁßÉ\þâÄGá“r¬riG·­µ|2Gƒè³]­ÔB“D›F9¤¬åYý#ö¨ Áu@g{«¹£ Êõ?>\ÏÎN06RVö: ÷¿r4¶Q™çˆa8k.ÉÚ}4ðIà¼6 €Šû,¸çÌkð;å”PW¼–“pϦáõÆÍ›ÍÉm2ºùz–\á…|ëÙº_¿¾é0´xϦßI¦•>q gÑ|N™;‘»œ<Ž”º2¿Ë)9Ê+/—‰\KŒ²õƒ¬)tAaTוã  ŠøVgS)=ƒùÆ(pí ú©ÀY_kKNòÓœÙláTgúvûPf5¥bm³Vñï‘ Ç¹ƒENç(9Ê-0·×áe',kçHj\ÊkëÕEçð&0,Ú ŸsáÓúh ¾2xR?þ£¦„¬*?¶ЊHå«H\«~tUs`,í!,5*"om ´*YVÎÀ4ðÁtFÅÀ O\ŸžŽ,×Á‘¥*Ü…±‚gÖÖ†ž$P©ÓIÓ.m™`çlJÖC­>˜©Œm ßµæ2²3KMàP(! Ÿ4Þ²äÒ Ñžcð‡€ö¸Á%lUÛ… 2“ÃGº9ÿ„ÕY4®ÍMÑ7T,-åÚŽ€ÏtIBt&Õ1T®³P|¹Ú9å¤ìÒoüôÈaX=ŒšßÒ«r˜gÀÔqc#’“¥­ÈhP££ç–¿*ä%\êܺ¼‰dv"úžƒÓÕ í£½\·KÉÏÇ |"ËåRb)G9ÒþÚÍ÷‰™oRΤ ¿8 í¬šqÌ­-Å4dæ‘ÞÙqËšBfîàÝw@OfI+Î6¶¶ôÙ—ÕË·•-;îi|ÙkúÔò–¿¾”å0;Ë¥—þ »àô.óç.+¦ìÁ “zu©ç iþ‹¿-,àω÷|ù¦wÍkñÛ´ö·MoMáøÕ2Ï>Z¼ö·kþ³ÓÚëgÓiÓŸë×´ÖF]Ïåó\tZæ·ç-\{ÝUŸö¼…¯õÆAÚ¢§——•K;ÊÀýʶDÐÿS¸F§hú©¾œIa¨ÀYó̹qÊÚ•ÚÂuüßL$a M}9™ ëFfD¤ÿ%ˆ@IDAT% #ý°»w»è'ôÑt ŒYQ«Âú¤`ÆÆFo/ñzx#+ç ÂŠˆk\”d¥ÊZOÒ®cvlíVÆŒ\î»ïwåío{Ùk¯©å#þ¡é'ô*«V³$Ê6S0B¶]„~Øç-/þ3kŽ\hüW¾lÖ€ìycº×»-ƒŒ^ƒŠ`éƒWÁs)_ÝAJ»¡õ»ñ½¼‹+ÓÚQgyiŸCFxuhÎÖ&†4>¨)ITi‡äL(ÐÆJ_=œCñÜ6:M[k?aÉa+™Js“cÙxaX09,¥¦#JóA^ÿò,šñ)¹¶–¢6f·ÜHŽ?é7èGð°ÍÒoôµ´mö3Ô!ràÈÚúKB×4u ~5`#Am ƒJC>A¬¶‰ni£MÕŠè…À¶ïµ]±èàÃÏeªº’ö—oÛOJlï íS~V-ùdÅ3iyh›$ûB­ÌzJ½‹£nÐÀ&ic «LúLÚ|S¡ÕìTU”¡ž óÿéPÕÌžkðp­ZÊ»ûÚÙRyƒÿ›çáÔ;0†Fp]ªMu:û\{8Ï Fòëɹ¿¾°¬'·ã”¹“~ #A•‹ÒOžŽ´ä»mû¶2yâÄ@Î_°0|´Ð5Ш‡xv$=ߺm+£ÿ^eÿYû–ÅK–°=3$\Û ìJ¿>ô…‘j…kes›¸?Ô'&iÙ?篺Æv‰3 [g¦Ü›%&Ú›LuôW-(¬ûªuI'Í¿tŽ©½5 NšeRåð‡Š• «D0 Y‚×'ÞJL” zÝH\ƒ•2·2%št<Ü,GÞAˆzFà­À dâò¯ãúfQñ¢k(z›¯:åµ–Y[ó¨ˆÊ‘JȯgÒExÈË„då Ë\¹T®ºdH²ºÔÚ£,¤å¨rWu*žz&à Gð”3u b¨³³Tê’™ èkrÍXY¨gsÍ(!òjéÙ¨æy~ùvÆ\ûº±+_©ËP ɦ,K&r¨ƒe¤ »/·[¾ÒÎ`"§Èà5¶±q³6&ð€«=3U¿!èVÞ*Wã‚ô’¸{ŒgH9 UÓ&~/ à ë¡Òç ñaIy$á:äUõWËÜ€©hñ‰Z´Ê!Þ„‹L‚“‡¥Ajõ¨zË_ƒ¦ 3€±aŒWmž„†‡8©s\[™©RFsõOº6ãÜ€®œÈâóµE:Cꃄ打g8·Œ­ßZÔ8mºiYHMGw5|¨‡®É  4“Z;Fà§jx+sŒ$=˜åï×·7Ë`¶§¬|={â÷`·—½<ÈgË$¦–‹/~73ÿOñ ä-åŒ3N-÷ßÿyKËá3ÊÊe›‰ŸKÊØ=‡òR¥¾F™?ÿ­,`¼¥c[Yµb}Y¾¸ÿ`6oDîÍóë×ò\ƒ1?JÙác5ëÃø‹ñ?Ë8,Z+°þn d|Ñ_k&¾©/æºñÛ††£r'/½4„¨¾AW¤È`~Ÿ¬Æ; ¯ßêZ•oíÏØ^)Žtpo>À‘ùÅ÷ODþ ìY–±ÄÇã¼W[xðáò½ï•ýÿOÄß;RW@);øÊxßJ bìÂo&Ê eÇXAw€­vƒ¬±™ÔRdI¬j¨x® æœs;¶êåÑw¹ë]ÂÚÑUõ“zbñ‰ùvôŒ•ä«ºÚø»t<¼"Xµ“ñÁ#í¾ºðI§3íiÅÍ@LÈ%Mã‰45c HÙ<í<´±rÕôèg°âË|c_õ‰ªƒC™Ó O™iS?2¯b~å* ?é{Dìk2_îZ–(z.cŠ)±…‡žrŠ“>0IÒËd>çhîœù2ÐЀâÃKÙEÉãLÆpßàÂÀ|yÚ¦e°(–tmôúyõ,‡Eâ É²J<4ð’éˆÎÃáè²–0ÕøT|AËIµU-õ)éµIk{:8@(ñ«sYéA¤Aºò° 67'®¶ÛSâ7["“|L:¡¤:¥òZ)›ÚÁ¹×Ê/Ùtœð Õ‰Ýplõ³Œâã2çËȶs×Dþ|¤ªì¸TéÝ×e=KGÇzÜ\æÏ¢ ê¿gYÏC¡#Y½¥>çž{þ/–ë¯ÿ}¹í?î)'Ž/óæ-,·Ýú`yåyo+óæ..ÆO–‚ÄÅ «¶—Ù«–•᣸{Šý,'m‘鸨%ÛyEBtõ7GŒO«×Шö¶|wƒÄ×P\Ý¥ºûhàcs:±8Ço!¦™sè{Jµ<¤”2¨¹õ[úä§:@ÒÅ“vÐÄãÊsË –º`µ;a]*dͯç vHÊL½"™@‘)~îE`<ñhèE>B.ÞЩõ`+e½vÍ6`»—¾túu×Ç¿‘ï™eÿýÇñï˜7u?D(‡Ô›ªŸõÚÃø˜3®kœP^R"³íQµAfÒu6êVî†!ä  < tj¤ï¤‰ŽÚÿªºË#‚ïó.a!gl,Kõ–Vd% uÊàB8Å'­…0"ôЧ<ùä5åsŸûj2dHù‹¿x7ÓãìôL=t.÷ª“±G‰ì :OŒBÞºõ³ô9”Eæ¹¾vüc?ËC•¥ùèg–ƒÉ†N\Ë0´Ýu$X>ÀUjyê‰Ägàæ.Œ2‹Þü¥ìI ékÿ"|ј¿‚¶qÑB“¿ÐÂx´í‹"äŒUå`‹Jÿ ?í* ªÇDj""_5F ç pð Œ:C¿–¿ñ öçôãFVùeà ¸<ò'q€_`ýFÚúcú/ÀzäN?ˆjš‰ð¼+"zøÉ“snâóÕP/µ­”ÕWÙe‘¨'MWD¹ŽúEÿª§xU ò4h1,Ìþ³Ã—ÿ1£G—óÏ{%ñ!l‰ö/Ìx2û$ß|Ë­å€ýö‹£<úØã¥73àƒé ï?kVyø‘GÊê5kÊqÇ]^pÀ¬ëQzø‘rý7¥cïƒU§žtBÙgï™ÑKø›~KΧN™\Ž}ÑÑeÀ€þeÅŠåw×\Ëú¼utÒû¥Ómg{3åyé™§óÚî=Òy¿å¶ÛÊÝ÷ÜG‡ÿ¬ 1è9/=Yf—ÞìvàáÝ-÷çvN`¶lÙZîºûžrÓ-·”—žq:»=Œ(/>îXîl ì翪Œ52þ[n…þ½÷gÏà÷ß/²ì¿ß,ÈÛËmwÜYÎ~éKÊÌÓ£Ã#èsýM¿g`ÔÏrÒŸÿˆãղРzôæ–}? éæÍÈ‹#õÔ«lݲƒ·n/õKÇm3 u¿~uçæf8/Ó›·£o7,=ÒÈ[azõí\;`ƒXÏëëÈ·nÝAP¬mܸ­ôgmd߬lDÕ?"Ö®Ü"ݸi+~ƒ,¼@%;fÐfôØ oÚÉ` YèXD71ÓÔ çS†-4úi³­È¾þýúõfàÉ9ôú¡¿Ê¸#GøSñ-Ko÷a`jºK\äoyù[^]¿“%[°G¿ýâç)ÿ¾”3upÃì¯ü”G‡òóg9mÁ_¶sr€Ë< [°MÊmÄfv µß¶­†Óåñ'VRG†²Gz?tÚŠ½¹eOLè Àìø‡Á&f¯Ú¸KÁÙy»Û9£¥(ðEäT‡´”ŸAOw“s€Là°>HÇ<³äÓœÆ.§1M°ôÜ\Ä2ˆ×η>„z!rnÐleB/I߆ÊRùåÈ€ žV9j€÷Ç€3–;«ÓÊr–Lš<–;´Êª =¨ßÚygY·²ÞIHû„4;‚mÌi±¸&Øü΃‹:s+5ü’ê×>ó¢SœáÅöÖÒÚ¿)6@°‰p΂pΘ²±ô0¦E(°/‹²È=ü‰Â,ƒQÇìªaùå®› ȧŠ‘må“K#N:0éœY@ð«³Ù– ú´s'0ˆÂØé¢œ ê\YwôÉÈåÊŒ"¹q6ó@wi¡:—Û)[ôU&øä®¬Ž‰!C›ôîăÞÄÇaCú³÷ýõåïø@9ã̱Œë åÚkZ>èT–Àl">TŸ2ÎûWÕÑR÷§«o*±yÔ²30óD¦~â§nOœnP-ŸJ$xR4ž„vРƵ$bCaÑUÖ>XZ3ÔÉO•ÉoH‹Mìç$Ï”\"K·´m>°’Aë±å¤“Ž/×\{w´®+‡rjY´h]ÚÙz§!@.ô)y¦Jâã•#œRTÖ7âX8‡UxYa‰à±_SN’Ê>Ï„€ÌÐB.5n©U5#¿”§}6ÍàD޾Pm@šöàÏ8b»&/ã‚õ3lQ NÐä [^ð°ÞA°ÆkòðS—ãæ¹©Š(›ð’qâ,‚¶6WZeÒ_õU3½6ÞÖgèrÑà€ Ï•òÔ%Õ3ô9±þ+$4 ¥À·ú Ï?&hgümF´iôpA~l,!1EA6Å3ÅuvùŽõÙÁGÞBì°Ì~{ø,ƒ+}›H·ÚȺZc‰t=‡õÛŠ×Èg¹ØÜos,:+´ÐUˆÄÓA­*i_rÁMÝo4C©m'üM€U5®“Zab'd­1EÐohbcr%žgÌPB9>Ï¡í` `fÿ¼?yYϬüïé$3¦¼’kß–øÁO|¦¼áuâhÛËu7ÞÌ>É£Ê^SG–“O<¾Ü@Gÿ”“O,‡|oW¼›ÎôŽrÔ ÌrœÏ|ékå½õMÅÎóM7ÿ>½£Žzaònäúœ³^J#õd¹óÎ;Ë <²¼êÏ_Q¾òõ˲圳ãÎÆ»œèœ³^†Œ›Ë7Ý ß©åäN(Ë–-/ .¢ã´ ¹I½dYôÎ(]×0¸ð5ç—Q#G"ãe„ñå4Þô¸uëÖòôÓOçŽÀ2fò`¼šAÏÖ­[Êè²×^ÓÊ)'Èëá–uk×—SO>¹¬[¿®<øàCåÁ‡./;óŒrÐ ¤!½¹êƒ®v`úó_2€Ú#ƒ…ç1uõ ½“cÛ³í¤“¶~Ý6tY[ÆŽœ€>ïÑåeøÈ”G2gö ÑÞeܸþå±ÇlÌ{”©“–'žÜ@¡o/“& )kWo-+×lâU÷ƒRéžš¿¾ ãõæƒèÎyl%½>e¼øsÖ¿“†}@™ÿ4Ï?П®G¼Ùô‘eî“kpñ]Ìœô†?wnºõ.ã'„VÅŸ8i@™÷ÔfÍ·•ña£­ì²‘å ÁÏv•yó——aƒ•ÁCz”ÙÊÜ£7{l÷/?æ-Ù]eò¤AÌÎl¢!Û ÝAÜÙÙBƒ´™ÁÖ`|vW™;o5¼0À„ÿœ•¥/úÎc«ƒ?iÒ`f(ÁÇ'Bkü×®^ƒí±á?¬Ä€göœåtÎû–qcÅw/èR‚?#üŽ2NïzðW®ZÏà’Ãçχ?øƒé|+ï^½±m¿òXö’.eJäW—íàÂöeú߆î¼5eè°e¹ÙÈß‹2´_µÕÿ©yÜUÚ¾¥Lš8„òßZžZ¹®ì >‘¤ÌÖ~#” Ô@¯>eø?þÝ(?ì7w##þmøô|sKY9wú¡škÿ•eð°~e¨úÏ^Uz0ð?ºO#ÿÎ2œ¹Oü{ŒêÏÎ.½i(g•«¯¾žfYÈaÈï]8­E‰aƒ¡ÆMðõ„™j·ØÛɯˑr‡Ç˜Óøv‚²­'±ÇÎf}š†2n&9NzÈà ͵þç:Çø¡×éŒSWR] –µkoºõÝ£6”â I£kZÏÉËÃl¤ÕÙ<)„ £á…NQˆkŸ¯QìZ‰ÂÛs°í Ϥ­mi! Òàæ§ò6É lR–É$‰)&ÚØ-¯ƒ×~j‹Ú¸C7rJžu€_• ÚB…¾öRF—%dI#ƒE—ö(¯3Ms› §ù|(§6«vØùåk_ùuyýÅg—sÎyiùüç/׿•n¸¹Ü{ï#媫ïäócà'•)Ó&B~bL¿‰òè±ÏÉ”%_ØQ•¿ñI»Û‰S¾5/åCz÷(®R‚a=Åç·n_\Ók¡–NúW=Kg@½C¸–—±,eå4ùŸ†3d› ÖÆiòk_äOçµ,*Œp)né¢áC¨’`ÀÓÁà7z‘ºÒ䵺E*-4õfåò°œ»Þ ˆ"À(^½ÛF‹Žà¥Î)n-Љo ¡qÔ»>ý{D»{w9ëìÓËW¾ò‰òÙÏL._ÿÆ?–}ö=ŠÁ3,‡éM;q1ŠuÆzâûF)íé]ý>5¦ób=D"û®ùÞµ‹ f¬O:{õëjëÔ=èàõ˜>ÍÝsÅ×F|·')?m¬/Hʲ’Ÿ`Êר™_ÿÕÅÛÀHÜ.åÞò©Oÿ´lÚ¸©¼åÍo(S&¿¨¬Z¹1“6±²ˆ¦,’·ézU/»rÈ™~™Ô.õhËD¼:k¬U°t1¦u"9ÔY;¹£M†ü¶¥^!¸"ÿN^*kTVGéx÷™Lðƒëó з´,ÐPä—ñ §X4²f)¢ЊÎ9T‰ƒXËÇkËÞ|Œ•·þj4øTþj -´LE ¾ñí ŽΫݳl‡Nsîäã«Þº#šßQÁsÛm€î=H”—ÍQÛù¹{¢æcÙï„@•IÛ¯*Ÿ:ˆ÷ž2‰Ž²‘ÎrбJà41_[nŒåUeJŽwÇh`ÂŒvÞ4ùjïTKø×¶PY´hxx¦¶Mrqpœ˜¾rAæq´íˆºˆ$[e/åg2 ZÂ6Ê( Fƒ€JljZkJðY‡3t+x5ú+Î9+â¿ýðG™…ìÇMå>>1K~–Ì™,i¹¼Ç™&…q™ÐˆÃËì_®¹æÚòm¶Ùr ±‰Nù‰ÇÏ[g ±ysGùà§>GGw]ùÈ{Þ”» 'LÈ2›«®½¶|î¿UÎ~Å)åܳ_–ÙþÕkÖÒñí—mÙ&ì9®<úèìòíïÿ ¬\»¡L™8®|ò£*ûî3³¼ö³ß,ç¿ÊÎûÖrå5×—qp–1('‘ñãÆÑ!šWÞvÉ[Ë´-oýË bì{î œË@çö;îŠ}|5ø×¿ùÏeþS&—O]úá²ÏÌå7W_›àpï½÷–‹Þ÷wå˜g•©“'G¿öKeÁâUåïßq1»dŒ‹Ü[ºÓæÏ²s{™ ÁE_mwßÈzÅ¡CG1ƒœ ¼ž|Tfí8͘Á *€·ê÷š6<ÎåúÎ)Sf×ó!Cú”{ `Ö—`œcï{ÔdÊF|·uFzú :ˆä;S ²L˜ "гÑC‡ô¥#9žÈýògÆ\ÿ›¾×È”…3ð{Ms™´¸;0i*ËÆøó®Ìà¡}ˈQË6ÒµÝô½F¡oeG~ümÛð÷b€Áálö¤‰ýðfݹk2|h¤ý3ë­ÿÏœ1:òºvÆtdÔ<Ëe¯½à/>2«îi$·–AÈ?bdäréü±¿³ï.q›>}«þö2múÐTæ­àO›6?r}Gö)cG ÂWÁ'‚NŸ>2³þÚofÊù›ŽüVC×`O’ÏÁÁÄ@gøð=Hš3gj¿mé`ë ÊáŒþŒ½†iÞàO<(¸ƒò<¸w³ÐûyÌÇ;.ù™¾× ôúkm6ûèß§<úàò2ˆ­?ùÉ óÖò•/§üËþƒõÞ‰;¡!OËJ\å÷wÐàþÜéàŽ¹[Á ëw­v¸ç<á¡Þ>¥)Çæ2KB`óA¸ÜÂ6g‚«6QN^‰_ð¢Ü*o>&™a$=aŸˆÙô‹Sn™ŸhhKG10縮Ñt¶„Ó”g42ƒ«àÊê•;Š©é s‘A‹:)|´‡iòÍÇ<2y*#o!€Õ¿l 2Iá¯(Ú*9œ“¯_!…ÂSéžËžÜi²à]¥^tÜ6nØš;K=vƒÈ³Ê›ÿú•,õØ·¼óß`ÆÿÅ儎eòbñùÖ²`ÁâòûßßÇÌð¢rå•ÿüøÈxɓ׽ûã„‹ƒ;×»nsð”Ý?Ë3 •-rº<©6CêÊŒ¹WMoYµ«ÕÄ-ƒ}„—Ž×Ý©¶mŠøÃfÕòKùZF–zW²û ¦ö‚èÂ46ª·Ü8én…W-Mã‘æWœJ͸'„ùÕÎMzd"ÝB’}À:.ÿÈD ™Aäø„)ÐÌ-ìa§Fë*耣ìö9Ä[­ìègý0:)übcy7ãÔ ²Ä‘ŒYžh‡¶.!dà­çã‰ÓwÜñt9Ëw¾ó/åýx{9àÀ}Ê›éz(“!CËò%)ÿz·LU¥ç×΋ä:ãJ‚iðË̲É«ú“ )ŽY‹¬OÞ¥ðî‹ËY·~¡£¶ôB?º³þ%³¥ÐŒ?¯OX>1jyR©ƒ/u%¡R)óÇ,÷ÞwWyå+/*‡zPùêW¿%G&9ú–U«6¥îDØ*¦v“žÄŒ®ºI`¸_óÓ7Iyk§tciÈi™ù섾խY²“úДr:‚ÑW~^òÝ*¥fuVï¾ê7ZÌM‡vj/}Ï‚GïvfYö5¦HÉ í)M±í¬B9´ô‹jK% #÷ê«Ê ÐÓo !%ªöÉ¥_’®õ;æÖsm—;¬ `Ê…ú$°ÕÖ*Š.ÀÙ—Èv–«1Jž¦žÊ7F‘:(Ú”H8 ¼—©Ë i Ê] ª±W&‰ ádNk;ñS¶úôùÛ­©’„…` `ÁèÜà ÙÍ• ê,‚JÌë oÜWLËÂXÝ@·n*ɺ×–§åT¾ÅåZÛFF±% ¼$õÃ:Y+J$qY=h±›JFô"A xáÐg~IP.7nf–slY±|Ef|š1ÙÏ'Ê#t¼½ï?a »#hò©NÎzC:ãÀÆëÓd9ΧYähZp½èŒiSË﮾&w~ý:Ø+WfýÊß]Sö;†W²Ï)½þõå%,ÉY¼di¹ò·Wá”=˜=D'hkÖä?9w.ÓöòŽKÞR@Ç·'ÎÚ13~K x˜™F¤³CoÍÁ‰³!»á¦›ÊqÇSnfÉÑ*9·³|çö»î-ûí»w :œûo|”øÐò®·_B§ØŽuOîÐ e Q?ž9ذqCy’g Náát´†—k¯¿¡œõ²—”ÿöe œVò€ÑCå翺¢ŒÇ–PÖª?ÓÌ»¯(ó½»qîŸÉƒJ?)ç½ò4fêN._øâ׃É%—+suùÞw®,_ûÚGXžñd¹ø¢ ÊÏ~vyèœsΙåßøNv6xÝ…ï/ç¿öÔrÚé§”Ïáq®·¾õâò›ßü®|ÿ{¿©øƒ±øWÄñÎ>ûŒrÙeâO+Çÿeè2’ž,éW^ö²3ËûÞwI¹âŠß•ï}þ_‡ƒÿóŸWþgŸ½›ÿðÍkN+§Ÿ®üßh俈ÎÅUåû?¸’Ù¦®øWDþ³Ï~ òÿsø_xáûÊkÏ?=òñ èÏqÉ%o,W€ÿ½Fþ'¢\„ü?ÿy‹_åŸ6mZQÿל*üO©üÅGÿ+¯ü]ùöw[ùŸˆþ?oìw¶öS|ó Þ×àŸ\¾ûwƒ¿ö¿ª|ç»W”¯ý£èÿTµ_ôßUÎQÿÿu¼¿¼öµg ?úc·5øßƒÿW¿öágØ7ev·ýÔ?ö³ü¿pYAøcÿïFÿóË'Ñÿ/ž©?å?ò{ý…ØmÿÏk¿*ÿÊOùi—ñ]tÑ_”Ÿ!¿A®–ß·éøM+ç¿úݬó>¾ì½÷ °}¹M~P9ýŒ_–W½êR9¼•I=‰q6b„=rMtÝý5‘Ó!<:a?îšÐà±ê¤)¯ lÖ³ ´éšN Í²£D+C&ø00Š‹ÔŸœ;ŽÏæòÿðÅ”áËÏebdÖÞe_âÛa‡ïYþ鲟–þðWeÞ‚UeÙâuåºë®.{ŒÞßŲ¹3˜tàí3ãºÏ{ø¼M:+is°IråWpíÖ4Êhla ã Xµ¥vÀv6œÐ¾³slÇ¿Ð2êžü LUöÏt~Õ¹NJ[Æš ’…ÍC °’…I-¬ô"QmPý­t=×±"53Üä[F¦×Î ²&¤Èy§èI]4€ yº4úÎJÃ|(Õ*ŸéÊ-.XžY³DFå¥Mgñ¥zÝÀƸŒ‹ê‹ óÔQEȬ4ômbYáô#ˆñ/*\p^ùà?V.¼ðÕåòËWÎ<ó5åþ5OQç+K—ÕçûìФn@ªÑÊEàP?.½ÎòéÉ(ÆA‘õ¬–>‹A2P bµ¿51bcý"Kj¨Üòªúq¼ô­x W•±6T_ËUž^JXãÜ%™Õ`+Ë›Þta™;w~ùÌg>ʄʋs—Õ~‰KAº9Cª4ô)LÇþ0­üšN°öƒ¨²È+o{Åðb»äÙ8,þ¦D®d™ ÊÂr‚' Ä-ëtॠG}ÀÚ¡«uE:ÚB]ëRIË8®‰½ü­ìùæZßs»`Ó4„vE?"·¦$[î! o[/* ÎÃÝ‚,1eT.ÏM¬GªYÖ}-"iÔ«‹é#R5´±C|¼^Õ²“¤eŸúJ*åœtí õÍ#]Ò$KüM'¾µ.+¤2/Cˆ3–¹tIˆÝC1ׂ7¸–g*¹H,8g’iÈ=Óý——>×b]¸òIK˜@`îŠKZT -{ JJU \O  >P;üÕòÐÞ>Wg9V«‡}b€X\> Çñª`&ýÁ ±‚èÔ6Ä ì̹ýºsŽŒ $ë7Qxܲ¡Sœ žË£kî·1ówÏ=÷2Ø”õü®öÁ]×ÿ/šóxùâ—¿V?ô–YŒ*§zr:_?üÉ¿•¯~ã›åˆÃfYȤ,íyû[ÿºüðG?áIýu¹“°rÕêrèÁ/('xËvæ³J„ßç-t Z·Ÿò ÿ¶ü´Ÿü=Dÿj?íõeeù/_®ºŠò{ä‰è¢ü®Í¶üvÛÿÊÿ|pQ|$å/þ‚å`ï ýÇà®ý*~µ¿u9úßqü•UøßC>^Öq7L†·ÞvÏÉÜW&²äjÝú­Ü¡ë5å>«ÑŽè: Þ¡ùà¿Ã ™åO+WðèÂòÔÜE4¸ ñ«…eñ¢ÖNì<&s6Œ:Ø—A}OžéÁ‡æÃÀ‰Ÿ8“Ÿ;]¬[÷eã ÿéŒZâ êþZ)øOpä:ƒ†è¤æ5x×ík‡Cà4¸Àêá'¤4¨¶„QòìÈyîŸÛ Æ9×.^Õ/ ðÄæàË 0~ø¯¸|§9É:ú—Èî¹rWˆd'w:^vF yöƒ:é3>ÎYB{ŽÍÆ}Ë~³Æ•[nyŒŽý׳äð„ß”‰ 'cfî3˜ÙÝ/¿ªŒ16wÂV³ÄpËÆ¦Ùéï=†;€<„mÜ!‚EîÌè"ˆ‘Õ_µˆ<ÈŸNE5Eä÷Ú2ª;p'<¸ÐÛ}'¤ÑG5ÕUjÉÏ×õЖΔj#;dÊ$¡ØD«’àÌž ¼sä̹hÝOÇÞY5à­‹öŽ”Á¯4–âAÓ$ ÉúiGÌC>Jhš·)ôAÏÍU£øŠ0êOzšh{!v\ã6¡ªXÑK¹²dL ÉâË‘d šo³ä§Ï§Îõœò¤å—M»zèWhH{+‹(µ“ŽLÎÁ“•½CÒ|ùœúœÏ<ñÌÒÁV>úÑ¿)wÝõ¿ïeÙíïÊßüÍ'Êoû/å ƒN¡“̃¹»é³AtØ^+Õú¤L1)æg·+#«i‘#‚vÊ¡W}†’$/K€¨çÒÖö²êKš< OfÒ•E ò[G¡Ñßúd9é'[‰#Fö+÷ÜýÛò¿ÿöãLfŽ)ïz×߃2.ƒÍ›µ·4 úÚY¬Á?å@~;ó~ˆ¾–{qúOS^¡x¢ JKk7ø!=ãÏdÆ?‘Ù2Œ?ê;A½‘ƺÅŒoë_ÊøxÈ;×Mš1OÿSï:ð`–_›s-ŠzÚ9‰3eJ"º%)²˜¬Á«ÿàß”«oŽÎ_½òÛëðÄlj )èñ[+ra–ÂȇÃrR¦ÈÙµ‡ ›¯­€ÌGðÄzäq0^c \µŸþ/tk94ò†ŽåœD…‹wÁ"vŠMTžÒÕ N¥BîZÄÖ2NáòKlÑFP–žw³zµ´ä}‚<Ó”¦ÚY&Âè1œ‹ï_uI\kª'H5u= ;\këÄød­7õáë*r·iӎݵb馲Ïþ#Ê~Næ)\8=×Až3Þ yIƹg½´Lgûï>|)`K:ð_øÇOò°hof—öf[¸ë(œîåU_RÆ^ÞûŽ·”Ã?¬\ú‰O•KÞü×åò+®(ïy÷»à2¦|ìï,‡rpùܿ ïyt´7–7¼þíä­-_ûÆeåx¾ýñ¿þ4Ï |˜«¯¼¢Êiå_þ<ÚþGùçïýˆŒ÷)÷>@°ú»¿ÁJ;Ë1ÇSº›YÎ:êàòÙO‚ŽÞÕå¢7¼—5Ž¿Ès oc‰Î)/>*­P¶ Ãy¯<—mì,xß{¢ý¯~}9ë¦'”K?ùéò±ýïò¡}¼\ðšós§áè£*eiåìã/êý³Ÿÿ¢ÜøûÛÊ'/ýpù·ûi¹swûñaä ì ô†×¿š«™åýzvúÔ?~6wRÜ1ÉÁžÏøB®8¥{Åå·±£F÷²nc ²¬âJ‹‡ëÖÝÁõ~4ÚÃX¾tç£Xʲ/k{o©™3^ÌÚò9œ/âî1t„}6à!ú:"δfõY ªXë=ÏuݘáŽÎìk9g‰Îôã¡eÛŽE¡¸¦¼øÅǰtà½åŸ¾ùÙ¤ÊHÖŠËs wrö.sž¸>y3˜Q™3{6ç‹áq<äÿ®#tϲv½Ý}x¨tƒ„›9ß“eA3ó ÌH½˜õðp¶´Lž|,Gñä™ð© U~ôŸ<´Ì›+þIJ÷ÌiÜ-ºŽs—!ÿù«ÿ±èï³”!‹¯¯Ûx'׳ʔÉÃé˜j¿ÑðÜž×sÿéðìaΖÿ"ðÙ½ª<ÊÓ‡3´æ™ŒÈ¿/yÃÉ“?úƒïƒd3NkÿÉØ®ø”Á§lw•µ,?ð'‚ýÇÂoøWüéÈ_õ_R¦L=®<õ¤ü‡yà|ÊãåߟµþCËÜ”ÿsÙÏÙßåe"ö›?·ê?@ù‰7Uü‰“‡Wí7cæ^”YSþ3Oà\û/¦SXé¶½GùÈ'.`@¶€]3>F:Íæ¸£²Dí‰'){Ž™–eR.Z³‡B' /õÆóʬýf‚ß—;u<¬ÎÝ#—´-[¶’Nkqs;ª%eá¢%<‹³:ƒ£œÏÀ”!Ïðˆ¾lšc¿.ïJýTFíÑ/w¤²dˆFÀZäÃÊ.sr‡ÛeÄÁ&òѰ¾ˆÂ»èܦ2xcÓDÚP¢YÉøoŽƒ÷=ªÃL|‘t:£²0›Ò$¸æUºmÇU^5øÃW¡øÏLº1ÝÆ$t-Ý48§ó=tcðÀ}³Tl/àš¿ Öûq{Î(/<ªÚù{ß»žûùÜ•<á„7°lëÍi$¿ü¥ŸP‡žB¾eÔhžGbpÕË™/õBN;>Tø±Rĺ„§ªÕX©ÓfÆ[j‰î6\"qj{b£WõÚ†J fæ?0àpä Í/í¡Ì…,8²µù>´ÛÍu´º„BÛB!¸μҎYŠ…½´ 24‹Ë 4r8™¥+Ô;B¢s @v!£8æ»þ8V %h!Gíøk¯ZVò³ÁN¼L8a‡!í+çúw$Úíø´}§¨{:z×D¾6wP¬íô¨Qô4bõhþÒŽ+ªð¤Õ Ѓlx¨W®µƒuHßôÜ"3fƒôûaÉì•?¤]ŸÎÝàïñ€ðY:x|:Ë«¹ËäîQAnµW­#ØDào¾jÕŽ’Û›Ám¾`zE䇎åU;K¬ ³vlýޝÀcëÖVÂb®!_౤!à+míÒy—°”iݺ5<‡øS&°f—óÏEyÁ§ägÛéÔhí¬©…â˜^xè“ñíÆ¹ryâL}œPm’þˆ[`´o|ÄÄ(£þÊ©?6òFnQ ͲVwY^{‡‚Ëz.nd©úŠU¹{\®„‘rZÂÀ_åNç‹_Jà‘oqZÛrª晩êöôEú°Rã>úW¯)_þò§èûÝÈ*…³ªÏ¹±ˆô”_¾Æ]µI™Ê[÷¡Ço–w\„sÒÓ‰—„¹áÉvbzª_ÙíH#§!ÜNqUKi¥ì5~Ì]í‡âÒÁÃÅ+|ì^íàý25¶:yàR}À‰tô‘­µ`ì-è{‚b˜Ôé<ˆLÛšåfj./ˆ7ºª£´”X%ô…žÐ³œt‹R;׺.° ‘ûhMãH(¨¼&±Oâ•ïêËó-Ð]Fk|4(ƒü20¤s³k%»LŸ9¬ÌÚŸ„8€y¾C\×oãúfàåvç]wÓ›XbYÏÒ¥KËy¯û«ò†×ž—¥:÷Üwk§;èà’ïW_ð†ò–¿º¨Á`àVvçqtuć³¾üÞò¾}¢¼å QŽñqt@çäÁ\vÇŸ¯]ö­ò®w\’µÇ.5šÌ]×ìÿ‡?*OΛÏ6¢t x÷åg¿4´ï情çNEûî³7kýî/ûÑO”ÿÍ{yâ|ùÙ/~ÉÎEóô„[¾b%ϼ´°ÿ~¼ä~œ­{ÞõaÞüøßÊg>uiÒ½{!Ì=È«cùðó¬Yû–ÛÐå×Wþ6Kƒ~ú³Ÿ—›o½=úŽü'žx<ðÇØ=cm9ä dyÑWÑÇ;ÚÓàý|GŠ?úðSeÎ#+YÎ45z3ˆ¡ã„» e{¾MìÔâÎa<Œêzðu¶æ\'Zµn¨öaÖºgÖê÷a w¨Y»žž6„%›6máîËvpÜêoÚ­<ØÙ/ÚUV­aW%x bï[nù%Ë…®)®~ó›ßKcð£rôQ‡–ËÖgž¡xÃx×%ëxØmúS4Ø û±&Ôu¯«yxÚ·Eº~ v1ßgÄïÀnÚizˆ?txßÐ_ÃC׃xÀ×ÜM£ú`Û9÷—¶öEç êßᔟ×õë‘EûQ«نÏoÝùg»#e±|½Æ%46lκÿaC‘û­§l}­½awͪðÇ~Ƚzífèô@ÿ>ÑÅà¯ÌéDúLBø£Ç:à†c?ékóÁì.åîG«×nb׌,õ[LΣ|¸?pÀ©å¾ûa0ð2eʤrÖY§óŒÆ¦Q®#y­ؼwv‘ʳÌþíàn$ወˆw4Nu Åa…±PùI€Î)_ œœ&ä—çðOH ÒlºôYÏ%¥ý*¶…צÃy Ó¶xÐ#3Óü¦A¦ll7ŒMvÈܽ©7õ­ÄÞåtºÖ¬tà<ºLºO¹ðÂ32ñ±KÁú_̄Ùg¾…uý¯¤no,ÿþ³[¸3ç n ö~uÁ;*lš@̨³Q´ØÁx’¾ –Ž,ç¨ÔÅ&\ä¿jÐÙI·1BI;v¡B(Dp9WéÌf{N½¬³ÒZDV~k7™…9¤¸æ<4ì|dp×tØ)œ”‡4‡¦m_fJ‚t¹*“´÷*¸ĤÜêçú .µSê‚JQ^îJ“Ž–ÎúïŠ,–¿ížb«€üÔ®­ U§Ö.ú’¹æƒ/<€Rßv¸ú”i˜Ä¶|‘ƒ#Ån¡ ’1Û—~}ýëßæ]GÐ_x=íÞV^f7˜XÌR±q0·áM} Ú5´ûziu²#,] ÚŽi+gµwr8­e! rëË–o{(µ•Nµ…º%]gSÓFþ¤æ²–´ƒØi¸ª}ü,õŠÎžu;¶Y&‚sHÙw¿© ÁÝ=çžuݾÛyަ“탷#Ø*s,·žÿþ£Ÿd]üˆr4ëàÝúÓ-Cï¹÷¾,x$ûõ,…9±L›:•8;™0CüÓ_üšÝWÆÒ1]_Ž8ôà,Ãñ…[KPüæªkèT­Ï-lwr ¾;þ¸ýæý>œ-:³ü¹\¦#Œk–M»ãλhìé´³-鿳óŽ!ŸyúiÌ6?]®øíUY‚Ãbàµ,%:é„ã˜AžÚݘžË3 ×bð]4¯üíï2øØ;¾:ýžÐÁgí»OùíUWgPòô‚È»Ú2°8òðC”/ PЇ%"W²æÞATß>}«S§xþÐÒúGÊ“ßî{¬<õïQ`»¶tblÄëLwe¸­ÇˆX¿’T7dÖ™º1"ÜÁ<=\ç™™"‚½ÛjÚ‚8skLÙ¹°ùÀ®£vT|Ò²-åƒ]ÝeðžÌÍšõ*êV:*tDKy’O×£WÒÿ rÀÁ{r7g\™2ewEÆ1Øe‡¦Áyh4’Q® póP±„•+×B`'†õ쵘ÁÊZl«³kÍå—?AžägSITÆŽéŸÁc?¶@T¿t€(doÆå œpÓ¶ñ ì ]Òa±œ,Ž¿Ö7í“k~µ÷.§µ€³ÈHª‡·ìùÏTÙvø%¿–Ÿ\¤Õ‡mg}ÖÆc 3_–û¢§7áSÞ­™ZŽd õ6õv™Æ±tä-üI_̨‘2áŒÔZ2ÑÆú¡¶ðÜ¿w˜|g„ï½p9ÔÈá#Ø4ÀÎRJºœù’}ËÛÞözÖbßS~ùˡ>ü6žÁ¹‡ð¿ÁRÌÉ,e˜_&OaØSKá7¤Lžj r‰ÌβÝzò¾ÊÒî*$aÌYâ×,¡Ñ·ÎGoKGVÂO{à›y»0Š´rˆ¾tøÓ_ô¥ø3!Ï—guí®>ƒø§ü=¯ügL"An‰¬u½LóKJ›ÕêÉ`XÓ¸™›6*D5•.¾¥¯¹Kšre²³Ì†qmᘞºGº|^Zµ €GÙ*#IÓWcÕR•½¹€œºª‰ö%Yÿæ·ÖÿˆL¢ù& ÐÀBW^‘Ã4lšòƒžæËµì1ŽýÐ7 ©˜ŸN–ðœ+¶àâ'&A_–QÁtBEØ‘ ý[Ù+Ý:øÔ¯<ð®îíw\ÎjÓÊ?}ã£iç>þñ/ð|ÉW¹xJî>¯ç½+¾I8|‰Ï–£\Ê ]RÝ'ß; Ê5€-Àà à1ðÀu,:• Þö?…¥áÜmÍdC†Îe+µ"C ½ð%sýl-“DS÷RN:åHžÕ¨/ˆàOÇiííDÒSOÎ+¿ùõíÜåbÒž)gh9Äö‚8¤ÎFH÷ÂŒç9œÈ‹¸~øQf§•Wñ ìËÿliìýÎZç_^~%û¸ËC°cGïíJ¯V˜úÂ-+È„qc«ƒÃÇÌ6:Ý-ìv?^ÛvüöÝw§ É“&Ô £˜/žòêªÖ_¼,w!Ò Ê%ŽœËm¤#©“&¦sžY©VOë>¸{Ža¦Q™9Z¹ÄÅ2…°Àá}8¹…‰S?”Ùñuçï<èôÊ勱ž¡•Yz.%‚]•»•áÙ¿Š!Pœ··ÞI·¹vB ÀY¯Óùå7U[¥ÅØ–’ß`sjZÍÐ_ ìDÇt”#¨œ [MÓÈU_6ÖžÌ ScPØ×4C°gU#¡ §ÑOÊUnuª‚ðòcbGÅó@‡:â¶ìl4±@•ôä¨NuöK8ƒdµ¡}}y4£Šç–n5x±¯r£¬êŸ¤'ítx»^;8Ñ´¹ùy{¬¼¥¡F¦Bóoš6Oc’‹¤gÊÑ’ ³h;äÂÖŽ$8ñíh%#®Íl'é)¤e`:N®X†ŽìøD¾hVvÂC0-'¡BBÙðqe€š©ÑßYñ>YVUÖçAíñê€ýQš·óì±ç°4àÞ¢ùÎòÝï~†™®.gœ~nwt¶õýÞY‚<‡¾Qù:H¶8¾pá’Ü=pyќ٠¸s8ú øx·¢=q2ŒÏ`–«ñüÁè¾e ñD¿ÉÃè ¬up ëòmí­)li<󳪄:h í€ç~žË9ˆu›}Ê+^q³”sË9ç¾ ¼ñ¯É.[×ßp7ò­œ·ÞúÏ] [´Nž@ç¿6”ÀtѾ…¥µÇP׿=)=ØhBdÛ¡ÄÕ0’·ÉÕÊæ§}áÄlT3 ¾á­Oé#ÚÔºdçÐ\M¤j¶ñН.¦‰'„w—õ܉=)âȈØ|äfvýywv¾úä'?ôàlJ±™ºB1…?‰i/h* #Nˆ‡±#ôêDMŽY¶×İÀœÇnÊã¿6·¶ 5‚AI”æp°µdɺrà &²ÜnTùó??»|úÓ_dÀzv™ ­Üépó7”—½|ïÜݵ{Šü§ãÿÊN@Gfî³W9æØ%å{—•‘cfYw¬\•nFw,`ýg‡7¦-,ßd»Š[³lÆ­/Ýã_|òÝEÖ…VN»^?ó¼Ët±æà¤ËÕ3ÏuØÝ`í)¿»Óëi—ë.PÏ}Ú¶ ý®4»ÊóôŸÓrè‚ù.W-Àü*Emô*g+šiâÖ†¯ÖÍV*Zš&*’ ’#»4¦V<êamæ ëvl• 1À¥ Àذv6TÌT\+&ggªV+¶³3vÒQ>~¥Qœ6O8R%h‚Bü°ÁÎ8­ýìÈL02÷™<Â/§$"m󸝔±×áÇo½€n`kð­ûƒ+¥#m‰kE® †Y^x7ò“glíør màV¶,k_½ö°¡OƒÏy-Mƒ-gà!Vµ#|´ºHu°Í*]â>žJ9ßb[é†8.לÐâ‡Ó´ l‘²iRЧ¾¹å®ŸÙùûÞ@oŸßqǯÙCþKåøãakÏ—ïÿæò’—ÀCÞGÐiÞZ60›í»îºëïað)v¹š÷.œD{ 3üý³}`&ò BƒKõXd'Ãv±Þ@®Èˆpðç|vàqFȉm -gwÇòÐñDÞnÛàÐØÙ}ÿ”—/…óïÍ|š½¬Ü}Ï“ð]ÄÇY÷®ÇatÂÇ”)<0ÙçøÐo`ž?˜6m2t»°+ƒ.çû°3Ò9Ìœîþ§ObiÓ‘eÞÜEåÂw×#åC;VŽ–>m\Ê~£bWò™â.îHäðÅ‚õE©©[] ÿéü²€“æ½XÆ:qò˜rÓsËc%^å=6ž–©•„R l ºÃrJNß¾¬•å# é`ú—¿:þßY@ÖÎZ¥ã –Õ¨š*SM2RKçŽÊh£EÙd» ¥ÙâÙó2ÈBØJèLÒx¥µƒ.0 „YÁ áC‹“Îò¥ú‹Kz$Wñ…“62(O˜¥K™jüA®ð Z|‰*#ú Àsí$ÃMÖ'W”j“ÀcÃjð7DÎrèâÈâÍÎíuía&FJÀªY±G5VÕ]zéÜ7Þ`ÖÒ¨þÊÓ œƒ_ë‘w[ü5 ÛØ&¸š¿t(P;yc²– ¡Å3]=Y‚¡6Òm>í,uõôߣêÌES°¾lJ}Ò9XÀD‚¹¤#\4ç7fmÇ»éÔÎÿ/þ_åÜsÏ*?øá¿Òùÿ KjN.×]óX™0‰ñ±û‘kÎÝ¡æ°Ãa°ðËÞÍr O3¸†AÀ‰LS†ÓQ_ÏJô§±'2R1¤ì•ÓktÎVo Û"Dom•P¯r8«ŽíXfã€JJ…Ô§SƒyVgÈ`ŸÆgBò|¾'vrP±–«n»m ·|}Bä.‡Ö;Œ—!ÍäêžÜñÎÄCyCôØì\4mÚÔrø³;T?v‡â%I€;[¶|ùª<±yóÆìd4š»£{²ÑÁ;ÞñÞ=•—líÁÎZOþöÞ`¯ªº÷Þ„Ìd 3áMB˜çD@¡ˆŠZ½¶µ*–ÛªµÖ¡µV[Djµj­­ÖÖëtZEPQ@@d™!1@Bæy ÷÷û¯sB¤õÞïë×øMž÷}žçœ½×^Ó^{íáìχ™:ñ>œß~.î6P˜ÈÚ›ssRóVoÑHÙîˆþdF^mcRå—òuòœgíÕŽ»—#Ðy“beLœåËmX[ëGÙb‰³Dq™O1M:¯ צal°¼ÓÞø“¶ùšÆex)zõ–¹üQòG<”Ë¿qÚ·¾*|ñ[­ í¸Ã™°JSþÀ{ÒÅfHåÀ“æ™àEx¢å§ Bû©KD?D~»ƒ‰»2-ueÅÔ¦¤À„Ñ«IÖû¡*§¢‘—Òs‘ƒ „5ÿâSöÒDÐIjDÖâOq þ‹/÷¾S?¿èÈòA¤2½ú•ã¼edÂh&ÄßÑ7 }…yj>k¿.ˆ0_Óêø3©ÓÝÜšÑ7YÇwnûÂç?Çç'ìþò|Ù~hûà/f˜§³Eèà¬Ê›?ÒšÍ`††´('JñøVù–¾¼@Ã|$¹ÈAõO˜‡}Óp§Pç¤Ê_ŸÏñ#èD:®_»õ&ÊÔ´S™›Î.F?¼ŽíÀ¿Ê`Åo°Ë¾¼â”– S¤}ëdXÅiÀ¤üpŸ7Ó§®«³,ùñY½Õ½òäJn/dò¯|tå«pæ:’hò[$õØ^o쉕­J?mp„+xо}ãh'Îíc·ØÀ|GßÁC:èX”Ì“®ò¢ÏmDËæÍy¥4 çRvºÄ-7ý·yT6­À¡Ñ C2;ÔO›RÛ:k¯¶IÕ寯ÿëpÚ}ÍÈ)Óî˜rEFWîì0ؘF÷¿ºúh~Óq蟻¼Ò~}ýh=&@e¾XÌ8_ü‚ñ|ZSƒ²ù¬îÐ ƒ —£}.˧iSØ Èþ¶Ä¥±J¸%4$ÃxÖÅI™†û_¸„a™ŒJÓôH¡åNg©GÉ¥#àž0Û±µü›.LJT%-¿¹/gA´iØÂW¸eÁÅ™:(|TêjVâñ ÎŽ'C*^:>„«j[×cÒ nUà°F@æVÆ;ët-;PÐpÒ¬Û`o"¦¯\ÓO!)þH+‡ÂXÑq9ùIµ *óB5en‚ü>QnÕ™"ÁÒ)ŠSE'¤Q¼òá)äòl…‘¼–$7ò®®Ñ‘Ï-ì5ž­]oúñÝí´§>¯½þ ¯â€±µ?{ëëYð{NÛÀ!Q3öqëWF…ELº!Cv§!»žNÀÁ™ºò’—þIû̧߷ãMÀöÝNi{1Ï߃Ãz^Ò„á4T5ÄNþngß:õ"k©óÆðNNxÔVðÞÅlâ¨gÓla*Û ÌŒQ^qG¸S¡”_YÝÞõà‡1ê8Ž-OçÖúà6ùö€Å¹«é´\~¹‹†/áó䋵l6pÒ LÚ‡ÅÑSÙ lö2„Ñýs˜ï?ŠQÒŸæ4s×( °]êr6:p4ýsŸ{§//`£€ÇØ9ìHU/i·Þ¼¤=Ì>/ö-…‡®yã3šÏmü®?¨)F£Ü©Y6þ·Ð óœ…-¾²Á ¸~"5dÌ V§[ž”—lR…\¥¨*yn¤ÊQ}P;Ä t÷Ž2ÆŸh”Ðp”Ö¦´0É‹¯Òç;tc¿$±ìÅZùíyIòÐHí(Š>> nZÊRŒS))‘Á²Àt±ƒ„C]),?âˤQã äg;ºpšœ6†Hgc¦èoûÏré³–—<êd”Pa°Í\Ò%˜ÎøÃOÅ?JL#QÚ/÷!~ßÞççÍN0JQA[M×Ö ðk6J£Ÿðeú‡_dµ3–´ +öôí!ž(‘ŠDxõ¢>+Z:Épå^œ ¢XnêsÌ1§¶[~òh;ýôSÚ§?õùöÚ×¾’…µsÛ+^q>[ýžØfÍ×Vàk2ä=©ÒÈžŠ?3:~ËHY‚pü§ôx¬xisŸÄ*Ï{ÿµ#U“/@•ŸòAk»‘³0¾Ä åÖÕü5Û–Ÿšª%oÈà‹/f2(§†'eéIT#÷ÚA߉ó1ï'ÏÞTgÑP9©ÀÒ¹õ¸¾)Æ ë‡àÎ:ÂNŸC?Ê”&|P¨C¡AbõiòPéxð‘«l +ß¼X~´ì®oKœSG¡‰®Sn¬£„ïìD;ÒÜ-ÿ‘”ÖEN+TÒ⟟.µ)o'ã ë9„¨¸H‹ Cˆ_¹js›1{lÞºº»ßÿã:òŒ8%ÇNBþ¿àÖòæ›5­»üå›,@€^Ù‰Œ1ý'¤úϦûOúÿK’TBÛçKt¥¯Bš2¥ˆ3Ðqë üàr #"¯Û,ÿVîæÆǦòŸRšÝm¸§ > E”ô„%¡¿ý%r*ý8ïHÃGô–ƒÞB(Hè…œ_ÂRá O:$݇õ® ÄmÔü›ÞÖl*ž„÷MFFáà T,‚†*¿ìÔ‚ÓR¬#S²êO¡£ðœ‘R S†žQ¤7ÏbQù5¼pŒ<»£íX)¥WñgrøÁW¤1ëh¸1àòQ%g•k$ꆆ™\…1e’géìÐ…Ž%E‡Šb¡’ÊùvðFTY\ŠAØhëa´Ë¥y®Ï^Æú›ƒ™@góéí¥œðÖ?½¨]ðß—ã¾Ëˆû«8Ëd1‹†kKYÖB±1µ¢¬–mù†µäw2}Gˆ»íÈKD…&â'e#Ô.ÁõTnX2¬Ývë¼öÜóÎçmÅѼ‘üw±uùløfg>ϲe4HÔ2goøN>1[ Â]Ê Ïf„?ùVÊâÅ ÿÆ‹ÍÓ•Ói½t¼h}Ûƒf4]/mÒùÆ(øüŠh …>ļWv~͇ ëhJŸ«c#qæç dˆŒEK‚ý.KÚ‚ú‹­’¸t P¶º‘gïÃ[OXË.–RD —?/Óùª××Èýt(:Þ®¶šàñ ËU:È…éWþ½B52ÃO:el8ãæ-îHYáÅwê(yþ¸TsѦ8ÍrýJ/iÓö™8i‡vŽÌ´¼ÝyÌ"à®ÀËüŽÏ¯”µ_ûßh *‘ÞÐ(ôÀ—™ªmž:d~j’þ¾!iCM— eŸ8Ë›ÎÍÛb ³ŒT¼^+šzN _U@tbP:Bç¯xº†:aÙ·Þ†<C«¦€ â‹tâÒ¸CF§Ä„[Œ·s•ÄÓ@¡2N¡#qh‚¯.èv£`V†JÅ ‹Té8€V±s…N@ ™°±Ž }#':OFE€‹n• Pñ(xú zçÐ+ç(*¯p¼ Ž‘nÌx£zäß&n!ðÙŠÇ–ˆù›H}'`òÄ¿øp’ÄðIhÑë—×Ñi,”^„·ºîª¥Qüè‡ ˆy°á-'µu¬¿6þ™ÚG#ÔÜ]¼è*òïEÎÓOóøODMŒ6³Î’Ðyó\R–ÎÕõ ¡»ñ&ÀNÀatnk/?ÿÍt\PÓö9­7œQï ŒþXyÈG ê.+;ÛRUòK8”ÿWt)6|Xî<άu håq›÷©“§äÍ®‡Êæ-§Õx”[Ó; hÝ©ïªoÑ·DîÚ‚íg€ʶô^5è¾ëeS·ýÖn,ºyg3 VÉV”žº‰ÜϾ2x×3õk ÿÇ4P#8sËAFI¼éœ e¯o¤º;ŒE9EÆŒ•µˆâcµ0ú©…``Ð] !^ЍåC)ÇÀ g¡¨« wŸN`ñê¡tÖA ³ñϦ4|å[ wæêå9ù¤’N4à”{ã*5rB$¨Mšûêœk8A”Ã\Ƨa,òÔó ¬â—6xâ]yæ> ãHë.4)< ´}xuJd?uŸNQê9••¥Ká=ALatôR¢Ã£®ÜÒTdyó á~í€òIát’„‰"LX¡à,å~ÒçàV÷+ s¬”¢Ö á+s`ÍWñf$QXI1ŠJ„qÇRHã”’Áì`3„żÿÚ.¹ä‹mÖ¬™íüóßH¢-mξãÙñk]íÞ#¢0ÎHb b‡q+6_½/gtðØão?¾ñ®ìôÉ, ®NÀÞÛY0ntN|¬{ñ{@ 3Ÿè#£•œÑ½ñ”|3ÜD`c7ÆõùÝu"y§8¡´É4zc(¼ç„dOdv y5'mc—û篧ñïb‡´3Ÿ~P{É‹ÿ¸ÝqǽíÆooÏ}î94pnoüÛo §/1}ça…Z›Í”¢}Ùy虤ݞ=þg a‘òîSx$ÿà×QOçö{róí·-Gß ]ÂgŸþ¢jG´3ÏØ·Í;½íÅ Ê„8}úäì¾60°O;üðCXß0†3H6 Ûµr‚²ë–±PyÓfÎV`›ÓXØæ³‹Ñ÷=Be>8è ðë.Ió;yÊPÞ" mÃYÏ1täàj@is؇æ²Å™Vxî¾S±Ñ±ÝŠ<êÜ7:„ùÖ1¶©9˜‰üVÙ-<&S/ƒ°5m'¶ÏsòÒ7u ¡}Ö¥7´rˆ&Í[qÊ› EløÒMY€€)Rþ»‚`xÑ5!ÿü¤!ÊMŸI¾TÃP>ô¥ð™Nà ò*46.IÓÅ–xÈ[i!nüa|‚´ü@ž“š–NÙ(AÂDa`& 0†{p¿ )â‘®¸Ú€Tœ„pò°ú%/rAÓ²åºoÜ^XYlèÙ|ïØ" lÁÏ/ÿº ”i°¥áÊoêšÈ_ñòRœ1à4:ÉΩ?îøg·+¾ó>ç±mïGóöðècjïxǛڜÎu’¹–kuM¢»ªÏä\~J·²å}GTGˆ†X’‡O4©-èô{ý·™…ò“÷ÂäËÚ[ßzQÀ‡½õ­.üu1Ҙ罌½ÏÀî¤3¸#ÈÙµòç3\Ð#è•Lt€¶Ý¨¼…Ó5© m"ZxÔtª>6þœgӛ㹠ὸ2Øe‚.ÊuñûÒ7LäÜz/ÿvB|®_íªà’'ÚWñ„¢_;A˜ÕǤ+ºj W—¦ø…yS‘Ðñ^{’¬ò̽ÏÂvŸøelµÈîàØ•_%‹oK×®[ËQ{FËÙÝrì˜19`væÞ3rÐë•ßýol'±3ܪ4üÝ~5‡QêG1hc#ß(]ćx+EÃÔˆüôÑVz-hïpŸÀ&ÈèÄÆ…¥Q c>r©©†2á5Do©¼­\­x ©Àü[±ÙÒ©WcÏÑÚä(Jů°ÞG97JzñŽ˜ú1P¬U¹cþÇH¤Ü‚ÉD¦ `3ib€/‹ÿ";ª¢ ÂÇ‚ '+Ò·åa97ÜðÍö—ùÞvÊ)Oa矷k¯ý*¯ÐŸÅ"_ÿ¾a’»â<¤Á!†º£ŠÈÜK]K£àÀvã ÷dað%Ÿù›ÓvtZöÿ_Ë"\×TC^+\7²éd¯9 çÐŒäÀú·;¶Ÿ¼Ƭ,ý¨C xð9\“_Ž’âàá#sPÖ&v,ÙÈ~ü·ÛšuÛpØ©šÚoýÖQløWíÊ+Èþ¼6{ά(ö½ïß×ÞôÆ‹8wi›5k/>^î)Ì}žØöžöLFè·e`Å©7i8@ ðÝE‚4rÝ*t?c4hO*”ì´b>9Ÿßs,ûW^u'Ÿï@cçÎòÄ!Œ{·é3&µ)SY_Àúƒ)S'1Ê4‚ÃÒöà~{žoo§æ9 £3’µ|ùªð±’=ÍJné£ËèÀ,n7^÷`{dÉ<ðº•jOË­]ƒ0}aÊÒH::¬— çÙú¨Ítdœå˜gF >y€¬–?œ¶ñ„…€ŠœëŠ¶â¿™,i*w¹5œòQ±æ¡ #á’ë ¿Ò<´ ¢âÿ:Tvä„Mc‡_Ô}2 ÊfÉŽ¯Ê4‹I'ß±ÂíR(ClR”\½/ ;Äk£þU#=%H²RJ›$DMÂÃf'¦Lx0·¾¥Þ((½Ø-jÈTOØPx$\4…17¹ÐæXãÕ<Š‹moà&ÅíqÏÞ¡Kp L'œ éäê Év'¿Â'iEc™\ºt o´ö¥¡³¾=‹ÿþ>Ö^ö²ßns(;/~ñ H|,[…N¡³àaŸ¥³èŽ´²`Ùõ.~žå9aÞð±l˜S¥/¸K–§=¸àqôÈ!mår¦µ½ÙžôÙí¦ŸÜÚ¾øEÍ{f:Ü¢R¡ñµÞ–±E.iÛØÍ%Œxc0¦Ä!,ôÜÁŒ/ðh¿ògÖy ß4ÀJáàgO^PVh‡EFCÔ>€ÀkÁ+}É·Ý‘?~ýÔ€G—FºÖ¤…ýˆC0ëðÀ ìq#å.¥RYBŸ@"*¿ÁÇ}M#" dc뽬rÓÉ#V¯¾¼(AGLS”S™K[Aä-rBWi,Ãù Ëjgˆm×^Ú£õΓ?÷ìßàìke¤þ¡E‹Ú7.»œ âðÛ£ñë[Û‘‡ÖnýÙíÄãŽm‡tp›Õ®þáÛwßÃBæ)í¨#gƒˆ=rfÔ¥—_Îú°˜&z@ýËÙRúÛßùN¦y Ì÷.¼,[ÖÉò˜Køº¸ã?tH«üqv’˜¸v]ðà)1P0âØˆˆƒíèˆÓðJ œ¸Å«G'<ÄPX^Sƒ'qDÉgœ-èÁÊxÝ~ño^ŸJ„+N¿¸E3ê nXèH‹W¶Å±tåÛ«ôèo±ƒäñ®V"Uñ P M)žJ#¬yŸÃ¯Á¥OFvÑÙ¤‰chü_CEý‡4Ö_Ô¾öõËÛßÿý_1‡÷9mé2Nصrò-TC!Í$INbãÖˆwí2ÖÜíÞ¼üåonŸøø»wtf <Æéˆ¶Žý¸wo´T´{ù”7ó=y‡\JT”ÍO«FŸÌiàd&y¼CU ièÔ˜BŽfôqè°A¡õ³;<äëÖ6yüÑmO¦Æœxâ‰í¶›çµß~ñÉíw÷…í _üz›ÿóEøFö×߸ÑÌÛø\@š!lË9¾}çÊû8S`LÛsa4xŽ¢Œnmë6læ ¯}1d·UUQöz)aû¯ÜqZØVÒ•s ¢d‡£ÊcÆŽd”S“öƦ´AFrQ®ƒ6«V¯o·þt9Ÿ‡à«o´÷ ‡«á¾ïÜ™tØÞtŸiì64]&°r*£\¸¸WÖ¨ëÕ«×¥æÖ¦‹/ÍAiëÖ¯m <œÓ‰—ØI˜ÿh»ÿ>Ï€xò£}ÈItÆ0Ê5¤a:ˆ§?;j»1Þp¿Åo¼2½ÙÍßÊs±/§Ú³@ŠÊüÊ”o“†pâa;z³Á‡(\µ|P»iÂS–bO…Œ íKd¦ö+¨éŒ¹ôÛ¢Ùdžè73 MƒzñIÄd öf ;B‚hÇñDð¿º%›üÉ“¿s_SRŠ Êpç¯4ãþlò3POÓÐ'ÂnÒ`¾sž)Êa]o=Pœ(¿Âêwmà™ÖM%R¾|pœ„mM;I‘ßдó†§`ÄÙpÂ[ •+6°SÖÎ9§½ú/h?»ýMí5¯¹€M®§Cý6Fæ¯o'>夶ü±5Y›âÛBù‹ÞÀ]˦ô(2U2 'Îß¾aýù¥2øŒâÐÂïüfûð‡?Máílj86 ¸¼í¬Q£ù]oJö~d\C>öË èŸoT¡édDV>ÓÂàÊ%[WèH˜ÔC„Sg¨7Ù”¦©o;¸²SHK(ëâ3xÒ©†¾ñò/?Ö›â³ñªþ #:—y*”†Éw¢|&Ph¯|  ¾ã½õ«œþÇÿÄ·vá„I!ß*k§K=ç--0^É¿¾×³Ýž†eÕ¿´Aa´ô²…`}#IØw½Õ³Î9‡ºhlûÒ—¿Ì®uÓ9pñÌvȶ»9äö´SO倯»Ú 7ý¤~ÚSÛQ‡Ñ®ûÑÚ]÷ÜÓÎ~Æ3ډǟЮ¾æºvØÁìÝw·¯_úMêŠ}ÛþûÍM£ƒ¹/øÍçÓ‰8<‡æº•¾y¶K/ð?î´/tšÎ¨Ù„о¬õ7îrÄøêW)+‰Zv)_¿Fþ¿Ò€Aa óÖ­å+(2.¶BAÉhNòÌ%«,W¾î¦ŽÍ•í]¹·q–×àn«ÁÊ}òY RÓ§CAúþ PA@ýB %˜»¯Ðé,H§a0p:”¸pÕŸm„û €qõ¾RçÞFwáÇYúÌŸ#çÊ*Y#4\G‘Ñ~¥—´‰Á5 Ú¤R&`B°Kd&©ŽIXžC#ÁKx£°uÝ´Ã…¸: ¦«ÎŠ iõj…e= êLÌê@qÂÊ T ›ŠøjL€Ú¦¡œÊAî+¿¼çcÞÈ0 F­¹ä]I¤tJ‹5±zI/¿y6?xg…y3ÄÇd”~q8’i,¯É~þ¯{í+rúç *ê}7M)N‡®.}ô“nŠxà ¯Ò¡žÊ•;¯îtܱQùÿ´½ü÷ÞÒ>ñ ×t YsžÆˆI- vW›ÒÒ¡;ï%cþkÿÚ€¬ËIi¡î*#ì†ÓpöMÅV›#8ù׺-4Ðo»íçÄÞÙF>¾}ö¾íÈ#ÎmŸþä•tvžÉ”žg¶¿û;ç /c¡í4v¶¯}õ¦öµ¯|Œ4Ç0‚3‰ŽÀF†¬Öi§f„ÞÑþ•«6”=ѹQ v¤ÚÍ1 \œ¯C´ u†LÑ«yÞi?ÊNIÀø¶`X[˜šô8ó¬­,œ»L²äé˜ÑÃÛavH]ËÊzœœ•ß̨ÖJ¦l]ú[‰¸š„GùôבíˆÃ§¶Y³§µ}öáÖ ì9nOFµF1b;€žF"çæŒr å ñòÇV¢ic#I[BGaõO(fjÑ]ÑÁó ¶y|Üδ¿¸ÏþÞ#XX>¬NAÆœNd^Zܲÿú{m×NƒzÊé»üF¡ÀÄž‘SÝúW;åh×*@íasÜÛ1÷Ðʘ?_Â{éliÃ,tØmè0eSÈ”i‰¯Oëœ%,¨ø’G¸ bƒP”Hç¶Ð…Ë›y”`“h Ê^¢ /ú ¢ G¼trñã†þ%H](@.‘ʯ~GtÜTVË7ùÄ Nº„w¨ûÇD»å§Ê£ðþ—lÂêʈÂd¬¼J>ŸpüsÚ?þã?µ«¯¾­}üãµË.ûp{ßû>ÂýÚQì&¦moäc•°£CBÞ)Žöa¹‰V,ä°L„ mG}ùdT£˜ÊwãKÚSNz^;í´“Û·¾}¶yE¶,µc¢/°1D .àÚQ¤z‹ãÍ ·°Vk2Uò'a—^h§Ùˆ5}ô(é’Ô¼ÒÖHoù¯ë@ž4Dô/ ñ+cÙr“:SË:¬SåÂED PgÑu ŸxSÀJaä!WÉ/};ôƒÁ'3:›*Æ@¯ì$@Y±×ˆA@”ci}ÒA>õ%ò”5øV?vlWtDË:–tú@a¤ÿôõÖ“0ÿ—=*‚>ÂÑçñÌœÙ~¾àçíºßÌ´Êû˜V¹–äÖgÖZîoøñMtôm“­k_ýÆ×ÛíwÞͺª½,YKÇa¶rõj6x˜†o\Ò>þ©O·?Ò~ãì³Òñ¼îúÛzÖ8PàT!·vp)zý/“èß#Š=æ €¶f~Aݳ$Qób<‹9£«—³öØõ8hº!ƒÌà__ÿ÷i@÷ì®b_ñG—02Çb”T;Jy§£¨P2ÕRM!ÄSÚ¨ŒS&uúVtG˜*_Å]NÊׄqHf·øØ÷òQ-SOT.NÂGÙ|Q†b\–yñé´ŠᵸËbeËÁÉ& (÷^y ßÉHLäðG¸€'`d9«AJœŽÅð¨@Xiàhø‹Î€¥>ЛDTÀ‘7• •’´éž¹ ÍŒ0"ƒ£=’/§Mdò@§«©D ³’•e5_¼èd)+Fá°»ßàˆû¯•Èšú –EpöŽŸ.4¿QŸw]¦¥Ó O|úJRg›œ‚åH[9›Û.Žd· øiíþö¥/]•yо:?`ÿ3Û¦ Ìù†7ûCi“N¾½c;òjgÚ€M ñÍt¹OŒé"4\WðÚÿøãã-ÃíüóÙè“;- xj7nS^Ø"ÔáF‘HâæI©:¹—Ž´ê¯hXÛØ°±°‰Ã¿„ÙÈš˜uÛm÷‚è>Ç´W¾ò\v1zC»øâiÏxÆ©íôÓOb~üƒœ`ü³Lµ»Øë´ }å+ÿøÃFÙ›ÉtwÑñ®¡)æ‰k§Ò)¥\1€ ?ð[–6+Q\{xìóH§û‹å{æÏp^x‡Ò„ñèº4ƒ¶ÓBÙÛ,·üiVDvÜvcîµWð˜ZáA…ȃˆŒ®tŽ6¸ø]¤AGU?MîÊN9à±Ýe‘O š_y³&õsQp0Ô7˜ÃOÒ•ï­vlñpF&Ì¡â%ÜÊ`¹Õ¯Hùá_Æ˯·^¢7²’¤ôЃXùýâeNŠÛºï}!7…VyµËvùˆ5« ë,ó\¦wñ倣>b5k¨nºù' RÛ>ðîw±¹Âcí»W_Ýî}`^;ëŒÓóFwÑ£F2mt=[‘ùßÈ|ÿ}öamØmÝú ¼UÏÙÛ‘gwÞìß®¿áÆöôÓOo¾ým¼±]Í×¶;˜*äpN—ÜÕWòl‡>¡Ö12€¬p¤ÌL_ò'÷Q)Œ3, ÙÕŒýÿ/×€5l“¸t1GªÛÀ …±ÍŒ£°ØÐ´Y4úòéèƒù™õ”ª*\"éTŠrT}bœ@Ê_ÜX`3b@œˆªêˆ(WÑõ»ðjIÞ–[Ѫò k¶ Áó’"N™/hD(hèwâT¼7=ðv$bÀ<ƹ%߯©CxîáÓ(6øÃ„0ðΜî[bG¾tLB˜úßKçhÅajyô·ôOÃNgeå.9ùÈ7‚ ËöžòMœiò–C\ ´ì #âÌaO>ÀEœ¿ªJ£à8FÕ)3àKzâÅ':P·~#„83€K¥O6DŠ_‹¼"w»Èæ}—).ßbÔwT;ý܆Œ8!‡è¸sÇdZi#³8«ƒ™v:R÷iü†ø³B 4o­œ]|üñÓ ¸»ÿ²·°;ÓêM@ÛNsÕ}ë%¬z´³ª ±]õ Ü„+–‡üÃ(Íf»®mKWnmì»#~‹p$}H{ç;_Ř¹íþèýŒfÚ:xÔîÕ.½ô*NF¼¾Ý{ïÃ4L™ïþ•·©SN͈ø°á{ãÌ72gžÑI>ÒËBG;ÊB…."3zvѵ<ªsuÁzŠîi­ÔÁF•6ÒXC.u–|Iæ—ݘNÜ~×]Ä'8K' /’+N´«NÅŸAÌ6šk[A<ƒmÕÆSù”tʦME£~÷Ó™u 4ihÓ˜w ‚„ü`!Ÿk¡öäi?pÜ̶Ï̽ÚdÎ7˜>c2‹ä&sJì˜6ÀiʸëHXfìú*ÊÇØÁêtžÖ¶L-ZÆùË[Áý’vó´ûîŸGü&>î”´–O¿@y$8Ærþ„!ÔOÈK½åÛ¢­È¸•iFÎÏu*«ÓH²í¡úßSPŒš5ª;zP_ñwÄš§òeƒ< ­ÇÀA*yKn%CÌ{m.å8ÆàSáÖÔq<¿±p•_%Ô<’–™)3Þb ž;éˆ[¯O Ö@IDAT*ÙF8ýO?òmy/ ¼Wð˜Æ?ã Û1ˆ rD»”O1[0’¿âëMm£˜ð, :Ñ… :qËÉÀëo!‘rݦü‘.,‚dúàÑGMa0á¬öÂ>§½ó¢÷gšÝܹWpšøùmþ¼ 4¨fµÇ˜·/«~¥<ñ›»adœ³‘ô5•RúðÎG2N•æ[n¾·½üü×¶Ã;¤}èCvä·ÆO¬`ôß…åa,‹ÏU‰: ±ÌÇÁ•<'Ä)¬ñÑ ñ9¥8 ú:C¬•ͪ},aõö›4ÊÁ_ÐcpΞ1_½Ê=›Qõ —XIƒÎ Võ±”{ñ^>< wø×bÂ#éÒ¹#,õ øD¿¼ÊZüš÷¦Ë¥Ä¸ê'ñuB¿ÂC¢ÂþFö\‰?pæ_îüÖš”E¥é³¼j‹Otäe×_èö'M˜Ø®e”þλîÎbß#8¢½àyÏm¿÷}m<‹‚WÓx¿wÞüöìß8»zÒIí.¦ø¸#Ð’Gi¿ùÜóÚƒ-j³gîyþ+Xôëåâûx€mœl{OŸÞa-ÁyÏ~VûÜ¿„ï[žÎ„þjW^Ñ6Æ–º&úÇئÔ)–zÉFÏœ{»äÁumÍè mäè!érç0®“L §}¦Ò_;‡í|ßÇ?ùW˜ÓÿGñ†=Aµ z>žœÞ"¨ï|ýâÓ/Òëã~}|‡#ý÷ì ³sú'¥yR\q^…ª°î”C÷dE+]ÜúµT¾ëÏüE+9Sù_•EÏP•¿8:—¢ŠY²²!¼:ãrUÈõ6G\Yål !zțƫÿŸA¶‰÷¦£v(çXÜÉ«x˱’WáUC,zqê 5mPjo½c]z£â𼑨Lsù#|îý¿.¥F§Ï¿… FÑâ|KžÀ«–º8¾µ[ÂLJI± D”.ñE¨(b'ç©¶»„ÐêX"ÌpÓƒE}š^y¹ñ^}¦/, ‘?$\•C/>ÃGÀùJ>û‰,$Ôa÷¢õn’ðŠÚ­í9fhûÑõßä5ý§ÛÐÞøÆ ÛÚ•‹i¤ÄQýâT°2Ä^ÂcPÈ öŠÃæ×JH½ÛÄèy/š¾eòOšv8v£Ñ·ŽQÁ¼·ýÞïý)Óþª}‹NÀÙœ±ÍšÍku:„C‡ ã•=‹ëÊ |Íîà‰!f.åÊ›…NG¨qçv'ûÿqó vTm鮿³v`YDOfN.ѱæJ _zÂøÉalÀÕ› ðznþ§|šs¦ƒnµl{’IgÎz™Grƒ¨»<Æ&ão çŸ ŒIªå¯<„ÁØ©D_)é·Ÿ°XB“LÃVabï²å vîÒ!"L½‰Q9lСC´.ð_MÙ>Ì­BÏmþ¶×“—÷µ·¼ùÕÙpà-ú¶ÓýëpÎã­÷zÞÐóMhTÀ¯þ7ÿ‘(ŠÍÛ‡è>mÍ”4JïçüV»ŸÎäßýÝÅ¡·Š· ¢õ£è)OŠÏ%‡æ“aÚŒeI=¨‘´Štdâ«ò1¿À«³‡tÞZpbê"HÁÃ}v"ZXÁ¢(qC¼´’ŽçLÐi¾ÅyÄsßQQnÀ¬_~’/É_‚äÅh¾•Q ŒÞš´³®ãÞÇøõVÁa~—¸| ™+•|¨•Šë¿mP›Ç¾HÖÛB? hy󲯗—Ò›êGw]' ã¶Çø_þ«¦Ì÷å4Ø_ô‚ßä4û¡íÏ.º˜ÁŠÍíu£G³‹ÛX˜ÀÏÅ‹·»<Ô.<ê(ô Ûï¼émíì£oÏ9çìì tËç>ßÜ¿ ¨?›ÏœßrkæÌhïûà‡ÚçÿõŠöš—üN;oúŒØ˜Ó3U­²FùÿåÒÂРü”Nñý)›øÇ¨$nž¢{÷»óºxóF*×u[“lTG%¸ Ü‚™OF¥ð˜&ɪLæñlæ©T°æ[ã§»-x² Rä0Ò¸íÌ3–ÏÓ6¼^›ÕVÜ=f€Nد]ýTØo­NÀ·¾ËÜüÓ™ZÂv,"\ÃÂ`çr;<#½îX掭hsfïÓÎ{î4&G´Ë¾qGûÌgßJcr0rüÇ4æ³kl;ÿåïþ¶6{ÖSS†Æï5„‘îÁà5¬ ŒÏ§õ¶ädPH«Ö|RóI[1JÙ”“pM7å†0l+ Xà“Dh¤6œâ<êDzgòø)á ôÚe"‰n“{UŽ|6¼N‡a:ÓÁöÈ ˆÓGP/-_¾|ìo>²Œ‘·eLYÕÓI¸çÞ‡Ú5?|¬ øH£¿öᆅϓF±zx:CèTºþ 'cÏ6ЕUõ'[yŽ=£»x3Б_3!Õ…y.ôÏ­ù¨›á¥çz m&Ú“Êæ›>Èä„úãrÐ"Š$ãÅïy&Dåy áÏä¡*C¸—'Ù†­JIKÓ°j8r î„!ñ v¡IIÞ–fí6ë ¾4þMàIô¼€ª “–²À;ñŽof®¿[ßžxÂyíóŸû6Ÿ[ÚUW½§}ô£·ÿóAí¯ßóvÞÒC9¦ƒIçM=„òŸ_¹ÈtJÉ$Œ7@¬y™¸×èö£}é}›írßyч€8;¬¦l!\d®üépFóD-s‰T¹£/n­·È+ã’m&6ÄKÿÚ’ñØòº››O SR˜±…'Dd…ðÆ—Æ¶”Œ«S 4úNûŽ61…¾ìš>GφÕsâì˜Y·aãi+`¯â6Î?éz%W¹˜‚/xƒÞ<´BL§ͳ@Mo~TºbÞ ÂkhGÉÛò9ÉôbÂÕ™pá+ Uu¿ÑqbvÕþ´+Ï‹Øñ猧Ö.üÓ7‡çö_síul¥Ì,#F¶öÛ¿=ïé§3±°zÈÁí²~(ej$óù]h¾ë¼l[0/_Þödºä‚ Û1GÙþè•Ð~_5ƒÆÿ­·ÝÖæý|ö:1k"y‰¿k„$c]Äž·SÚ±ÙÃWZ—å1Ô‘A.ÂÑlu´òåk«4Ô¼·@vÆf$æÈ>FîÆ…×‰Iü6åRð #~çUq}E«ë´¼˜>¯³H•×Õ8Ðø7ô¬Ÿ¹å^Û”ç»&ß<×?N fÞrRò °X²i‹_ø‹3ÖaY¥aGÚ‚FUI'NÕYñoH÷Bßà0Ü÷,@Y1>$çʼ~î¯;¬ÔH‡ú#­ª°´É0‘“ÄõêO’$ãü<÷‹¤a£UãÚÔiÃÛ˜‘C³Ždõ_¦Ÿé„”]Ûn·‘¹•Wê¶Ì=X9¦ )½6£’̉XC g‚Ô›7\Õ¨#OÈ‹‚y`=h¿¯…åc íA[tT6îšh|-x‘rÊ¢‘«Ã«)z|e¨†dÚVìÇT¹—bÙœk„Ò°_p)ƒ,PŠ¢ÒØx"̲¡ÏeÙö&|™€¢åò-Šr/cªÏñ'ÒZ¸ªqÆ©¬ú\{å«Îos÷›Õ.¸àwYXyJ˜=Ž­<7fW!e,«-îÁ§E»•‡‘£†¶®`ÝÈIÙzôšk®Ç|–ùÜÏn«ØF7]ùˆª”G=qñ•i=†kû0šÎPjé>#PšAÀ¨§”¯P†²@W•‹èÇ·ÚTCŒ`’ñ&e#sõ¯¡¡¾°Íœ>­}ÿ?h³ØùÿߺâÊøçú/]º¬ýøæ[ØŠùÐØØ=÷ÞÇ"âj?g³Ü{ÿýís_ø"Û…„ˆÛI{E»íöŸ±áƸ’ýïj cS”mÖöØ`Ë5:õäwr¥!J]…òSYè˜T<2qàÁèFÐ5icøF™qiÈ›ã`6m*Dï‰ €C+Šùi„Ô@ÙØÊSZšˆ&)!ž¯a¤‘€—ŽGž2U®…\.ÔVqNJNœ‘˜W€‰LE“?e0‰…HÕ¯§ð–ɦЀWîÁØñë=§Á¢¤ç7£ÙÈeš!Z•G\ôA4IóIA£`ÒÎ"è){ôG¸b¬Üq~0Ò2_<ÞÝÞ´H8j0<U݉OՈϴ;dŠõ l•(a€§Â1J/Ò2ÐÜå—ÌËŽ02­=ó2æÊK%ì=Ô¿»)ìNže·§&ÉX¦ËÜwï Fø'·‹.|#sæïm¿ÿû/ÍÁU«Wn(ÙH—„0q„<„mDVÙ-d Á7üûe>:„ ëê;ÑFÊ£ô50õBÐ[™zrÒS¦3rûßùÕz/û+Û9çœÉº„£Û™gîßÞööO0òò`»ö‡·¶ç³8ùî»ïmïÿÀwXPx)»E4G°ÕßCÿàƒàµÿ‰È¶{óÕ¿~+o5ÂüißЯ…ðp#úžj Yʪ:/žÕ¬ÂÃqðWá"/0Úo”±gG)åØ)”â gs\T|âÈ!SDÉ“MAÑ¥’ÞsÔ§<êïÌG$ ué5oÕ Q¥i¤{Oœi¸òÆ_ÝPÒhÄ¥Œ*“¨aBÙ".|è+˧èÿ¥Ü×u&LpX'l5=÷2(û¹õ‹«Ï/v¤¯O}H¾z/p~H—ÎÁ¡îЀú‹3 0·Ày¥&Nh§Ž‡P$t³Ò³à}ÂøQ,úç$]`-;>Ê+¡_ÉE'’ n»ýŽv ëÔ™káÆ³#šüÜq×=íºojh¸`÷žo_õ]Þ px.#ÿvínà-€&¹†í²y…µ…5I#FŒhóy p xUÚhL`*¤6dgJÕíj Åog0¾FãFÏ–Zof‡ß„›)ÉLRTOÙu¶fw™«œÐÀõéŒÏ\2¡p4ò¼]U†h\b¸?ãu FŸ¸”•|¸Xº"æ«Uâ,CLjñ¬ÃÆÈL!Å´ã€M6„6¸«KláœòOóKÔ,_ÿ™&N9qT¦ÖÑ*QdPwöÕRð+Å4”‚§”o0Èü†'9€ ò„êA(o¥m!ôÚ¡¥"ió—Š ¯ˆá \Q?iœ8¡w¤“!)1*µ£Uð•Ý3Ÿ­Àlà„qšž°¾S§-¤aRãø”íérKò+˜¨ã™sã—÷¤É¨AžáC`>å (£ l^¹k&ƒ½ô$íåêí]À Lø…Ù¼ “Ò¸Ï{ä—@ÉwmG‡ÉBp9jgz„¯ŽŽ¦øFNù)¡»Ïà3OEde¼5*£xà3ß ­>û…v`ø»o ŠÆ³bÎH§kM[ñ¶æ».Ó*žùÌW1òqf𻨲֚h9`“\ÐáÖù¨É7xNå!_g7‹DÈéL™J_ÞÃoº†²Ë Žs»×XAßyDz6ƒ…¨û0µýô§—ÓÆt ‹iÝ×Þñö÷ÁËn9ívÍš5íê, ¡ÿ~âfzÏ(æø~ £åÇv¥‡§¬Y·1S‡úLPæäµ°–” ín”¯ì$ ÇR>Ôv²H«õG€6£ Ê™<7¯"d~“GÀø§ÀÚRæ=3Ú´†`þ€£|’Ù¬÷0•$Fž-GÜA.„•y"m'е,’.t…—e¾d P¨•mÅ`¤Ã³6¥œÆ¤,ig‚ çŸæIî@ž*W)‘g©¨DŠÍ…?±Š(‘ ¸mðgÃbÌõ;±lqw%e Ö­Ý^FqcŠa4 í$,fÝÁÂ…‹Y\¾ŒÆå2:#.‚þž \ÝàGhM€ÆXÖ¦Œh#˜^ä›4’ŽA(Û1àÓŸÍ’ìêòWÀtâCQ¢yÀO¾Ð_b]™J£U}Ÿ |á–R¼yoiõ·KN¤5E:¤±en–†¿Âw&Qvb^ð—7L¢"]l´Œ‚t1;Rɇ¼†h§=I½dðÙXƒl|€ºä!Tû$SÛzÞÀynÀñ'<»ýÍûÞ×n¿ý>¦ð¼‰»þ©ýÕ»ÿ¾}áóð¶¤M#ŸÁ6 Ñ•:LC<¾¹¹þ†‡Ú³žý2¶ú<º}ùË—2…ù‡íðCžÃi­ëSþ$§ *R|%¿áMµçŽSýf•åT»½~Àà‰‹ô z’/–“È"ˆŸò&søò9¥áá·3(=ÿô)Á >y1{-ÛúóŒB‘r%›ì'ØÞGp‡˜#M:5rC¼zÚbï;†¸çY[ nGç|Ü10š>“†ËºIØøñðÛ…ó#oêÀZA6ó•_ïI— ô®âsê¥ÓY‡“—“'k×]¿¸-øùÕD²æªíÛŽ9vFôo¤ÁÆE²«/uçf{àö(b<Ûˆ7ܑߨh×o;=¨¿äsiÒNïœÿõUœì ë úËÍ ¢ôê]~™N™¯zŒé’ƒ3]…ÿ4ô`ïµ®.ý§©Ú1ªC"S×4¹·_Æ Ró|Ükº^æ¡a1 ¾P`Qq,ðÜ/X3QäM}ç=¨õWü‰‹ÎôoàAæèˆ8õ¥ç ’tbÈÀ™MÚSì[œU¶!aáq~ 0€ Xÿ‚˜†„Äã? PökJž„‘¿øâù«|Š2pðãÛ„$1 ­"!*k…ˆxÎ+VÕ¨Æ"K¦‹(éÊ—²øÃz"NOÙˆ•’o‰”]ýKc‹>SÁÊ•£ëN’èS@t⛣ÓÆ2ï–Ñ4}§² :"ªÚìŽ@ŒÈ» ì÷¿ÿSAn&½*÷ënû3µhöìÉm``*Ÿé”…½¡Éö¦“ØÊo¶j½4ˆ]ñFÒIXÃÛ¤µ¸ïêð÷g 𼣈ÃéAC€)Fð½•^ŒÇ.›º†$zÅ@tÏj§¡jѼ%Dñ§M @-‡ª”À›VÔ.LE¢Ü VÿF›·É+°gÝUú#¼˜FS“žöâ³Ù{æ·Ëfò«J†üÅ'÷ô0^ІóÓ{9SÑ7% ~ý/t3;·1µjÕªM¬;:³}ûŠ»øœËndkï¼ðMLÿšÝæî;+ƒ½ë³¼¡œÍ Á–PµlSă§µ›Ú«_õžlÝø6{ÙjvÑÓ.3˜M5™¡©4D-;¶q¼*L¿žžKÿ¬öJªïÊ  ,†øFì×7%õ†±tkn‹Qïžzš¥#SÁ#ÊÏà@á7«®«YH×üæ¿|¼¸a€²* *Ú¶“¼ó#ç¡©þðᤵï.|ÿ4¾M t¤,v‡ôÛ®½2/Å«ÍE—„‹ƒo‘zÃ¿î¸æU6è }§“«_ëáVò6gá¼õmÓÖëXwv'¨_˜·¹óæ- ówë»îŒ~Òa„¸þª¿*ïàIãÝE׿m”]ÅéÉv":evg‘·ÑqýàÂÖNªñÙÎ^¯ªqì ‘DO|%ßÐÕŽ|"*wÿ»øÁCÕƒæ¹6ð¡>“Ç–uò&î„›¯¬~›éúÿè>g)ÓÉVöj<Æ)g-d‘¤(Á°â™ ŧ©Gžwsª qQ¬NŠˆŒx†"°]KM8™×Û<¬tSˆäÐâ_ómš¤2¶Ó±…Q~À“x«#°t 0øe˜¼YdT 5Ÿm8 ïÊnBú¾¢ƒU6ÄeˆÆæ­¸QAHù Kú$6ctBé$땪òx>Ðyïw¦¨K€ww¤ÙÈ<£`Óðít”‘Oaàɯ_^…7ôyŠ®IW,)Zó´HIgêºäIÎùãvwÓ„˜¶¨®¾t´üÉ€èЕrd«q‘Q!5ЂáŽt¦¿<ô޹FÌÍ'¬Ø Hã¥òR…TÔvtjVZ¦×‚?z7¼Ë·ùjÃ-ô\YD©¸b×ò£ •¹hÀ2TúWH V*ukssÑ0Yé¼òd…B-š¿i¸©“„\é36M`UÜàDVÓHÓ2'™ 2<Ãt%u‚ªAFà ûߓþ®¹æ«ÄóaFÌl^ø~vq¹“ý¹`[2wü)›KBD.yÇ™›ÓL˜üdžÑK‘†?Šž ³E®`Ú;²Œ–QO;³·ÜÊ6›ì3?s`Ÿ¶ÿ“Û{Ñ©íòËnl¯úÃsÛYgŸÁiß`^åºvÏ=÷±#êvùåßf¡àâö•¯¾Ÿƒƒ®h¾óÛ³Î=¹ý/_Õæ°°×<¤Gã°Ê²ÏÒ—?ó6v¢¿AQæ•Á^Jaˆy’’IÿìPlÃÊC¥ú8s°:`H 8·Æ’/èWtéœTµhGÒ+;€Jg•¦x&:ëäЖ‘ä±8 éÖCjñÝ+gdI&Ä b]į<ñ˜|êBQ2BKl"V/9gœ¼¸„ïüÖ_æ}dá¹ìx•êÇKñüÒV„ÉSÙñ^„àJgdwI$QÆ\üXö"·¨øD÷áÇ2Vä¬GÌZÏD`¾Pê™^Ö!6¦†qŽ€oÆßãƒL\iÔ@l=;H­fzØ¿ýÛ}<ÙAàýŽËѽIí˜#æ´™©Ecǰé¤ñœU0œãØÍh/v0bñ3L­çÓ»/Âv±Y˜¼›ÜÀ[„•íç?ˆ7ì^´`1k\Ü­j>µ/§ä³G›2ÍÑE:´fÝqË2ËÊ4Oüùä®8Úû†û&@º!«ŠŠfch¤ì_•dg^}ÆžÉEuGÒ¤œhšDƒ:î¬|„듯&ÐÿÕ?AÕù0?©ªSë¤~Òíz3à±<šœ?~å)™ ¬8,nšÃ ÖsÜÞœ²Wæñë£.¸àÅè| ÛÐþ¨]ô®¿€Âì4^²Ä&pnÈÕ×ÜÚ^ÿº·³~`N»è¢¿æt8„| =€-<Ñ`”sàŠ…J_¹øuq´QõS^ys¤¨$&̲;×ֈ˔ȧíô» nÒ«c£QÖ-÷Ä[¯%C,# S?ò(\¦›`ú‰‹t(+~ú¦OCOøà–Yo WÏ!žg¿Ìë[‹Žù¡Ȇ'!,å>µéa™pË”Y–ÒÅßðd'VRõÜ5ò ¯ç±9Y~þÏWµU+\{ã›:ŽÿÔöÜß<œœçµÓO?…í™÷Ëg;¾ýÃ?|œÁŸkÚI§‚Vߤ/Dùòi[f+o‹×SÆÿ?{ÁfòD|OØÃ/Á"pí|oØÎÏ¿ìþÉpÝsò—ûÈMñï/gMx0ÙМýdŽüâ¥í« §ý²K9ÝMá4¤zãª?ÐfÊnYP¾c”+¹Oˆ`儹‰a”A§² Ó5ú‡zeN gÕ ÓcÈ‘°eT5GœK¸Kî:=-ÔK|–€L˜PÀ(ˆN!ä7•¸½Ìg8Œñ;®l½|)”ÀÕüÞ –zâE¹Â»äÁ%¡\ +Ï6¾Á‹ßUà( åðp솀°û£¼H"°8ZøD^q¾I¤D¥AL“¼N"•½8ù„%ÂÒ¶ŸzQ×ŬŽAÍ”nô-Ä=KUùàÅ-6t¤^*?” 20è®KÑmG—17Rp…ºÍJ«Nü®p•_q.¡TÁÚ›¼*9©TÈ'Ž([\k…“ðÊœðZ•ŠÊ1̼†Ò‹[Ê2a”±E#7à‹öy´“‚+nG-x«t’‘zr1œyÉLÌŒûDM‘ð‘Ÿ]:¸¾ãH.Qvld)?‰/©zºVö¢•+õ)O¥+d—/þÍqá’Ÿ6.Àc~ªMC)äaâÄÑ4þ¯môš?kÏÞ³Ú%ŸùSmÞßN9ùyl˹!¯âµ‡‚¯tñ•¯ðfZšÕ©³ÎÔ6â´.7Ýôsöç²0ÔET·‘p; Mi/{Ù‰LçØ·}ðƒ_k/}ÉyíÈ#gßäG8q÷œúöáŸß®»v!üü-¯‰Ol'Ÿt2;0Ìk¯{íEí]¿¡]véG3çò{ßûqûñu Û\¦†ø*ÕŠAÉc«ªI… &+aYF3‘'Q¼ é+;GNÕì'‰É,CŽŒcM™Òjá%¼J.¸:ý—MB:ò0|Ð…*Ê—|¿ü`¨È·xäÔr(âœ>÷‚qÚÐLÒ×'$?:ŒÉ³ NŸ‘VâÕI°—AEêàãF±² xW„NÐø`µIê ž+]„¡7ßÿŸßõ‹T3µôÕòßRHxWˆs£ ò’ú†`5펹Wµú^õ Fð® „åßóÌ×:dÐÌ÷Õ½ ˆSŸF˜©Î{,ƒ{ì1œåѤŸ‚í²}aŽÎo泉·K^ÝnºõjÒ.äãÛƒ)‹“g͜Ä),Hž˜7{ï=|c™R4Š•g´à”i¶;†õ (qÙ²¼QØ þͬ9XœS”W²Hùᇗ²Ÿø"°;½h Ï?°“0‰¤ C†ámÖìQm »ba§ ;¥³`™v­Ä¶ru”QÞ14B}D¥¼¾•M™Vf]ƒêÝÌF766Ôk <„Ö‰’g¯ â%ÀŒ ¼%—0wÄ!AÓd5ø¹'.uyfþT9‚8—ùŸÁ’&[ ƒOÃùa`S0làÀi¾?Ë‚`ÓpÂ1íò§í½óÅvì1sy»Ã^R¬ºúšÊ›¿äL¼ƒ·†`áøs耱…pÞÊAшð¶)ú‹A <§ZÙVòOågå±Êe¦RÁ¯h,áéÐF0žI ~Sîˆ,(ùUߥ{é“2$i$QxÊ}ÿL´œ¦m$ë䫃˜ñ¤·ü¤Ëtp 'ÄÕ³>$ÄH€lÊ8€@w%½ôX0d®Yž2+!x‘…ç´©Ëõ/ÃFÒÑfÀg8óÞU†sâ½lxúÆíæŸ<ÒN?cN;ü°ÓyÛ6‡·m.Ú—yò©V­ZÍgMXøÈG>ÉK˜4;¦®¥àkß ¡çðáÃÛÑGÖî›7/ÏžÜÛ_åëêI±Í¯è’_íÝ7GÚ5š)9;ÇlåáÂFVîŸÐNT™gáÃ[/=¯'Ãîfš5kŽæô§áŒK<ᦟ¿âr Ñô´†±^½Lú¯!È=fÜâ×þ‚|ÂDàÜÌ6¦{OŸ]ßsÿm 2—FþÍC  }hÇųþ:{É3 ˆ6~­¢\€Sᘎx•§ýee™¥ È€øeÎK0îm@x•þ-²ÑLàâÈOel02˜gÉk’y/¶hgÿ;ž1Låæ>׎›|Lc|ªMg¡ ç/ùND4aõé eT¢*½rã_*ÄÐ4\$"5{àÇ 8x£èÐÞ‚[r¾¹tc´þrY™¦! Óvý7OSV +†Þ¤’‘)ù„ó\ë;·*­ç+z•(:nƒ7Žùk®YÐN;ýií_sóëÛÛþüõ©,—¯X½Gà1½Ü*·eSKËV ÜçÒ~ùÓªu$Ciø?Nãছ.g~þ{ÚK^òBæóþc;ùä#Û!‡Ø^óêµ3Î8© °kË¥—^ݾòåo3-ãjFû¶ŸÝ¾”—µ}çÎeû¿É4þÏËÔ‹5k6µcŽÞ»}ÿêET6ƒ³­Ÿ´Ï:û¤öÝï^ÉB¬Sè8°E¨«í»«|0?)6ÖãðW—士8#ŸªDFt?¼ib9Q³x«CÏ#6(u‚ÄÜ&/ùŽ¡H Œ—½›Ïâ›6ÓA^åžd¶Æ|CJt¦ïh+¤ZêÖ´ò{×ÀŇ™#yûhý‚0’ e‚ µ䋟09áÚ‹¼§A¡Ý¿ð¦…éˆI¸ø.zÒapoy3•oÒ9‘—¡‡vlG(½Äw´ô£kS¦ñ v"ã ¯={¥ñhBÃ`¨ôÈ]ð[Œ¥ ;Ê'ÃЖ<ôL­I/1•_ĆŸZŒZ¶!ÞMÛXwÀBIG;€òc•3yúè6c`O:sòD‘,©\Wq†Ä•W.€‚olø¸hØÄî,4¤M;¹yì´6köÞLÌvœZ4köƒC™Z4š¯{ÞoÍ´¹eË–³«ÈcÙÁèÑG—±×ø#íÎDxhÑRÎËx ¼·ðñ׌r»SÒ¤6gß1TðÙg<4 Õ“znajs·Q6ÓHŒ=¡lTý¥ÃDC?y¡î@ixùÖÊ ßÒ–?@ÅÑ»ÚåŠ]«RêÀÎf®èóƒHë‚–÷¦2Ê[îãÃxH^’œ#ÞÚ:uB_a>ÿ¦dÍ;‡i=‡ùYlS[WàŽŸÍÚ€)L!¼ ùöîwŒ­OCL“•¨Äø‰ÝÇL b¹Ðfà16L»ùž²šòv Ö’bƒ– n­Ï´ªÀñ\­Š’ÕâoýR²šÖ$%' gù€§Öæ|Ž;ªoãëéùSÏd(Ý7Òá%gÊ‚72,Á–G1Ž/×0ùÆ=í8iÀÛAìŸ4wß{?z› ùìdX´xI;ã©§phÞ‘í“—|6!GðM¯>¼ä‘vîÙOoí¿ûÈÇ>ɴ‘)'ž)ߌ|¯]·Ž]ˆÆòýxüÈCdëÛfzO¹ÌJ%}®a×ÒĨ4¢ú݆ˆˆ);BëlTŠH Ëǯ ªtS ™iPhAÜÇ鯩t—zƒH9cý ÿJ‡mHWäX )0AIA!}ê ¸e€RVàÜÅè H!âtDßL.IĤD  勒 8O ™«6à9 ìþ7Ò…±ÂW†9)–!qUøL¦«* ±¨½¤Qœz_=áê à4ö‘[Ÿ“m¢äC•hd‰•<:,„€(Gé%•L>›lQw>ªSâk,Oph\ÅB8Å%©„íÒ¹Í\2l½Ó ‚sFÅ™JÑ|¶âÁìH%.«B‚Ša|p A‘yÓæCð>?´*Ò@ Õcò)鸇VäFèLí›F×ñŸQ]ùô£Îùã§tãRJž±›m8Î(Íg€´«Ýèøá6¬ &+\õ×竎!ö¯ýo0‚T;M£>=KQ¨5pó••|úTÜÄédä§l“;Îcð²ó`%?ŠÆÁ‚ù6´·µ÷ÿÍ[™š°S8_Àüùg±×¶øŸ4ø±?pD@Ù èÖ<*GYˆñ,êÊÇqÛÖþéŸ.i/}éË«ËýÙÎÏQ÷rØgö7ïýTÖP‡°£Âb¶÷ÛÆkà=Ú‘‡O`.ÿÔŒÌ8o{ ¬¼†ñFaÙÒuí¬³fµùóи™FÀ)'ý½8„ª^‘*;mµj†oîá+ö®Rø zTûe©0NωÃöÍc‚2òNƒÇ2 2¥†{Ѻ‹†¿É/“^U¶ù+"U^Yž =€Gþæ–¯øƒR£Oi!Ïá14M_ù'nIš?¡"#uÇ·´}æ‡øä¡ÏÀh‹F¦»Ãétö‚.4"1“h‡_IÊFG0©¡Ÿ©&ŒWÙdY¼„gÒÂWÒS6£.*…¿ÊËlÁàØµ6L¤§Îgä\€È/.•ÃíøÚ€-'\Nzí‘'ð¢ÏL%Êmé“ôÈk&¼IùHá“ ÃG^Ü >2'^É7‰À×3:LMÞ¢¿Š7*·ýÔ·SÚ³¶8»|ÊQ40ÏÌŽ[ÓmžJãÚFµëÊfsF‹Sàî¹çþv-S´®¼ênÒ8ÕΩoSs(ã‡Og×·wó¼­§ŸË”¼õip'ß°Cßj fÌ‚µ¿èY ûÏÿT¶^Gw dæ:-h‹ùDZ€×·….ZLyT[è Ó§Mió<ÔN<îØv ð¯_öoÔIL5c±ÿjFчÆ¢Þ ”±U)WcØýg- wüqg ;¾°‘í6ËúÞ`LcÍòzh¡aåÙÁÂ¥œòë›ú[I·ðá%íe/þmÎ+¹·}몫ÛáìT÷õ°íš÷â9cZÞLgÛÑ Ö·¯}ç‡í´cÏ´Bw#ò$ôõë7°öiZêKO&v›å‡×Ý…|»1’· 9K¾ù?óô§µÃ=¸]þí+Ùö›ó7vaÅ÷Ž_ËÐ(43žæA<Ðü÷¿oÊ`¶Qr쇯z²R œ©M[†i«ÜTĉׯ—Ï¥¦påÁ“4$­‘òr<¦”»‚9Jhœ†eJ¾ ’‡|†Z*­È#x;‘(\¦ÀoA·g§lÌÉ£¡ ÎÅ)‰K=`Lq–òY2Va6ºè­J^‡ZsðL+sè„ðR>Ï\a“†mJõÊÏJY‡€©ÃTjÝÈ™T¥¥S“ÿÒHÎ1¾\ vôÏ`Ïȸ$$­â0z+øäX¥-ÖTüÕU©{06ÌË«ºã~äHÞø' yU‚:TŽ$…A:ÒŸ¯·+@'ÁŒF׿[¥Mç"Ñ⯰6%fè„F€8¢åõ$?ÄÎ…ÎC*+nrÚ¬ÀÚSlSHi–D“ÿ‚pë•7?ü– Vº¬©¤IWeÉê“uÒá4OíÔèZQ.yl™<ù…ÎúÆSæÊK\bÕÈq:Žxöå„âg³åãDÞ ìÇ)‹ë2âY=ù1o};Gjô©>Ô‹r“…äeûÊIT’ðe¤-fÞA Ò`‰¿áY-Û1ÒV®z(åŽ8vÜbmDk ’µ]¡½ô'‘ž‚^šà±œT>’&q¦%¦”Ãr’a–0}Þ6uD\Éâ Ü$]ýO^桞®FØ 7?Í=?‚ ó%²ŠÔpþô3–-e ~ÒÂ@ÆבIQÓ&áD‘ßøÈD¸IV'†I;ë Bi”¬Þ .hˆˆÿ𫇬”ò .Ä*&Óɧr•ýFwÚñé¬@»AZø’9!£(ÏÁ$6žKw©lLªƒÐ…]fÏØì}YÌÔ ¹1Ùw¹b¤t,‡×Íâ ‚ ° ¾ì~Ôÿdï=öªª|ïÒ{#=! B ¡÷Žt±·Q§8:Ÿ£sïèØgœù¦}ã8–¹Žut@•é½H'@¤‘é’@îï÷ßç¼ ¨3¢#/ß½9ïû<Ï9{¯½öÚk¯µöÚõÀû ›¡{®Üzû²úJ7?ÁÇ=2^£ù .{î5±ì½ç^6:Ç :(o¶€“3„†|(ÏíÚißzì,Â’ÅË2ò·þ™uì;X\æÏ[T.XV{dEydÖlàçðñǧ¾ÿ`ܘ^jç vºõê›y烹_9èàá8>è(§uÉG9íHt(?#4òŠ{ä»Ê²\¨|UŽ#gí¯‚²QÖk•@Qhù=ø|OÇБÌÀL>6K³V®Z_å¨ÖA,ƒêE¼Ë±Ø7N~•rć”ÓO?¦Üvóœ2f\ÿl·!—ïÑ'4v Xm¶TZ%ªùüþ~€mLŽWxÈ/¢–8é&e%>›‡0ö~•á ø“˜W¬H`@[ÛàkY]XåÈrW<¦NC¤M½xå´gé2)ï2‹C”¸øntBõdÍ<ö0y‡çX:`à…¯Hgwü ¯t&Õbä„-:¯ ¯g6J‡ŸåtDä°ˆ©œœõ–2}\£qªu:u€×rìå܇9Jzê>{”»î¾·œ|Òq¤ófÐÞ£³×æ°3ʱ¹jß}GAû–ààr¶î=+/M×Zñôʲï>{³Œh¯òÝï}?{;tÆuˆßpÖks°t_ý³kÊÝ÷ÞŸöîŒS^C›³;2Ý“ea÷—ó/¼¤œpôáåˆÃËHû‡TV³©_˜ÆŽÍÌÀ—¿úu¥WÞó®·Ç¶3pÁÅ—”%ìéÎÌÁ¾ûLÓï ÀFQ.\T~páÅe eßoßiü®åÈêùqø½?xÿyIálÂÔ­÷¼ý-t”v¦ƒ³¹ì¹ëÎéԼᵧq’Ý.ð§_:—]~e:ŽÜ¯æ¥a_ú›O¤Ã"-^|)ЧÒáñê>öXYNÇcôèQå½ïz§MÄl,W_s]ù9|ð$£y/È2¡'O.ë7\Z†Ðã×<Ë/XëQõ&7GÇä§—pxKb!„•‚ö;ÃZ2 ƒõDœ‚ A¨e©é’V|R ÄeÝU¬üÖÔ±æÎ@IDATüòÌ­ä„wÄÈÍÚq«ÐÙ“Ðð)»h’Þü­¥Ðc¾š,óOxo( æ˜:„vù’ͪ¬;•¦«õD¸¨p<..4åÙ{€¸Ò€‰¤üƒö8 àQ’§õAþN‡Âu»q†”wèô!4ƒà“æ€ENLÁŽéx3^þ(õ¦‹dР8Zúªt•-Zsê&2 Ž”—¯˜m)ýŽhzЉ¥.¨ó€Î8(ótµ­›t”©Òj½5÷„è”H™H-U:KA ýd¢MÎQÁ!Jþ“V6´X ʤŒ‡È^+Â?iSÞ67ˆ í°š’0'+,“Ÿ¯ZZé†êŒéA˜ú£E8íΉϜì‰d9©añ_M®4ðhýƒ*øHcûaÇ´®§¬¼Y8ز»× ?Ëuú³I¾ëq¢& ÷Ú,öÐQöD«•Œ†ž÷ý;Hµ„Ï>\‡lbÙiÜ$Þ”=”‘P–4ŒŽ^»ÿ` YôzK‹8wϽâ ÷¢#òÔS«è(p–>Nß“,)Z¾ƒ§q¬sÌ霹¼gáæàu”})Ÿ^,Çø8Gó>ÊKù>W&O<|=sª—ú¢Ž¨—’Óݽ Lri¥K¸eU÷”‡pPæBzãf½å’wÄÃP¤—,ªîZÔ#¡upÀ7A÷îÓ={º÷0íJfaØÜÍ5oÞ墋.g©Ã“eäX^:îúCù^ó!k°ùÅ7Ÿ…«u‡dh×Vë=$CPK¦ƒ:ÒhIÄaùb j"‘ó¬p$šGt|Ê–¸b“RGhtƒ_|¤×¨ç¡•4‚Ÿ½uy¯ÞŒDÉjäË#fsI¤ù˜œuÿ^Å›P«+ÑMäÞ‘i;©ét/^²ŽY%—°=ÍGœƒÊ‘Gî^¦ís8G3³×eòøl|ïÕ«g:š:§=6—NæBŽk}¤Üsß‚2oΕåu¯ûýòzÞÑâ¡nn?îØ“2‹´¯£àkÖn€tj_9½à4—ÅT–´yÚ¶ °ŒeqòG¿‡ûš›n-“YF:røðòæ7¾>2|íu×Ó9Ø»œròkÊ­wÜ]Î<í¸<ßrË­ÈÇŽå裎¤|K˜XœÑòysç±çæ‘rò‰'ðÆù˜¡x”ËÝ,©™R^ÿº×R¦ÇYvz}9ð€±kùŸÿtÙyÒ„rúi§Â£EìU{¨,×qÇÎÏIg¼·üþ»ßÉšûçp¼H'a* ãÏ;ÿ‡,áÙœ˼‰eH³ÈÛ‚Nû¸±cX6{üØìáèéÓå›ß=Ÿ}E#‹¬»yÁ˜´X†w¼õÍåcŸùkÏÆf¶a>oÁ‹È~ïíoeÐí¹r͵×ÒYØ­œ~ÊÉœ°Ç;Nà›ob·Ãa#u}É€2¢ Î&`™mÉ|+BñEœ¢Œ„ŸÑ±x!°Æ[uÈ]·ŽÜX±*¾@V"q¤_&ê}VÐÅ!¨¿UD¤xøG¸BÛ(T””Ðæ_ʈ0p™Ê1°m i°JŸQZ M@m “§``”=ñù®°idJi5H.‹—ŽaÕ!pD®RªÔYNîÓ‘§eѬÞNœ(-O»ÿ€ÇŽKÜÁÅFÓüH“´á¥“•æSi©Ù’?ddpë*K«Vã¤/,L4æußb³¬3ðz&w}æ‰üjÉý­8¼Ëe´iL ¯2 ˜$"i…—6½LÉÍ»à?àä–Ë©§,m‘ïü% ¸k}’ËÞøº1£ÇâÈ:Ô©hG2R?ö¸ì d®¼Ê©¥lë8r”<Á¼£vµÌ €®Öá±D¡:ÀÀø‚œª 'IäßLƒÝë˲qg~YºdÞE•.V0ÆaJG• CSçÂú‘ÇÚ0dM>aa²e_Åâ™WbChd¿ àm¹«Êþåkœ›~1gêÎÒ8•Ç‘…&¿4DqžšN£§•˜.L™v+½û²9oèy×/®æÈ½eK×ó6ÅEå‚ ¯€Š¡l°Ü)k#ßÖ!]M•-ЖÍôðÜpÅЃžÎÂ~ö Y<©Ö¥ñŸ¶ÏxF$7sŒ(Ç1’ÎòBTôˆG ®ø„LËaÇÖãç”ïI@jª#é[`°’T¹4?út¨S^r ™ùî­<…Ô”AýU·¬a0%P©mĪ÷ ˆ•¶,ÇóÞJ;XM9–.B-—²¡ã,u:–‹[`=®×–r˜{(S÷¸må-XÓ±$/;'——ܶޏ£·nކO:­®#÷¥<ÊWª¢à6]f:B¬”zA³ÄÖTºL¼BÄ•AdÅ’¦vÔˆ‹o` Íehé…?–)ELZe0€MvÐ@þšòëœZ¿a±¦äuv­Ö[åWù"/27…|U~šÌ’eÛyôÁ6Ìø&·ZNÓH/¤¸H¬ä&aA+µ²Æš„ä§Ä¸TÈ®–Ð,BÞc£ˆS']:|XFû³'fÙ2š‹£¤mÙÈ~…Õ«7–ë¯[ÌhêR»´ÈŽâÞ|àPŒáøÌÑ,ÇZò¢¢Ñ£‡ãX £ƒ0† ™»0B8çúYœ—ÒuÍ2;vN;íDœ›®eï©»–o}sfœßÝá&c/g ×3ƒ±‘Ò–±ilw-gì›BÞ(J<+Ûäy4¥'\ñOmÔø—©Ö:ê3uà ßòÖZz,ëÌ™W•}èŸpH¯bÓãQðx KpÀSë¶ÅY%ƒz±¶ÅzñœüT¯Ù*³DêÐåàM:syKÅ=‚&m‹m’öÑzÖYôàW¯Éÿy+•OÂÖ«Ú)àåG“©ú&O²ô-…¨¹Ê RY”EOIc[g›$™æ?y¸á½+õë‹ÒÌ@Y›?5â<ç3šÎäÔò¶·Â~­)8¥£™u þ¾YŸâMë¾YÙµû·ßöKÛ<9K™«³Hûìãfõƒ˜A9ä­åÝTV­á¸O:³Ñ—IÁøIaå‹€®øXÂÛAܧè¤Nß{,)úÑ–AŒŒ{4¯K\~~Ͻåÿ/eÙc‹ËG?þ¾rÖkÏÌYüÓ§M+³gÏ.xÿ£Ë—ÿþ£,y63 K—.+—μ¼üð¼sËŸüñÑ^-+ïü£–ÑèÅ—?÷ÌÜWÞñçŸ){Uxè‘òá~ ì_úõéSœEøÈŸ}¨L™<‘¯þ,í‘·§¿íÍgÃåÃñ©µ=ŠeLý©¿(Svž\œõXðÙás6ÅÙö*ƒ Ñúïð¿½N`U#å;4ñ‚Óeµ^¬ë²V"éɈãb KEfX s­ìÖ ×øä©ŽYÙV¸ÍÃä5E•ëæ©ÍײÆ&òV!„G±(Ëažˬ³IµXÁà a-pÁqä-kïå«™ò‰“0Ó°¡1HÍüÞ§}¦¾i c”#U11ü5ߌjRA)—ÄGEyGÝ|ÁmR/n%C§uäè3é‰nêÒ‘³ìÔ7ôÅQ!/Ïz_ÍÑ Œ:÷â¨?GÍÌZ<©zosÉÒó§U©„Ôœ¸oè14¼ÎÁâ¬õTÒ&qoxÛä±µpæä¥|68ÚrÉw) ™*@…–vá½L©ó˜òÏ]“qM°—|Õ$©b3¯ú$߯ó’¢}ú•aœóô±Ç¦ÑïϺÙ@šümyÞ„ÎZyŸ0༳¾uæÏYÅúÅeÈð>p‹‘xœÆwRºì0Š‘"^ÖÄTrÎ9 ‘®OÄŒ,Ûý {:O2Y¼d-ç¥÷)ã ×eQ¡ƒ«à°ûy´(3ß–Qº¼¤0åð±²!á¹L++Ömåwà%¯­nËn"â ÐâÊ/È“ œ?IcPETåmk¦Ü$¡¨Ag|‡£|m&æoÙÁ¬ÀÍ7<´Ï¹7¦Æûø­d‰okÚ­8¼UÎLed /\ò‘µeì¸̲¹_Àhp{2S›ùµƒ‚!CÆ•µlD“LŸÍŸ+Óâð;¦˜_zxṡrLè¦x$ª°iOˆ Þx\ÜËßä[¹$ΰ\ ­–ßu×Õv5´@ƒµ‘äÊ0µr‚A«U;r‹áþøè¥È ?\íhZå|‰ëµÅWm0x=vµÍ¾Únó·ÔÐC¸õæ{D줚çM›7AÏ ,é‘§nN¬íR<Ü!Cº³, UV„ÇdŒÎ±‡†97<Ιó$GgÎ'5æy|¼vç”"¾8zŒ4Ž•}}ûö§óç õvMGAÈ÷¿ÿ=8Ê»ßýå2c?:Ðú<—E¯+ÃG³üfbœ KZ¯6ù¦51ü´q¹áÙ2×Z¶Æ·òäSM)D‘ÀúdÊa£â}×–O}ê[eï}†•ãO8‹cX=¶fÙ¡¾êe÷ÍO7Ï•ë¬ÊUòn~›äD5ôˆ†/e¡Þð  °ÒH¸ujŠúí]+ qþ5¡É3÷ ò‹xøIi|"òVÙY¼`Ÿ5Ù¨Þ‹uà–[xH;omîQ·<¸ªlØ|/±ž2µ oWžV~ï÷ŽaIÊ„2‡ß“y\«®“¨ÃÿÄ ËìÇæ•Ûn½¿\~…k÷oç3–ÏŽeâø¡,ó™˜Nc·´ÑÈ'‡@¸Þ£)¥Ý?;¤±.C¦¥Yù¶Jda-úÃXõÁy‘ždCü‰ÇÍr°çó&^ÿtmÿ=¼U÷ðC*ßþâ?Æ6Lž8‘Qõ5åzfΘ?Ÿ™‹}²$h>£ù^zyy|Ñ“åm¯?“€žŒŽ/-§õFŽîž„Ÿ•'fÝS>óßɨú7¾}nyÃ1‡¦ããÚzGõÍoô¨QœÒuo¹üšËq‡\F±ÇÁÍÁ®ÇÇi<‹/)“'Mʲ¢7“3,?¸àbÖê¤,¾dc™0aBœþÝé\¸lè!Þ,|Ö™g”w²/ÀM¾ñ?—%NÓ§Nå„KÊ÷ta9tß©Á¥ãægmÂØ1c8&{eäfï½öJÇâ/?þçìèO»¤Œ+9]̶¥7eìN½lÙ²tLà:üÒ·Bvav7+C£Ø:Xé-PöîW³Ëþ¦nb7bÜ™pM™¯”wóŒuÕã”'§N»p,)˸¨P ŸaË R<ßMCF~ÁçI—Ñ%zºö U*´øŸ6Œ6*ôœóGœJ%VàºÚ;ë;´1¨ŒFáÉnlbÝ„ ÒNËB88¯ “tn±CD¼S…Šké†!Sñ(‡Î«Œr*·´n‚w³σ{Òn—k3i-S7âž%ÝÖ¯9Ý›99ª í,—›jîP¯âÀ8ÔL=º^•ì(àëÏTçÀÁLyöfŠ˜%*‰ÓÉFRÊd%ro¹ ©?ÁŸQEa¡Óó²£­­Êg}H… Òã¦É²©’qvÄÇU@e#dKºxHýMKǼãâV~;c¤É&3T†Â{òlRå‚ìnšð^„òxiHÄM½9²³téš2e×Áå$Ž×ÊܺmÉ0ùöë—sà¤Se¾uk'.Rç—õ¹á´F9žfYÁìGŸ(÷ß˺Ü!ýãึ9î€ÂÀU§‚©]e€zt­|–u´ò ¾My{³=õÌ}hFfC—¢³ýzå9°†cù-ZV~~Ç*^^•Í’Ô¶k3ÍÚÕFkC”§Øgm$•œŽ:ºÃ¶+^8Dñc7‚;¦ ]ëˆkÏ43q’›´1 ‚VŽÔùÚ¨2­ñ4§ŒóùìÄÇÃ>X>ûÙÿÁR¡¥å–[î(÷Ü;+›õ7ÛÞÑÁpÏÁ‘ÇOdiȤҥFv›Ô¯ôeÞ§qDøõ[ÙµWšðÿ†üÔÓÅèéwÌ¥>Ÿcľgf‘V,çE[¼‡¥¾{o}?²œrúî¼iùe"£Ù£G`6Õ“cºñ¢4F÷YvrÇ?Ï’žë®¹¯Ü{¿{Ræð_†šˆ3=‚µû¯O{ì¦øÌ(#v6ŸÃ VM{÷AqªÔò«´+qú ʸBl' R­¶(†È2_~Õ+m φÐzÕé×ÌìÕRX—´¸Ì­'ëúßÃò¯Ç¢ÌÃáÏ[åqÖ³Ôå«ßüN9 '}çɓʌéÓ³6þ]ïÿ³Ì"ø&ù[ïy¨¼î”ãÓ6¹T¦ŒÙ={V¬àýàðˆÐY,o:õÄcé–ûw½cKî-£˜ypìžYsËûÿà=ðo GY?]±ï?=§÷ÜvÛíå¾YsÊ~Sw϶!̶õ‡¶9³ç”‡8ÙëOþðÝeÏÝÙ<·.G²Ã`çÛ5ûGymëÓÌž¬g¹ÏNCü`yëÙgd-ÿ·ÞUN=é58ôËËäI3àÞó¶^ܬ,ÏæÎ"Ë<ñȃn¸‘ÍÁ=S+ÖCì…¶¢NŸÂÉøZ8±L¯¯gÍÝèQ}ËþN(½zwaª& · ·½¨®T»a/½oásÖÄW!¨qõÞtíÕâßfê*NÂÔ'ï€à_£(ì¶yûâkÛØ*1ôýËÓKÅKâTð턹îq.oF]<méÇi=¨<‰Œ¢ ¨UñyÌè‘y&&MB¡î‘lkfÂ&­ÍŒÊç³$0¹çÆ@ 3§„ &G’¼(sðŽß Q9Ì0#Yê½ÚäR,œŠ¿¦ Èä›6°à†€Xó¹ çÿÃ8>oÆ­W~¶_/—xÜ¿åå¦&0´10ðáró ³Ë`:Õ)£©3÷fdã½u¤¦Ú«ÌY³þ{ÚÄÚÕHÛ³}ÜAegª ¦¹ý긮|Sâ#F )W^~W6öf4Lû»B Ź…¼T:ݶÊÔc¼e»˜˜1!ÐgjTy@±ÕW7ïf@XÍE5ÊN@Òl¢ìD$â 0 íFz“džh0Àx: É£±2±8DÇáÚç´y-|Jdv¤NXRGí¼8*fJnæÀ8⛥9¤±õG·ÝßêÌ4gÖ4ÂPÖJ«6ÚN#Rð´tVBqµ áhî£K†{ADôHüŽeÙò§ËŸ~è/ËEzbËl>»s<ï0ŠÊˆ'˃N8ia&¦¬µn:ˆxÅo›ȇYÿöví/Ào‘a«§ÃÔÓ™w²ÔfSÞº;yʰrÆ™ñr­‹ÿ`6ËzT¤ƒ6ž$5gö|:‹Ê½÷7éòÖ刪zš ²°É?š_ÓÆ!H¸ £,e‹4zéœÔÆGü#qüç ùT9l÷’9»c¼kׯÝXÆîÔ¯ì5u—È¥§¿ýê,Ø0;FG`OêdñbÎ,Ÿ·†©äP%Iy´m°½·‚¡g(bÙAæx¶‘™qÀÞlÞ1g:w¶SÑY}5å«ÞŽæí«ºk™yñ½¥÷(:Î`bÜW î¥ÑQÿ¢¸Zß~ûl<ÿ\mWyp©‹ÂQuÍDÞ‡@Píp#d‹¬f^âhJfÈ7vƒ|â8Ç!!„t±9b6IœÈ–2ðè¼H¡ep69¶ˆ{íWÊ@œÎŒË_E ŽMfÿwys¹¶ 3+vÿ#ßÀÕÁ™jëâ›'ºÒ™uÀaË„øKw€ é‘䔕æŠ.°[rÊ›·Ñ/gŽ3“bçK^úgÛûoÒ#bÊÆ½ñ‰mîñ­rÊΖ.oèÖÐÆ 7Ó¬O,X^–.~¦L›6gj:Êtté’5åÄ“÷Œóßž›.ÅÛ¯Î〲î[’;qzùÚWn`iÏÎåÏ?òtìF³AvuÖ¦ßrËŒî³vÿö‡éwÆÇÍÞ,`£ï”)ƒñõØÌÊT'³CnZ]µ’ågщZ6Íèò«¨(Õõ»ê_d_&MÕ?Ó‡çN¹Cƒ’/S;cÅF»ÚéWo ã‘8ôÎ6„0?.}É öðG7°ôÔ}+ýû(ßúü_av ü£9擆˜Õ°“sÈÁ–oýûw’™'öÜqçŒâ/);â8`þðãNΆfßÔ½„‘ôÝ'Œ-·0bÿžw½£\òí/"ÿ ³‰xݺµåo?÷År:3ëÖ¯cþnå[ßþNï}™Y¸÷ÞûÊ%W^]öÞ}×òà£sàÝsðs—òÉOÿUYÁ uo~ÃY9¦÷-øåÓÿc:gò,‡³gà–ÛqìÑ©Ã;¤Lüú7sZøž\º´œõ¦·…ýúö+ßÿÁ×RÆÙYùâW¾QNay’m®³s71kó®w¬+_þü?–éXHߎ-uŸ‚3"…}bS÷Þ‹½÷”{[Ž>x?f{V¦dºîKø­°êt=ž3ÏÞ—×iO¤÷ÀYÆBjÊšÙ~ýî9PuªƒçVôλL@9»•Ë/ºƒÍqaT¬S=}—%6ÕJ¢6#@Æg-$‘6F6^9Å&w(& èh­œ"Jóh£bH£±QxÑ'¿É¯Â‹ˆ„µ!κ~ƒ´Äš­lŒ‰XÅ nñ 4¸]hˆ"ˆÉS£a¬ßÊ»¥ßoÿ¥/ÓdÕùW@¶u *ó]Û¨±Ùcωåñ9÷dÉ⮥1ÐÀPçÊ‘›úuz„Ù¥ O?õL™:}lŽ:tsc:¤ï”BmÏ´á@m€7±wd,ë«—rŒä*–ÜõaÙ‰{ëU½Ä¶Îf3´*%uéà¾:Úu…ÀXÒìàšQ „:¯@p‹Ùàâ^Ö¥Žƒ7?âuzÒl±"…gn’H<<7 ·,)Õ®‰ È.ó»NHÛ9M¬øãìˆl™õ±fL¼tøxm¸Dç’ÑØæ€C”3„µ¶ÐåNîè©÷²¢Ž¤ îê,5¼5³”¾Z0òç$´ð”3kîä-ßõ^9ó2*ÉÄ‹«Ò^—~ZN“‘.KuëàO½Ðç]¦ ¥½á´wÖ;£2aïó˜46¾K ªÓÔÛ¿:‰­ž:sºÿ#pÜ×–+~| {xž,ß?ß5ÿ®ÝÏg06v`™6}ËFx‹uš%Î úºWKÇÚuæ¶Ó:ûY­EŠ"&Ÿ•TeÒñå7êè Eúˆ£ÂËžÛ{%µÊp#Œ„-zó«òkZóó ?$ÊR7›Ï›ÿx–ÕôÇÁï×·K¹ò§?‹?áÙríµ×•Kü“rÜáÓxWÆeœäý§í]®¿ñ¦ø>n*п¹…½ަJçÁ÷<Æhû.º´Ì]¶ªØ‰x”Í»wpZ/ÕÒ¦]yÕUåû\V&qªžïã¸ó±…I;yœsù5å´ÃH~nž>îЩeöœyåäÈÒìAXV.¸è’t&Q†{ï»'ý>föfÏÀ(и8{Ý`¼ë”)9¥è¼^þž9àGì-xbáâœ÷ÿ¯_ùZ9éD^ F¹\÷ιç•Ç9žtÙS«Ëÿè÷ƒo~ç{eÆž»æÞº‹ý†ÝÚ,ú[TvyÕŠõåÀCÇ׆˜i‚5Äêeûõ»ç@Ëk~Ÿç •n4Å2‹½¦+wÞüD6’*k(®(c£tÞ§q Qu#±ëasºH*Ó G¹ µé޾©†¤÷²1Ì­øÓÉ: 6 „å –©¿^é½7>ÕˆXŒLŽ‹Þx[ îí„Hƒ¨BùI=þÛ¨P6¶qu¯~¼úºŸ¬ Ô`T£AÚíW§q@C¨œ(Wö£sÆÙ¤ä™ä:„õ¢ž½Ö|ÚêÑ›!C¤Ññ”¯íu6tú—m€ tΘ紗j;ÚÊsã¶õ‰&£«iâ£ëØ=ê¼u^µ)ê¹*ûÄÏ ¾¨Žô®ÿÏH6²ààƒÖÉ™¤éüjDÉGm‰nmÌ<ÚÒý*˜>ÑIëœùH¤r•N8µMÙ{P‡ÇEØhz°(ÛÁ]–KûÅMlš÷Ø»À‰Ÿ+¶U8C®w9]¦)Qh /ü‘v šœ*Ë ù9gáUïÝ[¥}N¶ÒÖä•NÏÒ×R¬Ë•SÂì¬e¦D;ël Û¥KXCì¨XÞ,[Ì7éz¹tÃ=z«ž~¦ì»ÿ®È/EÊéE²#·ý«“8@Ý;X«Ó;iò˜rÙÅ”;þéâÒ§g_Þš¼#{©Îˆ¬x”ï[Ñ^{€CdyŠåƒÿ* Åš­Î¹aŠ$)Ô:–ÂU}¨áµÝc•,nrçw”W¶ Ê>€³jé ƒ°Î)üEé¼³ôh8o;— ¿‡|gíïS®bîØQ#ÊçÿõYïNeG^4yûÝ÷‘®KNú_ßøÄÙ¡±³3y¸dýƒ‹.++¾un™È1¹8!9.cuä~8Çi^sýÍå¼ g²\¨GÒïÊ‹ÌvçÍݾ=ØÙ€OòãeÿÃË‹ºz³${'%Éw_BöÍ¿ûXÚ²¯~ãÛåØ{¥\3¯ºþ÷fT~`Þ“ð—œèã;|Ù×u,ÉùÞ…—g†oVÀ¾ƒŸ^{#/êœ ÃÒ~Å57A¡oßTvßy<3ƒË öÌáÈÛÁì)ð=ѱ¸ü#Ÿ)ƒúõ.+×®/»s\«O;qF:AçÿðG)«ƒÉ¶¯¾PPÜ=Ø´­=á¸m7ö`L™.ØyʸŒøjåŒÄò…†­uÛÖñößF3~Gü­ÊãF˜¨¬ F—{ïz‚]é¼¾šõl^6cùµq‹£Ž§eÐ˜Û T(´ØÆ£mTœþQ³ë,A«¨MÃ'¬ZŽBmaH Æàö7¹¶_< ¸äT…¶iv|­â“ÉRYùoÒáèµÁäYTâ!Äzo“kÓåt68ù`Ä‚«%aûo§p íˆ9S¨c0zìÀòøÜúfB±µW—q¥ò­ÕL;*Škd†_íMÇ+ÓSH¶_ÂZSUUh¸a#³gQF­=@Ϲê®:‹³Zk\õ„~õÝu½Ú ¥Ë鬟3ƒÕ­ö«Å!RÓE‚D¦±BXvð…¤ÑY¨#‹b?4:€à½í™Ç¸:;!mŠaF×#€&œ6„‘/¾#oy©˜âÒQ†¬C½›„Ú+á!I0Àù‡‰»t„K¨ß)|òq=tN‘‰|ÈŒ¨e‹«ÝyðšcŸíæR òô¥O;œÞ`·yIøÔ÷t,£m•é CìjÕ2/ý©'à¡(uiZAó.l±YÄ' ;†^Ûõ4lè´/«Ó6Yq¼†ôíÝ­ìrÈDÂ<öÑ—kÑ9F檌’ ‰êO:{tî²yÛ|£mëí(l± ¶Þ“LUž”„XàÙ6·<©3b¯þˆo¸U¦ñcøö)i×Í9R_ê ”<#R&UÆôGmœ¥ˆƒþØœùåλîŽ-ª‡Ô¼Pv<ÞS›÷=m9 *}SÕ6âJ…1‘ølëQàI€ Gϵ—à€ü™5øhT…Ê=ž½’eÒºä§6Ü9fÏP‹ÀGÁ ]„Ù¸ù*Ô"eðͱ+9!fÊCÙÑ;‚ÇAÞ˜ýö«S9`x–¾ PFNr:˜r„¼éT(O¶T±BÙˆõÑÐ#×}9s~ÐàœWŒýéÔRýß¹µæBƒÓ“ë×z‹cˆ]¨ûtÚzvô]x-Îb\àègœ t1 =$—ºn>ÁëoÕ{nÀB„‚c:dEZxJþ‘#Ã|ÖFå†ô !oòÑñð¹FÕ_³4ªŒú4[×Ï×xA%‰ãáÚ9¿ƒÓ`o½àž,Ó¸Wm"y¨}3L4‰ ±>WG[:â,%Ky0zÑáØ} ó—ŸÎj<ú«“Þ.ï4‡8H1¬â_r‡¶†€T@­GºGvWümé -9ñkùÌ“ñØæØŽhÓüæaûW§p "H=X/ñ ¨½õë7V9‚¢*&ʘ:ÕÊ¨Âæ½³JÌÆ+S¶ÿ[÷•ÂK9U§M­ óNhî•ýša«ÜÊ’ñ´& ªª7$á¹¹\=M¾†§?º¯ÚQïžÌ$»ÂôsÏ¿ §Ú ÅI®çÙ3Hmšmd0Ï亥?Uî%ú€óòhÜö^¼^mïŸÅ¡ö2¬æƒŒÝ;Ÿå7¾‰wG¥®c#²ò/® [Êe?¾‚Šž*K9²TÇ^‡_:ì¦êÎæÍî[lh!¯ö¾-Cûœ¥÷IÙÐבxðÍ6o“v+ÐL'Æ«¿>eæ?Í{FÆ=Òa¾ÎÚ²¼‹c÷jçËù&¶a#úÑK¡%¯ ±È¶_Ëô%S®½èÝ9¨<úÐrŽäâ t•%²dCPˆzÏâªBEùXa<‡š;Õ6…ªÊgSk}£Åj"OÝ'À}”\€·ö ¦WÒYÁ#Š4*ÄK¸_\Èã)s—ŽX†X“ÀÅy ó@–À“gòÆIäfYöÎU˜Z†dBèö«S9@}fdÛ¡#Ø^6Ê]\ë5ò‡yâ(QVôä-Ä}8Dy¨£6­L´X¶ÿvÔ.ë$/ŠªQG,%°†l¨½ìÌÛ˜ÄÓEscM¬k×}´ú›¾ÐMÓÒá ›>²`:âÐæ 'µÎfî‰2'QÄ¥Æ)þ|ÂòУ5¸8ÿçìSœ ½¨CÀüD¥Mjs0Xü”„e7:V–*!ÀKeò-ð”,°•F]¢ -ùJ«ÈM ކ_Òã³@â _£]Ö^hªùšBE¼ÄñkQZçžI•`Mlx— soxÅN½õÜ©wes_sEO“ù&I–$ö™ðª˜s»ý«Ó8PåÌ©ÈJFŸjôèpk¨vÚ¬CU¥•¨ˆšÑÖ±äQ ÖjÕ•Ü*\MÃ]eSgÛò22@jõLéh?Fxm$¿ŠO\|¢[®¨Bäêé_‘{”!4"lÎTYs@ðù¼àËY —;µWë8ÿ²çmã~Õ½é¶{)Kã²™Ç,æ,þ™¨¯-dý}hÖì,åÙ‘%9Î\X–mñþ:÷bÚ®ÍaÛ°_uÿÒ´Ò쀱ïp‰mêÞª¶YN½û„²Âƒözt-Ï=³¹ Ùe0‰z„Á*¾Æeûõêà@ t­å¨˜ÊÚ5­+•J5e™…-†Ü¿#„*â1sTR‰°vmÚbÜ9üºÞó­f”Y‰ ù‹®‹­h¦Å‰ƒ`Þ1"6ÀÍÈB† sM¿WG™hUl›CGm+5 þ×YaÜTªPkôö«9 8XU.jÔ}"­ìX§„kôS—¦ÀéjÒ$˜ðö¼g,7†n7=0èUqÕFØÊ³^SßV6§3oêxÛÈ·N¨u'¤uìå¨~j\½V ¯Ž‰j7Ž{ qnÍÆß†ª*h‹À¥Mj1 ¬ã:mŠ/O§‰´ ¢È„¤ÁÔˆÅ9i¨R`§z,—É¥’´¢jmŒ)Lnp艱mÒƒ'6.¶2@1rO‚ŒªK1ù$kËÃsx“²o„¶Ñ¤N§è½7iì\x’Ve™áµl™—E×FŠ«HZò ¢úLTÄ”\alì-‡\´—Ä”w‹vÐW¿Ì”)+­Âx¤h-ž´R/<[wµþºý«“9}jêÅAmmþ¬2d¢Ê#U T×\ÔÔ30qú‘C†Ú(‰P\ùªr•ýxÄšÉÉ•>7!þÙö§Í÷^þ$½ ÊIt²5RyÕ΋Á{ñ·²Ê‹¬pP×<ýþ(³î'Ë tGî@vdÀýïò2Ï.Å%9uyQì`D“µË”¼ê|[Þ&²“~äЦ¼¼¥–A[´vͳ噵,!ïNyªqáýU-zg*µ½›jü¨ JMÛIEØžm8@Ä)ç! ÞÔ õ©++֖ˆÀêò89m=€øUÍxc.†"àÀs+ÂÄ[× ¸ÓÝ6´i´k»C¸*ç\ ÓžŒlY ¯ ]Ý;Ÿ’ 2z<ñÃ|¡Ãž½çÏ[ð{ç…üùÇ£é-NAÜ ò2]Û8‡”¤ÙþÕY°úkõYºü)7TNfxΞ­/;qíå`£q†TÇ VñFÄ+Æí×+Ï–÷þZêœmƒºh¸¨©iêëš¿ª÷5u­kîq²ÖÂÄÀT_W›¥LˆB̌:·uô™çØ òDЄO–bK&<;È îöâÖ¼ ÚŽ{‹¡æE¶(Ëõ^戃\ŸÔ8ÄÕÖÕ4m6¡CÆü(_&°Vá“a|Œ”º8a$§P¹—8é’gøAH&”˜†ÕªëtÈʧäK>ŠG Ys‚Ä%-ö_¼6ù`IynyR7kËsIN@¥ÜÒeX»Õñ1s×ïò¶RF¼³üÆD­ýÓ†42ªÌññ¾Ê¨øHœLÈC@â,¥…°Ãat«†vDµÁH¾ÐÀ iÁÅí4êÇT ä }qü;ÂLŒù$-Ï$=)0‘!Ãz ZMx‚ÂCëAZ²É“@íp:W&ÇæKRMôÁ/ͤNÞ0Sak8aÒ!¯ì¨‡•.QñÙ~u¬— ®PTSí·Ú~*T¶þz'2Ái€ÊR´ÖM”õ¬¾¯L!’ù®OU#3‚%9ØÁQgèø5Qµþ‰÷‰UõùVW’_ÎæÕôÎ웈gåBBý]gÐ|ñé&ÞX½|ÉúÒPö—90Ðà+¸’¼Þò¢KÚR&TqöJÞmœ8˜¨Ès‹Ó&IšGG0I_“¿è;°ÛDÖ’þjx·dµˆ^ôü¢‡B^Õ2m›¾¥Ó²<ÏþÞuëž+ëV󒸺çÅdÝÚ1:V¤bP•ÛÊŠ‚×ÝšÓö»Ná@“¦.Tt7q(Lq«íÛDQtî2kir2†šmPù3H…C<6æ6`mc˜§‡ÓX¤<°uxs0•Ò²õΆ,8 ä ¹¤IäÝð4X‰¥áx;¤­FHqš´:"³±OF¥µŽàÅà…‚ ÜþÕɈ# TY&jµ}åÈZÔÊxµ¿Í‚¢Œ(_q,R¿‘â@oÿê\¨Ö‹CÚ!õUgY{¢½ð¶.ƒ1Çxë³.Ù‘vF§‰Š^« 8¥³’ ²†€ ÒQïÜ›ÞDQÂê{€!¢º+ÂU;¤$ëi>Òiz凫18uÍ<02è@ÄÞ%+r¯xM§ ÆaN¨%il“„ÄNäm%6±03;ò¬ëñ•ooó"8P„$p¢•Fí[,8Ф l“XØCË(]:ío哸ª^uIÔ±À<%ÀŒH'í|òâ6F†Ýô)_Ò!"<#ý!J¦@wø`<é5âþ‹‚gÛD“>õ—|·½8™ ^jûH}Sq9ÁǺ¶•/€”3Ûe`«3Vçò2/å·Æ‘2r_u¾Ši‘õP‹ wcð16ÂLÔ÷üU°2] úÊy*z„'_ž¥»•=öª²?•wÅ,ßÌÈõff,#ˆÀå€nÿÔË)jß¶-ÁJ e·ØƲ™Ò[Ë”;n¥‰{ìË® %u- ð† ¶M¼Á•8ò¨…¨äÙà©Ì Dýjq›W}]“WšҴ×Q†Š¤Â›¶½å$y–&±»{¯ð?¶)0Ô3¼Ç«ƒù‘ ºCâ8XÖZCHƒûÏ+ͦ"•/«#‚Á0!åí*0z’ߨî6Ðû(w£ô Gzܤ®Nš 'MC¨¨wm¼üÖØ{Wß„iŒª_\‘ ¡Hx)\fdIÑ:S‰Ï Û*MNrC9LHÅn§Âì*¤Þ´”=J𼉕\©Ë—òC”¬‘þ—{ý¦é^n>ÿmðÚ ‰näSTF¬ºˆ'Èc›å±É,ï¡10Ûgÿ¤ƒ_b“¿œP(ýÀ[—j/5ºG*°Âl ]5¼³ ìLIvxQêqÇtjƒ«ð«üŠe63’¬¡–nÿU3¯Øªž0ýËHpV<Ð Œza á­Ké-µÜá¯xš¿œz}6A —@Ë -±¯à‹zJ¼”ùÛÔeâ+µJ —ñUÿJ¾ø\ã"÷Mþê|OŽ<í m3hÅEZ;[¼ Ø„(5¹ÔJ Bl 2b³Ëœ¼zëÃ+øÝÙùÿwÕ2X‘M]¤^"T«TQ&+Õ)ú*˜HU'ëù­WâKÆNmë¤wàù¯#NÆZaMžÈw5Ha‹°ô Œ"RY\ …©X•¾D}Ó&¿J(*07äÁ½˜ÞQ6#wÍS™‚¹¦‘DWùîUpYŒÐèQ_M¦¯¡¿Òùòh´®L'+äÏ«þJùk]ømCã§5N:9nް´)‡ñ×xxÇa© Å«¾ÔF ]œA™ý»–òÈè¨}É z¬82Žþ6½xi2XÓ“º·>Úlh¢íFò¯ É %wèv["‘ˆÆfÔ†”ó*"?&Žýa[3Œ“­]o¦M‹há–°È\l‘úÅI)Ä¿'&˜ÑýEþ´1„×ÎåilZìÎvÖò‚‘8²No’ýO),(ùZ~ŸCƒt€_ÛRšx÷å ?tŠ2ûɯ³$uÖ–¼àimì%‚08ØøŠpâD畼ŒÉ­¶?etƄڼøéÊx 5—¼èuHrp[”,_¨v$¶æñjýÖ?ï;l,WFžaYx(ÅÖHå’ûc+A^i¤ ר'þLë‰Øxãøõò[¨öàoZ'5‚xm‹ôÇ©6\<Ñt±tÞ%M–ÇS |iH¯ž=9Ö²gY¹z5o ì‘£¸4Ì1ÿ ™Á#¸ÖnXŸ—tïÎé¿FÚÿí+ÕA»b@w¡î4¦Lóí&3ÍL5NÖf»´Kqñx7!Ó`8z»óŠýÿ‹LªôoC*<[Ã)}e}W@>Ò 8r„þ«nY:Bùï½N `­F_Rµ I;bÚ:¥Â£ÎM°ÂiŒtÌZG·µoæ­â·ËM´yÚ“Øº§- Wk§: oít†¶8É)éLäWSŽùÕÞ5 öã4ÚÊk„šÜÛ@(ôÊ5h—é>367Âñ'ÏÉKÁ7½Aþ†~Â$ œ&Ó {ù ÕwÞ•²BÓfFšN;ù¤tf^qe9âƒyߪòàóҩ ‘Uù44§h˜økÙ|9Éþûî[–.[V_¸°ôëÓ7e•‡ZnÒÊ®ÖiJƒœWþËÕqñ‚BêËßX“¦.« ZáÙ\]í‚/}+¼ÝÕ²ÕO5bAö|é”FTä3ŸPÁoCH~Ú:} ¯(E¶Ød\‹T*Òܛި4ŠeS/Mª_ý³MIˆFc}V.¤Õ Xñ›ÄrÔn*òqÀ~û–åËW”¹óç…/ýpiä:’™†D ¾ßV^D+_Z™ø6´yx)ì¶@¿×àñ'8ùí §Eþ›þ‚ª¡Sù¦]Ÿ#bÜdZG"%¬–#Î|ºü„žªRŠˆõ‹0Â_®%GgŸ™¯{`¶ÊBŒäÛ°lÞ¼¥¾€¬ ì(b½É$Z2SŽâüseDŒ€µ³‚TlX @ fa4`ÐFx·Þ€E9¤«Ãè"nा³/×'.ZòdyÍñÇ– ãÇ—Ÿ]s /ÚØXÈ›Wñ&ÀgK¿¾}yýö³¼eðÙÒ‡s8r[O@RCº0:õL^Öa¹†Vö›±o¹ê'¼Å7!¾Ðë…²jõšðD<VŸ„_×¼ñЙùÞYW;’£CˆÌXŸü)‹T#Â¥™÷±Ž’Ô·‹ò¨zÆö4öG¹|™E‘òf#}ò¼w¯Þyk¤¯O3j¾Àø vßœh=¾á™ ¼uòÙtÜz÷êUyhÞÛò’t0] ¹Œ¶ëÉÇ[?pùªö¶^*Ô/ÿVî×B£úsv´¶nõú5ÉΑJ/Ëb~k‘_âÒ»w/f’zQ® qø÷£ƒøÓ«–·;*¾u²{·îÕ‘¥|ëŸy&xþ;äŲz,`ê’úkË^ tJ«åªwUk°Ö„ {T%ÁÔ\Új?ÑëÒÄÊâ&ƒz @G†Àš@ÛÆ+"F‹(ö„_—·T8ÀÈ<8I`–1šþzKBm‹ôk‚´›6ì/8ŠoáĘ#—ïÍ›ïÈ‚Ïf Ü‰‹2&ˆ“œÓ­ŒàbƒMb "QÂ%¢:'â!_x~w]~È8rÂ&? ûjø8 Œ¤B¾ÕýÜû¼ÚRÔç°>e'…儾 ‡Ï<8äœäŸ|’–:Ä êºá¹Ôc;!ɃX’™ßÑ×Ú!³ Z>‰«2 %¥ 4yÙ)ÔGlþd®óô)€€V„• À[Ö—u§¥Ú×%¿ë­o)Íž]®»éæ2lÇ¡ño‹_'È{ naE•Q,VBOðŠ€=ð„Ùðª ;‚÷q.¯àæwÝZòÛÖü‡ï¸cFÏÚü5¢: µa“mÕ°‰C#"A9órÔ­5âOco9R!)4ôÕQ¸®ž•g—Šh™òüryYQÔò¨/ÈèÅ‹1zôt{ü–Ò]Þúð/NXî¬#p ¿e’~ ’ÏäÞ4ÝCp½4šDuÁóVé©ìÞ£{Y¶l}¹í¶'I³¼y šöâ¡ 5<4>ò×K+˜aáR´¦Æ¥"ž l¨gqG¸·žLU¥ ÖQêØ¤,bïœKúuÐX¦ì¼s™7o^¹pæË´½ö,3y5¸2ëí*–>¬Œ¶s™ÿÄ‚²nýú¼¡O¹Ò¹7vL5rDyࡇë5e›Ç讲³~ý3e÷]wIý>òØfêÞiÖ3jTäqÙŠчÊíW˜ÊÁ6uƒH¶Óºï¨.nº2“Ô«G76tmÂÁbí𞟗µ É[9YùÔÓeìèQe·)»”Å4ôO._^Ókm‡üIÆ3ò<£èãÆŒ.»œÂ+Ü—•E‹Ÿ,ôÏË\,öEê´òÛgõ=Ú› ;í”NÆì¹óR?é@û"¹¬ÅK…˜FùÞeò¤Ðôð#aëº!;“Ã9È#‘¾ÞWÒ‹àÀeá¢ÅqX¤aDçΛžjûìŸWίF–ì¬[~íÁ‹¼€'ÊaQlèÚõÔ–¥u˜”ˆvc/²~Wy00þ¦ƒ˜gdAcDCä_xE6D[ìé«é‹¬.mÄ€À©s“Åž ofØ‹Ì";hµ,m¾5¹hÿÏP¡-‡6%Yâ,@íT€“:Œ8ƒPؤGèRF mCmðròñ)—ˆùô ^ºˆhÙaž¦6Ün‡‰àpÀ]/OjƒNtL‚ä ƒÑ]Š ‰‡ÿœÂFÚà…nígm_k¹tªm[¡,A”S]Uν”zÛéßT¦íõÆÏ•C:€¢l)]z)|(™‰Õï4vlÙ §ÿAlí¦MËΓ&¢gã)ÏuÑï§ž^™Û©{îÁщëËôÌŽ¹6­Õ{qØIp™gl„BÔÉ|Pß³4&«„UÉUVÕE3òéMªo•z6v·õ‡¬HäG0íjÀ‘ÅÈ.á¹hË1CU.›CdƒòÓÑ$Y…U´Rdö¶öüR/U7}Pëo½ÙœºÌÄœ„_Y¬ìñu8X¡‰Òç´9kóóOû€bVúy”†èž~Ž:Bë5å#RÍ<2€Ðž²pã¯y€ñ'vx0v@z*”7‰žå”--œù…Çê/é óÈ\mù¥!’HtŽBª{4ÐaâZ_¯MT¿ÍÄ„Öü&C|¯‰|v ýRoc $‡?ÝÕn2¢ö.ˆ´Ö(A…–†—sY(/”»þóò²•bãÔ6lÆ¢a¶±ÛȺ</•IZŸF ‡ œÊq4K:á”&§ëtº2mKø;qò𣠅]'kó¦ºìHGÉüW4ù¯\µgcS×Ñ2ózÍš¦±/4üBt¸–ÛQ/; C¡%‚À³ ±ŒéwGFÀ(ž/ðµËÒ¿Š|Öá´IkŸ>½Ë ðzï'…é˽H«asZóò™7’zŸ]øˆÓjö· »_ï}nEKðNŽú5÷mzÃŒka¼7®ŸËà!Ç—ýðÓeáÂÅåïxCŽð$¨³êâUE¬¦ƒ2£ˆí4Ÿ W1CN$$+Àw8U[`^£µ\ž’ŽA#‹I~†§ÀuÂE‡FG›_süqeœ¸~Œ:~Ðé”ÚÈüû9ß-'¿æÄ4 êÀ¨Q#3ú|ÑÅ—–…‹‡ü3O;%im°:àdgòÿt™KGáˆC.GqDF«•¥%Ì4œ÷ƒFœq6lGdshyøá‡ËÃ>VFŽžîN`Gš2ÔÝëÞcƒH)ÖmkIº1óôªgʬ›×”CŽ \7ZõÛ£qâ£QÓ¯sÅFa$íTÉËݦL!Ù–Ø”¥K—EïÎ=ÿ,«š^¦í³úÚ?ôW¿ù-–l½¦ì ¼2ê¾›n¾¥Ì¼ò'åÔל}ûé5×e$Q[qèÁ•«¯½–ÎÞ rÄa‡Æ9ÿ‡ ÂýºòÝóÎ/«UÔNéxµ—ƒÚ“ÆŒ)GqxêÇ¡C¨·!åλïæLæîeÒÄ evâþû(çýèBfv,¯;óŒ šhGm›yÅå*è9þ˜£3àñÀ¬GË3¦Göâ‰rÉåW”ýy>æÈ#·ÊË“ÊË~©¼Ìzì1ìå¯'/ÙÓýmÚÛŠ˜¼(ªoœTO«†[|nS‡µyCÛ©P?:¢ª¶³Žê¾J`mûÀA‡6ÔD¶MĘ6Þ€vI õ;vþ¤ÍÞ˜´gÆ'9_„VûÛàŽÛÔ’6„<ò‚DÓ6ô˜Æì ‰]Jz‘:„×Т[3¤,:2Y›÷`€'U+êš~ój:•É]mzmžeUig2Ê©Ãf[n²73þÅ^ËÄ­õa`Øb¸dZ ¢´jyMåe1j;íLœ÷¢†ðM›ž/½ûô(ÓfŒ,?8ÿ!b7—çÈ6¨COÛB·¿"îäKÚöj‹A_í…ý¼ã®»Ë)'Xn¿ã®2mêÞe<3µ+09˜ƒ<03qÓö™Ý?œ¥›Î8Ù§wŸ2ëÑGÊ÷ÑIN:á8ìÇ€ø&×ßpS™Ïì°¡Cã „éXþÔ9«LD–¨ÏZ_Öm4úEçjÿ•9EŽ0ë´¶Ó蜦úPÔÙææ^a"]d4™ kM+^qéÃÈ —r†'È7œTh€U•ð®(´£ûFi¤QÝŽ­ÆEÚä)Pþd@±Ê-¨{pJŽ®x;tBÄlôшæjÓÆùozï)Vh•’dZiâJ8_DÔr—#ðÈe–}‡ð]XÊg£yWõˆH ñõxÓøàud\XÁoC³YW$]EI¹Ï}åIê‰ZX­‰5Έuȳyxg:gÜ-{•¶–1/duˆà$NåK<É’€à—q™…—ØìqÛ09|ÀþåÍgŸ]6m¬œŠ Ö[åx‹ß0¯mŸÛûóbø6¬ýÝšÞ¼3ú‰£rÐA3XŽp(¿sR>;tö*ks.ƪ92L PrI„Ú鵈¤A„¿`ÏT-Wð•NÓ"¹uK(O„«¤ŽPÙd¦WO2õLXäÓ<:é²ílŒËIœb¶Ñ¹è’ËÊM·ßYþìï‹#hGq_dt1ξâÒO|ô–Ãqìßøþ—¯ÿýgËΓ'‘îÒr÷=÷•3N=¹¼þ¬×•™3/ÏHô Ç[fÍšUþýÜïe¤ÿÃü@°‹gêìÍ`dî™òísÎ-O¯\•ÊŽqG•¿‚|i •YúÒ‘%‹Ö•þC7§ ‘ÒFi5–óç®+§Ÿ¹{9æèËç?))z•éÓ†P¯Õöä÷eT«O®X^^‡ó¿73/S·Ýy§ÃÊÛÞòærNö7ÜVÞþæ7UÞÁkü7¿áõ8ÿ»”K.Yn¹ãNôê¬8 ·r¿3u¹Œ=Žv¥“?5~Ü8f3çnê^{—K.»,õÖÝ~ß¼·sÔ‘å‹ÿöõ² #Š/¸÷‰?ù¢Œ8˜`çoOÖ$_qÕOÊþõ+åg½¶œzòÉåún(ûÔ_–מ~j9òðÃKò;îè#±ƒ=Ë?ÿËé.,ŸùØŸ³„aßòÿö­8í.k˜Èzæ7 + .(?çžíßÒhœá†µô¶ÏÂèN{µy¾ŸqòÀ ]}Q9Z~µxÅ%ÎKA¹|nÃZzZ:·}Þö^Üí³iå¡—ygÇLâÃh×>U>ô§Ï”/ù;å†+cF÷#7¤0;!¶Éq"Hñj¹ìôØîΞ3/ú»œÀo|çœr±GãŒ,K—.ÅoPÆŽSÎÇ©wPðƒ*³™¸“ÈÑù¿ñÆ›Êy^\Ž>ìØñÀ¯[¿¡¸ÿå:_ÿÖ¿gÌÁ„›:Çæ¾”綇Ž\k?u测 ãÙÁ'ë×z‹òe[lóÜÅQ®vIOjU p¤³™XÛnÛkñ+u&ä\f·xÖïW>63`!§p„yç•%ŸÞo,šÀf°8¤+¾%"ÔÈÄrÛ¦i‚SÓK7¿–­Â¶–PñWÜôJ?aIÓ8õ=oü‘. 8ˆÛÌMa§(~œN?Èä‰þr:OüŠ.Ù^mgŸYL—!YGêMí9%_;k’ÔaÚ¤â7sùjGƒ´ÁŸ)(˜ec±2Hxî»8’/ÞÊs#eÉ©)ouv7{Ü0bn•@3—a„5iZºþË_Ñp­dÄÿ}¿ÿÞrëí·ÇI:ûu¯Åɹ‡i÷åe"äYgœ^þ¿/|9K'Î8íÔò0ŽÎ}Œ€éÀJ/Ü5ºùìß•¯~îïY ý\ù<¦§ïO:ñ„òÿôÏQvi´Óàeƒæè¾J°mþN§¿éì×3#p=ÿ§ÒïÕLñ-yr K0Æâð¿¦|á+_-3 p´À‚5t`tà\Æñƒ‹.Ž"½åggÚðQFÐ |ë›Þ˜u‚ŸþìßÄ);©]ZNeS¨£Â×bP† ­KBèËüR^´ß/¤µ‚dú¶Õ†5•‘4ÛÞд¿½MG:…ÛôUÁŒVyBt42 ˆ…TFì*ÞLÑ£3œ=}7º(¥õˆ=¬ –ÅŠfJ M²ì†…áÛvPáˆ@v½ÀÌųÏ>—eëé ÝM‡oGFvŒð`ùäßþS9é˜#Ê¬Çæ–{ï»?K¸”ܺŒq«®z áFÖè;Ð1ë‘Gp4Î…GÝ‘ç.eΜ9åœó¾Ï÷-Ü­åK_ûfqFáøce—òsh‰¼¬A^˜]¼Ð9†s¾{^¹yÙwê^/K^t¬ì¼=òÐêòÆ·îZ¾ôåe_‚³±/¶ ¿ª¦~1\kÒ¹Úû‹4ýŸò›òÖº¬í)»î:9³b3Ð¥Ë/¿º|æ3_U+SiÂØÆv¶ý}i}©;.ÍéÞ™®t¼›=§,^±’e£™5{¦Üxû]åHfáì8ƒ¶öו³y´¬c™ÓÑG‘Ù× žÍ œƒ}ÚT÷9Óç ÀW¾ö²}7fTåC§_TŒ¾mj~¹×ôÒ–i—¤’[ôØe úv| œ^ü:gc+€M" h&ÊŽÄIKXÛ^›Œ*h…Ç·ìö¼K« ª3+N»#¶ ~ÄšuCޏÔÔùc xhr݈!>ƒeNâô!LQI°sBaECÎxîÉX<ÆåŸçäïj! Nù[&6.zॠkšL2H®Ú9’ÆoÃqf¾Â6'ëãTü"àÝ ÌŽÈÓØ­ÒP&ñ>ÞV°ðœr&‰’¢úJúîñ›H’t t¯¼*D½§­wþÄRyo€tA G]_Öxm žböB5íšå]pQ™yý-åmgžRÞýη—]&N a] ì åºëo(ß½à’Œ|¹aò裎(F ‡yNÏ÷È”û ;Ê̲!Î¥>702Äᇕk˜-˜3ïñ4².ó±€Ûæß³GÏ8OçÿðGåëÿvN9ïüof:þ3Ÿýë2óº›ËÙ'_ÞþÖ7—ñ¬vdÌ©ø·ôÓeË‚'Êç>ÿŒÔT6â,ìµûî™­p4÷²Ÿü,†æ³ŸøhFå,÷!,ÿ¸…™„ßÿð§ ë Ê×þîeKncêÑ¥"ÝXïÛò7tP]6›ÑâÝwÝ•Qä§Ër–¡¹¹W™t©ÇÏ®¹–Y«Ù$<ëÑÇ«]Óp92µ’ÎÁœ¿Ét’ÕAG–í Û¹}†NÆ<Þ)i¨|葲/#¼Î?|© |%BžÐéfhgö¦OÛ£¼ãíoŒÎª×ÕHO­ÇçáY¿þ}3í~Ì1‡•©S÷,Ÿÿ篔yÞôm§ ÿŸ–±Ñ)µà©86üvÀ;`ߨ“Ñt ¤ÇÙ™ƒ¦ï êQî‡wû¶/K¦föDgy4gPtòÉQÏ„xü‰Y.ä¾;úÎâ0y§ÔíÏ®¹¥¼ïÔlî8 _DüZì˜N¼rÑvĤÏ"gû3Zø3›Žºlн‹À¹‘%c؃í±sè,‘3?#y&Î1ìk¸”a;ÉFóE‹¥C"ž5žŒ±¤×É?'¦C^˜ý¼ÿÁF^è\<8ë‘2~¿,y‰zV{:jl/–r®-?¿ç8N–skýn­-’Ä?x±~V9‰|*§y)²/¹¢ó «‘qFô1È› áÛ+*ðË °mÚ&AlT²iqðð pÛlMð‹`-ío‹RÈJ¸_rß–Ë4‰óFûÖ°&¼j${Žšsc¢\òÊ+õâmGþb¸ñUym\µËvêt¤'Mš=xðÁYÌB²W¥4½€~m°Ÿ\¿m{%¿)«ú¦ïàº~ucƒã™y€ÌêիʲUkD‘δ¶I½êÙ³WybÁÂr 2.;v£½iÓk¯—.[Z›3‡ÙÙS²2a%ò?yâ„ì ¯_ å×™mêEš²ÿ~D°iUf›:8§ðÄÅ&]»,¥ ,<ŒãʈuXEHL¶ËàŽhY¿„)ÂþØ èÖÓ@:~U"«Ãß8ªDg•ôQp þY2®¶•N‚«{V’{,Pè¬Ù5ùšˆ»áôÛái2Á§2ý ‰åR„)\² 5ooCqCHм'„;; ÐèFý˜ŸndPáÔ%Ó0#EžÆ¹T~êç›fÝðn¸¶R »’;¶Ovš¬W» >É1T Í—T|,eú3ͳ´þVXáÀÆR,^f‰’XBÌo*Ü©@pi T U¦'Y.3E;éˆCnŽ}Y×ìNô•,¹ÿÁ‡Ê!4än´±ñu*o/—ð´ëÿ[ÃãÒN×è©ôîøw ¯qÛ‘‰Œ&väß5½~OkyÃ[_&٠ϦÓpÂaÄ®IïÒ¢ï_xI9êÐË9ÿðÙ„M¿Sð/]¶¢¼ö´ÉYï{Û]?g”q:K€–f]¡£:e®ëuIÒ…ÿñ¯¡ßrïÈò ‘Ç¥ìƒØ,Ã)³½¬»_çÎ5ª}q_sòAeõÓ›x³›'–PõÖÊOzµâkðg³˜uk>„EÞ¹³­W8Œ°â“F‘ªøªV â¶ì‘‘Ý?ýÓ¢sô4ãÒ ã&eÈ;»ÒµÇ¹Ù(¸Y{G`ä< “4rMƒu§’ TE^ZìÛú©8U&I|Z®Nºd]F; .,‹–?]N`]µ£O2ªt衇°æ©ìqmªNM7GpœP¾…K–æÄ ;î{°|äÿ92ÎYuû—¯³NýF²‡ï84Nãžtîd©Ð=5žF!ì qeú+Ì‘Z î‰ñ¤—É|ó›ßkô‘ú£À©+j­G3¬€GGsH9묑q,®¼òÚrËm÷•cú²–¶Ú‹±_£Úªõ4øÎ®9Í=kö¼¬‹¿^¾ç­ufî!Fú\ꣴ-]¾‡¹wFÅÝ«ó䊧ËN,í¹ëÁ‡™U<çaPìb¶å :º:ÒîóNþœÙËÝv™œ‘@àÜŒ¬ê¬;ã¨#a§Î‘}ij7PNuHì@8ÿ$ðí,N–Í]{ç@‰—>¾éõ¯‹óå>7 : ðÞw½½<„ónGgŽÌ§?ûÿ–£ø8ãôSÁ=€zdòœs¿ËŒÐðcÈ6òr9ðä…N¥¼RÇ^®¼tiÊ4xPï²`áSå}ô%KÆÇ‡ö’s*¯¥v`Âûv ¡ÊˆuP­éêÒƒ ߦÕBµ8——ñÛ¦5¼ ‹Áๅ7ÜxÃ[‹ÇmžÛ4m¸ô9ÈÑÂ ×æ¹Õqx1q´å2­¸˜Ñɯxü´e3Îg/ÓøÜæmX›×sÜ‹£-WÞþ¶yŠ×<]ªc\›¿<3ù.¬÷Âo˜¸…iá_Z6áM'ÜJ>£ŸD° |ä#Ÿãy'lÙ°²Ñ*ÇÆ8‚¬ÎÕüÔ‡Îý¶mÝÀ@€Kr]5à à 6ÑƱŸ3{N™ŸWEù!ºr uÓŠQëÔö”à4¾È6Y§S¿ µÏiÏô%…ÖImÖÏð¬tXÐv,^¶Éüð§ÍSä<¨;v¸•Ú* ´ë&õò7Ž1©ÂU WäÒ%Â>óTí2Í 󮹉"¡¿õ%ê^!‹ßKÜú ¢€´¤Ö/!JÉ¿>j@¨I 2©íUƒÈgCó,Á†'pk™¤«=‰HÏ$éS•¿){Óa°S|éHdó¸S|y[žÈÛL}¦øjæ-\|+ÓP¡C`K•’áwFÞ5O邞0Á|¡9FH„äfRó)³²”!ÿ« ìÏå÷'š 0 û"‚„.*ÜЗ¹6K>òVi†VºÊJþÊ"JüÔÑ ¶›Íî­XØÈ‡çaHxŽAñŸm0¼U"ŠÔI€›9O†ÏØÙ$`x¶ðƒâ#¼ÀÙ ƒ'jzA‹\¤  ”{Û,uušqÑ7 }Ë·ü0×/}è—C,h\Ïíd]íE†r0:œQöqn`.ôJªw¼ûOÿ|yÔÃþVã¼WZü€û‘WpåíýËÑž%¼=o ¼š×ù[>Ì­Ár«Ý#¹EèùW—¯øòv{ï[¹‚ö§ävOâ_ÅÕýmâ6W¿Î %ó0*¥œ¹z×øÂ63\ç[ôL«¿…}}¿7Û<ÐØ&L&.·$ˆZõ¶ŸÑfŒÕç»m(6ÔÉo¯]ü ú°‚mýTâAöaï1¶V:Е?å‰c ëÈí<°Úª£‰ØŸ™…qo•ÆÚÏ9hå\$©Ð®-š{ÈGª) ^}·>’€¶ƒ«ì’P0*ª“4†Ÿ z4·;?íTÿ$w·%É«þ蕉=ò™O½Ò$-%Ð㣕Qþæt´xóKý×VèHß…%¢I´^ö¬´à†iÑÀÅLÎû“”‰#êëô²Í˜7Æ,5ùÆ¡ÂÉÌ"ñø±mUIÞ³uÒH¤À€Å% «ÓNdÿ®‹ì RP—‰Ê:Žë"-åVÅUýÄçgnI©®6NŠ]¹õÞm_Ûy§Kn[òx×»ßÍm—ðªw Š·•\ÃÉ¥'Â]ÊŠþ+_ýšåÞ¬è?é<‘Ü?ÄIÃ5<¼öîž…ù+¥W¼êU½Çú'ÿí“YY?Ú-&?û´§/w€îG¸böÎw½k¹Í­...ÇçŸ+½>K:ú‰øöÄ÷o=ä.Ý*£ã¶xÏÅ´8x‡å-o½ryúÏ_¾<ôa_H_¾”[ž>ªƒ·}s˜ìÄÈMÉãJÜ©åÖÜßÿÜüzoóù_¿íâJãZ¡ói¯$Þñ–· ŽNsKÖ»šûÖŸµ¸+·Ø|×w<±+)^A|ù+^±¼øw^έ-<”ÌÛ‚žò¯(›;qó[Þ²üñ{ÿœ Áñà;ùA¡;/Oþÿ¬«žú,†ù÷ÁÛ«ž-ö°_yËÀ#XhðÙ'_3ꢃW+ÿìÏ®ìe·àØ÷üû¶´+Þù®ånw¹ëòP®½òeÏm!Á[›Lìßû?|s“•«¸Ù“Nù¾öµ¯K¯Xüò?ì¡_±ü‡ýáÝxáÙ“â…ÛʼZ[™K?³xÁ'èÑàb'¤CcÂQ%Õf>š>:ù…:ïÜõkžñR8'wöþÖoÖÁ óÍ \ÍýŽ xÕ;q°ÝüEþ Ÿ¸¸`ŒˆÚ¡i ¬ÆØIÄÉ%>ÄDZqè+Ž…j‚o¯ŽžfäjšRüž畤†´µÓ6$âÔÄ_óÍ ¾Ô 9›9 Eý°¡ƒÜrrá‹n@ «L¾/\ä&eáKqyQBoì€ÉÈg¦)¥ Õ˜M ÷7 ’ƒ£¾£y*ÔF˜ú¢Ãîä5¯S…Œ'á<°øðG\â­œÀòÊ[ÚÊÿŽ€;ŽR>#6õõ*¹ûÓôÁ÷ò’û®·ßùÜŸ·Þí®w­o<žÅŸÝñ÷W¾äž÷[¾÷û¶åÙÏÿÕ”üã×ÛÂÓßË-À?óôg.ƒ+oޮ牀‹'éCÆt»3B{üÇ_ý´0ÎŒ'âgÏ}òÝ„¡Ç Åh‘ÂW}­˜²•‚†Áj¼/NqD}HÔ²on1ç‡m›a,~zëqÔɰJƒÇиÆ,1†meÐÄ]2êП:L_ׯ{æFNøi†¾zɵØv^5iÊ“6µ§ ]'è²@mÑ X’xvר@ØvO„§Ÿ)Û0|U¨-Øa5zL}ùš¼Dkòœ˜Åv`Ê×È¢>Òµ9~êC)Zkvšcí 0tü“¾þ{u0\˜— –Ù[–ã¶EUd>Vïß GÝö‹ˆ®„õã48Ä3;_wçq­úçårë=v¥üËüàåÉLnœ„ßýîw[^Ì›-N4ßÍep_Á÷#ß÷¤&ûaî+ðŒN¤qo”ó®¶9QÕ`þàWl7Q{ÿ½ü}JÝÄ eyæ‡OÄñ^^ügÿü_r_îù9©ù ÞðÛ¿ûòž9øîïú˯ñ¦–¯ã¶¿Ü>Hôº7¼iyÌW_¹üÃïüÎîÓõÄä¿}ü×vkÈûÿâªîÑ5qfû=fýkv¼Q»îú"¿#yÏ:¡lÁÑjb”´­P/€uÆVÑ„#jBÅÖ¡ìQž¸`BŠ„ͼæk(oß3¨¯€Jí;ƒ¤t9F Å™ Þ%*팓håœ× ]á½ìèi°Ãg4’Éø”Ž_7Ïæ {ßþô´§ÿB'¼>ö¢—üN—Ÿ/`àyÖ/=¯8õþó÷<Ü›¯œ¸ ç³?úcÿž{²/Csæ=Õ\%p‚xï{~1“Ú_ãaÓwpEáŽMø_Çmg^E:J¬ýü3ŸU?õþsOBtâxöæ±…>^©8¹þ8‰ñ€Nc2A›€cÇOqµìÈòUþâ&ä×\Ã;Æ ü: “*Ÿb3r}ÚôËŠÿSæi\q¹4Ÿýš¯îžÿûÝãnËË^ñªž!r,/îûñÿðSLôï×muNޝàäë^_òÅËyþÇÛ¶¾øîwåjÆ—?|ËÛx ù(÷ì_ÒÛ€ÞÄsþ€Ð£ñ°nò­A¦Lß æJ¾¿¨W ½]è o|SsŸÇDß·y¢÷6^z`^òw>ð.Ï๢cøòä•?~÷»{™'‹ïä&¼ÿý˜¸Ÿ\~í7_Ž9Ò‰ˆz<ðÚån<þË¿úBNß¹ÆËGyõát¢qô|ãåÙåó‹8YýŒã%ßš·Ì“/òŽ}·u |ÒÁuÝä"qÕé¦NzÍaêë-]£¶½n ÎÁß|ád‘ŠzÚȩ™ã{mg4©aÂ~ 6&¦ØÎÑö· I»lsû‚Mæó óÚò9gò*˜ò¯ÕoÇi)Çè‘~‚´):B¯ö')‘Žü$>NÔÇö8Lx:! nÚÄÔ¶[.Ì&H2²zI×ZD˜ ºÉ®>Úzñ\eGNi:FG«oém~g› ½ÏØ Ùy‚©‘¦Ž;'âž ›¶àãÉôsžÿ+¼)ïÄrú³÷÷?íÏì-~W²òÿ‹<Ëçx}ñ­.Z~ë·_Ús9Wq{ï¥<ôÔŸûϽ¸á¶,мïýï_.㛹ÚzI¿Êþtr®yÅW—Û³™ ÏÍø5NwoúƒWàrb}üqd?ñ8Ç"¼þO|ª'> (ý`´;qŽe¯ÈÏ »qeÇbJ•U¤¶mŸÂSé‹?zØxˆcêÒ!Zk¬künÏH¨a ;ûO›òšŸ¯Hö`ÝêÛW¥ÞIÈ.™ G•0³9xöJ³Ýd©’ÝiÆ6Ñ•Á!.ûÂ@8û¸¼×^ôoÈ€Æ^X™þ$W!xmy®ã§U«PßS΃æÄ¬éü (|¥9ÆoÂÊÚ4•ÓÐë°ú v¤Tã…6Žn«;2kN$Ò€ý¶YçvS{šD±ÙûÕý n'ðMþ™\;¹9ÌDÜ'ò_þòWô£Hw¿ëÝhŸÿ‚_é;2Ïd0|Åk^»üíG?œI÷ï5>úQ_Ù ˜o¾ð¾ÜÛðþsÞóÁ¾Ë.»w÷Ïzùÿb.Õ9Pøö—óöá?“erÖ§þ? ?òjr ì{¸÷#Èá?NÞ}'û ùA'¼Wþ€{‹ŸúÓ?×+û¾îñkçëC}ýà]ît‡åi?ÿLîáåu‚¼ZÌç |ë‡u·æ¢‚›Iã@˜hw»){î©wð؇~ŒFOöá«ÀšÀ“°ƒLƒ&æ¼c(4¹Úœ„“ DC"ÚD¢ÁäXàIEB»,ê òðÓˆãžã#çOÙÀŒÞŠ#ÕÍk?‚PÃ_o«›Xןme¥ûø42Å–æâSy“ßf“æÿÛoµöµ—®ú"®•ÉÁAß{b9¿Ð:õ ê­aMlð‰“yïõ·Ž¸ù{ç3™õ¤Ô•N'†ï`EørnõðDÖ˸÷Ä×ÍÉe“*nN#È&yæ¾ZÕ|9ás²6«9&áöºkùQ,–õM4¨ž[ (`ØO¥’IS HÍ·†y¢þ{ôá[sÿûý¸ç=òžÞôEw¾S·jCizÿ»W½ù»ÜN剺«„·ç>ÛÎç¤Î«{ÈÉ—'þsÒàï”Ü:þƒ¯#þ3žÛx዇“¾S䛋ÒÑ7Ü’ûñ]aïjè'~þFÀ_q…÷÷ø2„NrhëÄÚmbáø©ãÕoí¯|íë99:^~òýãð‡oéÄf~'Eb3âKŒ7 Ðø1^¼5H:Æ‹1b jOO}Å<¼”O½;ÁÅ_­ý[/}ysìËEßg!ÏÛ‚]ÜñÇCõ³'ÍÚÁö3ëÞÿ±·zÛ?O7Û³ŸâKã¯ÀÆã5ÖNjî݉#pëÓ.íE6íæå°Ï·ýpN’¡¿XBÌØ1—{¼AKÉ£ògáÐlÌXøÆpìjì“ÞÞ㢑m`ql|o<=¶AIhP'ƒñ-)×B»ýËM0dódh&Áê@ÊV½L.Ûm²ò¨I½«qų£Ot”MyÖ2`2©¨L›Œ U“ëšC1ÛÐ[J¤@˜ü'?2J:8žÖž]ˆGàó—hÀõzõyò76”°UÊ–ýr~É$~<¼çrˆƒÄà•±b}¦›ƒçó~å×[©’þ嬜]B‡R?Ôç°Âéí?þ.€«¹=ÿE¿½\È@z+ܹ#¯îr²ç ~æ?ÿBµ9¸ú:D¾ÛÜúV ÜO{Æ/6à9sÒïÀéæ ¿Ãí/Ó›Y¡›íªž¼}¸Ry\|æs~¹‡çþò£[~ð_ÿh¨—ÖM ¯á~_íò¥¬Ì>î1_½¼è·^ÂCËïèy‚Á;º])|÷æúc?Oaeñ¶ÜÏ«±¯b²àÛH\qð£¶ÏÒ¦3Ù%õ!«Ë8 Ø fûRÃ@oOíç|ueLÛôëЊ®•fVâˆä|Ñy L~± ì&€‡çxÙO1ìË…’܈#i!èÈ4(CXw»,DÆ.ƒ[öPDèÙyM,8¡Á§“!ŽÕêæÜùŒ=¥1^ÝœÌ:a7†mØÛæ âG<'žn•<ÐÊËë/¼ú››ðÛ=ÿFŸyj¿9¿Ô­ÁF= ÐqÚˆ„ÿ])2"ÔÉÖÇSžcËê³}¶“‰!pãß%O’¡t}½å£ùÞ•ÿlwª÷x?ÿ×£7íx<«-ØP»BÎ>í$Ñ|à¦Ìæ–Ur>ñÖœüpî}ÀùŠwþQ ¾6ÔÅ„½·-ýú‹^œýUt7:þ@àE·¸°~™«dQû6ˆC‡ªæã éA2v…÷Cc2I,@ÔsO[“ˆê§,ù77MN£@m0y“€Yx“•õÒW2éùE]ƒýjcå/ÞQh+"'øÄ´@&‰%TÞNŠ6~¡$YÉ_z£ëØ–”7þ‚·Ï“žÈéLÚÉÇô å´ïïôúŒòû«ínÞ=àI‚/èPGûŠcÜm»·M|ûU'ë o¶ØÚÏ”ý¦·þ×/†Í¬ëOŽq¬v°l|ló=åWçOÌ¥°ü©¾·ØªØ"0ô³¶3H&—Jk*A'îÖþÆ¡#4Û&þÛ$¡p×r­NU#_?Ã^RŽ^_Ф$;Q·ö YmÏÓ ¼}\yíðq.$U6ma°µÍ‚­ ´ÛŸ½]ºÄ¢}Ù:u–¦©MYZ„ÍHrtÛ‘®£1ÚØ]}ºšñ»}HÐ#ŸIw°øù´•%H61âz@ kI]ô;à wš·ú ʱòu2ä%Š¶ï® ªÌWúš§4€Ï1TèÌŒ0b¬˜ŸñŽóîžÛ&F·¿äè¼}BJçåò>Òküü±-ïÃ}åyâ`çœ÷i çÐIyªŸ•.W¶.rB„ðÛû¶]55¨ ”U×½üeéI†+|6ŸÏ%tÏø{†Ès'¬t’~ðèÁ.#š|»‹I¢dB'ñvLÇ_ö¹ƒ‹¾à"Q{Ï»—וÁW‰Îd⽯]ýÕGóÆ\~ŸÅ¦Of#ÈàãDódÉ™À¢ÉO~†‘˜¼3áOQá¯-c6+hvg…8Sׄ4‹và6$él•U þ'íT6‰vvèò1Ðݸk‘–´=²®È¥Â˜sd§>Þr³sñ°,LÂÆjrJøfÞ”i›X"*¾Þ±P²îo·íTXm³UhšmÛKײ[ö*ÙšäfÛÚÖÛm§t¬~)ÎÖ¤i­÷9šÓR²Ú¾ÚÔ‰„”mÔF¨‘¼ÔÄtóšÏÄ;j©¨ì7ÖGl6;OÖcþì{¯ŒòÜŠ±ÁUy&}!˜¾Ê=&F‹pæÆ%²ïŠñ4ÙÖ" Ü9±„[ îú—ý7yËÃ?i¹Å¬ê•_ABõÎ*`<8húÈ9§¦æÊ´ ´%2ÑG›´ëÙ2³l³>Uˆ‘q{óÏĬ± ¬cáúà=ÜïGÙ•cå“„UgÚv6ó t¶[Ùíç>¿#<àÁ{uOQd§U”ÕÌ’jÊ-NÖ7éOø8!k·PIDÌ&÷öç¹[C{é­ä§¹IßÍçsŠÎd“veR?ö-R(>Öe­¿Èpµg×Wµ"š¯UT|ùá!<3è, žœ?Ë­ËÏàÊ«‰‘7dð*ÀLÄÿøÝ²üØ?/oòaI··t;“×@;4¥¥.uöIJÇLl;°”½J´Ñ<Ém{Åó²_$M”ºuÚ%Ž[]âdàÇò§—sOî¸ßXú—s¯2{©~ã7m š«ÒÓîŸõ–^I7tŒJþ•i6ôi ¥“c/k礃•y{…V>ƒg:á€HOÄN¬‰ž:¡ÑáW(?b)EÌC Hã¹¼#Ú½bÙîÅçx¨´ík§°ß„†XQƒþ >ÀY^©‹ã‰6tÝ) ÿÉL›¤í”gʆ8£Ûîî3-û°öÖ©ó¶í)nU7ß^at¾;>%Ë&©ø¾*ÇùßUÐÓöEqô7ý­•ÅÏÀ¯Nð½•ï^÷¸E÷9ù8·çŠ" ­Of«­­ýv <Ê¿nÅeßßoC9rkd/ìiNìÅ€T!žÝö·î?Œ~¦ÿ¨F\@IDAT0{á?'ñAÌÉ­?fi?Ì¿/ÿèw|;™aËÀQo·Æ{c\Œ°Óââ´Ä. TFðe.{}ŒŠ¥¬æ€*«¹(”ú }€¿V6gôž´„¥¹«e;Aes^ù”dà½Ëüçm5¼«ô3Ÿ ÍÀc+}Ç qå¥!ÊbÚÅ¢ ÂHµÉ8ë•)jµ‰ $õt5÷*”i“Š6c­UÜ”kò‚!:ñ A´°âHatËV² c©+ ´ûo]•ð Ê: [_Þ½Ÿ)ÑmN8¤vfnª£^Û¶§¸·ºæ½mVlÇÛþÆhlugÄ^Aù8i-ÎŒ!þ&f,ÓlÍ5©°Î=ÿõ«Ö<.€\co‚Æø~|‹-+üpõáuä×´€›`¬²Ž Ò\ãNÉì+𳡷žX¿¬qÙYoÐã+)ó‹ó¹ÇT!öÙ+;(¡Ä±‡Mbßz¤!^uÃCCUe‡Ü‘]<ê³âK`t|`!ºåe“ß|¦~äI° ýŒ£c‡™ì‡‘ØÉ¿Ì72Ifìê‚dt{úB£T˜,øÆ/ úOàÐhpK>s‚jÇ1é Ï9‘ň\;‹ð7 B s•‚Jƒ Ô)£RŒf” ~¸¾Ê@,ÂÙíæ¶À¬Xê1ý:ñQÜ3Æ!læsïÆÎ„¸­þ;PÍ Ð§ë\p¹Z·¾<‚ȰCÞ€úté$Í'ù2¥}SôV}öQš¸ý¯zs%f\[û¢.¦.[—1\YN“«lvZ}s·{|pYlH=À¼Ñˆ\´°[¦Û,ý0A!'Ptò\2‘kØ Êa@ófÛÓd,Ÿk7åÛ‚| îŒß¼<ˆ¼·¹˜·‰,¾QdÆÉ¿[6«ô¹ÿ*ˆðVA„·”£äLYgòO$ RÌÈŽÀš>¹v›hÛ‚»ÀµŠ:ãÀ±3wùÈjè+-yZld¶2gQ;Q ÇÎjƒr­ñ䈢Œâ ‘“Ÿvå^«ªóXü†Þ•_g· žuÐpG$8ûuó[`wrÌ„ÄD¥L¤fR&†¦¼ B‚l‘´M*\€˜¬úéê´7vËÆÊñéÒùTp»´oòSµß8ÖµôÇ&‚ôãù§šsôçºw ·?÷7“ /³ ZÈ[{Ÿ¼¹ b†‡8­Bâ´îO-L|®á ièQ4CXŸ âÅÄ+‡BÈ¢Lq5Ï ³\JÈu¥©Ê1°J#ÕŸæ°$9ûÖI·9¸@å%Øh–ô¦­‚>²t%Eû1ÔÕ ­rVFЄ—$ö–žý§·+í¢èâ&BFzåÁ‰Q-—q’4'J ÝÎ à|Ö …->Š-êÝ»Yoß=ã™9a.¦§ÇõÅðÆ_ šÄ¯±Q¤ ÙüW6Îí[ÝögÃÎFDs(÷â’Øk%ZÅÕ³U: 4¾âÙÇÙ’‡ïM&÷öyó»Ú;Yã¸îL¯ûe Á™Rî®°FmWÓÈ>¾¼Ã®SÏÏñÏÍy¸HSîgŒÿy`¶C² VG´;ü "KïNÙÉ] óØá¿p1Žˆ¬õCS‰ßŽg·¦søgoô8wCӳǵ\ÁviæÏËðNêŒ!ï÷¶c7É#Pg¥`³r†“XóEç8Dâ±Ì MJš¸¶Á«,ß"4SÁ V€³Ûac® ƒ_fâ†?%¶<µÆ µ´[á­ z›X¬‘wFèõÿw!¦¿âúcŒ¦><@¿uÚ™€Û}moËQë-€çÌCðó™]¯6‘0O„3¹GšB˜ÌCŽq±2gQ«[y¸§–b¹#|sžcIÕÑ+åYŠžòr¥™¿èQç`è­ò²bH](áøØ†LJW~ó*'L]©ŸÕMÕ¶MX)ˇro¢á¸òE×B%˜™¨o:¡ €æ¡3Ƭ<üÍíÖ;)°zîù¥ž«b«Ý9”‡°à W•y™¸76“„q Œ²¼(FdÞ¾$ ìY1@ºöÓ‘Wà³Û™`ýnÌu‚‡O O,êÎZósý 'nÂÐq`Üó±_{r_ N°Ï8>1b­³¸Cï^ã*†LATAsž©hj!~ìßM¨ƒžV?ÏXK.Š´;7qs…Ü­îe¬ c [gLRô¨+ŒuúÑM•"á|ȹR´é_ 9´WÈl±wî±ðìW†²°ÈáD•—‹€9Ĩ+}blùMþb— ÐÙ:LûÈ7x£¿R #eÆÄ:ÖÚ'š$+ÜpÝü¡¯Nº`í DYg˜vPBÀº{OFèÌç{¿Iþùæó¹¢¿WÞ½åÏý]:úgóE¾âÀc?=-¸`gÐ÷¶%Õ8—cƒ@/E“Ì mË^ìµu+¬Ì¤0ƒ„C«‚r‰Âݺ{`OômóKxymŠ$­ÆY¿ö'Ç.yœ¦ÇUE9$-v©`y*Ÿc©4}@säâX~g·3Ãkìì¸Dç®ûñ(1«?‹'4f#Aø"©ç÷frFù¬_Ï —&…îÉ©ܦcÓçá>}h/us¢9‰Õ‡÷À¡Nwq`\½öi1Œ ÚüŸÀ È/MêlpÜ3?LL8$*'áÉ"…õƒÉ‹üË;>]žƒ˜*“@³+LìC¥v&"N3Ì©MM"Jªìp\s™íL®·@…žôÕ¡ÓÅSêU‘“Yz Pª”}hx[ÏÉ&Nà ¥ðkÐGÐrwøÒ•u\1o¢€¾#ÏȰ²©N[y Áð”•2Æ’«ÚJû¾ú7ùÉ0ãÑø‚¯uW '˜®þÇSµÎng†ô‹¾5^òb™b'}\<:LÛ7¤ÿä}qŠiëø'&f|µÿoÆ(´üP1+íÓ¿=>å ¢4h7váàÛL Œ¥y^FX¥=$‰§!`üY4kDÐ>'Œô ɓÓ -å¨Û×)_z ;õ ÜÉŠÉeí»öÿô• Òc·Ú~ˆ2O¿N:ÀõÍI>4}D<L²ÎšøRŸ>Q5á “ˆ"ð¥íæ_9ìgÔZæïÔ\N¹6*ê0¶Wþt¢Þ?eöx+G¥'i¶"•BúÍ +_Øx&#£Ng·3Ãö™ ýä'ïzRÛæn;ã·B·Û™DZÕÇÓú|6 ‰ëoÍæ~¤>¯D1^ }š"2:DZzæÀ€”¬tèþÒ¦ôM n¡2ÑšœÆšÃ®^§±¥Vaˆ¸®СŽ_wœÙ:±>wºk•6Pg·›ÙÆä¬ào&jãa’]«¬ÄƒÉvêýÿ žÌ–Ñ‘÷¸­ÇàzÛ‚žcÎHÚ79ì‡r¦ïZ”w²vw¾¤<.[¨7GÎÓäŒö±NBQŸú2ºÊkºÜh aT¡âõm'ç ÞÌ›¤/¯ÄˆnwgxâB}Ãô —â<~?£Wc8Èy¢Òìò’ßÙíf´nœÀ)ÄØûCÖç_¾f@4Œ }Û€l`ÐÕãÌ™€Ñ9 Ħ2`ìâ°&üèB_Jò‰¢ÌWZ–¤wä¤?™dRh!ÔAê´ƒ‚åÙ$>ÇMþ¨\s@ d#ˆÀŽìÆc}žýÙKÐcÉ3éÛ•†ãÇNò œþ¼éj’ÓD«—pÉ/惫X3úŒö&2&g'g’GG–`NóúcªCùªŽJ³«\]=ôù­|Kr l“o{n“|;2®wP6§”ƒˆ#!ÿÇʉ:*'óÁn“`ŠñÖÙÂä” v?f£Ä¸f⑦ôZô&­+é³ò^ ò¬kX6ÅMÚ,B“#Ù¨wÝd­=: Î-~ĺäa߀­¼E4_Ó§0Þm›<|MáLŒj¢lÞU*2i9zà¹Di_IPð!ÀÇÁÜ?ÕÆÅ4ONõ²rô´J]™Ž8‘¾-ì‹BM¥tÜ„•@ò »8½Ú{9°³_g€ôí‰þž'jzÐØ`‡ W•¿Û asüjßœèÐÛÄÁYÿ²SÔ6“p[‹ãšÏt”húàt"9­t# UÁ­…ž“ÿ9ÑŸ1À<1TÖüp2Ê;> ÐiÄVœ¸Ø$ƒ Œƒðì_³¥žè .Š®t tP¾ÈýlÛÖGìs¢1{Å'Æ©o%}9”Ateô´^Ú] rz–º®¬„ pNÂbÍ•¼]Ywc °Ñ•oio¡ÎJ6ÄiÇPÛ+G;ÑOYÑ£ç€î5 V]ÌÄð—L1°<»1Ðy'ùyvWâ8Rpéh½dìZ˜ÁÔ¨2`‹®ú§á'@!A`sñd™;"ÁIÕÀ¯t€› òxê(T>ÍÙ°¢Ð6ŠÑ†fgÄOø†Y`zЈ*-Èzû®ÌF†iÓ·ÛL¼{•‰Ø4ù4ÑgïU@it%Ê86å©’ O£ÅZÜ~Qì«—ÁÀ‰—ÀÛO„«Ö®Ò°Ö’ˆ+xù@þ¶ Áí9–S„šðA ‹~›\ðÐ~"ÉZ§œÃHA¡ØÇqEª‰|lëµC—öNRh"=WÑ’E?µ%ü”)Å´5eñ3€æ«iËØšêƒT{.#Á¹”O­Â…‹ù:{œ­v,òù¶Œê xõ÷IŸ"ò¸@“ãtÎí²sN_ƒ×YGr_« 9Ë콌mÙUµ¢£ 6w9Šê§á¥BÐ6 ™ Š0Ÿ­‡o}+¶ Ä‘]uPp\ °¥:WCÓf\’D«¤NW:¼\{õõ >@ ¶kǤy@?Å^8·•úÜÈñÖ°ÁíßßTûV¿SøŸ n?Þþã «ß¼Õoû›jß_¿ïßï¥cÙM¿'®<]5'§Ï#޼¸¹¶Z0‰âWãÍéF®uG6òA¾á8?ÿqíÕ'–ó/ô6 ‰AQ¥¿í¥èö©Žj÷{?üÖ²·þÆÊ{ëÄÙ{lÙí“É·ÁßÔ~(ÜîVç~ÃÛênêø¦ê÷ãÝÜV¿íwð¬@Šk®>ÎÉÙ¹ü²÷¢ÚWíÙhÎ÷º’7,×—«':ºž=çè“R|Íðç` qP¶ÞªKÚ²§…*±ç„‚$b=”p‡iÞ’!¼-!i0Wfs¢Í4Ý<ýU#y#HùH%"¼ÖAÔ… OƒdhŠ•ˆ“ ”%p™ÙÜûAέœÀY‚TÄÇcè–^)ÍÊáØ‰eðáÁ6øòZÑ©ˆVÂ@¹”£«j  '2’ÈètÌ‹˜+Ž{Ç–æ/Õ䎟àÙë®å×Ì`Ïelt5îÞµ>0†ˆcøDü-Æ2Žžñ~|ÕX Uýæü"<_ÕΟìfŒ_8¨àï$!,ÚÚ|99ðЈ¡ßÌètbi´¥ÊŸ‡8ôؼáM2=Ì Tp}Û%•^ëñ,ЯÅãÓ Ht¡:Lÿ1ÌE7'Ä— ûFÁmŽÃ±Ü•‰*»å”­CîÞ ½®ž¬Bë&ÍÙ7ÒÓ™VŠ«&À)‘|·9™:—¨‘ùGI€²"{Ä2Ó`›…؆ôÔÑA F¶Qoþ|öRûºœÁ0Éížcמ䵜ƒ¸ÝÖaó ¶³Ç70ÇçÕ>ÚŸžf%êØu'™0bl¡cç#_¹ãØ€H'\ Ù Èu¸ƒ¡À‡Ð¢ÍP˜€˜À·#‹¹ÑßcköÔDuÖ¶BcåH0’€x²³ÔÁ¨s ã{¨5ÒÑd@FÿˆÒºª!ÿm¢ïÊÆ)î3v¢x”ßd:Ðê× .ëµxSû rk¿©ãýõü¶¿©ö­~ÿ~ÃÛöûÛ·ã­ýÓÝïÇÛ¼ŸÎMµï¯ßŽ÷ï7zÖï”ñÏ ®L]Ëä9ç¹ÃÉn ™‚Aa0àõV:= þJp4¹ éÄîС#Ä÷qb›_üîA2#e¶m¿îÔßÔñV¿í÷ãßXý^˜­¼í?]øýpþMí÷ÃoÇÛ~ÃûTÇÿ¥pþ¶ßáGÅõŒÇ®ÿt뉾ã£?û_'¾MÎýuEšf¢p‚:‘t+Çl>§Þñ¾<$Cóƒ1²Nd×*kÆß$¹=Æ,a@-mÿ ãæyHƒ¨t‰)oµqâìÄF"{ñ­rS¾™wLá\r–!;9 €N^4€ ?aäM‰odR?™øm{³-h#Tò‚7,q€ èt‚^úÌŒ-Ç_';èÅq‹‹Ê¢ÛN?Ã&\«_5²œl@êI"x§ÀHAùCÛI°AnnARŸžå^ºçñkÛÇ®wœ8¶œwáÜjÕOº}ªöýÈûá?ÝãýpÝ­~ÛßTýÖ¾í7¸m¿¿~ÿñwSû ~Ûop7u¼Õª½þ³Ÿ^ûq®Ò±Hf?Ósž8†žfѰ(äÀ¿m’®Çƒ¿[€òDÕx;‡“ä™6BHŒCc†¯µÊÝ!4dBYÚ¾ÒÒÒî¬Çõ¬±4yz¶ ,­NÃÇ«ŽÊè”é«’–·qÒª¬è晽ý ¼N\„W€bÐj.dm:@Ÿ6iÊRŠYð`ä¤?ö‚„Vû‡x³©7Ç®¨'éJ¯öƒÁ4|Ð_×f]/úe|0ÇBs²Ïd\½…Ï­%"ÏÉ ˜T–óVPSÇÚ£Ù«ˆráEŤ¤ÿÊS2ã^ÀÁÔ_£é&WãŽó0ÞµN,çïÃ+Wvg·›ÏvïÁ=Æ*é!Þ”b‡+`ñ›É¼ ç6Á²®mœn§ĸןVãÁ†ë$}tnÕYƒCšâGBnhS\š»"Q4š8¢IpÂ,øÕl‰4Ùhh‚8¼×J8¾¤GÍZXÛê<#¿á³*W”‰âyœ½RÆ]ù$³É:%k,Åahï9¶Í–ÑÆ#·] ½<bíÅêBo”¶š þ†û½¸[ËþºÂFq/œå©ß/ë&ÙÖ¾ 9øÕê½Ë{?½½ðÓ¶AO'ùõ[ãr9ÍäÿȈjì$N…Ô?{ySZóºCrÏ!Nlyøº]·œ8+<‡™À™(7ÔöçTvpCù­ÚazàÛ¨nXãK7{÷âþ¶ýõCgƒR’ÝòFËšÙ¶¶ÙïÊwÃøÚ…µ´q´¼áß°¼ÑÙ43.÷–¥10âýõí·œ:ÁUÇcÜúqð0÷ÍÚ‚ÉŽSÍ ¬ 0Ö3h[¹m:XÙ÷½5ÆÔà|´A´ °zÚ[¯Õ0P7ƒ6rBà$u`.WÌÇ2 À™˜Gœ„8ÉæÄWZ•¶H{¦èh Rá$¡•EÚ< éä:æM‡ÑM._GèÈØÝà•½ü¬õ‰EÍH¡é Š Zº*/ÿšÝ8Š OuLv•>«‚wzgb¨˜ÒàÏöíÁCV”î¬öK‘cè ?/‚ˆã ³Ê€‚4EI94€j -åX¥£ h;D_½þú>·£>/ $Ü|ôI·•O0ûËi·Ò@˽¾GÙ{jj"·ÂN9U*n\6û[öJ<0…mCŽ»ø[»ûËÞºÝòH9ú|29ôél{¥Žû[NÑ1NÇÇÏ]³ˆrà «ôLßÜ:kŸd2o ÏØÇÈqÎÐ‹Ž Ås¥L)dµØOóª%7àÔ[P`[ð8ámÆK6÷Š.=6tDHw@T§ËçÐÐ;ö!ÉÍäd’Êr+”‡ïèŽøcX°á@ÂP¯¼D1ÿÙÎ÷ó%2­t•«Mx É|·~íñQ6Èf”´7òô€•ygé²I¤õ%‚øÁœqÀ+½Ç8SOöó*M€ð:ÅS˜¾Úlä×2“¤D“´«¶^ 8ɳ>@Š\yÿÂ`Mæ&à,!òpä8äN°-ÊÛ4‚„ô3 6fyÚfª65T³ u ùßèX›ì«Üÿ+õÕŠâÌê–\‡§2ÍÄ.!vÚå­ømêD{“/öýÂe²N ›±}Á­_YÖƒ‡EŒlÇëûFa]—b¡Ñ>ÌX½Úx˜É^cƒˆ­è ñ€¨÷Éî¬Ø‘â¹\Iö!6Y®W⓳Ø6¬b Pƒ_Œ×¿Ž3Öz55@ê)»žæV‰"Ã3®Z!F,äAÙ¾eqt°mµ][üŒ$ûVÊPQ§^bc• 39 <à@_†X :Ñ_éä×ÀW8ùJ•+PÖ¾èßôgsFˆJCY™À¤NY½B±3A“âš«mßd|a'w’ Q¥½~ÄOö³ÆakDþhäÃàÜvšƼþ¯úèóUNQÅß¾µ)ÎuWð;õRù£ì-¼c ¼ò Šé¸¶8‡íÊd¤ ¬6Wµ³Å^X¡/Œï€'z͈6›¥oÑv>ó<Ó2ÜÑM*žh/¥ÑCÝ(eXå±Mèu d«^[±)(h!Žƒ“0@H‡ ø!1ÇBið̾<ÄB˜Ï^ÍdjýÞEÑ:tå'9èùc|–[€cîÖ9õó°¾´€‚ùÈUK¼|ÓÐ>û⑊­6âLEiŸ¹€uâóíX!Ãel£=W]l×ç|FUåZ}ºö…¦W¢9m#Æjkê':o íø‚kê0vLü“1cÜ$ìØX¿3ŽW= V¿+”öt¨•„_ðÈzÊßåjÔ"=wàULOþ¡Ó¬~@ú‰²™[”ׇ€K^g0]™ð™\]œA˜[NxàR9vO‰?wXOQv¶‘t§]”ÝÖÞ¥ «¶åÆåá9nŒÖжb—RƒgG[½ ¥ëF!V1âhÂ4@¦ ÉÚ‹PWº‘‰Rgý­&„Z†*(Cä|Æ®;œg“×@pâå ë)î»<È œ«ßLð+¸VÆHÐ&(­g[›)­:´•ÆæSß·“9 s‹†¶ÒÞ&‚¡5£³%Éj­rc/¯ øRÿ”Ï•4ë}øÅ7ÀÈ×Ô3gÅr°•Êö´e#p£#¼‚¥²Uë}ˆ<·BÑ¥aâX“åæyù„Kî²YVn&:l;Ž虹¢h<ËN<´NŽÕI‹à’ØúBzNEõê±s™ÝIFvµM±©s¯q¤ýlŠTT&üƒ8IsæÃ<„w‚Ë'YÍT )©²Í*å–Å…Àš‹ ã¬ôMDÔ¬ÌÓ©$:ôLŠÊ^þk˜Z“l+$Èfœ˜Ä¬vS¿sûã×3§êúã\UüøñåV·:º=z$ÙÅ‘¾†³æá÷ éøÐ˜«>Úƒ„O(¯ð[)KæÀ˜Ð€W¿ÇGÓfþÔ£ 9UŸ„¾ÍóÀÖo…CÐ,‚=Ô×~é…\›,³¦ZýqÀŽ‘.ÖÇ¢E¤¤YœZ˜-ߨ® ‡pøÓ^Іžq«œc¿¤£~¥.3E Æ\‘7’{do2©SàKQ²¯2¯ˆõsýê±€%ûÇ@Ú4‰µå61ä”oC1ã1À6¢?’§îø‘²œš$ ˆ‡N ÛÅ~«]:Áˆxêg!»®º™ªâÙÌ0.ŸZ€ÉHŸAA™ý•â¾f¾ÄØì4}X¹R@{X&;‰LªŠ;vËÜ‚Aí Ð$¿|3†¡m¶N¬@ÞVHmßÔ)‡² tÞYm‹¥fBDïüÏ|ñZ!È"~ììSÒ¤J9¤˜ŒûO$­>P†UO(eª2´2‘t!æ” üóž¯yÅ÷ÄÅ# ÒTiRŠ)Cs;׸ö6[4áçxÚÐÃBC˜6pûÂfÁ ¯0~檢đh­/½iÎë§b>4&_L.Ür†D•³þ©½ˆ³ kß‘ÏÈ?²5&`V°mT.2ÐѲò/hÀÅöÔû%Qìë£WÕ5wÕ Ð®©’€@ÊÜ)Ý9äC_ºPÌÔ¦gáß’·ÔʲlpŠ1q•xÿr“CænA㡱á=‘n^ûi×ý›±%–>ŸÌ©H«ßöðuž-¹ÆF…~Ä éFý,@  åhOiâvxI_bŽ ’Ê'ŸmêÉ1[»Æ«©—©•|¸(ÍNXBPûoDE_Am_h³èVä¬þü‹.Š£<Ù–¢2×uñhVwLО¸ºS¨ñï*ôˆ£Ï†m¶’)¼{ûQ>óP]h`×/zPž[–”ŸzlÄ9¨z \FjÕôÔI.#*P‰¢ ç ‹ò¡ãËî»å ¾àèrŒÂ=PÇ|b@ ÕbuW7¶Ô’æúWçÇÈÙBÿ8HH\Q«᩟ròQtl`Ú(J?{< ¾Š$oj"‡8à]wõ±åÎ_xëåŽwºt¹æš«—÷¿ïƒô'ÙBOáV9Ãb±’DXއÙTT–ÊJ¤j~­n+ŽÌ¬ùF[Žl‚(¿ßÑ…ŸýÃ>Ì6Qc¾6ºÙ©Z¿¨ÓnZp&¸ ?Ì2°Ð¥áÝm¤þÌ>Ò—Æ Œ½•K{Û¦ÌòadlHØámá «yÒpNÒÕµÂØ@øÎÎI~‘Q}'¬_k4z@y¶‘Ëœ;q&w) a•nQ-BtÄ›h*ä–é@ÚÌñ",ö󭳿•#ÆqŽÚÜÛ·cšÈ7Œy倉3¥ƒl¶Ü½Úã>à”Kîà˜ \ÈÕÇ|Ú0R_3­zf1( ζå.岊*ZŽðÛ/öõ<ÆÂ¤'s;–@0PL ›Ä/ÊÔz‚k¦L,aT2xq€…®¼WËjIYULDãÑi…C1} é Ïm2=1!Ìš›‡I´²Eô5“Mm.³°Ó“‚Á¶ úêe ‚5?I$¹û,ôÓz p…¯52“®üùÌx)%€øŸIáPuóAôŒedÑ7]A‹¾TEà›“iÇ.¹¶YdÓ¾ï>¾²W]‘ÒE_q̧16Tå¤<Îá·\ØJ*¤7b£—ôªSfm%S?è‘ÌÉQAhš¬H»¡e s©n« ÕEØ!FJ÷|Ž £ºh‚?öQ>t¦TÒ"~¹Àü³¢KÊPü[R6¸¹FÊÝPu4€äªsëdF eE ¬¿¬Ëô*Ge«"’TH3NP4@4ÞEY8È[G «¾¶ObÐ, Ñ6?²›]2P®¹†※Åç7q—@Fn{ß²&îÁ*õæ£h£;¡\áõUð'DÑ„¬T‹3)nbÁu£[pê=‘ -Œª&[Ô÷ UhâX8ýO2rÓ ¾Ö ’Ž©ýO‘ ¢«àòÈ&Øy·¤ª,c'&– 9¡²#¹$ Åtk±ÑŠ:8-d"t_0‡!ŒÒŒ¯´ymê§ÌÚ;ÕŠúÓÄX"rlà%O«ÀG=­*OÇHhM»l +˜<ÜTÌÍcmÃq×:ÎÔæê»É¦dÀµr£N€i'¥ g|ýòþÑ¿<ìa^Žðl€ÉæÉ?üËÏýÜ3–Ç?þñ à0â€OȪviwPì3‘#?bG¦øT9(e¸dJðVÅn+töÅßbS<Ú5Îv+’ƒ›òë$qµ½¸îl€¯ÊXBLùBfîQõ‰ºEévUÔ+FÆ][¶iŽÍ «?<÷^r'Z‡û°;S4hôjT ´¦áòb<òÍP‰µ´•[OµbVtR׊ãNÝÇ^}ͱð‰–ƒ9ü”¶ê€9%Ð~g“Ú´ ÇToÁ}7_ëÊ“úD+:2ÆSê$-jƒÄ*Êc¿(N—|ìo°â’°MX¢¥l#÷¸rOÌÈCz#NÜܧ7µ•%À'î¤è·{³neHÙÔd]ýÔ Cm½¥ä®ýj¡Öxd5PðºPWH¿5ÍLÍ,.Q¥m’™<Щ û$hÐç#L»öP®­Mºàé×M‡émJäAˆ.ÊV®°ÚVýdö®* úQg.˜v>âÇ; ß××´‘c€oú_qÅ3^}í±åÅ/yï wj¹ç=o¹|Ñ]LßàŠüc l~ÈÞÒ bm+^2>Ú^¹00þÍES¸'†5ùFŸ-^bB½êY'Œ<šdEpý§}¥SŸã(;"l|VvÄ„þ44¼5Gšþå_øL´MádÜâ¨Lˆ!!„‰Š|Ýäe3_U nêk/ÿ$Ø„[À͆Á„zU–®t¶/êWÿöš]'pÀÏI$R Z“hjÛDf3ÖìëŽ%³>c" ªGfëÝz³Åñ£&y9™ÿÐøæŠ" ^ 0ÞïTL'¨lQÅGÀûQÝÔ–<Ä#Ž—Š!Íú ¨3Ð3ðGùG)8äÑ`lÊÛĚìG{ïÛ׈Ú.yG×l-mjg\If&ê€sœ]V‰Ç”Fޱ™4iR=á࡬Æ%µæ[”–½ò•‡œÇ<ߎ{El¢àd7&nqñkD•p1.A˜*Œ[ƒOÊÆŽÚøž3Lé«‚>«;ê)M1‡fè `lCý¼ùEŒŒޓޫ?~ÝrŸ{ß%¤+ÞñÞå‚óÏ_Žs£U älõ OòÔH‡¬3y^M¥ó³i2ý}Y*õÅ!¼•²û_•ÕPÈNÎ$5ÍïÙzT5~úOqõ´ÕA1DíiÍ„&—·ŠãJiË™$€Ì%…|“dª‡\T’m«ØKŒmt<ßÉ.–Z„E£6ªc$³íÆË ë‰ÃÂꂜãeT.k¥%wÊì :c¥À24µW*å(ÕìÏ—¶3r4-¾ È€tEÜ8Ñ+Uê³Mæ›@Û´Ò–àøD&C+Íh×¶’™b—wIîÀxKÀ$MóƒR©Õª³åNžµ4ê\€„¯ß­•”¶ ÉkCõàÈø¡GLx)Œ^Ú\ú +_—ßýÝ÷-ÿäû¿tyøÃ²|ó7=~yÛÛ®X~ùy¿½üÉ{þb¹àÜ3Ï•€bZþòW¾f»º#­l©OPä œëÉ4èʤOVôã_i›0RÚVFh–e]uÃo&^}¨½D¶/.çœ\Þþö?Zûد\.¾ø–ËûÞÿåÞ÷¹ÇòoþÍ?_~ã7_½¼þµXnq‹Ë—ÞûÖªüFÓܲ32K¾“ xÏê,õøCšù[¹Àe¹HƒÃJVʪØŽ¿ñ+¾&‹¹s`pÊí‰é@LAšÆ$DgW% õ*ÍïÒ¯Ý~Lål©Gj+2J©}kpËnì'’¤gŒØîäGiS^Y8¥£´„ݸËÉ·RœZ.¹ô–Ëþà¯.ÿøûþÏ媫>´¼éoåAµƒËƒüeË'¸­ð_ü«o^žôÝ_Ë€ÓËÛßñ¡åÍo½*Ü£G-ç󜀄Ðç7eM W±©RÞ$Ìò*›q¨@&p© KÙ¤×É:åyüÜv01çÀƒ¤ž_«Nò–œ7ߨ¡“¤'}ÛóµÉžN±?H@„›À !åùä,^¥ƒvÜîôçïûØò¿|ûã—gýâ÷.ùšÿf9zþ‘YÁŽ“*ýW|Êbûƒ‡yQ9\‘®iFûÈȈ«Îa‚ª® Y°X‹~Ò]¿ò7*¡€ ªÖ„xukÁ‚¶úÇ3º:¤y×p‹å¦ÖIU¹Òœ¾¬ü‹1 ,kŠ~ÜÄîÇs„ò@(m1€£‡²Ù·\¿€Ðì³·”Œj*-µÃ«¿£þÁK39Æ(b‰À÷È‘K©µB|ÿ‘Š$dϦPô¥ÉiCé$„\kÜ@È?+µ§WcµImÊfsyƒØ›6Û•‰väë7Ž~”óqìÆôÉ.op( ׳NÖË[ÖQ¹%)Íu¬ <ÛðöþqåslŽg4ðØJo\y %/*éÕ´ãÉ/UTMU¸êyôüÃË{ÿô£ËWüùò•üŠå›Xüx$û“¬ü¿á ï[.à è@e,ÒÜYŽP^dRŸZÙ•+Å Í“™™»à§ä’’q,œ:ŒϼD›),.–·yƒ8;cÐ:Èl1.BpêžßÖèVL•¦=ÿ֦܀e³Õ¾€4F ÇŸ¼“Üäà ։We¯pžÐ+Sôõ§F”<ÙæX”å|“OÄ‚Íâ*\„e"u±ú(‹*ŸÓ唦¶’ò 5ôÔKÒMP]éXVV7s†l¶Æ-ê•NÄèÕ‰làÚÂŽ®Î 7¿+Äå –¦ì"¸p ¦´åÀΈz̳^‡>–©Sw…å;1*bçhr ×-¸>Ÿ©«öÔþÔÏÜ[-Õ_oC?*íä[öÐζ×*–AÝ8¶/¸(9q%¬­@ç Œ¤Àæ„»=ŒíáV˃²"9šdà±A9s q…(*&ÛfÄàIÒ¦‰M¼ÇËbÓÈÚdÊÒ—É@]ÛTÌà?zôðòö+®Zþᓽ<øÁ÷ïIOzäò’¿i¹Ç—Ün¹†È’û­]5…ã¹²Ö×P•ÍSšV'>²¸D¨ –§É‚:3í¸´9PÚà ãI…"š"Åõ–ÙF´ÑÊÐÑIjvÑÙÑšøTs†¦Î4Im<”Qüħl½_®0%²Qge°í^’2™¨ ¶.$w@ØT®Á(7ºI˜Q %$àÁ· ÁKœ6k ‰$¡ƒ°~¤ =E„š­ Îõë„ç@ÛxÂÒ@Ž‚>°79²Øò‚‡¶Vºü™Ü½Àžßd"™Vgðƒ¥M’‘êÊÀÐßAA4`Ô%ܳœ½ìñ´‰ÉdK’ÁB×¾±Ã?Y¡`ÉÚvâÑÐõ¥jL‹jÇŸý ½ï÷e·]^õê·-z%ƒÞùG—ŸýÙg/¿ò‚×.yìj—R´â£ºEmqQßI3y¯›zhNX·¢¼UCZ­˜Õâ±úx°ÙB ü6Îæ8 ‘Í_ ýÚ¯½ÇòÊ—¿cùaný±í™Ï|õrßË.YþŠW}Þõ®wæ$á6\ݸ|ùÎïxƒú)úëë—7¿ó*~lèôrÙ}nË«ˆ9@ÈcÇ8Z­"?7mk)¿3µBýRØt¢‚}ËKÀ{‹†0Nܺ¥ øt/pzÂ8Uöi¦-à±ÚM¾>änÌ{qÓ•j0$}<Á5:Ç>ÉdôpíH =ðð~õw¼ýãì.Oø;ß°|Ã7>®§Ÿÿùç-Ï}Îï-·½Ýs¥ÚÓ‡ ‹^ÒÊ‘×DãÄýÇ¿¹¿žz@•¿â28þÕÇ[þ@K¢Cq!<0®,ªÒœ|Pð˜ upœpE[-ç ^vI­ÛW4­P>p‡ï*¿„ÙZ]Öv´«‘ÆŽÒ~Ôò¯ ÕÒ·”«o‹·‘ñšR!s’òÊÑò+ºU} $þ6€o jœÄå[islî±”)Xîx*5Çèâ`"l±U’˜øL²¡'-àŠR [%jžŒ›DÝJ¬–Õ˜¶xŽÞÞ.ršsfEpñòºÌ¥ÿÍšò•:Ñæßä@G)·<4þ™ …ŠÍ/l7Wm)›$&5õWž"? “¦I9UÀÉ¥PY€:e”DÊq E¯ žâÖÆ÷¾çÃË7|×>ð2òßë»úùÖ·^±|ûÿ=yÄ[€|¦lå“rÊd<£]2«%Uðq"¨ý{ýx›ˆv+G•tO8å€Àøh4²ß¨™tReœnr‚«>Bf“‰ë`ÀMke°´òL…¤Â8•¶&›ü¢¯´»:¹Nf €V3Óf¼­xÚR ›ŽÚbÞà¦>Òܬ¢Ž‚Ê/ØÌ¦8å y æÍk:Z3ßÑ[ÀàSý+l'zºMJnËox$ÇÅ>°â—ºñ¡ÒÔ&Œ9Á¿”µ.>5SW#ÀâÄr• ‹lqi¬]8éÑ*maÌýÆôô÷±¹Ì…Kw:€¿‹¸kÙùÁøËfìàø2VáXëÊ%+É„ƒU6 ¤0#*k·e``ü7gÅ/rT)àâ)3SÒhÈk3šU©T9.h#e öÓ‡HðŒŒ(§]í 5Œ2,3G ÆÓÄ>$ë!ŽÐŽÚ zЛ¿¬&,ÊsÆ,Ždý¢º¿<÷„'|Ýò‹Ïýƒå®´y)³ €Œ@3?I‹ÍücÀénA mE4ÚD±óДÁt¨0¢pþA¥±º›ú•|x]½È>&¡œ8 #u(K\Œ’’rUèÆÔI‚=ÂŽ–ßA((„i[4‡eÀºA»Rиá;ðl… Zè¦{ ¡¤$·t8aw‘»*¼òÒÍ´Qy*8FÅQ–]ÒmÅ;ëPpM­7L€7‰r:Þ6qX%“GƲ´Šo>ê<Ý!{¨£àµOÛ¦’Pc²Ñydi„‚G:`¥*z*iߌŠ|•†e»)ØgÂãÁÄbùÌ _ v1!É È_Ø=¹½àÐríµ×-Ï~Ö¯,o¸ümË•W~dù¹Ÿý'Ë«¸*ðû¯ûãåÒ;\´;áIîf‹-ˆKõ܉1 „,z9»MK,`L(ÔòœÞÄgºI/9Äs³VÙØ7zL4§G œ“€»}ñm–W¾â-¬šZ¾ì¾—2™?±\tñÑåþåS—Kn{áòá_³Üç>÷\n}당ÊñÖåþ6®`»W€óº×¾ØËÿÀã@IDAT]¾èV\ÍãnøóÄ"Çå\…]Ò¶Êë dŠØÉM ö“Ä¥ Hñ•—M‹X¯´—´ìnµZA©we¯ô®Ÿ¸§nú&–á`¬f±qÖYy u‘Áãnü÷ð‡]²ü§ÿôÂåÑ_õ°åîw¿ Ï3]»¼à¯\>ƃԷÇǧ€Ñö麖gBP[Ú“™=ñUZàSÂMJK `lž¬”#‰ íf,lvñ þcL[/] c{Œ65†fßÀØÒ¶½ô†_¢³àSŸ Ë6 oñ:V\M/Ÿ¡˜øw7ÇÉ_ à%OÛb [ uߘÅä6m_àiê ¹Ð!\0Qï&”´ùƒJÒ‰¼ò¯Jöòµí;xKþ4ö9Æ&MÒu›Ö-?¨M¶IÓN]tÄKddÓ~Ø’‰#Í+ m­%BR@ÊÊ6µƒßâ™A³“ŠôÔ—½ŽG¶ÍGšné[aô“‰ÞŽƒ`lê8¦"JŸ-AWy”U]ÊqìédaÓìí:‡8Y?—à¼ÿcËyG9i„æîŸåÞ÷ºçòÿÏ3¸%î¢å•¯|=ï)\xþéh¹ ÛB2k²Ÿ¶ø#ÝWQôç¡ýØ ¯¾øÖp{Ññø»Ø§˜Å†ðȮϻB«ñ/®ï÷>ø«“nwé-¸B« °¢±°_ÏiHLë Ãj;ÕõÉV͇_n©ŸAaˆc/™ëóY|œS6#B^´Ñ+h''ºÓ#êå¨ Êdη`‰ÕþÞE­|åZ&—äfó‰+l€\)Œ=\ 2<×oáDêÒ=$Ïá'+©Ä«ÒÈöSû’0X(¸¢:B³¼Iy¶‰a[Ç¿æ,M¯Ò Zê'=óT T)kcuR–‚ûƒÈÑUaE†yýÂ`P6 K4úê…¤u -ªl›<1ó_o™Ês²&}ŒÁKð”EÔ¡«uÑ­@1-Vãɯ¬rÅã·_ú˹]Êßûøþïÿ·Ëßý{_³¼ûßËÉÐLZ.ìj&êÚ@å ³ånõ„týÙ…jR3åURäl@ãìc£GÀ·(cÚŠ›Âd'ãhÒ°Ätýµ.Û0¯/ÅÆÐÓ Ó šñ†sLhÒ.öMý£ÌÊã.ÓQ’F–†d§ÑÛüo’-4üUû@ýbSÄxYÐp9žb4|ßé7š“7lÊè¨þr°ÇfÙ„vea íù˜C}A›~@ùÊö$¯Åpó¥j®BˆµJeË Pž5Ÿdë¡?…”p P´³t–}ÄckWË&âÖB£8õŦ^“9Öð“Zr‹·é|Ód›–HžõHQÍ¥.Нi¦üá^½¼î Y¾ã;¾|yâ¿uyÍk._þÝ¿{.Wø½÷ûÔò¯~ð§8 [–/{àí—ã,$èïnV+´«)§ré#ÌOUk˜aàÖ¡ÛÞî¢n÷xֳ߯3U—.GÏ;ÌXQäîê ¢¥RS/>RÆëŠ+>´<ñÛK;ÂUØ.w¼ý-—“L^fÄVÇ›±¡ Äd½±•õZ•:ÇÒÕ?]åÎFÄ8R)DzwÓ~êæ‚˜`| d?š°Jï€í+(٧ŧ·b èñŒ~³nó…]üŠ´­¾ž±IÆ´§Þª<ñ]uòS3}K{Vlõmž…RÚZ>ã)ãÌ’ýHé¾,%#jìeQ/.ÁóäÍ=æ®ñ6L«Ñ˜~Ä… õ¡ÛiX³šÑîs¦Ê>9j´0/¨@<õ e¬/Ȇ–Ùñ íÊÔnó0¨)‚@ÇEVK•É:m®ò6[ló¸:$G:Cê²Ò‡O>ˆôã\ÛïÅ(Sãð@;SsY<½r ·¥puÌN…ì¾d h'8d¢^¬õ¶‡I‚²Þº‰Ñª9öjCÆÖX)°ÇX8F^3ÉP ˶[F’€²i†­%ö븧—øÿcyÀ~S(Ïòõßðí ¤& i‚ž‘=hWîµuÎ܆‹zÊ|³Hµâ$Ÿô Qí”O7“ÉÀM°0è Ó¿ô ,v;ƒÎX³rÙ݇X¶aÚØ¦‚¨È–?ÇFDnlä@}©…Rz<KÚ& ŽítÀ69–6ªmö@•ÚúÁ³ÕM=i À¥( 24_vì±ém²ps*)i©Ø.ÿ*Ü£‹JÇ‚¢¾/é]'ˆ ´=a©¬ÞÈŽbÓM’†:ä€V‰¶r‰k ±ëŒÒ€_b:ÓD1/U¡ÑÄ«ÞàW@eò¿€:ézþ†Zሧ¶Ÿ`Õü—ˆ„²û8l£û Yæ•èîpKxÐ5 ?¾üÓúß-yÌC—'ÿàÓøÑ¾ÃË­ns!+ɾ‹[JúN1äà&÷ÔC0›ik-KCÓ%äU&§-' ÔnôŒÌújnCÈ€ës@k؃ϡ'e'œ¤k `šhIçžÏýþúä‚ ,W¾ïÃËùZ~ò?þÒr»ÛÝ’Wž~lyЃ¾ /Yþè]W.÷½ï]¡{îòö·ýÉòÁ¿ü84® ð,I+é1÷“ñ²'riò4®*;R›ßÐh.í32k?mæ) H…q¨^ €Ô ³6¥SÄiÏù¶­ nù±ˆ‹žvÜi§˜Ìþªò©å¸”Û»~gùõ_}ÙòêW¾gùžïùûË—ÜóîËSžòRö-w¾ó­V:গýOÛ'u:ÊM(£Q¤võoëø+&WM•Ñ«%n 6«bc²5 5œê‰‰ðªç‘–Ür”=1õ©Ûb¦/…c¼ ü•›ÊSÓaÄÊÞ{öè(Ÿtýä¼'ÏèÔóÍ&ƒ~Rعº5ÙÃÐlp§£Ë»Í Jn÷Á²qTþ”•ê­!„¡­‡b)KeÀ%²éb‰¿ ŽÝô ‚ =R@ ÛM ‚=$GgtɘÂتþsÀŸ £½”qAnˆíÒ„E9]úúH^©Ði$nî%Nú5WC^êV€þ´j‹2šêÅXoßÑþÙ^Ÿ–vªþ¢É;Jјãê©UéèGßxåüû£½vyÈÃî³<âÑüÊðõ×/ï|ç»—Ë/kz^tÑÑÆÓã<|€þîíACÞ>*Ÿ‰h×uÖt²k3ŒðLÁ‡>rÝryä[¸Sà[¿õ/—ðlÒsžû’ú‘·öà´•. €c?‚dó Û/½ä‹ Ÿ„ÿs—´WþÈWÛˆN“®ªÏ§lÓ÷E‘úlö©‘ß[4´¢:{7¾­ÄZ°«…Và ;}f-‹W±¾3Ÿ"6ˆ¯²·uÂëX÷)Á(Yp)§zLýæ'éWž$ÙEFú‰ïDŽ HÎ8‡l´ý6èae0NU@°ä ÔmDÚœŠl ;()ËĶPa.«È"§Ìùüi·í¶?aýSN1\õ•fý\dÿy­s¹ºÑ–®|øÛ¨§€Ã_4Ç‚±‰$à£iH5àb¥šü¹õÖIå†nw~H$évy­œ‚U†ˆ²þ¿ì½ô§Uyï»iL/Ì }˜aATF”&F@š¯Äs"±XˆÑ$æx¢FMÌ1š¨è²$’˜HD4Š *bEz“ÎÀÐë0Ãô ÷óù>ûýÏ,ÏÍ:÷ÞuÖ2g-Þ_ywyÊ÷yö³÷»ß.ié×NcÞ~XìÙ‘¥´rIÏÚŸÚî]í+, œÊ Q”ŽôHáŠÒ’xÒCä^«ÁeY&²a 1á¤!+èT i@¹ÎàR7ØÒŸé2¯Ë• >¯Ó`†:…€—Áȉ^NçcÐjÞ{ôÑol_>ç¢P›^³†Sæ,i˜ÞxØÅ£z~e_þƒ5a)àîüÀDïȆZÀæƒYÄ/<½“C§Tƪñ›þaR<´ #yfÍ”+šŸkÁÉŸ=MK{…`“VÖ:(¬U%Àú.XÑ‘`àOýÀ‡O,è0j Œu-P#\zJµ¤{½ jð© N;AÿCSGþ b)ËEšHLoê…`ª@®ÍÄJžLü‰ÓêÌW$à‡=Á0~ÛBKŒ»a§HúϪ;Œ"d91]$QEi‰èᤠ'úo þ:•î=U®/;°¬²—LJ©ê´‹–¤}ÌÀ«Þx™ôÈd™}¶!ï¶`ðáÉ/{Ùží¡oë8}ÀKd’{ÿOärtÕ™œ²#}OoTG¢êÇ…Bxö ÛVøa¤ÉÄÏ–&¶Íx†>š#òÒ<($Y.ð$ÆM© úQ?ã0ß0}j=ç{劵í±Gïk“§ŒmŸûì¹mGÎ ,^¼¤ýÁ)'²3°Cû›¿ù\[°ÇΑ¹è®ÛC=[=3°57rk={-ê×Ãê-[…¨]õW}UÚPQ.]!Ô'·¬×ãëêºZÇÊ%ªF aú<áKœ*Ô3nƒbq 'þL*übP‹G;ßûh{†Ë öÞg»ö±ýs›:ebû«¿:¾?®}ï¢ËÛžd,ä1ˆ²9†Ò€W ÀÔ’ Ææ£¯;™¤¡¿:êïNÌò+»²Ñ` ŠO´!}ÔPp\’†4Û/ߨ{FÜ>”ne£Ô‡»d\…ÝÊב¯>ä9ÿÊØ¦L5ºF–uÒgŒ¢P¼…2yêBÚ¿Lrƒ¸œ‘K{¬ª eS/cLÄÆ. ŸÕÁ tºGyèP²¨©mž¡˜hØøÊA´ÏÄÏvE"û`s„ÎlP!Œº¡Í¥­Åê·°”2Ç|uÅ—”GyÅ'ŽR›XaŒŒÅ¬#’?}?2qJ¬ ú­à;ì4k Ølßš!Šô/lv²V‚jŒÍ¶83"#Å:eÈÂ_?HiÊ™¾%\Üw.z¢ÝqÛšöÚ×ìÑÞô¦×¶{î¹£ìŸn_ùÚUmÁü©m'.}3Ö}ÒÙhôùÚ‡’ü8¯ü¢!ª«¶IϹö rsvѾûíëÛQ¿sjßþö%í’‹¯m¯<öEŒß¼ü=#/…Ô½¤øŠ«jÇóœö‡§¾†û˜.j?ûé]í…ûs–³ÚêuëŸaLT§ÍcLÚ|(È»Ý6ïS‘Q¬P•ûÓ®#ÛýjÙÐuÕvÔUuè²oSBc¿6ƒ ò`QIl“·HÊu yaùãË_a23ÒI-f€•<õq’é’%Ÿ:"7ð%´¿éýt±øÂ¶Ìí“@&~î,˜pE NllgãÛ#™‰™ÈÔ÷Ðë+ÿ=+¦L“줣!ò% qb×r`D_ôÊÄ’[ã* dÙΪ±Ï(5ý,éÌNÝýPûÄ'Ò^ú²ÚÂöi—üàÊÙêñ<ÖtÖö“sÙÞÚõëÚÍ¿fg€²ûÒ7ÚÙÜûÈ#Ë8ëqh{ÛÛþk›±Ýd6ÄóÚ~ÏßË7µ§–­áÝêØ<?qI1¾säu ýLˆ‘Í@¬#âJƒÚbåxÏÊŒ3ú ùq¢O´"g߈MëÇøÂ /ÔØ&-u œJ®>¡¿Í¨A>ꂉBôyÏËDžn2yÚøìyVàËç^Û¶ßaf;á„£Úž{íÊÄå&vB¸·i„e0Ï$\àtœQGáQ®ùA/ú„L¾xõG9¥ÚJX¼ÜŶ1†46;Ñ]®>K.ê“‹ž¼×¤X¢?¾T—¾§¼v΀¼öøê^~â¶–1 S|†LÛ'm¤(Ç mQ’=¢l5ã¬bJAÀOÞ>.y©ÄCRéÙ!©¨¡¼4‡àrÛ^¢äKIéÎCÈS¿ël}UVj‹Kùx˜H«/2-F®ê㺌MErYÊ¢\öÕÚ&‹ }()9óþÕ†^Y¦-¥^ãÎE~]P;ÖIáRtú#Ì) ¸â/‚¿äjúÚ”øøO»hŸr<àÙ†Õ«Öqã©öö·×¾õÍ?#–·¦Ÿ×Î8ãëíyÏ›Õ|ñÎmÚÞåÃÅâ.x6ãÊTVü;jz¢Ez9ÄÑ †ŽË¾èA†»îz¬Ý½øQCü³vá…ßmsçÎAßkß½hQ{øÑ¥í‰%Ëñƒr”âûLèŒMÇÿÜì|ÿ‚{®6pô¯½gål&•ÃáH›Ôz U¯?³¤¤ï8㎪L›aذM´/ öU¿`Ž[˜¬1 çãÆ”oï Á¬ž·Âû,¬8€VQºj;-á•.ó/茳Ü/¯?Í¢,íj“6@¨eU›‰dæd‰j(«ë"R§˜Ȩ̂(Ô^Ÿð¥¤ÜVŽ~4HÇ/ýZüñQá)ƒ&OYæZ/‚tGž-ª¾–4JÙ—F€òËb³$çµÈÄp‰A=Ù @x¤±¿ ¾l49Ôg›c–²Ú¬qÕ~©ÌœyJlÈ/¡q»yÞlIâZÛý+ýuº“Pú¤ŠBM$Ë_ù¦ú·¶UÞzofvÁ¿Eí@eÊl ‰"?\D¥ôÊkxûZ¡0éd–“]XB놾åÊÌx}ü)k ]d©.JSuÔg£e^#ñ…C{8c¹Îÿškïi‡~ ¿ƒÚu×.nc8M—~‚SóËy¥_±MÜb³`X”` „ngð‰"ÀS vh”‚ŒH`=lxãiY²ñQÁ·\ÚÍÁ …ÀÁ }êÅ%/ÎÓWÑhБI¸"£Náj†2 —3mA>óAtP•¤Þ .;¦¶z³cüDºlÒNqH[4nå¯?¯zýW~Q¦çzrÚ@ƒ A8¥(io¦¤×u]¾‘ÕUEYö¢Ó`Á…U<sBŒM4Fáï¨øÑ…“ȇNVX>äl`7ÜYb¼5úœ_ü†>‰¨iJ—¾êp5ˆäºú *p“Å7æÈ•·|\ùø¥–¹ªIíæ§Õ˜(N˜8Ž'íÌißä—^zU;ýôwµ9»mÏôiwc€I×`†oôl¦Rœ¤£G%,hÈbÎX˞أzÊåq)OPQÜ]ª¬!Xfí‡òj;jH»™<’‰Ç}R mä‘>/cRæØÑ<÷{Ú$®ÓÝ–.]ÍÁ·rsà˜vþùßoçŸ÷vß=·c_ù2žþõmǦs³ðv\>4%]ÍÙ¿U«¸N˜ÏhÞ3àSvbœø ‘à—Ú-2^µÏëÁÉi³þ&W¤‚–oÉï@f t88I ¬ÈeU,£*m®(lN¼™f1£OÆRÄÉáDG°c‰O=ÛsÔ1óÛW¿rIûìg¿uùÿ…G$Žã ÷ ä2ªŠQmâÛö®ÄTå&C Í(”6öØJ¥–YYg5±ÓZŒœ € ú m­¬ðÀd›³hNô)&'7C\+ÄIÒ0~É›KI»â\˜õ½â˜‚7YŠ5Âê’«¬Èƒ8]¼ç¥‰ô5±(\éÏmÃt™¶)ÕŽnP#1ò‘ãŒSmp;“Ê`T>± (¸LU&Hæ£ùW¦¶èOÖ‰5ù%ðOñÒáO'h)0KJ-pzÞÑIUaFXA¢<þe;]U ™~‡<5Ejb0ëù옑‰‰üévqh‘æ†8H†d鳪ÆH¬ßî`‰æ$~¡G•áÛ|ÙgmKžXÝfÏšÞvÜaJÛ}Þœà¾{Ñ#íß»£ê9Ò?š5}€É¶zµ'aäç=@‰Ëk’«F”ùÇ»¡u(rçâλžh¯yÍ¡ÜOô9®8˜3j_lgžùõv䑇¶³Î:­=ç9;·#~[Í=G.cº|ùÚöý‹ïc>ñâ¶×^óÚ?üý·ÚZêó”"úhÙˆFtDFÚ¶Õè‡@J[ÛF9 Tý]”ÃÙð`Æ7ŽSi£xœx‹cµjcõ6îÔÒ0)Sa{‚#rËWÚ§Ÿb2m©ÍX—|ü,A•öQ—1œ9²²T-ßè Ê£¹Ù®Cï'÷³šüê´ÍÅ-"áSn;³fL¡ÈI'Y§ƒ§NÍ1¿¡‘2ÁÚ\!AŽíæ¢åùñ¤å·‘êÓ ðÎDÑ•S±`³áeŸÁ•§Í˜Æ?‘†­åtÜKYÀÄà»á9ôÐmڙк!ÕðZÔ¡\þ• üTF/üäÉÀo–܉Iè³êD:±ec&Ÿ„¬ Õ $oÃXï†Ó𥊺ÒM• %Ä$ .ÂO”¢#Ð rñgðf% š›>»ÎøL™jµšµ¸jgŒDä•»ZÎFH£ltVÃUðŒ…¤^U.f½åMYH‚,œ >1YaPŠUì4VÒÖ¹WŸ2Šd×K¦ ñš6ê+ŒHÀâôR¥¦Tá eòUž `ºø¥Œ'­þŠAcNÏW<éKµºV¿.ÂÒħ¾â­‰ŒSŒÒ 7¦‘ú d’ Hû‘qš7 JÌ"4mË*de™üü“í¸L§ÍÁNÚöj©¬#8^Š¢«ïãÒŸ©S&´iS§ä5oöÊû/8‚囄•S¤Ô¯õ‹äÕt¹Òö‰LÌëtŠ B~‰š—ý¸v@‹íbµ<1¯~ ©³-ÃcZƒl{Û½‘߯q–ÞZËôÔFŽÀ™òf½‰¼ÝÛ#ß‹™ø_~ù¢¶ëΓÙùùA›¿Ç®íÁŸlïxÇÉmÁ‚yícÿðÏm OMRür9êcDõÉ„ c²Ã桘WÚc[N¹{i‡ ñ¥nûV§Ó_ª=ôÉÐ˵¡€SKyõmHøS§-’ÔÍ’Ó³¬’F›("µî±3÷ÄIã8òC{þ~;´·ŸvJ›¿`n{íkÿ¶rð¼6‘A#Õ—7?@Ø¥3ÕM²tsT“Û,öD“DE#~Û'haRœ>È¢rö›O ÙÈ‘ö`’øÕ=y•-ã–눠GÉŒ$BÞc 7ÇI‡0õ $Û"hõ‡ìÕì• ,Zc“OÇŒÞ)³-¥áåÅC±Aƒ»yÅ› q«CÝ|¥±Ý=:\ú„åTtR[aRœ>©í‚9üÒË刑2y+xȳà4à “à¤Ìz¾Á­~SøÄ>™¶Htö;x·fùe?2²¢@Ì%Ú¡N–v&ýË´‰2ðQ¦*'iê3.P— R$–¥b²€Ó¸†ÁIyö{ó{7—¶ÝqëòöùÏŸ’—~ñ_¿ÖþÛ{þ±ãxÛñD°Ù;Loëx3xÞ~R,D¢ ¯ŽfM‘íWW¨ORè{[§cÇŽj7Þü0—ï¼¼xÂÑy‰âSË—sàdÿvÞ¹¿Ê›ÈÿðÔßãÞ›Û¿ÿû%í .o{ï=©-Yºœ+ æ´Wu@ûÎE?ÉøqÌ1ûæþ9}^í]>4-žDFM ‹¡_´TyMaµ:‚Hð>íEâæ•±ÛJì¨=~©©€¬déMÓü§A:»£ʳ´uœNžö‰YÔ€c”ë©/âW¯%CÓåI‰tƒ~å°;u¶{ ‹°’›ÕWTñ™±™>Fte®<Ôê7Y«=Ô >q§ËßFÖ³fMf Û¦½ÿgµãŽ}^{ÿûßÈ©ío´G™ïÊu¯”í‡uûb$:Ū´’MBì0þÑ[Í[>Õ ÕVà1¦(°•2±èuÆ›þÏÀ>y¤±©·Vó³ ·N§èXâ; ÕÏp”RÙn¡ÚÈ5·0OŸ>ž§yLÌ#Rïºën\ܶ㽋¾óNÏßß}di;õͯâ¨Ý‚ö©OŸÙžxb¢¸Ohõ:lªÙÖœ (8s¤Gpä“Öpœ®.}Ÿñ¥ˆstÉk_ð†ÚÁ' ízÊŽp3øƒ£aˆî"¤¨t6ôiŸÖ’I«~ÐrˆÝÙ۟뎗.]Ù>ÿggGïÏß};:kÛ-7ÞÇ}ãxlÝž²üJé-•±$qŠ F`-¥Clû1\ò¸ƒÃ‡Ôá ]cn`ˆ3&bsd„Ãø±ß E‘Û™þHßtc›³„‘)•1ÈwHª¿³¡ÉD!qCZ]4Ž—àÕXgZùüŸ¬j!Ý'M±ÝKÖ$bƒ’66Â0,ã´˜•#3íƒ=…@ÒX¿ôb„´l£‰ÒRME¾ñ•jJ Ì”h?å$^ /‘ä%¯ :i¯Ï×Dúó…uF©1ºG®¸B ¡ýF89‹:"„²OÄ)Ž’È?di/Ö6;|…Iά¯ë ‹¶({ðs<@á¡Î—yæHú:oÞåˆõ[N=¦ÝwßÜ¡\Ò~úÓËÛ¯o¼»í0‹3|¼ðk®õëÖ•OJltj’ò2)+Hƒ+@Ôí4ú4Æ]Ûôcmãž.“4¶fÏß|ÉÂvó-·µß;ù½ÔíÔ^÷_žÛ>wÆ»ÚY_º°}ô£_h/دvÉ%W´¹óf¥~þó‡ÚË^öQ¼gûÁ®¥ÿmj“ÙÑV/A˭݉Óx°F+GÖ!Fõ]bSC´>Û/.NTYÍÐ¥–Hcxç@¡ ×Äž-àxš:cÙN^«¢‰Òb•VR}EGL˜ËÆId²ê:ïðبìÅä»gÌŒ5Øâ7éEæqIPVE¨„1ãaÇOyú‚ôùU¹úd*³èÙˆœ«bõñ œÆeÆnñÛrlÓŒÚžŠ«–ˆv|'Šd°Ô•TÆùØb)t ‰>Óu;E¿ÌÏ•V-^· ÑZ´ŽÁàöX'oú⺂9bPhs’¶¬Œ‹¨ú¤SO×X¤_r¯†ì”Ç&ÒÙ®ŠWÑ7IR·É3ßAQ&é6uµ*ø^—êà›é …¬Þ2'a € AÌÀ“#wÊÑDx°'²ËˆS@Q.¬%•Á%ÊÕG¹N’¨ï¸³A«_³ÙËç¸N7®}ÿ{¿núë·Fèa‡½“}^ÐÖ¯^Ÿ‰¾ ­.)X'N50k¯uVÀJyHÄAii1˜C·)Ÿ÷iRW9 )3JŠŽU¥#jc튟£×©IÂbšOíì F//ñ¬‡õ â„[é54%Û”³ƒöd ‡X $‘•#¦mõXÓë«m"¬ª£ã èô]RØF¿¹$°‡ý.ÒØE£ì!èÖ۔ʎ–™XàceEnÝž¥¡Uµ?Ê ÓÆ`ö®Õ-^•eÑ—È€ÜêvÑŽ ÆXc“ºÕ*þº˜š"—ô|h!Ò Ú–‰}™Êe‘fdQ‚xýv~J²Ë6+í;dÀ·€±ýìc¼©¨ J{Á.¾äãÿ'J‘×:¹„¹ä yéCm2"Ù5<Ú9¬Q¼ó3yJ—²ôK·1~Jmð"åw½TeìtPÜÐè1ƒ%ò°×vï¨âÓ4ˆ:yä×\hÄjÛÆð×û»àT†€{½kà±ÈA9ߤz¶"FÙÕ‰aÙ#H»÷º’Ðl`¯|ÌX®¿â{/gÈ@‘ˆN#-S’\:On.–Ú,¤uSÂÞ®k Ø1wk} °QdÉ$Æ­6—¨+'ë4¿vˆ5«Ÿn»ÍÖ® ÓRÒæÍοռ<Ç'‚ä˜Ixåñ3²wœPTi×…BD£B¾$tnŒ÷íÜ1£—YÒAJN}Ÿ=Њ•nŽMöŽØLÒ³ ñ'üi+xÒ¸$t.ŸŸ¾Ž'yÃDžîäÌ=;÷8ƒ-š€-ޤY#3)Ë ÒÖKR–ífpgI†¿¨I,mÑmA† žrPÈY‹¾‰ûjÒn!¹¼ý͹_oÿ‡7ð‚°YíÜ3¯hó÷œÌ5ò3r)LâPÈtï?±GºÖ€';µ@³a¢"d}C›ÒvZ ¬–Enǔƭƒ¶¢d¥ ݶø©Z%±Êh—–Z…!L¢D—ª£é Òñȉ›&MߦNŸÈ„bc»ñÆÅí\ßv›;™Ë…®oËW¬âèùŠvòëŽjûì³'Gѿ̛wâIaëÛäÉc8¢>&voÙûC6òÁP8uçè(úw™üÛ~¶X8¶dÇúĉýÿi—½.1kÊ@§¨§thcŸ¼"ÃOÜe6ŠvCŽþ)ÜùŠWìÕ¾òå‹Úþ ÷j;í:³í2g7L/nSð¢}Tž5ŽõeR¢iе=è3Ê«ö.›:Û&dÔ¬xjæiºÚÎϼ5šöË7ÚÔ“Õ¾ƒF£ÒéM}U䤥(öA æ«8ÚÈ»ÎÕŽ“Òd‰üü¥/)kdÂ4J¡‹ÎBéuQa”ã“ì˜K¤âÍÆ›jBT8Ê Ÿ˜Å ص¦–ˆ”‡¬31lTžpâ)£Nž³r©´Ý5è†a¸d#öC9þexC)éD “©`!ïö8¾Òñ±A¹òÂ'6sás-jûgµÝ`»8ÊŒbЧiûÐ+ÝPTŒ Ó¶ÇV,ºsþ裫ò²­Oœ~Z&ÞçœýöðC_l·ßú`;ö¸=8CÏ6Žm³/Óʼ= [j±˸™ñ8¯ÜZfíÑíFÅ'±tÓ¼,v “ÿÇ[ÍËó¦¶ .x7/|²]uõí„ã§ýíßþgHÏáʯáý$3xŠØcí…/œÕvß}6—mÌý7ƬgÖ1mú„\þ£3rÃ&‰l9‰ûl-Ç'âOúøwïPd`è÷Ÿý.;µø«&u\”/Ý–ÉÇ*ÿjˆèX+Oo¡Óf°7e;f)yå8N8èÌÔø¨K±”\ã03ŒO]h*c»æXêP‹ôâ§Î¼nRco¡Š ^_Òi‹/RqŽìƈþ 3)äeN>ىī…§ûÐ8×6cÔí]âFjÒÚãRh¤3ãUeiÊí×ÙÁ$›K|û„)ŒÈLß)¡°?Qš?çé ‘Qs¥š_èLa%Ã%ã¿m&uå4 ãsñÒÙ¦¬µÝO s!ÐÈ_ÆEO¶«È5*Ó6‘Ÿ&¥LÀp"8<8#:”¦ã¡MŸT]\Ëuqî-ï¾ÇLÞ¸7†GòMà†Ãm9"¢S‚Ä¿ZÔš2³C‚²¤‡u‘áÀ¾-é;|±DCýPçZ™Ã²E=<¶u[²ìѨ>å͇a\Zv þ¹ä²3¯ÊŒ¸dKL’÷–|Ò[.ï“–®rf=Më[@ŸzjÑz^‰þOEq’26m÷(=5¡³Á A‚¡w8Û8ò-N:†ã(âdTŸ× €X¼–ó7 ΆjĆ¡´:›ƒ`‚…¹V\Å|-Í€9¤£¬ôFé¬h'a¡c”ÏxLS­} ²:hyñüyËY;N‰Ì2¨dF±ïêE*YWŸ ‚*÷ŸLÝÐÌôíá³jã¢<ôf`!«%%§|— ¥ók‡%_ôzŒ3õ!„¸|r[òäÒvÞù¿lsÙAv©̒.&òn5,ÑÖÁA‰XëH©3ÆÜI‡dÄ¢u‘7׉ʤ}¬"@›€í௥ʎñ½%qĺ6,ü²,ˆmUA¹~p°Ç¤$½¿ãSÅä2!^šuðìÉŒe›ÚOv[;û¬+9’7¹ÝpÃmkKŸ\É»Nàzß=Ú¿þëùí†ëïΙ”‰“Æäè¡xFgç-Áª¥ý²;1ø‰‘¬5ÈP²M2¨ë£S_ÒF¶]6¾”à‡\¶ÂD Î˜²!ÓöLi}Æ/Oéü¬OÖs´/7\ÆÙž÷ü÷sÚ;ßyt{ûÛ_Ï% ?á>ˆ/µ£^±{¸”ç„!‹“-mÐïd£šö7¤~Àíå'Úà˜â"m8U#/?˜Œc3СK^[åÕÀèSNÿY–4·Ð ¨è_}õ¦Rˤö#62èüoÚ +lú³úDd…¸8E©èHîöÃlUd»áÍ8ô“Hׯ8‡ú>†)͸v)ßÊé¢M—þ ^øâQBg…¼þ¤a ͱ 1dbXÐç8þò^¡…3>±M¢O¨ÏOpH¡Í”™W¥4êu 4‰q#éL¤«måCøåSDxd§L–ÑÄ£—Ý®Zµ†ƒVcØ1Ϭuô¥qí®;ïm‹ï~,}nân^ßÄuþÌì3¦$l/pèD;º´L¹¶—©nMl²"ЩwòïÞ›9÷ -]ºªí6wFûów¿™ƒë9Ó° ×¶k¯½©Èã?_vØAmÚ4Æ‚oáÉi×µyÔèÒ'WµþìvÖ¿¼…†¥íßñ/ìdïÙfL›˜m®;ÓÕTÚ_ì…tö±øÄúHlØTc|rE«ñ¼q8™$‡q"ŒF>õ"ÚLâ,ÂØlÒ&EÚ¬")½+ãS¿’éÜ·,¨ÐøÑÝV[O].Ç#)Æš“È‚MÐHo›ç Ž‰Åa<€&ÛÌGF°’ÍA-«Sÿe¬DqðB•¸Ui8\óC¾;énw²R¯Å|jg Yôí­þ¸,ƺŸ’ˆ-Êsñ €c€¾¨ù@Š#Û>áñà3(øjzÄ óµÝò‚©Uü´Í"éh7Œ*ÒÒ3«i©q‚˜øLj %^Ú®šM¹©¦Láäã,}B:x¨’F¦$óz}])ô$n‘[÷ï¹k Fê=¹‡(bƒ¯›çæ…îÆÛ9ggC[­ü?`™Ì3³]ì4ÿ'/“&OÈ£ÿ´cööÓÚÕWÜÁÑJžúÁ€êÆÛè0¸ Óš«ÅäTg¢ÜŠDD‚T¤B¾p£ _ª*¨BÖ„T:’újI$k§T RêCP‰ÇKÂ]1UÁáÆÄ;ŠP†pSž~}M LP~±‘LQá²³ºaFcˆìCf† Ø*ð­µó8 z¤u°V'ô²f01yü*¿eN‚„(?ðf`Ng ü䣆zQØ.Ùpʧ|D¥"_ì“ ª°òu¥eöœè°4ߢŠ\ °;X‘k3K$¿Ï»¶MW¬[Ûzx9ýwä±m&oÞããA!Êe"N$˜*/þ0§ÄB딈*í0o»Â`[×ÀFÂvë€+kÿ ÌJ›Á(]Åu‘«}ÚËÏ} 2~Ü`6À Yꭣܴmh§ˆ9Å: Àø Џæ?Ïé~¦í²Ë4ž>57^qåmí›ßººí4{b»åæ;Ú¤Ix|èªöÆ7¾’ljîÓÎøç¯´[n½¿­X¶¾ÍâÝ'ŽA¶Gí¸!·Ÿ²Ö+ч#ô‡¸lK ùn¾TÒ|¼ÿÀ.µÅñ¬’pL È6µ.~HLCmãB›I|â‰~O[¸³3†I׫Nxg;q9Ð÷˜|­mozã‹ÛãO<ÕÖ`ûxžº² ýig\Ðw´„6ˆ¯6 ârƒ"|«_‰×ä%î´<6<&¤í4I(Mû]¡Ëèö¿ôi£u³õ3=,°P_Ô©WÂ¥PäÈ¢ì-øLý'z;¡“žàQÆHLP‰¬Xm½þg&·¦K72åèlñ³Ý[¾¡}ÍeÑÁ³È;¬“Ò„`U5úÓ22æë¾…8 *û2©P—„Œ82•\ÓuªÎ¶„¨äF9•o#ëk²9Sf}dk³šX7ìÈÈoÁ YFaÊRwa7IIƒ–"/r¨÷ /˘t_wÝ“l«|Ç;ÛvÛMoüÐçÚi§Îd{t›;fämà†üI°»УÅX쮵*“ëŒbt‘ht‰¸c‘i€åò1Déå?{¼ý•×dòÈ!ïå~ µ?zËÉ\.7§]ðõï¶wÙ¡Ý{ïÜTeÛy'Æbr×Ýf·wì½k»êŠy Èúvô+öÈÓƒ6p6уKÆ…p’½äS?P'F|oSˆ_ÿeB—´ ÃÎ_­ÙŸˆŠÝé;èâEÈ•%OŸÓŠñi&å*J[ôØ‘Ó2Q*7; ä7zÍ¿å”ÍàÌÀL~’þâç·ðæá_àŸI%ܡ͞=+GXÿ¯“oû¿pßöµ¯]Ô~þËÛÚ£÷­i{ì3•›'"›K8|9lË#è#½¶‰ÀÅ™ ±õPg6eMoJÏQ=påÂê½aSZ"-m§rñjbPýç%vŒo¢üò9?j7ýzE»ì²¿‰þÓþøãm{ž¬2qÂØìøPÌú¶6:AKnë>©³OˆÛr{ ÿª×0¦À>¹èHªÛ›KUl3~ñ½q}v¸aè­Ò…*5KõWøÈÅ8”s1hx‚JìÒ¨8PÌ+Q)È9yQŸ5m­ µý³OB+¯tÊ¢ÒIIYL6ÊžQózrÏüÈoÈ 7´¬ÏbÏ®EmÊU‡ ‡³±ÞEáÁÄîÌèt—=±¡(?9’ —BŽ:¥)ËO]|­F•wZéJiâÛb³_M;~±ÎMÅ0NQ‘…Y숄O}–¹äŸ¿`H¦â"•ÐŽö2 Ê—0ùß—wï¼õ­'µŸýìÚvýõ·òfßemåòu<^s6¸gg= øÓ>¨Ï$y+ÎK—ýÀË"mÙR.cÄÓ$mn[™8’Dþh®=±Ý—²3?uê(žþ36“ücÇ%s“Ú[ßöw\øñö{¯;¡}ö3gµo}ãš¶ð%»ä‘Ãßãñ£ùÈyñØœöww&ÏüÅ‘ÎÒÏݶÔgÛßwý¨Ì\¤ô1ó§ÿõ·È݉¶*Ûkmèg­ŒA6¶ §?Tc‹ ´9Öö6.cX[£'ÓfæÈíoéßʇ&—g+‡z·•¥SJ×wÖØþe_É0ÿ é—–…ÚŽ/¸ÒM¨#å‘#`Í¢^dPa±>.÷É €Æ³+´¥XœzxlÚži Wÿ B QVë6Ë•­ÕÅTø¥Ã7BRÙóÛlcß&é3*F®œˆvÍ*LæTB(K“ÈÁÛ£|€•òK¿Ã^[Ñþ>ŠíiZkò/6ê ’h÷_lR¡!yÇA|àœ­Ûa<¡ BõèuÚð¹S£nÆ¯Ðø‡õ HØÿjìŒbâ?ÏŸÿ‚9YͤçLeo}§Lþc„¼RÖÿô³ü7[Ò[÷ÿ5¿%Ï–éÿ•œ-i·Lÿ¯ø~³^Þÿ'þÿ¨ü7ùÿwäÑɱb6ôžÙ؆-ÉÃ.審1é(F’më¸ô´‘”˜0 ,4L)K@æ¿¢ÁU@IDAT®í}ã,¼¨ CJ>vÂG=Ø ôÉCwܾ²ýÁå 3;·ÿøò¶hÑÃm*/.òúKˆÒèÃõ´*Mÿ°,ê©APBtˆ<ᥘP$H«KŸrW”…´,¶““<7¶Ù°Kè ô(ÓG T¹8HæÚH:L œòÛ)¨ì=%rÍÚñÃ]Ìbt ËÀr2ðd’D…¸YGL´KÍ–ý&A5îäEÿYpdÖ"cç“.¿²Ë;`*¿L6eáf½±öR¡¹fÍš\:rÄÏi?¾4—»(3K÷µÒ‰9zj½ö±.W–¦üSU¾—F)+Á Å:—ÓHC˜` ¾ûÒÁPºP'±Y2>¨Äf+@˜¡Ñ6â[ÂâëCbµâð£\.ŠÚ˲âühƒ‰ÇrTpZ›8yÛvÝ ÷ð…K¹ìg#;ÜÓøÍj7ÝtG{å1/iò§¯áÉ%2Á~°-ºsi›Â=^gì%ÊNs±Î† zcTá×ôjTÅ’Í‹äËNhm \¡SW<Õ>n˜bu *‘³¦bÛŠ³“ÙÃY^fDášÕ+s™…Gf'ð"5}T¾ˆ†R6èRJt Ò¨°aÈÖä¿úHjTz¥A–v&M>;x©×ЖÐäÉn«Ý~­˜àNÚŠjã)õ*滇>¤2)•¯ï)Sy&ù q'î´; ò‰ô©£&ÍåOrÁZï’IŠÌ~£ j‚ÊåSî_)…!ý>Jáƒ1öw…’鳜ø†h›¼ò˜°Ð$Âã2±+þ£? ã™>*rþ;† “iiýWô29þ˜/>ë Ÿ¶(¾@Š=©?/ÞäYýÞS³œ—ï=¹b];ùµG´}÷Ý«]}Õ íïÿþ2v’—3‰žPÄíO‘£Üò:M»¨ÇñÙ%g£Ò<ÖYcï·Àc7E>ùLq^~´ä‰•¬Gs9ÜåjÊLþÊcƒ}©Þw¿{÷@Mi/Z¸}ú6ÎTÜÒfΞÐVðÖñ3&·ÓþøwÛM7ÜÑ~Åe“S¹‡Æ¾ ©pѦvÁl²Û1lŸ¬O¼Øf~h÷´grÖɘHè.0] JŽ<)IL¥[ zìÿÔ­®â‹ï’äÅíUþe5´!<ÙÆ¥ñ©A„6IS˜‡|ñQœx“ÇÉkfðs1|‡cÚ(ÎÀ¬ æÏß±qÄAíþûl_?óçmîž\zê½A?”'ªXYö¬€+UtbŒw«žƒF>¾< ¨Ÿ† ãgÚ‰òa‡¸ú¡Rj‘|³¯‘­ŠRS>WùºC½¥|TD¾>ÑúRt¦#‡tPLUv¸u– üöÆ‘Ô|þ+òÛ¶ú¥ªâwú>ÉÄzœŸr”]ÌOJ't‘©êx˜rÒ`Ô±—ì¾Î*­ gŒžæÉ>3Õ›sd„­dCüìòÛñ &r6âT®G´ä9É^Îá„Û ÚÚ;oH+€²áHšHL#œ[ OÔBoÔ w¨²©…Ü@›Ù–VB CÞ=ÞºÀ”†HþtJ)Œç  ` :wïCÎ_ðš'-‹”(O!ìôÎÒnÁÖ~ñº+ÈN«—ùçC¦ËÏäÁbUû‡\L¨?;dÊĨÏê:5åiôÔÆâ‡ì+ zäent‚GÁùáŸt|²ÝŽì¶XMýZµ‘Œyr5#-˜ÐøB~ôâöç~x;øàý¹ÞýÎvÿý˨}j 7âA¯ü DÊV–v‹‘úÙöŒob_)u€±>˜ñ%RÇÅ«7¯üyمʆ#AJÈ™†|Lá¯ŽÄÆbJ-×OÕÎ9*×y¬³ÜUÅå>ä•瘖ø„Ä7þé®]˜ôÏýÝ<4à™öã^ßÎ>ûç<]h[.ƒœ•þæäâä“k îÛÎã]#×ñÒÁeO­çæÛ©¼©xÛÜÔï徜+M§nœ»é³n·’¡Üã3Ú]Ø*þDžã6ø' ÐWül›ò+ õc¥øÇ.ŠÖñâ³ÉܧuÅ•··ü›íc{s;·³ÎþzûÆyW¶çî;»qåuºö–“;ŠòÅbÔ*\¬Êñg”ú§A@…!i ÆÖm4H"¹|üær7eâ{=Rã“`!¾;/ÈŽQt—„0#ÏO×V.‰ ¹íäâqšBÎñ€T‹ í¬vÔ}ÑŸJe]iÎhCšÜ¡XºÖ®ß·„„†»*$M?§N)Ã,ömÏ&©3—|2—›…LÖÍ¥ð·\î³øíø£÷å@ÐIÜ`kù~ûÆ…—¶•LOzõ<΢>“ÇÒq½­‚†ñ.ƒ0ú–ŸãC.=‰/ÑË·Æ Ï …,HŒWï75ÿpÄsúv“Ú›O=‰Ëfgñð’UíG—Ý–68á¸ÃyGÈ#Ô½¶­ß°¾½þõŸæ™ÿ»òèä©\*·2¶NžÄuþ|ä-ã;ï0-~L»!V{„Ívî>ÓÆÈ–q oŒëLPñµüøwpѻƩ\¦Ã:/«Ò š`­,‘¯·w‹½Æ 2ö¿´ºªmƒ±ïŒ¹È¡LÚ®#2d¤LªÒÓ ð¥oVUl•R^ãBÖQøùiŽ2»xE1’K£}Õ§¬Ý¼¬LˆŠ8´a,¥ÏT›3Àª.‘é:û…K;D\ùÒ´ó"íN…u>\þY¥lu E™Õ¿)ã‹@\êÇòµŒfç>¹QY:6c¨$ÖD~Ù#tšCÕÿ¤òÉ—ñS–ÕÄܶÓÏ#­"þ—’¤Ž«¥wKHa£nðÑæ eÑólhbc·ÛOŸ˜¹5/Ý”6cÎΕsl·Øs®‰f6¦xªûR<Ï.¿5Tpä¦tÌ8¦ œ0ˆ<X{š¶0e^WÍÆ¶“O™”t‚)ò Fÿ‰Éêà\ä'L$d _N‹'‘ Ý ÏN›Á>}XæFŽtöd Èt ;KçDFa³¤d&¾ÅkÇ3¥±N ˆ½®Ñþ”ëäe¢RÄ©6nÓ[$M©Åé¬Ò‘6ÅO¦3$€©ÐÔà u×'}„¢@ÛŠìéJíÊÓ©Š–*Ù©s’åMê9ƒ3¸_ˆÀcqw0ðô­~Ñ.:9‚¬×Õª1’br±,^ôˆ„Æ:V@ K j ÿêWíÕî¼ó6ÖÿÖŽ>ê@®ËÒ¾pÆ·ÛöÛOʽ(V¢b>“.éJ˜J:´ÊdðPªõ‰M $ øO²÷ë 9…ÂL>5&YÇnc.8‡xAZ‡ehfL•K<ð—Å)¸§ÜºÂš Šó¼ì53Çól<ÇM( ×Á)-KÔ[M]á6ÚYÔý—‰š¥’PÊC™üç@™Ô+€ŸVê?Í)ûJvÚ„¬¾oýÀ`=¶Aì[|…ôoä~Á æµ…ûo“ËlyôñvËMwµÕ+ÖðÌÿ­ÚΦ­á¹ÿ>å./#Ì °§‹eQ¦X3¸™d‰‹)7îÒ¾èÔ¡ØoŨ—ƒ î¸ïÊ“¯Öq‰òEß¹“Ýôǽ8뵉GÞ×^H_|äá%¹!ùƒzg=—´äÉ_/ùÖ©,úM‹™l_ ™ŽI_Ù(úBÍúcR–ÿè,œÊr‡.[yñšŽÏ©àÞ½a|ËÜOùÊ{tÅàÕ)3¡ t‡GrŠŒ7ñŒ²~#G¯&p½¨G'íD5PAøìòŸÂ>ZͧM:¡=ñèš¶0÷äœ_×NjȸÔBÌÀ… Ïñ0¤%XÜ6ä’¡ Ï·cº7P]ÉRqeÆ`Q$u£KW¢œÞ’ù ^U[5Lä¬ê“Ú3„"·‹G¶†Ne^ ÉGçæÁß2m”±S€võ:´ˆ$¿²a£ÜZ´CíÔtüvŠ=tWÒ‘³C¥Œ ³P“.¸Ð8y²Ü[ƒ¤¸ÔKt‹ÿ=I'˜WVdÀ7Ð/q¸)¨á!È#µøÄU:2ðH§ÑY(G‘噲ƒŸ¯ÐÕë½—öX³Ê£ÔöïgÚ³}sðèÜ÷´e"P&|:4H1:G Õ£|ët„>äg>¶Çïú#-‹$,uR®gr³2 m6`&U€,ý¯{2Iîéäѧ¥ÚÄ®BÄ>}­ÝŠ[Jhå1çR 3è…òÈÈLÇ&}?õÕÛ‡²ÆÍŒ“ò8C¥}ÿûWr¤ó'Ü 0¦{ì‹9ЏþÉíèc^Ô~àyí¢‹~Ún¸fïaXÞxéÌ6ƒ¾êÆÕ›®ÝaÍö×ÎWSkÖ…¿`âGÝÊÞˆ$Ù`h‹À²¡Aµ…§­iÃÄÀÐ õñÁN¾¼‘ñíoú§¶ï‹fr]ó;Û>Ü>ôá³Ús¸dSö½ ¸Z ^ÿ»cŒÕn=«_¬³­£OLÃOåXD½í?ôç\sÜw(J“2ŠrÍé툃lI’E,ÙìHS¢õvƒ¦¸Š^,¶]7$JÅ º* sXžr‹~åØ>Ö@OA&i4ŒGáâ/õ£j$øCY]k Md[N¡ü€·ù¤×g¥:õäu@UBuÕ/ub!o¼ÆŸdŒ‰ìÄÚ9øÙnÚÄÐ ’ÔáØ`©ÉR¨ÈÎ'õrù”ãM)ª™Þ¬$”£(O<ÝsÏ^ܵ¼½ú¤í ox-Ï|¤½ã´Ï´³¿ú˶çnÓxäð”ô)ßÿ“ %âŸøJ+2: Û$'Ä¡¸tlÊ@£š¡3$æ°_ßJ?nܨöÐýO¶—¹çø/äÌÜ.K½»ýÕ{?ÉËÿ˜{wþ¢½ù Ÿä2¾mÛg>óå¶ç»¶k®»³­ç=>Ös"gÊŽ>z¯v÷Ý÷æI{/X8+;±Þ k²gÕ=;…¾ŒðöK½ÝýâøY, T(\ÊH‘çö(LEÙPu&¥[ïÔÆRí”K¬nWRX¤éƒQBã+4ÃX/*N0àY€Q£íácRÚ$” yÆòØjÞ¾äÝ?_"žr%87dU|Š‘^9|cdK;zl·ì–ú5}Z¿6u\¤Û+‘R\Ð@”O‚J¾²E,×/ñå¶¢eÎÚxÏQtå…¿ë‘P¹q®m+jìSµ\[g|r„ÜÚ”U­$R•=´EùJõ8سVD°"ÇöËPîšOµñ¯Æ9°ßÝDX:, #ˆð—.@]50…F‡ÐȆÎ(¬ÓF5ñ­£»tÁàác *<¡¥ž0IoÅ}á.Š2r="ŸP¿\1– ÂÁ¨væ„ÊV‰yKèЄœ?×þ{Y 7Iz ýrÂn’v.ë”æQͤc28)sLç“‹ÒXØ;BÜÖå[ ŠLøQuÐj³ü 5¸—ýÕ Lf­_ àïàbãè°Sq¬c€kd¥­B§.‹PD¡úœìZ˜ò"EQ*‘újG%uo®g‚Q9úm§§·ï_|EûÅ/—´ Îg»—ë4ÿìÏÎcB;7Gì2Q€¸ÎXÚú¥;uì×ŸÆ +~|l}ƒÚ`^…ê$í¥%NtÌJãÇÓ¶ƒìL¨ô‰nú&tÐF'º”¡.W&Ú•8RoŽÂÙ€›¡ wÚ=º'2uŠËö‡Fh#—çÈÅ«XžLÈg ÕwÄ ÕÛ³Ã4{{_:ö /¼¦ÞåÜ€;ª½êÄ9z¸;SÚá¿óüö¹oûÁ%¿à±ƒwóÖumÁîÓx¹ïòÀ–a䌑8l·B, ðöKúXa‰@dŸ{ž"ïè;d¼ð"€ÆÆûroÐ<’ Ÿäq§×ßp3—Ni»ðÄ“ZÞ¦1ÎçÆá ì˜dÚ~Ññ R_瀀Ž2GRßQÉG—¦è#kä·7ZÏ?«`3f> òFP àˆ´9tÙaPõÆHhÉÆû 1e½v¢ÁŸt©IÊCDkI‡WOJ奈]wp[¥ßc+ãÆiªöût ÍV¶rjÇ8™ðÉš³ B!c¾¨ñù2Ù`­ªÚ2|úqd‘88ås×Wc—v( ö!øõ‘Vˆ4qK2ú ñ „1æ8X&—½EˆhÒçz½¸J¹}I%ü\Q®n·Í_¾lm;å”#Ûœ9;·o}ëGí›ß¼˜³‰÷·çì=“ƒ†ãroÑúþ<ÿLb±‰DñÞ^~çpÕÐlT¤?¢Ãö’N_ºÅh¸è ù«zˆ7q0l÷Ýgµoó—í˜WÁe/âÍç_nç|åLÎiïú³7¶³Ï}O;ýgq9Кvéo ïNn“x¢×…ÞÆ%r¯Ë£AßôGŸiÏÛ{:gC¹ˆ¾ âŠv|©_]²1¦p`Üã=8)ÈΡeVºÐø±Xkü¥ÒDè Œ@¬'P•XCºý"G{ÝvGŒÄ@Ø©Ï$ÿf|‚&8Â?‘§FWƒ\ukS½Cf*«Vó2î±Î‚Û¨ü¯ T”bÖ¤áu«åؘµ)+öªW΄ɶ{½¬2š"í`·žê'AâèÔbˆú?”hdP¯!8?„þÿÝ$Ф÷™âT„!Zû„¸äóþÆœŸ@®qYJn6ÜŽu2”³¨®ºôáH(eEp“ã›!Ù>õV¹mqÍOËR…ným¾æiƆþáߘP¼•ïÛÝYˈ>¡~¯Ò¿$aTXÉRmkoíY]š!î}xé i£”±R‡çÙ¿ß’lçtVZ/pÐ:½ÌÈü¬O]5Y(–šÚ(HaûRÄ’‰/‘`™‘Ȁ↰ڜÈ1ÈZVA·UŸ9äVÀ〒šYe( ¾ê¼LA鵿F D â’#&eTg¢ Ù±;`H‡‘ÒIC‡è²Ó’‰„pL[¼ø¡öè#Or€7sx#0ªA 5&˜P¡þ2->R¬†£^–gg‚AÕ8Ð¥.úÇ“&E†>¸ÄÊœì"¦áXy½bãzEML±¢š?eª~‰ ã…ˆ/jP” äY¬â;U‰ÙrÒŽúXÓæÊr\$/ÉÆáÊwØžÙ¶åÓíÎ \|ñU9Òô1ðÖá¹íæ›ïj>Êw¿ýöl?¾ôÊvÇø÷ñÕmÞœ)m"÷ lCÚ¸ÓîLf’ñw+ &õ»ÊOÿ˜ªöQÎ_Åy¼_~,r•ýÏpÍ3/‡›8¾söÅíWW<ÐþùŒ·µ½Ÿ»GûìgÏi?¼´Íàhè.¥P›;æ™t(#Z±µú¥¢)¡Þö©çóCm$ÁÖÛC9J@¨ÃÿËkC,às·;9ò 7d¤m‹lÜ(‘{Ê+Þœ Œã¯â¾éå Þ‹à%L.Ù!íW¯ˆ­‰Œ'dÅvʵ&ãžÖ¨Y RBâ•Db+hË,h?cÚ¸¯ÓçåW¼LI›BN"XZªüObôU†@Êã·Þ¡»j§¸”XìXçÙ7ñÖ¤P+l%þU”tƒ=U¦âóqÁ¾ ËgãOŸÉ%‚0í½×µÅ÷<ξ÷ryÙä6fÛ1<ëCd:yxúMšmú*¶ TœbÓÝ‹ðÀUcV/Ó»yŒck®­}í—^ú3Š—õ=¹luûÙ勸ç;ÜÏô`;è …í#÷áöÞ¿üb»íöûÛ§>ýžöû§×>üá3Ûž<‰håSk¹y|ûÄ'^×n¿m1ï¹½vðnàb‡Ù—æ(Ý'ZJ:˜?1_ æH}ú£ôE–1Âö®ÆVˆuÖºŽ3°¯.£„-‹¾W¡ã‹’“¥Æ¾R|ò³„¦° ö›טHÞ‡ZØ®Ãx­Îš«LéÒÇñÁ¨ï;¼xE4t½Xµ.÷¬ÑW¸ÅY˜-ÓÆÌ¤ä)¬“½ zÆŽ5¦Ð]PJvˆaØ&‹=c.>Ì<‡qFÓ×Lè£RH!ö+KÝ€2V'>ûØÖWÒjçÓ¾(.ü%¡<Èb ò3GFPüC™²õÚÐïóð8_5J¬:}F¦ueo…„5,þuRb^_ °Ó›TVù¸È;#¬Ò…º|#1í.Hñ:6hƒM‘34Šç—zm$Jê ±_„à;” ó§äºô§ƒèF–$ ž]~êÒM€è5Obm`d1à ‚ z›.3!F‡bƒAÇ|…àƒuxc¡é ‚’–NÖE'6˜ t\B]©iÏAÖ»"X|<›]3Á\â ¥#Ê XybB—•d$`Ñ/»É°‡Ÿ@!5 óàƒ‰Gç‘9bÑS¶—,{†eéìd7á6 ËÌð¿–eè3rÊSvÖð›Ãßñ]g輦=5Õ£újÃ:ò‘kÜÁïê"â4[š0¹êÐ6¥£Pé]X«ÕCÇlkúF¸î ñˆýS&äÚ(nj½ôG7ò²¿Ù\?{$7ö=Õ~ôƒ[ÚÎÍγ€êж²³tëw‡FQ¦]‚ªüf²¿ÂßVj¡¥¾V‰A¨Ã-î‘ØîmÍn«mGÊE~µ?“Jbº–.œ _âDšaÆ# g-µþt´Bi;ó GôA½Rmûì_fNZZþ|D Ië§Î˜ÔÖóødïIùÉe×ñ(ÄëÛž¿Ô˲°OÞ6êˑްpoμ\ËÑÓG¸$ge[0oZ›ÄÑx1oäý+¹îUWÆ>ƒ‹ºàiA¶ã”×RöýP\á¥ÞYüD`í²ëŒ<ôÆoÏQÜ]x¿Ë 7<˜w̘1ÊÚ(¦PëÙŸô%¤Nm´BúGZÛsüŒÂ,þôÍ´Ö0åCµMúPœ`)”V z»/Ôê1^]ÄðÏ`@QÆ %ÆÙ ?©âËøÍÜI‹öÕ˜T#Xí™–TNÔŽR©f÷™2éRwÒ0(tûnù1($Æ@wf½t±˜#.i}àÑ{°øemÑ­+Ú_ô5ípžærÖ™0‘þ—¼oÒäqðùHD‡•øù™,`w6ÈÊEÿÝNyýsÛ//¿¡-ºëá¶lɺ¶ó®Ü±í¨ô_j”pA®MYqˆ¿ü4Q$öšH™§}ÊñÁ‘>k;1ÉX»jSžTtçí´“>ûÑvÂë÷jÿôOïnWñÈÆóοŒ¥Moë}A“‚ÑÛÍ”rRåÄLz Pûf=Ãß6P7$²‘V†e©(°],$æ‹ úy¦nÀ W2«N6ô©0ªÚÜO¡FÚÒ˜V§¿ÈcÂNä“oµcû!užmq- }èÙ3ÕŒ¬‡h-4âÆ(³hG:·bö›˜%¶”. Î%dª.TXÏä(mOdƒ%òã¤.BòßÇ&êñR&©5œH㸠7ߨJ>>èÜ•V~ÉKöþÛ‹‚ąQyŽxÝëŽ`õѶzÍÚvÍÕ7´;n»?—Œã²1q®å ¿âŽÿ5‡Bñdamùuò…¨ªÂ²Oz0)CbWä”?S®ïgÙÐözÎ.mŪÕí’oÝÚÞú®#ÛÉLö‡û¯ºj!O8û$; ˹à.gÜÔ¾ôoçó¿kÚ>û̦|M{ïû^ÏYÎÚ©oùL;ø Û®¼Ðÿ«wô«m¢§³µ—ÚÛŸÜNx‰fÁ‰KH‡¸ÑFí©ñ ¶*Õ,‘,‚lœë,Óœñ˘oÉ Ýq˜­6¢>ô8fÕØkœ[Á¤Ñ(@NF7ñD`Ñx¹¦€³}¢.¼ ˆÍÈ´XÇò!9ú`“ÕYJDÈZ:fÑ÷"Šs&H¢ÞL>àÐqW|É!œzmé‘IJÄ)­ð%‰`U¤ý11L&þU Êâ4Ó&Á%Tð¦}Ы˜^KˆÙGŠG6qeßž"íÕúÁï^ì3@%P¡|åõ[ö(·ì‚Kyú6zèk!˜«=’t„ØHKÂŽ¸}_¶ƒv¨ymþYh¹°P$zs(ôpÞ,ßfÛ (ó®Ó²*s±µÅƒ…nPSÒ]kÄZ 7WÃjƒ‡ÉzŸeb]ãôƒˆÄF:yúž/·?q\;òÄ9<hÏpªí¸ãlîàýkãÓ2vx¸âºäD„¾B¤iç&~ègdùÕ¿:msž fâNËm„Þj ‘‹×o:7ýÍb—2²‚‚¬2sùmsS†×ÅÓÓ˽DžýÁvÒ'.Æ¿}¡ÁdY—QGu¥Tbí¨Ë±Ê¸Ë$„u&;ò)“ÿLFäAQù¤ø­µ6yé#÷DÁ#.£F!² 2OÆ>õš¦Nï‰3 e9zÇ:ƒ<|N]ÄbÃ1šj‘µ~GªÓ@ê†ÙÎ[ó ÀõkÖ·û/m÷>¼ºýÉiG¶ãOøžiûÓ?=¯íó¼Im·9ƒDìä-ãĵ1ì‹÷‚¿t ª”¯lóÆ&ˆc <–ƒ-ý‰DY•Ÿø®¦ˆ]ùðn3jt{pñrŽòÜ{ÙK8Â6ëÛÝwß׎?þoIÏiý¡·òîwµ—¼ä¿ó.£5y»÷Õ×ÞÎÓŠvDØ6Ì›ƒ5kÖñrÄ)iWïYÐîtŠÓ31NFÅk܈^LnZLŽ C"'¥J¢¬Àç­Á’ŸÞ‰]m3 àó_¡HDT4'¬ ‚ä»\uZÚ0Qoc"zĬ ¤M–5Ê(=¶Câ'ü½)¢#ø}JäþmQnÓ–h°Ÿô—”J«xå‡Qs’OôÉ—ŸnK¥p€,FçÅe#l¤ÉiZù†>–­8ºƒ$ýYòFpdЉ©X?ø³ £¬PÓ’»-£2.‰ÿ‰O›å÷ç”Y]–•O£}Ä©¶ð±¶¶½ÕÞæã@ÖO; S/½’GIt‡],Ú_ã…õò ¿Ú¶T»ÈS6˜H&tñ"û0$úCUü¥°³ÉœûLŠ:’‰Ñ¡–ãÙå?ƒj¯&¢a ÜaP&Ñb´ámIB¥=ÝŠØ®ˆä£B±B.uФ½ˆe­×5()O|‘µí˜ôÜÈfØ9å¨;Än1Õ5±ï@¥z+¨Ž–ÙáÙNlÝC÷Èp®_gm,”GGé²ÓÝs82c¯x­Wzub‰³QÑ/êS& e¨Sˆ9Õ«z[7"’”NYzÍiÛÞ±rÃ~ÍIˆÂ(Ÿ&ºtÏ‘ ^Jd]ÃU©Oÿ8}Íu¢ÃŸlHÈÆ("\i+ró¡ -¢ ê± I)­é·ªP¹Aó:ôÙ3§¶»¼nöôËÚ§?õûm§¶oïþ‹3ÚܹÓòÔŒMOstØsµú 1Cùüò§xu÷%0’ʈ=7„Е †v1/ôÝS,Š ñJüäè•­&­vˆÅ)_µ¸ÙÖ.c5µ(³*k-¯ÊuQ†€ ²œSKáWÛŒ%wÊw ¶lÉ©W·²ÕA"qMÚ‰„ñkƒº¡ÍË›r'Nª'ù”µ+®¼™ Ìœ XÑ^üâ½Û±ÜÈè£ /»ìÚvøá/h¿¾y—6,k+–óÔ.yØvÛѹA{#ÍSBÄÐ7ÒÙ k,Ö.})¤Ímœ)¢\ñR0ù’7ȯ¶L™Â£[p£o¤ýR†Œ`N¦Œ€ß±ÀèÉ$wê^’Ò]÷#`U°@g—yÅ©Ï39pÎDð ›8I›” Ô®38tÂZíÊ*Iò:)vŒ(>J F÷_´•zŘ¯ð ¸Ø¶t .;©ÚìƒʉCŸ¶„ÇÃN7®½ï¯NÉÿïñ$«3¾pNûõM÷µ“xâ×¼oXǽ*tU,:«¯¨º|Z} bI'×2w‚Ä'¯|Ú% 6ÅøÑÆ?´»Ië?“S/Eœ¿ûŒöÕ¯^½KSÛûÞZ[¾|e{ßû>ÕŽ;v~[µzCûÔ§ÎâÆÞ÷ð;‰7ž‰3móò´¼ .¸[Nm3¶›-ŸnG¾|GÆ:._ÂîœùAMô£-S+!º±IžqÄaá1鳕Ĕ˜§ #•ÚÔ9 T EPëo…´£mŠ Šßjï!„loÛM9Ž-i7åÈÁ½Œx2ï¸ßK¯IÚL›Ǒߑ•ê@‡L Ý¦ÀžËK–Áô’¢ièŒm4®Ù>¥÷À“®-&ðD.„ÅöEŸØÏc«íMMI¢Œ¼7·ëeTM×ÊJ›ýåR!€*3ý¢Ó*.r£Œ¿ ª<}+ã7tÁÆÚ°Ô&¿–egÙµå:!ÚI¨>9“ó/þN?—N[%´¢¨Rün  ±=3TË+Pûu9Zη•+!¥[H+=ÛºÐéu*Õ´¨’…5í Ÿ¥î`ENþÕJi‚³êl³ºüRœÜl Ìß–gFxä{vù­yÀØÉ€’ÆdòdøfÆAó’´«­Ò*Ðl[Ê3Á’pq‚@êùšåæýã[´†Š«Büœ0“AòÝÏ€W£duxG«…‡_QpéˆýÖ‚WnõLÅ•>ê\"ÁÑ.Zº¶t$¡hS ¦bÆ¡¦”G„è.ļÄ$GÝc•~¢Œ^£UØ1ÄWGû‘NmŒí%9T¡O;JÈ«9k}€8M²ÓR‘¼´£;hªN_YÁ®5ˆSêVšŠ`sQoƒ®ÓÖ$õàGŠÝ~ÙóäØS6i©­&kÉ&ÁâÀáÁž 8ñ„¼àênò»¹½ímGÅŸç}í§mÇ&ñæO_<§m(:%ÝœëœÁcfðÏÆ š.×Ï ¸š”•Qý¤qÅæd(cM]âD¤‚¥¨vHK+IVÊòÉ l¨Ã§­”Qg Úb"N¾óÙ.%°¢¸,ì².Ž#/eVÈkƒNulаôÁŠì­ŒÖ­”G¥o%ÕGNâxpI.úÕ¯nn<ðH{ìñeíyû.àò‹Û%?¸¬]|ɯڡ?¿ÝzË=ì(¬â­¥kÚŒ™¼¥t,OB²““ê6ø2ºP,ààwƒŠÃ)ü¨…9Úê–9\qïâ'¸ø±v(—&ùLõs¿rc{é!;´ñ¶Å}ÙäÄwzÙÆrƒV^ˆ{uÅ(¥Î¼t ¤Y #ö½Ç94¶’ÜÉw#gt£ô)uÕ }¢¦ û,³Nßék9JÝ6Oópª7™V„~ ¾fJ]¦þ Ì~8HN_¸mHªýŒò€%H OÖζÍ ƒTÙ˜m§HagP«Öv¡`˜h˜U–fªÌ¾R&[aÂ6ëXT©·-Ïœ4<ÖHëQ ,D>ÄÁ`U¿£ÏøŒÂáQ¾pÔÓnèôë}\,‚çÍÛ!oÐ={&oºæÚø[âž•'ÛdÎ^mpòÆãƒãk¡¸D¥þ·œ¼å‡Ä¡JѼ¹?Jfleì«= è…¯ÃãóúBÏõ²Q¦ÏV¯\ÇÑüÝ9‹v%o¸^ÇdþXãù’vÚi_j¶Ú¶½ùOˆ¾åO-o´;/3š·ðƒ'ämÞŽ'œÈSÎX»#S“Ÿò™^)?é1)š±AxŒ-ÛÐÖ–æÌ%iò›]Rþ(Ÿ‹A÷2›(¶åhCçÑN„ÄUÄjÊ cX,d‰‰ÿ`‘’EÙ/‰9ê{3XZX¥QO*Ê.wB†ö²ýb7¼6ºdl#©?þoöÞüïªÎó=Fz©H§„^- "¬â\½t•µ®k«Îè£ãÌ<êÎ8ãØÅ†ÎX ¨«¢H”"M^RH ûz½?çûOÜ«»÷yÖrïÃ÷W¾§|úùœÏ9ßómÆûI—Rƒ°ŽÐuPP²‹ ¯Š7l£¹ Ø—že‰Šò[åùíÛÑÊl1 ‹è“$ˆ7èˆ,bw J@‰2ƒ'|º–7õåjàaQ}QH•å›¶3X˜¦¤âBTù‚#‹o¯^Èw5¹ånÚN¹lÓ~”!2—Nºt†…ØÅOnÀWÈ´a Oñd¾á<Í‚ˆXú}þáç u%6-ê9°{>÷Ii?õ„gâ'<ðn0Á§<ïiVH=ª‹R%¤|þ¿¾ ƒn£ŒNí7l›òí1YŠd¶­æ§uôªÓerY"XN#ëÈ&r‰Uæ³çöŽ)ôÒîâéžê5ɦí2Ñ„Çfy$¨ÿLª¢k*C®Lo뎚‚OXZó4™ÆK4Û'~¥)ŨdÙÞù%è*‡b¸1ªèm%’‡êúg´Ì@!Ku®@¢Àvj÷Ñ¢ÌTúF(5Rš2±èLV¨—¶å’(;”à‘Á>› z1(å:W:àÁOyê‰$%‚!ªIéY‹üÙ“ é 4FËn‘ f¾¬)ÁIäÀ›•¬Ê°Š‰èÊUëÚaÛ77 ÏŸ¿s^|õà&ŸýÊaEbÔ•“ú h™K»XŒ´3nv=J#ШSúLj$#œ052‘B.åÌ®ªZKÑÆ5A€¼6’×`‹Ò‰2Øæ\_š¦þêí&Н&^êÏ–:¥“f >BF-õô#=e—eN>Ü:: ÚÖ+~h<ÍÖál[a忳—1PYtÃMwrÖŽ<«üZž¼òyîÁX×öÙk~;ƒ³~v^ûá.h‡¾„§—Ü’=¸a—dŒ N‚:¶Ó|š%)äÀÈÊj™îýyÖ˧©ì>o:—z\оô•Ÿµ§³?/<û+^¢t~ûíU7·IS'†—ö‹ÒL[$¿ ÍR¸ì2З‡ÐØiK ŠTx?„—EÀÁƒcKì7’AÀ«ä蜼|”ÁJ„J?¦ ’8Ñbµ¸Ô9žZ°€‹ãÏ{‰l'\ Í™RåÌf‚öST©ö¶î•ÙABÀéÄP0›3ù+»¾ÊW8ùÕfAù®F©k»¥2PÒB7 øqPçåu"ª#‰ÜÐÏÞIltë¿ÒÇx%0pîºüOí& ØûR\Z6–³R£óüûw¿û•¼Çb:ï‰øl®¡ÍAþBÞî3é=[TWH7_È Ã°ªbÈÛ]öö5Û$0æKQ$‚üg Ÿà ¬ñ5<íGg~<7ÏßÉYŠÉS&·W¾ò…í{gü¤}ò“_l/}éñmÚÔ)íÚënj/~ñsxÊÖí»§_ØöÜkNûŹ×ST;ôиà´vÛíws‰Û ünsñ,eß.]´9[d;Á;÷žuß™äaKuÙȌ؉BÇúõ4µ†×Ö[fŸðàÛX–ØB± ïžº^ 3DuAø+_ ã¦/ðÑ_Ë/ôŠ¡ÿ@=*¾CM¼ yÒ?Å´ýÂH¹ÄCt°ÈibXk ¬Î¤˜½›¸Ò«‹ŠkÆuÁ•Õ-q›ôï[M«}ˆŒöçabí³„£t"H›­º`F†”–BqÄáÀ[¸e‚NÍpÆÅ ¾òá}oŠ .oÒYú¨«¹†Õr6û…Ç’V¡ÿ”Õ2è\³yF€4¶È)éØFA b§d0=x"JU™œXÆ¿,.«XЬ@ÑÁ{[ho[ı8´ìxÒ¡Ü’øœ:)[UÀT&é»Ñ®Œ &VI+I½€”:|£tlK2Ø'¨YÍ ÀCÞ½K(“7Ÿúb°%Kf·ï|ëì¶|Åý\[ûšÜ<÷–·œÂ5·õMïÐÊœ@ádâB¾ ŽÂ$0£r™ÇV™4$g¡6âß¶÷šiÔþꨯÅñ0ºú¨¯¬Ô½_ùvÂÊZ³Ær¤ÝÄ)OÛUiÚÌ¥`G>~×6k®ï<¨Úv«f‘(rcÛ\î¤m(°}££l‰&,ñ réÅæ6‹7OáB'¶)€ì%J¢È'‘~ÑûR*èä Ðÿ)J Qj:lbØï”²o$ëB­ZD¨‚$a½ °ã%o©‰bÚéUM9 uüØ´ÇÖW¤r!ÙŠ`¬¾Î@K›Ò@ؤB2ÄÂH˜jgà‘¯8¬òHÏú{×lÈ¥)ÓÆÓ¯ok]x9—ÑlÌqì—®{Éš'>ôüÄ»"ÿô(}¡ûSú‰Ý”OÉ_}Bg)—Jƒx–º®—k™ª­è¡;r&Ò>ᮇ83yíµw¶ù vnœ{u›8á{í%/}Üý9oõýr{ûÛ_ߎ9vS»à‚‹Û—¿òÝ6jléø‘¾¡Þ¥íŸÿéË<±h2ï蘞˴†›¦_²ON¹ueµL†¢2YÜý@üPèíŸJuïyWº€…J߇ªmœ6u3~©´¼Eµo‰”bàL¯¢e‘u®˜Ll‹‘ a#l@"2HòÌ ŒLÚÂŒ•¶\H"E‰³Ô…¿íàá¸QTÐUj2v†i'Ò±¨ú³ÙJ>À*k ß‘Vnb þi RP —tyõ 7Ù&ž3žx0ÛY!#`Œi“ÔÏÐ.µ`S¦º¥gK‹,è XqQZÑSB)†®r(UìUcöN¹eÖ*2Ù'–›ºÎÌ—Ž ö¯žpe?dî|ƒË_É NOŠª<Üd‰l%'ü”³s•©í'¨’T0öÄ7g %RÆ3Û`.ÉARÛ^$éíkÓÝশiˆêêƒñûDÒgü>ðàƒ\÷:–ël¹9Ïëú€qò¿aÃÉ¿«œ¦re*ÆÔÛ×ÇAßAgY[dE& ]­ÙŒ6Ñf5`KGg·¢{u¨<&ÊŠ€±øÕK+» wT%HÈ•ðy„1¯ó:ü‹l PΛv3O½“†šèÄüÔ—òTëÝl ¥&8:uq¢Æ™­Š©UI’SEvÈH8ù}K‘ •]zrÌž–I×ÍÕ)$iˆ'o½HªÃf'L«w)„ePR]ÊL‰“ƒ ê¢ ÿéÂ*ää-6ÑetfA!›U*i;zk/ vÒÉGzû*’}›‡xêÆŒYœÄ ô/Y-ÞÔ>ü‘ñÄš[Û%¼éÖgàGOÕ0ð9i—£êñÉdQiÒŽÉS9Ø+òQ§%5Âò‘’[p"ž>Eræ@Êö—R…‡éøuƒÜ©ˆW€Ú£ÚW`ÛŒUAá\Š«oI„tŸùÄÿ;µxN§—v'í ©K×âMzö¡J{;‘«ƒhëË&‘M½x {¥ož ÊX=÷¼ßpýÿ¸¶–§2}å+§¶»î¾‡§š,j¯yõ í¼_\ØN>ùǹ„cÅò»°ÍCÜT¼¹Mš0.g“Ò—J¹»#&¢ñÉA ,¹”ÇNáÑŽw¯¹¯½ûo>Ö&s™Ò>ÿº¶îþûÛéß=·Í™QOƒò&b7–m z«mÇ"€:ÉÏÁ™¼¢'ùûï[ßvÙe\[´ç|Þ¢<%~fõcÛ¿·vàÀrC»íÖUíÖ›îjÿè·yOÄx.™™®«æFq=Ä­úpù²uñéGg1ŒŸ‹Bºj-:ÎnÒ„ÏXÏ_ÞÍdõYqôàØ;ª­çÉCp†k§±cÛÎøÞË^q8+¾ˆ{hƵoý¼\ÿÿæ¿|%—ý‚G~©í³Ï’öŽwžÔ–.Ù¹~Ø’¼”lÅŠÕíþ Úh\€ÐußEPeHFFòµÐU2«ƒUæâ㊥|üÄV¦Q'@™¨A?uÐ5¾JÀ1#7k Û`Ç=mBÞ¢( ì0Þ 2elTAWL²W†ºÉÖØb+­è#EyÙ:‰;àf|PŠS­|„w(“øÚB’æá'7ã±ò:±tSÎäE3¯$ÐJR瘟ÆeoÒÇkÏT$v”•ÙЙõ“U<åÈØfIÁ ™”[|þ¤qIz –kàÉ+}&àì= pÌÎ%ŒÄ}yÅÿˆƒ5`[Êõ_‰Ë22ƒgã¦/¤JÆèïåÄÀ—Gçêù~1¬&L¼—šQÒTÖØ1º÷ƒ+xÖ„¿ð œBD©+öÒd+ކÐñ(HàÙ÷ÿmÛI½êʆ\<Žœâ"YŒ 5J cgÙ~Np|öðúLÜÓÄíƽ<½DßQ÷7nÊÏ íÚu÷vöÌ™¶è€8úd¢ï`º7?ÍØe—ÀÝÿúÔyð°}Ú ä²xÑà@IDATBÈèÒÛmø¤ýPo6 uHY_Ý8þ¬k {­(þ¬`Ó¢9ç°£¤žrÍ+”vÞÌ5·É¥’Ò:Üpž¤ŒŽ'LÒ‹ž.þæI»Rîìã™LÚaurêä|ÒU—DÚ]…޾JX2†žì,;ÊPè>xÒN†”)ãUä†';t&¯a(1YWàäp’¥Ú©ˆU ¥½!šŽG½ÖR7·±žR õËÄ›}º2z‡¾€vë¨Ø˜ð2 ñ"Š…Ë.ÁQ~ôâÀ6|À5¦tèÄʪ´ÆCmîd€zñ¯®cux ÷ÌfžÈ £ÜÊ*ÝŽ ÖkÓæ‡zñ‘·ršï{®b Ëæ´±dŠä¶;8…^ú6â“Véߥ¥¾hw}„é`8›éMŽÂÙ6¥3ûnɑˬ u=í^[¦OÁW,ôéLÀÊF‰;FðmúR *Ë÷«mƒ§‚âK¬óÍ`k:úF¥Ü èÓVä?·6ï2}BÊÎ<ó׬†ÞΪíŠöÍoŸ· ÿ¶vøž\ºñšvüñǶõë6¶… fgw7 ?°qcD+½ð¨ð ã² É´þãåÞŸ0a§Ñí†ß­ÊSTŽäÅJ>òœ_ÜÈ;xQûi}“oì„i4¥¦¢Lé^õ8“1sÖøvÐaKyqÚÎàhÃòßÿõ=|ÿd´”éOMïO¥çŸŠÎ#\:6¡í³laÛkÙnm¿Ð6±áʧMf•_M;Ú®ÐwøÚ_„Öî•Â*Ö˜­ˆ+¬õ%·†¶ú¸éV‰9`65–ËîX~/ø‡Ûü=fá¿›ÛêÕ÷ð‚¯CÛ;ßõÚvO%[¸tFûô‰çµóÎûU;á„çeü~û_Ž'ûLç%†ëÚqO‹Û{Þývçª{ÚŒé“kQäÄ"åRÖòåLÈZá1CIÒ€©ʼnø¢/^7VÒÚ ym•þ€oKÿœ¡îiÙÛgòë("ÈCc8WS{[ä.2*§B„¿D"fʲJN‘óœaÒúÏ”"ÕŽÜÈÁá€"²Ú6ÌyäéP®q“B¶ÄF*„3>X2–ä”o‘7ñ!H¢âKàK©S•lÛÀH*\øSײ™ØÉ9€eÆ*hko¶ÒWšæŒ©R–7eI¥´×U›„®`i«Ž+9úà?ÒÆ!W¼*®);Ÿîûåày:zÁ»"GÒB)› ÚJžfƒôp/ uù»TF,qbhÄë|£~§—Ë;3¬¢¶×çÒÛàùЉ²‹ûÒÃ4}½ƒå± RÊoÛ3Ào7›J¸z±tÉ¢¶`þüöósÎã©´'>îð¶`ÁüvêégpÍêÆvÈA²Ú´s;í{?hî¿o{êÑOfpóq|[Ú¯/½”ín¾×þãþ×Ùîe{ñˆ°{Û~òÓ¶ú®»YqãqgdlOºWœ±1*ÚhNrÕ@5¸ÛT¸vÊ¥ÀؤºœAÌ·å‘M-­8:ð,­Æ !W)þM÷|Q߀Æ*Æyó nJPG”ò­Ëœ¨Š¯\ôödä¾b”£« ™ß)[HB§×(«ªJK“J±#¡mëÄÒù S2y‘#´Ø;­®›CHÂÀQ\W!¤æÏ2­ 7 B Jvt]£JÚâC‚1â@-×Ö±e&¹´yY °®¬KÓ-dŤc—^ÐìD§–—ÖE,JL7ôT{ËEHxšpUÂ@¡Á•AD¾u W¢òÖ¯K¡ÔÏ` z¨ ä[¨¹üc7nã}ïý·ö„'8á|EûÆ·~ÐÎ>÷wíç…_®…·q´Ö Rä±]³ ä¶Nbá€RYò(#}Ο“õÚØG.ö|ã ú—°üröCPõ†l Õ®ÚÛrýÌË$dKÅú@ÐâÔ…>ÀƒZ®/¥i€ÈÀ/hÁsûy¬%¤cw ° zFJi+FdP5Ø“×Óò>”ê[ížÍ¯OÙi/óÚ9±Ù³1gœñ«¬ôO›>¾}ÿg2Ù¹«tÐâöêלÐ.øå%íä¯ý˜§MbadC›Âã}/ÁXØ\«§z¡³mÅ–~CÌÇËá/žÝ¾pÒéŒkG±÷!×~ôÃór?Àü|‘½)͉Üâêóš":dmóÈ?~ÂŽmÙþ à?–çÇoêöVsÛǶ=Êô§²Léæp\p˜¿`Ì|°Ý|Ã=<1g’Ý>›îiSÚmJÛÉÇ~Ú³õ£}zyV5m?{¼}6=’v/>Æ[ÏXŠ3ŠÀ » Ç_¶Š=à<êóŽ{ÚÑGОwü38ó8•—åÝÔ>ö±¯µ|àsí]ïz}ûèÇßÎËÈ>ÕîÛ°–ñ}:ͽŒ|¯o 9ˆyò“Éü¯ŸòCÎ"Œidz[xο=Km”Ï®†Vƒ?dŠ~ô}'gV8®*\ÅÒ%g¹Á9Å…«­²Ž>¥i< 0¢#ð“äáÑÇÃ˪á¸pÏDX貿|«\y|ìgòŠÒeQ¦Ä1(¬´6w+ËÛ¾5&˜@/ƒ¡…þ” àázýè%~ÆÕÂT 2Ê®lZµ&—d@ª¸%4{{û”ÎŒK>ÊS1NHó)Ý?‡8\ä¡*º ò§ˆ&l+vþÅ83Ø)…ª¹…)¿ËeA”íÈ"NÎÞ‡'í¥.’á/ñ[ß_þÃAEÚ¨,ŒY'‚í ’¼…wQ§le–¶ª’¾)~ðJNØÐÑè” n2[Ç\zŽ«âè«5ži»Ž¨ÐHÑÑ-€[Xñ'\ £}M@#Â×>óDQ´¡•%£Ä î6:{Ø^~ êA¾„dŸ½÷j³fîÒn_±ªí·ï¾íàƒjÓ¹!hÅ«Û!Ä5´cbˆãŸól¥w7¹}kk¯nÇóÔ¶dᮡݫ~è!LúÒN>å<al{æŸ=ƒ³2ù×Û‹Þqt¯34dÚY§ÐÑiW–Ói_Ò:˜w¶û© ”)aªó•Ãð(¤=Â4è׺“NåWzq¯p™¸Â ì°e"!¬3@œ-¯ÌžùàOÕV$Ý8áƒB'#µ… €ÑU¶| “•U|Ò:å–t­ª;µ2ó“g W9€…r¥8rDoŽF‘ƒGÉ)Ï.W ‹nz,éLÈlxêŸ"*K2Ÿ`M%¼¬MK¡'P¤@²ô»ðÀô[ó*uÖ¶\â=b[mì£ÜÔ³· ½£SRATŒð†Gx)!?âÜáà ­éäeÃ'àöL€AsÁ¢™^Ä‹¦N™Ø^÷ê§~#+ua'ŸVi(sqU¶0¶a ž–¡HKHÁ&hËŸe(Ïöä”­¢DBÛ7Ö“Zäƒûü¥ýrY'"m¡~ä…!ïÇñÇÁQ¨´ ð¼êÿDQ&óÂû•¯²å \?<#<õÚ‰è't“5ͯ÷CíZIT]ì¥þòâ©Z)²WÏp3Ÿ™Ø„K0¢îÎsÙà{´7ðžwÎUÜ(¹¦ýŒË"n½y9 {´÷¼ç Ü7ð Þð¼>b¬¹ëþèñð&ˆid‹Þ(‰D2æÌž‚·6•ëÃo䑜M}Üó|õýÚ©§]ÝÖßÿ@äs@ Qlf²è5´Ê¯y×ýÏž3…•Úq¹4CƒÚOê§ÊCz{Ùo2ý©lSºÙÔÞ„íY¦]w›ÁŸ|X/ ˜ÉmšÇÍÚ¶øh„ãÀx;÷ØÈú¤ãH’2üˆ¥?Ä£p¥À¥O“þeW±¢gIŽá sõj:À¢¿ø¹mù+Úw¿ûž’µ"7+_rþòöŒ§¿+ãþ{ßû—íì³ÿ…×ÍkßþöÚe—ÜÌ¥Bc¸'m\›È¬ß]»Ý¾²©Ènd(Ðß !«žb²”: !2°ŠÜ‰¤ëZmú :SqWÐj£(¯žÍÈŸÄmåLòOqaaŸKß ½p(å£ÚXÑ.9¥W’fÌR6x—¼²¥ž|îIGê)—•<¤g}H€GE°=k4«x>À [1‰§Õ)PÑ;2j'õŠP¤+v×D“2‹3ت tÀ ²ò–ɧb#e¸¡H–EÒ]»Ø"¯¸ü„Q§`XÂG´ÝRj:`¡bŒ•–%–Ë,–‚àSúl”‘Ë´ÐÈPÒ2i"ó#ì–¹N.‚p0°±Ds%t27èxâgõ^À·dW7S¶yéïíL.UXöÐ. “´º’r¶}d“"ŒZb9ÏÃOÄõ#Ï(,¤›Ï!dxËÍw8á,+ Óî¦âãX¹¿îúy‚Áº¶pÁ‚¤-¿ñƹv!7Æ­äF¡ íÇgþ´=÷YÏäÚÿÚ?øãœöœÔ®ºæºÜ°™¬É使ÿÆ›oi]ö›v÷´#;¤M›2%o?µ.öèªüG¸Û>´ ?›'ÎLÚ•M›$0ëHVRî–ÀîžöÕiê4 N¬ãH,]BÒl:YÁ·ã3*G’8¤øÛÀ‰îOV:¢Ž&|V"¬ã#‚“?{R ` ŽH©óÚ)+PÖ5¤@vAÒY; tþ¬8¤c ÒoZù%^›ò¸’ô.‹ÂxSäÁ#¯\±EÈ•,…  Džü¤£”µò/ŽÙ¢[ubnàÈl_&ëƒO©¯Eöµè+m;%8¹†3Ä$ÈÆÁVNÆtU¡ ti ùÉÛâeP ŒBYø(íX$²Ô-—j™94(‹¼ÐQ·:s@èÒ÷ruL?ùé3Ûk^ùä¶÷ÞKÚ…^M{(/˜r’úH(!hBÅö¯—(™Ó7mèÁ¶‚•h]JpÛ×%‘JÑÕ œªgCí˜ò”|R)v0õH´ƒˆ«NÆ56å¨ÿ ’œ¥VZ¶u/k¡ü ¨V`Š“L0À¹h¢_ê%#Å ¸Ô)UÂPš€ìSc¢=rZçá\£­m¶URÛÈ .ý œl`–-™Ö–v?/@:és?lSYñß•›/¼ðÒœØÿ€ùí5¯{qûõ%W´/ÿÛ÷3ßșөS&d±#7k¾NΑ‹—Ìl—_ö»öü48cÛÇ?ör®_Ñ®¸ò>&çR-NÝ¢¤ºAĶ×Ô½…÷HLÎÂMùºÀJþØöh[À39ã¹_Ä›×ܵ)qcTB<íX¾Vþ62Á¥éJÄüŸÕïŒËñlûxqÑ Ñô ý*Ê–Ï»ð ŸsåÛîp/¹=Ú-·ÜÖžóœwR=©ý¤í/<¦½í¯öi7Ýtk{õ«þ‘·ùþ'Zæ´Ï}öäö¡ŸÖ>ô¡×s5Àníc=¹í0f‡¶„'=ÌÙ®¼¼Ú™LuÞµë=*r•ï:™×ýÕÉ“ùûè!åå³EÏre®¸­üåïv`HaèáûÆP 4 °¦µ“ücµ*‹a(5gŒ ¼ä“»…©è2n•EÒ¦»P–w’üÕBb8tB¡ÓNÔª/l¨à¯·QÆ óÐsgu­‚ gYI.@(!`¥bŒh«Ïx†QÛ”¥Y,t°–„ I5;ë•FXìAd·LÞQ‹´›#eTtº€ ßäT:ÁWÆbP»ä¨>J+_ÞKÑ骺ò ~êcRò1@ˆ—iÏÄr™ïYÔA7‹J®¾WwgòÊu0¦F÷[j™6#¹Õ˜2ë#i ¶ö?å¢(æ³Jt´ !‰Ž_})é@#Bñ-•»h”é´¹Ä,Ë@<1uë%@°Ýü#¹ÕzW­ZÅõ¤³Úû.ã€x–õ ž`1³zð©¿äŠ«Ú+^öÞvy×+i çïN#?ÒN?ã‡4ïX±’ç´7ýùë¹þ×8¯l?øÑ™ j›XIp`48hÈígÓ2ÀÒp:ÖˆC(g}ãñHEÈ™­VTq±L:ªK ×fûd'”9z–< !vtyJAG3ï¥5;Ž&Àude§Hç£}t¬êˆ%hp»·%R¬ å«bK[%¬J¦rå‘ãÈ©Ë4¨u"Mi&wª~,3°…"%žåÔÒöÉÇû#1­ÁnGŠ:iøo¾¾Àô`Ý s ÌËž B䵃v'rL b:2E6d4láCz8P mÊk¤ä„È™b:9{i(gtÇn™\H‰"™b\W|äŸdï:/µç£ ©¯­h#r¶h i™Ošyê“·ß][ûÁ/n/{ÙÓò6Ù}èä\:7s–/Ùq,eàcIä‚Aµ¯œ*Ìi%Ÿá}bse1¡ÔÒ`éDÁO.ĶÞ¤FLýÀS›®7èÆïÉ猉†”²m²!$BØbµ:$W=²& Áã/“YXXãà•6v¾ì*¹L„QI]­ËäIþÅ:Í-ÿ¬¶z9ž8Âä*ýä&7SC›a/{tðKœÛwß]ñ-¬–nnŸúÌwÛ&x»íÊå]ΣEïm Ïm¯yÍ‹Úe—]ÕNùÆOÛXfsÂΠÝr†É³S›µã–¶‘ǾNåQ¥+VÜÃ;l/{ùóÚ!:ÐMX“y|!~–øctðIç%ÕÈ8dú Y>ÕMQŒ¿è‰;ú8™)SÆqiÙæÜËwðAK¸§åH®å_Êã;—·_rIÛsŸûgíÿz˃í¯ø$gøg³ø0·½ÿ}/o7Ýx[[±r57 Oͼ·@áá»güÜâCÿT4$Éå¡Êa=e>É®â,º Ûý˜ªl’²HX7ǸêßöGi²Qf?v…7´Bßc {™aØT&iQ9ø×ò5Õ†®†+Ê©7—–”mãf¬Êµ¨b»×ä¬ Ø~lí.±œy@ÞŠŽêàA¯ÆUiùªÝ·r³ÔOÁFq…`‹¨Ð^h5Ç"Å¡oÊXÙ)t>·…»v½q·O„ÔNì1dìQ䘊å„=ùh¿ªvqRé{¾ ¾%‹Û69;±:{¶ØJ^É#18zB7ã«Ôð­‘1(}¼V$ªËì±D$Ô¶*G6ªÒ~gœg¨ ¬6×ügS†\)ÞqÌ#ñγ`ý/^¶·µtb§ÐTš’Qóãû1‚¬ák¥óB¯ÛÔÊG}3Ny7Ú•û›næÔôa¼8g?žY¾*ù£Ž|bÛwÙ>¬"ÜÒ–ß½6+ùêà Ã6Ä=k×¶½–.ÉY„U«îlÿö•“Ûî»Íåž‚Å\´w{í«^Ѿðůä€Â3 Ûã}6Bu'š˜Þ‘&go—òý/ jÝ ªÅ{#ê0ñ$pHÚ±’7æ]-rË„QÚö6ƒ•½¢‚U‡ë·D–mœI7vâ6Ð >æ¤äò 3©¾ÊËyK"ý^°¿§ gÎ%FPŒÖQ;À«t†rëU-ö °ú9ðÊJåd§cU!ùT’íüå…|D®Ð–~ð•QîJ#‚)ù›)=”A>’ÉžäC—}k¼ÃAÝÈBFè”ͤ–ú‡ÂRù¥;+­’PF2$¥E™r:.–£P‡-•§ Å¨y…v­R) 6¹$h›†št‹føYDýº¶p†§åðbyË5rgè.æ¢VVecªMSàÞùB8('yjúlš)¶á/¹ÒAÚJž­Œ$¸j{Ïê$Ö*O§K€h[¤=5YQ@'k-€þ ,åkè-u™—@CÛŠ­{àæ^y\™÷Ÿ+¯úmNYÌõþ7sϯ/ÿ 72Mc€[Ô~}ÙåmÑn³¹¬çÆ\+xÈû¶s.¸ˆSäÚñÏ}v›Ë™ƒE‹æ·7¼öUíÂ__Þ^ûêWµ¯}ý›ÜS0«ÍÛun»›•²\ª°ÍÀõh꽕7ïÊÍÆ®”}Øl mX;ŒNë—ªÔÃ(çGëÇùÅÖYØÙ× ÒxNfq_¯G;U9{Ôj¼xÂwš0ðô_²«#uóJÁ&KèíÀä1ZYaã%:wV¾#°âu #°ðõ—O§Hàryp~ÔÝ•…Ñ [h mwîç´  ÑÇ}dK§¢\ÛXfáLÇ¢@Û¢÷¤ä´L±ôSí“0¥ ÖåÓõ!]Ö”é0!M©´RZ\öê]¶ú(JR?„Söš[ÛR¯ZáÌU@¥ fúH¨S •ð bQZåÏ]B#ˆ@<ô›ÉdðÊ+nlŸøÔ™<šrOVŸ—´Ë¯X‘‰¤7|fb,¾<¡"ùe ‰ÏDóäÕqø‰Þ1\@E&}õñÓ‰vù;‡ÛÚ. Ê M”ðú`æFãž„Ú1 ÿj‹àkq"{6!gX”[™ØÇ±=ßÀ¼2wXËå®!ä ¨õÑÙHgÀVºI£ÌÚn8%%(˜ì{&6„†ó×¼á™ën÷Þ{1o&7}nhïÿû/µo~óg<)ms[Éã½)tÑ¢ÙíŸ?ø–ö’—<½­áq ·Ü²ºÝqûš¬Þ{©ÈÎ$xÐðµ¯^Ê}vi/|á³Ú!‡,m矋ÇèÕßG¹ ÀŠA«ÿN$2¦à¤u9ÛcûGÕ‰öc= _"_1¢¼––K[ê›ö›`ÿ*muú?íþ Ó;3A6¸Ú_ýPNk'm?!Q“™ŠL9Èœ:u|ûÍonn§žúžtuC;…ÇÛ~þç0ù¿½ýýßýgÞQ0·yæy<šx|{ËÛ^BŒÕþå_¾Âb„¾;§mæàC&h+“{“|Š'Lí@ ü©ˆÞµXCyK;qUEaggUÝ2¦¥Næå ÿ[8àBbx”ýªÛ7ìûµª^v”F"°ŒÂÃ~®Ò5V‘ì˜2 ”¼ËPRE2’¥ƒtB 2“ˆéÈꄟöT^òi{“áTñ(Ubjt·NiJDÓêÅ[ác䡆raQG•Ô3›²ƒ¨Ýb;Ê‹No 2™—Æ.P¹ËVq>ÖHhÉ?éxy©ùÌxw8«3è@õV—¾•ÏÊFÜ’Ç*!†¼´ÝÚ•îtÔܳ¬ahtõß”Í_t.J6œ/èÒW³ šº>æÀM™(Ê8¡ðw~4Œ5‘ÃIÛ¿ÆílëX,·üζ‘?+… ­ü±xœ#^Q“ÌÔvôçµu>1àæÛîষÕmÞn»µ[o»½-ç)÷óÜjõ»òªkÚ^‹¶Ÿ}^ÛséRVö_ÙŽ~Ê“Úóæåæ§ó.ø%×´ŽoG>á í¿þݻۚ7¿±Íç¡ëo¸!»ðfÄ\ ›`»íg+alàr>:€mË'‘ °·0ûʋ哪E Ù å ¬ Wÿfæ: ©tL:¤$¿œúÒË@ê[uOJ8’æa;:rø‰Ã…άy¯¿ó¯©ƒ”ΞFó²”’Ê%7×XŒ“‡lìÒq”‹Š´QE·“Ê®d@.&"éðÈ?tåjñEšüËPNVÔ×@žjå F:Wà̲¥›+¤A¨:²¸@:Ø&¼l§Pê|ÀOа3“.Ê ¢ ñÑ#¸Êh}é¯H…é iV7éh͈ÜuÊuð½HºúKx Œö2X81[t¾‘Ö60IWšN'1p{­î|>gÎ6µÿúþWpߪöå/ÿ,×ñò¥ò“ö°a ƒ>0š(v£^˜2™:R¹¤Q0Â[®¼î%/>‹lß`…½•$øS9í4˜.1A½Z¹H:opëžP¶!íuÖ뛑Dzè >{m—AÑÊ(V~Têì[NÀF38ç5öðÍÓ>ØÇ§¨S-5”‡ŸÔÈ×-“o„u_–/ál+;ß8bçÏc^¾…g­olóžÏ·©SǶŠgs#æ]mOµÚu×Ûþ/dzºzCûΩgóæÞMá±ë¼iíéÇ-lçüüâÜ[°üÿòŠ£Û7-ÏKËÆEÜìí†Ly¡Ÿí‚Cª“ãJü+’>ö÷èZ ÷„讃wé«ñ¦ø®i§&åA¦Ëÿãiº$0åsꤧf28«B.IçÌLì^q;…XrïÚûYT¸½ýÅ›žÛþús¹„wm;üðƒx÷Èe,ô­lËö›ßn¾å^lãy™œ¸n™ÐJ·w>ûŠ’˜Ïd«rÄŠøáþ)!¿4€¤o§\êì›'^$®5nú±‹gÒŠ>š-—ÜŒÔY#€Æ~?d-‘ŠûTåœô,e‚WgJ‹Ožî–jjm`J¦@§,ãUy‹0L"2e´f©qÁ±ÃÁ=”E†[ ¤¯í2žAÄØœ± s…n êsÿXhv” ’†6÷îjs1NÁÈñsêjìuþ¡ô`£/"ÈÛ¼ŠŒü­+}3Þ¦GþþYû€¤F;±P†¢{ö%“]ìÇÞEºŒ;Ôe `´Ô´ñZ|ÒúJâ©:RC”–– púqt"´çS—'É!ŠLd :1]jÓ€—©²gŽ#_T!œXÄÇ~–±ÒrÒz™m¾ÚY¿ƒœÐâW²œU5ö2î¨h~@e°¸0ÅÞ®¶M\ƒ°;+õßÿo?f…à,®ùߨ–í¹¸òÍo粟§OC—Z ýä§OjG=ñq jsÛeW\ÑÎ>çyN¶=é_¿ØžôÄ'pýád^ªsAûÅ¿ÊÁ:Zݰ]©McVGH+êlY›!i› í<4¶ÍlÆGÓÚ…Ÿ›€ ’ètp™|FÞRßÎŽLùl#KöÁ2xJAgã±…–IÞ#2uþ™€âÀ:¶ôSÂÕ,L‡•ZwúH¬uéBIK¾èÇá©Á׋.D³ %Ý|éÎ\58*ž.PÉ”9³3 +ÞJã»\ƒ©ÉbØ“6º‰ÈùJÖ ÀðùêG ¤Ô0ÕÃrq†Uú4‚° ´Šgòꩉ‚ƒéNÅdòîPmÏØ.jÿÈ–´@Âñ³R_a›H‡Ê …¡HUÞÐC.q¤KZx­è•›—ŠläR“»V¯i÷ñ>§³/ää¥=9Ù sñm³¬ò$Z¦O*¯THK›”þdJ²Šb­€VšNccÝ/4l3ä3x(ÿ¥ ÿêDöèP¼ª?HS²Ñ º¹D‡Rw)¿FlƒØè ºäÕ¦8ä+±ÈÅ®ÓVÁU9º|#’ES2:¾<Ò¢þ ]!8;')E„ó ½ðÝI…®”rX¢=}O £òøô•öŸG™ïUy°ýó?|±í4ql›3gg&ýr‰×T^Ú5‰' =»ÝróííûgœËSÖÖrv\»•³ ¸àU=‰a”…Š%"j‡a2+lDʯ?IA¦lF= "¯…Ø•£y’‰'ág:B“H?³V”Ò%ÑBBa Mzy©•mS°iv%L)ŒÄ¤hŽvŒm‘ÍÅÛ9cƒæá7ˆ!ÿhY‹g89Á2Ðgd^Ð[ ¥°ä4i‰[ìNªÔüÆ:òÀ3õ%í$ÝèÏ^ˆ´|á 1LM¶q¢a|Bû„D•9øÀå F ÊbCm ¿èLal!½BÉxg O>… [GH ).c_|VŠòP­~ãäXË©å‹Ýµ9ˆžqBµpV7{Fê Kñº^Û”qù¸„’ÖVj¶‘‘šígS>'ú À&ðÜ~ r/ûTK¯ßßÌ E>ÉgܸÚ÷ôÓ¼ñw ×ÅΙ5#oÔ<>âî«ßøNVûÇó±Ù3gg3×´–·«-—¤¥Ò¾´Þ‡#‘+ô¢‘N­Àz 휥ý¸6õ‹¼Ÿ`N{鋎ÈBÌõ¿»ƒ•Üzˆ‚òÅß`jÌöWB#„‚låÿ}íáiû¸ÞíǨb‹ *m©ŽÕ}±·Õðt¬§XFÓ¥çŒYâ ±ÃJ}Ù½-œ>M˜l¼œ<„é¾áB‚/»èÂëÚθ¤]|ù]íoÿö™íãyûÆ)ÿ-÷¢<ûùKãjÒÝ…(6r§ÀÿÞOBÞ˜Þc‚|­#Ÿ0LÚhÙŒÁÂú¨T{‘ľ¢.ઈ E¥‘¢l9}Èdù»Ed(ȘêÙw:¸«óöq™Kw 7Ç&°“Ÿ‘Î7÷>M†ªâ!9óòbK¤r›bJiØaHcÕÄR7+HÐ&Gò”bÇœ-—iE+¥Ï€rÑéL¥3wºö„AxÆÍ=Åæ¬J'îÐQÄüŒ<@¼g)(… ÝG•w zòäúDåíGć—מ%âY\¶1oçr+ÛXW‡ TdÓ†Iƒ‡tžÊ›ÎD=õßOõ*3Žd>H½I®é°Â¥1)ÙŸ»"uWääŒù¨k§¶œ_ÊÙ™t+þØ Ø‡B˜"‡-= ‘– LíÿüE&ƒU1bN:<”ÐÄ ñeÄîòeS ÷˜Ée$;´¯~õtV§µÏ|éuí¬³.ä¥>wð<ø©9c0ÂV*%~ iÖÁ‹lÑŽnTT{n…¥šMÛu¸¤H£´ñO_'m~M «-äãÏJy«‚DOÍš‘Ër òµ=! Mƒg줣5´™d4ø––.¢PG¡õd›è¿x|”ÑI…ïí Mi•ŸœùméH2~0TZ€z…°¼¥Ê“ªbç~ŠÊ·sRUü:ÏZÕ«8êSV<ÈßÄ"É߾糼ˆlbåŒÆ‹Y»ñcÑÎÅ»Ûn¾³Íâ‰V®×}'¸vúòqóÞ$/i1‡»eo7³¿Ê» äŸ¼åêD©$T.Û—ÌK•7£…tíyÌËOðAþOêÍÛw©ÊTÚ©4²+?—g+Ÿ'ðÊÁ¼Œ)Éx$™"¤áçb#s Öø$'è² OÁʲâ¡¢ÄI­¢$-'ÓâuüX•Âtv`cf øL„C@ÞaG%[ô 1(…Y/×N0„ru–§¶UæÊ—jŽEëw)›ÖWñ³h°îà§)¡¾Êùïèɧ>!X~¢¬1´2Öu^]ÌØªl Ú½è„q®°…ÒÙ¥’3R©•‰ísóz«m·móå80èrž¨ÕëLw{…Ü€»-íí#­”:–A;ÙfJu“–«‘PÔ €Éå)6Xà4Á‘9ÒéâÞ$ÔH3«9xO|Ô½¸vRvþm©‡é’¡‚¼ÎU²Týà±C°ÊdG"‰Nд¸Õ%ëuÛžvÎu”ä ªéf€u£wu*hPž &²ÝAáÄ:S¯pNä“¢ó+BŠùË$›‘/ÎXHÙŽ^ ¡Ø˜Ž’AF*Ù´Áf2í$)“?Áÿ˜R»XÈP2 ;@™4Ò’~‘±ÚÕ &w$ ½ß£¹@îX¹ÑÒö¶(o¢5!Sl‰Ì¨”`äÊJµŽíNµvæ£ý”Kým¿Ø_Þ´7btÿÂ>Úú I—pE+â(7t|iX{hǶ|åξÏÙµqãǶ¥{îÖÖÝ·>´#¯¼å¥Èl8 ¤¤‡32ѽ$O+õ°¥ÝÝ, ŪUp)kHJ²º”j8¸×î”+«ûè ¼nUZvKð¬4RU$ê ØØ1ô±‡òûµÝPØù¥ˆ´Ô…¡<‡~¤µwÀ¶ÃôñØ å±­lw!ž=ÔºÑR­‘lOv9 Åþ‘IŒ8Ÿ¼ =\Ðß&­ä´ßhA,‘òîÜT)ãQ¼ùw7ذ¹ýý{>Ã}ã²ð⥓˖íÙŽþ¡mò”IíöÛWä’Œën\Õv;­M8>4†³Êµ°Ô•ø½ëÂÿQøÿYýEüßX±=Êôûê–Çm•S‹§á ñwóú•Nª”´ ý¥Ç”8§ÅIÈCÊnúp÷qV_¦¬ ;6ä^èÕ;I$]2ý“ëÛ›ÞögmÙ¾KÛ'>ñ\^¸;O°òŒ•,<«N^WO_¿ ´ôîaâìsõ‡:/]QDÐÙìöUŒ<\\T$uÒr–¾Åü"ˆHCšÂè8 £l Ÿ vä+Q•ÕI¼óÓß©ÐFÒòc?Ï4u²(\Pê <øUg«Œÿt>òË=cIòì˨ŽñÄq[%´‚ Ž‚Z"8t„6?eCþr¿  <ìÄû&os r†#N2œ (ž’«âSlCÞØåñ‹“ÝÈ/rØÎ %u¿ÒHB~pLLü£F˜h ž,: EÍèDEh¯Ø6DshžLGN±'%Ê‚àvy¥/IK9àY²Qý¤¨ìUªä¨ùÞ]›})íiQo·0$Ÿ˜/ï".Wˆ(Oµ?NLÚŠù/{ñ$. hH;¶Ïqµó†-¶ÿÿþïöÚ¾•¤MÒrì P¦ã;Ã^ééh©ðȾ7¼A!Ž=8†NèFyõ@ÌÀ¤G ¯?Ä9â&:iªr½Ÿ+=.Õ¦¯è…80‘K§£#—ÿÔ^vÈDÒÁ ü:ޏÂt˜Ð@„®+´‡ËdÒ©JP|xH‡d’¦]ÓÀ¦¢X"ŸX‰½[&¼ÒA%—ô×6•÷g&£Ø <ö >´‡zi*+¥J"3ÀÅZÊgü þH&ð?œƒ;g]h@ÔV¹™JÃȃO„¸Yaˆâ#èÏÌ>5•nlCƒ“yÅ« ÷æê`'eaÁ„6¤&+EÁP·*£¸ðÈ5—Uª!ÀÎe¥nݺûÛûÞr{ÙKŸÂ›dnŸüÄÉmí½ë¹¶w—Àz©ËBi/c²E‡Ð– Ä¥›Ö‚·:SÖyÕ`ÊQx Ðâ‚‘²MV`€©>c]Ôm¿Á‚dX&ÀÊ ³ÄZ²M¥“°…Ú6$_Ó¡GZbîÒ'õI˜8PX&]u©ÓÂ…S~¾MZžÒT¸tTqýQhyla‰i”KOâÂÆOd.Uê+V *iqKß!)ž“*R'æö¥Fà;ªÍbbÿ0y}òÿø…¼TÌK~æÌšÉ»îo«W­koÛ‹Ûòåw¶sϽ*ðB–èªG@åúŸoòL[!ÇÚ¢4í-bª§cÃí¢óŸJ®ÿžþŸŠn& Šœ†f§ì~ôMO[é¯Dàô"Á+ŽˆÁF¥)Œ=‹å6ø¦±)4ª4¶©¼Ân&GÐñ ¼×ö¿ñ-OÏæÆéÓ'´-SvÊä_?2^Kßn™x %eì~ZÂFˆÈ•“^ÄÔpÃÇ#+ÕéR]†ê´”™§Â>#dN¤+£:‹c:Æ€_/4o “FêeÍG*e'qR"û¨s3&eqH|€|rc‰ð’,bÉSN_Ò¿Œ%Z6Þ/¼ –‡¬0”%í¸Q0Ž'Fh8èyÙ¡û’UE‡ÒlêOu—]”@oñ‰9Ó}׊푟òI3ˆ!Ÿ1a bÜÈê9¼õµœq0Ð(àÇq˜ãÔøD-+‚‘âBG|˜)§´"?ø¡H¥¶‘ƒD!ÇÆØòŒM"¿tá#’R‘nÛ¡Ýs(Ö¶‘S9ÈØFúM5I”ÊÕ ÊªŒ Ì1“s‹aÒ­€ËSk˜’_`•Ÿv±Oþ­¶LæÊepTIM/gë¦>‚–´EhÄþòãSg$Šá‡K€Âh+¥ÇR²â|iüdÄQh·4]Ü¥7?n™ŽÎ¢Hߪ9³±.=p;˜ÀJZÇ ˆÔë Ã‘¥ÅB×ÀÀ‹½pTÊ/óÉt ÝÎòúy³‘È]ŠžEêÉAòIq–Þ’ÎâD¬sŠœÃ$¢÷Sʨ:Ôc˜•Ý@â]]Çnîí8Ê Oká#½t$ xíTm ©A?àraƒ‡rD> Ÿo: *¹ÇÐQéJMô‹NÉ:21S ¢R’‰‘­C&Ó™Œ–òˆ¾â²’ÍÚ¬ð¤žv/rX]´;¨Å#›6‹c7teé4H¥ÓN›YÊ’ïAÍãÒåùzØÞmîÜ™í´SÏb5¨µéœæw2iÄN¢l–üüØëÔ× P‚*¥emDàý¬’(_Áƺ•¤´<>A;Pƒz]chÚþ®ZÅÁÃFÒ²MùØhСÝjâo‰›õà#‹*é[Êeà÷ØNÌ*‚_U«®Vf²ª¢l©÷OF†0Bñ€ÚÁAÒuý¬ò+›ä-eë6N‰zôòàG#uµ”_˜ÂM[æSå]ÞÃRÃɴ铿éO¶|à LÜ&´‰<£}éÒ…<~yW.ýº„ö*zŽ)YYï°úã¡Ïû¤ŸQ¿—yF¦m0”ËB çSáÊÆÛü»%±{tãQz£êÒÃû˜´ú¨êQ<~õU.õÞ£à%«²M—Ò?ó)ªòrK«$ñ„É#3—dõ~¶¥¾|òqXü «š4xú$¾DÛÓ´ICÿ¨¥ +çäqçí÷òŒÿéížñÚ©?æ%`×µù fÔÛÄ£ æ AÌo:Y|Á¶°ŸYÄ]°™“>åVæ‘úE—G¾’ã¹è§è’¾(}Ëé;Ö×Áºù‚O°Ò å‰öËà)xþ™²°v9x°Œ¢¨‘“Ö"à…š¶(Z™Þ£É½êã¦ûäfÕ¯Þ‰O¿–¶¸ÒËÞeÈYï~qÁ¤ä †/wic\mí7‚ëf_Oë$VWYÊ™(ñ8;ä@¬ôVÛÕ‰ôáh›ù6ÉCÀ »™èÔíãN;dVÖ0‚r_m?EA"X¥r@¦:b»h©.Á¥°(&|C£øJ þ.Œ•b%þ’Á—•£ö B‘¿Ø‚DæèµÄõçæ~PÒ¤2Z.fÎd¡_îC‘—åüELöà ¯Ž9(¶À¾”¼ÝDÆ¥Ax¡ÄÕ1•Ùqm´6Ž0T ”<¶mG¨A°Z×FÓu‡£É”V«–t¹Ë­kâ¤3xÙH&[:›Ž+%ÝÅ£sÝ#õR§N§Êd ¯Š“Cpp<À£˜2ÿKÆìAÊJ”p¤í¨ë`’vÅBì´AôµƒwÝÈkíaF½”ÃÁÇÕƒ•(á c£PåO™Ù“ï'Ó©ì°&´U0N:-&¼‚(I­cyÐ ß„õšHÆþÊxå±^RH!M3Vš!ºÅO”LRðSæ1À®_ÿ@»ñæ»x,è©›7J'Oàq~+9q¤J \"@VâÛlƒ –¥[\,¸|ÑØ° °Õþf¥³ÕŽ‘Ïv dª"On®£0þ ‡ Vñ‹L—i¤¿Aº|Bû)JØÀR‚]¼ %¹£Dã`y½/;‹z.>eyh Úm­‰£»~ãÛ.|rÄœ¼$$ö2Í/¼Á©³fò‘™õaùZ¤ùƒ"¬<sTÿÕÍ+µHâQ΄ÔiÀ(&‰c¸qóA,üûÞ÷Y®ážÒ¦M݉˾6Ç/ê 3) ôß3Œ¾ýƒl;óHæóο€IþØè”Ë9‘íþõëÛSŽ:2on?§·MŸ65>T—¯¸@‡¨ëõ%­,ÿC}3ý$ޥ™| )†u&´jô–1ÀŸ)x#½þñ¢çß.¿òÊvÝ 7¶É“&Ö$YÄÑzé?ÒW%s§oy=Ð7~\îÇFîÊnbÑäÞèÇ'1Gézÿ£­&KÕ¢¥wTFÚ_›é úUM"/äñ9íS6Ô¿²ÂDüÈõ÷Öc .„~¤½L [½•4hy¤1ýß·®Zyo ßü–ÚW\Ó>ò¡¯q 0¥íÁäß›qÇnü¤ÑAÇê'‘"e[ g»…§>.¦òØO°§5Fi"ßB®¤ñwhj  I[ÈG{—.ò„;“=š8<Ô-ÀSÛÒÏÖø>y¿Õ6Ôj[±bc±‘h›2)Æô&ˆÂ0ÍXi“¬òêKð6tÙkƒÈ‡ÚÖ!_¤£°bTHt^ЃWÆ{Û›|3Çèt¢2¶›ÕÒ’¾Ùø/m ³OE¥iá<äËF&°á£ü0/B©®ÅÀF9©ÔØþŠ|Ò¶ÃÖ1­ôÊ"ã@,"):äæ´Uµ—V¬³€(—VOýäeÜKœ×ù´2¤>†P\í$5ö¨Y=S¡Í% ];-rÝ~¦´‡´” nÐ uÉr ‹·ÅÈÞ&~U{I©ÚZ’¡ž¦ÍÌÚ­`½oóvLR¨Ñ a¡Šë0ƒ“鱿íÆ¶Ïˆs¥­*Nè #~N9Ë1q&“:aö¦É¸‹ßéÓÜX´>å•/$•ަe¹Ä@;8•@vêüJЕ÷ÉòWP2-Z™T.c;I'Ä¢öî]q T¢tFÉPVÁ”ŒÔÝ…™; -è~¬ÍLÛŒ!%ÕV­+püË£‹T‚0EÔ>dA…(ö(öPWV*‡@’6 ®’žøö±êŒÂz>!‚$ R–bŠW2‘*œ‹iMê8å6‡Œ²HÏ€¦í2ñ‰JfE®à†6âK˜²œ MmXÒ&H9ÙÁ¯Äsh “ ‘W˜Rt%¢º.]:—gy_Íྡྷ½õ-ÏÍãý.bße—IÜX:6mÞH 1ËKÃ2ù´µmŠaµk1'ï— è›7Ýã[í¢lÔ¥]·…n*C¤š¬l†ô¬¼ÚN½,\ˆ]ÍÈ"¼Š¾ÿæ½Á¶Ú™‚D+§<¡Õ)RÖÉPn€§–²*:jå„+/" ß«ŒÿYµ !pËѵ˜ÙR”SŸ˜žfC'lhY¸)‡øl¶õŽcQÓ&뤥.þ°³\úŒeãx¬¨ô&NڱݷnCžvåd7_üÁ R xÅëýëF÷ñN—¥K–¤m¯¸úš<¬a'žî6eòäLú½¬aOÞê~íµ×åLÀ”‡'å `üøñySüLÆ×ACžS¹WÁÕs'w>ú´:~Ÿ»vEÞÈ#¥×q)ÓCœÁšYCšêJ ¾¸n ZÑ;)±Ô2™¤­«ÇÂRªŽàhù›rðŠ‘´Ó°h zŒÐ'ÄVÖ8^‘:ŠbdT&EQBõŠ òuéœÍmác]Ýd¯‚Æ^1Ê…ÐÆbi©‘:ˆ£Ø2=È>“¤<Ôƒ¡Þ¶lêÄ£*¢¥ ›ZQߢ!->¶Q*Ü£~6ZÁ;>@ß-©”§ì˜rà”QüÌH9 >»Ÿƒå$Qª7˜—]4Nùໂf~¯^vò€ý÷ogüà‡í\.1zÖqOkG~7:ßÇJþÎmßeËÚÇ?õévä>o{ì>¯]/Ü¿¼|ë´ï¶ÿxüóràÙˆyóvk‹sõ]wsYÓ4¦qVlIû×/µÝÍäþ¸cß¾¹‘z:õNú?sÒ¿¶e{ïÕvŸ·{{ûÄó¥—'¼ð´WµÃðó€ã[§¯=ûÏŽk?âð´ÃZH.þõ¥Õb}Àÿ˜=íE)mbû§)ÉØD‰‡–ë/À¥¿³·®"ɧթ«…„¸PþÒ÷¤kl„¸ãýU¼{ÍýmíÝÚÿòí®»îi'}á;´ù¤öø', Ý­«ÕÈ"ÝÉ{ÈÌÄY¥ MuSN%QÎ.o¬•Â(IUµ·BÉÁ¼D©ËÆ®¢)ý‡7íWŸ—‚¶]½–Ìàââ“ÍOŠn^qjGâØåá®{ô(±TÀˆP’Dfu€«—޼©^ZJQ dFÎxh1ßÞ,ÌP-%Û¯'„Ô^5ÆÈɶ TvÑÔôe«W7ÙÖËh±qÆ0+Ù¹•£7MUä¿ÇÓNK‚NV•¿ô<OäcylC™ï&ÑöµjïøRæ‚Bµ¥¨ÇÄ–~ù€êÔíHè/Û=Ùøˆü»ñœ å/Õ&1n'@z–œ2—<íl«”›¤.°‚«öÑ6’‰EÈû>ãpø—é°UÓQ'}ú 8Õàî¥j¶ìV@â” *Ô…zÆÕM§±¼ †ËéhÏj äÀäûmo6à¨ø¡jàr\Ë–¦1uB$íJóIµ5ˆW£Ç9h|ëÊ¡’*b H¡ƒ#toÒÝœ˜IW¦®Ô&ï”&›F‹DÙüÙÑÅÌdC|‰<*pSBœñ*|({¡§®(eNÎj•‰«Ø]ù§Ú•8±³â² 䙬¿€å%¶ò@O7RŽa-JàBoÑüÏ QB½¸|+UðP·_0”Ç\¡ÛÕ|6v’ÈY‚NBÒJ»Æèb\$¡ÎÁ¢øIC çHW›–¾Ö‹É)ÕÙ@ä“Ô9º‘.u¥z¦ŽÃ‰.kÚÊAXù£»¶€‡KYÜòÔAÄ&pqaîìií⋯i_ü׳ۂ»¶ùówhTÛ‰7 -0…†[J”^iÃÐïõÚÇ'Óº—cù òdÐ'¢‘°Ò„ošCPáù/íP«O!ÓÛÌX™Ó¯6¸z†©\Éöˆ-¾ù¬è[o޽öËjVJ„ñ£PôÉŒÆN{[žk®m°ËEý0¦ „ (â¹l^òÛ_àÆè"9E/€µ‰²÷¶VqdY÷ùöê7½¥]}ÍoÛÿøÜ„ì$y «æsfÏj'¼àx®!_Õ¾ùÓÚ®sf·g>ã8VƯoïý‡jo}çß°z~OÛkÏ=Ya¾•çÑ™”««ã›ú{íþT&ïO=úÉíª«¯n/Ý_´ƒ}÷¨ÜÒöÛwY.ÓyâžÐ~Á½Gydûo÷úüY3f´ß\uUà¾rò×9ø¸2+øk×­…Þdn„žÇKÓnnõî¿Ë[ç÷ØcØmioúëw¶O|šÇ©réÎ^>9³žY8ý{g´£þãËÛ»Þó÷™ü,Y´ ]zùíÖ[om_ø·/5Wú_r‹ÚÎ|ð#k¯zÓ[›g ?ôÐ8,\°{Ìi§}÷{ícŸúlb£xÏBü¡±|Ä6»ö¶¡-íŸþ3¬:•MŸ³T—O_´Á{iïï^Wœþ@M] Òýâ9˜fqñ>ÞþÄ#÷kÇwp»þú[ÚjÞ.ý‡6Õ"N.Ñ‚´}vÈä„«o$”·âg©‘aؼ”Ø^ô¡Øx£„ÕÏ"… V,ÙZW}*Ú‡_ù:ÑšêRì ^2¥§ ³w$ò…8]™œF¬þ(-#lb,¶‚ U‰j쇼੉œ¦"Mdí}OqÐð/|óvhAËy³~M^ZrŠ–àJ˜Ÿ¤2A“tÐÂSE~æ;*ÆÊ)(ŒN è0âf…×@Žr…^@ÀÕ°TäÉê$´t„g‹  !œí“”U %žÉ&á¾GŠ fÖð“–$M§)¡ÒY¤ª+Û‘.»‚’r(…—äØD¤ Í•r€Œ× `ðR¶ ʤ<°R—jßÒSå‘jÙ¼RÊøÚ€2‹^³Zì¥ “'o{Âüö“Ÿüª­Zuo{õ«Ÿ“}ðƒ§´=öØ™‰ÑĶ ‡ßúdbº‘Qžø”i>LE’Ö—”Œ|ôVjû r ‚¤½é'˜ò»71`}gÀˆzòõ,ì+Þ”+K&»ô…òuíoÁ4mhkG%cB¶¨+ƒõÑG³é(Š ¾è9p}Ü-“kÀS_vlÖ[àWMj+ kõ›ÒgPCϘ¯\… ½ZÉ”€í6B'6éýÒéÆÀªÿáíTgàºíä3íÑkþÐîîIZ¿áæå<+V,oÿòñÛÒE‹Úâ{´k®¹®-]¼¤Ífâíõù;³ÿÖ7ÿE»ò7WµŸø™\rsô“ŽâìÒÚöÁØöcõ|'î ¸öw×·лcùŠ\nã³äãÏéˆ[ryͱG?;7»rþ®·½9×ÑO25+þÞàéK&÷]¶O{ß?ü#—!ÝÚ>ÁÛ{žýÌgä€å6Þ°7g¼ çznþÝkÏ%È¿¢}ék_oóçíÊ*þn¼'áööõo~›Kyvá@enøÞ ¿;WßݾsÚé¹àÓ÷ÎPÌÍ“®¾æÚ¬èoälÅÙ¿¼¸½çoe|Þ±}ä'¶9sæ´Cö[Ö~wý m!÷,]8?÷ œõóŸ·¯Ÿöýöô§ÉXÁÕõôŸ?ds½¢ÊmUÛÍm(#ÙÛxhs=aˆ#- ÚHŒé¾*]]7n2ÅÉ0 ìh&Tòr¹¹sgáÖ[Úþé[dÍæ©ñÌ Ã.ù$ñWdt¼(?ˆï uöGøå/}Ë\Áë“鿳)Ç K&h€ÖY>,B]º°ò¶UY¨S’ƒíz)pö´­›õ–»,—†[õJì+ì,M}ߛޮðuž_TJŽ„ø„¶PÎʸ—޲¢™Dák)àz&Ø´}b•–H¬ïrtšÈÐp‰%ªX!ßò“üÇ>J>Hõ+ZÃÿÖh@Rb­ÈÌÎ’± y3±p¾³AžY”‚¾ÓutSÅÀ iJÄN©ŠM{Ò"}(¡J)†Ø*¶øýMn‘4vM“Œ ÇzËFtacèAˆúZÚ„½ÀäŠna•Yèøk„¦¾p=¼,#jøÁ±ÇåÜw@l«ýB—EJZ]+¥ ›-áÆò)Å >ºlÓÆÑȹÍK:¹é±ý¿·zƒyNžë­G—É€A%'«¿àÒyq ë²Ê…¸ÃŠ¢uºo9F︔”oèÐ&“:©TuK+ìty´ôG6`< ÑE]É\Ý{«£¤†É lþÕ”Ê5Á‰.ÖuÝ“`˜ÞWø°‹LåÐWGæ ¡H&™ÞÜ T:eþ•Kºlv#‚ʸi+’±M×7åéHÚC+`fõT:Àis'ıøáE™ÕR6 ™S·:-+-’–ÉGõ“Ïô¨O'¯,~©.`i†Á]:Á—V%‹ 0ÚÑBfCÇAAymG‰B#dõ“ÀA(½D7Ð fe¸J[Æù Ÿ!'*u¶?ÍløUá2x¸:Ì"ñÃÜP§ ·´iÓ&´Ë.»š7¶W¾ò¸Ü€{Ê)gñ|ù©mÒÄq4 ø>g\vÊT2ø¯T<É!SFýàJ•A$ë]YÑ̶µv±XPNl+ Gª1pòÑÆÈ MA“©ö”»tµ•<Ü2˳²ü“H½+¤Sû—6Ê&=ôÍð‡jÿ°É_úLl³Tv†Ê®œecdäR‡=e%é—ÒM_G¸á©â†›xŶ8©Zq­2íRèBðÃøÊcrÒáçmú‡ÏÍä=×ßxS®ÉwµÿN/aÂ7«þ^7¿háÂ<&ôŠ+Çf0^Ðîã]¯}¿™ssÀªþj`Å_uçmÍ=k™xÎΤ82 ÏfF3¹_¸`A[ϵ³fÍÊÆÞH»råÊ\cË+Ú·N=­ýä£Ú³Ÿõ,ty¸ýôgg1Ñ>#qŸH´rõ]íЃ Ù[ÿoöÞ<èºìªÏ;êþº¿þzž¤¤VkBh@hÀH&`1ccLŠ2WH\e\¡Êv9U®¤œÊŽÿH\qÅÀ ‰)TÄ!#4B2ÂQR«©çnµ¤þzÎó\~ÙeÝÊä­F^pþv¿N/ŽŸ|3'/{ ·îðço)ø=ÇÆ{>ð¡íÛ¿õ[¸UŽ/ ¿«¯ºª“ lÏï@|€›+øÄÃEþYÇÏâ7Î"f×mÏγé«ÓˆåÜæô¹b^ŽÒ7û<à ¦¹V.gÐd¥Ùùl¦œ[5hæ¢9Ñ?uá†Ç¼%7h:ƒ[9îùÔÉí½ï¹iûÿð¯q2uÆö?ñúí‚ ÏÙ^þ²§ð½/lí¿T­=(ÀoÑ®ZÊïÝxjRgã¿V®ª2jãaŽÉÎe±ó™Ž]7û:ÙðÕÄFyŽ ¶+aÿzÙ?1"íØVŸÈŒÆAzO“!Æýå·€IhØø0ˆãåæ^‰ŠÖ[¦æ¯7`G‰æí1&²M_J¦ïPeØšûl;”ÁÐNå«ßÒ7qê:>®[ŒÈtôàˆ×&›òÜ·ÃÖÑhTœñ¨ÍvhlÚ èuÞS|ÒWÇ2¤ÒaÜdà¥Å{¸’3¦õ8¹-—Œ‘E îéÃC ƒö5,áˆ[>¡L{}NR Û1®`œyŒç.? ñ\)Á®háåöÿ©b`<ÄG¤~ìs¬3êúÍÎ5Áœ0Mô^tdù@ƒÆ¿Ým‘\c|Ùè`šgµ/†’Á–£ûûH”퇄1L²3™Mü›M#|QÃÛ©­IDþ‘‰[Ýf{¼4˜ŒÈø×Ø’—6“7¿ÅM6\s*:•Û'ò^ÿøó$eÔŒ\ Ó£‡š¹ Ø¡VµFÛs´Yµ{Bøé‚°QIŒØDÖg csEÞ\Ä‚³)§ÿì¥øJÎ΢ÑéKîwm¬®Ÿs0UH9O<ÚŒ/q¦¯ˆm8?:¦MR²fíÁ2T?:/&L2c•Ù¸°Ë›¨»šÀQl7ÛGL+E’¿iùRIX&øþ,ôÜôåœsÏÚ®½ö¦í=﹡«¨~Ù‹Ÿ¾=íéWð…ʰ‘?U6“DEÅ\OìuëeÚãl€ïd¬Y¾Z´è›lí,ÌËLž@ìý‘d7}ZC¢vAŒ¿bñW¶ÆfdxÂ×⥶Ésí…¡õü5ÕØ6à5ÎÌÐæ–­» {ó’Xé¿>ÀW*SJƒþ¦®ü‡‡—ˆÉŒujûÒƒnØÅQÝaƒE™¼TD¸zWW„ŽŽ!ô‘õÏóòé;êyâ¯d¼=´}ðÃ7r…ü¶íÖ;îâ˶ÏÞ>rÃGú²­O¼ù‘ýñíû?~¤“…<ÿyÛ57ÜØI‚÷Ëßð±›·³¿›õ—r/¿÷îßͿÀ9CKý1ºcÜt!};_ÒýëßñW·ò?ÿ³pÿï¼/ç·KŸüüü@ÕuòªW~%yy' ÞZô¡ÝÊÉÀe}ràÉÊ%|a÷N:NòEhù…`ëÞÃï•y¿À{+~]uåÛW½ê•Û;~÷w·ÿþüŸ¶¯}õ÷l×sÛÐGù´àSà_ȉÐu×]Ç'Ç{ª_¾é–Û8!š˜<—O%®»þz«Ëo2 èùÒ²Oò;Ÿ/εyl'_,»™Í™»àM/Ò›õ³{svxõ˼P®2õ¹å)h~Z¡¯·Üz×öâ=kûøÆí=ÿáC<¹é:¾}¢Ûjü¾!LR¡Â º¹)EeØ£rsÖ?Ú{nAóD4þl± C²Úã0ÏKbæ©äÅã¥êŽ!Ê4:ÍcÇŸOÚêi[ÅJ[26G â1ue7ûp­Ol'^Gv«ÉFu!ŸÇè]p¦rpÑwêüæü™ÍÊŸí˜ã¡2Z£ÞÞ-ÀËþ1ûHÿšK&H"?ù’1áØJ¼|`AŸ"ªž¿ $—øoSr6ZÃņþóbdØôqœqº ¥Y~l?K…ÂòÁ¡5IAÔw¼ÛÝL1 ™¨/;­-º “k ÎzÉ~hÝ_ò´Æ£ ¼QÚ×4Ãd£4* 넆E¦Â~ŸJÈk`ØêO«b¤olW¡ŠÁ?¯jUF— ÒØF SÒ’ÐÖy h‡oŸÉdóÈy|‹Ç¨=Ž«Á'!ÔŸ|Óʬy ˆSùN-?†ØŸÛæÏå£ô6‚öð±íÌÓÎá*ÏÙ=ÅB² 8I1åÕý­›¨%‰nÇ ±ÄT°³Î}â3­اZËbzÂ!ÎL>³X+´,óæÞýrpu²™sþŒõÎ.ÕùÁEŒØ.Ùò!^³ÌVÐUhž:¨ÁòËÅ—xÅö ‹ ä&ñÁ”ÖB9qOsRF‘ÃÓ³ê•÷€z°Y–ÓäΤdºÙ›Ú³ªæ {®ê¬8ætä(²9 ñ'Ÿ.8žŒ¹²á©W|‡ªìú4úäiqÇ[c¹¢…?±ÌÑc@ÛÓN= uCÛu\ê~0HB>Û‡ žD«‹g4Â.°Í{öëšU§–³–Uþ´ä·¸B Y:ZÁs¹o\a%ß½Ââ©Ó}øÀÙ<ëü¼§œ³ýÂ/þÆv×'îÝ~èï·s|b{Û[?°]ýäK·ãÇùr°·5b\ì´27y3DâùÏ‘h&;*EÉ6JT¡ázH hqôÅ›.<´uIC 7"|Õ³ËÑ%9`‰Ük:Ì6§:bìn%»«q9#)«.”iç¥D¾ %œ•}~Ùnr¿Xä‹CªFŠôÙÓ9Ñ“wò‚ö·!¤O`tUh÷K1Úõu2Ij‹Pâàî|ÆGýÊ÷•50¸…¦‚Ÿcs¡åâý îë¿ô’K¹æ‘í¯|Õ_è9ðÿÃ?ú!> :gûé×þ\_˜½ÅñÜê#Üû?ðí•_ù•Ûÿþã?ÅJoß^öÒ—nïà °çÀÿÝÜ3ÿdÍùëo|ãvnWÖç¢Ö˜í÷´íNp\XÿÅ¿øªíûà¿í$âÕßð_ßÇm¯ý…_Úþ›¿óý\ÝaWâ/ösÏ9w»þúoϼúI}r·Ë„¤_Ï'g±÷‘ž÷Š=·ôhïq~¸ìª'=‰Çâ¾3Û|té\síö¬§?}ûŽoýæíkþÒWoozÓonÏâ¶§óø.A£e|ÜÆ¢ÿ…/xÁöu_ùržjôàöèïeÿOüÔk¶ïúkßѧ×`×S¯~2Wÿ}’ßôóçwyVoìI^ߨ¿æ6{ê„ ІӜÜèß.:¬6ù­Oÿ"«q}˜ÓùÜæwÖñ3¶³Ï9±]réEÛÏÿÂ[8:Á ÀÙ]PlÎ7Ÿmkž1ufdæAú²ðX]òô‰Ò²Õï‰naìq!^gIMû£Ú¢_ÒÁÈgöê SØÇÌJ°z0¼Iš¼dDEÆe­m^¤hþŒÏ·‘˜•1öP_mîØ¡Rùœ—(Ou<È>!âž]zЖ¼kü‹¡Í~rb¿9 åÉ#ã'0 Úo{þ‰ƒÜ ¡_ykÙ-¶|º(B¿Êí_4ájNÅ)ó`Ä7ýµ`ƒOIæä´‘·ö…Ó#Ê!”„h HöY¶Ï½3¶ɓ±Žš€Üò¿µEHFŽ—ùP?„kŒ&Tmžþbï'¢ã ,þ¡·~—iaPX:á7¥ÕYöˆ ärÓÿÓ` _qèü™£ó½‘‘nÉ\RnNXíKÆ}>~LÿÙ÷°·M>š˜ˆ¥ã&ù«®ÓŸ÷ü—ÿ“œ Ps5ùX‹ž˜B@ºõ¡@ÃÀ×ß}ÊÃ#~ þðBÛ¡L{2¶KGi“Ì”wÚÎÿ(œøc†õã°ÿ ÝòfÏ£m8à0v{‰Ë²U¿öògßïí³?òëûŸ †¼KζƒþÆÞdÎfÚM±…±Û],r¹û”~à„ìØ9Ûq>âõKlûÂÆŽ+!€«sÙí[géf½ml})¨Òdš@2`5x4Èë®ro‰š(M–ìíþ(Y}ó­ŸÜ¾þë_ÊÝž¸½åÍïàÊÜÍ|t}ƒqðˆÎü!sHL02%=¥±qڈȄÚÇŽ,¢.Ú”÷í»ïÙ64qˆeí¹äôsíjbC§²vÑ™v9ãS˜íËÆ%¨ñâo&CÁdœØ8ªµUìñYä7nj’ÏRËvÅt0”´Í7KàÏë”dzºt\›k²>ÚÄäA‰Ÿ0ÞÕ“ …µE¥Éa¿{uÙ(™×Lðƒ_‚½%'ÿ¼MìÕ, 8K°þò ÇôÍ\U™òÞì¢áâ ÏåK›çþîë¶ïýÞWo—>þ‚í—^ÿ®íÂ‹ÎæD€g¹§PD|ÐV±}7.öªút`éjQ¡–ª’²Z)/ê¹E³MLäô!¬Ñ1£µk>„$Øèhâ9|aúßú¤5!cÝúÒŽ¤ÅFe—¼UÉ;¿ý-Ó¨Aç:8wkD>¶%#/•Cþ©_Ú€Ð0~»pC][ªd‘p°}ÅXW›6 _½EÿÐE|àû¸ßï–¹ Ô€Ïþ*†èò~÷+¸]æagé—|]Ø¿èKŸök¸þC×d{ÑbøC×x%ù¼®¬ûƒaŸà˾>ùæË^üBž+ÿåœ4^Õ•pyï¿qûWÎÝ>ÍcC}Lç§ù®,Î<ãÌíã7ßÜ…¿œ/Ô~ѳŸÙbÿGÿõOvÅÞû%â/ÿ²õôo zÍë~Ž'WàVŸçõØÍ[oþ8'°'øBð‡ûTá ,øßö[¿CŒOÏÆ³¹‚ÿ®ß{w1ðË¿—r;Ó{ßÿ~nã¹v{ßø _ñŠí‹Ÿóœ~gàSŸâ—„9yyÿ?¸} _B~ ú;n»•{/v<3ßüÄÃÛf~êßþ ·9ݹ=ýiOáSŠ›˜‡oLv~aû³Çº¾®ìžºt/·û>Âq†Oÿ%›Ž÷f¬•ËÆ`Í+—IªM$2æ?›9çã[½²ÿ{ï¾iûÆW¿|{Å+^¸½îg߸½ï}×qËÒ…œq‹1ãÜE™‰˜ä™tV÷äüä¹eç$ñmÛsKº$ä1 ]|Ø iÒìL¦¹ò{ðõi0¨ƒÒ›@üW·Àkì"àg³™*ÒŽÂð$DTÒÍ:ìúÔsjaN“£„H`ä9l¡PÛf\BÕ6vÖÛ²Wý¼è¼ñÇñ& â7‡Ùè¿¶VFF,ˆzê»±²ì/``ˆŒœ²8¯‚lò˜Cúgùt(ÿInã|òU—o¯|åK·òh×_ýÕßæ ýç•[ÆÒ-¯ÀÆrúP« eßÁ6µY»æ"ÜÄ@÷óI#™_Z¸€BLǪÓܧ¬*Ws¹aôGg8µÈ§_ê–`,¼h0€ƒŒ°mòY8t‹6ð*tP•ù¥åµNÖå©X³Þ"_hèØ>ûR6½+U×WãºËé£8+"6W·0ôÑf9oûßùƒY¢óQmá ‡¼ ˆSŸù5>ÝaƒEY·q‘À5sÌÕÖÎzlÃqÑp‡PÉFF5ø¥Á±Dc#>l0ˆ'ÐÛ:`d„Î⾺’Aš€ÙQÎ-–…¬ ž¹Úæ5ÄBÖ¾À)«±D%›SæBK{lB–}g ê²–Þ‰˜Bu–LÙs¯]Œ”ðÂà«ÓÐfÿè±Ý+êýRêiÞßL¹ìtN]Pg˜¾íöƒ³Çq>NZ1CEỞDï\]W52ÚÈ«~ÓþÊuí«N; ÜÄU·›·þÏþ.“ûK¶úOÿÅöö·¾g{üetðë E(èìXÕüuU²ä¯6ú ÏíH&=ÜËã¨^9uR?9bXKÿ>Hó[úyU(fÙÄ&Lí/Ís‰€åe7¸Æf¦xãf­ItoW\ Ò€®0ðkøõêlÌåK:ÀÇÚÆÉaÒ”ÂÂhNö`ðÈ¥“ÛBW‡¤‰Ø1}l_‰?:ha3ZÊCëß7ûíºí€¢ŠÆFxT‚E}\ÆêcW°Â”8u4~õ»xË›ºôî'rjƒêœâ²'’<ý¦"f ´ìÒþï»ïApp yAŸœàÓ_~ç;ÿ€ûºÏ-ÖÊ ǃ Ë1qV?›–½ÅÉfìñÄO•mµéçô´™`§Ù÷ €þÛ¿ªÛ‡ú©í2Ë0“;èkñB#Kà¥[£ä¤‡“¥È‘VÁ#z h”Ù¤>1ª‰=Õ‘Ï ºFëd›“|pÁ«h/èö£ëÚêgH‹ ”—.jµ»ß·±PŠq˜8*j ä‹Þ÷~úžíÜ‹Oçâ'môÑ©²;ÆgÛûKÀsžþÔ'wÏø¹Bîóûý1-ËÁòGÁŒŒ·}ðºë·oúú¯Ýžxå•ÛýäOoÏàK±÷òiÂwÿ¿Nž=¼½õí¿Å'_Þs©Ïqá­”>±çMo~ëv¿àÅ•§pŸýÉûNrEþºîÕw!îï œÇUý'í—v¯åKÀ~‘טÁml>þó.îí÷v#Otü/0ì,ýǸÿ]ûü‘/TÌ“ óÉ“¿0lœ<éy2·að;Þâä÷'´Å9ég˜Ôy÷í÷±¨áÄšøFü™C§Ã ¼ü·Ñœ¬=:$Ž Ðüî 7ܾ}ÕW¿`{úÓŸ¼ýöoÿúþLnºƒ~Ü™ $šˆÙ4¹¬õë¾ìe‡1”nSW;-Õ¯!Òø ]ܳIN6­BÒW7}“XÝ oÈØ$ƇפOz’Z¾rØ"&Ö¸œ«ñꆞ¿ÊÈ’‚ìQYsŒ¢6ñ:uÜÝ£®N7âë&¿F±õIhuÞ{Žmj§”ñh§Ød[1P¿ÅfsöÚ6:?NHð²ÇSí76ÔDX;mã‰ÇO‡lµüéÍ¿sûНxþöþñßÝÞö¶wlÿÝßÿ_·g?çÊNX›àS›ó°-à‹3âlV Œ#ã|æúR]ó§°¶7ï`‡–Úâ~ŠÍßÄß’üÓ,1È€úÒydÅ%ò—\?x1‰Á35™Á†(¶ì°ä“Œr{ˆI§<ö׎€´Éß%c߯ìNZl)Š®¼\±0’ðcÝQ¹c‚Ê•áÏãGKÈed±s­t?–@ E<“W_åóÝùGiøß‡Zd)Å"N‡:[Ñ Tƒ·ûG½El:tŸM¾Úy#@% éA5=¢ìWÙìJŸY>@ée‡¡J‰v¯ (ǦÃÁ¯vBRÙ·t§®élý™„VNãFS•8- Z [ÙêÆtJÊÔú‰Aïú&Rv¨×t²·JxÊDcEÔí^¼‚š2ŒÎ°ÓvÙ)²‚ªÏ ©„Þþ)ú"KñRN±±»…Ù£îdjmúÁ'=‚ ºÛôÓCåL¶Ê~%,H~1T»µÊ–¦žlÕƒé$«N"Ædü@}èˆe0eñ2‹„IlaïïíÍÏ=Ë͘à÷À¢±JÞ¢5äq÷ûc¥ö€»€ó=›¤Õçy?yÓƒ˨ɘúýÆ|ÝøGÃ×=í´‰`Ÿ˜áþ=LC:÷ ZCÉ=i"sú7ëã]*êMsë‡tÔæ?»Æxxø¹=ú“' 褠dI|ÓsØ€C•1Cì °,v}OEôšŒá†vO{cÕŠÜðÌ¢}då;Á-?çñ+ÁoøµßîDéû¾ï¿ÜnøÈMÐNl'øá°û9AØç#'yÑ@ ³¾³¸â(Ñj_ö•QõÀ~Ü“^sU“ý2_·™…,4û¯Oéòii Kd£b^Âè'lΣÊÙߎíú‚0ÚWnÀßý´°”÷Ù%<…±ZÔd³wúÈõ™gm ·ùR8?²YÏÕ‰¼^h^P…ž”¯TZrì¶ÁœOÈzò  /œóÁjo^Z1±}NºÐSìÈ8&Kóæ`«6|ŽM{ü¥]·¹§]Ù ©ûe]ÀǹEÕ/½† ¯·Ô\ÎwÛ^ñ²—vÅüÖ[¹'Ÿ§éx‹Ð|<Û¼“OŸÉÉÁõ\å—G^m½ÇãΚŘ6–ÇÞêŒáÜcâ^»œzÏÈ#4«æµ¾” `ÄÍ .8o{é+O^)¼î¼ó|ÂðÀv÷ü÷ŸPIšÙ§TÊh!ëXnŸºÑ²Ïyš«®6åe^@Z‘¬H1E¡ÕœSÐúŒ#sÓ‹.ó` *ÿê[¹ìì[B}ÓG3#ʵÏoºc(ª[!Çv¬m;µGU$oú(ÌL AûŒ…æ`¬¹PȨ.Ž¢'@ Ñ=^‹é­¦ê”ÏÖæcpk8@q&ÏÈ«fhCOy>%Ð Èæ ¸ä’Çðâ`îjKÇeóòÐâútÏkeà —²Ç;¾9Êdù‘ˆb§‘9­`-½ÓJF‰²•'TÉ2ù~AÁ¡9ç‡ßº1|nÂLIfëÓª¼tùÃâ–™c$€É$¶N'È76u– o†½yÚ0 †º¥ÛhÊs«ÌüQ?9=G8c㺺ôÅ&?Žšˆ‰3 Óí´ëØ’Û†aŠKJL'­w¡dÜgâh¹¯Q0F¾€zÖ†i?<Ó_†4“ÎÁ\PŸ‰$j‹ý%ܽtØ“¨º(ïàÏô…¨Znï<¨Ïÿâ‚D#œUÖrÛqœ€ÕÏb«A¢˜ý ãL‚”TŒ!ö½[“5±™ª>™ú#ßá£>sL Ø Ýâê±¶ØRÞ@«Ëw|+I)cލPÞÁh›ÀÄÙæ}B0®Žsß`96Ú5eÆ?í•¢˜üu| œ ‹¼-(Ãg³f;L™ ÀÚŒÀ r?ÐÉH*/n®w|<× [¤ÁlÜüÃpãå«þaqTãη*õ–”°Wà —ÿbñ§Œ6ˆcy?éTΉ­³—ýKÀܼÿž·~ùÅ`<¼ý›Ÿz}†ûÛßÿmÜNð¡íGø Ûó¿ôJNŽs«‰OPLð*/Ã!é«~j…ý¢=MšÒú×^}Wÿ`ì÷Ø35@ßCMSCÁÑ9¾+â5“óô+¢mŽmqÌç${¡…"Ž…æT9åq üÖÝÖ.CdóǘEgçbVÔW?†1ýoJàál#b0¥SdÍ~ûÔÃïaþaäôÍã>u‰ËÞÜØë =Ö¶/”Ïñª4ÛäÂý”ìŸ ?@yeü‚óÏí™{îù‘íË^ôn;‹yßökoøwßÁúÿçMomÁ¬UÚéŽÏåªü•Üן—ìt}èœ|=n¾(ì•~t¨ú¡î¯×®z³÷¡^õ–œ±Ì–±™]›§c{›#Ù<ÃÞXvÝ“…?3ÌSñxГxù„Á͘ø8]«Qÿ8oZ4VµØÖ9 oo„ÈsHæ¸ëYÍñ$2ÓüçÅåCý¿îúÛ¶¿üuWm/Å‹¶ó“¿´ýÆo¼{{ÊS/Lj¡Y "üúÎÛ¨¡¬:&¯^~Z íÐ9&X1ïz4³¼b01Êâ[ó-°³¯3’Ÿ“ó“±â9¶\ØÐq)êÔ„fSQ¦ûø™÷ Ÿí‚d— ×¶Ö¤Õ'fÍ“õ9ö)ËÛ!öª¤.^ö}‰ÿ0/]?î[z³¼ö›ôñÙ0¤2¥kÉ«‡¢Ø~?`d#d³m.Â=Ž”#rbÔÑšK⟎äŽb¶Œ5Òi6ÀÒá•}ïc½óÏO¸mé˜>¥Ê¶MáÆsqwÔ 6-môàž@aŒ7Ù!W¯3 `™-Í9fùíD0¢ÈŒvy$Þ8ì B“-m¶Ïbg´™È$å¥ ~mÒ¶Ý)‰ÚkÇ)=“ \ð¨/÷–9š¢N7;4Ð%·_™2Á€¬­Åº>óŸm‚±¹n‘IuA×ñ.æÜ:¢M™ñvìŒeúS<f›äWPýÊYOfq`Lf+ÉásÀ§… èbp7Q_C~0—=Y…Ó¶ÛO?ⳃ>f¡˜gŒ%6ô²²oÐŒ›vx[Náªðye|ís³e,bP‘¡lh; cß÷ È0òù]-;J]l4–û}–ÊÏxœ£Œ:hþMÌĤ¤ž©Š4«~;_÷» Ÿ’‹²ÒÃo_i˜ `€co„“æH|´94Üꈚ䉌vï\9{E¸ò—Bñ[ã ”l…Ú¶úe4òN*Ìæ¨¾°‰tó}ú ;i›‘;V—²nmÇ›v›ËÈ\¦’ÚŸ¶@’§øžÙìû“n<5ŽÍ°Í¤9ÆCd³º@ê`ïBM¢ qÞööwmŸ¸ûSÛýÿº}äú›¶7þú¿ß®ºz®Øöq´X*¨_-°‰'ULÁP>þ }N~i¡Ÿôøtš4¬ã³0CCûgs‰=rÖlwÑÐØD·y¯& Ð:•єSÂI™>sîi^QH)ã‡øåê‘<ÐÄÖ·=-Ä*§Ÿü4i·Y-êϹ •“¦°5j_sMòÎU½Ø†žÉ· S5R&õîqpÔℎæéÌ­ãÉ£d>;†O¬óV™]{ýöfž™¯eÞõ“®àÖ›ãÙý%_ô¬G!YÑ?Øõ˜jÞFß>v?³m éG“ÿØ5N6>‡Ìÿÿ³ñ~ö˜|ÈÙ.*óì¶.N?OrÒTŠÇyÄcÐ,¾ìUR…yÞcꇯ»u{ñKž¹}ó7ÿ¥íÍoyÇöº×þr·F=õ)Oè„Æy¼åÕÿŽ-‹Í{ª¡ÝüÕ<[|&˜žMº­@S·oÙn±1Kqcž:ƒ¨C>Ê[Juk#4[€çȽàçv¯´z^òÒgñû'·Ûn½{;“û5·ƒ,ºÍý¢¡©a¼ÒŸÉɉµšºâÏ>ƒÀ誎È9ÖõsƨLâ šÓ“?’í}¶‰7mQ¦OH–ÀjÏɨEîŠL¢Êލ0–÷…J°Å|×hÚj>¬"ªØsÇp „@Ú„ÍÑÔç€k¤hs"§} Ÿø€Dû«oæ819Û˜ >ÙƒAF¹«eFÎùާùOu{€“˜çþï}åÉ£ |7Ÿœ³oã͸ZŸî ÿíKS:eTvùôÑä\Ù@4V—–ùFô(ûxÞó/8gûÚ¿ü’¾£s?hvï§îßn¹ùn_zN·“È×ÚÈ7ļ`Õ¼i.8ÄÙϨǎÆtÇé)›9Õ¨%W«ïÌ5óÚšDé5hšmC³PN:#Ló»‚Ðû´™¶ÉmùN¼Tþ‘sÜžÚR-nö,vs¡±V]O \üé×¢¨«9´ð4_FUŒ¼˜1§ThcNíòÎ\•¥™­†ŽÿÚCq`¯lsÓphÎK`ø#‡Zt„BBŸÊ¨—~ÝãEu>Ñ68íE³|OšßPSÊfk<åÇ&'Œè´dÇ(›wÊ-^Óµ#/ïv€Mó¯Ð •HÃ]£þç”{˜{õ ƒ³>™¸å—Lü‹ÝkOp&åíhÚ÷þoe\ ¯A_}°¾äc}ñ)ᙾ§Ê6^à• ´µ‰™´cÙÅ´8VZ 'öŒ¼OŒNö0R[öŽÎÜAî@Ù)S¿Ø iYª [›X &H쉧NƒsÅ?yʶÎ"?+Lo $q튒\4{ÓÞYX‹•âe_V #Qò@Òêy ÀâD¹ìµ®m\úk€*§ÍâÐiêómvÆŸÒÑ„ÏXSžm²6óiUnYNg·“µÛ|ú¥e‹E›AÝ1Æn]?DïdµìÊŠ1}8;>Ö¯/­e+|" Û«¡Ð@wNI cïRÎ@×]ÇdqZ>MÛê'sm2êÒLrc“ êRë¾ù3rú·Çòp¿%:Š˜ó ùe—_€ƒl?ö#¿¸]våEÛ÷|Ï·l¿ýö¿}ìïØNœ31èã~xÊÔ8Ö;Ѳë}FCŒõlpQ§ÿä¯,4¶lÝÜ'Fz'¾1šXеÀP¶ïŠ*ºŒ­5.À{¢Dœð ÂVÔdlÛ±¢‚³[3ºÍO%šÈѧ-Z¡îàŠ1”‹¨ôDœ¢1šEɵË( µÚOÒ&/Å¢"î²WîÝ·ÊÊ® PÂDFf^–åOm»Ïçî³j^!ø ¾l˜U@IDAT»%î;FûUœß³©ü G§ïMŒ÷»ˆšÄ¹Wç ÇÐý'Úξìøöe/yþö+¿ò›ÛOþä¶§=í <îó‚¾ÀmžßökœU¯CÐŒ23ÌMR:…úD[&ÿ¦ÝZ6´sÜ)kæR¦Ø1êƒ`µyñšÍ'éUKØÜ-š•ŽùˆÚáÔŽ° ä‹c½!Ø&Ý0)à±Á=›ãy†È0I'tõHÞ5ÆlX­ ´¡úh*ÕÇ\‹3.Å¡•—œ 5çìâé‚wÀ(`hǸåK8Š!Ç\ÖñêÄPáfüÞ©,>öé36êƒìœ¸Ú—ÓÖâÌBUèw[†[Æž¦ ‚œš+ECD_´Åø;çJÞoÝt>·‚§œkcM¶(£Ön_³,¤Lj˜¦7'†6úç¦=p;u¤<Ð#^ q)04å,ÏúT¤ªó>I½=ŽûOu˪±.`a‡u·ö¼aAöÕŸ+®Ú?±˜ã˜¼ôcÖÌJ¬½.m?é¯F>`È9 À€hðÖsJa©3Ö€ç2K‘%ÏîÄ zx NuÞóÛwhC¼4²qch÷¼A-¨;‡6ÁZ"˜óŽ>x•{¶Ñ3D‹& aå«d´•W ´i%¯žËŸhA³>gÓæBØEIŒ@Ù®@zÄð¼ÒgË:ÙNXmò:£Æ­ Ýʱ(wo2n]M„>5Zt ˆ@f½\x÷ûæ[ø ×âLùü7fŠIsÛíf¶ðsD*-öŽ$'Šƒ)Ý F𪡯„ƒZÂÑ4[Ó4}<ʇâØ0>Ößy¥WN  --„´U_Ù<o£å_+Ÿ>ˆ¤½Ä£¥ò~F^?V”Üô£äüƒ” ã'M-¨ÀAÃ}­+»\ÚN9~è5ùÜôYk²›úø®±Ã7…‡f{×4È›º.iêë$’}÷Z_´ý>ÁF—~¬œÓ‰üKÑàxâžovÂÚ²VVÞ$Ÿ›1ÌöüCü…¡Íðdå*4æ Cu›_~:7¶ÉÛ,²åwÁái•yî Ù‡OzÒ%ÔOÛ^ûÚ_%îlßû·¿ûÀöÚŸyëöì/º|;~gõéá¶Aê{?í&(;‘±]{èm-FæQ(°E»dDÀþÓÂjú+(6–C9”pñ+Ó=úÈ—ú¯û3¯8V]hí÷ΛÓ2¨fωý ʃX ¨ËmPÓ˜îÄŒV8FãØ’™ƒ½|é¢Ò1ɃIôyëØš1’iòm?.h·ºìËùõåÝV¬TÕÀ ¦ ¶Å:¶+)ÑiË7X{p‰À´9K:6õ£Ûp4!Å_€¸DÉ„b ˆ²S~¸´\V»b€Ö}e'µ%ã@©•ÊÏnÚWG©ÇÁê“NŠI˜ýçeRt`§£¶:˜ÄØi¦º'{PáÓFp-uÐ$­@ÉnóÊv¹÷¤ìŠñ(S¢ö†’º©Î`7ó'1Àl té}êî´1NËþUW÷ °¹JIÛ–¤¨…­-VÂì'Ñ1• »ã l¦¦(6[u©XB"Ëþa‘¦¤qµºâjHrÈ7£g2Å~W~¼U¯ì‹‘ò.CûŠíä›}®-£SÛK§Ô­ü²o§¼O¶Â,£Ø´ ŸšWΊlÿ=æÇš±EË&gØ¿qí*Ï@ÏòRÖ —süNVš8¼a¢¹XÈíêÄðKˆ$±¿ˆŽãŸ/aÈ¡—:xÍOÈ<"|Îíû‰Œ\_ØþlD`úÚÞf‘g^P*«ídú½[ÄÈ ú _²¿…qvéãÏÝþÖßúÆíCüðöž÷~G•Þ/£˜/ùº(ô‚˜8õ¹cWø¡Äwʈ°Í Òç)ï ™.Øì«®~# îòJ~”%«|ð¸"¥¿t«Į̀Þs~ÚàYla͸wdO7£B:Ø{Ä ÜöÌ—Û¹aÙ cÇ#ÛaUÜ=ÒÖvl/f3O Òª™IZ¯€Ù- 8ÒÅvYäŽÍÆgñ`ŽÎ!Þ²Q 2k“ºÝ[‚dð¬ +Úh£ì˜·­•s¦‡ÆXáVº ›m oÜ¥·UG Úµi@(G¡ã´kP·L½Ü˵So¾•Ì_FYnm¥ò8QN`ÇáäIUÇ{å&îúÑ¿ø[þ#`¬Â±¤é¼= ¨-€p¦ †¥Ð°T*¤I»/¬û?Ê5ĤFUW¾¶ÙXع&«’vÌFÆýB šrÚEV,‘Ã)ÙkQ”ŠíõT/ ˆW En—o©‘íÄ.Ø-˪˜­¬Û&‘-¿+ £ç~¹š-$ÂÊ4‹ÌZW—u6º€¢ÊDhMä<’ÆníB7tï,ºù` ŠƒˆiŠvÒhÄ5ŽûÉMÎI×–¥e£rû¦œI9XZ\[¼,ëÀÉ’:`ªoß´Ý-[)[Í3 Ëïö2êBX¼Ù. …ù£"Or±NÈ!(¢Ê1ŸzÊ´˜[ì$ºâ&°ÛÊ9û/õª¯ÆôÅë¨/öü›iiÐ#Ÿñ`hÉÞ{L© 4»Æ¤Üxx[K[ÈŵÁ¦ÁÔËý“‡±o&½"FÅtèd\¿ÂÀªˆ"²©ÇÀ©§H¯ÄÍîø‘=ÅSA{ ²“ÁdçJój&>0á7Ç;o :TžÍ>ÿ`¹Ø)±G,^xÂsŠí­oy7W<£ò9ìßôM¯àyë'·7½ñÝÛÅ—œ·{"„qæ!-›RWnIY6UÝwSeûfuý@²Á!’¢âÀ0m–§9g2n\‘®7óÝ*¯^Ž4ï(²EJ¨bLI8Z|5Ð÷+"Ú ½Om׎Ø7b®liå=ŒÛµ è#5ØúW¸Ñe~t[b4ç@¯–¡˜zzSn4y¹È\ÚeüÂög!Íu rOVž‘'-dJD3ááí~•ûü ÎÝ^öòçòlwöÃfþ>ÍÿýúßáKÖoçó£}~×bÏãÉm=œœš\›,îÎó×üã?^Ôø ž Ø'¬ŒQ'ƒ m¬…·ì$ñö–âˆpw|zÜ뢘'ÐÔi*ç$`ò[r º¨~ýϦ±ƒ*ds׈Ÿ¸ÃN>ÓJÅünáI»òù³&­>U›5×Pò8_ f¹x­ÖµA K¼ƒi ,Êt‘ËÂn#í…Mf¶b°ó£WýBÔ& ¯l´<‡4Û±¯f¸%Hª¶© DÚG`W„þÓ‹Ç`É×¢¨Ú_6iƒ±Tà°¥ðG‡:΢vâ;zG׬]É®ñ?+®"Oœ&àö×Lhº¡.ÞÚ—}­¾r êô“|ZÙnÅB]Ұþ2¹dG06Iž¿†í{·|A‰ºy36,‘Õ¤MKN0ø¾"d–&'/q–LìŠîþ ¿s:Ÿ¬àApæ 1Ï ÷çŠÖðQ2Ê!ä° ØZ€R®Àt^œ²îl° h?ØÂ礋 ¢ºzm©KkîlK·R¬l…t©ÛÉf‚ÁÉÉHˆ"“Ž|žîUvö>1ÐVd,…ßížO(v}Ë>¬!`%M–}¹µ×þÕÖíH-ôª\‰HÙ}»Ð×pÔ+ï½b„“ˆž†*o±+“È@J‡©"­ÉI$°éçòzüD·Ñ/&M«W½bg‰njËlÞdQ¿Ï=ÚÔ3µ‘Yo#ºþ±‰¥å±j(›ïæAI£¬Øn©‹ÒIј{$̇ì£]ý©Ùs¾ÓÖAL›÷¬ÑtËï©SFécÕ´ïý> `Ú(–âÀ?]§¬}ÏŸbÑ®ÍsU#iøcüµ)’8oÕ“ìâˆ_³³ -?ÖíÄÓì>î?.†&$A>!=¢±FaæŒ]ñ„¯  ãC^äñŠÇŽ¥}”µ ñèNŽd •· ~, /íÉSn±k®ÙM µØmjâÑ3v4UÕneô5›‹ßøúƒQ²\rñyÅ÷g_ûÆí £ø7ÿæ7l½á¦í…/~úv&?˜ôü(?vt¬ç¿/ t#É‚ ×Hà„¾Œ?¦†¿ûæ Lý‡­}Üža™ùÚ;ý†ÍòŠ‘•éŸHòÓ`UrïcO'Hb dfFã>\öWye›2ði(̺F1ŒŠÑÎtÛ÷3¸•ª¶Ã€W— ×f ²$fí¶mð⋞ìWm”ë³Í Jš·t@‡ÅÎH M&Rˆ8c¬’a7‹v ‰Ã®%& BfB]¹ó´›ÿ´Š³Ù ¬vàéê§.fzin‘Ý-î]¿6@îÙððk“W>K ¨3 Ü$|õ½•‘»þ‹¿IôÑ[b`£°µÆLIÚÞçÈÕŸI;\ĵ4•Åi³Ø¯Ç¢;ê÷³óÚNSòêUN« ášÔ¨•Üâa‡}j˜Üïý£Ïv\^Ö>x„å­œX4wÒÓëðY°je Ç–šbÛvürª:ë)´Ëm³M†½ûÔƒZgï{ ó¶³¬•7XÚàWVJ ¿åóÜ)·6ªtª7^ß¡Ks[Ó“…‘‘f99˜,O E´’¨6yâa—;-;QÛÿBöòÒ„ÐSpø2˜òN²‡“¥”AVŸ[ì ,޶›"É6Z¹@ÁÉ/›^qå%=~ðß¾æW¶OÝsrû¯àÛ·›o¹}»öš›¶ Î;‡útúíK¸B»S5Ú ;Ê#ZÓ¾ìÝ0.;'žT“ çñË«3{ïÉ\Û&ÀމsC×ÅÐfùf¾‘}ŸûbáM;ÍÏtS×´|ˆNYLò>óšnæ’9åün¹-ÁÔ㉺FN.siü…¦# ö²=½Ñw?¶õßé;DÈ}aû³Çò1nìö‘žvýº/bn½å.¾KsÕöW^ýÕÛ¯ýÚ[¶ÿñ_äÎÎØžõ¬Ëâ{ˆÛ‚ü%óÓ8vÍÜ®ìø×m*`y2ëŸYâœ?ãÙ<œüŒ¥å4Jæÿ+kË«²Ïd\š•5ó®½åãâ6;žd|{^Ç%Q æ»Z°IÅåp…Êoj{Úç‰y<:•Õ—Y€Sc Ìœ¦müek¥ÆŽËÁY‰cùš>½Í,¤´n6}j¼KÀI/ÊÍœ8‘+:ÂÂâqNÞ™K'F²x­nìí=ÈØ1o}ú’§`¯bŠŒ{å‹¥æµÔ¼¢…rè;Ø0šF{Ôà›ß0åø ÛÍIÙÉ›Àìªb?¡…aúX°Ì" ¾2ˆ6#j&IÑO×BÆeyݲœÂ+îæÚƾVvìPòМ·´Á9Xf]§¿dl4Úå†0ŸØkÍð¥ ÓÕ?Xã$¶¬`&+õrpçöÊ¢¬ùÛR¢âo-ì´¹²9îqBiõÈ"doÈã±ÑÈrXN]çå/müx˜Á”…¿ cðRt”=‘ÛÏ8TOo1¦^ͱ˚ †Ãàmq0x9!€­ȳëª}dô½–ïÔµ)Àä4Cá EÅbŸ,¦C<ˆ¡¢ÁŠükÜò ¼¹J)®^€`{zF—òéµZ3\«\Àõ!™´ CÞ#á:Ǭól;µ¿6‚ÓÕZÀš60.¸šÅ2.¦;h’grSyT '¹…=˜ ¦ =WF Ó¸ƒåá`ú>ÿrG›§¶åÎ wá§á†Ì‘‚aØYU»¤çˆ~ê} Cœ©Ž²=>ê†88gCÆNdsPù‰ŠjÜö)Ïz¢àT¶"Í /M³08{¸y·rü Iƒ]m»Vø×}”¿Ø¬LTJWDprî „ùHrl/gÁIvå0nå‹–uÆÎ^=eì&‹ ߌ+òHÞcI¼;Ÿc°<9!vbï’o&:IxeÊçDÒ6 Í‚lß}¤«ÌSÎvxE˜1>nÌÉ¥:á_c &b«½k"h&Ú>¬výnWbÐk7€Æ˜Ýýü"©Ðçœsœ_ >¾ýÊëß¼ÝrÛÝÛw~ç×o÷¼û±û¥¾pÆq~ßýÓ»èAh¬)4/QŒaÎþ©Óø&\ãE_h(Vå…†vm³xÊR÷ojšN͆…žÐf®X1V7rö[òÅâ]Ü”'v³¹e<,½SoŽZïô©O `4¶ÙƒŽT (&RƒÚ°h.ŸÿÎQމ1«24m§;c;íî!è~|.ÛG‡ª¾°ý'‹Àä—óÙÛéüºð1ÆÙ1ö÷Ýÿàv?èõâ—ÚCá]»×¦‰bVbàæ;ñ .]݇ñêÂRp¶ôçâ¤x9vŒ²UÞÔ¾lšñ·ÿgVv&œùWª7¶ª0¦èÐþŠª\ÌU² ì™ØõÇÏÆŸØé{;ò`mÓwó<´T7er<­ bc?Ó%ëî–ìVÚÆƒ½,¾qP¤‘uqŽ6ìr¢LW”ëK¢8Ñ$Ñ 1¾ôÁF\K~âPJŸ>“Ö¤'© <;s—ΪöÁÄ5èh.–@»ûîO²¸9}{×;ß»Ý~û]ÛwÿWßÐM~ø_ýâvéÎåþ泋Ü8 L@ìýø\ŸµKüNdÒ ´®@-!åô­+6Bô ǹ¿¥‰Í|ðð±³ù%Øû‹<¢ÜL×µì ÛDŠåC¾è›·ü™gg;Á÷fÎâKòl7ÜtóöÂ>m{Õ«^´½ÿ×mø±[¶O~òÞí†ß¼]zÙùÜtfŸ¬ÕuŽ1reÿ}‹rŸ7íñÍ\l1E®µ>`.—§|qgê0–CÔòÚÌq¬9¯ùr8 øùkþoá§ÖaP©:Í[ë6jK:å2Òyh%o>в_ŒÐr›$ͧ«4Ìé Kpt뀹Ìx]"ò*¿•b’؇ÛQK g·ó»ëcäÖ?q;xAO¯ãp×Hª²:ù&Á^6ÛìÈÌØKza…Cþâè~·ÇØ…´*ékLÍF%ÆvêGê ©‹-šBG›ØiFiCÍãXöä­®Á ˜øZ †FØ¿xhl‘¶x”‰&^œ¶²‰9:ªåH®„9ºŒßΧ°qzù7>jƒó²æLgI¼Fn7J¬‘žuAqJƒ(‹÷ày¿¾œíèíùZÙ]¶qr®Þ«?Ip `MÔ÷x 2y½øŠ›ú3ù\ Zð>?1wMy¬… º3ƒÎ4ÍÞäô`è1=7×AJsôËP¿åIÕ:@;‚ 2Fž`,#­f‰üKµhD> œß ¶~à¯qdr [³K–¥a|Š0$®½ÿ 6‹þiÇ”wÿ: ·Äf& !JUÞF–ª‰dËê8ûcïŒÎÜØ;©¶ÝZÑgo—R¬äße”Ç^ÿü"›+#/«HðK#VzFQ(‡~^N˜¶…%q£E_ îŠemÅ#ê\2Ö„V@êU®oá©Ý>Óþë cG¾ ¶²3y˜€ÒËsqÅXÑÚØ¯LþéBór‚󯺊y¥7c·@c„T ºC¼†T‹?ArB1®Ú°ª×øDòµ÷žFPÏvJºUß@vÂÕ„êÐÝÊ‘‰Ž>kMNå/50;Y‹~tÂ(¶m]½±hÔ!Fv(`ÀÁhÇ~ׯl›(ÆW/ÜÆ>Ë{Ì瓳]?ýS‚2üX 7ñR&ôeêË%~±aÍ—GwyѵËwÊŠ…Gc¨ê³ìzã¶…ã´]ïê“÷m·Ý~ÇvòÞû¶oxõ˸¿ùüíçþM|Gà´íâ‹ÏŸÅóŠMß»XÓ¬Ư4_ëÓŽ‰ñâòÆtòaÈÆG>óȰf°Þoþ¥5kö ºaáæá“_‘e“ýµ§RDÆô,k­dáˆÆW5sŠ!<1CÖÌ\åTÑð‰9'A©O‡?IdK1FǸ4TŸ#ìøñíøvîvò{èû1`—©øçþHMl?‹'Ÿ¯MvÛÝêcöŸçMà”·]öÒ÷TÚA›ã›þ;~ÆyÛ™ÇÎÚîá6¹«¯¾œG{^½M So¦U^_Û†=érMÍêÐgF_þ±]¡‚XXÕ‡_ã‹’º”ô}x".?m™úÌÅ”#Á)³RçœvØP:¦Ãˆ¢†O<äõÖ•„ך$­ñA·‰9´yÇ2Û¾0ÖOç¼aB¡zBoV ׿ááæl°[!QaÚj›“ ÏÄ\½Yjø´Õ®Öî|éUÿ"Å-+e^ù&Á™cø+ê\º½ç"¹ÕÞªVŒˆlûJºâ ^[}ψU’Æ–ÚZ®ÉBSk}QÕNbAkŽe/Ü0€\ÌÆÃÃñ¢¸¨vÆcˆ•-jvÃk±ªi'(¯†<ÇÔe$Ù;Qà!Â8©aà $ à%³g)vîèž@¶Ë¡ÒD†KYê¾jž·œívD¸ê6Á\`ÐÙÜz9$­ƒ0´¾€¦Ï¦à Cd㬳7 õvÕÜ{åP¹£û»¦¹ «sÙÞ•Ä|AÆ? fëE=e Š­Øî†/pÔB¾|ëÉ.øºÇÛÖîv4åîÍ0M‡ÿ¦qµ_{ÐÛº¾·Hƒìžw.(O} ²¨ó¿úÀsÒÁ-Ùè‹p ôaY‰VldS_™Ä®›šV üÚøب]üÙ¦½(µ¨›¤±{ò*³¥80©˜ ññÖVMCȱàB¼v¡¨Ëi>ó¿´}ZÞ¦p±OÜÀšÉuø4HëÜÔ3ú×{´o—ÖlGÿú¤ìÛ’‡$ËnÓ®G9u:øg¼! ýÙ„O`vÅ(åŒþ\QÀ;W?•jÓ2r³5‰ƒgÉ?¹ä÷¯+PÔf“œ‹É}î~$ìCÕÈe¾Ä|Ø-›íåÑ]èÙ´%ër`´eÆ¢L· yó\“„LÆý̶“3c;°©sÆ‘X=ûì³¶óÎ=±½åÍïæi&ŸÜ¾ë»¾¦¾ÿâçÇAè _™6wû`t°®v¼±(+«ÇþR—>BGû´×zsŒÅcôŠÑUë5÷·úN)6ËK×#~rÊvô8ÌÑî,£!SÓŽD§<ÁCÙôŒcal’c¿ún¹ û:¥G¶:áê³裛¼šÝF››M¾êüaiMÎcŽ<+ë—\²h•{—(6Ètœ€ÚÅ@11Bã¨x2)F},bÐv8%?)j^è­›ðiÖv³o–g îü[}^ÚÕêrÇvE¦M¾L¾4è‡<Æbª°>y€—v\L`/<;.øò|ÅUl{TæäÓ“¢â0'êQƒ<ìØÌEësÅÔâž+ô=<ûb¼|¢ÍVMòŠ€6;™ØGÆÔÖÊY¦=ú"F*€V™ºÆûÚ§©~pPÇŠÀ©}Y>ćüÂëdWw0ø;XhN•GZèÅl'"mj N}ª˜“ ; ߦlêL%åÑ mšÚw…•×&›·½D£ Åê¶íT¾h£8ÉÆ %éw £KûF»}é—ŒWMmõG=bç,-û|aŸÙiÍ?°AÚ%—œ¿]ÊëMÿî]ünÀÜôuÛÅ]¸½ÿ½×r"pñöñÝÞÕN¿# ñy¸fPsBJv§ÆoIš8XlÀ?ks¼¨¿§Ý>rlìÁÑ×ÂIØêG²ã» ú‡X€¼ïb }Xþì®[—¤­ü¯øY¯¶e˜mâ)óD´÷À?ñž9lñc cÙÜñHVç\·É'|ãû1sœÛç}N×f)„?œy<ôÐéø³ËB/Pƒ7ïö°fÏ˜Ó Çã8)‡2±\] EËĶÉâòU)ˆŽõ[œ«Gc%y…u³<`©¯¨ øï­z§skÙY|ÂqzW¡$ï,aljƒ®?£{ µvl_<ȶSÍ›)§i1†²sÚâXÛû&uÓˆ ±'ÕÇx¦ÿéü —O ¹‘_Î~ñ‹¿˜Û~¾xû™ŸùåíúÿrñyÒÕÑUü _ò ,˜bwÜ'á2•þÜe~‘Ô§×ÕOèusžs¼¹™.ú®- ¯xè}P>É Š,ùÞí7ûÑÏ´-~5õ²®Qà*<òs,±VÜŀǶYL«¡ZØâ$¹Û‚ú,Ç}—Q/-ñÆãâtÄâî¶Î\Š~cŒ¹°Ï M*ǶìÄÙf;³ Ô”„HŒìwPÖ ¸ã‰}íFM<ãMç=®ÐäñÇί1Ði¡ˆÉ½6ÊÚÝmeØQ, >5Ù›ÎUÕÓÝžÑS/¦“¦Ú‡èÙwèõ‰æi€vhOB:A!;#,œZksÇ~¯ãL_,쌇•ÅRûw.””è5oc´Ù1óµcFù•Óå#5eWæ ÅtíoÊù%_ÐK{?|íÛ7Û+¶oÿö§òcyoé ¾þ×É{ïßNç©MàÛ¸ÒjÇ’¹¬ßè(¿Él' ”̱yåü°Ÿbƒþh›ù¾Œk.ôÀ˶Û;ñ"Ãq;ž%ªîâb|L>£¿¸4–´ÛF„Ò3cKU-h/ïÍ Ž½œSêRFÉq{Tut)9/ƒ£NýÙÅ&öЂ°ŒÇàÖW;c@ƒÝ‰tÃâØ”fÛÇÎY8îXEaÚ4¹d)׊â'Ñü“q½'™~AsöBOUø'qbÏ–‡8‘°ÛxÔgkP~ªÇ2³¿·õóè3üÊà Ìœ‰ØÔw» PË K9›õýzi]Aak³‰šqÒ1ǽ8å^ý32ê×_™Ü¯HƧ¬[¾R)ª098ö“Xµ•bH³iâÑ .¹ÛÄuôΰ!kÍØ¢Fã@]lEäD6=¹•z¿b Ÿ6››}Ú#_Hî÷«3y •½6ûíT£tŶú\¥œ±Ê:ŸaPÂõ¨£ ߊ˜Þ›/oà±òf~Glp§†ºó à±ßa©síœ/4ÀÈŠ#ß rR”N4ÇZYì îUÀ›²é¢ÖI¢+6õ0aÑêø6–µK›šaà_›‹nLÛf³ŽÝÑââçâ‹Îå—„¯ÝÞùŽ÷m/xá³¶ç>÷™Û¿þ±ÿk;yÿýÛE—œ»ÁÉì\w±â„¯¤}Ê?ÛØ1Ú&ö駬¯¥SŠq󢂲-@,ÐnH4 O²øçyFÛ³ž}9¿¾zQ/dØo_ØþlDà²Ë/ádì¡íÎ;îÙnºñŽíÞ“°€ædÍ>ÇÄzŠþÚóS«ï,¦8Ò9¢ËѾ6BMÊc¤&wó#å«czòžÏ¦ÚD÷8éH=ŒiÊ3çÛÊ|AÞœ¼ïäö‘nÞ¾îk^¼}ÕW_´]wýÛÍ7ßÚåÝrë]ü¸×9ÛÙ<1Ë“ËFªŽxâ¢4©Û(uî‹c¯c¢ÿŽ×xí±«1žAÓFqNˆÅ”Þÿ*8©XïȈY@]z¬ñǺ|–/Îñ¶ƒm™ª“òEŠ¢¾È¦BðÕ©9nóɹqÕçJ{t-3Š2ƒµùkD>8¶ÓÍõ‹óL¨ÁdÒ\lÐ$ùa–Ų~øDð1W µÑDãÐ$!^烆-`äQ6;—h·­*±EJµA•˜KÅ€Šj”’8Õd\ÈF€oJÍd”aZ[%q‚ömÔvŒxQ|–JA-ÑàׄĿÖ|Ë`O¡ÑÞ‡A‰+û̧ø;1Ÿ¦>óóTQì»á55D¯a«/Àí¸.˜Ý*%›1¤nWz/›°ùÝXmÙ헀̬“ o¦jIžZ€ß]EÞ6(ú5Še¬ÞØ–£±¢íÓÏÚÐò³»~GÄc'e1õ¹˜¨M_Géü˜Š_míÕ½o)^õÎ8i0™•ka·êbê’9dgöCbÁjD°³·]o¯q&lÉ?&Ó.N[I-£”a²²­@h¼õÛh1yü“}kÚ Åì( ¬«•M¿-Ë»bP%Ææ "¼dA÷£7¢ƒ¯·“tBšöB¹É.”þÉãßnŸ:õ…]5·A)‚…Úöû,‹+`bø/nùʾcËúØ"ÿ‘nòN`Ê` ¿Š2cbM‚ú71™8ëÆ`/ùÏø!°™ŒÀÄaÏ\Ue?ºåçP jˆµÏU¢U»¢Žã›±ZýK›¿,h_ŸºÑ lý¶a[ƒNóô8+Ľ82±Ö‡’W¹…›ø‘ïØÖ CnúQL7q&å•G-{/lYvÌ`lò #Š©9“·¹¨¾AL»E þJö!’Ñ$‰MÅbcÒ+¼flÇŽ=Ò´¢Æ0a”?ð:{¬N~È9qªkwEP-*hN¥’7‡Ó&˜Ò¢:rˆ!Á¨«¾þGÙ”© ëÆVy•&Æã“tãîÌ‘êõâ³hã©M‰‰ßYgßîºëSÛ§?ùéíö[ïÜ~÷¿¿=ïyOÝžû¼gò‹§¿¹}ôÆÛ·Ë/¿°x4ÂÜX‹Rƒžïý‚µ~¢¥Fû]ë#» i¯k¿c÷Èö'?|ßöÜ/¹z»è"¾˜Ì'~2æbóÿw[ùzõG‘},žÇjÿ<¦5¯ÑyÂEüÊôYÛûÞsãv?·kà©:]ö¤_ýôÈ.6-èsäQ=¼¢Gˆ©ÖøO£6·Qð˜W½ÜAz*sü£âÛOýòî§y’Ï3ŸõÄíiO½’û¸Oc±þvý5Û~ÿÝ×nW^qñvñ…ç•[ CtvX;Rç ‘LÛi.@åÉÊèSN ‰ð7î<QÞgËhp·NêÂÂ0¼ãj<€²E†¿X£{Æ’Ú‹Z<áM©S¨1Q¼‘ç}ø×Ø‹¿qRv¶A´ÞloóõŽQp–MàyŒÓ¶œìÕ=·CQ¦£Duó½µ’è‹_;Üœ÷c¥œê%i$ÅÞ–+”›ïp`ÿtñpŒ²°u| hí8˜«íÁ§"€ó)£j\ø¯o”éve¤C˜þ´7áÅ yº8X÷¢·ùÃa‘WrM”E`Ã1f‰eÕ¥iËѶÐî6f?F-pÜ\Ÿq¡­)€.„åè¾O)š®¶YÏê_jØ×À\X6ÆFy üñž*‹a³2$nœ.#Øi)ø»ÕåÂr¼8òð®Ûû4£~z@2cŽ,mU!VgŸ‹«Ñ˜?÷ý@j ”‰ÞQc¤Î–g¨@ê²7JAN©NÓ¤K%O½ ŸXc¼Ü;öðj¹F«Åÿ6ë€&7GžáY‹…<†Ô@É ñ€’‚á½…Ñ#HxµËßÑ”sÐË*ïq%¡XæB º@çÀîâu Î"^øÇ8P N¢v¼šl(Ðÿ¹ò˜ñ:!k¯Bþ‹¾ŽõKíþ5IÄ5:xŸÚB0•åmÃ1¶ÈÅ™]È%xÈù‰À¾IÞmYø@ G²VíJܲ¢1N¡"ý6“š*¡Ãì_ÿ1H׆uñZ`yÆ'£:À(J}xj½R;[¤-‘±MÞ‰áŠ]&ùfà ð\Éhh 1ïy5$˜†ê××ÁWyi‚Üî—²Á\ÿÒ”òà:Ðpy!ÿÎWî¶ÇkÇ«ÓïÙO{ÈûIßt!vË:ƃ}(ˆœ.ÓÉ$Þ< >ÿM„xôwòEiMÞí´|ÐJmÐ:Y×?Êð sì4¾S¾uì@ [ôUÃ;jµi0ƬÁÒG/@Ä®^øêé:‘͸ÏðWO¹Íæ¦oá– »¶—½â¹Ûí·Ý¹]“‚¾îk_¾½ýí¿·]sÍòýŸžr¬Û G|*Q¿y¬ŸÚY;o-V4Vôz±kúýá‡dyÿvõÓÏãIÏÝî£ì¦¹³P¥îDöjú©ŸõY‹…ÿ™BÅJ»Å?©ýÉ>†ücá?V{®Éôèø6ßB~€3iúª'_²]÷¡[ù~Ù=fvø ›÷ûøcòµO Èagîš²yQzö¶Ú(C.LÍ-æ4çé¢záèôZlŸæ)W¶›Ï¶=ñ‰—cß•ÛÿŸ¿°ýî;?°]tÁ9Û…Ý}þðEÖFüsaHLs¸‘‚ôà&+ š¸W15óÜŒqó¿‹ZM`òΘÖÖìõ¹9*ŒŸ`@– KФ_=ü7]Ó|PIÁ¦€ØYÉR«b¹q•£Dç!ÞÄ¡bQ©Ñ‹m`Ê«Ž²bœıLqî£ Ï·æ¥S2©O­RÃ>'7Ë&}X}¨EnÎyŒéÑî”È dÛ/\6×@êÑÑà•G9qæ c»˜ÁóFY³ÇX{¢";ü;žÖ脇¢ìBÆã’Xûœ«Œ«Ó†žø´Ò•‘*Ø›³ô(Çmõ×'ÒÝdàß^A.—P‡ÅÞs3ÚPò±¶5Ãî•wukÂn%¤-váë[Þ“Ymìö>–âgt~F¶ºÁV^㛥٦ÿ…ßióбÌM¨6wÂKy¸šOtžvÅiG{3­¦eU\ýŒe.@U^{Ì̵ð!s’>nÖ–}z:Ž•AXö5Úl™àY‡(°A²£ÝçÔ2moD€Ñ;»%Ÿ KC¡ÒœXIlÿÔa™©[pP.á1£Žæ‹MÕ¡Ëaº,TçCs;àS:´é‰Ú³™¹Þ6öŒáÆ Bvp¸©«IªÚ´‹Qâ³7g€ô«|( 3`½ø-‹œpr˜‰2y8­ƒÆº>¤™½«*u(Òƒ°<ý"O¼öƒAú׊á79\Xš°ý ôÙ°ÀY\A© â@î©’,bK£ßf’bOpTeŒ\ N¾È¤ú‰KØÔ›h³Ávô)ˆ®ú‘Ešý\¬ië65š—Ê"§ïã 1F¶ €A«L¥ñwôˆ?ù*È´ñNÑÞq‚š˜'gvL·ü¿ì½gÌ­×y¦·LžÃzÈÃN±S")ŠfEuÙ’eY¦<–=—1AL)ƒg`ØŽ'™üJ&ó'2@<±F²å:¶%KcY”DQ”ØE±‹E4{¯‡½äºî{­ý}¢e#±h}ßÞïZO¹Ÿ²Ê[ö»ßýÝcb ¼º¦¦Ããj°Vdf«\¢ HRŠñ*Ã[hðÒ'¡øFl ›¼d09’Ë“oÜΧ¢ØoÈ¢»æjT4#ÓÚsÆ]–¹ÙggÆ»ðé{ð¥IçÆl¦?m)Ï¿Wlwì388;ˆï*¼ÈN¹cÐô´l* a1þ´·A}ÏêvÛ ¼ºýj¥WóÿÿÖn¼öß+ÙuÐþãÀ]ûð(Múp'ƒ–ñ·æYÖs:8c.óÌa`Ÿ£Lßg“…ÍqèÚez3r²§>ÇHÆLÖQôS˜£®MÔŸyš&È1Ç>î»ïÑqæoGyøøO~°ÿ ãnuÓ—W/b=M©íVW cÊÚ$‘—Ø¥3§³:Ì#eÁîMDÄÀ÷¿²ž+è¹õQÂ,ÎóÜ"AÛ”d.lø±\ãT»æ›GóëŒ]Ø0±Ýë(ë‡Vµ“\³1±Ø‹}nM¥ëÂáZÛÔÓÆw匽ÊSÙDîÂMs¤‚C™`¤:<1H—;Nà+Ø(ØŽé2åUoÌ©`Xý4\`u¶d Xš–x#Nú¢û8$+{ C²ÔŸñX{Itú=ãŽ/¦$`q%ê‚ïà¤4*JfBÄÂꪰ¶³Naè_ŒG©úëT*SáhÖ ¥O³ÿã…æ»òÓé«i+ðÅÔW÷—3Ì´È›±8~y¡Yo¾ùÕ×Ĩå¸Ý›1hÉ|ìúfÞ­¾i#B´ñÖV9ÑrÂjÿÉ¢jõ ´Ý" \s€oS¾}RÝþðq%:Á”_Uäoù¦î&ÀšØ \v ÉðÑ…:èËf+¨ÖfôODÁåÆNÊ Õ¨lÞ$-|u‘ÑßpËN‡ˆcðZÎ$BV;É­UUÊr§,†ò‚Ç¡vr\PÅt2bÄd“ÿ9£Uncƒ”¯QäÙJ63€â—V¦Ãúh=Û.õC…Æá¶6éåO:8®óF®z W۱ϠY}­ÑŠ+¸ZÇÅøg}*$'ëKx1ÂŒ€¨³Lïᙣ`3Ðó£FÜ8ßX¤C‹#,~@4¤œÚ‡ÂKôcT·I¯B¬œáSßqYŸªS¿!ñçâæ_°Úc¡ë»gðÁO<ÓФóÒlÓÖ¾æ]Vò=…Ë9£‡’[Šz9y‘/´êMðôDפ©àÖEªò/J›§Øªjt:îµÕ|•mvÌwüOùàk½ôg|‚*™ðsÄaÖ$•˜IémɃÐáL]g§¼£L®/߃&Û¢rÂl_J³›-«ïgÙô0ò5tä×§ ÅWÓ˜˜c AÛNueVnÌS˜;<¥!\›úKÀôé¥ç_;x4ÂñÇž{¼ã7þdwüá|9xߘÿÌÏ~/TîŸýÌ7ÆáG4vØÇ\¾Ì`R"vàñÏÄŽ£ªÚñ@IDATʽ~\­mȈ<ÿ <’qlsJí—•)ÙY¥öý·¿­ðî,Oܵ/áÏ0ÛßérÖäÎEiÛ"èË8èì(?ëUe³® b;4G’•B¹ôö‹gùß<ÝçP~Ë⾜üîwÍýýÏŒ?ÿâ¥ã¸cà‘Ÿ{ñ Ù•Q=è:g ßÀøçxŷئÞ}Ø´fæR¼^á4¨EϺ®¾Àa5ºì“ñ;¿Ï ?¢98r¾ÓH³®tY«úÄ Ï~[÷_~_-¹W†ªã§»ÿ®Rî¥É÷BW‹‚Ú¦,ÿ‘P2ð½ûÎ[%§áz¼|1©žžhÁ;‡ãMcbâêD÷ SŽDª£ßP„ð?%¾È´°]Øõ/¤X­@ßãÓ–ŠVë¹k¾.&Ÿ1®æo#Á\ð—,iSâô-ë¶ŒõŒ'ùÓž‚tu« Ì)¿ÝWãÀ§Í˜Ð•æ³ïK}ú²ðÕÙ’ëI§ŸÍ»n˜¯ÜÞJ\~ÿ‚PÕdwÁžÖºq$–âɵ©‹3ÒÖ]ÑòñÆö‘Ö¢ÀÆQ]]Ô0ÖãêÚW8à•>~H3–5²oÔvä-¸ó.Í ã»!,c•°|µ/$h¢ØÊ7‡™ß³î…B¿úŸ¦À¹mζq$·,ÑLH;:ä¦a¼ „霃X!Þ4fâm7í;Açdäj¥h©f@F+dkvšfL­þÙ¨¥„Ÿz4xÛt0þ¶c¼|K— ¶|Œ›ºô8:“TiZÀiS+¢ªCŽæê0r‘*¼° >2´…0ŠuÏl¥Rùvl^T%ÎÕ™ú,„Љ´i™gå´­Á¥“DL¹Ê¢+.obÛ–Ø s·Ëcâ 2)æ[QlÇ+‰í‹ºW3½àB’®£b8²#€ž1aþ¨[2P·9­u¼ÊêYväpøåSeñ“T2}%ÄNÆ©ÁÎ'Cc)ú:Ù¨þGD‹­Û•ÆÔÜR—aYYÜÒZã­êª£°t‘1OHç~J“$ŸÒ/¥%ˆtJžXsd{Mò+b?HצèIû½ôÌexü'ÖðiùE4ñmû‰`†É´£l«Œ‘™³Ž‡ äm®ù¿Pˆèf‹ÁK¿ûñ» Jˆ¾‘5còO’Øš–øE»á7¦ìŠ?SOìêv, c¿ÅwÆNÖ@¤õÓ ÐµÃ›ûAå^ä Â{q`uü Gä€üÓ¿ýùáãAßúÖ7GùUáù‘sså÷O?w WvvŒ#¹=ȸ_ò„¤|â%¨¸ŒGS›]jˬõW‹-yú´ï—¿`’8¦Ò»  ôsÆâf%/cÐZn_dP0²ö8`Rºvo­ÿ×O ’—ÀfHŽ—}rß9Øg¿œ€î3žÙóÜøÈGÞËmjwŒ /¼tħSÏpÿÿí·Ü/ø 1Åv³½=«úæ#9þ{/{\2ÓŒG×!¯y-}íþ3_ignfn©¿5ÿTܺº‰\’¹†a#ùÑ`yóU¬,GÉ™ëœÃðô׿ì3Ÿ’õÌ5ÅS"oÚFZ@xêx°%=¹†mEEÆ`•5'³/ ¯o:—ß¶¾¹p”dês¡ô›‚fè u1’Oø„ª¯zU\ës<¶(¡Äà®cÊBå-~ÐŠÐºÖÆú±mœÆª:b¡\í·SÒ¶ŸE×Ç '˜_$Ì+¥&BQ½ös”·ñ1ëá&oÑ…£n…DS ”>YêUëk¿´‡Ž4ïÒò¥Œáx‹tÁDŒk39±][”C&BP‡ ¨y‘!® kŸãLÏñ‡II°ø!ž1(ç6uhÇCñ5Ò~W¼£FÇ¢Ú Ä”•ˆŠ³ *±Y{×É“¾U6øT³ÚmÌ~²²ÆlNjAS©n’«()ä»Î÷޽Ùéìè,W—*_é¾›òâUFtF'–Sò­ÛÑž±ÉI’ú»•+ÜÈ´³Ð4ÁË«d&6Ö‰“0’)E[WµÆ@¥Wm‹oÒ„Š)yÞÜÖ‚µæEY_¥P¡$n(J$F™KÓXÃClã7ÙŽ,:6³“°‚žþo-~Õ—¥Ïêx›„Aæ^\Ï‹¹3/Y¼‰·nlS´ü^Y¬Xœ¤gЇM¡¾ò¯®¨ÚÅžQ, lcS‰³?"g[:z]ÈPL¬’Ía±Åϰ”°üÍ‚š§æy#“‰ŸAR/òÓ¼i'J\Òg1ÐÒ‰Ò¾.îò rså^Ä8AõŠÜÚÉÖŠ2S›U“ûÆ/F™Ü«§`ˆ1­Å¸ %‡§íÜšÝ_YÕ®ªëSø¡¼z«ZωW)Qï!ï›<@SÎ7ûAûùGX˜Œ½%@¿‰C ˜ ‰3Ô1‘-ÝÙÔNn sìÅÿæJ9Oø*­\w;±Uº¯úÓ[r¦aBO1*^mK¦_n,ýt¦kJ)2ÐgMú$·OL=\H.£‹D=e§â•}þŽ>úÄù‡¿a›øÑ½g<Ã{Î{û©ã¤Ÿù㯌8ÜÍÁ—^ôDÈä+s®]¦c9ÐjŒ4+èé“·!åd™¶Ê3]ú~ù[šûØU&÷Q;RŒviú3“úêKÇ|:u®þ6æøSc«Ó×aXI~‘Ò /úÅ^ž8t _6Þš¿týãûáqÉ×®äGî.Ϙ»ëž‡Æ~íà{$f-õ–/š¸&çÈ LÌé{=õ^óÎGçY$¡oÊv]°ª¯ÎuãQÚYâ\ÎÁ¡â®û®uÊZV¼Î›UDÎ/÷ð"ÏÛËè·€=ú°â©"˜³+' j/y©ãC&ð=r„\¿¢ßk)ôÝ@¤OašÔ7â~y™p”ÅÜé€ôæª #L^mâ@ä”ñEqß[ÍhSïÚÞ²ß]òÓøÆ× ’ýwÐvKÉþŒmüŒ\ûRRØLÑé³±uljöx3%¹×<<=fj_˜æ¸6¡Å׌k¸òY§ÈIÅv@#7þ:©í¾Ö1DŽÑ1K9!SÅ¿ä͹²ÕWžhiP›ù§X³HK p×±¤ch c;Gs¶U;ÙÁRß<¤2Çgöu2xõ“ˆÊ¨¾—'2(Ô¿ÚÑÆêŸÞå2}Ÿ aSÍ[oóÙ<Éžq`Ê…bÍPç‚v.FÏŒ3SǾp› ­¶ušOÕ‹E7 °]°¦Ct"DU93a+ÁQ™9À2”‹^˜ EÖ71¦õÔTÉaÛ.0ú©í¥f¸~4’³1 Å+¬ææ`©NT“œ‚¢ãB5w²‰sEå•™pI·©%ÆcCgÔÇ&^®ö¨¯äèM­Ê§^£kC†î@YPA3ø)_ôäÚimN¸ØwÆñë¾xz°æt@ú„é+ìim¢!„Û¬¬È/û¦J™¼6z•­î’T&H2ãƒÛ,æaÌžÎä‘c)MoÉdÓ9©!ˆÐ’“¼¡‚À”ñ‹ž Åš˰"]ÁV%}Û¼yȾ:æ ½èÚ–C#PM Íæ.,ddF.¸dX?§ŒXb‹çã#QW ÎÂWH”…‹ÊÒÏÖ±]ùÚÎN²ù;;¥•‹Ai Rgا"û©S îK¾E¼ÄÚ±’ñ„%è´ƒ®> !¯ž6vÃÛ{%›D-¹²áÜáÏ<©ÜþOK‹xPá3îš¶È+Ò õ´§ÏîŸeçYùÄ›1ÇDÓ^ü„iÞ²ðšÝFü] k7RluR¦Ý½¼Ä€<èàýÇîÝŒ¯~ùŠñÈãOñI€†?á(žô†ñµ¯]=îæ‡–Ž8òàáï x¸¿FìdtÁ_ó/áN#Ù€áÖ€žxµ=½øþæoq<¸I¿:ÚVŸZ¡Os°Ë8ËÚÍ9›å`Ê–c<³ZÉŽZN²#è—Áçàÿ@n7{ü±ÆqŒµwžÖøSžRõå/c<ùäžqÏÝø°ï8òˆƒ3Öœ®ŸÎUךÆz0)ò3™i{µ€æšs6¢Í~q­5PUßÄ’£(e]³;ǵ¨˜o9–b327‘Íz£F¢­°u6ÎëðêC…ÜqBNLæ[¹¸þWöb©¯ ß‘¯í®5ÖËP‚U‚$ŒG¦qEZ?ÍS ŽFƹ>¥HVwâÆ/ìÓÈA·žüytÒ7 „SŒibJ¡¡þr°(u®ié[5—*¦3Œ±Ù7‡YW'hÇÉ_\ž¸ÆÒÜo tdõ Œô‹¤ì7"c\ê,Ö$x«Œö}g…z¢Ü\t¬]Šù£‚þ7ö *²Ó–ý6zæ5í(+ˆBèêu œpgLŽƒT¡Ëi®zâPÞÄRwá˜Sì8¦_¦4™~;Rç»Çâ›·  WÉlxk‰÷Ë vÜGy,njùXÐãIè)OÑz¶­R¯#@ÃDºÿOV‘®ó$?'½ëÔ‚¡Ü£°é2|éÞÛ¯ž$ùÐ^½²ÑÔ0E‹Ù"¿ªá£*¼ø šX …HýÜ!¨ ;Vòä@¼@ÆøbU•mhEÑD}ž@øœá‘f–`5ã‹•BÆ^ÆQ¯ èïÇN)«‰Þ–ýM(ð§GÝê_rHSû¶§loM¨XòÝÍã±(0µä–­g¡¼:l‘MI±a¿æy“&us­}uÀ‰ =_…µïâàÓ’kxËŒƒ,Aº¨†ùÉvæ6©«£ä¬=yÚ‹£Ù†ªb3íÜÂw¬â' ª®ëÑVj ⊵Ic³ž~°lh:gïð⓱翹3¦Ä_ºý¦Ï9û7?Éÿt]³(ŽÅ¼Ú¿Ê¯4èQv‚ú‚LÍ Ü±åGŽïú¯QMJˆ|Î W¢á‰‰HófmªÈ”=•m´ë'ˆúl€6.Ú^/b1 ùDþò=ã@þÌéÚệô„À"Ý<$Ñ”: Éã•1ˆŒy5‹ñK1‚§™Žƒæ2þ'Wò¥Añj©:T/qA_¶µ£tY™ËEWkSô bÍ•Œœd ä¢ê†á ì•—_?îáÉAï}ï[óƒPsrð¾÷Ÿ3¾uÍÍãê+nG{ÈØŸ³Wøâe.LàSû©öõÓ¾ÍAýšO4ðýòw(®?gôiG#¨¼Î©­zFrŽgõ²6TœÔgžtªáÀ=úøžqæ™'7½ñµã³ŸûʸüòkÇ>ºóîrbp¿píïZ¼Ä¯ø:ž€äW>y[¾@ÌsæõL²¼ŒEÓìz…¥¹ŽÂÉŒ˜“#|ë⮡Qý† |\ÆVŠx1/s «­€˜‡®½æ"Šõoâö`Y £ŸÌ"§¿Áïò’ºfÄKA&ÒQ~ÉuM‚ÖÕIô=t×5ó’“±”Cuùd|öŠtT›¶»Æà¡±Ydô?@!›syºxhÈ…cÐõƒzBC8Ç–ÊÆ×Ê›@d«–µ3È´50T|¢9Åk6 ?‘îZ©-u¢9ÐTgÚ¶¯³fê§rü¡¡ ÿ‘ÓŽL77òJ7T×u‰Ý÷ÛŽ{ó=»—‹[=]Ï~G }CLþö…GjÊbn ®iZó@Æwô¡M=€†Ÿ‰ÉK|å¦ßuQ-+¿bÕž•;‰¯.VÛùÍ éߺÐ,Ó¼V“k=f1Rm€mÅ\ÅŸ¹/“u2"h_§ÿ&Vç°vÀ4·U!­[T4%èRÝ‘$CVv”Ll3(ôSpÏdÈKÐlSSS¹í™kK+ZŒ\Ìk:‘!2X9PÈD³'Ê`«îLR´µÓ$êWäÔz°’æ$2rÓ®*úW;h­§ËHZIOa+«‰¤nÛÖÑ0´‘C¸Ỹðò(KÂÌq K”‘¡bâŸÂèÌÐj[^:Ø|JJtôG¨Xš9ËÕ#ˆÒ½°# §r´‘¶^à)§¿æ’¶‹]¾¢³Ô»ESmp¶SGI¼Zƒžv­DMyŸr‰3 2KDŠk¬)'_¯½•ÆÅœE. ;…ñ¹–¥!NT@\=1lRS{±å@·Àëdiî$TË- Q Ôb ì,Iõø8}b-Úr-Ã9ít•Z2§žIÔW1½Ê®Âès3E›è`Éû®NNVmG)É‘ŽEŠèíùÆŒOÌlx‹*­ÆÛr£Zæ&®IVcQ6qÄ<;åñaÎ^÷ú£Çí·ß;¾qÉõü€×ñã€ýöOžæç>ÈcEóä ÃùŽÀ®ökÿ©šù…}€´xÓ†¯ï—¿C°»èÏ dª™Ssœf¼ò–“SF›]ù®Oú˜¾gŒe#ó~œ'èø©‘r>Úóáÿ©Ê8ûÊ—.ëm@O>3®ä‘žr¿Ñgù¿È·‘];æÉ&ãuŽk÷—úå$èPÝuô;ô³£në ¡•È;©ˆ/²5’PõÛÑ»Lh>sXÂö!9 #íB4´;«Š›>øÑw®ëP°Íþ! „o/«õ[åm`Ñ,I<‘¢“ùŒÕé÷ê(Ñ¢ïŪÎípêÚ›5O¦º¯¢[K,Ú±ŽÍ…™uZÖQ¸"Úæ<1æá¬Ä|«&6Š'²Âì¹Í?Ê3-¡+&±Ÿb>ý+/ŽÎuF›Ñ¯åŒÇ¾Æ°½ñÓý¨V̯tãJüt%à’5)[E¯äNŒüõ—s‘ mÖÁ3iŸ¿W8Su:™®;B¡@ ²Ñ±m“àþ9D7åk±¯x.ªÙŽ^e‚MY…“£ Ù# V!lD”Rº|[Šñ·¾h¬fræºnÛ` ý©ŠÒ:–jsfàj϶¦ûƒ¸ bVOƒø!Dm¯*~P_cNôâÈQÅySÝÒ[€hy ì§e*¼vx‰†ƒI5§ñLN¨¹ª*CºÅÍ%õr ž–Î;:åLûFÀ©Ph“±êšpasáÔ”C³…v#ƒS V¼ˆÚ¤µÅ‰ËfNƒÈû‹ªt`e £á@[åŒø îVâ£,¢Ú&C¾U–§ô±%ÁØÆOhJå|ÄC°šÉFÃûÊeDÕ.`¯TPwÀ=Næ =æpVùù˜Ó@ <¾iØõÚMLPúä„x¥\þô‰—g¯küHŸµnŠ…Îv2' š2ød¼Š™ŠÍݨïDíU_ S/3Àøèƒö–YjžÄʧ+bQ2Ô¬Àð*¬èɪ&)® m ?¸Ð\0zÒ¥¢8-G~Ò d‚!O; ½iÇqîHŒG>Lx7StZ³ß]ªçÜ•©JüÔÇú²x&L“¸|V6Í(F#ÒÌ7aK¾"mB–q¤&ŽýéœÑ–ŸÁÍá¥S‘«?v\̃[ù9–ŒDsB§ˆ]žÔÍw†æºá ÷åJ‹!¶r‚MÛÅt ¬Ï‘#†Íô—ü36WÿK1|›C–+µ/r°Æ†?b<ΕÚO|êOÇ!<òíï8ƒß¸oüØGÎ/ð$–/ýù•¹…èx¬ Z$Ⱥ¶ý€­¸ë©f·Ó¾ßþ›ÏG{ÆŠ/NîtÐ×Ó»kíËš±Ž8Û\`a'àï@ìÍIÀ>;vއ~rüü/|d<þē㊫®Gq(?X÷Ô¸ô×ñ´¨ƒÇá¹½ìÅüW×8|XcÛAÎxrr Ù\lÁŸî##¸KŽí Eo©…•ýs2¸òŒNz6ÓmQ\B6ûêœy0'âE[§T÷'6 »Åßâ°˜µ4MpR"y›²ÀƒÁCKQ"YyiáóÖ6®‹êÄ+Õ’‹]úÀO36J°dÉc›©xÖÎ\B.BN’Ì•FàëMã“o KÇ<È¥Ä7èVëðâ°ÖŠÁŸ€þÃï Íä|s¼¦Lœ)°è·&%¾L ©ÄËvŸ%¯Zrhƒ4³2[üÆ$q­Ñj4Gà@ÏK"%v9¡Ò†'}±båÌ‚ý¾.€¨c ´ç{꾡i·˜³åËò;'Z@þå«ÍÈJ7ž½=¥þòÜñ{ÌŠñè¬ûðµJÐqÀtnœahšÛÚ¯%|ZÎ"\¿ˆ32íÝœ|Ç(ž5¥íëô+uÇ‘Xþ9Bcš7ÃÒð:¾Î9¸Úø¨iãJÓW}œÚÁ*@=“cÉðÖ–4\wø¾NÀ7© Iøvšm¯p ã»Î:,:]qfák`*g@àYÒ°øâaСž[…ÀK"Գȗ"pü¡Î6÷ÃÂôÀJýH#+l$Rµ‰&¢Ïb#,ÞôFº:ë:;BéˆwÐ)ml¢¥ÍuEH¶|íX 3ú:5UsÀÏî¶«Œ/òÁÁäÔŽôÍÓªÖ>Ðßì´Øf1јNgGî¶õ´cª>iMŸ_´!m>}€fŸéŸ¼úÊôž>$Ñ ¹§ýläc«£›Ñ—4Çú,¦ò•djaÔêâiËyJY¤Ù®yj¿Y”ÐŽÍÈšk*`ø!S3ÆH­•Ú ÇædªÛ| ãUý9}ÏF‹sL®óWY‹rÁ¦’¾$¦0V¨é³rÕ¨»¶äÖ“àbû’W«(Sëâ ULBŒÃ pµ³ XÆU½­ÙË.#‡ñÌu/¨|úÓÿž{ï^ûÉŒûî};Oög—fô»î'ÙIµN4¿Í ­–WÓ¾ß^™ù›Ìc€žpëc‚öoŽÃîCsÈ9šìoÒü•ç—8ܹ“kq >u/¸àýãÆoã*ÿu<~vŸq×xltã…±ù"cÊÇÎ:e²3'tÇË0YÓfñ¥OŽ÷Ôëf4¨¦ô ]çiú²:åÖ-|a8öki TÇ{¼3}µ‚¿D‰Aç“¶7EŒ9×qÞÔ¤$å¨ÌÙ69ñÈ<‘Œ'þÌwsеËúOnÍ£Ø=€&_ÎiËV`]¯Å• ¶–^ò( Yõu5¹$µ]Ó¥¥dg¤fû{Áöø¦~¹àKdzÎ`Cõ–ÚRT_k;ïà‡bñM?5˜t³$á»úÚË>Ñ~ê«ãM¶îïóÉä¸!+EIœÉ€™2«Øÿq¢¼è"”csÃkk~j›Ìeá%2Hö=V—Aì/_M¿Q{qǵßRTôZ‰¬yHÀ×÷ÅSC?#šÆcä !Iý„† RÑÅNÂÕ?êbyE_ IAË0t|u}Ìq®õ= zÄ#ñ93ç£cœÀñÊ-÷ðÚ¶Íò@G‰ßyÇH]Š®uŠ4s-T¿Ðßx£;˜ãO»Ú<´6 ÜÈ2 Luåë0<ÿq„M’¥ƒÖi¬À±%?Ívh?h0“OUDжe&¤iáƒM‹Mx±È’0ó,ÒÎR¸¸ &‰ðϸ”×/]j‡iÛݶ;ðÆÚ;ÌA­A8$8B•u0•¶æ—ƒ,ærúzµ¦í&…ÆÌ¡ÉH1OđɉL|Ò¬7¨Èæœ À^5@ßÿä]t1hcKì,Ìlc¼¸öÌ#²Ê¤è»›Îƒ´a‹¬(à›ƒWh30°…y…ÏZ; ÄT­¶×â…Dèk“µ‚õt¶ÉSrA€YCØM‡~'åB1¾ÚƬ½Æo6WʺÕ6ÒD2ꈗñÔz@*ê;¥XÙ ÅËÑ—“ÂÕž8ù¤kù¼Ñ­Žv,I·˜ú`{ÒmDÇÚïõË'9>óÙ‹Æcìÿé÷çÀï-o9qœzêÆ¾ðõñ,p<ð ýÆó<Ï}¿¹! ŒSŒþz[¾ŠŸ4¾zñåãšoÝ”öºþúïŒ]»öGðÅ^O ýbï:êÉònöW.j eŸçº¦=vàŽØÆbGn›’ëm*ýTš9àúИª—a¾Æz&èJ)Ÿ"T˜3çO!øú‡›±“ã 0ô2E¼ÄS¾ fµ;mÞgí‚>Õ½¬2.¬êÓöÍØÛ¿ï×,kòrÍXÓ}õy–ÚnßR7ßÊ ¯o=^XònµÇ#šU-mê½è"·C#ó0òJÍÞ¤ë²6â©ÈÅ pí4l âˆl6ë­z‚-}j¶,bêo®BC,™´ys íÛž¬È¨OúÝ‹ŠXÄ]\é­'…ÈdPG](ò¡{¢bÛzž4݈ÖUñS uûÁ:ÿ‹e¥‹6’ðÔž;=ëé<|‚׫,Ж]U”§•7‘ÙÍ«Xoʬ˜|”±âA`:^ƒZE¹],žyv X¨;ÿ(ÒÌ‘–ôÔ“¶ŽêKõµ’oú_¢šÜIÛ{{¹‘È•´È¢ŒXv¶IP}é˜ÑF—Æ|¢b‘ÕËŠ–RÑ÷ÓðÄ¢¢ß V?S·£ziTæ¡ñh"nKKÅÜJ¯öÕN© A‚Á`ÛµYspe|Å1ç×Ì%Ö,ÁGeí2Ždƒ¡é„Mþ’wñ5‘ôú”ŸÕÇŽ'¡W,öÛ| ع¯|ùÊñÀCOŒ÷¿÷Œ<²ñàÝ»ÆÇ?þ¡ñõ¯_5žÜóO Úw>vTõoÚ¶ùëxÛĶU͉?4¦oþh”·šØ¯ë7P¶‰þ T·Çcî^áW‘_àÑY$õÓ_vl»Þ_æþ?¥¬Ü(¿›µ½¿‘‚s3;ʭ ×>ç†ÃB}⩌IŸõì3ÏcŽ>|üàY§ó©ÐÅãúnO?õìøÖ7oÏ“¦Žâѳ>NÖûûÕ"¸"Ù—Ò¼XÔáËyWO”Ó/K‡%UOn3¯ôyé‰=fê@‰ç ª›úÄ ¦~À#JRGW0×jªñWèÄw ÆbÁôÖ£^œš~ £cÊ«,E»®Fk_a.ºÖ÷vÐ;ú“•±>Dkÿ†‹"vnǯڌ)y“=ßu)µ(S_\¶AÂݘ9IÅox‘ÁTK“eU_ƒEy/`ÊK¡ÿëØªzÅpÝRo ‰Š!u0BH+¾(™õ”m|óym)ªÎТ6”9ÏQ¥KnÃÄvþŠÙ˜êÖSä ^‹êÅ[Jð5LIìØïÝõ‰\‰n%ëÞ ZÆW¥ÑÐÁ޹>ðD l¹;…®M×ÿfY™bXÍÎ8ZV!êø²Y1äª-üp¡N<9 Ã}ŠšÊÙÆ~ôk_\i™?Ô"ÏV?ÛÇ óòbx C—$¤òÔCëø'¶ô‰ùû½’ƒô/Ô±Ó‘áwç"« DÈ[´-9 ÑÚ¦hÞùŒqȶrVêĈ¸÷ jœFf8žIºC× ÊƒêùÒªtþs_zXTù&ÀûœÈjn™DEQɸØtQÀft `’õÛ¨³¾3úítѬDlR¥W§Õ‰¡.¢Ó¤ƒ#7r¼Õž~Ïi ]Ëò¦Q¥«À¨L&^„’ñÄ%rì%—2«ã ’þÚ·ùbL°£’šÖ|9¼/®ïfp¦PÿòÄù‘Á! )¬ˆ|è9\Œbyê&gæúÊ«ñH·"â`M­—’AïÕä5½¦nyÔ-•ŽŠoÐ…”­Ïv!j´d)ÕÞÎæü°ÿ«Åfê‰#ü:hv'düº*¯H«ŸAÍJŠL’—±–q¨3¶~‡";ÅxI[«éŸñM\K1Çž|.¨8Ç<¦¯adöL§Ñ‰¦|t_;è¤ ¬§íԕö}MŽ‹8[¹Y¾1ŠéGõÁ—œÀ‚~Ÿ†jLR‹Ißš"sµt€0Zv,µ&J"ANA×lóß¹DÅO”víJEЂïŠÂµjò(~\?%ÎEµy‡ OÜh± =âõ!ã¾-ë9¡ÀÙäŠ5±âÊpv¼á±¾¿ÌIÂ>ûì'ñ=ëo¸}\zé ãØcqçÝü‚ëãÃ~×xøÑÇíwÜ>Øÿ.öhÿ¥’ƒIq ÃØZ×ç–ÕŽ@lûÅR1-O?ó ?µ+¿jìUèu‹‡Zß‚ÁñoÌMÑJúˆí÷Âù«yŒpbHŽðÓ¼ºûžwÿÌxáùr¢ò=mÿ•yˆßí÷ö<½Ê×±yàÜZÃßS{öÄþ_í+ž|¯|8PÄ“uÆ®'òHwÝxòɧǾûî3Þuþã;ß¹{œö–7ŽC=x|îó_Åþmã™gŸ_¾ðªqøa»‡zçXüIÙg‰chñÔþÐBŒØØÆç©Fœ)üz‡œ¶ZÛ{seûÌñ«ì F0»$<ˆýþšÄêº8•#¯îœ—qÌuøÎoç™±OsO'%S~¿ƒVߢ>ñÖŠÓ¹h¿ObÄ9ÇR?ÙÓ a™*þòCÀ’/A)Kw6ã ¢Qtm§¸´mÖQyé{±°mÝ ²ˆ6²jIÖíØ?+Ò·|õ…H?O>qPIíY6ÙgHFÆõÐ O/ql•õÊú#Ž±ŠŒ¹ìÉ•0ʲÕ^m´ÄžVjΜú±>wÝK¬J’c1¬jÎ\~ Ó†Dר8RyqQ³ŠZ̓üÕ/•­mó¢èšOŽ=íG—¬Hž³T¶ò3˜t‹‚bi7WgÐËGB–1ZËZ0uÄÑÞÊsNJõƒ²òéø²äÙèÓÞ‹þ‘§ûO,{O½Ê7[d¹yIþå"¥ü‰áðª[è;ò©ûœ#  ›O ¶Õ`@}CÆïR²#“X””$4YF 9—Nƒg„°§)‘M ÇÀõO)ëà+ pä-e Ò+a2£¯]ðeËÁn¢!¦Ø“K:)œ©*oìôs•A ^=¸sgN3¸Ð]4D×¢¸æôÏ'·xëO¯^nÅ¡ŒE ¯°)«Ž}OM# †F§/4§aímìDb¶Åáe”¤³vì©j‰Þ<ÈI!ÛP—¼lè8Q”‹ _œhÇ\ÛgxDÃHsÀOZ¦;V‘qO—Úa8áâß“À*9ì}å_ÿP¨µúÙì([zN1ì:¬Î2 éî@ Â+ƒ¤ñ8éŒÆ÷Ô´5•õ5“^5éÓZhGL튳ÆJÐËœ¬¿e'J³”Ó¬@mÚL¶–YûA1ÚO–ØkZ5V ö´Ä%Ïÿø¡£6jÔM›”XNK@2òitÌ4È 9}ΛZ‹žo]tºÈJ2[’q‡7#ÇkÖKÝL˜æCœcvÜi?SGn–JŒg~³MÆÜ „sÑua{N´eŸ%€‰)Vs$“êêÄtF,URh».l-ØQP¢ë…A¨0KvhÔs0Â6!šÈð†ƒžïÇàQGÂàçÆ¿xYn¹òŠëÇm·ÞÁIÂÎÜîä¾×+·h~êú˜ÇE6gýAuó‰ ²=Á Ƈ>8Þü¦7ާžzjüø}dûš×ŒG{|ì»Ï>ÄÆ«ð¤©o¾·c¾Ú'yö—¾X·¨g=_ò›msàÕjyž¬-þ^üpMÒ ÿùçŸçv—ÃLJäCc¿}öÍÉŠØKv»íï™í¿ÊïìÈ'†8âù Ïö‘Å“ ¼÷½Ÿ2zèaò¿OúKÙíù}5îvŸr8ðwì½#¿Ìë•üûì/ïæ“ž .øß 9xœpÂ1y”çEüŽÄ7¾þÍñìÓÏñÛßädçÅqìq‡;÷ŠÎËœ˜t_̈ÖUüÖs•ÕÓ¶ão®?¸ˆ8`-l”ÍšÑ ¯i¼~÷ª°›OïÔl_ÚÌ\h{IÛ·W1lnìÂuŸ4¢ñl;vô‚;íÅ ŠkŸ˜vúÊØ×_-‹iÉ:n5Ó‘ 9‘ÕÙ‘ôéò-:ð­[ˆi8º[ms¥?úßÉ[öð Æ)|bYq¦otŘQ±4ŸÛ¬&SÅè"§N¾s`æ\HôIlüË~ľ‡—Y§ ¸µ›TœÃ…Û(z¡Ï»2ƒØÄ‰2JEFœØÒ¶üö!C“¾•)êÂißQ“è-òp$4\OíÍþKeò%²ß.%äXQ7 ʰ­èS‰¯Ò•™¯ô–´9"dÈIõ£”±ÑñV´·ˆlö90ŠÝOƒÀ\3ºü¡Ðc&uš TçR³!¥’¹¬Ž¢ÉzÔo±"Ny:´|JN{¹•» ¬ZðôHÜ铺ø®~ '¶v8pͰ>WE¡îAZ³½tZqùNë«Qu ¤ÉÉÎS …´Ã_vÚ9Èf@ë\ên}U´ƒÄúL€úèzàdÛ\IWûS”Ò§–ð½¶SâykÈa‡ÂÀ½Æž§Ÿ畱?¿ëXã©§ŸïÐCá–’Ýãþà–“£óiÀý>ÀOwï9¼½5È'~è¡ ï5žåÀø9nzæÙg¹…é€\%µ_Úñ6"?Uxìñ'bÛƒø}ñáà]æöýò–#Ò?Ë!»ù±*Š;¯‡y$võû¶;þb¼ýì³s }ç]w]ñI1æÀe’¸íóà<3öø}~S÷JúsÏ?—ûååyëÓÁ4öÉ—láóì|O|váßÃ<:Ž;æ5㨣Ž7Üxcð¤ó&÷»òÁ'ædåë­Fv?_Ô=â°CÆ1Ç1ž|êéñÚ“¯ÃI㢯\†Ÿ?_îý]¾~ ß qlßwσôž<6–[¿ˆÓ8XQˆO4çD笡s¯;! 2¥^sê{HL pòûÈ•g¥ ý°[„L¡ÒªD) ”å|]u#–¼æb}–­L\uŠažÂÏúášY]¯Èû)¡¸YÓôsÚs|çä½V€r­€ïD”'¼ÒÁ’W?õG¾·;>r¼yËüÁõ⣊B@l¾IVôì†$Xœ““^!qª¯„v‹N£êÙ¦¯$aÈ—þ»Ž+SüT»ÖA¶‹adG.ØzW+æsõú`êÃ:f²±æštÛ®iÕ_ûäD\ÃXþ+/?²:Á¿õ½è,óUU(äÂt¨WüY^ßô*ý[ÐXçϾ“­`âYlÚ_Ö¬·ÿÍYÂu½¥(•õbdqÚöŠ[‰öoêw£-!®k\™¥8SšCÙbŸyí)VAtó-Ñ–7ÞN\ZÒíkc‘k‰o“—‹~à&âëøÓЋ)•Xã\ªX¢94­¥/ÁY}iÅouW"ñ]9û½M(‡sq ÿü#5[N›1qÒù1h+æ¦Ö ‰E‘â®xÅ΄ î` ® *Öõ£¶hèK ¨Í¿A[  ‰‚AkÅ3H#9ªÆw‘4šè Œ¨íe5`õà âÁ”Åw$#ã›FéêY28•€°,Ë ?¾ÌØP‹«H"!ÿ¥àøÑóÿ„ŒŒqÖÎâ/ ÛÅp“ÖÄï K¢”Ñ¢?SNg¬Ö[ì$+ÆX°êÔBÃÖ-ýBQ=_õK†júY™`H ƒwèzc.Û×Ôs°enõÄ.Q°¯#»¥+WàŒ›4$S|# „ú©#d|!†"äwÑtKisµ .sg¶v¹š©\JPŠKUºhP׿åK5ÜXÙfÑÕ8™øTì#eÖÂh$úñM5cEª`Óíù¨W_0îIþµcA€!¥yI—"\è›ü«âƦ,žì‰É´¡`ÎBK¾;8“ûqùÎ6ß9Æú´%¾IËb¦‚“K[Ö“ËÖ?'…ðµ™E±õ©Sú@Ì _¼‰MÎV?eœÀ°[Ó^ã¨Zp ËÏ I,ζ=Å_þfžBÊIºYàÅg.dûï¿“+Î<Ö‘ƒå/ÜMÑîÃ=2ÎûÛÇÛÞz:°»Æ 7Ý4¾ðÅ sEûÄãË}óßá Úƒr'yΞÉ#$¯Gu8¯Œ›nþö8ÿ¼·s`ÿ,·Ý…½ãç;ÞvÆ[s@{ó·o~á—ÆKϽ<^{òIÜ*t`>5x‘Ó?ø£?æ‰4ûáEFZòçsêò‰qäá‡ýøGùTãròÊøòE_×Þpú»ã«¡¾éo„äøæ·¾5.üÊEc7åüŸçž}?¢v_~½–'Ý@øá|jq'7/ò ¸É“ýb…þü©'ˆãÜqæÛÞ½›o¾yüy°_{òÉã rsî§¼éMì?:þø³ŸËÉÌ /òe[x|”O@Ž9f<øÐCãæo{<Ëÿ=÷ÜËö9¾ŸñîqÆé§‘É×Íã äÃØ×½öääÈOQ^xñ…ñûøG¼÷©<ÞÞ…Ðøá>gÜvÛ=ãmgž_.¾øª<¿ÿ  û¿ÿçc÷Á»8ø?Ñ—{BÂIÉK¹}|ÏÉ&Ûö¿ñvÌÁu¸2ºÌ½ãÇv²1ëLò¤F3ƒXYÆk&4|r}e3\Ñ-R»V¸Íú3ÛbÅt÷®9±ENráškWÆ…˜"f‚XmL1Ëyý`¨g©íC²ëK«kƒSOÉåŸæjÐmKרÞ9@_éSÑ럡šŸ¬ÑÔ=IqM3±-&uû.³­M±l*lÓ˜øé7Ý×(U›‚jCe0Õ³¢M±d§©N‹4uÍIòIÐÊJ‹ÌäÛ/fݾ¯E¸ðê³ò6Ò ÝvNH%ãGm‹XËñY‘°°•5éë»|YÑëÚ /zÝŠ¶ Öxƒ¶÷…JÙ_ê—IІrÁ,¦k¿XFh[/‹U£ÝNjH )ÇJ09®Œ’ã_;“œüÀX9.«|/ÜÈ×j ›¸¨9B= ¦øŽXÉ“ ªáøÚ¯©,Vs.›¶n%Äêª&!c$:íx ýÈnL_DjÓ‹aPá5Iv’_\@,ÜüC?î¨ÌÈ÷¨; €ƒ¢oà’…I0í«/ž¶mº…ߺívjq-xBLuƒå6³Xx¦aÁ‚©¬¬t,›°´¯å±NîˆÑO) Ó ¨=U‹‡†X´SÂÐ<âjžàˆ MúOS¾JÆ©-—·ÄlCî>L7…tà EéÂâ;¯ä~úåkzÔ ‰BìËÇFòªMAôT{ })ôlíÛÀÈ—B¿zE¯‹¸c»bC°òLN¦.Œ`'(¶ýCoÖÀ‘‚Ÿq…EIfLè*lE3~P÷ÄC5ýe½èË«|Ï(ÆKl9i“Ó寶5&€8Ó†Ö\,Ó–•ô½%Žt„ÖŠ™Å:àʨ©ž±5Gê¤Wâö–1ç vqµ.b‰o_ûÁJÒÓ5²8ÁŒHóªx”çÜÕÍ“¸ÎÕ€Ì8ÑïbvLì¨åªX^oâÄFÌü§GJ²"è›ÎäŠmk¹´ò£:yÀ€áê+·ÖØ'M1@–¾xÝa‘o@ú’ÈKÛ`õÃvhlôv•¤‚v<15ÄäÁá«_úõ ·§\À­;ï~×;s`z ÒïzçùãÝç¿c\~Uëo|ýëù‚'ø4àé\é?÷œsrP{Äá‡=Oí÷ßÿà8’ƒðG{l\wÓ­ã£?ú#ãçž;nºñ¦qÙe—ÓO;u¼ïÝïW^sÝxÇÛÏ?ù$þ›9q°l÷ÏžÚó4WÐÿàïÿ4_ Ý{|…ÿÇl|èƒ?œ+óöõý±qÖ™g1®¿áÆñNü=ëmgÄÆ‡~èãÃüซ«ý—^zÙxëi§óßqÞð$&WÅ°é ‘àÚöÏ+ô?ÊíLï8ï¼q'—_Žß°¿—¼è÷;ÑÿèG>ÌŸ—|ýœ>~‚““[oÿÎ8ìCÇ/þüÏåÓŒ¯^|1'/Œÿ½ŸH¾/¹ìŠñ3?õ÷ÆyçžîãÒË.Ûà^võ5äã\òñ±ä㦛nî¸ >o¿ZýuÒIÇãO8š[z®W_}Ãxœçö_|ñÕc/në9üˆÝüÐã‹1è|j.S&Ù¡âˆpürÍ«³/¤Ð2~i:ij³ÊŠ©î0‹?ç¼+‚ÓÁ¶ÿ*»QV;]g©“sVÌ`¨4µÍÁÜÒ‰~qïoMãë=Õ:KU(56Z:‘ýR¼Ñ*¸‰Ã3gl²þ1 uNzØêñˆïÊ/ ì`A[YSÎJƒœ%w.°5¦]Ê:H>ôÑž8Áfìåd…œÅ.L‘ƒ•6BÑ^ü0ÈorOSÁè ’ýÂÔ]Ç$f=yW绎=àÒáâXôeí_ê—ŸD­5¯2˜1wLéš²bSOð"óšIÍc.ikurØòg\)³úbÞ=ˆO¼BL¾›„HõÚgÖW;àȹnZ„·_ì7If¸^@Ù¦i…µ›Fûª±¯\®ïáAí>4‰¥þXK ˆ%ýšqR (ª¦ßÍ•uýFÏ’@ëT£ f¿¬¼Í”é«4_ä-ß­†—ýǺ ðìgUÒ4ÛŒ%ÁqrõÕ+\%p.$Þjƨª2’}uÿƒ2,‚"k º˜üZ¸]ÑÁ³˜&D'ª"+Ît ¨Ö'?€F¦V'WHR¢’~kÃ0 ‰/®¡¨—-ïú"ËŽÈx“ÏŸ' ¹*¬ä Ô°ó7;a)§Æ^°[ ¸Ø+qéfÚN¨òLXþæ•<ù’c𣉠•ßN£™8âÓº5#ÄÄ#žE—¬êC3pü[ygo‘ÎL®§Ï™€È(¯î*N ÝðcA½BbZ%h#ºýŽ®ÀÕc6ÇöËL¶ƒ$~¢(®?=¾)²0KÉÓw4צv2¾´Á¥.‹Õ„kl( °¾ Ci9úi«ƒ6DyÒòQrø±yÈÈ¢+9L_P˱!9ˆ±Uš|ª¾ºhÎ$$Äè×”QÞWSY]Þ¬޲¥œÙô¯EÙõEíßZÄ·9wuý1zÅq¨j^ß<¨p\¬Œi£W Èe BUß3Sæ×üh[§¬«Ç&EO­»v¼¬¼6”[‘d"“9€‚þh{óH?ãæQp™,bÊåFw†¶QÃüÅ_l¬ÜAê(ЭpÍY~L*aiÔoˆ9þô4s)šŽ¹•CdÂ¥Ÿœ;ÁF)QŠÇË+´ÆD}•È˾±ÏÄÌUÜÅR2uä2‹`#<}ê'‰Tì7xLF¾Å[Qyä±qç§Ÿzêøä§~k\ôõË80~%·Ú¸Ó8‚+åï:hÜpý Ä»·Ÿì'žpBpoºåöñ ?û3¹÷ÿ‘ÇŸÇpïÿµ×^;Þ÷ÎóÆ)§œ¼+¯¹vÜûÀÃã`®Ø{eÜÇÑÜ*t#Ÿ0üÒ¯þóqÊ_Ïswç`¼^ñÎ|òkå–¡oq2ò¿þoÿÇØóÜ ã>~A> p,wì±ÜÂóâøíßý½ñÍkoÈýõ§œò¦|²pò Çó8¨þ“?ùÌøÃÏ|~<ϧ ž¸ü£_ü‡ãž{ïÍm@/ŸÛèSïÇ÷ÖÓO{Ë8åÍoŸþß—\vå0¦C¸ÅI¿wsuÿPrqåUW‰?;øA­ƒ¹Ýè´·¼eÜy÷}ã¿ûoÿÉxìÑGÇ?ûµÿ/Û:^øüÆÿþ¯þ—ñ?¶uÚ)oÎ Ð'þí§ÆeW}s<øÈ|I÷ÐÄàAþÑGÅmB7ŒÿêŸþò8óô· o«òäÄâ³~ÿ÷¿Àm\æS‹'øµÞvq«Ðàן‘{‘—rvnÆo~"äHȘ`8¶2|©8l\'[Öûí4¬‰!'ã%çYðy Ž|dýí—Ìi}Õ~…bÓÅcÍâÁºFÀ1<^އ£[WdÚrÄk$ötÒvìiˆ–¼TŒQ‚ حʧž†¼b¨›êj_-ý`yw<ÔýBoÖ×üJÑot²ÿOÞµG.¡Õ×åW±ã*ª»Þˆ¯†ÿº”õ(v¥ n fø•§š<åùS$'W›žì§јøÊĨڑ—Ð/õæ Úr*ˆ1Ž£t=¡Apb…§uq ©#®±ÛHlq IÒ˜‹jél­N!¥ÐÍq@öÊE™þ0›tÝï&NM$–öÜ5¨“¾ì¿áËé1ÑmÎÌłʧȹ/ vx@IDAT޼ö`£ÈúœØ >s°,ÇS ‘j+Œø V‘ñk%4Xs h¶G-Œ€èØ6‹Ö&RêQâc†)R”¶p̯ÃXznãŠlÍvÿ,óž>µ?¦º¬Çä`é#Ÿºráú¾¸Tñ!tH›eÕ¯ôåÇ@ž©š§’{Läµ§¹ÊÎK ª ^¹…D©ØB¾cÍq·rµÝKXˆb‡Q‚HpõÅøÍ¢%ëÇwÝ9.ºäRnQ95ü?øw2žxòI®âŸ¹q·õì³sŸ<æÓa¤â ÔÛ…nä÷?6á×q…Û[]Ø¿\‘÷K·/r[ËQÜ*ä•ú³ßvÚxŽÛa>÷ï¿0Þü†×åï7ÒæcÁ±}#Wý…ç_üÚ/ç ýøãOóÉÀŸ]ôõqÁÿظõ–[Ǖ߼vœ:ü=Á¹’l¯â?üð#ã³öÅqæ[O#ާrBqß}÷{ï¹ïìŸ{â×AzÞ¿&·+yÏÛ9yÐo¿sàÉÊMœ¬œò¦×çàûÏ¿ø¥q:öÜévØaã¶ÛoçœyznAú7¿ù‰qüqÇò ÉQù4ánCºõÖÛÆûß÷žÜNtþùçý°ùx‘|÷¼³Þ–OR>û§Ÿg¼åÍã¨#ŽÈw$šŽ]ýÜ=ç?ÞåACNæHXvàÎMx=ˆšc0ɤ³äè¡’õ’´ „fÝ‹!W~ê;À²óp¬HVÞÒ[03hWÆvù”«k©Â–i‡ZñÌ(Oïèî°•èKjën‹ÀŒÆ×z-sM¥Ò§úª"Åo«EèA'²’7¶â¬40‚J¾[” ¥˜±_Ókßåü°ñ³£­—k?¢vö…Y®è…ø­`£qÕ~×^ó#•?tA½ò]zK¨qýÚÕs/ *¤_][ëf±¶liå-çHø"†x]S1î?~ö“àè¶ŸRÅGIzXG³nU,²PÃ*¿VÚxÕ¡Ålª7Ååíà“ÛŒ‰¾ú–Mp£ Ž'Ô“+v›ûõ^­®yÕm¯5žÄXãQÊãEójN2>Ws˜‹`1ªäÖ¤®¾‹œù†6 [›ÒŸŠ`KuMI+ñ¤6•3é'óãh‚Úæ¥ùV "íc–'¼óAÓO«1ÐX­ÉÃyÿÌ ÑU˜’9¢Ez Ù ÐÞ›8µ;% Q›öyØ2†Ãò]^Åâ@bëè’ (oàÀ2rài"Ô71MÅÍY¹Ì™`jÈ­ ö¥ 1©˜‘€‰-—¿8 Ü$Vœ‚c@È!ë6 HKLòF+Bí Ú(ô/œÚÔšô ¸ÆA=nŶlø¼¢Æ\ƒ=îHA×”idæÊÿsä¶‹BIÒ]DToþQDÙx"A%l’Á•Þ4§0tíM­© O\”‚SÛ¢÷Ï0; e´£ß6ÑÒf®Y±"!6/áÓ'ì ÅÚ^´`<9È ‹7þƒ6A”ÑîZÛ×B/oÁrŽ›ùËó}É¥yŠ|ûÀ’‰&æô×."ÆSs·q ¾~Ëwbd°8È»ðD—v¡“ÃiF‘æ+•ÚK·{u[\H‚÷`o¶!mrìuõ⩃niÛñKâVÉøWPÿõ-ÝËx¡™ºm9ȬñA34¯4çDT;êâ,HÅ‚g~Åò56q*æb'=˜¼ Öt5•éÒÌ[}Š€>‰C>ë®W*\#W‰b¿ºû¡’¹SaÝ&”þD0¾ÂkÿÆu³ '8¥éáæ$I˜êc7ò5›~ÑŽ6kôÆ\Ô[BÄ¥¬ú‰±d³™+†1˜…ʳjlæ4¡K’Ð`á©«žs'êßõ €1ûE\¿°úr½²½ûàƒƒýȣ׿îäñm®ðɽ÷{@-œ}ï•yåá ÞG€ÞÆ-0¯ç¾~ýºëî{ÇÉ'4®»þúqÙåWæ^ý=<sÿöWs°þvN(Ä{ì±'ò4}ð`vÅfžý-ýöÝwüüÏþ},mùqßý÷'¯x?ñÌ |AùPnï¹+÷ûïáK±'xü1n¹í;ã'>öããŽ;×Þýˆ>}øƒ?ħOŽ{ï`¼æ5GÏßDè:ôü ÏçKÆÇò©Âõ7\Ït.Í—wŸäh_¾D{Ýõ7æ$ɧúçÝ~4¯~ÑÖðS‹5ÀìC_ö‰ Y+¦¼õ•Ó$:ÿSaôº†Û§â(¥]û‚@Öé N®2‘U‡?5\qm³½^¬Lµs±)±!#ž °(«»ü·‘9‰?&£sµÃ#NÀWnÑÝ¿DOŠs3Àúá¤X´aΧ$^½¢x‘F@™0õË*¸æÄ‘=»³Ò£„§“ô¥—PÛÁ‚Õ|â |¢ cЕ’m‚Ü5u½PvNªÕÀùƒ–ɯOx™K9uÖŸØ2ñÃ)膤7ñŠÔêġЫkƧ´H(ß©“+áØÒ?¹[VÜfášfl×>ñtÚõÉüåà_lê›™h+—€¹.œzÓèη¹ìàè§ ] ñ­½H®;œm¢‰=}1³n±¾Gó¿©«á¢á$Šî\ lxð¨œ1§ß( ìâ.ïcзXÊ*ž l›bŸôLÖØ‹mÊ0hnÚŸ Ðzòƒœ¼‰wÞa8,ÌF&¼4ÂóΣ9ãÕk6mϺ1%?1Ußô ¾~¸p‚vú ŠSÛ} ) ìEÍëʵfÔ‹U·â/­Û¯–Ä+`›&6ǹÁf¥µí4‚9ÏFG åòŒ§~`CSü‰/Dü "ì9žò’oÝDªÌ˱c±iYøÊ¹c(DçÑÊ•ùS0ñ¤rí;±ä‘d‚.¨9?¼ê¶=AÜî³ÐÎgEëæ×?£Ë™t³’[Ÿt| ]gU!€ØÑÚöH«ýéÈfœ±eœ#߃ˆz`|†¢\|«äÔ>÷Š˜s>[êlïËÁ¶_½êºo«®¹žçÉŸ—ÇyzϽ_®}ŠVï¿öÆ[Æ?ð¾Üsã77rûŽåº¬¿GŽî7Ýz;O¤Ù?ßøõ_û•ñËÿò_g°qöYgå¾þc=†çÖ?9îðÁØÕWŸ¨ãÉ´¯|yÕGf¾û]ïÈ­:üÜ<þá?þgãò+¯á^÷#Æ·ùbíÙoy·æìæËÆwrE|ßñ=ñ„ãcÿ b8/ûºöæÛÆíßù‹|:áöc¼ûÝOšþ‹MòÆöîÙ÷1Þ²ó/þÇ_ÿê_¿Ÿ~×ÁOINäãIì<ðàÃyzÐÑ<Ýg?>Iøö·o-¿Ôì}oýÎãî{ï?ú#̧#_çV"O²Åö?ÿïuü ùx–ƒü•ãøÄÀOZîã;;ˆÝ[Ÿ¶÷õG)=š¾—Ò±u§l:¼S%cÅ‘d‘•QE®§!u°HF4ëŸ@æÅq£ºô©á¸´Þ1Í»Xv ´\ š},.ïÅdtMͯÄ+Ëá—¼.X§±S°®¸®:€¬ª©#ì_ ñSá¸à¢-lÚÊ+;_4sŒ¤­µŸU7sBæÔ³©Åà[] ÊRÅ~`þè™óYOÔQl3‡]™¡éZ.öÁŒÇ´õCØM™±ö#rÚç߾ˮE}­NPRQ™‰lú”Û8ƒ_ÛÐ'j.»võÚwô¬~) j/0°­0“_Þ²¯Púyf èü¯T㡵ŗb~ˆ¢/!s‚l¥;^F10¼u4ž¦šügl¸:u½V…Õ‰Œ ¢_Þ$cȈª¾ä¯ºE–†Œ‡³Ýÿ‹§úê|Èþkco«:_\ûµ*¢ßIÓß±VØòÁt]2!GŸ¡ùRš­SE3mÏ㡤 ”[@•Ç]8Ú¨¼ 0xIK‘}+´iå6ß`ó¶ù1„‰‰¢6:DBÌj´^6±ÖcWØ$¼2î¼<@Jds Ó4&mDœ‘. ãÑ%•ö÷w$tB›e©Kßtt¡k£·‡Ðqb)‡/žk;ÕfÞQd›ZvÀ’”•ÌÖ×´h%ؾQýPLÄQ¾WÕ!>¦Óq6ùªY|êÀRNŸ·#W¹–ìZý_%ù’&>ôŒ1ÛqÀ8•äÍ`)ÃoIŠ©¼% m%¹R2ù‚ÿ72õÕÜGË\âDNr‚ÆÔ©5³º4ÒL Y8ëNË dŒ"{‘ž¥>ƒWßjÇ„•·X*:£PÆÆÄÇE#W?`a "„TÐ>†q&n±“3+¤¦J#íDëþCÁð'\ôJB!yÁSD¡ªT›&gY`¸¨…@t–¬t@¯Ýºb"Aƒ¾²Ðºµ½Æ‚,õ: Õ<N?lO]›áM™'¦’éƒÝu*ïË<ª9Çj`õ‘J|É cÌj]èÊ›@ðb_Ú¶b‹ÒµÊF“òêE¦JÒÍPwTÓ¶S(;ÕÙŒ¿ð¢5A&&>'Våy‰“úcüÚá-ý¿ÆÍòGùè 6ýrºÑˆÏ+`I™Âù4£Rº¤¤›[m~í—þËܪr*÷µí’KÆ­qOî?óÌ3Æ?ùÏÿSù<Ägq ýÔ¸æºÇô‹g§¹·þ«W]?þ³üŸð}‚‡ÇÕ7Þ•Ç^¾ÿ}ïŸü­ßγëOç¶¢«xbУH{åþ~®æ?Í-7ÏðˆÏ=¼z"®‡\ñݱŸžãw |ªÎAãþõ_Ê‚|Ú÷âË?ƒ{õ_à >wÞywx{ž~4·=Â=ø÷?øäæ¶›ÿéWþi¾Ÿp ã´Ïnà¤åIêúüzq²é¨íäÄà™øý¾÷¾g|ⓟâ$aßqê©oßüæ5ãÎçvŸÛo¿½þq:<Î=‘1ç~!ù¿ù¯ÿ‹qÍ5ßâñ«‡“N:‘«ÿwo\ ·Ý<>ð÷ûÉßJÎýbñ•W^ɧ '÷r;•õ±Í/ðiÄì°ô“]è˜Ùé:G£CÞ1QÔñ ½c 6ö³½Ÿ!@ÅyîXLQ¨@s¬F>8S†MƒͬwYbÀv$Ö@åÕ)cÐ1îHWI À®Ë‘ëZ¥@„Âó\ÇKç†vÔˉ"Êù!ÈZkâ4,žòÚÅâN¿§î<2ÈÇ Ù &{ÐÔîa×(%ï&è&?Ú5ŸbŠ&Sv·1­/5_2ÆG¹ÄO#½È³ùV@½äÿoêÛ*%bµš8Œu ˆå‘¶Üoå˜Eìjhmƒ³òUV–›OÄãµÉ¨~‹b l­·ÄPpm'/¶$SÜÏö ¾º‚ æD?«ë!Aöç騃³M˜y°E^Q\dl,uHÁ–¹n’Ø FƒðÒ—3¤CÛâüIÿè0ÿëdEMÛŠ'ª%Ï6°ñCó6Äô0ë×ÂļØV&ÇñY#¥…‹€¹UzõŸ±8žýÓ7¹ê›(•†ç±TÇÓr¶4[¾2gµ‘àÍKÇVPM*RæbÙ6v1'Ú&¾½Ï9罿š?‘\Bñ 0ƒ°ž`¦@d¨Ï~Ù$£ ” U‘Ëâ&N›ðW]ç;q29!·@G7æLZäI&ÕÖ qi©£&Mt]ˆMjšrÊòßd{€J[™I_uš)¦Î?;ЭÈCÕvÌ4Þu–´øNG *å¨L)F“Á‡ò¯GlS·õ®)³W­ã¼ïâî»Ø/RýÒ7@ÃW~JÓ6_ñD‰bM¹´ NÕÉicúÂÖ~ò-ßóž³ÆqÇ3.ùÚãþ{y~ùû±£uÀ‡ªö_ŽúÙN?Í<Õa,Ø÷êP¢ªo¼6ùù3€ úð++|”•ŸùÍ0IÏ4âîTÁ”3*K­þ:1,ú_¿Í …ͳt«™RÆ£ô–Í 4)qˆ­1k^]ëû¸·Ù¨9Ýfä•)Ù¹²í˜1GÑ.[ãÚô­JÝäSÁÄ Mv­!¿M/žÑv'^;PtÙ¯}ænŽ›ô•a) ·½E+ÏáO?ÀTW;üiÚdhSe]fªi0Ó/f®Þ+žý8åGµÍNäkWcK&¾G³lûŒð@âKç…Òñ‘7ÇF ‚@?Èà\*#®äËrç¡JǸÔõ=[ÞR×¾þûG}-ùÊ+/Œ½wB³ò"7qÈ­(ߺîxwæÖO .äñ”ŸûÂ…|yõãÖÛnÏ‹|ò‰¹Jþ¥/_4®â ø%N ¼uåÛ·Ü2vƒñ,¿®{Ã7×ô¸–[fvîÜ{œpüñ\ï_½ø’ñ{ø'üRñqãñ'çñ˜·Œ<&ôÈ#Gð˜ÏÝ”/¾ú ï¹›gôßpWòy|çk_{2î¾2¾ôå¯ä6$¿°û;îäS‚‡Æ]<^Óöþûî‡ýgÇõà¿öø£ÆMødŒ'ð©€' W\yÕ¸üŠ«ÆCÜ“>íÆŽ_<ö÷ üÂí.ž½ï8ôQ¡û¿Ÿ&¨wñ×¾6~û÷þÝxÃëNÎ ‰'~Z¡Må½ÍÈOöpòp·úø%èÃ9ø¿›[¾ü•¯’»ÛƘ¼ø[Çc{§ùøêÅãÓ¿÷GãuäÔOü®ÃNòïþǵn«7üñ]gx;ù„€Yž~µgïÒßYŸìw_0€a8N½vìàè¨ê(qñç\Ê¥e°ÅOÝ)šfHåÅœW‰üwðFAŽO°t[[ø1תI -è©^ÿ]ºF»?ÍØÖ/þŒ[ÁìG… U»…RßËùs‚D§[mJ÷]CÖ,ÎËžk‹q8¶Œ%Òqj›°´gŒ™Ÿn]P"ǦI«ßÓJdcWÕ8‡3Ÿ¡ 6•ÄÖÚ÷Ø®†y­`ü3Ó–ž«|X- Mn 3Äé¿&gN£†Œ˜vš1ñ— ‚x·b%Дã¥_YÇÙ†—­5µû¾¶¶Œ3¯àG c¤ÖŠá±aŽg´¡¼îø·”©«sî™gŸÇ{Äxç»Îwßu߸ø¢«ùþþèÐ!æF;‰ ôõw³ º>ñBNxKlÉ«ã(Ícµ²ƒ™(¦ÂZƒ;^PÂïâ(%¶ï¨!ßœ•¿ö¹ʾA™hºÕo|žyZý q ›‹af|@ˉqx¥;_ÄJ Ð$[ÿÔïÚ@9Ó¦¢ü·>Û‹áüÐ~«·Æ¡ŠÉ±ºt\‡ç”ÑöŽ$Bt„=\g&3Ÿ0ÊSNMY•i¤ 81¶5œé4‘%TXP“è=ñyÅ"‰.Ñl œÌs¶*ålÉhЖ%:i­¶«ÛA¬ª® hà¶"£Úd#¿éøH>'ƒþÇNÞ#˜ñ–{@µ¦gI™´D$¦)üˆ1ªø†Žo9Óf&<¡,É’`)«1;ÞHRW†ªõí6%é‹Oøö•*¶-[†© [FÚs¥-D}e‡ ^À¤A 6 3Nsk÷6Ï*å­;ÇÕ2]Ddà›3‹àßË'–/^^쎉c¢âG•ê­ïªC›rµEݺþ} ǘƒ!‘¼BTws-¯Ðùœ±¨$j›R_«_Ptr!˜6µ7u²#…–…@ØS›­¹C0¹§LUõXþ„ §³ÌÊÂAŒ2£‡'ŒÒoâÌ" Δ©Ô˦Vã; y|…‘>FÅWÅÜôS-ñTtš³ +©O dg¡oÈdcb®¤wÀ𵋻FÔ»65î•ËÌãè×wcZ~H1‹Õ«¿]—‘ºéù™½âÉÔ‡® 袴Ä0Q•™¨±QžTdÕQ•J.§H6~zÔÛnþÍ'>A¯¬ïâ˽>‚sÝ~ò‰OþNž¤ãŽØûÔýUÚ9h¾qdÊøoñ$Oàd@ÚoüæoÇ‚uoÓ9á¸ã‚§Üëé÷ Þó®óse=?R5ûA Ä¿pá—Ço~êwrÒ¡Ú¸ö†›é¯½âßeW~3?f\α+¯¾&öýÑ/å?õ;/Û’a>I80WØìÃgýàÛ‚iÜøãÏüirñ}òÓñßã<ùÄã3¿Á­<ûóÃZ~Úrç]÷ðŠç¸uj'9ûæ$ꋾ–Ç”týòäÂï-üŸ¿ù[Ñó ÀÞru2Ÿ¸O¼š[«üb¯ßyx žã&Ýi›ô#Gö°sƱPZŽŒÏ>¶·3¿°?Žî×z•«¹âÉ ¸˜Ut}I=ZX!¿ÎÿŒHäSœ«Úg‚ˆ©¼óG{­C,0RŽ9'„N¡…¿ö’¾ó`¨O×CR9U±ßӢͶŸùQQ×¢-·³i;§7àëkbŒ? Pá?¯8h=P±L6 ^ü¯îwͦ…ƒNÖE@³†Îø5bî³Q×Z+5µ?ôÀÕQ+ëUc˜Kd_NŸëßôÛ´¶&®&@ô"‘ÇñÁ€„@\ÙìçЕ£ê ²d“ÞØ˜Q29¹¢¿s»“[¹f é1”]¸­íä’væf|¬éìßõÍ5Ï>‡ÔI¡)iÌ­Ñ´to]{:ï+c«9 s"Ybk_,nTæXz¤Q'.ŽSÄ^JÒ¨ØVÄø’Gê8”ìǾx”‰ãÁ¸%ù\± +ô h=ƒ$'' R£¿}~¬ãX0äXuÖjÕ4m‡Àòæg7•~#—âhGw§_×6±·¸ÿtÂÍXYÏÏþñE0iŠ*ýuÆCt’AO5ß m_³íhïfÁî^w®¨v/è6‚uÞÿîoÿƒ.T›“Øó;ñ:!ï·~ëï·në\!â FÓ¿ô£o~Æ tãRÏ|þÃßù­ïʈçÓwÿô¥ùç_ýëÓRèÇâ㔲?á—z}úïöâùžÜLŒ¹ñÂùW±çò½Éò‚;û|Úa½ý>¯xnž¸|‚ÿð˃äŸ>û•¤ïñÔÿ·ÑÓ¦zÚ56å~õWþvÞиù» ?äAŒÇ/A›ÆßŸEΛû¿ËŸ&½±¼çÃO=ŒÇïþ/{ß ˜"ê«1øÓ×Þ8*Õ?¨ãhãp´ÍkÚ @¦œk Õºc¾C·ßEs'8©P¬õì!wðL“oׯrâXÙ£.ì»UÏbòHï†;.(à6ŽwÚqN´yD¤.xžl#!àqµ®Œ35ÍÆ$]õć7?Ò¤=_mlN Ô šŸ¹„ÌVÕªÙRÝÖ ó6ÊHÉØ·±°ñÇÏöÇ qLžd”p+vsu;_‰ï»¡r#×úM/»ØòÕ˜#ä¸ÚE·ƒm>¤¹yABÏH‰Òã”Êäûtžñ5y¥gϱ(ÝÑþÅï ùûh•2btÐ_´sI;³ìPÄ1ÊÕ‰b´GŸ~×+úHÁØj£øÎ ¾GoÝ(‡w,ÔÓ77›WOÜ.ú£\!°gzblÐU2}$ê¤/¸ü–‡¤ð!$§¤JAGoóQÍIÐJ, iÍ7gžôq'5;Uñ@ç "”é ³‘7Oùú€çcŒiYº<ÿÖ>ðdƒÉqcª°ã2º¶Ì˾ҴzT×µÀÚT¿Äž/ÔG@Ú`nr>âJDv›FT¹µ' •¿Ï‚Ñ'<h±˜L:D‘þê6‡;±¦XŠpªT'Ûà•ÜúøVÆ@Ä‹O{;IŸ"Ü|P´€·@éK( \½2,Mì ½³Œ8vWEI4©T/mPŒã«¬C÷˜Ë°-¼!r?H¾ì*½íùË=pB˜ xã14ß&½¿ "òáU,ä?ìbWT0³¤î±”kßžŒà‰ÙšCÄÐ ž At¨€¸A(Õà‰²¼ær¼™9<üPf©4E@ã«>{‰ìˆGLßÔàN9*HŸˆR*=ª4î…q¿ ƒ@ÁAWfÎëopÍ„±6.×·#¦|2:tëÔqÐaŠ&×}:묆k DÃTÖwwÚгѸ“'â÷¯6ÄÌ?½ç­ªî¨E«¼^ÆKÔ¹w\ï¯dœù )wüÛÙÖ‹z{å²fRž1³ê^=ínCÐúÇÅ\ÌÏ}ù 3ŸÅ´Ý<¤–ŸER{ÒÝéó½Ð¡©¶ù©Rf8´;¸*#gLõ¡²¦óÌUqD„× À¡Ì¡á\×NHIé²1 Z•ªœÈÀPaóAN4ý5Bag 9õÊ?*PMÛ7)zÌOêA³oyßÚ¸€õ‚¾9óè9y\äãÃSŸ±ë‰µ­s¡•§KÈÿ<<éñ¶?üoô`‚Ëøè'^`ûKµßôéJ1»Åò•êmþöêµü›‡¸Èÿóþò‘˜wÓžÿÙxŸ|ý—Øœl7¿ÃÉË Æ¿$´ y0oŒ6þÊ||Cï@|ãà ÏÚnÐÁt›Ô³¶{›{Î…8$]b®H«6˜ ã Ý-yµ«ÞÎÃÖ÷ýXÓ«Ÿr§s`˜}Äé¾úúðm ß㓘íèw£w£RÔïÌè=*ðgƒ^ÊÒšÕóÙx‹Õ§ÚEQ˜;§`Ãàëokôá~¶¨Ýƒ}SäyÑS¡FuÝ&0¼7׊éø*Ž^鸲«'ñÇ͘6ÓÙhqÀ_÷5€ 6„œ×b¹ó`662ËKsÆÖÁ“JúéÚ¸8-åÊC×·rhðÅŒ}|ê"˜ã½&蟴ê«?ˆÞ\¹öá.›nËw#Ô”:òÆ·ö¹†kå—‰ÕàD•7cÝ8)e^ýÚËsbë÷Ð8œ ¿Ä´Í:k7¸žïé‹{tõ[Ë›[boÛ: § £EGßÑãÇn>,~ò!š_ÅU:|ŽŽgzÝ!éêÊçl[³õ tʱþ߸-Þs8j6b®†<ÐÓS{yoå2—7åõOCØÔDRuÃpwo°®'87žçßt¼º"nãÒô1L"“fš·©Ó?ÉIžÀ¼j‡zŒ@ð"Í”²A ¸éyê¦6=…p±;Q:@ “î~Ñì__»‰‘³³W bpÒõ#pªn~m æEûäÙùvLúe˜Nºü.<…¡¿{*æ œ<œXËºŠ¢m7ùÉÌç© BM$ëèƒíjñ;Ѻ89<Ÿs%ÿ,Rƒ.á™»}Ï»‹|”Õ?6+˜“´TF€<ï»ëˆC¸²Ì‚cnBÈx:è®”êÈ57‰ a×M_ÎPäÞó”Q“RÀvc~è´¨ùbMñª§=t÷Ñ'ØŽ4ÌV^«L5ØØ=–Âqµ§®z`€0925:6·ÖfžnMˆ›‡0‚˜)ÚÌ õ0/ÊI(g> (´Ä2RiX¬ñŠM€-‘ìÎ6†ù˜‚nûhM.ù›·B|¤„Ü< ±\Μ¸³Õ ø18|»¾õv{²ìÉÆ ÃѦï¼³ !ö:qå¯;󤯾^GÕ­?^Éf#"rêÝÎÞŒQ™íg6%1‘×›°‹[Í Ê¡oÛ…Mì8JíÓY´7žæ‹‡ëÇݪw4È~‰xj¸>±7CWéíøîOÉxçÓ!ñÂY¼ã}£mBïööˆ¿Ä׉Ê˶1û¤Ü¯àÚ7}º`÷ø†IoXï1ˆã×}üªÍ»åÞ—_ñ<]8ŒîBõ¾ûéÛ6Nÿ!?oÅò¢ÏÒ·û— ý¬­Q\+ 9²Oyoßa7&…–;ÛnÔŠ&(Žê£,ˆa_c«5×ú«õ÷¶WoâgK×D¶ãÎgûžKüBv•º~ÉÏ?t‘I7©aV£Èg–c_{€_¼ï9o<ž³Ý:]cÞÙ)f}iS@¢¾D:ô’wÛˆÐ4þnîõu‰ Bd»ÃTy'‚l–Ãß‘¹3vj¹ÜsM M‡ º “$]»ß<´ÄAŒsøsbú*ª¯o~¤sv¨Gúðëá }­¥\Šã(è_Èå[M1rž§ã"“žªÝÌæhæ]þp,ŽbBÖõ±¯¹)xTŒÙm¨fÛq¶ïú'’ñåÿµ ÿ¡0ç¶°®••Gö ~v_ö7ªÒ4ßD¡]爣¶k¯óÀw7l2ø9ŠôUÒ» u—÷ÖÀÊmº©KŽV’ʤѕ;%>Wáš“ÜT2ÅU-Ýü)_çw{š0Z7(öærÞ4ݱj˜r¾èLã-Çæ‹7Lftw¹Y×7ŽÎ¦*çþq,‘%bÉüPQqNµÜÁ7ÈýYª¡_ÿLÓ’²E±HÆï¶è59îÇ—åGÿ3¬»|eI·SbgV¼3•Ø.Šcy–îÞ!ÛŬ¶ÅãÅ1»â¹¹P4ÖúŶmͪ@¯ï½SlªÝ_ÌRt›‹±ªh £äEºâ$8žˆ‹ÖˆW¼I ¯NŸ  >šàíD1œ†Wƒg«ßŠyìÀ»¾‰¯7Jçƒã ^|ýz`´-Ë ¡ðñ…f7Fò´wuèìİñ–ï À×F•fL´ÔCC¶L8¾Ö¥À:¹,¸ðµ¥d“nºjŠëQEQ1¹ö’í+ôƹ²*_ýòÂH@s¼:¡‚XÝ#k¾9¸êÏü˜yµ8ðe!h'/uôÄ69ºº°›'ÛØ'Ó;+kDh“¯G4ü3fE•Ÿ:rýTRv¬ E’u­-˜#´]x1¨íýâRuoìÖÈ*k™Ø¹ uh{_ÄSκcÜc1RÇõ`Ève ùy Ô¢å¼¼$Ö$5=SÝ´\Hw—¯@Ó5²óÓõVºÆœŽæÆY+×Ê:žD׈œýb´"G'}¸ Ð¤% m± s-Úþxn31Ž{r¼k|OD·}’NCšÁ„}tOWÙF–þÎèÆ"|äE»ÑÕs-8:ùÃNèMöã?ïf²5I˜«%}Nÿò;³×Z’ñÏ :-OÇûáÝ+é|·~õÁ—?rÞgýËž>z~CŸäi¨Ûu”t<°}íƒey½VŸÉ*Ãæ~ë¥ík c<™ÒñhgUeÔ’b»Íñ®3ê²<ÖÆÈö¢5a–}_ÙƒÚ/ߢ~N?Ï|ûB7%'¼.ÜÑ›—έ˜5l"ãƒÞi³‰ˆó˱Ìïü„†h×–âèOtëJLÐ$«÷:ëI\„ê(¤{•¸Æô,¾B…5ßÛåN4¶eáÄ¢'nÙôXƒ¥Y¾ïÆ ó˜ âór¢¢´‘¸(Yê¤Ú“–YÌT1õo= ÁuR3XñóÝN:×…«|‰ú ˜‹‘¹³%¶¬m0âkÅ=[²[ó„UùʘçpsQ4¿l!ÈVßÑèf€:û„×N ޵…åt®úË)b?~jGyÍûîFTyMø†o×±°[ûÅ„|øŠÀlÜ”êGts7EÛ+€3– ãHI)Ú {XÄҌï%ÔÖØ¡U{G­5^hÅÈØÐ÷òÝMÿz’ÏN“{’Òãs^“Këß­u Yu«ñ;mÈÓ–`»8JÒ~·|þkôï¯ÛÞÿ^h juï\qä¬ ÕÔ­#*Á&xGÜúéœßÙ9cëxL·”³ †0áx\ƒ/jÔŸ5ú]¨DF1æ‘™’*Ñï\g (!ödNp±ÅÑÉ­„r¼ÖÜÀºx²˜i¯‹ÞüA/ÿôÍ!œžúåÌÆÁØäPÂ`õaNsÑÏÈ£¢€>ž¥ë8² ×Ùq¯%myboti¾xtP´]úä ÒG_\<€£¡ž¼6å úC;̓vTÐ#·ü¿-t¶x@8úý’!N좢íËTÜœöäè.´³ƒ¸˜ÚBN·å7Ö¶m°éÚDÄÁ£.ègaÒVh['žìl_^BJ¥¼ÂLëøÿæ,$—LzøG^üdIamCß¶1äA¼ü™ÌDã_º`©‹ÚàôžpÄÑüq¾h‰ yaòþÜ@¬‚RV %Ev¨tv3žþçYXÖ€}ì$îâ@`» yÃRGÈ´–^·Èš×¤G}u» “þëX7Na§ÂD좳ÉÙâì²€Ž^ž¬X{Í‘a«¾íD†¢c³“ ²ø ]/|*gß_¤Ü…šŒ}s2_`[©t碌/ï±—Gr•éüT;ÊYà)“Jšüºh¶]¯<5*¤ž¾¤“/bml5´æáëx¾<£´Âд—{Bæ»C§,?Òf­\Šcïö¿ÐØ8[ñmÖ ótuºqmìÿ =2««jb篘̋s´†|úØ:¸IJUÉBÊOÓ¼„Ð (C1 [û×–}Ú«ÁÍE«Z#™gg]F’ã̈öå+ ®Çùâ’¤o9 ·þ&Mˆ¿AÓž¬ôØÍ¾çÌÎ>LA²$®Fz⯌J[ƒ°À>Íз+›BÄÞ9@.[FŠø–æõ|xy®‚þÍpjúl<&£¼Ë…Ôß–™ÛFÖÕ¢í¨]‘ÔX[þzìù©žG%È‹6¬ ^Ó;ª§?ëÖ¡º;‡«³œ-f-mc½Ä/e ·µVTi Øž?]'Ð>ÎqÀR9S×ö"8Àé;<-±‚DcdPÇŽúRt$9ÐëÎ I]7hO@ä,! @Éc,lœã¨é>¦ÌK²xÕ¹ÃsX‡ÇqÌÚvþ˜è-’Õ qwQdÆ¥Ž[Lxß¹_jΓDßÍäkhðÑófh£Êƒvt÷hÝvu-[ñ^»Æ o€ø%`=ÃNÞº+ œ7Ñ`fü¬¾jÂÄ"Pÿ_QacÖÒ´G)C¯Iž´ؤ‰ååŅ̃XAS±±Õ·!8Å•†‰V”TèG'Ÿ-·Ã¢A4v.ƒc¿œDNŒs¡„j²Wç”Ïh‡70$ÍM/ñÓÌД‡—}vŒÅxæÔ¶¬œÐ/ÇI?œ¶Íé²uôMÂt•iLU̇`ÜA@®Kµh" G[›–üÚmêtâ(ã¹)ŽB«[m«Ð¼Z ´\¿š°Èè¨ ÏEµ_8õ –h³¯™C¦Ü Ô_Û€ŸßÚH ùÔgºZQݺÞ&]Çô 6k¹¤£ç Gs‡¯Nc%ÈÙÒ§= ½xà&º7cÙ78òœyÖV4æƒÐôGÿÐw:g”–qüéú{ZßÅm';ó¬È¥5~„r»nß›¶Õ‘¢3Xd&¬e€ÔrJÃûÌÉüä"Fô¯^Dc Pw~ÍŸi äÀ3‡eé;6^õštœÙõ¬Ck4°…‚ó“,¸)Ú ë—Ûå½ÔѪ#扫˜ V[pïVWßštsÓ˜&µ¸ŒÙS¢õ!ûeûDœ¼¸YMñ=E=¹Þm~Ì–¶Ç€ˆ»èy¾JßA3`%ò]Z˜Zj½ƒâ»6TzxnûÓïo]ð²V¯"m.РE¯9)ßð‘К¬ëî4*Ïì™ÚDNÉ{³qé8†ž«§›ö•±Ý Ïã¹îXåêlŽ?!鬪*³X±I.ñÁøâ™D8½ì‡¾£‹”Óh1kO[†“]X¨ëæ''ÎI¿;ºTf¡¯o!o&ûV@œ#‘‹„ íÌ«<ïúH,t ,¾ŸÆ`W§ü,ãàðt–AÈAL\}ä­¨ÈM¸æ3[ È±Ù÷gƒ-åËé2ùh÷´öÈËÁžüÝy.AÃCÎl=”éBƒ¯A}>‰2Ù$u\#Ô'ô!ô‘GcÓ¦±µÒþêÌBåUYm)\nVŠ¢$eÍv²7õþÝõ]´«ÒÜ¿-µ•¢R&ZEø6Ü9‘¨Ò5ÒK;‡&JZˆLJuv2užÀ9ìc=œrf sâå/‰Ò ŒÏqº¿D)Œ~øî{r *g>øbg2”ëk¿U`98âæ°1±ï§º•N_@!JSºÍÏÕ¡:Οц&øðÃ޾gUjøX=m#”⥃±®—r¾èoôW8×ÎE,1Õ6G2ù±‹ãÒé†Ýºl‡^µ ¦vŠ/!v­¯BX#Gvp݉½ü±*"sPh,(»26c¯•3—„Ol +Js# ëÈH—àx7tUSݹQÛúÅgeÈ^òÔ•ï¶ÜÌï«Tþè(W.8>_u;åsÄã˜aZ%WS¶Äl91æ¬çšI’yç¡£ŸõÛæh6‘r °ïù#vöÿ’ ” Â¸¶Xâäª#C{õh3{ÎŒ× !Mc»5¡_ý-2]ØÒ–%ïì-»*Ñ ž@ ¾äiÕDõb£bªb£ÀÁ@WÄ(H•OË G×E_M?-Žr *äô”e»t…Ù\¼fE¾ñÓ ]z³W\mÓßO®é×Пm"¹)o¦ƒ® 4/€U­›º›hÉ›ÅK¤ø‘kæe°Û‹{ ­èé›o¶öúŠ€‹’nä«sy@nÙ‹ Íe4k„=½tJ¬hJ#eŒb.>y96ïnÖ;âéÁÛäa®:’&Ú ;·ӇsñC_òΘ.@ï8ÈÉ—ÚJ€±‹§J“R–åd9A•X,Z›P§ ìô@Ò$>eKŸ¤ÈÚоâòY -Yl ëé?€ÙÑRyzð¥\¢ bÁhµ+Ë‹\Ç\Ú±y¿€$=è“ïâ³®•“á±½¨¶æYmðFfŽùÑ1èM—oÅìbtÇÁ±ÑãtüEA -ñk œ<‡Lð¬Ó?Òí¸šÀ ÔØ[”u3BåBSsáê+´ù¦Ôæ­NÔjÍß=Ôàho]À13n¨ª÷Y,óåá :çBr†|cjðÓókƒç÷-ÀønûkÞyÉßßX=;žÌU8±%êÀûÃÛ­ã&½s"hѵÞZÓ\Ï-;«{Á²‰«‚s1êh»hê òœÀíF¥Ê¼vh=Ó÷³à—ÏWá±µá>X­³OMpb8L y‘cÐÍÙLÐÂÖïËÇI¤ØÜÇå à¨å*6ý‡­>zú/y`cH û '¾«¨&È­­b—"€#:Ò½xÚ¼=ÖëlîÏÛax޲ŸôÃÕoÞ]ûÀ±&úÄT ù‘ í÷1µË-Ÿ8_y Û}ñn ¯OYŒ¥Áž'6ÅRJ,_ †úqÿbzÌ „4ä.ÝqKºæÿ†äÍâÊ:ÃaXNñŨ6–«oÔxõÚÚΈ1Õ%ƒw0ÄQC+Uïª+Ñ×ySõS$w´‡dg[ÐâøTžWç.„Í[ßrpn0{Ó‚à°c8#[ڔǧHÚJ7>~Љz}RE^ƒt}D9_ò|{>´e;zc°ºÛX‡rð§×9ô ìú.er†,?ü#0½wÐ9)jÀ„jN•+n|´¼:Çô…uW %Γ­nûö£õ{WbMŽT«þ6Cé"‘c°ð-{é^é‚îÒ…A:&NL;HuÙVRøÈƒ¼^‡¤™¶òqì"?UðÏàO~`®Œ©HùâH%e¦[Ñ"™I­ÁEñ"Ê·§˜à Š(&vô+{­¶¨Îz¸e U;!ª i=L¡ž>E½VÇ6o1²Ekº«Šñõ'9©÷´$€ºï“T¨ØvÌyY›øû^¥ñ-¿Ë³)›¤hóÇ‹ßþü©ýƒY1#™îulûæ×´Š¥«úcLÆñG®jµ¦èX..&ž>›#碲O“ŽoÝ‹hÇ‹Ýhç>¹ó©ÑÈ`ºS§xgͪٯ@iß:Ô‡ÊÎã­e¤Ë±ACw?!|ÕŽ€þÀXnÇ–¾ueùHM Œm|°F?¼”'ë'qÃ…O ú¢‘Rì½Zè*éùKƲü"ö¥½ÿÉ9í./ÃÛ¹jþ}5GsZlb)Ê! +KÿlfÄ“"„Õ1Ufy=ã ñÙò¥ìµ×®Zâ™›:3ˆ–¨’äuÓÏ`õ;ŠË;ôï—pã@IDATü(O“wj¾ ;ß±ãþ¾FòɇÏX/–g¾Ûþ&dÀºùô“O©Æ×A·f:pôÁBU±yà¸[ïÕô]ä#³ÂAv<+Éۃ ”üµ?ףΠ(û§”ÝÚgÓuà °ú|:£ªç…؈Z³¿æÄA:XÑðKïâ†A:ŸÁ2DMæÍ}«@ÿª_yRÓ=Ú®#¢ñ¥­hyT/Å6foûôîÌßÙ&T¡ð«¼fqvi.W`´¾i÷˜JETå/¨1\‹Ñ­ ß6¢­ö`”gc“ÇKÚÖ=y‚}ŠUãÞï1)—¬ö`û.Fè¶”ÓÆÖ¼ƒ)G'ú8 šêÙXý,+ú=»yÓøŠì¶õS¿8‘,!)>¥÷$b¾wޱ» ª³=õ‚fü#m=9Òòqòæ"’Ác³ñƒï9±z*¡á)çùd2¶ŸkŠÜx’ àpŒß:^k:zaF*yÇÈò€‚nÖè¨AÌÏÚ•Å|áÀ{|‰Q[÷ã地ï¼ëLrŸ†¤Z0Æ{ì4xØ2'iôÔ„ÌÏF!#µcÑKˆ‰º·£×ÈÚU·9³HDRG<¿±Ú–µ¯F0 ÚÇ¿.`ÐtçÀ8 ŠO®voP’ašÊ{G¥¨€¡oÐ˅ňrˆ˜øM(0”Ÿ öå¯{”í¦k"u·$H<Ùi¢è‡<Œ³ì|¥Ñwv»#•*ÏH´¯I}-µí诫‘þ‡l›¦“ƒƒÛåØ.ûØ¡ßEÉz¸…¢þÚû÷•»å¼š\”dÃ]>¡t7H·Ø¼~úØ‹'x­ë°ÍÚØCŸp‘ò¬*ÏL-³ÈOÃͰä‡m Žøû>4(–ßB_dýPYeÒ³¯¼èbJ+O’˜)ûð¥(níì"nr‚À'MûvëbVN6g'ûBà“¶|™êÕà]NÌHtq –FuÀŽ÷#I—mçÔ.^ÚÆ æ’yšŸ&I›n[‡Ô®Âå% ;XC}'DDAÛŽÃ|PR ¾=9ÿ¬«¡æœÌ^t¢m Ð7÷—Ë$Œíú prá‹C¿“ÀšŠN@†J®K‘> åKoÞH4ͯããÅY'Î$Ò1·ÓÔÿr@䯫 ¯År¡ï&ÄCAC‹ý¿w¶µ`¼BNwöØÏ¼ú0]{>áâ‡?øÑ‡?ÿìOùÏ´?Åö椲 ­ñÝþ"ŽìÆÐ¯l}ï“|øÁ÷ø N@ …EÃ1·V;6ÈÖ LTÕ¾toôý^®5tiN&Û–z²Õv€ìœS0ª={ÎÙ[wG¾[“&Úïp±>òßZoô@î<ÖÆ˜î[×÷4àÿ¬ç¼KϘôýb¥ÑÏäõ‘.moZÐ%bé`´.6?‘sÁШŽöZ7Q剫9½á¥Èä‹\ØâcÇM;Ê.±^ßLHvz^ w9rÈvƒó€˜Õ|§¨MmCZY¶{O?? Y‹.±ãóÀ\Ó…tÖ·84 5æðo·55Á#›Z×òpºÑ*åŒÄH‰º^],yŵ±W kö`5ɽỏ£³é¬}³xÓ×Cdzè—]äxÍÓElv”tñÇmÍ^®*ÍØe«Ýšd­ÿO„·Žd %*râü §“K…º×¦„™™×¦ú¯X“÷ôµ ^Xó×}V`¨âø§aç^È e;¯keß?óõ ¤,eª-ØwX<Õ‘:¼õ[«Ê·]{ôwþtL[ïŸj*CVvóÊÞV°)So‹CAŸÀ6,ˆV e8èÃч@5ðÃÕâCå{Q ý© dÕ6š-.<ñEÀ€73°”sÔ¯I¯¨åCZè ¬„täòiŒV ~¹sk’dæ¼§±ÿ• eó0œž¤ÄÄ¿“ðUl4×wdº•qñËçé8<_YöÀÖ¡ï጖}É.Žn"oƒÑŠ%‚5·êÞåÀ_¡ïkCñQÑ‹rÀæ"l x‰5÷ór+ôÁg1ú”ÿÐùÉ÷¬ôLŒX|M@áXí@jºhž¿{º¢ŒîØÔ›ÐÜd ë€râŒÙBÍUÍ —rµšÀ+úê(¡ÍØaêã– xÙÑx99¡*ÕEVÝÔéÚÀ£íÓΉ>ﲇ°ôˆsý|†Ñâ;ÙÝ OÖâh3Ÿ>õÔ/)ƒÝÖ† VßMCàå¶ jÚMþcËu,o!kôÓíë椘Õ÷I©ë~3Š ?KÛ“§üt¼µ…ŠÒÖ¥ù qàÿÝLm\N(ûëE/žùY—|VØNÀÎQâXž•Q€78>¹úÔ#NþÉOþøÃ_åĆÔwÛ/N[ ‚%ˆ‹ÿO?ýðk¿üë~ô£øøSV9˜{Â=­éÍŠF:¢úÞÄ;æ}zf ¨gÅYÖ/¼J“n2Pä?(ÎAkŠ‚ÊöÉ$„ž´Ž@o}ÜœP˜óœ6M¹îµ#Ñå `së“M}Ä?çGþ êm›OÆ7ÿ,Ùú¬€¿éÿ«ç|tžfRÿÞG¹9Ë‹I_22šÈmÊRAi)×qkçEMô䉧+élÚÌOv猒öœErK3’b¤R{7V¯s›—kˆ65Åÿ»~ÖµÎÊÆò³|´ŽX£±4c®ºqa±Ì,}ãÌ×ÚÇït!¤“« ú­7úm›ƒ«L_Ý”êk}|!†¨ë¯X»K²ºÊ,ã¦[ûƸœsª¡ö¿ÿ=®>éO(„g#-ê‹/ÞÛ6æ@=w¨sÎ Êßmã3ÏÍtÂ;—hî^ó8ìkÓsŒþÖ­Í›Ø0<_u­‰M¿²m|Ùаq ªGø·ŒCŽ…:á)ø‰SãÊ×zP8‰F°``«1Âñà:B®È÷ýf‹<@@8Y íZã[Z›ªâšG½òÁâB‘Ƨ]¬ÁÌ='ƒ¦exÍ™Ä=…íï: 4Ã8)‘-²4û‹ž¥mÉÍÉñp^%KrF›¹ ®4ö]0Òv`¥ åN#4O1§ÏÎ>õûèUu¶Í-ýÓ¢Øð‚ðùetݳ8ªV᳋'/îÌ#þlÏîWü§ÛÏöÙ‡ÿù§_~ø£?þ£Ÿ}ùç|µ„±7„3‹Î±/>ïï¶ÿ÷ØÌÁŽ5tÆù ÖîòÔÿïþÚo0öŸ}ø³Ÿü÷êÌY°_äNœz°ÊΆnS…n7ð:—ZƒYM«}ZÔoë.²®uŸPß^3·æ4×Ö®†+t —š²žÔ¥ÛE»s¤Z>sçØVÀú¹l pq:G©2ü8Kº¤;|ëPœˆÖº‹Ê¹Ž×`¯=HÈ¡ŒÛमæÅXOŠmµ+–7Æ/)Úšªc$ì²=_±Ê€8ØüØ¿nŽ&¾µ¦ëÚ(P)ÙÞ@)4Û&í4H×޸„ AýÌÛja}e2rüw­HÚßÚh|‰OžÉttíØºð ºò¿Ç&«—ÅqÖŒÔØå3Ç¡Îæ¬³ÖËD/½µj @Ùp]£´:h,§þÊ,çôõB£{þMNp?åFøÿëÿøð»¿ûg¼ÏŸ”Í/í8¾Q´9ÜbÔ)7Ž6«q×¼ÜW{8…âÜô*T\Ör¶ÇóúcvL”)ýj<ö³ªì‰yî°OX]t­Ê„ôÉXÂ! ÛÐOôð'6މºs”šôÃk¿Œ=:ͳâöžŸ* &W.Y#4Ý´1p;am<¤ÿáúµYþ =·#¿èº{• ãɰò*:öi¿ñ*.„”Û@1 Ê¥!‘w ØQUÜ&ë û ]ôŠä“®¦@ÆÄ»†:Èâ-¨¯Ay:‚€ÉÓï†K›'ÁËQäîäº`Š;â­ˆh)Ç£¶7¡œ@Êh Þñaú`+¯»`(Ÿ½Ë»ÛÌ*òb“™pu5®4Úæ}i‘ðïÈK73ˆ%·±‚ºŸôµ•ÿД3Î.ú'ýÖ?Êæ'-$ÄÉñÿ“ôá7~óï|øÁ~HÌ­ÖÜŒ‚-<}Ó7÷“¹áDy“KìMî ¥u£|•ñE|átôÙžï«i“~BÐb±[r`^¼ù™m„š€t6ÆQæZöõX¹ ±7£ÐO=ý,¯y|HÕ­HŽ­ª7ŽhIK‹qx“½üÌÕ92Œ·œ(/ˆük‰kô ×îl]O@êÿ‘ ‰óÇ!_œ—Ü‹~ó7¤a+gþ¬CZ8Ñ”ºÛ°§wéyŒHq#xe^3Îù(u£2,)wÎåÄUñŒ´H†2öË¿p€ÈoÕDv¾^ø,­#¤ÿØé‹/¿øðùOúáÏ~ú§>ÿüsn°?ï¢Rì[ƒ¶¿Ûþz2Ðèœô+ßÿþxÚù}žúÿRkŸkb_ÿysç¯ÕóÞVd3ì0žÚ~Íe*¯¶Ë¶:\û…i«š À‡ ï³ÖLs4«R–‡‹퀎&Å ·µóWæD6ÃÿšSÃí}îõÏ¥eulqlîôK6»=Õ£SÿÉT2»ñ@¾¸ê °X¼Ãt»E‘U}WDûÚeú1K#7qÄ'(~Šê]©ƒ(ô=Y± ´ñ°`¼p¼™1Æ!2<²ÍqúgÿüŸ ôÝö]¾ËÀÿå 8SöùÏ>|ñ…o? âIœÍ}3šFSûhûçl­òT~ß. ´­!ïÌӞ鿨û&úóF}ój~î_áÆã;ÐÏÿ2ߎïòßè ñí¾ ï´Ó~‘\WYÿ¸ðiç÷¾÷½Ž×öwÇï2ð]Þ2ÀŽsÅIåS)½†‚âU‹Ç]æ0©º@ÝÚà‹×µšr®÷šÆ–×-ºî’µk¯÷ºÒƒ§jÔsÝÒÃLD»¾zÓÝ'Î/Y%òÕë§¼Ø^¯õ¹¯®‚í:ÜŹ¾`ÃkÙœÊq¬xÝ V]®¥lûßˊžAè§2즦åÓáàõ–ë™—bÝÜц”íµ¼v<˜´ü`$ ”¤Ö¯¬€MˆáV H~:â7-v÷ñ(Ο43XÞí +œÿ\À'ð}¥G«]¨3 " ñ*’ˆv®oðSgWuÎ4k_9’ŠŠp=¤©®ѨV0 Øó g èJ¹Þ~ø“¿ú*bOY»ûÀ‚ñõä3<õ,Š=¯ •—nnCŒÇ‹7~'+.jÉ]ݼ‚ñ’¥åŒk!Œ®$ Þð˜ív¥42dHÙô+Ä“s¬Ô‡qqÃ)»ù0ñȯ¯aLú+>öþå_ù[~ÿ÷ÿý‡Ÿr·ÿ%_YÀü6]bËçŒOwŽŒ—óŽmŒ¦Ú…¸R¯#œ+ÐÑ[F<òn\Åå Cï†Êݾiãà>Ì"}ùþr„€Éc³ÜÂÈ »:ã%p쯭ïdwLRŒôÞiO‡Ž9ç9…“««$è¡OçøŸvøj'4‘³—®ý7#/:\®”y=0/óCЪõ4;oãÙ¸_”Ó?rÊ<$pÁ k“‰ïíKŒÆî÷Ö.~e3uñõÏÚ¦BÞdg9Aš0´›ÿ§=*:1Ž_¶Ãã© Õƒ>ì:ñ¯ìG‰låÿŒÝ(ìQÄÍ&Á¼¿çÀ»¡+<•»ÏvPîQ&x³šgLæÙºî Aµ—ê«õNUö¯ÞÔs;qÝä_ò˜°!”KpÜôbÖ.õåÙ‹òøóîâeÿe´Ë{?^fòój_,dôÍR.—åï%ö$íÁDv#.²ë­Ù<»üΑÑÞ·÷*"|··Ô±¤Ý=†è(þ蜾Ä+œüDBNvr‰\´·NMH#&án\ŽÄÉe¯¶4ù!I~¶÷È!^Ûo|i[GÍþ©jJÁãÈ ?šÑÖ~Ï£zñE›1lsëõجŒJÓÉØ;-òŒ>kßùøw11 Y4îæ+üxD™oåL›¹©^ßEU¾v£Ó Òhß·Û›²ûgMLlz5aZ ŸðuÉÿøþ× ?êÁ²ë˜êÍc(…·.wiíÊB¶O ¸Pñ:°ˆ<>ÒXB¯«軰ݸ'^ùÕí Ú\ãÉ®2}ûÖFWO./o* }¯-s«‘ñÅ ôæJ r]‹M¯³¾1L^W"çuÉݪÙlAó™R•G–¤,Ö«ˆØÅ6$Í»·›1ÌQ{£]Î Ü|ù öí©ê¥w:[šlÇhO§mâÕAôqT§ö5°×ÕÞŜԴž“YP²„jÑwäмÆÕêݹŽG%ØÂð+}Zp Q‡Ôo½Q‚›eÈöM;Y:GUXé]L.!AJSçÊ•.®»ñHwè·ÎûåDÁp§kn-“ãbÚ†<‰“Oo®ú=ˆpz%£5øÇ\.`\  _ ²6=‹ šŽ;.“µ€SDKÅ&­í9ü5{ÿÃïýÞ¿ûð/ÿÅ¿ã†_ñv—‹ï Ⱦú/ Žñò‰ß¿òÓ#£è_Üå⃈»Õ ã´ÿ¥“Fp’mM =z¹À'… Dy‹Ì§òZàédkäùåsu2*&~¨ŽŒý9¦üôu‹>£·Å„qдØäÇq¶fòº§ùÇ>œ¾«§¹‚N!úÑ\ Ñé–Àøˆ¯(°ÅÃxùXS§4ÁÁ¥ ² æ›óPsÓï4Ä·´ÓXè|ÖXÔ‘î/œî»·™+NãåFF!dª@ú-ÎøEÖ aO'ͳq ìWYÏrežñË鯺 –Žxn;Ö¹ Õv›9÷-¦É‘qÄ륭V.®cJft V#G[ßïöþiWŸ`¸6ÞסbÔ¾@"ÕÜÐ)Žpõ;cÅØ/>ËsÒ—‡N‹ux'Nt›C`ûÅðNxæ´ÅåÓ4ÿü‚è;-õã´}s™ú:Œyó¤æéÒÚòĦ‹{X`.é0Ê¢ß?&QJÙ Á?¹´6ô×ïT·³8bk'¯ ËÖ‰ ^sWÿ ï#íû»ê£+ž*úE.üýõ‡"¨¹¡'/ãÐÄr#[>4ö>ò·­É¾›¯¾¶g¿—4©U™þà#zbWSè”g «slŠ?%Õ·F¹&iÞ­Äyw~7É|‹[âÜñ×åŬOlì§AK^æÙˆ§lçûÖûûé: .ã·u =Æ«§š_À3 6}ÿ‹ì0¸O·©©;×ü…Ö¾Ú€ì]»N玳sÇ?Êa.ÍÓW}¾Æ–]ùæŠj#ç^‚wÇÍø—üâ嘕ÇìöÃ×sl¦;6dÕqÄ7JöüâÇìWãòú.=º@‹Þ¹G |ÓkÃÑÛ8Ì®êjm0_*Þ1~·¥ôë‘£qjØ…/ÍÌO×ïÀ3FÅá‹_4*Z³Ö8á´ðÐÄ Ç<Kck)8Èû—dÌÇ6æpìç×Éãl-fW…šÏ0Ôlý‚æŸ ­–t)9™ú|r±5‘9 Ív/ãQ޾¾¹öÿê¯ýøÃü£ÖŒ‚ÙÓªøêæ°1ã«¶ÇÃ+XÖ™y…‘^ò…´6è7ú­?Ž]´“ºŽó °H6pŸÅ·X“c³ª°q9~üÊâjZ*´Ö6ø¢¨¯Üråè\+úãvÝî§ócf¡Ã!§…FÀ“^ŽHf9 à (7ŸõͼOCÝ´Ó””^Gqý“‰æ%hŸ¦B„9¡6 ôW_}C¦“¾t• OT{Ø$2"éLâÅR±NYgʳ¸iØ@³¾ “>i)ãè-.m´éÆ,·Y±­5üê`R¿,¶N´úoð‘M™{GØ M5f‰FÈ©åd½Ã¤šXÜ4ÛŽ%¸Š³cR€æµQ·¡9 YNªê[E“ ×V'$aš3ÛbÝÜ@sõzíÂ%N·`Á/×E´ × fJÀ‰G®€¬Àû¥ÿðÃù$ÀIàÉønJ悜q‘(OÅuGH)éú¨ [c¨Ÿ.Oáfjµ’<_ŽéöÖŒ”Åâb+¦yØbpbŸó@À,žY1 µ¬…Dç£Ø£_8dGË—u{â¦ÿ O´í{k¤ÁíÔ«õœ?#¶ßN©}Ò*vºxÒoTô£ŸŒ?/Çår…Óïs–˜üd<]*‡¼p+˜(=¹åHÄùÒñø qó™äÀp®©â_.Çûü b= æg,âOJ‰ üM1q!g|J/Jëg½áQ^j±¡tÞ‹©–qÒTn'–wiHyÁ8XôéBca äÅM_/ôºúTMõ U%ÇK›;ï!ÓØNžž9‰wN|uª`ÞËyƇı;œ&7çC®¨à&É6ïü®9’¶náõoá8~àӘˬ~#$íVð徸΃À l-y+b)ýÔºÃ%½†´ä‚f§ûI^“ºéšÓ….½û}Weï×X%óQ÷´GLý=lºwkN4Ö¦j6­èy™{½UD^²¶«»Úrà”¨'·\ª¿‰«lz9v° ÛÞµÏ]ÐY4èG>Wo³µs¡u ÿ ƒ ðÇ6]Î/]O_—/þºó%ùÈ©²|ê¯zY_œpïyws+ýól.ÑñGÌjOœÙGĘøšoqå•uCŽÍYߎ­Ÿ0½®­ˆl·ÎìkRúvL•*TÅi)É ›Ü¡.í”Ê×u ÍŸ°‘Ñ^7NÑÇÓÚ®m€G[Ô]BÚó<§óú Jze…~”ä[K‘Öž3Å k»îfÖÑë•Oëöà_Ÿ4uì‰àö¬S´M{õgÛLWc ]?ikAw¢É#.Ùáʶoo¬¼aéaËé·F¢¿µcò ÇW6 îØP¶^¼ ,÷‹QœšyEyg/Í&á¤Í¢’ƒ›dçÄÐ5âð«›“£Õ5Xw‚¸ö--týd{k˜4!Ûh$ V7ÇПå!9—ËÆdÂsWû' Tö­%­asýÚÌ»£\þ«Q¯<)«—wüã ]þIˆ˜ÙÆH“׋ˆ€ÜoS/Ûî pœìæìdûºNµuÐAyYxZƒY€zû1 ¿a–àħsÕíå2!v#ÁO¦Äº`6Û ÝÏ;ÃØd@ÿÄT=ÀM¬²»ÈœïikGzž…ÉøäFMÚ-²I±c[ŠT@“ÚÁêT ñ˜9»Ø´µMVÐms0N³#-0«_ñOÔMd]Â*¥¢®4 éIà”+aþ{üÙy©-òE›»`ÒÇ’>³¦º‰tm#˜’VUL›áL7?ÒÛ ¾Y–Põ?|à&…‰5bµ·'qÕu‚ƒÓ,gçõ‹cbdY@Z·/ Ñf{¯6åýrt³#’^ì+ªD1ˆ¥Dáâ!g]øæÇZ °»µÑ&œ=¤øºÿ0‰má­J;ÏÉ‹3°y»7‘Šì‰xÒ[óëñY—=µ[d†n991—ô4Zíé‡}êº46×&å¶+v7SÊŸ1(³=²'¨­}zE—3ç¾ô(b4§¦ztáê›ßƒÍ_Vm»šCY-kÎ-=PtÙ¶¡Ô¸ ŽÞ]Ia¾iHòQ–óN4ÊÜ„ óu+׿ ~†"¿ŸE'NމɦŒÍ¯½hW‰ÎæG×EíB¼ë­Vù²™ä5Pa© ÏzÕ÷º×6Ÿ³±XÅÆ$ñj¤‹ùwœZO´—Èš*fÛ*ï€`k¾Iß üÍÖé©“_Œ‡MÚ_â{3Üb±*5ÝÒÛåÜ®öEVþKkåôsPN×9õóa±)§~¾Þ™ê†gµ ÇVwñIäö8`ØsHݸØÔ*×|ÚŠ$“-)ÓOwœËúéXëHî¼²èñbI,ŒÉ»7H%&ï'æÆvk`qBóŒÛ¾|ˆÔ_™Aß¶?ÝVø‘]zèzþº~drÊŒ¾\#X^‘G©ØŠ\:þ àÆÑÿƒR9¸ÅÞŸƒ‡™å› ,bã!yx´ÜI_«=Xyy}eåµ·ÕZ+N×yõ, Žû4ZœÒIß:oÑ4û²RèðëRœÙ9 ý4'uå*s|;&‚Ó´2åUŠUµZ“<Þ©Ñ#{SåØð YèaOŒ1»ƒå$VÚ»¹sö¤9p‚¢<0‰^øyÈWÈ`G ïf×EMZq:ðhÁi¾þèK˜çâq¢ê£gü*ä ô†~κéˆgÔ}âhÒ©‡ ²/½ù vØkLõE"nSâx›£µ<ä3~•É´¯œyéo5Žþóc8’£ãÍŸÿZÕ D³ó&dÝé÷©ÿü&"ž'zÀ¦Mî'MÅŽ šÑõ¶:ÀÛ¬V³tãE–J_û©ì¾t”P²“àòiÛª´Ž|,«)dãÛ[¾›ƒâO‡Y+w„š4 «‹ý9ò:R-AÐ÷ã’¤hX›ëRà»°Äžƒ|Þ („6åä.÷Ôôà#+·=ùož;PoṴ́Å6Ç{k_Vp¸“¿ë!¯­cˆ|å˜VI2a,^ÿ´£HñdÁÅ2¼üØo]R7 $àuA1¡t÷A˜§àùqÈ[lüĽù‘îIK«ÍwmlÀŽ7õ溇°ŠQé ð‡Ä9„Õ)|5¸Ýø!~÷µÇÂ6$ó~Ÿ.Sy³žrÙ¹±Îøk‹ ?Ù¡:çG·5Õüƒïlüê^àÂ7­w›¾¹ ÇZcmëC>Ñ”¥-sÛe‘O„ú«]­‰Õ| qÀ=ùÕºÚ7öÍÕWÍ9Îb‹Ë…^9Æyà¡ }‹Á.¯À9DßzlÎh4×yøãñWµ Ì£©Ï/ó¯ ±zœþóäSj¾aÕÅ¢€\}逑öÅ;ÇØ1òF¿‹XMLã4.Ö¥^_L 6ºØÚgÃJìülNÍF°— ¨•;Ôî|ˆ§råÛܰ¹¯E¿¨}ðtddxƒ%_[ŤÕp±˜P¡õ !ðæh‹nø×ž²´¡9èE©ïZ†ä×fW#PJ8ý&½É×ÖÁSmkùl¦§Íìñp¬ {£¢¾²Š¸?MkYB:þ8Æ£ol¦;Ÿ§9'‚JÝ<ù²¨Ì‡DØÑ_;ЩK£•YŒun%桪2´“¥ðTl¼‘äF'UMéGã8à©X–xw,iB2®¬ lŽÊ‡æ.aëÚœEN}…-PIbóº²è¡ã⦤Œ08Ún> SÞ3˜m&/ ë_›zê ¥6~¬É­û6àm3bò« B… ËÔ_œ”'ÇÔP“·ÒnYòd"IÇ|³Q—º žÐpÊbFÊ…šä,X¤„Wý›œìÈѤ‚ŸÍÄ“½æÙÎññ6(³»ÔEüòu’Ÿñ™ÆøŠ,sÎÒ¼Éïúß|T0ZÆ'Z oŒO± Ф¯s(6˜B”§â7^}—}‡åD’9(VÚ7Ýrþ±K×|‰•2ã0t˸"+s†Ù\ƒn´}—SÛ‚¿PÝ¥ª/ }÷»UãšL„¥J”‘oÐàmüfsÝëE"îÑ56ƒLGim-Fqh™?MYí5­ò.-Ý9ÕEBE¨þx5P2&m•"x}mõýô¥–5éø[“Ç : ˆ7J³©E›qÝÎavb,WâvêEð£ss\îÄ5p;)•hëkcb#_Ë£Tíû¢uyåâøoµ· ¦>#{R‚1ã{sVËð÷´Ã`ôåÊìPqý]ô ÷aUÞØ•wŽùG'¿óçh—$Úv‘õ¸§EëfAz¬’RNÄS¥üptJ·\*í¦¢lÞÖ…R-äˆ×(ñ¡{ãQ;<øºè…^£!œ¹Ó²¾ÚF ¼ÚOQÝUkÉ¢Â_~xÅ-ŽdiµîɰÆ2¿= ’sfmCvãØÜÑn‰F³>¥tžÍ2„Êhð YöU¥æ›k_ƒ¼Ù8*Ÿ YŸ 6'6&ÚÞÌÜ‹Å;7˜#ÛéÃG°uÿø϶Ra(¹yv<ØØHVØ`æ¬ßMGã9öt ùåG¦ lÒ3—R¢ŠËf.ܺ¶ôò=á·Ø‘sçÄnBES­æòÕfˆ9¥”ˆL¿2Ô`¥›´sqE믇 ˜,ûçÓ>ÿJе“ãß +„>;•‡àö}ŸÀKr, DwVïæ¤å$¯«1Vr‚Mß Ç/% cl'Ÿ^„GˆMŸ:JåÇ1nu³Œl÷ÍG}P÷<]þÔ±å¸ÎKù›7~]—_•g§£½›÷q„ž¿«»|šRl¹=9ÈqF ¦áWªŽé0N²k z9V4=i=ÖÀ!6sºÈAæØÖg7³ çÖ’¸¡rìÚãœ)\…{'sœR;¬ëhC<éúÐ5Š}Ú  Õ´ï9ø*›§®¡`;6Jºuà>£[8Ú}…ÎZ¸k„U$÷CÒ®¾¹…ªßt=؈îó44Wáæ~'T2ïÂè†g(Í0qóúR3‘šfœ¿"û—}¢—ku¶vu%CKذ.mïA×0·&Ó†o 1Ê-ÕÝ dqÞ매Z+ŒQ!í({¾"¦<œfYT¦H‰¤¹yâ7pyñw\óìa÷¹ÊñOß7'Ò€Ûæäˆ·€xîE¤›M’Î-ŒðABå1çÕæ^ºÂúã:ë<Ë èñ5”Ô,ê¤‘Ï Ž…Ð…,Þä[ñµÅ‹¬z|_»)€6ÅÙ‘',öRƒåÞBzHÆ!;>"Èta¡.Œ[ ét]UÇñ×À¾ÃhÇ –ÙÔùéÆ%ChŒm“š#â >“ýâ?»`)Yæ;á|4Sû1SxB·cíklµÂÑR,v–¦Lßld& SP£Ç{Éîf‰ ºP‚IæHáåN&ßÉ¥ö#èdìó^¥l¼rNžpÓ9Rö£@¶KEBVžnë‹c«ölå:~I©àPêWeœøï˜*²<ÀD!¸^\¤ÑZä§lqŸì/¿òÀ½qžÄdùÐOd|“£Í|¹( L_úuBjнœÄÐA);ܺ¨_°êž¸C;Fñd¹8Ê)8Ú÷Uè³›ÈÆ]”#¾Nd¶çãñ"=!{Ögw7¶¡¤£|92@ðüJK5Q?õ`êfdºÍk!åFgëDq<í¯æ'7qçªÒĶnœ‡®cÏœ†n_‹Êe“–'È6§Ïñø&D»M’Gܱi ‹2×ßäõ§¹DÁsÜ7"g*¨yúõ¤w6m9<ævÄ>ö"]G¥# s¯ïåiìzweõCÛÇoeòPí¡$|o°}±®Yk›”*,ú¹\ÌWåÉ ´xȫ⮓,GO–ÑÝIDÇ<X®OÓxų®Z³QŽg§ÒŠ÷ÈhN7>ù¯—Ã1uÜA^ŒùEª–Å[pi4éÐøÑ/ž{M¤ëš eŒ ¹lh¯Ñµ(E´°®ï“ƒúKmÇ0Ÿ$ÕÞk¹p!“lÀóéð=!u1y\Ô¶Ûn€ÀÐ=v¦lÏ©ËuýK!Q&`¬¢«Ö‰GßT„ Ö\•+MžDÓQ®ùí~¥æ‰D9&Ùš_ØÙ“…õa ï‰"øÈ Xs¨Ç Pˆ×¼©“=…èçÅ€àii~oL'Ÿ÷Í+1ÍéòÀ6¿ ‚D¾öµ¨¯‰àÇ\ôµLåb¥;*WÔ »=9sîʕߟ¬=yܜΠ¯Í­NÊà›cÛ£|Œå»ã¶–U _£¢•»ü1G‡æñð»Ð±¿ –€z €zvPT¾“„޳°³‘’þlrÅÏWZF«°~ôË¡ê΀­ÅáHJ÷yñÉÁ´b¬ëæ¾s¬Sü”ÑNÞqÚÖýÛKöÜ››+)Ž‚/ÿ:QöwwGíùrKºL#ø*Å%c3@ŒYÊÿbFM«Ä®d“D¢S¾q&c´ýü°Éëú=ÜŸ7BC<åîäX=_ýU–;?ONvŽC^8xqµ¬íÓ_Ï«“y¾`ó}-²0ÔﻀÅí_]Ñ<+¨Õz"ô­£†Wm×cð†1ñ·‘÷Ž÷l(—òÕ$ÉlÌvÈáY nsÔèD7)E˜Õ€a= Ú¹@u¹¹]Õ %Ô$æ¯ZS«³úK—MlŽ¿µ_M¼@àÏò°òÈeCÞ ÏGr”»ã“ŸN(¢YïŸÄ™JçDñÄ¥­/ŒO0r¨/Z‹š+zúÍH]oßy™bŸ$º|¿ôXaL6ÊŽ4ºðN ±”ëéb¡gðåBù½’Í1W¡ÓÆ ØÊÍiIíÖ’¸ÇlxstÓ?üioŽûE_T›-uœ5È•Sç Tð›ÖbfëŽÕé¨wok:1Ý ¢ŽXˆæÍÝaåô˜®G›¯ú™™”Ž.½ë\3(vrbÓÕ ȪU²'ߎløÖWy›0tq·û}X«‹ûL;ÖêLýÁzÕ¯c• ìž>~Zk}ª›£ú‡çÚ'žgƒ77_³^Ïï\XP‹C_´í ¨¨£æ„6¿L 5…¦y„#=k˜l¶ B"r 4óNÙ½é8¯DfÀ £òâ*Ïkö.Íþ´L <í6éÐËØÚíI¨2ðÑBP¿´+LÂ<˜Öð5iÅyo®=ûr:,pëEß(+MeÓ®¹´¯~x'âxßEŒÍ‰'¦•¦^KžÐ澸|ëý=)™ö¼_’¥mËCÉÉI Ì 6³ ¿Ô¬zùNÃ4jWÓdO'¢^\0”ç¥ ÷B µ@NÃBÀï­/oÊ“FÒ<Ú^7=ûÆýhÊWî(å_j7¶ÍÙDÐ;p*ÝŸÚKÃÉA7Q’_Ò™”R8Fioþ¾ÚËþ¼´§öh`Ì1ßupù¥×Äx¡ÁLjçΗ/xµtÏ^nà.¯[#ÌWÞ°îºî9æ¾&‡|woêvjaåñ×"ŠúÕݵDjƒ„_» –æ<½L·{´íÔ|ÖæPÓÏ‚¹q-GÞ Þ;ÏT›ä«e.”½Q?Î+‚>.q¤¡=:Æ0Y19ùûýèWb'Ðêdù×Ãб=íŒÑE¥.êéù§~GÓ1äM’kÂ6tæ¹a<ÎsÇM{9›° öÐÉ1g·ú„ÚÉ{~6†GSüo úCgÒØ0î0¯­‚46´¥L “¹’Š,¿7´(RgáÉ!ýp€²€,;açúÂ݉#ŸŸù:›zkþ6GP@Vw£ÛÆ%1]Û»µR¤wâ³'MÏÚM¼TçƒJ¥@£‘:~ÝkQ?ü±nÒi¿ŽÜó3,:] óé‚ØÆ¡€1¸ÍõÕs¸×G4ÚtEç-΄ÝgÏXW™&G_5rì”&…}K>bvwmÊ.mûÓ³a{UyŸšëóÆC¦ Êè™ £ÝëªaÉÛ;ãG”6CrK‰†~<³àh¿˜Ö=‘XhôÔc*噯¡8Wª3¤K^Bå¤Ü× dnìŽÃ ˜ðsõêE²ñ&‚nˆWmä¿•7lIΪHê.ˆ7Û®Mê»6Þó‡7);¿Ÿ#“Xïãùƒ«^F3EO[ ®þBÐN¿“çÃÍ…Hêñ§e ¢„ wˈG÷[Û= ©1-÷ZQ˜I]Fèntsúbnü®Gè!ûÜ™á±IR¹DÙ韊É`“–²´7*ùs“Lûøpûû97 r¶’Gû@«$¦¤ÎÊ2I[Z£ÕšöYAú½Ï±$ñý? ž\ r}Huî—¯»H(o!WÄSjàEƒo|ª&K#ûðÏùåx•g‚±9–ú‡oô]äÊ€ÏII°ä:°«ì“µ-Ž~iËÒ\Ï<¸áct¿;j7ñ*wu’‚Ö‰,‡Ò2ÉÒ?²¥Â £´òAïnÝíiÛÉ~1(¨„xϨ¬_Òú æôÝ&{s8’ºyw;c0oÃÝIOlnûð×mHÐpÔ¸8·ÀÕ<Ù@‚a|PÂIí*–Ëu¤3 ó!ÈÉGí7v¾ ‰ˆØ¹£|Û™c åíiÛ+>¿Â¤²yö5¼ Û”VTÛðO))Ñ6kà ß]^êàfvå rc¢=ÛñPúµX ˆÖNtî/‡JnmY+'éü´]JËW™È÷µÏ¨ëÇœ8R¡Šhm¿Œë‘z q˜¶óýÄ£ ‹sç.yó M‚ý?WÑ¥){wwÒ0䩹äPû*¸¥¸f]xþ$¢Ïø›(„6:Aµ"NXÍÁ#»¯Ï û¨ÇÑIÝÃÄùÔØkÏ÷õKÜÌ °:yä»5GÑÉûýæœÐO0·>ÊD©yY#=í6¿´ÍÛÛ Í翆H‘‰xοÒrŒ£ºLã_²ôºÎöøâà(gí4ð3ažX¾h¦^}cSˆ­ð¦£/Õ€(þüòuP¤§`Œ ™å½&÷m<Ô¡,ƒ×j€=°ç5¬m1m(‹@†ŠmùslꟋ¥Üû„N¬üÎŽúËÿøšøæÓ^ðÚøE¦%l`ëß}MâŒå K 7aƒ?}&{nNsLêjXIê•–+/Ð``(=ã·îUò‰¢øŒï÷¯”—Ým¬K8«7t²( båÓ=mOc÷t7:´.Í•x›? ¿Æ\\¡ûé¶ ¨_‹ðÁ²›O°¬ÝÕˆù¾>È4HúÉe˜¶'#Û‘Õ;kv‚tUÃCÜUÅ0Zü]¼ŠöϪÒ6ŽNÊÚU­  |¸/Og =*10B1Ö— ·¯<œøªLÏë ~ÚÊ+9{â2p‹9ÝwYã7×bŠ<;å@Ç2ÏK$æþ.^¦£á+ëÍWâê—,ƒÓIË|ÄÅŽ4sžnûÙB$oÕ—ÇkRë+©³©†»ƒÝøÓ7G¶‰ýò"ªñ‘5w](Ó®öO.µµ .k^SbðÑ|OIÕs´yk?æm¢«¡b>ö¦óLÿV?Æ¡•oÇÔññ9Âqr€‡QÞ-^hû$ 5ÁœÓ#·´òmÞ\xÜ…â|±V ¥ÃxÊ4¼$ÛÖ'ëN\óÊ!kY»õ­yžz€Í–X«É“5ÆCV9ôÝÿ["N_#ÃzvIpTó"ù¢ÚW©Ò€¨ÝN.[÷êJƸ/mßs’…‰~ö® ÆZÝ ,¿•“sÖXôãÎ×ú(E;yKG `ʦݔ¼Ù—^d`–q4Ðbç¸íK°ÒôAÛæBdu]ÍvžHZ¬tåßcùBq‹c~‰Œ~u,’›zrÿtkŸþ°nŠ©öâO4'\¹óA=åä âiû`SRuI«OC4?Ë„|ƒv,!ÜÚSÛ¸ŒD¾ÿçHž[ó^A·ãÈâYßpíïÙ…–¿¡"£ªùån}ió¿|Gcçxꋵq•«œ§ãßFFƒÖ@ç#AWq7cÊ÷—‰Óˆî¼RËó%ùsw6릚DÛŸbÖ¢™šžõrk©µòÈ‘ØJW[¶¬AœðšÉ\g±çœâ3oÛähC€u6©ÛBóF«ùr {JCEb»cš64gÿ§+Ráop N/€ÌŠ»_ÿˆA•ÿL˜ŒLnÍ ·G°”O¢4Ê®Në܃9‡éäSZ4U´´ô#J}ùlg²+ÓI„‘PÆXwáD[˜ô/”2äÀ ô ]Ñ™“.Õ8`ÚöJt|AGAÛß ’öNŽhŒ0ˆ„É',úHGˆå ´gC ò`q~…{¸údu&)›H“£ØIïÈš^ŠJdÁ¡ª¯\Ýpˆ¤aÞ"ŒZ¿ @«'¨bŒŠ?jZèR–§|@R6qsh’Ù‘†CNº  š6·¾»Ú¢n*ª€Lh.¤çÓš0 —]d¬±ý¯ bãbjS/‹à`fŽÐ.jºèfžâ…F¾õfëNvúνniDiCÛ·¸Ú›o2¥G¬½ ¬W«]*žl*ë»M»4èç?{u"@ó‚×vÿôªvæ%â3rÒxûTUý [VÀ#ÜùÙ B‘CNßОP½ŽŒ¹AÇ„÷³Í°QŽh×U¬,m\"›RУ®®jÃlµÑñ—„×ÈõY¾²7·ŒùÓƒ ëK¿x™Ó6lŠÒ_ŽJîä<7׈¹ʇ°’5ß´ú˜V4eGšÎ‰+ýg æç|ånú£Ïøy}=:zÙ<žÎ¤5É Ó~‚U>tœoiîÄmìbHü¬V#ÒW]1×׆?maeEÖ0Í·òËìÄÝ)lÅsO!ÚÝ )¿nuÔÔo,–S5}_;öœÿEÖÛ(ÆÎãH²;3ýþO¼Ýs#"A•¿»´K"ñ“H€¥*ûøt?hˆH¦W‰›8ý­ƒÖ@z€4¶ÅÑ~µ&™(žŒSµl5~5¥Reâøjº7ããxm±®‚n|8i~{þï§»­}cÊS"vâcß¶³j×öê4q.ÆÑÆœô;;ðF•VÍ3g—×4qZA²)âù•“óåØzS–‡0úh¬ÜfÍë<¡á5¥_³Fƒž×“–ÊÞ¯®ºfÄ\ZöЖÏë,÷Ë]{cÐ~2ëqC«¯uÐ_®Î¡Y,ÆbŒQzM¨Õ~•)Ç Æ2"Ø÷×b5ð³cKþûë?Ø# ¢ÄÅ“šÙ?8Ã÷–WõPí\k/±1fDQ¼¾6·ã±£jvôË;{k°z®Ö‚NV] ®=9jNò­sðËW/ÃF2‹Æö>Q9ºtWCfÍ\ó‡«óH¿!6>÷™ñ@졇X{> ~ý+g'ínLf»‡\ló îXyš£i?†Íˆþ#¥EE»âéİÆED›èG@·È»Ø¦ÂÁ„´¼øp†8¿O±—8rš¶ÆÍ< ©ÀÞ³XÉ딉‹…Ýñ°>3  |¸mDì“ìÞŸëÓT³¾ð¹ECš‡8È ¿•Á6Pþ“kš´‘ׯ±P‹½@cÖIª:3–³òžƒgH÷T‰Ú"Ð[s7¾W³¸†«/XEu¡lAE4#Ñh8¿¼¶áÉï"‘T ®£`=â?ˆ]:oä…"ÚÆƒäÁ¡€Æëj2a‰+õW[œëi[žŒ½hÑ·ŽRš;zü7Ÿ—)~Ž÷)¹ÊË%°=v˜}B‰ñ—»¢Åmßq¬§ÇdþåCßpH~Ç]¸1PìŽÑøf,ÏhlŒóÜ{!˜C5„}7+óÇA]žMŸ‚ÕEXþc§j˜qzs$šñå}¤6SÉ;î`Ìú|TS7ï4ˆß¯µÌR@u4ù¾¡8ùx°ºêànUêÖ>ÚÖàóÓnßKPóâ ÷z]›^gC^E`°5-=ÜíøeýÕôaBA”Y{ÑÔ·¬ÎRi>7ì°ä<狘Î÷`6 Õ =ÙÜÍ´vôª‰áÐëú?\›¤]ð{Î]ê”p+ÄåÔíÜ¡ ×>{å¾Ì¡&|ÌsÁã·Ô_ ,a–ˆÞƒ‘Ö…°^êÛåB–(äÒÄU_y§¦Íj?u= l@ž%ä'ª››Èb–Õ3 ™6èòÊÙ3µ/yÅ‘>üöÓŸþ©1HËK{yx6Úš¾Vl ý‰S &×Òn^úô§˜ooD 16ÜiòÉòƒgžÑ›i%C¾¾\l®81¨ÞŒ‚K9‹c§†-~shm(êk¾zµ~µ=0ÇßÚMüf4dDý@½¼\°}sØ"i>ôÐÂWus @~+¿èY˜ÏãîÙÚ¾ÿÌLè_³:èå굜‰ãà‡ìr´®¢Ùn”˜kõÃˇCê§Ó¯b³7 ºÌϹäC†HRcÓ±¿ëÏ\´B†ÂÚûa•1VïTTÐ8ί6zn¼.lÎÉÑàòÖæZ˜YÔz¼ø9G½¹ÿxíÝ8J­É btž1¾ýÍé‹"Tü Gç]h»Éj´YlS̘|¤^jãRšÈ¬Ú">&Œ]Œjñ¤Ó~U¢ hÊÞ¦øÆ.ì–ó•ñÙV@ú“]Þµa=é'vc»ð€žµÙ&s¸3ç8ù[£j×ð#¶µé›ó{C÷æ4Ž7×­†ŒÇ¡’ÉÃdU,g·ÙÑ¿ýI¨Ãçno¸þ<¾˜âYTÕïøŽñ(²]o (ÂÖU€”™úEóÀXÍÞà9W®ZϹº`Û_©(¨ÅaU©vlÀ†Ûž#Á6nP Ð?hO ­5×í{’‹-¨,>;sl¿È,¦—ç,ªI‚ C« ÅøÕS;õ­—öôýjÄ~ô\’ë?²9=œµjgA­”ë@n#§ØÃPÏu~Jj]dŽšêHõ99ÿCMãa5SÉE¹uèykç­Ë÷&\o×òý"ÇæÂ¦°ëÚQúvø­¬ÇŽ(Žo¹ÂoµÐKê¢}(ÇËËss+ûáÙ³ cà@_΀&Ç:ôSmt•±eÃEéM†8Æ•«¶qÐP†ãT PfÿZGæ…¼ÚÛ¥ï7?‹!öꙟ¢LÕ;r.(Ø´¿±ß çycxúa§XÖÍJ Úžº eSë·o$_^ TX©jGoyy­N®}t¹Îú 0©˜ãoÿ &²æ°8ÄÀÀ˜•ˆÃtrc=³wøÆ0Úe¨Œ~­ßóyÏóßÚܵý[ÿŠ¥`(í?;÷ªƒ?çµÂ–ïj”F™ÜÝÓm*±&¨ùä7xêÄZ“S87?? ¦-´OlìæÓú•bµTO^ÖUDÃÅý`þrù áúà/†ÝÃä«ßòžÎ£b(ð^ÚѺî®ïþ×E9Óo1Ž“kl׈ð‘-†X6Ê‘¨RᥜO¦³’¡¯åì·÷Í'üCüÅkØžuƒf¬GºÊlB|íZÂŒÞ)öÏDzËp-Z2ÍTíkfÒ”ä¼(qfè?ó÷Ét×\qðá9Ñ ‹ß„[ ñü_ ƒ˜ƒ[Ì©·-¯Ÿ™ e ÝüCŸ…Çà¶ºŽ«“ìÄš¹Z}}°¯—ž‹úoŒO˜hU5¡œYrÕGš §õ{2£;'Æ£ŽÆÚ2:Ö‰ÝçB1ċכ<ç–oÎ×nñþø) Ó¶–M^b×’šÓfã|&F7²Ùý8ÉQzüÞ¯¬ø€¤ç ±é,¢òôPhcò’~n®ïn69¦Ó®¯³×¯jU”¶#&#Dìôɪ¾ñ|Á©ñyµÆ¨‹èðË›äì±–ä6ù:Ó‚¾õø|Þ†?çR"kÎÑo}ú _h"¢[ü$ôUEðtv—ŸºzS ò¥k”üÆThÆñž5nµ'7–×Ô`æújÌÏo¹*4SʵMX<Ú2–;rLÞµ˜£“n×­FŠ=q…C¸i;B{=;ç9±¶A|þOž¿`r3EÏ/ãlý7#·‹ }oÍœíâz² o‚ºXœƒØ!½>¾JÉsEp4éö yíÕŽÛ úテÖ[Æ1îâyqÍû2í/À½ñâ*ߺÕ0| u5Ö>K:«­“¦ª€ù"vÊí—gM[3·/#„蛯Ïvæ:hؼûkz~)ò=©OÕ–šø¥b¥F¤¼¹×Ö sÿ¨žÄýn°íþÁ'zù€þ@Â~Ãh/ž¦zÙ¬¹¯VçßZ~zç _bÞ<`ÔhËFÝ»OokõÌj$²7΂;”£y/Æã[U$gMæ’›ÕÌ{ð« -9qXŽt¬5µt‰÷aÔ‡|úhÏ·/‘̵~j-ê åñÁÀ`¼V…Ü"»zèÕ^aLähz æ<´Ïeþg=h¢µF‡Ö¹…²q^ïÇéÅØ c*1ß&,·±(PõsH^{‡xó+ï]kxÑߪ¦|.ÚÕKðÿ`0Šj÷É Ñ“‚2õ.­¨©±jßYŸé={ex}Lký?NÏ¢ÌÇ!ýÖ͸áo/®hæðf‹óqÒçÕ}ñd)ŠXò‘§ë»F‹¹HÖ÷Z|fÑ¿_k½‚’|1„aH°…ôqÞž°Ÿâ" Ió•ÅqzÏc7»GÂSñ<3£G[ó¼îI´%.²ïõirùrnGþ€ðþÂô.™råF_ãÝ[èO~õ !, »=Èàì\:ïÇEƒËÞ \úª6ñrhtÅŒ+Ñ‹ñ±+lHúÌVäšÊ8s™ÒïÓϼTmÚ4ÑO MWä“›ƒYõkH弫߹¾ÿ¯Ošu|0h3´ºŒŸ°.N9˜‰äÚS#Õàv£âVdδ^+Þù‹±@ñ÷Iž¦º„5¾]ùeþæÐ¦6º°]næÛ±8³ÐMÜÚc+oxn‰í?ŽWO¹Ôò^2í€B—\í{CŽŽzSH=Ì"ÎÕr¨ •›PÕ}70=•‡Å~}ùª¤Å.ÿÒÚùÓ3lÍ(ï¡áóB¢ÌÖJµº*±¥‘¨}NÂgïÀžõãK¬m ¡kNÓF'ÿþñì;íPÎÓˆè§Á|ªo|¼vs ™:ymþÅ[’7C»Þï½Ç0Tb¶Í.ó‚Ñ›Î{À[s,šœ«õK‹²œÞyr‡µ¬ÒQ‡þê"æÓ™@×V¯B®F}ÞÕ¤ýfÄZЇ%ó {z8+ÌçƒÌQæ±N¶â*{ë¼Ë3 ØÃ"Žø'Bý¦9׿e¨øê䣴ýµµ*¦Co{7²‡çÈ9n<Þ®ƒN/†@t9Q™ãÑZM6„þÓ|a¹ hígd*2:cZÇÎùê»7ãÊrÃøËnúEµ§ò¹+÷ksª×v®çúQê`õá_¾QñlóøŠåO^bÃa«BìÕ¬{¹XÏ3çÚÑëÖ™  hçBà»:Ó1–Šø(ß¶ØÇiñ6ÈÞ®œ½¹ùט¼Ç™÷Û>vÁ{ÀàÚ»W$ Ö—/­>Ëëì“krõÔÑÎ׫¯÷óÝÙù¥‡™×ÿþÑ·W2¾ß=?®å×›Slí¶ì˜‹ÓW.! ¹Ïj‚ê;ÚS–œõà % „^Æ ˜;2‡ß3‹Žuxø `{×Ã_îj¬É€—çžo°Â>¾ÍÃ5×_y±òòõ“ë›—Û¼ym&aõrî÷æ ¹ ¸¥§Ý׸þhòäúÇ3¾³Œ£1³;®a /¶kõÛ÷å.° ³yµ<¢cvÃ4Ÿ œe]=!Û~"¦¿æë¿a›…×k¡TÕoX˜â»ÂìšXþµ7[×ì9ËëÇåp4:~æ&îj€Ð ‘ff§G¶fåœkOìrdì›>kÇL"£µfWW ©í³ ô­ ÈÍ?cü±µàa ¿îŒÄ$c‘±]spÌù߯]àbÙ¯ÿé·àu#ã&;ÁM¦‡S'Ÿ¯ò ˆ´z’ÆXŽ ìÔÏ-×ã9IIàÙ3(1%öÝ$´Û¥4»be¸I4„cµí×@ö§â¼²xÜÆŽÁøh=¹¤ÛÀ1 I49 ôUäƒÄ6—Ml?Îo7[î³éFE_?9+w}2¥³¥y FrËËÝL·±~¾»vCÐS¼‹:r­vjæ¶9;ÁIÇg.0:³Bê»^1,rñ—Û·Qaö¢è™U5[ÌŽ1òb’»\þÝÅœ»bó( x¿ÅݼÄMààr!n1Wú¡h]f'm 1u‹ c0ôÓ†Co,¾L„`F½*7aúKfÙ“C>Úƒ'|•Ûò[×5lu“²›Ä+^1½9ÙW¼-¸ÓË/³¢ t…ùµf¹ Žc_ë…®³N¼¬ÇvòÆ Kgh­cÔÑ9ž^«·l>˜1ƒHõÐ_&»*Þ?æzÒ‹œkC¬j“µo‚€ÓF,/㪟‚“s´ˆÖ²O²KfÄtÉ[ú­ \·wX „ø‹ö)tO'D7Ñøáu›wõú6㚌¡ˆ¿YRì^˜AŒô±¾ú™ ðóå7UE 3«7yn¸ÐñÚÊæpÎ}Bô[ ™¿~ÄÓï(p”·€ò }Ÿh7Òk¾Zb{0Â۹Ѩ<›¼zs‚ø=¼½©9+RÇcQ6?æ£núÑ¢ïí+9å²ÜÅü®1°m` ¸ª9·.OçíqÊ3¢”ãæÔ¾3Fóúï\·ka«Aé¸5m ½Nz#,±v£ÿ‹ÙÃ!¢„­1Àk®áÓúK·ºl ”óÐ7ÍÈÄèK]:®o÷«—;ý­¬Î÷ܪÑóLï=Wƒ5gh¤ÝçE¡¨}Î?ê«ßÖÀVö±’s ¾{±üÄÏqFæ‚b‰”b¾Zí“èÏ»Å/Kí ùáæÔ7œ«\¹fåwíÈp?æœi/—ò”£ÌÀ°VVÚ9G£a±uéØkИ£€Ñ¾7V‡ÂØE`¼=hu}uÆ"î†ÐÜæ™ êÍ`8I‹Tuw_tÊsæ`vrçºì¨‡û±ëKúvéû/hÞ¿œÄ2ö ƒvbdËÊk¥&¯½:ÒVžch(kA¾Ù.¼îG,ßõ¦£¶¹ÛXlÅ8=`1’x`žÔñ¥ŒoûâµæQ÷œF¶‹»öß:ú›“Å-.|Ÿm\'MââÉ-,Î…û‰jûÙ_z<ù!Pûã˜ÉN‡ã,2ÿ€î.NgiEœ½û{ 俯x‚Ë"œ£îI°dÒ…øa6…øÌnQ“`©«|gÄCŠã¢n‚]èZÒ Çâ0A}U„Ó½ކcl,Q÷;õM¤¶61Ò©·‰&¨çµ>‰r“@žõɦÞ÷Iœö æÛÃGâ$ÝíŠkÅôôª¡ Æœœ`ñª–°Õöá›}ì¢$`Š®öñÝü+“^PZp!ÊLx=]œs«þâè\,Ô <¬ Ï4âಿT0\³˜î-,MŒÏ%ò6KâìÙܱ½€z›±ÇG0îêówþúìÆkÄáë¿O§”Ùf?K†º*®×g¸Ê­Öé3•ù~ã7ÇxéË‹þ³sŸ ïnøò^ÎXT£±ÔÌÍy»ÏÖ¶qkš’˜2î~s¼#îbÞ‘ñ›qιÛXoXÇëÓ¶¹GðØõAŽ/KƧ–8›v:¿_Â嘢™è竵™d(ÉuI· •ÉüÁÑËùR?6À뻡&çàYëêâÞWý’”Óp2ððµ]×ÏwQ{Øk°ºhcF¾‘, £e8[b#ï«zÿ]Kxz¯ZçÇÅnžt Íý,8 E™·Ôå°k±µ%Ú–“ù«Ï5FÈ”~ûò®„/kQϵÕVýDŒM„Q>P>ŠÈŒõù2ŽÇð‡¿6r§›ág£?xè ƒÑem_Ô>þaà }ÈoͿРϴz0P7»óÇT F1±<;þÿ9iµ‡ xö8—áÊ*÷+B,ÊwúösÇÈܾ‡)%>œ<ûWáÔmçÒŽ9)U>®tlÙªW3?ÏÆ´ Ò¾œÆÆ1Ü“4ó sr·ú>5r5ÇÀþÛ§d2?PµŠ±°ù0‚wù\¬Y¡Ô‚ºSýël=LhœÞ4œy xî±Ê¿š3ÕgÚ5-“áüý?-Tœãyì9‡§¿¾>qVöEÂOW_æÐöÎuüžO>^Ú娡ø¼‘ãð´Ëê,®>ˆ‹`¡,FvÔÁ‹ŸîY;jëúš^}‡öÛ³Ýk0 ülè‡öCßµ³=€¾k°ûµŽÚæ ñdWŒ¨ÍRdk7‹>ÁÝÔ»ÖÉÙÜ´µyõWc¨#¯q@©ˆ/etO¯D™:åó;u:ó5–ïM­f³½)¢ „dnã¡Rß#Ø^dñZ;bè¥^\Ó„ʾÍps¸e´þª²üÉK¯ž›È®‰'c©h•8çðÿµB©Ô« ×—›D:xQÚ¥½‡7—\/)Amù½3¿F1ù€-ˆ?h$‰ ‰®?]‹¥~pôét ÂôëŸxaÐ:Ó–ÍNä„äÏ8,zŒÀgAï6‘Kë±wpÃÈ ¤>in`ñCrèá*6æ$vðsfÖë†Ägm,ì-”|ì«síqÚ˜º˜2ãgø¹â§ë[Œ‹:F-j.¤Ùny3x:c¨äµ ž%òÞÍÇíy¡ÃxuÈ¥øùJPuqìÛspµýd–SÌû ¾³u/?ô¢œ7½?±t2 §ŸÅ«©¾PîGrR˜V@1õ¥s»œ?ÖÞf»unQ üD¯°E\™±V+Óï"¢ÖŒ¬+n¼ 0üp®Îœ)olS¶¸ï•m(qÔw a0ñú‡ÿ7/ÔÖ<\tƒ?.Ïç ¿ƒÐ\qczùEÐ>ܲ¥ÿ| ïµÙ__ã$†Œmý™MtËàÌd'¦_Ùꇮ#o‡Ñí¡q’b\Ý2ù"ذcnÞNò6I+‚ yõЩ‡c ç?rÑAéy"HÔÙ7Jß#ÖYÍm\äªïöBã¶mýåôÖø›óã…hÞš—‹£.8yí{­nâ–û9¾•±)9!5}u’UÛwJÖÛÂñm®]«Ã)W%ú™ç¿BM«>Ý«5ÔÍú¯œÛJXªòñxyÀÁZš´òçH(q§3®kê݃­¯Ñ”õÁ“´çÿx½\ÀBù_<6ÿqDF¼ls\¿Ÿ¼üá¡AÑJhÙšr&¾¬í˜E€æ¦ÏðVúsâ(šÌ±G˜ÝXÕÿö:1þðúê¬mû1ÎÇ©°Æ Ðø»ÚÙaÍÑhßO©Á‘‰ü»%ÚhÙê$üru>݇„Ãrô¬¿K)¥ºšû™ši^ý¦Ãhdϯ1bA¾¨ÎEk3¨ð¶6ÿhH%‡|HØ3/OÈ› /žÓ86aßYö0gì—z±°ý>¨Ê‹\$ÊyøžJ²ÚØá(¾º¿-™ ÷gãÛßþ†»kH?‰?G»ÎfAÔËù-¶eëäZ¾ocƬ2V˜— Xe)$ Ùþ&çŽöðhL圔#¶šË±"àc¼7 9µßÂÒ럵0Ž]ЂySÄ—þX9F°…ˆÌïÏF(&ÇéDÏÝø)‹°Bã°••ŒÒØ`! ›¿á«˜ÁŠó»#*°PYY-гý@ㇷQ *|QfÉA»Zy©°>JÆyz2K85ÅG¶¾öò®<çwq¦o==c/Ž}kUÚª”U s~› á+AêÅËN[ùQ3çv›}\•=Ný3Vi¢Óë¡Ââôôgsz¡÷ÆûT¢éf+»ú*‡+‡Ç+;Tj=|.(âÁÓfK±óm}ºÝè°€œÏ¥eÛÈyÔˆ—'¾–`#Õ8ïøYi»—œgÀÏOlü4x\ ¥Ÿßr.»Ÿõ î¾b<¿D&3J€V; ßÞ§­gÜXøЯ º¹ÚÔõU6­×™v•M›†!±žÆÃø=˜„ýXiL{±*øw£cϽ ˆ/È‹Y,=HWà}­,4ã5îõ¿ÞŸ4îòË dy¸™—ûÏ®‡"#ÓZÔL¯g¡¤¸ÃW:mÅóüÔ~Õ)¹5ò¶S ­ÉpŲ§8Qu O…úÍ‹ÿ>esæZ-SÃ…/ãMY‡nŒ"ÒŸ±ŽÙguüuYŒêЄ…誌?ÕÉjDâú5𡯕ÈÞ9Òiñd$¦1옎=@–ô”ùÔ5'Ú+¤]ì|p—™ ºµàþÏ”á5WÇoi1·ªtù­˜E@)¥ÕP‰¸Ëcð¿{™@& Æ5Ó±…†jûËCÇx5Å@φ^#îGÖBGù–^ô9Oèu³Ðœ¿fO'ß’oq|åR¬e¸Æp6ý2º¸ÃÐþ×/"¸>£Û‡œo¼Òö›Í±æî³)jùùˆaû—f/ôKhªPÑòEÚ4¦øÓýŠ­nøtÉÃrê;79D;³È¼  qÅe¸ÿÄG‹ÿf!¹ ×âöÐîD-ZqCÔ¥Àt –ùòDs (±C:èz"™Íû½Áê‚®œ‚Úe¸¹Âòw46MïAª…¯Ð ËåFæŸðÒÇØ`ƒÌüÖÛNËö‰òoòÎï¦×Õfó˹ƒÝêñ.üÙhªD‡Ç±ÏD_£ëøê‘ Øý9ÒlÈÞs–ð&-s2¼Ñ›gô•,;Ò7†yŸ•˜Ó›µ•œwü\Üæ»Øi1+3øÅ!&ân°tÄ{œn r]ÖGõÅF¤ä3ŸOÎâaŽÊÌŠ˜:Gp·c’Cù‰½±}±…FìååÊÖE»Þ_Ò©: .¾Ý,'Ó×Mw-½r;›쌂`η©käzýÓþol5Ûú/@õÔ9H“îºÚ: ¿ÝTpÔŸ±žMQXŒåpºY›Ñ9[ßHíjDñoô»¦ñeltÿ¶NnøÆµ‰áìû_¡û0°›ÖÆ«.:{zê”ç“l¤Õöbð¥Y8rÄ·Y¿ÜB ô³Ãqn똹ÛMÚ:¬Y«å¨PÁm¾ŸX»þú»÷Eƒï’,ŠüXAÖØ/kta–齃Î~kÏ‹¶¿]C¹¨:ç‡áœ‰{zëÙѳ÷{½Æ~V-è_¼·/´Þ!×OjÎåyþÏ7ûÿÏ*¨Ê÷“=¬Hi¹zUÜ/Á퉢9 5O^+Rsî,.W͹… _®ÝUqž u£éaËáå=Ò<=zt ^ù1¸ò¡×~׋ë@=-9ç8³žòåqb€“Ø)´“¯Í°ÃCâäÚxe8§ 4ý$!¡ôeÀ:½a(A*…ká1 }iðP'úðï,Ï¢úSb(ìºál¼ãÝ<› 0ýE8±k`K('*΂Y‡ã«çä(¿…iK•ïC^üîA¸t;Mç`-~8x6Ç_–w`¸\à›m+û…šLÔå_µ9Ô¨½åŠÊel{9áØšÊ=PÜVz€÷ÓUß1¨H7\Sö'Tš•zr鳨"7ßPì,öjàõïu«÷•Y€´Y|!>Žs {ûÜK¦'#äxZOÿNuÚ¹ª:¯6bœœ“5xÜ¿{ê[®:¸o*õþ¢ˆ¾U×ß:toÊåLƒ­ôøz÷šèàïº|÷Ä(å?ä2{ò‹÷A ÐÊ´ñW퇃¢^z®¥­9°ä=öXkš³ØÚ0‡^ü F|[lÍ5Á}^…ՀΠC?<,ž#õ_Û¢ ›Jý7ÿ4:‚šfŽŸ…O':ù‹ÇPãŠL¿ITp1°ÊÇß óá³ÍG› Ë3~å×hù 0Ìe,Îa XøO"Êb#s9ÎgÕ<¡u¤}G‚„rònüô—‡vióx\c=„a•ÝÛ¬ëcs*«ÜÂÔ ™áÌ`¿³&Êb)h~“e<#_ OoR®ŽÅ°–r.¯·8O&„M?^A8䫲߅„{¯¥ð¬I¶,t6&íÝŸÞlõã@Œýt­0bë%›eHKõkÛ˜¿Ö™ÀÞ84 2B{Ñ>ý.uCç)2öíàe­ÊÝþçŽ}ºC<Þ=pà·OM†§Ÿêé´7o°’q0?OÊä:èôŽk(›cjg±èìïU`ûŒ³]€U\¾ÁsÖêm®Úl ³ÙP¨žÇ³f>‡« ×åˆ:gÓ§W£î¤Añäj>N‚oö¿F׌æpéT6rÀÙÚë;ÎÈ‚; ååMÊ?ŽoÏKdH}t=Á÷?ý’ì8+±å4ÛtE{¾¯jË;jE²¼â@§9N°‡¢÷˜*¦5²æÏõ'Žâw±‡ÄÑÜ5ôô|ªãÄš¶Ÿÿ_Æ]ÕÙ5¥ãϦZuÕ˜7—åÛŠxqŸc¹t ïÿG,ŸÁGıëúòMkÞ<$¼Y•³‰C#išCoÔÎŒûY×]6Í2WÄÚ-Ñѹ©qõÎñN¦üö)ñB¿ž¼ë¹ôä£ì^^2jéž,ZYY×ã'ãß…c-Ê7kð>I <kÅPqëÄø+î‚ÝÜ´¹¤ÄC÷âkcÚA¦røù͇üž\f[ÓæAþ‡g£œ°"˜3`×ÄfIs¦Û~ªÁ ZdÌöÝÉC8‹E_‹µÃ§ŽÊº/¹xù^.qعG(ŸŽÓøÙIª_ ómjR Ï®åéþùäa; ¡ÔÇtÖä 'Ó¼l»Þ¸š<Œ¬8øà$Ù7'Ó*Ǻy3šíÖªëÇ{ïê¦ÏêÖuÛüä¾4.„ '0eYãÓ}%k°fZMÔïèð¬O~øh&7×R­µŠÔ!'õâNðdêgN9h£Õ{È`»Aa¾kRnëì«Û qq.Äö…7ÀF†dØ_ŠêiT`I.·Wü6‹Ó¢‹N<@†U¤è+‡¯Ìšm. ¾ùª›R©˜”Ókt8hD1æ^¼¡çé„:ë=oôŸßÙkWúh¿NÖ7@D0U6mâ†Ão!«_žØSÛ—1ã¾#Õ5j˜î0ÔŽ ’úô§³£ÜÌ» –ò˜«‹p¿št‹3(äž±ô]žú¢ÅOÀµlû«Z»ýŽ.“ñ-|ä®j5Ñ~‚þá®\Ä–ä·>,Ëù0ëq˜Iõm¹œal01Ù9¬{µó´ˆ«EhˆñyÚy«‰Xn¨:fôsøq4‹ÎÛȰ›WhÅÄì{`Áy×ÚnfnÈ>p4»Ì¥±ý U»áÒ·$Î^ ÿn÷¦¨B/8‚)§â~K€ÌºÐ`ÓhÂà¥Æ·dÈÐë”o•IJ…K_ å'nMØï/öôd÷4b»xÝäñÓ³˜a¨³¶kÖ\o=CQaàÎh8r8ë0­Ë°Æpú‘òªßÿ/Îã#b¼hA§ˆ&indõxÙoÏûêM]B1õ3W:ö­õyLszeúd¯ÙðÒž¬z x¦Î‘kå¹å!gí­‡ ‹½¹ÚuŒrè©i7F&þ/Ž9¹c¸ZÎÇŸž©w× }ÅÎ+ekeúGxF´æH?upFö§öŽ›øÎ³SMBE%ÆÐe¹ä0Ã7®öô1^Ö®Þbl„UÂxoêºËëªø\ÿ`„(° Ÿâ5ð¦ú…¢òxÙ¥IQñoŒã×Ç§Ô {Ø^î¿Ô1ãyÄÀÁ¸qŠfœå¦d'Ø ÞæûÕsܶ©ˆ'cÊËÓ>™„•û¯H/Jú›sWãÖÌï*rŽ™el¶—,ÿqÐ9‡ö®€ÉMù}"vùÉ¡1Óy°tFrB]µ´™IÌw_t­üO-Ïk »4±IɵÚ}OáÛß迵÷rÒ^lm?ûGüe€¾/ 9•è½SþOö¬*¼r>-¦ßЉÒï³m0Ò¸l­ËIÛµw=5¸}|QËÌ8í§ê.¶žï׌]cË€lÎ_ŸÓ®ª­ i½ýcUœ|®XCÿÒýI°aU‘£º¥*¯ a§„èðm·ç«KÒüŠj½‘9êW]ô»æ\µçç‹0°ìòµüf¼¾DصûbŽßw 7vÏ âòe{5zå/‚«VŽYzà5¯õeT~Óq@0?„¶žO@|8úâ³Á¡5Ô5sG¶³s¾CT¦,m ì‡ÞD¡LU%9ϯ¤éâ\’túÇ"¨ø½bL«N¢ˆg—¾³æôi®/¸.9Ê¡dI¢0³LŠ l4™*×÷Ÿ-CD/îÞ•ŸéÛ…D .aÿ‚„7ÌÚ‡m@%+œýñ› qzM¾ÍHmââ?½¥0jÒjCÏ¡­„83–×ÌÃio¬ð4÷‹yìñcö–ž¿ÎÁˆt{k½_¼øx‘k`ë‚çD· äêd\eï8{qg±ÂËnv€cpÙ… ’²F×î{( â jþJW›=d '©÷®Ü©øÖiὩ…[Çüž$ä/ÆÊ±KN¯Ûå@/OTRô!þ5}ö:k'تøeó¸°¾™á‹Aº Ðïç ædðý­…ê±[„oóDþoüŒ·ªèk¬ñj-5îA ·Ž\#ò{ñ[×z¶°ôÀ*ÓKFOaÔÒÆÇ9¶*Ÿº÷7Øý0¯¯ïƒ{¾Þ¼ü>Ž[—#VO“D–¯â†Ç»û½â;iùýê_>†žËKyûEöx‡a@›A¼ýñe—~7:ÆŠVÓ/¦{štR«/2ý|)rÅU2—@9TÃì6o{Ó¡ßàÄ…fñ¦³¾‚Áyì†O ÿ—õ³è œÖ4£ñ’­kÎÊøÕcGš)MyþŽgcü`¡kÿ«i:­•/]Ñýöo€W}ÚžÙt!žõ(7ƒ"¬²ð ¼øþÔUÎÊ…ãªä\èh«¯#Mýud;±Žè¨AŽT ¿E÷_¯Ä¢ãïú1Æ~uJßÍÇË\GŰÃ~£CLòˆ«ÖóÇâkE–7ÝÄößÿ#Ù±’”:Gˆj[—ľ‡v(óqâ+ä|¶¶îúV%vHÛ‹¶?M¡ÎX½ñÃÌ–Ïúê ¯Ü_%ù/ÈÞ´Tׯ&$Ûú/xBl%a3N`« GͳzéDG1¦S6v´Þ¹‡í‡b¸à”ònoÔë¢?B†Å*Sdõø G o¡ûòѳŸRœ`kY_7÷Ä÷'EÖ~rs1®˜HÄãЯL+ƒz1O—¥磽hְ볺‰,â°Ã´¿çXM‡% ÀÖˆ* ›p;™ûL×µ¿2£ÿ?÷Ä–ëÃýƒrPÃ6C\ÛuOý”¨ÛÁ4UçÚh>Ëkê)~bøL¡—µu¬¾ºbþæàÀfÕ i,¬<)ÈvJ%}Ͱ¾žÉs;ôço0‹HØ}ï[]{»á0>{ÓRôçølN…·ýë# I÷é‰á<ÿ/¿|Ú-M o‚žéM+cü?/LtÄèÛÞ¤}~!Ë;P„]Î.¹ñ±p ˆ¥îF‹]¦ßj£ïu ‚ÞᣕÌv7lߦ´Z¨4Asà@‹ƒ]ƒNå);±ÍÛ¯¾³]Ú|žãåÖòxn¡h¼ a Š¡>òõæÁ ¿ [ñÕu Î7tq’Uó€¢ñ|ïr½hTmQ ­èšvÚ‹«ç?ÃæÃÙ›!r´ÿ0'mSi³tž®ÛL5UÙèCñE¯üÄ”_ òÝP»!"UɹMóì¼~ŽâAõÄö·Ö!ï¯?gsÛæ¬+xX3åxm€¸/èÇ¿¾Ôóê¡õáÞE.‹7‡C,‰‡žG9í7¯ñ—¢}73Ó—Ãêmp›äÕM f¯³&px²ì=` 2ÃoÝÕ_‡¼4Eáw2Çë ú|zƒýô¹’¿`x}¨ª"t;¦..súÎrç:b´ ¯¶·$E@WòÑÖï”Á Øbîè.½Ù¡æ{ Çû!Ç-€´æžsœ‡Ù 8 • ]»ÎC$‹°õŒüðÆ£m{ƒÑ,.žð¯FØî¦¼jäi6l+¹x>t5©ïÕ=r­½"]!ºî§h˜qí´ˆCÏ—mG¬ÍG|öÓtrG´5x{U¾bóÊáæ«ùt°Éu{Eü!¾xžÁ%Öp½&þ4÷û’ñ/w¹ˆù³µ®­Û€Ä㋾úB_ëá[[–‰Á©Â À`éz€F¾kpüºÚœSbXÿêP×€"€/ºêlb¡ÜñÍKÏ5cjÓŸc4æ/®üm¡*<ûZ¯îñrxu†—Û8•¨!†%'t|‡³ÈÅËÃÝ õ¾BuŸ§“×U×E¸!çÿ÷Ë fk~ã1N*¯feÝ]65EzH„Œ¯úk0¾¬a†VpëAg”É:…5D¯ud,MWá Íõ‹ í{ÖjHò=síö³æ±Ë-ý"èâíÿë‘vaÍÞñò1!Œ±å Æené#IA8³¡ÝêÉ·}±Ý iÛžN¶ðtÄq§ð>ßÜ (Tr­Ö†L 뢈Ckf ¢¼ÜO{ÆD¦Ø7Ò@>wWU?”;ËÂz¸¯1÷p5@qÒ¨cäK•úL°9£†„y¿îEÖüÎÿÔHŸëÅ‘×täˆo”4zùq¾0[Æj«ñœ½}ë |žÙ¤c[`‹ËoPFÎg¢ó] ŒM/C¸CÄÀÕ‰‘Çzò¤¯v }q>\ä5&§­XÕ•ÃÛ4g4í6­˜´ ©<´Ú,3‹Ð7<{@H…tdúrr]Æ@ªkNô›‚{:ÿ ôB®&j—ƒÖz÷uz½C8¬ýþ¨uÑêZ o«fh p[¼7”¬‹A“ç#ÈE\X5Ú<ÉII>‹ŽÃùaæ§PÌ0VFQdÙB¦€ÏOyÛ—ý)º WwÇmkŽ)V%"–h°¨æI²:Yu§_Ö ÀZ^Ù!SU¿GZ‡¾kWËž®Tú:[úŠ]3{˜|ˆ‹SæØ¸w“[Œ6—ædš+êP=ô± 8š/r7_™¥á`SŸÎµêÅ?ÌRσ‘ë” ®ã]7DqGM¯cNŽÀ´íÁõ|ä÷T´.¹®&¢áÏ?f·èÅ{.Ù!f\|Œä¼Ûz„¤öZŒ;r9éÀk»Ç|£¢b@º‡ëܹÁü|ì;G°Fœ7Þû½cç%Íôêêôå+Úô¦»–þc׊?ûa:Ê>!d$÷Lèósý˜1ýÎÈ¿>1­.s¬W×YÊãl5¶³ˆk+sû?ŽûuDŠ­[ÉkÙnÉ™~ ä[d窽ì®UÜËÇ(ý[øz-h½à„ÆûÅ ß›1,Œa¾åç_$ò¦•îŠßÛëÅfLçÈŽøIµ£gTÛxYŽýšÛTÙ/ªvÆcÄœ|?Õ @„Xåå5º}Ý€D*ðêf ‡áXÌ¢ç]Ÿ¡ÜÛßo>dZds2m˜“Ý›J73Ë„7þ£¤¹Pµ_Pň ‡ùÌFlcu­«\ÉñaÕõ…,µ´¦§"{¿CÀÁ÷2Ò`f º>©ÿ…BcmtÉľu W‰¹ |µµ«îâÆ7ŸA=ùßÿ+À ¶Þ°+’qTè„ÅÞ‡eX'Šž3Þ­š£~ûÔæà/'Nìã¼ÚÊÆ©Ñ¾UÀÉŒÿ¥=½¶ï9½1gÛíÕ«Rþáç¾½g =Æ{'âcÜÜ!®¡>tcLúI2Jñ¿}Jh²j>–‹¼´ñ0[úYîÍ»uA€Ï`­J’C5E¢®øt†ZÆVð*—OvE{:m\䨾zÛÃ;(±…P¢=;Ù&@æzÆÞõ¬†.aÇCì0vøÕsx›§>ËÑyv@öi¡¿ØbÚ¿ûVóÁxœÝ÷e¼¦Y¶#ÅÈx/' IÓCžî™ÓÔq}ò¶_m¬¹Ê^2ÓWgD>9c­^ËGßöìÁÀ˜‰´WÉ^w´pOåoŸ/{³Óû8»O3XkŒáXs¶P­QúÇÉâk4yqêÊËÀÉ+Æî‹›5 Ξ ýU6õà}ÏUÅË3Í[c¿:‡¥~ä2ð’®'TUÕŸöÞÈÙ½dòÊ}¨Ž_Òbú-_ e¨þÁaØ[«„äÓjh~î- wmEÊi=D×Ù!éG‘‹vöZÙ´Ówåd$¯„jõó› ”ÛS×S%êJ¤y²2êÛ$p\Ñ/¾á;¶›ST…†hñ÷µuõ šµ# eŽH8‰Y³sœ[\éNB:BüÍŒ¶ÞšFE1–}^%á+Ýýâü׿*—V-LâR›£þÚÍœ;€K7ç„",Æu°ÕøÝðN´šo ìm_ÛŽk÷Ÿÿõ?ý9t» ^}Ô,YzÙYwe|zð!±§]µáÝ~”mB!Ü2¹JBh¿_KåzÈZ»ÓGcµ]µSt;­L_U£ôÞEƒpÉÅ9ÒqUÈb¢Ã&¦›_#§€KL!9Á¿‡môÚhf}[;šÍó7îÚþA~¬Ì$ûû¨/žŸ¼’ë"†g䞛ǚû€7­’¯É}×\nBäØ',Qça³‹ÀqóáÒ¿# oCîݼ6f'æ8Mï0¦Ø³–½²zE¼¥ÿ¬K ³Ë'’ðr­ØçÕ.†›ôk®«bã ƶíããdøÆdÍ/ Çñ›ßWÏ~Šà•Ág4–ƒE‘òì¯~k>z â®Í Ö)—ÂiðšìÁœoýŒ³¼‘l†Y¤cIi0̌ӛû“&Ê:䀗ýwÝ>¤ÝO~µmÄ1Î׬9¶ÊÇóænšjŸ¥×‰óP2œÂ9 ‘¨åiÁiº¤; ÿ];èZŒ¼Ù¿°³Å¾Ú,ŒGùÊÍ-‹«¿1_œI´3ýç|­9Áùï}âéþž£‰ÛÀ /æjoM^}ÜãÚå ˆa=˶ë2PµúX3mûwv|Q,K±<)‰"=8îè´Ë88‹{þb/Â"Ic‘èH »÷Ó!È]½öíçÚ·k¤«–æ¨ -ÌÀ|Y]ÑBÜ‘Bf¯èîã$„|[•j¶!²’ÙöÜã(„|Q[‹¬ê3PåI^uˆÖ½GÞàX=Ü0J$ãÖZû¶=sÓyÛ‡´‡Û&C¹õ]àAí÷é‡+šüšŸæ~¼Ìy×ÃóC§PN¹7‘‚ËéËQŒU†•ÃXçLí¡³¦û@ÙœnïÉL/ãiï—A¿ÞiMáb½y‘ËÂÍ–~(œßÜæã! "¼ý‰T·æ˜Ï·3øqÔ–oìźöåýÇæôÚ›zû+@G¢¢©ÖXKѯ›‰QY¼T·d×ÿgr h4ôùña©ü½Ór!âêsQØo‰VN–ñÜ\Çõž¡´ž¬.¼¦ßØ-dãw3Õg¹Έ¨u·Å´#Xhðù–—>“Ò¡µ|Ÿˆi«ÎE¢‚#N-™%h1ÓTí.˜‹‡0ôp„Z€Nj”#â»ÖЊq1ó+r熤í6^‘©çè<ãßÅÎâ$2 ÓüDéâýÃCT#ùÓ$?¤öSÕ"TÓ¼,²g$Ñë`_#¥It2ú{°ºZ#Úæ§¬õ1½­ X[;ôÙ¤üRÞÝ+H$ÔW®ºûrShOÓÑkªmšvg½éÓI»¡Ø¸}Yv”êMQÙŽC~O“ñ³5/ÌnÕ‚a>>àVñ0·'¶¡/,xÙÌSßõWgäš7ŽÛté?¯Óû“ ¯˜ ŸEdÿÍr!»I\î­'‹bkÙE²µÔ†–Ê5§3Gùaã#¶iák}hm˜Úh-r0Ä¡WXdêÇéCNæÁß¹öK›øzÎÖ±±=£·Ó|Óy\1PüÖߌsËG»xálט½|ð“cÀ l‰IÈÙ™—0‡e¬Ú¡/T0Ú—ûl[cÚ %÷P$¿ôëávsEÝ{ò®Ü8>﯇}×!:}é<ÔKç–ÕÐ:˜£1U#,œ^Ùx½Tçù·&+GÎ^ {¨Üu«ãbáí78{óÆu2Ä WÛ\š‡aVWϺsˆËË’¨ÖßE±§geŠ·ÇÍE÷‘¯àÌuàéEŽãíýþJbùÿå"ôî|>qí+Jãí D.yÎÄØW•бªÊG÷ÓêG-z®ÿ®K=mÑ=ß!ÞüZ"¸mšñù°P¬>‰´£Âr²Æoõ‚”31PIDATÕõðˆ áéçdIÊK†C¦çæI<¿Wel¨·†•¹µÎ"Úÿ£ñ&^¿þ­_³ÉQ:Óú,¢ÆE_»L,”b¾´-pyÈǸ¢q‡®ŸÂ*_ª9 àæ–‘¶^] haY]ªE‘†÷2ôÕ%p1õ@ñ1­‚%å~ýÔä†Ïî‹Drá`Ná7k OkÀx×är ª¸Èñý“¶‹c#kƒÕZÒ:ùÒ.|,€æç:äÃ"Y¯€§ß!¿É×¾û6fq Z0ß°ŒûAÿp\t §ñ-„–n0QX)Ž'¼û­‡³C[-âåè«:Áhé]_ åäš\Ìj‘¡.‘·R*§{‹‹åj+æž3¬ÑÖöŒ‚mAŠXd‡WnÉ–'o.Å}æòHƒ—‰˜m¼Ô À êSC°_ a”ö‡'¡² ë$'ãÐú`ñwü¢ÿn&Uª­äó©bfƒÄ¬z1GxœuCtÙm «?ÿnªÃ-Ň«—·àA.¿®Îþæ`6*:rh¬J\hzg|x£lÝZQ.…,­ì¾¬‰û›­Íº^€?e–Åq|.væ77£!®Ñ,;Øúx`‚¼·/þí_ è¹-:_Ý ‰)N‹Ìà¶Š>½UP×´RTï›æâZèÇ”œÕ»ÙU3úzîHבµ|ÐÚ1ðî¨[7}|‡AûÞ1‹ÜLߨ J“¿]¼;Êwð‹«¹í¡–„ÅÚ¢W–²œ@4Žb…Œ÷ŸldV$•mêú²“ÆM&ÚûMW^6†ýØ´ii¡M6‹Ù?»…kVò›èÍ‘T.Ó0ª\i=(‰‚5©£E}9h™üÕ$UJ!_¹-çí ¬,͈g3l` Š|¬ém(L­,e&c°ô›ŸƒêfÔ›]¥tl-Üš#p{ÈËYô#c¾C_fÓiº¾g9y­ŒÏ꣑ëdkn>óÃŽöÐu›Ö#¾„b9ŒúqPÿÖ®ž*›CõêŽó(7&Zb}þúì× ƒ‚´„Ÿ_¿˜F(ŽóÇ5 ƒƒ ~¹ûæÑ˜j­I5ƒà½±9l)úÒN¹&;ɼuuzk6=\ãø¼¹þÌùÙ]õÓ7ÿà·D‘¸žÌÉD÷÷âÅ©øÈä$–cå¬Ûbªo•ι*/¤b&€s›˜þVîo+r©…¬ô„×aSì4ÆjN•¾÷0ÜCú5«ÌÇðZ.s mµûÆ –âýtÑ7BšÌg'è4‡ºXzNÔØþ·2>ßL”ËgpóqP1'_ööŸgó52C ò´›Wâj3¥–«‹.ösÎ'qRúg‹Hi::E[2ågoO©y'c­YyŸ´•‹Û+ªÿØÄõa1ørÖGLÕ”Ñêe½ÕÏ«ÊèæÀØ€ô»ÖŠžÑHØÅÈZ¾ë?Ü|RƸ°tªê¸µõË0Ó¶W"ó^5~èž'‚>‘6¦b›¿üñâûr Zª;õD±% L×õrÒ3­{´GÁ*ê±GFe`àÛ/ú0éj4Sm ô*aÅ·C ùBT[‡lX{»&5±6ͤµÃܲW¹<­mv ÝßsØâSétýªïˆ×a`#\Ìé+]#jt÷ð÷“€Ið ÿÍ•ãÕˆkèW‘Ñg&½ ŹôÛˆžôHþ±³k¿$·E*-’cÓS[ÔŠùB ×ÏUMm[A3x.ˆ›GVMà»@œ™LJ³½±@+_^B?ø6Ú¯ y¡{E°0¥)Y–R¤§˜Ó›ªjòåo)i/¶ß ^ømC"^rÏòu0*> ÝÁZÜÙ(¸gÛ õ0Ž,‚Û/Èä¬MôÂÕ÷âæ!‹Í™FNA®9„(К:jU jº›{ŸqH¿­¡è­Q‚õ?=fG@ÅIϱÍ6" ;X—>#7Ðf×yö«„·¬5®†ÞÕÀðr[áŸæHop– gë³vc¼HèR¥SÝKû܆!НÖg/9+DZõ,«§ã.hDJÍiœìë´ù¥ÐVb6}qØMðÏ–bÈ÷ù‡j š8ͩ㌲BÊàý^ò«‚RbäkÐÃÅkPÿŠ„œÌ52C×óÞltÑN}vؼŠStÎ|¿Y2"}%{ùß¼k×¾¥œþQ œD¥c¾ú¿æÍÆÑ{½Îw]¶ÎôËð&«Ýµ×Ú’}#×õ‘15ýdÿð°~sµ–è‡2(É3¶ÿö,jùdó²(Ú"$¢^’Á¾­¾Ú/#y¬"#ÛFŒÍÖK@Ðíú³ÐÚݯ}PoÎÓÅ6ã<¤ÖŠBÄ^ ý㣷¢ª…ö‹s@76¦uÊ9 ¡6§•¥X •Ldï^S†b—ú‰ÁåˆÅþ+9†È±ëBeddìC‚gç;TÉhâèHíÏÞ¤ýZSwÝ0/§i•ekÃ6…Ë¿ë¢ýãøçZ~Ç‹ñ¯ÃiL>Åy36oɹÎ;cêýaúÕ儯‚_ó<õ±ñ§ÆÑòÝ3E" Æ9 ËòÕÂóÒ±0èƒä3eE_Œ[7öEöeß[P<ñµõ:/}uóËØâ]ŽÑhÓ_àYø{©¨ÝèÑ8¶>tš{údO‹¾ òŸd÷NûpLgòìx°®ÖNÇõ„ÅüxŸþr[oCT[í+óZÐŽ9jÏRžbr­,sê_²Åc7×g—ݪ Y\”¼äa,«?ãæ†0L€r;~ö×x©7^ßÌ–O®Ì3‰]=ã:o~)˜ñø;düø©ríùægñÎÊPÆõÕú°.úóE•-¹jİx[§ºh§ogŽÅ'öýéfü[»gÓü訟/cÔ_\Áü#¶vº%®¡wd;܃ˆ2ñؼ,‰6B ë"zFÜ’UŽx別dŸP!軇勭Ÿ‚´‘ë‡Aü8”Fë ¶‹3íôÏö¢Èk—åð_Ñ´Ù†#K‹y\Qr¸¾‚—ÇVÄê·Êă”Ãá¨u¼ aÎ[öO<¿O€poLyå«­õÒn~ÇÛ4oöÏæû‰ ‚7;ÚàÖìvƒÍÚY³ê Þ "9;‹a‹‡ ¶ŸYƒX8†¨ßºK̬Æú¡…vhú4{k¿¹Qn%,Zÿá/TÉ1nèìç/ᇼJ zrU¼\·ëé‡-÷P,W.ÎUU[ô¼ìù﬩uð†ÒÍÉ,µýõ$DckU@; ­SÑíì{cBáf¦¼½ÃÔ¼œ_9‹ÿã BνՉ+ýõ)¼` Ö]å'ÏRëùS¥®•W—]³¬s…hßõ`¼5óEÿ8£Xt¾ÈLµvâe_9g‡‰8Û¶ÞóŠ‹ë­,ykO¬Þ@<]~‹ú®‡qLæe¿§>FûSpö÷ŸÊõ‰îYÇ-·oé»ûìâxg™ÝòÓOnïº+öåf”ò¶Ü_-­A FpÜ]ë-~&Ï®ÄÔ²6Ψy×Öåp ×€ êñò:ã¼V/ùæÂ·¸còcäng ²÷À«u’D»/5sG0fŸ­¹k•tôÅP¡´Ú0ö¬¼kTýjX÷˜™£¯¬Üw-¯²á¨YîÄx…Èü5tí^FhÆ!¾mš,F<µ9ŽÚ¢©þžÖí^R•t$õ협äÖ®gú f­o^ÚÅ8ªga]ê]ýè[“‘ûúYC/GÍ1{tXH–§~Ãu\hñy ýúJ}Îèði½ä„\¼§æ¼ë·Îúçþ&¥F>˜É¡{’šŸ°?íÖÀåx›6Vqû‚0ã]ÆpŽ•nѺqV÷¥“†X®=ƒœô4WŶ·^7büxp—iŒ'÷³Õ ßgÇé0{n £JäüçFYí‡ÄÔ¤|®æÝ¶TÏÍéQoÊ¿u_=,‚F–f÷¡Éڇ˟q¸Öýêè°»ÃQ7}sp>¢×ùîytðÛ“ |±‰C"ªI¼ÿ¶fTÒhÁcµ*¬Zìã¯Õ%ž…8´›Å¢ÈíaºÑ¬p.¾n›7 òÓ¸c=Ô‘“3T/ÍÛ,yðÐ÷ü£Þâ€* Jeµ eq‡•…Ñ€£×6ÙusŽ€$tÝÆŸêgKôëQts\£ŸÃÞµ¹x‹%ääû*ò3>­c5pÀHg]¸“«’Å×®d˜éÞ?¨Éfç’½oB#´·ZLûdÎZhpñ°Ó½ª<@²ãâå“tš—÷‹ C^Õnx­G3ƒ.€‚×\_I,5)¿ggÃÞ8h^ÀÕa@„–z¾õ4@YT;¹öTXÐÉÖ@Ú¾›²îŒßz1„µQì]¯úƒpSi]žœ7çãªØZ´†,•¬Y“Fq ˜<*ø~Š޼uï&}aoöÝJož‘SÊBe:}‡OcóåC5ÑzÃQíŒAs3Ò# iæ¨ L7YëÅ`5ÒÂMýìÊXŸ;Leæšýl<-¡ÖøÛì¶žÜLäe®3ÒÍ6®¢î8ÞV6#cƒxí‘@>œ_Óò÷¬Œ5Êœ…£,ÚêÐðj>‰.–îïáXyÁ”ÙKçÁ™p.ìz6Î&]Þ8¥ÇÆo9Ôô /ñöõ “›À¡ô0õϯ›¤Ö˜÷Ó}«µ/Ž1m²ðtµq¹û¢9gëÉ•æ Üåès3XþË£â?• ¾.êǯõê'ÎjCTÌwm+É‘ñbcŽ×­úÙ £×û8i«Ý®i1ÞË<è_,¥6íúË9b_ÆÝOš„|¿=-ðì†j ç&š«ö×&_môkåc¶Z"¯€‹oÎÚÇÙMÕÌÀöúðºùÉiÓµ‹š~¯¸‰"ŽxÞwÚçôÓ> 'Ï`v®Ê6ßþE®]“r!7Ýjù^‡“Š;Q´ù9HñV—÷ï¨Ú×äàÖÒ-Rƒ*äÈô«—æÂ¾5n‰«¨ÁöMf(¶΀뵻RsQ ùϵý) ó¡9_B½+gv_ïÇ{kgY§×VÝénä˜WK‰|sÏ*Øoº¶NÛãâB%u$¹¼ëßìúBÕޝùû∿vÏ‘­=k®ÞuRRÍ¥u²Q-ärÊYy°¡‡,>6ë\VsŒùÞóšëŸ¾1äê©%?;TÕÊWÛ÷hÊt¢ý+ú*r<]Ë[kêÇÙÕÜïksþüÐZÇæTéGj@µU/…¿c-ZS.¼ê6þÚ‰ÚQÝ×ò`ôªˆ®9UróŽs|4}ýóf󬿜5¹5Áà…5¤ýit~\vÎQ° †#¢^Æ®]lYiöÏv±$@Ûiþöí]Dµùï“Jç‹ôÀ#¯êcÜEV%Ôõ‡¶ßÖ ¯k9Ŷܿh%°Hƒk󤿺wÝmS²Uyµ™½9Q/^Ζ½w¸¾=̾°ác Gk—,wz~£ÿÖå‚bû‘†\v¬}GÅ;¤V ‡=ÀÌ®‚ÁŸÕP.ùÛW‡ßZÖ‘f]Cnöx©‚ÿö.­ÌOŽÓÅAŽš×è\.wZV…Ú÷–2}ó›¯öXÅã[} £!¬t U8ÇèªQ)gM¾=Ï»¯ñI»ò‡mdS¶þ¾{qÏŸ£kà0å!FKG£—8Å.,ÔÙqh®ÓŸlHóAï—{Ds%&²‡'‘o­µ`Ë·ÑÍN[ÿÓ`ýí/ƒ#CÿêÇ(«@ŸAè…õp3{|§Áá/#ë(„óÎô½§ÌjowŸÅ†"ø×ŽÊÑØç>ëÐ㺠R_Z9aм£áª¯æQ ´¶:Dæ=ã=øÐß&‹¡5;*ɼ*Åh ƒ#¶sãjsmôáZßÜ/¦QÄ6žk[w~J»GèRfC¯x¢”…'sÐÒxù#³Ïõ ý[%…3„êü6×7V6ŦçhÀÑZøiu@¨Ü–þ ðÕ#.;ŒLê›ëxýæmÙÄUÄ8 넬Lt¤py_I䟋¯Þõv­9ûO[Ôqt/±JågÝ´}ùëfq_#ÖöÆeeæksð8‚˜`ÌfBŸbã¢fhúûÂñâÒݺB®‡­ݸ² ¾k¾¹ ÿjÿ} +Ñ!,þ›ÄÖ Qª@¥É§…:J¨MéöVC(u-vDíši­î®²Ý¨‘c(„m¸ºÑêÌÑyÛÃ)¼¥ Óêðy?Zç¼­_—+gõ’(²ðÅlÎêm¢b‚°I›Gyø¢Í|°1¸Í áÔ0\åg¯ž¡¡5P3»zô—qý>}Í•Kçbõã Sœ5ñüíÓpAìÍ ùµG˱QÔ´ít#ŠHêÔ\ V¨'G5Š ,ïÂ1ãoh§ãÍÓ¿¯šb E›o®/bV’¯žh/‘ª&œëDLäýžêñË×ûD¬aL»ïòU)9¬·nâ§ÎºÃYXc!/·ŠV$seäI/ù~?2Í9 YÓ^µE¦úo+–@†JyqófEv*¸¹é[3×€fyt¹_Há.VAWµ›l>ôWKþé0jç }R7KÛòèAœ¾ÿã…뫵!$Fþ‚á6bÑÖœ·ùOç j_ìf*j»ÖUêûVK‡]{úÕ”[·[3ˆWyöS:js!Wgm•„…½Ì¬‰é<‡ºÈËÏQµ5çÖ|ÍG×`BFÀà½ÉÒ¦êe‡×ç_þ/öpº^‡(ªHSHס§(šÊöt¹Ôöß²ÌÐɆppÿ³rŽÐ}sg0Xx¯Dd1¸µæl>ÚÉчò’cCc¥™ßI7|~ÿÆÞ}{øvQä7_1†hf¬ÖÇK9¾™ð¬DåŠe¨Ÿbmå*öÔ¥×FdÃñ4ù[Õ\#='ŒlÁ›‡HNL<}8½ÿKC 9»¦Rº<´»á¸÷ô´áÇ„bêµøÕñ xó'~FãW'ñËÜìЊ)QÑ‘ù·Î«îaù´UðÍ>ןqEvû”YÈfNÝóª½ûºsè:á×D¯?Éx¯\õ§ikó*s'ö³®eû5âê²üvn9(éSñùïIµó¢íF‡§¯-ì±ÛÁõq^4ˆß8çz².+ú»Î°¼š,€<·®šGŸ½îí²!_øÛŒéê1V ïæ`úôܼ'ÓwÉe/Îj¿(­H¡ŒÕ\½¹I¥*èblÚÖnÝÕ¨gµ$eBO£Wƒ¸™'|ŒÈÕõŽ^ÿ<8˜AYpô›#¥‡ëLS$Ö¹5íÍ`ÞÕ¿ªˆ×¼õÍãÐ>ì¢~oA‚çp¬¤‘èö:å¤fŠ}u®` ôþU1g½J¯2"%¡” Ò~ª „Þ"èƒ| Ä"ˆÊ6¾¥ŒÌÀ|‹Óþ.Ü÷fÄ Ð÷ì1i2MÞ42QX¢„ÝÑLÊ&N¬]ZÈ N· uÂ×}T†Mã!PgQý”Üø>8qbµ€Àý»)ê×Îýý‡c ½£|…qŠøMÑ Ð]]´=ùŠó.Ec÷ãHGOOµ–\•Š4½ ÚÌÌùÓ`ß¼è>=gõçëƒÉuÑ\°lÍ)Çá;Öª5Â=ˆQ<¬û6‘<ï8¶Jû+NÕü0í'‡ïÅ›'BÛ\qöŽÿ¸TŒ&Dô¶øÕ:CdF–C{é®ÏØÞÂplh¥‡îÎŽãÙ¤ÉøóÐ(ÛÙŸœ“—ònjè¹N~ù‰*è›3ˆ‡\](ôé Žt]«î=_íþ¬”®‰Ù,àbî8U°µX þ(¹—0ø8sþ¹Øß¸|9¼õð[§ÇRÜŸ·{‹É›ÎFF³ë#&XÝzÓ£½Vœå§o—¨¦¼ª}y{=«µý8ʯ{úÉæÓêõÖ¾±„ÚŠ±'ô{X­Õ}àh_¯<».¦7kÖ˜¨~É·ùÝDeåO’YÏI û`Bc¨ñå°2`\Ðaó"íƒóó]·êµõòo¨åÇQ]ųïÉÀJŸ›[‡Z8MíU¨ý­îPUº{‹Yø’ÐNºÛ+ÑVÆTîËN­‘lgóÙ'¡êŸñó.2–Ã~ç½Ä&kêC»5–^ƒ»y3¾æ|…œ5±i÷Ð w¾û•3­| ÕQàEÊÃsŸƒŸ.ŽM=eºj½²[Û]aêk†Ø¼ƒ§ @'‘ˆ8GÃ.:ê»8±ÙBÏS ¯%Ñ»Y=Å»¸,ªñ{¤˜ ¼þ'3øyküºôKX|½_úÅnÜÍ•û|ƒ»›UX1¸|ÄÄÀOK\É&ýñÀø8ì’œ«‘˜ÇÓÌZîJ×4£¢®’Ÿ®D´»zéÝ…ÎA:â{z¯ß†äÔ þ~Y1ž­êaýpr|Â@X€]åYÿ™;çç¼µXÍ×cça‰»IIvÝì ÓÚÁ?ûCŒ»‡_A[\ ³µË<^L8~êL0ߦ\¼\…Ô‡‘°P Œ˜Û¹xn@X:ö_Í[åËQ÷vÕüˆd/pVà­?ä]T#¯‹ƒm@[F«I޶èœýd •㇤EpiãÈãï*WïËõÊ™\K3hÚ8‹ÛK‰†wè†ê:2W¾®°ž_Lc\ù¾³ÈB¹,ŽCÖãbŬ+!ùx­æZŠé¨g ^Ø> xˆÝ¬2Ð(³uìóÚÜ(öú¹e™TËÌ>+%×F°œ—;¶ß5<¯ä·FÌñ\¨räb¼ÐÉÍþ2Ä_ˆ`8\­—÷6ìrP>ж³~'Í/¡–ؼxÈÚ§?ÿßZkÞ»Íxî-'ÁG°ãcPLóp‰%Ó/ó@–]Hø„k'uCC¬‹¹jaÀw%æÜ›)\Óæ5Ch¼³½§%f0d}yL3߸4øæ/ÆPªY¸¯þÓtÀ±Ë_ÏïîfŽŒ,HN¬qúõ°PnˆÞˆÓaàýáø¹/½š#ùšÄ\›Ž\²@ŠÍ¥ufË7Ð/W¹ÆƒîÚá7©H¬À<°Å¿º‰á:U¡¶Îgµ‡q§íÖî-ªŸ¯¹lm*»†tmo¿1Îb&¼¸†±^Êâù5_±W)’vwpH Ϻbà«û» åuñ³¶¥¤^Ý,(…×¢.G½,¼þÆm{µrmºzÃÚúGŠï(ŽÉî§ãKzþ³ -ª n¦DW)Úûd>dÄÝÇ>_8H?]rØû¼Ò7_^ƒŽ‡¨üÌQÈÔ[h‡ì¬Yå`9ü] –ë:ŽçÌþ×üº#"A•ûÉ%‰‰)JöuÕõ©1ÄC_u{žêuVH©+4:«7ž2V!„¾ ö½yYâ|êhÁÍ›^¨²á=gFã¸?s±ìÖÇ×£T“Æpˆ Þñl-#‚ýì´‘n=åáA¦Ü”?bºŠ^ù?Š;W+>/¸å‡ƒÞí9^Éè…¥gT » ¢ØulC »<Â¥³øOÎÅZ+Ðù1ôR”dçÈ Hÿ" ½a‹ü›üÈ‹~Þ€  ]‘èðóx»[Àõ&¨Jaŵ¤b×`“M³Û$¾m„¹R²BÉ£ÂcIÂL¾8Œ>/.ÆcbÚYGÏ{–>ŸøÞÃdôOnÞ媒çO}ÉzÐÎÔ¾ÎÀwÒßúÂûÀÆ.Õ÷Ò¸‰‡NúBàÌ›y2–&ö³ùaôY†Ã7fèÊ›œÕ¹ðÊ-âŒÿonE¯/¥òŽDè$ ‹„z!ô‹Í&“I…hû2šlãÔËübhê‰]Cx´3¹.§MHŠæ«y«Ã¶Ç}ݬ‡-z°Ÿê C¾edKi=ùìÞZ»´ç꫊Ù9O»ú­»œ±yxcõÉó_~Ñ{Îïøö° 2€¼ã콇vݸ84²Nð_ßë¯7Æú-LÞ,—›sÚO¬Œ‘–{°ÅOzå|GÁZp¬!¸ÀëãÅaÔ/Ö÷5ÇL\kôùŠþøRwy°õ³5ɉƒ<ñmmŒ[Þ˜8íï‰+2p0ù’q»6UÈ¿µl¯?{„±•bÑ/–¹{"Uáï õWz+†à²Ù5£¸¾[rz"šGÆ-O6’ùⶆþŠbù5—fÛµ6rrh³Ü%´åc‡yMS¤`÷ o‚™Kz5­ ÛHÆ ýÂûDMLØEŠ=½|ƒÃÏâÙ)UMÇŠFÓ nófR7/Î'ãŸÚêfŒt}Z.Ëu`t@½Ù˜swâQdŽΫ1(¯ÿ»ÙwŒvk«÷q‡± ðá°šÐ^|Ö Uq€);ƪB®ëëÅ܃àaªgã?ÿýQ„šó”‹¨9à'Õèõ°¸™K¤ò¿ë<{ƒaÛX¯^ω1×E÷,vö¸–XZ'XÊH‡#îƒVu'˜hŽd¼ööº¨—¦Øo9o­¡VDJñ!öÆsÏDÚ({1¤¿×kÙþB÷äîÌ¡AÝUOrÕbé$„¤9ÁÙæNœUÎÖ¶ãÝúvô½›W!­øâpZ7>¹áÍ"Å!þyÓé*ð­Ý`þ­_—¿¦ÆÔò«9Û>PUÿxs´ùýÅ­LLWÄâ:øê‡ú]`3äp[§xRÖµd¼‹é&­=\í•>¯åäòÆ`rÇàïÀ°`ˆå`“YÇ›;»Ÿ¿*‰½$­¹7„,¯*Bˆwëa4‰¨…ê?tÒØ ¸ü)¾ˆLdÎJ©AiÞ©7rIÎÞå^ õªj„[>8<³Ÿÿ:’ѯ-‰=e¯ÆOµßvC0»O³ú‹‚Æ$“î Í2­6en™p¶VS-«Œò,ÃwÖÚa#1[qò5žrºsp²»'ÉɸËJœ§q™ÖÒvc¢6йñ.,«*>›<Ú)ðü]À S\ÎúÍ’þªóð=ŒIqÛ´trúço¾<ÊuSÂá4…æ…ûÅ‚?Ó~1Ïž¾›FpM]5¶êBMž:dA¸UºÄ4^u—µ~´CŒêO€¨šœÕI/JkgÏŒ×O¬F Øìhc_ldvw}ñ߸j¢ £¿XNk$îÕìoÜt¼ØÐ‰É72œð Ä› ‹Ë/ßйkÁyzÐTT4jg޶ôJ½Rèwxiÿ6•·¼`ì¿5p¸ã¡£>ÏqêüñæLÏKÊNÐWEèºïB¥}›lòƒë }ì¯Tßÿ^ÛÖ ¯Ù ,Ç8§qM F1Î AT9é¿»Ò·7Àšò:ßomÌ/²yÔ‰Þ°?qÛÖvW‹µ4#;ay!+ù|"ôç« j3Pª6ÕF¾¯_Ê@zô7ÚúøÿÊþû/;¦»k5Ù¿qˆÕcÏ…åýøÔí¹è0à—Ëjð,‹›N³–ó‹sõ2b7£{EJ[aÔôÔî`éikg[«¤_é×ãð¤:ÖÄXzx ë6lJÈé;«eé1°ö-ÚôÊl˜Ñ»¨ ¯udßÓƒ2ïFï¾0´Ý þ&¢Ÿrm3!ì%fàl?þ78'Ÿ¶ã“¼©™›9NÞÊÈ|P1¾©Õð‚¸‹ïÜT;k1ŒÆÙkk£œ”?Nå·ÍúëÐPwúìµ°OæâõGs2¢oŒÕ‹.F1Ø•‰ÝËKÎ]^p>`iOý’`3WpíºÍʉ¢•Ûo,bu£q9"°_½”·-o>} õÍ©è[ÌÁǧ4¬¶ß$Ù)G@| vÀ]ò*ævð2÷]FDû3\ÞUèѽ÷÷è]œýÚjÞV (›ý“G#†‹¾üÕ·˜Åt1Ûî•ÍÜˬ¿§³“Çæ¼r¶ àý…§LP\Há¸[ZŸ}J>³W[{½äz|6òG>*jõÅ ¶<œOqåÈÈöéºa<pŒY9a¶üÕ2wC&Ò¸QÓ¥cçi©ýzÝ8IÞpkfTp|»}cFàˆŽþ΂˜c}uƒL½›ÒßÉÈ>ë?Œ­ñ1OãÒÿ¦ÐäO¿óx52ÎlqÔž÷ÿy¨ `Ü›Çxð’1ÿ5ŒnsŒý|’ï{)ëÚ¤@é¯P]·Évã·ÿbyÜK)ö鬊biEްdÄ-ž+ŒÔ"›šÃÝX~±Ö¸'[»éþÙWP>”~~º`/7gWs÷…ËÞ˜™OëæïCý1êG{zsÔiþ‡ºñÞ¼ö­sZëí;w8'ÚtŒ°û þ:ߨ”o‹5²xÔàÏu™øæÚ1ªo\@z…RLÙ£x33~mZ£P1&Ï´Ëå,V1VA¥>Ø;Ⱥ¤A¡Ü6Í/rÇܦˆÆh89ÆFðl”;]ÓÚ½u>\yñ+ÞÁਸ਼zÖvòIqÒú]?ªšç{y•¹Øxß^W’µÅI9èÚ¨G®$þr>³)ÛŽÆ"Z>dę̻јÞBˆß\œîÏ~-óLˆ¼‘<;åþ¢71Àe¦—ÚóË…bMö×ANo–4a·Vï©õÐÊ0ûpÁ3¼[=ôëëSÿ Hëâr”ìÍÂÐûÊÏÿ0öîšlËy54çè.ÖÝG›°éó_H°çôõ!­úõ¯ŠO‡Á[EWG+!5«*ëõK¤ÖùiA3fíj‘”}o*õÌèW»ôÒÜ?†¡õ*cMÆî™Î­ï"º^·ÆP™¶{´ö^>>ͼjŠÜ®í?/Ëc¾µ™ÿ²죸Fï›-Ï“áÅå¡öìþÃï¦b²à G ‡%d0öÄ€^‚FNJ±É rÿÛŒûm›ZPØßC‡°Môe2„ǽ~ê·‚‡‹Yñ9ùBxò"Ò'Æm[¹"›îżþöÉ<~é‹6P]œÉ›h’ ˜^Ø¢¥Ö Õ› †—;Ma£¼<Ò{©.gÏæmdM ñ¬µrÙ^ž4“Iýøç¡¯!$s𵜼PÃ!JÆñq£ùˆm»xlBj¯…ÒPø•Û‘uZ¡7NòV½O@gÓe“âÔT÷=Ê:+¼Ýºm.F5yÆ[šKùKÖB’èü?gmëú‘°ÉÕüíŠnÁê• Í«‚#åFˆþÑ>ÍW»Ç;zÄÊbt_DD[.cý«›~ñ/ám߬ÐmñèÜÎû%óå¥Åÿ÷®òôÅb[>°p>ÍQO»£X“¼»hn,‡]ó/ÕUŒ¯‡Ùà^yžØ¹»q,Ƨ²Òꬅµ^}ÈW:íø±Hf6^Kc”p±¤Îž¤„¶+˜}Ù©bS²¾([ýóñi‹òÚ6o5­ÉÇ s:/{2à“)Áèš“&Å–GnRöú“¦8‚BÙL¤?Kâ±taãlŠƒã£È_=i‡óX·_[ã7÷òu7.YMO}Û‚ü|(ÿ§Oô̱mÄÖÂLs~„ƒ;¾ßŸÆJˆ],†µ¼õÿ1šc¾1؃0'åÆr›14 Õ}í·fÄUmiý;8ì ·,õs±¯2ó­ý¶å¾ûÎ…“!æìÛŒÃíY½üâl|¬/(öÖ°eŒA×¹¹ñcz³+„+‰ÚC`#µúqž„!¦;ßtA±ád_t P²c*ÕÕ ÎŒgúóÊZ–ÅûÚžÆìݽýøjµYȽüÂÂKöú÷>¿Ô5ǺÈ~€‘T¿Š‚ðºØ­§L¥l;××{:Î*Uð×ééTC4­íêk]pmÐà²Ë~×Ûó†ÎDÕ8æoF ©iØðä1ÛWGE¶ÅzæÞ½ìT°!{–õ=—Ï…rZ×ñ6YE$÷¬ÆÝøÓæ¨n˜—÷,d3¾õ†wœ ã„ðÆÆÙwOóYa+WÒ|_,hHM$¬l9_?Œt¦s÷¯`iûpºOázä ó.Ñxel9žy&dùásÅ3ÌÙo’Y8w9ˆ'ÚÆ”à¾Ê 5Ç&äÊ£ÐrºÈÛæ‡ÆÖ*Ë—ˆ£>îb5_±àš®Ï¯ÞJö"BÖw•ÍÆhX‚åkt7öã›Y.D¼\r™‰öl @t·0#ƒgQì!ØD¿i–Ûºƒ9\ö³G\èã9»qʸhÌÎÙÑMÚ:°ÇÄÈ­¥rÞv8vN}yi£X®ìeP‹æ›ÑèùáJÎ,Ïùø±q¼&Oì„,Jlw9/:šÒ(Ú'*ÖÆ…P.쬖ìÈLöâÐl:ñ¬¿gµx&#€Á~Qjã|müÁhÆcXó]¼'ÛE—Ë€¯ý¸‡“k×AÎNÑænNnbZãAÈe~‚†`_Lå­r?sûÚ²ëÏ´2÷¦I â\k}Å}WwOÌ‘ˆƒ—“yQýÖüü7:cJ'NˆÇW‹ÍŒì3gÞûL€sîC¯ïdóbäͧ]«Qõh¡âl6ÑÖm³Pc.|ÚôM§y$»ÀÛ„4!<™»ØÔ´1Ò®Í3giüñÞ¿b ë_·“§Ð5aÿ§Ã5#ùç„h/–îÁ§ÏÒ‚a¦2Åj숊M¤•óµ:í>©|æórÐÌíõLJÔö}°"@¿¢´{qg'ŸÚÙmŽØÍ©Ã(ùj±¯ãöÏÿý´¡ã5XÖ.}IEND®B`‚././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638214186.0 photutils-1.3.0/docs/psf_spec/block_template.rst0000644000214200020070000000264600000000000020457 0ustar00lbradley:orphan: BlockClassName ============== [Link to code if it exists] A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/culler_and_ender.rst0000644000214200020070000000266000000000000020753 0ustar00lbradleyCullerAndEnder ============== EJT: No currently existing code in `photutils`. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/finder.rst0000644000214200020070000000305100000000000016730 0ustar00lbradleyObjectFinder ============ EJT: Existing code documented at https://photutils.readthedocs.io/en/stable/api/photutils.detection.StarFinderBase.html - see the ``find_stars`` function for the basic API. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/fitter.rst0000644000214200020070000000057700000000000016770 0ustar00lbradleyFitter ====== The fitter block is unique in that it is a class not implemented as part of `photutils`. Rather, it is an object that follows the interface for fitters in the `astropy.modeling` package. Note that implicitly the fitters for PSF photometry are always fitters appropriate for *2D* models (i.e., 2 input dimensions for the x and y pixel coordinates and a "flux" output). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/group_maker.rst0000644000214200020070000000331500000000000017777 0ustar00lbradleyGroupMaker ========== EJT: Documented as the ``__call__`` method of ``GroupStarsBase`` - see https://photutils.readthedocs.io/en/stable/api/photutils.psf.groupstars.GroupStarsBase.html It'll be substantial work to re-design the photometry loops if this is changed in a backwards-incompatible manner, but of course that's possible if there's a good reason for it. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638214186.0 photutils-1.3.0/docs/psf_spec/index.rst0000644000214200020070000000055600000000000016577 0ustar00lbradley:orphan: PSF Photometry Block Diagram Specification ========================================== The block diagram: .. image:: block_diagram.png Blocks ------ .. toctree:: :maxdepth: 1 background_estimator block_template culler_and_ender finder fitter group_maker noise_data psf_model scene_maker single_object_model ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/noise_data.rst0000644000214200020070000000265000000000000017573 0ustar00lbradleyNoiseModel ========== EJT: No currently existing code in `photutils`. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/psf_model.rst0000644000214200020070000000355700000000000017444 0ustar00lbradleyPsfModel ======== EJT: the PSF models in the current ``photutils.psf`` are not explicitly defined as a specific class, but any 2D model (inputs: x,y, output: flux) can be considered a PSF Model. The `~photutils.psf.PRFAdapter` class is the clearest application specific example, however, and demonstrates the required convention for *names* of the PSF model's parameters. Hopefully that class can be used *directly* as this block, as it is meant to wrap any arbitrary other models to make it compatible with the machinery. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/scene_maker.rst0000644000214200020070000000317300000000000017742 0ustar00lbradleySceneMaker ========== EJT: This object is not currently in the block diagram, as it represents a step beyond the "baseline" PSF fitting machinery. It should be developed in parallel with the ``SingleObjectModel``, which really doesn't have reason to exist without the scene maker. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/psf_spec/single_object_model.rst0000644000214200020070000000373200000000000021456 0ustar00lbradleySingleObjectModel ================= EJT: This does not exist in the current `photutils.psf` model, because there is not an explicit separate single object model. Instead the psf_model is used directly, as the "single object model" is implicitly a delta function. To maintain backwards-compatibility, the new ``SingleObjectModel`` will need to default to the "point source" object model, and behave the same as the current behavior of a model with shape parameters is provided as the "psf model". But arguable that is *not* the desired behavior in the "new" paradigm that combines the ``SceneMaker``and the ``SingleObjectModel``. A single sentence summarizing this block. A longer description. Can be multiple paragraphs. You can link to other things like `photutils.background`. Parameters ---------- first_parameter_name : `~astropy.table.Table` Description of first input second_parameter_name : SomeOtherType Description of second input (if any) Returns ------- first_return : `~astropy.table.Table` Description of the first thing this block outputs. second_return Many blocks will only return one object, but if more things are returned they can be described here (e.g., in python this is ``first, second = some_function(...)``) Methods ------- Not all blocks will have these, but if desired some blocks can have methods that let you do something other than just running the block. E.g:: some_block = BlockClassName() output = some_block(input1, input2, ...) # this is what is documented above result = some_block.method_name(...) #this is documented here method_name ^^^^^^^^^^^ Description of method Parameters """""""""" first_parameter : type Description ... second_parameter : type Description ... Returns """"""" first_return : type Description ... Example Usage ------------- An example of *using* the block should be provided. This needs to be after a ``::`` in the rst and indented:: print("This is example code") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1614308896.0 photutils-1.3.0/docs/rtd_requirements.txt0000644000214200020070000000003200000000000017256 0ustar00lbradleymatplotlib<3.1 sphinx>2.* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639445440.0 photutils-1.3.0/docs/segmentation.rst0000644000214200020070000006520400000000000016364 0ustar00lbradley.. _image_segmentation: Image Segmentation (`photutils.segmentation`) ============================================= Introduction ------------ Photutils includes a general-use function to detect sources (both point-like and extended) in an image using a process called `image segmentation `_. After detecting sources using image segmentation, we can then measure their photometry, centroids, and morphological properties by using additional tools in Photutils. Source Extraction Using Image Segmentation ------------------------------------------ Photutils provides tools to detect astronomical sources using image segmentation, which is a process of assigning a label to every pixel in an image such that pixels with the same label are part of the same source. Detected sources must have a minimum number of connected pixels that are each greater than a specified threshold value in an image. The threshold level is usually defined at some multiple of the background noise (sigma) above the background. The image can also be filtered before thresholding to smooth the noise and maximize the detectability of objects with a shape similar to the filter kernel. Let's start by detecting sources in a synthetic image provided by the :ref:`photutils.datasets ` module:: >>> from photutils.datasets import make_100gaussians_image >>> data = make_100gaussians_image() The source segmentation/extraction is performed using the :func:`~photutils.segmentation.detect_sources` function. We will use a convenience function called :func:`~photutils.segmentation.detect_threshold` to produce a 2D detection threshold image using simple sigma-clipped statistics to estimate the background level and RMS. The threshold level is calculated using the ``nsigma`` input as the number of standard deviations (per pixel) above the background. Here we generate a simple threshold at 2 sigma (per pixel) above the background:: >>> from photutils.segmentation import detect_threshold >>> threshold = detect_threshold(data, nsigma=2.) For more sophisticated analyses, one should generate a 2D background and background-only error image (e.g., from your data reduction or by using :class:`~photutils.background.Background2D`). In that case, a 2-sigma threshold image is simply:: >>> threshold = bkg + (2.0 * bkg_rms) # doctest: +SKIP Note that if the threshold includes the background level (as above), then the image input into :func:`~photutils.segmentation.detect_sources` should *not* be background subtracted. In other words, the input threshold value(s) are compared directly to the input image. Because the threshold returned by :func:`~photutils.segmentation.detect_threshold` includes the background, we do not subtract the background from the data here. Let's find sources that have 5 connected pixels that are each greater than the corresponding pixel-wise ``threshold`` level defined above (i.e., 2 sigma per pixel above the background noise). Note that by default "connected pixels" means "8-connected" pixels, where pixels touch along their edges or corners. One can also use "4-connected" pixels that touch only along their edges by setting ``connectivity=4`` in :func:`~photutils.segmentation.detect_sources`. We will also input a 2D circular Gaussian kernel with a FWHM of 3 pixels to smooth the image prior to thresholding: .. doctest-requires:: scipy>=1.6.0 >>> from astropy.convolution import Gaussian2DKernel >>> from astropy.stats import gaussian_fwhm_to_sigma >>> from photutils.segmentation import detect_sources >>> sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. >>> kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) >>> kernel.normalize() >>> segm = detect_sources(data, threshold, npixels=5, kernel=kernel) The result is a :class:`~photutils.segmentation.SegmentationImage` object with the same shape as the data, where detected sources are labeled by different positive integer values. A value of zero is always reserved for the background. Let's plot both the image and the segmentation image showing the detected sources: .. doctest-skip:: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> from astropy.visualization import SqrtStretch >>> from astropy.visualization.mpl_normalize import ImageNormalize >>> norm = ImageNormalize(stretch=SqrtStretch()) >>> fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12.5)) >>> ax1.imshow(data, origin='lower', cmap='Greys_r', norm=norm) >>> ax1.set_title('Data') >>> cmap = segm.make_cmap(seed=123) >>> ax2.imshow(segm, origin='lower', cmap=cmap, interpolation='nearest') >>> ax2.set_title('Segmentation Image') .. plot:: from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image from photutils.segmentation import detect_threshold, detect_sources data = make_100gaussians_image() threshold = detect_threshold(data, nsigma=2.) sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() segm = detect_sources(data, threshold, npixels=5, kernel=kernel) norm = ImageNormalize(stretch=SqrtStretch()) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12.5)) ax1.imshow(data, origin='lower', cmap='Greys_r', norm=norm) ax1.set_title('Data') cmap = segm.make_cmap(seed=123) ax2.imshow(segm, origin='lower', cmap=cmap, interpolation='nearest') ax2.set_title('Segmentation Image') plt.tight_layout() When the segmentation image is generated using image thresholding (e.g., using :func:`~photutils.segmentation.detect_sources`), the source segments represent the isophotal footprints of each source. Source Deblending ----------------- In the example above, overlapping sources are detected as single sources. Separating those sources requires a deblending procedure, such as a multi-thresholding technique used by `SourceExtractor`_. Photutils provides a :func:`~photutils.segmentation.deblend_sources` function that deblends sources uses a combination of multi-thresholding and `watershed segmentation `_. Note that in order to deblend sources, they must be separated enough such that there is a saddle between them. The amount of deblending can be controlled with the two :func:`~photutils.segmentation.deblend_sources` keywords ``nlevels`` and ``contrast``. ``nlevels`` is the number of multi-thresholding levels to use. ``contrast`` is the fraction of the total source flux that a local peak must have to be considered as a separate object. Here's a simple example of source deblending: .. doctest-requires:: scipy>=1.6.0, skimage >>> from photutils.segmentation import deblend_sources >>> segm_deblend = deblend_sources(data, segm, npixels=5, kernel=kernel, ... nlevels=32, contrast=0.001) where ``segm`` is the :class:`~photutils.segmentation.SegmentationImage` that was generated by :func:`~photutils.segmentation.detect_sources`. Note that the ``npixels`` and ``kernel`` input values should match those used in :func:`~photutils.segmentation.detect_sources` to generate ``segm``. The result is a new :class:`~photutils.segmentation.SegmentationImage` object containing the deblended segmentation image: .. plot:: from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma from astropy.visualization import SqrtStretch from astropy.visualization.mpl_normalize import ImageNormalize import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image from photutils.segmentation import (detect_threshold, detect_sources, deblend_sources) data = make_100gaussians_image() threshold = detect_threshold(data, nsigma=2.) sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() segm = detect_sources(data, threshold, npixels=5, kernel=kernel) segm_deblend = deblend_sources(data, segm, npixels=5, kernel=kernel) norm = ImageNormalize(stretch=SqrtStretch()) fig, ax = plt.subplots(1, 1, figsize=(10, 6.5)) cmap = segm_deblend.make_cmap(seed=123) ax.imshow(segm_deblend, origin='lower', cmap=cmap, interpolation='nearest') ax.set_title('Deblended Segmentation Image') plt.tight_layout() Let's plot one of the deblended sources: .. plot:: from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image from photutils.segmentation import (detect_threshold, detect_sources, deblend_sources) data = make_100gaussians_image() threshold = detect_threshold(data, nsigma=2.) sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() segm = detect_sources(data, threshold, npixels=5, kernel=kernel) segm_deblend = deblend_sources(data, segm, npixels=5, kernel=kernel) fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 4)) slc = (slice(273, 297), slice(425, 444)) ax1.imshow(data[slc], origin='lower') ax1.set_title('Data') cmap1 = segm.make_cmap(seed=123) ax2.imshow(segm.data[slc], origin='lower', cmap=cmap1, interpolation='nearest') ax2.set_title('Original Segment') cmap2 = segm_deblend.make_cmap(seed=123) ax3.imshow(segm_deblend.data[slc], origin='lower', cmap=cmap2, interpolation='nearest') ax3.set_title('Deblended Segments') plt.tight_layout() Modifying a Segmentation Image ------------------------------ The :class:`~photutils.segmentation.SegmentationImage` object provides several methods that can be used to visualize or modify itself (e.g., combining labels, removing labels, removing border segments) prior to measuring source photometry and other source properties, including: * :meth:`~photutils.segmentation.SegmentationImage.reassign_label`: Reassign one or more label numbers. * :meth:`~photutils.segmentation.SegmentationImage.relabel_consecutive`: Reassign the label numbers consecutively, such that there are no missing label numbers (up to the maximum label number). * :meth:`~photutils.segmentation.SegmentationImage.keep_labels`: Keep only the specified labels. * :meth:`~photutils.segmentation.SegmentationImage.remove_labels`: Remove one or more labels. * :meth:`~photutils.segmentation.SegmentationImage.remove_border_labels`: Remove labeled segments near the image border. * :meth:`~photutils.segmentation.SegmentationImage.remove_masked_labels`: Remove labeled segments located within a masked region. * :meth:`~photutils.segmentation.SegmentationImage.outline_segments`: Outline the labeled segments for plotting. Centroids, Photometry, and Morphological Properties --------------------------------------------------- The :class:`~photutils.segmentation.SourceCatalog` class is the primary tool for measuring the centroids, photometry, and morphological properties of sources defined in a segmentation image. When the segmentation image is generated using image thresholding (e.g., using :func:`~photutils.segmentation.detect_sources`), the source segments represent the isophotal footprint of each source and the resulting photometry is effectively isophotal photometry. The source properties can be accessed using `~photutils.segmentation.SourceCatalog` attributes or output to an Astropy `~astropy.table.QTable` using the :meth:`~photutils.segmentation.SourceCatalog.to_table` method. Please see :class:`~photutils.segmentation.SourceCatalog` for the the many properties that can be calculated for each source. More properties are likely to be added in the future. Let's detect sources and measure their properties in a synthetic image. `~photutils.segmentation.SourceCatalog` requires that the input data be background-subtracted, so for this example we will use the :class:`~photutils.background.Background2D` class to produce a background and background noise image. After subtracting the background, we define a 2D detection threshold image using only the background RMS image. We set the threshold at the 2-sigma (per pixel) noise level. In this example, the threshold does not include the background level because it was already subtracted from the data: .. doctest-requires:: scipy>=1.6.0 >>> from astropy.convolution import Gaussian2DKernel >>> from photutils.datasets import make_100gaussians_image >>> from photutils.background import Background2D, MedianBackground >>> from photutils.segmentation import detect_threshold, detect_sources >>> data = make_100gaussians_image() >>> bkg_estimator = MedianBackground() >>> bkg = Background2D(data, (50, 50), filter_size=(3, 3), ... bkg_estimator=bkg_estimator) >>> data -= bkg.background # subtract the background >>> threshold = 2. * bkg.background_rms # above the background Now we find sources that have 5 connected pixels (``npixels`` keyword) that are each greater than the corresponding threshold image defined above. We also input a 2D circular Gaussian kernel with a FWHM of 3 pixels to filter the image prior to thresholding: .. doctest-requires:: scipy>=1.6.0, skimage >>> from astropy.stats import gaussian_fwhm_to_sigma >>> sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. >>> kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) >>> kernel.normalize() >>> npixels = 5 >>> segm = detect_sources(data, threshold, npixels=npixels, kernel=kernel) >>> segm_deblend = deblend_sources(data, segm, npixels=npixels, ... kernel=kernel, nlevels=32, ... contrast=0.001) As described earlier, the result is a :class:`~photutils.segmentation.SegmentationImage` where sources are labeled by different positive integer values. Now let's measure the properties of the detected sources defined in the segmentation image using the simplest call to :class:`~photutils.segmentation.SourceCatalog`. The output `~astropy.table.QTable` of source properties is generated by the :class:`~photutils.segmentation.SourceCatalog` :meth:`~photutils.segmentation.SourceCatalog.to_table` method. Each row in the table represents a source. The columns represent the calculated source properties. Note that the only a subset of the source properties are shown below. Please see `~photutils.segmentation.SourceCatalog` for the list of the many properties that are calculated for each source: .. doctest-requires:: scipy>=1.6.0, skimage >>> from photutils.segmentation import SourceCatalog >>> cat = SourceCatalog(data, segm_deblend) >>> tbl = cat.to_table() >>> tbl['xcentroid'].info.format = '.2f' # optional format >>> tbl['ycentroid'].info.format = '.2f' >>> tbl['kron_flux'].info.format = '.2f' >>> print(tbl) label xcentroid ycentroid ... segment_fluxerr kron_flux kron_fluxerr ... ----- --------- --------- ... --------------- --------- ------------ 1 235.16 1.10 ... nan 511.36 nan 2 494.14 5.80 ... nan 545.61 nan 3 207.29 10.04 ... nan 694.35 nan 4 364.81 11.16 ... nan 737.47 nan 5 258.28 11.83 ... nan 662.84 nan ... ... ... ... ... ... ... 91 427.05 147.44 ... nan 893.53 nan 92 426.63 211.10 ... nan 895.31 nan 93 419.74 216.64 ... nan 871.98 nan 94 433.95 280.72 ... nan 638.68 nan 95 434.08 288.93 ... nan 930.92 nan Length = 95 rows The error columns are NaN because we did not input an error array (see the Photometric Errors section below). Let's now plot the calculated elliptical Kron apertures (based on the shapes of each source) on the data: .. doctest-skip:: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> from astropy.visualization import simple_norm >>> norm = simple_norm(data, 'sqrt') >>> fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12.5)) >>> ax1.imshow(data, origin='lower', cmap='Greys_r', norm=norm) >>> ax1.set_title('Data') >>> cmap = segm_deblend.make_cmap(seed=123) >>> ax2.imshow(segm_deblend, origin='lower', cmap=cmap, ... interpolation='nearest') >>> ax2.set_title('Segmentation Image') >>> cat.plot_kron_apertures((2.5, 1.0), axes=ax1, color='white', lw=1.5) >>> cat.plot_kron_apertures((2.5, 1.0), axes=ax2, color='white', lw=1.5) .. plot:: from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image from photutils.background import Background2D, MedianBackground from photutils.segmentation import (detect_sources, deblend_sources, SourceCatalog) data = make_100gaussians_image() bkg_estimator = MedianBackground() bkg = Background2D(data, (50, 50), filter_size=(3, 3), bkg_estimator=bkg_estimator) data -= bkg.background threshold = 2. * bkg.background_rms sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() npixels = 5 segm = detect_sources(data, threshold, npixels=npixels, kernel=kernel) segm_deblend = deblend_sources(data, segm, npixels=npixels, kernel=kernel, nlevels=32, contrast=0.001) cat = SourceCatalog(data, segm_deblend) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12.5)) norm = simple_norm(data, 'sqrt') ax1.imshow(data, origin='lower', cmap='Greys_r', norm=norm) ax1.set_title('Data with Kron apertures') cmap = segm_deblend.make_cmap(seed=123) ax2.imshow(segm_deblend, origin='lower', cmap=cmap, interpolation='nearest') ax2.set_title('Segmentation Image with Kron apertures') cat.plot_kron_apertures((2.5, 1.0), axes=ax1, color='white', lw=1.5) cat.plot_kron_apertures((2.5, 1.0), axes=ax2, color='white', lw=1.5) plt.tight_layout() We can also create a `~photutils.segmentation.SourceCatalog` object containing only a specific subset of sources, defined by their label numbers in the segmentation image: .. doctest-requires:: scipy>=1.6.0, skimage >>> cat = SourceCatalog(data, segm_deblend) >>> labels = [1, 5, 20, 50, 75, 80] >>> cat_subset = cat.get_labels(labels) >>> tbl2 = cat_subset.to_table() >>> tbl2['xcentroid'].info.format = '.2f' # optional format >>> tbl2['ycentroid'].info.format = '.2f' >>> tbl2['kron_flux'].info.format = '.2f' >>> print(tbl2) label xcentroid ycentroid ... segment_fluxerr kron_flux kron_fluxerr ... ----- --------- --------- ... --------------- --------- ------------ 1 235.16 1.10 ... nan 511.36 nan 5 258.28 11.83 ... nan 662.84 nan 20 347.02 66.92 ... nan 816.42 nan 50 145.06 168.54 ... nan 714.87 nan 75 301.92 239.25 ... nan 520.57 nan 80 43.22 250.03 ... nan 641.60 nan By default, the :meth:`~photutils.segmentation.SourceCatalog.to_table` includes only a small subset of source properties. The output table properties can be specified (or excluded) in the `~astropy.table.QTable` via the ``columns`` or ``exclude_columns`` keywords: .. doctest-requires:: scipy>=1.6.0, skimage >>> cat = SourceCatalog(data, segm_deblend) >>> labels = [1, 5, 20, 50, 75, 80] >>> cat_subset = cat.get_labels(labels) >>> columns = ['label', 'xcentroid', 'ycentroid', 'area', 'segment_flux'] >>> tbl3 = cat_subset.to_table(columns=columns) >>> tbl3['xcentroid'].info.format = '.4f' # optional format >>> tbl3['ycentroid'].info.format = '.4f' >>> tbl3['segment_flux'].info.format = '.4f' >>> print(tbl3) label xcentroid ycentroid area segment_flux pix2 ----- --------- --------- ---- ------------ 1 235.1598 1.1019 38.0 421.5376 5 258.2846 11.8292 55.0 370.8626 20 347.0218 66.9183 73.0 479.8884 50 145.0620 168.5415 33.0 714.4084 75 301.9164 239.2531 37.0 210.0043 80 43.2195 250.0333 55.0 340.2903 A `~astropy.wcs.WCS` transformation can also be input to :class:`~photutils.segmentation.SourceCatalog` via the ``wcs`` keyword, in which case the sky coordinates of the source centroids can be calculated. Background Properties ^^^^^^^^^^^^^^^^^^^^^ Like with :func:`~photutils.aperture.aperture_photometry`, the ``data`` array that is input to :class:`~photutils.segmentation.SourceCatalog` should be background subtracted. If you input the background image that was subtracted from the data into the ``background`` keyword of :class:`~photutils.segmentation.SourceCatalog`, the background properties for each source will also be calculated: .. doctest-requires:: scipy>=1.6.0, skimage >>> cat = SourceCatalog(data, segm_deblend, background=bkg.background) >>> labels = [1, 5, 20, 50, 75, 80] >>> cat_subset = cat.get_labels(labels) >>> columns = ['label', 'background_centroid', 'background_mean', ... 'background_sum'] >>> tbl4 = cat_subset.to_table(columns=columns) >>> tbl4['background_centroid'].info.format = '{:.10f}' # optional format >>> tbl4['background_mean'].info.format = '{:.10f}' >>> tbl4['background_sum'].info.format = '{:.10f}' >>> print(tbl4) label background_centroid background_mean background_sum ----- ------------------- --------------- -------------- 1 5.2031158074 5.2024840319 197.6943932116 5 5.2369739745 5.2231049504 287.2707722718 20 5.2393038526 5.2751966277 385.0893538217 50 5.1749011227 5.1984776900 171.5497637688 75 5.1184051931 5.1235119327 189.5699415102 80 5.2010035835 5.2251343812 287.3823909639 Photometric Errors ^^^^^^^^^^^^^^^^^^ :class:`~photutils.segmentation.SourceCatalog` requires inputting a *total* error array, i.e., the background-only error plus Poisson noise due to individual sources. The :func:`~photutils.utils.calc_total_error` function can be used to calculate the total error array from a background-only error array and an effective gain. The ``effective_gain``, which is the ratio of counts (electrons or photons) to the units of the data, is used to include the Poisson noise from the sources. ``effective_gain`` can either be a scalar value or a 2D image with the same shape as the ``data``. A 2D effective gain image is useful for mosaic images that have variable depths (i.e., exposure times) across the field. For example, one should use an exposure-time map as the ``effective_gain`` for a variable depth mosaic image in count-rate units. Let's assume our synthetic data is in units of electrons per second. In that case, the ``effective_gain`` should be the exposure time (here we set it to 500 seconds). Here we use :func:`~photutils.utils.calc_total_error` to calculate the total error and input it into the :class:`~photutils.segmentation.SourceCatalog` class. When a total ``error`` is input, the `~photutils.segmentation.SourceCatalog.segment_fluxerr` property is calculated. `~photutils.segmentation.SourceCatalog.segment_flux` and `~photutils.segmentation.SourceCatalog.segment_fluxerr` are the instrumental flux and propagated flux error within the source segments: .. doctest-requires:: scipy>=1.6.0, skimage >>> from photutils.utils import calc_total_error >>> effective_gain = 500. >>> error = calc_total_error(data, bkg.background_rms, effective_gain) >>> cat = SourceCatalog(data, segm_deblend, error=error) >>> labels = [1, 5, 20, 50, 75, 80] >>> cat_subset = cat.get_labels(labels) # select a subset of objects >>> columns = ['label', 'xcentroid', 'ycentroid', 'segment_flux', ... 'segment_fluxerr'] >>> tbl5 = cat_subset.to_table(columns=columns) >>> tbl5['xcentroid'].info.format = '{:.4f}' # optional format >>> tbl5['ycentroid'].info.format = '{:.4f}' >>> tbl5['segment_flux'].info.format = '{:.4f}' >>> tbl5['segment_fluxerr'].info.format = '{:.4f}' >>> for col in tbl5.colnames: ... tbl5[col].info.format = '%.8g' # for consistent table output >>> print(tbl5) label xcentroid ycentroid segment_flux segment_fluxerr ----- --------- --------- ------------ --------------- 1 235.15975 1.1019039 421.53765 13.163281 5 258.28461 11.82915 370.8626 15.906871 20 347.02181 66.918298 479.88841 18.710953 50 145.06198 168.54152 714.40843 11.835923 75 301.91639 239.25306 210.00431 12.230255 80 43.219501 250.03333 340.29029 16.000811 Pixel Masking ^^^^^^^^^^^^^ Pixels can be completely ignored/excluded (e.g., bad pixels) when measuring the source properties by providing a boolean mask image via the ``mask`` keyword (`True` pixel values are masked) to the :class:`~photutils.segmentation.SourceCatalog` class. Filtering ^^^^^^^^^ `SourceExtractor`_'s centroid and morphological parameters are calculated from a filtered "detection" image. The usual downside of the filtering is the sources will be made more circular than they actually are (assuming a circular kernel is used, which is common). If you wish to reproduce `SourceExtractor`_ results, then use the :class:`~photutils.segmentation.SourceCatalog` ``kernel`` keyword to filter the ``data`` prior to centroid and morphological measurements. The kernel should be the same one used with :func:`~photutils.segmentation.detect_sources` that defined the segmentation image. If ``kernel`` is `None`, then the centroid and morphological measurements will be performed on the unfiltered ``data``. Note that photometry is *always* performed on the unfiltered ``data``. Reference/API ------------- .. automodapi:: photutils.segmentation :no-heading: .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/docs/test_function.rst0000644000214200020070000000012200000000000016537 0ustar00lbradleyPhotutils Test Function ======================= .. autofunction:: photutils.test ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1570647402.0 photutils-1.3.0/docs/utils.rst0000644000214200020070000000040100000000000015013 0ustar00lbradleyUtility Functions (`photutils.utils`) ===================================== Introduction ------------ The `photutils.utils` package contains general-purpose utility functions. Reference/API ------------- .. automodapi:: photutils.utils :no-heading: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9738927 photutils-1.3.0/docs/whats_new/0000755000214200020070000000000000000000000015125 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/whats_new/1.1.rst0000644000214200020070000000603100000000000016156 0ustar00lbradley.. doctest-skip-all .. _whatsnew-1.1: **************************** What's New in Photutils 1.1? **************************** Overview ======== Photutils 1.1 is a major release that adds new functionality since the 1.0 release. Here we highlight some of the major changes. Please see the :ref:`changelog` for the complete list of changes. New SourceCatalog class ======================= A new, significantly faster, `~photutils.segmentation.SourceCatalog` class was implemented. This new class simplifies the API and takes the place of the :func:`~photutils.segmentation.source_properties` function and the :class:`~photutils.segmentation.SourceProperties` and `~photutils.segmentation.LegacySourceCatalog` classes. The :func:`~photutils.segmentation.source_properties` function and :class:`~photutils.segmentation.SourceProperties` are now deprecated and will eventually be removed. The :func:`~photutils.segmentation.source_properties` function now returns a `~photutils.segmentation.LegacySourceCatalog` class (deprecated) to distinguish it from the new `~photutils.segmentation.SourceCatalog`. Optional keyword arguments in `~photutils.segmentation.SourceCatalog` can not be input as positional arguments. Please see the `~photutils.segmentation.SourceCatalog` documentation for the keywords inputs and their allowed values. Renamed properties ------------------ Note that many of the source properties have been slightly renamed in the new `~photutils.segmentation.SourceCatalog` class, e.g., * 'id' -> 'label' * 'background_at_centroid' -> 'background_centroid' * 'background_cutout' -> 'background' * 'background_cutout_ma' -> 'background_ma' * 'data_cutout' -> 'data' * 'data_cutout_ma' -> 'data_ma' * 'error_cutout' -> 'error' * 'error_cutout_ma' -> 'error_ma' * 'filtered_data_cutout_ma' -> 'convdata_ma' * 'minval_pos' -> 'minval_index' * 'minval_xpos' -> 'minval_xindex' * 'minval_ypos' -> 'minval_yindex' * 'maxval_pos' -> 'maxval_index' * 'maxval_xpos' -> 'maxval_xindex' * 'maxval_ypos' -> 'maxval_yindex' * 'semimajor_axis_sigma' -> 'semimajor_sigma' * 'semiminor_axis_sigma' -> 'semiminor_sigma' * 'source_sum' -> 'segment_flux' * 'source_sum_err' -> 'segment_fluxerr' Also the 'centroid' and 'cutout_centroid' properties now return centroids in (x, y) order to be consistent with the tools in ``photutils.centroid``. New methods and attributes -------------------------- The new `~photutils.segmentation.SourceCatalog` class has the following new methods: * :meth:`~photutils.segmentation.SourceCatalog.circular_aperture` * :meth:`~photutils.segmentation.SourceCatalog.circular_photometry` * :meth:`~photutils.segmentation.SourceCatalog.fluxfrac_radius` * :meth:`~photutils.segmentation.SourceCatalog.get_label` * :meth:`~photutils.segmentation.SourceCatalog.get_labels` and new attributes: * :attr:`~photutils.segmentation.SourceCatalog.fwhm` * :attr:`~photutils.segmentation.SourceCatalog.segment` * :attr:`~photutils.segmentation.SourceCatalog.segment_ma` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/whats_new/1.2.rst0000644000214200020070000000030200000000000016152 0ustar00lbradley.. doctest-skip-all .. _whatsnew-1.2: **************************** What's New in Photutils 1.2? **************************** Please see the :ref:`changelog` for the complete list of changes. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/docs/whats_new/index.rst0000644000214200020070000000013100000000000016761 0ustar00lbradley********** What's New ********** .. toctree:: :maxdepth: 1 1.2.rst 1.1.rst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640123871.976111 photutils-1.3.0/photutils/0000755000214200020070000000000000000000000014231 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/photutils/CITATION.rst0000644000214200020070000000445600000000000016206 0ustar00lbradleyCiting Photutils ---------------- If you use Photutils for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment: .. code-block:: text This research made use of Photutils, an Astropy package for detection and photometry of astronomical sources (Bradley et al. 20XX). where (Bradley et al. 20XX) is a citation to the `Zenodo record `_ of the Photutils version that was used. We also encourage citations in the main text wherever appropriate. All Photutils versions and citation formats can be found at https://doi.org/10.5281/zenodo.596036. For example, for Photutils v1.0.0 one would cite Bradley et al. 2020 with the BibTeX entry (https://zenodo.org/record/4044744/export/hx): .. code-block:: text @software{larry_bradley_2020_4044744, author = {Larry Bradley and Brigitta Sip{\H o}cz and Thomas Robitaille and Erik Tollerud and Z\`e Vin{\'{\i}}cius and Christoph Deil and Kyle Barbary and Tom J Wilson and Ivo Busko and Hans Moritz G{\"u}nther and Mihai Cara and Simon Conseil and Azalee Bostroem and Michael Droettboom and E. M. Bray and Lars Andersen Bratholm and P. L. Lim and Geert Barentsen and Matt Craig and Sergio Pascual and Gabriel Perren and Johnny Greco and Axel Donath and Miguel de Val-Borro and Wolfgang Kerzendorf and Yoonsoo P. Bach and Benjamin Alan Weaver and Francesco D'Eugenio and Harrison Souchereau and Leonardo Ferreira}, title = {astropy/photutils: 1.0.0}, month = sep, year = 2020, publisher = {Zenodo}, version = {1.0.0}, doi = {10.5281/zenodo.4044744}, url = {https://doi.org/10.5281/zenodo.4044744} } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/__init__.py0000644000214200020070000000452000000000000016343 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Photutils is an Astropy affiliated package to provide tools for detecting and performing photometry of astronomical sources. It also has tools for background estimation, ePSF building, PSF matching, centroiding, and morphological measurements. """ import warnings # Affiliated packages may add whatever they like to this file, but # should keep this content at the top. # ---------------------------------------------------------------------------- from ._astropy_init import * # noqa # ---------------------------------------------------------------------------- from .aperture import * # noqa from .background import * # noqa from .detection import * # noqa from .psf import * # noqa from .segmentation import * # noqa # deprecations from . import centroids from . import morphology __depr__ = {} __depr__[centroids] = ('centroid_com', 'centroid_quadratic', 'centroid_sources', 'centroid_epsf', 'centroid_1dg', 'gaussian1d_moments', 'centroid_2dg') __depr__[morphology] = ('data_properties', 'gini') __depr_mesg__ = ('`photutils.{attr}` is a deprecated alias for ' '`{module}.{attr}`. Instead, please use `from {module} ' 'import {attr}` to silence this warning.') __depr_attrs__ = {} for k, vals in __depr__.items(): for val in vals: __depr_attrs__[val] = (getattr(k, val), __depr_mesg__.format(module=k.__name__, attr=val)) del k, val, vals def __getattr__(attr): if attr in __depr_attrs__: obj, message = __depr_attrs__[attr] warnings.warn(message, DeprecationWarning, stacklevel=2) return obj raise AttributeError('module {!r} has no attribute {!r}' .format(__name__, attr)) # Set the bibtex entry to the article referenced in CITATION.rst. def _get_bibtex(): import os citation_file = os.path.join(os.path.dirname(__file__), 'CITATION.rst') with open(citation_file, 'r') as citation: refs = citation.read().split('@software')[1:] if len(refs) == 0: return '' bibtexreference = f"@software{refs[0]}" return bibtexreference __citation__ = __bibtex__ = _get_bibtex() del _astropy_init, _get_bibtex # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/_astropy_init.py0000644000214200020070000000054300000000000017470 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst import os __all__ = ['__version__', 'test'] try: from .version import version as __version__ except ImportError: __version__ = '' # Create the test function for self test from astropy.tests.runner import TestRunner test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils/_compiler.c0000644000214200020070000000524000000000000016347 0ustar00lbradley#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; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9804301 photutils-1.3.0/photutils/aperture/0000755000214200020070000000000000000000000016060 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/aperture/__init__.py0000644000214200020070000000054400000000000020174 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools to perform aperture photometry. """ from .bounding_box import * # noqa from .circle import * # noqa from .core import * # noqa from .ellipse import * # noqa from .mask import * # noqa from .photometry import * # noqa from .rectangle import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/_photometry_utils.py0000644000214200020070000000625100000000000022227 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module contains tools to validate and handle photometry inputs. """ import numpy as np def _validate_inputs(data, error): """ Validate inputs. ``data`` and ``error`` are converted to a `~numpy.ndarray`, if necessary. Used to parse inputs to `~photutils.aperture.aperture_photometry` and `~photutils.aperture.PixelAperture.do_photometry`. """ data = np.asanyarray(data) if data.ndim != 2: raise ValueError('data must be a 2D array.') if error is not None: error = np.asanyarray(error) if error.shape != data.shape: raise ValueError('error and data must have the same shape.') return data, error def _handle_units(data, error): """ Handle Quantity inputs. Any units on ``data`` and ``error` are removed. ``data`` and ``error`` are returned as `~numpy.ndarray`. The returned ``unit`` represents the unit for both ``data`` and ``error``. Used to parse inputs to `~photutils.aperture.aperture_photometry` and `~photutils.aperture.PixelAperture.do_photometry`. """ # check Quantity inputs inputs = (data, error) has_unit = [hasattr(x, 'unit') for x in inputs if x is not None] use_units = all(has_unit) if any(has_unit) and not use_units: raise ValueError('If data or error has units, then they both must ' 'have the same units.') # strip data and error units for performance if use_units: unit = data.unit data = data.value if error is not None: error = error.value else: unit = None return data, error, unit def _prepare_photometry_data(data, error, mask): """ Prepare data and error arrays for photometry. Error is converted to variance and masked values are set to zero in the output data and variance arrays. Used to parse inputs to `~photutils.aperture.aperture_photometry` and `~photutils.aperture.PixelAperture.do_photometry`. Parameters ---------- data : `~numpy.ndarray` The 2D array on which to perform photometry. error : `~numpy.ndarray` or `None` The pixel-wise Gaussian 1-sigma errors of the input ``data``. mask : array_like (bool) or `None` A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- data : `~numpy.ndarray` The 2D array on which to perform photometry, where masked values have been set to zero. variance : `~numpy.ndarray` or `None` The pixel-wise Gaussian 1-sigma variance of the input ``data``, where masked values have been set to zero. """ if error is not None: variance = error ** 2 else: variance = None if mask is not None: mask = np.asanyarray(mask) if mask.shape != data.shape: raise ValueError('mask and data must have the same shape.') data = data.copy() # do not modify input data data[mask] = 0. if variance is not None: variance[mask] = 0. return data, variance ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/attributes.py0000644000214200020070000001164600000000000020630 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines descriptor classes for aperture attribute validation. """ from astropy.coordinates import SkyCoord import astropy.units as u import numpy as np __all__ = ['ApertureAttribute', 'PixelPositions', 'SkyCoordPositions', 'Scalar', 'PositiveScalar', 'AngleOrPixelScalarQuantity'] class ApertureAttribute: """ Base descriptor class for aperture attribute validation. Parameters ---------- name : str The name of the attribute. description : str, optional The description of the attribute, which will be used as the attribute documentation. """ def __init__(self, name, description=''): self.name = name self.__doc__ = description def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[self.name] def __set__(self, instance, value): self._validate(value) if not isinstance(value, (u.Quantity, SkyCoord)): value = float(value) instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] def _validate(self, value): """ Validate the attribute value. An exception is raised if the value is invalid. """ raise NotImplementedError class PixelPositions(ApertureAttribute): """ Validate and set positions for pixel-based apertures. In all cases, pixel positions are converted to a 2D `~numpy.ndarray` (without units). """ def __set__(self, instance, value): # This is needed for zip to work seamlessly in Python 3 # (e.g., positions = zip(xpos, ypos)) if isinstance(value, zip): value = tuple(value) value = np.asanyarray(value).astype(float) # np.ndarray self._validate(value) if isinstance(value, u.Quantity): value = value.value if value.ndim == 2 and value.shape[1] != 2 and value.shape[0] == 2: raise ValueError('Input positions must be an (x, y) pixel ' 'position or a list or array of (x, y) pixel ' 'positions, e.g., [(x1, y1), (x2, y2), ' '(x3, y3)].') instance.__dict__[self.name] = value def _validate(self, value): if isinstance(value, u.Quantity) and value.unit != u.pixel: raise u.UnitsError(f'{self.name} must be in pixel units') if np.any(~np.isfinite(value)): raise ValueError(f'{self.name} must not contain any non-finite ' '(e.g., NaN or inf) positions') value = np.atleast_2d(value) if (value.shape[1] != 2 and value.shape[0] != 2) or value.ndim > 2: raise TypeError(f'{self.name} must be a (x, y) pixel position ' 'or a list or array of (x, y) pixel positions.') class SkyCoordPositions(ApertureAttribute): """ Check that value is a `~astropy.coordinates.SkyCoord`. """ def _validate(self, value): if not isinstance(value, SkyCoord): raise ValueError(f'{self.name} must be a SkyCoord instance') class Scalar(ApertureAttribute): """ Check that value is a scalar. """ def _validate(self, value): if not np.isscalar(value): raise ValueError(f'{self.name} must be a scalar') class PositiveScalar(ApertureAttribute): """ Check that value is a strictly positive (> 0) scalar. """ def _validate(self, value): if not np.isscalar(value) or value <= 0: raise ValueError(f'{self.name} must be a positive scalar') class AngleScalarQuantity(ApertureAttribute): """ Check that value is either an angular scalar `~astropy.units.Quantity`. """ def _validate(self, value): if isinstance(value, u.Quantity): if not value.isscalar: raise ValueError(f'{self.name} must be a scalar') if not value.unit.physical_type == 'angle': raise ValueError(f'{self.name} must have angular units') else: raise TypeError(f'{self.name} must be an astropy Quantity ' 'instance') class AngleOrPixelScalarQuantity(ApertureAttribute): """ Check that value is either an angular or a pixel scalar `~astropy.units.Quantity`. """ def _validate(self, value): if isinstance(value, u.Quantity): if not value.isscalar: raise ValueError(f'{self.name} must be a scalar') if not (value.unit.physical_type == 'angle' or value.unit == u.pixel): raise ValueError(f'{self.name} must have angular or pixel ' 'units') else: raise TypeError(f'{self.name} must be an astropy Quantity ' 'instance') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/bounding_box.py0000644000214200020070000003006300000000000021111 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a class for a rectangular bounding box. """ from astropy.io.fits.util import _is_int from astropy.utils import deprecated import numpy as np __all__ = ['BoundingBox'] class BoundingBox: """ A rectangular bounding box in integer (not float) pixel indices. Parameters ---------- ixmin, ixmax, iymin, iymax : int The bounding box pixel indices. Note that the upper values (``iymax`` and ``ixmax``) are exclusive as for normal slices in Python. The lower values (``ixmin`` and ``iymin``) must not be greater than the respective upper values (``ixmax`` and ``iymax``). Examples -------- >>> from photutils.aperture import BoundingBox >>> # constructing a BoundingBox like this is cryptic: >>> bbox = BoundingBox(1, 10, 2, 20) >>> # it's better to use keyword arguments for readability: >>> bbox = BoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) >>> bbox # nice repr, useful for interactive work BoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) >>> # sometimes it's useful to check if two bounding boxes are the same >>> bbox == BoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20) True >>> bbox == BoundingBox(ixmin=7, ixmax=10, iymin=2, iymax=20) False >>> # "center" and "shape" can be useful when working with numpy arrays >>> bbox.center # numpy order: (y, x) (10.5, 5.0) >>> bbox.shape # numpy order: (y, x) (18, 9) >>> # "extent" is useful when plotting the BoundingBox with matplotlib >>> bbox.extent # matplotlib order: (x, y) (0.5, 9.5, 1.5, 19.5) """ def __init__(self, ixmin, ixmax, iymin, iymax): if not _is_int(ixmin): raise TypeError('ixmin must be an integer') if not _is_int(ixmax): raise TypeError('ixmax must be an integer') if not _is_int(iymin): raise TypeError('iymin must be an integer') if not _is_int(iymax): raise TypeError('iymax must be an integer') if ixmin > ixmax: raise ValueError('ixmin must be <= ixmax') if iymin > iymax: raise ValueError('iymin must be <= iymax') self.ixmin = ixmin self.ixmax = ixmax self.iymin = iymin self.iymax = iymax @classmethod def from_float(cls, xmin, xmax, ymin, ymax): """ Return the smallest bounding box that fully contains a given rectangle defined by float coordinate values. Following the pixel index convention, an integer index corresponds to the center of a pixel and the pixel edges span from (index - 0.5) to (index + 0.5). For example, the pixel edge spans of the following pixels are: - pixel 0: from -0.5 to 0.5 - pixel 1: from 0.5 to 1.5 - pixel 2: from 1.5 to 2.5 In addition, because `BoundingBox` upper limits are exclusive (by definition), 1 is added to the upper pixel edges. See examples below. Parameters ---------- xmin, xmax, ymin, ymax : float Float coordinates defining a rectangle. The lower values (``xmin`` and ``ymin``) must not be greater than the respective upper values (``xmax`` and ``ymax``). Returns ------- bbox : `BoundingBox` object The minimal ``BoundingBox`` object fully containing the input rectangle coordinates. Examples -------- >>> from photutils.aperture import BoundingBox >>> BoundingBox.from_float(xmin=1.0, xmax=10.0, ymin=2.0, ymax=20.0) BoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=21) >>> BoundingBox.from_float(xmin=1.4, xmax=10.4, ymin=1.6, ymax=10.6) BoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=12) """ ixmin = int(np.floor(xmin + 0.5)) ixmax = int(np.ceil(xmax + 0.5)) iymin = int(np.floor(ymin + 0.5)) iymax = int(np.ceil(ymax + 0.5)) return cls(ixmin, ixmax, iymin, iymax) def __eq__(self, other): if not isinstance(other, BoundingBox): raise TypeError('Can compare BoundingBox only to another ' 'BoundingBox.') return ((self.ixmin == other.ixmin) and (self.ixmax == other.ixmax) and (self.iymin == other.iymin) and (self.iymax == other.iymax)) def __or__(self, other): return self.union(other) def __and__(self, other): return self.intersection(other) def __repr__(self): return (f'{self.__class__.__name__}(ixmin={self.ixmin}, ' f'ixmax={self.ixmax}, iymin={self.iymin}, ' f'iymax={self.iymax})') @property def center(self): """ The ``(y, x)`` center of the bounding box. """ return (0.5 * (self.iymax - 1 + self.iymin), 0.5 * (self.ixmax - 1 + self.ixmin)) @property def shape(self): """ The ``(ny, nx)`` shape of the bounding box. """ return self.iymax - self.iymin, self.ixmax - self.ixmin def get_overlap_slices(self, shape): """ Get slices for the overlapping part of the bounding box and an 2D array. Parameters ---------- shape : 2-tuple of int The shape of the 2D array. Returns ------- slices_large : tuple of slices or `None` A tuple of slice objects for each axis of the large array, such that ``large_array[slices_large]`` extracts the region of the large array that overlaps with the small array. `None` is returned if there is no overlap of the bounding box with the given image shape. slices_small : tuple of slices or `None` A tuple of slice objects for each axis of an array enclosed by the bounding box such that ``small_array[slices_small]`` extracts the region that is inside the large array. `None` is returned if there is no overlap of the bounding box with the given image shape. """ if len(shape) != 2: raise ValueError('input shape must have 2 elements.') xmin = self.ixmin xmax = self.ixmax ymin = self.iymin ymax = self.iymax if xmin >= shape[1] or ymin >= shape[0] or xmax <= 0 or ymax <= 0: # no overlap of the bounding box with the input shape return None, None slices_large = (slice(max(ymin, 0), min(ymax, shape[0])), slice(max(xmin, 0), min(xmax, shape[1]))) slices_small = (slice(max(-ymin, 0), min(ymax - ymin, shape[0] - ymin)), slice(max(-xmin, 0), min(xmax - xmin, shape[1] - xmin))) return slices_large, slices_small @property def extent(self): """ The extent of the mask, defined as the ``(xmin, xmax, ymin, ymax)`` bounding box from the bottom-left corner of the lower-left pixel to the upper-right corner of the upper-right pixel. The upper edges here are the actual pixel positions of the edges, i.e., they are not "exclusive" indices used for python indexing. This is useful for plotting the bounding box using Matplotlib. """ return (self.ixmin - 0.5, self.ixmax - 0.5, self.iymin - 0.5, self.iymax - 0.5) @deprecated('0.7', alternative='as_artist') def as_patch(self, **kwargs): # pragma: no cover """ Return a `matplotlib.patches.Rectangle` that represents the bounding box. Parameters ---------- **kwargs : dict Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- result : `matplotlib.patches.Rectangle` A matplotlib rectangular patch. """ return self.as_artist(**kwargs) def as_artist(self, **kwargs): """ Return a `matplotlib.patches.Rectangle` that represents the bounding box. Parameters ---------- **kwargs : dict Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- result : `matplotlib.patches.Rectangle` A matplotlib rectangular patch. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.aperture import BoundingBox bbox = BoundingBox(2, 7, 3, 8) fig = plt.figure() ax = fig.add_subplot(1, 1, 1) rng = np.random.default_rng(0) ax.imshow(rng.random((10, 10)), interpolation='nearest', cmap='viridis') ax.add_patch(bbox.as_artist(facecolor='none', edgecolor='white', lw=2.)) """ from matplotlib.patches import Rectangle return Rectangle(xy=(self.extent[0], self.extent[2]), width=self.shape[1], height=self.shape[0], **kwargs) def to_aperture(self): """ Return a `~photutils.aperture.RectangularAperture` that represents the bounding box. """ from .rectangle import RectangularAperture # prevent circular import xypos = self.center[::-1] # xy order height, width = self.shape return RectangularAperture(xypos, w=width, h=height, theta=0.) def plot(self, axes=None, origin=(0, 0), **kwargs): """ Plot the `BoundingBox` on a matplotlib `~matplotlib.axes.Axes` instance. Parameters ---------- axes : `matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : dict Any keyword arguments accepted by `matplotlib.patches.Patch`. """ aper = self.to_aperture() aper.plot(axes=axes, origin=origin, **kwargs) def union(self, other): """ Return a `BoundingBox` representing the union of this `BoundingBox` with another `BoundingBox`. Parameters ---------- other : `BoundingBox` The `BoundingBox` to join with this one. Returns ------- result : `BoundingBox` A `BoundingBox` representing the union of the input `BoundingBox` with this one. """ if not isinstance(other, BoundingBox): raise TypeError('BoundingBox can be joined only with another ' 'BoundingBox.') ixmin = min((self.ixmin, other.ixmin)) ixmax = max((self.ixmax, other.ixmax)) iymin = min((self.iymin, other.iymin)) iymax = max((self.iymax, other.iymax)) return BoundingBox(ixmin=ixmin, ixmax=ixmax, iymin=iymin, iymax=iymax) def intersection(self, other): """ Return a `BoundingBox` representing the intersection of this `BoundingBox` with another `BoundingBox`. Parameters ---------- other : `BoundingBox` The `BoundingBox` to intersect with this one. Returns ------- result : `BoundingBox` A `BoundingBox` representing the intersection of the input `BoundingBox` with this one. """ if not isinstance(other, BoundingBox): raise TypeError('BoundingBox can be intersected only with ' 'another BoundingBox.') ixmin = max(self.ixmin, other.ixmin) ixmax = min(self.ixmax, other.ixmax) iymin = max(self.iymin, other.iymin) iymax = min(self.iymax, other.iymax) if ixmax < ixmin or iymax < iymin: return None return BoundingBox(ixmin=ixmin, ixmax=ixmax, iymin=iymin, iymax=iymax) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/circle.py0000644000214200020070000003707200000000000017704 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines circular and circular-annulus apertures in both pixel and sky coordinates. """ import math import numpy as np from .attributes import (AngleOrPixelScalarQuantity, PixelPositions, PositiveScalar, SkyCoordPositions) from .core import PixelAperture, SkyAperture from .mask import ApertureMask from ..geometry import circular_overlap_grid __all__ = ['CircularMaskMixin', 'CircularAperture', 'CircularAnnulus', 'SkyCircularAperture', 'SkyCircularAnnulus'] class CircularMaskMixin: """ Mixin class to create masks for circular and circular-annulus aperture objects. """ def to_mask(self, method='exact', subpixels=5): """ Return a mask for the aperture. Parameters ---------- method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- mask : `~photutils.aperture.ApertureMask` or list of `~photutils.aperture.ApertureMask` A mask for the aperture. If the aperture is scalar then a single `~photutils.aperture.ApertureMask` is returned, otherwise a list of `~photutils.aperture.ApertureMask` is returned. """ use_exact, subpixels = self._translate_mask_mode(method, subpixels) if hasattr(self, 'r'): radius = self.r elif hasattr(self, 'r_out'): # annulus radius = self.r_out else: raise ValueError('Cannot determine the aperture radius.') masks = [] for bbox, edges in zip(np.atleast_1d(self.bbox), self._centered_edges): ny, nx = bbox.shape mask = circular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, radius, use_exact, subpixels) # subtract the inner circle for an annulus if hasattr(self, 'r_in'): mask -= circular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.r_in, use_exact, subpixels) masks.append(ApertureMask(mask, bbox)) if self.isscalar: return masks[0] else: return masks class CircularAperture(CircularMaskMixin, PixelAperture): """ A circular aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units r : float The radius of the circle in pixels. Raises ------ ValueError : `ValueError` If the input radius, ``r``, is negative. Examples -------- >>> from photutils.aperture import CircularAperture >>> aper = CircularAperture([10., 20.], 3.) >>> aper = CircularAperture((10., 20.), 3.) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = CircularAperture([pos1, pos2, pos3], 3.) >>> aper = CircularAperture((pos1, pos2, pos3), 3.) """ _shape_params = ('r',) positions = PixelPositions('positions', description='The center pixel position(s).') r = PositiveScalar('r', description='The radius in pixels.') def __init__(self, positions, r): self.positions = positions self.r = r @property def _xy_extents(self): return self.r, self.r @property def area(self): return math.pi * self.r ** 2 def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) patches = [] for xy_position in xy_positions: patches.append(mpatches.Circle(xy_position, self.r, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyCircularAperture` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyCircularAperture` object A `SkyCircularAperture` object. """ return SkyCircularAperture(**self._to_sky_params(wcs)) class CircularAnnulus(CircularMaskMixin, PixelAperture): """ A circular annulus aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units r_in : float The inner radius of the circular annulus in pixels. r_out : float The outer radius of the circular annulus in pixels. Raises ------ ValueError : `ValueError` If inner radius (``r_in``) is greater than outer radius (``r_out``). ValueError : `ValueError` If inner radius (``r_in``) is negative. Examples -------- >>> from photutils.aperture import CircularAnnulus >>> aper = CircularAnnulus([10., 20.], 3., 5.) >>> aper = CircularAnnulus((10., 20.), 3., 5.) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = CircularAnnulus([pos1, pos2, pos3], 3., 5.) >>> aper = CircularAnnulus((pos1, pos2, pos3), 3., 5.) """ _shape_params = ('r_in', 'r_out') positions = PixelPositions('positions', description='The center pixel position(s).') r_in = PositiveScalar('r_in', description='The inner radius in pixels.') r_out = PositiveScalar('r_out', description='The outer radius in pixels.') def __init__(self, positions, r_in, r_out): if not r_out > r_in: raise ValueError('r_out must be greater than r_in') self.positions = positions self.r_in = r_in self.r_out = r_out @property def _xy_extents(self): return self.r_out, self.r_out @property def area(self): return math.pi * (self.r_out ** 2 - self.r_in ** 2) def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) patches = [] for xy_position in xy_positions: patch_inner = mpatches.Circle(xy_position, self.r_in) patch_outer = mpatches.Circle(xy_position, self.r_out) path = self._make_annulus_path(patch_inner, patch_outer) patches.append(mpatches.PathPatch(path, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyCircularAnnulus` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyCircularAnnulus` object A `SkyCircularAnnulus` object. """ return SkyCircularAnnulus(**self._to_sky_params(wcs)) class SkyCircularAperture(SkyAperture): """ A circular aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. r : scalar `~astropy.units.Quantity` The radius of the circle, either in angular or pixel units. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyCircularAperture >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyCircularAperture(positions, 0.5*u.arcsec) """ _shape_params = ('r',) positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') r = AngleOrPixelScalarQuantity( 'r', description='The radius, in angular or pixel units.') def __init__(self, positions, r): self.positions = positions self.r = r def to_pixel(self, wcs): """ Convert the aperture to a `CircularAperture` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `CircularAperture` object A `CircularAperture` object. """ return CircularAperture(**self._to_pixel_params(wcs)) class SkyCircularAnnulus(SkyAperture): """ A circular annulus aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. r_in : scalar `~astropy.units.Quantity` The inner radius of the circular annulus, either in angular or pixel units. r_out : scalar `~astropy.units.Quantity` The outer radius of the circular annulus, either in angular or pixel units. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyCircularAnnulus >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyCircularAnnulus(positions, 0.5*u.arcsec, 1.0*u.arcsec) """ _shape_params = ('r_in', 'r_out') positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') r_in = AngleOrPixelScalarQuantity( 'r_in', description='The inner radius, in angular or pixel units.') r_out = AngleOrPixelScalarQuantity( 'r_out', description='The outer radius, in angular or pixel units.') def __init__(self, positions, r_in, r_out): if r_in.unit.physical_type != r_out.unit.physical_type: raise ValueError("r_in and r_out should either both be angles " "or in pixels.") self.positions = positions self.r_in = r_in self.r_out = r_out def to_pixel(self, wcs): """ Convert the aperture to a `CircularAnnulus` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `CircularAnnulus` object A `CircularAnnulus` object. """ return CircularAnnulus(**self._to_pixel_params(wcs)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/core.py0000644000214200020070000006316200000000000017372 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines the base aperture classes. """ import abc import copy import numpy as np from astropy.coordinates import SkyCoord import astropy.units as u from .bounding_box import BoundingBox from ._photometry_utils import (_handle_units, _prepare_photometry_data, _validate_inputs) from ..utils._wcs_helpers import _pixel_scale_angle_at_skycoord __all__ = ['Aperture', 'SkyAperture', 'PixelAperture'] class Aperture(metaclass=abc.ABCMeta): """ Abstract base class for all apertures. """ _shape_params = () positions = np.array(()) theta = None def __len__(self): if self.isscalar: raise TypeError(f'A scalar {self.__class__.__name__!r} object ' 'has no len()') return self.shape[0] def __getitem__(self, index): if self.isscalar: raise TypeError(f'A scalar {self.__class__.__name__!r} object ' 'cannot be indexed') kwargs = dict() for param in self._shape_params: kwargs[param] = getattr(self, param) return self.__class__(self.positions[index], **kwargs) def __iter__(self): for i in range(len(self)): yield self.__getitem__(i) def _positions_str(self, prefix=None): if isinstance(self, PixelAperture): return np.array2string(self.positions, separator=', ', prefix=prefix) elif isinstance(self, SkyAperture): return repr(self.positions) else: raise TypeError('Aperture must be a subclass of PixelAperture ' 'or SkyAperture') def __repr__(self): prefix = f'{self.__class__.__name__}' cls_info = [self._positions_str(prefix)] if self._shape_params is not None: for param in self._shape_params: cls_info.append(f'{param}={getattr(self, param)}') cls_info = ', '.join(cls_info) return f'<{prefix}({cls_info})>' def __str__(self): prefix = 'positions' cls_info = [ ('Aperture', self.__class__.__name__), (prefix, self._positions_str(prefix + ': '))] if self._shape_params is not None: for param in self._shape_params: cls_info.append((param, getattr(self, param))) fmt = [f'{key}: {val}' for key, val in cls_info] return '\n'.join(fmt) @property def shape(self): """ The shape of the instance. """ if isinstance(self.positions, SkyCoord): return self.positions.shape else: return self.positions.shape[:-1] @property def isscalar(self): """ Whether the instance is scalar (i.e., a single position). """ return self.shape == () class PixelAperture(Aperture): """ Abstract base class for apertures defined in pixel coordinates. """ @property def _default_patch_properties(self): """ A dictionary of default matplotlib.patches.Patch properties. """ mpl_params = dict() # matplotlib.patches.Patch default is ``fill=True`` mpl_params['fill'] = False return mpl_params @staticmethod def _translate_mask_mode(mode, subpixels, rectangle=False): if mode not in ('center', 'subpixel', 'exact'): raise ValueError(f'Invalid mask mode: {mode}') if rectangle and mode == 'exact': mode = 'subpixel' subpixels = 32 if mode == 'subpixels': if not isinstance(subpixels, int) or subpixels <= 0: raise ValueError('subpixels must be a strictly positive ' 'integer') if mode == 'center': use_exact = 0 subpixels = 1 elif mode == 'subpixel': use_exact = 0 elif mode == 'exact': use_exact = 1 subpixels = 1 return use_exact, subpixels @property @abc.abstractmethod def _xy_extents(self): """ The (x, y) extents of the aperture measured from the center position. In other words, the (x, y) extents are half of the aperture minimal bounding box size in each dimension. """ raise NotImplementedError('Needs to be implemented in a subclass.') @property def bbox(self): """ The minimal bounding box for the aperture. If the aperture is scalar then a single `~photutils.aperture.BoundingBox` is returned, otherwise a list of `~photutils.aperture.BoundingBox` is returned. """ positions = np.atleast_2d(self.positions) x_delta, y_delta = self._xy_extents xmin = positions[:, 0] - x_delta xmax = positions[:, 0] + x_delta ymin = positions[:, 1] - y_delta ymax = positions[:, 1] + y_delta bboxes = [BoundingBox.from_float(x0, x1, y0, y1) for x0, x1, y0, y1 in zip(xmin, xmax, ymin, ymax)] if self.isscalar: return bboxes[0] else: return bboxes @property def _centered_edges(self): """ A list of ``(xmin, xmax, ymin, ymax)`` tuples, one for each position, of the pixel edges after recentering the aperture at the origin. These pixel edges are used by the low-level `photutils.geometry` functions. """ edges = [] for position, bbox in zip(np.atleast_2d(self.positions), np.atleast_1d(self.bbox)): xmin = bbox.ixmin - 0.5 - position[0] xmax = bbox.ixmax - 0.5 - position[0] ymin = bbox.iymin - 0.5 - position[1] ymax = bbox.iymax - 0.5 - position[1] edges.append((xmin, xmax, ymin, ymax)) return edges @property def area(self): """ The exact area of the aperture shape. Returns ------- area : float The aperture area. """ raise NotImplementedError('Needs to be implemented in a subclass.') @abc.abstractmethod def to_mask(self, method='exact', subpixels=5): """ Return a mask for the aperture. Parameters ---------- method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- mask : `~photutils.aperture.ApertureMask` or list of `~photutils.aperture.ApertureMask` A mask for the aperture. If the aperture is scalar then a single `~photutils.aperture.ApertureMask` is returned, otherwise a list of `~photutils.aperture.ApertureMask` is returned. """ raise NotImplementedError('Needs to be implemented in a subclass.') def area_overlap(self, data, *, mask=None, method='exact', subpixels=5): """ Return the areas of the aperture masks that overlap with the data, i.e., how many pixels are actually used to calculate each sum. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array to multiply with the aperture mask. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from the area overlap. method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- areas : float or array_like The overlapping areas between the aperture masks and the data. """ apermasks = self.to_mask(method=method, subpixels=subpixels) if self.isscalar: apermasks = (apermasks,) if mask is not None: mask = np.asarray(mask) if mask.shape != data.shape: raise ValueError('mask and data must have the same shape') data = np.ones_like(data) vals = [apermask.get_values(data, mask=mask) for apermask in apermasks] # if the aperture does not overlap the data return np.nan areas = [val.sum() if val.shape != (0,) else np.nan for val in vals] if self.isscalar: return areas[0] else: return areas def _do_photometry(self, data, variance, method='exact', subpixels=5, unit=None): aperture_sums = [] aperture_sum_errs = [] masks = self.to_mask(method=method, subpixels=subpixels) if self.isscalar: masks = (masks,) for apermask in masks: values = apermask.get_values(data) # if the aperture does not overlap the data return np.nan aper_sum = values.sum() if values.shape != (0,) else np.nan aperture_sums.append(aper_sum) if variance is not None: values = apermask.get_values(variance) # if the aperture does not overlap the data return np.nan aper_var = values.sum() if values.shape != (0,) else np.nan aperture_sum_errs.append(np.sqrt(aper_var)) aperture_sums = np.array(aperture_sums) aperture_sum_errs = np.array(aperture_sum_errs) # apply units if unit is not None: aperture_sums = aperture_sums * unit # can't use *= w/old numpy aperture_sum_errs = aperture_sum_errs * unit return aperture_sums, aperture_sum_errs def do_photometry(self, data, error=None, mask=None, method='exact', subpixels=5): """ Perform aperture photometry on the input data. Parameters ---------- data : array_like or `~astropy.units.Quantity` instance The 2D array on which to perform photometry. ``data`` should be background subtracted. error : array_like or `~astropy.units.Quantity`, optional The pixel-wise Gaussian 1-sigma errors of the input ``data``. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`) . ``error`` must have the same shape as the input ``data``. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'`` A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- aperture_sums : `~numpy.ndarray` or `~astropy.units.Quantity` The sums within each aperture. aperture_sum_errs : `~numpy.ndarray` or `~astropy.units.Quantity` The errors on the sums within each aperture. Notes ----- `RectangularAperture` and `RectangularAnnulus` photometry with the "exact" method uses a subpixel approximation by subdividing each data pixel by a factor of 1024 (``subpixels = 32``). For rectangular aperture widths and heights in the range from 2 to 100 pixels, this subpixel approximation gives results typically within 0.001 percent or better of the exact value. The differences can be larger for smaller apertures (e.g., aperture sizes of one pixel or smaller). For such small sizes, it is recommend to set ``method='subpixel'`` with a larger ``subpixels`` size. """ # validate inputs data, error = _validate_inputs(data, error) # handle data, error, and unit inputs # output data and error are ndarray without units data, error, unit = _handle_units(data, error) # compute variance and apply input mask data, variance = _prepare_photometry_data(data, error, mask) return self._do_photometry(data, variance, method=method, subpixels=subpixels, unit=unit) @staticmethod def _make_annulus_path(patch_inner, patch_outer): """ Define a matplotlib annulus path from two patches. This preserves the cubic Bezier curves (CURVE4) of the aperture paths. """ import matplotlib.path as mpath path_inner = patch_inner.get_path() transform_inner = patch_inner.get_transform() path_inner = transform_inner.transform_path(path_inner) path_outer = patch_outer.get_path() transform_outer = patch_outer.get_transform() path_outer = transform_outer.transform_path(path_outer) verts_inner = path_inner.vertices[:-1][::-1] verts_inner = np.concatenate((verts_inner, [verts_inner[-1]])) verts = np.vstack((path_outer.vertices, verts_inner)) codes = np.hstack((path_outer.codes, path_inner.codes)) return mpath.Path(verts, codes) def _define_patch_params(self, origin=(0, 0), **kwargs): """ Define the aperture patch position and set any default matplotlib patch keywords (e.g., ``fill=False``). Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- xy_positions : `~numpy.ndarray` The aperture patch positions. patch_params : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. """ xy_positions = copy.deepcopy(np.atleast_2d(self.positions)) xy_positions[:, 0] -= origin[0] xy_positions[:, 1] -= origin[1] patch_params = self._default_patch_properties patch_params.update(kwargs) return xy_positions, patch_params @abc.abstractmethod def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ raise NotImplementedError('Needs to be implemented in a subclass.') def plot(self, axes=None, origin=(0, 0), **kwargs): """ Plot the aperture on a matplotlib `~matplotlib.axes.Axes` instance. Parameters ---------- axes : `matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : list of `~matplotlib.patches.Patch` A list of matplotlib patches for the plotted aperture. The patches can be used, for example, when adding a plot legend. """ import matplotlib.pyplot as plt if axes is None: axes = plt.gca() patches = self._to_patch(origin=origin, **kwargs) if self.isscalar: patches = (patches,) for patch in patches: axes.add_patch(patch) return patches def _to_sky_params(self, wcs): """ Convert the pixel aperture parameters to those for a sky aperture. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- sky_params : `dict` A dictionary of parameters for an equivalent sky aperture. """ sky_params = {} xpos, ypos = np.transpose(self.positions) sky_params['positions'] = wcs.pixel_to_world(xpos, ypos) # Aperture objects require scalar shape parameters (e.g., # radius, a, b, theta, etc.), therefore we must calculate the # pixel scale and angle at only a single sky position, which # we take as the first aperture position. For apertures with # multiple positions used with a WCS that contains distortions # (e.g., a spatially-dependent pixel scale), this may lead to # unexpected results (e.g., results that are dependent of the # order of the positions). There is no good way to fix this with # the current Aperture API allowing multiple positions. skypos = sky_params['positions'] if not self.isscalar: skypos = skypos[0] _, pixscale, angle = _pixel_scale_angle_at_skycoord(skypos, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: sky_params[theta_key] = (self.theta * u.rad) - angle.to(u.rad) shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) sky_params[shape_param] = (value * u.pix * pixscale).to(u.arcsec) return sky_params @abc.abstractmethod def to_sky(self, wcs): """ Convert the aperture to a `SkyAperture` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyAperture` object A `SkyAperture` object. """ raise NotImplementedError('Needs to be implemented in a subclass.') class SkyAperture(Aperture): """ Abstract base class for all apertures defined in celestial coordinates. """ def _to_pixel_params(self, wcs): """ Convert the sky aperture parameters to those for a pixel aperture. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- pixel_params : `dict` A dictionary of parameters for an equivalent pixel aperture. """ pixel_params = {} xpos, ypos = wcs.world_to_pixel(self.positions) pixel_params['positions'] = np.transpose((xpos, ypos)) # Aperture objects require scalar shape parameters (e.g., # radius, a, b, theta, etc.), therefore we must calculate the # pixel scale and angle at only a single sky position, which # we take as the first aperture position. For apertures with # multiple positions used with a WCS that contains distortions # (e.g., a spatially-dependent pixel scale), this may lead to # unexpected results (e.g., results that are dependent of the # order of the positions). There is no good way to fix this with # the current Aperture API allowing multiple positions. if self.isscalar: skypos = self.positions else: skypos = self.positions[0] _, pixscale, angle = _pixel_scale_angle_at_skycoord(skypos, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: pixel_params[theta_key] = (self.theta + angle).to(u.radian).value shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) if value.unit.physical_type == 'angle': pixel_params[shape_param] = ((value / pixscale) .to(u.pixel).value) else: pixel_params[shape_param] = value.value return pixel_params @abc.abstractmethod def to_pixel(self, wcs): """ Convert the aperture to a `PixelAperture` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `PixelAperture` object A `PixelAperture` object. """ raise NotImplementedError('Needs to be implemented in a subclass.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/ellipse.py0000644000214200020070000005363400000000000020102 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines elliptical and elliptical-annulus apertures in both pixel and sky coordinates. """ import math import astropy.units as u import numpy as np from .attributes import (AngleOrPixelScalarQuantity, AngleScalarQuantity, PixelPositions, PositiveScalar, Scalar, SkyCoordPositions) from .core import PixelAperture, SkyAperture from .mask import ApertureMask from ..geometry import elliptical_overlap_grid __all__ = ['EllipticalMaskMixin', 'EllipticalAperture', 'EllipticalAnnulus', 'SkyEllipticalAperture', 'SkyEllipticalAnnulus'] class EllipticalMaskMixin: """ Mixin class to create masks for elliptical and elliptical-annulus aperture objects. """ def to_mask(self, method='exact', subpixels=5): """ Return a mask for the aperture. Parameters ---------- method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- mask : `~photutils.aperture.ApertureMask` or list of `~photutils.aperture.ApertureMask` A mask for the aperture. If the aperture is scalar then a single `~photutils.aperture.ApertureMask` is returned, otherwise a list of `~photutils.aperture.ApertureMask` is returned. """ use_exact, subpixels = self._translate_mask_mode(method, subpixels) if hasattr(self, 'a'): a = self.a b = self.b elif hasattr(self, 'a_in'): # annulus a = self.a_out b = self.b_out else: raise ValueError('Cannot determine the aperture shape.') masks = [] for bbox, edges in zip(np.atleast_1d(self.bbox), self._centered_edges): ny, nx = bbox.shape mask = elliptical_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, a, b, self.theta, use_exact, subpixels) # subtract the inner ellipse for an annulus if hasattr(self, 'a_in'): mask -= elliptical_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.a_in, self.b_in, self.theta, use_exact, subpixels) masks.append(ApertureMask(mask, bbox)) if self.isscalar: return masks[0] else: return masks @staticmethod def _calc_extents(semimajor_axis, semiminor_axis, theta): """ Calculate half of the bounding box extents of an ellipse. """ cos_theta = np.cos(theta) sin_theta = np.sin(theta) semimajor_x = semimajor_axis * cos_theta semimajor_y = semimajor_axis * sin_theta semiminor_x = semiminor_axis * -sin_theta semiminor_y = semiminor_axis * cos_theta x_extent = np.sqrt(semimajor_x**2 + semiminor_x**2) y_extent = np.sqrt(semimajor_y**2 + semiminor_y**2) return x_extent, y_extent class EllipticalAperture(EllipticalMaskMixin, PixelAperture): """ An elliptical aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units a : float The semimajor axis of the ellipse in pixels. b : float The semiminor axis of the ellipse in pixels. theta : float, optional The rotation angle in radians of the ellipse semimajor axis from the positive ``x`` axis. The rotation angle increases counterclockwise. The default is 0. Raises ------ ValueError : `ValueError` If either axis (``a`` or ``b``) is negative. Examples -------- >>> from photutils.aperture import EllipticalAperture >>> aper = EllipticalAperture([10., 20.], 5., 3.) >>> aper = EllipticalAperture((10., 20.), 5., 3., theta=np.pi) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = EllipticalAperture([pos1, pos2, pos3], 5., 3.) >>> aper = EllipticalAperture((pos1, pos2, pos3), 5., 3., theta=np.pi) """ _shape_params = ('a', 'b', 'theta') positions = PixelPositions('positions', description='The center pixel position(s).') a = PositiveScalar('a', description='The semimajor axis in pixels.') b = PositiveScalar('b', description='The semiminor axis in pixels.') theta = Scalar('theta', description=('The counterclockwise rotation angle in ' 'radians of the ellipse semimajor axis from ' 'the positive x axis.')) def __init__(self, positions, a, b, theta=0.): self.positions = positions self.a = a self.b = b self.theta = theta @property def _xy_extents(self): return self._calc_extents(self.a, self.b, self.theta) @property def area(self): return math.pi * self.a * self.b def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) patches = [] theta_deg = self.theta * 180. / np.pi for xy_position in xy_positions: patches.append(mpatches.Ellipse(xy_position, 2.*self.a, 2.*self.b, theta_deg, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyEllipticalAperture` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyEllipticalAperture` object A `SkyEllipticalAperture` object. """ return SkyEllipticalAperture(**self._to_sky_params(wcs)) class EllipticalAnnulus(EllipticalMaskMixin, PixelAperture): r""" An elliptical annulus aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units a_in : float The inner semimajor axis of the elliptical annulus in pixels. a_out : float The outer semimajor axis of the elliptical annulus in pixels. b_out : float The outer semiminor axis of the elliptical annulus in pixels. b_in : `None` or float, optional The inner semiminor axis of the elliptical annulus in pixels. If `None`, then the the inner semiminor axis is calculated as: .. math:: b_{in} = b_{out} \left(\frac{a_{in}}{a_{out}}\right) theta : float, optional The rotation angle in radians of the ellipse semimajor axis from the positive ``x`` axis. The rotation angle increases counterclockwise. The default is 0. Raises ------ ValueError : `ValueError` If inner semimajor axis (``a_in``) is greater than outer semimajor axis (``a_out``). ValueError : `ValueError` If either the inner semimajor axis (``a_in``) or the outer semiminor axis (``b_out``) is negative. Examples -------- >>> from photutils.aperture import EllipticalAnnulus >>> aper = EllipticalAnnulus([10., 20.], 3., 8., 5.) >>> aper = EllipticalAnnulus((10., 20.), 3., 8., 5., theta=np.pi) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = EllipticalAnnulus([pos1, pos2, pos3], 3., 8., 5.) >>> aper = EllipticalAnnulus((pos1, pos2, pos3), 3., 8., 5., theta=np.pi) """ _shape_params = ('a_in', 'a_out', 'b_in', 'b_out', 'theta') positions = PixelPositions('positions', description='The center pixel position(s).') a_in = PositiveScalar('a_in', description='The inner semimajor axis in pixels.') a_out = PositiveScalar('a_out', description='The outer semimajor axis in pixels.') b_in = PositiveScalar('b_in', description='The inner semiminor axis in pixels.') b_out = PositiveScalar('b_out', description='The outer semiminor axis in pixels.') theta = Scalar('theta', description=('The counterclockwise rotation angle in ' 'radians of the ellipse semimajor axis from ' 'the positive x axis.')) def __init__(self, positions, a_in, a_out, b_out, b_in=None, theta=0.): if not a_out > a_in: raise ValueError('"a_out" must be greater than "a_in".') self.positions = positions self.a_in = a_in self.a_out = a_out self.b_out = b_out if b_in is None: b_in = self.b_out * self.a_in / self.a_out else: if not b_out > b_in: raise ValueError('"b_out" must be greater than "b_in".') self.b_in = b_in self.theta = theta @property def _xy_extents(self): return self._calc_extents(self.a_out, self.b_out, self.theta) @property def area(self): return math.pi * (self.a_out * self.b_out - self.a_in * self.b_in) def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) patches = [] theta_deg = self.theta * 180. / np.pi for xy_position in xy_positions: patch_inner = mpatches.Ellipse(xy_position, 2.*self.a_in, 2.*self.b_in, theta_deg) patch_outer = mpatches.Ellipse(xy_position, 2.*self.a_out, 2.*self.b_out, theta_deg) path = self._make_annulus_path(patch_inner, patch_outer) patches.append(mpatches.PathPatch(path, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyEllipticalAnnulus` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyEllipticalAnnulus` object A `SkyEllipticalAnnulus` object. """ return SkyEllipticalAnnulus(**self._to_sky_params(wcs)) class SkyEllipticalAperture(SkyAperture): """ An elliptical aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. a : scalar `~astropy.units.Quantity` The semimajor axis of the ellipse, either in angular or pixel units. b : scalar `~astropy.units.Quantity` The semiminor axis of the ellipse, either in angular or pixel units. theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the ellipse semimajor axis. For a right-handed world coordinate system, the position angle increases counterclockwise from North (PA=0). The default is 0 degrees. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyEllipticalAperture >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyEllipticalAperture(positions, 1.0*u.arcsec, 0.5*u.arcsec) """ _shape_params = ('a', 'b', 'theta') positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') a = AngleOrPixelScalarQuantity( 'a', description='The semimajor axis, in angular or pixel units.') b = AngleOrPixelScalarQuantity( 'b', description='The semiminor axis, in angular or pixel units.') theta = AngleScalarQuantity( 'theta', description=('The position angle in angular units of the ellipse ' 'semimajor axis.')) def __init__(self, positions, a, b, theta=0.*u.deg): if a.unit.physical_type != b.unit.physical_type: raise ValueError('a and b should either both be angles ' 'or in pixels') self.positions = positions self.a = a self.b = b self.theta = theta def to_pixel(self, wcs): """ Convert the aperture to an `EllipticalAperture` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `EllipticalAperture` object An `EllipticalAperture` object. """ return EllipticalAperture(**self._to_pixel_params(wcs)) class SkyEllipticalAnnulus(SkyAperture): r""" An elliptical annulus aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. a_in : scalar `~astropy.units.Quantity` The inner semimajor axis, either in angular or pixel units. a_out : scalar `~astropy.units.Quantity` The outer semimajor axis, either in angular or pixel units. b_out : scalar `~astropy.units.Quantity` The outer semiminor axis, either in angular or pixel units. b_in : `None` or scalar `~astropy.units.Quantity` The inner semiminor axis, either in angular or pixel units. If `None`, then the inner semiminor axis is calculated as: .. math:: b_{in} = b_{out} \left(\frac{a_{in}}{a_{out}}\right) theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the ellipse semimajor axis. For a right-handed world coordinate system, the position angle increases counterclockwise from North (PA=0). The default is 0 degrees. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyEllipticalAnnulus >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyEllipticalAnnulus(positions, 0.5*u.arcsec, 2.0*u.arcsec, ... 1.0*u.arcsec) """ _shape_params = ('a_in', 'a_out', 'b_in', 'b_out', 'theta') positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') a_in = AngleOrPixelScalarQuantity( 'a_in', description='The inner semimajor axis, in angular or pixel units.') a_out = AngleOrPixelScalarQuantity( 'a_out', description='The outer semimajor axis, in angular or pixel units.') b_in = AngleOrPixelScalarQuantity( 'b_in', description='The inner semiminor axis, in angular or pixel units.') b_out = AngleOrPixelScalarQuantity( 'b_out', description='The outer semiminor axis, in angular or pixel units.') theta = AngleScalarQuantity( 'theta', description=('The position angle in angular units of the ellipse ' 'semimajor axis.')) def __init__(self, positions, a_in, a_out, b_out, b_in=None, theta=0.*u.deg): if a_in.unit.physical_type != a_out.unit.physical_type: raise ValueError('a_in and a_out should either both be angles ' 'or in pixels') if a_out.unit.physical_type != b_out.unit.physical_type: raise ValueError('a_out and b_out should either both be angles ' 'or in pixels') if not a_out > a_in: raise ValueError('"a_out" must be greater than "a_in".') self.positions = positions self.a_in = a_in self.a_out = a_out self.b_out = b_out if b_in is None: b_in = self.b_out * self.a_in / self.a_out else: if b_in.unit.physical_type != b_out.unit.physical_type: raise ValueError('b_in and b_out should either both be ' 'angles or in pixels') if not b_out > b_in: raise ValueError('"b_out" must be greater than "b_in".') self.b_in = b_in self.theta = theta def to_pixel(self, wcs): """ Convert the aperture to an `EllipticalAnnulus` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `EllipticalAnnulus` object An `EllipticalAnnulus` object. """ return EllipticalAnnulus(**self._to_pixel_params(wcs)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/mask.py0000644000214200020070000002145500000000000017374 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines a class for aperture masks. """ import warnings import astropy.units as u import numpy as np __all__ = ['ApertureMask'] class ApertureMask: """ Class for an aperture mask. Parameters ---------- data : array_like A 2D array representing the fractional overlap of an aperture on the pixel grid. This should be the full-sized (i.e., not truncated) array that is the direct output of one of the low-level `photutils.geometry` functions. bbox : `photutils.aperture.BoundingBox` The bounding box object defining the aperture minimal bounding box. """ def __init__(self, data, bbox): self.data = np.asanyarray(data) if self.data.shape != bbox.shape: raise ValueError('mask data and bounding box must have the same ' 'shape') self.bbox = bbox self._mask = (self.data == 0) def __array__(self): """ Array representation of the mask data array (e.g., for matplotlib). """ return self.data @property def shape(self): """ The shape of the mask data array. """ return self.data.shape def get_overlap_slices(self, shape): """ Get slices for the overlapping part of the aperture mask and a 2D array. Parameters ---------- shape : 2-tuple of int The shape of the 2D array. Returns ------- slices_large : tuple of slices or `None` A tuple of slice objects for each axis of the large array, such that ``large_array[slices_large]`` extracts the region of the large array that overlaps with the small array. `None` is returned if there is no overlap of the bounding box with the given image shape. slices_small : tuple of slices or `None` A tuple of slice objects for each axis of the aperture mask array such that ``small_array[slices_small]`` extracts the region that is inside the large array. `None` is returned if there is no overlap of the bounding box with the given image shape. """ return self.bbox.get_overlap_slices(shape) def to_image(self, shape): """ Return an image of the mask in a 2D array of the given shape, taking any edge effects into account. Parameters ---------- shape : tuple of int The ``(ny, nx)`` shape of the output array. Returns ------- result : `~numpy.ndarray` A 2D array of the mask. """ if len(shape) != 2: raise ValueError('input shape must have 2 elements.') # find the overlap of the mask on the output image shape slices_large, slices_small = self.get_overlap_slices(shape) if slices_small is None: return None # no overlap # insert the mask into the output image image = np.zeros(shape) image[slices_large] = self.data[slices_small] return image def cutout(self, data, fill_value=0., copy=False): """ Create a cutout from the input data over the mask bounding box, taking any edge effects into account. Parameters ---------- data : array_like A 2D array on which to apply the aperture mask. fill_value : float, optional The value used to fill pixels where the aperture mask does not overlap with the input ``data``. The default is 0. copy : bool, optional If `True` then the returned cutout array will always be hold a copy of the input ``data``. If `False` and the mask is fully within the input ``data``, then the returned cutout array will be a view into the input ``data``. In cases where the mask partially overlaps or has no overlap with the input ``data``, the returned cutout array will always hold a copy of the input ``data`` (i.e., this keyword has no effect). Returns ------- result : `~numpy.ndarray` or `None` A 2D array cut out from the input ``data`` representing the same cutout region as the aperture mask. If there is a partial overlap of the aperture mask with the input data, pixels outside of the data will be assigned to ``fill_value``. `None` is returned if there is no overlap of the aperture with the input ``data``. """ data = np.asanyarray(data) if data.ndim != 2: raise ValueError('data must be a 2D array.') # find the overlap of the mask on the output image shape slices_large, slices_small = self.get_overlap_slices(data.shape) if slices_small is None: return None # no overlap cutout_shape = (slices_small[0].stop - slices_small[0].start, slices_small[1].stop - slices_small[1].start) if cutout_shape == self.shape: cutout = data[slices_large] if copy: cutout = np.copy(cutout) return cutout # cutout is always a copy for partial overlap if ~np.isfinite(fill_value): dtype = float else: dtype = data.dtype cutout = np.zeros(self.shape, dtype=dtype) cutout[:] = fill_value cutout[slices_small] = data[slices_large] if isinstance(data, u.Quantity): cutout <<= data.unit return cutout def multiply(self, data, fill_value=0.): """ Multiply the aperture mask with the input data, taking any edge effects into account. The result is a mask-weighted cutout from the data. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array to multiply with the aperture mask. fill_value : float, optional The value is used to fill pixels where the aperture mask does not overlap with the input ``data``. The default is 0. Returns ------- result : `~numpy.ndarray` or `None` A 2D mask-weighted cutout from the input ``data``. If there is a partial overlap of the aperture mask with the input data, pixels outside of the data will be assigned to ``fill_value`` before being multiplied with the mask. `None` is returned if there is no overlap of the aperture with the input ``data``. """ cutout = self.cutout(data, fill_value=fill_value) if cutout is None: return None else: # ignore multiplication with non-finite data values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) weighted_cutout = cutout * self.data # fill values outside of the mask but within the bounding box weighted_cutout[self._mask] = fill_value return weighted_cutout def get_values(self, data, mask=None): """ Get the mask-weighted pixel values from the data as a 1D array. If the ``ApertureMask`` was created with ``method='center'``, (where the mask weights are only 1 or 0), then the returned values will simply be pixel values extracted from the data. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array from which to get mask-weighted values. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is not returned in the result. Returns ------- result : `~numpy.ndarray` A 1D array of mask-weighted pixel values from the input ``data``. If there is no overlap of the aperture with the input ``data``, the result will be an empty array with shape (0,). """ slc_large, slc_small = self.get_overlap_slices(data.shape) if slc_large is None: return np.array([]) cutout = data[slc_large] apermask = self.data[slc_small] pixel_mask = (apermask > 0) # good pixels if mask is not None: if mask.shape != data.shape: raise ValueError('mask and data must have the same shape') pixel_mask &= ~mask[slc_large] # ignore multiplication with non-finite data values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) return (cutout * apermask)[pixel_mask] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/photometry.py0000644000214200020070000002311700000000000020650 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines tools to perform aperture photometry. """ import warnings import numpy as np from astropy.nddata import NDData, StdDevUncertainty from astropy.table import QTable import astropy.units as u from astropy.utils.exceptions import AstropyUserWarning from .core import Aperture, SkyAperture from ._photometry_utils import (_handle_units, _prepare_photometry_data, _validate_inputs) from ..utils._misc import _get_version_info __all__ = ['aperture_photometry'] def aperture_photometry(data, apertures, error=None, mask=None, method='exact', subpixels=5, wcs=None): """ Perform aperture photometry on the input data by summing the flux within the given aperture(s). Parameters ---------- data : array_like, `~astropy.units.Quantity`, `~astropy.nddata.NDData` The 2D array on which to perform photometry. ``data`` should be background-subtracted. If ``data`` is a `~astropy.units.Quantity` array, then ``error`` (if input) must also be a `~astropy.units.Quantity` array with the same units. See the Notes section below for more information about `~astropy.nddata.NDData` input. apertures : `~photutils.aperture.Aperture` or list of `~photutils.aperture.Aperture` The aperture(s) to use for the photometry. If ``apertures`` is a list of `~photutils.aperture.Aperture` then they all must have the same position(s). error : array_like or `~astropy.units.Quantity`, optional The pixel-wise Gaussian 1-sigma errors of the input ``data``. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`) . ``error`` must have the same shape as the input ``data``. If a `~astropy.units.Quantity` array, then ``data`` must also be a `~astropy.units.Quantity` array with the same units. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. wcs : WCS object, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Used only if the input ``apertures`` contains a `SkyAperture` object. Returns ------- table : `~astropy.table.QTable` A table of the photometry with the following columns: * ``'id'``: The source ID. * ``'xcenter'``, ``'ycenter'``: The ``x`` and ``y`` pixel coordinates of the input aperture center(s). * ``'sky_center'``: The sky coordinates of the input aperture center(s). Returned only if the input ``apertures`` is a `SkyAperture` object. * ``'aperture_sum'``: The sum of the values within the aperture. * ``'aperture_sum_err'``: The corresponding uncertainty in the ``'aperture_sum'`` values. Returned only if the input ``error`` is not `None`. The table metadata includes the Astropy and Photutils version numbers and the `aperture_photometry` calling arguments. Notes ----- `RectangularAperture` and `RectangularAnnulus` photometry with the "exact" method uses a subpixel approximation by subdividing each data pixel by a factor of 1024 (``subpixels = 32``). For rectangular aperture widths and heights in the range from 2 to 100 pixels, this subpixel approximation gives results typically within 0.001 percent or better of the exact value. The differences can be larger for smaller apertures (e.g., aperture sizes of one pixel or smaller). For such small sizes, it is recommend to set ``method='subpixel'`` with a larger ``subpixels`` size. If the input ``data`` is a `~astropy.nddata.NDData` instance, then the ``error``, ``mask``, and ``wcs`` keyword inputs are ignored. Instead, these values should be defined as attributes in the `~astropy.nddata.NDData` object. In the case of ``error``, it must be defined in the ``uncertainty`` attribute with a `~astropy.nddata.StdDevUncertainty` instance. """ if isinstance(data, NDData): nddata_attr = {'error': error, 'mask': mask, 'wcs': wcs} for key, value in nddata_attr.items(): if value is not None: warnings.warn('The {0!r} keyword is be ignored. Its value ' 'is obtained from the input NDData object.' .format(key), AstropyUserWarning) mask = data.mask wcs = data.wcs if isinstance(data.uncertainty, StdDevUncertainty): if data.uncertainty.unit is None: error = data.uncertainty.array else: error = data.uncertainty.array * data.uncertainty.unit if data.unit is not None: data = u.Quantity(data.data, unit=data.unit) else: data = data.data return aperture_photometry(data, apertures, error=error, mask=mask, method=method, subpixels=subpixels, wcs=wcs) # validate inputs data, error = _validate_inputs(data, error) # handle data, error, and unit inputs # output data and error are ndarray without units data, error, unit = _handle_units(data, error) # compute variance and apply input mask data, variance = _prepare_photometry_data(data, error, mask) single_aperture = False if isinstance(apertures, Aperture): single_aperture = True apertures = (apertures,) # convert sky to pixel apertures skyaper = False if isinstance(apertures[0], SkyAperture): if wcs is None: raise ValueError('A WCS transform must be defined by the input ' 'data or the wcs keyword when using a ' 'SkyAperture object.') # used to include SkyCoord position in the output table skyaper = True skycoord_pos = apertures[0].positions apertures = [aper.to_pixel(wcs) for aper in apertures] # compare positions in pixels to avoid comparing SkyCoord objects positions = apertures[0].positions for aper in apertures[1:]: if not np.array_equal(aper.positions, positions): raise ValueError('Input apertures must all have identical ' 'positions.') # define output table meta data meta = {} meta['name'] = 'Aperture photometry results' meta['version'] = _get_version_info() calling_args = f"method='{method}', subpixels={subpixels}" meta['aperture_photometry_args'] = calling_args tbl = QTable(meta=meta) positions = np.atleast_2d(apertures[0].positions) tbl['id'] = np.arange(positions.shape[0], dtype=int) + 1 xypos_pixel = np.transpose(positions) * u.pixel tbl['xcenter'] = xypos_pixel[0] tbl['ycenter'] = xypos_pixel[1] if skyaper: if skycoord_pos.isscalar: # create length-1 SkyCoord array tbl['sky_center'] = skycoord_pos.reshape((-1,)) else: tbl['sky_center'] = skycoord_pos sum_key_main = 'aperture_sum' sum_err_key_main = 'aperture_sum_err' for i, aper in enumerate(apertures): aper_sum, aper_sum_err = aper._do_photometry(data, variance, method=method, subpixels=subpixels, unit=unit) sum_key = sum_key_main sum_err_key = sum_err_key_main if not single_aperture: sum_key += f'_{i}' sum_err_key += f'_{i}' tbl[sum_key] = aper_sum if error is not None: tbl[sum_err_key] = aper_sum_err return tbl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/rectangle.py0000644000214200020070000006006100000000000020401 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines rectangular and rectangular-annulus apertures in both pixel and sky coordinates. """ import math import astropy.units as u import numpy as np from .attributes import (AngleOrPixelScalarQuantity, AngleScalarQuantity, PixelPositions, PositiveScalar, Scalar, SkyCoordPositions) from .core import PixelAperture, SkyAperture from .mask import ApertureMask from ..geometry import rectangular_overlap_grid __all__ = ['RectangularMaskMixin', 'RectangularAperture', 'RectangularAnnulus', 'SkyRectangularAperture', 'SkyRectangularAnnulus'] class RectangularMaskMixin: """ Mixin class to create masks for rectangular or rectangular-annulus aperture objects. """ def to_mask(self, method='exact', subpixels=5): """ Return a mask for the aperture. Parameters ---------- method : {'exact', 'center', 'subpixel'}, optional The method used to determine the overlap of the aperture on the pixel grid. Not all options are available for all aperture types. Note that the more precise methods are generally slower. The following methods are available: * ``'exact'`` (default): The the exact fractional overlap of the aperture and each pixel is calculated. The returned mask will contain values between 0 and 1. * ``'center'``: A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. The returned mask will contain values only of 0 (out) and 1 (in). * ``'subpixel'``: A pixel is divided into subpixels (see the ``subpixels`` keyword), each of which are considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. If ``subpixels=1``, this method is equivalent to ``'center'``. The returned mask will contain values between 0 and 1. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor in each dimension. That is, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- mask : `~photutils.aperture.ApertureMask` or list of `~photutils.aperture.ApertureMask` A mask for the aperture. If the aperture is scalar then a single `~photutils.aperture.ApertureMask` is returned, otherwise a list of `~photutils.aperture.ApertureMask` is returned. """ _, subpixels = self._translate_mask_mode(method, subpixels, rectangle=True) if hasattr(self, 'w'): w = self.w h = self.h elif hasattr(self, 'w_out'): # annulus w = self.w_out h = self.h_out else: raise ValueError('Cannot determine the aperture radius.') masks = [] for bbox, edges in zip(np.atleast_1d(self.bbox), self._centered_edges): ny, nx = bbox.shape mask = rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, w, h, self.theta, 0, subpixels) # subtract the inner circle for an annulus if hasattr(self, 'w_in'): mask -= rectangular_overlap_grid(edges[0], edges[1], edges[2], edges[3], nx, ny, self.w_in, self.h_in, self.theta, 0, subpixels) masks.append(ApertureMask(mask, bbox)) if self.isscalar: return masks[0] else: return masks @staticmethod def _calc_extents(width, height, theta): """ Calculate half of the bounding box extents of an ellipse. """ half_width = width / 2. half_height = height / 2. sin_theta = math.sin(theta) cos_theta = math.cos(theta) x_extent1 = abs((half_width * cos_theta) - (half_height * sin_theta)) x_extent2 = abs((half_width * cos_theta) + (half_height * sin_theta)) y_extent1 = abs((half_width * sin_theta) + (half_height * cos_theta)) y_extent2 = abs((half_width * sin_theta) - (half_height * cos_theta)) x_extent = max(x_extent1, x_extent2) y_extent = max(y_extent1, y_extent2) return x_extent, y_extent @staticmethod def _lower_left_positions(positions, width, height, theta): """ Calculate lower-left positions from the input center positions. Used for creating `~matplotlib.patches.Rectangle` patch for the aperture. """ half_width = width / 2. half_height = height / 2. sin_theta = math.sin(theta) cos_theta = math.cos(theta) xshift = (half_height * sin_theta) - (half_width * cos_theta) yshift = -(half_height * cos_theta) - (half_width * sin_theta) return np.atleast_2d(positions) + np.array([xshift, yshift]) class RectangularAperture(RectangularMaskMixin, PixelAperture): """ A rectangular aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units w : float The full width of the rectangle in pixels. For ``theta=0`` the width side is along the ``x`` axis. h : float The full height of the rectangle in pixels. For ``theta=0`` the height side is along the ``y`` axis. theta : float, optional The rotation angle in radians of the rectangle "width" side from the positive ``x`` axis. The rotation angle increases counterclockwise. The default is 0. Raises ------ ValueError : `ValueError` If either width (``w``) or height (``h``) is negative. Examples -------- >>> from photutils.aperture import RectangularAperture >>> aper = RectangularAperture([10., 20.], 5., 3.) >>> aper = RectangularAperture((10., 20.), 5., 3., theta=np.pi) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = RectangularAperture([pos1, pos2, pos3], 5., 3.) >>> aper = RectangularAperture((pos1, pos2, pos3), 5., 3., theta=np.pi) """ _shape_params = ('w', 'h', 'theta') positions = PixelPositions('positions', description='The center pixel position(s).') w = PositiveScalar('w', description='The full width in pixels.') h = PositiveScalar('h', description='The full height in pixels.') theta = Scalar('theta', description=('The counterclockwise rotation angle in ' 'radians of the rectangle "width" side from ' 'the positive x axis.')) def __init__(self, positions, w, h, theta=0.): self.positions = positions self.w = w self.h = h self.theta = theta @property def _xy_extents(self): return self._calc_extents(self.w, self.h, self.theta) @property def area(self): return self.w * self.h def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) xy_positions = self._lower_left_positions(xy_positions, self.w, self.h, self.theta) patches = [] theta_deg = self.theta * 180. / np.pi for xy_position in xy_positions: patches.append(mpatches.Rectangle(xy_position, self.w, self.h, theta_deg, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyRectangularAperture` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyRectangularAperture` object A `SkyRectangularAperture` object. """ return SkyRectangularAperture(**self._to_sky_params(wcs)) class RectangularAnnulus(RectangularMaskMixin, PixelAperture): r""" A rectangular annulus aperture defined in pixel coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : array_like or `~astropy.units.Quantity` The pixel coordinates of the aperture center(s) in one of the following formats: * single ``(x, y)`` pair as a tuple, list, or `~numpy.ndarray` * tuple, list, or `~numpy.ndarray` of ``(x, y)`` pairs * `~astropy.units.Quantity` instance of ``(x, y)`` pairs in pixel units w_in : float The inner full width of the rectangular annulus in pixels. For ``theta=0`` the width side is along the ``x`` axis. w_out : float The outer full width of the rectangular annulus in pixels. For ``theta=0`` the width side is along the ``x`` axis. h_out : float The outer full height of the rectangular annulus in pixels. h_in : `None` or float The inner full height of the rectangular annulus in pixels. If `None`, then the inner full height is calculated as: .. math:: h_{in} = h_{out} \left(\frac{w_{in}}{w_{out}}\right) For ``theta=0`` the height side is along the ``y`` axis. theta : float, optional The rotation angle in radians of the rectangle "width" side from the positive ``x`` axis. The rotation angle increases counterclockwise. The default is 0. Raises ------ ValueError : `ValueError` If inner width (``w_in``) is greater than outer width (``w_out``). ValueError : `ValueError` If either the inner width (``w_in``) or the outer height (``h_out``) is negative. Examples -------- >>> from photutils.aperture import RectangularAnnulus >>> aper = RectangularAnnulus([10., 20.], 3., 8., 5.) >>> aper = RectangularAnnulus((10., 20.), 3., 8., 5., theta=np.pi) >>> pos1 = (10., 20.) # (x, y) >>> pos2 = (30., 40.) >>> pos3 = (50., 60.) >>> aper = RectangularAnnulus([pos1, pos2, pos3], 3., 8., 5.) >>> aper = RectangularAnnulus((pos1, pos2, pos3), 3., 8., 5., theta=np.pi) """ _shape_params = ('w_in', 'w_out', 'h_in', 'h_out', 'theta') positions = PixelPositions('positions', description='The center pixel position(s).') w_in = PositiveScalar('w_in', description='The inner full width in pixels.') w_out = PositiveScalar('w_out', description='The outer full width in pixels.') h_in = PositiveScalar('h_in', description='The inner full height in pixels.') h_out = PositiveScalar('h_out', description='The outer full height in pixels.') theta = Scalar('theta', description=('The counterclockwise rotation angle in ' 'radians of the rectangle "width" side from ' 'the positive x axis.')) def __init__(self, positions, w_in, w_out, h_out, h_in=None, theta=0.): if not w_out > w_in: raise ValueError('"w_out" must be greater than "w_in"') self.positions = positions self.w_in = w_in self.w_out = w_out self.h_out = h_out if h_in is None: h_in = self.w_in * self.h_out / self.w_out else: if not h_out > h_in: raise ValueError('"h_out" must be greater than "h_in"') self.h_in = h_in self.theta = theta @property def _xy_extents(self): return self._calc_extents(self.w_out, self.h_out, self.theta) @property def area(self): return self.w_out * self.h_out - self.w_in * self.h_in def _to_patch(self, origin=(0, 0), **kwargs): """ Return a `~matplotlib.patches.patch` for the aperture. Parameters ---------- origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : `~matplotlib.patches.patch` or list of `~matplotlib.patches.patch` A patch for the aperture. If the aperture is scalar then a single `~matplotlib.patches.patch` is returned, otherwise a list of `~matplotlib.patches.patch` is returned. """ import matplotlib.patches as mpatches xy_positions, patch_kwargs = self._define_patch_params(origin=origin, **kwargs) inner_xy_positions = self._lower_left_positions(xy_positions, self.w_in, self.h_in, self.theta) outer_xy_positions = self._lower_left_positions(xy_positions, self.w_out, self.h_out, self.theta) patches = [] theta_deg = self.theta * 180. / np.pi for xy_in, xy_out in zip(inner_xy_positions, outer_xy_positions): patch_inner = mpatches.Rectangle(xy_in, self.w_in, self.h_in, theta_deg) patch_outer = mpatches.Rectangle(xy_out, self.w_out, self.h_out, theta_deg) path = self._make_annulus_path(patch_inner, patch_outer) patches.append(mpatches.PathPatch(path, **patch_kwargs)) if self.isscalar: return patches[0] else: return patches def to_sky(self, wcs): """ Convert the aperture to a `SkyRectangularAnnulus` object defined in celestial coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `SkyRectangularAnnulus` object A `SkyRectangularAnnulus` object. """ return SkyRectangularAnnulus(**self._to_sky_params(wcs)) class SkyRectangularAperture(SkyAperture): """ A rectangular aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. w : scalar `~astropy.units.Quantity` The full width of the rectangle, either in angular or pixel units. For ``theta=0`` the width side is along the North-South axis. h : scalar `~astropy.units.Quantity` The full height of the rectangle, either in angular or pixel units. For ``theta=0`` the height side is along the East-West axis. theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the rectangle "width" side. For a right-handed world coordinate system, the position angle increases counterclockwise from North (PA=0). The default is 0 degrees. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyRectangularAperture >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyRectangularAperture(positions, 1.0*u.arcsec, 0.5*u.arcsec) """ _shape_params = ('w', 'h', 'theta') positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') w = AngleOrPixelScalarQuantity( 'w', description='The full width, in angular or pixel units.') h = AngleOrPixelScalarQuantity( 'h', description='The full height, in angular or pixel units.') theta = AngleScalarQuantity( 'theta', description=('The position angle (in angular units) of the ' 'rectangle "width" side.')) def __init__(self, positions, w, h, theta=0.*u.deg): if w.unit.physical_type != h.unit.physical_type: raise ValueError('"w" and "h" should either both be angles or ' 'in pixels') self.positions = positions self.w = w self.h = h self.theta = theta def to_pixel(self, wcs): """ Convert the aperture to a `RectangularAperture` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `RectangularAperture` object A `RectangularAperture` object. """ return RectangularAperture(**self._to_pixel_params(wcs)) class SkyRectangularAnnulus(SkyAperture): r""" A rectangular annulus aperture defined in sky coordinates. The aperture has a single fixed size/shape, but it can have multiple positions (see the ``positions`` input). Parameters ---------- positions : `~astropy.coordinates.SkyCoord` The celestial coordinates of the aperture center(s). This can be either scalar coordinates or an array of coordinates. w_in : scalar `~astropy.units.Quantity` The inner full width of the rectangular annulus, either in angular or pixel units. For ``theta=0`` the width side is along the North-South axis. w_out : scalar `~astropy.units.Quantity` The outer full width of the rectangular annulus, either in angular or pixel units. For ``theta=0`` the width side is along the North-South axis. h_out : scalar `~astropy.units.Quantity` The outer full height of the rectangular annulus, either in angular or pixel units. h_in : `None` or scalar `~astropy.units.Quantity` The outer full height of the rectangular annulus, either in angular or pixel units. If `None`, then the inner full height is calculated as: .. math:: h_{in} = h_{out} \left(\frac{w_{in}}{w_{out}}\right) For ``theta=0`` the height side is along the East-West axis. theta : scalar `~astropy.units.Quantity`, optional The position angle (in angular units) of the rectangle "width" side. For a right-handed world coordinate system, the position angle increases counterclockwise from North (PA=0). The default is 0 degrees. Examples -------- >>> from astropy.coordinates import SkyCoord >>> import astropy.units as u >>> from photutils.aperture import SkyRectangularAnnulus >>> positions = SkyCoord(ra=[10., 20.], dec=[30., 40.], unit='deg') >>> aper = SkyRectangularAnnulus(positions, 3.0*u.arcsec, 8.0*u.arcsec, ... 5.0*u.arcsec) """ _shape_params = ('w_in', 'w_out', 'h_in', 'h_out', 'theta') positions = SkyCoordPositions( 'positions', description='The center position(s) in sky coordinates.') w_in = AngleOrPixelScalarQuantity( 'w_in', description='The inner full width, in angular or pixel units.') w_out = AngleOrPixelScalarQuantity( 'w_out', description='The outer full width, in angular or pixel units.') h_in = AngleOrPixelScalarQuantity( 'h_in', description='The inner full height, in angular or pixel units.') h_out = AngleOrPixelScalarQuantity( 'h_out', description='The outer full height, in angular or pixel units.') theta = AngleScalarQuantity( 'theta', description=('The position angle (in angular units) of the ' 'rectangle "width" side.')) def __init__(self, positions, w_in, w_out, h_out, h_in=None, theta=0.*u.deg): if w_in.unit.physical_type != w_out.unit.physical_type: raise ValueError('w_in and w_out should either both be angles or ' 'in pixels') if w_out.unit.physical_type != h_out.unit.physical_type: raise ValueError('w_out and h_out should either both be angles ' 'or in pixels') if not w_out > w_in: raise ValueError('"w_out" must be greater than "w_in".') self.positions = positions self.w_in = w_in self.w_out = w_out self.h_out = h_out if h_in is None: h_in = self.w_in * self.h_out / self.w_out else: if h_in.unit.physical_type != h_out.unit.physical_type: raise ValueError('h_in and h_out should either both be ' 'angles or in pixels') if not h_out > h_in: raise ValueError('"h_out" must be greater than "h_in".') self.h_in = h_in self.theta = theta def to_pixel(self, wcs): """ Convert the aperture to a `RectangularAnnulus` object defined in pixel coordinates. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- aperture : `RectangularAnnulus` object A `RectangularAnnulus` object. """ return RectangularAnnulus(**self._to_pixel_params(wcs)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9827542 photutils-1.3.0/photutils/aperture/tests/0000755000214200020070000000000000000000000017222 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/aperture/tests/__init__.py0000644000214200020070000000000000000000000021321 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/aperture/tests/test_aperture_common.py0000644000214200020070000000351400000000000024035 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides base classes for aperture tests. """ from astropy.coordinates import SkyCoord from astropy.tests.helper import assert_quantity_allclose from numpy.testing import assert_array_equal class BaseTestApertureParams: index = 2 slc = slice(0, 2) expected_slc_len = 2 class BaseTestAperture(BaseTestApertureParams): def test_index(self): aper = self.aperture[self.index] assert isinstance(aper, self.aperture.__class__) assert aper.isscalar expected_positions = self.aperture.positions[self.index] if isinstance(expected_positions, SkyCoord): assert_quantity_allclose(aper.positions.ra, expected_positions.ra) assert_quantity_allclose(aper.positions.dec, expected_positions.dec) else: assert_array_equal(aper.positions, expected_positions) for shape_param in aper._shape_params: assert (getattr(aper, shape_param) == getattr(self.aperture, shape_param)) def test_slice(self): aper = self.aperture[self.slc] assert isinstance(aper, self.aperture.__class__) assert len(aper) == self.expected_slc_len expected_positions = self.aperture.positions[self.slc] if isinstance(self.aperture.positions, SkyCoord): assert_quantity_allclose(aper.positions.ra, expected_positions.ra) assert_quantity_allclose(aper.positions.dec, expected_positions.dec) else: assert_array_equal(aper.positions, expected_positions) for shape_param in aper._shape_params: assert (getattr(aper, shape_param) == getattr(self.aperture, shape_param)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/tests/test_bounding_box.py0000644000214200020070000001113700000000000023313 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the bounding_box module. """ from numpy.testing import assert_allclose import pytest from ..bounding_box import BoundingBox from ..rectangle import RectangularAperture from ...utils._optional_deps import HAS_MATPLOTLIB # noqa def test_bounding_box_init(): bbox = BoundingBox(1, 10, 2, 20) assert bbox.ixmin == 1 assert bbox.ixmax == 10 assert bbox.iymin == 2 assert bbox.iymax == 20 def test_bounding_box_init_minmax(): with pytest.raises(ValueError): BoundingBox(100, 1, 1, 100) with pytest.raises(ValueError): BoundingBox(1, 100, 100, 1) def test_bounding_box_inputs(): with pytest.raises(TypeError): BoundingBox([1], [10], [2], [9]) with pytest.raises(TypeError): BoundingBox([1, 2], 10, 2, 9) with pytest.raises(TypeError): BoundingBox(1.0, 10.0, 2.0, 9.0) with pytest.raises(TypeError): BoundingBox(1.3, 10, 2, 9) with pytest.raises(TypeError): BoundingBox(1, 10.3, 2, 9) with pytest.raises(TypeError): BoundingBox(1, 10, 2.3, 9) with pytest.raises(TypeError): BoundingBox(1, 10, 2, 9.3) def test_bounding_box_from_float(): # This is the example from the method docstring bbox = BoundingBox.from_float(xmin=1.0, xmax=10.0, ymin=2.0, ymax=20.0) assert bbox == BoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=21) bbox = BoundingBox.from_float(xmin=1.4, xmax=10.4, ymin=1.6, ymax=10.6) assert bbox == BoundingBox(ixmin=1, ixmax=11, iymin=2, iymax=12) def test_bounding_box_eq(): bbox = BoundingBox(1, 10, 2, 20) assert bbox == BoundingBox(1, 10, 2, 20) assert bbox != BoundingBox(9, 10, 2, 20) assert bbox != BoundingBox(1, 99, 2, 20) assert bbox != BoundingBox(1, 10, 9, 20) assert bbox != BoundingBox(1, 10, 2, 99) with pytest.raises(TypeError): assert bbox == (1, 10, 2, 20) def test_bounding_box_repr(): bbox = BoundingBox(1, 10, 2, 20) assert repr(bbox) == 'BoundingBox(ixmin=1, ixmax=10, iymin=2, iymax=20)' def test_bounding_box_shape(): bbox = BoundingBox(1, 10, 2, 20) assert bbox.shape == (18, 9) def test_bounding_box_center(): bbox = BoundingBox(1, 10, 2, 20) assert bbox.center == (10.5, 5) def test_bounding_box_get_overlap_slices(): bbox = BoundingBox(1, 10, 2, 20) slc = ((slice(2, 20, None), slice(1, 10, None)), (slice(0, 18, None), slice(0, 9, None))) assert bbox.get_overlap_slices((50, 50)) == slc bbox = BoundingBox(-10, -1, 2, 20) assert bbox.get_overlap_slices((50, 50)) == (None, None) bbox = BoundingBox(-10, 10, -10, 20) slc = ((slice(0, 20, None), slice(0, 10, None)), (slice(10, 30, None), slice(10, 20, None))) assert bbox.get_overlap_slices((50, 50)) == slc def test_bounding_box_extent(): bbox = BoundingBox(1, 10, 2, 20) assert_allclose(bbox.extent, (0.5, 9.5, 1.5, 19.5)) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_bounding_box_as_artist(): bbox = BoundingBox(1, 10, 2, 20) patch = bbox.as_artist() assert_allclose(patch.get_xy(), (0.5, 1.5)) assert_allclose(patch.get_width(), 9) assert_allclose(patch.get_height(), 18) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_bounding_box_to_aperture(): bbox = BoundingBox(1, 10, 2, 20) aper = RectangularAperture((5.0, 10.5), w=9., h=18., theta=0.) bbox_aper = bbox.to_aperture() assert_allclose(bbox_aper.positions, aper.positions) assert bbox_aper.w == aper.w assert bbox_aper.h == aper.h assert bbox_aper.theta == aper.theta @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_bounding_box_plot(): # TODO: check the content of the plot bbox = BoundingBox(1, 10, 2, 20) bbox.plot() def test_bounding_box_union(): bbox1 = BoundingBox(1, 10, 2, 20) bbox2 = BoundingBox(5, 21, 7, 32) bbox_union_expected = BoundingBox(1, 21, 2, 32) bbox_union1 = bbox1 | bbox2 bbox_union2 = bbox1.union(bbox2) assert bbox_union1 == bbox_union_expected assert bbox_union1 == bbox_union2 with pytest.raises(TypeError): bbox1.union((5, 21, 7, 32)) def test_bounding_box_intersect(): bbox1 = BoundingBox(1, 10, 2, 20) bbox2 = BoundingBox(5, 21, 7, 32) bbox_intersect_expected = BoundingBox(5, 10, 7, 20) bbox_intersect1 = bbox1 & bbox2 bbox_intersect2 = bbox1.intersection(bbox2) assert bbox_intersect1 == bbox_intersect_expected assert bbox_intersect1 == bbox_intersect2 with pytest.raises(TypeError): bbox1.intersection((5, 21, 7, 32)) assert bbox1.intersection(BoundingBox(30, 40, 50, 60)) is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/tests/test_circle.py0000644000214200020070000000645000000000000022101 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the circle module. """ from astropy.coordinates import SkyCoord import astropy.units as u import numpy as np from numpy.testing import assert_allclose import pytest from .test_aperture_common import BaseTestAperture from ..circle import (CircularAperture, CircularAnnulus, SkyCircularAperture, SkyCircularAnnulus) from ...utils._optional_deps import HAS_MATPLOTLIB # noqa POSITIONS = [(10, 20), (30, 40), (50, 60), (70, 80)] RA, DEC = np.transpose(POSITIONS) SKYCOORD = SkyCoord(ra=RA, dec=DEC, unit='deg') UNIT = u.arcsec class TestCircularAperture(BaseTestAperture): aperture = CircularAperture(POSITIONS, r=3.) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plot(self): self.aperture.plot() @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plot_returns_patches(self): from matplotlib import pyplot as plt from matplotlib.patches import Patch my_patches = self.aperture.plot() assert isinstance(my_patches, list) for patch in my_patches: assert isinstance(patch, Patch) # test creating a legend with these patches plt.legend(my_patches, list(range(len(my_patches)))) class TestCircularAnnulus(BaseTestAperture): aperture = CircularAnnulus(POSITIONS, r_in=3., r_out=7.) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plot(self): self.aperture.plot() @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plot_returns_patches(self): from matplotlib import pyplot as plt from matplotlib.patches import Patch my_patches = self.aperture.plot() assert isinstance(my_patches, list) for p in my_patches: assert isinstance(p, Patch) # make sure I can create a legend with these patches labels = list(range(len(my_patches))) plt.legend(my_patches, labels) class TestSkyCircularAperture(BaseTestAperture): aperture = SkyCircularAperture(SKYCOORD, r=3.*UNIT) class TestSkyCircularAnnulus(BaseTestAperture): aperture = SkyCircularAnnulus(SKYCOORD, r_in=3.*UNIT, r_out=7.*UNIT) def test_slicing(): xypos = [(10, 10), (20, 20), (30, 30)] aper1 = CircularAperture(xypos, r=3) aper2 = aper1[0:2] assert len(aper2) == 2 aper3 = aper1[0] assert aper3.isscalar with pytest.raises(TypeError): len(aper3) with pytest.raises(TypeError): _ = aper3[0] def test_area_overlap(): data = np.ones((11, 11)) xypos = [(0, 0), (5, 5), (50, 50)] aper = CircularAperture(xypos, r=3) areas = aper.area_overlap(data) assert_allclose(areas, [10.304636, np.pi*9., np.nan]) aper2 = CircularAperture(xypos[1], r=3) area2 = aper2.area_overlap(data) assert_allclose(area2, np.pi * 9.) def test_area_overlap_mask(): data = np.ones((11, 11)) mask = np.zeros((11, 11), dtype=bool) mask[0, 0:2] = True mask[5, 5:7] = True xypos = [(0, 0), (5, 5), (50, 50)] aper = CircularAperture(xypos, r=3) areas = aper.area_overlap(data, mask=mask) areas_exp = np.array([10.304636, np.pi*9., np.nan]) - 2. assert_allclose(areas, areas_exp) with pytest.raises(ValueError): mask = np.zeros((3, 3), dtype=bool) aper.area_overlap(data, mask=mask) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/aperture/tests/test_ellipse.py0000644000214200020070000000225100000000000022270 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the ellipse module. """ from astropy.coordinates import SkyCoord import astropy.units as u import numpy as np from .test_aperture_common import BaseTestAperture from ..ellipse import (EllipticalAperture, EllipticalAnnulus, SkyEllipticalAperture, SkyEllipticalAnnulus) POSITIONS = [(10, 20), (30, 40), (50, 60), (70, 80)] RA, DEC = np.transpose(POSITIONS) SKYCOORD = SkyCoord(ra=RA, dec=DEC, unit='deg') UNIT = u.arcsec class TestEllipticalAperture(BaseTestAperture): aperture = EllipticalAperture(POSITIONS, a=10., b=5., theta=np.pi/2.) class TestEllipticalAnnulus(BaseTestAperture): aperture = EllipticalAnnulus(POSITIONS, a_in=10., a_out=20., b_out=17, theta=np.pi/3) class TestSkyEllipticalAperture(BaseTestAperture): aperture = SkyEllipticalAperture(SKYCOORD, a=10.*UNIT, b=5.*UNIT, theta=30*u.deg) class TestSkyEllipticalAnnulus(BaseTestAperture): aperture = SkyEllipticalAnnulus(SKYCOORD, a_in=10.*UNIT, a_out=20.*UNIT, b_out=17.*UNIT, theta=60*u.deg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/tests/test_mask.py0000644000214200020070000001371200000000000021572 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the mask module. """ import astropy.units as u import numpy as np from numpy.testing import assert_allclose, assert_almost_equal import pytest from ..bounding_box import BoundingBox from ..circle import CircularAperture, CircularAnnulus from ..mask import ApertureMask from ..rectangle import RectangularAnnulus POSITIONS = [(-20, -20), (-20, 20), (20, -20), (60, 60)] def test_mask_input_shapes(): with pytest.raises(ValueError): mask_data = np.ones((10, 10)) bbox = BoundingBox(5, 10, 5, 10) ApertureMask(mask_data, bbox) def test_mask_array(): mask_data = np.ones((10, 10)) bbox = BoundingBox(5, 15, 5, 15) mask = ApertureMask(mask_data, bbox) data = np.array(mask) assert_allclose(data, mask.data) def test_mask_get_overlap_slices(): aper = CircularAperture((5, 5), r=10.) mask = aper.to_mask() slc = ((slice(0, 16, None), slice(0, 16, None)), (slice(5, 21, None), slice(5, 21, None))) assert mask.get_overlap_slices((25, 25)) == slc def test_mask_cutout_shape(): mask_data = np.ones((10, 10)) bbox = BoundingBox(5, 15, 5, 15) mask = ApertureMask(mask_data, bbox) with pytest.raises(ValueError): mask.cutout(np.arange(10)) with pytest.raises(ValueError): mask.to_image((10,)) def test_mask_cutout_copy(): data = np.ones((50, 50)) aper = CircularAperture((25, 25), r=10.) mask = aper.to_mask() cutout = mask.cutout(data, copy=True) data[25, 25] = 100. assert cutout[10, 10] == 1. # test quantity data data2 = np.ones((50, 50)) * u.adu cutout2 = mask.cutout(data2, copy=True) assert cutout2.unit == data2.unit data2[25, 25] = 100. * u.adu assert cutout2[10, 10].value == 1. @pytest.mark.parametrize('position', POSITIONS) def test_mask_cutout_no_overlap(position): data = np.ones((50, 50)) aper = CircularAperture(position, r=10.) mask = aper.to_mask() cutout = mask.cutout(data) assert cutout is None weighted_data = mask.multiply(data) assert weighted_data is None image = mask.to_image(data.shape) assert image is None @pytest.mark.parametrize('position', POSITIONS) def test_mask_cutout_partial_overlap(position): data = np.ones((50, 50)) aper = CircularAperture(position, r=30.) mask = aper.to_mask() cutout = mask.cutout(data) assert cutout.shape == mask.shape weighted_data = mask.multiply(data) assert weighted_data.shape == mask.shape image = mask.to_image(data.shape) assert image.shape == data.shape def test_mask_multiply(): radius = 10. data = np.ones((50, 50)) aper = CircularAperture((25, 25), r=radius) mask = aper.to_mask() data_weighted = mask.multiply(data) assert_almost_equal(np.sum(data_weighted), np.pi * radius**2) # test that multiply() returns a copy data[25, 25] = 100. assert data_weighted[10, 10] == 1. def test_mask_multiply_quantity(): radius = 10. data = np.ones((50, 50)) * u.adu aper = CircularAperture((25, 25), r=radius) mask = aper.to_mask() data_weighted = mask.multiply(data) assert data_weighted.unit == u.adu assert_almost_equal(np.sum(data_weighted.value), np.pi * radius**2) # test that multiply() returns a copy data[25, 25] = 100. * u.adu assert data_weighted[10, 10].value == 1. @pytest.mark.parametrize('value', (np.nan, np.inf)) def test_mask_nonfinite_fill_value(value): aper = CircularAnnulus((0, 0), 10, 20) data = np.ones((101, 101)).astype(int) cutout = aper.to_mask().cutout(data, fill_value=value) assert ~np.isfinite(cutout[0, 0]) def test_mask_multiply_fill_value(): aper = CircularAnnulus((0, 0), 10, 20) data = np.ones((101, 101)).astype(int) cutout = aper.to_mask().multiply(data, fill_value=np.nan) xypos = ((20, 20), (5, 5), (5, 35), (35, 5), (35, 35)) for x, y in xypos: assert np.isnan(cutout[y, x]) def test_mask_nonfinite_in_bbox(): """ Regression test that non-finite data values outside of the mask but within the bounding box are set to zero. """ data = np.ones((101, 101)) data[33, 33] = np.nan data[67, 67] = np.inf data[33, 67] = -np.inf data[22, 22] = np.nan data[22, 23] = np.inf radius = 20. aper1 = CircularAperture((50, 50), r=radius) aper2 = CircularAperture((5, 5), r=radius) wdata1 = aper1.to_mask(method='exact').multiply(data) assert_allclose(np.sum(wdata1), np.pi * radius**2) wdata2 = aper2.to_mask(method='exact').multiply(data) assert_allclose(np.sum(wdata2), 561.6040111923013) def test_mask_get_values(): aper = CircularAnnulus(((0, 0), (50, 50), (100, 100)), 10, 20) data = np.ones((101, 101)) values = [mask.get_values(data) for mask in aper.to_mask()] shapes = [val.shape for val in values] sums = [np.sum(val) for val in values] assert shapes[0] == (278,) assert shapes[1] == (1068,) assert shapes[2] == (278,) sums_expected = (245.621534, 942.477796, 245.621534) assert_allclose(sums, sums_expected) def test_mask_get_values_no_overlap(): aper = CircularAperture((-100, -100), r=3) data = np.ones((51, 51)) values = aper.to_mask().get_values(data) assert values.shape == (0,) def test_mask_get_values_mask(): aper = CircularAperture((24.5, 24.5), r=10.) data = np.ones((51, 51)) mask = aper.to_mask() with pytest.raises(ValueError): mask.get_values(data, mask=np.ones(3)) arr = mask.get_values(data, mask=None) assert_allclose(np.sum(arr), 100. * np.pi) data_mask = np.zeros(data.shape, dtype=bool) data_mask[25:] = True arr2 = mask.get_values(data, mask=data_mask) assert_allclose(np.sum(arr2), 100. * np.pi / 2.) def test_rectangular_annulus_hin(): aper = RectangularAnnulus((25, 25), 2, 4, 20, h_in=18, theta=0) mask = aper.to_mask(method='center') assert mask.data.shape == (21, 5) assert np.count_nonzero(mask.data) == 40 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/aperture/tests/test_photometry.py0000644000214200020070000010010300000000000023040 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the photometry module. """ import pytest import numpy as np from numpy.testing import (assert_allclose, assert_array_equal, assert_array_less) from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.nddata import NDData, StdDevUncertainty from astropy.table import Table import astropy.units as u from astropy.wcs import WCS from astropy.wcs.utils import pixel_to_skycoord from ..photometry import aperture_photometry from ..circle import (CircularAperture, CircularAnnulus, SkyCircularAperture, SkyCircularAnnulus) from ..ellipse import (EllipticalAperture, EllipticalAnnulus, SkyEllipticalAperture, SkyEllipticalAnnulus) from ..rectangle import (RectangularAperture, RectangularAnnulus, SkyRectangularAperture, SkyRectangularAnnulus) from ...datasets import get_path, make_4gaussians_image, make_wcs, make_gwcs from ...utils._optional_deps import HAS_GWCS, HAS_MATPLOTLIB # noqa APERTURE_CL = [CircularAperture, CircularAnnulus, EllipticalAperture, EllipticalAnnulus, RectangularAperture, RectangularAnnulus] TEST_APERTURES = list(zip(APERTURE_CL, ((3.,), (3., 5.), (3., 5., 1.), (3., 5., 4., 12./5., 1.), (5, 8, np.pi / 4), (8, 12, 8, 16./3., np.pi / 8)))) @pytest.mark.parametrize(('aperture_class', 'params'), TEST_APERTURES) def test_outside_array(aperture_class, params): data = np.ones((10, 10), dtype=float) aperture = aperture_class((-60, 60), *params) fluxtable = aperture_photometry(data, aperture) # aperture is fully outside array: assert np.isnan(fluxtable['aperture_sum']) @pytest.mark.parametrize(('aperture_class', 'params'), TEST_APERTURES) def test_inside_array_simple(aperture_class, params): data = np.ones((40, 40), dtype=float) aperture = aperture_class((20., 20.), *params) table1 = aperture_photometry(data, aperture, method='center', subpixels=10) table2 = aperture_photometry(data, aperture, method='subpixel', subpixels=10) table3 = aperture_photometry(data, aperture, method='exact', subpixels=10) true_flux = aperture.area assert table1['aperture_sum'] < table3['aperture_sum'] if not isinstance(aperture, (RectangularAperture, RectangularAnnulus)): assert_allclose(table3['aperture_sum'], true_flux) assert_allclose(table2['aperture_sum'], table3['aperture_sum'], atol=0.1) @pytest.mark.skipif('not HAS_MATPLOTLIB') @pytest.mark.parametrize(('aperture_class', 'params'), TEST_APERTURES) def test_aperture_plots(aperture_class, params): # This test should run without any errors, and there is no return # value. # TODO: check the content of the plot aperture = aperture_class((20., 20.), *params) aperture.plot() def test_aperture_pixel_positions(): pos1 = (10, 20) pos2 = [(10, 20)] pos3 = u.Quantity((10, 20), unit=u.pixel) pos4 = u.Quantity([(10, 20)], unit=u.pixel) r = 3 ap1 = CircularAperture(pos1, r) ap2 = CircularAperture(pos2, r) ap3 = CircularAperture(pos3, r) ap4 = CircularAperture(pos4, r) assert not np.array_equal(ap1.positions, ap2.positions) assert_allclose(ap1.positions, ap3.positions) assert_allclose(ap2.positions, ap4.positions) class BaseTestAperturePhotometry: def test_array_error(self): # Array error error = np.ones(self.data.shape, dtype=float) if not hasattr(self, 'mask'): mask = None true_error = np.sqrt(self.area) else: mask = self.mask # 1 masked pixel true_error = np.sqrt(self.area - 1) table1 = aperture_photometry(self.data, self.aperture, method='center', mask=mask, error=error) table2 = aperture_photometry(self.data, self.aperture, method='subpixel', subpixels=12, mask=mask, error=error) table3 = aperture_photometry(self.data, self.aperture, method='exact', mask=mask, error=error) if not isinstance(self.aperture, (RectangularAperture, RectangularAnnulus)): assert_allclose(table3['aperture_sum'], self.true_flux) assert_allclose(table2['aperture_sum'], table3['aperture_sum'], atol=0.1) assert np.all(table1['aperture_sum'] < table3['aperture_sum']) if not isinstance(self.aperture, (RectangularAperture, RectangularAnnulus)): assert_allclose(table3['aperture_sum_err'], true_error) assert_allclose(table2['aperture_sum_err'], table3['aperture_sum_err'], atol=0.1) assert np.all(table1['aperture_sum_err'] < table3['aperture_sum_err']) class TestCircular(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) r = 10. self.aperture = CircularAperture(position, r) self.area = np.pi * r * r self.true_flux = self.area class TestCircularArray(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = ((20., 20.), (25., 25.)) r = 10. self.aperture = CircularAperture(position, r) self.area = np.pi * r * r self.area = np.array((self.area, ) * 2) self.true_flux = self.area class TestCircularAnnulus(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) r_in = 8. r_out = 10. self.aperture = CircularAnnulus(position, r_in, r_out) self.area = np.pi * (r_out * r_out - r_in * r_in) self.true_flux = self.area class TestCircularAnnulusArray(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = ((20., 20.), (25., 25.)) r_in = 8. r_out = 10. self.aperture = CircularAnnulus(position, r_in, r_out) self.area = np.pi * (r_out * r_out - r_in * r_in) self.area = np.array((self.area, ) * 2) self.true_flux = self.area class TestElliptical(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) a = 10. b = 5. theta = -np.pi / 4. self.aperture = EllipticalAperture(position, a, b, theta=theta) self.area = np.pi * a * b self.true_flux = self.area class TestEllipticalAnnulus(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) a_in = 5. a_out = 8. b_out = 5. theta = -np.pi / 4. self.aperture = EllipticalAnnulus(position, a_in, a_out, b_out, theta=theta) self.area = (np.pi * (a_out * b_out) - np.pi * (a_in * b_out * a_in / a_out)) self.true_flux = self.area class TestRectangularAperture(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) h = 5. w = 8. theta = np.pi / 4. self.aperture = RectangularAperture(position, w, h, theta=theta) self.area = h * w self.true_flux = self.area class TestRectangularAnnulus(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) position = (20., 20.) h_out = 8. w_in = 8. w_out = 12. h_in = w_in * h_out / w_out theta = np.pi / 8. self.aperture = RectangularAnnulus(position, w_in, w_out, h_out, theta=theta) self.area = h_out * w_out - h_in * w_in self.true_flux = self.area class TestMaskedSkipCircular(BaseTestAperturePhotometry): def setup_class(self): self.data = np.ones((40, 40), dtype=float) self.mask = np.zeros((40, 40), dtype=bool) self.mask[20, 20] = True position = (20., 20.) r = 10. self.aperture = CircularAperture(position, r) self.area = np.pi * r * r self.true_flux = self.area - 1 class BaseTestDifferentData: def test_basic_circular_aperture_photometry(self): aperture = CircularAperture(self.position, self.radius) table = aperture_photometry(self.data, aperture, method='exact') assert_allclose(table['aperture_sum'].value, self.true_flux) assert table['aperture_sum'].unit, self.fluxunit assert np.all(table['xcenter'].value == np.transpose(self.position)[0]) assert np.all(table['ycenter'].value == np.transpose(self.position)[1]) class TestInputNDData(BaseTestDifferentData): def setup_class(self): data = np.ones((40, 40), dtype=float) self.data = NDData(data, unit=u.adu) self.radius = 3 self.position = [(20, 20), (30, 30)] self.true_flux = np.pi * self.radius * self.radius self.fluxunit = u.adu @pytest.mark.remote_data def test_wcs_based_photometry_to_catalogue(): pathcat = get_path('spitzer_example_catalog.xml', location='remote') pathhdu = get_path('spitzer_example_image.fits', location='remote') hdu = fits.open(pathhdu) data = u.Quantity(hdu[0].data, unit=hdu[0].header['BUNIT']) wcs = WCS(hdu[0].header) scale = hdu[0].header['PIXSCAL1'] catalog = Table.read(pathcat) pos_skycoord = SkyCoord(catalog['l'], catalog['b'], frame='galactic') photometry_skycoord = aperture_photometry( data, SkyCircularAperture(pos_skycoord, 4 * u.arcsec), wcs=wcs) photometry_skycoord_pix = aperture_photometry( data, SkyCircularAperture(pos_skycoord, 4. / scale * u.pixel), wcs=wcs) assert_allclose(photometry_skycoord['aperture_sum'], photometry_skycoord_pix['aperture_sum']) # Photometric unit conversion is needed to match the catalogue factor = (1.2 * u.arcsec) ** 2 / u.pixel converted_aperture_sum = (photometry_skycoord['aperture_sum'] * factor).to(u.mJy / u.pixel) fluxes_catalog = catalog['f4_5'].filled() # There shouldn't be large outliers, but some differences is OK, as # fluxes_catalog is based on PSF photometry, etc. assert_allclose(fluxes_catalog, converted_aperture_sum.value, rtol=1e0) assert(np.mean(np.fabs(((fluxes_catalog - converted_aperture_sum.value) / fluxes_catalog))) < 0.1) # close the file hdu.close() def test_wcs_based_photometry(): data = make_4gaussians_image() wcs = make_wcs(data.shape) # hard wired positions in make_4gaussian_image pos_orig_pixel = u.Quantity(([160., 25., 150., 90.], [70., 40., 25., 60.]), unit=u.pixel) try: pos_skycoord = wcs.pixel_to_world(pos_orig_pixel[0], pos_orig_pixel[1]) except AttributeError: # for Astropy < 3.1 pos_skycoord = pixel_to_skycoord(pos_orig_pixel[0], pos_orig_pixel[1], wcs) pos_skycoord_s = pos_skycoord[2] photometry_skycoord_circ = aperture_photometry( data, SkyCircularAperture(pos_skycoord, 3 * u.arcsec), wcs=wcs) photometry_skycoord_circ_2 = aperture_photometry( data, SkyCircularAperture(pos_skycoord, 2 * u.arcsec), wcs=wcs) photometry_skycoord_circ_s = aperture_photometry( data, SkyCircularAperture(pos_skycoord_s, 3 * u.arcsec), wcs=wcs) assert_allclose(photometry_skycoord_circ['aperture_sum'][2], photometry_skycoord_circ_s['aperture_sum']) photometry_skycoord_circ_ann = aperture_photometry( data, SkyCircularAnnulus(pos_skycoord, 2 * u.arcsec, 3 * u.arcsec), wcs=wcs) photometry_skycoord_circ_ann_s = aperture_photometry( data, SkyCircularAnnulus(pos_skycoord_s, 2 * u.arcsec, 3 * u.arcsec), wcs=wcs) assert_allclose(photometry_skycoord_circ_ann['aperture_sum'][2], photometry_skycoord_circ_ann_s['aperture_sum']) assert_allclose(photometry_skycoord_circ_ann['aperture_sum'], photometry_skycoord_circ['aperture_sum'] - photometry_skycoord_circ_2['aperture_sum']) photometry_skycoord_ell = aperture_photometry( data, SkyEllipticalAperture(pos_skycoord, 3 * u.arcsec, 3.0001 * u.arcsec, theta=45 * u.arcsec), wcs=wcs) photometry_skycoord_ell_2 = aperture_photometry( data, SkyEllipticalAperture(pos_skycoord, 2 * u.arcsec, 2.0001 * u.arcsec, theta=45 * u.arcsec), wcs=wcs) photometry_skycoord_ell_s = aperture_photometry( data, SkyEllipticalAperture(pos_skycoord_s, 3 * u.arcsec, 3.0001 * u.arcsec, theta=45 * u.arcsec), wcs=wcs) photometry_skycoord_ell_ann = aperture_photometry( data, SkyEllipticalAnnulus(pos_skycoord, 2 * u.arcsec, 3 * u.arcsec, 3.0001 * u.arcsec, theta=45 * u.arcsec), wcs=wcs) photometry_skycoord_ell_ann_s = aperture_photometry( data, SkyEllipticalAnnulus(pos_skycoord_s, 2 * u.arcsec, 3 * u.arcsec, 3.0001 * u.arcsec, theta=45 * u.arcsec), wcs=wcs) assert_allclose(photometry_skycoord_ell['aperture_sum'][2], photometry_skycoord_ell_s['aperture_sum']) assert_allclose(photometry_skycoord_ell_ann['aperture_sum'][2], photometry_skycoord_ell_ann_s['aperture_sum']) assert_allclose(photometry_skycoord_ell['aperture_sum'], photometry_skycoord_circ['aperture_sum'], rtol=5e-3) assert_allclose(photometry_skycoord_ell_ann['aperture_sum'], photometry_skycoord_ell['aperture_sum'] - photometry_skycoord_ell_2['aperture_sum'], rtol=1e-4) photometry_skycoord_rec = aperture_photometry( data, SkyRectangularAperture(pos_skycoord, 6 * u.arcsec, 6 * u.arcsec, 0 * u.arcsec), method='subpixel', subpixels=20, wcs=wcs) photometry_skycoord_rec_4 = aperture_photometry( data, SkyRectangularAperture(pos_skycoord, 4 * u.arcsec, 4 * u.arcsec, 0 * u.arcsec), method='subpixel', subpixels=20, wcs=wcs) photometry_skycoord_rec_s = aperture_photometry( data, SkyRectangularAperture(pos_skycoord_s, 6 * u.arcsec, 6 * u.arcsec, 0 * u.arcsec), method='subpixel', subpixels=20, wcs=wcs) photometry_skycoord_rec_ann = aperture_photometry( data, SkyRectangularAnnulus(pos_skycoord, 4 * u.arcsec, 6 * u.arcsec, 6 * u.arcsec, theta=0 * u.arcsec), method='subpixel', subpixels=20, wcs=wcs) photometry_skycoord_rec_ann_s = aperture_photometry( data, SkyRectangularAnnulus(pos_skycoord_s, 4 * u.arcsec, 6 * u.arcsec, 6 * u.arcsec, theta=0 * u.arcsec), method='subpixel', subpixels=20, wcs=wcs) assert_allclose(photometry_skycoord_rec['aperture_sum'][2], photometry_skycoord_rec_s['aperture_sum']) assert np.all(photometry_skycoord_rec['aperture_sum'] > photometry_skycoord_circ['aperture_sum']) assert_allclose(photometry_skycoord_rec_ann['aperture_sum'][2], photometry_skycoord_rec_ann_s['aperture_sum']) assert_allclose(photometry_skycoord_rec_ann['aperture_sum'], photometry_skycoord_rec['aperture_sum'] - photometry_skycoord_rec_4['aperture_sum'], rtol=1e-4) def test_basic_circular_aperture_photometry_unit(): data1 = np.ones((40, 40), dtype=float) data2 = u.Quantity(data1*u.adu) radius = 3 position = (20, 20) true_flux = np.pi * radius * radius unit = u.adu table1 = aperture_photometry(data1, CircularAperture(position, radius)) table2 = aperture_photometry(data2, CircularAperture(position, radius)) assert_allclose(table1['aperture_sum'], true_flux) assert_allclose(table2['aperture_sum'].value, true_flux) assert table2['aperture_sum'].unit == data2.unit == unit def test_aperture_photometry_with_error_units(): """Test aperture_photometry when error has units (see #176).""" data1 = np.ones((40, 40), dtype=float) data2 = u.Quantity(data1, unit=u.adu) error = u.Quantity(data1, unit=u.adu) radius = 3 true_flux = np.pi * radius * radius unit = u.adu position = (20, 20) table1 = aperture_photometry(data2, CircularAperture(position, radius), error=error) assert_allclose(table1['aperture_sum'].value, true_flux) assert_allclose(table1['aperture_sum_err'].value, np.sqrt(true_flux)) assert table1['aperture_sum'].unit == unit assert table1['aperture_sum_err'].unit == unit def test_aperture_photometry_inputs_with_mask(): """ Test that aperture_photometry does not modify the input data or error array when a mask is input. """ data = np.ones((5, 5)) aperture = CircularAperture((2, 2), 2.) mask = np.zeros_like(data, dtype=bool) data[2, 2] = 100. # bad pixel mask[2, 2] = True error = np.sqrt(data) data_in = data.copy() error_in = error.copy() t1 = aperture_photometry(data, aperture, error=error, mask=mask) assert_array_equal(data, data_in) assert_array_equal(error, error_in) assert_allclose(t1['aperture_sum'][0], 11.5663706144) t2 = aperture_photometry(data, aperture) assert_allclose(t2['aperture_sum'][0], 111.566370614) TEST_ELLIPSE_EXACT_APERTURES = [(3.469906, 3.923861394, 3.), (0.3834415188257778, 0.3834415188257778, 0.3)] @pytest.mark.parametrize('x,y,r', TEST_ELLIPSE_EXACT_APERTURES) def test_ellipse_exact_grid(x, y, r): """ Test elliptical exact aperture photometry on a grid of pixel positions. This is a regression test for the bug discovered in this issue: https://github.com/astropy/photutils/issues/198 """ data = np.ones((10, 10)) aperture = EllipticalAperture((x, y), r, r, 0.) t = aperture_photometry(data, aperture, method='exact') actual = t['aperture_sum'][0] / (np.pi * r ** 2) assert_allclose(actual, 1) @pytest.mark.parametrize('value', [np.nan, np.inf]) def test_nan_inf_mask(value): """Test that nans and infs are properly masked [267].""" data = np.ones((9, 9)) mask = np.zeros_like(data, dtype=bool) data[4, 4] = value mask[4, 4] = True radius = 2. aper = CircularAperture((4, 4), radius) tbl = aperture_photometry(data, aper, mask=mask) desired = (np.pi * radius**2) - 1 assert_allclose(tbl['aperture_sum'], desired) def test_aperture_partial_overlap(): data = np.ones((20, 20)) error = np.ones((20, 20)) xypos = [(10, 10), (0, 0), (0, 19), (19, 0), (19, 19)] r = 5. aper = CircularAperture(xypos, r=r) tbl = aperture_photometry(data, aper, error=error) assert_allclose(tbl['aperture_sum'][0], np.pi * r ** 2) assert_array_less(tbl['aperture_sum'][1:], np.pi * r ** 2) unit = u.MJy / u.sr tbl = aperture_photometry(data * unit, aper, error=error * unit) assert_allclose(tbl['aperture_sum'][0].value, np.pi * r ** 2) assert_array_less(tbl['aperture_sum'][1:].value, np.pi * r ** 2) assert_array_less(tbl['aperture_sum_err'][1:].value, np.pi * r ** 2) assert tbl['aperture_sum'].unit == unit assert tbl['aperture_sum_err'].unit == unit def test_pixel_aperture_repr(): aper = CircularAperture((10, 20), r=3.0) assert ', r=3.0 pix)>') a_str = ('Aperture: SkyCircularAperture\npositions: \n' 'r: 3.0 pix') assert repr(aper) == a_repr assert str(aper) == a_str aper = SkyCircularAnnulus(s, r_in=3.*u.pix, r_out=5*u.pix) a_repr = (', r_in=3.0 pix, r_out=5.0 pix)>') a_str = ('Aperture: SkyCircularAnnulus\npositions: \n' 'r_in: 3.0 pix\nr_out: 5.0 pix') assert repr(aper) == a_repr assert str(aper) == a_str aper = SkyEllipticalAperture(s, a=3*u.pix, b=5*u.pix, theta=15*u.deg) a_repr = (', a=3.0 pix, b=5.0 pix,' ' theta=15.0 deg)>') a_str = ('Aperture: SkyEllipticalAperture\npositions: \n' 'a: 3.0 pix\nb: 5.0 pix\ntheta: 15.0 deg') assert repr(aper) == a_repr assert str(aper) == a_str aper = SkyEllipticalAnnulus(s, a_in=3*u.pix, a_out=5*u.pix, b_out=3*u.pix, theta=15*u.deg) a_repr = (', a_in=3.0 pix, ' 'a_out=5.0 pix, b_in=1.8 pix, b_out=3.0 pix, ' 'theta=15.0 deg)>') a_str = ('Aperture: SkyEllipticalAnnulus\npositions: \n' 'a_in: 3.0 pix\na_out: 5.0 pix\nb_in: 1.8 pix\n' 'b_out: 3.0 pix\ntheta: 15.0 deg') assert repr(aper) == a_repr assert str(aper) == a_str aper = SkyRectangularAperture(s, w=3*u.pix, h=5*u.pix, theta=15*u.deg) a_repr = (', w=3.0 pix, h=5.0 pix' ', theta=15.0 deg)>') a_str = ('Aperture: SkyRectangularAperture\npositions: \n' 'w: 3.0 pix\nh: 5.0 pix\ntheta: 15.0 deg') assert repr(aper) == a_repr assert str(aper) == a_str aper = SkyRectangularAnnulus(s, w_in=5*u.pix, w_out=10*u.pix, h_out=6*u.pix, theta=15*u.deg) a_repr = (', w_in=5.0 pix, ' 'w_out=10.0 pix, h_in=3.0 pix, h_out=6.0 pix, ' 'theta=15.0 deg)>') a_str = ('Aperture: SkyRectangularAnnulus\npositions: \n' 'w_in: 5.0 pix\nw_out: 10.0 pix\nh_in: 3.0 pix\n' 'h_out: 6.0 pix\ntheta: 15.0 deg') assert repr(aper) == a_repr assert str(aper) == a_str def test_rectangular_bbox(): # odd sizes width = 7 height = 3 a = RectangularAperture((50, 50), w=width, h=height, theta=0) assert a.bbox.shape == (height, width) a = RectangularAperture((50.5, 50.5), w=width, h=height, theta=0) assert a.bbox.shape == (height + 1, width + 1) a = RectangularAperture((50, 50), w=width, h=height, theta=90.*np.pi/180.) assert a.bbox.shape == (width, height) # even sizes width = 8 height = 4 a = RectangularAperture((50, 50), w=width, h=height, theta=0) assert a.bbox.shape == (height + 1, width + 1) a = RectangularAperture((50.5, 50.5), w=width, h=height, theta=0) assert a.bbox.shape == (height, width) a = RectangularAperture((50.5, 50.5), w=width, h=height, theta=90.*np.pi/180.) assert a.bbox.shape == (width, height) def test_elliptical_bbox(): # integer axes a = 7 b = 3 ap = EllipticalAperture((50, 50), a=a, b=b, theta=0) assert ap.bbox.shape == (2*b + 1, 2*a + 1) ap = EllipticalAperture((50.5, 50.5), a=a, b=b, theta=0) assert ap.bbox.shape == (2*b, 2*a) ap = EllipticalAperture((50, 50), a=a, b=b, theta=90.*np.pi/180.) assert ap.bbox.shape == (2*a + 1, 2*b + 1) # fractional axes a = 7.5 b = 4.5 ap = EllipticalAperture((50, 50), a=a, b=b, theta=0) assert ap.bbox.shape == (2*b, 2*a) ap = EllipticalAperture((50.5, 50.5), a=a, b=b, theta=0) assert ap.bbox.shape == (2*b + 1, 2*a + 1) ap = EllipticalAperture((50, 50), a=a, b=b, theta=90.*np.pi/180.) assert ap.bbox.shape == (2*a, 2*b) @pytest.mark.skipif('not HAS_GWCS') @pytest.mark.parametrize('wcs_type', ('wcs', 'gwcs')) def test_to_sky_pixel(wcs_type): data = make_4gaussians_image() if wcs_type == 'wcs': wcs = make_wcs(data.shape) elif wcs_type == 'gwcs': wcs = make_gwcs(data.shape) ap = CircularAperture(((12.3, 15.7), (48.19, 98.14)), r=3.14) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.r, ap2.r) ap = CircularAnnulus(((12.3, 15.7), (48.19, 98.14)), r_in=3.14, r_out=5.32) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.r_in, ap2.r_in) assert_allclose(ap.r_out, ap2.r_out) ap = EllipticalAperture(((12.3, 15.7), (48.19, 98.14)), a=3.14, b=5.32, theta=103.*np.pi/180.) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.a, ap2.a) assert_allclose(ap.b, ap2.b) assert_allclose(ap.theta, ap2.theta) ap = EllipticalAnnulus(((12.3, 15.7), (48.19, 98.14)), a_in=3.14, a_out=15.32, b_out=4.89, theta=103.*np.pi/180.) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.a_in, ap2.a_in) assert_allclose(ap.a_out, ap2.a_out) assert_allclose(ap.b_out, ap2.b_out) assert_allclose(ap.theta, ap2.theta) ap = RectangularAperture(((12.3, 15.7), (48.19, 98.14)), w=3.14, h=5.32, theta=103.*np.pi/180.) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.w, ap2.w) assert_allclose(ap.h, ap2.h) assert_allclose(ap.theta, ap2.theta) ap = RectangularAnnulus(((12.3, 15.7), (48.19, 98.14)), w_in=3.14, w_out=15.32, h_out=4.89, theta=103.*np.pi/180.) ap2 = ap.to_sky(wcs).to_pixel(wcs) assert_allclose(ap.positions, ap2.positions) assert_allclose(ap.w_in, ap2.w_in) assert_allclose(ap.w_out, ap2.w_out) assert_allclose(ap.h_out, ap2.h_out) assert_allclose(ap.theta, ap2.theta) def test_position_units(): """Regression test for unit check.""" pos = (10, 10) * u.pix pos = np.sqrt(pos**2) ap = CircularAperture(pos, r=3.) assert_allclose(ap.positions, np.array([10, 10])) def test_radius_units(): """Regression test for unit check.""" pos = SkyCoord(10, 10, unit='deg') r = 3.*u.pix r = np.sqrt(r**2) ap = SkyCircularAperture(pos, r=r) assert ap.r.value == 3.0 assert ap.r.unit == u.pix def test_scalar_aperture(): """ Regression test to check that length-1 aperture list appends a "_0" on the column names to be consistent with list inputs. """ data = np.ones((20, 20), dtype=float) ap = CircularAperture((10, 10), r=3.) colnames1 = aperture_photometry(data, ap, error=data).colnames assert (colnames1 == ['id', 'xcenter', 'ycenter', 'aperture_sum', 'aperture_sum_err']) colnames2 = aperture_photometry(data, [ap], error=data).colnames assert (colnames2 == ['id', 'xcenter', 'ycenter', 'aperture_sum_0', 'aperture_sum_err_0']) colnames3 = aperture_photometry(data, [ap, ap], error=data).colnames assert (colnames3 == ['id', 'xcenter', 'ycenter', 'aperture_sum_0', 'aperture_sum_err_0', 'aperture_sum_1', 'aperture_sum_err_1']) def test_nan_in_bbox(): """ Regression test that non-finite data values outside of the aperture mask but within the bounding box do not affect the photometry. """ data1 = np.ones((101, 101)) data2 = data1.copy() data1[33, 33] = np.nan data1[67, 67] = np.inf data1[33, 67] = -np.inf data1[22, 22] = np.nan data1[22, 23] = np.inf error = data1.copy() aper1 = CircularAperture((50, 50), r=20.) aper2 = CircularAperture((5, 5), r=20.) tbl1 = aperture_photometry(data1, aper1, error=error) tbl2 = aperture_photometry(data2, aper1, error=error) assert_allclose(tbl1['aperture_sum'], tbl2['aperture_sum']) assert_allclose(tbl1['aperture_sum_err'], tbl2['aperture_sum_err']) tbl3 = aperture_photometry(data1, aper2, error=error) tbl4 = aperture_photometry(data2, aper2, error=error) assert_allclose(tbl3['aperture_sum'], tbl4['aperture_sum']) assert_allclose(tbl3['aperture_sum_err'], tbl4['aperture_sum_err']) def test_scalar_skycoord(): """ Regression test to check that scalar SkyCoords are added to the table as a length-1 SkyCoord array. """ data = make_4gaussians_image() wcs = make_wcs(data.shape) try: skycoord = wcs.pixel_to_world(90, 60) except AttributeError: # for Astropy < 3.1 skycoord = pixel_to_skycoord(90, 60, wcs) aper = SkyCircularAperture(skycoord, r=0.1*u.arcsec) tbl = aperture_photometry(data, aper, wcs=wcs) assert isinstance(tbl['sky_center'], SkyCoord) def test_nddata_input(): data = np.arange(400).reshape((20, 20)) error = np.sqrt(data) mask = np.zeros((20, 20), dtype=bool) mask[8:13, 8:13] = True unit = 'adu' wcs = make_wcs(data.shape) try: skycoord = wcs.pixel_to_world(10, 10) except AttributeError: # for Astropy < 3.1 skycoord = pixel_to_skycoord(10, 10, wcs) aper = SkyCircularAperture(skycoord, r=0.7*u.arcsec) tbl1 = aperture_photometry(data*u.adu, aper, error=error*u.adu, mask=mask, wcs=wcs) uncertainty = StdDevUncertainty(error) nddata = NDData(data, uncertainty=uncertainty, mask=mask, wcs=wcs, unit=unit) tbl2 = aperture_photometry(nddata, aper) for column in tbl1.columns: if column == 'sky_center': # cannot test SkyCoord equality continue assert_allclose(tbl1[column], tbl2[column]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/aperture/tests/test_rectangle.py0000644000214200020070000000227700000000000022607 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the rectangle module. """ from astropy.coordinates import SkyCoord import astropy.units as u import numpy as np from .test_aperture_common import BaseTestAperture from ..rectangle import (RectangularAperture, RectangularAnnulus, SkyRectangularAperture, SkyRectangularAnnulus) POSITIONS = [(10, 20), (30, 40), (50, 60), (70, 80)] RA, DEC = np.transpose(POSITIONS) SKYCOORD = SkyCoord(ra=RA, dec=DEC, unit='deg') UNIT = u.arcsec class TestRectangularAperture(BaseTestAperture): aperture = RectangularAperture(POSITIONS, w=10., h=5., theta=np.pi/2.) class TestRectangularAnnulus(BaseTestAperture): aperture = RectangularAnnulus(POSITIONS, w_in=10., w_out=20., h_out=17, theta=np.pi/3) class TestSkyRectangularAperture(BaseTestAperture): aperture = SkyRectangularAperture(SKYCOORD, w=10.*UNIT, h=5.*UNIT, theta=30*u.deg) class TestSkyRectangularAnnulus(BaseTestAperture): aperture = SkyRectangularAnnulus(SKYCOORD, w_in=10.*UNIT, w_out=20.*UNIT, h_out=17.*UNIT, theta=60*u.deg) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9843557 photutils-1.3.0/photutils/background/0000755000214200020070000000000000000000000016350 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/__init__.py0000644000214200020070000000041100000000000020455 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools to estimate the background and background RMS in an image. """ from .background_2d import * # noqa from .core import * # noqa from .interpolators import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/_utils.py0000644000214200020070000000522300000000000020223 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines nan-ignoring statistical functions, using bottleneck for performance if available. """ import astropy.units as u import numpy as np from ..utils._optional_deps import HAS_BOTTLENECK if HAS_BOTTLENECK: import bottleneck as bn def move_tuple_axes_first(array, axis): """ Bottleneck can only take integer axis, not tuple, so this function takes all the axes to be operated on and combines them into the first dimension of the array so that we can then use axis=0. """ # Figure out how many axes we are operating over naxis = len(axis) # Add remaining axes to the axis tuple axis += tuple(i for i in range(array.ndim) if i not in axis) # The new position of each axis is just in order destination = tuple(range(array.ndim)) # Reorder the array so that the axes being operated on are at the # beginning array_new = np.moveaxis(array, axis, destination) # Collapse the dimensions being operated on into a single dimension # so that we can then use axis=0 with the bottleneck functions array_new = array_new.reshape((-1,) + array_new.shape[naxis:]) return array_new def nanmean(array, axis=None): """ A nanmean function that uses bottleneck if available. """ if HAS_BOTTLENECK: if isinstance(axis, tuple): array = move_tuple_axes_first(array, axis=axis) axis = 0 if isinstance(array, u.Quantity): return array.__array_wrap__(bn.nanmean(array, axis=axis)) else: return bn.nanmean(array, axis=axis) else: return np.nanmean(array, axis=axis) def nanmedian(array, axis=None): """ A nanmedian function that uses bottleneck if available. """ if HAS_BOTTLENECK: if isinstance(axis, tuple): array = move_tuple_axes_first(array, axis=axis) axis = 0 if isinstance(array, u.Quantity): return array.__array_wrap__(bn.nanmedian(array, axis=axis)) else: return bn.nanmedian(array, axis=axis) else: return np.nanmedian(array, axis=axis) def nanstd(array, axis=None, ddof=0): """ A nanstd function that uses bottleneck if available. """ if HAS_BOTTLENECK: if isinstance(axis, tuple): array = move_tuple_axes_first(array, axis=axis) axis = 0 if isinstance(array, u.Quantity): return array.__array_wrap__(bn.nanstd(array, axis=axis, ddof=ddof)) else: return bn.nanstd(array, axis=axis, ddof=ddof) else: return np.nanstd(array, axis=axis, ddof=ddof) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/background_2d.py0000644000214200020070000006747200000000000021446 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines classes to estimate the 2D background and background RMS in an image. """ import warnings from astropy.nddata import NDData from astropy.stats import SigmaClip import astropy.units as u from astropy.utils import lazyproperty from astropy.utils.decorators import deprecated from astropy.utils.exceptions import AstropyUserWarning import numpy as np from numpy.lib.index_tricks import index_exp from .core import SExtractorBackground, StdBackgroundRMS from .interpolators import BkgZoomInterpolator from ._utils import nanmedian from ..utils import ShepardIDWInterpolator __all__ = ['Background2D'] __doctest_requires__ = {('Background2D'): ['scipy']} class Background2D: """ Class to estimate a 2D background and background RMS noise in an image. The background is estimated using (sigma-clipped) statistics in each box of a grid that covers the input ``data`` to create a low-resolution, and possibly irregularly-gridded, background map. The final background map is calculated by interpolating the low-resolution background map. Invalid data values (i.e., NaN or inf) are automatically masked. Parameters ---------- data : array_like or `~astropy.nddata.NDData` The 2D array from which to estimate the background and/or background RMS map. box_size : int or array_like (int) The box size along each axis. If ``box_size`` is a scalar then a square box of size ``box_size`` will be used. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. For best results, the box shape should be chosen such that the ``data`` are covered by an integer number of boxes in both dimensions. When this is not the case, see the ``edge_method`` keyword for more options. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from calculations. ``mask`` is intended to mask sources or bad pixels. Use ``coverage_mask`` to mask blank areas of an image. ``mask`` and ``coverage_mask`` differ only in that ``coverage_mask`` is applied to the output background and background RMS maps (see ``fill_value``). coverage_mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. ``coverage_mask`` should be `True` where there is no coverage (i.e., no data) for a given pixel (e.g., blank areas in a mosaic image). It should not be used for bad pixels (in that case use ``mask`` instead). ``mask`` and ``coverage_mask`` differ only in that ``coverage_mask`` is applied to the output background and background RMS maps (see ``fill_value``). fill_value : float, optional The value used to fill the output background and background RMS maps where the input ``coverage_mask`` is `True`. exclude_percentile : float in the range of [0, 100], optional The percentage of masked pixels in a box, used as a threshold for determining if the box is excluded. If a box has more than ``exclude_percentile`` percent of its pixels masked then it will be excluded from the low-resolution map. Masked pixels include those from the input ``mask`` and ``coverage_mask``, those resulting from the data padding (i.e., if ``edge_method='pad'``), and those resulting from sigma clipping (if ``sigma_clip`` is used). Setting ``exclude_percentile=0`` will exclude boxes that have any masked pixels. Note that completely masked boxes are always excluded. For best results, ``exclude_percentile`` should be kept as low as possible (as long as there are sufficient pixels for reasonable statistical estimates). The default is 10.0. filter_size : int or array_like (int), optional The window size of the 2D median filter to apply to the low-resolution background map. If ``filter_size`` is a scalar then a square box of size ``filter_size`` will be used. If ``filter_size`` has two elements, they should be in ``(ny, nx)`` order. A filter size of ``1`` (or ``(1, 1)``) means no filtering. filter_threshold : int, optional The threshold value for used for selective median filtering of the low-resolution 2D background map. The median filter will be applied to only the background boxes with values larger than ``filter_threshold``. Set to `None` to filter all boxes (default). edge_method : {'pad', 'crop'}, optional The method used to determine how to handle the case where the image size is not an integer multiple of the ``box_size`` in either dimension. Both options will resize the image to give an exact multiple of ``box_size`` in both dimensions. * ``'pad'``: pad the image along the top and/or right edges. This is the default and recommended method. Ideally, the ``box_size`` should be chosen such that an integer number of boxes is only slightly larger than the ``data`` size to minimize the amount of padding. * ``'crop'``: crop the image along the top and/or right edges. This method should be used sparingly. Best results will occur when ``box_size`` is chosen such that an integer number of boxes is only slightly smaller than the ``data`` size to minimize the amount of cropping. sigma_clip : `astropy.stats.SigmaClip` instance, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=10``. bkg_estimator : callable, optional A callable object (a function or e.g., an instance of any `~photutils.background.BackgroundBase` subclass) used to estimate the background in each of the boxes. The callable object must take in a 2D `~numpy.ndarray` or `~numpy.ma.MaskedArray` and have an ``axis`` keyword. Internally, the background will be calculated along ``axis=1`` and in this case the callable object must return a 1D `~numpy.ndarray`, where np.nan values are used for masked pixels. If ``bkg_estimator`` includes sigma clipping, it will be ignored (use the ``sigma_clip`` keyword here to define sigma clipping). The default is an instance of `~photutils.background.SExtractorBackground`. bkgrms_estimator : callable, optional A callable object (a function or e.g., an instance of any `~photutils.background.BackgroundRMSBase` subclass) used to estimate the background RMS in each of the boxes. The callable object must take in a 2D `~numpy.ndarray` or `~numpy.ma.MaskedArray` and have an ``axis`` keyword. Internally, the background RMS will be calculated along ``axis=1`` and in this case the callable object must return a 1D `~numpy.ndarray`, where np.nan values are used for masked pixels. If ``bkgrms_estimator`` includes sigma clipping, it will be ignored (use the ``sigma_clip`` keyword here to define sigma clipping). The default is an instance of `~photutils.background.StdBackgroundRMS`. interpolator : callable, optional A callable object (a function or object) used to interpolate the low-resolution background or background RMS image to the full-size background or background RMS maps. The default is an instance of `BkgZoomInterpolator`, which uses the `scipy.ndimage.zoom` function. Notes ----- Better performance will generally be obtained if you have the `bottleneck`_ package installed. If there is only one background box element (i.e., ``box_size`` is the same size as (or larger than) the ``data``), then the background map will simply be a constant image. .. _bottleneck: https://github.com/pydata/bottleneck """ def __init__(self, data, box_size, *, mask=None, coverage_mask=None, fill_value=0.0, exclude_percentile=10.0, filter_size=(3, 3), filter_threshold=None, edge_method='pad', sigma_clip=SigmaClip(sigma=3.0, maxiters=10), bkg_estimator=SExtractorBackground(sigma_clip=None), bkgrms_estimator=StdBackgroundRMS(sigma_clip=None), interpolator=BkgZoomInterpolator()): if isinstance(data, (u.Quantity, NDData)): # includes CCDData self.unit = data.unit data = data.data else: self.unit = None self.data = self._validate_array(data, 'data', shape=False) self.mask = self._validate_array(mask, 'mask') self.coverage_mask = self._validate_array(coverage_mask, 'coverage_mask') self.total_mask = self._combine_masks() box_size = self._process_size_input(box_size) # box_size cannot be larger than the data array size self.box_size = np.array((min(box_size[0], data.shape[0]), min(box_size[1], data.shape[1]))) self.fill_value = fill_value if exclude_percentile < 0 or exclude_percentile > 100: raise ValueError('exclude_percentile must be between 0 and 100 ' '(inclusive).') self.exclude_percentile = exclude_percentile self.filter_size = self._process_size_input(filter_size) self.filter_threshold = filter_threshold self.edge_method = edge_method self.sigma_clip = sigma_clip bkg_estimator.sigma_clip = None bkgrms_estimator.sigma_clip = None self.bkg_estimator = bkg_estimator self.bkgrms_estimator = bkgrms_estimator self.interpolator = interpolator self.nboxes = None self.box_npixels = None self.nboxes_tot = None self._box_data = None self._box_idx = None self._mesh_idx = None self._bkg_stats = None self._bkgrms_stats = None self._prepare_box_data() @staticmethod def _process_size_input(array): array = np.atleast_1d(array).astype(int) if len(array) == 1: array = np.repeat(array, 2) if len(array) != 2: raise ValueError('box_size and filter_size inputs must have only ' '1 or 2 elements') return array def _validate_array(self, array, name, shape=True): if name in ('mask', 'coverage_mask') and array is np.ma.nomask: array = None if array is not None: array = np.asanyarray(array) if array.ndim != 2: raise ValueError(f'{name} must be a 2D array.') if shape and array.shape != self.data.shape: raise ValueError(f'data and {name} must have the same shape.') return array def _combine_masks(self): if self.mask is None and self.coverage_mask is None: return None if self.mask is None: return self.coverage_mask elif self.coverage_mask is None: return self.mask else: return np.logical_or(self.mask, self.coverage_mask) def _prepare_data(self): """ Prepare the data. This method: * converts the data to float dtype (and makes a copy) * automatically masks non-finite values * replaces all masked values with NaN * converts MaskedArray to ndarray using NaN as masked values """ # float array type is needed to insert nans into the array self.data = self.data.astype(float) # makes a copy # include non-finite values in the total mask bad_mask = ~np.isfinite(self.data) if np.any(bad_mask): if self.total_mask is None: self.total_mask = bad_mask else: self.total_mask |= bad_mask warnings.warn('Input data contains invalid values (NaNs or ' 'infs), which were automatically masked.', AstropyUserWarning) # replace all masked values with NaN if self.total_mask is not None: self.data[self.total_mask] = np.nan # convert MaskedArray to ndarray using np.nan as masked values if isinstance(self.data, np.ma.MaskedArray): self.data = self.data.filled(np.nan) def _reshape_data(self): """ First, pad or crop the 2D data array so that there are an integer number of boxes in both dimensions. Then reshape it into a different 2D array where each row represents the data in a single box. """ self.nboxes = self.data.shape // self.box_size extra_size = self.data.shape % self.box_size if np.sum(extra_size) != 0: # pad or crop the data if self.edge_method == 'pad': pad_size = self.box_size - extra_size pad_width = ((0, pad_size[0]), (0, pad_size[1])) data = np.pad(self.data, pad_width, mode='constant', constant_values=np.nan) self.nboxes = data.shape // self.box_size elif self.edge_method == 'crop': crop_size = self.nboxes * self.box_size crop_slc = index_exp[0:crop_size[0], 0:crop_size[1]] data = self.data[crop_slc] else: raise ValueError('edge_method must be "pad" or "crop"') else: data = self.data self.box_npixels = np.prod(self.box_size) self.nboxes_tot = np.prod(self.nboxes) # a reshaped 2D array with box data along the x axis self._box_data = np.swapaxes(data.reshape( self.nboxes[0], self.box_size[0], self.nboxes[1], self.box_size[1]), 1, 2).reshape(self.nboxes_tot, self.box_npixels) @lazyproperty def _box_npixels_threshold(self): # * boxes that are completely masked are always excluded # * boxes that contain more than ``exclude_percentile`` percent # masked pixels are also excluded: # - for exclude_percentile=0, only boxes where nmasked=0 will # be included # - for exclude_percentile=100, all boxes will be included # *unless* they are completely masked threshold = self.exclude_percentile / 100. * self.box_npixels # always exclude completely masked boxes if self.exclude_percentile == 100: threshold -= 1 return threshold def _get_box_indices(self): """ Define the x and y indices of the boxes that will be used to compute background statistics. The box array (self._box_data) is a 2D array where each row represents the data in a single box. The ``exclude_percentile`` keyword determines which boxes are not used for the background interpolation. """ # the number of NaN pixels in each box nmasked = np.count_nonzero(np.isnan(self._box_data), axis=1) # define indices of good (included) boxes box_idx = np.where(nmasked <= self._box_npixels_threshold)[0] if box_idx.size == 0: raise ValueError('All boxes contain > {0} ({1} percent per ' 'box) masked pixels (or all are completely ' 'masked). Please check your data or increase ' '"exclude_percentile" to allow more boxes to ' 'be included.' .format(self._box_npixels_threshold, self.exclude_percentile)) return box_idx def _select_initial_boxes(self): # perform a first cut on rejecting boxes self._box_idx = self._get_box_indices() if self._box_idx.size != self._box_data.shape[0]: self._box_data = self._box_data[self._box_idx, :] def _sigmaclip_boxes(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=AstropyUserWarning) if self.sigma_clip is not None: self._box_data = self.sigma_clip(self._box_data, axis=1, masked=False) # perform box rejection on sigma-clipped data (i.e., for any # newly-masked pixels) idx = self._get_box_indices() self._box_idx = self._box_idx[idx] if self._box_idx.size != self._box_data.shape[0]: self._box_data = self._box_data[idx, :] # the indices of the good pixels in the low-resolution 2D mesh self._mesh_idx = np.unravel_index(self._box_idx, self.nboxes) def _prepare_box_data(self): """ Prepare the box data by reshaping, masking (with NaNs), and sigma clipping the data. """ self._prepare_data() self._reshape_data() self._select_initial_boxes() self._sigmaclip_boxes() def _make_2d_array(self, data): """ Convert a 1D array of values to a 2D array given the indices in ``self._mesh_idx``. Parameters ---------- data : 1D `~numpy.ndarray` A 1D array of values. Returns ------- result : 2D `~numpy.ndarray` A 2D array. Pixels not defined in ``mesh_idx`` are assigned a value of np.nan. """ data2d = np.full(self.nboxes, np.nan) data2d[self._mesh_idx] = data return data2d def _interpolate_meshes(self, data, n_neighbors=10, eps=0.0, power=1.0, reg=0.0): """ Use IDW interpolation to fill in any masked pixels in the low-resolution 2D mesh background and background RMS images. This is required to use a regular-grid interpolator to expand the low-resolution image to the full size image. Parameters ---------- data : 1D `~numpy.ndarray` A 1D array of mesh values. n_neighbors : int, optional The maximum number of nearest neighbors to use during the interpolation. eps : float, optional Set to use approximate nearest neighbors; the kth neighbor is guaranteed to be no further than (1 + ``eps``) times the distance to the real *k*-th nearest neighbor. See `scipy.spatial.cKDTree.query` for further information. power : float, optional The power of the inverse distance used for the interpolation weights. See the Notes section for more details. reg : float, optional The regularization parameter. It may be used to control the smoothness of the interpolator. See the Notes section for more details. Returns ------- result : 2D `~numpy.ndarray` A 2D array of the mesh values where masked pixels have been filled by IDW interpolation. """ yx = np.column_stack(self._mesh_idx) interp_func = ShepardIDWInterpolator(yx, data) yi, xi = np.mgrid[0:self.nboxes[0], 0:self.nboxes[1]] yx_indices = np.column_stack((yi.ravel(), xi.ravel())) img1d = interp_func(yx_indices, n_neighbors=n_neighbors, power=power, eps=eps, reg=reg) return img1d.reshape(self.nboxes) def _make_mesh_image(self, box_stats): """ Calculate the filtered low-resolution background or background RMS "mesh" image from the 1D box statistics data. """ # make the unfiltered 2D mesh arrays (these are not masked) if box_stats.size == self.nboxes_tot: # no masked boxes mesh_img = self._make_2d_array(box_stats) else: # interpolate masked boxes mesh_img = self._interpolate_meshes(box_stats) return mesh_img def _selective_filter(self, data): """ Filter only pixels above ``filter_threshold`` in the background mesh. The same pixels are filtered in both the background and background RMS meshes. Parameters ---------- data : 2D `~numpy.ndarray` A 2D array of mesh values. Returns ------- filtered_data : 2D `~numpy.ndarray` The filtered 2D array of mesh values. """ data_out = np.copy(data) yx_indices = np.column_stack( np.nonzero(self._unfiltered_background_mesh > self.filter_threshold)) for i, j in yx_indices: yfs, xfs = self.filter_size hyfs, hxfs = yfs // 2, xfs // 2 yidx0 = max(i - hyfs, 0) yidx1 = min(i - hyfs + yfs, data.shape[0]) xidx0 = max(j - hxfs, 0) xidx1 = min(j - hxfs + xfs, data.shape[1]) data_out[i, j] = np.median(data[yidx0:yidx1, xidx0:xidx1]) return data_out def _filter_meshes(self, data): """ Apply a 2D median filter to a low-resolution 2D mesh image. """ if np.array_equal(self.filter_size, [1, 1]): return data if self.filter_threshold is None: # filter the entire array from scipy.ndimage import generic_filter filtdata = generic_filter(data, nanmedian, size=self.filter_size, mode='constant', cval=np.nan) else: # selectively filter the array filtdata = self._selective_filter(data) return filtdata @lazyproperty def _unfiltered_background_mesh(self): """ The unfiltered low-resolution background image. This array is needed separately from background_mesh to compute which pixels are to be selectively filtered (if ``filter_threshold`` is input). """ self._bkg_stats = self.bkg_estimator(self._box_data, axis=1) return self._make_mesh_image(self._bkg_stats) @lazyproperty def background_mesh(self): """ The low-resolution background image. This image is equivalent to the low-resolution "MINIBACKGROUND" background map in SourceExtractor. """ return self._filter_meshes(self._unfiltered_background_mesh) @lazyproperty def background_rms_mesh(self): """ The low-resolution background RMS image. This image is equivalent to the low-resolution "MINIBACKGROUND" background rms map in SourceExtractor. """ self._bkgrms_stats = self.bkgrms_estimator(self._box_data, axis=1) mesh_img = self._make_mesh_image(self._bkgrms_stats) return self._filter_meshes(mesh_img) @lazyproperty def background_mesh_masked(self): """ The background 2D (masked) array mesh prior to any interpolation. The array has NaN values where meshes were excluded. """ data = np.full(self.background_mesh.shape, np.nan) data[self._mesh_idx] = self.background_mesh[self._mesh_idx] return data @lazyproperty def background_rms_mesh_masked(self): """ The background RMS 2D (masked) array mesh prior to any interpolation. The array has NaN values where meshes were excluded. """ data = np.full(self.background_rms_mesh.shape, np.nan) data[self._mesh_idx] = self.background_rms_mesh[self._mesh_idx] return data @lazyproperty @deprecated('1.2', alternative='background_mesh_masked') def background_mesh_ma(self): return self.background_mesh_masked # pragma: no cover @lazyproperty @deprecated('1.2', alternative='background_rms_mesh_masked') def background_rms_mesh_ma(self): return self.background_rms_mesh_masked # pragma: no cover @lazyproperty def _mesh_yxpos(self): box_cen = (self.box_size - 1) / 2. return (self._mesh_idx * self.box_size[:, None]) + box_cen[:, None] @lazyproperty def _mesh_xypos(self): return np.flipud(self._mesh_yxpos) @lazyproperty def mesh_nmasked(self): """ A 2D array of the number of masked pixels in each mesh. NaN values indicate where meshes were excluded. """ return self._make_2d_array( np.count_nonzero(np.isnan(self._box_data), axis=1)) @lazyproperty def background_median(self): """ The median value of the 2D low-resolution background map. This is equivalent to the value SourceExtractor prints to stdout (i.e., "(M+D) Background: "). """ _median = np.median(self.background_mesh) if self.unit is not None: _median <<= self.unit return _median @lazyproperty def background_rms_median(self): """ The median value of the low-resolution background RMS map. This is equivalent to the value SourceExtractor prints to stdout (i.e., "(M+D) RMS: "). """ _rms_median = np.median(self.background_rms_mesh) if self.unit is not None: _rms_median <<= self.unit return _rms_median @lazyproperty def background(self): """A 2D `~numpy.ndarray` containing the background image.""" bkg = self.interpolator(self.background_mesh, self) if self.coverage_mask is not None: bkg[self.coverage_mask] = self.fill_value if self.unit is not None: bkg <<= self.unit return bkg @lazyproperty def background_rms(self): """A 2D `~numpy.ndarray` containing the background RMS image.""" bkg_rms = self.interpolator(self.background_rms_mesh, self) if self.coverage_mask is not None: bkg_rms[self.coverage_mask] = self.fill_value if self.unit is not None: bkg_rms <<= self.unit return bkg_rms def plot_meshes(self, *, axes=None, marker='+', markersize=None, color='blue', outlines=False, **kwargs): """ Plot the low-resolution mesh boxes on a matplotlib Axes instance. Parameters ---------- axes : `matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. marker : str, optional The marker to use to mark the center of the boxes. Default is '+'. markersize: float, optional The marker size in points**2. The default is ``matplotlib.rcParams['lines.markersize'] ** 2``. If set to 0, then the box center markers will not be plotted. color : str, optional The color for the markers and the box outlines. Default is 'blue'. outlines : bool, optional Whether or not to plot the box outlines in addition to the box centers. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Used only if ``outlines`` is True. """ import matplotlib.pyplot as plt kwargs['color'] = color if axes is None: axes = plt.gca() axes.scatter(*self._mesh_xypos, s=markersize, marker=marker, color=color) if outlines: from ..aperture import RectangularAperture xypos = np.column_stack(self._mesh_xypos) apers = RectangularAperture(xypos, self.box_size[1], self.box_size[0], 0.) apers.plot(axes=axes, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/core.py0000644000214200020070000005735600000000000017672 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines classes to estimate the background and background RMS in an array of any dimension. """ import abc import warnings from astropy.stats import (biweight_location, biweight_scale, mad_std, SigmaClip) import astropy.units as u import numpy as np from ._utils import nanmean, nanmedian, nanstd SIGMA_CLIP = SigmaClip(sigma=3.0, maxiters=10) __all__ = ['BackgroundBase', 'BackgroundRMSBase', 'MeanBackground', 'MedianBackground', 'ModeEstimatorBackground', 'MMMBackground', 'SExtractorBackground', 'BiweightLocationBackground', 'StdBackgroundRMS', 'MADStdBackgroundRMS', 'BiweightScaleBackgroundRMS'] class BackgroundBase(metaclass=abc.ABCMeta): """ Base class for classes that estimate scalar background values. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. """ def __init__(self, sigma_clip=SIGMA_CLIP): if not isinstance(sigma_clip, SigmaClip) and sigma_clip is not None: raise TypeError('sigma_clip must be an astropy SigmaClip ' 'instance or None') self.sigma_clip = sigma_clip def __repr__(self): return (f'<{self.__class__.__name__}' f'(sigma_clip={repr(self.sigma_clip)})>') def __call__(self, data, axis=None, masked=False): return self.calc_background(data, axis=axis, masked=masked) @abc.abstractmethod def calc_background(self, data, axis=None, masked=False): """ Calculate the background value. Parameters ---------- data : array_like or `~numpy.ma.MaskedArray` The array for which to calculate the background value. axis : int or `None`, optional The array axis along which the background is calculated. If `None`, then the entire array is used. masked : bool, optional If `True`, then a `~numpy.ma.MaskedArray` is returned. If `False`, then a `~numpy.ndarray` is returned, where masked values have a value of NaN. The default is `False`. Returns ------- result : float, `~numpy.ndarray`, or `~numpy.ma.MaskedArray` The calculated background value. If ``masked`` is `False`, then a `~numpy.ndarray` is returned, otherwise a `~numpy.ma.MaskedArray` is returned. A scalar result is always returned as a float. """ raise NotImplementedError() # pragma: no cover class BackgroundRMSBase(metaclass=abc.ABCMeta): """ Base class for classes that estimate scalar background RMS values. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. """ def __init__(self, sigma_clip=SIGMA_CLIP): if not isinstance(sigma_clip, SigmaClip) and sigma_clip is not None: raise TypeError('sigma_clip must be an astropy SigmaClip ' 'instance or None') self.sigma_clip = sigma_clip def __repr__(self): return (f'<{self.__class__.__name__}' f'(sigma_clip={repr(self.sigma_clip)})>') def __call__(self, data, axis=None, masked=False): return self.calc_background_rms(data, axis=axis, masked=masked) @abc.abstractmethod def calc_background_rms(self, data, axis=None, masked=False): """ Calculate the background RMS value. Parameters ---------- data : array_like or `~numpy.ma.MaskedArray` The array for which to calculate the background RMS value. axis : int or `None`, optional The array axis along which the background RMS is calculated. If `None`, then the entire array is used. masked : bool, optional If `True`, then a `~numpy.ma.MaskedArray` is returned. If `False`, then a `~numpy.ndarray` is returned, where masked values have a value of NaN. The default is `False`. Returns ------- result : float, `~numpy.ndarray`, or `~numpy.ma.MaskedArray` The calculated background RMS value. If ``masked`` is `False`, then a `~numpy.ndarray` is returned, otherwise a `~numpy.ma.MaskedArray` is returned. A scalar result is always returned as a float. """ raise NotImplementedError() # pragma: no cover class MeanBackground(BackgroundBase): """ Class to calculate the background in an array as the (sigma-clipped) mean. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import MeanBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = MeanBackground(sigma_clip) The background value can be calculated by using the `calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def calc_background(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = nanmean(data, axis=axis) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class MedianBackground(BackgroundBase): """ Class to calculate the background in an array as the (sigma-clipped) median. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import MedianBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = MedianBackground(sigma_clip) The background value can be calculated by using the `calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def calc_background(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = nanmedian(data, axis=axis) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class ModeEstimatorBackground(BackgroundBase): """ Class to calculate the background in an array using a mode estimator of the form ``(median_factor * median) - (mean_factor * mean)``. Parameters ---------- median_factor : float, optional The multiplicative factor for the data median. Defaults to 3. mean_factor : float, optional The multiplicative factor for the data mean. Defaults to 2. sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import ModeEstimatorBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = ModeEstimatorBackground(median_factor=3.0, mean_factor=2.0, ... sigma_clip=sigma_clip) The background value can be calculated by using the `calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def __init__(self, median_factor=3.0, mean_factor=2.0, **kwargs): super().__init__(**kwargs) self.median_factor = median_factor self.mean_factor = mean_factor def calc_background(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = ((self.median_factor * nanmedian(data, axis=axis)) - (self.mean_factor * nanmean(data, axis=axis))) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class MMMBackground(ModeEstimatorBackground): """ Class to calculate the background in an array using the DAOPHOT MMM algorithm. The background is calculated using a mode estimator of the form ``(3 * median) - (2 * mean)``. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import MMMBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = MMMBackground(sigma_clip=sigma_clip) The background value can be calculated by using the `~photutils.background.core.ModeEstimatorBackground.calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def __init__(self, **kwargs): kwargs['median_factor'] = 3.0 kwargs['mean_factor'] = 2.0 super().__init__(**kwargs) class SExtractorBackground(BackgroundBase): """ Class to calculate the background in an array using the Source Extractor algorithm. The background is calculated using a mode estimator of the form ``(2.5 * median) - (1.5 * mean)``. If ``(mean - median) / std > 0.3`` then the median is used instead. .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import SExtractorBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = SExtractorBackground(sigma_clip) The background value can be calculated by using the `calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def calc_background(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) _median = np.atleast_1d(nanmedian(data, axis=axis)) _mean = np.atleast_1d(nanmean(data, axis=axis)) _std = np.atleast_1d(nanstd(data, axis=axis)) bkg = np.atleast_1d((2.5 * _median) - (1.5 * _mean)) bkg = np.where(_std == 0, _mean, bkg) idx = np.where(_std != 0) condition = (np.abs(_mean[idx] - _median[idx]) / _std[idx]) < 0.3 bkg[idx] = np.where(condition, bkg[idx], _median[idx]) if bkg.size == 1: bkg = bkg[0] result = bkg if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class BiweightLocationBackground(BackgroundBase): """ Class to calculate the background in an array using the biweight location. Parameters ---------- c : float, optional Tuning constant for the biweight estimator. Default value is 6.0. M : float, optional Initial guess for the biweight location. Default value is `None`. sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import BiweightLocationBackground >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkg = BiweightLocationBackground(sigma_clip=sigma_clip) The background value can be calculated by using the `calc_background` method, e.g.: >>> bkg_value = bkg.calc_background(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 Alternatively, the background value can be calculated by calling the class instance as a function, e.g.: >>> bkg_value = bkg(data) >>> print(bkg_value) # doctest: +FLOAT_CMP 49.5 """ def __init__(self, c=6, M=None, **kwargs): super().__init__(**kwargs) self.c = c self.M = M def calc_background(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = biweight_location(data, c=self.c, M=self.M, axis=axis, ignore_nan=True) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class StdBackgroundRMS(BackgroundRMSBase): """ Class to calculate the background RMS in an array as the (sigma-clipped) standard deviation. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import StdBackgroundRMS >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkgrms = StdBackgroundRMS(sigma_clip) The background RMS value can be calculated by using the `calc_background_rms` method, e.g.: >>> bkgrms_value = bkgrms.calc_background_rms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 28.86607004772212 Alternatively, the background RMS value can be calculated by calling the class instance as a function, e.g.: >>> bkgrms_value = bkgrms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 28.86607004772212 """ def calc_background_rms(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = nanstd(data, axis=axis) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class MADStdBackgroundRMS(BackgroundRMSBase): r""" Class to calculate the background RMS in an array as using the `median absolute deviation (MAD) `_. The standard deviation estimator is given by: .. math:: \sigma \approx \frac{{\textrm{{MAD}}}}{{\Phi^{{-1}}(3/4)}} \approx 1.4826 \ \textrm{{MAD}} where :math:`\Phi^{{-1}}(P)` is the normal inverse cumulative distribution function evaluated at probability :math:`P = 3/4`. Parameters ---------- sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import MADStdBackgroundRMS >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkgrms = MADStdBackgroundRMS(sigma_clip) The background RMS value can be calculated by using the `calc_background_rms` method, e.g.: >>> bkgrms_value = bkgrms.calc_background_rms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 37.06505546264005 Alternatively, the background RMS value can be calculated by calling the class instance as a function, e.g.: >>> bkgrms_value = bkgrms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 37.06505546264005 """ def calc_background_rms(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) result = mad_std(data, axis=axis, ignore_nan=True) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result class BiweightScaleBackgroundRMS(BackgroundRMSBase): """ Class to calculate the background RMS in an array as the (sigma-clipped) biweight scale. Parameters ---------- c : float, optional Tuning constant for the biweight estimator. Default value is 9.0. M : float, optional Initial guess for the biweight location. Default value is `None`. sigma_clip : `astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object that defines the sigma clipping parameters. If `None` then no sigma clipping will be performed. The default is to perform sigma clipping with ``sigma=3.0`` and ``maxiters=5``. Examples -------- >>> from astropy.stats import SigmaClip >>> from photutils.background import BiweightScaleBackgroundRMS >>> data = np.arange(100) >>> sigma_clip = SigmaClip(sigma=3.0) >>> bkgrms = BiweightScaleBackgroundRMS(sigma_clip=sigma_clip) The background RMS value can be calculated by using the `calc_background_rms` method, e.g.: >>> bkgrms_value = bkgrms.calc_background_rms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 30.09433848589339 Alternatively, the background RMS value can be calculated by calling the class instance as a function, e.g.: >>> bkgrms_value = bkgrms(data) >>> print(bkgrms_value) # doctest: +FLOAT_CMP 30.09433848589339 """ def __init__(self, c=9.0, M=None, **kwargs): super().__init__(**kwargs) self.c = c self.M = M def calc_background_rms(self, data, axis=None, masked=False): if self.sigma_clip is not None: data = self.sigma_clip(data, axis=axis, masked=False) else: # convert to ndarray with masked values as np.nan if isinstance(data, np.ma.MaskedArray): data = data.filled(np.nan) # ignore RuntimeWarning where axis is all NaN with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) # this is need to fix a bug in astropy 4.3.1 where # biweight_scale can drop the unit in some cases if isinstance(data, u.Quantity): result = data.__array_wrap__( biweight_scale(data, c=self.c, M=self.M, axis=axis, ignore_nan=True)) else: result = biweight_scale(data, c=self.c, M=self.M, axis=axis, ignore_nan=True) if masked and isinstance(result, np.ndarray): result = np.ma.masked_where(np.isnan(result), result) return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/interpolators.py0000644000214200020070000001312200000000000021626 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module defines interpolator classes for Background2D. """ import numpy as np from ..utils import ShepardIDWInterpolator __all__ = ['BkgZoomInterpolator', 'BkgIDWInterpolator'] __doctest_requires__ = {('BkgZoomInterpolator'): ['scipy']} class BkgZoomInterpolator: """ This class generates full-sized background and background RMS images from lower-resolution mesh images using the `~scipy.ndimage.zoom` (spline) interpolator. This class must be used in concert with the `Background2D` class. Parameters ---------- order : int, optional The order of the spline interpolation used to resize the low-resolution background and background RMS mesh images. The value must be an integer in the range 0-5. The default is 3 (bicubic interpolation). mode : {'reflect', 'constant', 'nearest', 'wrap'}, optional Points outside the boundaries of the input are filled according to the given mode. Default is 'reflect'. cval : float, optional The value used for points outside the boundaries of the input if ``mode='constant'``. Default is 0.0 grid_mode : bool, optional If `True` (default), the samples are considered as the centers of regularly-spaced grid elements. If `False`, the samples are treated as isolated points. For zooming 2D images, this keyword should be set to `True`, which makes zoom's behavior consistent with `scipy.ndimage.map_coordinates` and `skimage.transform.resize`. The `False` option is provided only for backwards-compatibility. """ def __init__(self, *, order=3, mode='reflect', cval=0.0, grid_mode=True): self.order = order self.mode = mode self.cval = cval self.grid_mode = grid_mode def __call__(self, mesh, bkg2d_obj): """ Resize the 2D mesh array. Parameters ---------- mesh : 2D `~numpy.ndarray` The low-resolution 2D mesh array. bkg2d_obj : `Background2D` object The `Background2D` object that prepared the ``mesh`` array. Returns ------- result : 2D `~numpy.ndarray` The resized background or background RMS image. """ mesh = np.asanyarray(mesh) if np.ptp(mesh) == 0: return np.zeros_like(bkg2d_obj.data) + np.min(mesh) from scipy.ndimage import zoom if bkg2d_obj.edge_method == 'pad': # The mesh is first resized to the larger padded-data size # (i.e., zoom_factor should be an integer) and then cropped # back to the final data size. zoom_factor = bkg2d_obj.box_size result = zoom(mesh, zoom_factor, order=self.order, mode=self.mode, cval=self.cval, grid_mode=self.grid_mode) return result[0:bkg2d_obj.data.shape[0], 0:bkg2d_obj.data.shape[1]] else: # The mesh is resized directly to the final data size. zoom_factor = np.array(bkg2d_obj.data.shape) / mesh.shape return zoom(mesh, zoom_factor, order=self.order, mode=self.mode, cval=self.cval) class BkgIDWInterpolator: """ This class generates full-sized background and background RMS images from lower-resolution mesh images using inverse-distance weighting (IDW) interpolation (`~photutils.utils.ShepardIDWInterpolator`). This class must be used in concert with the `Background2D` class. Parameters ---------- leafsize : float, optional The number of points at which the k-d tree algorithm switches over to brute-force. ``leafsize`` must be positive. See `scipy.spatial.cKDTree` for further information. n_neighbors : int, optional The maximum number of nearest neighbors to use during the interpolation. power : float, optional The power of the inverse distance used for the interpolation weights. reg : float, optional The regularization parameter. It may be used to control the smoothness of the interpolator. """ def __init__(self, *, leafsize=10, n_neighbors=10, power=1.0, reg=0.0): self.leafsize = leafsize self.n_neighbors = n_neighbors self.power = power self.reg = reg def __call__(self, mesh, bkg2d_obj): """ Resize the 2D mesh array. Parameters ---------- mesh : 2D `~numpy.ndarray` The low-resolution 2D mesh array. bkg2d_obj : `Background2D` object The `Background2D` object that prepared the ``mesh`` array. Returns ------- result : 2D `~numpy.ndarray` The resized background or background RMS image. """ mesh = np.asanyarray(mesh) if np.ptp(mesh) == 0: return np.zeros_like(bkg2d_obj.data) + np.min(mesh) yxpos = np.column_stack(bkg2d_obj._mesh_yxpos) mesh1d = mesh[bkg2d_obj._mesh_idx] interp_func = ShepardIDWInterpolator(yxpos, mesh1d, leafsize=self.leafsize) # the position coordinates used when calling the interpolator ny, nx = bkg2d_obj.data.shape yi, xi = np.mgrid[0:ny, 0:nx] yx_indices = np.column_stack((yi.ravel(), xi.ravel())) data = interp_func(yx_indices, n_neighbors=self.n_neighbors, power=self.power, reg=self.reg) return data.reshape(bkg2d_obj.data.shape) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9851272 photutils-1.3.0/photutils/background/tests/0000755000214200020070000000000000000000000017512 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/background/tests/__init__.py0000644000214200020070000000000000000000000021611 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/tests/test_background_2d.py0000644000214200020070000003270700000000000023640 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the background_2d module. """ import itertools from astropy.nddata import NDData, CCDData from astropy.tests.helper import catch_warnings import astropy.units as u from astropy.utils.exceptions import AstropyUserWarning import numpy as np from numpy.testing import assert_allclose, assert_equal import pytest from ..core import MeanBackground from ..background_2d import Background2D from ..interpolators import BkgZoomInterpolator, BkgIDWInterpolator from ...utils._optional_deps import HAS_MATPLOTLIB, HAS_SCIPY # noqa DATA = np.ones((100, 100)) BKG_RMS = np.zeros((100, 100)) BKG_MESH = np.ones((4, 4)) BKG_RMS_MESH = np.zeros((4, 4)) PADBKG_MESH = np.ones((5, 5)) PADBKG_RMS_MESH = np.zeros((5, 5)) FILTER_SIZES = [(1, 1), (3, 3)] INTERPOLATORS = [BkgZoomInterpolator(), BkgIDWInterpolator()] DATA1 = DATA << u.ct DATA2 = NDData(DATA, unit=None) DATA3 = NDData(DATA, unit=u.ct) DATA4 = CCDData(DATA, unit=u.ct) @pytest.mark.skipif('not HAS_SCIPY') class TestBackground2D: @pytest.mark.parametrize(('filter_size', 'interpolator'), list(itertools.product(FILTER_SIZES, INTERPOLATORS))) def test_background(self, filter_size, interpolator): bkg = Background2D(DATA, (25, 25), filter_size=filter_size, interpolator=interpolator) assert_allclose(bkg.background, DATA) assert_allclose(bkg.background_rms, BKG_RMS) assert_allclose(bkg.background_mesh, BKG_MESH) assert_allclose(bkg.background_rms_mesh, BKG_RMS_MESH) assert bkg.background_median == 1.0 assert bkg.background_rms_median == 0.0 @pytest.mark.parametrize('data', [DATA1, DATA3, DATA4]) def test_background_nddata(self, data): """ Test with NDData and CCDData, and also test units. """ bkg = Background2D(data, (25, 25), filter_size=3) assert isinstance(bkg.background, u.Quantity) assert isinstance(bkg.background_rms, u.Quantity) assert isinstance(bkg.background_median, u.Quantity) assert isinstance(bkg.background_rms_median, u.Quantity) bkg = Background2D(DATA2, (25, 25), filter_size=3) assert_allclose(bkg.background, DATA) assert_allclose(bkg.background_rms, BKG_RMS) assert_allclose(bkg.background_mesh, BKG_MESH) assert_allclose(bkg.background_rms_mesh, BKG_RMS_MESH) assert bkg.background_median == 1.0 assert bkg.background_rms_median == 0.0 @pytest.mark.parametrize('interpolator', INTERPOLATORS) def test_background_rect(self, interpolator): """ Regression test for interpolators with non-square input data. """ data = np.arange(12).reshape(3, 4) rms = np.zeros((3, 4)) bkg = Background2D(data, (1, 1), filter_size=1, interpolator=interpolator) assert_allclose(bkg.background, data, atol=0.005) assert_allclose(bkg.background_rms, rms) assert_allclose(bkg.background_mesh, data) assert_allclose(bkg.background_rms_mesh, rms) assert bkg.background_median == 5.5 assert bkg.background_rms_median == 0.0 @pytest.mark.parametrize('interpolator', INTERPOLATORS) def test_background_nonconstant(self, interpolator): data = np.copy(DATA) data[25:50, 50:75] = 10. bkg_low_res = np.copy(BKG_MESH) bkg_low_res[1, 2] = 10. bkg1 = Background2D(data, (25, 25), filter_size=(1, 1), interpolator=interpolator) assert_allclose(bkg1.background_mesh, bkg_low_res) assert bkg1.background.shape == data.shape bkg2 = Background2D(data, (25, 25), filter_size=(1, 1), edge_method='pad', interpolator=interpolator) assert_allclose(bkg2.background_mesh, bkg_low_res) assert bkg2.background.shape == data.shape def test_no_sigma_clipping(self): data = np.copy(DATA) data[10, 10] = 100. bkg1 = Background2D(data, (25, 25), filter_size=(1, 1), bkg_estimator=MeanBackground()) bkg2 = Background2D(data, (25, 25), filter_size=(1, 1), sigma_clip=None, bkg_estimator=MeanBackground()) assert bkg2.background_mesh[0, 0] > bkg1.background_mesh[0, 0] @pytest.mark.parametrize('filter_size', FILTER_SIZES) def test_resizing(self, filter_size): bkg1 = Background2D(DATA, (23, 22), filter_size=filter_size, bkg_estimator=MeanBackground(), edge_method='crop') bkg2 = Background2D(DATA, (23, 22), filter_size=filter_size, bkg_estimator=MeanBackground(), edge_method='pad') assert_allclose(bkg1.background, bkg2.background, rtol=2e-6) assert_allclose(bkg1.background_rms, bkg2.background_rms) @pytest.mark.parametrize('box_size', ([(25, 25), (23, 22)])) def test_background_mask(self, box_size): """ Test with an input mask. Note that box_size=(23, 22) tests the resizing of the image and mask. """ data = np.copy(DATA) data[25:50, 25:50] = 100. mask = np.zeros(DATA.shape, dtype=bool) mask[25:50, 25:50] = True bkg = Background2D(data, box_size, filter_size=(1, 1), mask=mask, bkg_estimator=MeanBackground()) assert_allclose(bkg.background, DATA, rtol=2.e-5) assert_allclose(bkg.background_rms, BKG_RMS) # test edge crop with mask bkg2 = Background2D(data, box_size, filter_size=(1, 1), mask=mask, bkg_estimator=MeanBackground(), edge_method='crop') assert_allclose(bkg2.background, DATA, rtol=2.e-5) def test_mask(self): data = np.copy(DATA) data[25:50, 25:50] = 100. mask = np.zeros(DATA.shape, dtype=bool) mask[25:50, 25:50] = True bkg1 = Background2D(data, (25, 25), filter_size=(1, 1), mask=None, bkg_estimator=MeanBackground()) assert_equal(bkg1.background_mesh, bkg1.background_mesh_masked) assert_equal(bkg1.background_rms_mesh, bkg1.background_rms_mesh_masked) assert np.count_nonzero(np.isnan(bkg1.mesh_nmasked)) == 0 bkg2 = Background2D(data, (25, 25), filter_size=(1, 1), mask=mask, bkg_estimator=MeanBackground()) assert (np.count_nonzero(~np.isnan(bkg2.background_mesh_masked)) < bkg2.nboxes_tot) assert (np.count_nonzero(~np.isnan(bkg2.background_rms_mesh_masked)) < bkg2.nboxes_tot) assert np.count_nonzero(np.isnan(bkg2.mesh_nmasked)) == 1 @pytest.mark.parametrize('fill_value', [0., np.nan, -1.]) def test_coverage_mask(self, fill_value): data = np.copy(DATA) data[:50, :50] = np.nan mask = np.isnan(data) with catch_warnings(AstropyUserWarning): bkg1 = Background2D(data, (25, 25), filter_size=(1, 1), coverage_mask=mask, fill_value=fill_value, bkg_estimator=MeanBackground()) assert_equal(bkg1.background[:50, :50], fill_value) assert_equal(bkg1.background_rms[:50, :50], fill_value) # test combination of masks mask = np.zeros(DATA.shape, dtype=bool) coverage_mask = np.zeros(DATA.shape, dtype=bool) mask[:50, :25] = True coverage_mask[:50, 25:50] = True bkg2 = Background2D(data, (25, 25), filter_size=(1, 1), mask=mask, coverage_mask=mask, fill_value=0.0, bkg_estimator=MeanBackground()) assert_equal(bkg1.background_mesh, bkg2.background_mesh) assert_equal(bkg1.background_rms_mesh, bkg2.background_rms_mesh) def test_mask_nonfinite(self): data = DATA.copy() data[0, 0:50] = np.nan bkg = Background2D(data, (25, 25), filter_size=(1, 1)) assert_allclose(bkg.background, DATA, rtol=1e-5) def test_masked_array(self): data = DATA.copy() data[0, 0:50] = True mask = np.zeros(DATA.shape, dtype=bool) mask[0, 0:50] = True data_ma1 = np.ma.MaskedArray(DATA, mask=mask) data_ma2 = np.ma.MaskedArray(data, mask=mask) bkg1 = Background2D(data, (25, 25), filter_size=(1, 1)) bkg2 = Background2D(data_ma1, (25, 25), filter_size=(1, 1)) bkg3 = Background2D(data_ma2, (25, 25), filter_size=(1, 1)) assert_allclose(bkg1.background, bkg2.background, rtol=1e-5) assert_allclose(bkg2.background, bkg3.background, rtol=1e-5) def test_completely_masked(self): with pytest.raises(ValueError): mask = np.ones(DATA.shape, dtype=bool) Background2D(DATA, (25, 25), mask=mask) def test_zero_padding(self): """Test case where padding is added only on one axis.""" bkg = Background2D(DATA, (25, 22), filter_size=(1, 1)) assert_allclose(bkg.background, DATA, rtol=1e-5) assert_allclose(bkg.background_rms, BKG_RMS) assert bkg.background_median == 1.0 assert bkg.background_rms_median == 0.0 def test_exclude_percentile(self): """Only meshes greater than filter_threshold are filtered.""" data = np.copy(DATA) data[0:50, 0:50] = np.nan bkg = Background2D(data, (25, 25), filter_size=(1, 1), exclude_percentile=100.) assert len(bkg._box_idx) == 12 def test_filter_threshold(self): """Only meshes greater than filter_threshold are filtered.""" data = np.copy(DATA) data[25:50, 50:75] = 10. bkg = Background2D(data, (25, 25), filter_size=(3, 3), filter_threshold=9.) assert_allclose(bkg.background, DATA) assert_allclose(bkg.background_mesh, BKG_MESH) bkg2 = Background2D(data, (25, 25), filter_size=(3, 3), filter_threshold=11.) # no filtering assert bkg2.background_mesh[1, 2] == 10 def test_filter_threshold_high(self): """No filtering because filter_threshold is too large.""" data = np.copy(DATA) data[25:50, 50:75] = 10. ref_data = np.copy(BKG_MESH) ref_data[1, 2] = 10. b = Background2D(data, (25, 25), filter_size=(3, 3), filter_threshold=100.) assert_allclose(b.background_mesh, ref_data) def test_filter_threshold_nofilter(self): """No filtering because filter_size is (1, 1).""" data = np.copy(DATA) data[25:50, 50:75] = 10. ref_data = np.copy(BKG_MESH) ref_data[1, 2] = 10. b = Background2D(data, (25, 25), filter_size=(1, 1), filter_threshold=1.) assert_allclose(b.background_mesh, ref_data) def test_scalar_sizes(self): bkg1 = Background2D(DATA, (25, 25), filter_size=(3, 3)) bkg2 = Background2D(DATA, 25, filter_size=3) assert_allclose(bkg1.background, bkg2.background) assert_allclose(bkg1.background_rms, bkg2.background_rms) def test_invalid_box_size(self): with pytest.raises(ValueError): Background2D(DATA, (5, 5, 3)) def test_invalid_filter_size(self): with pytest.raises(ValueError): Background2D(DATA, (5, 5), filter_size=(3, 3, 3)) def test_invalid_exclude_percentile(self): with pytest.raises(ValueError): Background2D(DATA, (5, 5), exclude_percentile=-1) with pytest.raises(ValueError): Background2D(DATA, (5, 5), exclude_percentile=101) def test_mask_nomask(self): bkg = Background2D(DATA, (25, 25), filter_size=(1, 1), mask=np.ma.nomask) assert bkg.mask is None bkg = Background2D(DATA, (25, 25), filter_size=(1, 1), coverage_mask=np.ma.nomask) assert bkg.coverage_mask is None def test_invalid_mask(self): with pytest.raises(ValueError): Background2D(DATA, (25, 25), filter_size=(1, 1), mask=np.zeros((2, 2))) with pytest.raises(ValueError): Background2D(DATA, (25, 25), filter_size=(1, 1), mask=np.zeros((2, 2, 2))) def test_invalid_coverage_mask(self): with pytest.raises(ValueError): Background2D(DATA, (25, 25), filter_size=(1, 1), coverage_mask=np.zeros((2, 2))) with pytest.raises(ValueError): Background2D(DATA, (25, 25), filter_size=(1, 1), coverage_mask=np.zeros((2, 2, 2))) def test_invalid_edge_method(self): with pytest.raises(ValueError): Background2D(DATA, (23, 22), filter_size=(1, 1), edge_method='not_valid') def test_invalid_mesh_idx_len(self): with pytest.raises(ValueError): bkg = Background2D(DATA, (25, 25), filter_size=(1, 1)) bkg._make_2d_array(np.arange(3)) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plot_meshes(self): """ This test should run without any errors, but there is no return value. """ bkg = Background2D(DATA, (25, 25)) bkg.plot_meshes(outlines=True) def test_crop(self): data = np.ones((300, 500)) bkg = Background2D(data, (74, 99), edge_method='crop') assert_allclose(bkg.background_median, 1.0) assert_allclose(bkg.background_rms_median, 0.0) assert_allclose(bkg.background_mesh.shape, (4, 5)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/background/tests/test_core.py0000644000214200020070000001762400000000000022065 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the core module. """ from astropy.stats import SigmaClip import astropy.units as u import numpy as np from numpy.testing import assert_allclose import pytest from ..core import (BiweightLocationBackground, BiweightScaleBackgroundRMS, MADStdBackgroundRMS, MeanBackground, MedianBackground, MMMBackground, ModeEstimatorBackground, SExtractorBackground, StdBackgroundRMS) from ...datasets import make_noise_image BKG = 0.0 STD = 0.5 DATA = make_noise_image((100, 100), distribution='gaussian', mean=BKG, stddev=STD, seed=0) BKG_CLASS = [MeanBackground, MedianBackground, ModeEstimatorBackground, MMMBackground, SExtractorBackground, BiweightLocationBackground] RMS_CLASS = [StdBackgroundRMS, MADStdBackgroundRMS, BiweightScaleBackgroundRMS] SIGMA_CLIP = SigmaClip(sigma=3.) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_constant_background(bkg_class): data = np.ones((100, 100)) bkg = bkg_class(sigma_clip=SIGMA_CLIP) bkgval = bkg.calc_background(data) assert not np.ma.isMaskedArray(bkgval) assert_allclose(bkgval, 1.0) assert_allclose(bkg(data), bkg.calc_background(data)) mask = np.zeros(data.shape, dtype=bool) mask[0, 0:10] = True data = np.ma.MaskedArray(data, mask=mask) bkgval = bkg.calc_background(data) assert not np.ma.isMaskedArray(bkgval) assert_allclose(bkgval, 1.0) assert_allclose(bkg(data), bkg.calc_background(data)) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background(bkg_class): bkg = bkg_class(sigma_clip=SIGMA_CLIP) bkgval = bkg.calc_background(DATA) assert not np.ma.isMaskedArray(bkgval) assert_allclose(bkgval, BKG, atol=0.02) assert_allclose(bkg(DATA), bkg.calc_background(DATA)) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_nosigmaclip(bkg_class): bkg = bkg_class(sigma_clip=None) bkgval = bkg.calc_background(DATA) assert not np.ma.isMaskedArray(bkgval) assert_allclose(bkgval, BKG, atol=0.1) assert_allclose(bkg(DATA), bkg.calc_background(DATA)) # test with masked array mask = np.zeros(DATA.shape, dtype=bool) mask[0, 0:10] = True data = np.ma.MaskedArray(DATA, mask=mask) bkgval = bkg.calc_background(data) assert not np.ma.isMaskedArray(bkgval) assert_allclose(bkgval, BKG, atol=0.1) assert_allclose(bkg(data), bkg.calc_background(data)) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_axis(bkg_class): bkg = bkg_class(sigma_clip=SIGMA_CLIP) bkg_arr = bkg.calc_background(DATA, axis=0) bkgi = [] for i in range(100): bkgi.append(bkg.calc_background(DATA[:, i])) bkgi = np.array(bkgi) assert_allclose(bkg_arr, bkgi) bkg_arr = bkg.calc_background(DATA, axis=1) bkgi = [] for i in range(100): bkgi.append(bkg.calc_background(DATA[i, :])) bkgi = np.array(bkgi) assert_allclose(bkg_arr, bkgi) def test_sourceextrator_background_zero_std(): data = np.ones((100, 100)) bkg = SExtractorBackground(sigma_clip=None) assert_allclose(bkg.calc_background(data), 1.0) def test_sourceextrator_background_skew(): data = np.arange(100) data[70:] = 1.e7 bkg = SExtractorBackground(sigma_clip=None) assert_allclose(bkg.calc_background(data), np.median(data)) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms(rms_class): bkgrms = rms_class(sigma_clip=SIGMA_CLIP) assert_allclose(bkgrms.calc_background_rms(DATA), STD, atol=1.e-2) assert_allclose(bkgrms(DATA), bkgrms.calc_background_rms(DATA)) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_axis(rms_class): bkgrms = rms_class(sigma_clip=SIGMA_CLIP) rms_arr = bkgrms.calc_background_rms(DATA, axis=0) rmsi = [] for i in range(100): rmsi.append(bkgrms.calc_background_rms(DATA[:, i])) rmsi = np.array(rmsi) assert_allclose(rms_arr, rmsi) rms_arr = bkgrms.calc_background_rms(DATA, axis=1) rmsi = [] for i in range(100): rmsi.append(bkgrms.calc_background_rms(DATA[i, :])) rmsi = np.array(rmsi) assert_allclose(rms_arr, rmsi) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_nosigmaclip(rms_class): bkgrms = rms_class(sigma_clip=None) assert_allclose(bkgrms.calc_background_rms(DATA), STD, atol=1.e-2) assert_allclose(bkgrms(DATA), bkgrms.calc_background_rms(DATA)) # test with masked array mask = np.zeros(DATA.shape, dtype=bool) mask[0, 0:10] = True data = np.ma.MaskedArray(DATA, mask=mask) rms = bkgrms.calc_background_rms(data) assert not np.ma.isMaskedArray(bkgrms) assert_allclose(rms, STD, atol=0.01) assert_allclose(bkgrms(data), bkgrms.calc_background_rms(data)) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_masked(bkg_class): bkg = bkg_class(sigma_clip=None) mask = np.zeros(DATA.shape, dtype=bool) mask[0, 0:10] = True data = np.ma.MaskedArray(DATA, mask=mask) # test masked array with masked=True with axis bkgval1 = bkg(data, masked=True, axis=1) bkgval2 = bkg.calc_background(data, masked=True, axis=1) assert np.ma.isMaskedArray(bkgval1) assert_allclose(np.mean(bkgval1), np.mean(bkgval2)) assert_allclose(np.mean(bkgval1), BKG, atol=0.01) # test masked array with masked=False with axis bkgval2 = bkg.calc_background(data, masked=False, axis=1) assert not np.ma.isMaskedArray(bkgval2) assert_allclose(np.nanmean(bkgval2), BKG, atol=0.01) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_masked(rms_class): bkgrms = rms_class(sigma_clip=None) mask = np.zeros(DATA.shape, dtype=bool) mask[0, 0:10] = True data = np.ma.MaskedArray(DATA, mask=mask) # test masked array with masked=True with axis rms1 = bkgrms(data, masked=True, axis=1) rms2 = bkgrms.calc_background_rms(data, masked=True, axis=1) assert np.ma.isMaskedArray(rms1) assert_allclose(np.mean(rms1), np.mean(rms2)) assert_allclose(np.mean(rms1), STD, atol=0.01) # test masked array with masked=False with axis rms3 = bkgrms.calc_background_rms(data, masked=False, axis=1) assert not np.ma.isMaskedArray(rms3) assert_allclose(np.nanmean(rms3), STD, atol=0.01) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_axis_tuple(bkg_class): bkg = bkg_class(sigma_clip=None) bkg_val1 = bkg.calc_background(DATA, axis=None) bkg_val2 = bkg.calc_background(DATA, axis=(0, 1)) assert_allclose(bkg_val1, bkg_val2) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_units(bkg_class): data = np.ones((100, 100)) << u.Jy bkg = bkg_class(sigma_clip=SIGMA_CLIP) bkgval = bkg.calc_background(data) assert isinstance(bkgval, u.Quantity) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_units(rms_class): data = np.ones((100, 100)) << u.Jy bkgrms = rms_class(sigma_clip=SIGMA_CLIP) rmsval = bkgrms.calc_background_rms(data) assert isinstance(rmsval, u.Quantity) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_invalid_sigmaclip(bkg_class): with pytest.raises(TypeError): bkg_class(sigma_clip=3) @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_invalid_sigmaclip(rms_class): with pytest.raises(TypeError): rms_class(sigma_clip=3) @pytest.mark.parametrize('bkg_class', BKG_CLASS) def test_background_repr(bkg_class): bkg = bkg_class() bkg_repr = repr(bkg) assert bkg_repr == str(bkg) assert bkg_repr.startswith(f'<{bkg.__class__.__name__}(sigma_clip=') @pytest.mark.parametrize('rms_class', RMS_CLASS) def test_background_rms_repr(rms_class): bkgrms = rms_class() rms_repr = repr(bkgrms) assert rms_repr == str(bkgrms) assert rms_repr.startswith(f'<{bkgrms.__class__.__name__}(sigma_clip=') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9861305 photutils-1.3.0/photutils/centroids/0000755000214200020070000000000000000000000016223 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/photutils/centroids/__init__.py0000644000214200020070000000027500000000000020340 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for centroiding sources. """ from .core import * # noqa from .gaussian import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639449781.0 photutils-1.3.0/photutils/centroids/core.py0000644000214200020070000005140200000000000017527 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ The module contains tools for centroiding sources. """ import inspect import warnings from astropy.nddata.utils import overlap_slices from astropy.utils.decorators import deprecated from astropy.utils.exceptions import AstropyUserWarning import numpy as np from ..utils._round import _py2intround __all__ = ['centroid_com', 'centroid_quadratic', 'centroid_sources', 'centroid_epsf'] def centroid_com(data, mask=None, oversampling=1): """ Calculate the centroid of an n-dimensional array as its "center of mass" determined from moments. Non-finite values (e.g., NaN or inf) in the ``data`` array are automatically masked. Parameters ---------- data : array_like The input n-dimensional array. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. oversampling : int or tuple of two int, optional Oversampling factors of pixel indices. If ``oversampling`` is a scalar this is treated as both x and y directions having the same oversampling factor; otherwise it is treated as ``(x_oversamp, y_oversamp)``. Returns ------- centroid : `~numpy.ndarray` The coordinates of the centroid in pixel order (e.g., ``(x, y)`` or ``(x, y, z)``), not numpy axis order. """ data = data.astype(float) if mask is not None and mask is not np.ma.nomask: mask = np.asarray(mask, dtype=bool) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data[mask] = 0. oversampling = np.atleast_1d(oversampling) if len(oversampling) == 1: oversampling = np.repeat(oversampling, 2) oversampling = oversampling[::-1] # reverse to (y, x) order if np.any(oversampling <= 0): raise ValueError('Oversampling factors must all be positive numbers.') badmask = ~np.isfinite(data) if np.any(badmask): warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'inf) that were automatically masked.', AstropyUserWarning) data[badmask] = 0. total = np.sum(data) indices = np.ogrid[[slice(0, i) for i in data.shape]] # note the output array is reversed to give (x, y) order return np.array([np.sum(indices[axis] * data) / total / oversampling[axis] for axis in range(data.ndim)])[::-1] def centroid_quadratic(data, xpeak=None, ypeak=None, fit_boxsize=5, search_boxsize=None, mask=None): """ Calculate the centroid of an n-dimensional array by fitting a 2D quadratic polynomial. A second degree 2D polynomial is fit within a small region of the data defined by ``fit_boxsize`` to calculate the centroid position. The initial center of the fitting box can specified using the ``xpeak`` and ``ypeak`` keywords. If both ``xpeak`` and ``ypeak`` are `None`, then the box will be centered at the position of the maximum value in the input ``data``. If ``xpeak`` and ``ypeak`` are specified, the ``search_boxsize`` optional keyword can be used to further refine the initial center of the fitting box by searching for the position of the maximum pixel within a box of size ``search_boxsize``. `Vakili & Hogg (2016) `_ demonstrate that 2D quadratic centroiding comes very close to saturating the `Cramér-Rao lower bound `_ in a wide range of conditions. Parameters ---------- data : numpy.ndarray Image data. xpeak, ypeak : float or `None`, optional The initial guess of the position of the centroid. If either ``xpeak`` or ``ypeak`` is `None` then the position of the maximum value in the input ``data`` will be used as the initial guess. fit_boxsize : int or tuple of int, optional The size (in pixels) of the box used to define the fitting region. If ``fit_boxsize`` has two elements, they should be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. search_boxsize : int or tuple of int, optional The size (in pixels) of the box used to search for the maximum pixel value if ``xpeak`` and ``ypeak`` are both specified. If ``fit_boxsize`` has two elements, they should be in ``(ny, nx)`` order. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. This parameter is ignored if either ``xpeak`` or ``ypeak`` is `None`. In that case, the entire array is search for the maximum value. mask : bool `~numpy.ndarray`, optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from calculations. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. Notes ----- Use ``fit_boxsize = (3, 3)`` to match the work of `Vakili & Hogg (2016) `_ for their 2D second-order polynomial centroiding method. References ---------- .. [1] Vakili and Hogg 2016; arXiv:1610.05873 (https://arxiv.org/abs/1610.05873) """ if ((xpeak is None and ypeak is not None) or (xpeak is not None and ypeak is None)): raise ValueError('xpeak and ypeak must both be input or "None"') if xpeak is not None and ((xpeak < 0) or (xpeak > data.shape[1] - 1)): raise ValueError('xpeak is outside of the input data') if ypeak is not None and ((ypeak < 0) or (ypeak > data.shape[0] - 1)): raise ValueError('ypeak is outside of the input data') data = np.asanyarray(data, dtype=float) ny, nx = data.shape badmask = ~np.isfinite(data) if np.any(badmask): warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'inf) that were automatically masked.', AstropyUserWarning) data[badmask] = np.nan if mask is not None: if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data[mask] = np.nan fit_boxsize = _process_boxsize(fit_boxsize, data.shape) if np.product(fit_boxsize) < 6: raise ValueError('fit_boxsize is too small. 6 values are required ' 'to fit a 2D quadratic polynomial.') if xpeak is None or ypeak is None: yidx, xidx = np.unravel_index(np.nanargmax(data), data.shape) else: xidx = _py2intround(xpeak) yidx = _py2intround(ypeak) if search_boxsize is not None: search_boxsize = _process_boxsize(search_boxsize, data.shape) slc_data, _ = overlap_slices(data.shape, search_boxsize, (yidx, xidx), mode='trim') cutout = data[slc_data] yidx, xidx = np.unravel_index(np.nanargmax(cutout), cutout.shape) xidx += slc_data[1].start yidx += slc_data[0].start # if peak is at the edge of the data, return the position of the maximum if xidx == 0 or xidx == nx - 1 or yidx == 0 or yidx == ny - 1: warnings.warn('maximum value is at the edge of the data and its ' 'position was returned; no quadratic fit was ' 'performed', AstropyUserWarning) return np.array((xidx, yidx), dtype=float) # extract the fitting region slc_data, _ = overlap_slices(data.shape, fit_boxsize, (yidx, xidx), mode='trim') xidx0, xidx1 = (slc_data[1].start, slc_data[1].stop) yidx0, yidx1 = (slc_data[0].start, slc_data[0].stop) # shift the fitting box if it was clipped by the data edge if (xidx1 - xidx0) < fit_boxsize[1]: if xidx0 == 0: xidx1 = min(nx, xidx0 + fit_boxsize[1]) if xidx1 == nx: xidx0 = max(0, xidx1 - fit_boxsize[1]) if (yidx1 - yidx0) < fit_boxsize[0]: if yidx0 == 0: yidx1 = min(ny, yidx0 + fit_boxsize[0]) if yidx1 == ny: yidx0 = max(0, yidx1 - fit_boxsize[0]) cutout = data[yidx0:yidx1, xidx0:xidx1].ravel() if np.count_nonzero(~np.isnan(cutout)) < 6: warnings.warn('at least 6 unmasked data points are required to ' 'perform a 2D quadratic fit', AstropyUserWarning) return np.array((np.nan, np.nan)) # fit a 2D quadratic polynomial to the fitting region xi = np.arange(xidx0, xidx1) yi = np.arange(yidx0, yidx1) x, y = np.meshgrid(xi, yi) x = x.ravel() y = y.ravel() coeff_matrix = np.vstack((np.ones_like(x), x, y, x * y, x * x, y * y)).T try: c = np.linalg.lstsq(coeff_matrix, cutout, rcond=None)[0] except np.linalg.LinAlgError: warnings.warn('quadratic fit failed', AstropyUserWarning) return np.array((np.nan, np.nan)) # analytically find the maximum of the polynomial _, c10, c01, c11, c20, c02 = c det = 4 * c20 * c02 - c11**2 if det <= 0 or ((c20 > 0.0 and c02 >= 0.0) or (c20 >= 0.0 and c02 > 0.0)): warnings.warn('quadratic fit does not have a maximum', AstropyUserWarning) return np.array((np.nan, np.nan)) xm = (c01 * c11 - 2.0 * c02 * c10) / det ym = (c10 * c11 - 2.0 * c20 * c01) / det if 0.0 < xm < (nx - 1.0) and 0.0 < ym < (ny - 1.0): xycen = np.array((xm, ym), dtype=float) else: warnings.warn('quadratic polynomial maximum value falls outside ' 'of the image', AstropyUserWarning) return np.array((np.nan, np.nan)) return xycen def _process_boxsize(box_size, data_shape): box_size = np.round(np.atleast_1d(box_size)).astype(int) if len(box_size) == 1: box_size = np.repeat(box_size, 2) if len(box_size) > 2: raise ValueError('box size must contain only 1 or 2 values') if np.any(box_size < 0): raise ValueError('box size must be >= 0') # box_size cannot be larger than the data shape box_size = (min(box_size[0], data_shape[0]), min(box_size[1], data_shape[1])) return box_size def centroid_sources(data, xpos, ypos, box_size=11, footprint=None, mask=None, centroid_func=centroid_com, **kwargs): """ Calculate the centroid of sources at the defined positions. A cutout image centered on each input position will be used to calculate the centroid position. The cutout image is defined either using the ``box_size`` or ``footprint`` keyword. The ``footprint`` keyword can be used to create a non-rectangular cutout image. Parameters ---------- data : array_like The 2D array of the image. xpos, ypos : float or array-like of float The initial ``x`` and ``y`` pixel position(s) of the center position. A cutout image centered on this position be used to calculate the centroid. box_size : int or array-like of int, optional The size of the cutout image along each axis. If ``box_size`` is a number, then a square cutout of ``box_size`` will be created. If ``box_size`` has two elements, they should be in ``(ny, nx)`` order. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : `~numpy.ndarray` of bools, optional A 2D boolean array where `True` values describe the local footprint region to cutout. ``footprint`` can be used to create a non-rectangular cutout image, in which case the input ``xpos`` and ``ypos`` represent the center of the minimal bounding box for the input ``footprint``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A 2D boolean array with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. ``error`` must have the same shape as ``data``. ``error`` will be used only if supported by the input ``centroid_func``. centroid_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray`, representing the x and y centroids. The default is `~photutils.centroids.centroid_com`. **kwargs : `dict` Any additional keyword arguments accepted by the ``centroid_func``. Returns ------- xcentroid, ycentroid : `~numpy.ndarray` The ``x`` and ``y`` pixel position(s) of the centroids. NaNs will be returned where the centroid failed. This is usually due a ``box_size`` that is too small when using a fitting-based centroid function (e.g., `centroid_1dg`, `centroid_2dg`, or `centroid_quadratic`. """ xpos = np.atleast_1d(xpos) ypos = np.atleast_1d(ypos) if xpos.ndim != 1: raise ValueError('xpos must be a 1D array.') if ypos.ndim != 1: raise ValueError('ypos must be a 1D array.') if (np.any(np.min(xpos) < 0) or np.any(np.min(ypos) < 0) or np.any(np.max(xpos) > data.shape[1] - 1) or np.any(np.max(ypos) > data.shape[0] - 1)): raise ValueError('xpos, ypos values contains point(s) outside of ' 'input data') if footprint is None: if box_size is None: raise ValueError('box_size or footprint must be defined.') box_size = np.atleast_1d(box_size) if len(box_size) == 1: box_size = np.repeat(box_size, 2) if len(box_size) != 2: raise ValueError('box_size must have 1 or 2 elements.') footprint = np.ones(box_size, dtype=bool) else: footprint = np.asanyarray(footprint, dtype=bool) if footprint.ndim != 2: raise ValueError('footprint must be a 2D array.') spec = inspect.getfullargspec(centroid_func) if 'mask' not in spec.args: raise ValueError('The input "centroid_func" must have a "mask" ' 'keyword.') # drop any **kwargs not supported by the centroid_func centroid_kwargs = {} for key, val in kwargs.items(): if key in spec.args: centroid_kwargs[key] = val xcentroids = [] ycentroids = [] for xp, yp in zip(xpos, ypos): slices_large, slices_small = overlap_slices(data.shape, footprint.shape, (yp, xp)) data_cutout = data[slices_large] footprint_mask = np.logical_not(footprint) # trim footprint mask if it has only partial overlap on the data footprint_mask = footprint_mask[slices_small] if mask is not None: # combine the input mask cutout and footprint mask mask_cutout = np.logical_or(mask[slices_large], footprint_mask) else: mask_cutout = footprint_mask centroid_kwargs.update({'mask': mask_cutout}) if 'error' in centroid_kwargs: error_cutout = centroid_kwargs['error'][slices_large] centroid_kwargs['error'] = error_cutout if 'xpeak' in centroid_kwargs and 'ypeak' in centroid_kwargs: centroid_kwargs['xpeak'] -= slices_large[1].start centroid_kwargs['ypeak'] -= slices_large[0].start try: xcen, ycen = centroid_func(data_cutout, **centroid_kwargs) except (ValueError, TypeError): xcen, ycen = np.nan, np.nan xcentroids.append(xcen + slices_large[1].start) ycentroids.append(ycen + slices_large[0].start) return np.array(xcentroids), np.array(ycentroids) @deprecated('1.2') def centroid_epsf(data, mask=None, oversampling=4, shift_val=0.5): """ Calculate centering shift of data using pixel symmetry, as described by Anderson and King (2000; PASP 112, 1360) in their ePSF-fitting algorithm. Calculate the shift of a 2-dimensional symmetric image based on the asymmetry between f(x, N) and f(x, -N), along with the differential df/dy(x, shift_val) and df/dy(x, -shift_val). Non-finite values (e.g., NaN or inf) in the ``data`` array are automatically masked. Parameters ---------- data : array_like The input n-dimensional array. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. oversampling : int or tuple of two int, optional Oversampling factors of pixel indices. If ``oversampling`` is a scalar this is treated as both x and y directions having the same oversampling factor. Otherwise it is treated as ``(x_oversamp, y_oversamp)``. shift_val : float, optional The undersampled value at which to compute the shifts. Default is half a pixel. It must be a strictly positive number. Returns ------- centroid : tuple of floats The (x, y) coordinates of the centroid in pixel order. """ data = data.astype(float) if mask is not None and mask is not np.ma.nomask: mask = np.asarray(mask, dtype=bool) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data[mask] = 0. oversampling = np.atleast_1d(oversampling) if len(oversampling) == 1: oversampling = np.repeat(oversampling, 2) if np.any(oversampling <= 0): raise ValueError('Oversampling factors must all be positive numbers.') if shift_val <= 0: raise ValueError('shift_val must be a positive number.') # Assume the center of the ePSF is the middle of an odd-sized grid. xidx_0 = int((data.shape[1] - 1) / 2) x_0 = np.arange(data.shape[1], dtype=float)[xidx_0] / oversampling[0] yidx_0 = int((data.shape[0] - 1) / 2) y_0 = np.arange(data.shape[0], dtype=float)[yidx_0] / oversampling[1] x_shiftidx = np.around((shift_val * oversampling[0])).astype(int) y_shiftidx = np.around((shift_val * oversampling[1])).astype(int) badmask = ~np.isfinite([data[y, x] for x in [xidx_0, xidx_0 + x_shiftidx, xidx_0 + x_shiftidx - 1, xidx_0 + x_shiftidx + 1] for y in [yidx_0, yidx_0 + y_shiftidx, yidx_0 + y_shiftidx - 1, yidx_0 + y_shiftidx + 1]]) if np.any(badmask): raise ValueError('One or more centroiding pixels is set to a ' 'non-finite value, e.g., NaN or inf.') # In Anderson & King (2000) notation this is psi_E(0.5, 0.0) and # values used to compute derivatives. psi_pos_x = data[yidx_0, xidx_0 + x_shiftidx] psi_pos_x_m1 = data[yidx_0, xidx_0 + x_shiftidx - 1] psi_pos_x_p1 = data[yidx_0, xidx_0 + x_shiftidx + 1] # Our derivatives are simple differences across two data points, but # this must be in units of the undersampled grid, so 2 pixels becomes # 2/oversampling pixels dpsi_pos_x = np.abs(psi_pos_x_p1 - psi_pos_x_m1) / (2. / oversampling[0]) # psi_E(-0.5, 0.0) and derivative components. psi_neg_x = data[yidx_0, xidx_0 - x_shiftidx] psi_neg_x_m1 = data[yidx_0, xidx_0 - x_shiftidx - 1] psi_neg_x_p1 = data[yidx_0, xidx_0 - x_shiftidx + 1] dpsi_neg_x = np.abs(psi_neg_x_p1 - psi_neg_x_m1) / (2. / oversampling[0]) x_shift = (psi_pos_x - psi_neg_x) / (dpsi_pos_x + dpsi_neg_x) # psi_E(0.0, 0.5) and derivatives. psi_pos_y = data[yidx_0 + y_shiftidx, xidx_0] psi_pos_y_m1 = data[yidx_0 + y_shiftidx - 1, xidx_0] psi_pos_y_p1 = data[yidx_0 + y_shiftidx + 1, xidx_0] dpsi_pos_y = np.abs(psi_pos_y_p1 - psi_pos_y_m1) / (2. / oversampling[1]) # psi_E(0.0, -0.5) and derivative components. psi_neg_y = data[yidx_0 - y_shiftidx, xidx_0] psi_neg_y_m1 = data[yidx_0 - y_shiftidx - 1, xidx_0] psi_neg_y_p1 = data[yidx_0 - y_shiftidx + 1, xidx_0] dpsi_neg_y = np.abs(psi_neg_y_p1 - psi_neg_y_m1) / (2. / oversampling[1]) y_shift = (psi_pos_y - psi_neg_y) / (dpsi_pos_y + dpsi_neg_y) return x_0 + x_shift, y_0 + y_shift ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639097588.0 photutils-1.3.0/photutils/centroids/gaussian.py0000644000214200020070000001766700000000000020430 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ The module contains tools for centroiding sources using Gaussians. """ import warnings from astropy.modeling.fitting import LevMarLSQFitter from astropy.modeling.models import Const1D, Const2D, Gaussian1D, Gaussian2D from astropy.utils.decorators import deprecated from astropy.utils.exceptions import AstropyUserWarning import numpy as np __all__ = ['centroid_1dg', 'gaussian1d_moments', 'centroid_2dg'] def centroid_1dg(data, error=None, mask=None): """ Calculate the centroid of a 2D array by fitting 1D Gaussians to the marginal ``x`` and ``y`` distributions of the array. Non-finite values (e.g., NaN or inf) in the ``data`` or ``error`` arrays are automatically masked. These masks are combined. Parameters ---------- data : array_like The 2D data array. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. """ data = np.ma.asanyarray(data) if mask is not None and mask is not np.ma.nomask: mask = np.asanyarray(mask) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data.mask |= mask if np.any(~np.isfinite(data)): data = np.ma.masked_invalid(data) warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'inf) that were automatically masked.', AstropyUserWarning) if error is not None: error = np.ma.masked_invalid(error) if data.shape != error.shape: raise ValueError('data and error must have the same shape.') data.mask |= error.mask error.mask = data.mask xy_error = [np.sqrt(np.ma.sum(error**2, axis=i)) for i in (0, 1)] xy_weights = [(1.0 / xy_error[i].clip(min=1.e-30)) for i in (0, 1)] else: xy_weights = [np.ones(data.shape[i]) for i in (1, 0)] # assign zero weight where an entire row or column is masked if np.any(data.mask): bad_idx = [np.all(data.mask, axis=i) for i in (0, 1)] for i in (0, 1): xy_weights[i][bad_idx[i]] = 0. xy_data = [np.ma.sum(data, axis=i).data for i in (0, 1)] constant_init = np.ma.min(data) centroid = [] for (data_i, weights_i) in zip(xy_data, xy_weights): params_init = _gaussian1d_moments(data_i) g_init = Const1D(constant_init) + Gaussian1D(*params_init) fitter = LevMarLSQFitter() x = np.arange(data_i.size) g_fit = fitter(g_init, x, data_i, weights=weights_i) centroid.append(g_fit.mean_1.value) return np.array(centroid) def _gaussian1d_moments(data, mask=None): """ Estimate 1D Gaussian parameters from the moments of 1D data. This function can be useful for providing initial parameter values when fitting a 1D Gaussian to the ``data``. Parameters ---------- data : array_like (1D) The 1D array. mask : array_like (1D bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- amplitude, mean, stddev : float The estimated parameters of a 1D Gaussian. """ if np.any(~np.isfinite(data)): data = np.ma.masked_invalid(data) warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'inf) that were automatically masked.', AstropyUserWarning) else: data = np.ma.array(data) if mask is not None and mask is not np.ma.nomask: mask = np.asanyarray(mask) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data.mask |= mask data.fill_value = 0. data = data.filled() x = np.arange(data.size) x_mean = np.sum(x * data) / np.sum(data) x_stddev = np.sqrt(abs(np.sum(data * (x - x_mean)**2) / np.sum(data))) amplitude = np.ptp(data) return amplitude, x_mean, x_stddev @deprecated('1.2') def gaussian1d_moments(data, mask=None): """ Estimate 1D Gaussian parameters from the moments of 1D data. This function can be useful for providing initial parameter values when fitting a 1D Gaussian to the ``data``. Parameters ---------- data : array_like (1D) The 1D array. mask : array_like (1D bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- amplitude, mean, stddev : float The estimated parameters of a 1D Gaussian. """ return _gaussian1d_moments(data, mask=mask) # pragma: no cover def centroid_2dg(data, error=None, mask=None): """ Calculate the centroid of a 2D array by fitting a 2D Gaussian (plus a constant) to the array. Non-finite values (e.g., NaN or inf) in the ``data`` or ``error`` arrays are automatically masked. These masks are combined. Parameters ---------- data : array_like The 2D data array. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. """ from ..morphology import data_properties # prevent circular imports data = np.ma.asanyarray(data) if mask is not None and mask is not np.ma.nomask: mask = np.asanyarray(mask) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data.mask |= mask if np.any(~np.isfinite(data)): data = np.ma.masked_invalid(data) warnings.warn('Input data contains non-finite values (e.g., NaN or ' 'infs) that were automatically masked.', AstropyUserWarning) if error is not None: error = np.ma.masked_invalid(error) if data.shape != error.shape: raise ValueError('data and error must have the same shape.') data.mask |= error.mask weights = 1.0 / error.clip(min=1.e-30) else: weights = np.ones(data.shape) if np.ma.count(data) < 7: raise ValueError('Input data must have a least 7 unmasked values to ' 'fit a 2D Gaussian plus a constant.') # assign zero weight to masked pixels if data.mask is not np.ma.nomask: weights[data.mask] = 0. mask = data.mask data.fill_value = 0. data = data.filled() # Subtract the minimum of the data as a rough background estimate. # This will also make the data values positive, preventing issues with # the moment estimation in data_properties. Moments from negative data # values can yield undefined Gaussian parameters, e.g., x/y_stddev. props = data_properties(data - np.min(data), mask=mask) constant_init = 0. # subtracted data minimum above g_init = (Const2D(constant_init) + Gaussian2D(amplitude=np.ptp(data), x_mean=props.xcentroid, y_mean=props.ycentroid, x_stddev=props.semimajor_sigma.value, y_stddev=props.semiminor_sigma.value, theta=props.orientation.value)) fitter = LevMarLSQFitter() y, x = np.indices(data.shape) gfit = fitter(g_init, x, y, data, weights=weights) return np.array([gfit.x_mean_1.value, gfit.y_mean_1.value]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9870481 photutils-1.3.0/photutils/centroids/tests/0000755000214200020070000000000000000000000017365 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/centroids/tests/__init__.py0000644000214200020070000000000000000000000021464 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639449781.0 photutils-1.3.0/photutils/centroids/tests/test_core.py0000644000214200020070000003144000000000000021730 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the core module. """ import itertools import warnings from astropy.modeling.models import Gaussian2D from astropy.utils.exceptions import (AstropyUserWarning, AstropyDeprecationWarning) import numpy as np from numpy.testing import assert_allclose import pytest from ..core import (centroid_com, centroid_quadratic, centroid_sources, centroid_epsf) from ..gaussian import centroid_1dg, centroid_2dg from ...psf import IntegratedGaussianPRF from ...utils._optional_deps import HAS_SCIPY # noqa XCEN = 25.7 YCEN = 26.2 XSTDS = [3.2, 4.0] YSTDS = [5.7, 4.1] THETAS = np.array([30., 45.]) * np.pi / 180. DATA = np.zeros((3, 3)) DATA[0:2, 1] = 1. DATA[1, 0:2] = 1. DATA[1, 1] = 2. CENTROID_FUNCS = (centroid_com, centroid_quadratic, centroid_1dg, centroid_2dg) # NOTE: the fitting routines in astropy use scipy.optimize @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize(('x_std', 'y_std', 'theta'), list(itertools.product(XSTDS, YSTDS, THETAS))) def test_centroid_com(x_std, y_std, theta): model = Gaussian2D(2.4, XCEN, YCEN, x_stddev=x_std, y_stddev=y_std, theta=theta) y, x = np.mgrid[0:50, 0:47] data = model(x, y) xc, yc = centroid_com(data) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) xc, yc = centroid_quadratic(data) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=0.015) # test with mask mask = np.zeros(data.shape, dtype=bool) data[10, 10] = 1.e5 mask[10, 10] = True xc, yc = centroid_com(data, mask=mask) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) xc, yc = centroid_quadratic(data, mask=mask) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=0.015) # test with oversampling for oversampling in [4, (4, 6)]: if not hasattr(oversampling, '__len__'): _oversampling = (oversampling, oversampling) else: _oversampling = oversampling xc, yc = centroid_com(data, mask=mask, oversampling=oversampling) desired = [XCEN / _oversampling[0], YCEN / _oversampling[1]] assert_allclose((xc, yc), desired, rtol=0, atol=1.e-3) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize('use_mask', [True, False]) def test_centroid_com_nan_withmask(use_mask): xc_ref = 24.7 yc_ref = 25.2 model = Gaussian2D(2.4, xc_ref, yc_ref, x_stddev=5.0, y_stddev=5.0) y, x = np.mgrid[0:50, 0:50] data = model(x, y) data[20, :] = np.nan if use_mask: mask = np.zeros(data.shape, dtype=bool) mask[20, :] = True nwarn = 0 else: mask = None nwarn = 1 with warnings.catch_warnings(record=True) as warnlist: xc, yc = centroid_com(data, mask=mask) assert_allclose(xc, xc_ref, rtol=0, atol=1.e-3) assert yc > yc_ref assert len(warnlist) == nwarn if nwarn == 1: assert issubclass(warnlist[0].category, AstropyUserWarning) with warnings.catch_warnings(record=True) as warnlist: xc, yc = centroid_quadratic(data, mask=mask) assert_allclose(xc, xc_ref, rtol=0, atol=0.15) assert len(warnlist) == 1 # always warns because of NaN if nwarn == 1: assert issubclass(warnlist[0].category, AstropyUserWarning) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_com_invalid_inputs(): data = np.zeros((4, 4)) mask = np.zeros((2, 2), dtype=bool) with pytest.raises(ValueError): centroid_com(data, mask=mask) with pytest.raises(ValueError): centroid_com(data, oversampling=-1) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_quadratic_xypeak(): data = np.zeros((11, 11)) data[5, 5] = 100 data[7, 7] = 110 data[9, 9] = 120 xycen1 = centroid_quadratic(data, fit_boxsize=3) assert_allclose(xycen1, (9, 9)) xycen2 = centroid_quadratic(data, xpeak=5, ypeak=5, fit_boxsize=3) assert_allclose(xycen2, (5, 5)) xycen3 = centroid_quadratic(data, xpeak=5, ypeak=5, fit_boxsize=3, search_boxsize=5) assert_allclose(xycen3, (7, 7)) with pytest.raises(ValueError): centroid_quadratic(data, xpeak=15, ypeak=5) with pytest.raises(ValueError): centroid_quadratic(data, xpeak=5, ypeak=15) with pytest.raises(ValueError): centroid_quadratic(data, xpeak=15, ypeak=15) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_quadratic_npts(): data = np.zeros((3, 3)) data[1, 1] = 1 mask = np.zeros(data.shape, dtype=bool) mask[0, :] = True mask[2, :] = True with warnings.catch_warnings(record=True) as warnlist: centroid_quadratic(data, mask=mask) assert issubclass(warnlist[0].category, AstropyUserWarning) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_quadratic_invalid_inputs(): data = np.zeros((4, 4)) mask = np.zeros((2, 2), dtype=bool) with pytest.raises(ValueError): centroid_quadratic(data, xpeak=3, ypeak=None) with pytest.raises(ValueError): centroid_quadratic(data, xpeak=None, ypeak=3) with pytest.raises(ValueError): centroid_quadratic(data, fit_boxsize=(2, 2, 2)) with pytest.raises(ValueError): centroid_quadratic(data, fit_boxsize=(-2, 2)) with pytest.raises(ValueError): centroid_quadratic(data, fit_boxsize=(2, 2)) with pytest.raises(ValueError): centroid_quadratic(data, mask=mask) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_quadratic_edge(): data = np.zeros((11, 11)) data[1, 1] = 100 data[9, 9] = 100 xycen = centroid_quadratic(data, xpeak=1, ypeak=1, fit_boxsize=5) assert_allclose(xycen, (0.923077, 0.923077)) xycen = centroid_quadratic(data, xpeak=9, ypeak=9, fit_boxsize=5) assert_allclose(xycen, (9.076923, 9.076923)) data = np.zeros((5, 5)) data[0, 0] = 100 with warnings.catch_warnings(record=True) as warnlist: xycen = centroid_quadratic(data) assert_allclose(xycen, (0, 0)) assert issubclass(warnlist[0].category, AstropyUserWarning) @pytest.mark.skipif('not HAS_SCIPY') class TestCentroidSources: def setup_class(self): ysize = 50 xsize = 47 yy, xx = np.mgrid[0:ysize, 0:xsize] data = np.zeros((ysize, xsize)) xcen = (1, 25, 25, 35, 46) ycen = (1, 25, 12, 35, 49) for xc, yc in zip(xcen, ycen): model = Gaussian2D(10.0, xc, yc, x_stddev=2, y_stddev=2, theta=0) data += model(xx, yy) self.xpos = xcen self.ypos = ycen self.data = data @staticmethod def test_centroid_sources(): theta = np.pi / 6. model = Gaussian2D(2.4, XCEN, YCEN, x_stddev=3.2, y_stddev=5.7, theta=theta) y, x = np.mgrid[0:50, 0:47] data = model(x, y) error = np.ones(data.shape, dtype=float) mask = np.zeros(data.shape, dtype=bool) mask[10, 10] = True xpos = [25.] ypos = [26.] xc, yc = centroid_sources(data, xpos, ypos, box_size=21, mask=mask) assert_allclose(xc, (25.67,), atol=1e-1) assert_allclose(yc, (26.18,), atol=1e-1) xc, yc = centroid_sources(data, xpos, ypos, error=error, box_size=11, centroid_func=centroid_1dg) assert_allclose(xc, (25.67,), atol=1e-1) assert_allclose(yc, (26.41,), atol=1e-1) with pytest.raises(ValueError): centroid_sources(data, 25, [[26]], box_size=11) with pytest.raises(ValueError): centroid_sources(data, [[25]], 26, box_size=11) with pytest.raises(ValueError): centroid_sources(data, 25, 26, box_size=(1, 2, 3)) with pytest.raises(ValueError): centroid_sources(data, 25, 26, box_size=None, footprint=None) with pytest.raises(ValueError): centroid_sources(data, 25, 26, footprint=np.ones((3, 3, 3))) def test_func(data): return 1 with pytest.raises(ValueError): centroid_sources(data, [25], 26, centroid_func=test_func) @pytest.mark.parametrize('centroid_func', CENTROID_FUNCS) def test_xypos(self, centroid_func): with pytest.raises(ValueError): centroid_sources(self.data, 47, 50, box_size=5, centroid_func=centroid_func) def test_gaussian_fits_npts(self): xcen, ycen = centroid_sources(self.data, self.xpos, self.ypos, box_size=3, centroid_func=centroid_1dg) assert_allclose(xcen, np.full(5, np.nan)) assert_allclose(ycen, np.full(5, np.nan)) xcen, ycen = centroid_sources(self.data, self.xpos, self.ypos, box_size=3, centroid_func=centroid_2dg) xres = np.copy(self.xpos).astype(float) yres = np.copy(self.ypos).astype(float) xres[-1] = np.nan yres[-1] = np.nan assert_allclose(xcen, xres) assert_allclose(ycen, yres) xcen, ycen = centroid_sources(self.data, self.xpos, self.ypos, box_size=5, centroid_func=centroid_1dg) assert_allclose(xcen, xres) assert_allclose(ycen, yres) xcen, ycen = centroid_sources(self.data, self.xpos, self.ypos, box_size=3, centroid_func=centroid_quadratic) assert_allclose(xcen, xres) assert_allclose(ycen, yres) @staticmethod def test_centroid_quadratic_kwargs(): data = np.zeros((11, 11)) data[5, 5] = 100 data[7, 7] = 110 data[9, 9] = 120 xycen1 = centroid_sources(data, xpos=5, ypos=5, box_size=9, centroid_func=centroid_quadratic, fit_boxsize=3) assert_allclose(xycen1, ([9], [9])) xycen2 = centroid_sources(data, xpos=7, ypos=7, box_size=5, centroid_func=centroid_quadratic, fit_boxsize=3) assert_allclose(xycen2, ([9], [9])) xycen3 = centroid_sources(data, xpos=7, ypos=7, box_size=5, centroid_func=centroid_quadratic, xpeak=7, ypeak=7, fit_boxsize=3) assert_allclose(xycen3, ([7], [7])) xycen4 = centroid_sources(data, xpos=5, ypos=5, box_size=5, centroid_func=centroid_quadratic, xpeak=5, ypeak=5, fit_boxsize=3) assert_allclose(xycen4, ([5], [5])) xycen5 = centroid_sources(data, xpos=5, ypos=5, box_size=5, centroid_func=centroid_quadratic, fit_boxsize=5) assert_allclose(xycen5, ([7], [7])) def test_mask(self): mask = np.ones(self.data.shape, dtype=bool) xcen1, ycen1 = centroid_sources(self.data, 25, 23, box_size=(55, 55)) xcen2, ycen2 = centroid_sources(self.data, 25, 23, box_size=(55, 55), mask=mask) assert not np.allclose(xcen1, xcen2) assert not np.allclose(ycen1, ycen2) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize('oversampling', (4, (4, 6))) def test_centroid_epsf(oversampling): sigma = 0.5 psf = IntegratedGaussianPRF(sigma=sigma) offsets = np.array((0.1, 0.03)) if not hasattr(oversampling, '__len__'): _oversampling = (oversampling, oversampling) else: _oversampling = oversampling x = np.arange(1 + 25 * _oversampling[0]) / _oversampling[0] y = np.arange(1 + 25 * _oversampling[1]) / _oversampling[1] x0 = x[-1] / 2 y0 = y[-1] / 2 x -= x0 y -= y0 data = psf.evaluate(x=x.reshape(1, -1), y=y.reshape(-1, 1), flux=1., x_0=offsets[0], y_0=offsets[1], sigma=sigma) mask = np.zeros(data.shape, dtype=bool) data[25, 25] = 1000. mask[25, 25] = True with pytest.warns(AstropyDeprecationWarning): xc, yc = centroid_epsf(data, mask=mask, oversampling=oversampling) desired = np.array((x0, y0)) + offsets assert_allclose((xc, yc), desired, rtol=1e-3, atol=1e-2) def test_centroid_epsf_exceptions(): data = np.ones((5, 5), dtype=float) mask = np.zeros((4, 5), dtype=int) mask[2, 2] = 1 with pytest.warns(AstropyDeprecationWarning): with pytest.raises(ValueError): centroid_epsf(data, mask) with pytest.raises(ValueError): centroid_epsf(data, shift_val=-1) with pytest.raises(ValueError): centroid_epsf(data, oversampling=-1) data = np.ones((21, 21), dtype=float) data[10, 10] = np.inf with pytest.warns(AstropyDeprecationWarning): with pytest.raises(ValueError): centroid_epsf(data) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/centroids/tests/test_gaussian.py0000644000214200020070000001064200000000000022613 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the core module. """ import itertools import warnings from astropy.modeling.models import Gaussian1D, Gaussian2D from astropy.utils.exceptions import AstropyUserWarning import numpy as np from numpy.testing import assert_allclose import pytest from ..gaussian import centroid_1dg, centroid_2dg, _gaussian1d_moments from ...utils._optional_deps import HAS_SCIPY # noqa XCEN = 25.7 YCEN = 26.2 XSTDS = [3.2, 4.0] YSTDS = [5.7, 4.1] THETAS = np.array([30., 45.]) * np.pi / 180. DATA = np.zeros((3, 3)) DATA[0:2, 1] = 1. DATA[1, 0:2] = 1. DATA[1, 1] = 2. # NOTE: the fitting routines in astropy use scipy.optimize @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize(('x_std', 'y_std', 'theta'), list(itertools.product(XSTDS, YSTDS, THETAS))) def test_centroids(x_std, y_std, theta): model = Gaussian2D(2.4, XCEN, YCEN, x_stddev=x_std, y_stddev=y_std, theta=theta) y, x = np.mgrid[0:50, 0:47] data = model(x, y) xc, yc = centroid_1dg(data) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) xc, yc = centroid_2dg(data) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) # test with errors error = np.sqrt(data) xc, yc = centroid_1dg(data, error=error) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) xc, yc = centroid_2dg(data, error=error) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) # test with mask mask = np.zeros(data.shape, dtype=bool) data[10, 10] = 1.e5 mask[10, 10] = True xc, yc = centroid_1dg(data, mask=mask) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) xc, yc = centroid_2dg(data, mask=mask) assert_allclose((xc, yc), (XCEN, YCEN), rtol=0, atol=1.e-3) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize('use_mask', [True, False]) def test_centroids_nan_withmask(use_mask): xc_ref = 24.7 yc_ref = 25.2 model = Gaussian2D(2.4, xc_ref, yc_ref, x_stddev=5.0, y_stddev=5.0) y, x = np.mgrid[0:50, 0:50] data = model(x, y) data[20, :] = np.nan if use_mask: mask = np.zeros(data.shape, dtype=bool) mask[20, :] = True nwarn = 0 else: mask = None nwarn = 1 with warnings.catch_warnings(record=True) as warnlist: xc, yc = centroid_1dg(data, mask=mask) assert_allclose([xc, yc], [xc_ref, yc_ref], rtol=0, atol=1.e-3) assert len(warnlist) == nwarn if nwarn == 1: assert issubclass(warnlist[0].category, AstropyUserWarning) with warnings.catch_warnings(record=True) as warnlist: xc, yc = centroid_2dg(data, mask=mask) assert_allclose([xc, yc], [xc_ref, yc_ref], rtol=0, atol=1.e-3) assert len(warnlist) == nwarn if nwarn == 1: assert issubclass(warnlist[0].category, AstropyUserWarning) @pytest.mark.skipif('not HAS_SCIPY') def test_invalid_mask_shape(): data = np.zeros((4, 4)) mask = np.zeros((2, 2), dtype=bool) with pytest.raises(ValueError): centroid_1dg(data, mask=mask) with pytest.raises(ValueError): centroid_2dg(data, mask=mask) with pytest.raises(ValueError): _gaussian1d_moments(data, mask=mask) @pytest.mark.skipif('not HAS_SCIPY') def test_invalid_error_shape(): error = np.zeros((2, 2), dtype=bool) with pytest.raises(ValueError): centroid_1dg(np.zeros((4, 4)), error=error) with pytest.raises(ValueError): centroid_2dg(np.zeros((4, 4)), error=error) @pytest.mark.skipif('not HAS_SCIPY') def test_centroid_2dg_dof(): data = np.ones((2, 2)) with pytest.raises(ValueError): centroid_2dg(data) def test_gaussian1d_moments(): x = np.arange(100) desired = (75, 50, 5) g = Gaussian1D(*desired) data = g(x) result = _gaussian1d_moments(data) assert_allclose(result, desired, rtol=0, atol=1.e-6) data[0] = 1.e5 mask = np.zeros(data.shape).astype(bool) mask[0] = True result = _gaussian1d_moments(data, mask=mask) assert_allclose(result, desired, rtol=0, atol=1.e-6) data[0] = np.nan mask = np.zeros(data.shape).astype(bool) mask[0] = True with warnings.catch_warnings(record=True) as warnlist: result = _gaussian1d_moments(data, mask=mask) assert_allclose(result, desired, rtol=0, atol=1.e-6) assert len(warnlist) == 1 assert issubclass(warnlist[0].category, AstropyUserWarning) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/photutils/conftest.py0000644000214200020070000000407700000000000016440 0ustar00lbradley# This file is used to configure the behavior of pytest when using the Astropy # test infrastructure. It needs to live inside the package in order for it to # get picked up when running the tests inside an interpreter using # packagename.test import os try: from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS ASTROPY_HEADER = True except ImportError: ASTROPY_HEADER = False def pytest_configure(config): if ASTROPY_HEADER: config.option.astropy_header = True # Customize the following lines to add/remove entries from the # list of packages for which version numbers are displayed when # running the tests. PYTEST_HEADER_MODULES['Cython'] = 'Cython' # noqa PYTEST_HEADER_MODULES['Numpy'] = 'numpy' # noqa PYTEST_HEADER_MODULES['Astropy'] = 'astropy' # noqa PYTEST_HEADER_MODULES['Scipy'] = 'scipy' # noqa PYTEST_HEADER_MODULES['Matplotlib'] = 'matplotlib' # noqa PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' # noqa PYTEST_HEADER_MODULES['scikit-learn'] = 'sklearn' # noqa PYTEST_HEADER_MODULES.pop('Pandas', None) # noqa PYTEST_HEADER_MODULES.pop('h5py', None) # noqa from . import __version__ packagename = os.path.basename(os.path.dirname(__file__)) TESTED_VERSIONS[packagename] = __version__ # Uncomment the last two lines in this block to treat all DeprecationWarnings as # exceptions. For Astropy v2.0 or later, there are 2 additional keywords, # as follow (although default should work for most cases). # To ignore some packages that produce deprecation warnings on import # (in addition to 'compiler', 'scipy', 'pygments', 'ipykernel', and # 'setuptools'), add: # modules_to_ignore_on_import=['module_1', 'module_2'] # To ignore some specific deprecation warning messages for Python version # MAJOR.MINOR or later, add: # warnings_to_ignore_by_pyver={(MAJOR, MINOR): ['Message to ignore']} from astropy.tests.helper import enable_deprecations_as_exceptions # noqa enable_deprecations_as_exceptions() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640123871.987905 photutils-1.3.0/photutils/datasets/0000755000214200020070000000000000000000000016041 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/datasets/__init__.py0000644000214200020070000000032700000000000020154 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for making or loading datasets for examples and tests. """ from .load import * # noqa from .make import * # noqa ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9884531 photutils-1.3.0/photutils/datasets/data/0000755000214200020070000000000000000000000016752 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610514269.0 photutils-1.3.0/photutils/datasets/data/README.rst0000644000214200020070000000025600000000000020444 0ustar00lbradleyThis folder contains data files bundled with `photutils`. fermi_counts.fits.gz -------------------- Fermi LAT counts image created by Christoph Deil (copied from Gammapy). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1402529802.0 photutils-1.3.0/photutils/datasets/data/fermi_counts.fits.gz0000644000214200020070000005066000000000000022764 0ustar00lbradley‹{‰Rfermi_counts_gc.fitsí}m\DZÞY.%ù^$äK¾$› øJFL“Ô‹e3qšZË„%JW†å‹€ ©•Ęä2»«·Ÿ™Y¶Tûl½wõ™³2 ¢v¦OwUuuÕSÕ}ÎÌÜ¿ûáÇìïíýn¡ƒ½ë{Žž}qtüôdïôhïwîïœ>|öùÃãϹ{{¿¿{ðñÝ¿ü~³â÷ðøøá÷{Ÿ?<}¸wúýóCžËtïö_îÞßøÝXñ{öõÓ¿ï}Ñ8?~zøìäñѳ–ßþ_öï½'Î7Lf/é%½¤—ô’^ÒKzI/é†Öåäý»ÝûÝÞëw?¼ýþª’Þ{\¾¾wÖzøÝéYUjñÓêç·ÞºwíÎíOÎûË'·¯]»öóŸÿü—{«ÿ­¸??><9|¶ZéÓ¯÷žýßÃG§k?zzxúÕÑç{w>Y¹Ë†ß›¿ºqó„öÿûõrøÅáñá³G‡’‹œ×ï“?ßþ€_ß½¿Úè·÷ðt£Ì±‡ñ÷ö?8`ù]»ñ«µýþrm³¤Ÿ=:Þ8ó¯Õr?ÿêû“Ç>Yí@Wf] }~tòøÎ§÷îžñ{ýóÃ/7ŒÎÛïã6ìëgO×|^æ»^7_¬Ç탋ëñÞþÈ‚lÖcÃïfÝzðñq¶+ý" ²YžßÙz|–X7#ëñ™¾Ÿ|tp[›ïݧ¿\MõèôáÆêo¬¤þB±ß{·Ö˜¼Öo…'o]»yóÚÍwnܸõæ[·n¼ûúŠáŸî=:><㷙ý7>[ѵ?¼öÞ{_}uëéÓ[''{Ÿnøýáîû÷n¸¿â÷·ÇÏž~þàÑÑ×+oxððW_¬ðxÍ0Dûìß¿óÑÇgþwÿà‚ýž=|z¸6Ûéá“ÓGGÏ÷¾<|vx¼ÒöÙ—gxŽîÞ»ðɧýVî¼iâù=~vrzüõÓ•'« ×ö»öÑïïoìwã7×Öÿ½}pó[o¿uë­_ÿjí×ë9Ÿœ><>=3ÞÃgŸï>~¡ôÊ+þvrxüÍ‹ÕútÃoÿÞ{m=Îø­ÖcóßüWLÜîÜýpí‚+~ͧÎÏwív'{_o†oX>Y±þ±Ïùâšß_÷?ùHô¿GOŽý}ÇÇ/âÞXß¿ûŸÝ_ûßÁ§ßæl=¹µf'ߟœ>]iºŠ®UŒ|}rˆÇlk~ŸìÿaÍïƒîÜþà¿Aà‹ãõRoxl¦¿p1ìüéödzùí+~ß~µÂµ<|1óÏqJçÿÕÓ½¿>Û{øüù“Çï|ÿÁGŸðù’ò[u<›ó·«ñ_?{øÍÃÇOþmŠ«©?|öýٵϿ¾÷Þý?íoìÇÐÛ¢ÙEÚÿߟ޽÷Ñ_.ò[y÷f}÷WéüÙÑwë%Y¥ºÝàëÉóÃG¿X¡Øé…sÅUhìòçýOVëññáéj^>~ôÕá““£g›e¹¾·‰êëëXüø®C¿;Ÿìß>øè“õú~yºB˜=\ßûG_œ~ûðø,2¾9<Þ•gðµŠß ˜QúãÝû+vŸ1’>xxzg ]>|~pøôù:$~ùÍ*k½µ6Å;×o¼}ýÆ;{oÞ¼uã×·Þúí*<\Íëûøí÷Ü1Ç|ÿ¸çO÷?ýp5ßÇ_üñèñ_ÿøìñç««¿¯oæûÇ÷>Ý[I~ô÷“¯Ÿî}ýüóMäþå¿>XáøÊó‚ß _n¯Ømðþ×ïÜüÍM´ßá6¹ÈÅõ½û«á¬ÞX•æ>¼}ÿOoìÿyÿÞÁƒ;«…½ÿË·~ùñ»þõ/^g¦ÆÒ{÷WhuÆï½U(oö« pßÍà¿V¯½~óÖYÉ™eõ‚ßj¾gù| 5üVó=ãÇás‚_«‡^?¸ýûp¿•à·BÓ3~·Þ?¸[ ßÊ~omøíßÛÿäýÏ*ìwÆïÃÃ?ï跲߿›ëd{ëæ‹ê4Éu3ß·7üþº¿ÊÃ|pûÞûì§uÜÌ÷m±žLð[Í÷Œßz®ïôòÛÿËÁºü[ó»÷à£{¨ßu²ÓßTZ5<=X'_=yrôíÆ¿z|rztüý&7®j¾Çë~|ôtU´=ÿútò«œûý^Ë ¿kEÔøý~ÿý»÷~ЋêtøÍz7ô`­Ô‹"øß6yÿÿhó­ÖïîÆ2OŸÙçÖÞõ“G«¢öÑW׿|òðäôéóëß|ëÆµ?ܼ~ãúñ7ßúí;ï¼ûöÍ·|sóÍ6u|tÔøæ]„ï?YW«š{µ¾·öÞX鱿6Ü•r'ÿíw7~±÷¯ÿº÷Æþ7§ûO>_•lëòç­võ/ô|wµÉY÷ø!ÿžïù?_ôüÍwohÛ!Q¿;ë{»w?:+OÏ9ãÕׯ¿[o~7Î}ztVt¬Jê•Sì®K·[„Ÿfý/ŽŽÖ~ÒÜäß¾<}|&ã¹0¬ñ#ÿ|òäÁwÏ<_µ<8¿oÝ\ó}°*¡Ÿ¯xþË/ÜÆ|-…0Äoì7~?–×ßþÅÏßÜûÝïönn|äGcqÓsû-bd 衧 9ñ8ÌüÀþÏ¿Ê9̰ù~uôôðúáÑ·‡Ï®¯KÒë«]ÛÓÇןutzôìúªpöòÁ·‡‡òýƒooÜøíÙ ¾¹qãææø!œ¹YÔßœ[ÔMš_ÃÄ‹ªdÕôâÕª­Õ«ÖÆom¥Õ~ÿÆÍõõM%¸±ù»zÿÖoß}ó×ï¾yã­uã†+ÍýëÆöþ‡ùn.lD½“ðâ ?¦Þøtͽñ#6§˜ou¾\?·ò"‹·¤ž¢jý^>Oó’^ÒKzI/é%½¤Ñ4ÍC;3ɹ̱öÝa^_éSG”é¯éY©ƒ&ÓË{'1Ö÷šæŽ%NÞ6ãyþ.$=—¤ÿ:þTâa4Uͧ'ŸxÇTcæòê×;ÏøˆŒÝé,‡_Æyr^U¾±Æ=u’·ÖYÎq6ÎêQUëq¤éÔë'#±c >» áËK¨÷¤÷#äeó¡‡säi\d>Zß‘óFR\-_–¨S£lœfÇrãªjàžs‹¹i ûB«–«ÂÜ%û?¥H½Þÿb_Ôs¾×#wÔØjsíq«xxx{qv[qšñIϹ}ô}Oô´GåÌuΗÅVUÚ´Š.ËYÛ(¬ŽêQ5~é6IÕs§ñU]÷eûõ𯨫2}¥1K¯gFÉßÖùJ5Uï]¤÷sҒψ3´íœ6wmâ‘›¹ÿ²”u½Ì˜ù’Æçó/×z>òì‹{Ïœ":DúY>3 o"{ú¨ŒŠºä²ÅÏeÓwéôS±§7άšhiöµ×Ÿ£ÏÈñ£({v°Ôù¬i®³ß¥ð®ËÕK]ïmÕÆ•µÝ½-žs=G=wñèµ_lzÎ¥Ë6|¤z\”8_ø©ãÍM™{‘þ?Õçó–â7œÿüŒÝó™ÃG×öKð‹Ë®ÿ4꜎‹ÇmÅÑ4*ž¶±ÏXâó5^Zж±k•þÔ“_©|Ïܸ1ǹNïxÏ&*iÔ³=ýG¬I¦ofï¥%âÿÈÜF½ÛÓ?;fmÐýs4-EFÛÔ§úÌ,Z¿djoné=[³t ÿ¬¾‘öŒ.s“5§Þšui1š¥jÿþ)ÐÈóϾ#¯ÖuÜ-Éï3X3k£´­û@ÙõÞÖ3-?Ç×s¯ïèsï|æ”Ý‹i=²G’§ž[¢Þ£)ZS.ÅFÛÈÛžû(ìÔøpÏsD0`d¶-y½Ô9K4;ç:ÿõðÎÆ\Ä_zq·ç{ô8‹F<ãV‘'æ: B²¾¯l[±‘‘—½?µä3õËÆw¹—)E1µbo”é[ͧúüškáÕë•å_5n4/äKkùŠÜ±äX¯8§¬î_ÅϳvK^JÛÀÌ¥Èé¥mŸ•/ ÷¸ó2žÁJ´ÏÐ\±þûÚ”¿'5ÁXÿޏ—²ÔøöèºísÎ%ÄU–.£Îm³ÎÎò‹øÐeX³UAœ«kûÌYTËsÝÃØ6UœÿZ}·] VÒ¶q¡ªfÈì'qMw„×½²zÇÍÍ3J#íÁÆžziäù~æ,³³k›ùbiõ‡–¸·ãdYõˆ·ŸÄ4mkOhñªXÿÞº¥ºÖÔøáþ6#/ëS#~:;†ŽÍâÛ¨Øé‰ályÙq{IgNž¾ÕBûl«nïáU}.•?ªÖÌ—?—æ>Oñ½ŽùÛ”³ ŒòðÛÕ{Ð <äê–¹êÓj;SÞ&£ýUÒÍãw=þ¡µEjÃȘ¹jLj¬9q[㑟ªÞci|ªÏþ2õFÎe¦¥øxOüõʉʛû»€«Ï5(Eμç#=úôRï³÷ÑZ màõ¯ž\7·Ý%yÈ/ò»•™ÞkK«-G¯MvGp·µkß:«ªjÕÑç #Î’$9#ûæƒ4âY-/Uøó¶ÏZztÕöL޹/õPt­çÖsŽü¶-¢9Û¼cé{o-¤ÅKÔïGÿ>aÅ^¡×‡,<ù|¾;,‘³Çˆoxøyè2Çrä\Å{­ò¢G¿ÑÄùotÏYM6Ý­uÒ~ft_e¿Ñ{ìHî‹à¯g-zr“¥Ç÷cŸˆ/Eë— Íu†cшg¡"ø7 «—ˆ¯ÄoçéÓ«gï^;3Ϊ“$ýzýó²Ö^ÊÎ/š»2û‡ˆ]çüþímŸqTÊϬD'î|Ã+÷§FÛð¯ê:ÅÛ18Sÿ"õüݨ³ )ïÌù J´w¼ã3g^¬ä}í:öãöm‘3XM®&»Ê3ùµ‚*÷’ej…LŸ¥œ{e® ¶ë3$ k3û|Á—ˆ=*cõÍäð9×rTmŠsÏàu¶¾Ÿ;¤{%Q¿òàµÕ‘×;¦Šw5¶ebidÅgí3u¨5ÎsÝC46Öñó³#uðRñÀ’iím<û#Mfï|«ãGÓ+Ûœ½z0":ßž:4CZLdjßÈûê=‰&#;¾Š*yVåæl Ô#ÛÛ¯çÙ÷l_Í®¯:dTQOþ´ø-*kË^Šìû²¼¼X«ôè“yfãyGàÓCž³ Ϙ¬ÜmÔÓªÀ¾è\­Xõ½}žÚ\;'Ä~Úø Ì‹K‘þV¿,¯ ®Z1‹¥{äRûHÜŽ­Ö‰“Ä}oêÈÜÕ“[³ø¤Åp…îÛÆm¤9ÏL­ªá{óÕ#‹[دz ½ùõŸ•rZ&xæ\‰ÑÞübÉÎ×[KDtÒxpuÅzË/¶±êå«­IE~îáåñïL š‘ã!+ž+ꈌþ#rüˆ>•ã´±­ÀÞLŒö÷žÕs2#­:ùiyÐSiü9=­¾^5Y^š^™8¬Æžl !ýfDÔF=X¤½ÏÖ¢Uq—­×~ T•ó­ö›Y¸ É¹"ôãøàøž|š­·qçÔ™ïqÈÆV¶¿$¯gO õåðÞ““zc8’ó¢øÓ›OFDÚÇX¼*dfpbTèåÕs!b³ªúgî<;rçå­“²ò310⹎ª|›Ñ%"¯ê÷çôiiÏVÉwD^çÆUÛ-ãk¿âž—ïÁÝ©ŸOu^ñðÔpÆÂ Í·zlñßJÊ~—‡zöðÜu\‹lò´U¬…´ÈòÊôíõÕª¹gøZõ\Ïo¨4ýz~+(S‡fûJ~Tµ·ôøI6î*kE­oÍOnŽs`gå>§ºfò®u—ð:=“ââ8òݯ\äãçØÿXýGò‘'¼k®­Ëœµ·ßRîˆðÏê…Tñ[b½ý½ùmD]™sõÞ£’*ðWêŸ]‡[yócÕ÷.е}ÅÑ'"/’ë³þV»¢¦å:i}´Tª·½Ô§Þ5¶ÎÒªœ ‡KÒs†^Š`W«dïgGütgê·¿·Vã®q¹4«OtÞ^õU”oÕ˜ ¼ÉR¤æ˜ Û¢y®J©N•bÑ‹èžÉ¢lž‹Ä¼ÅÛ²ƒôÚsÆÏéåsµOJ{‰~Õ}½kÇ}® ûkŸ}óØAò‹+ðº"†‘oT¯ÈumL/yí™cö·‰´š¨7ï{Ö k‡h½ƒçÌÑ¢jߊ̹'Þ´óæ,O‹8¿ã®Y<¬Ü“©#µAd\DžÔ'ãѸæÆië%æemæý®I«Æñø@U:G¼EeIùEš7gw)Oys^4v«jH¯?D)ãVný,FeóJs°oÆF#êÄ^¼ÐâAòY ã<>¦µizxÆZ„úáë]Òæù,§{iÏ¢röó♥_Åù‚–ü¾$é ýíWož•Ö»:ÿzyD×ÇKÑgq¼~Öc3Mf?¯OFùfÛ. q±Óó]ÞœôððÔØÒ˜lM!®ŽˆþÒ~Ek“ö¹sølE=ì¹7è‘Cç¼KÚ𺛼Ÿ9ñê%õÑê1ªËȵÌäG´e¦fÍ~__V¦fc ë¼X˜_D¬/ôúj¶¯„SÞZÄ«&»‘çÙ',L¶ÖÅ’EßGrw]³Ÿ‹4¾\?ézo|jgNø>â/ø¹¡®GâÂÓ_[o/E1жÎ+žû¿™Z4‹S™œEùxmñÊ;¢ò·ðg޼¡µiö‹ÌG‹MO¼j~`ñôÜg­¸wèë݃h9gŽú2“oFS÷¥6®Ô/òœRâý~:ìÓƒŸT¶VÇpr-òÖ4VÿhέÐ%zûDñÔÓó•ÈøŒLlóÔkYÙ]¿Òò@Ô×¢ëéyþ9b#O®[SälÑÞÜoQfŒ—·¶ÿµmv…kïö·ac¦¦”Ú)OOâ­g¼ûªjêÁ.'jµ˜ä›žÏÉZTy¶­ñíñ¥¨ÿ¶×^;H‰sq±ÅÅ`¡°ÆÖðÇöâ·ÖNÉÚ—dó”7F$œáâ–’ö]óžûœ^óúsæ;“­þ^Ì¥zJÏOIºà¾`wúÑo­Ü¤Õ\_«žMœãÚ*j+¯ßèØh}›¡ª:¦‚"qÐKÞg+¬ü©óbʉî<˜JõÉø¢FÃ<¿‰ÍsVüIqÒÚ¥{Ä?K^K¼y4"+’ÿw”¸ÒøbÍÃÉAÒÎ’8ŠÔí–N™Z‚ËyIë6â³%‘ÏR{k òÄi†¬ùzr‡…‰·6Öâ!éáåÁÕa´¯´¦QìÈ`M#m?›±KgÇã5ü|c„21Ðó¸ŸlÝnù­)®Nç÷ Ó¤ÛLòi¬›¼¼P_NiœFVÌk2"Ÿaµâœãѯ—0—qkåÕ^[óÕð0#ÏÓ¯çZ¤¶îÉ•ÜXŒ oÌ[6öú>GÒ^€»Ó“°^ã^kñï‰1ÚÆÅ}Õ籪ê&m ¥µÓÚ£²#c×5Þ«/þþó‹W>ž9á²÷FG¬ŸçÚú¯÷w³}"ýpL¶¾”Ú#zdóŽGÎMÊKÚü«b·——uBßWŸé"z}^#í3q½~¬é¥álçõ‰è~‡# ã¼ëoñ¶æ£­µ¯¹±¼÷ºÖýþiõïµÕ¿ÿ²ú÷_§ó9Dª5‘V‹Ò¶Ìó'yê,éó?Üx­ïšv᯴=uj”‡6xë=ë}¤Þìi÷7ßFÖÉ[ kmšœH®ÌèíÅnI®T_y>Áa\‹c,}­:Õ»&ÚùæUAž¥×Þ“W,ü·Æyðѫ׺?æ†5­÷ ?›ÎòÇzñšb=ÅJŸimhS-GEÖMÃI+Ÿrq¡ù¨äŸVN‘øKý¢ù£‘Sž³ø9Î{¹u·ò$Ó’_¹²rmöÙ)zjWoþá®EmÅÝ·öÖ\_N‡¨NÞšË[ažŒÞ_‹ø,’dÓ¨¸³Zé5Öûx­½_ãÞ+/þþ‡él?r•´IzIzÐ6Ï™P&wxûIÏ(OB;Î׊Km^ÌF}¬>š<üç•˽ÏàÇ»g^Ö#zDÚ£}£>ѧw ès–ÈÓËÛz>ª—4 ÓðŽã#ùm—žïÑ0“ÓMºæÍIkí_䬯ŠNg+jµLÓoGxý ù+åïºJÚÆÒçÀ8~ÑüŠzJ|µ:’æÕiâ}Ú+ZßfpE»fa•ã+pAóá(Ž×•‰ÇÁ(?ºö[°“Ö–ÍgÚGr®ÅS:óÏ®}V©¯Õ'ú,&—⠇ІÜ:Yò"íÙœÏùŠ뛼vf…{Žʵ^cÆ6b»%Çz/Å•”S´˜ëÅ(^õÈð>?çyÞyJµl„8ß¶ðNÃ~O­©=¼×¤…këÅ… ü¤|8[zðSò!K_OÞG^Ü}Iïž\¥a8‡Ù\ìÉåQ|¥mZ>÷òõÆ‘d+úk¾hŒzî­Yï38€}9~’?KßMÙ^kX!å?mœ—<¹!sÖïÉ]Œ†­»ÓÅú¥‘§¶èÍ>™|¯­_Ö¦š_¡/Xuo‹sim¤’bžÃ݉iß™.Ön(Çû¯e±õ´î‘Fb?g‘\01:p¹KÃ8‹?^oóÆuo}®NçÏr<þ/a®¥W¦®¥ý´ýµ¦‡ƒQ\Ö0OÃ?íšæKÒœ=<²ùŒÃŽbm—ÚÚ½·Ÿ1¼4 ×¹½¤—‡+¼8fñ´®yó#ʎȳps½GÖš¸8ðø¤$Ûj—| ¯k˜À½·|Púl¶ôW{­µ­ÉºCñ:r®£åBéµ”#=6çäqyÏë'§¤û(Y1låcÚæñ7ª¯ô žØŽÜG“tiã%ýè{+†²Ï2Eqyýwý,ù:‡ÐïÚ±ø´ØÕÎǦ‰^¨çtÒbyFÉë¿ÖkªG'é8<§î½WdµI¼$ý<ã"ùóÎvÖy°ä#‘œÄñ‘ô±ô¶ôàúxI£ùš…Õ‘8ÐÖei:dc…öçjié^­¦‹Ç_¹qT¦ÇÆœ®¹fF)ê^¿ }[ÎxÍËÅ›¦¯$›ÓÅK–ŸÐ÷8îÙéûU-ÜkºDHÂ+®°wf%éŠzZqÄá2§’šLIžg^œ/zb]ãíÅÚfù#Õûx1^³“•c¸÷<•üTòAí¸þvrºZçTZüïÂ{*ã%µI×½ñEÉ:3òø§¦Sï8ßzÖÕ’ÇñÌ<—"­¡ägôºW&cN‘dJ˜‰Äù!ç÷¨ŽÁñ´ýêdÏSÏÉãÎ<ø¢ñ—Ú´sNž´¶ôA»sd}qyG»¯EûrÏ@qxFåàP$‰Gžõó`úxdÏ<ÁxϼQVÔ$¾TwêÿÜ=IIž¥—öž“ý,œñP4' Eîÿp~â­½­}Ÿ–sü±óKoœp:FúröÃ>Ü9«µŽXÿ!qx§Å€4õ¢¼8²Ö•òçÆõÚßÂ0ÌÜžSz–m)Õª»ooÍ6žüý%_ñâ7ןËURÜq:H÷½½ßË*}î1SòkNljé+­þ¥¾ÐÎf88ùÑ{÷RÌr<%ݽ¯%~šj¾¬ùR#\7)¥ó@:&bËG´±/ª'’tÏŒêAÇzqóg®ÊB\‘âÇIë)ÝÇÔâ ãÆsdùö‘Î)¸>”h~¦Mãgw6ŠºKûiiM¸k´4–ãÑtD}¸ë”8L³|Pk—jtN®·žG9ܾSŠiœ§Æåt²æÄéï­a,½<×¹¿HÞs”­A=6£Ïrã$Üy;qk'­§—g#í™//k^\\aɾT/./pŸáÐ|ŽkËæIîšÕÞ®qòÛk »w ‡7ZÌéÉÉ×|ûã3n6îL¼m§P®¦£„×Ú\"{^äëÍG¨¿fŽ„ã´Íûœ®(ïÊt~ §)f_/Žxr“¶þÈOóY‰—Ä1FÃ>ÉîtNÖs¹Üžû£NíÝOs1"í¯¹{#Ó—òÐjWïþVZ Ï4l–0†{ÏÅ™D_¶°êèåMùJ6ÐHŠWN†äçÚ\iv”|SãMÛ_}ñÏ€#çéÓÇš—gÜ^K50ö‘tm¯Â{G³‰öÛ{’Or˜Ž}P.¶!YüiüÓ>R?Nõ!̹è\_)N-Ìð’7î9œoqÂÉGÿ—润‹vÅñÔ—®Nç×…Û·îÂ{ ³ZÜÓ=?Õ‰óE«­ýŒ·¶÷ÕÖ–óGÔÍ㟴æçè=ÝÆS«û¿qݤ}‡åãŽpëNeKõ —ÃP'É÷¥—óöçOWåaýƒë¡}o•ƒ¿5„:!iëíÁd‹pmaÞ´tF¬˜È{i߃:j¹Aò )7Ð>ÍæfYÄÅ•”Q7:¦bdkC~Ò~”óuîyX”gù ^£×)î#_œëšÚ÷H^yñš“oá ·VNIï%¡Äù«g\›”s9}$ÙFiúr¿Y&ñåj`idá¾å§†i±ê±“¦+µ=WksÏëXø¦éÌa$âú"/Å7ÎÇÂ(-×IûB-v‘8Œ’®5³bíƒ>„|­8Ôöô\¬hØËùW‡pó r¹ù£¾Ú?Ú_Ó–Ã{i<7OÔ‹“ÅÍ­ý¥ûQÉ9]hÞâpš«©ÐæW oëÏ̓¾·~‹ž«QO_8F›3ö‘j Î’Îô:mÇÜ$a g'-N9ì“âV:סüÖÔâýIÒÑ£¿ÔG‹[©N•tçÖR‹-Úoý÷UÒ_Zc-WHx…ûkÜWÒØA¾\Í€:qížïxÒü–úŠÛœµkønØg®ss§ã8>öÒ÷t¿Á=‡¸!a¦fwOþš9ô5ú'ËÒ‡«uè¼ñž¿Xxµ¦öÝJܺâñzQÛóIõG»ÆÅƼ„ãZm3Á5ÚŽñ)żƃúbg+ÖÑŽœOaÌ!îHAÇKù‚ë+Õ T_)þ¸u•jUJÞﲓÖ1±Žå|ë*Œ§g²‡°Ÿ`\› çS’¿qñÍÍ›«q¹z•Ê@âbžÓ[G ƒ¨´ß¢˜˜>Òo Q=¤ïÓÑ|ýñ…«¸ë¯LmÁ£5Ö«Ðg0~¸9LП¾çüÃÚÿHú#_ÎŽxašÎïéu©l|hMÏ«`®A_¡ü¤X¦óÄxÄëT_¼†uŽãt¡üÐîÜþ‡›—•9µµç®q{m®®‘üUõæIÎ/¸vl“ê.ko#á#õA®?wÿypÏO®‰žÝîÀiÞÔNø·ço8 s9~œoq×iŸÆ“Þãábý‰bú.Þ›§|1–´8˜kèçRüÐyá~Jóu.×j¯¹ç÷®B_J¸7k}¸X±Îg,_àlÊ=‹‹²¸5ELAûI¶ÁñO)&ôK/ÎæØc’Ži÷m¸zrbú£Ýp.šè8ŒõöšÓ›ÊåüŸ«ûè|P_Š C¹}=‡ T? [ék´‡7tîôúUè;Me¶qÔŸ^ëVÝÆá37h3ºÁ³(œ ç¸öt.hoÄ_´+Û“ò—Oå#I¶Ã>ÓtÞR|Ð6¬k%œåæ*õ·ìÓdbŒ`,Jµ^×to6E_¦<¥ûÜ&!hDçˆXù•ú—KÑ–T6Ý?ìNçmÜÚ'2–¾§ça­1»µ¡L)Ss÷̨íðû‡Ð'¨ h_û¸~h_´!ú”vomÄa0çSTW)Ïb}%ùíú_û~ø¶¦-ß¶õÄ<„8Eu£:^1œŽtÜ9cö`ý0Mçqw—Ìk®½2÷ oQß« oœó<´­Ö„Ò¹‡%\ŠÏFJøKe`_*S«M%=h_|ÝüL«»è{ëqNÔ§Ðܽj[íÞ±Trûn¬†IW˜œ©L.h|h±*ÅW/Òv®ÆáôD¼§uçCÈïUr;£@{p6@{Q½Zæ±ÖÛããþ•ÚX’Ïù+ýÛx"Qÿår7‡éœ->í <©^í=bç4?Šwë¾ëµjßñÛd½6ýˆ­ˆ›’­Ðwq 9Œâæ<)ïGqÏ¢~´ÆÁyá=.¿µqt-Zô+ÊŸûìy#®nàæËõÕì†x±#­EÓã›»ïCåJ˜.õå®q<¤34ìŽz`ÞÅsN.‡MÓy»q¾.]Ÿ˜vª'?´…V«I˜Bç#í‰[ûU×ú¬éCy¢PGúwšÎÛú5¢Åºn-¶°Þ£>Ì݃D¿Aœçü‡®úü4ý³Ü3üv§‹q޶h23¨?Ðþ8?¬¯h¦±ÁùêÐæ³;·5ž)qµÍ-íÚzM<š~m½©©ž\¾@ü£r9,äòÆ·ÈWª(:?j›W Ã•W&Y'j{/¦éâhìp„ö¡s–êÄb\Ô“‹»†m¾®S^ȃbæs<¿C|šàºåKÆâ¾—«Û<¿0ÏH¾‰:âz!6Ðkt½q´¾—æÃÉåÖ×ý˜âžAÑ8ÁÕ×󱉴a¾¢1߯`<4Â}#êD÷\^ã| ±ªí蜗Ú|0Néßç¯M?>3òOÓùuâöæèóÔF˜÷(Z—O/d"!~#.bô‡Æk¬f3êë˜7h=ñÏÓy_§¼¨­qÎt]±ÖÄúa"íTíËa'ÖèRþ¢¯ƒ¦é"oäÏÅ5Ö/4Žwa<æ: Oé{ô?är%<áâ´ñàäÒXÆ5E]'¦Oû» ©Ïq¹ëU¦ ç8M×Ã/ÃéƒûN?+¯Lë.ԓÛi=ÜÚ[íú‡>„sCŸ£ÔÖ²½FÝÚ8Z'ÒØÆØÃ<έÑDþJ¹Ú‚ž1M0ž{¾»_ÄÅù4Ï8~ý÷ÕéGœ£ÏÒ5ÆùТø€sçð—¶qµ÷#.LÂ{Œ_Êcš.Ötœï´÷íà4ñëÊéÖÚ(_ÄßѯQ6ú]̘Sðo«±éZ¿FôÆzƒö¥±†µ*êŠygš.Η®áUèÏÙ†Æe³óÕé<>N0Žòk„÷)¨|Ê“Êl5ÆÎ4—‹v¡„˜Ìá;µ)òÇqè T¬%^…¾ë¹ýûÿþóêß$}®>Í_#¨nˆ›4?#Ñ1\ŽjxÖâ…ÚabÚû9¬Ã\EóúZ3Á8J¨ òçr·v\ ÷0'¢_IÏp±DmÄåJÎní:â"õqúL bêÁ7qñï±Fŵ£u"½‡GûKøIýwš.ú=Å`N¦„hG¬—<9AzMm1M÷›¸'Ãó1j'<ì‹×p<[Ógwº8?zނפ¸‘Ö•¶ÑýW¿Oðþ ´Q_§s§òwá/­I¸œ¾ü§éÇøhDÇJkƒsnrp^LÓù¹4MÓù«ÅUÛËýlú±Â=¾–öSô/Î…Ë;4)/Ü×Òµ£9ŸÃHNäóêtž?ú5b‡ P& ÛðœþãpèµéüºP»N0ñyÓÜÍí ¸X×r7Ž®õajcnmv }"í­â3ú>‡EWá:—ç¥=.Ú }uG~ˆ#\ÎáÆcžâæÆá5Ú‰êCåp9Q´]ò—ê…÷‰9Ÿ@}&2†Ú®}û+}þŒòi×¹øä°—âÀ.ü£v³úÞ_âæÚx·¿ Ô'©oM¤þ¥üiÜ¢R}§é¼N˜S)íÀ5®îkúpÿZžù'"‡Ö†»Óùß.à0}ïHþMç€×è9Æ ú2}9”Ο‹äGuB›bNÇ9ruÆ4שéÂíwp[~çâ‚ÃHº^QÛ¢ °m\×zbÆqøCyR´æ¤c±^âêÎ8ì¤~€{¤iºÈ_s¥½&â&ú·œí&f,úÚ׆‹Yinxßʦ÷ª8þ¨âåÕößœšC¨~xÇÑ÷R¼ÓyÒkæà>³uë'Å §Ór)_ÄWií&¸Fý±ûÓxäb…öÅ9MЧ®­†ïÎp˜ÂåF‰·ˆ·hKn^?\;:ïs±Ì­'‡ö•ðŒË[Ôfíî¸õäâÇÑë\ç{¸†8ªî}¸ØãäaŽálŽv™˜ñÒ™…tŽ€úáëiº(SŠn ^Çk¸nqzPûãz4ÜHªýÛˆžQáÜѯµx¤ý^›.êLqk‚þéÃÕž­ýÇå>©ŽòêGç„þ3Mϲñ™3<ǧþ¾Cúp±Ž1xúpþÂÍCÊ·hÒÚp1ƒºáÜׄ÷#¹5Çùp~Ž:ríÓt1hîŒI®‘øJxŽþ„²Ú\‘Ú ûJשÿhºsïQgN')†%Ûr2¯À_j#´9žq¾ˆqÌù4g·ÖÎå^úšâ7ú+òhzrxLy#¯ÖÆñB>»pòÁ3Òv‹%ç˜ß&èÏù ­¤û'í:Úw‚6Îo¹ó< 'íËÙ’³›VsQþÜG‹g Ï´¸§íœÿL~Š$ÕY\å-áô·õEý‘/§Êåüû"¦Ó±xåigûèCX#j¸Ku”öáx®%ÙB«%° õxpk ­òÃsl“ü–êÏp˜ÈÉ‘æ%ù•´.´m=î5rÃÖ ®SÌAÞøÜ-â· v¤|QOªG#ÎQÎDú¡]=úMð^Zw Ë´÷ØÎǽªôšÓ‰ówIIÊ^—ÎÛ{®^àêGŽ$,àôàü…ó.o ÎøL±ÄŸÓm-åoËϧé|,q±§E]¸8ÐúIëOek˜C_ã½InWIúqyqbúHçx”'ÚCZNç¿í5÷y"Ä ®æðè%áæΞܺKµ(mãöê\sº£,Ô}­ÍcwâךÚùHy\óO-ÿp¼¨.œí8%¾Ö{ÔÕCXËqëÃÙ…ú!wÊñÔöÏ~#áÙóÄüm:MÐN× ÏÐñŒKÂWi­¹÷k_kÞGë‹|¤s±ÖOÚ3pü´µ§ò8ûj„}$\“ð_søHýHº_GÇIçi\_ª«d+lC½¨Þ8?ŠÇ!®S8ŸÀzÏÔ¸³]#¹¹JØŒ±lá©Ö†ã%[Jc%¬ vi¹¸åôæø5âîIs¢üð5ò”ÆqçO¨/î×è½Ͼ ¯K±Ïù7Æ3.²Î’žZ^àâEÃgf([ò+)†P/꿜ÍRT|Mûr5¾6_mèXÉO¥õÍwœ4 Wµ9¬IºoÃÙ˜Ó‡òÔb–ëO¯{ð µyî ¾ }9INÛ¹= §Ç ñB:Ç”|JâI_Kkƒ¹ÆÚSã>ûI52ÕuõÄ£EÜ3([ª§‰·/æ\©¶Ú™Î¯!7_ëÕ×ŸÚ uôÄ¥ôëF¨;gîs^t¼Eœý9°üCÂxs%–|WÓÓAZi¼GN#i¯/õ—ôÒ°/¢&Óª1i†c$›âûH}!‘´Å>^Lçtôìi¬õãü™;{âò77No­ŽÇùÓqÚwº iñ'=—´#\Ã9xžÁv‹'Îßz-å¡ îùmN7i¿§Í_KÏÎJñ¤í…©¾4/J5„‡¤ùHø"ÕbV OLŒ% Ó¤5¢<½g9Ÿ E1]ó)Ï8mݤ~Üù'[³wÆÂTî:ŽÏ朓ÆOó!ä×^s¸Œ¥Ú»íO4_´Îx°%iß&ù––K¸½ òÅkßÔüÑòEÉæíµtö|¸yI{6ŠÛÔ—$¼°æÄÕø8ÔÊÇkÆRRl7þ8^Ã^‰§„3’–? <¼æ­y%²žgòðÓöO^\çdYþ¯½ÖÖ_Â;mÎ~Xó’â/2ŽÃH‹?¶iö“t²b@ÛKKõ‡Ukb¿Ièká¶£¼æ>wbr׬ë´ÝÚ‡qµ g;mΜI¶œ¦‹±%Õ«–|œ#'¿Éãü ç‹û2ª?÷,Ÿ$OÒŸƒ<=ØÄ½—ôâdFðY›—öI Ë$_ôê$µi¶ÉøµÕ§‘ôYGKô mÝ,ý¼vúKŸoÔb©ýÕâ_cãg¤ñ? ϸXçHÃ'”-á*¾æžßËb…4Î:£ôìMÖ_aúàx_NÓùï³Hª§¡éÑl§}OígéŒï­5ÕtÔæíÍOÞZ¼-~™{—–¾Z?nÆòóúWó4¿Îà=ë&=gãÅCާtÆÂõ—ÖŽê†ØÏÙƒ’ç+ÊäÚ-¬ÔH³½'nPŽõä0É'<û7I~†$½0'xýX‹kË>Ú½s齯³½÷ø3ömï¥3^K¾´–]Qw|ïÅ7.½9‹9Hž=a$†=þAßGìšÁ¯]аtœ†MˆÍÚ}a8ž(“Ó£ý»)ßõ¿«L;÷9Un}µVjãbÏâ£ëü¼Xfá…—´{gžxo¯­¼ÀµsýÑÎQ¼ä®q¾Ëݳ‰ØM#ïz{Ú½q6‡îZ³Î¸1oéºv~®Ù"rFíÁl/I¸­ÉñžƒzH«¹véLÉÃ[²­µf\—·¹ïPãdcÓ°Úš¿–{­½Œû™œ< =û8I'¯ÕŸÊçleñ•òåËÉxP’ö´MËçš-=¹œ#m~Ïy4wFHÿ¡ŸX÷K<ë§µ¡ ½¸kùp…íµqžvϽ&ɇzs”ER¬®_sß%&é—yÀÛÎ]×θ#Äõ®?®¾–~Còu=ß«¦éÊŘÔߛù¾qúdÇc»–k,>‘uF™=ñJùà˜èÚeÈk'ÍϼxÀÕ%˜«q|ë£õóRd="ï5žÞ=¥'§Zí½<1âÍ£¸Ž‘1HÑ{ÐÖñ¦7^{ŽÆÂǨ½9’puŽìSé8‹¤<#á®us#ñcQD¾4ÖŠ¡FÜÙcôŒhÝWú5”cñ¡¹kÒ{ï5/yž‘Åvï&£Ÿ”;µ|êÕAÛçGó¢ô^#­ÎÈðˆ`„×4ì‘Ö"³ïæäYã%LãtÒäHzpm‘û2Ò™ 7Ž;çpÉë#˜o¸ñYŸ±¾óCjËä«­‘tJº®ÉŒ¬-76kcçëZÆæ‘>ÜótŒt¯/³ð`'§‡wŽž:–’öÜG-²¾ç6ŠÛyÎéûèºY¸já{ä^w=ëß.Hkì±{Ã)i÷Yð½?9ûÓ×Ñ3@´Q/þYX̵epŽó-´‹„oÈ;¢«ÖÛ1$ŸÒ°Ð3‡²Ö€ÊõÜWöòò´·k\ªñ¢×¥Üf­ŸÇ75ŸÄ³çu<ùÅËOÃ+oLX|{Ρ½ºS½µ˜Òüųw–j&Êד±/ž yî5[>ì™—Ó$²Ö±Ç_$þŽÈêÕ±±ênL$ιïÅB,Òîé{|ÉsU"/Frã,½¸°ðI{£åîÜQ‹)-V¬|eµ!?©¿‡_fí2ë¦õõ>Ë@ûzÏþ5¹ÙyxÆHµ½¦Õ e|É“«ñLuÕøc—÷´>œž/OÍñåâWúŽø¨ÏDâHÂ?Ú®}Ž<ªö·ÆZ~ç‘]ù,¥fË,ŸÞq¨‡gŸê½.­•çÙ=I?/i1Ê]ó¬³w>)ý4œòÔš<ÔQÓ‹ë#µyúhÏRry0ól¢7ïrkyî š³¹Ü¼¼ë¡É£ÔsvhéÔ“ ‡ÐÆFó¼·¯õ9ò‰<ƒ åGîºÖ¯‚¢ºVñƒ"çá^?·p=ó,Ф„9Úy©%Ǫy²:{ãÊÊmquk„¸Ø“ødy{s9Žáþjü8>–n#(ZJ˜×«£•Ç«ê%¯|nݬø³žYñÆOÖ–^=3Tñl„·Ÿ·¿÷ù`K^eó<®½4/éùĨoKŸQ¤~ÌÍC›[o}á5iŒ4Nš»%#›S¥þ‘³T©†Ç^ŠæÌ^,ª—­,]¢6ˆà’Ͻu”w#6ò>ï\!+ÊÈßÓ~VJ¸§ñÔxEÆôò§ãðþ:òôÌÉò[+GÐ>Z®öÄGE}„y³•$7›Ó35htm¬œ­¬>Öšzô°â#[ÃUÖš’>èãyïs|¸öP¯¢ë8‚zãldŸHަäÉ5²âÝ#ŸŽ“t±ÎÃ<ÏŽeÉSC!~Gòwk¯¾7 ‘õLš7ßkØá£]÷òÒÆgI[ç‘á!ë™@Ižç³Z}y¾ëãÍ‘ÜéµEëŸY³9òÊì©á"¼½|³ç 5biµ³†ñܹU¦Nµ®KñªÅD»¤s:OÔ¾_)B#Æj>áO׌{ÞÜâÙ/D÷ºÕ±%åpÏà3M^ß·úhzjºUåËϼ㣵®§Þ«~VËEÚÚecΪI¥÷™ú•ÒÚn‘{"^]¼ŸýäàµH,TáõnŒ&wD}àñ•žš%Ú?ÂO['î>p7¢úpc#¶³¾ÏÉ’eµ{^Gek¸á“é7'yãÑÂ}J‘Å{ÿ BÑgr+ûFȪٰ֑l„ßÓŽ×=zhü[íæ¡ÌgW=ØÌÕ5\ã­½:Jï›,íúܤÕdR ¥ñ²ê7_´ÖòŒ«Ê¡QYÚó–ÞMâí‰ÿ?‰—÷š…I¾^òú¤uÝŠMO-è‘íå£Õٺ““½ã´v‰¯Çf#bWjëy>/j«¾´Ö¡*'I:U]‹Ê®ä5'Yþå}VŸ»æÙ [¾µIµ ¥ZrkE)#_ª%«eG®k¸Ñóš¶õ®UÆ÷£|GÔÕT™Ó®0¯­1ZNò^«ð…Þqk›ÙcõÆz¦¶âøUc{DvTL­æãÉžºš»9wŠÚÏ[‹{Û£Ôã7ÜÜÑ^ÑïM@Þ–M´ü'áú{ã/ò Ië/QD§¨lnN‘=Œwoáy~_[}%},ž|ÑøTç;K.×··«¬}8ŠÔš}ïmåKoÌj}-}v”kUà‘FÖçü³þ:_8,«¨ÛGÌ%âOY™ÙzËêS9ßQäÑ{y¥WŽ7—Wɯ–1‚¬ZxdD}(»7°Ú¸vo“xõ¬±'ÿhíÖxé3ð_wkÏ~Ö0ÓVõ¹®l®ï­${yäVãGE½#­¯§ö§Ôû]–>ž~=6¶> ÕËy¨!«|À{'wÑg‚¢6‰~dz‡¨m8y;Â?M'IúIû]•g}W þ^¡'xô–Þ[c²µ^ËÆT”<¹VòÇÏ+j©LŸ©½3¼zuék=£êͯ=Ø5—ý8^žšÖ³Þ£âÛs>¦ñöÔÍÞÏÉyëÒªìé[7U¹ SëzõÏøOÏ÷ŒdäZå§é±­|Ðd{çäÙ+XsóÌu®ïcÈ'·ò¹rÄ‚^ÿ“Ö`T¬"ÿ ï;Ïé7˜ñ5®M4oyò]$Æ,ò|-JV ¦õÑÆÐq†{ý*2_§Tx÷UñÍm½¹pi”­O#µñlìY·Œnõ¤÷ZöœÙSc÷ä¼lÜ÷ö‹•/-=¬ÏíFëµõ?ϳQë4M±ïùÏð§ý£µˆ¶&œÞQ{÷Pœˆ¢±Ô«K%ï옞>’/p¾aý7Þâ©é±W¦6P¯ÿö`—ć{ïÕ«"ö½±Âí!¬üìÍß8뻀,²ösa§'ö*åeøEj6Ϻg}9J‘ú®Š,ìlò‘2µž÷û£2*ÉÒ ±£JF´OO²l\Þ1’/nË'4Ûxü×s=²o‰ÔÙ<±u•?Gq®:ÏIµÐ¶)û}¹È#2VùHílɑڪ¨×Ÿ{cÈê7b]=|%,²êhÍÌ]S½õІÓQ~=zDIÚ#H¸&­wdm-òîKz)cÞüX…W;“¼žž±Qþ‘ß/òêßS³aî‰Ì§Êo=ò¢˜à­'{}nDœUðŒÆ[¤6æd ŽEs€ÆÛ’½¦žïfaïÈþ#óÝ–|K·,Eô‘âLó,롹â8ƒcÙ|N×*>â½6JV»Þ;¯lNåÆY±^‘çzÇUä¿êuî?:¶=y”ËWsÆI„‡µïŒê!ÍŽÑ\gÅTðw'"cGP4n%ßœ3ïjDõˆ|µ;‘º·ÊW4œŒ~n7R£oƒªsÖèçm<÷=¢û!麵'ðÈjšÊ:E’˽ÎÈÊÚÛZskœ§¾‰Î)Ã#‹_sÄ|¥Œh¼Js¯Ú›Tçÿß“hßÐSK{rbO]4º†¬¦ŠÜÐ#¯çùí3¨Z®áújäÅ;IŸh¾“â3£gE>¨¢h™oöslÛö¯ «öèÕ±×½u\DŸJþÖÜ"ù,ƒAYŸñPEMé±±«^̪š«vMZ‹‘¹w ¸»d¹Û®—·I=˜R=X©ñÈø}Å~U‹oîŒnIµS…®¾²Ö«bŸÖ›_22=ý<û_iŒG~åïަžøª¦&¯ò{´«r휶ðÆf•NsÆi%Uä©(¯Þ½—µŸ« ϳÜ^=´ë•öߟ*»¢ã¸ÜìñŠzon\Ëø=WÇE0«wSEÙ¼ìÝëfuðú[FŽå5i¯¹xmµ2Êööï•Å­-Wóir³zDj¦ŠÜ¨õÍŽ Yš{ïBåE>ÐHòwš¯9{Æ É‡25~ï^Vºfa~U­ÑkëèøÈï&÷èÞûû–â°¶’¼˜§a×úuö»Ãµ>sû5>Z§Ò÷^UP¤æÊÄCVíºÔó…©<Ôó9oÿŠý ¶-ý;`«hä>.½¹¾G^tL%.J<=ß)nái&pûÔ±ëzö"ÙOË+=ØÒ“w­Üƒmžï¦çøTÕ}3|GÒÒ0¥ZÞè}¯öm¹™¯ mø'3³W÷ò¶ê‘4zŸ¥ÉÕÚ/½5µÕ®õ«¶†éšÍ=:ØTàÒ?ŠêŸù^ù Y5ÈO<µA´¶óø”UõÔȽc2{Ï ]¬ñ=õ+¾ÏÔ#Uy¬»#ûK®£{(‹sëU1nŽXi—¹ê¤mïu¢Øbý¦÷Þ[㌨s¼|²²{ϸ×kꉟž:e›{ƒ9b![x‹°G‡¬xô™ã·d¥~h³h â%MçÞz`„~]ªeTÈ÷®[oáAûFý*òÝ'Ú8Žzëâ5Z–GÕÞªgœ¶/Û“ÒW“ãÙsxøôR$.¼úYØÖ›{²±,é%Ų'ß i>Ð[?õŒ™ƒ¢þ«}_-öÙ&Í•#GÅ$£sUOX|GaaUáï‘gÙ|Äþ!j—9ÖÊ"ÔÙûí‘ëÙ¾~ZîòèQQ'gø{0>BUû9KÆHz׈êPñ{bÒžFÚ·jµ“·–µt©îëå׋u=²{øUî•GÈñ¶EÉ㯕ò$ÞÿQy¡ýÍždìÒ;ߥî[-Q¿¨NôJt¬ç7 ¤qÑ<ãå]‘+µq\Kóñè9c=vË<³™«Ö•W™KçÀÞ ßž\Ø‹¥VííñÙˆ®KÃÏQ¹_â9W}UÅ_Ò¿×nÕu–Ƴ÷=}Ov¿¡ê5ˆîq¤ö{à Ÿ¯ŠsOÏxîw%\Ûž æˆó‘Q5f†ÅXkßÙÚ²Ÿw‰êB_[¾Y—ž½yÅø¬¼È¹Àˆ}Z¤ÞÂ1sî½Xã;'Gúw-¥xŽÎ#º§Í|Wìœ5[ÕÚ Ÿès}’m£v™«NËúyÔ"ò³}GÔ†RåúôÊöäÎ 9=Ï&VQU]݃ÜÚâ:·g3{¯Q˜TÍoDG|¹šæÚ#6VîÍìÅ¢×zó$Çûý·Ú{K‡ ýz×Ú¯}o–g„"õ{V—lÝáÁù¥7>ªö(wDþÞöiTå?Ñš-:ߪõ¨&/®Î×£iiòzða©5ØHûnâ›Ñgî³%’f—LnÙYªµ¼u˜WfeiLU½Ø3wK~fëáÛK\~ªXGm¾YÌãé»ÓFÊ®ðç9b1ËSï¨_Œðo¯œ¹žQÅÞö沞þ~RnÈÈš«V‰âr†gïü2X‘]é«=ºnƒ¢øÛÃ/šGå±(U×{š « ¯WÄQdl¶Två~oŽõ³(£®oG瘷WOÍg=óÕäfÞW‘5¯%'ÏyíÍ™süælf/õ7<éú6ü`)øPEsÆØy2óÝ9ZÞ™Ó?£µŒ•C¤qÖøê}_Ïø*û:«˜‹*üœëÛkß^_©´sÕÞw‰T=—Þó4 ‡zÖ ‹G^ªä‘¥ÍËÒÞ`t>ŠâNvï:jm§õY$mÍæzþº‡Ï\¾á‘¿>¯%œuPù£ëÎ¥ïFðÉä· ÖeÎizs¢—ßœu:í¿í¸¢„Ï|ŒÐÍû\Éœqˆü­ý`µnKñk^£Ï/zp|)6´¨gŽÚïüFyYä=;ÉêâÑuŽÏÂWМ{%”Ë½Ž¶EeEùôìµé{Ëÿ3ò¬¾™ßè]J~ÈüvWškvÏœåSA‘½K¯Œª~V_nNÛÈ×ÙºÛÝ,57y)ºŸó\‹ö—~“ r?¿í<Í‘7¯USÅž·jO@Û<9·ú÷©ÙsÍ×*ñyÔþÜŠ•‘û¿ŒíæŽåm`‡uŽ^¡ÓÎÄóª²{eýZÉ_ã±ä|ÞûÛ=Ù±‹ø‘GŸ±ï­g22Ökƒù<3‡ê3ù|,;z}¥gÏìù¬ó¶Îg4’rxÔ‘zÁKUg=ÛÀÔ¹ðpô\*u‰®CÅ~o„_fiÄÙˆ5&ËkýþZÏ9ÚœµÁ¶øŽÞ£,!oT¬cOÌWŽÑøpk9VY4çÃ6ÈÒgiºFúEöQQŠœqhzK}¹3´Qg'#öܼ3÷ë*È»–ž9/©õÈ¡C… 2q.±~ÿvnŒÌÔ=]«ööò®×\çDÿÈÔ³©àÕK½2³ço£p+ãß#÷Z’Ž–žÿÈñX}†·„Üíc1Om_9zo·íÚ(Ãc„ΣîåeûÀ)nÝGŸÁE÷ѺÑÚ=ÉÊø­ôͨOfeGöò—…zÖ$º·X¿Ïà‡§™ƒ$[-57ŒàÕó “¹k^žK±ÅTU_ôæ/Oûh½î=þ·«\óò—rÓÜ¿:£´}<ñ0Z¿Ñ{Ïì~Ìë÷#ê·LŸÖoÄ^:sÞš‘3'--îªý§bÍFúôe¡Þ³Ñ(æŒÞÓGú¤¹}k®<6÷ÙWeÏTæðÕjª>ïíáQ}žë=Ÿ“|µ"4w]¹”šii{ÒmQï9_v/ºd;WíW²wm=ú SãFåVå¥jUDñÄÛ¿G–Ô–É—½²GаoY>RµŸ_^ž#~óÚcInä»y4æ&ÏœGગÏ;iûIÚ‰LŽ[¢O¬iIº¬©›¬µí©Uæ"¯o¢o{ÇX1¿„ýØ´´3¼*Zzí3âLinÏWužW…CžxŽRv.™}}¯/Ðó—9êK”ïmÏöë¡j|[Âb)¼æ¦HŒ/ažÕ:hŸEâß\±·:+s®é¡Ê߃Ëô™³&Ñr}fO½4º úÍ]ëõòÑ|£÷¼T?çg¸©æ;¢Ϫz?£ËÒã4BK¨‹·µßïÅ3n:õê<·ÌQ䎥Ÿ»hr²ydäœGå›9È[#fæU]cxe\F²ì\U[õ¬IïÞÌÛ÷§²¦¶9Ÿ‘ùmDŽ™«fž³6÷öûsÝÀ 횇·ÇFÖ=|ß‹ŸHUë8çYÒølCÞÒrÍæ6¢žµxnë|ˆk¯¨u·}Oii~­‘ÇîKˆ nüˆß½Íè1§ÜÔ{~iáËœgSÛ®-ç–×kÛŠûTKöó%ë&‘v<"×i|¬séÈ~‚ãÕ³o}®Øãû™ñÑ{‘þÛ¼gâÝ â{ תϽz¨'–4͵WÐ>'^MsÝ“ÑüÞSCY×¢úD© kªög•5ef_‘[éÏ^âµ¥kJ4'¶y~ÕKÛæÃáÆ6ö@saЬôÿ,á«æ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/datasets/load.py0000644000214200020070000002177100000000000017342 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for loading example datasets, from both within photutils and remote servers. """ from urllib.error import HTTPError, URLError from astropy.io import fits from astropy.table import Table from astropy.utils.data import download_file, get_pkg_data_filename __all__ = ['get_path', 'load_spitzer_image', 'load_spitzer_catalog', 'load_irac_psf', 'load_fermi_image', 'load_star_image', 'load_simulated_hst_star_image'] def get_path(filename, location='local', cache=True, show_progress=False): """ Get path (location on your disk) for a given file. Parameters ---------- filename : str File name in the local or remote data folder. location : {'local', 'remote', 'photutils-datasets'} File location. ``'local'`` means bundled with ``photutils``. ``'remote'`` means the astropy data server (or the photutils-datasets repo as a backup) or the Astropy cache on your machine. ``'photutils-datasets'`` means the photutils-datasets repo or the Astropy cache on your machine. cache : bool, optional Whether to cache the contents of remote URLs. Default is `True`. show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- path : str Path (location on your disk) of the file. Examples -------- >>> from astropy.io import fits >>> from photutils.datasets import get_path >>> hdulist = fits.open(get_path('fermi_counts.fits.gz')) """ datasets_url = ('https://github.com/astropy/photutils-datasets/raw/' f'main/data/{filename}') if location == 'local': path = get_pkg_data_filename('data/' + filename) elif location == 'remote': # pragma: no cover try: url = f'https://data.astropy.org/photometry/{filename}' path = download_file(url, cache=cache, show_progress=show_progress) except (URLError, HTTPError): # timeout or not found path = download_file(datasets_url, cache=cache, show_progress=show_progress) elif location == 'photutils-datasets': # pragma: no cover path = download_file(datasets_url, cache=cache, show_progress=show_progress) else: raise ValueError(f'Invalid location: {location}') return path def load_spitzer_image(show_progress=False): # pragma: no cover """ Load a 4.5 micron Spitzer image. The catalog for this image is returned by :func:`load_spitzer_catalog`. Parameters ---------- show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- hdu : `~astropy.io.fits.ImageHDU` The 4.5 micron Spitzer image in a FITS image HDU. See Also -------- load_spitzer_catalog Examples -------- .. plot:: :include-source: from photutils.datasets import load_spitzer_image hdu = load_spitzer_image() plt.imshow(hdu.data, origin='lower', vmax=50) """ path = get_path('spitzer_example_image.fits', location='remote', show_progress=show_progress) hdu = fits.open(path)[0] return hdu def load_spitzer_catalog(show_progress=False): # pragma: no cover """ Load a 4.5 micron Spitzer catalog. The image from which this catalog was derived is returned by :func:`load_spitzer_image`. Parameters ---------- show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- catalog : `~astropy.table.Table` The catalog of sources. See Also -------- load_spitzer_image Examples -------- .. plot:: :include-source: from photutils.datasets import load_spitzer_catalog catalog = load_spitzer_catalog() plt.scatter(catalog['l'], catalog['b']) plt.xlabel('Galactic l') plt.ylabel('Galactic b') plt.xlim(18.39, 18.05) plt.ylim(0.13, 0.30) """ path = get_path('spitzer_example_catalog.xml', location='remote', show_progress=show_progress) table = Table.read(path) return table def load_irac_psf(channel, show_progress=False): # pragma: no cover """ Load a Spitzer IRAC PSF image. Parameters ---------- channel : int (1-4) The IRAC channel number: * Channel 1: 3.6 microns * Channel 2: 4.5 microns * Channel 3: 5.8 microns * Channel 4: 8.0 microns show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- hdu : `~astropy.io.fits.ImageHDU` The IRAC PSF in a FITS image HDU. Examples -------- .. plot:: :include-source: from astropy.visualization import LogStretch, ImageNormalize from photutils.datasets import load_irac_psf hdu1 = load_irac_psf(1) hdu2 = load_irac_psf(2) hdu3 = load_irac_psf(3) hdu4 = load_irac_psf(4) norm = ImageNormalize(hdu1.data, stretch=LogStretch()) fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) ax1.imshow(hdu1.data, origin='lower', interpolation='nearest', norm=norm) ax1.set_title('IRAC Ch1 PSF') ax2.imshow(hdu2.data, origin='lower', interpolation='nearest', norm=norm) ax2.set_title('IRAC Ch2 PSF') ax3.imshow(hdu3.data, origin='lower', interpolation='nearest', norm=norm) ax3.set_title('IRAC Ch3 PSF') ax4.imshow(hdu4.data, origin='lower', interpolation='nearest', norm=norm) ax4.set_title('IRAC Ch4 PSF') plt.tight_layout() plt.show() """ channel = int(channel) if channel < 1 or channel > 4: raise ValueError('channel must be 1, 2, 3, or 4') filepath = f'irac_ch{channel}_flight.fits' path = get_path(filepath, location='remote', show_progress=show_progress) hdu = fits.open(path)[0] return hdu def load_fermi_image(show_progress=False): """ Load a Fermi counts image for the Galactic center region. Parameters ---------- show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- hdu : `~astropy.io.fits.ImageHDU` A FITS image HDU. Examples -------- .. plot:: :include-source: from photutils.datasets import load_fermi_image hdu = load_fermi_image() plt.imshow(hdu.data, vmax=10, origin='lower', interpolation='nearest') """ path = get_path('fermi_counts.fits.gz', location='local', show_progress=show_progress) hdu = fits.open(path)[1] return hdu def load_star_image(show_progress=False): # pragma: no cover """ Load an optical image of stars. This is an image of M67 from photographic data obtained as part of the National Geographic Society - Palomar Observatory Sky Survey (NGS-POSS). The image was digitized from the POSS-I Red plates as part of the Digitized Sky Survey produced at the Space Telescope Science Institute. Parameters ---------- show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- hdu : `~astropy.io.fits.ImageHDU` The M67 image in a FITS image HDU. Examples -------- .. plot:: :include-source: from photutils.datasets import load_star_image hdu = load_star_image() plt.imshow(hdu.data, origin='lower', interpolation='nearest') """ path = get_path('M6707HH.fits', location='remote', show_progress=show_progress) hdu = fits.open(path)[0] return hdu def load_simulated_hst_star_image(show_progress=False): # pragma: no cover """ Load a simulated HST WFC3/IR F160W image of stars. The simulated image does not contain any background or noise. Parameters ---------- show_progress : bool, optional Whether to display a progress bar during the download (default is `False`). Returns ------- hdu : `~astropy.io.fits.ImageHDU` A FITS image HDU containing the simulated HST star image. Examples -------- .. plot:: :include-source: from photutils.datasets import load_simulated_hst_star_image hdu = load_simulated_hst_star_image() plt.imshow(hdu.data, origin='lower', interpolation='nearest') """ path = get_path('hst_wfc3ir_f160w_simulated_starfield.fits', location='photutils-datasets', show_progress=show_progress) hdu = fits.open(path)[0] return hdu ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/datasets/make.py0000644000214200020070000007725400000000000017347 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for making example datasets for examples and tests. """ from astropy import coordinates as coord from astropy.convolution import discretize_model from astropy.io import fits from astropy.modeling import models from astropy.table import QTable import astropy.units as u from astropy.wcs import WCS import numpy as np from ..psf import IntegratedGaussianPRF from ..utils._misc import _get_version_info __all__ = ['apply_poisson_noise', 'make_noise_image', 'make_random_models_table', 'make_random_gaussians_table', 'make_model_sources_image', 'make_gaussian_sources_image', 'make_4gaussians_image', 'make_100gaussians_image', 'make_wcs', 'make_gwcs', 'make_imagehdu', 'make_gaussian_prf_sources_image'] __doctest_requires__ = {('make_gwcs'): ['gwcs']} def apply_poisson_noise(data, seed=None): """ Apply Poisson noise to an array, where the value of each element in the input array represents the expected number of counts. Each pixel in the output array is generated by drawing a random sample from a Poisson distribution whose expectation value is given by the pixel value in the input array. Parameters ---------- data : array-like The array on which to apply Poisson noise. Every pixel in the array must have a positive value (i.e., counts). seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Returns ------- result : `~numpy.ndarray` The data array after applying Poisson noise. See Also -------- make_noise_image Examples -------- .. plot:: :include-source: from photutils.datasets import make_4gaussians_image from photutils.datasets import apply_poisson_noise data1 = make_4gaussians_image(noise=False) data2 = apply_poisson_noise(data1, seed=0) # plot the images import matplotlib.pyplot as plt fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 8)) ax1.imshow(data1, origin='lower', interpolation='nearest') ax1.set_title('Original image') ax2.imshow(data2, origin='lower', interpolation='nearest') ax2.set_title('Original image with Poisson noise applied') """ data = np.asanyarray(data) if np.any(data < 0): raise ValueError('data must not contain any negative values') rng = np.random.default_rng(seed) return rng.poisson(data) def make_noise_image(shape, distribution='gaussian', mean=None, stddev=None, seed=None): r""" Make a noise image containing Gaussian or Poisson noise. Parameters ---------- shape : 2-tuple of int The shape of the output 2D image. distribution : {'gaussian', 'poisson'} The distribution used to generate the random noise: * ``'gaussian'``: Gaussian distributed noise. * ``'poisson'``: Poisson distributed noise. mean : float The mean of the random distribution. Required for both Gaussian and Poisson noise. The default is 0. stddev : float, optional The standard deviation of the Gaussian noise to add to the output image. Required for Gaussian noise and ignored for Poisson noise (the variance of the Poisson distribution is equal to its mean). seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Returns ------- image : 2D `~numpy.ndarray` Image containing random noise. See Also -------- apply_poisson_noise Examples -------- .. plot:: :include-source: # make Gaussian and Poisson noise images from photutils.datasets import make_noise_image shape = (100, 100) image1 = make_noise_image(shape, distribution='gaussian', mean=0., stddev=5.) image2 = make_noise_image(shape, distribution='poisson', mean=5.) # plot the images import matplotlib.pyplot as plt fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) ax1.imshow(image1, origin='lower', interpolation='nearest') ax1.set_title('Gaussian noise ($\mu=0$, $\sigma=5.$)') ax2.imshow(image2, origin='lower', interpolation='nearest') ax2.set_title('Poisson noise ($\mu=5$)') """ if mean is None: raise ValueError('"mean" must be input') rng = np.random.default_rng(seed) if distribution == 'gaussian': if stddev is None: raise ValueError('"stddev" must be input for Gaussian noise') image = rng.normal(loc=mean, scale=stddev, size=shape) elif distribution == 'poisson': image = rng.poisson(lam=mean, size=shape) else: raise ValueError(f'Invalid distribution: {distribution}. Use either ' '"gaussian" or "poisson".') return image def make_random_models_table(n_sources, param_ranges, seed=None): """ Make a `~astropy.table.QTable` containing randomly generated parameters for an Astropy model to simulate a set of sources. Each row of the table corresponds to a source whose parameters are defined by the column names. The parameters are drawn from a uniform distribution over the specified input ranges. The output table can be input into :func:`make_model_sources_image` to create an image containing the model sources. Parameters ---------- n_sources : float The number of random model sources to generate. param_ranges : dict The lower and upper boundaries for each of the model parameters as a dictionary mapping the parameter name to its ``(lower, upper)`` bounds. seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Returns ------- table : `~astropy.table.QTable` A table of parameters for the randomly generated sources. Each row of the table corresponds to a source whose model parameters are defined by the column names. The column names will be the keys of the dictionary ``param_ranges``. See Also -------- make_random_gaussians_table, make_model_sources_image Notes ----- To generate identical parameter values from separate function calls, ``param_ranges`` must have the same parameter ranges and the ``seed`` must be the same. Examples -------- >>> from photutils.datasets import make_random_models_table >>> n_sources = 5 >>> param_ranges = {'amplitude': [500, 1000], ... 'x_mean': [0, 500], ... 'y_mean': [0, 300], ... 'x_stddev': [1, 5], ... 'y_stddev': [1, 5], ... 'theta': [0, np.pi]} >>> sources = make_random_models_table(n_sources, param_ranges, ... seed=0) >>> for col in sources.colnames: ... sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) amplitude x_mean y_mean x_stddev y_stddev theta --------- --------- ---------- --------- --------- --------- 818.48084 456.37779 244.75607 1.7026225 1.1132787 1.2053586 634.89336 303.31789 0.82155005 4.4527157 1.4971331 3.1328274 520.48676 364.74828 257.22128 3.1658449 3.6824977 3.0813851 508.26382 271.8125 10.075673 2.1988476 3.588758 2.1536937 906.63512 467.53621 218.89663 2.6907489 3.4615404 2.0434781 """ rng = np.random.default_rng(seed) meta = {'version': _get_version_info()} sources = QTable(meta=meta) for param_name, (lower, upper) in param_ranges.items(): # Generate a column for every item in param_ranges, even if it # is not in the model (e.g., flux). However, such columns will be # ignored when rendering the image. sources[param_name] = rng.uniform(lower, upper, n_sources) return sources def make_random_gaussians_table(n_sources, param_ranges, seed=None): """ Make a `~astropy.table.QTable` containing randomly generated parameters for 2D Gaussian sources. Each row of the table corresponds to a Gaussian source whose parameters are defined by the column names. The parameters are drawn from a uniform distribution over the specified input ranges. The output table can be input into :func:`make_gaussian_sources_image` to create an image containing the 2D Gaussian sources. Parameters ---------- n_sources : float The number of random Gaussian sources to generate. param_ranges : dict The lower and upper boundaries for each of the `~astropy.modeling.functional_models.Gaussian2D` parameters as a dictionary mapping the parameter name to its ``(lower, upper)`` bounds. The dictionary keys must be valid `~astropy.modeling.functional_models.Gaussian2D` parameter names or ``'flux'``. If ``'flux'`` is specified, but not ``'amplitude'`` then the 2D Gaussian amplitudes will be calculated and placed in the output table. If both ``'flux'`` and ``'amplitude'`` are specified, then ``'flux'`` will be ignored. Model parameters not defined in ``param_ranges`` will be set to the default value. seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Returns ------- table : `~astropy.table.QTable` A table of parameters for the randomly generated Gaussian sources. Each row of the table corresponds to a Gaussian source whose parameters are defined by the column names. See Also -------- make_random_models_table, make_gaussian_sources_image Notes ----- To generate identical parameter values from separate function calls, ``param_ranges`` must have the same parameter ranges and the ``seed`` must be the same. Examples -------- >>> from photutils.datasets import make_random_gaussians_table >>> n_sources = 5 >>> param_ranges = {'amplitude': [500, 1000], ... 'x_mean': [0, 500], ... 'y_mean': [0, 300], ... 'x_stddev': [1, 5], ... 'y_stddev': [1, 5], ... 'theta': [0, np.pi]} >>> sources = make_random_gaussians_table(n_sources, param_ranges, ... seed=0) >>> for col in sources.colnames: ... sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) amplitude x_mean y_mean x_stddev y_stddev theta --------- --------- ---------- --------- --------- --------- 818.48084 456.37779 244.75607 1.7026225 1.1132787 1.2053586 634.89336 303.31789 0.82155005 4.4527157 1.4971331 3.1328274 520.48676 364.74828 257.22128 3.1658449 3.6824977 3.0813851 508.26382 271.8125 10.075673 2.1988476 3.588758 2.1536937 906.63512 467.53621 218.89663 2.6907489 3.4615404 2.0434781 To specifying the flux range instead of the amplitude range: >>> param_ranges = {'flux': [500, 1000], ... 'x_mean': [0, 500], ... 'y_mean': [0, 300], ... 'x_stddev': [1, 5], ... 'y_stddev': [1, 5], ... 'theta': [0, np.pi]} >>> sources = make_random_gaussians_table(n_sources, param_ranges, ... seed=0) >>> for col in sources.colnames: ... sources[col].info.format = '%.8g' # for consistent table output >>> print(sources) flux x_mean y_mean x_stddev y_stddev theta amplitude --------- --------- ---------- --------- --------- --------- --------- 818.48084 456.37779 244.75607 1.7026225 1.1132787 1.2053586 68.723678 634.89336 303.31789 0.82155005 4.4527157 1.4971331 3.1328274 15.157778 520.48676 364.74828 257.22128 3.1658449 3.6824977 3.0813851 7.1055501 508.26382 271.8125 10.075673 2.1988476 3.588758 2.1536937 10.251089 906.63512 467.53621 218.89663 2.6907489 3.4615404 2.0434781 15.492093 Note that in this case the output table contains both a flux and amplitude column. The flux column will be ignored when generating an image of the models using :func:`make_gaussian_sources_image`. """ sources = make_random_models_table(n_sources, param_ranges, seed=seed) # convert Gaussian2D flux to amplitude if 'flux' in param_ranges and 'amplitude' not in param_ranges: model = models.Gaussian2D(x_stddev=1, y_stddev=1) if 'x_stddev' in sources.colnames: xstd = sources['x_stddev'] else: xstd = model.x_stddev.value # default if 'y_stddev' in sources.colnames: ystd = sources['y_stddev'] else: ystd = model.y_stddev.value # default sources = sources.copy() sources['amplitude'] = sources['flux'] / (2. * np.pi * xstd * ystd) return sources def make_model_sources_image(shape, model, source_table, oversample=1): """ Make an image containing sources generated from a user-specified model. Parameters ---------- shape : 2-tuple of int The shape of the output 2D image. model : 2D astropy.modeling.models object The model to be used for rendering the sources. source_table : `~astropy.table.Table` Table of parameters for the sources. Each row of the table corresponds to a source whose model parameters are defined by the column names, which must match the model parameter names. Column names that do not match model parameters will be ignored. Model parameters not defined in the table will be set to the ``model`` default value. oversample : float, optional The sampling factor used to discretize the models on a pixel grid. If the value is 1.0 (the default), then the models will be discretized by taking the value at the center of the pixel bin. Note that this method will not preserve the total flux of very small sources. Otherwise, the models will be discretized by taking the average over an oversampled grid. The pixels will be oversampled by the ``oversample`` factor. Returns ------- image : 2D `~numpy.ndarray` Image containing model sources. See Also -------- make_random_models_table, make_gaussian_sources_image Examples -------- .. plot:: :include-source: from astropy.modeling.models import Moffat2D from photutils.datasets import (make_random_models_table, make_model_sources_image) model = Moffat2D() n_sources = 10 shape = (100, 100) param_ranges = {'amplitude': [100, 200], 'x_0': [0, shape[1]], 'y_0': [0, shape[0]], 'gamma': [5, 10], 'alpha': [1, 2]} sources = make_random_models_table(n_sources, param_ranges, seed=0) data = make_model_sources_image(shape, model, sources) plt.imshow(data) """ image = np.zeros(shape, dtype=float) yidx, xidx = np.indices(shape) params_to_set = [] for param in source_table.colnames: if param in model.param_names: params_to_set.append(param) # Save the initial parameter values so we can set them back when # done with the loop. It's best not to copy a model, because some # models (e.g., PSF models) may have substantial amounts of data in # them. init_params = {param: getattr(model, param) for param in params_to_set} try: for source in source_table: for param in params_to_set: setattr(model, param, source[param]) if oversample == 1: image += model(xidx, yidx) else: image += discretize_model(model, (0, shape[1]), (0, shape[0]), mode='oversample', factor=oversample) finally: for param, value in init_params.items(): setattr(model, param, value) return image def make_gaussian_sources_image(shape, source_table, oversample=1): r""" Make an image containing 2D Gaussian sources. Parameters ---------- shape : 2-tuple of int The shape of the output 2D image. source_table : `~astropy.table.Table` Table of parameters for the Gaussian sources. Each row of the table corresponds to a Gaussian source whose parameters are defined by the column names. With the exception of ``'flux'``, column names that do not match model parameters will be ignored (flux will be converted to amplitude). If both ``'flux'`` and ``'amplitude'`` are present, then ``'flux'`` will be ignored. Model parameters not defined in the table will be set to the default value. oversample : float, optional The sampling factor used to discretize the models on a pixel grid. If the value is 1.0 (the default), then the models will be discretized by taking the value at the center of the pixel bin. Note that this method will not preserve the total flux of very small sources. Otherwise, the models will be discretized by taking the average over an oversampled grid. The pixels will be oversampled by the ``oversample`` factor. Returns ------- image : 2D `~numpy.ndarray` Image containing 2D Gaussian sources. See Also -------- make_model_sources_image, make_random_gaussians_table Examples -------- .. plot:: :include-source: # make a table of Gaussian sources from astropy.table import QTable table = QTable() table['amplitude'] = [50, 70, 150, 210] table['x_mean'] = [160, 25, 150, 90] table['y_mean'] = [70, 40, 25, 60] table['x_stddev'] = [15.2, 5.1, 3., 8.1] table['y_stddev'] = [2.6, 2.5, 3., 4.7] table['theta'] = np.radians(np.array([145., 20., 0., 60.])) # make an image of the sources without noise, with Gaussian # noise, and with Poisson noise from photutils.datasets import make_gaussian_sources_image from photutils.datasets import make_noise_image shape = (100, 200) image1 = make_gaussian_sources_image(shape, table) image2 = image1 + make_noise_image(shape, distribution='gaussian', mean=5., stddev=5.) image3 = image1 + make_noise_image(shape, distribution='poisson', mean=5.) # plot the images import matplotlib.pyplot as plt fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(8, 12)) ax1.imshow(image1, origin='lower', interpolation='nearest') ax1.set_title('Original image') ax2.imshow(image2, origin='lower', interpolation='nearest') ax2.set_title('Original image with added Gaussian noise' ' ($\mu = 5, \sigma = 5$)') ax3.imshow(image3, origin='lower', interpolation='nearest') ax3.set_title('Original image with added Poisson noise ($\mu = 5$)') """ model = models.Gaussian2D(x_stddev=1, y_stddev=1) if 'x_stddev' in source_table.colnames: xstd = source_table['x_stddev'] else: xstd = model.x_stddev.value # default if 'y_stddev' in source_table.colnames: ystd = source_table['y_stddev'] else: ystd = model.y_stddev.value # default colnames = source_table.colnames if 'flux' in colnames and 'amplitude' not in colnames: source_table = source_table.copy() source_table['amplitude'] = (source_table['flux'] / (2. * np.pi * xstd * ystd)) return make_model_sources_image(shape, model, source_table, oversample=oversample) def make_gaussian_prf_sources_image(shape, source_table): r""" Make an image containing 2D Gaussian sources. Parameters ---------- shape : 2-tuple of int The shape of the output 2D image. source_table : `~astropy.table.Table` Table of parameters for the Gaussian sources. Each row of the table corresponds to a Gaussian source whose parameters are defined by the column names. With the exception of ``'flux'``, column names that do not match model parameters will be ignored (flux will be converted to amplitude). If both ``'flux'`` and ``'amplitude'`` are present, then ``'flux'`` will be ignored. Model parameters not defined in the table will be set to the default value. Returns ------- image : 2D `~numpy.ndarray` Image containing 2D Gaussian sources. See Also -------- make_model_sources_image, make_random_gaussians_table Examples -------- .. plot:: :include-source: # make a table of Gaussian sources from astropy.table import QTable table = QTable() table['amplitude'] = [50, 70, 150, 210] table['x_0'] = [160, 25, 150, 90] table['y_0'] = [70, 40, 25, 60] table['sigma'] = [15.2, 5.1, 3., 8.1] # make an image of the sources without noise, with Gaussian # noise, and with Poisson noise from photutils.datasets import make_gaussian_prf_sources_image from photutils.datasets import make_noise_image shape = (100, 200) image1 = make_gaussian_prf_sources_image(shape, table) image2 = (image1 + make_noise_image(shape, distribution='gaussian', mean=5., stddev=5.)) image3 = (image1 + make_noise_image(shape, distribution='poisson', mean=5.)) # plot the images import matplotlib.pyplot as plt fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(8, 12)) ax1.imshow(image1, origin='lower', interpolation='nearest') ax1.set_title('Original image') ax2.imshow(image2, origin='lower', interpolation='nearest') ax2.set_title('Original image with added Gaussian noise' ' ($\mu = 5, \sigma = 5$)') ax3.imshow(image3, origin='lower', interpolation='nearest') ax3.set_title('Original image with added Poisson noise ($\mu = 5$)') """ model = IntegratedGaussianPRF(sigma=1) if 'sigma' in source_table.colnames: sigma = source_table['sigma'] else: sigma = model.sigma.value # default colnames = source_table.colnames if 'flux' not in colnames and 'amplitude' in colnames: source_table = source_table.copy() source_table['flux'] = (source_table['amplitude'] * (2. * np.pi * sigma * sigma)) return make_model_sources_image(shape, model, source_table, oversample=1) def make_4gaussians_image(noise=True): """ Make an example image containing four 2D Gaussians plus a constant background. The background has a mean of 5. If ``noise`` is `True`, then Gaussian noise with a mean of 0 and a standard deviation of 5 is added to the output image. Parameters ---------- noise : bool, optional Whether to include noise in the output image (default is `True`). Returns ------- image : 2D `~numpy.ndarray` Image containing four 2D Gaussian sources. See Also -------- make_100gaussians_image Examples -------- .. plot:: :include-source: from photutils.datasets import make_4gaussians_image image = make_4gaussians_image() plt.imshow(image, origin='lower', interpolation='nearest') """ table = QTable() table['amplitude'] = [50, 70, 150, 210] table['x_mean'] = [160, 25, 150, 90] table['y_mean'] = [70, 40, 25, 60] table['x_stddev'] = [15.2, 5.1, 3., 8.1] table['y_stddev'] = [2.6, 2.5, 3., 4.7] table['theta'] = np.radians(np.array([145., 20., 0., 60.])) shape = (100, 200) data = make_gaussian_sources_image(shape, table) + 5. if noise: rng = np.random.RandomState(12345) data += rng.normal(loc=0., scale=5., size=shape) return data def make_100gaussians_image(noise=True): """ Make an example image containing 100 2D Gaussians plus a constant background. The background has a mean of 5. If ``noise`` is `True`, then Gaussian noise with a mean of 0 and a standard deviation of 2 is added to the output image. Parameters ---------- noise : bool, optional Whether to include noise in the output image (default is `True`). Returns ------- image : 2D `~numpy.ndarray` Image containing 100 2D Gaussian sources. See Also -------- make_4gaussians_image Examples -------- .. plot:: :include-source: from photutils.datasets import make_100gaussians_image image = make_100gaussians_image() plt.imshow(image, origin='lower', interpolation='nearest') """ n_sources = 100 flux_range = [500, 1000] xmean_range = [0, 500] ymean_range = [0, 300] xstddev_range = [1, 5] ystddev_range = [1, 5] params = {'flux': flux_range, 'x_mean': xmean_range, 'y_mean': ymean_range, 'x_stddev': xstddev_range, 'y_stddev': ystddev_range, 'theta': [0, 2 * np.pi]} rng = np.random.RandomState(12345) sources = QTable() for param_name, (lower, upper) in params.items(): # Generate a column for every item in param_ranges, even if it # is not in the model (e.g., flux). However, such columns will # be ignored when rendering the image. sources[param_name] = rng.uniform(lower, upper, n_sources) xstd = sources['x_stddev'] ystd = sources['y_stddev'] sources['amplitude'] = sources['flux'] / (2. * np.pi * xstd * ystd) shape = (300, 500) data = make_gaussian_sources_image(shape, sources) + 5. if noise: rng = np.random.RandomState(12345) data += rng.normal(loc=0., scale=2., size=shape) return data def make_wcs(shape, galactic=False): """ Create a simple celestial `~astropy.wcs.WCS` object in either the ICRS or Galactic coordinate frame. Parameters ---------- shape : 2-tuple of int The shape of the 2D array to be used with the output `~astropy.wcs.WCS` object. galactic : bool, optional If `True`, then the output WCS will be in the Galactic coordinate frame. If `False` (default), then the output WCS will be in the ICRS coordinate frame. Returns ------- wcs : `astropy.wcs.WCS` object The world coordinate system (WCS) transformation. See Also -------- make_gwcs, make_imagehdu Notes ----- The `make_gwcs` function returns an equivalent WCS transformation to this one, but as a `gwcs.wcs.WCS` object. Examples -------- >>> from photutils.datasets import make_wcs >>> shape = (100, 100) >>> wcs = make_wcs(shape) >>> print(wcs.wcs.crpix) # doctest: +FLOAT_CMP [50. 50.] >>> print(wcs.wcs.crval) # doctest: +FLOAT_CMP [197.8925 -1.36555556] """ wcs = WCS(naxis=2) rho = np.pi / 3. scale = 0.1 / 3600. # 0.1 arcsec/pixel in deg/pix wcs.pixel_shape = shape wcs.wcs.crpix = [shape[1] / 2, shape[0] / 2] # 1-indexed (x, y) wcs.wcs.crval = [197.8925, -1.36555556] wcs.wcs.cunit = ['deg', 'deg'] wcs.wcs.cd = [[-scale * np.cos(rho), scale * np.sin(rho)], [scale * np.sin(rho), scale * np.cos(rho)]] if not galactic: wcs.wcs.radesys = 'ICRS' wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] else: wcs.wcs.ctype = ['GLON-CAR', 'GLAT-CAR'] return wcs def make_gwcs(shape, galactic=False): """ Create a simple celestial gWCS object in the ICRS coordinate frame. This function requires the `gwcs `_ package. Parameters ---------- shape : 2-tuple of int The shape of the 2D array to be used with the output `~gwcs.wcs.WCS` object. galactic : bool, optional If `True`, then the output WCS will be in the Galactic coordinate frame. If `False` (default), then the output WCS will be in the ICRS coordinate frame. Returns ------- wcs : `gwcs.wcs.WCS` object The generalized world coordinate system (WCS) transformation. See Also -------- make_wcs, make_imagehdu Notes ----- The `make_wcs` function returns an equivalent WCS transformation to this one, but as an `astropy.wcs.WCS` object. Examples -------- >>> from photutils.datasets import make_gwcs >>> shape = (100, 100) >>> gwcs = make_gwcs(shape) >>> print(gwcs) From Transform -------- ---------------- detector linear_transform icrs None """ from gwcs import wcs as gwcs_wcs from gwcs import coordinate_frames as cf rho = np.pi / 3. scale = 0.1 / 3600. # 0.1 arcsec/pixel in deg/pix shift_by_crpix = (models.Shift((-shape[1] / 2) + 1) & models.Shift((-shape[0] / 2) + 1)) cd_matrix = np.array([[-scale * np.cos(rho), scale * np.sin(rho)], [scale * np.sin(rho), scale * np.cos(rho)]]) rotation = models.AffineTransformation2D(cd_matrix, translation=[0, 0]) rotation.inverse = models.AffineTransformation2D( np.linalg.inv(cd_matrix), translation=[0, 0]) tan = models.Pix2Sky_TAN() celestial_rotation = models.RotateNative2Celestial(197.8925, -1.36555556, 180.0) det2sky = shift_by_crpix | rotation | tan | celestial_rotation det2sky.name = 'linear_transform' detector_frame = cf.Frame2D(name='detector', axes_names=('x', 'y'), unit=(u.pix, u.pix)) if galactic: sky_frame = cf.CelestialFrame(reference_frame=coord.Galactic(), name='galactic', unit=(u.deg, u.deg)) else: sky_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), name='icrs', unit=(u.deg, u.deg)) pipeline = [(detector_frame, det2sky), (sky_frame, None)] return gwcs_wcs.WCS(pipeline) def make_imagehdu(data, wcs=None): """ Create a FITS `~astropy.io.fits.ImageHDU` containing the input 2D image. Parameters ---------- data : 2D array-like The input 2D data. wcs : `None` or `~astropy.wcs.WCS`, optional The world coordinate system (WCS) transformation to include in the output FITS header. Returns ------- image_hdu : `~astropy.io.fits.ImageHDU` The FITS `~astropy.io.fits.ImageHDU`. See Also -------- make_wcs Examples -------- >>> from photutils.datasets import make_imagehdu, make_wcs >>> shape = (100, 100) >>> data = np.ones(shape) >>> wcs = make_wcs(shape) >>> hdu = make_imagehdu(data, wcs=wcs) >>> print(hdu.data.shape) (100, 100) """ data = np.asanyarray(data) if data.ndim != 2: raise ValueError('data must be a 2D array') if wcs is not None: header = wcs.to_header() else: header = None return fits.ImageHDU(data, header=header) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9893372 photutils-1.3.0/photutils/datasets/tests/0000755000214200020070000000000000000000000017203 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/datasets/tests/__init__.py0000644000214200020070000000000000000000000021302 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/datasets/tests/test_load.py0000644000214200020070000000105300000000000021532 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the load module. """ import pytest from .. import get_path, load def test_get_path(): with pytest.raises(ValueError): get_path('filename', location='invalid') def test_load_fermi_image(): hdu = load.load_fermi_image() assert len(hdu.header) == 81 assert hdu.data.shape == (201, 401) @pytest.mark.remote_data def test_load_star_image(): hdu = load.load_star_image() assert len(hdu.header) == 104 assert hdu.data.shape == (1059, 1059) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/datasets/tests/test_make.py0000644000214200020070000001450000000000000021531 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the make module. """ from astropy.modeling.models import Moffat2D from astropy.table import Table import numpy as np from numpy.testing import assert_allclose import pytest from .. import (apply_poisson_noise, make_4gaussians_image, make_100gaussians_image, make_gaussian_prf_sources_image, make_gaussian_sources_image, make_gwcs, make_model_sources_image, make_noise_image, make_random_gaussians_table, make_random_models_table, make_wcs) from ...utils._optional_deps import HAS_GWCS, HAS_SCIPY # noqa SOURCE_TABLE = Table() SOURCE_TABLE['flux'] = [1, 2, 3] SOURCE_TABLE['x_mean'] = [30, 50, 70.5] SOURCE_TABLE['y_mean'] = [50, 50, 50.5] SOURCE_TABLE['x_stddev'] = [1, 2, 3.5] SOURCE_TABLE['y_stddev'] = [2, 1, 3.5] SOURCE_TABLE['theta'] = np.array([0., 30, 50]) * np.pi / 180. SOURCE_TABLE_PRF = Table() SOURCE_TABLE_PRF['x_0'] = [30, 50, 70.5] SOURCE_TABLE_PRF['y_0'] = [50, 50, 50.5] # Without sigma, make_gaussian_prf_sources_image will default to sigma = 1 # so we can ignore it when converting to amplitude SOURCE_TABLE_PRF['amplitude'] = np.array([1, 2, 3]) / (2 * np.pi) def test_make_noise_image(): shape = (100, 100) image = make_noise_image(shape, 'gaussian', mean=0., stddev=2.) assert image.shape == shape assert_allclose(image.mean(), 0., atol=1.) def test_make_noise_image_poisson(): shape = (100, 100) image = make_noise_image(shape, 'poisson', mean=1.) assert image.shape == shape assert_allclose(image.mean(), 1., atol=1.) def test_make_noise_image_nomean(): """Test if ValueError raises if mean is not input.""" with pytest.raises(ValueError): shape = (100, 100) make_noise_image(shape, 'gaussian', stddev=2.) def test_make_noise_image_nostddev(): """ Test if ValueError raises if stddev is not input for Gaussian noise. """ with pytest.raises(ValueError): shape = (100, 100) make_noise_image(shape, 'gaussian', mean=2.) def test_apply_poisson_noise(): shape = (100, 100) data = np.ones(shape) result = apply_poisson_noise(data) assert result.shape == shape assert_allclose(result.mean(), 1., atol=1.) def test_apply_poisson_noise_negative(): """Test if negative image values raises ValueError.""" with pytest.raises(ValueError): shape = (100, 100) data = np.zeros(shape) - 1. apply_poisson_noise(data) def test_make_gaussian_sources_image(): shape = (100, 100) image = make_gaussian_sources_image(shape, SOURCE_TABLE) assert image.shape == shape assert_allclose(image.sum(), SOURCE_TABLE['flux'].sum()) @pytest.mark.skipif('not HAS_SCIPY') def test_make_gaussian_prf_sources_image(): shape = (100, 100) image = make_gaussian_prf_sources_image(shape, SOURCE_TABLE_PRF) assert image.shape == shape # Without sigma in table, image assumes sigma = 1 flux = SOURCE_TABLE_PRF['amplitude'] * (2 * np.pi) assert_allclose(image.sum(), flux.sum()) def test_make_gaussian_sources_image_amplitude(): table = SOURCE_TABLE.copy() table.remove_column('flux') table['amplitude'] = [1, 2, 3] shape = (100, 100) image = make_gaussian_sources_image(shape, table) assert image.shape == shape def test_make_gaussian_sources_image_oversample(): shape = (100, 100) image = make_gaussian_sources_image(shape, SOURCE_TABLE, oversample=10) assert image.shape == shape assert_allclose(image.sum(), SOURCE_TABLE['flux'].sum()) def test_make_random_gaussians_table(): n_sources = 5 param_ranges = dict([('amplitude', [500, 1000]), ('x_mean', [0, 500]), ('y_mean', [0, 300]), ('x_stddev', [1, 5]), ('y_stddev', [1, 5]), ('theta', [0, np.pi])]) table = make_random_gaussians_table(n_sources, param_ranges, seed=0) assert len(table) == n_sources def test_make_random_gaussians_table_flux(): n_sources = 5 param_ranges = dict([('flux', [500, 1000]), ('x_mean', [0, 500]), ('y_mean', [0, 300]), ('x_stddev', [1, 5]), ('y_stddev', [1, 5]), ('theta', [0, np.pi])]) table = make_random_gaussians_table(n_sources, param_ranges, seed=0) assert 'amplitude' in table.colnames assert len(table) == n_sources def test_make_4gaussians_image(): shape = (100, 200) data_sum = 176219.18059091491 image = make_4gaussians_image() assert image.shape == shape assert_allclose(image.sum(), data_sum, rtol=1.e-6) def test_make_100gaussians_image(): shape = (300, 500) data_sum = 826182.24501251709 image = make_100gaussians_image() assert image.shape == shape assert_allclose(image.sum(), data_sum, rtol=1.e-6) def test_make_random_models_table(): model = Moffat2D(amplitude=1) param_ranges = {'x_0': (0, 300), 'y_0': (0, 500), 'gamma': (1, 3), 'alpha': (1.5, 3)} source_table = make_random_models_table(10, param_ranges) # most of the make_model_sources_image options are exercised in the # make_gaussian_sources_image tests image = make_model_sources_image((300, 500), model, source_table) assert image.sum() > 1 def test_make_wcs(): shape = (100, 200) wcs = make_wcs(shape) assert wcs.pixel_shape == shape assert wcs.wcs.radesys == 'ICRS' wcs = make_wcs(shape, galactic=True) assert wcs.wcs.ctype[0] == 'GLON-CAR' assert wcs.wcs.ctype[1] == 'GLAT-CAR' @pytest.mark.skipif('not HAS_GWCS') def test_make_gwcs(): shape = (100, 200) wcs = make_gwcs(shape) assert wcs.pixel_n_dim == 2 assert wcs.available_frames == ['detector', 'icrs'] assert wcs.output_frame.name == 'icrs' assert wcs.output_frame.axes_names == ('lon', 'lat') wcs = make_gwcs(shape, galactic=True) assert wcs.pixel_n_dim == 2 assert wcs.available_frames == ['detector', 'galactic'] assert wcs.output_frame.name == 'galactic' assert wcs.output_frame.axes_names == ('lon', 'lat') @pytest.mark.skipif('not HAS_GWCS') def test_make_wcs_compare(): shape = (200, 300) wcs = make_wcs(shape) gwcs_obj = make_gwcs(shape) sc1 = wcs.pixel_to_world((50, 75), (50, 100)) sc2 = gwcs_obj.pixel_to_world((50, 75), (50, 100)) assert_allclose(sc1.ra, sc2.ra) assert_allclose(sc1.dec, sc2.dec) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9913957 photutils-1.3.0/photutils/detection/0000755000214200020070000000000000000000000016207 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/photutils/detection/__init__.py0000644000214200020070000000047700000000000020330 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for detecting sources in an astronomical image. """ from .core import * # noqa from .daofinder import * # noqa from .irafstarfinder import * # noqa from .peakfinder import * # noqa from .starfinder import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/photutils/detection/core.py0000644000214200020070000002277300000000000017524 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module implements the base class and star finder kernel for detecting stars in an astronomical image. Each star-finding class should define a method called ``find_stars`` that finds stars in an image. """ import abc import math import warnings from astropy.stats import gaussian_fwhm_to_sigma import numpy as np from .peakfinder import find_peaks from ..utils.exceptions import NoDetectionsWarning __all__ = ['StarFinderBase'] class StarFinderBase(metaclass=abc.ABCMeta): """ Abstract base class for star finders. """ def __call__(self, data, mask=None): return self.find_stars(data, mask=mask) @staticmethod def _find_stars(convolved_data, kernel, threshold, *, min_separation=0.0, mask=None, exclude_border=False): """ Find stars in an image. Parameters ---------- convolved_data : 2D array_like The convolved 2D array. kernel : `_StarFinderKernel` The convolution kernel. threshold : float The absolute image value above which to select sources. This threshold should be the threshold input to the star finder class multiplied by the kernel relerr. min_separation : float, optional The minimum separation for detected objects in pixels. mask : 2D bool array, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when searching for stars. exclude_border : bool, optional Set to `True` to exclude sources found within half the size of the convolution kernel from the image borders. The default is `False`, which is the mode used by IRAF's `DAOFIND`_ and `starfind`_ tasks. Returns ------- result : Nx2 `~numpy.ndarray` A Nx2 array containing the (x, y) pixel coordinates. .. _DAOFIND: https://iraf.net/irafhelp.php?val=daofind .. _starfind: https://iraf.net/irafhelp.php?val=starfind """ # define a local footprint for the peak finder if min_separation == 0: # daofind if isinstance(kernel, np.ndarray): footprint = np.ones(kernel.shape) else: footprint = kernel.mask.astype(bool) else: # define a local circular footprint for the peak finder idx = np.arange(-min_separation, min_separation + 1) xx, yy = np.meshgrid(idx, idx) footprint = np.array((xx**2 + yy**2) <= min_separation**2, dtype=int) # pad the convolved data and mask by half the kernel size (or # x/y radius) to allow for detections near the edges if isinstance(kernel, np.ndarray): ypad = (kernel.shape[0] - 1) // 2 xpad = (kernel.shape[1] - 1) // 2 else: ypad = kernel.yradius xpad = kernel.xradius if not exclude_border: pad = ((ypad, ypad), (xpad, xpad)) pad_mode = 'constant' convolved_data = np.pad(convolved_data, pad, mode=pad_mode, constant_values=0.0) if mask is not None: mask = np.pad(mask, pad, mode=pad_mode, constant_values=False) # find local peaks in the convolved data # suppress any NoDetectionsWarning from find_peaks with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=NoDetectionsWarning) tbl = find_peaks(convolved_data, threshold, footprint=footprint, mask=mask) if exclude_border: xmax = convolved_data.shape[1] - xpad ymax = convolved_data.shape[0] - ypad mask = ((tbl['x_peak'] > xpad) & (tbl['y_peak'] > ypad) & (tbl['x_peak'] < xmax) & (tbl['y_peak'] < ymax)) tbl = tbl[mask] if tbl is None: return None xpos, ypos = tbl['x_peak'], tbl['y_peak'] if not exclude_border: xpos -= xpad ypos -= ypad return np.transpose((xpos, ypos)) @abc.abstractmethod def find_stars(self, data, mask=None): """ Find stars in an astronomical image. Parameters ---------- data : 2D array_like The 2D image array. mask : 2D bool array, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when searching for stars. Returns ------- table : `~astropy.table.Table` or `None` A table of found stars. If no stars are found then `None` is returned. """ raise NotImplementedError('Needs to be implemented in a subclass.') class _StarFinderKernel: """ Container class for a 2D Gaussian density enhancement kernel. The kernel has negative wings and sums to zero. It is used by both `DAOStarFinder` and `IRAFStarFinder`. Parameters ---------- fwhm : float The full-width half-maximum (FWHM) of the major axis of the Gaussian kernel in units of pixels. ratio : float, optional The ratio of the minor and major axis standard deviations of the Gaussian kernel. ``ratio`` must be strictly positive and less than or equal to 1.0. The default is 1.0 (i.e., a circular Gaussian kernel). theta : float, optional The position angle (in degrees) of the major axis of the Gaussian kernel, measured counter-clockwise from the positive x axis. sigma_radius : float, optional The truncation radius of the Gaussian kernel in units of sigma (standard deviation) [``1 sigma = FWHM / 2.0*sqrt(2.0*log(2.0))``]. The default is 1.5. normalize_zerosum : bool, optional Whether to normalize the Gaussian kernel to have zero sum, The default is `True`, which generates a density-enhancement kernel. Notes ----- The class attributes include the dimensions of the elliptical kernel and the coefficients of a 2D elliptical Gaussian function expressed as: ``f(x,y) = A * exp(-g(x,y))`` where ``g(x,y) = a*(x-x0)**2 + 2*b*(x-x0)*(y-y0) + c*(y-y0)**2`` References ---------- .. [1] https://en.wikipedia.org/wiki/Gaussian_function """ def __init__(self, fwhm, ratio=1.0, theta=0.0, sigma_radius=1.5, normalize_zerosum=True): if fwhm < 0: raise ValueError('fwhm must be positive.') if ratio <= 0 or ratio > 1: raise ValueError('ratio must be positive and less or equal ' 'than 1.') if sigma_radius <= 0: raise ValueError('sigma_radius must be positive.') self.fwhm = fwhm self.ratio = ratio self.theta = theta self.sigma_radius = sigma_radius self.xsigma = self.fwhm * gaussian_fwhm_to_sigma self.ysigma = self.xsigma * self.ratio theta_radians = np.deg2rad(self.theta) cost = np.cos(theta_radians) sint = np.sin(theta_radians) xsigma2 = self.xsigma**2 ysigma2 = self.ysigma**2 self.a = (cost**2 / (2.0 * xsigma2)) + (sint**2 / (2.0 * ysigma2)) # CCW self.b = 0.5 * cost * sint * ((1.0 / xsigma2) - (1.0 / ysigma2)) self.c = (sint**2 / (2.0 * xsigma2)) + (cost**2 / (2.0 * ysigma2)) # find the extent of an ellipse with radius = sigma_radius*sigma; # solve for the horizontal and vertical tangents of an ellipse # defined by g(x,y) = f self.f = self.sigma_radius**2 / 2.0 denom = (self.a * self.c) - self.b**2 # nx and ny are always odd self.nx = 2 * int(max(2, math.sqrt(self.c * self.f / denom))) + 1 self.ny = 2 * int(max(2, math.sqrt(self.a * self.f / denom))) + 1 self.xc = self.xradius = self.nx // 2 self.yc = self.yradius = self.ny // 2 # define the kernel on a 2D grid yy, xx = np.mgrid[0:self.ny, 0:self.nx] self.circular_radius = np.sqrt((xx - self.xc)**2 + (yy - self.yc)**2) self.elliptical_radius = (self.a * (xx - self.xc)**2 + 2.0 * self.b * (xx - self.xc) * (yy - self.yc) + self.c * (yy - self.yc)**2) self.mask = np.where( (self.elliptical_radius <= self.f) | (self.circular_radius <= 2.0), 1, 0).astype(int) self.npixels = self.mask.sum() # NOTE: the central (peak) pixel of gaussian_kernel has a value of 1. self.gaussian_kernel_unmasked = np.exp(-self.elliptical_radius) self.gaussian_kernel = self.gaussian_kernel_unmasked * self.mask # denom = variance * npixels denom = ((self.gaussian_kernel**2).sum() - (self.gaussian_kernel.sum()**2 / self.npixels)) self.relerr = 1.0 / np.sqrt(denom) # normalize the kernel to zero sum if normalize_zerosum: self.data = ((self.gaussian_kernel - (self.gaussian_kernel.sum() / self.npixels)) / denom) * self.mask else: self.data = self.gaussian_kernel self.shape = self.data.shape ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/photutils/detection/daofinder.py0000644000214200020070000006120400000000000020517 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module implements the DAOStarFinder class. """ import inspect import warnings from astropy.nddata import extract_array from astropy.table import QTable from astropy.utils import lazyproperty import numpy as np from .core import StarFinderBase, _StarFinderKernel from ..utils._convolution import _filter_data from ..utils.exceptions import NoDetectionsWarning from ..utils._misc import _get_version_info __all__ = ['DAOStarFinder'] class DAOStarFinder(StarFinderBase): """ Detect stars in an image using the DAOFIND (`Stetson 1987 `_) algorithm. DAOFIND (`Stetson 1987; PASP 99, 191 `_) searches images for local density maxima that have a peak amplitude greater than ``threshold`` (approximately; ``threshold`` is applied to a convolved image) and have a size and shape similar to the defined 2D Gaussian kernel. The Gaussian kernel is defined by the ``fwhm``, ``ratio``, ``theta``, and ``sigma_radius`` input parameters. ``DAOStarFinder`` finds the object centroid by fitting the marginal x and y 1D distributions of the Gaussian kernel to the marginal x and y distributions of the input (unconvolved) ``data`` image. ``DAOStarFinder`` calculates the object roundness using two methods. The ``roundlo`` and ``roundhi`` bounds are applied to both measures of roundness. The first method (``roundness1``; called ``SROUND`` in `DAOFIND`_) is based on the source symmetry and is the ratio of a measure of the object's bilateral (2-fold) to four-fold symmetry. The second roundness statistic (``roundness2``; called ``GROUND`` in `DAOFIND`_) measures the ratio of the difference in the height of the best fitting Gaussian function in x minus the best fitting Gaussian function in y, divided by the average of the best fitting Gaussian functions in x and y. A circular source will have a zero roundness. A source extended in x or y will have a negative or positive roundness, respectively. The sharpness statistic measures the ratio of the difference between the height of the central pixel and the mean of the surrounding non-bad pixels in the convolved image, to the height of the best fitting Gaussian function at that point. Parameters ---------- threshold : float The absolute image value above which to select sources. fwhm : float The full-width half-maximum (FWHM) of the major axis of the Gaussian kernel in units of pixels. ratio : float, optional The ratio of the minor to major axis standard deviations of the Gaussian kernel. ``ratio`` must be strictly positive and less than or equal to 1.0. The default is 1.0 (i.e., a circular Gaussian kernel). theta : float, optional The position angle (in degrees) of the major axis of the Gaussian kernel measured counter-clockwise from the positive x axis. sigma_radius : float, optional The truncation radius of the Gaussian kernel in units of sigma (standard deviation) [``1 sigma = FWHM / (2.0*sqrt(2.0*log(2.0)))``]. sharplo : float, optional The lower bound on sharpness for object detection. sharphi : float, optional The upper bound on sharpness for object detection. roundlo : float, optional The lower bound on roundness for object detection. roundhi : float, optional The upper bound on roundness for object detection. sky : float, optional The background sky level of the image. Setting ``sky`` affects only the output values of the object ``peak``, ``flux``, and ``mag`` values. The default is 0.0, which should be used to replicate the results from `DAOFIND`_. exclude_border : bool, optional Set to `True` to exclude sources found within half the size of the convolution kernel from the image borders. The default is `False`, which is the mode used by `DAOFIND`_. brightest : int, None, optional Number of brightest objects to keep after sorting the full object list. If ``brightest`` is set to `None`, all objects will be selected. peakmax : float, None, optional Maximum peak pixel value in an object. Only objects whose peak pixel values are *strictly smaller* than ``peakmax`` will be selected. This may be used to exclude saturated sources. By default, when ``peakmax`` is set to `None`, all objects will be selected. .. warning:: `DAOStarFinder` automatically excludes objects whose peak pixel values are negative. Therefore, setting ``peakmax`` to a non-positive value would result in exclusion of all objects. xycoords : `None` or Nx2 `~numpy.ndarray` The (x, y) pixel coordinates of the approximate centroid positions of identified sources. If ``xycoords`` are input, the algorithm will skip the source-finding step. See Also -------- IRAFStarFinder Notes ----- For the convolution step, this routine sets pixels beyond the image borders to 0.0. The equivalent parameters in `DAOFIND`_ are ``boundary='constant'`` and ``constant=0.0``. The main differences between `~photutils.detection.DAOStarFinder` and `~photutils.detection.IRAFStarFinder` are: * `~photutils.detection.IRAFStarFinder` always uses a 2D circular Gaussian kernel, while `~photutils.detection.DAOStarFinder` can use an elliptical Gaussian kernel. * `~photutils.detection.IRAFStarFinder` calculates the objects' centroid, roundness, and sharpness using image moments. References ---------- .. [1] Stetson, P. 1987; PASP 99, 191 (https://ui.adsabs.harvard.edu/abs/1987PASP...99..191S/abstract) .. [2] https://iraf.net/irafhelp.php?val=daofind .. _DAOFIND: https://iraf.net/irafhelp.php?val=daofind """ def __init__(self, threshold, fwhm, ratio=1.0, theta=0.0, sigma_radius=1.5, sharplo=0.2, sharphi=1.0, roundlo=-1.0, roundhi=1.0, sky=0.0, exclude_border=False, brightest=None, peakmax=None, xycoords=None): if not np.isscalar(threshold): raise TypeError('threshold must be a scalar value.') if not np.isscalar(fwhm): raise TypeError('fwhm must be a scalar value.') self.threshold = threshold self.fwhm = fwhm self.ratio = ratio self.theta = theta self.sigma_radius = sigma_radius self.sharplo = sharplo self.sharphi = sharphi self.roundlo = roundlo self.roundhi = roundhi self.sky = sky self.exclude_border = exclude_border self.brightest = self._validate_brightest(brightest) self.peakmax = peakmax if xycoords is not None: xycoords = np.asarray(xycoords) if xycoords.ndim != 2 or xycoords.shape[1] != 2: raise ValueError('xycoords must be shaped as a Nx2 array') self.xycoords = xycoords self.kernel = _StarFinderKernel(self.fwhm, self.ratio, self.theta, self.sigma_radius) self.threshold_eff = self.threshold * self.kernel.relerr @staticmethod def _validate_brightest(brightest): if brightest is not None: if brightest <= 0: raise ValueError('brightest must be >= 0') bright_int = int(brightest) if bright_int != brightest: raise ValueError('brightest must be an integer') brightest = bright_int return brightest def _get_raw_catalog(self, data, mask=None): convolved_data = _filter_data(data, self.kernel.data, mode='constant', fill_value=0.0, check_normalization=False) if self.xycoords is None: xypos = self._find_stars(convolved_data, self.kernel, self.threshold_eff, mask=mask, exclude_border=self.exclude_border) else: xypos = self.xycoords if xypos is None: warnings.warn('No sources were found.', NoDetectionsWarning) return None cat = _DAOStarFinderCatalog(data, convolved_data, xypos, self.kernel, self.threshold, sky=self.sky, sharplo=self.sharplo, sharphi=self.sharphi, roundlo=self.roundlo, roundhi=self.roundhi, brightest=self.brightest, peakmax=self.peakmax) return cat def find_stars(self, data, mask=None): """ Find stars in an astronomical image. Parameters ---------- data : 2D array_like The 2D image array. mask : 2D bool array, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when searching for stars. Returns ------- table : `~astropy.table.QTable` or `None` A table of found stars with the following parameters: * ``id``: unique object identification number. * ``xcentroid, ycentroid``: object centroid. * ``sharpness``: object sharpness. * ``roundness1``: object roundness based on symmetry. * ``roundness2``: object roundness based on marginal Gaussian fits. * ``npix``: the total number of pixels in the Gaussian kernel array. * ``sky``: the input ``sky`` parameter. * ``peak``: the peak, sky-subtracted, pixel value of the object. * ``flux``: the object flux calculated as the peak density in the convolved image divided by the detection threshold. This derivation matches that of `DAOFIND`_ if ``sky`` is 0.0. * ``mag``: the object instrumental magnitude calculated as ``-2.5 * log10(flux)``. The derivation matches that of `DAOFIND`_ if ``sky`` is 0.0. `None` is returned if no stars are found. """ cat = self._get_raw_catalog(data, mask=mask) if cat is None: return None # apply all selection filters cat = cat.apply_all_filters() if cat is None: return None # create the output table return cat.to_table() class _DAOStarFinderCatalog: """ Class to create a catalog of the properties of each detected star, as defined by `DAOFIND`_. Parameters ---------- data : 2D `~numpy.ndarray` The 2D image. convolved_data : 2D `~numpy.ndarray` The convolved 2D image. xypos: Nx2 `numpy.ndarray` A Nx2 array of (x, y) pixel coordinates denoting the central positions of the stars. kernel : `_StarFinderKernel` The convolution kernel. This kernel must match the kernel used to create the ``convolved_data``. threshold : float The absolute image value above which sources were selected. sky : float, optional The local sky level around the source. ``sky`` is used only to calculate the source peak value, flux, and magnitude. The default is 0. .. _DAOFIND: https://iraf.net/irafhelp.php?val=daofind """ def __init__(self, data, convolved_data, xypos, kernel, threshold, sky=0., sharplo=0.2, sharphi=1.0, roundlo=-1.0, roundhi=1.0, brightest=None, peakmax=None): self.data = data self.convolved_data = convolved_data self.xypos = np.atleast_2d(xypos) self.kernel = kernel self.threshold = threshold self._sky = sky # DAOFIND has no sky input -> same as sky=0. self.sharplo = sharplo self.sharphi = sharphi self.roundlo = roundlo self.roundhi = roundhi self.brightest = brightest self.peakmax = peakmax self.id = np.arange(len(self)) + 1 self.threshold_eff = threshold * kernel.relerr self.cutout_shape = kernel.shape self.cutout_center = tuple([(size - 1) // 2 for size in kernel.shape]) self.default_columns = ('id', 'xcentroid', 'ycentroid', 'sharpness', 'roundness1', 'roundness2', 'npix', 'sky', 'peak', 'flux', 'mag') def __len__(self): return len(self.xypos) def __getitem__(self, index): newcls = object.__new__(self.__class__) init_attr = ('data', 'convolved_data', 'kernel', 'threshold', '_sky', 'sharplo', 'sharphi', 'roundlo', 'roundhi', 'brightest', 'peakmax', 'threshold_eff', 'cutout_shape', 'cutout_center', 'default_columns') for attr in init_attr: setattr(newcls, attr, getattr(self, attr)) # xypos determines ordering and isscalar # NOTE: always keep as a 2D array, even for a single source attr = 'xypos' value = getattr(self, attr)[index] setattr(newcls, attr, np.atleast_2d(value)) keys = set(self.__dict__.keys()) & set(self._lazyproperties) keys.add('id') for key in keys: value = self.__dict__[key] # do not insert lazy attributes that are always scalar (e.g., # isscalar), i.e., not an array/list for each source if np.isscalar(value): continue # value is always at least a 1D array, even for a single source value = np.atleast_1d(value[index]) newcls.__dict__[key] = value return newcls @lazyproperty def isscalar(self): """ Whether the instance is scalar (e.g., a single source). """ return self.xypos.shape == (1, 2) @property def _lazyproperties(self): """ Return all lazyproperties (even in superclasses). """ def islazyproperty(obj): return isinstance(obj, lazyproperty) return [i[0] for i in inspect.getmembers(self.__class__, predicate=islazyproperty)] def reset_ids(self): """Reset the ID column to be consecutive integers.""" self.id = np.arange(len(self)) + 1 def make_cutouts(self, data): cutouts = [] for xpos, ypos in self.xypos: cutouts.append(extract_array(data, self.cutout_shape, (ypos, xpos), fill_value=0.0)) return np.array(cutouts) @lazyproperty def cutout_data(self): return self.make_cutouts(self.data) @lazyproperty def cutout_convdata(self): return self.make_cutouts(self.convolved_data) @lazyproperty def data_peak(self): return self.cutout_data[:, self.cutout_center[0], self.cutout_center[1]] @lazyproperty def convdata_peak(self): return self.cutout_convdata[:, self.cutout_center[0], self.cutout_center[1]] @lazyproperty def roundness1(self): # set the central (peak) pixel to zero for the sum4 calculation cutout_conv = self.cutout_convdata.copy() cutout_conv[:, self.cutout_center[0], self.cutout_center[1]] = 0.0 # calculate the four roundness quadrants. # the cutout size always matches the kernel size, which has odd # dimensions. # quad1 = bottom right # quad2 = bottom left # quad3 = top left # quad4 = top right # 3 3 4 4 4 # 3 3 4 4 4 # 3 3 x 1 1 # 2 2 2 1 1 # 2 2 2 1 1 quad1 = cutout_conv[:, 0:self.cutout_center[0] + 1, self.cutout_center[1] + 1:] quad2 = cutout_conv[:, 0:self.cutout_center[0], 0:self.cutout_center[1] + 1] quad3 = cutout_conv[:, self.cutout_center[0]:, 0:self.cutout_center[1]] quad4 = cutout_conv[:, self.cutout_center[0] + 1:, self.cutout_center[1]:] axis = (1, 2) sum2 = (-quad1.sum(axis=axis) + quad2.sum(axis=axis) - quad3.sum(axis=axis) + quad4.sum(axis=axis)) sum4 = np.abs(cutout_conv).sum(axis=axis) # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) roundness1 = 2.0 * sum2 / sum4 return roundness1 @lazyproperty def sharpness(self): # mean value of the unconvolved data (excluding the peak) cutout_data_masked = self.cutout_data * self.kernel.mask data_mean = ((np.sum(cutout_data_masked, axis=(1, 2)) - self.data_peak) / (self.kernel.npixels - 1)) return (self.data_peak - data_mean) / self.convdata_peak def daofind_marginal_fit(self, axis=0): """ Fit 1D Gaussians, defined from the marginal x/y kernel distributions, to the marginal x/y distributions of the original (unconvolved) image. These fits are used calculate the star centroid and roundness2 ("GROUND") properties. Parameters ---------- axis : {0, 1}, optional The axis for which the marginal fit is performed: * 0: for the x axis * 1: for the y axis Returns ------- dx : float The fractional shift in x or y (depending on ``axis`` value) of the image centroid relative to the maximum pixel. hx : float The height of the best-fitting Gaussian to the marginal x or y (depending on ``axis`` value) distribution of the unconvolved source data. """ # define triangular weighting functions along each axis, peaked # in the middle and equal to one at the edge ycen, xcen = self.cutout_center xx = xcen - np.abs(np.arange(self.cutout_shape[1]) - xcen) + 1 yy = ycen - np.abs(np.arange(self.cutout_shape[0]) - ycen) + 1 xwt, ywt = np.meshgrid(xx, yy) if axis == 0: # marginal distributions along x axis wt = xwt[0] # 1D wts = ywt # 2D size = self.cutout_shape[1] center = xcen sigma = self.kernel.xsigma dxx = center - np.arange(size) elif axis == 1: # marginal distributions along y axis wt = np.transpose(ywt)[0] # 1D wts = xwt # 2D size = self.cutout_shape[0] center = ycen sigma = self.kernel.ysigma dxx = np.arange(size) - center # compute marginal sums for given axis wt_sum = np.sum(wt) dx = center - np.arange(size) # weighted marginal sums kern_sum_1d = np.sum(self.kernel.gaussian_kernel_unmasked * wts, axis=axis) kern_sum = np.sum(kern_sum_1d * wt) kern2_sum = np.sum(kern_sum_1d**2 * wt) dkern_dx = kern_sum_1d * dx dkern_dx_sum = np.sum(dkern_dx * wt) dkern_dx2_sum = np.sum(dkern_dx**2 * wt) kern_dkern_dx_sum = np.sum(kern_sum_1d * dkern_dx * wt) data_sum_1d = np.sum(self.cutout_data * wts, axis=axis + 1) data_sum = np.sum(data_sum_1d * wt, axis=1) data_kern_sum = np.sum(data_sum_1d * kern_sum_1d * wt, axis=1) data_dkern_dx_sum = np.sum(data_sum_1d * dkern_dx * wt, axis=1) data_dx_sum = np.sum(data_sum_1d * dxx * wt, axis=1) # perform linear least-squares fit (where data = sky + hx*kernel) # to find the amplitude (hx) hx_numer = data_kern_sum - (data_sum * kern_sum) / wt_sum hx_denom = kern2_sum - (kern_sum**2 / wt_sum) # reject the star if the fit amplitude is not positive mask1 = (hx_numer <= 0.) | (hx_denom <= 0.) # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) # compute fit amplitude hx = hx_numer / hx_denom # sky = (data_sum - (hx * kern_sum)) / wt_sum # compute centroid shift dx = ((kern_dkern_dx_sum - (data_dkern_dx_sum - dkern_dx_sum * data_sum)) / (hx * dkern_dx2_sum / sigma**2)) dx2 = data_dx_sum / data_sum hsize = size / 2.0 mask2 = (np.abs(dx) > hsize) mask3 = (data_sum == 0.) mask4 = (mask2 & mask3) mask5 = (mask2 & ~mask3) dx[mask4] = 0.0 dx[mask5] = dx2[mask5] mask6 = (np.abs(dx) > hsize) dx[mask6] = 0.0 hx[mask1] = np.nan dx[mask1] = np.nan return np.transpose((dx, hx)) @lazyproperty def dx_hx(self): return self.daofind_marginal_fit(axis=0) @lazyproperty def dy_hy(self): return self.daofind_marginal_fit(axis=1) @lazyproperty def dx(self): return np.transpose(self.dx_hx)[0] @lazyproperty def dy(self): return np.transpose(self.dy_hy)[0] @lazyproperty def hx(self): return np.transpose(self.dx_hx)[1] @lazyproperty def hy(self): return np.transpose(self.dy_hy)[1] @lazyproperty def xcentroid(self): return np.transpose(self.xypos)[0] + self.dx @lazyproperty def ycentroid(self): return np.transpose(self.xypos)[1] + self.dy @lazyproperty def roundness2(self): """ The star roundness. This roundness parameter represents the ratio of the difference in the height of the best fitting Gaussian function in x minus the best fitting Gaussian function in y, divided by the average of the best fitting Gaussian functions in x and y. A circular source will have a zero roundness. A source extended in x or y will have a negative or positive roundness, respectively. """ return 2.0 * (self.hx - self.hy) / (self.hx + self.hy) @lazyproperty def peak(self): return self.data_peak - self.sky @lazyproperty def flux(self): return ((self.convdata_peak / self.threshold_eff) - (self.sky * self.npix)) @lazyproperty def mag(self): # ignore RunTimeWarning if flux is <= 0 with warnings.catch_warnings(): warnings.simplefilter('ignore', category=RuntimeWarning) mag = -2.5 * np.log10(self.flux) mag[self.flux <= 0] = np.nan return mag @lazyproperty def sky(self): return np.full(len(self), fill_value=self._sky) @lazyproperty def npix(self): return np.full(len(self), fill_value=self.kernel.data.size) def apply_filters(self): """Filter the catalog.""" mask = (~np.isnan(self.dx) & ~np.isnan(self.dy) & ~np.isnan(self.hx) & ~np.isnan(self.hy)) mask &= ((self.sharpness > self.sharplo) & (self.sharpness < self.sharphi) & (self.roundness1 > self.roundlo) & (self.roundness1 < self.roundhi) & (self.roundness2 > self.roundlo) & (self.roundness2 < self.roundhi)) if self.peakmax is not None: mask &= (self.peak < self.peakmax) newcat = self[mask] if len(newcat) == 0: warnings.warn('Sources were found, but none pass the sharpness, ' 'roundness, or peakmax criteria', NoDetectionsWarning) return None return newcat def select_brightest(self): """ Sort the catalog by the brightest fluxes and select the top brightest sources. """ newcat = self if self.brightest is not None: idx = np.argsort(self.flux)[::-1][:self.brightest] newcat = self[idx] return newcat def apply_all_filters(self): """ Apply all filters, select the brightest, and reset the source ids. """ cat = self.apply_filters() if cat is None: return None cat = cat.select_brightest() cat.reset_ids() return cat def to_table(self, columns=None): meta = {'version': _get_version_info()} table = QTable(meta=meta) if columns is None: columns = self.default_columns for column in columns: table[column] = getattr(self, column) return table ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/photutils/detection/irafstarfinder.py0000644000214200020070000004423700000000000021576 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module implements the IRAFStarFinder class. """ import inspect import warnings from astropy.nddata import extract_array from astropy.table import QTable from astropy.utils import lazyproperty import numpy as np from .core import StarFinderBase, _StarFinderKernel from ..utils._convolution import _filter_data from ..utils._misc import _get_version_info from ..utils._moments import _moments, _moments_central from ..utils.exceptions import NoDetectionsWarning __all__ = ['IRAFStarFinder'] class IRAFStarFinder(StarFinderBase): """ Detect stars in an image using IRAF's "starfind" algorithm. `IRAFStarFinder` searches images for local density maxima that have a peak amplitude greater than ``threshold`` above the local background and have a PSF full-width at half-maximum similar to the input ``fwhm``. The objects' centroid, roundness (ellipticity), and sharpness are calculated using image moments. Parameters ---------- threshold : float The absolute image value above which to select sources. fwhm : float The full-width half-maximum (FWHM) of the 2D circular Gaussian kernel in units of pixels. minsep_fwhm : float, optional The minimum separation for detected objects in units of ``fwhm``. sigma_radius : float, optional The truncation radius of the Gaussian kernel in units of sigma (standard deviation) [``1 sigma = FWHM / 2.0*sqrt(2.0*log(2.0))``]. sharplo : float, optional The lower bound on sharpness for object detection. sharphi : float, optional The upper bound on sharpness for object detection. roundlo : float, optional The lower bound on roundness for object detection. roundhi : float, optional The upper bound on roundness for object detection. sky : float, optional The background sky level of the image. Inputing a ``sky`` value will override the background sky estimate. Setting ``sky`` affects only the output values of the object ``peak``, ``flux``, and ``mag`` values. The default is ``None``, which means the sky value will be estimated using the `starfind`_ method. exclude_border : bool, optional Set to `True` to exclude sources found within half the size of the convolution kernel from the image borders. The default is `False`, which is the mode used by `starfind`_. brightest : int, None, optional Number of brightest objects to keep after sorting the full object list. If ``brightest`` is set to `None`, all objects will be selected. peakmax : float, None, optional Maximum peak pixel value in an object. Only objects whose peak pixel values are *strictly smaller* than ``peakmax`` will be selected. This may be used to exclude saturated sources. By default, when ``peakmax`` is set to `None`, all objects will be selected. .. warning:: `IRAFStarFinder` automatically excludes objects whose peak pixel values are negative. Therefore, setting ``peakmax`` to a non-positive value would result in exclusion of all objects. xycoords : `None` or Nx2 `~numpy.ndarray` The (x, y) pixel coordinates of the approximate centroid positions of identified sources. If ``xycoords`` are input, the algorithm will skip the source-finding step. Notes ----- For the convolution step, this routine sets pixels beyond the image borders to 0.0. The equivalent parameters in IRAF's `starfind`_ are ``boundary='constant'`` and ``constant=0.0``. IRAF's `starfind`_ uses ``hwhmpsf``, ``fradius``, and ``sepmin`` as input parameters. The equivalent input values for `IRAFStarFinder` are: * ``fwhm = hwhmpsf * 2`` * ``sigma_radius = fradius * sqrt(2.0*log(2.0))`` * ``minsep_fwhm = 0.5 * sepmin`` The main differences between `~photutils.detection.DAOStarFinder` and `~photutils.detection.IRAFStarFinder` are: * `~photutils.detection.IRAFStarFinder` always uses a 2D circular Gaussian kernel, while `~photutils.detection.DAOStarFinder` can use an elliptical Gaussian kernel. * `~photutils.detection.IRAFStarFinder` calculates the objects' centroid, roundness, and sharpness using image moments. See Also -------- DAOStarFinder References ---------- .. [1] https://iraf.net/irafhelp.php?val=starfind .. _starfind: https://iraf.net/irafhelp.php?val=starfind """ def __init__(self, threshold, fwhm, sigma_radius=1.5, minsep_fwhm=2.5, sharplo=0.5, sharphi=2.0, roundlo=0.0, roundhi=0.2, sky=None, exclude_border=False, brightest=None, peakmax=None, xycoords=None): if not np.isscalar(threshold): raise TypeError('threshold must be a scalar value.') if not np.isscalar(fwhm): raise TypeError('fwhm must be a scalar value.') self.threshold = threshold self.fwhm = fwhm self.sigma_radius = sigma_radius self.minsep_fwhm = minsep_fwhm self.sharplo = sharplo self.sharphi = sharphi self.roundlo = roundlo self.roundhi = roundhi self.sky = sky self.exclude_border = exclude_border self.brightest = self._validate_brightest(brightest) self.peakmax = peakmax if xycoords is not None: xycoords = np.asarray(xycoords) if xycoords.ndim != 2 or xycoords.shape[1] != 2: raise ValueError('xycoords must be shaped as a Nx2 array') self.xycoords = xycoords self.kernel = _StarFinderKernel(self.fwhm, ratio=1.0, theta=0.0, sigma_radius=self.sigma_radius) self.min_separation = max(2, int((self.fwhm * self.minsep_fwhm) + 0.5)) @staticmethod def _validate_brightest(brightest): if brightest is not None: if brightest <= 0: raise ValueError('brightest must be >= 0') bright_int = int(brightest) if bright_int != brightest: raise ValueError('brightest must be an integer') brightest = bright_int return brightest def _get_raw_catalog(self, data, mask=None): convolved_data = _filter_data(data, self.kernel.data, mode='constant', fill_value=0.0, check_normalization=False) if self.xycoords is None: xypos = self._find_stars(convolved_data, self.kernel, self.threshold, min_separation=self.min_separation, mask=mask, exclude_border=self.exclude_border) else: xypos = self.xycoords if xypos is None: warnings.warn('No sources were found.', NoDetectionsWarning) return None cat = _IRAFStarFinderCatalog(data, convolved_data, xypos, self.kernel, sky=self.sky, sharplo=self.sharplo, sharphi=self.sharphi, roundlo=self.roundlo, roundhi=self.roundhi, brightest=self.brightest, peakmax=self.peakmax) return cat def find_stars(self, data, mask=None): """ Find stars in an astronomical image. Parameters ---------- data : 2D array_like The 2D image array. mask : 2D bool array, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when searching for stars. Returns ------- table : `~astropy.table.QTable` or `None` A table of found objects with the following parameters: * ``id``: unique object identification number. * ``xcentroid, ycentroid``: object centroid. * ``fwhm``: object FWHM. * ``sharpness``: object sharpness. * ``roundness``: object roundness. * ``pa``: object position angle (degrees counter clockwise from the positive x axis). * ``npix``: the total number of (positive) unmasked pixels. * ``sky``: the local ``sky`` value. * ``peak``: the peak, sky-subtracted, pixel value of the object. * ``flux``: the object instrumental flux. * ``mag``: the object instrumental magnitude calculated as ``-2.5 * log10(flux)``. `None` is returned if no stars are found. """ cat = self._get_raw_catalog(data, mask=mask) if cat is None: return None # apply all selection filters cat = cat.apply_all_filters() if cat is None: return None # create the output table return cat.to_table() class _IRAFStarFinderCatalog: """ Class to create a catalog of the properties of each detected star, as defined by IRAF's ``starfind`` task. Parameters ---------- data : 2D `~numpy.ndarray` The 2D image. convolved_data : 2D `~numpy.ndarray` The convolved 2D image. xypos: Nx2 `numpy.ndarray` A Nx2 array of (x, y) pixel coordinates denoting the central positions of the stars. kernel : `_StarFinderKernel` The convolution kernel. This kernel must match the kernel used to create the ``convolved_data``. sky : `None` or float, optional The local sky level around the source. If sky is ``None``, then a local sky level will be (crudely) estimated using the IRAF ``starfind`` calculation. """ def __init__(self, data, convolved_data, xypos, kernel, sky=None, sharplo=0.2, sharphi=1.0, roundlo=-1.0, roundhi=1.0, brightest=None, peakmax=None): self.data = data self.convolved_data = convolved_data self.xypos = xypos self.kernel = kernel self._sky = sky self.sharplo = sharplo self.sharphi = sharphi self.roundlo = roundlo self.roundhi = roundhi self.brightest = brightest self.peakmax = peakmax self.id = np.arange(len(self)) + 1 self.cutout_shape = kernel.shape self.default_columns = ('id', 'xcentroid', 'ycentroid', 'fwhm', 'sharpness', 'roundness', 'pa', 'npix', 'sky', 'peak', 'flux', 'mag') def __len__(self): return len(self.xypos) def __getitem__(self, index): newcls = object.__new__(self.__class__) init_attr = ('data', 'convolved_data', 'kernel', '_sky', 'sharplo', 'sharphi', 'roundlo', 'roundhi', 'brightest', 'peakmax', 'cutout_shape', 'default_columns') for attr in init_attr: setattr(newcls, attr, getattr(self, attr)) # xypos determines ordering and isscalar # NOTE: always keep as a 2D array, even for a single source attr = 'xypos' value = getattr(self, attr)[index] setattr(newcls, attr, np.atleast_2d(value)) keys = set(self.__dict__.keys()) & set(self._lazyproperties) keys.add('id') for key in keys: value = self.__dict__[key] # do not insert lazy attributes that are always scalar (e.g., # isscalar), i.e., not an array/list for each source if np.isscalar(value): continue # value is always at least a 1D array, even for a single source value = np.atleast_1d(value[index]) newcls.__dict__[key] = value return newcls @lazyproperty def isscalar(self): """ Whether the instance is scalar (e.g., a single source). """ return self.xypos.shape == (1, 2) @property def _lazyproperties(self): """ Return all lazyproperties (even in superclasses). """ def islazyproperty(obj): return isinstance(obj, lazyproperty) return [i[0] for i in inspect.getmembers(self.__class__, predicate=islazyproperty)] def reset_ids(self): """Reset the ID column to be consecutive integers.""" self.id = np.arange(len(self)) + 1 @lazyproperty def sky(self): if self._sky is None: skymask = ~self.kernel.mask.astype(bool) # 1=sky, 0=obj nsky = np.count_nonzero(skymask) axis = (1, 2) if nsky == 0.: sky = (np.max(self.cutout_data_nosub, axis=axis) - np.max(self.cutout_convdata, axis=axis)) else: sky = (np.sum(self.cutout_data_nosub * skymask, axis=axis) / nsky) else: sky = np.full(len(self), fill_value=self._sky) return sky def make_cutouts(self, data): cutouts = [] for xpos, ypos in self.xypos: cutouts.append(extract_array(data, self.cutout_shape, (ypos, xpos), fill_value=0.0)) return np.array(cutouts) @lazyproperty def cutout_data_nosub(self): return self.make_cutouts(self.data) @lazyproperty def cutout_data(self): data = ((self.cutout_data_nosub - self.sky[:, np.newaxis, np.newaxis]) * self.kernel.mask) # IRAF starfind discards negative pixels data[data < 0] = 0.0 return data @lazyproperty def cutout_convdata(self): return self.make_cutouts(self.convolved_data) @lazyproperty def npix(self): return np.count_nonzero(self.cutout_data, axis=(1, 2)) @lazyproperty def moments(self): return np.array([_moments(arr, order=1) for arr in self.cutout_data]) @lazyproperty def cutout_centroid(self): moments = self.moments # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) ycentroid = moments[:, 1, 0] / moments[:, 0, 0] xcentroid = moments[:, 0, 1] / moments[:, 0, 0] return np.transpose((ycentroid, xcentroid)) @lazyproperty def cutout_xcentroid(self): return np.transpose(self.cutout_centroid)[1] @lazyproperty def cutout_ycentroid(self): return np.transpose(self.cutout_centroid)[0] @lazyproperty def cutout_xorigin(self): return np.transpose(self.xypos)[0] - self.kernel.xradius @lazyproperty def cutout_yorigin(self): return np.transpose(self.xypos)[1] - self.kernel.yradius @lazyproperty def xcentroid(self): return self.cutout_xcentroid + self.cutout_xorigin @lazyproperty def ycentroid(self): return self.cutout_ycentroid + self.cutout_yorigin @lazyproperty def peak(self): return np.array([np.max(arr) for arr in self.cutout_data]) @lazyproperty def flux(self): return np.array([np.sum(arr) for arr in self.cutout_data]) @lazyproperty def mag(self): return -2.5 * np.log10(self.flux) @lazyproperty def moments_central(self): moments = np.array([_moments_central(arr, center=(xcen_, ycen_), order=2) for arr, xcen_, ycen_ in zip(self.cutout_data, self.cutout_xcentroid, self.cutout_ycentroid)]) return moments / self.moments[:, 0, 0][:, np.newaxis, np.newaxis] @lazyproperty def mu_sum(self): return self.moments_central[:, 0, 2] + self.moments_central[:, 2, 0] @lazyproperty def mu_diff(self): return self.moments_central[:, 0, 2] - self.moments_central[:, 2, 0] @lazyproperty def fwhm(self): return 2.0 * np.sqrt(np.log(2.0) * self.mu_sum) @lazyproperty def roundness(self): return np.sqrt(self.mu_diff**2 + 4.0 * self.moments_central[:, 1, 1]**2) / self.mu_sum @lazyproperty def sharpness(self): return self.fwhm / self.kernel.fwhm @lazyproperty def pa(self): pa = np.rad2deg(0.5 * np.arctan2(2.0 * self.moments_central[:, 1, 1], self.mu_diff)) pa = np.where(pa < 0, pa + 180, pa) return pa def apply_filters(self): """Filter the catalog.""" mask = np.count_nonzero(self.cutout_data, axis=(1, 2)) > 1 mask &= ((self.sharpness > self.sharplo) & (self.sharpness < self.sharphi) & (self.roundness > self.roundlo) & (self.roundness < self.roundhi)) if self.peakmax is not None: mask &= (self.peak < self.peakmax) newcat = self[mask] if len(newcat) == 0: warnings.warn('Sources were found, but none pass the sharpness, ' 'roundness, or peakmax criteria', NoDetectionsWarning) return None return newcat def select_brightest(self): """ Sort the catalog by the brightest fluxes and select the top brightest sources. """ newcat = self if self.brightest is not None: idx = np.argsort(self.flux)[::-1][:self.brightest] newcat = self[idx] return newcat def apply_all_filters(self): """ Apply all filters, select the brightest, and reset the source ids. """ cat = self.apply_filters() if cat is None: return None cat = cat.select_brightest() cat.reset_ids() return cat def to_table(self, columns=None): meta = {'version': _get_version_info()} table = QTable(meta=meta) if columns is None: columns = self.default_columns for column in columns: table[column] = getattr(self, column) return table ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/detection/peakfinder.py0000644000214200020070000001725200000000000020700 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for finding local peaks in an astronomical image. """ import warnings from astropy.table import QTable import numpy as np from ..utils.exceptions import NoDetectionsWarning from ..utils._misc import _get_version_info __all__ = ['find_peaks'] def find_peaks(data, threshold, box_size=3, footprint=None, mask=None, border_width=None, npeaks=np.inf, centroid_func=None, error=None, wcs=None): """ Find local peaks in an image that are above above a specified threshold value. Peaks are the maxima above the ``threshold`` within a local region. The local regions are defined by either the ``box_size`` or ``footprint`` parameters. ``box_size`` defines the local region around each pixel as a square box. ``footprint`` is a boolean array where `True` values specify the region shape. If multiple pixels within a local region have identical intensities, then the coordinates of all such pixels are returned. Otherwise, there will be only one peak pixel per local region. Thus, the defined region effectively imposes a minimum separation between peaks unless there are identical peaks within the region. If ``centroid_func`` is input, then it will be used to calculate a centroid within the defined local region centered on each detected peak pixel. In this case, the centroid will also be returned in the output table. Parameters ---------- data : array_like The 2D array of the image. threshold : float or array-like The data value or pixel-wise data values to be used for the detection threshold. A 2D ``threshold`` must have the same shape as ``data``. See `~photutils.segmentation.detect_threshold` for one way to create a ``threshold`` image. box_size : scalar or tuple, optional The size of the local region to search for peaks at every point in ``data``. If ``box_size`` is a scalar, then the region shape will be ``(box_size, box_size)``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. footprint : `~numpy.ndarray` of bools, optional A boolean array where `True` values describe the local footprint region within which to search for peaks at every point in ``data``. ``box_size=(n, m)`` is equivalent to ``footprint=np.ones((n, m))``. Either ``box_size`` or ``footprint`` must be defined. If they are both defined, then ``footprint`` overrides ``box_size``. mask : array_like, bool, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. border_width : bool, optional The width in pixels to exclude around the border of the ``data``. npeaks : int, optional The maximum number of peaks to return. When the number of detected peaks exceeds ``npeaks``, the peaks with the highest peak intensities will be returned. centroid_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The ``centroid_func`` must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword, and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray` objects, representing the x and y centroids, respectively. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. ``error`` is used only if ``centroid_func`` is input (the ``error`` array is passed directly to the ``centroid_func``). wcs : `None` or WCS object, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). If `None`, then the sky coordinates will not be returned in the output `~astropy.table.Table`. Returns ------- output : `~astropy.table.Table` or `None` A table containing the x and y pixel location of the peaks and their values. If ``centroid_func`` is input, then the table will also contain the centroid position. If no peaks are found then `None` is returned. """ from scipy.ndimage import maximum_filter data = np.asanyarray(data) if np.all(data == data.flat[0]): warnings.warn('Input data is constant. No local peaks can be found.', NoDetectionsWarning) return None if not np.isscalar(threshold): threshold = np.asanyarray(threshold) if data.shape != threshold.shape: raise ValueError('A threshold array must have the same shape as ' 'the input data.') # remove NaN values to avoid runtime warnings nan_mask = np.isnan(data) if np.any(nan_mask): data = np.copy(data) # ndarray data[nan_mask] = np.nanmin(data) if footprint is not None: data_max = maximum_filter(data, footprint=footprint, mode='constant', cval=0.0) else: data_max = maximum_filter(data, size=box_size, mode='constant', cval=0.0) peak_goodmask = (data == data_max) # good pixels are True if mask is not None: mask = np.asanyarray(mask) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape') peak_goodmask = np.logical_and(peak_goodmask, ~mask) if border_width is not None: for i in range(peak_goodmask.ndim): peak_goodmask = peak_goodmask.swapaxes(0, i) peak_goodmask[:border_width] = False peak_goodmask[-border_width:] = False peak_goodmask = peak_goodmask.swapaxes(0, i) peak_goodmask = np.logical_and(peak_goodmask, (data > threshold)) y_peaks, x_peaks = peak_goodmask.nonzero() peak_values = data[y_peaks, x_peaks] nxpeaks = len(x_peaks) if nxpeaks > npeaks: idx = np.argsort(peak_values)[::-1][:npeaks] x_peaks = x_peaks[idx] y_peaks = y_peaks[idx] peak_values = peak_values[idx] if nxpeaks == 0: warnings.warn('No local peaks were found.', NoDetectionsWarning) return None # construct the output table meta = {'version': _get_version_info()} colnames = ['x_peak', 'y_peak', 'peak_value'] coldata = [x_peaks, y_peaks, peak_values] table = QTable(coldata, names=colnames, meta=meta) if wcs is not None: skycoord_peaks = wcs.pixel_to_world(x_peaks, y_peaks) table.add_column(skycoord_peaks, name='skycoord_peak', index=2) # perform centroiding if centroid_func is not None: from ..centroids import centroid_sources # prevents circular import if not callable(centroid_func): raise TypeError('centroid_func must be a callable object') x_centroids, y_centroids = centroid_sources( data, x_peaks, y_peaks, box_size=box_size, footprint=footprint, error=error, mask=mask, centroid_func=centroid_func) table['x_centroid'] = x_centroids table['y_centroid'] = y_centroids if wcs is not None: skycoord_centroids = wcs.pixel_to_world(x_centroids, y_centroids) idx = table.colnames.index('y_centroid') + 1 table.add_column(skycoord_centroids, name='skycoord_centroid', index=idx) return table ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636384183.0 photutils-1.3.0/photutils/detection/starfinder.py0000644000214200020070000003110700000000000020724 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module implements the StarFinder class. """ import inspect import warnings from astropy.nddata import overlap_slices from astropy.table import QTable from astropy.utils import lazyproperty import numpy as np from .core import StarFinderBase from ..utils._convolution import _filter_data from ..utils._misc import _get_version_info from ..utils._moments import _moments, _moments_central from ..utils.exceptions import NoDetectionsWarning __all__ = ['StarFinder'] class StarFinder(StarFinderBase): """ Detect stars in an image using a user-defined kernel. Parameters ---------- threshold : float The absolute image value above which to select sources. kernel : `numpy.ndarray` A 2D array of the PSF kernel. min_separation : float, optional The minimum separation for detected objects in pixels. exclude_border : bool, optional Whether to exclude sources found within half the size of the convolution kernel from the image borders. brightest : `None` or int, optional The number of brightest objects to return in the output table. If ``brightest`` is set to `None`, all objects will be returned. peakmax : `None` or float, optional The maximum allowed peak pixel value in an object. Only objects whose maximum pixel values are strictly smaller than ``peakmax`` will be selected. This may be used to exclude saturated sources. If set to `None`, all objects will be selected. .. warning:: `StarFinder` automatically excludes objects whose maximum pixel values are negative. Therefore, setting ``peakmax`` to a non-positive value would result in excluding all objects. Notes ----- For the convolution step, this routine sets pixels beyond the image borders to 0.0. The source properties are calculated using image moments. See Also -------- DAOStarFinder, IRAFStarFinder """ def __init__(self, threshold, kernel, min_separation=5.0, exclude_border=False, brightest=None, peakmax=None): self.threshold = threshold self.kernel = kernel if min_separation < 0: raise ValueError('min_separation must be >= 0') self.min_separation = min_separation self.exclude_border = exclude_border self.brightest = self._validate_brightest(brightest) self.peakmax = peakmax @staticmethod def _validate_brightest(brightest): if brightest is not None: if brightest <= 0: raise ValueError('brightest must be >= 0') bright_int = int(brightest) if bright_int != brightest: raise ValueError('brightest must be an integer') brightest = bright_int return brightest def _get_raw_catalog(self, data, mask=None): kernel = self.kernel kernel /= np.max(kernel) # normalize max value to 1.0 denom = np.sum(kernel**2) - (np.sum(kernel)**2 / kernel.size) kernel = (kernel - np.sum(kernel) / kernel.size) / denom convolved_data = _filter_data(data, kernel, mode='constant', fill_value=0.0, check_normalization=False) xypos = self._find_stars(convolved_data, kernel, self.threshold, min_separation=self.min_separation, mask=mask, exclude_border=self.exclude_border) if xypos is None: warnings.warn('No sources were found.', NoDetectionsWarning) return None cat = _StarFinderCatalog(data, xypos, self.kernel.shape, brightest=self.brightest, peakmax=self.peakmax) return cat def find_stars(self, data, mask=None): """ Find stars in an astronomical image. Parameters ---------- data : 2D array_like The 2D image array. mask : 2D bool array, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when searching for stars. Returns ------- table : `~astropy.table.QTable` or `None` A table of found objects with the following parameters: * ``id``: unique object identification number. * ``xcentroid, ycentroid``: object centroid. * ``fwhm``: object FWHM. * ``roundness``: object roundness. * ``pa``: object position angle (degrees counter clockwise from the positive x axis). * ``max_value``: the maximum pixel value in the source * ``flux``: the source instrumental flux. * ``mag``: the source instrumental magnitude calculated as ``-2.5 * log10(flux)``. `None` is returned if no stars are found or no stars meet the roundness and peakmax criteria. """ cat = self._get_raw_catalog(data, mask=mask) if cat is None: return None # apply all selection filters cat = cat.apply_all_filters() if cat is None: return None # create the output table return cat.to_table() class _StarFinderCatalog: """ Class to calculate the properties of each detected star. Parameters ---------- data : 2D `~numpy.ndarray` The 2D image. xypos: Nx2 `numpy.ndarray` A Nx2 array of (x, y) pixel coordinates denoting the central positions of the stars. shape: tuple of int The shape of the stars cutouts. The shape in both dimensions must be odd and match the shape of the smoothing kernel. """ def __init__(self, data, xypos, shape, brightest=None, peakmax=None): self.data = data self.xypos = np.atleast_2d(xypos) self.shape = shape self.brightest = brightest self.peakmax = peakmax self.id = np.arange(len(self)) + 1 self.default_columns = ('id', 'xcentroid', 'ycentroid', 'fwhm', 'roundness', 'pa', 'max_value', 'flux', 'mag') def __len__(self): return len(self.xypos) def __getitem__(self, index): newcls = object.__new__(self.__class__) init_attr = ('data', 'shape', 'brightest', 'peakmax', 'default_columns') for attr in init_attr: setattr(newcls, attr, getattr(self, attr)) # xypos determines ordering and isscalar # NOTE: always keep as 2D array, even for a single source attr = 'xypos' value = getattr(self, attr)[index] isscalar = value.shape == (2,) setattr(newcls, attr, np.atleast_2d(value)) keys = set(self.__dict__.keys()) & set(self._lazyproperties) keys.add('id') for key in keys: value = self.__dict__[key] if key in ('slices', 'cutout_data'): # apply fancy indices to list properties value = np.array(value + [None], dtype=object)[:-1][index] if isscalar: value = [value] # noqa else: value = value.tolist() else: # value is always at least a 1D array, even for a single # source value = np.atleast_1d(value[index]) newcls.__dict__[key] = value return newcls @lazyproperty def isscalar(self): """ Whether the instance is scalar (e.g., a single source). """ return self.xypos.shape == (1, 2) @property def _lazyproperties(self): """ Return all lazyproperties (even in superclasses). """ def islazyproperty(obj): return isinstance(obj, lazyproperty) return [i[0] for i in inspect.getmembers(self.__class__, predicate=islazyproperty)] def reset_ids(self): """Reset the ID column to be consecutive integers.""" self.id = np.arange(len(self)) + 1 @lazyproperty def slices(self): slices = [] for xpos, ypos in self.xypos: slc, _ = overlap_slices(self.data.shape, self.shape, (ypos, xpos), mode='trim') slices.append(slc) return slices @lazyproperty def bbox_xmin(self): return np.array([slc[1].start for slc in self.slices]) @lazyproperty def bbox_ymin(self): return np.array([slc[0].start for slc in self.slices]) @lazyproperty def cutout_data(self): cutout = [] for slc in self.slices: cdata = self.data[slc] cdata[cdata < 0] = 0.0 # exclude negative pixels cutout.append(cdata) return cutout @lazyproperty def moments(self): return np.array([_moments(arr, order=1) for arr in self.cutout_data]) @lazyproperty def cutout_centroid(self): moments = self.moments # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) ycentroid = moments[:, 1, 0] / moments[:, 0, 0] xcentroid = moments[:, 0, 1] / moments[:, 0, 0] return np.transpose((ycentroid, xcentroid)) @lazyproperty def cutout_xcentroid(self): return np.transpose(self.cutout_centroid)[1] @lazyproperty def cutout_ycentroid(self): return np.transpose(self.cutout_centroid)[0] @lazyproperty def xcentroid(self): return self.cutout_xcentroid + self.bbox_xmin @lazyproperty def ycentroid(self): return self.cutout_ycentroid + self.bbox_ymin @lazyproperty def max_value(self): return np.array([np.max(arr) for arr in self.cutout_data]) @lazyproperty def flux(self): return np.array([np.sum(arr) for arr in self.cutout_data]) @lazyproperty def mag(self): return -2.5 * np.log10(self.flux) @lazyproperty def moments_central(self): moments = np.array([_moments_central(arr, center=(xcen_, ycen_), order=2) for arr, xcen_, ycen_ in zip(self.cutout_data, self.cutout_xcentroid, self.cutout_ycentroid)]) return moments / self.moments[:, 0, 0][:, np.newaxis, np.newaxis] @lazyproperty def mu_sum(self): return self.moments_central[:, 0, 2] + self.moments_central[:, 2, 0] @lazyproperty def mu_diff(self): return self.moments_central[:, 0, 2] - self.moments_central[:, 2, 0] @lazyproperty def fwhm(self): return 2.0 * np.sqrt(np.log(2.0) * self.mu_sum) @lazyproperty def roundness(self): return np.sqrt(self.mu_diff**2 + 4.0 * self.moments_central[:, 1, 1]**2) / self.mu_sum @lazyproperty def pa(self): pa = np.rad2deg(0.5 * np.arctan2(2.0 * self.moments_central[:, 1, 1], self.mu_diff)) pa = np.where(pa < 0, pa + 180, pa) return pa def apply_filters(self): """Filter the catalog.""" newcat = self if self.peakmax is not None: mask = (self.max_value < self.peakmax) newcat = self[mask] if len(newcat) == 0: warnings.warn('Sources were found, but none pass the peakmax ' 'criterion.', NoDetectionsWarning) return None return newcat def select_brightest(self): """ Sort the catalog by the brightest fluxes and select the top brightest sources. """ newcat = self if self.brightest is not None: idx = np.argsort(self.flux)[::-1][:self.brightest] newcat = self[idx] return newcat def apply_all_filters(self): """ Apply all filters, select the brightest, and reset the source ids. """ cat = self.apply_filters() if cat is None: return None cat = cat.select_brightest() cat.reset_ids() return cat def to_table(self, columns=None): meta = {'version': _get_version_info()} table = QTable(meta=meta) if columns is None: columns = self.default_columns for column in columns: table[column] = getattr(self, column) return table ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9926338 photutils-1.3.0/photutils/detection/tests/0000755000214200020070000000000000000000000017351 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/detection/tests/__init__.py0000644000214200020070000000000000000000000021450 0ustar00lbradley././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9982674 photutils-1.3.0/photutils/detection/tests/data/0000755000214200020070000000000000000000000020262 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh08.0_fwhm01.0.txt0000644000214200020070000000646400000000000026703 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 88.46990059776188 58.48886418244315 0.8762686366344555 0.5208952612806851 -0.4122721184034913 25.0 0.0 41.219046927862664 1.7835016184670074 -0.6281837694550259 2 327.4237440417888 65.44679142919296 0.9157937804266859 0.04525792010641404 0.7509409883712366 25.0 0.0 22.751918266079162 1.0446321071558946 -0.04740842477087964 3 35.3030291035496 100.99885418235492 0.9642784405023673 0.8997709899005626 0.4262749271277977 25.0 0.0 20.102985322609683 1.0144135507640197 -0.015537604949063939 4 207.35081374580741 114.17862428357554 0.8924335237573308 0.9346385083631819 -0.518807699686835 25.0 0.0 29.754644643579955 1.3969918280271438 -0.3629846640809128 5 290.43063459156474 113.06404861735642 0.898082603591199 -0.05099656208774482 -0.23165223978862307 25.0 0.0 46.446092373907746 1.900514493186413 -0.6971779646253444 6 200.92861884647542 131.13339385033788 0.9484922230198581 0.22387509766367475 -0.27402570026586753 25.0 0.0 35.98220832083827 1.0238148922854393 -0.025553606135659993 7 314.47263346346375 129.3969056629089 0.8887239985499058 -0.415497488077957 -0.981667389821615 25.0 0.0 37.2058611011273 1.462461712936029 -0.41271126282013204 8 10.651479210211352 141.89200443111378 0.8874321043149441 0.1038427584432066 0.6135710025317431 25.0 0.0 31.413937832962418 1.033701663235285 -0.035988037644573874 9 125.38583354043459 147.12287931014475 0.9045758881095933 0.018740208509969953 0.9822435679992049 25.0 0.0 47.21336918933935 1.6322194134762291 -0.5319463475288915 10 145.14914494499064 169.8101867232563 0.8849770368050671 0.15624574204422784 0.17519144635671952 25.0 0.0 83.38951276487094 5.207804548890224 -1.7916366915254869 11 394.08757117974244 186.3394174191881 0.8802050226751206 0.35607460226075754 0.09146988324132699 25.0 0.0 110.35005452953492 7.319441130206205 -2.161194805332651 12 206.70671685848117 198.52606581209523 0.8981872955879281 -0.914696580134408 0.03140476242070839 25.0 0.0 34.44956649698919 1.1330527665066235 -0.13562533880024377 13 48.107054682499836 199.00601737015663 0.8865292982659505 -0.25299384635714167 -0.29924860045592516 25.0 0.0 46.62575574744383 1.6718429529072263 -0.5579886972773125 14 426.0377480701111 211.01291166877795 0.8767982886712559 -0.18983407274095807 0.46718907337348503 25.0 0.0 76.36050307691691 4.157089774588257 -1.5469735085756775 15 256.89065375229984 218.8702651417756 0.8919076919119704 -0.6574612471815833 0.9250674332112004 25.0 0.0 31.145480617595414 1.3162412640878085 -0.29833875446097147 16 256.72686483357546 220.39592428590072 0.8657852930536363 -0.6868946128505259 0.7806774957564329 25.0 0.0 35.92343258547273 1.3961484705669713 -0.36232901226396025 17 10.748287200873916 224.20057800236486 0.8530832269725744 -0.6801043879386687 0.5439043607509917 25.0 0.0 55.76846840338447 2.9686701641588544 -1.1814048693326866 18 355.7777559665791 251.24144545680184 0.9076091250435318 0.03794133476381934 0.3071068705575514 25.0 0.0 34.568381338063475 1.194628367857395 -0.19308205841850107 19 140.49990182298185 275.4562630541407 0.8956136505852358 -0.12260502762650799 0.28058974799126774 25.0 0.0 29.39237884586493 1.0140636761486634 -0.015163066319055899 20 434.4873254828115 287.5722770696709 0.9358461422138549 0.7057077340015893 -0.2758037043682071 25.0 0.0 33.411944319652875 1.0657776539076416 -0.06916652543885461 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh08.0_fwhm01.5.txt0000644000214200020070000001112200000000000026673 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 441.25133516377946 31.44957631443119 0.7184774674544862 0.2540826457659647 0.33820558007054197 25.0 0.0 30.239994137265167 1.0192109043077802 -0.020660153587305028 2 88.79813107802212 58.81911572547198 0.6412388176817487 0.5199389854407557 -0.44797314831347357 25.0 0.0 41.219046927862664 2.4129090339570833 -0.9563523735710178 3 327.7440399874977 64.66175083365357 0.8223868740403482 -0.11894585756005756 0.8073553959462378 25.0 0.0 22.751918266079162 1.1516880702259864 -0.15333717054005433 4 230.91695759765258 65.9894379571403 0.7813631775972321 0.4774209234454751 -0.05879164786084709 25.0 0.0 10.922343382751748 1.022964317973155 -0.02465121336733659 5 99.95188614111407 98.10665397029018 0.9127911170174968 0.5594087143983953 0.8968738122629868 25.0 0.0 24.984494810798402 1.0915006340968902 -0.09505998086117816 6 71.4226160551628 111.66081527853272 0.619266520607775 -0.05983418788263739 0.4400254302666273 25.0 0.0 35.0913969385426 1.2593500685848913 -0.2503661753928418 7 206.54934787497413 114.06617385814583 0.7025595284696313 0.9767349175473904 -0.5834171877869196 25.0 0.0 29.754644643579955 1.7568577222085846 -0.6118414797745659 8 290.7911753889566 113.64828526813636 0.7060267926521758 -0.07996579234428655 -0.22858041274721436 25.0 0.0 46.446092373907746 2.393405398069706 -0.9475406654982715 9 341.27356655729886 114.4587775240903 0.5829404636239032 -0.7700415392818774 -0.3323734952946145 25.0 0.0 28.65755220774481 1.1798300357956324 -0.17954862031485538 10 200.33443823023921 130.4136477492795 0.8372831521724797 0.22155345712781147 -0.23896573945028038 25.0 0.0 35.98220832083827 1.1482404524644518 -0.1500821074451444 11 313.52800888962287 129.7741880324062 0.6842530424213573 -0.3959581050304958 -0.9378705237493801 25.0 0.0 37.2058611011273 1.88054900173178 -0.6857116359467628 12 10.867021165882411 142.54063824155378 0.6947552024609119 0.11878849281679056 0.6710643184520064 25.0 0.0 31.413937832962418 1.3072195133461557 -0.29087130527521743 13 125.13873545366265 148.07110612241445 0.7149614754541866 -0.019729049737502204 0.9618448688309467 25.0 0.0 47.21336918933935 2.04451774222952 -0.776477209234867 14 344.82714268313856 166.27735480015838 0.5551520317059323 0.4640472588827457 -0.18830878258989417 25.0 0.0 27.631382749951754 1.025656792366861 -0.027505151167814216 15 145.04161669340027 168.43979551421813 0.6418746565917449 -0.02736696618181218 0.1153966295770878 25.0 0.0 82.74848117792747 6.884763660996454 -2.0947225911530913 16 394.65502401524253 187.37357173892647 0.6692365367195389 0.3413113809170677 0.08770438935586308 25.0 0.0 110.35005452953492 9.530860217374974 -2.44783025023155 17 480.1442854950754 188.34294135511698 0.9105534140545337 0.8093219184197902 0.9481584847725933 25.0 0.0 23.07234835743072 1.0771739794755455 -0.08071463480942372 18 48.67675424075205 200.4511294253465 0.637320895047539 -0.571948558370518 -0.3831872251686798 25.0 0.0 47.2892911443403 2.2190254912470717 -0.865405728140129 19 426.64088605663267 211.0035331399752 0.6532673887534252 -0.22682231797770286 0.43515782187184765 25.0 0.0 76.36050307691691 5.523929114370442 -1.8556202421835335 20 305.6288921959932 216.1599509336425 0.5855765105365481 -0.007370981867631505 0.3512345640013347 25.0 0.0 26.565020985170975 1.1775541535016305 -0.17745222161727764 21 257.6002296718719 217.77146625690213 0.6907143669914155 -0.6586594988741606 0.9959103449675526 25.0 0.0 31.145480617595414 1.6827007962410163 -0.5650172505850533 22 256.265775002328 220.7587474377063 0.6330394123373777 -0.6815183858246547 0.824192111519761 25.0 0.0 35.92343258547273 1.8904319045703657 -0.691402595542391 23 10.906589762292864 224.07169129851232 0.5915881373471015 -0.7302006348846717 0.4954582336704235 25.0 0.0 55.76846840338447 4.238223669541717 -1.5679596814822927 24 355.91280082770714 252.31749264525953 0.7520228837688127 0.008366802620332457 0.31569853010198345 25.0 0.0 34.568381338063475 1.4274161278306714 -0.38637649844105226 25 139.59065348749309 275.1884233884024 0.7154853585505654 -0.18441687745205232 0.2548543643003899 25.0 0.0 29.39237884586493 1.2567102535141665 -0.24808789627317712 26 121.14388458069425 284.8118762076604 0.7574664205028767 0.18170258067684428 0.16985955244315368 25.0 0.0 12.415425631454825 1.0092893140555754 -0.010039187860971983 27 433.5551160149825 288.4794012861741 0.8097201173279633 0.6995511520935703 -0.27322026456220144 25.0 0.0 33.411944319652875 1.2195121002893914 -0.21546528461092726 28 471.001033909076 297.8111400615592 0.7934292830937713 0.6600959397482591 0.23721748404523008 25.0 0.0 10.524420303596958 1.030199275938046 -0.032303100766317684 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh08.0_fwhm02.0.txt0000644000214200020070000001163700000000000026702 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 441.3499172083869 31.402878654917814 0.6332823251229039 0.30085528401312445 0.3680354928019628 25.0 0.0 30.239994137265167 1.1572788429613632 -0.15859503387229676 2 0.8496908619052633 40.04305508436788 0.45619405869493956 -0.22810071231008808 0.7232872696244564 25.0 0.0 35.54828630019193 2.6662543983339755 -1.064753962208504 3 88.79956140550843 58.84568931520913 0.5582944988778225 0.5136344718362899 -0.4745844416283111 25.0 0.0 41.219046927862664 2.773675000415222 -1.1076389304199645 4 14.009742492766007 62.28240400026239 0.593724897815523 0.11641944621802239 0.15287632672275359 25.0 0.0 21.481924963251483 1.0271337239470224 -0.029067471679799464 5 327.70218373934637 64.7320262123344 0.8815643782278983 -0.25650986945379883 0.8672581850871744 25.0 0.0 22.751918266079162 1.0752641632256226 -0.07878792932136536 6 230.95864015413127 65.96089819494524 0.7532093773427389 0.5841683591471404 0.017434633001189193 25.0 0.0 10.922343382751748 1.062076683475109 -0.06538968642057963 7 7.328434526380554 69.5156220324513 0.5970223288209795 -0.9902229999019926 -0.36777328827511824 25.0 0.0 33.2880647048335 1.7553590999611413 -0.6109149375862932 8 99.90338602734467 97.94590143760601 0.9369113497949474 0.374463505034526 0.6482107118468855 25.0 0.0 24.984494810798402 1.0642779488426795 -0.0676376598520223 9 34.271919795938956 97.94540042765118 0.43062202850526665 0.9672893343791893 -0.1317036542420994 25.0 0.0 19.27625317566386 1.008392639521492 -0.009074167513830694 10 71.39201494648567 111.67855457865032 0.5196724941219817 -0.07293019182386253 0.3886579935129361 25.0 0.0 35.0913969385426 1.501939736999388 -0.44163126906439054 11 206.5524841594021 114.05764949550421 0.65064321348857 0.9917528207729931 -0.6371226317458737 25.0 0.0 29.754644643579955 1.8986065838796236 -0.696087456404153 12 290.81980079648145 113.6713740964111 0.6430087933779546 -0.09541554335946509 -0.2261751623126485 25.0 0.0 46.446092373907746 2.630139011027921 -1.0499467572765857 13 341.22590232689697 114.47439289227742 0.5191441535865722 -0.8605565678486781 -0.3979504981737904 25.0 0.0 28.65755220774481 1.325909399182182 -0.30628462318182176 14 313.4809249369467 129.79531153878077 0.6196365438299545 -0.36416701339821556 -0.9040072517542573 25.0 0.0 37.2058611011273 2.078368421780885 -0.7943063380385879 15 200.50844425205247 130.3249145251344 0.36839322322757534 0.09087190631722986 -0.22452517495084987 25.0 0.0 31.0622608687501 1.3149161534868317 -0.2972451515721313 16 10.872821371928692 142.50648812470342 0.6466298462154676 0.13850521836332988 0.718945864012764 25.0 0.0 31.413937832962418 1.4056679733123312 -0.36970687498996746 17 124.98844282543168 147.82993328621993 0.4556966042329859 -0.04138895629887515 0.67091498778985 25.0 0.0 45.57926969566492 2.325230959735237 -0.9161652420508276 18 344.7017528266213 166.2683042839413 0.4663098831416211 0.48104307824902254 -0.1252798314146538 25.0 0.0 27.631382749951754 1.2220741014411993 -0.21774385124610166 19 145.03763842687044 168.50637255629331 0.5701020092842325 -0.029731702887011314 0.10804684626541045 25.0 0.0 82.74848117792747 7.757912197426944 -2.2243621500740023 20 394.70347564634125 187.545695732136 0.6053888128969426 -0.17663263158596582 0.15140081222163074 25.0 0.0 111.07213962758318 10.54663671636893 -2.5577849669998876 21 480.1438082712454 188.24825828577048 0.961962354511558 0.7093991969108856 0.8500100573287711 25.0 0.0 23.07234835743072 1.0204491563081264 -0.021978427370778375 22 48.70622094028462 200.38639624622746 0.5563443241052142 -0.5730567456777012 -0.38716010778330917 25.0 0.0 47.2892911443403 2.544104543607104 -1.0138373839688648 23 426.68129895275075 210.99941585113444 0.5819568229039869 -0.25649780804993694 0.4101751816817609 25.0 0.0 76.36050307691691 6.205924359380613 -1.9820161947559602 24 305.66418356557824 216.1095259373655 0.49589375430169474 0.018404255168539715 0.40299126313766576 25.0 0.0 26.565020985170975 1.3916629465991646 -0.35883516046196595 25 10.914912580605002 224.06603573687934 0.5057191364084963 -0.7830489024133768 0.45914226256028107 25.0 0.0 55.76846840338447 4.961946854725386 -1.7391302710501153 26 355.83177174460354 252.09033401412367 0.5247423718988266 -0.01542369728380577 0.03998195526685211 25.0 0.0 35.77616395203726 1.6536995591431092 -0.5461415265566998 27 139.57526527495273 275.22358348387263 0.6714573499096007 -0.2016681835803816 0.23336192730652747 25.0 0.0 29.39237884586493 1.3402186094935478 -0.3179391100037833 28 121.14216985229885 284.808001888565 0.756547266010023 0.1816100396332748 0.29688354539695194 25.0 0.0 12.415425631454825 1.0113492584189694 -0.012252900872686396 29 433.3024759192166 288.60871367718977 0.4437378655853536 0.4050695181704022 -0.2599032109478057 25.0 0.0 29.897454380832716 1.3995809443498977 -0.36499505205356075 30 470.994349464043 297.7565141346699 0.7661467132938748 0.5285364269442019 0.10769026748619974 25.0 0.0 10.524420303596958 1.0677650194503476 -0.07118922259211052 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh10.0_fwhm01.0.txt0000644000214200020070000000400600000000000026662 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 88.46990059776188 58.48886418244315 0.8762686366344555 0.5208952612806851 -0.4122721184034913 25.0 0.0 41.219046927862664 1.426801294773606 -0.38590873693488487 2 207.35081374580741 114.17862428357554 0.8924335237573308 0.9346385083631819 -0.518807699686835 25.0 0.0 29.754644643579955 1.117593462421715 -0.12070963156077162 3 290.43063459156474 113.06404861735642 0.898082603591199 -0.05099656208774482 -0.23165223978862307 25.0 0.0 46.446092373907746 1.5204115945491306 -0.45490293210520344 4 314.47263346346375 129.3969056629089 0.8887239985499058 -0.415497488077957 -0.981667389821615 25.0 0.0 37.2058611011273 1.169969370348823 -0.17043623029999083 5 125.38583354043459 147.12287931014475 0.9045758881095933 0.018740208509969953 0.9822435679992049 25.0 0.0 47.21336918933935 1.3057755307809833 -0.2896713150087505 6 145.14914494499064 169.8101867232563 0.8849770368050671 0.15624574204422784 0.17519144635671952 25.0 0.0 83.38951276487094 4.166243639112179 -1.5493616590053458 7 394.08757117974244 186.3394174191881 0.8802050226751206 0.35607460226075754 0.09146988324132699 25.0 0.0 110.35005452953492 5.855552904164964 -1.9189197728125098 8 48.107054682499836 199.00601737015663 0.8865292982659505 -0.25299384635714167 -0.29924860045592516 25.0 0.0 46.62575574744383 1.337474362325781 -0.31571366475717144 9 426.0377480701111 211.01291166877795 0.8767982886712559 -0.18983407274095807 0.46718907337348503 25.0 0.0 76.36050307691691 3.3256718196706054 -1.3046984760555367 10 256.89065375229984 218.8702651417756 0.8919076919119704 -0.6574612471815833 0.9250674332112004 25.0 0.0 31.145480617595414 1.0529930112702468 -0.05606372194083043 11 256.72686483357546 220.39592428590072 0.8657852930536363 -0.6868946128505259 0.7806774957564329 25.0 0.0 35.92343258547273 1.116918776453577 -0.12005397974381918 12 10.748287200873916 224.20057800236486 0.8530832269725744 -0.6801043879386687 0.5439043607509917 25.0 0.0 55.76846840338447 2.3749361313270834 -0.9391298368125456 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh10.0_fwhm01.5.txt0000644000214200020070000000522300000000000026671 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 88.79813107802212 58.81911572547198 0.6412388176817487 0.5199389854407557 -0.44797314831347357 25.0 0.0 41.219046927862664 1.9303272271656666 -0.7140773410508767 2 71.4226160551628 111.66081527853272 0.619266520607775 -0.05983418788263739 0.4400254302666273 25.0 0.0 35.0913969385426 1.007480054867913 -0.008091142872700756 3 206.54934787497413 114.06617385814583 0.7025595284696313 0.9767349175473904 -0.5834171877869196 25.0 0.0 29.754644643579955 1.405486177766868 -0.3695664472544251 4 290.7911753889566 113.64828526813636 0.7060267926521758 -0.07996579234428655 -0.22858041274721436 25.0 0.0 46.446092373907746 1.914724318455765 -0.7052656329781305 5 313.52800888962287 129.7741880324062 0.6842530424213573 -0.3959581050304958 -0.9378705237493801 25.0 0.0 37.2058611011273 1.504439201385424 -0.4434366034266216 6 10.867021165882411 142.54063824155378 0.6947552024609119 0.11878849281679056 0.6710643184520064 25.0 0.0 31.413937832962418 1.0457756106769245 -0.04859627275507633 7 125.13873545366265 148.07110612241445 0.7149614754541866 -0.019729049737502204 0.9618448688309467 25.0 0.0 47.21336918933935 1.6356141937836162 -0.5342021767147259 8 145.04161669340027 168.43979551421813 0.6418746565917449 -0.02736696618181218 0.1153966295770878 25.0 0.0 82.74848117792747 5.507810928797164 -1.8524475586329505 9 394.65502401524253 187.37357173892647 0.6692365367195389 0.3413113809170677 0.08770438935586308 25.0 0.0 110.35005452953492 7.62468817389998 -2.2055552177114084 10 48.67675424075205 200.4511294253465 0.637320895047539 -0.571948558370518 -0.3831872251686798 25.0 0.0 47.2892911443403 1.7752203929976573 -0.6231306956199879 11 426.64088605663267 211.0035331399752 0.6532673887534252 -0.22682231797770286 0.43515782187184765 25.0 0.0 76.36050307691691 4.419143291496353 -1.6133452096633925 12 257.6002296718719 217.77146625690213 0.6907143669914155 -0.6586594988741606 0.9959103449675526 25.0 0.0 31.145480617595414 1.346160636992813 -0.32274221806491205 13 256.265775002328 220.7587474377063 0.6330394123373777 -0.6815183858246547 0.824192111519761 25.0 0.0 35.92343258547273 1.5123455236562926 -0.4491275630222501 14 10.906589762292864 224.07169129851232 0.5915881373471015 -0.7302006348846717 0.4954582336704235 25.0 0.0 55.76846840338447 3.3905789356333735 -1.3256846489621514 15 355.91280082770714 252.31749264525953 0.7520228837688127 0.008366802620332457 0.31569853010198345 25.0 0.0 34.568381338063475 1.1419329022645373 -0.14410146592091136 16 139.59065348749309 275.1884233884024 0.7154853585505654 -0.18441687745205232 0.2548543643003899 25.0 0.0 29.39237884586493 1.0053682028113333 -0.00581286375303618 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/daofind_test_thresh10.0_fwhm02.0.txt0000644000214200020070000000645700000000000026677 0ustar00lbradleyid xcentroid ycentroid sharpness roundness1 roundness2 npix sky peak flux mag 1 0.8496908619052633 40.04305508436788 0.45619405869493956 -0.22810071231008808 0.7232872696244564 25.0 0.0 35.54828630019193 2.1330035186671803 -0.8224789296883628 2 88.79956140550843 58.84568931520913 0.5582944988778225 0.5136344718362899 -0.4745844416283111 25.0 0.0 41.219046927862664 2.2189400003321778 -0.8653638978998236 3 7.328434526380554 69.5156220324513 0.5970223288209795 -0.9902229999019926 -0.36777328827511824 25.0 0.0 33.2880647048335 1.404287279968913 -0.3686399050661522 4 71.39201494648567 111.67855457865032 0.5196724941219817 -0.07293019182386253 0.3886579935129361 25.0 0.0 35.0913969385426 1.2015517895995103 -0.19935623654424928 5 206.5524841594021 114.05764949550421 0.65064321348857 0.9917528207729931 -0.6371226317458737 25.0 0.0 29.754644643579955 1.5188852671036988 -0.45381242388401194 6 290.81980079648145 113.6713740964111 0.6430087933779546 -0.09541554335946509 -0.2261751623126485 25.0 0.0 46.446092373907746 2.1041112088223364 -0.8076717247564446 7 341.22590232689697 114.47439289227742 0.5191441535865722 -0.8605565678486781 -0.3979504981737904 25.0 0.0 28.65755220774481 1.0607275193457455 -0.0640095906616806 8 313.4809249369467 129.79531153878077 0.6196365438299545 -0.36416701339821556 -0.9040072517542573 25.0 0.0 37.2058611011273 1.662694737424708 -0.5520313055184468 9 200.50844425205247 130.3249145251344 0.36839322322757534 0.09087190631722986 -0.22452517495084987 25.0 0.0 31.0622608687501 1.0519329227894652 -0.0549701190519901 10 10.872821371928692 142.50648812470342 0.6466298462154676 0.13850521836332988 0.718945864012764 25.0 0.0 31.413937832962418 1.124534378649865 -0.12743184246982645 11 124.98844282543168 147.82993328621993 0.4556966042329859 -0.04138895629887515 0.67091498778985 25.0 0.0 45.57926969566492 1.8601847677881895 -0.6738902095306866 12 145.03763842687044 168.50637255629331 0.5701020092842325 -0.029731702887011314 0.10804684626541045 25.0 0.0 82.74848117792747 6.206329757941555 -1.9820871175538612 13 394.70347564634125 187.545695732136 0.6053888128969426 -0.17663263158596582 0.15140081222163074 25.0 0.0 111.07213962758318 8.437309373095143 -2.315509934479746 14 48.70622094028462 200.38639624622746 0.5563443241052142 -0.5730567456777012 -0.38716010778330917 25.0 0.0 47.2892911443403 2.035283634885683 -0.7715623514487234 15 426.68129895275075 210.99941585113444 0.5819568229039869 -0.25649780804993694 0.4101751816817609 25.0 0.0 76.36050307691691 4.96473948750449 -1.7397411622358192 16 305.66418356557824 216.1095259373655 0.49589375430169474 0.018404255168539715 0.40299126313766576 25.0 0.0 26.565020985170975 1.1133303572793316 -0.11656012794182484 17 10.914912580605002 224.06603573687934 0.5057191364084963 -0.7830489024133768 0.45914226256028107 25.0 0.0 55.76846840338447 3.9695574837803087 -1.4968552385299745 18 355.83177174460354 252.09033401412367 0.5247423718988266 -0.01542369728380577 0.03998195526685211 25.0 0.0 35.77616395203726 1.3229596473144873 -0.3038664940365588 19 139.57526527495273 275.22358348387263 0.6714573499096007 -0.2016681835803816 0.23336192730652747 25.0 0.0 29.39237884586493 1.072174887594838 -0.07566407748364207 20 433.3024759192166 288.60871367718977 0.4437378655853536 0.4050695181704022 -0.2599032109478057 25.0 0.0 29.897454380832716 1.1196647554799182 -0.12272001953341974 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm01.0.txt0000644000214200020070000000232300000000000027741 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 333.32967620268926 0.4922715732671872 1.2389649115388133 1.2389649115388133 0.1632730683139897 26.751332591232657 5.0 2.757612577414644 7.470385598813738 17.87026271806645 -3.1303273432349914 2 145.0334216330022 168.39799687136576 1.8918921328609848 1.8918921328609848 0.028604480029024524 118.25194333308633 12.0 16.46507005975501 66.92444270511594 406.86074671794165 -6.523614479572233 3 394.7628492905675 187.59049488469253 1.8259269954390203 1.8259269954390203 0.10712610861905057 131.1948064263936 11.0 20.151710346274722 90.92042928130846 527.6471044360048 -6.80585889802106 4 89.21399519459558 198.23805598325862 1.9699369976778611 1.9699369976778611 0.140512496831343 139.49061946497739 10.0 5.046355096294733 8.916265520602554 20.380914697547418 -3.2730591782220264 5 355.940683491333 251.70235184396 1.7522952194564725 1.7522952194564725 0.11598331430783908 52.09576852830077 10.0 19.585761501344383 16.190402450692876 88.75108703293594 -4.870434202614015 6 378.5365504434283 273.08553439796117 1.9282124502623856 1.9282124502623856 0.09786276609110564 14.700858135363946 8.0 4.744381064470213 8.45586154664661 18.53113999199971 -3.1697553422808005 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm01.5.txt0000644000214200020070000000504300000000000027750 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 333.32967620268926 0.4922715732671872 1.2389649115388133 0.8259766076925422 0.1632730683139897 26.751332591232657 5.0 2.757612577414644 7.470385598813738 17.87026271806645 -3.1303273432349914 2 230.98520543324977 65.96062094855215 1.1045257887737336 0.7363505258491557 0.08474087768196263 175.16563683708682 7.0 4.906490485436556 6.015852897315193 10.355696896573836 -2.5379483261935247 3 71.21785006913522 111.76487474565829 2.041287275833218 1.360858183888812 0.13679659319094278 77.80260478241559 11.0 21.001696161595063 14.089700776947538 107.15742741630842 -5.075055696823615 4 290.88594008616707 113.76868287298815 2.0132511884469353 1.3421674589646235 0.09921593553744701 172.63151133250068 12.0 21.19225795812126 25.253834415786486 157.2638285640319 -5.491572110730435 5 200.23683161129125 130.21911528245474 2.068730352200946 1.3791535681339642 0.0772733520073215 8.31574194223727 13.0 20.931309472117615 15.050898848720653 96.98092311348633 -4.966715784084392 6 344.3860440796261 166.22517276873882 1.8853557992763632 1.2569038661842422 0.11363886565891375 142.91674058093935 11.0 18.685078458637985 11.30249791672627 62.9878292683161 -4.498141604100731 7 145.04228403850357 168.6571618278717 1.9066993974327817 1.2711329316218545 0.01132060172865575 34.75701626174368 12.0 16.35083410366516 67.03867866120578 413.9858186416701 -6.5424636608266695 8 394.7628492905675 187.59049488469253 1.8259269954390203 1.2172846636260135 0.10712610861905057 131.1948064263936 11.0 20.151710346274722 90.92042928130846 527.6471044360048 -6.80585889802106 9 243.47516596181347 197.2493890490569 1.7827428108977648 1.1884952072651764 0.17856028127225676 98.90230482066524 8.0 14.345012293985468 8.2293190831917 34.45785068493779 -3.843220461887511 10 439.9142185199256 197.98835995057703 1.431190377976779 0.9541269186511859 0.14568617232313683 165.69248581691983 8.0 5.521741711694806 6.475686441345677 12.410831724229434 -2.734502217876459 11 48.84191877412477 200.23603953697418 2.044906279964065 1.3632708533093767 0.15397555058991969 8.669913148933764 13.0 21.891667190520494 25.397623953819803 185.00884809430943 -5.667981247854074 12 305.8226567441267 215.56678011460988 1.8118994096837677 1.2079329397891785 0.15302459233581697 71.90351161795856 10.0 16.561617853554058 12.39941531540315 70.11439579638966 -4.614517989747707 13 355.940683491333 251.70235184396 1.7522952194564725 1.1681968129709817 0.11598331430783908 52.09576852830077 10.0 19.585761501344383 16.190402450692876 88.75108703293594 -4.870434202614015 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm02.0.txt0000644000214200020070000000616500000000000027752 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 0.9463803041037677 40.05930171275073 2.1172882811901297 1.0586441405950648 0.15700998967332774 103.73294282811226 12.0 12.639904993835687 22.908381306356247 185.7046678317134 -5.672057050519047 2 13.990504104618 62.55334108277186 1.9112198263358675 0.9556099131679338 0.15520530817401776 50.54366391030623 11.0 13.70300905281337 7.8569227383370634 45.67009375413011 -4.14907975787997 3 230.98520543324977 65.96062094855215 1.1045257887737336 0.5522628943868668 0.08474087768196263 175.16563683708682 7.0 4.906490485436556 6.015852897315193 10.355696896573836 -2.5379483261935247 4 71.21785006913522 111.76487474565829 2.041287275833218 1.020643637916609 0.13679659319094278 77.80260478241559 11.0 21.001696161595063 14.089700776947538 107.15742741630842 -5.075055696823615 5 290.88594008616707 113.76868287298815 2.0132511884469353 1.0066255942234676 0.09921593553744701 172.63151133250068 12.0 21.19225795812126 25.253834415786486 157.2638285640319 -5.491572110730435 6 476.3854019214201 123.17381910435793 1.9095864585977254 0.9547932292988627 0.049460563907413366 141.18558248492255 10.0 13.976425258573249 7.392939939729255 49.30462300038852 -4.232219105973428 7 344.3860440796261 166.22517276873882 1.8853557992763632 0.9426778996381816 0.11363886565891375 142.91674058093935 11.0 18.685078458637985 11.30249791672627 62.9878292683161 -4.498141604100731 8 145.04228403850357 168.6571618278717 1.9066993974327817 0.9533496987163909 0.01132060172865575 34.75701626174368 12.0 16.35083410366516 67.03867866120578 413.9858186416701 -6.5424636608266695 9 433.96150057644644 172.11618975119973 1.5605837424622429 0.7802918712311214 0.09193780106796648 95.87470166499466 8.0 4.626744929765067 5.595633376366347 17.514050923880667 -3.1084665203219473 10 394.77243654370204 187.3653258347458 1.8382851657518942 0.9191425828759471 0.07664537864845197 57.84086299955225 11.0 20.06339188622815 91.00874774135504 535.3679088447971 -6.821630837610465 11 243.47516596181347 197.2493890490569 1.7827428108977648 0.8913714054488824 0.17856028127225676 98.90230482066524 8.0 14.345012293985468 8.2293190831917 34.45785068493779 -3.843220461887511 12 439.9142185199256 197.98835995057703 1.431190377976779 0.7155951889883895 0.14568617232313683 165.69248581691983 8.0 5.521741711694806 6.475686441345677 12.410831724229434 -2.734502217876459 13 48.84191877412477 200.23603953697418 2.044906279964065 1.0224531399820325 0.15397555058991969 8.669913148933764 13.0 21.891667190520494 25.397623953819803 185.00884809430943 -5.667981247854074 14 305.8226567441267 215.56678011460988 1.8118994096837677 0.9059497048418839 0.15302459233581697 71.90351161795856 10.0 16.561617853554058 12.39941531540315 70.11439579638966 -4.614517989747707 15 292.1680865716507 245.66007865305278 1.9132639276510426 0.9566319638255213 0.13981297941973697 131.64176988617768 12.0 17.382922811106205 26.281111155294354 151.39790010868757 -5.450299628835927 16 355.815359095706 252.07315473269793 2.040873269492875 1.0204366347464375 0.04014555235557765 166.92243604044648 13.0 20.266399267107513 15.509764684929745 112.63065620256117 -5.129141535958461 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm01.0.txt0000644000214200020070000000121600000000000027732 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 145.0334216330022 168.39799687136576 1.8918921328609848 1.8918921328609848 0.028604480029024524 118.25194333308633 12.0 16.46507005975501 66.92444270511594 406.86074671794165 -6.523614479572233 2 394.7628492905675 187.59049488469253 1.8259269954390203 1.8259269954390203 0.10712610861905057 131.1948064263936 11.0 20.151710346274722 90.92042928130846 527.6471044360048 -6.80585889802106 3 355.940683491333 251.70235184396 1.7522952194564725 1.7522952194564725 0.11598331430783908 52.09576852830077 10.0 19.585761501344383 16.190402450692876 88.75108703293594 -4.870434202614015 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm01.5.txt0000644000214200020070000000262700000000000027746 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 71.21785006913522 111.76487474565829 2.041287275833218 1.360858183888812 0.13679659319094278 77.80260478241559 11.0 21.001696161595063 14.089700776947538 107.15742741630842 -5.075055696823615 2 290.88594008616707 113.76868287298815 2.0132511884469353 1.3421674589646235 0.09921593553744701 172.63151133250068 12.0 21.19225795812126 25.253834415786486 157.2638285640319 -5.491572110730435 3 145.04228403850357 168.6571618278717 1.9066993974327817 1.2711329316218545 0.01132060172865575 34.75701626174368 12.0 16.35083410366516 67.03867866120578 413.9858186416701 -6.5424636608266695 4 394.7628492905675 187.59049488469253 1.8259269954390203 1.2172846636260135 0.10712610861905057 131.1948064263936 11.0 20.151710346274722 90.92042928130846 527.6471044360048 -6.80585889802106 5 48.84191877412477 200.23603953697418 2.044906279964065 1.3632708533093767 0.15397555058991969 8.669913148933764 13.0 21.891667190520494 25.397623953819803 185.00884809430943 -5.667981247854074 6 305.8226567441267 215.56678011460988 1.8118994096837677 1.2079329397891785 0.15302459233581697 71.90351161795856 10.0 16.561617853554058 12.39941531540315 70.11439579638966 -4.614517989747707 7 355.940683491333 251.70235184396 1.7522952194564725 1.1681968129709817 0.11598331430783908 52.09576852830077 10.0 19.585761501344383 16.190402450692876 88.75108703293594 -4.870434202614015 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1524619372.0 photutils-1.3.0/photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm02.0.txt0000644000214200020070000000374700000000000027746 0ustar00lbradleyid xcentroid ycentroid fwhm sharpness roundness pa npix sky peak flux mag 1 0.9463803041037677 40.05930171275073 2.1172882811901297 1.0586441405950648 0.15700998967332774 103.73294282811226 12.0 12.639904993835687 22.908381306356247 185.7046678317134 -5.672057050519047 2 71.21785006913522 111.76487474565829 2.041287275833218 1.020643637916609 0.13679659319094278 77.80260478241559 11.0 21.001696161595063 14.089700776947538 107.15742741630842 -5.075055696823615 3 290.88594008616707 113.76868287298815 2.0132511884469353 1.0066255942234676 0.09921593553744701 172.63151133250068 12.0 21.19225795812126 25.253834415786486 157.2638285640319 -5.491572110730435 4 344.3860440796261 166.22517276873882 1.8853557992763632 0.9426778996381816 0.11363886565891375 142.91674058093935 11.0 18.685078458637985 11.30249791672627 62.9878292683161 -4.498141604100731 5 145.04228403850357 168.6571618278717 1.9066993974327817 0.9533496987163909 0.01132060172865575 34.75701626174368 12.0 16.35083410366516 67.03867866120578 413.9858186416701 -6.5424636608266695 6 394.77243654370204 187.3653258347458 1.8382851657518942 0.9191425828759471 0.07664537864845197 57.84086299955225 11.0 20.06339188622815 91.00874774135504 535.3679088447971 -6.821630837610465 7 48.84191877412477 200.23603953697418 2.044906279964065 1.0224531399820325 0.15397555058991969 8.669913148933764 13.0 21.891667190520494 25.397623953819803 185.00884809430943 -5.667981247854074 8 305.8226567441267 215.56678011460988 1.8118994096837677 0.9059497048418839 0.15302459233581697 71.90351161795856 10.0 16.561617853554058 12.39941531540315 70.11439579638966 -4.614517989747707 9 292.1680865716507 245.66007865305278 1.9132639276510426 0.9566319638255213 0.13981297941973697 131.64176988617768 12.0 17.382922811106205 26.281111155294354 151.39790010868757 -5.450299628835927 10 355.815359095706 252.07315473269793 2.040873269492875 1.0204366347464375 0.04014555235557765 166.92243604044648 13.0 20.266399267107513 15.509764684929745 112.63065620256117 -5.129141535958461 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/detection/tests/test_daofinder.py0000644000214200020070000001542400000000000022723 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for DAOStarFinder. """ import itertools import os.path as op from astropy.table import Table from astropy.tests.helper import catch_warnings import numpy as np from numpy.testing import assert_allclose import pytest from ..daofinder import DAOStarFinder from ...datasets import make_100gaussians_image from ...utils.exceptions import NoDetectionsWarning from ...utils._optional_deps import HAS_SCIPY # noqa DATA = make_100gaussians_image() THRESHOLDS = [8.0, 10.0] FWHMS = [1.0, 1.5, 2.0] @pytest.mark.skipif('not HAS_SCIPY') class TestDAOStarFinder: @pytest.mark.parametrize(('threshold', 'fwhm'), list(itertools.product(THRESHOLDS, FWHMS))) def test_daofind(self, threshold, fwhm): starfinder = DAOStarFinder(threshold, fwhm, sigma_radius=1.5) tbl = starfinder(DATA) datafn = f'daofind_test_thresh{threshold:04.1f}_fwhm{fwhm:04.1f}.txt' datafn = op.join(op.dirname(op.abspath(__file__)), 'data', datafn) tbl_ref = Table.read(datafn, format='ascii') assert tbl.colnames == tbl_ref.colnames for col in tbl.colnames: assert_allclose(tbl[col], tbl_ref[col]) def test_daofind_threshold_fwhm_inputs(self): with pytest.raises(TypeError): DAOStarFinder(threshold=np.ones((2, 2)), fwhm=3.) with pytest.raises(TypeError): DAOStarFinder(threshold=3., fwhm=np.ones((2, 2))) def test_daofind_include_border(self): starfinder = DAOStarFinder(threshold=10, fwhm=2, sigma_radius=1.5, exclude_border=False) tbl = starfinder(DATA) assert len(tbl) == 20 def test_daofind_exclude_border(self): starfinder = DAOStarFinder(threshold=10, fwhm=2, sigma_radius=1.5, exclude_border=True) tbl = starfinder(DATA) assert len(tbl) == 19 def test_daofind_nosources(self): data = np.ones((3, 3)) with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = DAOStarFinder(threshold=10, fwhm=1) tbl = starfinder(data) assert tbl is None assert 'No sources were found.' in str(warning_lines[0].message) def test_daofind_sharpness(self): """Sources found, but none pass the sharpness criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = DAOStarFinder(threshold=50, fwhm=1.0, sharplo=1.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_daofind_roundness(self): """Sources found, but none pass the roundness criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = DAOStarFinder(threshold=50, fwhm=1.0, roundlo=1.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_daofind_peakmax(self): """Sources found, but none pass the peakmax criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = DAOStarFinder(threshold=50, fwhm=1.0, peakmax=1.0) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_daofind_flux_negative(self): """Test handling of negative flux (here created by large sky).""" data = np.ones((5, 5)) data[2, 2] = 10. starfinder = DAOStarFinder(threshold=0.1, fwhm=1.0, sky=10) tbl = starfinder(data) assert not np.isfinite(tbl['mag']) def test_daofind_negative_fit_peak(self): """ Regression test that sources with negative fit peaks (i.e., hx/hy<=0) are excluded. """ starfinder = DAOStarFinder(threshold=7., fwhm=1.5, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf) tbl = starfinder(DATA) assert len(tbl) == 102 def test_daofind_peakmax_filtering(self): """ Regression test that objects with ``peak`` >= ``peakmax`` are filtered out. """ peakmax = 20 starfinder = DAOStarFinder(threshold=7., fwhm=1.5, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf, peakmax=peakmax) tbl = starfinder(DATA) assert len(tbl) == 37 assert all(tbl['peak'] < peakmax) def test_daofind_brightest_filtering(self): """ Regression test that only top ``brightest`` objects are selected. """ brightest = 40 peakmax = 20 starfinder = DAOStarFinder(threshold=7., fwhm=1.5, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf, brightest=brightest) tbl = starfinder(DATA) # combined with peakmax assert len(tbl) == brightest starfinder = DAOStarFinder(threshold=7., fwhm=1.5, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf, brightest=brightest, peakmax=peakmax) tbl = starfinder(DATA) assert len(tbl) == 37 def test_daofind_mask(self): """Test DAOStarFinder with a mask.""" starfinder = DAOStarFinder(threshold=10, fwhm=1.5) mask = np.zeros(DATA.shape, dtype=bool) mask[100:200] = True tbl1 = starfinder(DATA) tbl2 = starfinder(DATA, mask=mask) assert len(tbl1) > len(tbl2) def test_inputs(self): with pytest.raises(ValueError): DAOStarFinder(threshold=10, fwhm=1.5, brightest=-1) with pytest.raises(ValueError): DAOStarFinder(threshold=10, fwhm=1.5, brightest=3.1) def test_xycoords(self): starfinder1 = DAOStarFinder(threshold=30, fwhm=2, sigma_radius=1.5, exclude_border=True) tbl1 = starfinder1(DATA) xycoords = np.array([[145, 169], [395, 187], [427, 211], [11, 224]]) starfinder2 = DAOStarFinder(threshold=30, fwhm=2, sigma_radius=1.5, exclude_border=True, xycoords=xycoords) tbl2 = starfinder2(DATA) assert np.all(tbl1 == tbl2) def test_invalid_xycoords(self): xycoords = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]) with pytest.raises(ValueError): DAOStarFinder(threshold=10, fwhm=1.5, xycoords=xycoords) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/detection/tests/test_irafstarfinder.py0000644000214200020070000001345000000000000023770 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for IRAFStarFinder. """ import itertools import os.path as op from astropy.table import Table from astropy.tests.helper import catch_warnings import numpy as np from numpy.testing import assert_allclose import pytest from ..irafstarfinder import IRAFStarFinder from ...datasets import make_100gaussians_image from ...utils.exceptions import NoDetectionsWarning from ...utils._optional_deps import HAS_SCIPY # noqa DATA = make_100gaussians_image() THRESHOLDS = [8.0, 10.0] FWHMS = [1.0, 1.5, 2.0] @pytest.mark.skipif('not HAS_SCIPY') class TestIRAFStarFinder: @pytest.mark.parametrize(('threshold', 'fwhm'), list(itertools.product(THRESHOLDS, FWHMS))) def test_irafstarfind(self, threshold, fwhm): starfinder = IRAFStarFinder(threshold, fwhm, sigma_radius=1.5) tbl = starfinder(DATA) datafn = (f'irafstarfind_test_thresh{threshold:04.1f}_' f'fwhm{fwhm:04.1f}.txt') datafn = op.join(op.dirname(op.abspath(__file__)), 'data', datafn) tbl_ref = Table.read(datafn, format='ascii') assert tbl.colnames == tbl_ref.colnames for col in tbl.colnames: assert_allclose(tbl[col], tbl_ref[col]) def test_irafstarfind_threshold_fwhm_inputs(self): with pytest.raises(TypeError): IRAFStarFinder(threshold=np.ones((2, 2)), fwhm=3.) with pytest.raises(TypeError): IRAFStarFinder(threshold=3., fwhm=np.ones((2, 2))) def test_irafstarfind_nosources(self): data = np.ones((3, 3)) with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = IRAFStarFinder(threshold=10, fwhm=1) tbl = starfinder(data) assert tbl is None assert 'No sources were found.' in str(warning_lines[0].message) def test_irafstarfind_sharpness(self): """Sources found, but none pass the sharpness criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = IRAFStarFinder(threshold=50, fwhm=1.0, sharplo=2.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_irafstarfind_roundness(self): """Sources found, but none pass the roundness criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = IRAFStarFinder(threshold=50, fwhm=1.0, roundlo=1.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_irafstarfind_peakmax(self): """Sources found, but none pass the peakmax criteria.""" with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = IRAFStarFinder(threshold=50, fwhm=1.0, peakmax=1.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_irafstarfind_sky(self): starfinder = IRAFStarFinder(threshold=25.0, fwhm=2.0, sky=10.) tbl = starfinder(DATA) assert len(tbl) == 4 def test_irafstarfind_largesky(self): with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = IRAFStarFinder(threshold=25.0, fwhm=2.0, sky=100.) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_irafstarfind_peakmax_filtering(self): """ Regression test that objects with ``peak`` >= ``peakmax`` are filtered out. """ peakmax = 20 starfinder = IRAFStarFinder(threshold=7., fwhm=2, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf, peakmax=peakmax) tbl = starfinder(DATA) assert len(tbl) == 117 assert all(tbl['peak'] < peakmax) def test_irafstarfind_brightest_filtering(self): """ Regression test that only top ``brightest`` objects are selected. """ brightest = 40 starfinder = IRAFStarFinder(threshold=7., fwhm=2, roundlo=-np.inf, roundhi=np.inf, sharplo=-np.inf, sharphi=np.inf, brightest=brightest) tbl = starfinder(DATA) assert len(tbl) == brightest def test_irafstarfind_mask(self): """Test IRAFStarFinder with a mask.""" starfinder = IRAFStarFinder(threshold=10, fwhm=1.5) mask = np.zeros(DATA.shape, dtype=bool) mask[100:200] = True tbl1 = starfinder(DATA) tbl2 = starfinder(DATA, mask=mask) assert len(tbl1) > len(tbl2) def test_inputs(self): with pytest.raises(ValueError): IRAFStarFinder(10, 1.5, brightest=-1) with pytest.raises(ValueError): IRAFStarFinder(10, 1.5, brightest=3.1) def test_xycoords(self): starfinder1 = IRAFStarFinder(threshold=30, fwhm=2, sigma_radius=1.5, exclude_border=True) tbl1 = starfinder1(DATA) xycoords = np.array([[145, 169], [395, 187], [427, 211], [11, 224]]) starfinder2 = IRAFStarFinder(threshold=30, fwhm=2, sigma_radius=1.5, exclude_border=True, xycoords=xycoords) tbl2 = starfinder2(DATA) assert np.all(tbl1 == tbl2) def test_invalid_xycoords(self): xycoords = np.array([[1, 2, 3, 4], [5, 6, 7, 8]]) with pytest.raises(ValueError): IRAFStarFinder(threshold=10, fwhm=1.5, xycoords=xycoords) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/detection/tests/test_peakfinder.py0000644000214200020070000001301300000000000023070 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the peakfinder module. """ import warnings from astropy.tests.helper import assert_quantity_allclose, catch_warnings import numpy as np from numpy.testing import assert_array_equal import pytest from ..peakfinder import find_peaks from ...centroids import centroid_com from ...datasets import make_4gaussians_image, make_gwcs, make_wcs from ...utils.exceptions import NoDetectionsWarning from ...utils._optional_deps import HAS_GWCS, HAS_SCIPY # noqa PEAKDATA = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 1]]).astype(float) PEAKREF1 = np.array([[0, 0], [2, 2]]) IMAGE = make_4gaussians_image() FITSWCS = make_wcs(IMAGE.shape) @pytest.mark.skipif('not HAS_SCIPY') class TestFindPeaks: def test_box_size(self): """Test with box_size.""" tbl = find_peaks(PEAKDATA, 0.1, box_size=3) assert_array_equal(tbl['x_peak'], PEAKREF1[:, 1]) assert_array_equal(tbl['y_peak'], PEAKREF1[:, 0]) assert_array_equal(tbl['peak_value'], [1., 1.]) def test_footprint(self): """Test with footprint.""" tbl = find_peaks(PEAKDATA, 0.1, footprint=np.ones((3, 3))) assert_array_equal(tbl['x_peak'], PEAKREF1[:, 1]) assert_array_equal(tbl['y_peak'], PEAKREF1[:, 0]) assert_array_equal(tbl['peak_value'], [1., 1.]) def test_mask(self): """Test with mask.""" mask = np.zeros(PEAKDATA.shape, dtype=bool) mask[0, 0] = True tbl = find_peaks(PEAKDATA, 0.1, box_size=3, mask=mask) assert len(tbl) == 1 assert_array_equal(tbl['x_peak'], PEAKREF1[1, 0]) assert_array_equal(tbl['y_peak'], PEAKREF1[1, 1]) assert_array_equal(tbl['peak_value'], 1.0) def test_maskshape(self): """Test if make shape doesn't match data shape.""" with pytest.raises(ValueError): find_peaks(PEAKDATA, 0.1, mask=np.ones((5, 5))) def test_thresholdshape(self): """Test if threshold shape doesn't match data shape.""" with pytest.raises(ValueError): find_peaks(PEAKDATA, np.ones((2, 2))) def test_npeaks(self): """Test npeaks.""" tbl = find_peaks(PEAKDATA, 0.1, box_size=3, npeaks=1) assert_array_equal(tbl['x_peak'], PEAKREF1[1, 1]) assert_array_equal(tbl['y_peak'], PEAKREF1[1, 0]) def test_border_width(self): """Test border exclusion.""" with catch_warnings(NoDetectionsWarning) as warning_lines: tbl = find_peaks(PEAKDATA, 0.1, box_size=3, border_width=3) assert tbl is None assert len(warning_lines) > 0 assert ('No local peaks were found.' in str(warning_lines[0].message)) def test_box_size_int(self): """Test non-integer box_size.""" tbl1 = find_peaks(PEAKDATA, 0.1, box_size=5.) tbl2 = find_peaks(PEAKDATA, 0.1, box_size=5.5) assert_array_equal(tbl1, tbl2) def test_centroid_func_callable(self): """Test that centroid_func is callable.""" with pytest.raises(TypeError): find_peaks(PEAKDATA, 0.1, box_size=2, centroid_func=True) def test_wcs(self): """Test with astropy WCS.""" columns = ['skycoord_peak', 'skycoord_centroid'] tbl = find_peaks(IMAGE, 100, wcs=FITSWCS, centroid_func=centroid_com) for column in columns: assert column in tbl.colnames @pytest.mark.skipif('not HAS_GWCS') def test_gwcs(self): """Test with gwcs.""" columns = ['skycoord_peak', 'skycoord_centroid'] gwcs_obj = make_gwcs(IMAGE.shape) tbl = find_peaks(IMAGE, 100, wcs=gwcs_obj, centroid_func=centroid_com) for column in columns: assert column in tbl.colnames @pytest.mark.skipif('not HAS_GWCS') def test_wcs_values(self): gwcs_obj = make_gwcs(IMAGE.shape) tbl1 = find_peaks(IMAGE, 100, wcs=FITSWCS, centroid_func=centroid_com) tbl2 = find_peaks(IMAGE, 100, wcs=gwcs_obj, centroid_func=centroid_com) columns = ['skycoord_peak', 'skycoord_centroid'] for column in columns: assert_quantity_allclose(tbl1[column].ra, tbl2[column].ra) assert_quantity_allclose(tbl1[column].dec, tbl2[column].dec) def test_constant_array(self): """Test for empty output table when data is constant.""" data = np.ones((10, 10)) with catch_warnings(NoDetectionsWarning) as warning_lines: tbl = find_peaks(data, 0.) assert tbl is None assert len(warning_lines) > 0 assert ('Input data is constant.' in str(warning_lines[0].message)) def test_no_peaks(self): """ Test for an empty output table with the expected column names when no peaks are found. """ with catch_warnings(NoDetectionsWarning): tbl = find_peaks(IMAGE, 10000) assert tbl is None tbl = find_peaks(IMAGE, 100000, centroid_func=centroid_com) assert tbl is None tbl = find_peaks(IMAGE, 100000, wcs=FITSWCS) assert tbl is None tbl = find_peaks(IMAGE, 100000, wcs=FITSWCS, centroid_func=centroid_com) assert tbl is None def test_data_nans(self): """Test that data with NaNs does not issue Runtime warning.""" data = np.copy(PEAKDATA) data[1, 1] = np.nan with warnings.catch_warnings(): warnings.filterwarnings('error') find_peaks(data, 0.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/detection/tests/test_starfinder.py0000644000214200020070000000530700000000000023130 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for StarFinder. """ from astropy.modeling.models import Gaussian2D from astropy.tests.helper import catch_warnings import numpy as np import pytest from ..starfinder import StarFinder from ...datasets import make_100gaussians_image from ...utils.exceptions import NoDetectionsWarning from ...utils._optional_deps import HAS_SCIPY # noqa DATA = make_100gaussians_image() y, x = np.mgrid[0:25, 0:25] g = Gaussian2D(1, 12, 12, 3, 2, theta=np.pi / 6.) PSF = g(x, y) @pytest.mark.skipif('not HAS_SCIPY') class TestStarFinder: def test_starfind(self): finder1 = StarFinder(10, PSF) finder2 = StarFinder(30, PSF) tbl1 = finder1(DATA) tbl2 = finder2(DATA) assert len(tbl1) > len(tbl2) def test_inputs(self): with pytest.raises(ValueError): StarFinder(10, PSF, min_separation=-1) with pytest.raises(ValueError): StarFinder(10, PSF, brightest=-1) with pytest.raises(ValueError): StarFinder(10, PSF, brightest=3.1) def test_nosources(self): with catch_warnings(NoDetectionsWarning) as warning_lines: finder = StarFinder(100, PSF) tbl = finder(DATA) assert tbl is None assert 'No sources were found.' in str(warning_lines[0].message) def test_min_separation(self): finder1 = StarFinder(10, PSF, min_separation=0) finder2 = StarFinder(10, PSF, min_separation=50) tbl1 = finder1(DATA) tbl2 = finder2(DATA) assert len(tbl1) > len(tbl2) def test_peakmax(self): finder1 = StarFinder(10, PSF, peakmax=None) finder2 = StarFinder(10, PSF, peakmax=50) tbl1 = finder1(DATA) tbl2 = finder2(DATA) assert len(tbl1) > len(tbl2) with catch_warnings(NoDetectionsWarning) as warning_lines: starfinder = StarFinder(10, PSF, peakmax=5) tbl = starfinder(DATA) assert tbl is None assert ('Sources were found, but none pass' in str(warning_lines[0].message)) def test_brightest(self): finder = StarFinder(10, PSF, brightest=10) tbl = finder(DATA) assert len(tbl) == 10 fluxes = tbl['flux'] assert fluxes[0] == np.max(fluxes) finder = StarFinder(40, PSF, peakmax=120) tbl = finder(DATA) assert len(tbl) == 1 def test_mask(self): starfinder = StarFinder(10, PSF) mask = np.zeros(DATA.shape, dtype=bool) mask[0:100] = True tbl1 = starfinder(DATA) tbl2 = starfinder(DATA, mask=mask) assert len(tbl1) > len(tbl2) assert min(tbl2['ycentroid']) > 100 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9986548 photutils-1.3.0/photutils/extern/0000755000214200020070000000000000000000000015536 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/extern/__init__.py0000644000214200020070000000024100000000000017644 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools that are bundled with the package but are external to it. """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9992888 photutils-1.3.0/photutils/geometry/0000755000214200020070000000000000000000000016064 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/photutils/geometry/__init__.py0000644000214200020070000000037500000000000020202 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides low-level geometry functions. """ from .circular_overlap import * # noqa from .elliptical_overlap import * # noqa from .rectangular_overlap import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/geometry/circular_overlap.pyx0000644000214200020070000002025600000000000022167 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ The functions defined here allow one to determine the exact area of overlap of a rectangle and a circle (written by Thomas Robitaille). """ import numpy as np cimport numpy as np __all__ = ['circular_overlap_grid'] cdef extern from "math.h": double asin(double x) double sin(double x) double sqrt(double x) DTYPE = np.float64 ctypedef np.float64_t DTYPE_t # NOTE: Here we need to make sure we use cimport to import the C functions from # core (since these were defined with cdef). This also requires the core.pxd # file to exist with the function signatures. from .core cimport area_arc, area_triangle, floor_sqrt def circular_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double r, int use_exact, int subpixels): """ circular_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, r, use_exact, subpixels) Area of overlap between a circle and a pixel grid. The circle is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float Extent of the grid in the x and y direction. nx, ny : int Grid dimensions. r : float The radius of the circle. use_exact : 0 or 1 If ``1`` calculates exact overlap, if ``0`` uses ``subpixel`` number of subpixels to calculate the overlap. subpixels : int Each pixel resampled by this factor in each dimension, thus each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- frac : `~numpy.ndarray` (float) 2-d array of shape (ny, nx) giving the fraction of the overlap. """ cdef unsigned int i, j cdef double x, y, dx, dy, d, pixel_radius cdef double bxmin, bxmax, bymin, bymax cdef double pxmin, pxcen, pxmax, pymin, pycen, pymax # Define output array cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) # Find the width of each element in x and y dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny # Find the radius of a single pixel pixel_radius = 0.5 * sqrt(dx * dx + dy * dy) # Define bounding box bxmin = -r - 0.5 * dx bxmax = +r + 0.5 * dx bymin = -r - 0.5 * dy bymax = +r + 0.5 * dy for i in range(nx): pxmin = xmin + i * dx # lower end of pixel pxcen = pxmin + dx * 0.5 pxmax = pxmin + dx # upper end of pixel if pxmax > bxmin and pxmin < bxmax: for j in range(ny): pymin = ymin + j * dy pycen = pymin + dy * 0.5 pymax = pymin + dy if pymax > bymin and pymin < bymax: # Distance from circle center to pixel center. d = sqrt(pxcen * pxcen + pycen * pycen) # If pixel center is "well within" circle, count full # pixel. if d < r - pixel_radius: frac[j, i] = 1. # If pixel center is "close" to circle border, find # overlap. elif d < r + pixel_radius: # Either do exact calculation or use subpixel # sampling: if use_exact: frac[j, i] = circular_overlap_single_exact( pxmin, pymin, pxmax, pymax, r) / (dx * dy) else: frac[j, i] = circular_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, r, subpixels) # Otherwise, it is fully outside circle. # No action needed. return frac # NOTE: The following two functions use cdef because they are not # intended to be called from the Python code. Using def makes them # callable from outside, but also slower. In any case, these aren't useful # to call from outside because they only operate on a single pixel. cdef double circular_overlap_single_subpixel(double x0, double y0, double x1, double y1, double r, int subpixels): """Return the fraction of overlap between a circle and a single pixel with given extent, using a sub-pixel sampling method.""" cdef unsigned int i, j cdef double x, y, dx, dy, r_squared cdef double frac = 0. # Accumulator. dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels r_squared = r ** 2 x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy if x * x + y * y < r_squared: frac += 1. return frac / (subpixels * subpixels) cdef double circular_overlap_single_exact(double xmin, double ymin, double xmax, double ymax, double r): """ Area of overlap of a rectangle and a circle """ if 0. <= xmin: if 0. <= ymin: return circular_overlap_core(xmin, ymin, xmax, ymax, r) elif 0. >= ymax: return circular_overlap_core(-ymax, xmin, -ymin, xmax, r) else: return circular_overlap_single_exact(xmin, ymin, xmax, 0., r) \ + circular_overlap_single_exact(xmin, 0., xmax, ymax, r) elif 0. >= xmax: if 0. <= ymin: return circular_overlap_core(-xmax, ymin, -xmin, ymax, r) elif 0. >= ymax: return circular_overlap_core(-xmax, -ymax, -xmin, -ymin, r) else: return circular_overlap_single_exact(xmin, ymin, xmax, 0., r) \ + circular_overlap_single_exact(xmin, 0., xmax, ymax, r) else: if 0. <= ymin: return circular_overlap_single_exact(xmin, ymin, 0., ymax, r) \ + circular_overlap_single_exact(0., ymin, xmax, ymax, r) if 0. >= ymax: return circular_overlap_single_exact(xmin, ymin, 0., ymax, r) \ + circular_overlap_single_exact(0., ymin, xmax, ymax, r) else: return circular_overlap_single_exact(xmin, ymin, 0., 0., r) \ + circular_overlap_single_exact(0., ymin, xmax, 0., r) \ + circular_overlap_single_exact(xmin, 0., 0., ymax, r) \ + circular_overlap_single_exact(0., 0., xmax, ymax, r) cdef double circular_overlap_core(double xmin, double ymin, double xmax, double ymax, double r): """ Assumes that the center of the circle is <= xmin, ymin (can always modify input to conform to this). """ cdef double area, d1, d2, x1, x2, y1, y2 if xmin * xmin + ymin * ymin > r * r: area = 0. elif xmax * xmax + ymax * ymax < r * r: area = (xmax - xmin) * (ymax - ymin) else: area = 0. d1 = floor_sqrt(xmax * xmax + ymin * ymin) d2 = floor_sqrt(xmin * xmin + ymax * ymax) if d1 < r and d2 < r: x1, y1 = floor_sqrt(r * r - ymax * ymax), ymax x2, y2 = xmax, floor_sqrt(r * r - xmax * xmax) area = ((xmax - xmin) * (ymax - ymin) - area_triangle(x1, y1, x2, y2, xmax, ymax) + area_arc(x1, y1, x2, y2, r)) elif d1 < r: x1, y1 = xmin, floor_sqrt(r * r - xmin * xmin) x2, y2 = xmax, floor_sqrt(r * r - xmax * xmax) area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, x1, ymin, xmax, ymin) + area_triangle(x1, y1, x2, ymin, x2, y2)) elif d2 < r: x1, y1 = floor_sqrt(r * r - ymin * ymin), ymin x2, y2 = floor_sqrt(r * r - ymax * ymax), ymax area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, xmin, y1, xmin, ymax) + area_triangle(x1, y1, xmin, y2, x2, y2)) else: x1, y1 = floor_sqrt(r * r - ymin * ymin), ymin x2, y2 = xmin, floor_sqrt(r * r - xmin * xmin) area = (area_arc(x1, y1, x2, y2, r) + area_triangle(x1, y1, x2, y2, xmin, ymin)) return area ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1599103037.0 photutils-1.3.0/photutils/geometry/core.pxd0000644000214200020070000000133500000000000017533 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst #cython: language_level=3 # This file is needed in order to be able to cimport functions into other Cython files cdef double distance(double x1, double y1, double x2, double y2) cdef double area_arc(double x1, double y1, double x2, double y2, double R) cdef double area_triangle(double x1, double y1, double x2, double y2, double x3, double y3) cdef double area_arc_unit(double x1, double y1, double x2, double y2) cdef int in_triangle(double x, double y, double x1, double y1, double x2, double y2, double x3, double y3) cdef double overlap_area_triangle_unit_circle(double x1, double y1, double x2, double y2, double x3, double y3) cdef double floor_sqrt(double x) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/geometry/core.pyx0000644000214200020070000002751200000000000017565 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ The functions here are the core geometry functions. """ import numpy as np cimport numpy as np cdef extern from "math.h": double asin(double x) double sin(double x) double cos(double x) double sqrt(double x) double fabs(double x) from cpython cimport bool DTYPE = np.float64 ctypedef np.float64_t DTYPE_t cimport cython ctypedef struct point: double x double y ctypedef struct intersections: point p1 point p2 cdef double floor_sqrt(double x): """ In some of the geometrical functions, we have to take the sqrt of a number and we know that the number should be >= 0. However, in some cases the value is e.g. -1e-10, but we want to treat it as zero, which is what this function does. Note that this does **not** check whether negative values are close or not to zero, so this should be used only in cases where the value is expected to be positive on paper. """ if x > 0: return sqrt(x) else: return 0 # NOTE: The following two functions use cdef because they are not intended to be # called from the Python code. Using def makes them callable from outside, but # also slower. Some functions currently return multiple values, and for those we # still use 'def' for now. cdef double distance(double x1, double y1, double x2, double y2): """ Distance between two points in two dimensions. Parameters ---------- x1, y1 : float The coordinates of the first point x2, y2 : float The coordinates of the second point Returns ------- d : float The Euclidean distance between the two points """ return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) cdef double area_arc(double x1, double y1, double x2, double y2, double r): """ Area of a circle arc with radius r between points (x1, y1) and (x2, y2). References ---------- http://mathworld.wolfram.com/CircularSegment.html """ cdef double a, theta a = distance(x1, y1, x2, y2) theta = 2. * asin(0.5 * a / r) return 0.5 * r * r * (theta - sin(theta)) cdef double area_triangle(double x1, double y1, double x2, double y2, double x3, double y3): """ Area of a triangle defined by three vertices. """ return 0.5 * abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) cdef double area_arc_unit(double x1, double y1, double x2, double y2): """ Area of a circle arc with radius R between points (x1, y1) and (x2, y2) References ---------- http://mathworld.wolfram.com/CircularSegment.html """ cdef double a, theta a = distance(x1, y1, x2, y2) theta = 2. * asin(0.5 * a) return 0.5 * (theta - sin(theta)) cdef int in_triangle(double x, double y, double x1, double y1, double x2, double y2, double x3, double y3): """ Check if a point (x,y) is inside a triangle """ cdef int c = 0 c += ((y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1) c += ((y2 > y) != (y3 > y) and x < (x3 - x2) * (y - y2) / (y3 - y2) + x2) c += ((y3 > y) != (y1 > y) and x < (x1 - x3) * (y - y3) / (y1 - y3) + x3) return c % 2 == 1 cdef intersections circle_line(double x1, double y1, double x2, double y2): """Intersection of a line defined by two points with a unit circle""" cdef double a, b, delta, dx, dy cdef double tolerance = 1.e-10 cdef intersections inter dx = x2 - x1 dy = y2 - y1 if fabs(dx) < tolerance and fabs(dy) < tolerance: inter.p1.x = 2. inter.p1.y = 2. inter.p2.x = 2. inter.p2.y = 2. elif fabs(dx) > fabs(dy): # Find the slope and intercept of the line a = dy / dx b = y1 - a * x1 # Find the determinant of the quadratic equation delta = 1. + a * a - b * b if delta > 0.: # solutions exist delta = sqrt(delta) inter.p1.x = (- a * b - delta) / (1. + a * a) inter.p1.y = a * inter.p1.x + b inter.p2.x = (- a * b + delta) / (1. + a * a) inter.p2.y = a * inter.p2.x + b else: # no solution, return values > 1 inter.p1.x = 2. inter.p1.y = 2. inter.p2.x = 2. inter.p2.y = 2. else: # Find the slope and intercept of the line a = dx / dy b = x1 - a * y1 # Find the determinant of the quadratic equation delta = 1. + a * a - b * b if delta > 0.: # solutions exist delta = sqrt(delta) inter.p1.y = (- a * b - delta) / (1. + a * a) inter.p1.x = a * inter.p1.y + b inter.p2.y = (- a * b + delta) / (1. + a * a) inter.p2.x = a * inter.p2.y + b else: # no solution, return values > 1 inter.p1.x = 2. inter.p1.y = 2. inter.p2.x = 2. inter.p2.y = 2. return inter cdef point circle_segment_single2(double x1, double y1, double x2, double y2): """ The intersection of a line with the unit circle. The intersection the closest to (x2, y2) is chosen. """ cdef double dx1, dy1, dx2, dy2 cdef intersections inter cdef point pt1, pt2, pt inter = circle_line(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 # Can be optimized, but just checking for correctness right now dx1 = fabs(pt1.x - x2) dy1 = fabs(pt1.y - y2) dx2 = fabs(pt2.x - x2) dy2 = fabs(pt2.y - y2) if dx1 > dy1: # compare based on x-axis if dx1 > dx2: pt = pt2 else: pt = pt1 else: if dy1 > dy2: pt = pt2 else: pt = pt1 return pt cdef intersections circle_segment(double x1, double y1, double x2, double y2): """ Intersection(s) of a segment with the unit circle. Discard any solution not on the segment. """ cdef intersections inter, inter_new cdef point pt1, pt2 inter = circle_line(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 if (pt1.x > x1 and pt1.x > x2) or (pt1.x < x1 and pt1.x < x2) or (pt1.y > y1 and pt1.y > y2) or (pt1.y < y1 and pt1.y < y2): pt1.x, pt1.y = 2., 2. if (pt2.x > x1 and pt2.x > x2) or (pt2.x < x1 and pt2.x < x2) or (pt2.y > y1 and pt2.y > y2) or (pt2.y < y1 and pt2.y < y2): pt2.x, pt2.y = 2., 2. if pt1.x > 1. and pt2.x < 2.: inter_new.p1 = pt1 inter_new.p2 = pt2 else: inter_new.p1 = pt2 inter_new.p2 = pt1 return inter_new cdef double overlap_area_triangle_unit_circle(double x1, double y1, double x2, double y2, double x3, double y3): """ Given a triangle defined by three points (x1, y1), (x2, y2), and (x3, y3), find the area of overlap with the unit circle. """ cdef double d1, d2, d3 cdef bool in1, in2, in3 cdef bool on1, on2, on3 cdef double area cdef double PI = np.pi cdef intersections inter cdef point pt1, pt2, pt3, pt4, pt5, pt6, pt_tmp # Find distance of all vertices to circle center d1 = x1 * x1 + y1 * y1 d2 = x2 * x2 + y2 * y2 d3 = x3 * x3 + y3 * y3 # Order vertices by distance from origin if d1 < d2: if d2 < d3: pass elif d1 < d3: x2, y2, d2, x3, y3, d3 = x3, y3, d3, x2, y2, d2 else: x1, y1, d1, x2, y2, d2, x3, y3, d3 = x3, y3, d3, x1, y1, d1, x2, y2, d2 else: if d1 < d3: x1, y1, d1, x2, y2, d2 = x2, y2, d2, x1, y1, d1 elif d2 < d3: x1, y1, d1, x2, y2, d2, x3, y3, d3 = x2, y2, d2, x3, y3, d3, x1, y1, d1 else: x1, y1, d1, x2, y2, d2, x3, y3, d3 = x3, y3, d3, x2, y2, d2, x1, y1, d1 if d1 > d2 or d2 > d3 or d1 > d3: raise Exception("ERROR: vertices did not sort correctly") # Determine number of vertices inside circle in1 = d1 < 1 in2 = d2 < 1 in3 = d3 < 1 # Determine which vertices are on the circle on1 = fabs(d1 - 1) < 1.e-10 on2 = fabs(d2 - 1) < 1.e-10 on3 = fabs(d3 - 1) < 1.e-10 if on3 or in3: # triangle is completely in circle area = area_triangle(x1, y1, x2, y2, x3, y3) elif in2 or on2: # If vertex 1 or 2 are on the edge of the circle, then we use the dot # product to vertex 3 to determine whether an intersection takes place. intersect13 = not on1 or x1 * (x3 - x1) + y1 * (y3 - y1) < 0. intersect23 = not on2 or x2 * (x3 - x2) + y2 * (y3 - y2) < 0. if intersect13 and intersect23 and not on2: pt1 = circle_segment_single2(x1, y1, x3, y3) pt2 = circle_segment_single2(x2, y2, x3, y3) area = area_triangle(x1, y1, x2, y2, pt1.x, pt1.y) \ + area_triangle(x2, y2, pt1.x, pt1.y, pt2.x, pt2.y) \ + area_arc_unit(pt1.x, pt1.y, pt2.x, pt2.y) elif intersect13: pt1 = circle_segment_single2(x1, y1, x3, y3) area = area_triangle(x1, y1, x2, y2, pt1.x, pt1.y) \ + area_arc_unit(x2, y2, pt1.x, pt1.y) elif intersect23: pt2 = circle_segment_single2(x2, y2, x3, y3) area = area_triangle(x1, y1, x2, y2, pt2.x, pt2.y) \ + area_arc_unit(x1, y1, pt2.x, pt2.y) else: area = area_arc_unit(x1, y1, x2, y2) elif on1: # The triangle is outside the circle area = 0.0 elif in1: # Check for intersections of far side with circle inter = circle_segment(x2, y2, x3, y3) pt1 = inter.p1 pt2 = inter.p2 pt3 = circle_segment_single2(x1, y1, x2, y2) pt4 = circle_segment_single2(x1, y1, x3, y3) if pt1.x > 1.: # indicates no intersection # Code taken from `sep.h`. # TODO: use `sep` and get rid of this Cython code. if (((0.-pt3.y) * (pt4.x-pt3.x) > (pt4.y-pt3.y) * (0.-pt3.x)) != ((y1-pt3.y) * (pt4.x-pt3.x) > (pt4.y-pt3.y) * (x1-pt3.x))): area = area_triangle(x1, y1, pt3.x, pt3.y, pt4.x, pt4.y) \ + (PI - area_arc_unit(pt3.x, pt3.y, pt4.x, pt4.y)) else: area = area_triangle(x1, y1, pt3.x, pt3.y, pt4.x, pt4.y) \ + area_arc_unit(pt3.x, pt3.y, pt4.x, pt4.y) else: if (pt2.x - x2)**2 + (pt2.y - y2)**2 < (pt1.x - x2)**2 + (pt1.y - y2)**2: pt1, pt2 = pt2, pt1 area = area_triangle(x1, y1, pt3.x, pt3.y, pt1.x, pt1.y) \ + area_triangle(x1, y1, pt1.x, pt1.y, pt2.x, pt2.y) \ + area_triangle(x1, y1, pt2.x, pt2.y, pt4.x, pt4.y) \ + area_arc_unit(pt1.x, pt1.y, pt3.x, pt3.y) \ + area_arc_unit(pt2.x, pt2.y, pt4.x, pt4.y) else: inter = circle_segment(x1, y1, x2, y2) pt1 = inter.p1 pt2 = inter.p2 inter = circle_segment(x2, y2, x3, y3) pt3 = inter.p1 pt4 = inter.p2 inter = circle_segment(x3, y3, x1, y1) pt5 = inter.p1 pt6 = inter.p2 if pt1.x <= 1.: xp, yp = 0.5 * (pt1.x + pt2.x), 0.5 * (pt1.y + pt2.y) area = overlap_area_triangle_unit_circle(x1, y1, x3, y3, xp, yp) \ + overlap_area_triangle_unit_circle(x2, y2, x3, y3, xp, yp) elif pt3.x <= 1.: xp, yp = 0.5 * (pt3.x + pt4.x), 0.5 * (pt3.y + pt4.y) area = overlap_area_triangle_unit_circle(x3, y3, x1, y1, xp, yp) \ + overlap_area_triangle_unit_circle(x2, y2, x1, y1, xp, yp) elif pt5.x <= 1.: xp, yp = 0.5 * (pt5.x + pt6.x), 0.5 * (pt5.y + pt6.y) area = overlap_area_triangle_unit_circle(x1, y1, x2, y2, xp, yp) \ + overlap_area_triangle_unit_circle(x3, y3, x2, y2, xp, yp) else: # no intersections if in_triangle(0., 0., x1, y1, x2, y2, x3, y3): return PI else: return 0. return area ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/geometry/elliptical_overlap.pyx0000644000214200020070000001524600000000000022510 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ The functions defined here allow one to determine the exact area of overlap of an ellipse and a triangle (written by Thomas Robitaille). The approach is to divide the rectangle into two triangles, and reproject these so that the ellipse is a unit circle, then compute the intersection of a triangle with a unit circle. """ import numpy as np cimport numpy as np __all__ = ['elliptical_overlap_grid'] cdef extern from "math.h": double asin(double x) double sin(double x) double cos(double x) double sqrt(double x) from cpython cimport bool DTYPE = np.float64 ctypedef np.float64_t DTYPE_t cimport cython # NOTE: Here we need to make sure we use cimport to import the C functions from # core (since these were defined with cdef). This also requires the core.pxd # file to exist with the function signatures. from .core cimport distance, area_triangle, overlap_area_triangle_unit_circle def elliptical_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double rx, double ry, double theta, int use_exact, int subpixels): """ elliptical_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, rx, ry, use_exact, subpixels) Area of overlap between an ellipse and a pixel grid. The ellipse is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float Extent of the grid in the x and y direction. nx, ny : int Grid dimensions. rx : float The semimajor axis of the ellipse. ry : float The semiminor axis of the ellipse. theta : float The position angle of the semimajor axis in radians (counterclockwise). use_exact : 0 or 1 If set to 1, calculates the exact overlap, while if set to 0, uses a subpixel sampling method with ``subpixel`` subpixels in each direction. subpixels : int If ``use_exact`` is 0, each pixel is resampled by this factor in each dimension. Thus, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- frac : `~numpy.ndarray` 2-d array giving the fraction of the overlap. """ cdef unsigned int i, j cdef double x, y, dx, dy cdef double bxmin, bxmax, bymin, bymax cdef double pxmin, pxmax, pymin, pymax cdef double norm # Define output array cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) # Find the width of each element in x and y dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny norm = 1. / (dx * dy) # For now we use a bounding circle and then use that to find a bounding box # but of course this is inefficient and could be done better. # Find bounding circle radius r = max(rx, ry) # Define bounding box bxmin = -r - 0.5 * dx bxmax = +r + 0.5 * dx bymin = -r - 0.5 * dy bymax = +r + 0.5 * dy for i in range(nx): pxmin = xmin + i * dx # lower end of pixel pxmax = pxmin + dx # upper end of pixel if pxmax > bxmin and pxmin < bxmax: for j in range(ny): pymin = ymin + j * dy pymax = pymin + dy if pymax > bymin and pymin < bymax: if use_exact: frac[j, i] = elliptical_overlap_single_exact( pxmin, pymin, pxmax, pymax, rx, ry, theta) * norm else: frac[j, i] = elliptical_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, rx, ry, theta, subpixels) return frac # NOTE: The following two functions use cdef because they are not # intended to be called from the Python code. Using def makes them # callable from outside, but also slower. In any case, these aren't useful # to call from outside because they only operate on a single pixel. cdef double elliptical_overlap_single_subpixel(double x0, double y0, double x1, double y1, double rx, double ry, double theta, int subpixels): """ Return the fraction of overlap between a ellipse and a single pixel with given extent, using a sub-pixel sampling method. """ cdef unsigned int i, j cdef double x, y cdef double frac = 0. # Accumulator. cdef double inv_rx_sq, inv_ry_sq cdef double cos_theta = cos(theta) cdef double sin_theta = sin(theta) cdef double dx, dy cdef double x_tr, y_tr dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels inv_rx_sq = 1. / (rx * rx) inv_ry_sq = 1. / (ry * ry) x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy # Transform into frame of rotated ellipse x_tr = y * sin_theta + x * cos_theta y_tr = y * cos_theta - x * sin_theta if x_tr * x_tr * inv_rx_sq + y_tr * y_tr * inv_ry_sq < 1.: frac += 1. return frac / (subpixels * subpixels) cdef double elliptical_overlap_single_exact(double xmin, double ymin, double xmax, double ymax, double rx, double ry, double theta): """ Given a rectangle defined by (xmin, ymin, xmax, ymax) and an ellipse with major and minor axes rx and ry respectively, position angle theta, and centered at the origin, find the area of overlap. """ cdef double cos_m_theta = cos(-theta) cdef double sin_m_theta = sin(-theta) cdef double scale # Find scale by which the areas will be shrunk scale = rx * ry # Reproject rectangle to frame of reference in which ellipse is a # unit circle x1, y1 = ((xmin * cos_m_theta - ymin * sin_m_theta) / rx, (xmin * sin_m_theta + ymin * cos_m_theta) / ry) x2, y2 = ((xmax * cos_m_theta - ymin * sin_m_theta) / rx, (xmax * sin_m_theta + ymin * cos_m_theta) / ry) x3, y3 = ((xmax * cos_m_theta - ymax * sin_m_theta) / rx, (xmax * sin_m_theta + ymax * cos_m_theta) / ry) x4, y4 = ((xmin * cos_m_theta - ymax * sin_m_theta) / rx, (xmin * sin_m_theta + ymax * cos_m_theta) / ry) # Divide resulting quadrilateral into two triangles and find # intersection with unit circle return (overlap_area_triangle_unit_circle(x1, y1, x2, y2, x3, y3) + overlap_area_triangle_unit_circle(x1, y1, x4, y4, x3, y3)) * scale ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/geometry/rectangular_overlap.pyx0000644000214200020070000000775600000000000022704 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst # cython: language_level=3 """ This module provides tools to calculate the area of overlap between a rectangle and a pixel grid. """ import numpy as np cimport numpy as np __all__ = ['rectangular_overlap_grid'] cdef extern from "math.h": double asin(double x) double sin(double x) double cos(double x) double sqrt(double x) double fabs(double x) from cpython cimport bool DTYPE = np.float64 ctypedef np.float64_t DTYPE_t cimport cython def rectangular_overlap_grid(double xmin, double xmax, double ymin, double ymax, int nx, int ny, double width, double height, double theta, int use_exact, int subpixels): """ rectangular_overlap_grid(xmin, xmax, ymin, ymax, nx, ny, width, height, use_exact, subpixels) Area of overlap between a rectangle and a pixel grid. The rectangle is centered on the origin. Parameters ---------- xmin, xmax, ymin, ymax : float Extent of the grid in the x and y direction. nx, ny : int Grid dimensions. width : float The width of the rectangle height : float The height of the rectangle theta : float The position angle of the rectangle in radians (counterclockwise). use_exact : 0 or 1 If set to 1, calculates the exact overlap, while if set to 0, uses a subpixel sampling method with ``subpixel`` subpixels in each direction. subpixels : int If ``use_exact`` is 0, each pixel is resampled by this factor in each dimension. Thus, each pixel is divided into ``subpixels ** 2`` subpixels. Returns ------- frac : `~numpy.ndarray` 2-d array giving the fraction of the overlap. """ cdef unsigned int i, j cdef double x, y, dx, dy cdef double pxmin, pxmax, pymin, pymax # Define output array cdef np.ndarray[DTYPE_t, ndim=2] frac = np.zeros([ny, nx], dtype=DTYPE) if use_exact == 1: raise NotImplementedError("Exact mode has not been implemented for " "rectangular apertures") # Find the width of each element in x and y dx = (xmax - xmin) / nx dy = (ymax - ymin) / ny # TODO: can implement a bounding box here for efficiency (as for the # circular and elliptical aperture photometry) for i in range(nx): pxmin = xmin + i * dx # lower end of pixel pxmax = pxmin + dx # upper end of pixel for j in range(ny): pymin = ymin + j * dy pymax = pymin + dy frac[j, i] = rectangular_overlap_single_subpixel( pxmin, pymin, pxmax, pymax, width, height, theta, subpixels) return frac cdef double rectangular_overlap_single_subpixel(double x0, double y0, double x1, double y1, double width, double height, double theta, int subpixels): """ Return the fraction of overlap between a rectangle and a single pixel with given extent, using a sub-pixel sampling method. """ cdef unsigned int i, j cdef double x, y cdef double frac = 0. # Accumulator. cdef double cos_theta = cos(theta) cdef double sin_theta = sin(theta) cdef double half_width, half_height half_width = width / 2. half_height = height / 2. dx = (x1 - x0) / subpixels dy = (y1 - y0) / subpixels x = x0 - 0.5 * dx for i in range(subpixels): x += dx y = y0 - 0.5 * dy for j in range(subpixels): y += dy # Transform into frame of rotated rectangle x_tr = y * sin_theta + x * cos_theta y_tr = y * cos_theta - x * sin_theta if fabs(x_tr) < half_width and fabs(y_tr) < half_height: frac += 1. return frac / (subpixels * subpixels) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1640123872.000618 photutils-1.3.0/photutils/geometry/tests/0000755000214200020070000000000000000000000017226 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/geometry/tests/__init__.py0000644000214200020070000000000000000000000021325 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/geometry/tests/test_circular_overlap_grid.py0000644000214200020070000000174200000000000025204 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the circular_overlap_grid module. """ import itertools from numpy.testing import assert_allclose import pytest from .. import circular_overlap_grid grid_sizes = [50, 500, 1000] circ_sizes = [0.2, 0.4, 0.8] use_exact = [0, 1] subsamples = [1, 5, 10] arg_list = ['grid_size', 'circ_size', 'use_exact', 'subsample'] @pytest.mark.parametrize(('grid_size', 'circ_size', 'use_exact', 'subsample'), list(itertools.product(grid_sizes, circ_sizes, use_exact, subsamples))) def test_circular_overlap_grid(grid_size, circ_size, use_exact, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = circular_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, circ_size, use_exact, subsample) assert_allclose(g.max(), 1.0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/geometry/tests/test_elliptical_overlap_grid.py0000644000214200020070000000242100000000000025515 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the elliptical_overlap_grid module. """ import itertools from numpy.testing import assert_allclose import pytest from .. import elliptical_overlap_grid grid_sizes = [50, 500, 1000] maj_sizes = [0.2, 0.4, 0.8] min_sizes = [0.2, 0.4, 0.8] angles = [0.0, 0.5, 1.0] use_exact = [0, 1] subsamples = [1, 5, 10] arg_list = ['grid_size', 'maj_size', 'min_size', 'angle', 'use_exact', 'subsample'] @pytest.mark.parametrize(('grid_size', 'maj_size', 'min_size', 'angle', 'use_exact', 'subsample'), list(itertools.product(grid_sizes, maj_sizes, min_sizes, angles, use_exact, subsamples))) def test_elliptical_overlap_grid(grid_size, maj_size, min_size, angle, use_exact, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = elliptical_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, maj_size, min_size, angle, use_exact, subsample) assert_allclose(g.max(), 1.0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/geometry/tests/test_rectangular_overlap_grid.py0000644000214200020070000000176200000000000025711 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the rectangular_overlap_grid module. """ import itertools from numpy.testing import assert_allclose import pytest from .. import rectangular_overlap_grid grid_sizes = [50, 500, 1000] rect_sizes = [0.2, 0.4, 0.8] angles = [0.0, 0.5, 1.0] subsamples = [1, 5, 10] arg_list = ['grid_size', 'rect_size', 'angle', 'subsample'] @pytest.mark.parametrize(('grid_size', 'rect_size', 'angle', 'subsample'), list(itertools.product(grid_sizes, rect_sizes, angles, subsamples))) def test_rectangular_overlap_grid(grid_size, rect_size, angle, subsample): """ Test normalization of the overlap grid to make sure that a fully enclosed pixel has a value of 1.0. """ g = rectangular_overlap_grid(-1.0, 1.0, -1.0, 1.0, grid_size, grid_size, rect_size, rect_size, angle, 0, subsample) assert_allclose(g.max(), 1.0) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0037444 photutils-1.3.0/photutils/isophote/0000755000214200020070000000000000000000000016063 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/isophote/__init__.py0000644000214200020070000000062600000000000020200 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for fitting elliptical isophotes to galaxy images. """ from .ellipse import * # noqa from .fitter import * # noqa from .geometry import * # noqa from .harmonics import * # noqa from .integrator import * # noqa from .isophote import * # noqa from .model import * # noqa from .sample import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/ellipse.py0000644000214200020070000010201500000000000020071 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a class to fit elliptical isophotes. """ import warnings from astropy.utils.exceptions import AstropyUserWarning import numpy as np from .fitter import (DEFAULT_CONVERGENCE, DEFAULT_FFLAG, DEFAULT_MAXGERR, DEFAULT_MAXIT, DEFAULT_MINIT, CentralEllipseFitter, EllipseFitter) from .geometry import EllipseGeometry from .integrator import BILINEAR from .isophote import Isophote, IsophoteList from .sample import CentralEllipseSample, EllipseSample __all__ = ['Ellipse'] class Ellipse: r""" Class to fit elliptical isophotes to a galaxy image. The isophotes in the image are measured using an iterative method described by `Jedrzejewski (1987; MNRAS 226, 747) `_. See the **Notes** section below for details about the algorithm. Parameters ---------- image : 2D `~numpy.ndarray` The image array. geometry : `~photutils.isophote.EllipseGeometry` instance or `None`, optional The optional geometry that describes the first ellipse to be fitted. If `None`, a default `~photutils.isophote.EllipseGeometry` instance is created centered on the image frame with ellipticity of 0.2 and a position angle of 90 degrees. threshold : float, optional The threshold for the object centerer algorithm. By lowering this value the object centerer becomes less strict, in the sense that it will accept lower signal-to-noise data. If set to a very large value, the centerer is effectively shut off. In this case, either the geometry information supplied by the ``geometry`` parameter is used as is, or the fit algorithm will terminate prematurely. Note that once the object centerer runs successfully, the (x, y) coordinates in the ``geometry`` attribute (an `~photutils.isophote.EllipseGeometry` instance) are modified in place. The default is 0.1 Notes ----- The image is measured using an iterative method described by `Jedrzejewski (1987; MNRAS 226, 747) `_. Each isophote is fitted at a pre-defined, fixed semimajor axis length. The algorithm starts from a first-guess elliptical isophote defined by approximate values for the (x, y) center coordinates, ellipticity, and position angle. Using these values, the image is sampled along an elliptical path, producing a 1-dimensional function that describes the dependence of intensity (pixel value) with angle (E). The function is stored as a set of 1D numpy arrays. The harmonic content of this function is analyzed by least-squares fitting to the function: .. math:: y = y0 + (A1 * \sin(E)) + (B1 * \cos(E)) + (A2 * \sin(2 * E)) + (B2 * \cos(2 * E)) Each one of the harmonic amplitudes (A1, B1, A2, and B2) is related to a specific ellipse geometric parameter in the sense that it conveys information regarding how much the parameter's current value deviates from the "true" one. To compute this deviation, the image's local radial gradient has to be taken into account too. The algorithm picks up the largest amplitude among the four, estimates the local gradient, and computes the corresponding increment in the associated ellipse parameter. That parameter is updated, and the image is resampled. This process is repeated until any one of the following criteria are met: 1. the largest harmonic amplitude is less than a given fraction of the rms residual of the intensity data around the harmonic fit. 2. a user-specified maximum number of iterations is reached. 3. more than a given fraction of the elliptical sample points have no valid data in then, either because they lie outside the image boundaries or because they were flagged out from the fit by sigma-clipping. In any case, a minimum number of iterations is always performed. If iterations stop because of reasons 2 or 3 above, then those ellipse parameters that generated the lowest absolute values for harmonic amplitudes will be used. At this point, the image data sample coming from the best fit ellipse is fitted by the following function: .. math:: y = y0 + (An * sin(n * E)) + (Bn * cos(n * E)) with :math:`n = 3` and :math:`n = 4`. The corresponding amplitudes (A3, B3, A4, and B4), divided by the semimajor axis length and local intensity gradient, measure the isophote's deviations from perfect ellipticity (these amplitudes, divided by semimajor axis and gradient, are the actual quantities stored in the output `~photutils.isophote.Isophote` instance). The algorithm then measures the integrated intensity and the number of non-flagged pixels inside the elliptical isophote, and also inside the corresponding circle with same center and radius equal to the semimajor axis length. These parameters, their errors, other associated parameters, and auxiliary information, are stored in the `~photutils.isophote.Isophote` instance. Errors in intensity and local gradient are obtained directly from the rms scatter of intensity data along the fitted ellipse. Ellipse geometry errors are obtained from the errors in the coefficients of the first and second simultaneous harmonic fit. Third and fourth harmonic amplitude errors are obtained in the same way, but only after the first and second harmonics are subtracted from the raw data. For more details, see the error analysis in `Busko (1996; ASPC 101, 139) `_. After fitting the ellipse that corresponds to a given value of the semimajor axis (by the process described above), the axis length is incremented/decremented following a pre-defined rule. At each step, the starting, first-guess, ellipse parameters are taken from the previously fitted ellipse that has the closest semimajor axis length to the current one. On low surface brightness regions (those having large radii), the small values of the image radial gradient can induce large corrections and meaningless values for the ellipse parameters. The algorithm has the ability to stop increasing semimajor axis based on several criteria, including signal-to-noise ratio. See the `~photutils.isophote.Isophote` documentation for the meaning of the stop code reported after each fit. The fit algorithm provides a k-sigma clipping algorithm for cleaning deviant sample points at each isophote, thus improving convergence stability against any non-elliptical structure such as stars, spiral arms, HII regions, defects, etc. The fit algorithm has no way of finding where, in the input image frame, the galaxy to be measured is located. The center (x, y) coordinates need to be close to the actual center for the fit to work. An "object centerer" function helps to verify that the selected position can be used as starting point. This function scans a 10x10 window centered either on the (x, y) coordinates in the `~photutils.isophote.EllipseGeometry` instance passed to the constructor of the `~photutils.isophote.Ellipse` class, or, if any one of them, or both, are set to `None`, on the input image frame center. In case a successful acquisition takes place, the `~photutils.isophote.EllipseGeometry` instance is modified in place to reflect the solution of the object centerer algorithm. In some cases the object centerer algorithm may fail, even though there is enough signal-to-noise to start a fit (e.g., in objects with very high ellipticity). In those cases the sensitivity of the algorithm can be decreased by decreasing the value of the object centerer threshold parameter. The centerer works by looking to where a quantity akin to a signal-to-noise ratio is maximized within the 10x10 window. The centerer can thus be shut off entirely by setting the threshold to a large value >> 1 (meaning, no location inside the search window will achieve that signal-to-noise ratio). A note of caution: the ellipse fitting algorithm was designed explicitly with an elliptical galaxy brightness distribution in mind. In particular, a well defined negative radial intensity gradient across the region being fitted is paramount for the achievement of stable solutions. Use of the algorithm in other types of images (e.g., planetary nebulae) may lead to inability to converge to any acceptable solution. """ def __init__(self, image, geometry=None, threshold=0.1): self.image = image if geometry is not None: self._geometry = geometry else: _x0 = image.shape[1] / 2 _y0 = image.shape[0] / 2 self._geometry = EllipseGeometry(_x0, _y0, 10., eps=0.2, pa=np.pi/2) self.set_threshold(threshold) def set_threshold(self, threshold): """ Modify the threshold value used by the centerer. Parameters ---------- threshold : float The new threshold value to use. """ self._geometry.centerer_threshold = threshold def fit_image(self, sma0=None, minsma=0., maxsma=None, step=0.1, conver=DEFAULT_CONVERGENCE, minit=DEFAULT_MINIT, maxit=DEFAULT_MAXIT, fflag=DEFAULT_FFLAG, maxgerr=DEFAULT_MAXGERR, sclip=3., nclip=0, integrmode=BILINEAR, linear=None, maxrit=None, fix_center=False, fix_pa=False, fix_eps=False): # This parameter list is quite large and should in principle be # simplified by re-distributing these controls to somewhere else. # We keep this design though because it better mimics the flat # architecture used in the original STSDAS task `ellipse`. """ Fit multiple isophotes to the image array. This method loops over each value of the semimajor axis (sma) length (constructed from the input parameters), fitting a single isophote at each sma. The entire set of isophotes is returned in an `~photutils.isophote.IsophoteList` instance. Note that the fix_XXX parameters act in unison. Meaning, if one of them is set via this call, the others will assume their default (False) values. This effectively overrides any settings that are present in the internal `~photutils.isophote.EllipseGeometry` instance that is carried along as a property of this class. If an instance of `~photutils.isophote.EllipseGeometry` was passed to this class' constructor, that instance will be effectively overridden by the fix_XXX parameters in this call. Parameters ---------- sma0 : float, optional The starting value for the semimajor axis length (pixels). This value must not be the minimum or maximum semimajor axis length, but something in between. The algorithm can't start from the very center of the galaxy image because the modelling of elliptical isophotes on that region is poor and it will diverge very easily if not tied to other previously fit isophotes. It can't start from the maximum value either because the maximum is not known beforehand, depending on signal-to-noise. The ``sma0`` value should be selected such that the corresponding isophote has a good signal-to-noise ratio and a clearly defined geometry. If set to `None` (the default), one of two actions will be taken: if a `~photutils.isophote.EllipseGeometry` instance was input to the `~photutils.isophote.Ellipse` constructor, its ``sma`` value will be used. Otherwise, a default value of 10. will be used. minsma : float, optional The minimum value for the semimajor axis length (pixels). The default is 0. maxsma : float or `None`, optional The maximum value for the semimajor axis length (pixels). When set to `None` (default), the algorithm will increase the semimajor axis until one of several conditions will cause it to stop and revert to fit ellipses with sma < ``sma0``. step : float, optional The step value used to grow/shrink the semimajor axis length (pixels if ``linear=True``, or a relative value if ``linear=False``). See the ``linear`` parameter. The default is 0.1. conver : float, optional The main convergence criterion. Iterations stop when the largest harmonic amplitude becomes smaller (in absolute value) than ``conver`` times the harmonic fit rms. The default is 0.05. minit : int, optional The minimum number of iterations to perform. A minimum of 10 (the default) iterations guarantees that, on average, 2 iterations will be available for fitting each independent parameter (the four harmonic amplitudes and the intensity level). For the first isophote, the minimum number of iterations is 2 * ``minit`` to ensure that, even departing from not-so-good initial values, the algorithm has a better chance to converge to a sensible solution. maxit : int, optional The maximum number of iterations to perform. The default is 50. fflag : float, optional The acceptable fraction of flagged data points in the sample. If the actual fraction of valid data points is smaller than this, the iterations will stop and the current `~photutils.isophote.Isophote` will be returned. Flagged data points are points that either lie outside the image frame, are masked, or were rejected by sigma-clipping. The default is 0.7. maxgerr : float, optional The maximum acceptable relative error in the local radial intensity gradient. This is the main control for preventing ellipses to grow to regions of too low signal-to-noise ratio. It specifies the maximum acceptable relative error in the local radial intensity gradient. `Busko (1996; ASPC 101, 139) `_ showed that the fitting precision relates to that relative error. The usual behavior of the gradient relative error is to increase with semimajor axis, being larger in outer, fainter regions of a galaxy image. In the current implementation, the ``maxgerr`` criterion is triggered only when two consecutive isophotes exceed the value specified by the parameter. This prevents premature stopping caused by contamination such as stars and HII regions. A number of actions may happen when the gradient error exceeds ``maxgerr`` (or becomes non-significant and is set to `None`). If the maximum semimajor axis specified by ``maxsma`` is set to `None`, semimajor axis growth is stopped and the algorithm proceeds inwards to the galaxy center. If ``maxsma`` is set to some finite value, and this value is larger than the current semimajor axis length, the algorithm enters non-iterative mode and proceeds outwards until reaching ``maxsma``. The default is 0.5. sclip : float, optional The sigma-clip sigma value. The default is 3.0. nclip : int, optional The number of sigma-clip iterations. The default is 0, which means sigma-clipping is skipped. integrmode : {'bilinear', 'nearest_neighbor', 'mean', 'median'}, optional The area integration mode. The default is 'bilinear'. linear : bool, optional The semimajor axis growing/shrinking mode. If `False` (default), the geometric growing mode is chosen, thus the semimajor axis length is increased by a factor of (1. + ``step``), and the process is repeated until either the semimajor axis value reaches the value of parameter ``maxsma``, or the last fitted ellipse has more than a given fraction of its sampled points flagged out (see ``fflag``). The process then resumes from the first fitted ellipse (at ``sma0``) inwards, in steps of (1./(1. + ``step``)), until the semimajor axis length reaches the value ``minsma``. In case of linear growing, the increment or decrement value is given directly by ``step`` in pixels. If ``maxsma`` is set to `None`, the semimajor axis will grow until a low signal-to-noise criterion is met. See ``maxgerr``. maxrit : float or `None`, optional The maximum value of semimajor axis to perform an actual fit. Whenever the current semimajor axis length is larger than ``maxrit``, the isophotes will be extracted using the current geometry, without being fitted. This non-iterative mode may be useful for sampling regions of very low surface brightness, where the algorithm may become unstable and unable to recover reliable geometry information. Non-iterative mode can also be entered automatically whenever the ellipticity exceeds 1.0 or the ellipse center crosses the image boundaries. If `None` (default), then no maximum value is used. fix_center : bool, optional Keep center of ellipse fixed during fit? The default is False. fix_pa : bool, optional Keep position angle of semi-major axis of ellipse fixed during fit? The default is False. fix_eps : bool, optional Keep ellipticity of ellipse fixed during fit? The default is False. Returns ------- result : `~photutils.isophote.IsophoteList` instance A list-like object of `~photutils.isophote.Isophote` instances, sorted by increasing semimajor axis length. """ # multiple fitted isophotes will be stored here isophote_list = [] # get starting sma from appropriate source: keyword parameter, # internal EllipseGeometry instance, or fixed default value. if not sma0: if self._geometry: sma = self._geometry.sma else: sma = 10. else: sma = sma0 # Override geometry instance with parameters set at the call. if isinstance(linear, bool): self._geometry.linear_growth = linear else: linear = self._geometry.linear_growth if fix_center and fix_pa and fix_eps: warnings.warn(': Everything is fixed. Fit not possible.', AstropyUserWarning) return IsophoteList([]) if fix_center or fix_pa or fix_eps: # Note that this overrides the geometry instance for good. self._geometry.fix = np.array([fix_center, fix_center, fix_pa, fix_eps]) # first, go from initial sma outwards until # hitting one of several stopping criteria. noiter = False first_isophote = True while True: # first isophote runs longer minit_a = 2 * minit if first_isophote else minit first_isophote = False isophote = self.fit_isophote(sma, step, conver, minit_a, maxit, fflag, maxgerr, sclip, nclip, integrmode, linear, maxrit, noniterate=noiter, isophote_list=isophote_list) # check for failed fit. if (isophote.stop_code < 0 or isophote.stop_code == 1): # in case the fit failed right at the outset, return an # empty list. This is the usual case when the user # provides initial guesses that are too way off to enable # the fitting algorithm to find any meaningful solution. if len(isophote_list) == 1: warnings.warn('No meaningful fit was possible.', AstropyUserWarning) return IsophoteList([]) self._fix_last_isophote(isophote_list, -1) # get last isophote from the actual list, since the last # `isophote` instance in this context may no longer be OK. isophote = isophote_list[-1] # if two consecutive isophotes failed to fit, # shut off iterative mode. Or, bail out and # change to go inwards. if len(isophote_list) > 2: if ((isophote.stop_code == 5 and isophote_list[-2].stop_code == 5) or isophote.stop_code == 1): if maxsma and maxsma > isophote.sma: # if a maximum sma value was provided by # user, and the current sma is smaller than # maxsma, keep growing sma in non-iterative # mode until reaching it. noiter = True else: # if no maximum sma, stop growing and change # to go inwards. break # reset variable from the actual list, since the last # `isophote` instance may no longer be OK. isophote = isophote_list[-1] # update sma. If exceeded user-defined # maximum, bail out from this loop. sma = isophote.sample.geometry.update_sma(step) if maxsma and sma >= maxsma: break # reset sma so as to go inwards. first_isophote = isophote_list[0] sma, step = first_isophote.sample.geometry.reset_sma(step) # now, go from initial sma inwards towards center. while True: isophote = self.fit_isophote(sma, step, conver, minit, maxit, fflag, maxgerr, sclip, nclip, integrmode, linear, maxrit, going_inwards=True, isophote_list=isophote_list) # if abnormal condition, fix isophote but keep going. if isophote.stop_code < 0: self._fix_last_isophote(isophote_list, 0) # but if we get an error from the scipy fitter, bail out # immediately. This usually happens at very small radii # when the number of data points is too small. if isophote.stop_code == 3: break # reset variable from the actual list, since the last # `isophote` instance may no longer be OK. isophote = isophote_list[-1] # figure out next sma; if exceeded user-defined # minimum, or too small, bail out from this loop sma = isophote.sample.geometry.update_sma(step) if sma <= max(minsma, 0.5): break # if user asked for minsma=0, extract special isophote there if minsma == 0.0: # isophote is appended to isophote_list _ = self.fit_isophote(0.0, isophote_list=isophote_list) # sort list of isophotes according to sma isophote_list.sort() return IsophoteList(isophote_list) def fit_isophote(self, sma, step=0.1, conver=DEFAULT_CONVERGENCE, minit=DEFAULT_MINIT, maxit=DEFAULT_MAXIT, fflag=DEFAULT_FFLAG, maxgerr=DEFAULT_MAXGERR, sclip=3., nclip=0, integrmode=BILINEAR, linear=False, maxrit=None, noniterate=False, going_inwards=False, isophote_list=None): """ Fit a single isophote with a given semimajor axis length. The ``step`` and ``linear`` parameters are not used to actually grow or shrink the current fitting semimajor axis length. They are necessary so the sampling algorithm can know where to start the gradient computation and also how to compute the elliptical sector areas (when area integration mode is selected). Parameters ---------- sma : float The semimajor axis length (pixels). step : float, optional The step value used to grow/shrink the semimajor axis length (pixels if ``linear=True``, or a relative value if ``linear=False``). See the ``linear`` parameter. The default is 0.1. conver : float, optional The main convergence criterion. Iterations stop when the largest harmonic amplitude becomes smaller (in absolute value) than ``conver`` times the harmonic fit rms. The default is 0.05. minit : int, optional The minimum number of iterations to perform. A minimum of 10 (the default) iterations guarantees that, on average, 2 iterations will be available for fitting each independent parameter (the four harmonic amplitudes and the intensity level). For the first isophote, the minimum number of iterations is 2 * ``minit`` to ensure that, even departing from not-so-good initial values, the algorithm has a better chance to converge to a sensible solution. maxit : int, optional The maximum number of iterations to perform. The default is 50. fflag : float, optional The acceptable fraction of flagged data points in the sample. If the actual fraction of valid data points is smaller than this, the iterations will stop and the current `~photutils.isophote.Isophote` will be returned. Flagged data points are points that either lie outside the image frame, are masked, or were rejected by sigma-clipping. The default is 0.7. maxgerr : float, optional The maximum acceptable relative error in the local radial intensity gradient. When fitting a single isophote by itself this parameter doesn't have any effect on the outcome. sclip : float, optional The sigma-clip sigma value. The default is 3.0. nclip : int, optional The number of sigma-clip iterations. The default is 0, which means sigma-clipping is skipped. integrmode : {'bilinear', 'nearest_neighbor', 'mean', 'median'}, optional The area integration mode. The default is 'bilinear'. linear : bool, optional The semimajor axis growing/shrinking mode. When fitting just one isophote, this parameter is used only by the code that define the details of how elliptical arc segments ("sectors") are extracted from the image when using area extraction modes (see the ``integrmode`` parameter). maxrit : float or `None`, optional The maximum value of semimajor axis to perform an actual fit. Whenever the current semimajor axis length is larger than ``maxrit``, the isophotes will be extracted using the current geometry, without being fitted. This non-iterative mode may be useful for sampling regions of very low surface brightness, where the algorithm may become unstable and unable to recover reliable geometry information. Non-iterative mode can also be entered automatically whenever the ellipticity exceeds 1.0 or the ellipse center crosses the image boundaries. If `None` (default), then no maximum value is used. noniterate : bool, optional Whether the fitting algorithm should be bypassed and an isophote should be extracted with the geometry taken directly from the most recent `~photutils.isophote.Isophote` instance stored in the ``isophote_list`` parameter. This parameter is mainly used when running the method in a loop over different values of semimajor axis length, and we want to change from iterative to non-iterative mode somewhere along the sequence of isophotes. When set to `True`, this parameter overrides the behavior associated with parameter ``maxrit``. The default is `False`. going_inwards : bool, optional Parameter to define the sense of SMA growth. When fitting just one isophote, this parameter is used only by the code that defines the details of how elliptical arc segments ("sectors") are extracted from the image, when using area extraction modes (see the ``integrmode`` parameter). The default is `False`. isophote_list : list or `None`, optional If not `None` (the default), the fitted `~photutils.isophote.Isophote` instance is appended to this list. It must be created and managed by the caller. Returns ------- result : `~photutils.isophote.Isophote` instance The fitted isophote. The fitted isophote is also appended to the input list input to the ``isophote_list`` parameter. """ geometry = self._geometry # if available, geometry from last fitted isophote will be # used as initial guess for next isophote. if isophote_list: geometry = isophote_list[-1].sample.geometry # do the fit if noniterate or (maxrit and sma > maxrit): isophote = self._non_iterative(sma, step, linear, geometry, sclip, nclip, integrmode) else: isophote = self._iterative(sma, step, linear, geometry, sclip, nclip, integrmode, conver, minit, maxit, fflag, maxgerr, going_inwards) # store result in list if isophote_list is not None and isophote.valid: isophote_list.append(isophote) return isophote def _iterative(self, sma, step, linear, geometry, sclip, nclip, integrmode, conver, minit, maxit, fflag, maxgerr, going_inwards=False): if sma > 0.: # iterative fitter sample = EllipseSample(self.image, sma, astep=step, sclip=sclip, nclip=nclip, linear_growth=linear, geometry=geometry, integrmode=integrmode) fitter = EllipseFitter(sample) else: # sma == 0 requires special handling sample = CentralEllipseSample(self.image, 0.0, geometry=geometry) fitter = CentralEllipseFitter(sample) isophote = fitter.fit(conver, minit, maxit, fflag, maxgerr, going_inwards) return isophote def _non_iterative(self, sma, step, linear, geometry, sclip, nclip, integrmode): sample = EllipseSample(self.image, sma, astep=step, sclip=sclip, nclip=nclip, linear_growth=linear, geometry=geometry, integrmode=integrmode) sample.update(geometry.fix) # build isophote without iterating with an EllipseFitter isophote = Isophote(sample, 0, True, stop_code=4) return isophote @staticmethod def _fix_last_isophote(isophote_list, index): if isophote_list: isophote = isophote_list.pop() # check if isophote is bad; if so, fix its geometry # to be like the geometry of the index-th isophote # in list. isophote.fix_geometry(isophote_list[index]) # force new extraction of raw data, since # geometry changed. isophote.sample.values = None isophote.sample.update(isophote.sample.geometry.fix) # we take the opportunity to change an eventual # negative stop code to its' positive equivalent. code = (5 if isophote.stop_code < 0 else isophote.stop_code) # build new instance so it can have its attributes # populated from the updated sample attributes. new_isophote = Isophote(isophote.sample, isophote.niter, isophote.valid, code) # add new isophote to list isophote_list.append(new_isophote) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/fitter.py0000644000214200020070000004105100000000000017733 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a class to fit ellipses. """ import math from astropy import log import numpy as np import numpy.ma as ma from .harmonics import (first_and_second_harmonic_function, fit_first_and_second_harmonics) from .isophote import CentralPixel, Isophote from .sample import EllipseSample __all__ = ['EllipseFitter'] __doctest_skip__ = ['EllipseFitter.fit'] PI2 = np.pi / 2 MAX_EPS = 0.95 MIN_EPS = 0.05 DEFAULT_CONVERGENCE = 0.05 DEFAULT_MINIT = 10 DEFAULT_MAXIT = 50 DEFAULT_FFLAG = 0.7 DEFAULT_MAXGERR = 0.5 class EllipseFitter: """ Class to fit ellipses. Parameters ---------- sample : `~photutils.isophote.EllipseSample` instance The sample data to be fitted. """ def __init__(self, sample): self._sample = sample def fit(self, conver=DEFAULT_CONVERGENCE, minit=DEFAULT_MINIT, maxit=DEFAULT_MAXIT, fflag=DEFAULT_FFLAG, maxgerr=DEFAULT_MAXGERR, going_inwards=False): """ Fit an elliptical isophote. Parameters ---------- conver : float, optional The main convergence criterion. Iterations stop when the largest harmonic amplitude becomes smaller (in absolute value) than ``conver`` times the harmonic fit rms. The default is 0.05. minit : int, optional The minimum number of iterations to perform. A minimum of 10 (the default) iterations guarantees that, on average, 2 iterations will be available for fitting each independent parameter (the four harmonic amplitudes and the intensity level). For the first isophote, the minimum number of iterations is 2 * ``minit`` to ensure that, even departing from not-so-good initial values, the algorithm has a better chance to converge to a sensible solution. maxit : int, optional The maximum number of iterations to perform. The default is 50. fflag : float, optional The acceptable fraction of flagged data points in the sample. If the actual fraction of valid data points is smaller than this, the iterations will stop and the current `~photutils.isophote.Isophote` will be returned. Flagged data points are points that either lie outside the image frame, are masked, or were rejected by sigma-clipping. The default is 0.7. maxgerr : float, optional The maximum acceptable relative error in the local radial intensity gradient. This is the main control for preventing ellipses to grow to regions of too low signal-to-noise ratio. It specifies the maximum acceptable relative error in the local radial intensity gradient. `Busko (1996; ASPC 101, 139) `_ showed that the fitting precision relates to that relative error. The usual behavior of the gradient relative error is to increase with semimajor axis, being larger in outer, fainter regions of a galaxy image. In the current implementation, the ``maxgerr`` criterion is triggered only when two consecutive isophotes exceed the value specified by the parameter. This prevents premature stopping caused by contamination such as stars and HII regions. A number of actions may happen when the gradient error exceeds ``maxgerr`` (or becomes non-significant and is set to `None`). If the maximum semimajor axis specified by ``maxsma`` is set to `None`, semimajor axis growth is stopped and the algorithm proceeds inwards to the galaxy center. If ``maxsma`` is set to some finite value, and this value is larger than the current semimajor axis length, the algorithm enters non-iterative mode and proceeds outwards until reaching ``maxsma``. The default is 0.5. going_inwards : bool, optional Parameter to define the sense of SMA growth. When fitting just one isophote, this parameter is used only by the code that defines the details of how elliptical arc segments ("sectors") are extracted from the image, when using area extraction modes (see the ``integrmode`` parameter in the `~photutils.isophote.EllipseSample` class). The default is `False`. Returns ------- result : `~photutils.isophote.Isophote` instance The fitted isophote, which also contains fit status information. Examples -------- >>> from photutils.isophote import EllipseSample, EllipseFitter >>> sample = EllipseSample(data, sma=10.) >>> fitter = EllipseFitter(sample) >>> isophote = fitter.fit() """ sample = self._sample # this flag signals that limiting gradient error (`maxgerr`) # wasn't exceeded yet. lexceed = False # here we keep track of the sample that caused the minimum harmonic # amplitude(in absolute value). This will eventually be used to # build the resulting Isophote in cases where iterations run to # the maximum allowed (maxit), or the maximum number of flagged # data points (fflag) is reached. minimum_amplitude_value = np.Inf minimum_amplitude_sample = None # these must be passed throughout the execution chain. fixed_parameters = self._sample.geometry.fix for i in range(maxit): # Force the sample to compute its gradient and associated values. sample.update(fixed_parameters) # The extract() method returns sampled values as a 2-d numpy array # with the following structure: # values[0] = 1-d array with angles # values[1] = 1-d array with radii # values[2] = 1-d array with intensity values = sample.extract() # We have to check for a zero-length condition here, and bail out # in case it is detected. The scipy fitter won't raise an exception # for zero-length input arrays, but just prints an "INFO" message. # This may result in an infinite loop. if len(values[2]) < 1: s = str(sample.geometry.sma) log.warning("Too small sample to warrant a fit. SMA is " + s) sample.geometry.fix = fixed_parameters return Isophote(sample, i + 1, False, 3) # Fit harmonic coefficients. Failure in fitting is # a fatal error; terminate immediately with sample # marked as invalid. try: coeffs = fit_first_and_second_harmonics(values[0], values[2]) coeffs = coeffs[0] except Exception as e: log.warning(e) sample.geometry.fix = fixed_parameters return Isophote(sample, i + 1, False, 3) # Mask out coefficients that control fixed ellipse parameters. free_coeffs = ma.masked_array(coeffs[1:], mask=fixed_parameters) # Largest non-masked harmonic in absolute value drives the # correction. largest_harmonic_index = np.argmax(np.abs(free_coeffs)) largest_harmonic = free_coeffs[largest_harmonic_index] # see if the amplitude decreased; if yes, keep the # corresponding sample for eventual later use. if abs(largest_harmonic) < minimum_amplitude_value: minimum_amplitude_value = abs(largest_harmonic) minimum_amplitude_sample = sample # check if converged model = first_and_second_harmonic_function(values[0], coeffs) residual = values[2] - model if ((conver * sample.sector_area * np.std(residual)) > np.abs(largest_harmonic)): # Got a valid solution. But before returning, ensure # that a minimum of iterations has run. if i >= minit - 1: sample.update(fixed_parameters) return Isophote(sample, i + 1, True, 0) # it may not have converged yet, but the sample contains too # many invalid data points: return. if sample.actual_points < (sample.total_points * fflag): # when too many data points were flagged, return the # best fit sample instead of the current one. minimum_amplitude_sample.update(fixed_parameters) return Isophote(minimum_amplitude_sample, i + 1, True, 1) # pick appropriate corrector code. corrector = _CORRECTORS[largest_harmonic_index] # generate *NEW* EllipseSample instance with corrected # parameter. Note that this instance is still devoid of other # information besides its geometry. It needs to be explicitly # updated for computations to proceed. We have to build a new # EllipseSample instance every time because of the lazy # extraction process used by EllipseSample code. To minimize # the number of calls to the area integrators, we pay a # (hopefully smaller) price here, by having multiple calls to # the EllipseSample constructor. sample = corrector.correct(sample, largest_harmonic) sample.update(fixed_parameters) # see if any abnormal (or unusual) conditions warrant # the change to non-iterative mode, or go-inwards mode. proceed, lexceed = self._check_conditions( sample, maxgerr, going_inwards, lexceed) if not proceed: sample.update(fixed_parameters) return Isophote(sample, i + 1, True, -1) # Got to the maximum number of iterations. Return with # code 2, and handle it as a valid isophote. Use the # best fit sample instead of the current one. minimum_amplitude_sample.update(fixed_parameters) return Isophote(minimum_amplitude_sample, maxit, True, 2) @staticmethod def _check_conditions(sample, maxgerr, going_inwards, lexceed): proceed = True # If center wandered more than allowed, put it back # in place and signal the end of iterative mode. # if wander: # if abs(dx) > WANDER(al)) or abs(dy) > WANDER(al): # sample.geometry.x0 -= dx # sample.geometry.y0 -= dy # STOP(al) = ST_NONITERATE # proceed = False # check if an acceptable gradient value could be computed. if sample.gradient_error and sample.gradient_relative_error: if not going_inwards and ( sample.gradient_relative_error > maxgerr or sample.gradient >= 0.0): if lexceed: proceed = False else: lexceed = True else: proceed = False # check if ellipse geometry diverged. if abs(sample.geometry.eps > MAX_EPS): proceed = False if (sample.geometry.x0 < 1. or sample.geometry.x0 > sample.image.shape[1] or sample.geometry.y0 < 1. or sample.geometry.y0 > sample.image.shape[0]): proceed = False # See if eps == 0 (round isophote) was crossed. # If so, fix it but still proceed if sample.geometry.eps < 0.: sample.geometry.eps = min(-sample.geometry.eps, MAX_EPS) if sample.geometry.pa < PI2: sample.geometry.pa += PI2 else: sample.geometry.pa -= PI2 # If ellipse is an exact circle, computations will diverge. # Make it slightly flat, but still proceed if sample.geometry.eps == 0.0: sample.geometry.eps = MIN_EPS return proceed, lexceed class _ParameterCorrector: def correct(self, sample, harmonic): raise NotImplementedError class _PositionCorrector(_ParameterCorrector): @staticmethod def finalize_correction(dx, dy, sample): new_x0 = sample.geometry.x0 + dx new_y0 = sample.geometry.y0 + dy return EllipseSample(sample.image, sample.geometry.sma, x0=new_x0, y0=new_y0, astep=sample.geometry.astep, sclip=sample.sclip, nclip=sample.nclip, eps=sample.geometry.eps, position_angle=sample.geometry.pa, linear_growth=sample.geometry.linear_growth, integrmode=sample.integrmode) class _PositionCorrector0(_PositionCorrector): def correct(self, sample, harmonic): aux = -harmonic * (1. - sample.geometry.eps) / sample.gradient dx = -aux * math.sin(sample.geometry.pa) dy = aux * math.cos(sample.geometry.pa) return self.finalize_correction(dx, dy, sample) class _PositionCorrector1(_PositionCorrector): def correct(self, sample, harmonic): aux = -harmonic / sample.gradient dx = aux * math.cos(sample.geometry.pa) dy = aux * math.sin(sample.geometry.pa) return self.finalize_correction(dx, dy, sample) class _AngleCorrector(_ParameterCorrector): def correct(self, sample, harmonic): eps = sample.geometry.eps sma = sample.geometry.sma gradient = sample.gradient correction = (harmonic * 2. * (1. - eps) / sma / gradient / ((1. - eps)**2 - 1.)) # '% np.pi' to make angle lie between 0 and np.pi radians new_pa = (sample.geometry.pa + correction) % np.pi return EllipseSample(sample.image, sample.geometry.sma, x0=sample.geometry.x0, y0=sample.geometry.y0, astep=sample.geometry.astep, sclip=sample.sclip, nclip=sample.nclip, eps=sample.geometry.eps, position_angle=new_pa, linear_growth=sample.geometry.linear_growth, integrmode=sample.integrmode) class _EllipticityCorrector(_ParameterCorrector): def correct(self, sample, harmonic): eps = sample.geometry.eps sma = sample.geometry.sma gradient = sample.gradient correction = harmonic * 2. * (1. - eps) / sma / gradient new_eps = min((sample.geometry.eps - correction), MAX_EPS) return EllipseSample(sample.image, sample.geometry.sma, x0=sample.geometry.x0, y0=sample.geometry.y0, astep=sample.geometry.astep, sclip=sample.sclip, nclip=sample.nclip, eps=new_eps, position_angle=sample.geometry.pa, linear_growth=sample.geometry.linear_growth, integrmode=sample.integrmode) # instances of corrector code live here: _CORRECTORS = [_PositionCorrector0(), _PositionCorrector1(), _AngleCorrector(), _EllipticityCorrector()] class CentralEllipseFitter(EllipseFitter): """ A special Fitter class to handle the case of the central pixel in the galaxy image. """ def fit(self, conver=DEFAULT_CONVERGENCE, minit=DEFAULT_MINIT, maxit=DEFAULT_MAXIT, fflag=DEFAULT_FFLAG, maxgerr=DEFAULT_MAXGERR, going_inwards=False): """ Perform just a simple 1-pixel extraction at the current (x0, y0) position using bilinear interpolation. The input parameters are ignored, but included simple to match the calling signature of the parent class. Returns ------- result : `~photutils.isophote.CentralEllipsePixel` instance The central pixel value. For convenience, the `~photutils.isophote.CentralEllipsePixel` class inherits from the `~photutils.isophote.Isophote` class, although it's not really a true isophote but just a single intensity value at the central position. Thus, most of its attributes are hardcoded to `None` or other default value when appropriate. """ # default values fixed_parameters = np.array([False, False, False, False]) self._sample.update(fixed_parameters) return CentralPixel(self._sample) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/geometry.py0000644000214200020070000004677400000000000020312 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a container class to store parameters for the geometry of an ellipse. """ import math from astropy import log import numpy as np __all__ = ['EllipseGeometry'] IN_MASK = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] OUT_MASK = [ [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1], [1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1], [1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1], [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1], [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], ] def _area(sma, eps, phi, r): """ Compute elliptical sector area. """ aux = r * math.cos(phi) / sma signal = aux / abs(aux) if abs(aux) >= 1.: aux = signal return abs(sma**2 * (1.-eps) / 2. * math.acos(aux)) class EllipseGeometry: r""" Container class to store parameters for the geometry of an ellipse. Parameters that describe the relationship of a given ellipse with other associated ellipses are also encapsulated in this container. These associated ellipses may include, e.g., the two (inner and outer) bounding ellipses that are used to build sectors along the elliptical path. These sectors are used as areas for integrating pixel values, when the area integration mode (mean or median) is used. This class also keeps track of where in the ellipse we are when performing an 'extract' operation. This is mostly relevant when using an area integration mode (as opposed to a pixel integration mode) Parameters ---------- x0, y0 : float The center pixel coordinate of the ellipse. sma : float The semimajor axis of the ellipse in pixels. eps : ellipticity The ellipticity of the ellipse. pa : float The position angle (in radians) of the semimajor axis in relation to the positive x axis of the image array (rotating towards the positive y axis). Position angles are defined in the range :math:`0 < PA <= \pi`. Avoid using as starting position angle of 0., since the fit algorithm may not work properly. When the ellipses are such that position angles are near either extreme of the range, noise can make the solution jump back and forth between successive isophotes, by amounts close to 180 degrees. astep : float, optional The step value for growing/shrinking the semimajor axis. It can be expressed either in pixels (when ``linear_growth=True``) or as a relative value (when ``linear_growth=False``). The default is 0.1. linear_growth : bool, optional The semimajor axis growing/shrinking mode. The default is `False`. fix_center : bool, optional Keep center of ellipse fixed during fit? The default is False. fix_pa : bool, optional Keep position angle of semi-major axis of ellipse fixed during fit? The default is False. fix_eps : bool, optional Keep ellipticity of ellipse fixed during fit? The default is False. """ def __init__(self, x0, y0, sma, eps, pa, astep=0.1, linear_growth=False, fix_center=False, fix_pa=False, fix_eps=False): self.x0 = x0 self.y0 = y0 self.sma = sma self.eps = eps self.pa = pa self.astep = astep self.linear_growth = linear_growth # Fixed parameters are flagged in here. Note that the # ordering must follow the same ordering used in the # fitter._CORRECTORS list. self.fix = np.array([fix_center, fix_center, fix_pa, fix_eps]) # limits for sector angular width self._phi_min = 0.05 self._phi_max = 0.2 # variables used in the calculation of the sector angular width sma1, sma2 = self.bounding_ellipses() inner_sma = min((sma2 - sma1), 3.) self._area_factor = (sma2 - sma1) * inner_sma # sma can eventually be zero! if self.sma > 0.: self.sector_angular_width = max(min((inner_sma / self.sma), self._phi_max), self._phi_min) self.initial_polar_angle = self.sector_angular_width / 2. self.initial_polar_radius = self.radius(self.initial_polar_angle) def find_center(self, image, threshold=0.1, verbose=True): """ Find the center of a galaxy. If the algorithm is successful the (x, y) coordinates in this `~photutils.isophote.EllipseGeometry` (i.e., the ``x0`` and ``y0`` attributes) instance will be modified. The isophote fit algorithm requires an initial guess for the galaxy center (x, y) coordinates and these coordinates must be close to the actual galaxy center for the isophote fit to work. This method provides can provide an initial guess for the galaxy center coordinates. See the **Notes** section below for more details. Parameters ---------- image : 2D `~numpy.ndarray` The image array. Masked arrays are not recognized here. This assumes that centering should always be done on valid pixels. threshold : float, optional The centerer threshold. To turn off the centerer, set this to a large value (i.e., >> 1). The default is 0.1. verbose : bool, optional Whether to print object centering information. The default is `True`. Notes ----- The centerer function scans a 10x10 window centered on the (x, y) coordinates in the `~photutils.isophote.EllipseGeometry` instance passed to the constructor of the `~photutils.isophote.Ellipse` class. If any of the `~photutils.isophote.EllipseGeometry` (x, y) coordinates are `None`, the center of the input image frame is used. If the center acquisition is successful, the `~photutils.isophote.EllipseGeometry` instance is modified in place to reflect the solution of the object centerer algorithm. In some cases the object centerer algorithm may fail even though there is enough signal-to-noise to start a fit (e.g., objects with very high ellipticity). In those cases the sensitivity of the algorithm can be decreased by decreasing the value of the object centerer threshold parameter. The centerer works by looking where a quantity akin to a signal-to-noise ratio is maximized within the 10x10 window. The centerer can thus be shut off entirely by setting the threshold to a large value (i.e., >> 1; meaning no location inside the search window will achieve that signal-to-noise ratio). """ self._centerer_mask_half_size = len(IN_MASK) / 2 self.centerer_threshold = threshold # number of pixels in each mask sz = len(IN_MASK) self._centerer_ones_in = np.ma.masked_array(np.ones(shape=(sz, sz)), mask=IN_MASK) self._centerer_ones_out = np.ma.masked_array(np.ones(shape=(sz, sz)), mask=OUT_MASK) self._centerer_in_mask_npix = np.sum(self._centerer_ones_in) self._centerer_out_mask_npix = np.sum(self._centerer_ones_out) # Check if center coordinates point to somewhere inside the frame. # If not, set then to frame center. shape = image.shape _x0 = self.x0 _y0 = self.y0 if (_x0 is None or _x0 < 0 or _x0 >= shape[1] or _y0 is None or _y0 < 0 or _y0 >= shape[0]): _x0 = shape[1] / 2 _y0 = shape[0] / 2 max_fom = 0. max_i = 0 max_j = 0 # scan all positions inside window window_half_size = 5 for i in range(int(_x0 - window_half_size), int(_x0 + window_half_size) + 1): for j in range(int(_y0 - window_half_size), int(_y0 + window_half_size) + 1): # ensure that it stays inside image frame i1 = int(max(0, i - self._centerer_mask_half_size)) j1 = int(max(0, j - self._centerer_mask_half_size)) i2 = int(min(shape[1] - 1, i + self._centerer_mask_half_size)) j2 = int(min(shape[0] - 1, j + self._centerer_mask_half_size)) window = image[j1:j2, i1:i2] # averages in inner and outer regions. inner = np.ma.masked_array(window, mask=IN_MASK) outer = np.ma.masked_array(window, mask=OUT_MASK) inner_avg = np.sum(inner) / self._centerer_in_mask_npix outer_avg = np.sum(outer) / self._centerer_out_mask_npix # standard deviation and figure of merit inner_std = np.std(inner) outer_std = np.std(outer) stddev = np.sqrt(inner_std**2 + outer_std**2) fom = (inner_avg - outer_avg) / stddev if fom > max_fom: max_fom = fom max_i = i max_j = j # figure of merit > threshold: update geometry with new coordinates. if max_fom > threshold: self.x0 = float(max_i) self.y0 = float(max_j) if verbose: log.info(f'Found center at x0 = {self.x0:5.1f}, ' f'y0 = {self.y0:5.1f}') else: if verbose: log.info('Result is below the threshold -- keeping the ' 'original coordinates.') def radius(self, angle): """ Calculate the polar radius for a given polar angle. Parameters ---------- angle : float The polar angle (radians). Returns ------- radius : float The polar radius (pixels). """ return (self.sma * (1. - self.eps) / np.sqrt(((1. - self.eps) * np.cos(angle))**2 + (np.sin(angle))**2)) def initialize_sector_geometry(self, phi): """ Initialize geometry attributes associated with an elliptical sector at the given polar angle ``phi``. This function computes: * the four vertices that define the elliptical sector on the pixel array. * the sector area (saved in the ``sector_area`` attribute) * the sector angular width (saved in ``sector_angular_width`` attribute) Parameters ---------- phi : float The polar angle (radians) where the sector is located. Returns ------- x, y : 1D `~numpy.ndarray` The x and y coordinates of each vertex as 1D arrays. """ # These polar radii bound the region between the inner # and outer ellipses that define the sector. sma1, sma2 = self.bounding_ellipses() eps_ = 1. - self.eps # polar vector at one side of the elliptical sector self._phi1 = phi - self.sector_angular_width / 2. r1 = (sma1 * eps_ / math.sqrt((eps_ * math.cos(self._phi1))**2 + (math.sin(self._phi1))**2)) r2 = (sma2 * eps_ / math.sqrt((eps_ * math.cos(self._phi1))**2 + (math.sin(self._phi1))**2)) # polar vector at the other side of the elliptical sector self._phi2 = phi + self.sector_angular_width / 2. r3 = (sma2 * eps_ / math.sqrt((eps_ * math.cos(self._phi2))**2 + (math.sin(self._phi2))**2)) r4 = (sma1 * eps_ / math.sqrt((eps_ * math.cos(self._phi2))**2 + (math.sin(self._phi2))**2)) # sector area sa1 = _area(sma1, self.eps, self._phi1, r1) sa2 = _area(sma2, self.eps, self._phi1, r2) sa3 = _area(sma2, self.eps, self._phi2, r3) sa4 = _area(sma1, self.eps, self._phi2, r4) self.sector_area = abs((sa3 - sa2) - (sa4 - sa1)) # angular width of sector. It is calculated such that the sectors # come out with roughly constant area along the ellipse. self.sector_angular_width = max(min((self._area_factor / (r3 - r4) / r4), self._phi_max), self._phi_min) # compute the 4 vertices that define the elliptical sector. vertex_x = np.zeros(shape=4, dtype=float) vertex_y = np.zeros(shape=4, dtype=float) # vertices are labelled in counterclockwise sequence vertex_x[0:2] = np.array([r1, r2]) * math.cos(self._phi1 + self.pa) vertex_x[2:4] = np.array([r4, r3]) * math.cos(self._phi2 + self.pa) vertex_y[0:2] = np.array([r1, r2]) * math.sin(self._phi1 + self.pa) vertex_y[2:4] = np.array([r4, r3]) * math.sin(self._phi2 + self.pa) vertex_x += self.x0 vertex_y += self.y0 return vertex_x, vertex_y def bounding_ellipses(self): """ Compute the semimajor axis of the two ellipses that bound the annulus where integrations take place. Returns ------- sma1, sma2 : float The smaller and larger values of semimajor axis length that define the annulus bounding ellipses. """ if self.linear_growth: a1 = self.sma - self.astep / 2. a2 = self.sma + self.astep / 2. else: a1 = self.sma * (1. - self.astep / 2.) a2 = self.sma * (1. + self.astep / 2.) return a1, a2 def polar_angle_sector_limits(self): """ Return the two polar angles that bound the sector. The two bounding polar angles become available only after calling the :meth:`~photutils.isophote.EllipseGeometry.initialize_sector_geometry` method. Returns ------- phi1, phi2 : float The smaller and larger values of polar angle that bound the current sector. """ return self._phi1, self._phi2 def to_polar(self, x, y): r""" Return the radius and polar angle in the ellipse coordinate system given (x, y) pixel image coordinates. This function takes care of the different definitions for position angle (PA) and polar angle (phi): .. math:: -\pi < PA < \pi 0 < phi < 2 \pi Note that radius can be anything. The solution is not tied to the semimajor axis length, but to the center position and tilt angle. Parameters ---------- x, y : float The (x, y) image coordinates. Returns ------- radius, angle : float The ellipse radius and polar angle. """ # We split in between a scalar version and a # vectorized version. This is necessary for # now so we don't pay a heavy speed penalty # that is incurred when using vectorized code. # The split in two separate functions helps in # the profiling analysis: most of the time is # spent in the scalar function. if isinstance(x, (int, float)): return self._to_polar_scalar(x, y) else: return self._to_polar_vectorized(x, y) def _to_polar_scalar(self, x, y): x1 = x - self.x0 y1 = y - self.y0 radius = x1**2 + y1**2 if radius > 0.0: radius = math.sqrt(radius) angle = math.asin(abs(y1) / radius) else: radius = 0. angle = 1. if x1 >= 0. and y1 < 0.: angle = 2*np.pi - angle elif x1 < 0. and y1 >= 0.: angle = np.pi - angle elif x1 < 0. and y1 < 0.: angle = np.pi + angle pa1 = self.pa if self.pa < 0.: pa1 = self.pa + 2*np.pi angle = angle - pa1 if angle < 0.: angle = angle + 2*np.pi return radius, angle def _to_polar_vectorized(self, x, y): x1 = np.atleast_2d(x) - self.x0 y1 = np.atleast_2d(y) - self.y0 radius = x1**2 + y1**2 angle = np.ones(radius.shape) imask = (radius > 0.0) radius[imask] = np.sqrt(radius[imask]) angle[imask] = np.arcsin(np.abs(y1[imask]) / radius[imask]) radius[~imask] = 0. angle[~imask] = 1. idx = (x1 >= 0.) & (y1 < 0) angle[idx] = 2*np.pi - angle[idx] idx = (x1 < 0.) & (y1 >= 0.) angle[idx] = np.pi - angle[idx] idx = (x1 < 0.) & (y1 < 0.) angle[idx] = np.pi + angle[idx] pa1 = self.pa if self.pa < 0.: pa1 = self.pa + 2*np.pi angle = angle - pa1 angle[angle < 0] += 2*np.pi return radius, angle def update_sma(self, step): """ Calculate an updated value for the semimajor axis, given the current value and the step value. The step value must be managed by the caller to support both modes: grow outwards and shrink inwards. Parameters ---------- step : float The step value. Returns ------- sma : float The new semimajor axis length. """ if self.linear_growth: sma = self.sma + step else: sma = self.sma * (1. + step) return sma def reset_sma(self, step): """ Change the direction of semimajor axis growth, from outwards to inwards. Parameters ---------- step : float The current step value. Returns ------- sma, new_step : float The new semimajor axis length and the new step value to initiate the shrinking of the semimajor axis length. This is the step value that should be used when calling the :meth:`~photutils.isophote.EllipseGeometry.update_sma` method. """ if self.linear_growth: sma = self.sma - step step = -step else: aux = 1. / (1. + step) sma = self.sma * aux step = aux - 1. return sma, step ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/harmonics.py0000644000214200020070000001021600000000000020420 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for computing and fitting harmonic functions. """ import numpy as np __all__ = ['first_and_second_harmonic_function', 'fit_first_and_second_harmonics', 'fit_upper_harmonic'] def _least_squares_fit(optimize_func, parameters): # call the least squares fitting # function and handle the result. from scipy.optimize import leastsq solution = leastsq(optimize_func, parameters, full_output=True) if solution[4] > 4: raise RuntimeError("Error in least squares fit: " + solution[3]) # return coefficients and covariance matrix return (solution[0], solution[1]) def first_and_second_harmonic_function(phi, c): r""" Compute the harmonic function value used to calculate the corrections for ellipse fitting. This function includes simultaneously both the first and second order harmonics: .. math:: f(phi) = c[0] + c[1]*\sin(phi) + c[2]*\cos(phi) + c[3]*\sin(2*phi) + c[4]*\cos(2*phi) Parameters ---------- phi : float or `~numpy.ndarray` The angle(s) along the elliptical path, going towards the positive y axis, starting coincident with the position angle. That is, the angles are defined from the semimajor axis that lies in the positive x quadrant. c : `~numpy.ndarray` of shape (5,) Array containing the five harmonic coefficients. Returns ------- result : float or `~numpy.ndarray` The function value(s) at the given input angle(s). """ return (c[0] + c[1]*np.sin(phi) + c[2]*np.cos(phi) + c[3]*np.sin(2*phi) + c[4]*np.cos(2*phi)) def fit_first_and_second_harmonics(phi, intensities): r""" Fit the first and second harmonic function values to a set of (angle, intensity) pairs. This function is used to compute corrections for ellipse fitting: .. math:: f(phi) = y0 + a1*\sin(phi) + b1*\cos(phi) + a2*\sin(2*phi) + b2*\cos(2*phi) Parameters ---------- phi : float or `~numpy.ndarray` The angle(s) along the elliptical path, going towards the positive y axis, starting coincident with the position angle. That is, the angles are defined from the semimajor axis that lies in the positive x quadrant. intensities : `~numpy.ndarray` The intensities measured along the elliptical path, at the angles defined by the ``phi`` parameter. Returns ------- y0, a1, b1, a2, b2 : float The fitted harmonic coefficient values. """ a1 = b1 = a2 = b2 = 1. def optimize_func(x): return first_and_second_harmonic_function( phi, np.array([x[0], x[1], x[2], x[3], x[4]])) - intensities return _least_squares_fit(optimize_func, [np.mean(intensities), a1, b1, a2, b2]) def fit_upper_harmonic(phi, intensities, order): r""" Fit upper harmonic function to a set of (angle, intensity) pairs. With ``order`` set to 3 or 4, the resulting amplitudes, divided by the semimajor axis length and local gradient, measure the deviations from perfect ellipticity. The harmonic function that is fit is: .. math:: y(phi, order) = y0 + An*\sin(order*phi) + Bn*\cos(order*phi) Parameters ---------- phi : float or `~numpy.ndarray` The angle(s) along the elliptical path, going towards the positive y axis, starting coincident with the position angle. That is, the angles are defined from the semimajor axis that lies in the positive x quadrant. intensities : `~numpy.ndarray` The intensities measured along the elliptical path, at the angles defined by the ``phi`` parameter. order : int The order of the harmonic to be fitted. Returns ------- y0, An, Bn : float The fitted harmonic values. """ an = bn = 1. def optimize_func(x): return (x[0] + x[1]*np.sin(order*phi) + x[2]*np.cos(order*phi) - intensities) return _least_squares_fit(optimize_func, [np.mean(intensities), an, bn]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/integrator.py0000644000214200020070000002663500000000000020627 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for pixel integration. """ import math import numpy.ma as ma __all__ = ['INTEGRATORS', 'NEAREST_NEIGHBOR', 'BILINEAR', 'MEAN', 'MEDIAN'] # integration modes NEAREST_NEIGHBOR = 'nearest_neighbor' BILINEAR = 'bilinear' MEAN = 'mean' MEDIAN = 'median' class _Integrator: """ Base class that supports different kinds of pixel integration methods. Parameters ---------- image : 2D `~numpy.ndarray` The image array. geometry : `~photutils.isophote.EllipseGeometry` instance Object that encapsulates geometry information about current ellipse. angles : list Output list; contains the angle values along the elliptical path. radii : list Output list; contains the radius values along the elliptical path. intensities : list Output list; contains the extracted intensity values along the elliptical path. """ def __init__(self, image, geometry, angles, radii, intensities): self._image = image self._geometry = geometry self._angles = angles self._radii = radii self._intensities = intensities # for bounds checking self._i_range = range(0, self._image.shape[1] - 1) self._j_range = range(0, self._image.shape[0] - 1) def integrate(self, radius, phi): """ The three input lists (angles, radii, intensities) are appended with one sample point taken from the image by a chosen integration method. Sub classes should implement the actual integration method. Parameters ---------- radius : float The length of the radius vector in pixels. phi : float The polar angle of radius vector. """ raise NotImplementedError def _reset(self): """ Reset the lists containing results. This method is for internal use and shouldn't be used by external callers. """ self._angles = [] self._radii = [] self._intensities = [] def _store_results(self, phi, radius, sample): self._angles.append(phi) self._radii.append(radius) self._intensities.append(sample) def get_polar_angle_step(self): """ Return the polar angle step used to walk over the elliptical path. The polar angle step is defined by the actual integrator subclass. Returns ------- result : float The polar angle step. """ raise NotImplementedError def get_sector_area(self): """ Return the area of elliptical sectors where the integration takes place. This area is defined and managed by the actual integrator subclass. Depending on the integrator, the area may be a fixed constant, or may change along the elliptical path, so it's up to the caller to use this information in a correct way. Returns ------- result : float The sector area. """ raise NotImplementedError def is_area(self): """ Return the type of the integrator. An area integrator gets it's value from operating over a (generally variable) number of pixels that define a finite area that lays around the elliptical path, at a certain point on the image defined by a polar angle and radius values. A pixel integrator, by contrast, integrates over a fixed and normally small area related to a single pixel on the image. An example is the bilinear integrator, which integrates over a small, fixed, 5-pixel area. This method checks if the integrator is of the first type or not. Returns ------- result : boolean True if this is an area integrator, False otherwise. """ raise NotImplementedError class _NearestNeighborIntegrator(_Integrator): def integrate(self, radius, phi): self._r = radius # Get image coordinates of (radius, phi) pixel i = int(radius * math.cos(phi + self._geometry.pa) + self._geometry.x0) j = int(radius * math.sin(phi + self._geometry.pa) + self._geometry.y0) # ignore data point if outside image boundaries if (i in self._i_range) and (j in self._j_range): sample = self._image[j][i] if sample is not ma.masked: self._store_results(phi, radius, sample) def get_polar_angle_step(self): return 1. / self._r def get_sector_area(self): return 1. def is_area(self): return False class _BiLinearIntegrator(_Integrator): def integrate(self, radius, phi): self._r = radius # Get image coordinates of (radius, phi) pixel x_ = radius * math.cos(phi + self._geometry.pa) + self._geometry.x0 y_ = radius * math.sin(phi + self._geometry.pa) + self._geometry.y0 i = int(x_) j = int(y_) fx = x_ - i fy = y_ - j # ignore data point if outside image boundaries if (i in self._i_range) and (j in self._j_range): # in the future, will need to handle masked pixels here qx = 1. - fx qy = 1. - fy if (self._image[j][i] is not ma.masked and self._image[j+1][i] is not ma.masked and self._image[j][i+1] is not ma.masked and self._image[j+1][i+1] is not ma.masked): sample = (self._image[j][i] * qx * qy + self._image[j + 1][i] * qx * fy + self._image[j][i + 1] * fx * qy + self._image[j + 1][i + 1] * fy * fx) self._store_results(phi, radius, sample) def get_polar_angle_step(self): return 1. / self._r def get_sector_area(self): return 2. def is_area(self): return False class _AreaIntegrator(_Integrator): def __init__(self, image, geometry, angles, radii, intensities): super().__init__(image, geometry, angles, radii, intensities) # build auxiliary bilinear integrator to be used when # sector areas contain a too small number of valid pixels. self._bilinear_integrator = INTEGRATORS[BILINEAR](image, geometry, angles, radii, intensities) def integrate(self, radius, phi): self._phi = phi # Get image coordinates of the four vertices of the elliptical sector. vertex_x, vertex_y = self._geometry.initialize_sector_geometry(phi) self._sector_area = self._geometry.sector_area # step in polar angle to be used by caller next time # when updating the current polar angle `phi` to point # to the next sector. self._phistep = self._geometry.sector_angular_width # define rectangular image area that encompasses the elliptical # sector. We have to account for rounding of pixel indices. i1 = int(min(vertex_x)) - 1 j1 = int(min(vertex_y)) - 1 i2 = int(max(vertex_x)) + 1 j2 = int(max(vertex_y)) + 1 # polar angle limits for this sector phi1, phi2 = self._geometry.polar_angle_sector_limits() # ignore data point if the elliptical sector lies # partially, ou totally, outside image boundaries if (i1 in self._i_range) and (j1 in self._j_range) and \ (i2 in self._i_range) and (j2 in self._j_range): # Scan rectangular image area, compute sample value. npix = 0 accumulator = self.initialize_accumulator() for j in range(j1, j2): for i in range(i1, i2): # Check if polar coordinates of each pixel # put it inside elliptical sector. rp, phip = self._geometry.to_polar(i, j) # check if inside angular limits if phip < phi2 and phip >= phi1: # check if radius is inside bounding ellipses sma1, sma2 = self._geometry.bounding_ellipses() aux = ((1. - self._geometry.eps) / math.sqrt(((1. - self._geometry.eps) * math.cos(phip))**2 + (math.sin(phip))**2)) r1 = sma1 * aux r2 = sma2 * aux if rp < r2 and rp >= r1: # update accumulator with pixel value pix_value = self._image[j][i] if pix_value is not ma.masked: accumulator, npix = self.accumulate( pix_value, accumulator) # If 6 or less pixels were sampled, get the bilinear # interpolated value instead. if npix in range(0, 7): # must reset integrator to remove older samples. self._bilinear_integrator._reset() self._bilinear_integrator.integrate(radius, phi) # because it was reset, current value is the only one stored # internally in the bilinear integrator instance. Move it # from the internal integrator to this instance. if len(self._bilinear_integrator._intensities) > 0: sample_value = self._bilinear_integrator._intensities[0] self._store_results(phi, radius, sample_value) elif npix > 6: sample_value = self.compute_sample_value(accumulator) self._store_results(phi, radius, sample_value) def get_polar_angle_step(self): _, phi2 = self._geometry.polar_angle_sector_limits() phistep = self._geometry.sector_angular_width / 2. + phi2 - self._phi return phistep def get_sector_area(self): return self._sector_area def is_area(self): return True def initialize_accumulator(self): raise NotImplementedError def accumulate(self, pixel_value, accumulator): raise NotImplementedError def compute_sample_value(self, accumulator): raise NotImplementedError class _MeanIntegrator(_AreaIntegrator): def initialize_accumulator(self): accumulator = 0. self._npix = 0 return accumulator def accumulate(self, pixel_value, accumulator): accumulator += pixel_value self._npix += 1 return accumulator, self._npix def compute_sample_value(self, accumulator): return accumulator / self._npix class _MedianIntegrator(_AreaIntegrator): def initialize_accumulator(self): accumulator = [] self._npix = 0 return accumulator def accumulate(self, pixel_value, accumulator): accumulator.append(pixel_value) self._npix += 1 return accumulator, self._npix def compute_sample_value(self, accumulator): accumulator.sort() return accumulator[int(self._npix/2)] # Specific integrator subclasses can be instantiated from here. INTEGRATORS = { NEAREST_NEIGHBOR: _NearestNeighborIntegrator, BILINEAR: _BiLinearIntegrator, MEAN: _MeanIntegrator, MEDIAN: _MedianIntegrator } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/isophote.py0000644000214200020070000006563700000000000020310 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides classes to store the results of isophote fits. """ from astropy.table import QTable import astropy.units as u import numpy as np from .harmonics import (first_and_second_harmonic_function, fit_first_and_second_harmonics, fit_upper_harmonic) from ..utils._misc import _get_version_info __all__ = ['Isophote', 'IsophoteList'] class Isophote: """ Container class to store the results of single isophote fit. The extracted data sample at the given isophote (sampled intensities along the elliptical path on the image) is also kept as an attribute of this class. The container concept helps in segregating information directly related to the sample, from information that more closely relates to the fitting process, such as status codes, errors for isophote parameters, and the like. Parameters ---------- sample : `~photutils.isophote.EllipseSample` instance The sample information. niter : int The number of iterations used to fit the isophote. valid : bool The status of the fitting operation. stop_code : int The fitting stop code: * 0: Normal. * 1: Fewer than the pre-specified fraction of the extracted data points are valid. * 2: Exceeded maximum number of iterations. * 3: Singular matrix in harmonic fit, results may not be valid. This also signals an insufficient number of data points to fit. * 4: Small or wrong gradient, or ellipse diverged. Subsequent ellipses at larger or smaller semimajor axis may have the same constant geometric parameters. It's also used when the user turns off the fitting algorithm via the ``maxrit`` fitting parameter (see the `~photutils.isophote.Ellipse` class). * 5: Ellipse diverged; not even the minimum number of iterations could be executed. Subsequent ellipses at larger or smaller semimajor axis may have the same constant geometric parameters. * -1: Internal use. Attributes ---------- rms : float The root-mean-square of intensity values along the elliptical path. int_err : float The error of the mean (rms / sqrt(# data points)). ellip_err : float The ellipticity error. pa_err : float The position angle error (radians). x0_err : float The error associated with the center x coordinate. y0_err : float The error associated with the center y coordinate. pix_stddev : float The estimate of pixel standard deviation (rms * sqrt(average sector integration area)). grad : float The local radial intensity gradient. grad_error : float The measurement error of the local radial intensity gradient. grad_r_error : float The relative error of local radial intensity gradient. tflux_e : float The sum of all pixels inside the ellipse. npix_e : int The total number of valid pixels inside the ellipse. tflux_c : float The sum of all pixels inside a circle with the same ``sma`` as the ellipse. npix_c : int The total number of valid pixels inside a circle with the same ``sma`` as the ellipse. sarea : float The average sector area on the isophote (pixel**2). ndata : int The number of extracted data points. nflag : int The number of discarded data points. Data points can be discarded either because they are physically outside the image frame boundaries, because they were rejected by sigma-clipping, or they are masked. a3, b3, a4, b4 : float The higher order harmonics that measure the deviations from a perfect ellipse. These values are actually the raw harmonic amplitudes divided by the local radial gradient and the semimajor axis length, so they can directly be compared with each other. a3_err, b3_err, a4_err, b4_err : float The errors associated with the ``a3``, ``b3``, ``a4``, and ``b4`` attributes. """ def __init__(self, sample, niter, valid, stop_code): self.sample = sample self.niter = niter self.valid = valid self.stop_code = stop_code self.intens = sample.mean self.rms = np.std(sample.values[2]) self.int_err = self.rms / np.sqrt(sample.actual_points) self.pix_stddev = self.rms * np.sqrt(sample.sector_area) self.grad = sample.gradient self.grad_error = sample.gradient_error self.grad_r_error = sample.gradient_relative_error self.sarea = sample.sector_area self.ndata = sample.actual_points self.nflag = sample.total_points - sample.actual_points # flux contained inside ellipse and circle (self.tflux_e, self.tflux_c, self.npix_e, self.npix_c) = self._compute_fluxes() self._compute_errors() # deviations from a perfect ellipse (self.a3, self.b3, self.a3_err, self.b3_err) = self._compute_deviations(sample, 3) (self.a4, self.b4, self.a4_err, self.b4_err) = self._compute_deviations(sample, 4) # This method is useful for sorting lists of instances. Note # that __lt__ is the python3 way of supporting sorting. def __lt__(self, other): if hasattr(other, 'sma'): return self.sma < other.sma raise ValueError('Comparison object does not have a "sma" attribute.') def __str__(self): return str(self.to_table()) @property def sma(self): """The semimajor axis length (pixels).""" return self.sample.geometry.sma @property def eps(self): """The ellipticity of the ellipse.""" return self.sample.geometry.eps @property def pa(self): """The position angle (radians) of the ellipse.""" return self.sample.geometry.pa @property def x0(self): """The center x coordinate (pixel).""" return self.sample.geometry.x0 @property def y0(self): """The center y coordinate (pixel).""" return self.sample.geometry.y0 def _compute_fluxes(self): """ Compute integrated flux inside ellipse, as well as inside a circle defined with the same semimajor axis. Pixels in a square section enclosing circle are scanned; the distance of each pixel to the isophote center is compared both with the semimajor axis length and with the length of the ellipse radius vector, and integrals are updated if the pixel distance is smaller. """ # Compute limits of square array that encloses circle. sma = self.sample.geometry.sma x0 = self.sample.geometry.x0 y0 = self.sample.geometry.y0 xsize = self.sample.image.shape[1] ysize = self.sample.image.shape[0] imin = max(0, int(x0 - sma - 0.5) - 1) jmin = max(0, int(y0 - sma - 0.5) - 1) imax = min(xsize, int(x0 + sma + 0.5) + 1) jmax = min(ysize, int(y0 + sma + 0.5) + 1) # Integrate if (jmax - jmin > 1) and (imax - imin) > 1: y, x = np.mgrid[jmin:jmax, imin:imax] radius, angle = self.sample.geometry.to_polar(x, y) radius_e = self.sample.geometry.radius(angle) midx = (radius <= sma) values = self.sample.image[y[midx], x[midx]] tflux_c = np.ma.sum(values) npix_c = np.ma.count(values) midx2 = (radius <= radius_e) values = self.sample.image[y[midx2], x[midx2]] tflux_e = np.ma.sum(values) npix_e = np.ma.count(values) else: tflux_e = 0. tflux_c = 0. npix_e = 0 npix_c = 0 return tflux_e, tflux_c, npix_e, npix_c def _compute_deviations(self, sample, n): """ Compute deviations from a perfect ellipse, based on the amplitudes and errors for harmonic "n". Note that we first subtract the first and second harmonics from the raw data. """ try: # upper (third and fourth) harmonics up_coeffs, up_inv_hessian = fit_upper_harmonic(sample.values[0], sample.values[2], n) a = up_coeffs[1] / self.sma / sample.gradient b = up_coeffs[2] / self.sma / sample.gradient def errfunc(x, phi, order, intensities): return (x[0] + x[1] * np.sin(order * phi) + x[2] * np.cos(order * phi) - intensities) up_var_residual = np.std(errfunc(up_coeffs, self.sample.values[0], n, self.sample.values[2]), ddof=len(up_coeffs))**2 up_covariance = up_inv_hessian * up_var_residual ce = np.sqrt(np.diag(up_covariance)) # this comes from the old code. Likely it was based on # empirical experience with the STSDAS task, so we leave # it here without too much thought. gre = self.grad_r_error if self.grad_r_error is not None else 0.64 a_err = abs(a) * np.sqrt((ce[1] / up_coeffs[1])**2 + gre**2) b_err = abs(b) * np.sqrt((ce[2] / up_coeffs[2])**2 + gre**2) except Exception: # we want to catch everything a = b = a_err = b_err = None return a, b, a_err, b_err def _compute_errors(self): """ Compute parameter errors based on the diagonal of the covariance matrix of the four harmonic coefficients for harmonics n=1 and n=2. """ try: coeffs, covariance = fit_first_and_second_harmonics( self.sample.values[0], self.sample.values[2]) model = first_and_second_harmonic_function(self.sample.values[0], coeffs) var_residual = np.std(self.sample.values[2] - model, ddof=len(coeffs)) ** 2 errors = np.sqrt(np.diagonal(covariance * var_residual)) eps = self.sample.geometry.eps pa = self.sample.geometry.pa # parameter errors result from direct projection of # coefficient errors. These showed to be the error estimators # that best convey the errors measured in Monte Carlo # experiments (see Busko 1996; ASPC 101, 139). ea = abs(errors[2] / self.grad) eb = abs(errors[1] * (1. - eps) / self.grad) self.x0_err = np.sqrt((ea * np.cos(pa))**2 + (eb * np.sin(pa))**2) self.y0_err = np.sqrt((ea * np.sin(pa))**2 + (eb * np.cos(pa))**2) self.ellip_err = (abs(2. * errors[4] * (1. - eps) / self.sma / self.grad)) if abs(eps) > np.finfo(float).resolution: self.pa_err = (abs(2. * errors[3] * (1. - eps) / self.sma / self.grad / (1. - (1. - eps)**2))) else: self.pa_err = 0. except Exception: # we want to catch everything self.x0_err = self.y0_err = self.pa_err = self.ellip_err = 0. def fix_geometry(self, isophote): """ Fix the geometry of a problematic isophote to be identical to the input isophote. This method should be called when the fitting goes berserk and delivers an isophote with bad geometry, such as ellipticity > 1 or another meaningless situation. This is not a problem in itself when fitting any given isophote, but will create an error when the affected isophote is used as starting guess for the next fit. Parameters ---------- isophote : `~photutils.isophote.Isophote` instance The isophote from which to take the geometry information. """ self.sample.geometry.eps = isophote.sample.geometry.eps self.sample.geometry.pa = isophote.sample.geometry.pa self.sample.geometry.x0 = isophote.sample.geometry.x0 self.sample.geometry.y0 = isophote.sample.geometry.y0 def sampled_coordinates(self): """ Return the (x, y) coordinates where the image was sampled in order to get the intensities associated with this isophote. Returns ------- x, y : 1D `~numpy.ndarray` The x and y coordinates as 1D arrays. """ return self.sample.coordinates() def to_table(self): """ Return the main isophote parameters as an astropy `~astropy.table.QTable`. Returns ------- result : `~astropy.table.QTable` An astropy `~astropy.table.QTable` containing the main isophote parameters. """ return _isophote_list_to_table([self]) class CentralPixel(Isophote): """ Specialized Isophote class for the galaxy central pixel. This class holds only a single intensity value at the central position. Thus, most of its attributes are hardcoded to `None` or a default value when appropriate. Parameters ---------- sample : `~photutils.utils.EllipseSample` instance The sample information. """ def __init__(self, sample): self.sample = sample self.niter = 0 self.valid = True self.stop_code = 0 self.intens = sample.mean # some values are set to zero to ease certain tasks # such as model building and plotting magnitude errors self.rms = None self.int_err = 0.0 self.pix_stddev = None self.grad = 0.0 self.grad_error = None self.grad_r_error = None self.sarea = None self.ndata = sample.actual_points self.nflag = sample.total_points - sample.actual_points self.tflux_e = self.tflux_c = self.npix_e = self.npix_c = None self.a3 = self.b3 = 0.0 self.a4 = self.b4 = 0.0 self.a3_err = self.b3_err = 0.0 self.a4_err = self.b4_err = 0.0 self.ellip_err = 0. self.pa_err = 0. self.x0_err = 0. self.y0_err = 0. @property def eps(self): return 0. @property def pa(self): return 0. @property def x0(self): return self.sample.geometry.x0 class IsophoteList: """ Container class that provides the same attributes as the `~photutils.isophote.Isophote` class, but for a list of isophotes. The attributes of this class are arrays representing the values of the attributes for the entire list of `~photutils.isophote.Isophote` instances. See the `~photutils.isophote.Isophote` class for a description of the attributes. The class extends the `list` functionality, thus provides basic list behavior such as slicing, appending, and support for '+' and '+=' operators. Parameters ---------- iso_list : list of `~photutils.isophote.Isophote` A list of `~photutils.isophote.Isophote` instances. """ def __init__(self, iso_list): self._list = iso_list def __len__(self): return len(self._list) def __delitem__(self, index): self._list.__delitem__(index) def __setitem__(self, index, value): self._list.__setitem__(index, value) def __getitem__(self, index): if isinstance(index, slice): return IsophoteList(self._list[index]) return self._list.__getitem__(index) def __iter__(self): return self._list.__iter__() def sort(self): self._list.sort() def insert(self, index, value): self._list.insert(index, value) def append(self, value): self.insert(len(self) + 1, value) def extend(self, value): self._list.extend(value._list) def __iadd__(self, value): self.extend(value) return self def __add__(self, value): temp = self._list[:] # shallow copy temp.extend(value._list) return IsophoteList(temp) def get_closest(self, sma): """ Return the `~photutils.isophote.Isophote` instance that has the closest semimajor axis length to the input semimajor axis. Parameters ---------- sma : float The semimajor axis length. Returns ------- isophote : `~photutils.isophote.Isophote` instance The isophote with the closest semimajor axis value. """ index = (np.abs(self.sma - sma)).argmin() return self._list[index] def _collect_as_array(self, attr_name): return np.array(self._collect_as_list(attr_name), dtype=float) def _collect_as_list(self, attr_name): return [getattr(iso, attr_name) for iso in self._list] @property def sample(self): """ The isophote `~photutils.isophote.EllipseSample` information. """ return self._collect_as_list('sample') @property def sma(self): """The semimajor axis length (pixels).""" return self._collect_as_array('sma') @property def intens(self): """The mean intensity value along the elliptical path.""" return self._collect_as_array('intens') @property def int_err(self): """The error of the mean intensity (rms / sqrt(# data points)).""" return self._collect_as_array('int_err') @property def eps(self): """The ellipticity of the ellipse.""" return self._collect_as_array('eps') @property def ellip_err(self): """The ellipticity error.""" return self._collect_as_array('ellip_err') @property def pa(self): """The position angle (radians) of the ellipse.""" return self._collect_as_array('pa') @property def pa_err(self): """The position angle error (radians).""" return self._collect_as_array('pa_err') @property def x0(self): """The center x coordinate (pixel).""" return self._collect_as_array('x0') @property def x0_err(self): """The error associated with the center x coordinate.""" return self._collect_as_array('x0_err') @property def y0(self): """The center y coordinate (pixel).""" return self._collect_as_array('y0') @property def y0_err(self): """The error associated with the center y coordinate.""" return self._collect_as_array('y0_err') @property def rms(self): """ The root-mean-square of intensity values along the elliptical path. """ return self._collect_as_array('rms') @property def pix_stddev(self): """ The estimate of pixel standard deviation (rms * sqrt(average sector integration area)). """ return self._collect_as_array('pix_stddev') @property def grad(self): """The local radial intensity gradient.""" return self._collect_as_array('grad') @property def grad_error(self): """ The measurement error of the local radial intensity gradient. """ return self._collect_as_array('grad_error') @property def grad_r_error(self): """ The relative error of local radial intensity gradient. """ return self._collect_as_array('grad_r_error') @property def sarea(self): """The average sector area on the isophote (pixel**2).""" return self._collect_as_array('sarea') @property def ndata(self): """The number of extracted data points.""" return self._collect_as_array('ndata') @property def nflag(self): """ The number of discarded data points. Data points can be discarded either because they are physically outside the image frame boundaries, because they were rejected by sigma-clipping, or they are masked. """ return self._collect_as_array('nflag') @property def niter(self): """The number of iterations used to fit the isophote.""" return self._collect_as_array('niter') @property def valid(self): """The status of the fitting operation.""" return self._collect_as_array('valid') @property def stop_code(self): """The fitting stop code.""" return self._collect_as_array('stop_code') @property def tflux_e(self): """The sum of all pixels inside the ellipse.""" return self._collect_as_array('tflux_e') @property def tflux_c(self): """ The sum of all pixels inside a circle with the same ``sma`` as the ellipse. """ return self._collect_as_array('tflux_c') @property def npix_e(self): """The total number of valid pixels inside the ellipse.""" return self._collect_as_array('npix_e') @property def npix_c(self): """ The total number of valid pixels inside a circle with the same ``sma`` as the ellipse. """ return self._collect_as_array('npix_c') @property def a3(self): """ A third-order harmonic coefficient. See the :func:`~photutils.isophote.fit_upper_harmonic` function for details. """ return self._collect_as_array('a3') @property def b3(self): """ A third-order harmonic coefficient. See the :func:`~photutils.isophote.fit_upper_harmonic` function for details. """ return self._collect_as_array('b3') @property def a4(self): """ A fourth-order harmonic coefficient. See the :func:`~photutils.isophote.fit_upper_harmonic` function for details. """ return self._collect_as_array('a4') @property def b4(self): """ A fourth-order harmonic coefficient. See the :func:`~photutils.isophote.fit_upper_harmonic` function for details. """ return self._collect_as_array('b4') @property def a3_err(self): """ The error associated with `~photutils.isophote.IsophoteList.a3`. """ return self._collect_as_array('a3_err') @property def b3_err(self): """ The error associated with `~photutils.isophote.IsophoteList.b3`. """ return self._collect_as_array('b3_err') @property def a4_err(self): """ The error associated with `~photutils.isophote.IsophoteList.a4`. """ return self._collect_as_array('a4_err') @property def b4_err(self): """ The error associated with `~photutils.isophote.IsophoteList.b3`. """ return self._collect_as_array('b4_err') def to_table(self, columns='main'): """ Convert an `~photutils.isophote.IsophoteList` instance to a `~astropy.table.QTable` with the main isophote parameters. Returns ------- result : `~astropy.table.QTable` An astropy QTable with the main isophote parameters. """ return _isophote_list_to_table(self, columns) def get_names(self): """ Print the names of the properties of an `~photutils.isophote.IsophoteList` instance. """ list_names = list(_get_properties(self).keys()) return list_names def _get_properties(isophote_list): """ Return the properties of an `~photutils.isophote.IsophoteList` instance. Parameters ---------- isophote_list : `~photutils.isophote.IsophoteList` instance A list of isophotes. Returns ------- result : `dict` An OrderedDict with the list of the isophote_list properties. """ properties = dict() for an_item in isophote_list.__class__.__dict__: p_type = isophote_list.__class__.__dict__[an_item] # Exclude the sample property if type(p_type) == property and 'sample' not in an_item: properties[str(an_item)] = str(an_item) return properties def _isophote_list_to_table(isophote_list, columns='main'): """ Convert an `~photutils.isophote.IsophoteList` instance to a `~astropy.table.QTable`. Parameters ---------- isophote_list : list of `~photutils.isophote.Isophote` or \ `~photutils.isophote.IsophoteList` instance A list of isophotes. columns : list of str A list of properties to export from the isophote_list. If ``columns`` is 'all' or 'main', it will pick all or few of the main properties. Returns ------- result : `~astropy.table.QTable` An astropy QTable with the selected or all isophote parameters. """ properties = {} meta = {'version': _get_version_info()} isotable = QTable(meta=meta) # main_properties: `List` # A list of main parameters matching the original names of # the isophote_list parameters def __rename_properties(properties, orig_names=['int_err', 'eps', 'ellip_err', 'grad_r_error', 'nflag'], new_names=['intens_err', 'ellipticity', 'ellipticity_err', 'grad_rerror', 'nflag']): """ Simple renaming for some of the isophote_list parameters. Parameters ---------- properties : `dict` A dictionary with the list of the isophote_list parameters. orig_names : list A list of original names in the isophote_list parameters to be renamed. new_names : list A list of new names matching in length of the orig_names. Returns ------- properties: `dict` A dictionary with the list of the renamed isophote_list parameters. """ main_properties = ['sma', 'intens', 'int_err', 'eps', 'ellip_err', 'pa', 'pa_err', 'grad', 'grad_error', 'grad_r_error', 'x0', 'x0_err', 'y0', 'y0_err', 'ndata', 'nflag', 'niter', 'stop_code'] for an_item in main_properties: if an_item in orig_names: properties[an_item] = new_names[orig_names.index(an_item)] else: properties[an_item] = an_item return properties if columns == 'all': properties = _get_properties(isophote_list) properties = __rename_properties(properties) elif columns == 'main': properties = __rename_properties(properties) else: for an_item in columns: properties[an_item] = an_item for k, v in properties.items(): isotable[v] = np.array([getattr(iso, k) for iso in isophote_list]) if k in ('pa', 'pa_err'): isotable[v] = isotable[v] * 180. / np.pi * u.deg return isotable ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/model.py0000644000214200020070000001406200000000000017540 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module profiles tools for building a model elliptical galaxy image from a list of isophotes. """ import numpy as np from .geometry import EllipseGeometry __all__ = ['build_ellipse_model'] def build_ellipse_model(shape, isolist, fill=0., high_harmonics=False): """ Build a model elliptical galaxy image from a list of isophotes. For each ellipse in the input isophote list the algorithm fills the output image array with the corresponding isophotal intensity. Pixels in the output array are in general only partially covered by the isophote "pixel". The algorithm takes care of this partial pixel coverage by keeping track of how much intensity was added to each pixel by storing the partial area information in an auxiliary array. The information in this array is then used to normalize the pixel intensities. Parameters ---------- shape : 2-tuple The (ny, nx) shape of the array used to generate the input ``isolist``. isolist : `~photutils.isophote.IsophoteList` instance The isophote list created by the `~photutils.isophote.Ellipse` class. fill : float, optional The constant value to fill empty pixels. If an output pixel has no contribution from any isophote, it will be assigned this value. The default is 0. high_harmonics : bool, optional Whether to add the higher-order harmonics (i.e., ``a3``, ``b3``, ``a4``, and ``b4``; see `~photutils.isophote.Isophote` for details) to the result. Returns ------- result : 2D `~numpy.ndarray` The image with the model galaxy. """ from scipy.interpolate import LSQUnivariateSpline # the target grid is spaced in 0.1 pixel intervals so as # to ensure no gaps will result on the output array. finely_spaced_sma = np.arange(isolist[0].sma, isolist[-1].sma, 0.1) # interpolate ellipse parameters # End points must be discarded, but how many? # This seems to work so far nodes = isolist.sma[2:-2] intens_array = LSQUnivariateSpline( isolist.sma, isolist.intens, nodes)(finely_spaced_sma) eps_array = LSQUnivariateSpline( isolist.sma, isolist.eps, nodes)(finely_spaced_sma) pa_array = LSQUnivariateSpline( isolist.sma, isolist.pa, nodes)(finely_spaced_sma) x0_array = LSQUnivariateSpline( isolist.sma, isolist.x0, nodes)(finely_spaced_sma) y0_array = LSQUnivariateSpline( isolist.sma, isolist.y0, nodes)(finely_spaced_sma) grad_array = LSQUnivariateSpline( isolist.sma, isolist.grad, nodes)(finely_spaced_sma) a3_array = LSQUnivariateSpline( isolist.sma, isolist.a3, nodes)(finely_spaced_sma) b3_array = LSQUnivariateSpline( isolist.sma, isolist.b3, nodes)(finely_spaced_sma) a4_array = LSQUnivariateSpline( isolist.sma, isolist.a4, nodes)(finely_spaced_sma) b4_array = LSQUnivariateSpline( isolist.sma, isolist.b4, nodes)(finely_spaced_sma) # Return deviations from ellipticity to their original amplitude meaning a3_array = -a3_array * grad_array * finely_spaced_sma b3_array = -b3_array * grad_array * finely_spaced_sma a4_array = -a4_array * grad_array * finely_spaced_sma b4_array = -b4_array * grad_array * finely_spaced_sma # correct deviations cased by fluctuations in spline solution eps_array[np.where(eps_array < 0.)] = 0. result = np.zeros(shape=shape) weight = np.zeros(shape=shape) eps_array[np.where(eps_array < 0.)] = 0.05 # for each interpolated isophote, generate intensity values on the # output image array # for index in range(len(finely_spaced_sma)): for index in range(1, len(finely_spaced_sma)): sma0 = finely_spaced_sma[index] eps = eps_array[index] pa = pa_array[index] x0 = x0_array[index] y0 = y0_array[index] geometry = EllipseGeometry(x0, y0, sma0, eps, pa) intens = intens_array[index] # scan angles. Need to go a bit beyond full circle to ensure # full coverage. r = sma0 phi = 0. while phi <= 2*np.pi + geometry._phi_min: # we might want to add the third and fourth harmonics # to the basic isophotal intensity. harm = 0. if high_harmonics: harm = (a3_array[index] * np.sin(3.*phi) + b3_array[index] * np.cos(3.*phi) + a4_array[index] * np.sin(4.*phi) + b4_array[index] * np.cos(4.*phi)) / 4. # get image coordinates of (r, phi) pixel x = r * np.cos(phi + pa) + x0 y = r * np.sin(phi + pa) + y0 i = int(x) j = int(y) if (i > 0 and i < shape[1] - 1 and j > 0 and j < shape[0] - 1): # get fractional deviations relative to target array fx = x - float(i) fy = y - float(j) # add up the isophote contribution to the overlapping pixels result[j, i] += (intens + harm) * (1. - fy) * (1. - fx) result[j, i + 1] += (intens + harm) * (1. - fy) * fx result[j + 1, i] += (intens + harm) * fy * (1. - fx) result[j + 1, i + 1] += (intens + harm) * fy * fx # add up the fractional area contribution to the # overlapping pixels weight[j, i] += (1. - fy) * (1. - fx) weight[j, i + 1] += (1. - fy) * fx weight[j + 1, i] += fy * (1. - fx) weight[j + 1, i + 1] += fy * fx # step towards next pixel on ellipse phi = max((phi + 0.75 / r), geometry._phi_min) r = max(geometry.radius(phi), 0.5) # if outside image boundaries, ignore. else: break # zero weight values must be set to 1. weight[np.where(weight <= 0.)] = 1. # normalize result /= weight # fill value result[np.where(result == 0.)] = fill return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/sample.py0000644000214200020070000003726000000000000017726 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides a class to sample data along an elliptical path. """ import copy import numpy as np from .geometry import EllipseGeometry from .integrator import INTEGRATORS __all__ = ['EllipseSample'] class EllipseSample: """ Class to sample image data along an elliptical path. The image intensities along the elliptical path can be extracted using a selection of integration algorithms. The ``geometry`` attribute describes the geometry of the elliptical path. Parameters ---------- image : 2D `~numpy.ndarray` The input image. sma : float The semimajor axis length in pixels. x0, y0 : float, optional The (x, y) coordinate of the ellipse center. astep : float, optional The step value for growing/shrinking the semimajor axis. It can be expressed either in pixels (when ``linear_growth=True``) or as a relative value (when ``linear_growth=False``). The default is 0.1. eps : float, optional The ellipticity of the ellipse. The default is 0.2. pa : float, optional The position angle of ellipse in relation to the positive x axis of the image array (rotating towards the positive y axis). The default is 0. sclip : float, optional The sigma-clip sigma value. The default is 3.0. nclip : int, optional The number of sigma-clip iterations. Set to zero to skip sigma-clipping. The default is 0. linear_growth : bool, optional The semimajor axis growing/shrinking mode. The default is `False`. integrmode : {'bilinear', 'nearest_neighbor', 'mean', 'median'}, optional The area integration mode. The default is 'bilinear'. geometry : `~photutils.isophote.EllipseGeometry` instance or `None` The geometry that describes the ellipse. This can be used in lieu of the explicit specification of parameters ``sma``, ``x0``, ``y0``, ``eps``, etc. In any case, the `~photutils.isophote.EllipseGeometry` instance becomes an attribute of the `~photutils.isophote.EllipseSample` object. The default is `None`. Attributes ---------- values : 2D `~numpy.ndarray` The sampled values as a 2D array, where the rows contain the angles, radii, and extracted intensity values, respectively. mean : float The mean intensity along the elliptical path. geometry : `~photutils.isophote.EllipseGeometry` instance The geometry of the elliptical path. gradient : float The local radial intensity gradient. gradient_error : float The error associated with the local radial intensity gradient. gradient_relative_error : float The relative error associated with the local radial intensity gradient. sector_area : float The average area of the sectors along the elliptical path from which the sample values were integrated. total_points : int The total number of sample values that would cover the entire elliptical path. actual_points : int The actual number of sample values that were taken from the image. It can be smaller than ``total_points`` when the ellipse encompasses regions outside the image, or when sigma-clipping removed some of the points. """ def __init__(self, image, sma, x0=None, y0=None, astep=0.1, eps=0.2, position_angle=0., sclip=3., nclip=0, linear_growth=False, integrmode='bilinear', geometry=None): self.image = image self.integrmode = integrmode if geometry: # when the geometry is inherited from somewhere else, # its sma attribute must be replaced by the value # explicitly passed to the constructor. self.geometry = copy.deepcopy(geometry) self.geometry.sma = sma else: # if no center was specified, assume it's roughly # coincident with the image center _x0 = x0 _y0 = y0 if not _x0 or not _y0: _x0 = image.shape[1] / 2 _y0 = image.shape[0] / 2 self.geometry = EllipseGeometry(_x0, _y0, sma, eps, position_angle, astep, linear_growth) # sigma-clip parameters self.sclip = sclip self.nclip = nclip # extracted values associated with this sample. self.values = None self.mean = None self.gradient = None self.gradient_error = None self.gradient_relative_error = None self.sector_area = None # total_points reports the total number of pairs angle-radius that # were attempted. actual_points reports the actual number of sampled # pairs angle-radius that resulted in valid values. self.total_points = 0 self.actual_points = 0 def extract(self): """ Extract sample data by scanning an elliptical path over the image array. Returns ------- result : 2D `~numpy.ndarray` The rows of the array contain the angles, radii, and extracted intensity values, respectively. """ # the sample values themselves are kept cached to prevent # multiple calls to the integrator code. if self.values is not None: return self.values else: s = self._extract() self.values = s return s def _extract(self, phi_min=0.05): # Here the actual sampling takes place. This is called only once # during the life of an EllipseSample instance, because it's an # expensive calculation. This method should not be called from # external code. # If one wants to force it to re-run, then do: # # sample.values = None # # before calling sample.extract() # individual extracted sample points will be stored in here angles = [] radii = [] intensities = [] sector_areas = [] # reset counters self.total_points = 0 self.actual_points = 0 # build integrator integrator = INTEGRATORS[self.integrmode](self.image, self.geometry, angles, radii, intensities) # initialize walk along elliptical path radius = self.geometry.initial_polar_radius phi = self.geometry.initial_polar_angle # In case of an area integrator, ask the integrator to deliver a # hint of how much area the sectors will have. In case of too # small areas, tests showed that the area integrators (mean, # median) won't perform properly. In that case, we override the # caller's selection and use the bilinear integrator regardless. if integrator.is_area(): integrator.integrate(radius, phi) area = integrator.get_sector_area() # this integration that just took place messes up with the # storage arrays and the constructors. We have to build a new # integrator instance from scratch, even if it is the same # kind as originally selected by the caller. angles = [] radii = [] intensities = [] if area < 1.0: integrator = INTEGRATORS['bilinear']( self.image, self.geometry, angles, radii, intensities) else: integrator = INTEGRATORS[self.integrmode](self.image, self.geometry, angles, radii, intensities) # walk along elliptical path, integrating at specified # places defined by polar vector. Need to go a bit beyond # full circle to ensure full coverage. while phi <= np.pi*2. + phi_min: # do the integration at phi-radius position, and append # results to the angles, radii, and intensities lists. integrator.integrate(radius, phi) # store sector area locally sector_areas.append(integrator.get_sector_area()) # update total number of points self.total_points += 1 # update angle and radius to be used to define # next polar vector along the elliptical path phistep_ = integrator.get_polar_angle_step() phi += min(phistep_, 0.5) radius = self.geometry.radius(phi) # average sector area is calculated after the integrator had # the opportunity to step over the entire elliptical path. self.sector_area = np.mean(np.array(sector_areas)) # apply sigma-clipping. angles, radii, intensities = self._sigma_clip(angles, radii, intensities) # actual number of sampled points, after sigma-clip removed outliers. self.actual_points = len(angles) # pack results in 2-d array result = np.array([np.array(angles), np.array(radii), np.array(intensities)]) return result def _sigma_clip(self, angles, radii, intensities): if self.nclip > 0: for i in range(self.nclip): # do not use list.copy()! must be python2-compliant. angles, radii, intensities = self._iter_sigma_clip( angles[:], radii[:], intensities[:]) return np.array(angles), np.array(radii), np.array(intensities) def _iter_sigma_clip(self, angles, radii, intensities): # Can't use scipy or astropy tools because they use masked arrays. # Also, they operate on a single array, and we need to operate on # three arrays simultaneously. We need something that physically # removes the clipped points from the arrays, since that is what # the remaining of the `ellipse` code expects. r_angles = [] r_radii = [] r_intensities = [] values = np.array(intensities) mean = np.mean(values) sig = np.std(values) lower = mean - self.sclip * sig upper = mean + self.sclip * sig count = 0 for k in range(len(intensities)): if intensities[k] >= lower and intensities[k] < upper: r_angles.append(angles[k]) r_radii.append(radii[k]) r_intensities.append(intensities[k]) count += 1 return r_angles, r_radii, r_intensities def update(self, fixed_parameters=None): """ Update this `~photutils.isophote.EllipseSample` instance. This method calls the :meth:`~photutils.isophote.EllipseSample.extract` method to get the values that match the current ``geometry`` attribute, and then computes the the mean intensity, local gradient, and other associated quantities. """ if fixed_parameters is None: fixed_parameters = np.array([False, False, False, False]) self.geometry.fix = fixed_parameters step = self.geometry.astep # Update the mean value first, using extraction from main sample. s = self.extract() self.mean = np.mean(s[2]) # Get sample with same geometry but at a different distance from # center. Estimate gradient from there. gradient, gradient_error = self._get_gradient(step) # Check for meaningful gradient. If no meaningful gradient, try # another sample, this time using larger radius. Meaningful # gradient means something shallower, but still close to within # a factor 3 from previous gradient estimate. If no previous # estimate is available, guess it by adding the error to the # current gradient. previous_gradient = self.gradient if not previous_gradient: previous_gradient = gradient + gradient_error # solution adopted before 08/12/2019 # previous_gradient = -0.05 # good enough, based on usage if gradient >= (previous_gradient / 3.): # gradient is negative! gradient, gradient_error = self._get_gradient(2 * step) # If still no meaningful gradient can be measured, try with # previous one, slightly shallower. A factor 0.8 is not too far # from what is expected from geometrical sampling steps of 10-20% # and a deVaucouleurs law or an exponential disk (at least at its # inner parts, r <~ 5 req). Gradient error is meaningless in this # case. if gradient >= (previous_gradient / 3.): gradient = previous_gradient * 0.8 gradient_error = None self.gradient = gradient self.gradient_error = gradient_error if gradient_error and gradient < 0.: self.gradient_relative_error = gradient_error / np.abs(gradient) else: self.gradient_relative_error = None def _get_gradient(self, step): gradient_sma = (1. + step) * self.geometry.sma gradient_sample = EllipseSample( self.image, gradient_sma, x0=self.geometry.x0, y0=self.geometry.y0, astep=self.geometry.astep, sclip=self.sclip, nclip=self.nclip, eps=self.geometry.eps, position_angle=self.geometry.pa, linear_growth=self.geometry.linear_growth, integrmode=self.integrmode) sg = gradient_sample.extract() mean_g = np.mean(sg[2]) gradient = (mean_g - self.mean) / self.geometry.sma / step s = self.extract() sigma = np.std(s[2]) sigma_g = np.std(sg[2]) gradient_error = (np.sqrt(sigma**2 / len(s[2]) + sigma_g**2 / len(sg[2])) / self.geometry.sma / step) return gradient, gradient_error def coordinates(self): """ Return the (x, y) coordinates associated with each sampled point. Returns ------- x, y : 1D `~numpy.ndarray` The x and y coordinate arrays. """ angles = self.values[0] radii = self.values[1] x = np.zeros(len(angles)) y = np.zeros(len(angles)) for i in range(len(x)): x[i] = (radii[i] * np.cos(angles[i] + self.geometry.pa) + self.geometry.x0) y[i] = (radii[i] * np.sin(angles[i] + self.geometry.pa) + self.geometry.y0) return x, y class CentralEllipseSample(EllipseSample): """ An `~photutils.isophote.EllipseSample` subclass designed to handle the special case of the central pixel in the galaxy image. """ def update(self, fixed_parameters): """ Update this `~photutils.isophote.EllipseSample` instance with the intensity integrated at the (x0, y0) center position using bilinear integration. The local gradient is set to `None`. 'fixed_parameters' is ignored in this subclass. """ s = self.extract() self.mean = s[2][0] self.gradient = None self.gradient_error = None self.gradient_relative_error = None def _extract(self): angles = [] radii = [] intensities = [] integrator = INTEGRATORS['bilinear'](self.image, self.geometry, angles, radii, intensities) integrator.integrate(0.0, 0.0) self.total_points = 1 self.actual_points = 1 return np.array([np.array(angles), np.array(radii), np.array(intensities)]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0069144 photutils-1.3.0/photutils/isophote/tests/0000755000214200020070000000000000000000000017225 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/isophote/tests/__init__.py0000644000214200020070000000000000000000000021324 0ustar00lbradley././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0098968 photutils-1.3.0/photutils/isophote/tests/data/0000755000214200020070000000000000000000000020136 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588730504.0 photutils-1.3.0/photutils/isophote/tests/data/M51_table.fits0000644000214200020070000006250000000000000022541 0ustar00lbradleySIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H ORIGIN = 'STScI-STSDAS/TABLES' / Tables version 2002-02-22 FILENAME= 'M51_table.fits' / name of file NEXTEND = 1 / number of extensions in file END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 52 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'dev$pix ' END Eñ°ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€ñ)ÿÿÿÿC~§ÿÿÿÿÄ϶ÿÿÿÿÿÿÿÿÁ‰Bÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__EÖ¥nA·÷ŒBêƒGB¥ÓZ=J‹=³ ´BSäB“°ñC€ñ)<ÁQ&C~§<Á[ÄÏ‚ÇD ÑŒ>Æe§?Y~`Áy•;n¡h;mÑ¡Eñ°Eñ°Á‰BÁ‰B=¨i=8 –¼ÔÛº=›‰=H?ç<Ìh¬½‡À„<õ¸! @)kM@?µµEÔ A»oBø<\B¯‡z=#¡b=­ kB@^-BÅLC€ð=<Íð¤C}À<ͽ)Ä͆YDÆ7>ÀÉ?^¼üÁDE;¬¾;~ºjEñ°Eñ°Á‰BÁ‰B=—”™=.v;¼Î'9=¡&;úsx<‰Ñ½ª;F= H @}“Ä@?!azEÑ‘ÀAäRžC‡BÍέ=J[=¹kÆB[w¹B_ |C€íT<ôš~C~U<ó ’ÄÊLTD#¼6>Ï3>?dùÁ";—¾;—Eñ°Eñ°Á‰BÁ‰B=´í%=G+Û¼Ïok=d=†›==Ô ½f*<<ã×€ A:ª@?1„ÓEÎ} AâãÞCYBÌ„=eàa=¨iB=¥B2©5C€í<ôÄC}<ô;uÄÈ‚DŽ¢>¾!?iœÁÍE;™ {;˜]LEñ°Eñ°Á‰BÁ‰B=••]=+¶Ž¼Éyù=MŒ:Ö<‡†ñ½©Â—= ©ü @£2@?CEOEËSÌAñxiCècBÙ¨=„Á•=£Ì)BHÉBC€êu=ÕËC}=ÿˆÄŽD>¹¾Ë?o>6Áˆ©;¥m;¤©úEñ°Eñ°Á‰BÁ‰B= pD=(&C¼Ï(ä<õ×<ÈÊ7<—Û½2<ôÔ6 @¹*þ@?VÌ>EÇçB\DC# ÈBçg¯=•Û*=–¤SB86éA÷PíC€é=­¶C}=vÄÍm,DÈZ>¦·ã?uÁ=*;²ì ;²Eñ°Eñ°Á‰BÁ‰B=‚l¤=±7¼ÎYc<ñÏ«¼ …‰µ?zë’ÁÜÜ;­¤;¬.¢Eñ°Eñ°Á‰BÁ‰B<ÇM;<ìˆ÷¼»ßÄ<ã&½aM<|“ô½“ã<¶ ? 2@ëì@?óãE¿uAëÖCQBÔ”d=©HÆ=<þ¡AáZuAŠðC€ç<ȺC~<ÎìÄõÓ^CÁä’>Iêô?€|CÁua;«éƒ;«˜Eñ°GÈÁ‰BÁ4¹E¼Ag<¡Ôž½çÞ<¥Œ…½Cr-Bz?ƒ•kÁï±;•zŒ;”ÔPGÈGÈÁ4¹EÁ4¹E¼F”³%E²6oA¯Œ4BßǾBž<=¾&M<Õ<–Aç)÷A _/C€é<‰ƒ‰C}´<Ži´Å¹%Cs")=ê{&?†Á²Á>Y;‰,Œ;ˆž@GÈGÈÁ4¹EÁ4¹E¼I‘Ÿ£yÖ@?¬÷E©û9A™ò­BÄ>ôBŠÄN=·'‘<©ÃóAð@åûBC€êÐÙ"@?ÑJEE—žA£m¶BÐTÔB“P=¼]<‘_Aó*@À‰C€í#GRÿGRÿÁ=LòÁ=Lò ¼\=;îs“<(Á;äN‰<o~;Ô »º¿ø;ÉÇë ? 2è@?æ8E‹ƒA”¼|B½š/B†®=®u\}A >LB[lB'«=Âc°<dA®s @+J[C€îÇ<(*Ct›<,]Ä¥þøB–Ìæ=h¶?Ÿ7[Á <ü;R$£;QŽG°‡G½.ÁF>GÁGrºRv|;-WGå(H¦ÁJƬÁMQñ%¼ Û;_Ã<Í"8;sYÀ;‰ú ;QC@:$;Q ‹ ?ׯ@@`PæE ~f@ï „BA¦ØBîµ=ʶw<2XAñî@UuÉC€ðÉ<¢a?Ch<¨c¥Ä ÜAóøG=`žœ?¯#KÁ2;l×|;l÷«@@v¿dE¾ @èOyBDóeB Cå=Ч݄@@‡¶DíJ@Ù¢¾B@]WB·=ÚøE<[4øB«6@q$›C€é×<òQgCjå<ùT6æF·AÁÜj=•<?·¯§ÁÅ;’•;~¶‡H9@H*”@ÁP)pÁQ°d19¼¸c¹;Ÿú»aõC~Z=?û5Ã`î;A°¢=ÈiK?À¦ÓÁ…¡;ŸMt;ž’àHEH^@ÁT<+ÁVEÊIY¼æ_<ë¼F»¡;îL¼ÆÚ;½’¼7Ý;­dÕ =éý@@´¡²D½*µ@þ¤ÙBƒ=B9š!=¿‹€<¦€BðK@ÒÓ)C€Ø’=v—pCZ=yæÃ<ØjA§‡7=ã/?ÅLÀþe;»Ñ;º˜6Hd €HiÔ@ÁV»­ÁW*þ]a½<€¼e±6<Kr¼þêî;þzç9þ`;â-)" >¸èŸ@@ƱÞD¯•=A=(Bì¯BH¶,=¤S<³íOB ÕÈA×5C€Ý±=‘ÃXC¥>=”’Ã#%ƒA›C=ó ­?ÊÀûÎH;ÎÃÈ;Í“@Hz·H…l`ÁXaÁYvdmy½ <4¸*»1µ´<%^¼úŸ<gGÙ@@ÚuD£ÙAµIB™œwBY=1=–88<Øb{Aõ³×A*2jC€æ(=¼€ŽC¼=ÂeùÃUIAíz> Yä?ÎíSÀù=÷;èß;æ‰çHŽ” H”º ÁZ™Á[YZ‡‘¼ê‰ƒ<\Cä<3”ïedÂÉIkA‚¯S>&5?ÓêµÀ÷í;þ‡5;ü·aHŸß@H¨ÀÁ\š¦Á]z]£±¼©<†Q<¦iT<†ãø;{€<}¼<Êô<„oã- >Tè¡@A;=DÛÆA|B©[ BoG={Ð÷=-ÝAVÕJA¢Y·CÈ>4ŽaCø¸>=aý† Au<ÿ>jh?ÙåÀôáÍ<]µ<hVH· HÀÅ@Á^õ†Á_ÚÖËݼ‰îq<µÛw9mY}<´(Q="Éè<»š,¼2Î<®mM2 >çV@At]D‹ØßA6ïBÀÿýBˆx¹=ƒz=KÎÁÙBA¶¯ñCl÷>kNßC‚1«>rÞÇÂ`@ A]Ų>}+Ø?ÞB ÀóæE<æÂ<²sHʺ€HÖš Á`ºÔÁa·ýï¼Ýx<ÒÌ5=A ©<᪇½|Ïo<äGûÀ(M<À9À7 >xóÙ@A Dˆ0TA"BºRšBƒÀ=ÏêÌ=÷4Á¾’úA3=ÃC£œ>Eð•C‚\ÿ>OÛzÂn±€A9ÀÜ>G8·?ãž©Àòú< e5< Q&HáYÀHô Áb‘KÁcóÁ=¼©_e<—ë‹={—<¹%î½(xÞ<›½¯»Ãžµ<ŒsK<>%Ì>@A0D‚¨@ûüâB².«B{üâ>r˜<é/Á‰\@ÑW?CÄß>$|‘C‚kw>4OЮAïn> |¯?éÈÀñ‰¥<†'<‚áHùw@I ðÁdUiÁeöÕGy½;G P¥<Ï6fÁ­[8@¡ÒÕCÐ/>$&C‚jH>3‚.ÂkxADý> ]ÿ?îºÉÀï—á<®ä<³æI 80Ik Áf>Áh`yѽ/žL<\Õg={C P¥=$ h ëA CÐ/>“ZõC‚jH>˜5¾”A©G>u«U?ô|}ÀíRz< Ž<ÿIlàI0épÁh|ÅÁjg™Ó)<\’¨<¯êl=°½U<ò\æ½ARþ<‘ŽK¾Øæ= !M2ACÂÎ@AjAŠDcßûAcäB˲B‹k>râÍ=&ÃÛÂ"R)@³ÌÂC«{>®™qC‚?m>´SÁë«B@ÿ„ð>ŠÈ ?úaºÀìÇÐ<K]<ë I)`IJ*àÁiŸ Ál¹ ¡<+\;<ËÊ=¨ã =T$½G6<Ý»‘¾ "Ù=$²ÕP2AÜ@A€×?D_îAÔ/BØÓØB™Qù>“òf=†Â"R)@‹MÆCÏÕ>·19C‚ K>¾œ1Áã0°@ôçG>‰úÏ@5®Àì,Ž<&ä¶<%VÈI8ZÀIeG0ÁkÜÁnè®I%½Ìt<úÖ}=<.1<à}R¼Ø}™<ª´O¾B)=" ‹T2@ßy‚@A¹’D]¶ÆA³>Bß_þBó0>· <šÔ&¨Ê?ïåÇCö8>W@7C‚D¥>e"†Â9ŽG@Ö²¦>O@M"ÀëÔ<)¬<(äICÁIƒ«ÐÁl)°ÁqP²}Õ½a9 ‘d@A›åºD<û'@ó(ðBájOBŸd†>6L <”7{Â[·@KáÎCzã>M—C‚?m>BâEÂLyÈ@µié=ã u@w«ÀæG<<3¸T<1ìúI„\(I–8ÁqgçÁsšÇ×±½!ð·<+©à=É<*ŸÁ¼b¹°<ìð¼ªð<zÔn @O @A«|³D*ñ @÷ÛBí¤uB¨ ß>Z³¤<¼;•ÂY7Ê@\þCB>“‚C‚%>Šû»Âd¯@­ž…>Ï@ µÀÀâÊéô<{X2¼|É Óè<š·ÂKà@YºC€ÀŸ>‹ñýCŽH>„î×Â1rØ@×ó*>Å·@ ÙÀáõó<üô<|\2I›Ç˜I¼Á Áti>a^ÊC;ø>X¡ŒÂ?@ŽûÄ=ì7@nrÀÙ¿ü<]–‚Ÿ>š)ZC‚eV>¡•ÙÁê¿@Œ1ñ>àú@ê ÀÚ^?hûê@{ ÀÌÒ<ˆ½v<†«ŠIúv`Ju<Á||=Á}Ü K ¼v;M<ûB=H7<¨mļKÄ.^w³@";ÀÈ…Ö<®´J<«V\J 7¤JKÈÁ~ºÁ~´[ ™¡=`vÚ<ÏvÞ=²øÈ<ñ.¼„=<·ä½w< ÆÐ2A J @BæbCÍÑ@¦‡ÓBÞ¾ŽB =ŠY=2Ú”ÂsèíA˜}ºCV¤?^ÀœCðß?Wþ¿Àö{?a=é¼n@ßãÀÄ7¬<¤¸ï<¡º~Jé J›PÁ¹ÏÁ€!‡¼£-ä<ºÎê=Œ×ì<Å´¢½ŠÜ<Àáк‡û<·:Èå2Bã@B'C‚0@¥n–BèB¤"k=ŠY= 5QÂsèíAo.VC€?@÷C„<›?:PBÁ 3o@‰>ovÉ@"´£ÀÁ?{<²_}<®Þ*J'<øJ,u(Á€ÁOÁ¥ûa½u7}<µ¢–½“ó5<ÆE½?ý×<™½þ=ðQX=¸Xü2B%@B7̇CwÝu@z.ëB² #B|Z>-åX<ɽfB¡r8@éFC|Éa?*;+C‚û?\fÁ¶P?‰7Ô>^õ@&¡ À¿‰ˆ-åX<·o±Øg@ƒã#C|Éa?*tóC‚û?äzÀ‡wž?nÎ> 2@*¥¨À¸òh<1’©JM¹ÜÁÝkÁ‚ßû]<ô¦ÿ`HµÏuC‚’¥>¯%0À•¯>Ü%u=¼AT@.ÃÀ¶§™<„Z;ÿ3ÅJIž¬J`‹Á‚a ÁƒP}%ù;d]#;óöb<¤4 ;ûb[¼‰`ä;둦=! %<S3 ?poñ@Bt¢ìC?Ù?fò¥AÆöVAŒ°C~'f?[ÌC‚’¥?XÓÊ¿±‹ÿÿÿÿÿÿÿÿ@2ùçÀ®Ôø;Ò;ÐÑJr¶Jtm¼Áƒý}Á„ +-=-ñ;ßO'FæFähFßžzFÞ‘FÛ«FØuŽFÕÆFÒA4FÏšæFˆDFÉ\¼FÅiF¾’¨F¼¦rFºâcF·WF²˜IF®M¬F©jBF¨ :F£FŸÑÓF›ô£F–‘hF“ÃjF3¬FŽ‚dFˆäµF‡&ÅFƒ‘µF€W‹F "ÆF&ÿF*ŸâF+±F1H¸F34ºF6±þF:íbF>7{FA rFFæöFJ³ÆFO,²FT3FVzÎF]ITF`±¤FhyFlÐXFsàFy(ÐFFº_F„3tF‡ÖêF‰QÂF `FºBFœ™ÎF¢wÜF¤°F©€F­/™F°0æF²|äF¶æ’F¸¶DF¿ŸZFÃ8FÅð¡FÊ ’FÌœ¡FÏÔ¤FÕÒrF×YàFÛH-FÜ´UFáÜÜFäAgFå«…FæÛEFéX®FíKcFï?'Fï2Fñ÷fFñ…YFòFíAŠFíQFèHSFç2:Fåo×Fã’LFà,FÚú%FÖ+áFÔ™üFÑ$âFÐXÔFÉìFůFÃX‹FÀ¢9Fº°ªF·9F±€#F¯ÁIFª1®F§ éF¡ø²Fžï»Fšè F—·-F“\F{½FŠ}­F‡‚šF„A8F‚wF$áyF'îbF*ÅF-ÃõF1D™F3•ŸF:Ž¡F=¨FB®FEÌÏFGÔ[FOnÁFRLøFU‰F[ùŒFb$Fc¶FkÒÃFnÜ®Fv­F}ÂÂFF„;F…•WF‰£‚F](F‘Í÷F’²UF–¶lFšmžFÁEF¢iF¥3F©‘+F®ƒiF±úŒF´1áF¹cèF¼çäFÀD%FÄ^=FǦÎFË¿FϽFÒžRFÖaFÛ Fß¿¥FàšñFåQ$FçJ8Fì Fì$FñœŽFñF¶Fô@òFö¯TFùNFû5aFý!ÏFú wFù`ÖFýœÿFü™¢Fú¨šFùÁ)FúEÝFú 4F÷[Fó:¦Fò˜«Fð«FìÚâFêÀÛFæø,FãÝíFá5÷FÛ²F×CFÔøFÎP,FËÕDFÆIôFÂHnF¾¿eF¹ÈìF¶˜DF²ÂF®F©|0F¦³‹F¡_FžÁ™F˜I†F–<_F’NFŽˆ’F‰òF‡ôIF„@F$À\F(+¬F,k«F2"JF4O’F6*£F?jpF=ï|FE³FGÀcFL#yFRú|FUà¢FZ+EFaŒWFdm¥Fné¾Fnø^Fqþ¢F|ÄñF€õèFƒûèF‡KÁFŠyÀFŒå#F‘uF’íœF˜oòFšÃFž¥­F ­£F¦n´FªspF­<F²¹ôFµ‰‚F»zQF¾¸4FÃ\ýFǨNFÊB[FÐyfFÓ~‰FÔ-CFÙ»#FßlFä HFæþFêÞìFî®FFñðÑFòaÔFôúF÷œdFû(óFýe‹Fþ†qGÝ8GÃGºrGSGpGÌ_Gw]GÿÕG…CG©(G4ÐFþ\yFûgšF÷»|F÷×FòÌÅFïó¸FîàƒFéªÅFäUPFâq FÛÂJFÚ’vFÓáFÑî‹FÏ$úFÇ[ÖFÄë>F½Œ°F¼@IF´ˆ F´OˆF­õ•F¨òêF¤‡%F¡´ßFCÁ›FHø—FKešFTÈ;FS1AF[Ž#FbZŽFc}~Fj0ÑFrPôFvüF|¼F›pFƒ¬?FˆAFŒ€cFŽSAF“P¯F–$uFš•øFœªèF¡‹õF¦ÖAFª}/F®wiF´ pF¸}þF½HFÀiFÇl¡FËq£FÐ7FÖG¶FÚqFÜþFàªFè FëƒÄFñãJFõãÔFøé FþõGHuGÅG¤ñGÜKG {G ~-G 8'G Z©GØG’ÖGxŒG¯G[zGruGÀG`›GrGpBGüGG1"G ?%G ¶G dGHGŠºGçúGÜFýÖFùÓFôç|Fï¥ÌFêôFçõîFàs@FÛ™F׿FÑóFÎOFÉ‚FÃÌÒF¾¢F¹ü…F²ÖF¯y=F©6F¥ž¶F¢ÔŽF³F˜ÐÓF•­FúFô FëZFæ©7Fß-¬FÙÚ€FÓÆ·FÍkbFÅ*ÚF€NF¼Ÿ^F·fýF°¶.F­³ F¦mF¡³¾Fv¢F˜ÅôF”i1F4ÑSF8¨æF@yFDZFF3FLOFQ,RFWh\F[êuF`¡‚FeòFmŒcFr®]Fu¸F€yF‚ÁÕF…xRF‰¨µFŽƒFŒ;F–>’F™oŸFž:F£„ÔF§ ~F©ý„F°c=F¶h¡FºZ_FÀ@|FÄÍVFʵôFÑá#FÖgŽFÝQFæC$FêTêFð(Fö†EFü ¹GªGu÷GõG GÛZG÷ýGé½GGªG?¦GÙPG!‰«G$‰´G&¼íG)H=G*xžG,kžG.îŒG/JG0ýG1jG1HG2LõG0|ÈG/ý2G/9~G-ÞYG,ŒTG*RéG&¢æG#ÞÀG"BGÜPGŒ:G0ÏGGÑGdG{dG é˜G £.GH'Gz©Fÿ¶²Föƒ—FðÂFê(ˆFäµ"FÞ FÖ­EFÒ…¾FË¿ FÄp F¾ê€F¸ƒ÷Fµ¡eF¯‹ÇF©’¬F¤‹íF 1qF›ÀgF– F7ÇqF;öFC¼OFD©FM¨rFP7°FTà‘FX Fa mFb¥oFi‡&Fp+Fx.F}jFƒ F„Ý2F‰…BFŒ„$FŽ6ÈF• ¿F™ÑXF]­F [zF¥£eF«ÆF°-F²ÕÛF¹NQF¿;IFÄá¾F̈FÒ>=FÖt˜FÝ”FãÎÂFêè|FïWGuª±GtƒûG@Ú‡G@AG?I/G>e4G<õÄG;öÞG:ìDG8/ŒG4;ÕG/ä1G-µâG*«'G'‚G$­$G ‘Gù GìGX¾GÁEG îG`ÌG-YGÉÇFûN°FôFFëm5FçDaFà¥OFÚ?›FÑ|ÙFÍžîFÆPaFÀAF»OÒF´F®"YF©r´F¥”ûFŸ%Fš8Gw„ƒG{XÖGu¡ÄGwQGMÐÈGH±ˆGF¥GEù`GC#°GAÜ}G?ÏG;GvG:G5ý~G1ÄQG-¥G)N·ôG@ÍFGEÎhGI PGu¸GxuðGo»Gnk[Gt™Gx{”Gxz!GS:ãGS‘GOûøGO-ßGKÐmGKbúGGÔGCnG@¦ÌGªóGC1qGFu£GJ¾FGO ¿GB;†GI‹GO˜uGS–JGY2QG]fòGmûGvlùGvGƒGr;´Gsr4GxzÉGx|6GxzZGx{…Gx{ˆGx|/GnâHGn&Ghê½Gcü´Ga½,G] ÊGW>˜GSU˜GJšGH7GB=!G<„G6¿§G1G,)G%GŽG ÆGN¨GFäG2³G ‰GèÍGÆ5Fý !FôƒšFëcóFäQáFÜëFÔjÍFÍÝDFÆJ¦FÁÓÌF¹b¬F´EžF­&F¨÷xF¥FùFE/üFH°°FN8®FT—ÓFY5‘F`ÿöFeÚ·FkìDFrîÃF{ÐuF€º¬F„b0F‡ìkFŒÒFú5F”û!Fš>€FœØýF¡/1F§8JF¬ëF³ÐŸF¸ZZF¾1^FÄëèFÌrFÑU‡FÚA¹Fà{¥FéGFï4ZFù%GW%G²µG þ¤GbcGËGÖ…GøˆG"g°G(G,õ–G4<ÝG8Ì­GB pGF)ÀGLT&GR/7GXÃfG\¸¸Gc£ËGiðßG{âGtQÒGwr†Gq÷9GyÅGx|aGx{šGx{GxzÙGxzGwÌGt·šGpÜ«GqO-GpF‹GiìdGe>GaޱGY}ÔGUC®GNåÝGH´ÐGAïFG;G6G/ÔùG)“$G$2pG¶ÒGÓˆG±8G à§G š•Gz„FÿíæFöp©F﵊FåÔëFá0fFØBFÑDŽFÉ FÁ×SF»ð3FµÈF°‘ÚF«qîF¤=ŽFGFKŒïFRÔÖFVqÌF]Õ/F`)IFlvOFnÓ¯Fx¹½F}‡F‚iF†m4F‹º²FŒï'F“¡pF—5F›,­F 3F¦º\F«ËvF°•¦F´•7F»€FÀÒTFÈbËFÍÙAFÔ¨„FÞ‹FåããFìŒ"FôÍFþÌGWyGºÇG å‘G¾=GbÀG+cG#HóG'ûØG.®G4<¶G;UGAEÙGGÆúGNë«GUrÊGXÿsGbê¥Ghx8Gn mGtl,Gr› GvuGv„=Gn3Gw¢GkrãGx}ØGx{´Gx{=GvqGr`^GuðÂGrS}Gu<ªGu‹Gs•™GpÂGið1Gd! G]GWöGPX/GHñGB/qG;Æ G46ôG.ì¯G(¿wG!yøGA€G¸®GQæG õíG~;G›ßFþœµFôÉFé2KFàñéFÙSÄFÓ*FËFÆFVF¿ÁF·[F²WF¬rÿF§ ðFI+FO*ëFP§rFW0ØF^|¦Ff­ŸFjêPFpwúFwa”Fž®FƒÂBF‰ ¼F‹©FU¢F”ŠF™]uFþF¡×¯F¦’¼F­ÊmF´íF¹—SF¾œwFÅ£žFËÅFÑëÉFÚ××Fâ|«Fê¯FòôAFúÌïGëG)ÄG ËöG8G˜êGØÏG!ÊnG(èÕG.ˆOG4¹*G:æNGA;GIÊrGN̲GVîzG^ÃÑGg/æGlÛGrõŽGxr:GrGtaBGpèwGs¤ëGo}öGsQGyAMGz;Gv#—GnìÛGwFGwFGv‚[GprSGnâGy,Gq¥GtG;Gq‘-GmJÈGdÀ½G^,RGW2!GM÷¢GFçÅG@¹NG8ÿÎG2¾G-¨¾G$s.GÓpGW.GgùGG {nGí’G FòèkFí ¨Få2úFÞh©FÕºFÏ5FÈ”•FÀèÍFºFµ8¹F®F¨ FHLÜFL¯eFVC>FZÌÇFb6ÇFfõ¨Fl×bFramF{ÂÄF‚ïùF„Š=FŠ\8FŒªíF’ÆÔF˜×"F™‡˜FŸôrF¦¾÷F¨KLF¯QòFµ€Fºš^FÀèàFÉ2ÓFÏÛ¨F×ñ·FÜFåëíFîFök=Fÿ•GRœG ³G#GÚGÛG ÖG'm]G,«cG2H G;}G?èGH®tGOè™GXŽ–G_{ÀGgÍïGpÃ0GwtGwÏQGplÙGq“¦GvÅGu?QGs«!GsñFGtoOGoxßGo¸3GsÎüGxtXGnÚGqøâGp¿±GpN›GwÊçGyViGwTGw<«GmœçGkò0GmÑGd^úG[`GV…·GM®¾GFÚÌG>–G6,·G/¤VG*bVG#²žGu5GÞ…Gˆ"G X8GH·GJFùãFòº†FçуFß|SFÖÄFЄFÈÙ*FÁžëF¸cQF·ŠóF¯9ÒF©2ˆFLA†FPXFW>ÉF\‡Fg&ýFlf¼Fmõ•Fyì#F~SF‚ F…YF‹9FÅ$F”w0F•Ü F•‚F ®òF¦åâF¬´ÂF²…ãF¸¹ÎF¾ÄGFÄYRFË9 FÔýFÜÑÿFã™ Fê}‡Fô»FþX’G¾ÂG}áGÈYG|øGÈGðG#´G)‹G1“ G8ëG?oãGGž³GP:HGY G``µGg]ÚGp~KGt?GtJÈGoùõGo‚SGs+‘GrQ®Gp¡GqrnGzžûGr«¶Gv¨€Gs®EGsZàGoy!GqwaGw\íGu˜¡Gx–GvswGrxÉGn¼ãGr¢ÀGp GkŠÎGtûÞGlß GbÐöG[CGS1ÏGKx9GC§G;4G4¼ëG-"ÖG%fGåÎG‰xGEG DGljGÛ·FýKàFôÅÎFëd·FãêÚFÙžœFÑáFÍYÂFÄØƒF½ðF¶ÜbF°ÕžF©#øFJê”FTiûFYq]F`-ÅFdìÿFmcFt1‚Fz‚þF~ýåFƒ¥Fˆ½/F‹KdF‘|‡F”øüF—Ú/FŸD&F¢äÎFª5öF®Ó‹F´øâF»¶FÁ![FÇOFÎPÖFÕójFá~HFæ™ÞFFùoCGyJG÷¹G AzG˜ÃG‹GÒ]G!ÚÉG*#7G/ã?G7ÒÝG?àGEômGO‹GVÉJG_çGllGqÉ{GošÖGww0Gn±Gv*ÑGq\BGod0GsÑGw‚µGsÙ»GsŒ¤GrL—Gna‚GqéºGs pGxÓGniGu…GqáÌGsuÒGz}Gs4Gv(GvÒGyvôGv~hGw®GsÁGkn"Ga®8GYOGNÚgGH@G@ûG8~AG0ZG(£ÄG"#9G¬ƒG>ÐGjF憼Fñ=F÷eG<G2¿G ÿyGG?ïGòXG#ãòG+™»G2Ñ‘G;8GDGJñÊGT¾G^lGhölGo‘Gq*Gs•mGrÿgGrAsGp Gs—Guó“GqGvž}Gq‡GrHâGs©Gu×)Gu_7Goµ²GsÉâGt»¿Gxò|GuÌGnÝèGv×ôGrLçGriêGsYG{r`Gn…óGskGvôGwí GuÝùGn‡&GcX7GZ-óGQ´ÖGFóG?ªÍG71øG/EG'LÑG‰AGBíGxGеGÏÔGBôFûFð;FêQäFßËqFØžôFÐnFÈA9FÁ…àF¹û¾Fµ±F®#…FRüsFYñF^7£Fa™ÁFlÌFpØFzÿŸF}u˜F„È´F‡ÕkFŒZýF‘ùGF•źF™™¹FŸFF¦UÅF« äF¯ì2F¸*«F½QêFÄØ_FÊaFÒ!FÚÏŠFàTõFëQFõü¬FÿYãGo,G qÙGødG &G=ƒG"G(âÃG/ñÁG6vG@S=GJ¼XGQ#æG[øGdžÜGpŠHGv‹GxØÍGx|{Gs$2GzÙ6GsÆ Gu´GGwläGw×ÕGv]²GuÏGr×·Gsµ¬Grú{GsÂÇGxGp^Gv®^GmOÄGppGvðGwDGvýìGtiÃGrRžGpŸGr‘hGpp GueîGtjG}Gs3óGheGG_gGT$GJÓ[GBH4G:ÏG2ZwG)_ G"„CG£_G‘GæG s´GÌfG‹Fô÷0Fí+cFã‡ÜFÚßQFÑ1›FÈÀRFÂ>‰F»ºFµ sF­?§FUÇaFUý:F\LFi4ÌFjÉ¢Ft4F|æ”F€ñæF„ê÷FˆÒØFŽæF‘Ù|F–y\F›Ù F¡"æF§ûF¬/F±ÝÈF¹E­F¼‹OFà FÌ&|FÓÛßFÝ?GrÁZGwý¤Gw¥GsÉÿGu³XGtÀGv]yGpÔ¬GvOÇGp]Gb?GXŸGH†G?2/G58ôG/¾ÙG(kG~‘G ›G¯G gGggG±ÃFøR!FîÓFã¿*FÜãFÒißFËwsFÃÀF¾ã—F¶WTF²–|FUw&F\käF`gÊFj7õFo­FwÛãF}$JFá¼F‡yÜF‹@FÙ3F“ç|F™2Fž=BF£b'F©qKF­×ÓF·aÝF»ºFœFÉß…FÐõVFÙîFâúFêy¶Fô\FÿuûGò¶G ΤG¯æGS"GçäG#ò:G+¡G2ßÛG;ønGDPuGN°4GX¤jGdj´Gp%Gsf‚GsŸGsöpGoØùGvÔVGonGtLqGs¹FšuÆFŸBF¥X`FªbGF¯¦óF¶ÜF½‘ÉFÆ1kFÌRÈFÓ .FÛ]FäÓ¤FíPFøÝ9GðÉGIàG s™G lG³ G´LG&´ÀG.ûG7ãGA£)GJ;hGTÛTG_9œGiý&Gtd GrÝKGt Go¡>Gt¥ GvÎ:GuÕGu•ŠGu”GuÖ%GuGvܰGu#AGv¯9GsT¢Gqf Gx4ûGx@7GvQZGv5rGs„©GwåÒGs)lGvž|Gs¾ÌGo•GpÓ,Gr±cGsSOGn±Gqu˜GtŒDGz‘Gw©gGoœnGuš›GoOžGcV¶GXZÅGM‚GEA‰G:U¨G3¯ìG*EÏG"YGü4G]øGÌLGÄGÝ`Fú®£Fñ+˜Fæ =FÝ FדFͰbFÇñÛF¾ªF·ùÈF³¦FY¥ôF_OèFe¦EFk÷VFqÏnF|O‹F€WØF„þF‰éØFŒ¦ÈF‘äãF•ëF›ÜËF¢>F§2ÜF¬vF±`‚F·JcF¾ñÛFÇÙ­FÎ"}FÖìFÞ1=FætCFïÀ²FýúGŒÛG gG †·GGÚ-G"‰ÈG*w'G3ÇG:5GC'~GMñçGX…ˆGcW²GpËòGreGl½ÆGu2®GrW˜GjÞºGqØGq¬?Gr¦œGuIGpGuâqGu¹àGm¨/Gs,¦GnœRGr`·Gv"÷Gm6‘Gk‹6GoakGn³LGs;wGu¡Gu|îGo·êGuB[G{, Gu²GvTGqsWGo">GtaøGr…GvmGp=?Gpþ Go{jGgGZÀñGPW—GEìG=i½G3‹|G,‹hG#üùGÑGTŽG´ñGUG\GFû{1FôCoFémbFßZxFÖxkFÎ}FÆÛxF¾Ò„F¸rËF³]eFZlbF_²1FeäTFn®FveêF|mF¼²F…ÉÓF‰¼iFŽiFk.F—<£Fž=ÁF¢[öF¥àíF¬÷²F²ì-F¹ÕUFÀhFÉu¶FС5F×ÀFá‹¢FëXFò÷¸FýoÈGŠÓG @‡GãšGü“GÑ'G%wÊG,ªG5_ƒG>\¤GF½PGRºÝG^u;GjÜLGsþ>GqÙHGvÆ—Gv»GzšGr·VGqküGtGwó Gw"½GlðMGv«ÙGx‹ŽGv[ Gu–¦Gx™]GwÂGrÌŸGt¥Gx:Gs:ÉGp0£GríG{GthOGsY¶Gt)GvÈ÷Gu:Guñ¬GsÍGr‘dGo]Gx¦Gt¢¿GxáPG{®GGt³‡Gg—G^}=GRÖGF@(G?qG4•G,Þ¹G$ãäG[8G“GG¬G ‘GÌFý¦ FòáuFéRÈFßytFØÕéFÏw;FÇüªF¿‹gFºiÂF±‡ÍF\œ8Fa+’FhÕšFlñ¾FtFÞF}PµF‚u¯F†ßœFЇFòcF‘,ÁF™WFœw¶F£ÙNF¨YfF®H‘F¶MXF»ƒ=FÂ.,FÊ9FÒŒ"FÚÛ±FäJ¾Fïk°FòÆFþÊVGÅîG ЮG0ÞG¦dGbVG&ñ¢G/§'G8‚·GA\[GK´|GV DGb¤ GnÍGtGwÅëGwÄGqeGvO«GtæGrqcGr—Gvl^Gp…&Guµ Guï Gv÷LGv(—Gs0ÌGsçDGxQGn «Gv=°Gs¤Gv‡ÏGpÂlGvÚ–GuXGGz6GGrMÔGp—@Gs\˜Gu2ÐGxGqܰGv̾Gs´÷G|~GpòKGt=Gsè½GwgÙGlÍÜG^KGS!dGJ9G?à¯G6¡G-¥¯G$×PGÀêG'GVùG ‡;G“Fÿ§æFôJ¤FêHFâ}FØ.MFÐEUFɪ°FÀ¥ÄFº—F³T\F]¶‚FbÛFiðûFmÎFvÓF{XDFƒÛF†¯wFŠïUFhãF”7sF™u¶FŸìF¤ ûFªõ[F¯™@F¸”kF¾½FÅ AFÎW¥FÓM”FÛ²FæHšFî‰UFùÈÕGÔÉGÓšG ‘'GtÄGx³EGt’ˆGw~Gv„ßGv?ƒGpÿLGv«íGwP¨Gt]µGs:ÍGz*óGnñ,Gu$GsyúGsý^GuÆpGsÛ9GpìÁGrPAGuŒ–Gs¯ŒGv›ÊGsê¶Gq4{Gz8`Gwû\Gv]KGviGqŒôGx œGl%G`Û¸GUœEGIóJG@j”G7.ÛG.0G&«cG®4GªtG€cG ÓG)ëFýEçFö#zFëúïFàõ…FÙ#|FÑhŠFÈ“SFÁ£©Fº%F³ë5F^_ÍFdJ¹Fiõ­FpX7Fw2‘F}x‡Fƒ›NF‡FŒ’oFã˜F– hFšXDFŸÖwF¤ï”F«EcF±Ö&F·CÒFÀRFÅÔFÏç FÕ ¨FÛ¯CFæE Fï¿ÎFújëGüÅG bGØG¿žG`G$(-G+AïG3z G>DdGEþGQ>G]¼‰Gi“Gv>¤GtÊÛGuZÐGp–úGvÐmGpÉoGw§‹GqwGpö¥GpÎDGohêGs«uGoñùGsõG{²)Goö™GpõïGq¼GsrGp ¨Gz|ìGrÎèGt¼4GwηGs ƒGsæíGy6çGsÈGp—™Gol7GmîªG`íÀGT¶7GKm7G@Gt¶ôGqžYGr¤UGnæ°Gv¨GmGPGmàÄGbxRGT¼GJÀuG@Š”G7HËG.G'|éGâGCýGjþG ¢G33G­FóØûFì ñFãŸßFØd×FÑ"fFÈñÿFÀoVF¼Œ9F±IF_–Fdˆ¬Fk†3FrÌMF~™pFÏF„ܧF‰ÙèFŒÖÌF’º£F˜xšFœ†F #LF¦G&F¬r-F´z×F¹ãFÁ`GFÉÓÜFÏ(™FÙ¤Fáy‘FéóºFôhYGG”‚G 0nGíGÙG!R[G(÷ïG/3íG8òyGBç³GKCœGVÊ‚Ga6GqÁgGv³nGpÇÑGow˜Gk‹ìGuæGtm’Gq/TGz¹ŸGu“Gt/7G~}7GrEGqÆ8Gwý‰GuâÂGv©¥GtåÔGoŸ.GsƒgGpÒ,Gz7ñGw2ÀGw—GnhòGplÓGoûGr§÷Gr{\Gq³ÃGsÒGsßGv4(Go¬fGm°ÊGwUGvz±GvËšGq°¹GsÚ Gn^‚GbrGV »GJ<ÚG@ƒ(G7XÓG0,ùG&ýhG¨Gë G4óG GÅG—ôFþ)¶Fóž0FégFà#[FØáØFÌ¥yFÇyFÁéúF¸¼«F²¾éF]ÛFfm€FjsøFsÏFz2bFù¿F…žèF‰îÚFë$F’[ŒF—ìuFœ_¾F¤ƒF¥hF®·KF³ÉèF»açFÁ¾FÉ~>FЫFFØ+¤FâÓFì«AFõ‰GA°GašG ¥7GÙÏGÚ(G"ÝG+6¨G2X‰G;C^GD6GMrÌGWq~GeI[Gq—KGx“ãGmÔþGnÝGuCUGqѺGv,GnòGr8dGr-QGk’kGq!ÕGsÉGqxGsÁ¾Gn)6Gt·PGu’íGu¶¡Gv@Gt–nGqe¨GwGpä GtuGtÚGqäAGq’ÌGq[GrGw“†GoßEGo9ÓGyíGn¨1GwñG{ÚpGsæ¥G}ÅÁGvêHGn{°G`± GSÕ¤GJÒëG?}G7sÑG-“µG&šGŒ GÐñG{¿G JG©õFþòFFóØÇFé‹FᤥF×wÍFÐ+0FÈq¹FÁ€ùF¸#2F³½VF`OÕFi'…Fl8®FtêF{€ÀFƒ;œF†ßTF‰ã]FŽMÃF’ÊF—…‹Fœ'ûF¡Ô¸F¨JöF®>AF´%_F¼ÝœFÁçFÊ xFѸÀFÙ§ÊFáT–Fì¼hF÷eÆGUœGßG 4G¹ïGžG$$&G,vG3H¿G<ùšGDÓ©GNb³G[¢GezGqì_GuóSGu'UGvïCGzsrG~S|G|qåGv%GvɯGE+¿GQ7&G[Q…Gg'‡GrÙGx’Gt½©Gq9„Goó×Gt÷…Guƒ"Gs®"GqÐGq™×Gxb}GußÓGsåGqSvGrǶGq£MGvWGt’YGt¤Gv£2Gs¬üGto>GrtBGu»GvVGsšþGrî¡Gt®»Gv)GpäOGr¥àGv] G{¹Go. !G5_ÿG-<¦G#pÊGÂhGî~GÃÈG¨GþôFütEFò`hFéBjFß BFÖ¤FÎ÷ßFÆðGyº­GsgGx&uGrJ±GtŸˆGvrvGw—MGsÎÅGqGsGwWÏGrÆ¢Gu?Gtò Gw~¶GxŽÞGtjÁGoT¯Go_GuÖFGpÔWGu*°Gt(ÞGqƒ«GmQDGx›qGpÇâGiÀG[ø)GQÐFGF!GG=eÀG4ªiG+x”G#úêGT÷GøGæG †Gñ6Fù5Fò8fFçmÝFß¿•FÕ#!FÍSFlj¬F½¬F·\ÓF²F^ÞÌFiGXFoÚCF{0úF×ÑFƒÐ[F‡”F‹æßF.F“+ûF™ÝFœ²£F¥öÝFªä‡F¯E¹F¶ VF½®ÏFÂpæFÌ!ÆFÒaFÚ·Få=Fð¢VF÷$¤Gb@GiG À¸GÍ4G¥G#Ó‚G+ðåG5IºG>ãGE׉GQ± G[ÝGgåGtöGrŸ€GpËÒGyS…GrÑÇGlœ˜Gy#…Gp%ëGsÉ FædƒFíçÜF÷îÃG·GwùG ñGü?GâG"KÁG)=‹G2x-G;ÀjGD UGLôpGW‘EGdHÖGoAÇGv]ãGqÜ3GvÚ»GrÔÃGv!£Gq·DGr«AGx¦åGqþNGtuôGtÞ©Gu.FGsøäGxGs››Gyá/Gt'‘Gq&CGt¸#Gu´ìGszGuƒGqr¿G{~,Gp3GpñôGpäpG{¯'GwG|GqgÓGuÚ;GtGtï£Gpm¡GtZGxj¿Gyi\GmzGbˆëGYh GOùGEÀøG;ÙXG1i~G)ÛG"‘˜GîmGÍxG _‘G›¯G$FøëØFïò_FçIßFÝ÷,FÕ§{FÌÍÝFÆÓF¾^F¸@ÕF²âÎFª‡÷F`‚7FfìFpHFtj F}þ1F‚ë›F†- FˆàñF]ªF•0óF—PGFzF£×ÒF©ÄyF®®‹F·åF¾Þ>FÄ0¤FÌFÒ¸FÛâØFá®—Fí|}F÷·lGß~GTëG %kG• G¾G _G*)áG0ÉþG:xËGBÕWGM6!GW[¬GbúAGmìúGqMÕGtnGwJ½Gs9Gt”Gq&Gxì6Gt¹ Go ÉGyòdGrÁ9GsuGzEGuÍ+GtRðGyµ¼Gp`Gt‡Gr‚èG|ÎF†‰FÊ FÔ FÛn FåS‚Fì<^Fõ˜EFÿ¦GÖG »'G¿^Gb}G Ÿ¤G'¾G/c‘G7 3GA8nGJ¤GUÃÉG^ÈTGkxƒGu²–GuyÕGqnÆGuªýGt#´Gu(qGs¢Gx¸@GlM.Gu%RGn.ÆGrÿ•Gv&ñGqÝIGs.GqV–GnFöGqιGuú?Gw)ÛGu|ËGsÈ¿Gx~6GvœžGlbGyçGpLGp:GxâGu¤JGsÉGy0KGu×G|±xGsßàGlé‚GrW1Ggà“G[;_GPQ»GF„ˆG=¢\G5· G,î¶G%¯›Gé GçG”ôG 2G¨ðFþ’±Fõ~kFì“wFâé£FÛ´ÆFÒFÉïKFÅF¼]F¶ïoF­ÄF¨‘F]ÄjFfÁFlFu DF}[ýFF† Fˆ,&FŽ?F’5ÌF—FýÛF¡Ê´F©F¬3F¶2ºF½‘ FÂÈFÊòPFÑ[FÙ ©FàsFêQXFóþG8·Gè‹G :’G•?GLOG„jG%õG."áG5.àG>6‡GI^LGRçÚG\\_GioAGt©4GtÃ$Gw½¿Go£Gt“;Gq¼0Gp OGs™¯GwÂŒGq¸öGrãÄGr)ßGtGqÑÃGuå=GuúóGsGs2(Gu˜‚Gq0’GwêÌGt‹-GnêYGtë¼GvXGukÛGqRÃGs™8GyE¡Gs˜%Gq–”GqÄEGu=ˆGs‡¤Gq¶·Grt¨Gkf?Gag`GVqFGLž‹GB»?G9ù›G1;£G)õÜG!rÐGÓøGxG‰íG H Gf¥FýÄàFó…’FêÿFFß™GF×FкÊFȱ@FÁtëF¹Î¸F²Ê'F®gžF©¯F_Fgæ7Fm) Fu§F|î¨F‚$F…Y•F‰ŸæF%F”©ñF˜CGF›RxF¢©BF¦ÐlF­‚úF³í¢F¸~ŸG4£G.LG&?F]Þ’Fe ¹Fn2IFrNgFy««F€"ÍF…&Fˆ4¿FF’†F–&¢FšŒ-F¡&hF¥?ÖF­sF´¥µF¸=FÀ÷FÇjiF΀ÐFÖ(åFáPFçRÈFñ+‚Fú6“G‹†G’öG ÆGœÍGJ‘G§6G'ªÓG/£dG6ЩG?SìGIyFGRŸ«G\p\GgU0GqÛëGv2Gs„¢Gu‘ûGvã-Gp™§GsØ>Gq¢vGsø}Gp>TGpJšGpü-Gq] Gs,MGu\CGs¢GxÕGpëMGsÏÇGpmÚGqfGyV…/G5€ÂG.|ÝG'ŽG„’GbÃG×tG tCG*êG£öFú‘Fó¬íFé{FáÕðFÖ™FψÅFÉ\:F F¹„RF´£DF®‡‘Fª$;F£È’F^óüFg©äFiåÿFno}Fy yF€_F„.FˆRFFbF–šaF›ƒ+F¢ F¥—NFªÃÿF±sšFº“F¿FÄ5FÌþ˜FÔ™FÞ–0Fåi†Fð¶(Føy#G)GuKG «GHGZåGÝ…G%kÒG+veG5o™G<ÀÊGCЛGOJGVG`<GkïGrÅCGpNæGt=lGt¹ÌGp¡ŽGw*áGsêžGyÙcGvkèGròùGz¿Gr. GzkJGu ÿGs/GshHGr[BGqKçGwVGwwJGuˆ!Gu@ÍGró­Gu%GuóGsÓ"GuPèGsFGt ‘GwLqGté×Go$“GeŽG]ÚJGS"äGJ`[GArÇG:?eG0½ØG*ƒG$€ŸG±GG´uG äGÔŽFÿÜJFøzFí«xFäzôFÝ=ÏFÖsF̹¦FÅ~ŽF½°òF¸±nF² ûF®#ÇF¦IF¡`jF[Ô1Fa’ FiÛFqÎÊFxlÈF€™þFƒ%FˆÖ"FŒlÝFÛÂF•÷Fš˜ìFŸ»F¤,øF©sF±òFµÑF½ùŸFÅú‹F̤`FÓš^FÜK¡Fâ·FFõf^FÿŸ_GnÖG 3_GØSGeeG¥[G$?G*%0G1BG8†kG@ºÿGI˜øGR©·G[{ÆGeÙYGp*.Gs(èGt"‚Gvq+Gw¨ƒGu£XGo×ßG|ïqGpŽGoœ3Gsp]GtBGvëÉGt@úGu¡#GuþG|„àGyÍ}GtrGt sGq½GsPpGqFGnÐbGsÆGwUíGk·°GsâÖGw~öGr@ GqD=Gh{ôG^dšGTô‘GLÌßGE,G=yG6†«G/=´G(mŒG!´EGc/G$G&ËG qG7ÍGXFöê‘FìÛFæ„hFÝnÀFÖ§ðFÎÆ¢Fǃ|F¿þSFºé’F²‚F®fF©–ðF£»ßFŸI^FY ?Fc3éFiÂNFl´(FrúÀF|ŠF‚ŒF†(#F‹Ð£FÕÀF•#½F˜»ÈFFBF£ŸNF§ÙŽF­f|F³MhFºÁ÷FÁêÔFÈa FÎ^ðF×M‰FßéPFéSFñÿ³Fú½ÈGÎüG°TG h“GíG×—G¹G$:¨G+.´G2UVG8ÀÏG@ó.GH×ÇGQ!QGXéiGa-Gl_ØGu½–GtÍœGsxGx¡hGvÂûGrè+GsN‘Gtk™GwjGwr‘Gsã‚Gup$GqQEGqgGqZ®Go‰^Gup1GvkGv÷GyóìGtoìGvfGqP|Gu„ïGv9–Gr€^Gq®GgpWG_êGW1^GO fGG”²G?à}G7¬­G2TdG*P˜G$ ²Gi¢GœèGÞìG ²G?G ¡Fú…ðFóº!FérŸFáFRFÚ÷LFÑŸúFÌ”FíèF¿–>F¶5F²-Fªø§F¥ý†F¢$“FšãFYpF_ÄPFf Fk´ÑFt™¾FyˆUF5F…iFŠFöÃF“Y(F˜ãFžpF ÁF¥Ö¦F¬ÃÚF²tF¸ÈFÁC–FÇ\ FÎnûFÕ jFݰ½FáøÓFîÂFùÆFþʯGµxG ¬ŠGUüGíG9XG!^¹G([ G-°ÑG4ù5G<˜GD“ÇGKCGT IG[ë[GbãGkfGqÈãGu °GoèGr>òGtO…Grß¼Gw€@Gp@&GtħGm¶ãGv±¤GtË‚Gp†˜GwGuò¯Gt`Gqª™Gs_ÓGs‰×Gm ¹GzFÖGwÁòGp&€Gv(Gp‚GhNöG_'GV>GPãÚGH³ÕG@߇G:& G2ÿ†G+‡€G%/¾GGì¯G†„G¡G uG¾˜FþuFô1•FîœFåÄ FÜBÔF×yFϳFÈqFÁTÑF·Õ0FµíF®dzF¨_µF¢ ÀFž~F™Ø2FX‹ëF_ˆFd¶OFkFs0F|:xFlòF„€~Fв*FŒJ¾F’ ßF–…F›­ F “iF¤ïbFªÏ;F±|F·´F¾©üFÅ\+FË[.FÓøšFÛF☸FéS>FôÔÇFû‚ÐG¥GjG …°G@@GYGÈG%ZSG,&G1ÎZG7¡|G=¯:GGW#GLMGU-þG\É×Gd´Gjã,Gs1OGsSÉGr¸þGq¾Go½Gqø)Gu+0GrœGoܳGq€ËGv«žGu­ûGxMÝGv”mGs78Gr#EGlÔnGx*´GpUDGo¤GsfÝGo Gnö‘GeábG\ЖGVÜ"GO:dGHi¯GB+ÐG9¶G4LEG-¿JG&EG!Gë½G6óG€õG ž6GsuGeFùðÇFñmòFê)QFá FÛ-ÂFÒbOFÌfÑFÅvôF¼ÈÎF¸¸ F°º±F¬¼ÌF¦3»F …•F›´F˜@ŽFX3¬F]šÕFa§³FlDMFoظFy"F~#FƒWÈF†ÐŠFвßFûJF–…Fš^FžO@F¦¯¯Fª»/F±íFµÑ{F»^ZFÃ\ŽFÈÍÍFÐ×xFÙp@FàwmFç÷FñÈæF÷€«G=VGù¸G è|GÛgGæ]G+XG!±G&ȪG-žîG3 µG9'¬G>¦ôGGV–GM“5GV:[G]gÈGb_Gk´GpèGw¨GtÀïGyûªGt÷}Gs©¯GvŽGt2¿GwlGtÎòGu:´GyºHGs•žGq—Gy>Gt4*Gz”WGrCXGugŽGoÃGiÞ÷Gc…G]2GV×GM¶TGF¨ýG@X¼G:KÂG4jâG.\G(ݽG!ìlGS±Gg«Gö€G "G{ìG´ïFÿ³ÐFô6xF죈FåFÜàÚFÖ¢FÍbµFÇXFÀ6Fº¬×F³¬F°7ÿF§•F£·CFŸªnF™uF–ÃFUmNFZº Fd\ÑFiùÕFq´FuuÊFÒFƒÊXF‡‰F‰CFã^F“ÂpF˜`kFžHßF£´äF§ŠBF­²ŽFµÖDFºFÀêFÇzïFÍjFÕ`sFÜ©tFãùúFìŠÎFö#[FüçáGƒG¤OG óG UGGØyG#‰G(ÓnG. ÊG5Ø G;ªAGBR¢GHK‚GNQÑGTNþG\"+GaGg–#GnEGr·RGs¤Gx 7Gs.¹GpÅÏGyÍÏGrŠmGp¶cGuDÖGr@Gq1ÛGq ÷Gsc‡GsÐ1Gq©GrÍGl,Gfé=G_ÔÕG[tfGSœïGLGG×GAýðG:G4tAG/¤þG(;ZG"ÙGC\GÔÁG³DG##G¢ GŒFÿƒFøXìFìû†Fçµ¶FÝKêFØN¹FÑЮFÉ<FÃþF¼­F¶<îF±Ä2F¬ØF¦øF¡CqF7QF™tZF“2?FT_«F[’ÀFbaâFf¡…Fk%ÕFv¹!F} HF€ÖíF†-FŠEªFžIF’2æF˜FFœCiF¢øF§% F«›`F±3ÏF·ÔöF¿= FÅE/FËFÓëFÙ¢Fßå\FéäFðRFøjGÆaGUµG Ù˜GY€G¬‚G¸ÁGùÌG$áfG*SèG/ëàG6]ÏG;PªG@˜IGGª¡GN;GRµGY.oG[Ô%GbÿxGdþ‚GlibGn(oGpœ$Gn¿GrNlGvG­GvZÓGxycGqv³Gt§GoüªGnãÅGt‚oGsŒûGe¸Gaó*G[sÆGV_¸GO/ØGIì»GC´¦G@MG8f1G4ƒLG-¢'G(G"ÐÞG†G!GΑG}G 1îGÞ.G!¥Fú‰FòíÜFêçþFáWŠFÜÉFÖ ÌFÏÀFÇFƒFÀ`èF¹œjF´'F®]ÊFª)>F¤¹FŸ4:FšCF•‰F’PFR•vFZtÌF_›ÕFgõ FkÒFrº»F{KmF‚,›F„kÑFˆWIF!QF‘ççF–!1Fœ´Fžƒ±F¥m…F©˜öF±Å F¶¨äF½ ýFŸÐFÈ{OFÐqFÕÝrFÝÎrFä ÎFíøFöº0FýþèGCíG­^G ¾(GòìGÿDGNG ÔRG$¾WG+´¬G0’þG5;›G:…kG@ÆHGFÒóGISRGPÄ:GT´LGYr.G]&ÕGa>yGe@¬GhýGhšÕGq>Gu¤JGr9/Gt(êGqåGsVGwŸ¼Gx¸žGw´òGlk³G[LrGUÌGPذGM÷éGGÝêGA̲G=\G8A¯G1 êG-ŒëG(WíG"bËG‚öGNRGìåGGG ö>GqG›Fû—}Fòs«Fë©Fä§éFÜV»FÖªöFϱûFÉ}XFÄ3hF¼-™F·ÆF±ð7F«gF¦;VF i©Fœ ˆF——‚F”݆FÝ/FS¼ŠF[±»F_Ò‘Fc[±Fkô³FsÒ©FzDñF€¨9Fƒ®úF‡¾šFŠ{ FŠÈF”âNF˜¾8F¢dF£‚ªF¨þ_F®zFµíóF¹A§F¿@áFÅÐZFÌ]+FÔ¤²FØÅÙFâfÄFèñÞFñ LFú÷¿GVÙGƒG øÜG NèGšOGfG^G!8ÿG%ÜëG*ÍrG0JyG5ZG9w5G?¥%GCE£GGÇGL „GOÜàGSxaG9{kG=¿XG@\æGD²ŸGF³§GG“ZGJh~GMJ'GLÝGMêGyNôGo(GuÞ²GuûCGr×IGr©úGDËVGA<ØGAhG:ýÎG8(ÞG4¸ÍG/Ñ-G,~`G'*[G#ãÇGö¸Gë‘G‚ÑG-šGqG FæG¹GÆÖG‰F÷è˜FðÇÍFë ²FßÇZFÜ=šFÔ~¬Fζ½FÈŠrFÄ;ÊF½ÝF·ª¢F²r†F¬øŒF¨ÐBF¢5LFŸFš‡±F”ùèFïŒF¢pF‰FLËFU'¢FXZûF]¿?FböHFk*yFq3™FzúSF}8—F„q€FˆNñFŒNºFDîF‘ÍžF™Ã8F«µF¤gF¦„AF¬{¶F±·®F·lëF¿àØFÅzÓFʪAFÏÉFשñFß~±FäŸJFêð€Fô]FúRõGGhúG¿íG MGˆ7G.®G%ÌGìG êzG$Ø G(ë¾G-G/ {F“`ÙFŒåÌFŠ·ûF†ã–FLñFOÜÄFY‡9F^Z4FcÜZFk”ãFoµ Fv›íF~5’F‚±ÊF†êFŠHmFŽUÔF‘’ÍF—ìLFšÆEF¡eF¤°2F©èF¯ý'F´ƒ±F¼-FÁ‹åFÅíõF͈RFÔÙÎFÙ‰ FàÐSFåû?FêjFôRVFûÀG,âGlhG !IG ’RGóËGžkG÷”GªLGí|G"n%G'9œG)o;G-­ôG/°ˆG2üG5ê1G6µÈG8„eG:ʃG:ªDG<òÊG<’:G¬F¢ŠªFqF—9,F“=¦FsÉFRAF‰'³F† \FL¡MFQÎ:FTµæFZ¶oFa”>Fh€‘Fn4ãFt½*F{F€˜YF„TÆF‰ÜFŒˆdFš§F•àF™®PFÕ™F¤^gF¨M–F¬,íF±¦F·¾¦F¿éFÂÍZFËJ3FÏÏ©FÕpFÜPÄFãn³FéMFñ6F÷ŸFýàGUßGUG y±G ‚"GZœG£ñG ¨GÜ!GÜG!ØÂG$ÛgG&XÿG(h G+­G-ôOG0‡qG0ØXG3ÏG4bàG4FG4mŸG5ApG5g G[øºGpòþG0ˆ¢G.fÍG-aRG+ÅG(Á)G%™ïG"¢UGmÔGË!G€¾G/G¸„GG  GÍ~GøMGgJFýšFøFòÚFéýUFåCFÞ¹?FÙ¼0FÐ8åFÌv¹FÆ‹ÖFÀVF»tÃF¶Ø¿F±•VF­sF¨e¥F íFŸ"F™UF•~F‘¯FŽFŠÅ8F‡dFƒîWFG`åFNÒfFT+¼FX'0F_ÜÚFekFmêJFp‰bFwN‡F}Ï£FƒF†¥OFŠ—&F3'F”~F–FšòƒFŸzæF¤´bF¬ F¯ŽFµ:ZFº2FÀþFÇnoFËQFÑ8FÖÉxFÞ™-Få§ÀFë'Fò¶FøOFü»4GŠGæ[GÿG +æGcJGGŽÁG7JG>ÇG»BG!ž G"ø¼G%}G&ŸG(ü1G*ßëG+jG+QÐG,*G,=$G,׉G+øG,OåG*´ëG)‡ŽG(j~G%#ŸG#ÝäG#_ÝGN\Gu1Gú:GpÂGDbGåÈG­G ‰fG“#GGGÏFþWŸFõrïFð© Fê’FãGñFÞ>F×ý¢FÓ¹FÊ÷CFÇi¢FÃ7¶F¾^xF·­¥F±È…F­nF¨HaF¢È™F NAFœ˜äF–žF’@ FGF‹¼F‰/IF„‹ÇFQxFHàFL»FPŽ FV¦F[œFc>wFièøFnuµFx¬FzëˆF‚dDF„èyF‰‘FŽYªF‘ˆ•F–Q:F›vµFžhîF£úsF§hiF­á(F±ÿ9F·rÌF¾ —F £Fȸ0FÎV„FÒ¤¹FØN(FàD³Få!âFíFñËÛF÷èFÿ†-G7=GíãG%ºG ƒeGܾGdçGQ²G™“GxGc~GC G oõG ™€G"G#éÏG#úmG$b­G%ª·G%å“G%‡{G&z¿G%ã¨G#PG#€G"h^G ÒUG·G_:G*ÆGÿUGÛõG:G©éG Á˜G +jGeTGø‹GMÂFû5PFôÛ»FïénFêÅ™Fâ›ãFßG†F×vâFÒ„uFÎ¥+FÈëòF¹cF¼ÿF¶1=F³áÄF¯iªF©³F¥¿IF Ì[FœŽòF˜«F”aFOòF‹£Fˆ;’F†$æFgF}÷ÙFJ4µFIòÌFO"FTÔâFX|qF_óÃFeÝðFmÑFr¡Fw ¡F~KÁF…?ÀF†¢ëF‹þžFJF‘øŒF˜T¾F›Ó>FŸ(vF¥¾Fª. F®ÍF´¿ÎF¹BMF¾:öFÄõmFÈ– FÎìÎFÓÞÇFÜFáP Fæ²îFêFsFñSÄF÷/éFý}Gg_GAZGf.G G ÚÆGáhGHìGÝG^KGJG½¯GùGå›GWóG"çGêGÎÒG Ø¿G‚xG VG˜€G6§G×¥Gc¢G·ZGXG3…GòG.GŸmG \†G E–G\ûGøGþuFþõF÷´MFõºFíù£FêÂÀFáìFÝ´wF× ÄFÒÛõF͘æFÆÞFÃ9F» ÇF¶°·F³Â!F­>%F«|nF¦÷F¡–˜F^ÞF˜ç0F—”§F‘:äF­†FŠ‹ÊF†ðžFƒÙ’F€¬¹Fzú_FD8…FJSÄFM²FPTîFYy9F_ƨFeJ£Fk†FÐ]F•×7F™Q"FK¼F¢4ÀF§p,F¬f/F¯‚œF³÷9F·ñ¹F¿» FÁì|FÆãeFÍ» FÐFÕAFÛ%Fá³FäSFé™.FîÍoFó¦ÓFø.¤Fý,ÃGæçGïuGÐ`G"½GƒBG Å`G OG hG ^ÜG ÝýG á†GÃ0Gå‰G¶ôGSäG ù+G KñG XÊG :qG >ÀG‚ Gó Gt:GöGë±GvÍFüU™Fø#gFõŒ8Fñ‘{Fëÿ2F掉FâCÕFÝÀSF×-FÔxÛFÎë³FÊõçFÆÕ$FÀ ¼F½*CF¸ žF³_lF° ëF¬ŒšF§²{F¢ìFŸfëF›jÍF™ ŠF”¸F_°²FeùXFkÌäFpÊ*FxÂF€0=F‚ÿØF†èF‹¹F0ŽF’ž~F–€.FšjŒFžÕF¤3F¨lF¬ F¯G–F´¢üF¸CF½×’FÂ4ÓFÆ•FÍ{~FÐ~VFÖ ]FÛÀ¼Fßá›Få«-FèÖÌFí;Fò 'Fõ«wFú_aFýÖ©G ÑGþGâGðÊGK`Gÿ¼GýBGá#G 3üG ÑÃG B G÷KG Z˜G gYGã:G+ÈGcG<ÎGÔ®G¢GÍFÿ”Fûâ³FövÐFõ¡¦Fò¥;FíÅoFêNÿFäšYFß3—FÝBóFÙ FÔFÍ6bFʾ|FÆ]FÁ.ÑF»ËaF·iF´hF¯4BFª'F¦^åF¢Ü$FŸ©2F›ÿÒF–œF“‘>F‡zFŒ FŠØOF‡jF„v|F½mFy^FtlÓFo¹Fj;F>UYF@mÄFDÌïFK9“FL+nFSFX|žF^ÉxFc[ÊFjÑ,Fn¾TFx/×F~ëF‚ îF…¸÷F‰XíFŒü¦FäôF”1ýF™BFZF G2F¦¸¢F©èkF®ŽçF°« Fµ¸òFº»ßF¾6ÓFÄ"5FɨFËÜfFÑu¹FÔFÚ?ßFßL4FájFèZñFêâéFî̉FòÅHFöGo¹GöGˆGÒ£GªnG·.GüËGÛtGÌ&GæäGùGþ/G—‚Fþ'Fû%‹Fø£XFõIËFò:FïvFëÑ Fè˜FäAQFáåêFÚéjF×%!FÓ ¼FÑ*F̵¸FÇÏUFÃ×kFÀFºæMF´»bF±ìF¯4FгåFˆ'±Fƒ#$F‚+/F|özFxV:FqÓ"Flõ“FdéRFe¶F7ãrF¸FeìF]9•F7ªOF=F@+FB&VFGÑFJ¹ÿFRžFUªbFY¡ÃF`4FeddFjÞÔFs"ƒFv©ôFzEjFƒ+òF„^CF‡'ÐFŒ‚„F!F“${F—%­F™DÝF’F£RQF¥Ô F¨Ñ¯F®ÐèF±tCFµÖµF¸E5F½ÏeFÁËFÆÈ¹FÊPñFÎk$FÐ~RFÔèFØjFÚÕFFßFÞˆAFäp´Fäô,FæÖÚFèk˜Fí ¨Fì­$FíHþFïºàFíå½FFîÍqFîÚWFï…Fî»,FêÝVFëyËFêIáFè¨FåõWFã~ªF༬FÜ‚uFÚ€F×»WFÔ<¬FÒ“FÐc:FÌPdFÅ«AFÆ@zFÁbF¾ ÀFºŠFµ‹]F²k?F°{ûF¬süF¨Š)F¥2ÄF¢™GFŸ]êF›ždF—2¿F”)PFIñFŽAFЉFˆ¤ðF…’•F‚zF}ó—FwmZFsΖFnøðFj:†FdêF]ЙFX¬3F5î4F:4F<*zF@³KFG$F‰i;F‹öÇFŒÈðF‘¯ÊF”0)F˜Fœ-QFŸàF \‘F¨õFª1ÒF­¥F°ÎòF´uœF·-8FºT¿F¿!ÑFÂÀFÄ­5FÈXŒFÊÚ³FͯzFÑd6FÓ‡*FÕÈ,FØò“FÙò=FÛ~FÝXF߉FÞ˜¬F߯ØFݳFÞ&óFßFßÛæFàOãFÝÓHFÜSøFÛ~ÑFÚ–F׫_FÖ¸÷FÒ¬FÑ»þFÏpØFË­ªFÊN¨FÈŽFÆVÇFÃüF¿ÜÂF¾eÉF¹¦÷F¶‘›F²£SF¯ …F®8³F«mËF¥]„F¤FŸÓÇF¥îF™òÊF˜,F’ôFonFéF‹ F‡>^F…ËêFÇF~ˆFxðdFpךFnè'FkÕ•FfÔF`åñF^(ÀFU®(FUsÃF1ñ’F4]þF9òF;­FA‡^FC©²FNà–FP,OFT¶pFZðF^ùFd.FhŸÔFl–»FrrBFuE[F€ÌF€ÿ€F…é'FˆWùFІ0FÐúF‘¨F•O=F˜ }F›¿ùFŸªiF¡;F¥›F©ë;Fª£ÚF®Ú"F²c»F¶ÔoF¹"çF¼IF¿Ê[FÂd£FÆ.FÈiFÉäÃFÊbˆFФBFÏå4¡F@ÃFJ·µFM]«FQBÔFXfæF^ò„FcvÈFeLõFeQ´FoFsÜÓFwß–F|ƆF‚ F„Ì„F‡žÐF‰€›FŽßF@‚F•ËœF—€lFšÓíFá$F¡µ F¥­µF¨¿›FªŽF­ôÍF±§ÇFµ3±F·kÁFº2•F½FÀ wF¦üFÆ‘¬FǸ]FÉXËFÌÑcFÌ€‹FÍ}ÂFÎô²FÏ”bFÒTôFÐ)FÑßJFÐàDFÑÃjFÐoFÏøÍFωèFÏ|©FÍØ¾FÌårFÊŠFÈoÜFÉ¢§FÅ5ÅFÁÐ FÁ HF¼LÕF½E0Fº>fF¶ÑüF³ÌF°êáF®øxF¨üF§³F¤²¹F£ òF 6áFœíF™ªÚF˜&÷F”tFKFqœFŠ6%FˆöØF‡©­F…ñÕF‚Ù2F||F{” Fr¼?FmA¯FirÞFeIbFaÐÔF_ dFUÍ1FU!FRvFJúÔF)ðtF.°ñF1¤HF: ˆF;­ðF<^”FDrLFJ¿5FQÇ)FX?ˆFXnAF\*VFb _Ff‹FißFm7£Fv˜œFzsEF}ÑeF‚AeFƒ¿¡FˆZÌF‹Ý®FjF‘Ï»F“Þ·F˜h-F™ƒFÛF¡ÌaF¤€F©Ò¯F¨²œF¬¾ãF¯±¦F²Õ(Fµ4ñF¸JUF¹a=F½˜QF¾PFÀˆFÂq‘FÄf FÆ`üFÇ®eFÈ€ÑFË[FÉèuFÉ FËÀžFË“nF˦3FÉçïFʨäFÊÿ9FÇ«FÇ,’FÄÎWFÂÐFÂqFÀó?F¿–ØF¼‚¤Fº?F·fhF´ÆvF±Õ­F±dF­ÄeFªÈëF¨8iF¨|ŽF¢k†F¡‹éFŸ*FœL‘F—~:F–HçF“äßFÝ)FŽÔPF‹nKF‰$ãF…æŒFƒƒíFÜâF}"ŠFyÇNFt `FoyÂFh\FeÄ8F`-F\:ÂF\^FT€FQÖVFJ¢ FJ1 F(ÊHF-Â÷F2b“F4¼QF7F?xFA"ŽFIµFMú–FS¡ÐFW?FYjF^í1Fc²Fd¥ˆFl‡•Fm YFtžFzMaF~è´F‚Í·F…,F‰fÖFŠæjFÊÈF´úF”cšF–>'F™€’FœÁ€F _£F¡ëÌF¦ÅHF¨ÐnFªF®(èF®æ’F²Û¤F´‹\F¶WF¹}F»F¾>F¾wzFÀŽFÂÊ4FјFÄrœFÃnkFÄo1FÄC6FÂØÿFÂe¯FÄz8FÂ%óFÀí¨FÃ+¼F¿¬ÚF½ÇF¾ÌKFº»¡F¹¾qF¹GFµVþF³1%F±ÒF°,¨F­bÀF¬Ó’F©e'F¦±jF¥qÝF¡¶bFžxÿF6¢F˜ï—F—†¶F”‹àF“÷.F’StFŒQ¢Fб:Fˆ³¨F†¨`F…XkF}ý*F{4xFxëfFtx°FnQ“Fiu-FfŽÃF`•óF]ð@FY, FVeöFP«±FL‹FEÀ\FCÁF(’õF+'\F0‡KF37F5_JF9Ò¤F?è]FE6pFL FN;ZFS!FVÞF[-PF`>F^§,Fd‘XFk‘CFroXFu0Fx‚«Fv«Fj9F…rBF‡ñvFŠòJFü¬F‘N¡F”ëF•–/F— F0ŒF"]F¡yçF¤*ìF¦ÔDF¨´Fª´ÏF¯À F° F³24F²9jF¶7èF¸ÏFºÅFº›ãF»ÑzF¹VsF»½]F¾…0F¾_…F½‰ØF¾G>F½OjF»ë F»' F»nšF»UF¸—F·zÌF¸°8Fµ<þF²¨ÔF³'öF±åßF­ßzF­¥{F©À–F¨ë"F§žºF¥¹ÅF£*EF û:F8ôFšj§F™F…F•ùÃF“ÓFÙ FêF‹þÀFŠæôF†KòF„I·F‚R=F.×FÿaFx·FsÈFmð0FkÙ%Ff sFaûVF]&5FY+ÀFTyÜFQ àFL¯vFK@]FC&ŒF@ò›F%±èF)wGF-7hF0á˜F4µœF91õF:‡pFCoéFD£FIèýFL?ùFQeFV ¯FW&ìF^!¨F_áêFgsÇFj Fo=qFt…ôF{ üF~ˆF‚ ÃF„œÂF†aõFŠ©MFŒÏF †F†5F•‚F˜wyFšeFnœFŸ„ÝF¡Ñ®F¥F¥¾ÅF¨ñïF&J^F(F)F/@uF1èöF8<0F9_@F=‰éFA‰.FGFKSÌFMU×FP›MFS·,FYVuF\¡Fb$ªFf?öFkŒ{Fm“ÕFuu½Fyî‡F{ÓUF‚dÑF…Fˆ‚—F‰GZFŒºÀFòF‘£cF“dÈF–½…F—ïÉF›¬uFÓôFŸ¤sF¡3ÕF¥ÈF¥XF¦aF¨²þF«ÎvF¬ŠF¬¹F®æ’F°ÿÒF°|ÑF¯)ŠF±ËF°ÈçF±²»F±" F²CèF¯*F°PNF®§]F®õF­ÜF¬ŒéF«£FªðF¨©F§:F¥W—F£˜F¡Ç$FŸ0ZFÝFœñ=Fš¡F—R(F—²ƒF•˜F‘µáF‘FWF‹ÞºF‰ÃnF†çgF†ëF‚ãF‚ ÌF£Fw Fs~ßFnšFl”ÕFj/“Fh©NFbu¶F\RFYwXFY£íFQÔDFL|sFI[ÕFDFCJ«F>bñF<9eF!F&zF(ÌF*ž”F.B’F4^F7v²F9rhF=ÐbFAj[FDÙ¨FGô>FJ’rFN<FQ›FT×ÁF]§ÔFa}FfQ‘Fj9IFlí.Ft FwãžF|«F€@SF„‚’F†ÜÓFˆ¶F‹ˆFHþFŽ”˜F“)F“8ÌF•O(F˜ýIFš›F:ÍFŸÉÇF !ËF£¢AF£CáF¥%qF¦¤Fª{bFªnF«žûFª`üF«F«²ðFªìGF«ùFSFœéâF›ÌòF›t™FšŸºF˜?F•áQF”õmF“Þ2F”ê¾FbžF§FŽzgF‹^åF‰YªFˆ+ Fˆ,sFƒìTFƒŒF‚áF}goF~µ¯FxÃ…FsÙšFon¤FluaFh¨EFf`ƒF`}#F]äXFZkFFXZFU.AFPÔzFMÛçFNaýFG„iFDŽxF>~F;ÛxF7¬éF7F2¡ÈFu¥F!;ÛF!‚uF$ô”F(ïeF*ÚâF//»F0\yF5é8F7½½F;¤F>–FAÅtFEæFJwcFMVFQÎ5FRàXFYâ¡F_ßF_ Fa¾½Fiq;Fk»Fo79FtmÚFxiFF|aTF€š;FƒoF„ºaF…©ùFˆ›áF‡ðUFŒ¸¦FŽ(FîF‘ÿNF’ÞâF“¹•F”÷¨F—ÉÎF˜F™|†F—Ú*FšrTFœÏÜFšßÌFœ‘ Fœ?¾F›9+Fœy-Fœ!3FœAF›áTF™nVF˜êZF™‘2F—à˜F–ÞjF•ˆÜF”x F•PÙF’êÄF’FíFŽyFo±FŒÊFŠzœF‰]ÙF‡„#F†)ÇF„á±F‚Ã4F€Ä—F}ÈFFzZ]FxaJFt}2Fp¸SFn™ÉFgu(Ff¶ZFcŽFdDF\!ôFZ;óFT¾)FRzFP—FMs>FI3FEÚÙFD1µF@KôF9FF8,F4ØtF5ïF/Ó-F žF±ŸF ’-F"ìF&oßF,7íF+x F0‡yF0+5F3ˆ7F9\ÔF<âF=äFAïeFC×{FKÆFJ} FMá FUßFWšFYÆFaqÇFbÿzFg›¹FhØŸFm–#Fr·Ftñ¼F}AF~(yF€1×F‚ÔŠF„‹KF†CFˆñeFˆàF‹®¢FœóFûF‚ F‘‡ñF’ÃF’æŽF“ˆF•®FF–Á/F•·6F—*xF–SìF—uŽF–‚ÔF—¯?F•M™F–—ÁF–CF•@ëF”˨F’¾F’îbF’–fF’}¤FìWFôF&tF'ÜF‹XFŠ0˜FˆòÇF‰gF†Ñ?F„x FÏñF€ñ†F€äFx™9FwD©Fuª‡FvV¹Fm5¿FkòÿFf´Ff•FaÁ2F^ä5F\OFW¡õFUù%FPHFKVñFJ FHÏTFE2ãF?ø˜F?ŒyF;þWF8A³F2sÌF1«F/†F+ÀFù¦F+˜FËFTáF"´9F'ø´F*«[F,#±F/yF1åPF5CÁF7ZF:F={mFC$FE‡0FG9FKª©FNV¤FQM£FUÐFWŒúF_ìF`õäFdɉFfìãFmEFqAFrö”Fw…ÃF|ªF×ÅF€œöFƒÉ8F„rF…ÎF……©F‰ 3F‰à“FŒPmF‹÷ FçFàDF‡êF”RFtlF‘F‘£dF“زF‘þpF‘ÁÁF‘§QF’¥F‘õ‰F‘áHF>F޲^FöòFŽÀFŽ@FÙàF‹k`FŠB.FŠ FŠQF‡F†¦2FƒäFƒ„FF€ÐF¾,F|ôFuÿÁFvÙFsü!Fq Fm˜ÎFg*FgçrFaÅŒFcFjF\RKFX *FU‚àFS%!FR´RFMs‰FH*0FI½ƒFB:FB>F=“F:°ýF8,F3\F2¦øF0]F,éqF'Ó«Fà'FFñFnÀF>F!6²F"j¹F&ÃF*‰ñF-ûF/F„]ÄF„ÜðF†ÉÚFˆ;F‡Â„FˆúöF‰(òF‰LˆFŠ;tF‰æLFŠÀF‰ØÇF‰/âFˆ?šF‰NIF‰{F†²ðF…ì,F…ƒåF…ßìF„³4Fƒ|xFº F~çYF€„²F~ëæFzvXFvLÇFw0íFsþæFoá_FoÍ5FlæFi„YFd¾ÃFaõ>FaœF[‰ÝF[3\FW‹FWÚ>FRlòFM8«FO\ÝFMøFF4ˆFDÉëFBì–F>°èF=¨øF:4úF5E‚F1 F.?qF/‚¨F+JDF'Œ¦F'[ F ìÈFÙ§FñFu/Fx€FsqFÞ.F!l[F$)¼F)LF)eÑF-¿ F/½SF2@F5¦ÑF:=ÖF;N;F=»F?æ¢FC<ŸFD^FFHñDFMÃFO>=FSÂöFQØFY[,F[,•F^áôFdpuFfÂÐFg*‚Fkð>FmmºFnaYFrEFu!»Fx{ F{ª‹F.ÍF€>ŠF‚€‹F‚7ïF€üFƒ 9F„ûFƒ*>FƒîF†˜sF„´˜F…±•F„rF…›ÎF„ÞF…Z¦Fƒ›rF„ ÉF‚å·FƒË.F«sFLFøF|ïLF~^öFyP9Fxå}FwÆuFv„`Frv¾FqµîFt£*Fm9£FkÄéFlÈ)FgZFbXFFaÕOF^¥¼F[êFUJFUÈÚFSn"FQ*FMæ±FKÏ:FIœFF2FEÁ²FA)nF;£ÄF:rF5ŸùF7N­F4ìÉF1œF/¼F)x—F(­F%F$[JFêã././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588730504.0 photutils-1.3.0/photutils/isophote/tests/data/synth_highsnr_table.fits0000644000214200020070000007020000000000000025062 0ustar00lbradleySIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H ORIGIN = 'STScI-STSDAS/TABLES' / Tables version 2002-02-22 FILENAME= 'synth_highsnr_table.fits' / name of file NEXTEND = 1 / number of extensions in file END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 69 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth_highsnr.fits' END F’*ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€ÿÿÿÿÿC€~|ÿÿÿÿÃvÿÿÿÿÿÿÿÿÁÅ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F‘dA’¶{B»ÜB„>Â>“û3>)ÕbÂ> Ažý6C€ÿ=]UžC€~|=WŸcÄ¢QD Xd?‰©Ò?Y~`Á‡;'; á¢F’*F’*ÁÅ+ÁÅ+¼ÖÙ=ä%.»Ž½=ÔnF½•w·=§ÔD¾ ½Œ>­Aò =Fâ:@?µµF%ªA±[HBâBŸÝ÷>“û3>)ÂÂ=ûpAžëC€€=sZ¦C€~S=mËÄ ˆžD?‰ŽI?^¼üÁz;,a×;+ÿ/F’*F’*ÁÅ+ÁÅ+¼×P÷=ä§»ŽQD=ÔSÀ½•=³=§uð¾ ¬¶>­ . =´dö@?!azF£dAÖ§¸CÐüBÁ|Ï>“û3>)¾¶Â>ÀAžçþC€€=…ÖÝC€~(=‚a¾Ä²D'V]?‰’/?dùÁj©;QvA;PÒçF’*F’*ÁÅ+ÁÅ+¼× «=䈻)ü=ÔO'½•ˆ|=§Ã; ¨ƒ>­ _ =Ó9'@?1„ÓFµBØ!C%…Bê>“û3>)À,Â=þ­ S =@?CEOF GB'CHT¾C §Ê>“û3>)ÊjÂ>ûAžòòC€þ=¡üÝC€}È=ÎQÄ6Á@;šì;šB£F’*F’*ÁÅ+ÁÅ+¼ÕÕd=ä» =Ô_•½•}È=§Ç¤¾ ³œ>­$Ž =Þm.@?VÌ>F `B>&Crd‰C+eÅ>“û3>)ÆÖÂ=ùvAžïšC€€=²*NC€}Œ=­”?ÄO6YD^½‰?‰—’?uÁ#‹;¼Ã†;»½¥F’*F’*ÁÅ+ÁÅ+¼×ƒ=ä!»­¹ =ü™š@?lGF HÎBfÞC’¤ÏCOb¹>“û3>)ÅÐÂ=ýpAžî¥C€€=Ãû#C€}N=¾í³ÄcîXDuO?‰—.?zë’ÁØ;æM;äÓÂF’*F’*ÁÅ+ÁÅ+¼Öö=ä·»Ž@=ÔXÞ½•Z =§i¾ ¯ú>­Ð >&•·@?óãF ÷B‹6îC±vçCzøÿ>“û3>)ȆÂ>Ažñ.C€ý=ךC€} =ÒdÄzÀ˜D†Çi?‰™}?€|CÁÖƒ< É< ¯ôF’*G5ö´ÁÅ+Á:ºŸ¼Õ=ä »Œ“=Ô\ν•¼M=¨Q¾ ¯ë>­® >‰“è@?Žò­FX÷B¨È÷C×(×C˜$ >“K±=ë7éÂ=eA] C€õ=£ðC€|¼=ŸûÚÄÇÛ„DŽZO?6Wk?ƒ•kÁ¢<,ßf<+6-F’*G5ö´ÁÅ+Á:ºŸ¼—D7=ší©»\Ñî=’½Í½F¿=Y¾^dD>7¿ ?Ô=ç@?>%F´B¦Í‘CÔ¢C–Z¬>wdª=…"Â7'úA¾C€€Ô=Bn§C€~µ=A&«Å(èÉD`}">ªI?†Á²Á¶<0uÕ<.¹1G5ö´G5ö´Á:ºŸÁ:ºŸ¼,Ž="š‹»´ƒò=à‹¼·rãSC=(ÉÓÂ;Ôl@Ó'OC€=VC€Z=’uÅH¤ïDç>J½÷?ŠÁ,Z<ûÿ<Ê‹G5ö´G5ö´Á:ºŸÁ:ºŸ»‡ ï<Ģ˻‹Zì<½ -¼€2y<+µ ½…*j<ƒP+ >LDù@?¾C„Eëü;BNxdCƒ™xC:>Gêv<í»âÂ:cö@œ-;C€:<ËýC€R<Èö]ÅMU CãB·> «u?U}Á;ô";òYÓGrâ‡G’®Á?¾áÁC‘ »!ºí<ˆ§­»ñÚ<ƒ\q¼Rè< Ɔ½-xÂ<#~ç ={Ò@?ÑJEEÜ6DB*ÀNCYªyCé½>C%6<·ÙîÂ9ï@vÓ~C€€è<¬ ªC€(<ªv*ÅHÅC¥Ëâ=ÓgÛ?½öÁëm;Ø*Â;Öá3Grâ‡G’®Á?¾áÁC‘ »o£a<ý“<„WÂ8òÈ@6‚ÒC€&<‡A]C€S<†C:Å=šùCPªÃ=ŒÞG?”;xÁ‚Ì;­Ì;¬¿‰G’®G’®ÁC‘ÁC‘ »€æ€<¡gºËj<Ò¼ ‘B;°u¼¬{®;­ðB =Âý@?ý=µE¹@A’ÍB»"·B„S*>5KÇ<·BÂ7Δ?Ö¿^C€<<'ÐC€Ñ<&ªÅ2ÈC\Ë=O|¡?—Î…ÁæÚ;\Ìh;\^G’®G’®ÁC‘ÁC‘ »¾Í±;˜b:;—5T»½Yµ;‡>U»²1; =L\@@ HWE¦:ÙAªVBØÈâB™J9>*üû5z;×ZÜÂënú@@(ˆ-EŒãÕA-2ýBm)¨B'³ >?<‚ÏÂ1Ü?®©KC€€.<@¤pC€€üI`@@9b˜EËè@ô9¤B2Aû½Õ>D›ä;ŸäüÂ6Xî?WQ·C€é<ÂC€€f<6xÄ®¢AAþe<ºv>?¦üÆÁ¼;äb;˜dH ª3H?QÁN5ÁNåv9ζ¾;=rUº³a;5º¨ù¡;øÿ»ÍÁ&;-7 > m³@@KìtEiÑ@™xAÔf4A–0^>AN‚;Y?Â3î?Õ C€ý;Á¸3C€ù;Áù²Ä‰¹A´#<¨'×?«›Áëz:¨w:¨0÷H ª3H9´ÁN5ÁSI%¸£nž:ùë7¸¨çs:ùŽf:½Í:ç1‹;M":çô =¸ó´@@`PæESÌÇ@¡:äAþíUA´B»>AÌr;‹›ýÂ8¶Å?90 C€€š< 8HC€ù< bkÄWÿ§A’ž<­/R4-@@v¿dEABû@'šõAŠø’ADˆÞ>FŸ…;BÂ4¼Z>¿µ:C€»; ¹ C€ý; ’YÄ=ÑAÔ F÷D:Ö¬WÂ18>ŠjsC€€;€ ïC€€;€ZÄ&‘-@Ù<&Õq?·¯§Á â¯:<ª‡:>G²b; 9Â3î¦>°XjC€ü;¶ ÁC€€ ;¶rÄ…ô@£5 <"Šï?¼‘Á Î:gÚg:gÃNHtDKH…”ÁWí4ÁYjŽ;E9ï»:£ôx¹À¨o:¥0Ø7Y¿:”§;'±:•Ì´2?Q÷ @@¤5çE–—?Šæ A:@@ºÿ|>I­:”{‹Â35ô>=E*C€ò;W2C€>;WnÃä ï@HI:ŒXàÂ2ë£>4bÊC€€T;_²ÖC€€H;`2ö×@ \Ë;Ć?ÅLÁn'9ýX 9ýEH“¸5H e˜Á[;Á\©9Sa9µxm:)2¹kK:'c[¹5:Í:²Ã:Œö =<‘@@ƱÞDæ7?%‰M@­>@tÔß>Jdù:R’*Â49j>ˆC€€¢;8­†C€º;8ž¶Ã›V‘?¡x9;… i?ÊÁ›ã9Ǫ]9È hH¡çH´^Á\ºÁ^³ ayº2y¾9÷ýz8:âº9ö´¶¸3Ã9溂+¿9äç # <$YH@@ÚuDÍýÚ>É0@^ø@ ]>I¸": àùÂ3¾Ù=¶” C€€+; ~aC€½; ‰üÃ}æ–?<>§;=Í?ÎíSÁ­v9‡Ê9‡­BH³ÌHÆgãÁ^žÁ`Zúw‘ºi›9¦©M¹=)ö9£ÄÆ·œ¯¿9¡N†9ör¹9žŒ“' ;ÒÃ}@@ðk´D¸{g>_$á?ÿ¤Å?´Äq>Jë¤9©)#Â4’°=TOC€å:²ü¤C€ù:²Ö:ÃRn±?ïÃ;Ê?ÓêµÀý†9(.è9'üƒHÂÖùHÛ´=Á` QÁb q‹±9ª¾9Kà 7ƒš–9L{À¸ÞÚI9@|,¹Ê€ƒ9A$ * ‰¤„@%?ébl>Kee9æ/sÂ3“¢=óC€€;¨ÃC€×;¾Ã./>ËÖ;y?ÙåÀù™,9h7Û9hÿHÝÎÞHõzÀÁbJÔÁd Ƴݹ¤˜9‰Fž7à"9Š<ú9ŒŠÔ9ðç¹ÿ–Y9‚ç. ;~Ç@At]D’ÀŠ>9î?깚?¥ùÄ>K9¯UÎÂ3à’=\ºùC€é:à¿sC€µ:àÉ–Ã °Ø>„Na:ï X?ÞB Àõ’î9/÷$90óHðËKI(äÁc¸Áex%Ó¹o{9P 39=A9Oä½µ,N]9Iȵ9®¹v9I¹ 3 ;˜K@A D‚¬X>°Ÿ? s?‰7C>KF¶9—$Â4ó==¡ÀC€ð:Ô× C€€:ÔÕÒÂè€>:•Ñ:ÍÇ+?ãž©ÀñŠÉ94e9ðŸI¥IFáÁeECÁg÷ý=8q*98зãÈô98þú67Q947š9”°Z94w·8:ˆQq@A0Dhh~=Ñêã?’®?Nœ>K°:9tÀøÂ4C*=iàC€€:¾^C€€:¾L9½¨û=ö$¾:¦Æ?éÈÀíw8ú›ž8ûdYI¬YIÍåÁfÊÁh‡e/y86ì9q÷¸çœ‘9“ù¸ƒ»u9Õj8šmR9ß> ;:̪@AA™šDNXy=¦´ ?sz?+Õs>KÅû9OýÂ42=ÌïC€€ :±¶ÎC€ÿ:±°qÂ™ÈÆ=© :Œ´+?îºÉÀéU8àmÍ8àÌI÷¯I/9ÁhS¨Áj8°mѸC9ZV·“Ø8ÿö²¸ŸªÀ8ø19{̈8÷}D :öLÀ@ATõÃD7z=kŽI?3>ýJ|>KÖ9.&@Â4)s<ØžžC€€5:¢é>C€ë:¢ßÂyBb=P¿Û:Ve?ô|}Àå-Õ8²“¯8²ì“I,) I=PÁiî”Ák™h½)¹‘­8ÒÀÒ·ÞÀà8Ôk®¸"û8Ê]9{`Ì8˯„J :oÇ©@AjAŠD"b¬= x>áä÷>Ÿ»A>Lõ8×E’Â47<†ÌxC€÷:^E_C€ü:^C ÂIÓ<íÅV:_f?úaºÀá18p¤‚8r ’IÔË>7›ì>LMÍ8¬:Â4ƒ§›>lS€>LD¼8ºÙfÂ4á1%¡=ú†>L¥8~û Â4ÿ<.C€Ù:/0 C€ü:/.•ÁÏÄt<ó¸9¦Gß@w«ÀÔƒÅ7ëï[7êì·IqÁtI…j¨ÁoÔXÁq‹Cµ±8—mx8€·„g8Q¶¸†Z82¸¸¬8uqm :R³@A«|³CÉ`E<%×o> “L=ã>L‰8Q‹ºÂ4:<èC€ü:oÃC€€:lBÁ§?#jD<úa¬@ µÀÀÐg-7æbù7ãÚãI„5{Iœ^ÁqbÓÁrñ’©5XO8è·Z9ÿ8ü¶©{u7ûNf¸±³7üvhx 9×lm@A¼¢ÅC³2x< ˜B>ÊH=ËY™>L…s8?SÂ4Š;»»ÄC€õ9ùèÒC€ô9ùèCÁ„Û,>Ô;C<ÌyZ@ ÙÀÌYQ7Ý 7ÙêJIIœµ%ÁrÂ~ÁtVÖ{Õ5àÁ7º]¶A*#7º©·R67ºHU6£M7ºnð„ :bX@AÏÙCŸâK:žu¤<¨§€LŸþ8òÂ3þÂ;³‹C€Ü:íC€ß:ž+ÁKe>”r”<º×@nrÀÈc6¥6†lI›ßI©*tÁt'Áu« -¶¢ p7²V¸7wé‘7²®ã6¾¥7®Db¸`67®ñú‘ 9–Å@Aä?ÕCä:„$Å<“GVL©7ÛiäÂ4±;ˆbeC€€9ÜaëC€ô9Üa]Á#Êæ>Jó§<žš@ê Àćs6uÝ6…œøI¨I¸[ÎÁuŒrÁw)‚ý ·Qâ/7‡”ü¶#ðœ7ˆ=6lm7ƒ’ƒ¸Ké‡7„êŸ 9Q:¸@AûC€‡;ÇW=:Òö=Å>L«7´ÖßÂ3ý³;`ðæC€ó9ÇÚ/C€ò9ÇÚßÁ Ú> Di<‰@{ ÀÀÍY70P˜7)±ÀI¶XÆIÇãdÁvø¶Áx‘- ¯ µÔš7`(k¶ͧ7a"ï6ˆ¯ê7[›]¸dO7\Üf¯ 9T¾@B BCgô¡;#Å=GÂñ= @±>Lª7‹ò^Â4;-?C€€9©í–C€û9©ì‰ÀÆcñ=¬­<^@";À½;~7F7@žÝIÅëÝIÙ"ôÁxe3ÁzB »¡·ªÄæ7*å-·3y7, ë¶™0ð7*n7¬7+±üÀ 7ù¤Û@BæbCRi°>L°Ã7_ï‡Â3þË; m½C€€9•ÎÝC€€ 9•Ï!ÀšYŒ=€ËpL¼7;ųÂ3ý—:é•5C€ü9Š!C€ý9Š!”Àpã°=ï°<(çö@"´£À¶©Í6œïø6¡|IéÎJqÁ{L¼­7(yÂ3ý]:¶ÖûC€þ9n«C€ÿ9n›À7ö<ÒØ7<´3@&¡ À³³6ïüÌ6ïEˆIý™âJ .Á|³ŸÁ~n$«Õ6ŒªÍ6¶P:¶J¨6·B=6žbm6³” ·m°6´  8\A@BJ-ÈC#o:€+Ù<¾>c<†…Ê>L¾è6ÞÕŸÂ4»:Š ñC€ö9F_C€ó9F^ÎÀBD<¬<Cµ@*¥¨À°ú6ÛE´6Ù<J aŠJz˜Á~8IÁ€f ]5Ÿ—ˆ6Š|`5­‹36‹»µêË6‰*û·~Ñ6‰Áý 7ÿ¡Ö@B^eCÊ$:~ç¤<ÆX£<Œ@>L¿Ý6¶ÑãÂ3ÿ:cQOC€€92øïC€ù92ù ¿Ù&¿>LÂã6˜ñÂ4°:>¹C€ü9$êC€û9$é忤×<[¢;ÈGM@2ùçÀ¬Eå3“nˆ³“nˆJ&.J9€GÁ€³2Á§Ä$Ë-ñ¶¿-ƒ6=šÐ´V¡ö6=ÝæµŠ Ë6=Sj¶÷Ù6=­KV 7¾f:@B†ŒÏC~>LÃá6pŸÂ3ÿÒ:¡›C€þ9]¬C€€9]¶¿w’”;¤®z;ªI@7J¾ÀªK¾2áϲáÏJ6Š„JM8{ÁƒÿÁ‚ˆg,_7©6²çs6ö%¶|àj6?ùµT¿g6ã¤4ܰ6={x 7pé@@B”JBÿâh:„n<í8Ù<§½È>LÄð6RÂ3ÿò:¾MC€€9òñC€€9òó¿;é;YêJ;•Ì@;¶9À¨¥7»}7 JI¬}Jc†ÜÁ‚a£ÁƒmÖ5·C)3“-(6Ð^4§+Û6CÀ4di66É4a3°6IF 7à–d@B¢Î8Bõ ô9Õ}LÆY6ôôÂ4e9Æ GC€ÿ8äqC€€8äP¿ |;˸;ƒR@@<ýÀ§•6<·6a³*J_ÏçJ~µÁƒI<Á„cA Qi³ ^5Æ/EµŠ¬5Æ{4†e$5ÂÆ¶Á¿â5Ã=éÇ 7@N°@B³ Bì˜>LÇ76 Î Â3ÿÒ9«–+C€ÿ8Ùj¼C€ü8Ùj˾уÃ:·õ‹;`Æ@@Dß²À¥Ä€3Ôxw³ÔxwJy‰-JŽetÁ„;'Á…aN¿biµµ¦5«°µÈµ—5¬µ®>5«Á5¯§b5«­jô 7cR@BÄþ¦BäÍÚ>LÈ5ÜzhÂ4"9‰BýC€€8¿QÊC€ÿ8¿QÁ¾š…œ:hÙ;@áå@IŸÀ¤¬º4yLÍ´yLÍJ‹ÏÃJ ¬_Á…8IÁ†m‰_Uw=²†ÿã5‰ecµm¥5‰Ë#´ z5ˆº6/6Y5‰:& 6׆º@Bر·BÞÙ>LÈ¿5½,UÂ49kŠäC€€8´’C€ÿ8´’ ¾c=h:†k;%Ø@N{¦À£Â4HR´HRJc¡J¶ !Á†?Á‡ƒ'sG)5Y…5kiíµ’‘L5l-´†iå5jÜN5»)ã5k¡‡] 6 l¶@Bî]BÚ¿>LÉË5š[Â49@ £C€ÿ8¢C€€8¢¾&Ř9½åë;¿ñ@SvJÀ¢ÿi³¯u3¯uJ²)&JÏdEÁ‡SCÁˆ¥‹k®M4ž‰»5@.²µ@(Ä5@Ⳙ5@‚4Âh35@á‘™ 6H”Ë@C™BÖ">LÊc5„B¢Â49$ªC€þ8˜ÀõC€ý8˜Àð½ó‡Ý9u¾[;)À@XªÀ¢_d4*F´*FJÊáwJíÅzÁˆt-Á‰Õ ¨¹Òý2û)û5$£OµŠHõ5% 1Œ[¯5$/ƒµŸ`!5$¼Ü 6Ø@C5ÂBÓt>LÊ£5^f7Â3ÿÿ9 hC€ÿ8AÐC€þ8Aн¯øy9Få:݇Þ@]È…À¡Ý4ÐÓ´ÐÓJèQKö€Á‰¡tÁ‹ÉÌ1ÿ5³…ǧ5 –µ_Z5 ½4„³»5 uµ¨ò5 †c% 68½‰@C¡‰BЊÆ9ÄgÒ<3<6–ß>LË5BPçÂ3ÿÿ8ñ¸àC€€8‡ºQC€ÿ8‡ºQ½}Tæ8ž~®: *@c!À¡sþ6Þ$6…L]K·=K¨NÁŠÚoÁŒV¼÷4Ñ´sÓø4ñÚ©µ9Ø]4òÄ´¤4ñ@µqå.4ò@ u 5Ç8à@C.~}BΔz9FIû<¾Ú;Ác*>LË5#³äÂ3ÿÿ8Ë¿C€þ8{ÍC€ý8{ν5ƒ8•G’:Ó@h›¸À¡ê6¶ç5úÂöK±ºK8´ˆÁŒ|Á¨À*Óuµ³%-4Ëøµ`:4Ì¡ã2š.á4˶05~94ÌsÆÎ 5·@í@C?ñŠBÍ m9ñÇ<;ðr<ä£>LË~5ÞyÂ3ÿÿ8²þC€ý8s4=C€þ8s4>½§8dB‰:ã @n7¢À Ý96256)“K3öKWø¶ÁnåÁ}i³Ä4V™4³\ˆ´ ðÝ4´³¶Ox4²âKµMˆð4³š&/ 4Ǭe@CS#KBËÔ®>LË”4ÿöÇÂ3ÿÿ8Ÿ6}C€þ8mù?C€ÿ8mù?¼µÂ[@sö-À ¨À³„33„3KROLK}¬XÁŽÉkÁjCµ¹#4Bje4Ÿ~,´š_ 4 ©³NO¤4Ÿuü2%„E4 þš 5.†„@Ch@lBÊäõ>LËÆ4ãŸÂ3ÿÿ8C˜C€ÿ8h=ŸC€þ8h=Ÿ¼~Ór7¸@i:¹º@yØ-À Ì4û´ûKvÊÙK•”hÁ-Á‘Ù—–)´”'£4y®´4úa³‰É4hú´c‚›4÷o 3·òJ@CzBÊ,:{<ÝR<œHT>LÌ4ÏœoÂ3ÿÿ8ãC€ÿ8i†“C€þ8i†”¼1bN7¥ð­:ï{ø@Þ~À `6¼+œ6®Ç¡K‘dmK°óPÁ‘™ÝÁ“N§€Ñ! ´ˆùP4O|´“ÉÚ4Ó'³€…u4:Ž´èÒ4ÊË‘ 4yNÖ@CŒƒ#BÉžm>LÌ4¼ÎÂ3ÿõ8pmC€þ8kÛ»C€ÿ8kÛ¿»õŒH7€N;ħ@ƒÀ G´²ÖZ’2ÖZ’K«éLÌ5¡cÂ3ÿõ9c C€þ8ðÉFC€ÿ8ðÉQ»¨=@†-ÌÀ 5³† 3† KÁºKÓ›(Á”ßÁ”Ü[rŸÑc²¡«s4óˆ ´»×Ú4´úÀ´}v]4΢aµ›?4ÉÔî,‘5eß\@././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588730504.0 photutils-1.3.0/photutils/isophote/tests/data/synth_lowsnr_table.fits0000644000214200020070000007020000000000000024744 0ustar00lbradleySIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H ORIGIN = 'STScI-STSDAS/TABLES' / Tables version 2002-02-22 FILENAME= 'synth_lowsnr_table.fits' / name of file NEXTEND = 1 / number of extensions in file END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 55 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth_lowsnr.fits' END FGŸÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€}àÿÿÿÿC€}rÿÿÿÿÃX‘ðÿÿÿÿÿÿÿÿÁ¼Cÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F„MA’§àBºó™—>2EžÂH¦ A¡ÔŽC€}à=nMC€}r=adÃñwlD Ε?“)[?Y~`Á…ø;/; ØQFGŸFGŸÁ¼CÁ¼C;†mh=èdž=c&>¾)>¤b¾£ +>»Êƒ >_¨@?µµFÌA³"ABäZB¡x>˜ð7>1G@ÂF A¡~HC€~=¡|C€}G=vÝ€Äÿ¨DŽ)?‘˜:?^¼üÁy‹;.,C;-¹kFGŸFGŸÁ¼CÁ¼C»žAÒ=ç`j=jÕ>d½î¡> l¿¾¤ $>» >òw@?!azFŸ¾AÙ`0C ŒæBÃð™>˜ x>0ëxÂEñëA¡òC€~!=Ž—C€}E=‡R©ÄwD)rç?÷å?dùÁj7;Tî;S€RFGŸFGŸÁ¼CÁ¼C»Ò‚‹=çC‡=LK9=ú2Y½î!>Î ¾£íU>¹þ =“Hi@?1„ÓF„B²VC'á„Bíkb>—†Ä>0?uÂE]UA¡ÉÓC€~3=›^C€}0=”IGÄ%¥ÝD:¥?9~?iœÁWƒ;Æ;€£ëFGŸFGŸÁ¼CÁ¼C¼ \—=æåå=4P=ó]¬½çù>¾Ü¾£Î¦>¸çn >ƒò@?CEOF KëB×:CKÁüC>–Ù>0¾ÂDù1A¢*rC€~M=ª[C€}2=¢ÍoÄ7]/DM÷¦?ÇT?o>6Á@¡;Œð;œà„FGŸFGŸÁ¼CÁ¼C¼"¶P=æîµ=ù(=í8ҽ䫽>‡*¾£ÐK>¸W¶ >¥éó@?VÌ>F jáBAs~CvšPC._ß>–Ù>/UÆÂD…9A¡‹ÿC€~m=º‘¦C€}=²‚ëÄJˆ’Db¢7?;.?uÁ$à;¿÷Ã;¾õ'FGŸFGŸÁ¼CÁ¼C¼@ž=æÏo=ج=è½ßó=þâA¾£°¿>·' ?Ÿu@?lGF VYBjöùC•ÂàCSË[>–8>/^ÂD?A¢HÌC€~„=ÌÒ`C€}=ÄEUÄ`[Dz8 ?Žð?zë’Áˆ;ëj;é˜ÜFGŸFGŸÁ¼CÁ¼C¼S¸E=æñ.<ßû =äàç½ÛÄ=ùÖí¾£Åú>·9û ?7/@?óãF ŒBŽ[LCµxEC€Q‹>–8>/rÂD$A¢’C€~ž=à÷ÍC€|þ=ׇÄw8DD‰ÏÈ?Ž´Å?€|CÁØù<æØ<¾eFGŸG6 Á¼CÁ:¼x¼`Tõ=æïy<Ǹ˜=âᱽܼ~=úz“¾£Œ>¶­- >aMY@?Žò­FtmB¬ÒÿCÜNúC›È>•‚Ê=÷ÒiÂD‹AfšC€~³=®ð~C€|í=§¤¶ÄÁ/D“Æ?C 8?ƒ•kÁ¥‘<0â 0á¡ ?€Î@?>%F1‡B²–šCã¨C ú1>y¹þ=ŒûCÂ;óAd¬C€€®=OZ´C€=KädÅ*<DqÀ >µÅæ?†Á²Áy<<¾r<:ÄQG6 G6 Á:¼xÁ:¼x¼k07=- )»j—=%4.¼ÿW&SòJ=3'-Â? @ßz C€€þ= ì(C€û= D<ÅIHD*Â>XVw?ŠÁ-¶<£¨<BîG6 G6 Á:¼xÁ:¼x»þ _<Ðßjº?`Z<È_ ¼™à<6i齌ÿ%<=? =ö´R@?¾C„EëóBXH÷C‰ÚîCBôé>Hø\<ø«²Â;ª @¢˜äC€=<ÔÀ\C€€ <Ò$-ÅM]ÐCî)Ý>q?U}Ág;ÿ²ë;ýà«GrÙ¢G’"Á?¾>ÁC »Æóœ<Ž¶Ò¸¶Gj<‰̼Y[[<³ ½7*6<*ÈŒ >Z@ÿ@?ÑJEEÜ0îB.[C^BÆC)‚>Eÿ<»˜RÂ9d(@y©C€€â<¯·vC€á<®<"ÅHt©Cª7½=Ùb%?½öÁë;ÜÉÇ;Ûe«GrÙ¢G’"Á?¾>ÁC »íæ?€“<†ÄÂ6ã€@7ͧC€<‰àC€€ <‰EúÅ=—C\?ž=•"ô?”;xÁƒ;°è ;°­G’"G’"ÁCÁC ¼óí<E¹€ò<=£»ø¼];±¬%¼¬·û;¯ññ =”¯@?ý=µE¹×A©„B×’B˜nr>7&<<-Ï×Â49?÷RC€,¹ö<§œÂ/ýv@<9ÜC€€<šUC€n<›-LÅ¥C¨=€½?›w¤Á ÿ;ª#;©TjG’"GºëËÁCÁG<´ ¼yUÖ;çyºò2–;âÇ/»$éÓ;•˜<…Ëe;—­œ @èh¨@@5úE˜—A:Y»Bv„ÀB.P >4-<2Â5Þ‰?¼†¿C€~Õ<0—(C€€º<0:’ÄÑäžB¦åT=KŽÁ?Ÿ7[ÁŒ ;)ñ;)7GâëÕHgpÁJ›ÁM ºÅrX;“%ºFà'; c» ¤½; ª„ìø›@@(ˆ-EŒ©‹ACgðB…É B=3x>=ÂV<R€Â-‚Õ?ÈéC€À<[îC€€û<][ŠÄ¬yKByñ=8Ü?£7Á";AP@;@ÏYGâëÕHgpÁJ›ÁM »aûÎ;¥-:Á¢;£Ú®»-q¥;¢½Éº—=;¡©N ?P1m@@9b˜EÊìA±XBa€aBt!>D—;ËéÂ4?ˆ´ÔC€€{<(iC€b<(4Ä®3ÿBô&<àÅá?¦üÆÁ¼k;%×È;%oJH ‘‡H»ÁNÁNáºÊ• ;oàÄ:¹ß[;eEª»iûH;D˻ݲý;:ÿ™ =ðÏÄ@@KìtEiöy@¬™4BrçA·|>B,>;„ÅÂ3‘ã?-.C€€ˆ;ëùŸC€;ì9ĉ3–B kz=£?«›Áî@:Í%l:ÌôØH ‘‡H9gÁNÁS¡%:=ÅK;9·;}ª;2ï9át—;á};q;¼~ >[Ìñ@@`PæET;^A•ŸBf0rB"Ä´>D½ß;ü_¬Â:¤«?¥xnC€äît;>fºH,ÊîH9gÁQé»ÁS¡!%;VîB;ÌÝ;›;‹Ô›9"y;‹2<;ZY¾;‰6 >t2@@v¿dEA†-@ºžûB¼ìAÚÕ!>FÄü;¢.zÂ7Ÿ¥?SC€€w<1^ÒC€‚‚<0‰±Ä?Ò AªQ<ãM»?³\pÁ ¢[;!,;æ.HEÑVHP=^ÁTC:ÁU')-;²ž;&*;w\-;#[ë;»Ör; ëYºÐÀÌ; LH ={iÄ@@‡¶E.¹ò@ª­íBÐ AÑ é>D"è;˜×ëÂ3( ?G@lC€€²<6-íC€‚‰<6cÄ(Ý…A…Ãb<ÊÉ?·¯§Á Ûð;Ýâ;¥HEÑVHnéÁTC:ÁW{Í)9;Í€z;m&;%yj;];ú¨:×{Á9ººÁ:Õ  <1”ü@@•HFE¼„@‘ÁBC A¼uô>C%X;•øfÂ6ì?Ht±C€€(JHc;©îŽÂ5ö?XTC€}ßO’i;–@Â4å?;ü}C€{<PZ¢;PpýÂ5¥?QEC€|´<8uC€~V<7ªÜÓz@õúÀ<ÈÄ%?ÊÁ©ª:Èi…:ÈÐHŸÇH´B×Á\…”Á^°u_yºÞ?:î˜;|f;:ìŸ[ºû:âλ-G:à!# <§*`@@ÚuDÍ-µ@WV´AíºF’;œÜÂ3ÈÔ?K¦C€€É<–µC€‚)<–À;Ãw@@Õåc<Ýw'?ÎíSÁ›ß;û;ºkH³e`HÆpÁ^›Á`[±w‘»=&ý;&Û²<!6;$Ú»{ú`;ý]»w‰;¥ ' =µ@@ðk´D¶‹n@6n\AÓy{A•ˆû>:‡a;“Ÿ£Â4/ì?J=:C€-<›B–C€ƒŠ<›9lÃKpŸ@È•j<ügá?ÓêµÀý(; ’; „HŃvHÛ®ÓÁ`FîÁb ±9 F;Ø<$ä;åÊ»˜; j³;ið1; !Þ+ =Ä0@A;=D¤ #@j‚[BkAÈöÚ>>Ú!;ʰËÂ9}V?‡Ž¥C€}<ë  C€~ˆ<éõiÃ+ÆZ@Ðw˜=W|?ÙåÀùqo;FöÙ;FbòHݼ¬Hõ-ÁbIgÁd8³Ý;‹,;n^;Ðê;lx¿:z6»;lå®:ë·K;km„/ >’<´@At]D‰ånA<&B¸ÄÅB‚¦²=ÚŸ;Ò};Âeù'?êBC€€= C€€<ùL&à È'AOâ0=Áô?ÞB ÀóiQ<î8<įHû´ÊIQÙÁd},Áe}|ç»Æ ò;b®ã;Ò";_»/dË;^&:öêÓ;] 62B~O@A Dtzv@øß½B¨÷jBnôe=ÚŸ;éãfÂeù'@,ÀC€€=‡¥C€€=¥HÂݺàA%åˆ=¿‰ƒ?ãž©Àï9Z< /< ê7I ËIk×Áf7ãÁgZ=»*°y;{BÉ;§ˆr;~'í;Æ‹¢;qD;Õ‘ë;s²;2B{—@A0De†@No‰B¬TAË/=>>¶E<B[Â.‘Ú?°ÃC€‰Û=KØÄC€vÙ=M\¥Â«ˆæ@“ÐÁ=\™á?éÈÀí;z)û;yN©I_IôÁfö»Áh‹5y;ž Õ;œ&<;œÔżû;‡€Ã<~4;‰´ > ?*$ú@AA™šDRÙm@Ih·B·¦AÎT>Z ;óÈÂ7¼x?ˆC€‚h=RbC€pÎ=Q& ‰@aô =4(Ü?îºÉÀê$;…;„€&I{I/NÃÁh)cÁj?eÑ»ÕM!;”K©;Àp¢;”œü8³';’•Ò;¡‰Ó;“3FC >’ÓÐ@ATõÃD8á’@7{jB ‚~AÅKì>O9;òQ]Â5]Ú?”àGC€=c«@C€zý=c2ˆÂ‡H@L>=A?ô|}Àå„&;Š/é;‰¨.I+ĸI=ÕwÁiäcÁk¡¹);ž×¿;”(º{';”Ñ’O9<+ÏzÂAÔF?Ô¯rC€À=³¾æC€‰=°*âÂFïQ@=Ô=tGò?úaºÀá û;µ™~;´«¡II?;<YÂ3±2?´Ÿ~C€‰=¡ØXC€‚ =¡êøÂÍ”@åx=i”ä@5®Àܱ;”çø;”GSINxGI`x]Ám³ÁnŠn•%;çU8;®Ú»@ëZ;®`×¼:¯_;©X7»šŒc;© iZ >OÓœ@A¹’D?ò@87ÆB!0ÍAãõ>>UPÌXG<8ÉÂCš ?Ú—'C€Ž²>õtC€y,=û·ÁÐá¤?éW_=Žý@w«ÀÔÓ;Ä.Š;Ã8IqCNI…«»ÁoËEÁq“º­±<4÷;ãÜ<&ã˜;ä÷V<`3;â(»a;âžl ?2þ@A«|³CÅ—‚@Û§BM·AÎç>.ò~<†.àÂCš @@= C€u%>HxTC€yµ>D ‹Á~ ø@,·x>. l@ µÀÀϾq;Ól;Ò/‡I…ÕÿIæöÁq™8Árú…­©<¶Þ<(q;WôE<_ˆ;˜Xw< K¼?4M<U¸z ?ÜfE@A¼¢ÅC±“@ 2ñBl’AÉk>2Ó)e±C€oO>)8ºÁŠ ã@á=òýM@ ÙÀËï;;Û*®;ÙÖPI‘,IœÿäÁsÍÁt_¥Õ<ÕÆ;ú‹:;ø:ª:c$;ö.þ¼|Ê;ù~™† ? ¹ø@AÏÙC¢ •@ 7,BfAÍ|>Zf<]*ÇÂ%pÝ@¥ÆC€_à>IlC€z>N2ÁZ’·?øZ—>p«@nrÀÈÚ;ì%Æ;ê™ I™ø6I©ŒÕÁtrÁuµ y-;ú´,< %=<‘þ< yš»»”p< ZÕ;”s< È2@Œz@Aä?ÕC¹n@AWB%ÓAêƒ~>Zf‚}C€z>…6Á0š*?Ê;ì>“ê@ê ÀįQ<ê<¼áI§5YI¸†8ÁuwGÁw-€ß ¼ 9¬<$¸»ïtÔ<$!g¼#PN<#ö;‚<"Êcž2AG@AûCà[?÷ÚúBAËÀu>ZçC€k·>ƒ»ÃC€Íx>†ÛÝÁØ?¹Õ_>¦í@{ ÀÁ*1<H<¸IµIÈ-Áv×ñÁx•j ¼Pâs<ïî:e¥ <”Z»¼¹A<(<_n<JÔ­2@Rp@B BCin“@ ‡8B+©“AòÄn>Zç<¿kHÂ%pÝ@a·C€Vº>èGC€µ¡>íèÑÀ¼Í?¨S>cˆ@";À½sì<(}<&‹ÕIĈ£IÙw„ÁxEéÁz ƒ¡:qÊB`)Á©%C€i<>½7¸À»ìÇ?Ý>Bîû@ßãÀ¹×/<@–<>IØ Iëç}Áyì-Á{qíU¼¶³L<;¨u69Ì=DÂTÆõ@ÐåC€Õ1?_NqC€„¼?Uó½À*t3?jâÆ>°b_@"´£À¶±u<"ßÊ@ŒS=CY'ÂD~A±C€ßZ?žiûChÄ?šÐ¿å*H?Eè…>Ý?@&¡ À²¼<0°Ë<.òºIÿ­/J ?2Á|×ÞÁ~sÙ Õ=!ûÈ= Ú3<Õ\(<ÿü¼˜G<öM3»«Ð¹<ïaÇ? kw@BJ-ÈC6r?¹²B þ¹AÅû›>r¸=×Â.b@Ôf’C€¬å?aâ¡C€R1?cG“À #ä>Ór>Cì¬@*¥¨À°(<"ÖE!€Ú<ÑèÂ'@Ú@¡œC€ÿù?F"C€Ÿ[?I ¸À ‘?â>}uò@.ÃÀ®ZŸ<*z<(Ý3Jh¼J(«kÁ€1Á€ÔBí%ù»·LìG­Ö=R1ÝÂ'@ÚAò0CfÖ?ß»”C¬-?ãÔ0¿’•‘>²J>›|&@2ùçÀ¬9’G­Ö=EÓÂRæ@û”dC€Ý?îB0C~µô?äU”¿v³>ŽÐ>”šÄ@7J¾Àªè<7’<5) J7`¢JM‘ÁŽ)Á‚Œ+,µ7©=b‚ß= Tö¼9›7<õ‹n=ab=q =±¦=HVy2?ß±@B”JBþñ,? ²B AÎW>(‘=<9‚ÂWTA ËSC~?ô29C€~?êHA¿`W9>kÿÿ>†¦ÿ@;¶9À¨nÔ<0º<.VåJNämJcõ$Á‚šqÁƒr 8-C)<Ò<æÀñ¼rŸ<âçµ=–0¾=üµ½7@,<ò ȧ ?^Ư@B¢Î8Bô’ò?ž}PBh8AÓK&>IÀT=Ø€ÕÂWTAˆoVC©¼@ž½RC‚Fí@–áȾ§×u>\¨­?(Gš@@<ýÀ¦ýŒ<5<36ÖJ`dËJ~8ÁƒOÁ„fíAOQi=»Ôj=¶0J¼2wÿ=‡IU>C¢{>«ê½z4 =œv@Ç?®[T@././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588730504.0 photutils-1.3.0/photutils/isophote/tests/data/synth_table.fits0000644000214200020070000007020000000000000023340 0ustar00lbradleySIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H ORIGIN = 'STScI-STSDAS/TABLES' / Tables version 2002-02-22 FILENAME= 'synth_table.fits' / name of file NEXTEND = 1 / number of extensions in file END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 69 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth.fits' END F’*ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€€ÿÿÿÿC€~zÿÿÿÿÃvôÿÿÿÿÿÿÿÿÁÅ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F‘^A’˜ÃBºßùB„#ø>“ü7>)Ä›Â=ýœAžì›C€€=]?€C€~z=W‹(ĨPD R$?‰0?Y~`Á‡Œ;‹; ÉF’*F’*ÁÅ+ÁÅ+¼× h=䨻‹z«=ÔV½•O.=§—Ô¾ °>­#^ =s z@?µµF%²A±q™Bâ2BŸò>“ü7>)À Â=þ AžèUC€€=sYC€~T=mÄ ŠÍD$P?‰•å?^¼üÁz€;,w®;,üF’*F’*ÁÅ+ÁÅ+¼× a=ä+»Ë =ÔSP½•Uª=§•«¾ «5>­ =Šƒf@?!azF£gAÖ´qCÙBÁˆG>“ü7>)ÊBÂ>¥AžñæC€€=…àC€~*=‚jÄ­D'Zù?‰š[?dùÁj©;Q|;PäcF’*F’*ÁÅ+ÁÅ+¼ÖSû=ä5»ŽQ¾=Ô`,½•­ÿ=§÷¾ ²!>­" =ÊŽ@?1„ÓF¿BÙC%†&Bê>“ü7>)¿Â>üAžçkC€€=“9ùC€}ú=kÒÄ+D\D87?‰G?iœÁWk;~‘s;}­PF’*F’*ÁÅ+ÁÅ+¼Öžj=䑻޲:=ÔQ뽕›ï=§ÖÀ¾ ¨Z>­ Þ =ï+¿@?CEOF G B)#CHWcC ©©>“ü7>)ÉåÂ>ïAžñC€þ=¡ýµC€}É=ÍÄ6Á@;šî2;šDoF’*F’*ÁÅ+ÁÅ+¼ÕuS=ä »_=Ô`T½•Àã=¨ æ¾ ±/>­"4 > U@?VÌ>F `B>?CrY\C+]Þ>“ü7>)·œÂ=ùÞAžàpC€€=²ÁC€}‹=­„àÄO@ÇD^·z?‰Œç?uÁ#‹;¼³š;»»¬F’*F’*ÁÅ+ÁÅ+¼×^Î=äÖ»Œë¨=ÔF(½•)=§^ü¾ £ž>­© >6™‘@?lGF HÐBfbC’¢—CO_•>“ü7>)úÂ=ÿEAžëÈC€€=ÃùC€}N=¾ë5ÄcïDuQ?‰–6?zë’ÁØ;æG‰;äÑÆF’*F’*ÁÅ+ÁÅ+¼× )=äË»•|=ÔV`½•l =§­Ð¾ ­Í>­< >;t@?óãF öýB‹6ˆC±vdCzøF>“ü7>)Ç{Â>ÔAžïLC€þ=טÏC€} =ÒÐÄz¿ûD†Æá?‰™H?€|CÁÖƒ< Ë < ¬ôF’*G5ö´ÁÅ+Á:ºŸ¼Õø=äó»´=Ô\T½•¢½=§ê»¾ ¯ô>­u >jöq@?Žò­FXãB¨ÈªC×(tC˜#Ã>“IÕ=ë7ÁÂ=dÇA]3C€ö=£ïÄC€|¼=ŸûLÄÇÝzDŽY’?6T¯?ƒ•kÁ¢<,á-<+3”F’*G5ö´ÁÅ+Á:ºŸ¼—rb=ší×»]¢k=’¼Î½Eú)=N$¾^c >4€ ?Öý@?>%F±B¦Î{CÔ£2C–[>wcA=… Â7'eAþC€€Ô=BoC€~¶=A'KÅ(éÞD`|r>ª­?†Á²Áµ<0t}<.¼¬G5ö´G5ö´Á:ºŸÁ:ºŸ¼,‹Ô="šÀ»´µI=àß¼·o5SD©=(Î*Â;Ô @Ó+IC€€ÿ=ZC€Z=–ÅH£}Dèß>JÁÆ?ŠÁ,]<þ~<ÌÖG5ö´G5ö´Á:ºŸÁ:ºŸ»‡ T<ĨW»‹N<½|¼€:6<+¸<½….Œ<ƒTÏ > Ê1@?¾C„Eëü;BNxdCƒ™xC:>Géù<íº¿Â:d@œ,ÒC€:<ËôC€R<ÈõRÅMV$CãB> ªM?U}Á;ô";òYÓGrâ‡G’®Á?¾áÁC‘ »!º<ˆ¦ñ»ñ<ƒ[¼¼Rç\< ŵ½-wÓ<#} ={Ò@?ÑJEEÜ6MB*Á,CY«”Cê…>C$ô<·Ù~Â9ï+@vÓ3C€€è<¬ >C€(<ªu¹ÅHŲC¥Ëí=ÓgB?½öÁën;Ø-®;ÖßyGrâ‡G’®Á?¾áÁC‘ »o‘™<ýi<„^Â8òÇ@6‚ÿC€&<‡A`C€S<†C=Å=šùCPªÃ=ŒÞG?”;xÁ‚Ì;­Ì;¬¿‰G’®G’®ÁC‘ÁC‘ »€æ€<¡gºËj<Ò¼ ‘B;°u¼¬{®;­ðB =Âý@?ý=µE¹@A’ÍB»"·B„S*>5K<·IÂ7Î’?Ö¿•C€<<'ÓC€Ñ<&ªÅ2ÈC\Ë=O|¡?—Î…ÁæÚ;\Ìh;\^G’®G’®ÁC‘ÁC‘ »¾Í±;˜b:;—5T»½Yµ;‡>U»²1; =L\@@ HWE¦:àAªBØÈB™Iþ>*ý;áËG’®G»+¿ÁC‘ÁGB¥ ¼W<;Ûã3ºêÀe;Ù_qºmåj;†ð<‡¡^;ˆD @u`˜@@5úE˜úŸAþôBK·«B Ì>5 ;×[ñÂçXº@@(ˆ-EŒãÕA-5ÿBm-ÇB'µô>?ñ<Â1Ûê?®§˜C€€.<@¢wC€€üC®#@@9b˜EËì@ôR1B2úAû×#>Dœ;Ÿê(Â6X§?WXŽC€é<’ C€€f<:ÈÄ®¢·Aþm¼<º|?¦üÆÁ¼;êÒ;¯çH ª3H?QÁN5ÁNåv9ο™;=xº³;5 º©:K;ÿ»ÍÃ;3 >ÝH@@KìtEiÐà@}AÔ;¬A–K>AM|;Y øÂ3 Ÿ?ÌâC€ý;Á¬?C€ù;Áí‹Ä‰ A´8<¨K?«›Áëv:¨[ :¨ ½H ª3H9´ÁN5ÁSI%¸£âš:ùÛu¸¨¢:ù~Í:½Ñ::ç˜;T§:æì& =¿Ùç@@`PæEȘ@¡'mAþÎŽA´,ø>A˱;‹¡(Â8¶ˆ?97rC€€š< =GC€ù< gmÄWÿA’ <­M?¯#KÁ 3¨:Ó—):ÓcH,ëÃH9´ÁQíÁSI!%ºy]Í;#EÕ¹ƒ‰;!C`ºK(t;àÐ;Žãf;, >Lìß@@v¿dEAC@'¢jAŠþÁAD‘>F ;M Â4¼K>¿Ã)C€º; Å±C€ý; žiÄ=ÐøA»JF÷o:Ö­àÂ18>ŠkZC€€;€!ßC€€;€žLÄ&¼@Ùf²<'Í?·¯§Á â®:<°v:<ËHEãŽHn$ÁTDÔÁW|J)9:P(:}}¸ 6ù:{µ<:cÒ(:j*óº½F´:h–H =U@@•HFE`@ qAld_A''‰>G²Æ; ;ŽÂ3îl>°[@C€ü;¶ýC€€;¶¾Ä…ô@¢ßÝ<"6?¼‘Á Ð:gÅì:gf™HtDKH…”ÁWí4ÁYjŽ;E9ìŽ:£÷E¹À·Á:¥3Ÿ7"B°:”ªö;'‹:•І2?S€@@¤5çE–œ?Š™Aðé@º—Ä>I­7:”v™Â36 >=>ÀC€ò;W*ýC€>;WfÚÃä-@HIp:ŒTFÂ2ë¥>4\ôC€€S;_«zC€€I;_þÒö×Ü@ ¾;Å Ý?ÅLÁn'9þ]ª9þmêH“¸5H e˜Á[;Á\©9Sa9´ô:)f¹×*:'^ž¹Fß:3:²ÄŸ:‡u =8ä‡@@ƱÞDæ6ú?$}ñ@¬¡@sIq>JdÂ:R”fÂ49¡>ƒ C€€¢;8¯wC€º;8 šÃ›Vk? º;„q5?ÊÁ›ã9Æßu9ÆWêH¡çH´^Á\ºÁ^³ ayº2Ë;9÷ø˜8=>¥9ö¯è¸4ç`9澺‚# 9ää3# <=º@@ÚuDÍýÕ>ÈsÛ@]K+@zm>I¸: ÜfÂ3¾É=¶Ž?C€€+; yðC€½; …ŽÃ}æP?;R0;<Þá?ÎíSÁ­u9†þ‚9‡uÌH³ÌHÆgãÁ^žÁ`Zúw‘ºLô9¦¦’¹=à9£Â·š‹9¡Mc9öN9ž‹u' ;Ú71@@ðk´D¸{i>^£_?ÿf?´[‡>Jë©9©&JÂ4’µ=TK‚C€å:²ù¢C€ù:²Ó7ÃRnÛ?ÖÌ;¼Ü?ÓêµÀý†9'ž?9'ÌNHÂÖùHÛ´=Á` QÁb q‹±9‘à9KÝ`7M°9Lxý¸Þ¨¥9@x;¹Ê9A * ‰€A@$ÛŸ?é$ï>KeW9æ26Â3“¦=‘²C€€;ª[C€×;ÀÃ.B>Ì; W?ÙåÀù™,9hÞ9gàHÝÎÞHõzÀÁbJÔÁd Ƴݹ¤5B9‰G©7mZ9Š> 9Œwÿ9òa¹ÿ˜d9‚èŸ. ;r{ö@At]D’ÀŠ>;|+?ì°1?§]&>K 9¯PMÂ3à‘=\´C€é:à¸gC€µ:à‰à °Ï>ƒ»4:î~?ÞB Àõ’î91'91žØHðËKI(äÁc¸Áex%Ó¹gÿ9PÜ9='9OÞhµp¼9IÁÖ9®¾9I²/3 ;*I±@A D‚¬X>d–?Á¨Ý?ˆð#>KF·9—ÿÂ4ó==¡C€ð:ÔÖÖC€€:ÔÕžÂè€>:Z:Í…E?ãž©ÀñŠÉ9ÎH9ЉI¥IFáÁeECÁg÷ý=8q-98Ïô·ã¹Q98þÏ67£G947b9”±94w8:‰JØ@A0Dhh„=Ù&?—!?Uº–>K°M9tÄ/Â4C&=kßC€€:¾`œC€€:¾N¾Â½© =ûCã:©“š?éÈÀíw9°ó9ú°I¬YIÍåÁfÊÁh‡e/y86-Î9s§¸çÍ'9•¥¸„©9ÕÛ8š¿Š9L> ;;k@AA™šDNXy=¤ÂÍ?p-¤?)Ôê>KÅú9PˆÂ44=ÐdC€€ :±»C€ÿ:±µ#™ÈÓ=§õ„:‹ÌH?îºÉÀéU8ݼ8ÞwI÷¯I/9ÁhS¨Áj8°mѸ™@9]í·“§:8ÿýݸŸ‰Á8ø#¿9{ß›8÷ƒŽD :ðß^@ATõÃD7{=l·E?3ü_>þ‰Ô>KÖ|9.#WÂ4)r<Ø›C€€5:¢æ…C€ë:¢ÜUÂyBz=Ph}:V 7?ô|}Àå-Õ8³V8´I,) I=PÁiî”Ák™h½)¹i8Ò½a·Þf8Ôh7¸"9-8Éþ9{Tñ8ˬŸJ :psÔ@AjAŠD"b¬=”/>×ng>˜U9>Lô8×FcÂ47<†ÌýC€÷:^F8C€ü:^CåÂIÓ<éÂ:°?úaºÀá18e„)8fë„I²Ã²>|Ϥ>LMX8¬(ÇÂ4ˆ”é´>R˜;>LD¢8ºÜÞÂ4ßYëã>þ>L¤8~óXÂ4<(êC€Ù:/*ðC€ü:/(ãÁÏÄt<uE9¶ì@w«ÀÔƒÅ8ñÑ8pIqÁtI…j¨ÁoÔXÁq‹Cµ±8—KÓ8m·:58M ¸V:8/o¸¸”£8rÂm :LI}@A«|³CÉ`F;é§þ=â<–=Ÿù6>L‰8Q”QÂ49<ípC€ü:vBC€€:rÁÁ§ ?#i°<ú`¿@ µÀÀÐg-7¢7 êI„5{Iœ^ÁqbÓÁrñ’©4謰8î·Y˜8‘¶©£¿7ûYG¸±·Ì7üMx 9Úc@A¼¢ÅC³2w;æI?=éÛN=¥\”>L…m8@Â4†;»¼ÅC€õ9ùê"C€ô9ùé”Á„Û%>Ô:™<ÌxÁ@ ÙÀÌYP7°µè7´ |IIœµ%ÁrÂ~ÁtVÖ{Õ5Ç´7º‘=¶Cc7º©í·RãF7ºHÄ6£“ž7ºo[„ : -@AÏÙCŸâJ;ì*4=û[â=±¼Ó>L 8÷Â3þÀ;³‘kC€Ü:¢’C€ß:¢ÏÁKes>”tF<ºÙ2@nrÀÈc7Ë&O7Ï8I›ßI©*tÁt'Áu« -¶ l¯7²\~7ya7²´»6¾„Ì7®K©¸_éÒ7®ù'‘ 9“ÝÙ@Aä?ÕCä>L©~7ÛqžÂ4´;ˆg5C€€9Üi¯C€ô9ÜiÁ#Êæ>JóÉ<žš6@ê Àćs´,âu4,âuI¨I¸[ÎÁuŒrÁw)‚ý ·QZA7‡š¶$l'7ˆB>6i³¨7ƒ–¦¸Lî7„†&Ÿ 9M–@AûC€‡;f¤¹=†×=>²>L«7´ÛwÂ3ý³;`ö˜C€ó9ÇßCC€ò9ÇßòÁ Ú> E<‰Ò@{ ÀÀÍY7|U7ub I¶XÆIÇãdÁvø¶Áx‘- ¯ µÏBd7`.L¶ˆˆ7a(Ö6‰Ê7[¡ý¸Xé7\㯠9HÓÖ@B BCgô¡;LË´=zÒ=1[¹>Lª7‹ó¯Â4;-žÞC€€9©ï1C€û9©î$ÀÆcñ=¬è<^@";À½;~7xY'7rò¦IÅëÝIÙ"ôÁxe3ÁzB »¡·ªÄ¨7*è·2•7,ζ™!Ô7*qO7ÀT7+µ5À 8…±@BæbCRi°>L°Ç7_îÂ3þË; lÈC€€9•Í×C€€ 9•ÎÀšYŒ=€ËoL¼7;ÆÂÂ3ý—:é–ŠC€ü9Š!ÞC€ý9Š"\Àpã°=ïò<(è<@"´£À¶©Í6¼É[6ÁfËIéÎJqÁ{L¼­7)¬Â3ý]:¶Ø{C€þ9n C€ÿ9n À7ö<Ò׿<³ß@&¡ À³³6Ê&©6ÉoƒIý™âJ .Á|³ŸÁ~n$«Õ6Œ¤S6¶Q¿¶J¡m6·CÅ6žP6³•O·m»6´¡J 8Zé@BJ-ÈC#o:Lã~<˜¯L¾è6ÞÃEÂ4¸:Š•‡C€ö9FN®C€ó9FNxÀBD<¬9<Cg@*¥¨À°ú6®ðu6¬æþJ aŠJz˜Á~8IÁ€f ]5Ÿì‡6Špõ5­SÊ6ŠöEµçó*6‰ ·ir6‰· 8kT@B^eCÊ$:{ŸÚ<ÃË.<Šrq>L¿Ý6¶Ñ†Â3ÿ:cPÚC€€92ø“C€ù92ø±¿Ù&¿>LÂã6˜òüÂ4°:>»zC€ü9$ìC€û9$ëó¿¤×<\è;ÈII@2ùçÀ¬Eå3“nˆ³“nˆJ&.J9€GÁ€³2Á§Ä$Ë-ñ¶¿R6=m´[Ç56=à„µŠÂ³6=Uö^6=¯§V 7»š@B†ŒÏC~>LÃá6pEæÂ3ÿÑ:»UC€þ9v%C€€9v/¿w’”;¤®ß;ªIø@7J¾ÀªK¾2áϲáÏJ6Š„JM8{ÁƒÿÁ‚ˆg,_7©6²ã6 ¶}Ñ6YµW¼6ül4Ý¢;6VQx 7r×@B”JBÿâh:„oÍ<íä*<¨6ë>LÄð6RÂ3ÿò:»ÏC€€9ðTC€€9ðW¿;é;Yì4;•@;¶9À¨¥7»}7 JI¬}Jc†ÜÁ‚a£ÁƒmÖ5·C)3•dX6Íß4§)C6A?4d¦P6Ɖ4YY˜6FÌ 7Þ}@B¢Î8Bõ ô9Ï‘LÆY6õAÂ4e9Æ ¦C€ÿ8äÞC€€8伿 |;Ä‚;‚ýÀ@@<ýÀ§•6z³46])©J_ÏçJ~µÁƒI<Á„cA Qi³w15Æ/¿µ‰Ä…5Æö4„65ÂÇ ¶Á´D5Ã>ÝÇ 7@y4@B³ Bì˜>LÇ76 ÏÔÂ3ÿÑ9«˜gC€ÿ8ÙmC€ü8ÙmŸ¾ÑƒÃ:·øí;`Êb@Dß²À¥Ä€3Ôxw³ÔxwJy‰-JŽetÁ„;'Á…aN¿biµRr5«ƒŠµÉj 5¬òµ®éè5« 85­Ôë5«¯ßô 7eM¿@BÄþ¦BäÍÚ>LÈ5Üw+Â4"9‰@ûC€€8¿NûC€ÿ8¿Nñ¾š…œ:hÃe;@Ïç@IŸÀ¤¬º4yLÍ´yLÍJ‹ÏÃJ ¬_Á…8IÁ†m‰_Uw=².Ž5‰bëµæ5‰È«´þ'5ˆ‡h60Ô÷5ˆúè& 6Ø| @Bر·BÞÙ>LÈ¿5½9ßÂ49k›¿C€€8´žûC€ÿ8´žø¾c=h:XL;$Ýã@N{¦À£Â4HR´HRJc¡J¶ !Á†?Á‡ƒ'sG)5Zl5kz>µ’ïº5l(‰´…ƒ5jîÆ5¹žÀ5k´] 6¦$À@Bî]BÚ¿>LÉÊ5šQ6Â49?þbC€ÿ8¡ù¿C€€8¡ù½¾&Ř9½ý¸;Ò5@SvJÀ¢ÿi³¯u3¯uJ²)&JÏdEÁ‡SCÁˆ¥‹k®M4¯d;5@!ݵ=âÛ5@Õ ³ùëè5@ ¦4Æ€œ5@Ó¨™ 6N.@C™BÖ":.ºú<Ðì <“»W>LÊa5„Â49$_ØC€þ8˜|C€ý8˜|½ó‡9zä;Þy@XªÀ¢_d6éÅQ6ÙïmJÊáwJíÅzÁˆt-Á‰Õ ¨¹Òý3Ah5$YÒµ‰dÚ5$ÖR09 5#ç_µž?^5$s§Ü 60Ø@C5ÂBÓt>LÊ£5^.­Â49 EüC€þ8‹C€ÿ8‰½¯øy9"³:ÝS6@]È…À¡Ý4ÐÓ´ÐÓJèQKö€Á‰¡tÁ‹ÉÌ1ÿ5³¼uU5 tdµK5 åé4ŒQ5 å{µ¨©ð5 dI% 6>ÙP@C¡‰BЊÆ9Ñ}O<‰¶ LË5B™æÂ48ò¯C€€8‡íNC€ÿ8‡íM½}Tæ8Ÿ¼þ:¡k¹@c!À¡sþ6‡îI6KK·=K¨NÁŠÚoÁŒV¼÷4Ñ´q}°4ò>4µ0BÜ4ó'ù´â™4ñ©¤µmð4òªžu 5Íš[@C.~}BΔz9qçÒ<&Ó;ëìý>LË5#ûÂ3ÿû8̲C€ÿ8| 7C€þ8| :½5ƒ8–n:Ô@±@h›¸À¡ê6*ŸÂ6JNK±ºK8´ˆÁŒ|Á¨À*Óuµ1‰nÊ4ÌNݵx 4Ìù2”ô^4Ìí5à¦4ÌÔ×Î 5«©§@C?ñŠBÍ m9-CI;ú—@;±1É>LË}5ÖÂ48²ózC€ý8s%ñC€þ8s%ñ½§8^#:Ý`!@n7¢À Ý95÷5åœuK3öKWø¶ÁnåÁ}i³Ä43rÑ4³Où´5z4³ön³—ˆ24²ã}µ@ï4³›L/ 4ÕP~@CS#KBËÔ®>LË’5ÈWÂ48 5tC€ÿ8ovUC€€8ovT¼µÂ[@sö-À ¨À³„33„3KROLK}¬XÁŽÉkÁjCµ¹#4c¾ó4 u‚´´ÊÐ4¡ é³Zå”4 lø³ª4¡ÿš 5eˆ@Ch@lBÊäõ>LËÆ4ä*¹Â3ÿý8ñ×C€ÿ8i\C€þ8i\¼~Ór7¯ÿ×:°Ïl@yØ-À Ì4û´ûKvÊÙK•”hÁ-Á‘Ù—–)´“E94Žà´ÁÓM4Ž¡/´³,4Ž ­´h0A4Ž›Ä 4&ll@CzBÊ,:]à<ãë< Ý×>LÌ4ОoÂ3ÿý8ºNC€ÿ8j¨ÉC€þ8j¨Ê¼1bN7ªÎµ:ö‚=@Þ~À `6Á«l6´GnK‘dmK°óPÁ‘™ÝÁ“N§€Ñ! ´š14ëj´›n04‚o±²dú~4×X´ 14‚hG‘ 4i(ÿ@CŒƒ#BÉžm>LÌ4¼ÊâÂ3ÿú8qkC€ÿ8lÙ@C€þ8lÙB»õŒH7vý;ÀD@ƒÀ G´²ÖZ’2ÖZ’K«éLÌ5üÂ3ÿú9ÄVC€ÿ8ñk„C€þ8ñk‹»¨=@†-ÌÀ 5³† 3† KÁºKÓ›(Á”ßÁ”Ü[rŸÑc³Ì#4ôµ´á:ª4µgO´lc&4Ï,³µ˜c4Ê\,‘5„›@././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588730504.0 photutils-1.3.0/photutils/isophote/tests/data/synth_table_mean.fits0000644000214200020070000023540000000000000024345 0ustar00lbradleySIMPLE = T / file does conform to FITS standard BITPIX = 16 / number of bits per data pixel NAXIS = 0 / number of data axes EXTEND = T / There may be standard extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H ORIGIN = 'STScI-STSDAS/TABLES' / Tables version 2002-02-22 FILENAME= 'synth_table.fits' / name of file NEXTEND = 3 / number of extensions in file END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 69 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth.fits' END F’*ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€€ÿÿÿÿC€~zÿÿÿÿÃvôÿÿÿÿÿÿÿÿÁÅ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F‘^A’˜ÃBºßùB„#ø>“ü7>)Ä›Â=ýœAžì›C€€=]?€C€~z=W‹(ĨPD R$?‰0?Y~`Á‡Œ;‹; ÉF’*F’*ÁÅ+ÁÅ+¼× h=䨻‹z«=ÔV½•O.=§—Ô¾ °>­#^ =s z@?µµF%²A±q™Bâ2BŸò>“ü7>)À Â=þ AžèUC€€=sYC€~T=mÄ ŠÍD$P?‰•å?^¼üÁz€;,w®;,üF’*F’*ÁÅ+ÁÅ+¼× a=ä+»Ë =ÔSP½•Uª=§•«¾ «5>­ =Šƒf@?!azF£gAÖ´qCÙBÁˆG>“ü7>)ÊBÂ>¥AžñæC€€=…àC€~*=‚jÄ­D'Zù?‰š[?dùÁj©;Q|;PäcF’*F’*ÁÅ+ÁÅ+¼ÖSû=ä5»ŽQ¾=Ô`,½•­ÿ=§÷¾ ²!>­" =ÊŽ@?1„ÓF¿BÙC%†&Bê>“ü7>)¿Â>üAžçkC€€=“9ùC€}ú=kÒÄ+D\D87?‰G?iœÁWk;~‘s;}­PF’*F’*ÁÅ+ÁÅ+¼Öžj=䑻޲:=ÔQ뽕›ï=§ÖÀ¾ ¨Z>­ Þ =ï+¿@?CEOF G B)#CHWcC ©©>“ü7>)ÉåÂ>ïAžñC€þ=¡ýµC€}É=ÍÄ6Á@;šî2;šDoF’*F’*ÁÅ+ÁÅ+¼ÕuS=ä »_=Ô`T½•Àã=¨ æ¾ ±/>­"4 > U@?VÌ>F `B>?CrY\C+]Þ>“ü7>)·œÂ=ùÞAžàpC€€=²ÁC€}‹=­„àÄO@ÇD^·z?‰Œç?uÁ#‹;¼³š;»»¬F’*F’*ÁÅ+ÁÅ+¼×^Î=äÖ»Œë¨=ÔF(½•)=§^ü¾ £ž>­© >6™‘@?lGF HÐBfbC’¢—CO_•>“ü7>)úÂ=ÿEAžëÈC€€=ÃùC€}N=¾ë5ÄcïDuQ?‰–6?zë’ÁØ;æG‰;äÑÆF’*F’*ÁÅ+ÁÅ+¼× )=äË»•|=ÔV`½•l =§­Ð¾ ­Í>­< >;t@?óãF öýB‹6ˆC±vdCzøF>“ü7>)Ç{Â>ÔAžïLC€þ=טÏC€} =ÒÐÄz¿ûD†Æá?‰™H?€|CÁÖƒ< Ë < ¬ôF’*G5ö´ÁÅ+Á:ºŸ¼Õø=äó»´=Ô\T½•¢½=§ê»¾ ¯ô>­u >jöq@?Žò­FXãB¨ÈªC×(tC˜#Ã>“IÕ=ë7ÁÂ=dÇA]3C€ö=£ïÄC€|¼=ŸûLÄÇÝzDŽY’?6T¯?ƒ•kÁ¢<,á-<+3”F’*G5ö´ÁÅ+Á:ºŸ¼—rb=ší×»]¢k=’¼Î½Eú)=N$¾^c >4€ ?Öý@?>%F±B¦Î{CÔ£2C–[>wcA=… Â7'eAþC€€Ô=BoC€~¶=A'KÅ(éÞD`|r>ª­?†Á²Áµ<0t}<.¼¬G5ö´G5ö´Á:ºŸÁ:ºŸ¼,‹Ô="šÀ»´µI=àß¼·o5SD©=(Î*Â;Ô @Ó+IC€€ÿ=ZC€Z=–ÅH£}Dèß>JÁÆ?ŠÁ,]<þ~<ÌÖG5ö´G5ö´Á:ºŸÁ:ºŸ»‡ T<ĨW»‹N<½|¼€:6<+¸<½….Œ<ƒTÏ > Ê1@?¾C„Eëü;BNxdCƒ™xC:>Géù<íº¿Â:d@œ,ÒC€:<ËôC€R<ÈõRÅMV$CãB> ªM?U}Á;ô";òYÓGrâ‡G’®Á?¾áÁC‘ »!º<ˆ¦ñ»ñ<ƒ[¼¼Rç\< ŵ½-wÓ<#} ={Ò@?ÑJEEÜ6MB*Á,CY«”Cê…>C$ô<·Ù~Â9ï+@vÓ3C€€è<¬ >C€(<ªu¹ÅHŲC¥Ëí=ÓgB?½öÁën;Ø-®;ÖßyGrâ‡G’®Á?¾áÁC‘ »o‘™<ýi<„^Â8òÇ@6‚ÿC€&<‡A`C€S<†C=Å=šùCPªÃ=ŒÞG?”;xÁ‚Ì;­Ì;¬¿‰G’®G’®ÁC‘ÁC‘ »€æ€<¡gºËj<Ò¼ ‘B;°u¼¬{®;­ðB =Âý@?ý=µE¹@A’ÍB»"·B„S*>5K<·IÂ7Î’?Ö¿•C€<<'ÓC€Ñ<&ªÅ2ÈC\Ë=O|¡?—Î…ÁæÚ;\Ìh;\^G’®G’®ÁC‘ÁC‘ »¾Í±;˜b:;—5T»½Yµ;‡>U»²1; =L\@@ HWE¦:àAªBØÈB™Iþ>*ý;áËG’®G»+¿ÁC‘ÁGB¥ ¼W<;Ûã3ºêÀe;Ù_qºmåj;†ð<‡¡^;ˆD @u`˜@@5úE˜úŸAþôBK·«B Ì>5 ;×[ñÂçXº@@(ˆ-EŒãÕA-5ÿBm-ÇB'µô>?ñ<Â1Ûê?®§˜C€€.<@¢wC€€üC®#@@9b˜EËì@ôR1B2úAû×#>Dœ;Ÿê(Â6X§?WXŽC€é<’ C€€f<:ÈÄ®¢·Aþm¼<º|?¦üÆÁ¼;êÒ;¯çH ª3H?QÁN5ÁNåv9ο™;=xº³;5 º©:K;ÿ»ÍÃ;3 >ÝH@@KìtEiÐà@}AÔ;¬A–K>AM|;Y øÂ3 Ÿ?ÌâC€ý;Á¬?C€ù;Áí‹Ä‰ A´8<¨K?«›Áëv:¨[ :¨ ½H ª3H9´ÁN5ÁSI%¸£âš:ùÛu¸¨¢:ù~Í:½Ñ::ç˜;T§:æì& =¿Ùç@@`PæEȘ@¡'mAþÎŽA´,ø>A˱;‹¡(Â8¶ˆ?97rC€€š< =GC€ù< gmÄWÿA’ <­M?¯#KÁ 3¨:Ó—):ÓcH,ëÃH9´ÁQíÁSI!%ºy]Í;#EÕ¹ƒ‰;!C`ºK(t;àÐ;Žãf;, >Lìß@@v¿dEAC@'¢jAŠþÁAD‘>F ;M Â4¼K>¿Ã)C€º; Å±C€ý; žiÄ=ÐøA»JF÷o:Ö­àÂ18>ŠkZC€€;€!ßC€€;€žLÄ&¼@Ùf²<'Í?·¯§Á â®:<°v:<ËHEãŽHn$ÁTDÔÁW|J)9:P(:}}¸ 6ù:{µ<:cÒ(:j*óº½F´:h–H =U@@•HFE`@ qAld_A''‰>G²Æ; ;ŽÂ3îl>°[@C€ü;¶ýC€€;¶¾Ä…ô@¢ßÝ<"6?¼‘Á Ð:gÅì:gf™HtDKH…”ÁWí4ÁYjŽ;E9ìŽ:£÷E¹À·Á:¥3Ÿ7"B°:”ªö;'‹:•І2?S€@@¤5çE–œ?Š™Aðé@º—Ä>I­7:”v™Â36 >=>ÀC€ò;W*ýC€>;WfÚÃä-@HIp:ŒTFÂ2ë¥>4\ôC€€S;_«zC€€I;_þÒö×Ü@ ¾;Å Ý?ÅLÁn'9þ]ª9þmêH“¸5H e˜Á[;Á\©9Sa9´ô:)f¹×*:'^ž¹Fß:3:²ÄŸ:‡u =8ä‡@@ƱÞDæ6ú?$}ñ@¬¡@sIq>JdÂ:R”fÂ49¡>ƒ C€€¢;8¯wC€º;8 šÃ›Vk? º;„q5?ÊÁ›ã9Æßu9ÆWêH¡çH´^Á\ºÁ^³ ayº2Ë;9÷ø˜8=>¥9ö¯è¸4ç`9澺‚# 9ää3# <=º@@ÚuDÍýÕ>ÈsÛ@]K+@zm>I¸: ÜfÂ3¾É=¶Ž?C€€+; yðC€½; …ŽÃ}æP?;R0;<Þá?ÎíSÁ­u9†þ‚9‡uÌH³ÌHÆgãÁ^žÁ`Zúw‘ºLô9¦¦’¹=à9£Â·š‹9¡Mc9öN9ž‹u' ;Ú71@@ðk´D¸{i>^£_?ÿf?´[‡>Jë©9©&JÂ4’µ=TK‚C€å:²ù¢C€ù:²Ó7ÃRnÛ?ÖÌ;¼Ü?ÓêµÀý†9'ž?9'ÌNHÂÖùHÛ´=Á` QÁb q‹±9‘à9KÝ`7M°9Lxý¸Þ¨¥9@x;¹Ê9A * ‰€A@$ÛŸ?é$ï>KeW9æ26Â3“¦=‘²C€€;ª[C€×;ÀÃ.B>Ì; W?ÙåÀù™,9hÞ9gàHÝÎÞHõzÀÁbJÔÁd Ƴݹ¤5B9‰G©7mZ9Š> 9Œwÿ9òa¹ÿ˜d9‚èŸ. ;r{ö@At]D’ÀŠ>;|+?ì°1?§]&>K 9¯PMÂ3à‘=\´C€é:à¸gC€µ:à‰à °Ï>ƒ»4:î~?ÞB Àõ’î91'91žØHðËKI(äÁc¸Áex%Ó¹gÿ9PÜ9='9OÞhµp¼9IÁÖ9®¾9I²/3 ;*I±@A D‚¬X>d–?Á¨Ý?ˆð#>KF·9—ÿÂ4ó==¡C€ð:ÔÖÖC€€:ÔÕžÂè€>:Z:Í…E?ãž©ÀñŠÉ9ÎH9ЉI¥IFáÁeECÁg÷ý=8q-98Ïô·ã¹Q98þÏ67£G947b9”±94w8:‰JØ@A0Dhh„=Ù&?—!?Uº–>K°M9tÄ/Â4C&=kßC€€:¾`œC€€:¾N¾Â½© =ûCã:©“š?éÈÀíw9°ó9ú°I¬YIÍåÁfÊÁh‡e/y86-Î9s§¸çÍ'9•¥¸„©9ÕÛ8š¿Š9L> ;;k@AA™šDNXy=¤ÂÍ?p-¤?)Ôê>KÅú9PˆÂ44=ÐdC€€ :±»C€ÿ:±µ#™ÈÓ=§õ„:‹ÌH?îºÉÀéU8ݼ8ÞwI÷¯I/9ÁhS¨Áj8°mѸ™@9]í·“§:8ÿýݸŸ‰Á8ø#¿9{ß›8÷ƒŽD :ðß^@ATõÃD7{=l·E?3ü_>þ‰Ô>KÖ|9.#WÂ4)r<Ø›C€€5:¢æ…C€ë:¢ÜUÂyBz=Ph}:V 7?ô|}Àå-Õ8³V8´I,) I=PÁiî”Ák™h½)¹i8Ò½a·Þf8Ôh7¸"9-8Éþ9{Tñ8ˬŸJ :psÔ@AjAŠD"b¬=”/>×ng>˜U9>Lô8×FcÂ47<†ÌýC€÷:^F8C€ü:^CåÂIÓ<éÂ:°?úaºÀá18e„)8fë„I²Ã²>|Ϥ>LMX8¬(ÇÂ4ˆ”é´>R˜;>LD¢8ºÜÞÂ4ßYëã>þ>L¤8~óXÂ4<(êC€Ù:/*ðC€ü:/(ãÁÏÄt<uE9¶ì@w«ÀÔƒÅ8ñÑ8pIqÁtI…j¨ÁoÔXÁq‹Cµ±8—KÓ8m·:58M ¸V:8/o¸¸”£8rÂm :LI}@A«|³CÉ`F;é§þ=â<–=Ÿù6>L‰8Q”QÂ49<ípC€ü:vBC€€:rÁÁ§ ?#i°<ú`¿@ µÀÀÐg-7¢7 êI„5{Iœ^ÁqbÓÁrñ’©4謰8î·Y˜8‘¶©£¿7ûYG¸±·Ì7üMx 9Úc@A¼¢ÅC³2w;æI?=éÛN=¥\”>L…m8@Â4†;»¼ÅC€õ9ùê"C€ô9ùé”Á„Û%>Ô:™<ÌxÁ@ ÙÀÌYP7°µè7´ |IIœµ%ÁrÂ~ÁtVÖ{Õ5Ç´7º‘=¶Cc7º©í·RãF7ºHÄ6£“ž7ºo[„ : -@AÏÙCŸâJ;ì*4=û[â=±¼Ó>L 8÷Â3þÀ;³‘kC€Ü:¢’C€ß:¢ÏÁKes>”tF<ºÙ2@nrÀÈc7Ë&O7Ï8I›ßI©*tÁt'Áu« -¶ l¯7²\~7ya7²´»6¾„Ì7®K©¸_éÒ7®ù'‘ 9“ÝÙ@Aä?ÕCä>L©~7ÛqžÂ4´;ˆg5C€€9Üi¯C€ô9ÜiÁ#Êæ>JóÉ<žš6@ê Àćs´,âu4,âuI¨I¸[ÎÁuŒrÁw)‚ý ·QZA7‡š¶$l'7ˆB>6i³¨7ƒ–¦¸Lî7„†&Ÿ 9M–@AûC€‡;f¤¹=†×=>²>L«7´ÛwÂ3ý³;`ö˜C€ó9ÇßCC€ò9ÇßòÁ Ú> E<‰Ò@{ ÀÀÍY7|U7ub I¶XÆIÇãdÁvø¶Áx‘- ¯ µÏBd7`.L¶ˆˆ7a(Ö6‰Ê7[¡ý¸Xé7\㯠9HÓÖ@B BCgô¡;LË´=zÒ=1[¹>Lª7‹ó¯Â4;-žÞC€€9©ï1C€û9©î$ÀÆcñ=¬è<^@";À½;~7xY'7rò¦IÅëÝIÙ"ôÁxe3ÁzB »¡·ªÄ¨7*è·2•7,ζ™!Ô7*qO7ÀT7+µ5À 8…±@BæbCRi°>L°Ç7_îÂ3þË; lÈC€€9•Í×C€€ 9•ÎÀšYŒ=€ËoL¼7;ÆÂÂ3ý—:é–ŠC€ü9Š!ÞC€ý9Š"\Àpã°=ïò<(è<@"´£À¶©Í6¼É[6ÁfËIéÎJqÁ{L¼­7)¬Â3ý]:¶Ø{C€þ9n C€ÿ9n À7ö<Ò׿<³ß@&¡ À³³6Ê&©6ÉoƒIý™âJ .Á|³ŸÁ~n$«Õ6Œ¤S6¶Q¿¶J¡m6·CÅ6žP6³•O·m»6´¡J 8Zé@BJ-ÈC#o:Lã~<˜¯L¾è6ÞÃEÂ4¸:Š•‡C€ö9FN®C€ó9FNxÀBD<¬9<Cg@*¥¨À°ú6®ðu6¬æþJ aŠJz˜Á~8IÁ€f ]5Ÿì‡6Špõ5­SÊ6ŠöEµçó*6‰ ·ir6‰· 8kT@B^eCÊ$:{ŸÚ<ÃË.<Šrq>L¿Ý6¶Ñ†Â3ÿ:cPÚC€€92ø“C€ù92ø±¿Ù&¿>LÂã6˜òüÂ4°:>»zC€ü9$ìC€û9$ëó¿¤×<\è;ÈII@2ùçÀ¬Eå3“nˆ³“nˆJ&.J9€GÁ€³2Á§Ä$Ë-ñ¶¿R6=m´[Ç56=à„µŠÂ³6=Uö^6=¯§V 7»š@B†ŒÏC~>LÃá6pEæÂ3ÿÑ:»UC€þ9v%C€€9v/¿w’”;¤®ß;ªIø@7J¾ÀªK¾2áϲáÏJ6Š„JM8{ÁƒÿÁ‚ˆg,_7©6²ã6 ¶}Ñ6YµW¼6ül4Ý¢;6VQx 7r×@B”JBÿâh:„oÍ<íä*<¨6ë>LÄð6RÂ3ÿò:»ÏC€€9ðTC€€9ðW¿;é;Yì4;•@;¶9À¨¥7»}7 JI¬}Jc†ÜÁ‚a£ÁƒmÖ5·C)3•dX6Íß4§)C6A?4d¦P6Ɖ4YY˜6FÌ 7Þ}@B¢Î8Bõ ô9Ï‘LÆY6õAÂ4e9Æ ¦C€ÿ8äÞC€€8伿 |;Ä‚;‚ýÀ@@<ýÀ§•6z³46])©J_ÏçJ~µÁƒI<Á„cA Qi³w15Æ/¿µ‰Ä…5Æö4„65ÂÇ ¶Á´D5Ã>ÝÇ 7@y4@B³ Bì˜>LÇ76 ÏÔÂ3ÿÑ9«˜gC€ÿ8ÙmC€ü8ÙmŸ¾ÑƒÃ:·øí;`Êb@Dß²À¥Ä€3Ôxw³ÔxwJy‰-JŽetÁ„;'Á…aN¿biµRr5«ƒŠµÉj 5¬òµ®éè5« 85­Ôë5«¯ßô 7eM¿@BÄþ¦BäÍÚ>LÈ5Üw+Â4"9‰@ûC€€8¿NûC€ÿ8¿Nñ¾š…œ:hÃe;@Ïç@IŸÀ¤¬º4yLÍ´yLÍJ‹ÏÃJ ¬_Á…8IÁ†m‰_Uw=².Ž5‰bëµæ5‰È«´þ'5ˆ‡h60Ô÷5ˆúè& 6Ø| @Bر·BÞÙ>LÈ¿5½9ßÂ49k›¿C€€8´žûC€ÿ8´žø¾c=h:XL;$Ýã@N{¦À£Â4HR´HRJc¡J¶ !Á†?Á‡ƒ'sG)5Zl5kz>µ’ïº5l(‰´…ƒ5jîÆ5¹žÀ5k´] 6¦$À@Bî]BÚ¿>LÉÊ5šQ6Â49?þbC€ÿ8¡ù¿C€€8¡ù½¾&Ř9½ý¸;Ò5@SvJÀ¢ÿi³¯u3¯uJ²)&JÏdEÁ‡SCÁˆ¥‹k®M4¯d;5@!ݵ=âÛ5@Õ ³ùëè5@ ¦4Æ€œ5@Ó¨™ 6N.@C™BÖ":.ºú<Ðì <“»W>LÊa5„Â49$_ØC€þ8˜|C€ý8˜|½ó‡9zä;Þy@XªÀ¢_d6éÅQ6ÙïmJÊáwJíÅzÁˆt-Á‰Õ ¨¹Òý3Ah5$YÒµ‰dÚ5$ÖR09 5#ç_µž?^5$s§Ü 60Ø@C5ÂBÓt>LÊ£5^.­Â49 EüC€þ8‹C€ÿ8‰½¯øy9"³:ÝS6@]È…À¡Ý4ÐÓ´ÐÓJèQKö€Á‰¡tÁ‹ÉÌ1ÿ5³¼uU5 tdµK5 åé4ŒQ5 å{µ¨©ð5 dI% 6>ÙP@C¡‰BЊÆ9Ñ}O<‰¶ LË5B™æÂ48ò¯C€€8‡íNC€ÿ8‡íM½}Tæ8Ÿ¼þ:¡k¹@c!À¡sþ6‡îI6KK·=K¨NÁŠÚoÁŒV¼÷4Ñ´q}°4ò>4µ0BÜ4ó'ù´â™4ñ©¤µmð4òªžu 5Íš[@C.~}BΔz9qçÒ<&Ó;ëìý>LË5#ûÂ3ÿû8̲C€ÿ8| 7C€þ8| :½5ƒ8–n:Ô@±@h›¸À¡ê6*ŸÂ6JNK±ºK8´ˆÁŒ|Á¨À*Óuµ1‰nÊ4ÌNݵx 4Ìù2”ô^4Ìí5à¦4ÌÔ×Î 5«©§@C?ñŠBÍ m9-CI;ú—@;±1É>LË}5ÖÂ48²ózC€ý8s%ñC€þ8s%ñ½§8^#:Ý`!@n7¢À Ý95÷5åœuK3öKWø¶ÁnåÁ}i³Ä43rÑ4³Où´5z4³ön³—ˆ24²ã}µ@ï4³›L/ 4ÕP~@CS#KBËÔ®>LË’5ÈWÂ48 5tC€ÿ8ovUC€€8ovT¼µÂ[@sö-À ¨À³„33„3KROLK}¬XÁŽÉkÁjCµ¹#4c¾ó4 u‚´´ÊÐ4¡ é³Zå”4 lø³ª4¡ÿš 5eˆ@Ch@lBÊäõ>LËÆ4ä*¹Â3ÿý8ñ×C€ÿ8i\C€þ8i\¼~Ór7¯ÿ×:°Ïl@yØ-À Ì4û´ûKvÊÙK•”hÁ-Á‘Ù—–)´“E94Žà´ÁÓM4Ž¡/´³,4Ž ­´h0A4Ž›Ä 4&ll@CzBÊ,:]à<ãë< Ý×>LÌ4ОoÂ3ÿý8ºNC€ÿ8j¨ÉC€þ8j¨Ê¼1bN7ªÎµ:ö‚=@Þ~À `6Á«l6´GnK‘dmK°óPÁ‘™ÝÁ“N§€Ñ! ´š14ëj´›n04‚o±²dú~4×X´ 14‚hG‘ 4i(ÿ@CŒƒ#BÉžm>LÌ4¼ÊâÂ3ÿú8qkC€ÿ8lÙ@C€þ8lÙB»õŒH7vý;ÀD@ƒÀ G´²ÖZ’2ÖZ’K«éLÌ5üÂ3ÿú9ÄVC€ÿ8ñk„C€þ8ñk‹»¨=@†-ÌÀ 5³† 3† KÁºKÓ›(Á”ßÁ”Ü[rŸÑc³Ì#4ôµ´á:ª4µgO´lc&4Ï,³µ˜c4Ê\,‘5„›@XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 69 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth.fits' END F’*ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€€ÿÿÿÿC€~zÿÿÿÿÃvôÿÿÿÿÿÿÿÿÁÅ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F‘^A’˜ÃBºßùB„#ø>“ü7>)Ä›Â=ýœAžì›C€€=]?€C€~z=W‹(ĨPD R$?‰0?Y~`Á‡Œ;‹; ÉF’*F’*ÁÅ+ÁÅ+¼× h=䨻‹z«=ÔV½•O.=§—Ô¾ °>­#^ =s z@?µµF%²A±q™Bâ2BŸò>“ü7>)À Â=þ AžèUC€€=sYC€~T=mÄ ŠÍD$P?‰•å?^¼üÁz€;,w®;,üF’*F’*ÁÅ+ÁÅ+¼× a=ä+»Ë =ÔSP½•Uª=§•«¾ «5>­ =Šƒf@?!azF£gAÖ´qCÙBÁˆG>“ü7>)ÊBÂ>¥AžñæC€€=…àC€~*=‚jÄ­D'Zù?‰š[?dùÁj©;Q|;PäcF’*F’*ÁÅ+ÁÅ+¼ÖSû=ä5»ŽQ¾=Ô`,½•­ÿ=§÷¾ ²!>­" =ÊŽ@?1„ÓF¿BÙC%†&Bê>“ü7>)¿Â>üAžçkC€€=“9ùC€}ú=kÒÄ+D\D87?‰G?iœÁWk;~‘s;}­PF’*F’*ÁÅ+ÁÅ+¼Öžj=䑻޲:=ÔQ뽕›ï=§ÖÀ¾ ¨Z>­ Þ =ï+¿@?CEOF G B)#CHWcC ©©>“ü7>)ÉåÂ>ïAžñC€þ=¡ýµC€}É=ÍÄ6Á@;šî2;šDoF’*F’*ÁÅ+ÁÅ+¼ÕuS=ä »_=Ô`T½•Àã=¨ æ¾ ±/>­"4 > U@?VÌ>F `B>?CrY\C+]Þ>“ü7>)·œÂ=ùÞAžàpC€€=²ÁC€}‹=­„àÄO@ÇD^·z?‰Œç?uÁ#‹;¼³š;»»¬F’*F’*ÁÅ+ÁÅ+¼×^Î=äÖ»Œë¨=ÔF(½•)=§^ü¾ £ž>­© >6™‘@?lGF HÐBfbC’¢—CO_•>“ü7>)úÂ=ÿEAžëÈC€€=ÃùC€}N=¾ë5ÄcïDuQ?‰–6?zë’ÁØ;æG‰;äÑÆF’*F’*ÁÅ+ÁÅ+¼× )=äË»•|=ÔV`½•l =§­Ð¾ ­Í>­< >;t@?óãF öýB‹6ˆC±vdCzøF>“ü7>)Ç{Â>ÔAžïLC€þ=טÏC€} =ÒÐÄz¿ûD†Æá?‰™H?€|CÁÖƒ< Ë < ¬ôF’*G5ö´ÁÅ+Á:ºŸ¼Õø=äó»´=Ô\T½•¢½=§ê»¾ ¯ô>­u >jöq@?Žò­FXãB¨ÈªC×(tC˜#Ã>“IÕ=ë7ÁÂ=dÇA]3C€ö=£ïÄC€|¼=ŸûLÄÇÝzDŽY’?6T¯?ƒ•kÁ¢<,á-<+3”F’*G5ö´ÁÅ+Á:ºŸ¼—rb=ší×»]¢k=’¼Î½Eú)=N$¾^c >4€ ?Öý@?>%F±B¦Î{CÔ£2C–[>wcA=… Â7'eAþC€€Ô=BoC€~¶=A'KÅ(éÞD`|r>ª­?†Á²Áµ<0t}<.¼¬G5ö´G5ö´Á:ºŸÁ:ºŸ¼,‹Ô="šÀ»´µI=àß¼·o5SD©=(Î*Â;Ô @Ó+IC€€ÿ=ZC€Z=–ÅH£}Dèß>JÁÆ?ŠÁ,]<þ~<ÌÖG5ö´G5ö´Á:ºŸÁ:ºŸ»‡ T<ĨW»‹N<½|¼€:6<+¸<½….Œ<ƒTÏ > Ê1@?¾C„Eëü;BNxdCƒ™xC:>Géù<íº¿Â:d@œ,ÒC€:<ËôC€R<ÈõRÅMV$CãB> ªM?U}Á;ô";òYÓGrâ‡G’®Á?¾áÁC‘ »!º<ˆ¦ñ»ñ<ƒ[¼¼Rç\< ŵ½-wÓ<#} ={Ò@?ÑJEEÜ6MB*Á,CY«”Cê…>C$ô<·Ù~Â9ï+@vÓ3C€€è<¬ >C€(<ªu¹ÅHŲC¥Ëí=ÓgB?½öÁën;Ø-®;ÖßyGrâ‡G’®Á?¾áÁC‘ »o‘™<ýi<„^Â8òÇ@6‚ÿC€&<‡A`C€S<†C=Å=šùCPªÃ=ŒÞG?”;xÁ‚Ì;­Ì;¬¿‰G’®G’®ÁC‘ÁC‘ »€æ€<¡gºËj<Ò¼ ‘B;°u¼¬{®;­ðB =Âý@?ý=µE¹@A’ÍB»"·B„S*>5K<·IÂ7Î’?Ö¿•C€<<'ÓC€Ñ<&ªÅ2ÈC\Ë=O|¡?—Î…ÁæÚ;\Ìh;\^G’®G’®ÁC‘ÁC‘ »¾Í±;˜b:;—5T»½Yµ;‡>U»²1; =L\@@ HWE¦:àAªBØÈB™Iþ>*ý;áËG’®G»+¿ÁC‘ÁGB¥ ¼W<;Ûã3ºêÀe;Ù_qºmåj;†ð<‡¡^;ˆD @u`˜@@5úE˜úŸAþôBK·«B Ì>5 ;×[ñÂçXº@@(ˆ-EŒãÕA-5ÿBm-ÇB'µô>?ñ<Â1Ûê?®§˜C€€.<@¢wC€€üC®#@@9b˜EËì@ôR1B2úAû×#>Dœ;Ÿê(Â6X§?WXŽC€é<’ C€€f<:ÈÄ®¢·Aþm¼<º|?¦üÆÁ¼;êÒ;¯çH ª3H?QÁN5ÁNåv9ο™;=xº³;5 º©:K;ÿ»ÍÃ;3 >ÝH@@KìtEiÐà@}AÔ;¬A–K>AM|;Y øÂ3 Ÿ?ÌâC€ý;Á¬?C€ù;Áí‹Ä‰ A´8<¨K?«›Áëv:¨[ :¨ ½H ª3H9´ÁN5ÁSI%¸£âš:ùÛu¸¨¢:ù~Í:½Ñ::ç˜;T§:æì& =¿Ùç@@`PæEȘ@¡'mAþÎŽA´,ø>A˱;‹¡(Â8¶ˆ?97rC€€š< =GC€ù< gmÄWÿA’ <­M?¯#KÁ 3¨:Ó—):ÓcH,ëÃH9´ÁQíÁSI!%ºy]Í;#EÕ¹ƒ‰;!C`ºK(t;àÐ;Žãf;, >Lìß@@v¿dEAC@'¢jAŠþÁAD‘>F ;M Â4¼K>¿Ã)C€º; Å±C€ý; žiÄ=ÐøA»JF÷o:Ö­àÂ18>ŠkZC€€;€!ßC€€;€žLÄ&¼@Ùf²<'Í?·¯§Á â®:<°v:<ËHEãŽHn$ÁTDÔÁW|J)9:P(:}}¸ 6ù:{µ<:cÒ(:j*óº½F´:h–H =U@@•HFE`@ qAld_A''‰>G²Æ; ;ŽÂ3îl>°[@C€ü;¶ýC€€;¶¾Ä…ô@¢ßÝ<"6?¼‘Á Ð:gÅì:gf™HtDKH…”ÁWí4ÁYjŽ;E9ìŽ:£÷E¹À·Á:¥3Ÿ7"B°:”ªö;'‹:•І2?S€@@¤5çE–œ?Š™Aðé@º—Ä>I­7:”v™Â36 >=>ÀC€ò;W*ýC€>;WfÚÃä-@HIp:ŒTFÂ2ë¥>4\ôC€€S;_«zC€€I;_þÒö×Ü@ ¾;Å Ý?ÅLÁn'9þ]ª9þmêH“¸5H e˜Á[;Á\©9Sa9´ô:)f¹×*:'^ž¹Fß:3:²ÄŸ:‡u =8ä‡@@ƱÞDæ6ú?$}ñ@¬¡@sIq>JdÂ:R”fÂ49¡>ƒ C€€¢;8¯wC€º;8 šÃ›Vk? º;„q5?ÊÁ›ã9Æßu9ÆWêH¡çH´^Á\ºÁ^³ ayº2Ë;9÷ø˜8=>¥9ö¯è¸4ç`9澺‚# 9ää3# <=º@@ÚuDÍýÕ>ÈsÛ@]K+@zm>I¸: ÜfÂ3¾É=¶Ž?C€€+; yðC€½; …ŽÃ}æP?;R0;<Þá?ÎíSÁ­u9†þ‚9‡uÌH³ÌHÆgãÁ^žÁ`Zúw‘ºLô9¦¦’¹=à9£Â·š‹9¡Mc9öN9ž‹u' ;Ú71@@ðk´D¸{i>^£_?ÿf?´[‡>Jë©9©&JÂ4’µ=TK‚C€å:²ù¢C€ù:²Ó7ÃRnÛ?ÖÌ;¼Ü?ÓêµÀý†9'ž?9'ÌNHÂÖùHÛ´=Á` QÁb q‹±9‘à9KÝ`7M°9Lxý¸Þ¨¥9@x;¹Ê9A * ‰€A@$ÛŸ?é$ï>KeW9æ26Â3“¦=‘²C€€;ª[C€×;ÀÃ.B>Ì; W?ÙåÀù™,9hÞ9gàHÝÎÞHõzÀÁbJÔÁd Ƴݹ¤5B9‰G©7mZ9Š> 9Œwÿ9òa¹ÿ˜d9‚èŸ. ;r{ö@At]D’ÀŠ>;|+?ì°1?§]&>K 9¯PMÂ3à‘=\´C€é:à¸gC€µ:à‰à °Ï>ƒ»4:î~?ÞB Àõ’î91'91žØHðËKI(äÁc¸Áex%Ó¹gÿ9PÜ9='9OÞhµp¼9IÁÖ9®¾9I²/3 ;*I±@A D‚¬X>d–?Á¨Ý?ˆð#>KF·9—ÿÂ4ó==¡C€ð:ÔÖÖC€€:ÔÕžÂè€>:Z:Í…E?ãž©ÀñŠÉ9ÎH9ЉI¥IFáÁeECÁg÷ý=8q-98Ïô·ã¹Q98þÏ67£G947b9”±94w8:‰JØ@A0Dhh„=Ù&?—!?Uº–>K°M9tÄ/Â4C&=kßC€€:¾`œC€€:¾N¾Â½© =ûCã:©“š?éÈÀíw9°ó9ú°I¬YIÍåÁfÊÁh‡e/y86-Î9s§¸çÍ'9•¥¸„©9ÕÛ8š¿Š9L> ;;k@AA™šDNXy=¤ÂÍ?p-¤?)Ôê>KÅú9PˆÂ44=ÐdC€€ :±»C€ÿ:±µ#™ÈÓ=§õ„:‹ÌH?îºÉÀéU8ݼ8ÞwI÷¯I/9ÁhS¨Áj8°mѸ™@9]í·“§:8ÿýݸŸ‰Á8ø#¿9{ß›8÷ƒŽD :ðß^@ATõÃD7{=l·E?3ü_>þ‰Ô>KÖ|9.#WÂ4)r<Ø›C€€5:¢æ…C€ë:¢ÜUÂyBz=Ph}:V 7?ô|}Àå-Õ8³V8´I,) I=PÁiî”Ák™h½)¹i8Ò½a·Þf8Ôh7¸"9-8Éþ9{Tñ8ˬŸJ :psÔ@AjAŠD"b¬=”/>×ng>˜U9>Lô8×FcÂ47<†ÌýC€÷:^F8C€ü:^CåÂIÓ<éÂ:°?úaºÀá18e„)8fë„I²Ã²>|Ϥ>LMX8¬(ÇÂ4ˆ”é´>R˜;>LD¢8ºÜÞÂ4ßYëã>þ>L¤8~óXÂ4<(êC€Ù:/*ðC€ü:/(ãÁÏÄt<uE9¶ì@w«ÀÔƒÅ8ñÑ8pIqÁtI…j¨ÁoÔXÁq‹Cµ±8—KÓ8m·:58M ¸V:8/o¸¸”£8rÂm :LI}@A«|³CÉ`F;é§þ=â<–=Ÿù6>L‰8Q”QÂ49<ípC€ü:vBC€€:rÁÁ§ ?#i°<ú`¿@ µÀÀÐg-7¢7 êI„5{Iœ^ÁqbÓÁrñ’©4謰8î·Y˜8‘¶©£¿7ûYG¸±·Ì7üMx 9Úc@A¼¢ÅC³2w;æI?=éÛN=¥\”>L…m8@Â4†;»¼ÅC€õ9ùê"C€ô9ùé”Á„Û%>Ô:™<ÌxÁ@ ÙÀÌYP7°µè7´ |IIœµ%ÁrÂ~ÁtVÖ{Õ5Ç´7º‘=¶Cc7º©í·RãF7ºHÄ6£“ž7ºo[„ : -@AÏÙCŸâJ;ì*4=û[â=±¼Ó>L 8÷Â3þÀ;³‘kC€Ü:¢’C€ß:¢ÏÁKes>”tF<ºÙ2@nrÀÈc7Ë&O7Ï8I›ßI©*tÁt'Áu« -¶ l¯7²\~7ya7²´»6¾„Ì7®K©¸_éÒ7®ù'‘ 9“ÝÙ@Aä?ÕCä>L©~7ÛqžÂ4´;ˆg5C€€9Üi¯C€ô9ÜiÁ#Êæ>JóÉ<žš6@ê Àćs´,âu4,âuI¨I¸[ÎÁuŒrÁw)‚ý ·QZA7‡š¶$l'7ˆB>6i³¨7ƒ–¦¸Lî7„†&Ÿ 9M–@AûC€‡;f¤¹=†×=>²>L«7´ÛwÂ3ý³;`ö˜C€ó9ÇßCC€ò9ÇßòÁ Ú> E<‰Ò@{ ÀÀÍY7|U7ub I¶XÆIÇãdÁvø¶Áx‘- ¯ µÏBd7`.L¶ˆˆ7a(Ö6‰Ê7[¡ý¸Xé7\㯠9HÓÖ@B BCgô¡;LË´=zÒ=1[¹>Lª7‹ó¯Â4;-žÞC€€9©ï1C€û9©î$ÀÆcñ=¬è<^@";À½;~7xY'7rò¦IÅëÝIÙ"ôÁxe3ÁzB »¡·ªÄ¨7*è·2•7,ζ™!Ô7*qO7ÀT7+µ5À 8…±@BæbCRi°>L°Ç7_îÂ3þË; lÈC€€9•Í×C€€ 9•ÎÀšYŒ=€ËoL¼7;ÆÂÂ3ý—:é–ŠC€ü9Š!ÞC€ý9Š"\Àpã°=ïò<(è<@"´£À¶©Í6¼É[6ÁfËIéÎJqÁ{L¼­7)¬Â3ý]:¶Ø{C€þ9n C€ÿ9n À7ö<Ò׿<³ß@&¡ À³³6Ê&©6ÉoƒIý™âJ .Á|³ŸÁ~n$«Õ6Œ¤S6¶Q¿¶J¡m6·CÅ6žP6³•O·m»6´¡J 8Zé@BJ-ÈC#o:Lã~<˜¯L¾è6ÞÃEÂ4¸:Š•‡C€ö9FN®C€ó9FNxÀBD<¬9<Cg@*¥¨À°ú6®ðu6¬æþJ aŠJz˜Á~8IÁ€f ]5Ÿì‡6Špõ5­SÊ6ŠöEµçó*6‰ ·ir6‰· 8kT@B^eCÊ$:{ŸÚ<ÃË.<Šrq>L¿Ý6¶Ñ†Â3ÿ:cPÚC€€92ø“C€ù92ø±¿Ù&¿>LÂã6˜òüÂ4°:>»zC€ü9$ìC€û9$ëó¿¤×<\è;ÈII@2ùçÀ¬Eå3“nˆ³“nˆJ&.J9€GÁ€³2Á§Ä$Ë-ñ¶¿R6=m´[Ç56=à„µŠÂ³6=Uö^6=¯§V 7»š@B†ŒÏC~>LÃá6pEæÂ3ÿÑ:»UC€þ9v%C€€9v/¿w’”;¤®ß;ªIø@7J¾ÀªK¾2áϲáÏJ6Š„JM8{ÁƒÿÁ‚ˆg,_7©6²ã6 ¶}Ñ6YµW¼6ül4Ý¢;6VQx 7r×@B”JBÿâh:„oÍ<íä*<¨6ë>LÄð6RÂ3ÿò:»ÏC€€9ðTC€€9ðW¿;é;Yì4;•@;¶9À¨¥7»}7 JI¬}Jc†ÜÁ‚a£ÁƒmÖ5·C)3•dX6Íß4§)C6A?4d¦P6Ɖ4YY˜6FÌ 7Þ}@B¢Î8Bõ ô9Ï‘LÆY6õAÂ4e9Æ ¦C€ÿ8äÞC€€8伿 |;Ä‚;‚ýÀ@@<ýÀ§•6z³46])©J_ÏçJ~µÁƒI<Á„cA Qi³w15Æ/¿µ‰Ä…5Æö4„65ÂÇ ¶Á´D5Ã>ÝÇ 7@y4@B³ Bì˜>LÇ76 ÏÔÂ3ÿÑ9«˜gC€ÿ8ÙmC€ü8ÙmŸ¾ÑƒÃ:·øí;`Êb@Dß²À¥Ä€3Ôxw³ÔxwJy‰-JŽetÁ„;'Á…aN¿biµRr5«ƒŠµÉj 5¬òµ®éè5« 85­Ôë5«¯ßô 7eM¿@BÄþ¦BäÍÚ>LÈ5Üw+Â4"9‰@ûC€€8¿NûC€ÿ8¿Nñ¾š…œ:hÃe;@Ïç@IŸÀ¤¬º4yLÍ´yLÍJ‹ÏÃJ ¬_Á…8IÁ†m‰_Uw=².Ž5‰bëµæ5‰È«´þ'5ˆ‡h60Ô÷5ˆúè& 6Ø| @Bر·BÞÙ>LÈ¿5½9ßÂ49k›¿C€€8´žûC€ÿ8´žø¾c=h:XL;$Ýã@N{¦À£Â4HR´HRJc¡J¶ !Á†?Á‡ƒ'sG)5Zl5kz>µ’ïº5l(‰´…ƒ5jîÆ5¹žÀ5k´] 6¦$À@Bî]BÚ¿>LÉÊ5šQ6Â49?þbC€ÿ8¡ù¿C€€8¡ù½¾&Ř9½ý¸;Ò5@SvJÀ¢ÿi³¯u3¯uJ²)&JÏdEÁ‡SCÁˆ¥‹k®M4¯d;5@!ݵ=âÛ5@Õ ³ùëè5@ ¦4Æ€œ5@Ó¨™ 6N.@C™BÖ":.ºú<Ðì <“»W>LÊa5„Â49$_ØC€þ8˜|C€ý8˜|½ó‡9zä;Þy@XªÀ¢_d6éÅQ6ÙïmJÊáwJíÅzÁˆt-Á‰Õ ¨¹Òý3Ah5$YÒµ‰dÚ5$ÖR09 5#ç_µž?^5$s§Ü 60Ø@C5ÂBÓt>LÊ£5^.­Â49 EüC€þ8‹C€ÿ8‰½¯øy9"³:ÝS6@]È…À¡Ý4ÐÓ´ÐÓJèQKö€Á‰¡tÁ‹ÉÌ1ÿ5³¼uU5 tdµK5 åé4ŒQ5 å{µ¨©ð5 dI% 6>ÙP@C¡‰BЊÆ9Ñ}O<‰¶ LË5B™æÂ48ò¯C€€8‡íNC€ÿ8‡íM½}Tæ8Ÿ¼þ:¡k¹@c!À¡sþ6‡îI6KK·=K¨NÁŠÚoÁŒV¼÷4Ñ´q}°4ò>4µ0BÜ4ó'ù´â™4ñ©¤µmð4òªžu 5Íš[@C.~}BΔz9qçÒ<&Ó;ëìý>LË5#ûÂ3ÿû8̲C€ÿ8| 7C€þ8| :½5ƒ8–n:Ô@±@h›¸À¡ê6*ŸÂ6JNK±ºK8´ˆÁŒ|Á¨À*Óuµ1‰nÊ4ÌNݵx 4Ìù2”ô^4Ìí5à¦4ÌÔ×Î 5«©§@C?ñŠBÍ m9-CI;ú—@;±1É>LË}5ÖÂ48²ózC€ý8s%ñC€þ8s%ñ½§8^#:Ý`!@n7¢À Ý95÷5åœuK3öKWø¶ÁnåÁ}i³Ä43rÑ4³Où´5z4³ön³—ˆ24²ã}µ@ï4³›L/ 4ÕP~@CS#KBËÔ®>LË’5ÈWÂ48 5tC€ÿ8ovUC€€8ovT¼µÂ[@sö-À ¨À³„33„3KROLK}¬XÁŽÉkÁjCµ¹#4c¾ó4 u‚´´ÊÐ4¡ é³Zå”4 lø³ª4¡ÿš 5eˆ@Ch@lBÊäõ>LËÆ4ä*¹Â3ÿý8ñ×C€ÿ8i\C€þ8i\¼~Ór7¯ÿ×:°Ïl@yØ-À Ì4û´ûKvÊÙK•”hÁ-Á‘Ù—–)´“E94Žà´ÁÓM4Ž¡/´³,4Ž ­´h0A4Ž›Ä 4&ll@CzBÊ,:]à<ãë< Ý×>LÌ4ОoÂ3ÿý8ºNC€ÿ8j¨ÉC€þ8j¨Ê¼1bN7ªÎµ:ö‚=@Þ~À `6Á«l6´GnK‘dmK°óPÁ‘™ÝÁ“N§€Ñ! ´š14ëj´›n04‚o±²dú~4×X´ 14‚hG‘ 4i(ÿ@CŒƒ#BÉžm>LÌ4¼ÊâÂ3ÿú8qkC€ÿ8lÙ@C€þ8lÙB»õŒH7vý;ÀD@ƒÀ G´²ÖZ’2ÖZ’K«éLÌ5üÂ3ÿú9ÄVC€ÿ8ñk„C€þ8ñk‹»¨=@†-ÌÀ 5³† 3† KÁºKÓ›(Á”ßÁ”Ü[rŸÑc³Ì#4ôµ´á:ª4µgO´lc&4Ï,³µ˜c4Ê\,‘5„›@XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 160 / width of table in bytes NAXIS2 = 69 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 40 TTYPE1 = 'SMA ' / label for field 1 TFORM1 = '1E ' / data format of field: 4-byte REAL TUNIT1 = 'pixel ' / physical unit of field TTYPE2 = 'INTENS ' / label for field 2 TFORM2 = '1E ' / data format of field: 4-byte REAL TTYPE3 = 'INT_ERR ' / label for field 3 TFORM3 = '1E ' / data format of field: 4-byte REAL TTYPE4 = 'PIX_VAR ' / label for field 4 TFORM4 = '1E ' / data format of field: 4-byte REAL TTYPE5 = 'RMS ' / label for field 5 TFORM5 = '1E ' / data format of field: 4-byte REAL TTYPE6 = 'ELLIP ' / label for field 6 TFORM6 = '1E ' / data format of field: 4-byte REAL TTYPE7 = 'ELLIP_ERR' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'PA ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TUNIT8 = 'degrees ' / physical unit of field TTYPE9 = 'PA_ERR ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TUNIT9 = 'degrees ' / physical unit of field TTYPE10 = 'X0 ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TUNIT10 = 'pixel ' / physical unit of field TTYPE11 = 'X0_ERR ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'pixel ' / physical unit of field TTYPE12 = 'Y0 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TUNIT12 = 'pixel ' / physical unit of field TTYPE13 = 'Y0_ERR ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TUNIT13 = 'pixel ' / physical unit of field TTYPE14 = 'GRAD ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'GRAD_ERR' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'GRAD_R_ERR' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'RSMA ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TUNIT17 = 'pixel**1/4' / physical unit of field TTYPE18 = 'MAG ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'MAG_LERR' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'MAG_UERR' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'TFLUX_E ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'TFLUX_C ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'TMAG_E ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'TMAG_C ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'NPIX_E ' / label for field 25 TFORM25 = '1J ' / data format of field: 4-byte INTEGER TTYPE26 = 'NPIX_C ' / label for field 26 TFORM26 = '1J ' / data format of field: 4-byte INTEGER TTYPE27 = 'A3 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'A3_ERR ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'B3 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'B3_ERR ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'A4 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'A4_ERR ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'B4 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'B4_ERR ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'NDATA ' / label for field 35 TFORM35 = '1J ' / data format of field: 4-byte INTEGER TTYPE36 = 'NFLAG ' / label for field 36 TFORM36 = '1J ' / data format of field: 4-byte INTEGER TTYPE37 = 'NITER ' / label for field 37 TFORM37 = '1J ' / data format of field: 4-byte INTEGER TTYPE38 = 'STOP ' / label for field 38 TFORM38 = '1J ' / data format of field: 4-byte INTEGER TTYPE39 = 'A_BIG ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TTYPE40 = 'SAREA ' / label for field 40 TFORM40 = '1E ' / data format of field: 4-byte REAL TUNIT40 = 'pixel ' / physical unit of field TDISP1 = 'F7.2 ' / display format TDISP2 = 'G10.3 ' / display format TDISP3 = 'G10.3 ' / display format TDISP4 = 'G9.3 ' / display format TDISP5 = 'G9.3 ' / display format TDISP6 = 'F6.4 ' / display format TDISP7 = 'F6.4 ' / display format TDISP8 = 'F6.2 ' / display format TDISP9 = 'F6.2 ' / display format TDISP10 = 'F7.2 ' / display format TDISP11 = 'F6.2 ' / display format TDISP12 = 'F7.2 ' / display format TDISP13 = 'F6.2 ' / display format TDISP14 = 'G8.3 ' / display format TDISP15 = 'G6.3 ' / display format TDISP16 = 'G6.3 ' / display format TDISP17 = 'F7.5 ' / display format TDISP18 = 'G7.3 ' / display format TDISP19 = 'G7.3 ' / display format TDISP20 = 'G7.3 ' / display format TDISP21 = 'G12.5 ' / display format TDISP22 = 'G12.5 ' / display format TDISP23 = 'G7.3 ' / display format TDISP24 = 'G7.3 ' / display format TDISP25 = 'I6 ' / display format TNULL25 = -2147483647 / undefined value for column TDISP26 = 'I6 ' / display format TNULL26 = -2147483647 / undefined value for column TDISP27 = 'G9.3 ' / display format TDISP28 = 'G7.3 ' / display format TDISP29 = 'G9.3 ' / display format TDISP30 = 'G7.3 ' / display format TDISP31 = 'G9.3 ' / display format TDISP32 = 'G7.3 ' / display format TDISP33 = 'G9.3 ' / display format TDISP34 = 'G7.3 ' / display format TDISP35 = 'I5 ' / display format TNULL35 = -2147483647 / undefined value for column TDISP36 = 'I5 ' / display format TNULL36 = -2147483647 / undefined value for column TDISP37 = 'I3 ' / display format TNULL37 = -2147483647 / undefined value for column TDISP38 = 'I2 ' / display format TNULL38 = -2147483647 / undefined value for column TDISP39 = 'G9.3 ' / display format TDISP40 = 'F5.1 ' / display format IMAGE = 'synth.fits' END F’*ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿC€€ÿÿÿÿC€~zÿÿÿÿÃvôÿÿÿÿÿÿÿÿÁÅ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€€ÿÿÿÿÿÿÿÿ?__F‘^A’˜ÃBºßùB„#ø>“ü7>)Ä›Â=ýœAžì›C€€=]?€C€~z=W‹(ĨPD R$?‰0?Y~`Á‡Œ;‹; ÉF’*F’*ÁÅ+ÁÅ+¼× h=䨻‹z«=ÔV½•O.=§—Ô¾ °>­#^ =s z@?µµF%²A±q™Bâ2BŸò>“ü7>)À Â=þ AžèUC€€=sYC€~T=mÄ ŠÍD$P?‰•å?^¼üÁz€;,w®;,üF’*F’*ÁÅ+ÁÅ+¼× a=ä+»Ë =ÔSP½•Uª=§•«¾ «5>­ =Šƒf@?!azF£gAÖ´qCÙBÁˆG>“ü7>)ÊBÂ>¥AžñæC€€=…àC€~*=‚jÄ­D'Zù?‰š[?dùÁj©;Q|;PäcF’*F’*ÁÅ+ÁÅ+¼ÖSû=ä5»ŽQ¾=Ô`,½•­ÿ=§÷¾ ²!>­" =ÊŽ@?1„ÓF¿BÙC%†&Bê>“ü7>)¿Â>üAžçkC€€=“9ùC€}ú=kÒÄ+D\D87?‰G?iœÁWk;~‘s;}­PF’*F’*ÁÅ+ÁÅ+¼Öžj=䑻޲:=ÔQ뽕›ï=§ÖÀ¾ ¨Z>­ Þ =ï+¿@?CEOF G B)#CHWcC ©©>“ü7>)ÉåÂ>ïAžñC€þ=¡ýµC€}É=ÍÄ6Á@;šî2;šDoF’*F’*ÁÅ+ÁÅ+¼ÕuS=ä »_=Ô`T½•Àã=¨ æ¾ ±/>­"4 > U@?VÌ>F `B>?CrY\C+]Þ>“ü7>)·œÂ=ùÞAžàpC€€=²ÁC€}‹=­„àÄO@ÇD^·z?‰Œç?uÁ#‹;¼³š;»»¬F’*F’*ÁÅ+ÁÅ+¼×^Î=äÖ»Œë¨=ÔF(½•)=§^ü¾ £ž>­© >6™‘@?lGF HÐBfbC’¢—CO_•>“ü7>)úÂ=ÿEAžëÈC€€=ÃùC€}N=¾ë5ÄcïDuQ?‰–6?zë’ÁØ;æG‰;äÑÆF’*F’*ÁÅ+ÁÅ+¼× )=äË»•|=ÔV`½•l =§­Ð¾ ­Í>­< >;t@?óãF öýB‹6ˆC±vdCzøF>“ü7>)Ç{Â>ÔAžïLC€þ=טÏC€} =ÒÐÄz¿ûD†Æá?‰™H?€|CÁÖƒ< Ë < ¬ôF’*G5ö´ÁÅ+Á:ºŸ¼Õø=äó»´=Ô\T½•¢½=§ê»¾ ¯ô>­u >jöq@?Žò­FXãB¨ÈªC×(tC˜#Ã>“IÕ=ë7ÁÂ=dÇA]3C€ö=£ïÄC€|¼=ŸûLÄÇÝzDŽY’?6T¯?ƒ•kÁ¢<,á-<+3”F’*G5ö´ÁÅ+Á:ºŸ¼—rb=ší×»]¢k=’¼Î½Eú)=N$¾^c >4€ ?Öý@?>%F±B¦Î{CÔ£2C–[>wcA=… Â7'eAþC€€Ô=BoC€~¶=A'KÅ(éÞD`|r>ª­?†Á²Áµ<0t}<.¼¬G5ö´G5ö´Á:ºŸÁ:ºŸ¼,‹Ô="šÀ»´µI=àß¼·o5SD©=(Î*Â;Ô @Ó+IC€€ÿ=ZC€Z=–ÅH£}Dèß>JÁÆ?ŠÁ,]<þ~<ÌÖG5ö´G5ö´Á:ºŸÁ:ºŸ»‡ T<ĨW»‹N<½|¼€:6<+¸<½….Œ<ƒTÏ > Ê1@?¾C„Eëü;BNxdCƒ™xC:>Géù<íº¿Â:d@œ,ÒC€:<ËôC€R<ÈõRÅMV$CãB> ªM?U}Á;ô";òYÓGrâ‡G’®Á?¾áÁC‘ »!º<ˆ¦ñ»ñ<ƒ[¼¼Rç\< ŵ½-wÓ<#} ={Ò@?ÑJEEÜ6MB*Á,CY«”Cê…>C$ô<·Ù~Â9ï+@vÓ3C€€è<¬ >C€(<ªu¹ÅHŲC¥Ëí=ÓgB?½öÁën;Ø-®;ÖßyGrâ‡G’®Á?¾áÁC‘ »o‘™<ýi<„^Â8òÇ@6‚ÿC€&<‡A`C€S<†C=Å=šùCPªÃ=ŒÞG?”;xÁ‚Ì;­Ì;¬¿‰G’®G’®ÁC‘ÁC‘ »€æ€<¡gºËj<Ò¼ ‘B;°u¼¬{®;­ðB =Âý@?ý=µE¹@A’ÍB»"·B„S*>5K<·IÂ7Î’?Ö¿•C€<<'ÓC€Ñ<&ªÅ2ÈC\Ë=O|¡?—Î…ÁæÚ;\Ìh;\^G’®G’®ÁC‘ÁC‘ »¾Í±;˜b:;—5T»½Yµ;‡>U»²1; =L\@@ HWE¦:àAªBØÈB™Iþ>*ý;áËG’®G»+¿ÁC‘ÁGB¥ ¼W<;Ûã3ºêÀe;Ù_qºmåj;†ð<‡¡^;ˆD @u`˜@@5úE˜úŸAþôBK·«B Ì>5 ;×[ñÂçXº@@(ˆ-EŒãÕA-5ÿBm-ÇB'µô>?ñ<Â1Ûê?®§˜C€€.<@¢wC€€üC®#@@9b˜EËì@ôR1B2úAû×#>Dœ;Ÿê(Â6X§?WXŽC€é<’ C€€f<:ÈÄ®¢·Aþm¼<º|?¦üÆÁ¼;êÒ;¯çH ª3H?QÁN5ÁNåv9ο™;=xº³;5 º©:K;ÿ»ÍÃ;3 >ÝH@@KìtEiÐà@}AÔ;¬A–K>AM|;Y øÂ3 Ÿ?ÌâC€ý;Á¬?C€ù;Áí‹Ä‰ A´8<¨K?«›Áëv:¨[ :¨ ½H ª3H9´ÁN5ÁSI%¸£âš:ùÛu¸¨¢:ù~Í:½Ñ::ç˜;T§:æì& =¿Ùç@@`PæEȘ@¡'mAþÎŽA´,ø>A˱;‹¡(Â8¶ˆ?97rC€€š< =GC€ù< gmÄWÿA’ <­M?¯#KÁ 3¨:Ó—):ÓcH,ëÃH9´ÁQíÁSI!%ºy]Í;#EÕ¹ƒ‰;!C`ºK(t;àÐ;Žãf;, >Lìß@@v¿dEAC@'¢jAŠþÁAD‘>F ;M Â4¼K>¿Ã)C€º; Å±C€ý; žiÄ=ÐøA»JF÷o:Ö­àÂ18>ŠkZC€€;€!ßC€€;€žLÄ&¼@Ùf²<'Í?·¯§Á â®:<°v:<ËHEãŽHn$ÁTDÔÁW|J)9:P(:}}¸ 6ù:{µ<:cÒ(:j*óº½F´:h–H =U@@•HFE`@ qAld_A''‰>G²Æ; ;ŽÂ3îl>°[@C€ü;¶ýC€€;¶¾Ä…ô@¢ßÝ<"6?¼‘Á Ð:gÅì:gf™HtDKH…”ÁWí4ÁYjŽ;E9ìŽ:£÷E¹À·Á:¥3Ÿ7"B°:”ªö;'‹:•І2?S€@@¤5çE–œ?Š™Aðé@º—Ä>I­7:”v™Â36 >=>ÀC€ò;W*ýC€>;WfÚÃä-@HIp:ŒTFÂ2ë¥>4\ôC€€S;_«zC€€I;_þÒö×Ü@ ¾;Å Ý?ÅLÁn'9þ]ª9þmêH“¸5H e˜Á[;Á\©9Sa9´ô:)f¹×*:'^ž¹Fß:3:²ÄŸ:‡u =8ä‡@@ƱÞDæ6ú?$}ñ@¬¡@sIq>JdÂ:R”fÂ49¡>ƒ C€€¢;8¯wC€º;8 šÃ›Vk? º;„q5?ÊÁ›ã9Æßu9ÆWêH¡çH´^Á\ºÁ^³ ayº2Ë;9÷ø˜8=>¥9ö¯è¸4ç`9澺‚# 9ää3# <=º@@ÚuDÍýÕ>ÈsÛ@]K+@zm>I¸: ÜfÂ3¾É=¶Ž?C€€+; yðC€½; …ŽÃ}æP?;R0;<Þá?ÎíSÁ­u9†þ‚9‡uÌH³ÌHÆgãÁ^žÁ`Zúw‘ºLô9¦¦’¹=à9£Â·š‹9¡Mc9öN9ž‹u' ;Ú71@@ðk´D¸{i>^£_?ÿf?´[‡>Jë©9©&JÂ4’µ=TK‚C€å:²ù¢C€ù:²Ó7ÃRnÛ?ÖÌ;¼Ü?ÓêµÀý†9'ž?9'ÌNHÂÖùHÛ´=Á` QÁb q‹±9‘à9KÝ`7M°9Lxý¸Þ¨¥9@x;¹Ê9A * ‰€A@$ÛŸ?é$ï>KeW9æ26Â3“¦=‘²C€€;ª[C€×;ÀÃ.B>Ì; W?ÙåÀù™,9hÞ9gàHÝÎÞHõzÀÁbJÔÁd Ƴݹ¤5B9‰G©7mZ9Š> 9Œwÿ9òa¹ÿ˜d9‚èŸ. ;r{ö@At]D’ÀŠ>;|+?ì°1?§]&>K 9¯PMÂ3à‘=\´C€é:à¸gC€µ:à‰à °Ï>ƒ»4:î~?ÞB Àõ’î91'91žØHðËKI(äÁc¸Áex%Ó¹gÿ9PÜ9='9OÞhµp¼9IÁÖ9®¾9I²/3 ;*I±@A D‚¬X>d–?Á¨Ý?ˆð#>KF·9—ÿÂ4ó==¡C€ð:ÔÖÖC€€:ÔÕžÂè€>:Z:Í…E?ãž©ÀñŠÉ9ÎH9ЉI¥IFáÁeECÁg÷ý=8q-98Ïô·ã¹Q98þÏ67£G947b9”±94w8:‰JØ@A0Dhh„=Ù&?—!?Uº–>K°M9tÄ/Â4C&=kßC€€:¾`œC€€:¾N¾Â½© =ûCã:©“š?éÈÀíw9°ó9ú°I¬YIÍåÁfÊÁh‡e/y86-Î9s§¸çÍ'9•¥¸„©9ÕÛ8š¿Š9L> ;;k@AA™šDNXy=¤ÂÍ?p-¤?)Ôê>KÅú9PˆÂ44=ÐdC€€ :±»C€ÿ:±µ#™ÈÓ=§õ„:‹ÌH?îºÉÀéU8ݼ8ÞwI÷¯I/9ÁhS¨Áj8°mѸ™@9]í·“§:8ÿýݸŸ‰Á8ø#¿9{ß›8÷ƒŽD :ðß^@ATõÃD7{=l·E?3ü_>þ‰Ô>KÖ|9.#WÂ4)r<Ø›C€€5:¢æ…C€ë:¢ÜUÂyBz=Ph}:V 7?ô|}Àå-Õ8³V8´I,) I=PÁiî”Ák™h½)¹i8Ò½a·Þf8Ôh7¸"9-8Éþ9{Tñ8ˬŸJ :psÔ@AjAŠD"b¬=”/>×ng>˜U9>Lô8×FcÂ47<†ÌýC€÷:^F8C€ü:^CåÂIÓ<éÂ:°?úaºÀá18e„)8fë„I²Ã²>|Ϥ>LMX8¬(ÇÂ4ˆ”é´>R˜;>LD¢8ºÜÞÂ4ßYëã>þ>L¤8~óXÂ4<(êC€Ù:/*ðC€ü:/(ãÁÏÄt<uE9¶ì@w«ÀÔƒÅ8ñÑ8pIqÁtI…j¨ÁoÔXÁq‹Cµ±8—KÓ8m·:58M ¸V:8/o¸¸”£8rÂm :LI}@A«|³CÈýë<,ß|=Ø[=˜Ì±>Mÿj8’’Â4 ï<6KMC€ø:]XÆC€€:]VOÁ¤ª·?$Ê/=v@ µÀÀÐV17ñ97ív I„5{Iœ^ÁqbÓÁrñ’©¸ÅG81˜a7‰y81ûû7]4Ì8$ÔZ¸à* 8&š2 :7Nå@A¼¢ÅC²áu<.=»Ÿz=„«b>N·8~yòÂ4Ì<¿C€ð:TZC€ÿ:TQÁuÙ>Òtó<Ð4@ ÙÀÌI˜7è½·7ég±IIœµ%ÁrÂ~ÁtVÖ{Õ5Þ!—8`ÿ5’ë8½W8^ËÛ8¢Ê¸x¶;84“2 9ª¸@AÏÙCŸŸ >$ÔT@Ov?‘°£>M«–:—@HÂ4Ì><ïC€ð<Š˜wC€ÿ<Š—ÉÁG‚Ì>‘‘@<ºÈs@nrÀÈTi:‹Ë:~ëI›ßI©*tÁt'Áu« -5Äad:6uø:m‰Ö:7i¹À?9:'+ã:ö:(:ž22>|be@LÍÚAä?ÕCŽ‹x>jÝ@Žñƒ?Ï—”>J¥‡:ôþÂ4Ì>šfèC€<õï¾C€€î<õî‡Á!ú >Ujª<¨¥º@ê ÀÄf:e•:dÞ/I¨HþI¸[ÎÁu“ÒÁw)‚ º^Râ:”M繆Úr:•D!9ޏY:}j:ý_“:¶'2 >9\ä@òÂzAûC€Õ> Þ @; Î?yI>J¥‡:ªÖdÂ4Ì>WU§C€€Â<¼¥BC€-<¼¤RÀú÷…>º–<’œ€@{ ÀÀ®h:Ý·:Ø…I¶XÆIÇãdÁvø¶Áx‘- ¯ :qÞú:Må9áö:Nk§9Oa:LT¹ÀRf:Mñ22>K‘ AnÞB BCg€Š>b—@_%?‹i&>Lgc:Öw8Â3}>†¤^C€Ì=C€€#=®`ÀÂnÄ=Ã×+<€íC@";À½*:4¤E:4|IIÅúYIÙ"ôÁxfxÁzB ½¡¹sÖ]:ƒ ã8§ã7:„b¹Oùa:rÉ»:Uƒ:sTy7 =°¡UA#íÓBæbCQÜ]=Æ,T@!±Æ?Ay>Lœ›:¥xÂ3×#>QTñC€Ì<Þ±gC€€#<ÞºÀ•Î=Šv,¥:>BIÖ›Ië‘ÁyÍÁ{k'¹oñÖ:M;­¹Eo:L9\@:L(.º $:KH= =[¶A2Ï9B'C?®Œ=]A”?Ƨî>âaê>MIm:TgŠÂ4Ô>âjC€G¶­_>MIm:A9ÝÂ4Ô=ñISC€€µ<œ¢×C€ <œ #À4êÙ<ÜÐé<:@&¡ À³ª 9‡¹9†æ©Iý™âJ .Á|³ŸÁ~n$«Õ·±ì„9î1&¹"ã09ïO|9 eP9ëv"º “79ìà«I =ªrs>LÛg:I%pÂ3ö4=ý®™C€ž<´ûC€õ<´­À …™<²<ˆ@*¥¨À°íÚ9T”9YˆJ aŠJz˜Á~8IÁ€f ]·Õl–9úk½·X199ùb¸‹‡9úAl7fÕ9ù±zQ :âÜæAo†ƒB^eCý=´5?¡TC>žÙl>Lôœ:UÆìÂ3ö4>¦C€ž<Ò[vC€õ<Ò]t¿ÔÜY<_ }<y[@.ÃÀ®uº9w=9v¹JpuJ(w•ÁÉNÁ€Ñ–]%ù8SM:B7¬{:ÉW8x¼:4úºɵ: hY <³“ÒA„Bt¢ìC<2<ˆî?5(:>)q9>Lôœ:õÂ3ö4=¢¾,C€ž<‹Ì¤C€õ<‹Íô¿ Æ$< Qž;Ú§@2ùçÀ¬=•9×g9¡ïJ& ÀJ9€GÁ€²Á§Ä$Å-ñ8ŸÁŸ9 þ󏻨F9 o”8Ÿ T9 Éo¸©09 lUb2=Vç˜A’O¥B†ŒÏCnÄ>/>Mcë: ÐËÂ3ö4=­‡C€ž<¥zâC€õ<¥|¢¿sÎÙ;¶mï;¿V@7J¾ÀªGÈ8ý·8üJ6qKJM8{Á‚ÌÁ‚ˆg,S7©8´x9¬ŽM´êò9­ï˜7P-z9«™H9ÅD9¬·Ýj <¯Þ9A£€UB”JBÿª·÷¥>KðÁ: »óÂ3ö4=®L’C€€0<·C€c<·S¿8 ;n-Ï;¥—S@;¶9À¨ˆ8ÜßB8ÜÈrJIÌyJc†ÜÁ‚cÁƒmÖ5ÇC)¸8Ûÿ9­)¤·=÷E9®óÜ6%í»9­Ä9%9­úòq ;Î4A¸Ê˜B¢Î8BôËÚ<…? ó=Μ`>L˜:«=Â3ö4=žäaC€€j<¸*SC€¡<¸-¿ DT;#€;˜´N@@<ýÀ§¡8¬ @8«ý™J_æßJ~µÁƒJ Á„cAQi¶ ÷9Ÿ'z8—Ù9žùI8(šÂ9˜0:€×d9—4*w ;¶AÔI%B³ Bëò®;¾û¶>¸a=„aµ>LÆ£9ÄÔ9Â3ö4=qL¶C€€j<š|C€¡<š~ɾÌÁ:ÊFÂ;|æÿ@Dß²À¥¾/8a«08`T„Jy‰-JŽetÁ„;'Á…aN¿bi8™~y9r£Ù¶ª6…9qœa¸¬IŠ9ož*9«Nq9pTÀ{ ;dçÀAøLÏBÄþ¦Bä«{;}¬:>†ì™=1÷„>L®]9“øÂ3ö4=8‘ÅC€[<¼C€€®< ¾—",:wÃ;X]?@IŸÀ¤§8(8J‹ÓVJ ¬_Á…8‚Á†m‰_Yw=8‡-k97à 8>› 97EH8øÛ97Co¹€96Åè~ ;¨aDB$ÄBر·BÞ¾Ú;#}ë>?NÙ<åf">L®]9‘fÂ3ö4=5\úC€€Y<‹p)C€±<‹r¿¾^Y:;60@N{¦À£¾ 7Íèe7Ê!ìJc¡J¶ !Á†?Á‡ƒ'sG)¸ êc94§1¸"غ94+‘·þÚ«94–h¸.žC94¡~ ;š B2 ÙBî]BÙó/:úEr>! :<¯”œ>M%9‚©ãÂ4:D="¿4C€€Y<‰è9C€±<‰Øþ¾#ŒÎ9Ñ:Ž;#¿ñ@SvJÀ¢üt7 ºm7žßJ²…JÏdEÁ‡R™Áˆ¥‹[®M¶s{9"u¢·¨=9"v7;»9"K…¸ +Ã9!ÜN~ <÷€BWX)C™BÖò:ø÷5>079<®ª>M%9uuÂ3ä™=ÝC€€Y<Žp¿C€±<Žx&½îo69„à&;ª#@XªÀ¢\Û7¢7¡;£JÊÚÆJíÅzÁˆsäÁ‰Õ ¨±Òý·àÖ39wŽ8ƒÄW9:8ðB9‹à9 -p9$~ ;F¢õB‚H›C5ÂBÒ÷ò:–¿¥=ê LÝ¥9®ÏÂ4È<ÇsC€€YLÚ,9fÂ3û­<ÄQ®C€S<].éC€€·<]0¸½wñ8Õ -:ÛùÊ@c!À¡rA2–½²–½K¶lK¨NÁŠÚaÁŒV¼÷ 4Ñ8¿8ðÈ5cçI8Ã*Ûµ½mÛ8ÀÛb¹7ƒ8ÀWq~ :†©B¾ÈvC.~}BÎ<:îb0>`–Ž<§=”>Lç§9 é[Â3û­<È„ÈC€€ -_LÁÀ8ëäÂ3û­<’Ü„C€€ ¸»<(EV>LËp8ã²"Â3û­<ðEC€€ U÷)Láæ8ÒØÂ3þÉ<‚èC€ä@yØ-À 87hpe7lYKvÂìK•”hÁ,ÖÁ‘Ùƒ–)7 8‚”j5+oü8‚; ¶Ét`8‚>8N Ž8äñ~ 8ZÚÞCLyqCzBÊ):®½>pÝÏLÝ¥8š¨Â3þÉL¿â8¥1DÂ4<–ÌC€õ<}XTC€Û<}UÑ»ðÙÓ@ƒÀ GY³ñýL3ñýLK«êÏKÆ …Á“|Á”I ]ŒK¶ì7l8‹ì5û*8OšÓ·nDl8m]Ö8)k8lzqK3 7óRQC•¶Cš@BÉ1';/ ‡>¨°ƒ<Ì­>L¿â8€ ‹Â4<¯[C€õ<‹òC€Û<‹ïý»¥`…8µ2ª<Œ>Ä@†-ÌÀ 4Ú7ð?¯7óG{KÁçKÓ›(Á”íÁ”Ü[r¥Ñc¶f 8Hòê7¼V8H= 1.: aux = saux return abs(a ** 2 * (1. - eps) / 2. * math.acos(aux)) def test_angles(phi_min=0.05, phi_max=0.2): a = 40. astep = 1.1 eps = 0.1 # r = a a1 = a * (1. - ((1. - 1. / astep) / 2.)) a2 = a * (1. + (astep - 1.) / 2.) r3 = a2 r4 = a1 aux = min((a2 - a1), 3.) sarea = (a2 - a1) * aux dphi = max(min((aux / a), phi_max), phi_min) phi = dphi / 2. phi2 = phi - dphi / 2. aux = 1. - eps r3 = a2 * aux / np.sqrt((aux * np.cos(phi2))**2 + (np.sin(phi2))**2) r4 = a1 * aux / np.sqrt((aux * np.cos(phi2))**2 + (np.sin(phi2))**2) ncount = 0 while phi < np.pi*2: phi1 = phi2 r1 = r4 r2 = r3 phi2 = phi + dphi / 2. aux = 1. - eps r3 = a2 * aux / np.sqrt((aux * np.cos(phi2))**2 + (np.sin(phi2))**2) r4 = a1 * aux / np.sqrt((aux * np.cos(phi2))**2 + (np.sin(phi2))**2) sa1 = sector_area(a1, eps, phi1, r1) sa2 = sector_area(a2, eps, phi1, r2) sa3 = sector_area(a2, eps, phi2, r3) sa4 = sector_area(a1, eps, phi2, r4) area = abs((sa3 - sa2) - (sa4 - sa1)) # Compute step to next sector and its angular span dphi = max(min((sarea / (r3 - r4) / r4), phi_max), phi_min) phistep = dphi / 2. + phi2 - phi ncount += 1 assert 11.0 < area < 12.4 phi = phi + min(phistep, 0.5) # r = (a * (1. - eps) / np.sqrt(((1. - eps) * np.cos(phi))**2 + # (np.sin(phi))**2)) assert ncount == 72 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_ellipse.py0000644000214200020070000001317300000000000022300 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the ellipse module. """ import math from astropy.io import fits from astropy.modeling.models import Gaussian2D import numpy as np import pytest from .make_test_data import make_test_image from ..ellipse import Ellipse from ..geometry import EllipseGeometry from ..isophote import Isophote, IsophoteList from ...datasets import get_path, make_noise_image from ...utils._optional_deps import HAS_SCIPY # noqa # define an off-center position and a tilted sma POS = 384 PA = 10. / 180. * np.pi # build off-center test data. It's fine to have a single np array to use # in all tests that need it, but do not use a single instance of # EllipseGeometry. The code may eventually modify it's contents. The safe # bet is to build it wherever it's needed. The cost is negligible. OFFSET_GALAXY = make_test_image(x0=POS, y0=POS, pa=PA, noise=1.e-12, seed=0) @pytest.mark.skipif('not HAS_SCIPY') class TestEllipse: def setup_class(self): # centered, tilted galaxy self.data = make_test_image(pa=PA, seed=0) @pytest.mark.remote_data def test_find_center(self): path = get_path('isophote/M51.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) data = hdu[0].data hdu.close() geometry = EllipseGeometry(252, 253, 10., 0.2, np.pi/2) geometry.find_center(data) assert geometry.x0 == 257. assert geometry.y0 == 258. def test_basic(self): ellipse = Ellipse(self.data) isophote_list = ellipse.fit_image() assert isinstance(isophote_list, IsophoteList) assert len(isophote_list) > 1 assert isinstance(isophote_list[0], Isophote) # verify that the list is properly sorted in sem-major axis length assert isophote_list[-1] > isophote_list[0] # the fit should stop where gradient loses reliability. assert len(isophote_list) == 69 assert isophote_list[-1].stop_code == 1 def test_linear(self): ellipse = Ellipse(self.data) isophote_list = ellipse.fit_image(linear=True, step=2.) # verify that the list is properly sorted in sem-major axis length assert isophote_list[-1] > isophote_list[0] # difference in sma between successive isohpotes must be constant. step = isophote_list[-1].sma - isophote_list[-2].sma assert math.isclose((isophote_list[-2].sma - isophote_list[-3].sma), step, rel_tol=0.01) assert math.isclose((isophote_list[-3].sma - isophote_list[-4].sma), step, rel_tol=0.01) assert math.isclose((isophote_list[2].sma - isophote_list[1].sma), step, rel_tol=0.01) def test_fit_one_ellipse(self): ellipse = Ellipse(self.data) isophote = ellipse.fit_isophote(40.) assert isinstance(isophote, Isophote) assert isophote.valid def test_offcenter_fail(self): # A first guess ellipse that is centered in the image frame. # This should result in failure since the real galaxy # image is off-center by a large offset. ellipse = Ellipse(OFFSET_GALAXY) isophote_list = ellipse.fit_image() assert len(isophote_list) == 0 def test_offcenter_fit(self): # A first guess ellipse that is roughly centered on the # offset galaxy image. g = EllipseGeometry(POS+5, POS+5, 10., eps=0.2, pa=PA, astep=0.1) ellipse = Ellipse(OFFSET_GALAXY, geometry=g) isophote_list = ellipse.fit_image() # the fit should stop when too many potential sample # points fall outside the image frame. assert len(isophote_list) == 63 assert isophote_list[-1].stop_code == 1 def test_offcenter_go_beyond_frame(self): # Same as before, but now force the fit to goo # beyond the image frame limits. g = EllipseGeometry(POS+5, POS+5, 10., eps=0.2, pa=PA, astep=0.1) ellipse = Ellipse(OFFSET_GALAXY, geometry=g) isophote_list = ellipse.fit_image(maxsma=400.) # the fit should go to maxsma, but with fixed geometry assert len(isophote_list) == 71 assert isophote_list[-1].stop_code == 4 # check that no zero-valued intensities were left behind # in the sample arrays when sampling outside the image. for iso in isophote_list: assert not np.any(iso.sample.values[2] == 0) def test_ellipse_shape(self): """Regression test for #670/673.""" ny = 500 nx = 150 g = Gaussian2D(100., nx / 2., ny / 2., 20, 12, theta=40.*np.pi/180.) y, x = np.mgrid[0:ny, 0:nx] noise = make_noise_image((ny, nx), distribution='gaussian', mean=0., stddev=2., seed=0) data = g(x, y) + noise ellipse = Ellipse(data) # estimates initial center isolist = ellipse.fit_image() assert len(isolist) == 54 @pytest.mark.remote_data @pytest.mark.skipif('not HAS_SCIPY') class TestEllipseOnRealData: def test_basic(self): path = get_path('isophote/M105-S001-RGB.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) data = hdu[0].data[0] hdu.close() g = EllipseGeometry(530., 511, 30., 0.2, 20./180.*3.14) ellipse = Ellipse(data, geometry=g) isophote_list = ellipse.fit_image() assert len(isophote_list) >= 60 # check that isophote at about sma=70 got an uneventful fit assert isophote_list.get_closest(70.).stop_code == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_fitter.py0000644000214200020070000001641100000000000022136 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the fitter module. """ from astropy.io import fits import numpy as np from numpy.testing import assert_allclose import pytest from .make_test_data import make_test_image from ..fitter import CentralEllipseFitter, EllipseFitter from ..geometry import EllipseGeometry from ..harmonics import fit_first_and_second_harmonics from ..integrator import MEAN from ..isophote import Isophote from ..sample import CentralEllipseSample, EllipseSample from ...datasets import get_path from ...utils._optional_deps import HAS_SCIPY # noqa DATA = make_test_image(seed=0) DEFAULT_POS = 256 DEFAULT_FIX = np.array([False, False, False, False]) def test_gradient(): sample = EllipseSample(DATA, 40.) sample.update(DEFAULT_FIX) assert_allclose(sample.mean, 200.02, atol=0.01) assert_allclose(sample.gradient, -4.222, atol=0.001) assert_allclose(sample.gradient_error, 0.0003, atol=0.0001) assert_allclose(sample.gradient_relative_error, 7.45e-05, atol=1.e-5) assert_allclose(sample.sector_area, 2.00, atol=0.01) @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_raw(): """ This test performs a raw (no EllipseFitter), 1-step correction in one single ellipse coefficient. """ # pick first guess ellipse that is off in just # one of the parameters (eps). sample = EllipseSample(DATA, 40., eps=2*0.2) sample.update(DEFAULT_FIX) s = sample.extract() harmonics = fit_first_and_second_harmonics(s[0], s[2]) y0, a1, b1, a2, b2 = harmonics[0] # when eps is off, b2 is the largest (in absolute value). assert abs(b2) > abs(a1) assert abs(b2) > abs(b1) assert abs(b2) > abs(a2) correction = (b2 * 2. * (1. - sample.geometry.eps) / sample.geometry.sma / sample.gradient) new_eps = sample.geometry.eps - correction # got closer to test data (eps=0.2) assert_allclose(new_eps, 0.21, atol=0.01) @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_small_radii(): sample = EllipseSample(DATA, 2.) fitter = EllipseFitter(sample) isophote = fitter.fit() assert isinstance(isophote, Isophote) assert isophote.ndata == 13 @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_eps(): # initial guess is off in the eps parameter sample = EllipseSample(DATA, 40., eps=2*0.2) fitter = EllipseFitter(sample) isophote = fitter.fit() assert isinstance(isophote, Isophote) g = isophote.sample.geometry assert g.eps >= 0.19 assert g.eps <= 0.21 @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_pa(): data = make_test_image(pa=np.pi/4, noise=0.01, seed=0) # initial guess is off in the pa parameter sample = EllipseSample(data, 40) fitter = EllipseFitter(sample) isophote = fitter.fit() g = isophote.sample.geometry assert g.pa >= (np.pi/4 - 0.05) assert g.pa <= (np.pi/4 + 0.05) @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_xy(): pos = DEFAULT_POS - 5 data = make_test_image(x0=pos, y0=pos, seed=0) # initial guess is off in the x0 and y0 parameters sample = EllipseSample(data, 40) fitter = EllipseFitter(sample) isophote = fitter.fit() g = isophote.sample.geometry assert g.x0 >= (pos - 1) assert g.x0 <= (pos + 1) assert g.y0 >= (pos - 1) assert g.y0 <= (pos + 1) @pytest.mark.skipif('not HAS_SCIPY') def test_fitting_all(): # build test image that is off from the defaults # assumed by the EllipseSample constructor. pos = DEFAULT_POS - 5 angle = np.pi / 4 eps = 2 * 0.2 data = make_test_image(x0=pos, y0=pos, eps=eps, pa=angle, seed=0) sma = 60. # initial guess is off in all parameters. We find that the initial # guesses, especially for position angle, must be kinda close to the # actual value. 20% off max seems to work in this case of high SNR. sample = EllipseSample(data, sma, position_angle=(1.2 * angle)) fitter = EllipseFitter(sample) isophote = fitter.fit() assert isophote.stop_code == 0 g = isophote.sample.geometry assert g.x0 >= (pos - 1.5) # position within 1.5 pixel assert g.x0 <= (pos + 1.5) assert g.y0 >= (pos - 1.5) assert g.y0 <= (pos + 1.5) assert g.eps >= (eps - 0.01) # eps within 0.01 assert g.eps <= (eps + 0.01) assert g.pa >= (angle - 0.05) # pa within 5 deg assert g.pa <= (angle + 0.05) sample_m = EllipseSample(data, sma, position_angle=(1.2 * angle), integrmode=MEAN) fitter_m = EllipseFitter(sample_m) isophote_m = fitter_m.fit() assert isophote_m.stop_code == 0 @pytest.mark.remote_data @pytest.mark.skipif('not HAS_SCIPY') class TestM51: def setup_class(self): path = get_path('isophote/M51.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) self.data = hdu[0].data hdu.close() def test_m51(self): # here we evaluate the detailed convergence behavior # for a particular ellipse where we can see the eps # parameter jumping back and forth. # sample = EllipseSample(self.data, 13.31000001, eps=0.16, # position_angle=((-37.5+90)/180.*np.pi)) # sample.update() # fitter = EllipseFitter(sample) # isophote = fitter.fit() # we start the fit with initial values taken from # previous isophote, as determined by the old code. # sample taken in high SNR region sample = EllipseSample(self.data, 21.44, eps=0.18, position_angle=(36./180.*np.pi)) fitter = EllipseFitter(sample) isophote = fitter.fit() assert isophote.ndata == 119 assert_allclose(isophote.intens, 685.4, atol=0.1) # last sample taken by the original code, before turning inwards. sample = EllipseSample(self.data, 61.16, eps=0.219, position_angle=((77.5+90)/180*np.pi)) fitter = EllipseFitter(sample) isophote = fitter.fit() assert isophote.ndata == 382 assert_allclose(isophote.intens, 155.0, atol=0.1) def test_m51_outer(self): # sample taken at the outskirts of the image, so many # data points lay outside the image frame. This checks # for the presence of gaps in the sample arrays. sample = EllipseSample(self.data, 330., eps=0.2, position_angle=((90)/180*np.pi), integrmode='median') fitter = EllipseFitter(sample) isophote = fitter.fit() assert not np.any(isophote.sample.values[2] == 0) def test_m51_central(self): # this code finds central x and y offset by about 0.1 pixel wrt the # spp code. In here we use as input the position computed by this # code, thus this test is checking just the extraction algorithm. g = EllipseGeometry(257.02, 258.1, 0.0, 0.0, 0.0, 0.1, False) sample = CentralEllipseSample(self.data, 0.0, geometry=g) fitter = CentralEllipseFitter(sample) isophote = fitter.fit() # the central pixel intensity is about 3% larger than # found by the spp code. assert isophote.ndata == 1 assert isophote.intens <= 7560. assert isophote.intens >= 7550. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/isophote/tests/test_geometry.py0000644000214200020070000001140500000000000022472 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the geometry module. """ import numpy as np from numpy.testing import assert_allclose import pytest from ..geometry import EllipseGeometry @pytest.mark.parametrize('astep, linear_growth', [(0.2, False), (20., True)]) def test_geometry(astep, linear_growth): geometry = EllipseGeometry(255., 255., 100., 0.4, np.pi/2, astep, linear_growth) sma1, sma2 = geometry.bounding_ellipses() assert_allclose((sma1, sma2), (90.0, 110.0), atol=0.01) # using an arbitrary angle of 0.5 rad. This is to avoid a polar # vector that sits on top of one of the ellipse's axis. vertex_x, vertex_y = geometry.initialize_sector_geometry(0.6) assert_allclose(geometry.sector_angular_width, 0.0571, atol=0.01) assert_allclose(geometry.sector_area, 63.83, atol=0.01) assert_allclose(vertex_x, [215.4, 206.6, 213.5, 204.3], atol=0.1) assert_allclose(vertex_y, [316.1, 329.7, 312.5, 325.3], atol=0.1) def test_to_polar(): # trivial case of a circle centered in (0.,0.) geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 0.2, False) r, p = geometry.to_polar(100., 0.) assert_allclose(r, 100., atol=0.1) assert_allclose(p, 0., atol=0.0001) r, p = geometry.to_polar(0., 100.) assert_allclose(r, 100., atol=0.1) assert_allclose(p, np.pi/2., atol=0.0001) # vector with length 100. at 45 deg angle r, p = geometry.to_polar(70.71, 70.71) assert_allclose(r, 100., atol=0.1) assert_allclose(p, np.pi/4., atol=0.0001) # position angle tilted 45 deg from X axis geometry = EllipseGeometry(0., 0., 100., 0.0, np.pi/4., 0.2, False) r, p = geometry.to_polar(100., 0.) assert_allclose(r, 100., atol=0.1) assert_allclose(p, np.pi*7./4., atol=0.0001) r, p = geometry.to_polar(0., 100.) assert_allclose(r, 100., atol=0.1) assert_allclose(p, np.pi/4., atol=0.0001) # vector with length 100. at 45 deg angle r, p = geometry.to_polar(70.71, 70.71) assert_allclose(r, 100., atol=0.1) assert_allclose(p, np.pi*2., atol=0.0001) def test_area(): # circle with center at origin geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 0.2, False) # sector at 45 deg on circle vertex_x, vertex_y = geometry.initialize_sector_geometry(45./180.*np.pi) assert_allclose(vertex_x, [65.21, 79.70, 62.03, 75.81], atol=0.01) assert_allclose(vertex_y, [62.03, 75.81, 65.21, 79.70], atol=0.01) # sector at 0 deg on circle vertex_x, vertex_y = geometry.initialize_sector_geometry(0) assert_allclose(vertex_x, [89.97, 109.97, 89.97, 109.96], atol=0.01) assert_allclose(vertex_y, [-2.25, -2.75, 2.25, 2.75], atol=0.01) def test_area2(): # circle with center at 100.,100. geometry = EllipseGeometry(100., 100., 100., 0.0, 0., 0.2, False) # sector at 45 deg on circle vertex_x, vertex_y = geometry.initialize_sector_geometry(45./180.*np.pi) assert_allclose(vertex_x, [165.21, 179.70, 162.03, 175.81], atol=0.01) assert_allclose(vertex_y, [162.03, 175.81, 165.21, 179.70], atol=0.01) # sector at 225 deg on circle vertex_x, vertex_y = geometry.initialize_sector_geometry(225./180.*np.pi) assert_allclose(vertex_x, [34.79, 20.30, 37.97, 24.19], atol=0.01) assert_allclose(vertex_y, [37.97, 24.19, 34.79, 20.30], atol=0.01) def test_reset_sma(): geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 0.2, False) sma, step = geometry.reset_sma(0.2) assert_allclose(sma, 83.33, atol=0.01) assert_allclose(step, -0.1666, atol=0.001) geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 20., True) sma, step = geometry.reset_sma(20.) assert_allclose(sma, 80., atol=0.01) assert_allclose(step, -20., atol=0.01) def test_update_sma(): geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 0.2, False) sma = geometry.update_sma(0.2) assert_allclose(sma, 120., atol=0.01) geometry = EllipseGeometry(0., 0., 100., 0.0, 0., 20., True) sma = geometry.update_sma(20.) assert_allclose(sma, 120., atol=0.01) def test_polar_angle_sector_limits(): geometry = EllipseGeometry(0., 0., 100., 0.3, np.pi/4, 0.2, False) geometry.initialize_sector_geometry(np.pi/3) phi1, phi2 = geometry.polar_angle_sector_limits() assert_allclose(phi1, 1.022198, atol=0.0001) assert_allclose(phi2, 1.072198, atol=0.0001) def test_bounding_ellipses(): geometry = EllipseGeometry(0., 0., 100., 0.3, np.pi/4, 0.2, False) sma1, sma2 = geometry.bounding_ellipses() assert_allclose((sma1, sma2), (90.0, 110.0), atol=0.01) def test_radius(): geometry = EllipseGeometry(0., 0., 100., 0.3, np.pi/4, 0.2, False) r = geometry.radius(0.0) assert_allclose(r, 100.0, atol=0.01) r = geometry.radius(np.pi/2) assert_allclose(r, 70.0, atol=0.01) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_harmonics.py0000644000214200020070000001500400000000000022621 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the harmonics module. """ import numpy as np from numpy.testing import assert_allclose import pytest from .make_test_data import make_test_image from ..harmonics import (first_and_second_harmonic_function, fit_first_and_second_harmonics, fit_upper_harmonic) from ..sample import EllipseSample from ..fitter import EllipseFitter from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') def test_harmonics_1(): from scipy.optimize import leastsq # noqa # this is an almost as-is example taken from stackoverflow N = 100 # number of data points t = np.linspace(0, 4*np.pi, N) # create artificial data with noise: # mean = 0.5, amplitude = 3., phase = 0.1, noise-std = 0.01 rng = np.random.default_rng(0) data = 3.0 * np.sin(t + 0.1) + 0.5 + 0.01 * rng.standard_normal(N) # first guesses for harmonic parameters guess_mean = np.mean(data) guess_std = 3 * np.std(data) / 2**0.5 guess_phase = 0 # Minimize the difference between the actual data and our "guessed" # parameters # optimize_func = lambda x: x[0] * np.sin(t + x[1]) + x[2] - data def optimize_func(x): return x[0] * np.sin(t + x[1]) + x[2] - data est_std, est_phase, est_mean = leastsq( optimize_func, [guess_std, guess_phase, guess_mean])[0] # recreate the fitted curve using the optimized parameters data_fit = est_std * np.sin(t + est_phase) + est_mean residual = data - data_fit assert_allclose(np.mean(residual), 0., atol=0.001) assert_allclose(np.std(residual), 0.01, atol=0.01) @pytest.mark.skipif('not HAS_SCIPY') def test_harmonics_2(): # this uses the actual functional form used for fitting ellipses N = 100 E = np.linspace(0, 4*np.pi, N) y0_0 = 100. a1_0 = 10. b1_0 = 5. a2_0 = 8. b2_0 = 2. rng = np.random.default_rng(0) data = (y0_0 + a1_0*np.sin(E) + b1_0*np.cos(E) + a2_0*np.sin(2*E) + b2_0*np.cos(2*E) + 0.01*rng.standard_normal(N)) harmonics = fit_first_and_second_harmonics(E, data) y0, a1, b1, a2, b2 = harmonics[0] data_fit = (y0 + a1*np.sin(E) + b1*np.cos(E) + a2*np.sin(2*E) + b2*np.cos(2*E) + 0.01*rng.standard_normal(N)) residual = data - data_fit assert_allclose(np.mean(residual), 0., atol=0.01) assert_allclose(np.std(residual), 0.015, atol=0.01) @pytest.mark.skipif('not HAS_SCIPY') def test_harmonics_3(): """Tests an upper harmonic fit.""" N = 100 E = np.linspace(0, 4*np.pi, N) y0_0 = 100. a1_0 = 10. b1_0 = 5. order = 3 rng = np.random.default_rng(0) data = (y0_0 + a1_0*np.sin(order*E) + b1_0*np.cos(order*E) + 0.01*rng.standard_normal(N)) harmonic = fit_upper_harmonic(E, data, order) y0, a1, b1 = harmonic[0] rng = np.random.default_rng(0) data_fit = (y0 + a1*np.sin(order*E) + b1*np.cos(order*E) + 0.01*rng.standard_normal(N)) residual = data - data_fit assert_allclose(np.mean(residual), 0., atol=0.01) assert_allclose(np.std(residual), 0.015, atol=0.014) @pytest.mark.skipif('not HAS_SCIPY') class TestFitEllipseSamples: def setup_class(self): # major axis parallel to X image axis self.data1 = make_test_image(seed=0) # major axis tilted 45 deg wrt X image axis self.data2 = make_test_image(pa=np.pi/4, seed=0) def test_fit_ellipsesample_1(self): sample = EllipseSample(self.data1, 40.) s = sample.extract() harmonics = fit_first_and_second_harmonics(s[0], s[2]) y0, a1, b1, a2, b2 = harmonics[0] assert_allclose(np.mean(y0), 200.019, atol=0.001) assert_allclose(np.mean(a1), -0.000138, atol=0.001) assert_allclose(np.mean(b1), 0.000254, atol=0.001) assert_allclose(np.mean(a2), -5.658e-05, atol=0.001) assert_allclose(np.mean(b2), -0.00911, atol=0.001) # check that harmonics subtract nicely model = first_and_second_harmonic_function( s[0], np.array([y0, a1, b1, a2, b2])) residual = s[2] - model assert_allclose(np.mean(residual), 0., atol=0.001) assert_allclose(np.std(residual), 0.015, atol=0.01) def test_fit_ellipsesample_2(self): # initial guess is rounder than actual image sample = EllipseSample(self.data1, 40., eps=0.1) s = sample.extract() harmonics = fit_first_and_second_harmonics(s[0], s[2]) y0, a1, b1, a2, b2 = harmonics[0] assert_allclose(np.mean(y0), 188.686, atol=0.001) assert_allclose(np.mean(a1), 0.000283, atol=0.001) assert_allclose(np.mean(b1), 0.00692, atol=0.001) assert_allclose(np.mean(a2), -0.000215, atol=0.001) assert_allclose(np.mean(b2), 10.153, atol=0.001) def test_fit_ellipsesample_3(self): # initial guess for center is offset sample = EllipseSample(self.data1, x0=220., y0=210., sma=40.) s = sample.extract() harmonics = fit_first_and_second_harmonics(s[0], s[2]) y0, a1, b1, a2, b2 = harmonics[0] assert_allclose(np.mean(y0), 152.660, atol=0.001) assert_allclose(np.mean(a1), 55.338, atol=0.001) assert_allclose(np.mean(b1), 33.091, atol=0.001) assert_allclose(np.mean(a2), 33.036, atol=0.001) assert_allclose(np.mean(b2), -14.306, atol=0.001) def test_fit_ellipsesample_4(self): sample = EllipseSample(self.data2, 40., eps=0.4) s = sample.extract() harmonics = fit_first_and_second_harmonics(s[0], s[2]) y0, a1, b1, a2, b2 = harmonics[0] assert_allclose(np.mean(y0), 245.102, atol=0.001) assert_allclose(np.mean(a1), -0.003108, atol=0.001) assert_allclose(np.mean(b1), -0.0578, atol=0.001) assert_allclose(np.mean(a2), 28.781, atol=0.001) assert_allclose(np.mean(b2), -63.184, atol=0.001) def test_fit_upper_harmonics(self): data = make_test_image(noise=1.e-10, seed=0) sample = EllipseSample(data, 40) fitter = EllipseFitter(sample) iso = fitter.fit(maxit=400) assert_allclose(iso.a3, -6.825e-7, atol=1.e-9) assert_allclose(iso.b3, 1.68e-6, atol=1.e-8) assert_allclose(iso.a4, -4.36e-6, atol=1.e-8) assert_allclose(iso.b4, 4.73e-5, atol=1.e-7) assert_allclose(iso.a3_err, 8.152e-6, atol=1.e-7) assert_allclose(iso.b3_err, 8.115e-6, atol=1.e-7) assert_allclose(iso.a4_err, 7.501e-6, atol=1.e-7) assert_allclose(iso.b4_err, 7.473e-6, atol=1.e-7) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/isophote/tests/test_integrator.py0000644000214200020070000001204300000000000023014 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the integrator module. """ from astropy.io import fits import numpy as np import numpy.ma as ma from numpy.testing import assert_allclose import pytest from ..integrator import BILINEAR, MEAN, MEDIAN, NEAREST_NEIGHBOR from ..sample import EllipseSample from ...datasets import get_path @pytest.mark.remote_data class TestData: def setup_class(self): path = get_path('isophote/synth_highsnr.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) self.data = hdu[0].data hdu.close() def make_sample(self, masked=False, sma=40., integrmode=BILINEAR): if masked: data = ma.masked_values(self.data, 200., atol=10.0, rtol=0.) else: data = self.data sample = EllipseSample(data, sma, integrmode=integrmode) s = sample.extract() assert len(s) == 3 assert len(s[0]) == len(s[1]) assert len(s[0]) == len(s[2]) return s, sample @pytest.mark.remote_data class TestUnmasked(TestData): def test_bilinear(self): s, sample = self.make_sample() assert len(s[0]) == 225 # intensities assert_allclose(np.mean(s[2]), 200.76, atol=0.01) assert_allclose(np.std(s[2]), 21.55, atol=0.01) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.0, atol=0.01) assert sample.total_points == 225 assert sample.actual_points == 225 def test_bilinear_small(self): # small radius forces sub-pixel sampling s, sample = self.make_sample(sma=10.) # intensities assert_allclose(np.mean(s[2]), 1045.4, atol=0.1) assert_allclose(np.std(s[2]), 143.0, atol=0.1) # radii assert_allclose(np.max(s[1]), 10.0, atol=0.1) assert_allclose(np.min(s[1]), 8.0, atol=0.1) assert sample.total_points == 57 assert sample.actual_points == 57 def test_nearest_neighbor(self): s, sample = self.make_sample(integrmode=NEAREST_NEIGHBOR) assert len(s[0]) == 225 # intensities assert_allclose(np.mean(s[2]), 201.1, atol=0.1) assert_allclose(np.std(s[2]), 21.8, atol=0.1) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.0, atol=0.01) assert sample.total_points == 225 assert sample.actual_points == 225 def test_mean(self): s, sample = self.make_sample(integrmode=MEAN) assert len(s[0]) == 64 # intensities assert_allclose(np.mean(s[2]), 199.9, atol=0.1) assert_allclose(np.std(s[2]), 21.3, atol=0.1) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.0, atol=0.01) assert_allclose(sample.sector_area, 12.4, atol=0.1) assert sample.total_points == 64 assert sample.actual_points == 64 def test_mean_small(self): s, sample = self.make_sample(sma=5., integrmode=MEAN) assert len(s[0]) == 29 # intensities assert_allclose(np.mean(s[2]), 2339.0, atol=0.1) assert_allclose(np.std(s[2]), 284.7, atol=0.1) # radii assert_allclose(np.max(s[1]), 5.0, atol=0.01) assert_allclose(np.min(s[1]), 4.0, atol=0.01) assert_allclose(sample.sector_area, 2.0, atol=0.1) assert sample.total_points == 29 assert sample.actual_points == 29 def test_median(self): s, sample = self.make_sample(integrmode=MEDIAN) assert len(s[0]) == 64 # intensities assert_allclose(np.mean(s[2]), 199.9, atol=0.1) assert_allclose(np.std(s[2]), 21.3, atol=0.1) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.01, atol=0.01) assert_allclose(sample.sector_area, 12.4, atol=0.1) assert sample.total_points == 64 assert sample.actual_points == 64 @pytest.mark.remote_data class TestMasked(TestData): def test_bilinear(self): s, sample = self.make_sample(masked=True, integrmode=BILINEAR) assert len(s[0]) == 157 # intensities assert_allclose(np.mean(s[2]), 201.52, atol=0.01) assert_allclose(np.std(s[2]), 25.21, atol=0.01) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.0, atol=0.01) assert sample.total_points == 225 assert sample.actual_points == 157 def test_mean(self): s, sample = self.make_sample(masked=True, integrmode=MEAN) assert len(s[0]) == 51 # intensities assert_allclose(np.mean(s[2]), 199.9, atol=0.1) assert_allclose(np.std(s[2]), 24.12, atol=0.1) # radii assert_allclose(np.max(s[1]), 40.0, atol=0.01) assert_allclose(np.min(s[1]), 32.0, atol=0.01) assert_allclose(sample.sector_area, 12.4, atol=0.1) assert sample.total_points == 64 assert sample.actual_points == 51 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_isophote.py0000644000214200020070000002317100000000000022474 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the isophote module. """ from astropy.io import fits import numpy as np from numpy.testing import assert_allclose import pytest from .make_test_data import make_test_image from ..ellipse import Ellipse from ..fitter import EllipseFitter from ..geometry import EllipseGeometry from ..isophote import Isophote, IsophoteList from ..sample import EllipseSample from ...datasets import get_path from ...utils._optional_deps import HAS_SCIPY # noqa DEFAULT_FIX = np.array([False, False, False, False]) @pytest.mark.remote_data @pytest.mark.skipif('not HAS_SCIPY') class TestIsophote: def setup_class(self): path = get_path('isophote/M51.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) self.data = hdu[0].data hdu.close() def test_fit(self): # low noise image, fitted perfectly by sample data = make_test_image(noise=1.e-10, seed=0) sample = EllipseSample(data, 40) fitter = EllipseFitter(sample) iso = fitter.fit(maxit=400) assert iso.valid assert iso.stop_code == 0 or iso.stop_code == 2 # fitted values assert iso.intens <= 201. assert iso.intens >= 199. assert iso.int_err <= 0.0010 assert iso.int_err >= 0.0009 assert iso.pix_stddev <= 0.03 assert iso.pix_stddev >= 0.02 assert abs(iso.grad) <= 4.25 assert abs(iso.grad) >= 4.20 # integrals assert iso.tflux_e <= 1.85E6 assert iso.tflux_e >= 1.82E6 assert iso.tflux_c <= 2.025E6 assert iso.tflux_c >= 2.022E6 # deviations from perfect ellipticity. Note # that sometimes a None covariance can be # generated by scipy.optimize.leastsq assert iso.a3 is None or abs(iso.a3) <= 0.01 assert iso.b3 is None or abs(iso.b3) <= 0.01 assert iso.a4 is None or abs(iso.a4) <= 0.01 assert iso.b4 is None or abs(iso.b4) <= 0.01 def test_m51(self): sample = EllipseSample(self.data, 21.44) fitter = EllipseFitter(sample) iso = fitter.fit() assert iso.valid assert iso.stop_code == 0 or iso.stop_code == 2 # geometry g = iso.sample.geometry assert g.x0 >= (257 - 1.5) # position within 1.5 pixel assert g.x0 <= (257 + 1.5) assert g.y0 >= (259 - 1.5) assert g.y0 <= (259 + 2.0) assert g.eps >= (0.19 - 0.05) # eps within 0.05 assert g.eps <= (0.19 + 0.05) assert g.pa >= (0.62 - 0.05) # pa within 5 deg assert g.pa <= (0.62 + 0.05) # fitted values assert_allclose(iso.intens, 682.9, atol=0.1) assert_allclose(iso.rms, 83.27, atol=0.01) assert_allclose(iso.int_err, 7.63, atol=0.01) assert_allclose(iso.pix_stddev, 117.8, atol=0.1) assert_allclose(iso.grad, -36.08, atol=0.1) # integrals assert iso.tflux_e <= 1.20e6 assert iso.tflux_e >= 1.19e6 assert iso.tflux_c <= 1.38e6 assert iso.tflux_c >= 1.36e6 # deviations from perfect ellipticity. Note # that sometimes a None covariance can be # generated by scipy.optimize.leastsq assert iso.a3 is None or abs(iso.a3) <= 0.05 assert iso.b3 is None or abs(iso.b3) <= 0.05 assert iso.a4 is None or abs(iso.a4) <= 0.05 assert iso.b4 is None or abs(iso.b4) <= 0.05 def test_m51_niter(self): # compares with old STSDAS task. In this task, the # default for the starting value of SMA is 10; it # fits with 20 iterations. sample = EllipseSample(self.data, 10) fitter = EllipseFitter(sample) iso = fitter.fit() assert iso.valid assert iso.niter == 50 class TestIsophoteList: def setup_class(self): data = make_test_image(seed=0) self.slen = 5 self.isolist_sma10 = self.build_list(data, sma0=10., slen=self.slen) self.isolist_sma100 = self.build_list(data, sma0=100., slen=self.slen) self.isolist_sma200 = self.build_list(data, sma0=200., slen=self.slen) @staticmethod def build_list(data, sma0, slen=5): iso_list = [] for k in range(slen): sample = EllipseSample(data, float(k + sma0)) sample.update(DEFAULT_FIX) iso_list.append(Isophote(sample, k, True, 0)) result = IsophoteList(iso_list) return result def test_basic_list(self): # make sure it can be indexed as a list. result = self.isolist_sma10[:] assert isinstance(result[0], Isophote) # make sure the important arrays contain floats. # especially the sma array, which is derived # from a property in the Isophote class. assert isinstance(result.sma, np.ndarray) assert isinstance(result.sma[0], float) assert isinstance(result.intens, np.ndarray) assert isinstance(result.intens[0], float) assert isinstance(result.rms, np.ndarray) assert isinstance(result.int_err, np.ndarray) assert isinstance(result.pix_stddev, np.ndarray) assert isinstance(result.grad, np.ndarray) assert isinstance(result.grad_error, np.ndarray) assert isinstance(result.grad_r_error, np.ndarray) assert isinstance(result.sarea, np.ndarray) assert isinstance(result.niter, np.ndarray) assert isinstance(result.ndata, np.ndarray) assert isinstance(result.nflag, np.ndarray) assert isinstance(result.valid, np.ndarray) assert isinstance(result.stop_code, np.ndarray) assert isinstance(result.tflux_c, np.ndarray) assert isinstance(result.tflux_e, np.ndarray) assert isinstance(result.npix_c, np.ndarray) assert isinstance(result.npix_e, np.ndarray) assert isinstance(result.a3, np.ndarray) assert isinstance(result.a4, np.ndarray) assert isinstance(result.b3, np.ndarray) assert isinstance(result.b4, np.ndarray) samples = result.sample assert isinstance(samples, list) assert isinstance(samples[0], EllipseSample) iso = result.get_closest(13.6) assert isinstance(iso, Isophote) assert_allclose(iso.sma, 14., atol=1e-6) def test_extend(self): # the extend method shouldn't return anything, # and should modify the first list in place. inner_list = self.isolist_sma10[:] outer_list = self.isolist_sma100[:] assert len(inner_list) == self.slen assert len(outer_list) == self.slen inner_list.extend(outer_list) assert len(inner_list) == 2 * self.slen # the __iadd__ operator should behave like the # extend method. inner_list = self.isolist_sma10[:] outer_list = self.isolist_sma100[:] inner_list += outer_list assert len(inner_list) == 2 * self.slen # the __add__ operator should create a new IsophoteList # instance with the result, and should not modify # the operands. inner_list = self.isolist_sma10[:] outer_list = self.isolist_sma100[:] result = inner_list + outer_list assert isinstance(result, IsophoteList) assert len(inner_list) == self.slen assert len(outer_list) == self.slen assert len(result) == 2 * self.slen def test_slicing(self): iso_list = self.isolist_sma10[:] assert len(iso_list) == self.slen assert len(iso_list[1:-1]) == self.slen - 2 assert len(iso_list[2:-2]) == self.slen - 4 def test_combined(self): # combine extend with slicing. inner_list = self.isolist_sma10[:] outer_list = self.isolist_sma100[:] sublist = inner_list[2:-2] dummy = sublist.extend(outer_list) assert not dummy assert len(sublist) == 2*self.slen - 4 # try one more slice. even_outer_list = self.isolist_sma200 sublist.extend(even_outer_list[1:-1]) assert len(sublist) == 2*self.slen - 4 + 3 # combine __add__ with slicing. sublist = inner_list[2:-2] result = sublist + outer_list assert isinstance(result, IsophoteList) assert len(sublist) == self.slen - 4 assert len(result) == 2*self.slen - 4 result = inner_list[2:-2] + outer_list assert isinstance(result, IsophoteList) assert len(result) == 2*self.slen - 4 def test_sort(self): inner_list = self.isolist_sma10[:] outer_list = self.isolist_sma100[:] result = outer_list[2:-2] + inner_list assert result[-1].sma < result[0].sma result.sort() assert result[-1].sma > result[0].sma @pytest.mark.skipif('not HAS_SCIPY') def test_to_table(self): test_img = make_test_image(nx=55, ny=55, x0=27, y0=27, background=100., noise=1.e-6, i0=100., sma=10., eps=0.2, pa=0., seed=1) g = EllipseGeometry(27, 27, 5, 0.2, 0) ellipse = Ellipse(test_img, geometry=g, threshold=0.1) isolist = ellipse.fit_image(maxsma=27) assert len(isolist.get_names()) >= 30 # test for get_names tbl = isolist.to_table() assert len(tbl.colnames) == 18 tbl = isolist.to_table(columns='all') assert len(tbl.colnames) >= 30 tbl = isolist.to_table(columns='main') assert len(tbl.colnames) == 18 tbl = isolist.to_table(columns=['sma']) assert len(tbl.colnames) == 1 tbl = isolist.to_table(columns=['tflux_e', 'tflux_c', 'npix_e', 'npix_c']) assert len(tbl.colnames) == 4 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_model.py0000644000214200020070000000526500000000000021746 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the model module. """ from astropy.io import fits import numpy as np import os.path as op import pytest from .make_test_data import make_test_image from ..ellipse import Ellipse from ..geometry import EllipseGeometry from ..model import build_ellipse_model from ...datasets import get_path from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.remote_data @pytest.mark.skipif('not HAS_SCIPY') def test_model(): path = get_path('isophote/M105-S001-RGB.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) data = hdu[0].data[0] hdu.close() g = EllipseGeometry(530., 511, 10., 0.1, 10./180.*np.pi) ellipse = Ellipse(data, geometry=g, threshold=1.e5) isophote_list = ellipse.fit_image() model = build_ellipse_model(data.shape, isophote_list, fill=np.mean(data[10:100, 10:100])) assert data.shape == model.shape residual = data - model assert np.mean(residual) <= 5.0 assert np.mean(residual) >= -5.0 @pytest.mark.skipif('not HAS_SCIPY') def test_model_simulated_data(): data = make_test_image(nx=200, ny=200, i0=10., sma=5., eps=0.5, pa=np.pi/3., noise=0.05, seed=0) g = EllipseGeometry(100., 100., 5., 0.5, np.pi/3.) ellipse = Ellipse(data, geometry=g, threshold=1.e5) isophote_list = ellipse.fit_image() model = build_ellipse_model(data.shape, isophote_list, fill=np.mean(data[0:50, 0:50])) assert data.shape == model.shape residual = data - model assert np.mean(residual) <= 5.0 assert np.mean(residual) >= -5.0 @pytest.mark.skipif('not HAS_SCIPY') def test_model_minimum_radius(): # This test requires a "defective" image that drives the # model building algorithm into a corner, where it fails. # With the algorithm fixed, it bypasses the failure and # succeeds in building the model image. filepath = op.join(op.dirname(op.abspath(__file__)), 'data', 'minimum_radius_test.fits') hdu = fits.open(filepath) data = hdu[0].data g = EllipseGeometry(50., 45, 530., 0.1, 10. / 180. * np.pi) g.find_center(data) ellipse = Ellipse(data, geometry=g) isophote_list = ellipse.fit_image(sma0=40, minsma=0, maxsma=350., step=0.4, nclip=3) model = build_ellipse_model(data.shape, isophote_list, fill=np.mean(data[0:50, 0:50])) # It's enough that the algorithm reached this point. The # actual accuracy of the modelling is being tested elsewhere. assert data.shape == model.shape ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/isophote/tests/test_regression.py0000644000214200020070000001724200000000000023024 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Despite being cast as a unit test, this code implements regression testing of the Ellipse algorithm, against results obtained by the stsdas$analysis/isophote task 'ellipse'. The stsdas task was run on test images and results were stored in tables. The code here runs the Ellipse algorithm on the same images, producing a list of Isophote instances. The contents of this list then get compared with the contents of the corresponding table. Some quantities are compared in assert statements. These were designed to be executed only when the synth_highsnr.fits image is used as input. That way, we are mainly checking numerical differences that originate in the algorithms themselves, and not caused by noise. The quantities compared this way are: - mean intensity: less than 1% diff. for sma > 3 pixels, 5% otherwise - ellipticity: less than 1% diff. for sma > 3 pixels, 20% otherwise - position angle: less than 1 deg. diff. for sma > 3 pixels, 20 deg. otherwise - X and Y position: less than 0.2 pixel diff. For the M51 image we have mostly good agreement with the SPP code in most of the parameters (mean isophotal intensity agrees within a fraction of 1% mostly), but every now and then the ellipticity and position angle of the semi-major axis may differ by a large amount from what the SPP code measures. The code also stops prematurely wrt the larger sma values measured by the SPP code. This is caused by a difference in the way the gradient relative error is measured in each case, and suggests that the SPP code may have a bug. The not-so-good behavior observed in the case of the M51 image is to be expected though. This image is exactly the type of galaxy image for which the algorithm *wasn't* designed for. It has an almost negligible smooth ellipsoidal component, and a lot of lumpy spiral structure that causes the radial gradient computation to go berserk. On top of that, the ellipticity is small (roundish isophotes) throughout the image, causing large relative errors and instability in the fitting algorithm. For now, we can only check the bilinear integration mode. The mean and median modes cannot be checked since the original 'ellipse' task has a bug that causes the creation of erroneous output tables. A partial comparison could be made if we write new code that reads the standard output of 'ellipse' instead, captured from screen, and use it as reference for the regression. """ import math import os.path as op from astropy.io import fits from astropy.table import Table import numpy as np import pytest from ..ellipse import Ellipse from ..integrator import BILINEAR from ...datasets import get_path from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.remote_data @pytest.mark.skipif('not HAS_SCIPY') # @pytest.mark.parametrize('name', ['M51', 'synth', 'synth_lowsnr', # 'synth_highsnr']) @pytest.mark.parametrize('name', ['synth_highsnr']) def test_regression(name, integrmode=BILINEAR, verbose=False): """ NOTE: The original code in SPP won't create the right table for the MEAN integration moder, so use the screen output at synth_table_mean.txt to compare results visually with synth_table_mean.fits. """ filename = f'{name}_table.fits' path = op.join(op.dirname(op.abspath(__file__)), 'data', filename) table = Table.read(path) nrows = len(table['SMA']) path = get_path(f'isophote/{name}.fits', location='photutils-datasets', cache=True) hdu = fits.open(path) data = hdu[0].data hdu.close() ellipse = Ellipse(data) isophote_list = ellipse.fit_image() # isophote_list = ellipse.fit_image(sclip=2., nclip=3) fmt = ("%5.2f %6.1f %8.3f %8.3f %8.3f %9.5f %6.2f " "%6.2f %6.2f %5.2f %4d %3d %3d %2d") for row in range(nrows): try: iso = isophote_list[row] except IndexError: # skip non-existent rows in isophote list, if that's the case. break # data from Isophote sma_i = iso.sample.geometry.sma intens_i = iso.intens int_err_i = iso.int_err if iso.int_err else 0. pix_stddev_i = iso.pix_stddev if iso.pix_stddev else 0. rms_i = iso.rms if iso.rms else 0. ellip_i = iso.sample.geometry.eps if iso.sample.geometry.eps else 0. pa_i = iso.sample.geometry.pa if iso.sample.geometry.pa else 0. x0_i = iso.sample.geometry.x0 y0_i = iso.sample.geometry.y0 rerr_i = (iso.sample.gradient_relative_error if iso.sample.gradient_relative_error else 0.) ndata_i = iso.ndata nflag_i = iso.nflag niter_i = iso.niter stop_i = iso.stop_code # convert to old code reference system pa_i = (pa_i - np.pi/2) / np.pi * 180. x0_i += 1 y0_i += 1 # ref data from table sma_t = table['SMA'][row] intens_t = table['INTENS'][row] int_err_t = table['INT_ERR'][row] pix_stddev_t = table['PIX_VAR'][row] rms_t = table['RMS'][row] ellip_t = table['ELLIP'][row] pa_t = table['PA'][row] x0_t = table['X0'][row] y0_t = table['Y0'][row] rerr_t = table['GRAD_R_ERR'][row] ndata_t = table['NDATA'][row] nflag_t = table['NFLAG'][row] niter_t = table['NITER'][row] if table['NITER'][row] else 0 stop_t = table['STOP'][row] if table['STOP'][row] else -1 # relative differences sma_d = (sma_i - sma_t) / sma_t * 100. if sma_t > 0. else 0. intens_d = (intens_i - intens_t) / intens_t * 100. int_err_d = ((int_err_i - int_err_t) / int_err_t * 100. if int_err_t > 0. else 0.) pix_stddev_d = ((pix_stddev_i - pix_stddev_t) / pix_stddev_t * 100. if pix_stddev_t > 0. else 0.) rms_d = (rms_i - rms_t) / rms_t * 100. if rms_t > 0. else 0. ellip_d = (ellip_i - ellip_t) / ellip_t * 100. pa_d = pa_i - pa_t # diff in angle is absolute x0_d = x0_i - x0_t # diff in position is absolute y0_d = y0_i - y0_t rerr_d = rerr_i - rerr_t # diff in relative error is absolute ndata_d = (ndata_i - ndata_t) / ndata_t * 100. nflag_d = 0 niter_d = 0 stop_d = 0 if stop_i == stop_t else -1 if verbose: print("* data " + fmt % (sma_i, intens_i, int_err_i, pix_stddev_i, rms_i, ellip_i, pa_i, x0_i, y0_i, rerr_i, ndata_i, nflag_i, niter_i, stop_i)) print(" ref " + fmt % (sma_t, intens_t, int_err_t, pix_stddev_t, rms_t, ellip_t, pa_t, x0_t, y0_t, rerr_t, ndata_t, nflag_t, niter_t, stop_t)) print(" diff " + fmt % (sma_d, intens_d, int_err_d, pix_stddev_d, rms_d, ellip_d, pa_d, x0_d, y0_d, rerr_d, ndata_d, nflag_d, niter_d, stop_d)) print() if name == "synth_highsnr" and integrmode == BILINEAR: assert abs(x0_d) <= 0.21 assert abs(y0_d) <= 0.21 if sma_i > 3.: assert abs(intens_d) <= 1. else: assert abs(intens_d) <= 5. if not math.isnan(ellip_d): if sma_i > 3.: assert abs(ellip_d) <= 1. # 1% else: assert abs(ellip_d) <= 20. # 20% if not math.isnan(pa_d): if sma_i > 3.: assert abs(pa_d) <= 1. # 1 deg. else: assert abs(pa_d) <= 20. # 20 deg. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/photutils/isophote/tests/test_sample.py0000644000214200020070000000325100000000000022120 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the sample module. """ import numpy as np import pytest from .make_test_data import make_test_image from ..integrator import BILINEAR, MEAN, MEDIAN, NEAREST_NEIGHBOR from ..isophote import Isophote from ..sample import EllipseSample DEFAULT_FIX = np.array([False, False, False, False]) DATA = make_test_image(background=100., i0=0., noise=10., seed=0) # the median is not so good at estimating rms @pytest.mark.parametrize('integrmode, amin, amax', [(NEAREST_NEIGHBOR, 7., 15.), (BILINEAR, 7., 15.), (MEAN, 7., 15.), (MEDIAN, 6., 15.)]) def test_scatter(integrmode, amin, amax): """ Check that the pixel standard deviation can be reliably estimated from the rms scatter and the sector area. The test data is just a flat image with noise, no galaxy. We define the noise rms and then compare how close the pixel std dev estimated at extraction matches this input noise. """ sample = EllipseSample(DATA, 50., astep=0.2, integrmode=integrmode) sample.update(DEFAULT_FIX) iso = Isophote(sample, 0, True, 0) assert iso.pix_stddev < amax assert iso.pix_stddev > amin def test_coordinates(): sample = EllipseSample(DATA, 50.) sample.update(DEFAULT_FIX) x, y = sample.coordinates() assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) def test_sclip(): sample = EllipseSample(DATA, 50., nclip=3) sample.update(DEFAULT_FIX) x, y = sample.coordinates() assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0106983 photutils-1.3.0/photutils/morphology/0000755000214200020070000000000000000000000016430 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/morphology/__init__.py0000644000214200020070000000036600000000000020546 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for measuring morphological properties of objects in an astronomical image. """ from .core import * # noqa from .non_parametric import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/morphology/core.py0000644000214200020070000000355100000000000017736 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for measuring morphological properties of sources. """ import numpy as np __all__ = ['data_properties'] def data_properties(data, mask=None, background=None): """ Calculate the morphological properties (and centroid) of a 2D array (e.g., an image cutout of an object) using image moments. Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array of the image. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. background : float, array_like, or `~astropy.units.Quantity`, optional The background level that was previously present in the input ``data``. ``background`` may either be a scalar value or a 2D image with the same shape as the input ``data``. Inputting the ``background`` merely allows for its properties to be measured within each source segment. The input ``background`` does *not* get subtracted from the input ``data``, which should already be background-subtracted. Returns ------- result : `~photutils.segmentation.SourceCatalog` instance A `~photutils.segmentation.SourceCatalog` object. """ # prevent circular imports from ..segmentation import SegmentationImage, SourceCatalog segment_image = SegmentationImage(np.ones(data.shape, dtype=int)) if background is not None: background = np.atleast_1d(background) if background.shape == (1,): background = np.zeros(data.shape) + background return SourceCatalog(data, segment_image, mask=mask, background=background)[0] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/morphology/non_parametric.py0000644000214200020070000000352400000000000022007 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides functions for measuring non-parametric morphologies of sources. """ import numpy as np __all__ = ['gini'] def gini(data): r""" Calculate the `Gini coefficient `_ of a 2D array. The Gini coefficient is calculated using the prescription from `Lotz et al. 2004 `_ as: .. math:: G = \frac{1}{\left | \bar{x} \right | n (n - 1)} \sum^{n}_{i} (2i - n - 1) \left | x_i \right | where :math:`\bar{x}` is the mean over all pixel values :math:`x_i`. The Gini coefficient is a way of measuring the inequality in a given set of values. In the context of galaxy morphology, it measures how the light of a galaxy image is distributed among its pixels. A Gini coefficient value of 0 corresponds to a galaxy image with the light evenly distributed over all pixels while a Gini coefficient value of 1 represents a galaxy image with all its light concentrated in just one pixel. Usually Gini's measurement needs some sort of preprocessing for defining the galaxy region in the image based on the quality of the input data. As there is not a general standard for doing this, this is left for the user. Parameters ---------- data : array-like The 2D data array or object that can be converted to an array. Returns ------- gini : `float` The Gini coefficient of the input 2D array. """ flattened = np.sort(np.ravel(data)) npix = np.size(flattened) normalization = np.abs(np.mean(flattened)) * npix * (npix - 1) kernel = (2. * np.arange(1, npix + 1) - npix - 1) * np.abs(flattened) return np.sum(kernel) / normalization ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0114696 photutils-1.3.0/photutils/morphology/tests/0000755000214200020070000000000000000000000017572 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/morphology/tests/__init__.py0000644000214200020070000000000000000000000021671 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/morphology/tests/test_core.py0000644000214200020070000000255100000000000022136 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the core module. """ import numpy as np from numpy.testing import assert_allclose import pytest from ..core import data_properties from ...utils._optional_deps import HAS_SKIMAGE # noqa XCS = [25.7] YCS = [26.2] XSTDDEVS = [3.2, 4.0] YSTDDEVS = [5.7, 4.1] THETAS = np.array([30., 45.]) * np.pi / 180. DATA = np.zeros((3, 3)) DATA[0:2, 1] = 1. DATA[1, 0:2] = 1. DATA[1, 1] = 2. @pytest.mark.skipif('not HAS_SKIMAGE') def test_data_properties(): data = np.ones((2, 2)).astype(float) mask = np.array([[False, False], [True, True]]) props = data_properties(data, mask=None) props2 = data_properties(data, mask=mask) properties = ['xcentroid', 'ycentroid'] result = [getattr(props, i) for i in properties] result2 = [getattr(props2, i) for i in properties] assert_allclose([0.5, 0.5], result, rtol=0, atol=1.e-6) assert_allclose([0.5, 0.0], result2, rtol=0, atol=1.e-6) assert props.area.value == 4.0 assert props2.area.value == 2.0 @pytest.mark.skipif('not HAS_SKIMAGE') def test_data_properties_bkg(): data = np.ones((3, 3)).astype(float) props = data_properties(data, background=1.0) assert props.area.value == 9.0 assert props.background_sum == 9.0 with pytest.raises(ValueError): data_properties(data, background=[1.0, 2.0]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/morphology/tests/test_non_parametric.py0000644000214200020070000000070300000000000024204 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the non_parametric module. """ import numpy as np from ..non_parametric import gini def test_gini(): """ Test Gini coefficient calculation. """ data_evenly_distributed = np.ones((100, 100)) data_point_like = np.zeros((100, 100)) data_point_like[50, 50] = 1 assert gini(data_evenly_distributed) == 0. assert gini(data_point_like) == 1. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0137994 photutils-1.3.0/photutils/psf/0000755000214200020070000000000000000000000015021 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/psf/__init__.py0000644000214200020070000000057000000000000017134 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools to perform point-spread-function (PSF) photometry. """ from .epsf import * # noqa from .epsf_stars import * # noqa from .groupstars import * # noqa from .matching import * # noqa from .models import * # noqa from .photometry import * # noqa from .utils import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/epsf.py0000644000214200020070000010523600000000000016337 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools to build and fit an effective PSF (ePSF) based on Anderson and King (2000; PASP 112, 1360) and Anderson (2016), ISR WFC3 2016-12. """ import copy import time import warnings from astropy.modeling.fitting import LevMarLSQFitter from astropy.nddata.utils import (overlap_slices, PartialOverlapError, NoOverlapError) from astropy.stats import SigmaClip from astropy.utils.exceptions import AstropyUserWarning import numpy as np from .epsf_stars import EPSFStar, EPSFStars, LinkedEPSFStar from .models import EPSFModel from ..centroids import centroid_com, centroid_epsf from ..utils._optional_deps import HAS_BOTTLENECK # noqa from ..utils._round import _py2intround __all__ = ['EPSFFitter', 'EPSFBuilder'] class EPSFFitter: """ Class to fit an ePSF model to one or more stars. Parameters ---------- fitter : `astropy.modeling.fitting.Fitter`, optional A `~astropy.modeling.fitting.Fitter` object. fit_boxsize : int, tuple of int, or `None`, optional The size (in pixels) of the box centered on the star to be used for ePSF fitting. This allows using only a small number of central pixels of the star (i.e., where the star is brightest) for fitting. If ``fit_boxsize`` is a scalar then a square box of size ``fit_boxsize`` will be used. If ``fit_boxsize`` has two elements, they should be in ``(ny, nx)`` order. The size must be greater than or equal to 3 pixels for both axes. If `None`, the fitter will use the entire star image. fitter_kwargs : dict-like, optional Any additional keyword arguments (except ``x``, ``y``, ``z``, or ``weights``) to be passed directly to the ``__call__()`` method of the input ``fitter``. """ def __init__(self, fitter=LevMarLSQFitter(), fit_boxsize=5, **fitter_kwargs): self.fitter = fitter self.fitter_has_fit_info = hasattr(self.fitter, 'fit_info') if fit_boxsize is not None: fit_boxsize = np.atleast_1d(fit_boxsize).astype(int) if len(fit_boxsize) == 1: fit_boxsize = np.repeat(fit_boxsize, 2) min_size = 3 if any([size < min_size for size in fit_boxsize]): raise ValueError(f'size must be >= {min_size} for x and y') self.fit_boxsize = fit_boxsize # remove any fitter keyword arguments that we need to set remove_kwargs = ['x', 'y', 'z', 'weights'] fitter_kwargs = copy.deepcopy(fitter_kwargs) for kwarg in remove_kwargs: if kwarg in fitter_kwargs: del fitter_kwargs[kwarg] self.fitter_kwargs = fitter_kwargs def __call__(self, epsf, stars): """ Fit an ePSF model to stars. Parameters ---------- epsf : `EPSFModel` An ePSF model to be fitted to the stars. stars : `EPSFStars` object The stars to be fit. The center coordinates for each star should be as close as possible to actual centers. For stars than contain weights, a weighted fit of the ePSF to the star will be performed. Returns ------- fitted_stars : `EPSFStars` object The fitted stars. The ePSF-fitted center position and flux are stored in the ``center`` (and ``cutout_center``) and ``flux`` attributes. """ if len(stars) == 0: return stars if not isinstance(epsf, EPSFModel): raise TypeError('The input epsf must be an EPSFModel.') # make a copy of the input ePSF epsf = epsf.copy() # perform the fit fitted_stars = [] for star in stars: if isinstance(star, EPSFStar): fitted_star = self._fit_star(epsf, star, self.fitter, self.fitter_kwargs, self.fitter_has_fit_info, self.fit_boxsize) elif isinstance(star, LinkedEPSFStar): fitted_star = [] for linked_star in star: fitted_star.append( self._fit_star(epsf, linked_star, self.fitter, self.fitter_kwargs, self.fitter_has_fit_info, self.fit_boxsize)) fitted_star = LinkedEPSFStar(fitted_star) fitted_star.constrain_centers() else: raise TypeError('stars must contain only EPSFStar and/or ' 'LinkedEPSFStar objects.') fitted_stars.append(fitted_star) return EPSFStars(fitted_stars) def _fit_star(self, epsf, star, fitter, fitter_kwargs, fitter_has_fit_info, fit_boxsize): """ Fit an ePSF model to a single star. The input ``epsf`` will usually be modified by the fitting routine in this function. Make a copy before calling this function if the original is needed. """ if fit_boxsize is not None: try: xcenter, ycenter = star.cutout_center large_slc, _ = overlap_slices(star.shape, fit_boxsize, (ycenter, xcenter), mode='strict') except (PartialOverlapError, NoOverlapError): warnings.warn('The star at ({star.center[0]}, ' '{star.center[1]}) cannot be fit because ' 'its fitting region extends beyond the star ' 'cutout image.', AstropyUserWarning) star = copy.deepcopy(star) star._fit_error_status = 1 return star data = star.data[large_slc] weights = star.weights[large_slc] # define the origin of the fitting region x0 = large_slc[1].start y0 = large_slc[0].start else: # use the entire cutout image data = star.data weights = star.weights # define the origin of the fitting region x0 = 0 y0 = 0 # Define positions in the undersampled grid. The fitter will # evaluate on the defined interpolation grid, currently in the # range [0, len(undersampled grid)]. yy, xx = np.indices(data.shape, dtype=float) xx = xx + x0 - star.cutout_center[0] yy = yy + y0 - star.cutout_center[1] # define the initial guesses for fitted flux and shifts epsf.flux = star.flux epsf.x_0 = 0.0 epsf.y_0 = 0.0 try: fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=data, weights=weights, **fitter_kwargs) except TypeError: # fitter doesn't support weights fitted_epsf = fitter(model=epsf, x=xx, y=yy, z=data, **fitter_kwargs) fit_error_status = 0 if fitter_has_fit_info: fit_info = copy.copy(fitter.fit_info) if 'ierr' in fit_info and fit_info['ierr'] not in [1, 2, 3, 4]: fit_error_status = 2 # fit solution was not found else: fit_info = None # compute the star's fitted position x_center = star.cutout_center[0] + fitted_epsf.x_0.value y_center = star.cutout_center[1] + fitted_epsf.y_0.value star = copy.deepcopy(star) star.cutout_center = (x_center, y_center) # set the star's flux to the ePSF-fitted flux star.flux = fitted_epsf.flux.value star._fit_info = fit_info star._fit_error_status = fit_error_status return star class EPSFBuilder: """ Class to build an effective PSF (ePSF). See `Anderson and King (2000; PASP 112, 1360) `_ and `Anderson (2016), ISR WFC3 2016-12 `_ for details. Parameters ---------- oversampling : int or tuple of two int, optional The oversampling factor(s) of the ePSF relative to the input ``stars`` along the x and y axes. The ``oversampling`` can either be a single float or a tuple of two floats of the form ``(x_oversamp, y_oversamp)``. If ``oversampling`` is a scalar then the oversampling will be the same for both the x and y axes. shape : float, tuple of two floats, or `None`, optional The shape of the output ePSF. If the ``shape`` is not `None`, it will be derived from the sizes of the input ``stars`` and the ePSF oversampling factor. If the size is even along any axis, it will be made odd by adding one. The output ePSF will always have odd sizes along both axes to ensure a well-defined central pixel. smoothing_kernel : {'quartic', 'quadratic'}, 2D `~numpy.ndarray`, or `None` The smoothing kernel to apply to the ePSF. The predefined ``'quartic'`` and ``'quadratic'`` kernels are derived from fourth and second degree polynomials, respectively. Alternatively, a custom 2D array can be input. If `None` then no smoothing will be performed. recentering_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The callable must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally ``error`` and ``oversampling`` keywords. The callable object must return a tuple of two 1D `~numpy.ndarray` variables, representing the x and y centroids. recentering_maxiters : int, optional The maximum number of recentering iterations to perform during each ePSF build iteration. fitter : `EPSFFitter` object, optional A `EPSFFitter` object use to fit the ePSF to stars. To set fitter options, a new object with specific options should be passed in - the default uses simply the default options. To see more of these options, see the `EPSFFitter` documentation. maxiters : int, optional The maximum number of iterations to perform. progress_bar : bool, option Whether to print the progress bar during the build iterations. norm_radius : float, optional The pixel radius over which the ePSF is normalized. shift_val : float, optional The undersampled value at which to compute the shifts. It must be a strictly positive number. recentering_boxsize : float or tuple of two floats, optional The size (in pixels) of the box used to calculate the centroid of the ePSF during each build iteration. If a single integer number is provided, then a square box will be used. If two values are provided, then they should be in ``(ny, nx)`` order. center_accuracy : float, optional The desired accuracy for the centers of stars. The building iterations will stop if the centers of all the stars change by less than ``center_accuracy`` pixels between iterations. All stars must meet this condition for the loop to exit. flux_residual_sigclip : `~astropy.stats.SigmaClip` object, optional A `~astropy.stats.SigmaClip` object used to determine which pixels are ignored based on the star sampling flux residuals, when computing the average residual of ePSF grid points in each iteration step. Notes ----- If your image image contains NaN values, you may see better performance if you have the `bottleneck`_ package installed. .. _bottleneck: https://github.com/pydata/bottleneck """ def __init__(self, oversampling=4., shape=None, smoothing_kernel='quartic', recentering_func=centroid_com, recentering_maxiters=20, fitter=EPSFFitter(), maxiters=10, progress_bar=True, norm_radius=5.5, shift_val=0.5, recentering_boxsize=(5, 5), center_accuracy=1.0e-3, flux_residual_sigclip=SigmaClip(sigma=3, cenfunc='median', maxiters=10)): if oversampling is None: raise ValueError("'oversampling' must be specified.") oversampling = np.atleast_1d(oversampling).astype(int) if len(oversampling) == 1: oversampling = np.repeat(oversampling, 2) if np.any(oversampling <= 0.0): raise ValueError('oversampling must be a positive number.') self._norm_radius = norm_radius self._shift_val = shift_val self.oversampling = oversampling self.shape = self._init_img_params(shape) if self.shape is not None: self.shape = self.shape.astype(int) self.recentering_func = recentering_func self.recentering_maxiters = recentering_maxiters self.recentering_boxsize = self._init_img_params(recentering_boxsize) self.recentering_boxsize = self.recentering_boxsize.astype(int) self.smoothing_kernel = smoothing_kernel if not isinstance(fitter, EPSFFitter): raise TypeError('fitter must be an EPSFFitter instance.') self.fitter = fitter if center_accuracy <= 0.0: raise ValueError('center_accuracy must be a positive number.') self.center_accuracy_sq = center_accuracy**2 maxiters = int(maxiters) if maxiters <= 0: raise ValueError("'maxiters' must be a positive number.") self.maxiters = maxiters self.progress_bar = progress_bar if not isinstance(flux_residual_sigclip, SigmaClip): raise ValueError("'flux_residual_sigclip' must be an" " astropy.stats.SigmaClip function.") self.flux_residual_sigclip = flux_residual_sigclip # store each ePSF build iteration self._epsf = [] def __call__(self, stars): return self.build_epsf(stars) @staticmethod def _init_img_params(param): """ Initialize 2D image-type parameters that can accept either a single or two values. """ if param is not None: param = np.atleast_1d(param) if len(param) == 1: param = np.repeat(param, 2) return param def _create_initial_epsf(self, stars): """ Create an initial `EPSFModel` object. The initial ePSF data are all zeros. If ``shape`` is not specified, the shape of the ePSF data array is determined from the shape of the input ``stars`` and the oversampling factor. If the size is even along any axis, it will be made odd by adding one. The output ePSF will always have odd sizes along both axes to ensure a central pixel. Parameters ---------- stars : `EPSFStars` object The stars used to build the ePSF. Returns ------- epsf : `EPSFModel` The initial ePSF model. """ norm_radius = self._norm_radius shift_val = self._shift_val oversampling = self.oversampling shape = self.shape # define the ePSF shape if shape is not None: shape = np.atleast_1d(shape).astype(int) if len(shape) == 1: shape = np.repeat(shape, 2) else: # Stars class should have odd-sized dimensions, and thus we # get the oversampled shape as oversampling * len + 1; if # len=25, then newlen=101, for example. x_shape = (np.ceil(stars._max_shape[0]) * oversampling[0] + 1).astype(int) y_shape = (np.ceil(stars._max_shape[1]) * oversampling[1] + 1).astype(int) shape = np.array((y_shape, x_shape)) # verify odd sizes of shape shape = [(i + 1) if i % 2 == 0 else i for i in shape] data = np.zeros(shape, dtype=float) # ePSF origin should be in the undersampled pixel units, not the # oversampled grid units. The middle, fractional (as we wish for # the center of the pixel, so the center should be at (v.5, w.5) # detector pixels) value is simply the average of the two values # at the extremes. xcenter = stars._max_shape[0] / 2. ycenter = stars._max_shape[1] / 2. epsf = EPSFModel(data=data, origin=(xcenter, ycenter), oversampling=oversampling, norm_radius=norm_radius, shift_val=shift_val) return epsf def _resample_residual(self, star, epsf): """ Compute a normalized residual image in the oversampled ePSF grid. A normalized residual image is calculated by subtracting the normalized ePSF model from the normalized star at the location of the star in the undersampled grid. The normalized residual image is then resampled from the undersampled star grid to the oversampled ePSF grid. Parameters ---------- star : `EPSFStar` object A single star object. epsf : `EPSFModel` object The ePSF model. Returns ------- image : 2D `~numpy.ndarray` A 2D image containing the resampled residual image. The image contains NaNs where there is no data. """ # Compute the normalized residual by subtracting the ePSF model # from the normalized star at the location of the star in the # undersampled grid. x = star._xidx_centered y = star._yidx_centered stardata = (star._data_values_normalized - epsf.evaluate(x=x, y=y, flux=1.0, x_0=0.0, y_0=0.0)) x = epsf.oversampling[0] * star._xidx_centered y = epsf.oversampling[1] * star._yidx_centered epsf_xcenter, epsf_ycenter = (int((epsf.data.shape[1] - 1) / 2), int((epsf.data.shape[0] - 1) / 2)) xidx = _py2intround(x + epsf_xcenter) yidx = _py2intround(y + epsf_ycenter) resampled_img = np.full(epsf.shape, np.nan) mask = np.logical_and(np.logical_and(xidx >= 0, xidx < epsf.shape[1]), np.logical_and(yidx >= 0, yidx < epsf.shape[0])) xidx_ = xidx[mask] yidx_ = yidx[mask] resampled_img[yidx_, xidx_] = stardata[mask] return resampled_img def _resample_residuals(self, stars, epsf): """ Compute normalized residual images for all the input stars. Parameters ---------- stars : `EPSFStars` object The stars used to build the ePSF. epsf : `EPSFModel` object The ePSF model. Returns ------- epsf_resid : 3D `~numpy.ndarray` A 3D cube containing the resampled residual images. """ shape = (stars.n_good_stars, epsf.shape[0], epsf.shape[1]) epsf_resid = np.zeros(shape) for i, star in enumerate(stars.all_good_stars): epsf_resid[i, :, :] = self._resample_residual(star, epsf) return epsf_resid def _smooth_epsf(self, epsf_data): """ Smooth the ePSF array by convolving it with a kernel. Parameters ---------- epsf_data : 2D `~numpy.ndarray` A 2D array containing the ePSF image. Returns ------- result : 2D `~numpy.ndarray` The smoothed (convolved) ePSF data. """ from scipy.ndimage import convolve if self.smoothing_kernel is None: return epsf_data # do this check first as comparing a ndarray to string causes a warning elif isinstance(self.smoothing_kernel, np.ndarray): kernel = self.smoothing_kernel elif self.smoothing_kernel == 'quartic': # from Polynomial2D fit with degree=4 to 5x5 array of # zeros with 1. at the center # Polynomial2D(4, c0_0=0.04163265, c1_0=-0.76326531, # c2_0=0.99081633, c3_0=-0.4, c4_0=0.05, # c0_1=-0.76326531, c0_2=0.99081633, c0_3=-0.4, # c0_4=0.05, c1_1=0.32653061, c1_2=-0.08163265, # c1_3=0., c2_1=-0.08163265, c2_2=0.02040816, # c3_1=-0.)> kernel = np.array( [[+0.041632, -0.080816, 0.078368, -0.080816, +0.041632], [-0.080816, -0.019592, 0.200816, -0.019592, -0.080816], [+0.078368, +0.200816, 0.441632, +0.200816, +0.078368], [-0.080816, -0.019592, 0.200816, -0.019592, -0.080816], [+0.041632, -0.080816, 0.078368, -0.080816, +0.041632]]) elif self.smoothing_kernel == 'quadratic': # from Polynomial2D fit with degree=2 to 5x5 array of # zeros with 1. at the center # Polynomial2D(2, c0_0=-0.07428571, c1_0=0.11428571, # c2_0=-0.02857143, c0_1=0.11428571, # c0_2=-0.02857143, c1_1=-0.) kernel = np.array( [[-0.07428311, 0.01142786, 0.03999952, 0.01142786, -0.07428311], [+0.01142786, 0.09714283, 0.12571449, 0.09714283, +0.01142786], [+0.03999952, 0.12571449, 0.15428215, 0.12571449, +0.03999952], [+0.01142786, 0.09714283, 0.12571449, 0.09714283, +0.01142786], [-0.07428311, 0.01142786, 0.03999952, 0.01142786, -0.07428311]]) else: raise TypeError('Unsupported kernel.') return convolve(epsf_data, kernel) def _recenter_epsf(self, epsf, centroid_func=centroid_com, box_size=(5, 5), maxiters=20, center_accuracy=1.0e-4): """ Calculate the center of the ePSF data and shift the data so the ePSF center is at the center of the ePSF data array. Parameters ---------- epsf : `EPSFModel` object The ePSF model. centroid_func : callable, optional A callable object (e.g., function or class) that is used to calculate the centroid of a 2D array. The callable must accept a 2D `~numpy.ndarray`, have a ``mask`` keyword and optionally an ``error`` keyword. The callable object must return a tuple of two 1D `~numpy.ndarray` variables, representing the x and y centroids. box_size : float or tuple of two floats, optional The size (in pixels) of the box used to calculate the centroid of the ePSF during each build iteration. If a single integer number is provided, then a square box will be used. If two values are provided, then they should be in ``(ny, nx)`` order. maxiters : int, optional The maximum number of recentering iterations to perform. center_accuracy : float, optional The desired accuracy for the centers of stars. The building iterations will stop if the center of the ePSF changes by less than ``center_accuracy`` pixels between iterations. Returns ------- result : 2D `~numpy.ndarray` The recentered ePSF data. """ epsf_data = epsf._data epsf = EPSFModel(data=epsf._data, origin=epsf.origin, oversampling=epsf.oversampling, norm_radius=epsf._norm_radius, shift_val=epsf._shift_val, normalize=False) xcenter, ycenter = epsf.origin y, x = np.indices(epsf._data.shape, dtype=float) x /= epsf.oversampling[0] y /= epsf.oversampling[1] dx_total, dy_total = 0, 0 iter_num = 0 center_accuracy_sq = center_accuracy ** 2 center_dist_sq = center_accuracy_sq + 1.e6 center_dist_sq_prev = center_dist_sq + 1 while (iter_num < maxiters and center_dist_sq >= center_accuracy_sq): iter_num += 1 # Anderson & King (2000) recentering function depends # on specific pixels, and thus does not need a cutout if self.recentering_func == centroid_epsf: epsf_cutout = epsf_data else: slices_large, _ = overlap_slices(epsf_data.shape, box_size, (ycenter * self.oversampling[1], xcenter * self.oversampling[0])) epsf_cutout = epsf_data[slices_large] mask = ~np.isfinite(epsf_cutout) try: # find a new center position xcenter_new, ycenter_new = centroid_func( epsf_cutout, mask=mask, oversampling=epsf.oversampling, shift_val=epsf._shift_val) except TypeError: # centroid_func doesn't accept oversampling and/or shift_val # keywords - try oversampling alone try: xcenter_new, ycenter_new = centroid_func( epsf_cutout, mask=mask, oversampling=epsf.oversampling) except TypeError: # centroid_func doesn't accept oversampling and # shift_val xcenter_new, ycenter_new = centroid_func(epsf_cutout, mask=mask) if self.recentering_func != centroid_epsf: xcenter_new += slices_large[1].start/self.oversampling[0] ycenter_new += slices_large[0].start/self.oversampling[1] # Calculate the shift; dx = i - x_star so if dx was positively # incremented then x_star was negatively incremented for a given i. # We will therefore actually subsequently subtract dx from xcenter # (or x_star). dx = xcenter_new - xcenter dy = ycenter_new - ycenter center_dist_sq = dx**2 + dy**2 if center_dist_sq >= center_dist_sq_prev: # don't shift break center_dist_sq_prev = center_dist_sq dx_total += dx dy_total += dy epsf_data = epsf.evaluate(x=x, y=y, flux=1.0, x_0=xcenter - dx_total, y_0=ycenter - dy_total) return epsf_data def _build_epsf_step(self, stars, epsf=None): """ A single iteration of improving an ePSF. Parameters ---------- stars : `EPSFStars` object The stars used to build the ePSF. epsf : `EPSFModel` object, optional The initial ePSF model. If not input, then the ePSF will be built from scratch. Returns ------- epsf : `EPSFModel` object The updated ePSF. """ if len(stars) < 1: raise ValueError('stars must contain at least one EPSFStar or ' 'LinkedEPSFStar object.') if epsf is None: # create an initial ePSF (array of zeros) epsf = self._create_initial_epsf(stars) else: # improve the input ePSF epsf = copy.deepcopy(epsf) # compute a 3D stack of 2D residual images residuals = self._resample_residuals(stars, epsf) # compute the sigma-clipped average along the 3D stack with warnings.catch_warnings(): warnings.simplefilter('ignore', category=RuntimeWarning) warnings.simplefilter('ignore', category=AstropyUserWarning) residuals = self.flux_residual_sigclip(residuals, axis=0, masked=False, return_bounds=False) if HAS_BOTTLENECK: import bottleneck residuals = bottleneck.nanmedian(residuals, axis=0) else: residuals = np.nanmedian(residuals, axis=0) # interpolate any missing data (np.nan) mask = ~np.isfinite(residuals) if np.any(mask): residuals = _interpolate_missing_data(residuals, mask, method='cubic') # fill any remaining nans (outer points) with zeros residuals[~np.isfinite(residuals)] = 0. # add the residuals to the previous ePSF image new_epsf = epsf._data + residuals # smooth and recenter the ePSF new_epsf = self._smooth_epsf(new_epsf) epsf = EPSFModel(data=new_epsf, origin=epsf.origin, oversampling=epsf.oversampling, norm_radius=epsf._norm_radius, shift_val=epsf._shift_val, normalize=False) epsf._data = self._recenter_epsf( epsf, centroid_func=self.recentering_func, box_size=self.recentering_boxsize, maxiters=self.recentering_maxiters) # Return the new ePSF object, but with undersampled grid pixel # coordinates. xcenter = (epsf._data.shape[1] - 1) / 2. / epsf.oversampling[0] ycenter = (epsf._data.shape[0] - 1) / 2. / epsf.oversampling[1] return EPSFModel(data=epsf._data, origin=(xcenter, ycenter), oversampling=epsf.oversampling, norm_radius=epsf._norm_radius, shift_val=epsf._shift_val) def build_epsf(self, stars, init_epsf=None): """ Build iteratively an ePSF from star cutouts. Parameters ---------- stars : `EPSFStars` object The stars used to build the ePSF. init_epsf : `EPSFModel` object, optional The initial ePSF model. If not input, then the ePSF will be built from scratch. Returns ------- epsf : `EPSFModel` object The constructed ePSF. fitted_stars : `EPSFStars` object The input stars with updated centers and fluxes derived from fitting the output ``epsf``. """ iter_num = 0 n_stars = stars.n_stars fit_failed = np.zeros(n_stars, dtype=bool) epsf = init_epsf dt = 0. center_dist_sq = self.center_accuracy_sq + 1. centers = stars.cutout_center_flat while (iter_num < self.maxiters and not np.all(fit_failed) and np.max(center_dist_sq) >= self.center_accuracy_sq): t_start = time.time() iter_num += 1 if self.progress_bar: if iter_num == 1: dt_str = ' [? s/iter]' else: dt_str = f' [{dt:.1f} s/iter]' print(f'PROGRESS: iteration {iter_num:d} (of max ' f'{self.maxiters}){dt_str}', end='\r') # build/improve the ePSF epsf = self._build_epsf_step(stars, epsf=epsf) # fit the new ePSF to the stars to find improved centers # we catch fit warnings here -- stars with unsuccessful fits # are excluded from the ePSF build process with warnings.catch_warnings(): message = '.*The fit may be unsuccessful;.*' warnings.filterwarnings('ignore', message=message, category=AstropyUserWarning) stars = self.fitter(epsf, stars) # find all stars where the fit failed fit_failed = np.array([star._fit_error_status > 0 for star in stars.all_stars]) if np.all(fit_failed): raise ValueError('The ePSF fitting failed for all stars.') # permanently exclude fitting any star where the fit fails # after 3 iterations if iter_num > 3 and np.any(fit_failed): idx = fit_failed.nonzero()[0] for i in idx: stars.all_stars[i]._excluded_from_fit = True # if no star centers have moved by more than pixel accuracy, # stop the iteration loop early dx_dy = stars.cutout_center_flat - centers dx_dy = dx_dy[np.logical_not(fit_failed)] center_dist_sq = np.sum(dx_dy * dx_dy, axis=1, dtype=np.float64) centers = stars.cutout_center_flat self._epsf.append(epsf) dt = time.time() - t_start return epsf, stars def _interpolate_missing_data(data, mask, method='cubic'): """ Interpolate missing data as identified by the ``mask`` keyword. Parameters ---------- data : 2D `~numpy.ndarray` An array containing the 2D image. mask : 2D bool `~numpy.ndarray` A 2D boolean mask array with the same shape as the input ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. The masked data points are those that will be interpolated. method : {'cubic', 'nearest'}, optional The method of used to interpolate the missing data: * ``'cubic'``: Masked data are interpolated using 2D cubic splines. This is the default. * ``'nearest'``: Masked data are interpolated using nearest-neighbor interpolation. Returns ------- data_interp : 2D `~numpy.ndarray` The interpolated 2D image. """ from scipy import interpolate data_interp = np.array(data, copy=True) if len(data_interp.shape) != 2: raise ValueError("'data' must be a 2D array.") if mask.shape != data.shape: raise ValueError("'mask' and 'data' must have the same shape.") y, x = np.indices(data_interp.shape) xy = np.dstack((x[~mask].ravel(), y[~mask].ravel()))[0] z = data_interp[~mask].ravel() if method == 'nearest': interpol = interpolate.NearestNDInterpolator(xy, z) elif method == 'cubic': interpol = interpolate.CloughTocher2DInterpolator(xy, z) else: raise ValueError('Unsupported interpolation method.') xy_missing = np.dstack((x[mask].ravel(), y[mask].ravel()))[0] data_interp[mask] = interpol(xy_missing) return data_interp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/epsf_stars.py0000644000214200020070000007012100000000000017545 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools to extract cutouts of stars and data structures to hold the cutouts for fitting and building ePSFs. """ import warnings from astropy.nddata import NDData from astropy.nddata.utils import (overlap_slices, NoOverlapError, PartialOverlapError) from astropy.table import Table from astropy.utils import lazyproperty from astropy.utils.exceptions import AstropyUserWarning from astropy.wcs import WCS from astropy.wcs.utils import skycoord_to_pixel import numpy as np from ..aperture import BoundingBox __all__ = ['EPSFStar', 'EPSFStars', 'LinkedEPSFStar', 'extract_stars'] class EPSFStar: """ A class to hold a 2D cutout image and associated metadata of a star used to build an ePSF. Parameters ---------- data : `~numpy.ndarray` A 2D cutout image of a single star. weights : `~numpy.ndarray` or `None`, optional A 2D array of the weights associated with the input ``data``. cutout_center : tuple of two floats or `None`, optional The ``(x, y)`` position of the star's center with respect to the input cutout ``data`` array. If `None`, then the center of of the input cutout ``data`` array will be used. origin : tuple of two int, optional The ``(x, y)`` index of the origin (bottom-left corner) pixel of the input cutout array with respect to the original array from which the cutout was extracted. This can be used to convert positions within the cutout image to positions in the original image. ``origin`` and ``wcs_large`` must both be input for a linked star (a single star extracted from different images). wcs_large : `None` or WCS object, optional A WCS object associated with the large image from which the cutout array was extracted. It should not be the WCS object of the input cutout ``data`` array. The WCS object must support the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). ``origin`` and ``wcs_large`` must both be input for a linked star (a single star extracted from different images). id_label : int, str, or `None`, optional An optional identification number or label for the star. """ def __init__(self, data, weights=None, cutout_center=None, origin=(0, 0), wcs_large=None, id_label=None): self._data = np.asanyarray(data) self.shape = self._data.shape if weights is not None: if weights.shape != data.shape: raise ValueError('weights must have the same shape as the ' 'input data array.') self.weights = np.asanyarray(weights, dtype=float).copy() else: self.weights = np.ones_like(self._data, dtype=float) self.mask = (self.weights <= 0.) # mask out invalid image data invalid_data = np.logical_not(np.isfinite(self._data)) if np.any(invalid_data): self.weights[invalid_data] = 0. self.mask[invalid_data] = True self._cutout_center = cutout_center self.origin = np.asarray(origin) self.wcs_large = wcs_large self.id_label = id_label self.flux = self.estimate_flux() self._excluded_from_fit = False self._fitinfo = None def __array__(self): """ Array representation of the mask data array (e.g., for matplotlib). """ return self._data @property def data(self): """The 2D cutout image.""" return self._data @property def cutout_center(self): """ A `~numpy.ndarray` of the ``(x, y)`` position of the star's center with respect to the input cutout ``data`` array. """ return self._cutout_center @cutout_center.setter def cutout_center(self, value): if value is None: value = ((self.shape[1] - 1) / 2., (self.shape[0] - 1) / 2.) else: if len(value) != 2: raise ValueError('The "cutout_center" attribute must have ' 'two elements in (x, y) form.') self._cutout_center = np.asarray(value) @property def center(self): """ A `~numpy.ndarray` of the ``(x, y)`` position of the star's center in the original (large) image (not the cutout image). """ return self.cutout_center + self.origin @lazyproperty def slices(self): """ A tuple of two slices representing the cutout region with respect to the original (large) image. """ return (slice(self.origin[1], self.origin[1] + self.shape[1]), slice(self.origin[0], self.origin[0] + self.shape[0])) @lazyproperty def bbox(self): """ The minimal `~photutils.aperture.BoundingBox` for the cutout region with respect to the original (large) image. """ return BoundingBox(self.slices[1].start, self.slices[1].stop, self.slices[0].start, self.slices[0].stop) def estimate_flux(self): """ Estimate the star's flux by summing values in the input cutout array. Missing data is filled in by interpolation to better estimate the total flux. """ from .epsf import _interpolate_missing_data if np.any(self.mask): data_interp = _interpolate_missing_data(self.data, method='cubic', mask=self.mask) data_interp = _interpolate_missing_data(data_interp, method='nearest', mask=self.mask) flux = np.sum(data_interp, dtype=float) else: flux = np.sum(self.data, dtype=float) return flux def register_epsf(self, epsf): """ Register and scale (in flux) the input ``epsf`` to the star. Parameters ---------- epsf : `EPSFModel` The ePSF to register. Returns ------- data : `~numpy.ndarray` A 2D array of the registered/scaled ePSF. """ yy, xx = np.indices(self.shape, dtype=float) xx = xx - self.cutout_center[0] yy = yy - self.cutout_center[1] return self.flux * epsf.evaluate(xx, yy, flux=1.0, x_0=0.0, y_0=0.0) def compute_residual_image(self, epsf): """ Compute the residual image of the star data minus the registered/scaled ePSF. Parameters ---------- epsf : `EPSFModel` The ePSF to subtract. Returns ------- data : `~numpy.ndarray` A 2D array of the residual image. """ return self.data - self.register_epsf(epsf) @lazyproperty def _xy_idx(self): """ 1D arrays of x and y indices of unmasked pixels in the cutout reference frame. """ yidx, xidx = np.indices(self._data.shape) return xidx[~self.mask].ravel(), yidx[~self.mask].ravel() @lazyproperty def _xidx(self): """ 1D arrays of x indices of unmasked pixels in the cutout reference frame. """ return self._xy_idx[0] @lazyproperty def _yidx(self): """ 1D arrays of y indices of unmasked pixels in the cutout reference frame. """ return self._xy_idx[1] @property def _xidx_centered(self): """ 1D array of x indices of unmasked pixels, with respect to the star center, in the cutout reference frame. """ return self._xy_idx[0] - self.cutout_center[0] @property def _yidx_centered(self): """ 1D array of y indices of unmasked pixels, with respect to the star center, in the cutout reference frame. """ return self._xy_idx[1] - self.cutout_center[1] @lazyproperty def _data_values(self): """1D array of unmasked cutout data values.""" return self.data[~self.mask].ravel() @lazyproperty def _data_values_normalized(self): """ 1D array of unmasked cutout data values, normalized by the star's total flux. """ return self._data_values / self.flux @lazyproperty def _weight_values(self): """ 1D array of unmasked weight values. """ return self.weights[~self.mask].ravel() class EPSFStars: """ Class to hold a list of `EPSFStar` and/or `LinkedEPSFStar` objects. Parameters ---------- star_list : list of `EPSFStar` or `LinkedEPSFStar` objects A list of `EPSFStar` and/or `LinkedEPSFStar` objects. """ def __init__(self, stars_list): if isinstance(stars_list, (EPSFStar, LinkedEPSFStar)): self._data = [stars_list] elif isinstance(stars_list, list): self._data = stars_list else: raise ValueError('stars_list must be a list of EPSFStar and/or ' 'LinkedEPSFStar objects.') def __len__(self): return len(self._data) def __getitem__(self, index): return self.__class__(self._data[index]) def __delitem__(self, index): del self._data[index] def __iter__(self): for i in self._data: yield i # explicit set/getstate to avoid infinite recursion # from pickler using __getattr__ def __getstate__(self): return self.__dict__ def __setstate__(self, d): self.__dict__ = d def __getattr__(self, attr): if attr in ['cutout_center', 'center', 'flux', '_excluded_from_fit']: result = np.array([getattr(star, attr) for star in self._data]) else: result = [getattr(star, attr) for star in self._data] if len(self._data) == 1: result = result[0] return result def _getattr_flat(self, attr): values = [] for item in self._data: if isinstance(item, LinkedEPSFStar): values.extend(getattr(item, attr)) else: values.append(getattr(item, attr)) return np.array(values) @property def cutout_center_flat(self): """ A `~numpy.ndarray` of the ``(x, y)`` position of all the stars' centers (including linked stars) with respect to the input cutout ``data`` array, as a 2D array (``n_all_stars`` x 2). Note that when `EPSFStars` contains any `LinkedEPSFStar`, the ``cutout_center`` attribute will be a nested 3D array. """ return self._getattr_flat('cutout_center') @property def center_flat(self): """ A `~numpy.ndarray` of the ``(x, y)`` position of all the stars' centers (including linked stars) with respect to the original (large) image (not the cutout image) as a 2D array (``n_all_stars`` x 2). Note that when `EPSFStars` contains any `LinkedEPSFStar`, the ``center`` attribute will be a nested 3D array. """ return self._getattr_flat('center') @lazyproperty def all_stars(self): """ A list of all `EPSFStar` objects stored in this object, including those that comprise linked stars (i.e., `LinkedEPSFStar`), as a flat list. """ stars = [] for item in self._data: if isinstance(item, LinkedEPSFStar): stars.extend(item.all_stars) else: stars.append(item) return stars @property def all_good_stars(self): """ A list of all `EPSFStar` objects stored in this object that have not been excluded from fitting, including those that comprise linked stars (i.e., `LinkedEPSFStar`), as a flat list. """ stars = [] for star in self.all_stars: if star._excluded_from_fit: continue else: stars.append(star) return stars @lazyproperty def n_stars(self): """ The total number of stars. A linked star is counted only once. """ return len(self._data) @lazyproperty def n_all_stars(self): """ The total number of `EPSFStar` objects, including all the linked stars within `LinkedEPSFStar`. Each linked star is included in the count. """ return len(self.all_stars) @property def n_good_stars(self): """ The total number of `EPSFStar` objects, including all the linked stars within `LinkedEPSFStar`, that have not been excluded from fitting. Each non-excluded linked star is included in the count. """ return len(self.all_good_stars) @lazyproperty def _max_shape(self): """ The maximum x and y shapes of all the `EPSFStar` objects (including linked stars). """ return np.max([star.shape for star in self.all_stars], axis=0) class LinkedEPSFStar(EPSFStars): """ A class to hold a list of `EPSFStar` objects for linked stars. Linked stars are `EPSFStar` cutouts from different images that represent the same physical star. When building the ePSF, linked stars are constrained to have the same sky coordinates. Parameters ---------- star_list : list of `EPSFStar` objects A list of `EPSFStar` objects for the same physical star. Each `EPSFStar` object must have a valid ``wcs_large`` attribute to convert between pixel and sky coordinates. """ def __init__(self, stars_list): for star in stars_list: if not isinstance(star, EPSFStar): raise ValueError('stars_list must contain only EPSFStar ' 'objects.') if star.wcs_large is None: raise ValueError('Each EPSFStar object must have a valid ' 'wcs_large attribute.') super().__init__(stars_list) def constrain_centers(self): """ Constrain the centers of linked `EPSFStar` objects (i.e., the same physical star) to have the same sky coordinate. Only `EPSFStar` objects that have not been excluded during the ePSF build process will be used to constrain the centers. The single sky coordinate is calculated as the mean of sky coordinates of the linked stars. """ if len(self._data) < 2: # no linked stars return idx = np.logical_not(self._excluded_from_fit).nonzero()[0] if idx.size == 0: warnings.warn('Cannot constrain centers of linked stars because ' 'all the stars have been excluded during the ePSF ' 'build process.', AstropyUserWarning) return good_stars = [self._data[i] for i in idx] coords = [] for star in good_stars: wcs = star.wcs_large xposition = star.center[0] yposition = star.center[1] try: coords.append(wcs.pixel_to_world_values(xposition, yposition)) except AttributeError: if isinstance(wcs, WCS): # for Astropy < 3.1 WCS support coords.append(wcs.all_pix2world(xposition, yposition, 0)) else: raise ValueError('Input wcs does not support the shared ' 'WCS interface.') # compute mean cartesian coordinates lon, lat = np.transpose(coords) lon *= np.pi / 180. lat *= np.pi / 180. x_mean = np.mean(np.cos(lat) * np.cos(lon)) y_mean = np.mean(np.cos(lat) * np.sin(lon)) z_mean = np.mean(np.sin(lat)) # convert mean cartesian coordinates back to spherical hypot = np.hypot(x_mean, y_mean) mean_lon = np.arctan2(y_mean, x_mean) mean_lat = np.arctan2(z_mean, hypot) mean_lon *= 180. / np.pi mean_lat *= 180. / np.pi # convert mean sky coordinates back to center pixel coordinates # for each star for star in good_stars: try: center = star.wcs_large.world_to_pixel_values(mean_lon, mean_lat) except AttributeError: # for Astropy < 3.1 WCS support center = star.wcs_large.all_world2pix(mean_lon, mean_lat, 0) star.cutout_center = np.array(center) - star.origin def extract_stars(data, catalogs, size=(11, 11)): """ Extract cutout images centered on stars defined in the input catalog(s). Stars where the cutout array bounds partially or completely lie outside of the input ``data`` image will not be extracted. Parameters ---------- data : `~astropy.nddata.NDData` or list of `~astropy.nddata.NDData` A `~astropy.nddata.NDData` object or a list of `~astropy.nddata.NDData` objects containing the 2D image(s) from which to extract the stars. If the input ``catalogs`` contain only the sky coordinates (i.e., not the pixel coordinates) of the stars then each of the `~astropy.nddata.NDData` objects must have a valid ``wcs`` attribute. catalogs : `~astropy.table.Table`, list of `~astropy.table.Table` A catalog or list of catalogs of sources to be extracted from the input ``data``. To link stars in multiple images as a single source, you must use a single source catalog where the positions defined in sky coordinates. If a list of catalogs is input (or a single catalog with a single `~astropy.nddata.NDData` object), they are assumed to correspond to the list of `~astropy.nddata.NDData` objects input in ``data`` (i.e., a separate source catalog for each 2D image). For this case, the center of each source can be defined either in pixel coordinates (in ``x`` and ``y`` columns) or sky coordinates (in a ``skycoord`` column containing a `~astropy.coordinates.SkyCoord` object). If both are specified, then the pixel coordinates will be used. If a single source catalog is input with multiple `~astropy.nddata.NDData` objects, then these sources will be extracted from every 2D image in the input ``data``. In this case, the sky coordinates for each source must be specified as a `~astropy.coordinates.SkyCoord` object contained in a column called ``skycoord``. Each `~astropy.nddata.NDData` object in the input ``data`` must also have a valid ``wcs`` attribute. Pixel coordinates (in ``x`` and ``y`` columns) will be ignored. Optionally, each catalog may also contain an ``id`` column representing the ID/name of stars. If this column is not present then the extracted stars will be given an ``id`` number corresponding the the table row number (starting at 1). Any other columns present in the input ``catalogs`` will be ignored. size : int or array_like (int), optional The extraction box size along each axis. If ``size`` is a scalar then a square box of size ``size`` will be used. If ``size`` has two elements, they should be in ``(ny, nx)`` order. The size must be greater than or equal to 3 pixel for both axes. Size must be odd in both axes; if either is even, it is padded by one to force oddness. Returns ------- stars : `EPSFStars` instance A `EPSFStars` instance containing the extracted stars. """ if isinstance(data, NDData): data = [data] if isinstance(catalogs, Table): catalogs = [catalogs] for img in data: if not isinstance(img, NDData): raise ValueError('data must be a single or list of NDData ' 'objects.') for cat in catalogs: if not isinstance(cat, Table): raise ValueError('catalogs must be a single or list of Table ' 'objects.') if len(catalogs) == 1 and len(data) > 1: if 'skycoord' not in catalogs[0].colnames: raise ValueError('When inputting a single catalog with multiple ' 'NDData objects, the catalog must have a ' '"skycoord" column.') if any([img.wcs is None for img in data]): raise ValueError('When inputting a single catalog with multiple ' 'NDData objects, each NDData object must have ' 'a wcs attribute.') else: for cat in catalogs: if 'x' not in cat.colnames or 'y' not in cat.colnames: if 'skycoord' not in cat.colnames: raise ValueError('When inputting multiple catalogs, ' 'each one must have a "x" and "y" ' 'column or a "skycoord" column.') else: if any([img.wcs is None for img in data]): raise ValueError('When inputting catalog(s) with ' 'only skycoord positions, each ' 'NDData object must have a wcs ' 'attribute.') if len(data) != len(catalogs): raise ValueError('When inputting multiple catalogs, the number ' 'of catalogs must match the number of input ' 'images.') size = np.atleast_1d(size) if len(size) == 1: size = np.repeat(size, 2) # Force size to odd numbers such that there is always a central pixel with # even spacing either side of the pixel. size = tuple(_size+1 if _size % 2 == 0 else _size for _size in size) min_size = 3 if size[0] < min_size or size[1] < min_size: raise ValueError(f'size must be >= {min_size} for x and y') if len(catalogs) == 1: # may included linked stars use_xy = True if len(data) > 1: use_xy = False # linked stars require skycoord positions stars = [] # stars is a list of lists, one list of stars in each image for img in data: stars.append(_extract_stars(img, catalogs[0], size=size, use_xy=use_xy)) # transpose the list of lists, to associate linked stars stars = list(map(list, zip(*stars))) # remove 'None' stars (i.e., no or partial overlap in one or # more images) and handle the case of only one "linked" star stars_out = [] n_input = len(catalogs[0]) * len(data) n_extracted = 0 for star in stars: good_stars = [i for i in star if i is not None] n_extracted += len(good_stars) if not good_stars: continue # no overlap in any image elif len(good_stars) == 1: good_stars = good_stars[0] # only one star, cannot be linked else: good_stars = LinkedEPSFStar(good_stars) stars_out.append(good_stars) else: # no linked stars stars_out = [] for img, cat in zip(data, catalogs): stars_out.extend(_extract_stars(img, cat, size=size, use_xy=True)) n_input = len(stars_out) stars_out = [star for star in stars_out if star is not None] n_extracted = len(stars_out) n_excluded = n_input - n_extracted if n_excluded > 0: warnings.warn('{} star(s) were not extracted because their cutout ' 'region extended beyond the input image.' .format(n_excluded), AstropyUserWarning) return EPSFStars(stars_out) def _extract_stars(data, catalog, size=(11, 11), use_xy=True): """ Extract cutout images from a single image centered on stars defined in the single input catalog. Parameters ---------- data : `~astropy.nddata.NDData` A `~astropy.nddata.NDData` object containing the 2D image from which to extract the stars. If the input ``catalog`` contains only the sky coordinates (i.e., not the pixel coordinates) of the stars then the `~astropy.nddata.NDData` object must have a valid ``wcs`` attribute. catalog : `~astropy.table.Table` A single catalog of sources to be extracted from the input ``data``. The center of each source can be defined either in pixel coordinates (in ``x`` and ``y`` columns) or sky coordinates (in a ``skycoord`` column containing a `~astropy.coordinates.SkyCoord` object). If both are specified, then the value of the ``use_xy`` keyword determines which coordinates will be used. size : int or array_like (int), optional The extraction box size along each axis. If ``size`` is a scalar then a square box of size ``size`` will be used. If ``size`` has two elements, they should be in ``(ny, nx)`` order. The size must be greater than or equal to 3 pixel for both axes. Size must be odd in both axes; if either is even, it is padded by one to force oddness. use_xy : bool, optional Whether to use the ``x`` and ``y`` pixel positions when both pixel and sky coordinates are present in the input catalog table. If `False` then sky coordinates are used instead of pixel coordinates (e.g., for linked stars). The default is `True`. Returns ------- stars : list of `EPSFStar` objects A list of `EPSFStar` instances containing the extracted stars. """ # Force size to odd numbers such that there is always a central pixel with # even spacing either side of the pixel. if np.isscalar(size): size = size+1 if size % 2 == 0 else size else: size = tuple(_size+1 if _size % 2 == 0 else _size for _size in size) colnames = catalog.colnames if ('x' not in colnames or 'y' not in colnames) or not use_xy: try: xcenters, ycenters = data.wcs.world_to_pixel(catalog['skycoord']) except AttributeError: # for Astropy < 3.1 WCS support xcenters, ycenters = skycoord_to_pixel(catalog['skycoord'], data.wcs, origin=0, mode='all') else: xcenters = catalog['x'].data.astype(float) ycenters = catalog['y'].data.astype(float) if 'id' in colnames: ids = catalog['id'] else: ids = np.arange(len(catalog), dtype=int) + 1 if data.uncertainty is None: weights = np.ones_like(data.data) else: if data.uncertainty.uncertainty_type == 'weights': weights = np.asanyarray(data.uncertainty.array, dtype=float) else: warnings.warn('The data uncertainty attribute has an unsupported ' 'type. Only uncertainty_type="weights" can be ' 'used to set weights. Weights will be set to 1.', AstropyUserWarning) weights = np.ones_like(data.data) if data.mask is not None: weights[data.mask] = 0. stars = [] for xcenter, ycenter, obj_id in zip(xcenters, ycenters, ids): try: large_slc, _ = overlap_slices(data.data.shape, size, (ycenter, xcenter), mode='strict') data_cutout = data.data[large_slc] weights_cutout = weights[large_slc] except (PartialOverlapError, NoOverlapError): stars.append(None) continue origin = (large_slc[1].start, large_slc[0].start) cutout_center = (xcenter - origin[0], ycenter - origin[1]) star = EPSFStar(data_cutout, weights_cutout, cutout_center=cutout_center, origin=origin, wcs_large=data.wcs, id_label=obj_id) stars.append(star) return stars ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/groupstars.py0000644000214200020070000002143200000000000017606 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides classes to perform grouping of stars. """ import abc from astropy.table import Column import numpy as np __all__ = ['DAOGroup', 'DBSCANGroup', 'GroupStarsBase'] class GroupStarsBase(metaclass=abc.ABCMeta): """ This base class provides the basic interface for subclasses that are capable of classifying stars in groups. """ def __call__(self, starlist): """ Classify stars into groups. Parameters ---------- starlist : `~astropy.table.Table` List of star positions. Columns named as ``x_0`` and ``y_0``, which corresponds to the centroid coordinates of the sources, must be provided. Returns ------- group_starlist : `~astropy.table.Table` ``starlist`` with an additional column named ``group_id`` whose unique values represent groups of mutually overlapping stars. """ return self.group_stars(starlist) @abc.abstractmethod def group_stars(self, starlist): """ Classify stars into groups. Parameters ---------- starlist : `~astropy.table.Table` List of star positions. Columns named as ``x_0`` and ``y_0``, which corresponds to the centroid coordinates of the sources, must be provided. Returns ------- group_starlist : `~astropy.table.Table` ``starlist`` with an additional column named ``group_id`` whose unique values represent groups of mutually overlapping stars. """ raise NotImplementedError('Needs to be implemented in a subclass.') class DAOGroup(GroupStarsBase): """ This class implements the DAOGROUP algorithm presented by Stetson (1987). The method ``group_stars`` divides an entire starlist into sets of distinct, self-contained groups of mutually overlapping stars. It accepts as input a list of stars and determines which stars are close enough to be capable of adversely influencing each others' profile fits. Parameters ---------- crit_separation : float or int Distance, in units of pixels, such that any two stars separated by less than this distance will be placed in the same group. Notes ----- Assuming the psf fwhm to be known, ``crit_separation`` may be set to k*fwhm, for some positive real k. See Also -------- photutils.detection.DAOStarFinder References ---------- [1] Stetson, Astronomical Society of the Pacific, Publications, (ISSN 0004-6280), vol. 99, March 1987, p. 191-222. Available at: https://ui.adsabs.harvard.edu/abs/1987PASP...99..191S/abstract """ def __init__(self, crit_separation): self.crit_separation = crit_separation @property def crit_separation(self): return self._crit_separation @crit_separation.setter def crit_separation(self, crit_separation): if not isinstance(crit_separation, (float, int)): raise ValueError('crit_separation is expected to be either float' f'or int. Received {type(crit_separation)}.') elif crit_separation < 0.0: raise ValueError('crit_separation is expected to be a positive ' f'real number. Got {crit_separation}.') else: self._crit_separation = crit_separation def group_stars(self, starlist): cstarlist = starlist.copy() if 'id' not in cstarlist.colnames: cstarlist.add_column(Column(name='id', data=np.arange(len(cstarlist)) + 1)) cstarlist.add_column(Column(name='group_id', data=np.zeros(len(cstarlist), dtype=int))) if not np.array_equal(cstarlist['id'], np.arange(len(cstarlist)) + 1): raise ValueError('id column must be an integer-valued sequence ' f'starting from 1. Got {cstarlist["id"]}') n = 1 while (cstarlist['group_id'] == 0).sum() > 0: init_star = cstarlist[np.where(cstarlist['group_id'] == 0)[0][0]] index = self.find_group(init_star, cstarlist[cstarlist['group_id'] == 0]) cstarlist['group_id'][index-1] = n k = 1 K = len(index) while k < K: init_star = cstarlist[cstarlist['id'] == index[k]] tmp_index = self.find_group( init_star, cstarlist[cstarlist['group_id'] == 0]) if len(tmp_index) > 0: cstarlist['group_id'][tmp_index-1] = n index = np.append(index, tmp_index) K = len(index) k += 1 n += 1 return cstarlist def find_group(self, star, starlist): """ Find the ids of those stars in ``starlist`` which are at a distance less than ``crit_separation`` from ``star``. Parameters ---------- star : `~astropy.table.Row` Star which will be either the head of a cluster or an isolated one. starlist : `~astropy.table.Table` List of star positions. Columns named as ``x_0`` and ``y_0``, which corresponds to the centroid coordinates of the sources, must be provided. Returns ------- result : `~numpy.ndarray` Array containing the ids of those stars which are at a distance less than ``crit_separation`` from ``star``. """ star_distance = np.hypot(star['x_0'] - starlist['x_0'], star['y_0'] - starlist['y_0']) distance_criteria = star_distance < self.crit_separation return np.asarray(starlist[distance_criteria]['id']) class DBSCANGroup(GroupStarsBase): """ Class to create star groups according to a distance criteria using the Density-based Spatial Clustering of Applications with Noise (DBSCAN) from scikit-learn. Parameters ---------- crit_separation : float or int Distance, in units of pixels, such that any two stars separated by less than this distance will be placed in the same group. min_samples : int, optional (default=1) Minimum number of stars necessary to form a group. metric : string or callable (default='euclidean') The metric to use when calculating distance between each pair of stars. algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, optional The algorithm to be used to actually find nearest neighbors. leaf_size : int, optional (default = 30) Leaf size passed to BallTree or cKDTree. References ---------- [1] Scikit Learn DBSCAN. https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html#sklearn.cluster.DBSCAN Notes ----- * The attribute ``crit_separation`` corresponds to ``eps`` in `sklearn.cluster.DBSCAN `_. * This class provides more general algorithms than `photutils.psf.DAOGroup`. More precisely, `photutils.psf.DAOGroup` is a special case of `photutils.psf.DBSCANGroup` when ``min_samples=1`` and ``metric=euclidean``. Additionally, `photutils.psf.DBSCANGroup` may be faster than `photutils.psf.DAOGroup`. """ def __init__(self, crit_separation, min_samples=1, metric='euclidean', algorithm='auto', leaf_size=30): self.crit_separation = crit_separation self.min_samples = min_samples self.metric = metric self.algorithm = algorithm self.leaf_size = leaf_size def group_stars(self, starlist): from sklearn.cluster import DBSCAN cstarlist = starlist.copy() if 'id' not in cstarlist.colnames: cstarlist.add_column(Column(name='id', data=np.arange(len(cstarlist)) + 1)) if not np.array_equal(cstarlist['id'], np.arange(len(cstarlist)) + 1): raise ValueError('id column must be an integer-valued sequence ' 'starting from 1. Got {cstarlist["id"]}') pos_stars = np.transpose((cstarlist['x_0'], cstarlist['y_0'])) dbscan = DBSCAN(eps=self.crit_separation, min_samples=self.min_samples, metric=self.metric, algorithm=self.algorithm, leaf_size=self.leaf_size) cstarlist['group_id'] = (dbscan.fit(pos_stars).labels_ + np.ones(len(cstarlist), dtype=int)) return cstarlist ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0145955 photutils-1.3.0/photutils/psf/matching/0000755000214200020070000000000000000000000016613 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/psf/matching/__init__.py0000644000214200020070000000033700000000000020727 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools to generate kernels for matching point spread functions. """ from .fourier import * # noqa from .windows import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/matching/fourier.py0000644000214200020070000000652500000000000020650 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for matching PSFs using Fourier methods. """ import numpy as np from numpy.fft import fft2, ifft2, fftshift, ifftshift __all__ = ['resize_psf', 'create_matching_kernel'] def resize_psf(psf, input_pixel_scale, output_pixel_scale, order=3): """ Resize a PSF using spline interpolation of the requested order. Parameters ---------- psf : 2D `~numpy.ndarray` The 2D data array of the PSF. input_pixel_scale : float The pixel scale of the input ``psf``. The units must match ``output_pixel_scale``. output_pixel_scale : float The pixel scale of the output ``psf``. The units must match ``input_pixel_scale``. order : float, optional The order of the spline interpolation (0-5). The default is 3. Returns ------- result : 2D `~numpy.ndarray` The resampled/interpolated 2D data array. """ from scipy.ndimage import zoom ratio = input_pixel_scale / output_pixel_scale return zoom(psf, ratio, order=order) / ratio**2 def create_matching_kernel(source_psf, target_psf, window=None): """ Create a kernel to match 2D point spread functions (PSF) using the ratio of Fourier transforms. Parameters ---------- source_psf : 2D `~numpy.ndarray` The source PSF. The source PSF should have higher resolution (i.e., narrower) than the target PSF. ``source_psf`` and ``target_psf`` must have the same shape and pixel scale. target_psf : 2D `~numpy.ndarray` The target PSF. The target PSF should have lower resolution (i.e., broader) than the source PSF. ``source_psf`` and ``target_psf`` must have the same shape and pixel scale. window : callable, optional The window (or taper) function or callable class instance used to remove high frequency noise from the PSF matching kernel. Some examples include: * `~photutils.psf.matching.HanningWindow` * `~photutils.psf.matching.TukeyWindow` * `~photutils.psf.matching.CosineBellWindow` * `~photutils.psf.matching.SplitCosineBellWindow` * `~photutils.psf.matching.TopHatWindow` For more information on window functions and example usage, see :ref:`psf_matching`. Returns ------- kernel : 2D `~numpy.ndarray` The matching kernel to go from ``source_psf`` to ``target_psf``. The output matching kernel is normalized such that it sums to 1. """ # inputs are copied so that they are not changed when normalizing source_psf = np.copy(np.asanyarray(source_psf)) target_psf = np.copy(np.asanyarray(target_psf)) if source_psf.shape != target_psf.shape: raise ValueError('source_psf and target_psf must have the same shape ' '(i.e., registered with the same pixel scale).') # ensure input PSFs are normalized source_psf /= source_psf.sum() target_psf /= target_psf.sum() source_otf = fftshift(fft2(source_psf)) target_otf = fftshift(fft2(target_psf)) ratio = target_otf / source_otf # apply a window function in frequency space if window is not None: ratio *= window(target_psf.shape) kernel = np.real(fftshift((ifft2(ifftshift(ratio))))) return kernel / kernel.sum() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0153756 photutils-1.3.0/photutils/psf/matching/tests/0000755000214200020070000000000000000000000017755 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/psf/matching/tests/__init__.py0000644000214200020070000000000000000000000022054 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/matching/tests/test_fourier.py0000644000214200020070000000273000000000000023043 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the fourier module. """ from astropy.modeling.fitting import LevMarLSQFitter from astropy.modeling.models import Gaussian2D import numpy as np from numpy.testing import assert_allclose import pytest from ..fourier import create_matching_kernel, resize_psf from ..windows import SplitCosineBellWindow from ....utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') def test_resize_psf(): psf1 = np.ones((5, 5)) psf2 = resize_psf(psf1, 0.1, 0.05) assert psf2.shape == (10, 10) @pytest.mark.skipif('not HAS_SCIPY') def test_create_matching_kernel(): """Test with noiseless 2D Gaussians.""" size = 25 cen = (size - 1) / 2. y, x = np.mgrid[0:size, 0:size] std1 = 3. std2 = 5. gm1 = Gaussian2D(1., cen, cen, std1, std1) gm2 = Gaussian2D(1., cen, cen, std2, std2) g1 = gm1(x, y) g2 = gm2(x, y) g1 /= g1.sum() g2 /= g2.sum() window = SplitCosineBellWindow(0.0, 0.2) k = create_matching_kernel(g1, g2, window=window) fitter = LevMarLSQFitter() gfit = fitter(gm1, x, y, k) assert_allclose(gfit.x_stddev, gfit.y_stddev) assert_allclose(gfit.x_stddev, np.sqrt(std2**2 - std1**2), 0.06) def test_create_matching_kernel_shapes(): """Test with wrong PSF shapes.""" with pytest.raises(ValueError): psf1 = np.ones((5, 5)) psf2 = np.ones((3, 3)) create_matching_kernel(psf1, psf2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/matching/tests/test_windows.py0000644000214200020070000000372400000000000023066 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the windows module. """ import numpy as np from numpy.testing import assert_allclose import pytest from ..windows import (CosineBellWindow, HanningWindow, SplitCosineBellWindow, TopHatWindow, TukeyWindow) from ....utils._optional_deps import HAS_SCIPY # noqa def test_hanning(): win = HanningWindow() data = win((5, 5)) ref = [0., 0.19715007, 0.5, 0.19715007, 0.] assert_allclose(data[1, :], ref) def test_hanning_numpy(): """Test Hanning window against 1D numpy version.""" size = 101 cen = (size - 1) // 2 shape = (size, size) win = HanningWindow() data = win(shape) ref1d = np.hanning(shape[0]) assert_allclose(data[cen, :], ref1d) def test_tukey(): win = TukeyWindow(0.5) data = win((5, 5)) ref = [0., 0.63312767, 1., 0.63312767, 0.] assert_allclose(data[1, :], ref) @pytest.mark.skipif('not HAS_SCIPY') def test_tukey_scipy(): """Test Tukey window against 1D scipy version.""" # scipy.signal.tukey was introduced in Scipy v0.16.0 from scipy.signal import tukey size = 101 cen = (size - 1) // 2 shape = (size, size) alpha = 0.4 win = TukeyWindow(alpha=alpha) data = win(shape) ref1d = tukey(shape[0], alpha=alpha) assert_allclose(data[cen, :], ref1d) def test_cosine_bell(): win = CosineBellWindow(alpha=0.8) data = win((7, 7)) ref = [0., 0., 0.19715007, 0.5, 0.19715007, 0., 0.] assert_allclose(data[2, :], ref) def test_split_cosine_bell(): win = SplitCosineBellWindow(alpha=0.8, beta=0.2) data = win((5, 5)) ref = [0., 0.3454915, 1., 0.3454915, 0.] assert_allclose(data[2, :], ref) def test_tophat(): win = TopHatWindow(beta=0.5) data = win((5, 5)) ref = [0., 1., 1., 1., 0.] assert_allclose(data[2, :], ref) def test_invalid_shape(): with pytest.raises(ValueError): win = HanningWindow() win((5,)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/matching/windows.py0000644000214200020070000001517300000000000020666 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides window (or tapering) functions for matching PSFs using Fourier methods. """ import numpy as np __all__ = ['SplitCosineBellWindow', 'HanningWindow', 'TukeyWindow', 'CosineBellWindow', 'TopHatWindow'] def _radial_distance(shape): """ Return an array where each value is the Euclidean distance from the array center. Parameters ---------- shape : tuple of int The size of the output array along each axis. Returns ------- result : `~numpy.ndarray` An array containing the Euclidean radial distances from the array center. """ if len(shape) != 2: raise ValueError('shape must have only 2 elements') position = (np.asarray(shape) - 1) / 2. x = np.arange(shape[1]) - position[1] y = np.arange(shape[0]) - position[0] xx, yy = np.meshgrid(x, y) return np.sqrt(xx**2 + yy**2) class SplitCosineBellWindow: """ Class to define a 2D split cosine bell taper function. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. beta : float, optional The inner diameter as a fraction of the array size beyond which the taper begins. ``beta`` must be less or equal to 1.0. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import SplitCosineBellWindow taper = SplitCosineBellWindow(alpha=0.4, beta=0.3) data = taper((101, 101)) plt.imshow(data, cmap='viridis', origin='lower') plt.colorbar() A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import SplitCosineBellWindow taper = SplitCosineBellWindow(alpha=0.4, beta=0.3) data = taper((101, 101)) plt.plot(data[50, :]) """ def __init__(self, alpha, beta): self.alpha = alpha self.beta = beta def __call__(self, shape): """ Return a 2D split cosine bell. Parameters ---------- shape : tuple of int The size of the output array along each axis. Returns ------- result : `~numpy.ndarray` A 2D array containing the cosine bell values. """ radial_dist = _radial_distance(shape) npts = (np.array(shape).min() - 1.) / 2. r_inner = self.beta * npts r = radial_dist - r_inner r_taper = int(np.floor(self.alpha * npts)) if r_taper != 0: f = 0.5 * (1.0 + np.cos(np.pi * r / r_taper)) else: f = np.ones(shape) f[radial_dist < r_inner] = 1. r_cut = r_inner + r_taper f[radial_dist > r_cut] = 0. return f class HanningWindow(SplitCosineBellWindow): """ Class to define a 2D `Hanning (or Hann) window `_ function. The Hann window is a taper formed by using a raised cosine with ends that touch zero. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import HanningWindow taper = HanningWindow() data = taper((101, 101)) plt.imshow(data, cmap='viridis', origin='lower') plt.colorbar() A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import HanningWindow taper = HanningWindow() data = taper((101, 101)) plt.plot(data[50, :]) """ def __init__(self): super().__init__(alpha=1.0, beta=0.0) class TukeyWindow(SplitCosineBellWindow): """ Class to define a 2D `Tukey window `_ function. The Tukey window is a taper formed by using a split cosine bell function with ends that touch zero. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import TukeyWindow taper = TukeyWindow(alpha=0.4) data = taper((101, 101)) plt.imshow(data, cmap='viridis', origin='lower') plt.colorbar() A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import TukeyWindow taper = TukeyWindow(alpha=0.4) data = taper((101, 101)) plt.plot(data[50, :]) """ def __init__(self, alpha): super().__init__(alpha=alpha, beta=1.0 - alpha) class CosineBellWindow(SplitCosineBellWindow): """ Class to define a 2D cosine bell window function. Parameters ---------- alpha : float, optional The percentage of array values that are tapered. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import CosineBellWindow taper = CosineBellWindow(alpha=0.3) data = taper((101, 101)) plt.imshow(data, cmap='viridis', origin='lower') plt.colorbar() A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import CosineBellWindow taper = CosineBellWindow(alpha=0.3) data = taper((101, 101)) plt.plot(data[50, :]) """ def __init__(self, alpha): super().__init__(alpha=alpha, beta=0.0) class TopHatWindow(SplitCosineBellWindow): """ Class to define a 2D top hat window function. Parameters ---------- beta : float, optional The inner diameter as a fraction of the array size beyond which the taper begins. ``beta`` must be less or equal to 1.0. Examples -------- .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import TopHatWindow taper = TopHatWindow(beta=0.4) data = taper((101, 101)) plt.imshow(data, cmap='viridis', origin='lower', interpolation='nearest') plt.colorbar() A 1D cut across the image center: .. plot:: :include-source: import matplotlib.pyplot as plt from photutils.psf import TopHatWindow taper = TopHatWindow(beta=0.4) data = taper((101, 101)) plt.plot(data[50, :]) """ def __init__(self, beta): super().__init__(alpha=0.0, beta=beta) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/models.py0000644000214200020070000012424600000000000016667 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides models for doing PSF/PRF-fitting photometry. """ import copy import itertools import warnings from astropy.modeling import Fittable2DModel, Parameter from astropy.nddata import NDData from astropy.utils.exceptions import AstropyWarning import numpy as np from ..aperture import CircularAperture __all__ = ['NonNormalizable', 'FittableImageModel', 'EPSFModel', 'GriddedPSFModel', 'IntegratedGaussianPRF', 'PRFAdapter'] class NonNormalizable(AstropyWarning): """ Used to indicate that a :py:class:`FittableImageModel` model is non-normalizable. """ class FittableImageModel(Fittable2DModel): r""" A fittable 2D model of an image allowing for image intensity scaling and image translations. This class takes 2D image data and computes the values of the model at arbitrary locations (including at intra-pixel, fractional positions) within this image using spline interpolation provided by :py:class:`~scipy.interpolate.RectBivariateSpline`. The fittable model provided by this class has three model parameters: an image intensity scaling factor (``flux``) which is applied to (normalized) image, and two positional parameters (``x_0`` and ``y_0``) indicating the location of a feature in the coordinate grid on which the model is to be evaluated. If this class is initialized with ``flux`` (intensity scaling factor) set to `None`, then ``flux`` is going to be estimated as ``sum(data)``. Parameters ---------- data : numpy.ndarray Array containing 2D image. origin : tuple, None, optional A reference point in the input image ``data`` array. When origin is `None`, origin will be set at the middle of the image array. If ``origin`` represents the location of a feature (e.g., the position of an intensity peak) in the input ``data``, then model parameters ``x_0`` and ``y_0`` show the location of this peak in an another target image to which this model was fitted. Fundamentally, it is the coordinate in the model's image data that should map to coordinate (``x_0``, ``y_0``) of the output coordinate system on which the model is evaluated. Alternatively, when ``origin`` is set to ``(0,0)``, then model parameters ``x_0`` and ``y_0`` are shifts by which model's image should be translated in order to match a target image. normalize : bool, optional Indicates whether or not the model should be build on normalized input image data. If true, then the normalization constant (*N*) is computed so that .. math:: N \cdot C \cdot \sum\limits_{i,j} D_{i,j} = 1, where *N* is the normalization constant, *C* is correction factor given by the parameter ``normalization_correction``, and :math:`D_{i,j}` are the elements of the input image ``data`` array. normalization_correction : float, optional A strictly positive number that represents correction that needs to be applied to model's data normalization (see *C* in the equation in the comments to ``normalize`` for more details). A possible application for this parameter is to account for aperture correction. Assuming model's data represent a PSF to be fitted to some target star, we set ``normalization_correction`` to the aperture correction that needs to be applied to the model. That is, ``normalization_correction`` in this case should be set to the ratio between the total flux of the PSF (including flux outside model's data) to the flux of model's data. Then, best fitted value of the ``flux`` model parameter will represent an aperture-corrected flux of the target star. In the case of aperture correction, ``normalization_correction`` should be a value larger than one, as the total flux, including regions outside of the aperture, should be larger than the flux inside the aperture, and thus the correction is applied as an inversely multiplied factor. fill_value : float, optional The value to be returned by the `evaluate` or ``astropy.modeling.Model.__call__`` methods when evaluation is performed outside the definition domain of the model. kwargs : dict, optional Additional optional keyword arguments to be passed directly to the `compute_interpolator` method. See `compute_interpolator` for more details. oversampling : int or tuple of two int, optional The oversampling factor(s) of the model in the ``x`` and ``y`` directions. If ``oversampling`` is a scalar it will be treated as being the same in both x and y; otherwise a tuple of two floats will be treated as ``(x_oversamp, y_oversamp)``. """ flux = Parameter(description='Intensity scaling factor for image data.', default=1.0) x_0 = Parameter(description='X-position of a feature in the image in ' 'the output coordinate grid on which the model is ' 'evaluated.', default=0.0) y_0 = Parameter(description='Y-position of a feature in the image in ' 'the output coordinate grid on which the model is ' 'evaluated.', default=0.0) def __init__(self, data, flux=flux.default, x_0=x_0.default, y_0=y_0.default, normalize=False, normalization_correction=1.0, origin=None, oversampling=1, fill_value=0.0, **kwargs): self._fill_value = fill_value self._img_norm = None self._normalization_status = 0 if normalize else 2 self._store_interpolator_kwargs(**kwargs) self._set_oversampling(oversampling) if normalization_correction <= 0: raise ValueError("'normalization_correction' must be strictly " "positive.") self._normalization_correction = normalization_correction self._data = np.array(data, copy=True, dtype=float) if not np.all(np.isfinite(self._data)): raise ValueError("All elements of input 'data' must be finite.") # set input image related parameters: self._ny, self._nx = self._data.shape self._shape = self._data.shape if self._data.size < 1: raise ValueError("Image data array cannot be zero-sized.") # set the origin of the coordinate system in image's pixel grid: self.origin = origin flux = self._initial_norm(flux, normalize) super().__init__(flux, x_0, y_0) # initialize interpolator: self.compute_interpolator(**kwargs) def _initial_norm(self, flux, normalize): if flux is None: if self._img_norm is None: self._img_norm = self._compute_raw_image_norm() flux = self._img_norm self._compute_normalization(normalize) return flux def _compute_raw_image_norm(self): """ Helper function that computes the uncorrected inverse normalization factor of input image data. This quantity is computed as the *sum of all pixel values*. .. note:: This function is intended to be overridden in a subclass if one desires to change the way the normalization factor is computed. """ return np.sum(self._data, dtype=float) def _compute_normalization(self, normalize): r""" Helper function that computes (corrected) normalization factor of the original image data. This quantity is computed as the inverse "raw image norm" (or total "flux" of model's image) corrected by the ``normalization_correction``: .. math:: N = 1/(\Phi * C), where :math:`\Phi` is the "total flux" of model's image as computed by `_compute_raw_image_norm` and *C* is the normalization correction factor. :math:`\Phi` is computed only once if it has not been previously computed. Otherwise, the existing (stored) value of :math:`\Phi` is not modified as :py:class:`FittableImageModel` does not allow image data to be modified after the object is created. .. note:: Normally, this function should not be called by the end-user. It is intended to be overridden in a subclass if one desires to change the way the normalization factor is computed. """ self._normalization_constant = 1.0 / self._normalization_correction if normalize: # compute normalization constant so that # N*C*sum(data) = 1: if self._img_norm is None: self._img_norm = self._compute_raw_image_norm() if self._img_norm != 0.0 and np.isfinite(self._img_norm): self._normalization_constant /= self._img_norm self._normalization_status = 0 else: self._normalization_constant = 1.0 self._normalization_status = 1 warnings.warn("Overflow encountered while computing " "normalization constant. Normalization " "constant will be set to 1.", NonNormalizable) else: self._normalization_status = 2 @property def oversampling(self): """ The factor by which the stored image is oversampled. An input to this model is multiplied by this factor to yield the index into the stored image. """ return self._oversampling def _set_oversampling(self, value): value = np.atleast_1d(value) if np.any(~np.isfinite(value)): raise ValueError('Oversampling factor must be a finite value') if np.any(value <= 0): raise ValueError('Oversampling factor must be greater than 0') if len(value) == 1: value = np.repeat(value, 2) if len(value) != 2: raise ValueError('Oversampling factor must have 1 or 2 ' 'elements') if value.ndim != 1: raise ValueError('Oversampling factor must be 1D') value_int = value.astype(int) if np.any(value_int != value): # e.g., 2.0 is OK, 2.1 is not raise ValueError('Oversampling elements must be integers') self._oversampling = value @property def data(self): """Get original image data.""" return self._data @property def normalized_data(self): """Get normalized and/or intensity-corrected image data.""" return self._normalization_constant * self._data @property def normalization_constant(self): """Get normalization constant.""" return self._normalization_constant @property def normalization_status(self): """ Get normalization status. Possible status values are: - 0: **Performed**. Model has been successfully normalized at user's request. - 1: **Failed**. Attempt to normalize has failed. - 2: **NotRequested**. User did not request model to be normalized. """ return self._normalization_status @property def normalization_correction(self): """ Set/Get flux correction factor. .. note:: When setting correction factor, model's flux will be adjusted accordingly such that if this model was a good fit to some target image before, then it will remain a good fit after correction factor change. """ return self._normalization_correction @normalization_correction.setter def normalization_correction(self, normalization_correction): old_cf = self._normalization_correction self._normalization_correction = normalization_correction self._compute_normalization(normalize=self._normalization_status != 2) # adjust model's flux so that if this model was a good fit to # some target image, then it will remain a good fit after # correction factor change: self.flux *= normalization_correction / old_cf @property def shape(self): """ A tuple of dimensions of the data array in numpy style (ny, nx). """ return self._shape @property def nx(self): """Number of columns in the data array.""" return self._nx @property def ny(self): """Number of rows in the data array.""" return self._ny @property def origin(self): """ A tuple of ``x`` and ``y`` coordinates of the origin of the coordinate system in terms of pixels of model's image. When setting the coordinate system origin, a tuple of two `int` or `float` may be used. If origin is set to `None`, the origin of the coordinate system will be set to the middle of the data array (``(npix-1)/2.0``). .. warning:: Modifying ``origin`` will not adjust (modify) model's parameters ``x_0`` and ``y_0``. """ return (self._x_origin, self._y_origin) @origin.setter def origin(self, origin): if origin is None: self._x_origin = (self._nx - 1) / 2.0 self._y_origin = (self._ny - 1) / 2.0 elif hasattr(origin, '__iter__') and len(origin) == 2: self._x_origin, self._y_origin = origin else: raise TypeError("Parameter 'origin' must be either None or an " "iterable with two elements.") @property def x_origin(self): """X-coordinate of the origin of the coordinate system.""" return self._x_origin @property def y_origin(self): """Y-coordinate of the origin of the coordinate system.""" return self._y_origin @property def fill_value(self): """ Fill value to be returned for coordinates outside of the domain of definition of the interpolator. If ``fill_value`` is `None`, then values outside of the domain of definition are the ones returned by the interpolator. """ return self._fill_value @fill_value.setter def fill_value(self, fill_value): self._fill_value = fill_value def _store_interpolator_kwargs(self, **kwargs): """ This function should be called in a subclass whenever model's interpolator is (re-)computed. """ self._interpolator_kwargs = copy.deepcopy(kwargs) @property def interpolator_kwargs(self): """ Get current interpolator's arguments used when interpolator was created. """ return self._interpolator_kwargs def compute_interpolator(self, **kwargs): """ Compute/define the interpolating spline. This function can be overridden in a subclass to define custom interpolators. Parameters ---------- **kwargs : dict, optional Additional optional keyword arguments: - **degree** : int, tuple, optional Degree of the interpolating spline. A tuple can be used to provide different degrees for the X- and Y-axes. Default value is degree=3. - **s** : float, optional Non-negative smoothing factor. Default value s=0 corresponds to interpolation. See :py:class:`~scipy.interpolate.RectBivariateSpline` for more details. Notes ----- * When subclassing :py:class:`FittableImageModel` for the purpose of overriding :py:func:`compute_interpolator`, the :py:func:`evaluate` may need to overridden as well depending on the behavior of the new interpolator. In addition, for improved future compatibility, make sure that the overriding method stores keyword arguments ``kwargs`` by calling ``_store_interpolator_kwargs`` method. * Use caution when modifying interpolator's degree or smoothness in a computationally intensive part of the code as it may decrease code performance due to the need to recompute interpolator. """ from scipy.interpolate import RectBivariateSpline if 'degree' in kwargs: degree = kwargs['degree'] if hasattr(degree, '__iter__') and len(degree) == 2: degx = int(degree[0]) degy = int(degree[1]) else: degx = int(degree) degy = int(degree) if degx < 0 or degy < 0: raise ValueError("Interpolator degree must be a non-negative " "integer") else: degx = 3 degy = 3 smoothness = kwargs.get('s', 0) x = np.arange(self._nx, dtype=float) y = np.arange(self._ny, dtype=float) self.interpolator = RectBivariateSpline( x, y, self._data.T, kx=degx, ky=degy, s=smoothness ) self._store_interpolator_kwargs(**kwargs) def evaluate(self, x, y, flux, x_0, y_0, use_oversampling=True): """ Evaluate the model on some input variables and provided model parameters. Parameters ---------- use_oversampling : bool, optional Whether to use the oversampling factor to calculate the model pixel indices. The default is `True`, which means the input indices will be multiplied by this factor. """ if use_oversampling: xi = self._oversampling[0] * (np.asarray(x) - x_0) yi = self._oversampling[1] * (np.asarray(y) - y_0) else: xi = np.asarray(x) - x_0 yi = np.asarray(y) - y_0 xi = xi.astype(float) yi = yi.astype(float) xi += self._x_origin yi += self._y_origin f = flux * self._normalization_constant evaluated_model = f * self.interpolator.ev(xi, yi) if self._fill_value is not None: # find indices of pixels that are outside the input pixel grid and # set these pixels to the 'fill_value': invalid = (((xi < 0) | (xi > self._nx - 1)) | ((yi < 0) | (yi > self._ny - 1))) evaluated_model[invalid] = self._fill_value return evaluated_model class EPSFModel(FittableImageModel): """ A class that models an effective PSF (ePSF). The EPSFModel is normalized such that the sum of the PSF over the (undersampled) pixels within the the input ``norm_radius`` is 1.0. This means that when the EPSF is fit to stars, the resulting flux corresponds to aperture photometry within a circular aperture of radius ``norm_radius``. While this class is a subclass of `FittableImageModel`, it is very similar. The primary differences/motivation are a few additional parameters necessary specifically for ePSFs. Parameters ---------- oversampling : int or tuple of two int, optional The oversampling factor(s) of the model in the ``x`` and ``y`` directions. If ``oversampling`` is a scalar it will be treated as being the same in both x and y; otherwise a tuple of two floats will be treated as ``(x_oversamp, y_oversamp)``. norm_radius : float, optional The radius inside which the ePSF is normalized by the sum over undersampled integer pixel values inside a circular aperture. shift_val : float, optional The fractional undersampled pixel amount (equivalent to an integer oversampled pixel value) at which to evaluate the asymmetric ePSF centroid corrections. """ def __init__(self, data, flux=1.0, x_0=0.0, y_0=0.0, normalize=True, normalization_correction=1.0, origin=None, oversampling=1, fill_value=0.0, norm_radius=5.5, shift_val=0.5, **kwargs): self._norm_radius = norm_radius self._shift_val = shift_val super().__init__(data=data, flux=flux, x_0=x_0, y_0=y_0, normalize=normalize, normalization_correction=normalization_correction, origin=origin, oversampling=oversampling, fill_value=fill_value, **kwargs) def _initial_norm(self, flux, normalize): if flux is None: if self._img_norm is None: self._img_norm = self._compute_raw_image_norm() flux = self._img_norm if normalize: self._compute_normalization() else: self._img_norm = self._compute_raw_image_norm() return flux def _compute_raw_image_norm(self): """ Compute the normalization of input image data as the flux within a given radius. """ xypos = (self._nx / 2., self._ny / 2.) # TODO: generalize "radius" (ellipse?) is oversampling is # different along x/y axes radius = self._norm_radius * self.oversampling[0] aper = CircularAperture(xypos, r=radius) flux, _ = aper.do_photometry(self._data, method='exact') return flux[0] / np.product(self.oversampling) def _compute_normalization(self): """ Helper function that computes (corrected) normalization factor of the original image data. For the ePSF this is defined as the sum over the inner N (default=5.5) pixels of the non-oversampled image. Will re-normalize the data to the value calculated. """ if self._img_norm is None: if np.sum(self._data) == 0: self._img_norm = 1 else: self._img_norm = self._compute_raw_image_norm() if self._img_norm != 0.0 and np.isfinite(self._img_norm): self._data /= (self._img_norm * self._normalization_correction) self._normalization_status = 0 else: self._normalization_status = 1 self._img_norm = 1 warnings.warn("Overflow encountered while computing " "normalization constant. Normalization " "constant will be set to 1.", NonNormalizable) def normalized_data(self): """ Overloaded dummy function that also returns self._data, as the normalization occurs within _compute_normalization in EPSFModel, and as such self._data will sum, accounting for under/oversampled pixels, to 1/self._normalization_correction. """ return self._data @FittableImageModel.origin.setter def origin(self, origin): if origin is None: self._x_origin = (self._nx - 1) / 2.0 / self.oversampling[0] self._y_origin = (self._ny - 1) / 2.0 / self.oversampling[1] elif (hasattr(origin, '__iter__') and len(origin) == 2): self._x_origin, self._y_origin = origin else: raise TypeError("Parameter 'origin' must be either None or an " "iterable with two elements.") def compute_interpolator(self, **kwargs): """ Compute/define the interpolating spline. This function can be overridden in a subclass to define custom interpolators. Parameters ---------- **kwargs : dict, optional Additional optional keyword arguments: - **degree** : int, tuple, optional Degree of the interpolating spline. A tuple can be used to provide different degrees for the X- and Y-axes. Default value is degree=3. - **s** : float, optional Non-negative smoothing factor. Default value s=0 corresponds to interpolation. See :py:class:`~scipy.interpolate.RectBivariateSpline` for more details. Notes ----- * When subclassing :py:class:`FittableImageModel` for the purpose of overriding :py:func:`compute_interpolator`, the :py:func:`evaluate` may need to overridden as well depending on the behavior of the new interpolator. In addition, for improved future compatibility, make sure that the overriding method stores keyword arguments ``kwargs`` by calling ``_store_interpolator_kwargs`` method. * Use caution when modifying interpolator's degree or smoothness in a computationally intensive part of the code as it may decrease code performance due to the need to recompute interpolator. """ from scipy.interpolate import RectBivariateSpline if 'degree' in kwargs: degree = kwargs['degree'] if hasattr(degree, '__iter__') and len(degree) == 2: degx = int(degree[0]) degy = int(degree[1]) else: degx = int(degree) degy = int(degree) if degx < 0 or degy < 0: raise ValueError("Interpolator degree must be a non-negative " "integer") else: degx = 3 degy = 3 if 's' in kwargs: smoothness = kwargs['s'] else: smoothness = 0 # Interpolator must be set to interpolate on the undersampled # pixel grid, going from 0 to len(undersampled_grid) x = np.arange(self._nx, dtype=float) / self.oversampling[0] y = np.arange(self._ny, dtype=float) / self.oversampling[1] self.interpolator = RectBivariateSpline( x, y, self._data.T, kx=degx, ky=degy, s=smoothness) self._store_interpolator_kwargs(**kwargs) def evaluate(self, x, y, flux, x_0, y_0): """ Evaluate the model on some input variables and provided model parameters. """ xi = np.asarray(x) - x_0 + self._x_origin yi = np.asarray(y) - y_0 + self._y_origin evaluated_model = flux * self.interpolator.ev(xi, yi) if self._fill_value is not None: # find indices of pixels that are outside the input pixel # grid and set these pixels to the 'fill_value': invalid = (((xi < 0) | (xi > (self._nx - 1) / self.oversampling[0])) | ((yi < 0) | (yi > (self._ny - 1) / self.oversampling[1]))) evaluated_model[invalid] = self._fill_value return evaluated_model class GriddedPSFModel(Fittable2DModel): """ A fittable 2D model containing a grid PSF models defined at specific locations that are interpolated to evaluate a PSF at an arbitrary (x, y) position. Parameters ---------- data : `~astropy.nddata.NDData` An `~astropy.nddata.NDData` object containing the grid of reference PSF arrays. The data attribute must contain a 3D `~numpy.ndarray` containing a stack of the 2D PSFs (the data shape should be (N_psf, PSF_ny, PSF_nx)). The meta attribute must be `dict` containing the following: * ``'grid_xypos'``: A list of the (x, y) grid positions of each reference PSF. The order of positions should match the first axis of the 3D `~numpy.ndarray` of PSFs. In other words, ``grid_xypos[i]`` should be the (x, y) position of the reference PSF defined in ``data[i]``. * ``'oversampling'``: The integer oversampling factor of the PSF. The meta attribute may contain other properties such as the telescope, instrument, detector, and filter of the PSF. """ flux = Parameter(description='Intensity scaling factor for the PSF ' 'model.', default=1.0) x_0 = Parameter(description='x position in the output coordinate grid ' 'where the model is evaluated.', default=0.0) y_0 = Parameter(description='y position in the output coordinate grid ' 'where the model is evaluated.', default=0.0) def __init__(self, data, flux=flux.default, x_0=x_0.default, y_0=y_0.default, fill_value=0.0): if not isinstance(data, NDData): raise TypeError('data must be an NDData instance.') if data.data.ndim != 3: raise ValueError('The NDData data attribute must be a 3D numpy ' 'ndarray') if 'grid_xypos' not in data.meta: raise ValueError('"grid_xypos" must be in the nddata meta ' 'dictionary.') if len(data.meta['grid_xypos']) != data.data.shape[0]: raise ValueError('The length of grid_xypos must match the number ' 'of input PSFs.') if 'oversampling' not in data.meta: raise ValueError('"oversampling" must be in the nddata meta ' 'dictionary.') if not np.isscalar(data.meta['oversampling']): raise ValueError('oversampling must be a scalar value') self.data = np.array(data.data, copy=True, dtype=float) self.meta = data.meta self.grid_xypos = data.meta['grid_xypos'] self.oversampling = data.meta['oversampling'] self._grid_xpos, self._grid_ypos = np.transpose(self.grid_xypos) self._xgrid = np.unique(self._grid_xpos) # also sorts values self._ygrid = np.unique(self._grid_ypos) # also sorts values if (len(list(itertools.product(self._xgrid, self._ygrid))) != len(self.grid_xypos)): raise ValueError('"grid_xypos" must form a regular grid.') self._xgrid_min = self._xgrid[0] self._xgrid_max = self._xgrid[-1] self._ygrid_min = self._ygrid[0] self._ygrid_max = self._ygrid[-1] super().__init__(flux, x_0, y_0) @staticmethod def _find_bounds_1d(data, x): """ Find the index of the lower bound where ``x`` should be inserted into ``a`` to maintain order. The index of the upper bound is the index of the lower bound plus 2. Both bound indices must be within the array. Parameters ---------- data : 1D `~numpy.ndarray` The 1D array to search. x : float The value to insert. Returns ------- index : int The index of the lower bound. """ idx = np.searchsorted(data, x) if idx == 0: idx0 = 0 elif idx == len(data): # pragma: no cover idx0 = idx - 2 else: idx0 = idx - 1 return idx0 def _find_bounding_points(self, x, y): """ Find the indices of the grid points that bound the input ``(x, y)`` position. Parameters ---------- x, y : float The ``(x, y)`` position where the PSF is to be evaluated. Returns ------- indices : list of int A list of indices of the bounding grid points. """ if not np.isscalar(x) or not np.isscalar(y): # pragma: no cover raise TypeError('x and y must be scalars') if (x < self._xgrid_min or x > self._xgrid_max or y < self._ygrid_min or y > self._ygrid_max): # pragma: no cover raise ValueError('(x, y) position is outside of the region ' 'defined by grid of PSF positions') x0 = self._find_bounds_1d(self._xgrid, x) y0 = self._find_bounds_1d(self._ygrid, y) points = list(itertools.product(self._xgrid[x0:x0 + 2], self._ygrid[y0:y0 + 2])) indices = [] for xx, yy in points: indices.append(np.argsort(np.hypot(self._grid_xpos - xx, self._grid_ypos - yy))[0]) return indices @staticmethod def _bilinear_interp(xyref, zref, xi, yi): """ Perform bilinear interpolation of four 2D arrays located at points on a regular grid. Parameters ---------- xyref : list of 4 (x, y) pairs A list of 4 ``(x, y)`` pairs that form a rectangle. zref : 3D `~numpy.ndarray` A 3D `~numpy.ndarray` of shape ``(4, nx, ny)``. The first axis corresponds to ``xyref``, i.e., ``refdata[0, :, :]`` is the 2D array located at ``xyref[0]``. xi, yi : float The ``(xi, yi)`` point at which to perform the interpolation. The ``(xi, yi)`` point must lie within the rectangle defined by ``xyref``. Returns ------- result : 2D `~numpy.ndarray` The 2D interpolated array. """ if len(xyref) != 4: raise ValueError('xyref must contain only 4 (x, y) pairs') if zref.shape[0] != 4: raise ValueError('zref must have a length of 4 on the first ' 'axis.') xyref = [tuple(i) for i in xyref] idx = sorted(range(len(xyref)), key=xyref.__getitem__) xyref = sorted(xyref) # sort by x, then y (x0, y0), (_x0, y1), (x1, _y0), (_x1, _y1) = xyref if x0 != _x0 or x1 != _x1 or y0 != _y0 or y1 != _y1: raise ValueError('The refxy points do not form a rectangle.') if not np.isscalar(xi): xi = xi[0] if not np.isscalar(yi): yi = yi[0] if not x0 <= xi <= x1 or not y0 <= yi <= y1: raise ValueError('The (x, y) input is not within the rectangle ' 'defined by xyref.') data = np.asarray(zref)[idx] weights = np.array([(x1 - xi) * (y1 - yi), (x1 - xi) * (yi - y0), (xi - x0) * (y1 - yi), (xi - x0) * (yi - y0)]) norm = (x1 - x0) * (y1 - y0) return np.sum(data * weights[:, None, None], axis=0) / norm def _compute_local_model(self, x_0, y_0): """ Return `FittableImageModel` for interpolated PSF at some (x_0, y_0). """ # NOTE: this is needed because the PSF photometry routines input # length-1 values instead of scalars. TODO: fix the photometry # routines. if not np.isscalar(x_0): x_0 = x_0[0] if not np.isscalar(y_0): y_0 = y_0[0] if (x_0 < self._xgrid_min or x_0 > self._xgrid_max or y_0 < self._ygrid_min or y_0 > self._ygrid_max): # position is outside of the grid, so simply use the # closest reference PSF self._ref_indices = np.argsort(np.hypot(self._grid_xpos - x_0, self._grid_ypos - y_0))[0] self._psf_interp = self.data[self._ref_indices, :, :] else: # find the four bounding reference PSFs and interpolate self._ref_indices = self._find_bounding_points(x_0, y_0) xyref = np.array(self.grid_xypos)[self._ref_indices] psfs = self.data[self._ref_indices, :, :] self._psf_interp = self._bilinear_interp(xyref, psfs, x_0, y_0) # Construct the model using the interpolated supersampled data psfmodel = FittableImageModel(self._psf_interp, oversampling=self.oversampling) return psfmodel def evaluate(self, x, y, flux, x_0, y_0): """ Evaluate the `GriddedPSFModel` for the input parameters. """ # Get the local PSF at the (x_0,y_0) psfmodel = self._compute_local_model(x_0, y_0) # now evaluate the PSF at the (x_0, y_0) subpixel position on # the input (x, y) values return psfmodel.evaluate(x, y, flux, x_0, y_0) class IntegratedGaussianPRF(Fittable2DModel): r""" Circular Gaussian model integrated over pixels. Because it is integrated, this model is considered a PRF, *not* a PSF (see :ref:`psf-terminology` for more about the terminology used here.) This model is a Gaussian *integrated* over an area of ``1`` (in units of the model input coordinates, e.g., 1 pixel). This is in contrast to the apparently similar `astropy.modeling.functional_models.Gaussian2D`, which is the value of a 2D Gaussian *at* the input coordinates, with no integration. So this model is equivalent to assuming the PSF is Gaussian at a *sub-pixel* level. Parameters ---------- sigma : float Width of the Gaussian PSF. flux : float, optional Total integrated flux over the entire PSF x_0 : float, optional Position of the peak in x direction. y_0 : float, optional Position of the peak in y direction. Notes ----- This model is evaluated according to the following formula: .. math:: f(x, y) = \frac{F}{4} \left[ {\rm erf} \left(\frac{x - x_0 + 0.5} {\sqrt{2} \sigma} \right) - {\rm erf} \left(\frac{x - x_0 - 0.5} {\sqrt{2} \sigma} \right) \right] \left[ {\rm erf} \left(\frac{y - y_0 + 0.5} {\sqrt{2} \sigma} \right) - {\rm erf} \left(\frac{y - y_0 - 0.5} {\sqrt{2} \sigma} \right) \right] where ``erf`` denotes the error function and ``F`` the total integrated flux. """ flux = Parameter(default=1) x_0 = Parameter(default=0) y_0 = Parameter(default=0) sigma = Parameter(default=1, fixed=True) _erf = None @property def bounding_box(self): halfwidth = 4 * self.sigma return ((int(self.y_0 - halfwidth), int(self.y_0 + halfwidth)), (int(self.x_0 - halfwidth), int(self.x_0 + halfwidth))) def __init__(self, sigma=sigma.default, x_0=x_0.default, y_0=y_0.default, flux=flux.default, **kwargs): if self._erf is None: from scipy.special import erf self.__class__._erf = erf super().__init__(n_models=1, sigma=sigma, x_0=x_0, y_0=y_0, flux=flux, **kwargs) def evaluate(self, x, y, flux, x_0, y_0, sigma): """Model function Gaussian PSF model.""" return (flux / 4 * ((self._erf((x - x_0 + 0.5) / (np.sqrt(2) * sigma)) - self._erf((x - x_0 - 0.5) / (np.sqrt(2) * sigma))) * (self._erf((y - y_0 + 0.5) / (np.sqrt(2) * sigma)) - self._erf((y - y_0 - 0.5) / (np.sqrt(2) * sigma))))) class PRFAdapter(Fittable2DModel): """ A model that adapts a supplied PSF model to act as a PRF. It integrates the PSF model over pixel "boxes". A critical built-in assumption is that the PSF model scale and location parameters are in *pixel* units. Parameters ---------- psfmodel : a 2D model The model to assume as representative of the PSF renormalize_psf : bool If True, the model will be integrated from -inf to inf and re-scaled so that the total integrates to 1. Note that this renormalization only occurs *once*, so if the total flux of ``psfmodel`` depends on position, this will *not* be correct. xname : str or None The name of the ``psfmodel`` parameter that corresponds to the x-axis center of the PSF. If None, the model will be assumed to be centered at x=0. yname : str or None The name of the ``psfmodel`` parameter that corresponds to the y-axis center of the PSF. If None, the model will be assumed to be centered at y=0. fluxname : str or None The name of the ``psfmodel`` parameter that corresponds to the total flux of the star. If None, a scaling factor will be applied by the ``PRFAdapter`` instead of modifying the ``psfmodel``. Notes ----- This current implementation of this class (using numerical integration for each pixel) is extremely slow, and only suited for experimentation over relatively few small regions. """ flux = Parameter(default=1) x_0 = Parameter(default=0) y_0 = Parameter(default=0) def __init__(self, psfmodel, renormalize_psf=True, flux=flux.default, x_0=x_0.default, y_0=y_0.default, xname=None, yname=None, fluxname=None, **kwargs): self.psfmodel = psfmodel.copy() if renormalize_psf: from scipy.integrate import dblquad self._psf_scale_factor = 1. / dblquad(self.psfmodel, -np.inf, np.inf, lambda x: -np.inf, lambda x: np.inf)[0] else: self._psf_scale_factor = 1 self.xname = xname self.yname = yname self.fluxname = fluxname # these can be used to adjust the integration behavior. Might be # used in the future to expose how the integration happens self._dblquadkwargs = {} super().__init__(n_models=1, x_0=x_0, y_0=y_0, flux=flux, **kwargs) def evaluate(self, x, y, flux, x_0, y_0): """The evaluation function for PRFAdapter.""" if self.xname is None: dx = x - x_0 else: dx = x setattr(self.psfmodel, self.xname, x_0) if self.xname is None: dy = y - y_0 else: dy = y setattr(self.psfmodel, self.yname, y_0) if self.fluxname is None: return (flux * self._psf_scale_factor * self._integrated_psfmodel(dx, dy)) else: setattr(self.psfmodel, self.yname, flux * self._psf_scale_factor) return self._integrated_psfmodel(dx, dy) def _integrated_psfmodel(self, dx, dy): from scipy.integrate import dblquad # infer type/shape from the PSF model. Seems wasteful, but the # integration step is a *lot* more expensive so its just peanuts out = np.empty_like(self.psfmodel(dx, dy)) outravel = out.ravel() for i, (xi, yi) in enumerate(zip(dx.ravel(), dy.ravel())): outravel[i] = dblquad(self.psfmodel, xi-0.5, xi+0.5, lambda x: yi-0.5, lambda x: yi+0.5, **self._dblquadkwargs)[0] return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/photometry.py0000644000214200020070000013004500000000000017610 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides classes to perform PSF-fitting photometry. """ import warnings from astropy.modeling.fitting import LevMarLSQFitter from astropy.nddata.utils import overlap_slices, NoOverlapError from astropy.stats import SigmaClip, gaussian_sigma_to_fwhm from astropy.table import Column, QTable, hstack, vstack from astropy.utils.exceptions import AstropyUserWarning import numpy as np from .groupstars import DAOGroup from .utils import (_extract_psf_fitting_names, get_grouped_psf_model, subtract_psf) from ..aperture import CircularAperture, aperture_photometry from ..background import MMMBackground from ..detection import DAOStarFinder from ..utils.exceptions import NoDetectionsWarning from ..utils._misc import _get_version_info __all__ = ['BasicPSFPhotometry', 'IterativelySubtractedPSFPhotometry', 'DAOPhotPSFPhotometry'] class BasicPSFPhotometry: """ This class implements a PSF photometry algorithm that can find sources in an image, group overlapping sources into a single model, fit the model to the sources, and subtracting the models from the image. This is roughly equivalent to the DAOPHOT routines FIND, GROUP, NSTAR, and SUBTRACT. This implementation allows a flexible and customizable interface to perform photometry. For instance, one is able to use different implementations for grouping and finding sources by using ``group_maker`` and ``finder`` respectivelly. In addition, sky background estimation is performed by ``bkg_estimator``. Parameters ---------- group_maker : callable or `~photutils.psf.GroupStarsBase` ``group_maker`` should be able to decide whether a given star overlaps with any other and label them as belonging to the same group. ``group_maker`` receives as input an `~astropy.table.Table` object with columns named as ``id``, ``x_0``, ``y_0``, in which ``x_0`` and ``y_0`` have the same meaning of ``xcentroid`` and ``ycentroid``. This callable must return an `~astropy.table.Table` with columns ``id``, ``x_0``, ``y_0``, and ``group_id``. The column ``group_id`` should contain integers starting from ``1`` that indicate which group a given source belongs to. See, e.g., `~photutils.psf.DAOGroup`. bkg_estimator : callable, instance of any \ `~photutils.background.BackgroundBase` subclass, or None ``bkg_estimator`` should be able to compute either a scalar background or a 2D background of a given 2D image. See, e.g., `~photutils.background.MedianBackground`. If None, no background subtraction is performed. psf_model : `astropy.modeling.Fittable2DModel` instance PSF or PRF model to fit the data. Could be one of the models in this package like `~photutils.psf.sandbox.DiscretePRF`, `~photutils.psf.IntegratedGaussianPRF`, or any other suitable 2D model. This object needs to identify three parameters (position of center in x and y coordinates and the flux) in order to set them to suitable starting values for each fit. The names of these parameters should be given as ``x_0``, ``y_0`` and ``flux``. `~photutils.psf.prepare_psf_model` can be used to prepare any 2D model to match this assumption. fitshape : int or length-2 array-like Rectangular shape around the center of a star which will be used to collect the data to do the fitting. Can be an integer to be the same along both axes. For example, 5 is the same as (5, 5), which means to fit only at the following relative pixel positions: [-2, -1, 0, 1, 2]. Each element of ``fitshape`` must be an odd number. finder : callable or instance of any \ `~photutils.detection.StarFinderBase` subclasses or None ``finder`` should be able to identify stars, i.e., compute a rough estimate of the centroids, in a given 2D image. ``finder`` receives as input a 2D image and returns an `~astropy.table.Table` object which contains columns with names: ``id``, ``xcentroid``, ``ycentroid``, and ``flux``. In which ``id`` is an integer-valued column starting from ``1``, ``xcentroid`` and ``ycentroid`` are center position estimates of the sources and ``flux`` contains flux estimates of the sources. See, e.g., `~photutils.detection.DAOStarFinder`. If ``finder`` is ``None``, initial guesses for positions of objects must be provided. fitter : `~astropy.modeling.fitting.Fitter` instance Fitter object used to compute the optimized centroid positions and/or flux of the identified sources. See `~astropy.modeling.fitting` for more details on fitters. aperture_radius : `None` or float The radius (in units of pixels) used to compute initial estimates for the fluxes of sources. ``aperture_radius`` must be set if initial flux guesses are not input to the photometry class via the ``init_guesses`` keyword. For tabular PSF models (e.g., an `EPSFModel`), you must input the ``aperture_radius`` keyword. For analytical PSF models, alternatively you may define a FWHM attribute on your input psf_model. extra_output_cols : list of str, optional List of additional columns for parameters derived by any of the intermediate fitting steps (e.g., ``finder``), such as roundness or sharpness. Notes ----- Note that an ambiguity arises whenever ``finder`` and ``init_guesses`` (keyword argument for ``do_photometry``) are both not ``None``. In this case, ``finder`` is ignored and initial guesses are taken from ``init_guesses``. In addition, an warning is raised to remaind the user about this behavior. If there are problems with fitting large groups, change the parameters of the grouping algorithm to reduce the number of sources in each group or input a ``star_groups`` table that only includes the groups that are relevant (e.g., manually remove all entries that coincide with artifacts). References ---------- [1] Stetson, Astronomical Society of the Pacific, Publications, (ISSN 0004-6280), vol. 99, March 1987, p. 191-222. Available at: https://ui.adsabs.harvard.edu/abs/1987PASP...99..191S/abstract """ def __init__(self, group_maker, bkg_estimator, psf_model, fitshape, finder=None, fitter=LevMarLSQFitter(), aperture_radius=None, extra_output_cols=None): self.group_maker = group_maker self.bkg_estimator = bkg_estimator self.psf_model = psf_model self.fitter = fitter self.fitshape = fitshape self.finder = finder self.aperture_radius = aperture_radius self._pars_to_set = None self._pars_to_output = None self._residual_image = None self._extra_output_cols = extra_output_cols @property def fitshape(self): return self._fitshape @fitshape.setter def fitshape(self, value): value = np.asarray(value) # assume a lone value should mean both axes if value.shape == (): value = np.array((value, value)) if value.size == 2: if np.all(value) > 0: if np.all(value % 2) == 1: self._fitshape = tuple(value) else: raise ValueError('fitshape must be odd integer-valued, ' f'received fitshape={value}') else: raise ValueError('fitshape must have positive elements, ' f'received fitshape={value}') else: raise ValueError('fitshape must have two dimensions, ' f'received fitshape={value}') @property def aperture_radius(self): return self._aperture_radius @aperture_radius.setter def aperture_radius(self, value): if isinstance(value, (int, float)) and value > 0: self._aperture_radius = value elif value is None: self._aperture_radius = value else: raise ValueError('aperture_radius must be a positive number') def get_residual_image(self): """ Return an image that is the result of the subtraction between the original image and the fitted sources. Returns ------- residual_image : 2D array-like, `~astropy.io.fits.ImageHDU`, \ `~astropy.io.fits.HDUList` """ return self._residual_image def set_aperture_radius(self): """ Set the fallback aperture radius for initial flux calculations in cases where no flux is supplied for a given star. """ if hasattr(self.psf_model, 'fwhm'): self.aperture_radius = self.psf_model.fwhm.value elif hasattr(self.psf_model, 'sigma'): self.aperture_radius = (self.psf_model.sigma.value * gaussian_sigma_to_fwhm) # If PSF model doesn't have FWHM or sigma value -- as it # is not a Gaussian; most likely because it's an ePSF -- # then we fall back on fitting a circle of the average # size of the fitting box. As ``fitshape`` is the width # of the box, we need (width-1)/2 as the radius. else: self.aperture_radius = float(np.amin((np.asanyarray( self.fitshape) - 1) / 2)) warnings.warn('aperture_radius is None and could not ' 'be determined by psf_model. Setting ' 'radius to the smallest fitshape size. ' 'This aperture radius will be used if ' 'initial fluxes require computing for any ' 'input stars. If fitshape is significantly ' 'larger than the psf_model core lengthscale, ' 'consider supplying a specific aperture_radius.', AstropyUserWarning) def __call__(self, image, init_guesses=None): """ Perform PSF photometry. See `do_photometry` for more details including the `__call__` signature. """ return self.do_photometry(image, init_guesses) def do_photometry(self, image, init_guesses=None): """ Perform PSF photometry in ``image``. This method assumes that ``psf_model`` has centroids and flux parameters which will be fitted to the data provided in ``image``. A compound model, in fact a sum of ``psf_model``, will be fitted to groups of stars automatically identified by ``group_maker``. Also, ``image`` is not assumed to be background subtracted. If ``init_guesses`` are not ``None`` then this method uses ``init_guesses`` as initial guesses for the centroids. If the centroid positions are set as ``fixed`` in the PSF model ``psf_model``, then the optimizer will only consider the flux as a variable. Parameters ---------- image : 2D array-like, `~astropy.io.fits.ImageHDU`, \ `~astropy.io.fits.HDUList` Image to perform photometry. init_guesses: `~astropy.table.Table` Table which contains the initial guesses (estimates) for the set of parameters. Columns 'x_0' and 'y_0' which represent the positions (in pixel coordinates) for each object must be present. 'flux_0' can also be provided to set initial fluxes. If 'flux_0' is not provided, aperture photometry is used to estimate initial values for the fluxes. Additional columns of the form '_0' will be used to set the initial guess for any parameters of the ``psf_model`` model that are not fixed. If ``init_guesses`` supplied with ``extra_output_cols`` the initial values are used; if the columns specified in ``extra_output_cols`` are not given in ``init_guesses`` then NaNs will be returned. Returns ------- output_tab : `~astropy.table.Table` or None Table with the photometry results, i.e., centroids and fluxes estimations and the initial estimates used to start the fitting process. Uncertainties on the fitted parameters are reported as columns called ``_unc`` provided that the fitter object contains a dictionary called ``fit_info`` with the key ``param_cov``, which contains the covariance matrix. If ``param_cov`` is not present, uncertanties are not reported. """ if self.bkg_estimator is not None: image = image - self.bkg_estimator(image) if self.aperture_radius is None: self.set_aperture_radius() skip_group_maker = False if init_guesses is not None: # make sure the code does not modify user's input init_guesses = init_guesses.copy() if self.finder is not None: warnings.warn('Both init_guesses and finder are different ' 'than None, which is ambiguous. finder is ' 'going to be ignored.', AstropyUserWarning) colnames = init_guesses.colnames if 'group_id' in colnames: warnings.warn('init_guesses contains a "group_id" column. ' 'The group_maker step will be skipped.', AstropyUserWarning) skip_group_maker = True if 'flux_0' not in colnames: positions = np.transpose((init_guesses['x_0'], init_guesses['y_0'])) apertures = CircularAperture(positions, r=self.aperture_radius) init_guesses['flux_0'] = aperture_photometry( image, apertures)['aperture_sum'] # if extra_output_cols have been given, check whether init_guesses # was supplied with extra_output_cols pre-attached and populate # columns not given with NaNs if self._extra_output_cols is not None: for col_name in self._extra_output_cols: if col_name not in init_guesses.colnames: init_guesses[col_name] = np.full(len(init_guesses), np.nan) else: if self.finder is None: raise ValueError('Finder cannot be None if init_guesses are ' 'not given.') sources = self.finder(image) if len(sources) > 0: positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) apertures = CircularAperture(positions, r=self.aperture_radius) sources['aperture_flux'] = aperture_photometry( image, apertures)['aperture_sum'] # init_guesses should be the initial 3 required # parameters (x, y, flux) and then concatenated with any # additional sources, if there are any init_guesses = QTable(names=['x_0', 'y_0', 'flux_0'], data=[sources['xcentroid'], sources['ycentroid'], sources['aperture_flux']]) # Currently only needed for the finder, as group_maker and # nstar return the original table with new columns, unlike # finder self._get_additional_columns(sources, init_guesses) self._define_fit_param_names() for p0, param in self._pars_to_set.items(): if p0 not in init_guesses.colnames: init_guesses[p0] = (len(init_guesses) * [getattr(self.psf_model, param).value]) if skip_group_maker: star_groups = init_guesses else: star_groups = self.group_maker(init_guesses) output_tab, self._residual_image = self.nstar(image, star_groups) star_groups = star_groups.group_by('group_id') if hasattr(output_tab, 'update'): # requires Astropy >= 5.0 star_groups.update(output_tab) else: common_cols = set(star_groups.colnames).intersection( output_tab.colnames) for name, col in output_tab.items(): if name in common_cols: star_groups.replace_column(name, col, copy=True) else: star_groups.add_column(col, name=name, copy=True) star_groups.meta = {'version': _get_version_info()} return star_groups def nstar(self, image, star_groups): """ Fit, as appropriate, a compound or single model to the given ``star_groups``. Groups are fitted sequentially from the smallest to the biggest. In each iteration, ``image`` is subtracted by the previous fitted group. Parameters ---------- image : numpy.ndarray Background-subtracted image. star_groups : `~astropy.table.Table` This table must contain the following columns: ``id``, ``group_id``, ``x_0``, ``y_0``, ``flux_0``. ``x_0`` and ``y_0`` are initial estimates of the centroids and ``flux_0`` is an initial estimate of the flux. Additionally, columns named as ``_0`` are required if any other parameter in the psf model is free (i.e., the ``fixed`` attribute of that parameter is ``False``). Returns ------- result_tab : `~astropy.table.QTable` Astropy table that contains photometry results. image : numpy.ndarray Residual image. """ result_tab = QTable() for param_tab_name in self._pars_to_output.keys(): result_tab.add_column(Column(name=param_tab_name)) unc_tab = QTable() for param, isfixed in self.psf_model.fixed.items(): if not isfixed: unc_tab.add_column(Column(name=param + "_unc")) y, x = np.indices(image.shape) star_groups = star_groups.group_by('group_id') for n in range(len(star_groups.groups)): group_psf = get_grouped_psf_model(self.psf_model, star_groups.groups[n], self._pars_to_set) usepixel = np.zeros_like(image, dtype=bool) for row in star_groups.groups[n]: usepixel[overlap_slices(large_array_shape=image.shape, small_array_shape=self.fitshape, position=(row['y_0'], row['x_0']), mode='trim')[0]] = True fit_model = self.fitter(group_psf, x[usepixel], y[usepixel], image[usepixel]) param_table = self._model_params2table(fit_model, star_groups.groups[n]) result_tab = vstack([result_tab, param_table]) param_cov = self.fitter.fit_info.get('param_cov', None) if param_cov is not None: unc_tab = vstack([unc_tab, self._get_uncertainties( len(star_groups.groups[n]))]) # do not subtract if the fitting did not go well try: image = subtract_psf(image, self.psf_model, param_table, subshape=self.fitshape) except NoOverlapError: pass if param_cov is not None: result_tab = hstack([result_tab, unc_tab]) return result_tab, image def _get_additional_columns(self, in_table, out_table): """ Function to parse additional columns from ``in_table`` and add them to ``out_table``. """ if self._extra_output_cols is not None: for col_name in self._extra_output_cols: if col_name in in_table.colnames: out_table[col_name] = in_table[col_name] def _define_fit_param_names(self): """ Convenience function to define mappings between the names of the columns in the initial guess table (and the name of the fitted parameters) and the actual name of the parameters in the model. This method sets the following parameters on the ``self`` object: * ``pars_to_set`` : Dict which maps the names of the parameters initial guesses to the actual name of the parameter in the model. * ``pars_to_output`` : Dict which maps the names of the fitted parameters to the actual name of the parameter in the model. """ xname, yname, fluxname = _extract_psf_fitting_names(self.psf_model) self._pars_to_set = {'x_0': xname, 'y_0': yname, 'flux_0': fluxname} self._pars_to_output = {'x_fit': xname, 'y_fit': yname, 'flux_fit': fluxname} for p, isfixed in self.psf_model.fixed.items(): p0 = p + '_0' pfit = p + '_fit' if p not in (xname, yname, fluxname) and not isfixed: self._pars_to_set[p0] = p self._pars_to_output[pfit] = p def _get_uncertainties(self, star_group_size): """ Retrieve uncertainties on fitted parameters from the fitter object. Parameters ---------- star_group_size : int Number of stars in the given group. Returns ------- unc_tab : `~astropy.table.QTable` A table which contains uncertainties on the fitted parameters. The uncertainties are reported as one standard deviation. """ unc_tab = QTable() for param_name in self.psf_model.param_names: if not self.psf_model.fixed[param_name]: unc_tab.add_column(Column(name=param_name + "_unc", data=np.empty(star_group_size))) k = 0 n_fit_params = len(unc_tab.colnames) param_cov = self.fitter.fit_info.get('param_cov', None) for i in range(star_group_size): unc_tab[i] = np.sqrt(np.diag(param_cov))[k: k + n_fit_params] k = k + n_fit_params return unc_tab def _model_params2table(self, fit_model, star_group): """ Place fitted parameters into an astropy table. Parameters ---------- fit_model : `astropy.modeling.Fittable2DModel` instance PSF or PRF model to fit the data. Could be one of the models in this package like `~photutils.psf.sandbox.DiscretePRF`, `~photutils.psf.IntegratedGaussianPRF`, or any other suitable 2D model. star_group : `~astropy.table.Table` the star group instance. Returns ------- param_tab : `~astropy.table.QTable` A table that contains the fitted parameters. """ param_tab = QTable() for param_tab_name in self._pars_to_output.keys(): param_tab.add_column(Column(name=param_tab_name, data=np.empty(len(star_group)))) if len(star_group) > 1: for i in range(len(star_group)): for param_tab_name, param_name in self._pars_to_output.items(): # get sub_model corresponding to star with index i as name # name was set in utils.get_grouped_psf_model() # we can't use model['name'] here as that only # searches leaves and we might want a intermediate # node of the tree sub_models = [model for model in fit_model.traverse_postorder() if model.name == i] if len(sub_models) != 1: raise ValueError('sub_models must have a length of 1') sub_model = sub_models[0] param_tab[param_tab_name][i] = getattr(sub_model, param_name).value else: for param_tab_name, param_name in self._pars_to_output.items(): param_tab[param_tab_name] = getattr(fit_model, param_name).value return param_tab class IterativelySubtractedPSFPhotometry(BasicPSFPhotometry): """ This class implements an iterative algorithm to perform point spread function photometry in crowded fields. This consists of applying a loop of find sources, make groups, fit groups, subtract groups, and then repeat until no more stars are detected or a given number of iterations is reached. Parameters ---------- group_maker : callable or `~photutils.psf.GroupStarsBase` ``group_maker`` should be able to decide whether a given star overlaps with any other and label them as belonging to the same group. ``group_maker`` receives as input an `~astropy.table.Table` object with columns named as ``id``, ``x_0``, ``y_0``, in which ``x_0`` and ``y_0`` have the same meaning of ``xcentroid`` and ``ycentroid``. This callable must return an `~astropy.table.Table` with columns ``id``, ``x_0``, ``y_0``, and ``group_id``. The column ``group_id`` should contain integers starting from ``1`` that indicate which group a given source belongs to. See, e.g., `~photutils.psf.DAOGroup`. bkg_estimator : callable, instance of any \ `~photutils.background.BackgroundBase` subclass, or None ``bkg_estimator`` should be able to compute either a scalar background or a 2D background of a given 2D image. See, e.g., `~photutils.background.MedianBackground`. If None, no background subtraction is performed. psf_model : `astropy.modeling.Fittable2DModel` instance PSF or PRF model to fit the data. Could be one of the models in this package like `~photutils.psf.sandbox.DiscretePRF`, `~photutils.psf.IntegratedGaussianPRF`, or any other suitable 2D model. This object needs to identify three parameters (position of center in x and y coordinates and the flux) in order to set them to suitable starting values for each fit. The names of these parameters should be given as ``x_0``, ``y_0`` and ``flux``. `~photutils.psf.prepare_psf_model` can be used to prepare any 2D model to match this assumption. fitshape : int or length-2 array-like Rectangular shape around the center of a star which will be used to collect the data to do the fitting. Can be an integer to be the same along both axes. For example, 5 is the same as (5, 5), which means to fit only at the following relative pixel positions: [-2, -1, 0, 1, 2]. Each element of ``fitshape`` must be an odd number. finder : callable or instance of any \ `~photutils.detection.StarFinderBase` subclasses ``finder`` should be able to identify stars, i.e., compute a rough estimate of the centroids, in a given 2D image. ``finder`` receives as input a 2D image and returns an `~astropy.table.Table` object which contains columns with names: ``id``, ``xcentroid``, ``ycentroid``, and ``flux``. In which ``id`` is an integer-valued column starting from ``1``, ``xcentroid`` and ``ycentroid`` are center position estimates of the sources and ``flux`` contains flux estimates of the sources. See, e.g., `~photutils.detection.DAOStarFinder` or `~photutils.detection.IRAFStarFinder`. fitter : `~astropy.modeling.fitting.Fitter` instance Fitter object used to compute the optimized centroid positions and/or flux of the identified sources. See `~astropy.modeling.fitting` for more details on fitters. aperture_radius : float The radius (in units of pixels) used to compute initial estimates for the fluxes of sources. If ``None``, one FWHM will be used if it can be determined from the ```psf_model``. niters : int or None Number of iterations to perform of the loop FIND, GROUP, SUBTRACT, NSTAR. If None, iterations will proceed until no more stars remain. Note that in this case it is *possible* that the loop will never end if the PSF has structure that causes subtraction to create new sources infinitely. extra_output_cols : list of str, optional List of additional columns for parameters derived by any of the intermediate fitting steps (e.g., ``finder``), such as roundness or sharpness. Notes ----- If there are problems with fitting large groups, change the parameters of the grouping algorithm to reduce the number of sources in each group or input a ``star_groups`` table that only includes the groups that are relevant (e.g., manually remove all entries that coincide with artifacts). References ---------- [1] Stetson, Astronomical Society of the Pacific, Publications, (ISSN 0004-6280), vol. 99, March 1987, p. 191-222. Available at: https://ui.adsabs.harvard.edu/abs/1987PASP...99..191S/abstract """ def __init__(self, group_maker, bkg_estimator, psf_model, fitshape, finder, fitter=LevMarLSQFitter(), niters=3, aperture_radius=None, extra_output_cols=None): super().__init__(group_maker, bkg_estimator, psf_model, fitshape, finder, fitter, aperture_radius, extra_output_cols) self.niters = niters @property def niters(self): return self._niters @niters.setter def niters(self, value): if value is None: self._niters = None else: try: if value <= 0: raise ValueError('niters must be positive.') else: self._niters = int(value) except ValueError: raise ValueError('niters must be None or an integer or ' 'convertable into an integer.') @property def finder(self): return self._finder @finder.setter def finder(self, value): if value is None: raise ValueError("finder cannot be None for " "IterativelySubtractedPSFPhotometry - you may " "want to use BasicPSFPhotometry. Please see the " "Detection section on photutils documentation.") else: self._finder = value def do_photometry(self, image, init_guesses=None): """ Perform PSF photometry in ``image``. This method assumes that ``psf_model`` has centroids and flux parameters which will be fitted to the data provided in ``image``. A compound model, in fact a sum of ``psf_model``, will be fitted to groups of stars automatically identified by ``group_maker``. Also, ``image`` is not assumed to be background subtracted. If ``init_guesses`` are not ``None`` then this method uses ``init_guesses`` as initial guesses for the centroids. If the centroid positions are set as ``fixed`` in the PSF model ``psf_model``, then the optimizer will only consider the flux as a variable. Parameters ---------- image : 2D array-like, `~astropy.io.fits.ImageHDU`, \ `~astropy.io.fits.HDUList` Image to perform photometry. init_guesses: `~astropy.table.Table` Table which contains the initial guesses (estimates) for the set of parameters. Columns 'x_0' and 'y_0' which represent the positions (in pixel coordinates) for each object must be present. 'flux_0' can also be provided to set initial fluxes. If 'flux_0' is not provided, aperture photometry is used to estimate initial values for the fluxes. Additional columns of the form '_0' will be used to set the initial guess for any parameters of the ``psf_model`` model that are not fixed. If ``init_guesses`` supplied with ``extra_output_cols`` the initial values are used; if the columns specified in ``extra_output_cols`` are not given in ``init_guesses`` then NaNs will be returned. Returns ------- output_table : `~astropy.table.Table` or None A table with the photometry results, i.e., centroids and fluxes estimations and the initial estimates used to start the fitting process. Uncertainties on the fitted parameters are reported as columns called ``_unc`` provided that the fitter object contains a dictionary called ``fit_info`` with the key ``param_cov``, which contains the covariance matrix. """ if init_guesses is not None: table = super().do_photometry(image, init_guesses) table['iter_detected'] = np.ones(table['x_fit'].shape, dtype=int) # n_start = 2 because it starts in the second iteration # since the first iteration is above output_table = self._do_photometry(n_start=2) output_table = vstack([table, output_table]) else: if self.bkg_estimator is not None: self._residual_image = image - self.bkg_estimator(image) else: self._residual_image = image if self.aperture_radius is None: self.set_aperture_radius() output_table = self._do_photometry() output_table.meta = {'version': _get_version_info()} return QTable(output_table) def _do_photometry(self, n_start=1): """ Helper function which performs the iterations of the photometry process. Parameters ---------- n_start : int Integer representing the start index of the iteration. It is 1 if init_guesses are None, and 2 otherwise. Returns ------- output_table : `~astropy.table.Table` or None Table with the photometry results, i.e., centroids and fluxes estimations and the initial estimates used to start the fitting process. """ output_table = QTable() self._define_fit_param_names() for (init_parname, fit_parname) in zip(self._pars_to_set.keys(), self._pars_to_output.keys()): output_table.add_column(Column(name=init_parname)) output_table.add_column(Column(name=fit_parname)) sources = self.finder(self._residual_image) n = n_start while((sources is not None and len(sources) > 0) and (self.niters is None or n <= self.niters)): positions = np.transpose((sources['xcentroid'], sources['ycentroid'])) apertures = CircularAperture(positions, r=self.aperture_radius) sources['aperture_flux'] = aperture_photometry( self._residual_image, apertures)['aperture_sum'] init_guess_tab = QTable(names=['id', 'x_0', 'y_0', 'flux_0'], data=[sources['id'], sources['xcentroid'], sources['ycentroid'], sources['aperture_flux']]) self._get_additional_columns(sources, init_guess_tab) for param_tab_name, param_name in self._pars_to_set.items(): if param_tab_name not in (['x_0', 'y_0', 'flux_0']): init_guess_tab.add_column( Column(name=param_tab_name, data=(getattr(self.psf_model, param_name) * np.ones(len(sources))))) star_groups = self.group_maker(init_guess_tab) table, self._residual_image = super().nstar( self._residual_image, star_groups) star_groups = star_groups.group_by('group_id') table = hstack([star_groups, table]) table['iter_detected'] = n * np.ones(table['x_fit'].shape, dtype=int) output_table = vstack([output_table, table]) # do not warn if no sources are found beyond the first iteration with warnings.catch_warnings(): warnings.simplefilter('ignore', NoDetectionsWarning) sources = self.finder(self._residual_image) n += 1 return output_table class DAOPhotPSFPhotometry(IterativelySubtractedPSFPhotometry): """ This class implements an iterative algorithm based on the DAOPHOT algorithm presented by Stetson (1987) to perform point spread function photometry in crowded fields. This consists of applying a loop of find sources, make groups, fit groups, subtract groups, and then repeat until no more stars are detected or a given number of iterations is reached. Basically, this classes uses `~photutils.psf.IterativelySubtractedPSFPhotometry`, but with grouping, finding, and background estimation routines defined a priori. More precisely, this class uses `~photutils.psf.DAOGroup` for grouping, `~photutils.detection.DAOStarFinder` for finding sources, and `~photutils.background.MMMBackground` for background estimation. Those classes are based on GROUP, FIND, and SKY routines used in DAOPHOT, respectively. The parameter ``crit_separation`` is associated with `~photutils.psf.DAOGroup`. ``sigma_clip`` is associated with `~photutils.background.MMMBackground`. ``threshold`` and ``fwhm`` are associated with `~photutils.detection.DAOStarFinder`. Parameters from ``ratio`` to ``roundhi`` are also associated with `~photutils.detection.DAOStarFinder`. Parameters ---------- crit_separation : float or int Distance, in units of pixels, such that any two stars separated by less than this distance will be placed in the same group. threshold : float The absolute image value above which to select sources. fwhm : float The full-width half-maximum (FWHM) of the major axis of the Gaussian kernel in units of pixels. psf_model : `astropy.modeling.Fittable2DModel` instance PSF or PRF model to fit the data. Could be one of the models in this package like `~photutils.psf.sandbox.DiscretePRF`, `~photutils.psf.IntegratedGaussianPRF`, or any other suitable 2D model. This object needs to identify three parameters (position of center in x and y coordinates and the flux) in order to set them to suitable starting values for each fit. The names of these parameters should be given as ``x_0``, ``y_0`` and ``flux``. `~photutils.psf.prepare_psf_model` can be used to prepare any 2D model to match this assumption. fitshape : int or length-2 array-like Rectangular shape around the center of a star which will be used to collect the data to do the fitting. Can be an integer to be the same along both axes. For example, 5 is the same as (5, 5), which means to fit only at the following relative pixel positions: [-2, -1, 0, 1, 2]. Each element of ``fitshape`` must be an odd number. sigma : float, optional Number of standard deviations used to perform sigma clip with a `astropy.stats.SigmaClip` object. ratio : float, optional The ratio of the minor to major axis standard deviations of the Gaussian kernel. ``ratio`` must be strictly positive and less than or equal to 1.0. The default is 1.0 (i.e., a circular Gaussian kernel). theta : float, optional The position angle (in degrees) of the major axis of the Gaussian kernel measured counter-clockwise from the positive x axis. sigma_radius : float, optional The truncation radius of the Gaussian kernel in units of sigma (standard deviation) [``1 sigma = FWHM / (2.0*sqrt(2.0*log(2.0)))``]. sharplo : float, optional The lower bound on sharpness for object detection. sharphi : float, optional The upper bound on sharpness for object detection. roundlo : float, optional The lower bound on roundess for object detection. roundhi : float, optional The upper bound on roundess for object detection. fitter : `~astropy.modeling.fitting.Fitter` instance Fitter object used to compute the optimized centroid positions and/or flux of the identified sources. See `~astropy.modeling.fitting` for more details on fitters. niters : int or None Number of iterations to perform of the loop FIND, GROUP, SUBTRACT, NSTAR. If None, iterations will proceed until no more stars remain. Note that in this case it is *possible* that the loop will never end if the PSF has structure that causes subtraction to create new sources infinitely. aperture_radius : float The radius (in units of pixels) used to compute initial estimates for the fluxes of sources. If ``None``, one FWHM will be used if it can be determined from the ```psf_model``. extra_output_cols : list of str, optional List of additional columns for parameters derived by any of the intermediate fitting steps (e.g., ``finder``), such as roundness or sharpness. Notes ----- If there are problems with fitting large groups, change the parameters of the grouping algorithm to reduce the number of sources in each group or input a ``star_groups`` table that only includes the groups that are relevant (e.g., manually remove all entries that coincide with artifacts). References ---------- [1] Stetson, Astronomical Society of the Pacific, Publications, (ISSN 0004-6280), vol. 99, March 1987, p. 191-222. Available at: https://ui.adsabs.harvard.edu/abs/1987PASP...99..191S/abstract """ def __init__(self, crit_separation, threshold, fwhm, psf_model, fitshape, sigma=3., ratio=1.0, theta=0.0, sigma_radius=1.5, sharplo=0.2, sharphi=1.0, roundlo=-1.0, roundhi=1.0, fitter=LevMarLSQFitter(), niters=3, aperture_radius=None, extra_output_cols=None): self.crit_separation = crit_separation self.threshold = threshold self.fwhm = fwhm self.sigma = sigma self.ratio = ratio self.theta = theta self.sigma_radius = sigma_radius self.sharplo = sharplo self.sharphi = sharphi self.roundlo = roundlo self.roundhi = roundhi group_maker = DAOGroup(crit_separation=self.crit_separation) bkg_estimator = MMMBackground(sigma_clip=SigmaClip(sigma=self.sigma)) finder = DAOStarFinder(threshold=self.threshold, fwhm=self.fwhm, ratio=self.ratio, theta=self.theta, sigma_radius=self.sigma_radius, sharplo=self.sharplo, sharphi=self.sharphi, roundlo=self.roundlo, roundhi=self.roundhi) super().__init__(group_maker=group_maker, bkg_estimator=bkg_estimator, psf_model=psf_model, fitshape=fitshape, finder=finder, fitter=fitter, niters=niters, aperture_radius=aperture_radius, extra_output_cols=extra_output_cols) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/sandbox.py0000644000214200020070000003377700000000000017052 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module stores work related to photutils.psf that is not quite ready for prime-time (i.e., is not considered a stable public API), but is included either for experimentation or as legacy code. """ from astropy.modeling import Fittable2DModel, Parameter from astropy.modeling.fitting import LevMarLSQFitter from astropy.nddata.utils import extract_array, subpixel_indices from astropy.table import Table import numpy as np from ..segmentation._utils import mask_to_mirrored_value __all__ = ['DiscretePRF', 'Reproject'] __doctest_requires__ = {('Reproject'): ['gwcs']} class DiscretePRF(Fittable2DModel): """ A discrete Pixel Response Function (PRF) model. The discrete PRF model stores images of the PRF at different subpixel positions or offsets as a lookup table. The resolution is given by the subsampling parameter, which states in how many subpixels a pixel is divided. In the typical case of wanting to create a PRF from an image with many point sources, use the `~DiscretePRF.create_from_image` method, rather than directly initializing this class. The discrete PRF model class in initialized with a 4 dimensional array, that contains the PRF images at different subpixel positions. The definition of the axes is as following: 1. Axis: y subpixel position 2. Axis: x subpixel position 3. Axis: y direction of the PRF image 4. Axis: x direction of the PRF image The total array therefore has the following shape (subsampling, subsampling, prf_size, prf_size) Parameters ---------- prf_array : ndarray Array containing PRF images. normalize : bool Normalize PRF images to unity. Equivalent to saying there is *no* flux outside the bounds of the PRF images. subsampling : int, optional Factor of subsampling. Default = 1. Notes ----- See :ref:`psf-terminology` for more details on the distinction between PSF and PRF as used in this module. """ flux = Parameter('flux') x_0 = Parameter('x_0') y_0 = Parameter('y_0') def __init__(self, prf_array, normalize=True, subsampling=1): # Array shape and dimension check if subsampling == 1: if prf_array.ndim == 2: prf_array = np.array([[prf_array]]) if prf_array.ndim != 4: raise TypeError('Array must have 4 dimensions.') if prf_array.shape[:2] != (subsampling, subsampling): raise TypeError('Incompatible subsampling and array size') if np.isnan(prf_array).any(): raise Exception("Array contains NaN values. Can't create PRF.") # Normalize if requested if normalize: for i in range(prf_array.shape[0]): for j in range(prf_array.shape[1]): prf_array[i, j] /= prf_array[i, j].sum() # Set PRF asttributes self._prf_array = prf_array self.subsampling = subsampling constraints = {'fixed': {'x_0': True, 'y_0': True}} x_0 = 0 y_0 = 0 flux = 1 super().__init__(n_models=1, x_0=x_0, y_0=y_0, flux=flux, **constraints) self.fitter = LevMarLSQFitter() @property def prf_shape(self): """Shape of the PRF image.""" return self._prf_array.shape[-2:] def evaluate(self, x, y, flux, x_0, y_0): """ Discrete PRF model evaluation. Given a certain position and flux the corresponding image of the PSF is chosen and scaled to the flux. If x and y are outside the boundaries of the image, zero will be returned. Parameters ---------- x : float x coordinate array in pixel coordinates. y : float y coordinate array in pixel coordinates. flux : float Model flux. x_0 : float x position of the center of the PRF. y_0 : float y position of the center of the PRF. """ # Convert x and y to index arrays x = (x - x_0 + 0.5 + self.prf_shape[1] // 2).astype('int') y = (y - y_0 + 0.5 + self.prf_shape[0] // 2).astype('int') # Get subpixel indices y_sub, x_sub = subpixel_indices((y_0, x_0), self.subsampling) # Out of boundary masks x_bound = np.logical_or(x < 0, x >= self.prf_shape[1]) y_bound = np.logical_or(y < 0, y >= self.prf_shape[0]) out_of_bounds = np.logical_or(x_bound, y_bound) # Set out of boundary indices to zero x[x_bound] = 0 y[y_bound] = 0 result = flux * self._prf_array[int(y_sub), int(x_sub)][y, x] # Set out of boundary values to zero result[out_of_bounds] = 0 return result @classmethod def create_from_image(cls, imdata, positions, size, fluxes=None, mask=None, mode='mean', subsampling=1, fix_nan=False): """ Create a discrete point response function (PRF) from image data. Given a list of positions and size this function estimates an image of the PRF by extracting and combining the individual PRFs from the given positions. NaN values are either ignored by passing a mask or can be replaced by the mirrored value with respect to the center of the PRF. Note that if fluxes are *not* specified explicitly, it will be flux estimated from an aperture of the same size as the PRF image. This does *not* account for aperture corrections so often will *not* be what you want for anything other than quick-look needs. Parameters ---------- imdata : array Data array with the image to extract the PRF from positions : List or array or `~astropy.table.Table` List of pixel coordinate source positions to use in creating the PRF. If this is a `~astropy.table.Table` it must have columns called ``x_0`` and ``y_0``. size : odd int Size of the quadratic PRF image in pixels. mask : bool array, optional Boolean array to mask out bad values. fluxes : array, optional Object fluxes to normalize extracted PRFs. If not given (or None), the flux is estimated from an aperture of the same size as the PRF image. mode : {'mean', 'median'} One of the following modes to combine the extracted PRFs: * 'mean': Take the pixelwise mean of the extracted PRFs. * 'median': Take the pixelwise median of the extracted PRFs. subsampling : int Factor of subsampling of the PRF (default = 1). fix_nan : bool Fix NaN values in the data by replacing it with the mirrored value. Assuming that the PRF is symmetrical. Returns ------- prf : `photutils.psf.sandbox.DiscretePRF` Discrete PRF model estimated from data. """ # Check input array type and dimension. if np.iscomplexobj(imdata): raise TypeError('Complex type not supported') if imdata.ndim != 2: raise ValueError(f'{imdata.ndim}-d array not supported. ' 'Only 2-d arrays supported.') if size % 2 == 0: raise TypeError("Size must be odd.") if fluxes is not None and len(fluxes) != len(positions): raise TypeError('Position and flux arrays must be of equal ' 'length.') if mask is None: mask = np.isnan(imdata) if isinstance(positions, (list, tuple)): positions = np.array(positions) if isinstance(positions, Table) or \ (isinstance(positions, np.ndarray) and positions.dtype.names is not None): # One can do clever things like # positions['x_0', 'y_0'].as_array().view((positions['x_0'].dtype, # 2)) # but that requires positions['x_0'].dtype is # positions['y_0'].dtype. # Better do something simple to allow type promotion if required. pos = np.empty((len(positions), 2)) pos[:, 0] = positions['x_0'] pos[:, 1] = positions['y_0'] positions = pos if isinstance(fluxes, (list, tuple)): fluxes = np.array(fluxes) if mode == 'mean': combine = np.ma.mean elif mode == 'median': combine = np.ma.median else: raise Exception('Invalid mode to combine prfs.') data_internal = np.ma.array(data=imdata, mask=mask) prf_model = np.ndarray(shape=(subsampling, subsampling, size, size)) positions_subpixel_indices = \ np.array([subpixel_indices(_, subsampling) for _ in positions], dtype=int) for i in range(subsampling): for j in range(subsampling): extracted_sub_prfs = [] sub_prf_indices = np.all(positions_subpixel_indices == [j, i], axis=1) if not sub_prf_indices.any(): raise ValueError('The source coordinates do not sample ' 'all sub-pixel positions. Reduce the ' 'value of the subsampling parameter.') positions_sub_prfs = positions[sub_prf_indices] for k, position in enumerate(positions_sub_prfs): x, y = position extracted_prf = extract_array(data_internal, (size, size), (y, x)) # Check shape to exclude incomplete PRFs at the boundaries # of the image if (extracted_prf.shape == (size, size) and np.ma.sum(extracted_prf) != 0): # Replace NaN values by mirrored value, with respect # to the prf's center if fix_nan: prf_nan = extracted_prf.mask if prf_nan.any(): if (prf_nan.sum() > 3 or prf_nan[size // 2, size // 2]): continue else: extracted_prf = mask_to_mirrored_value( extracted_prf, prf_nan, (size // 2, size // 2)) # Normalize and add extracted PRF to data cube if fluxes is None: extracted_prf_norm = (np.ma.copy(extracted_prf) / np.ma.sum(extracted_prf)) else: fluxes_sub_prfs = fluxes[sub_prf_indices] extracted_prf_norm = (np.ma.copy(extracted_prf) / fluxes_sub_prfs[k]) extracted_sub_prfs.append(extracted_prf_norm) else: continue prf_model[i, j] = np.ma.getdata( combine(np.ma.dstack(extracted_sub_prfs), axis=2)) return cls(prf_model, subsampling=subsampling) class Reproject: """ Class to reproject pixel coordinates between unrectified and rectified images. Parameters ---------- wcs_original, wcs_rectified : `~astropy.wcs.WCS` or `~gwcs.wcs.WCS` The WCS objects for the original (unrectified) and rectified images. origin : {0, 1} Whether to use 0- or 1-based pixel coordinates. """ def __init__(self, wcs_original, wcs_rectified): self.wcs_original = wcs_original self.wcs_rectified = wcs_rectified @staticmethod def _reproject(wcs1, wcs2, x, y): """ Perform the forward transformation of ``wcs1`` followed by the inverse transformation of ``wcs2``. Parameters ---------- wcs1, wcs2 : WCS objects World coordinate system (WCS) transformations that support the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). x, y : float or array-like of float The input pixel coordinates. Returns ------- x, y: float or array-like of float The reprojected pixel coordinates. """ try: skycoord = wcs1.pixel_to_world(x, y) return wcs2.world_to_pixel(skycoord) except AttributeError: raise ValueError('Input wcs objects do not support the shared ' 'WCS interface.') def to_rectified(self, x, y): """ Convert the input (x, y) positions from the original (unrectified) image to the rectified image. Parameters ---------- x, y : float or array-like of float The zero-index pixel coordinates in the original (unrectified) image. Returns ------- x, y: float or array-like The zero-index pixel coordinates in the rectified image. """ return self._reproject(self.wcs_original, self.wcs_rectified, x, y) def to_original(self, x, y): """ Convert the input (x, y) positions from the rectified image to the original (unrectified) image. Parameters ---------- x, y : float or array-like of float The zero-index pixel coordinates in the rectified image. Returns ------- x, y: float or array-like The zero-index pixel coordinates in the original (unrectified) image. """ return self._reproject(self.wcs_rectified, self.wcs_original, x, y) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0176163 photutils-1.3.0/photutils/psf/tests/0000755000214200020070000000000000000000000016163 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/psf/tests/__init__.py0000644000214200020070000000000000000000000020262 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/tests/test_epsf.py0000644000214200020070000001641600000000000020541 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the epsf module. """ import itertools from astropy.modeling.fitting import LevMarLSQFitter from astropy.nddata import NDData from astropy.table import Table from astropy.stats import SigmaClip import numpy as np from numpy.testing import assert_allclose, assert_almost_equal import pytest from ..epsf import EPSFBuilder, EPSFFitter from ..epsf_stars import extract_stars, EPSFStars from ..models import IntegratedGaussianPRF, EPSFModel from ...datasets import make_gaussian_prf_sources_image from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') class TestEPSFBuild: def setup_class(self): """ Create a simulated image for testing. """ from scipy.spatial import cKDTree shape = (750, 750) # define random star positions nstars = 100 rng = np.random.default_rng(0) xx = rng.uniform(low=0, high=shape[1], size=nstars) yy = rng.uniform(low=0, high=shape[0], size=nstars) # enforce a minimum separation min_dist = 25 coords = [(yy[0], xx[0])] for xxi, yyi in zip(xx, yy): newcoord = [yyi, xxi] dist, _ = cKDTree([newcoord]).query(coords, 1) if np.min(dist) > min_dist: coords.append(newcoord) yy, xx = np.transpose(coords) zz = rng.uniform(low=0, high=200000., size=len(xx)) # define a table of model parameters self.stddev = 2. sources = Table() sources['amplitude'] = zz sources['x_0'] = xx sources['y_0'] = yy sources['sigma'] = np.zeros(len(xx)) + self.stddev sources['theta'] = 0. self.data = make_gaussian_prf_sources_image(shape, sources) self.nddata = NDData(self.data) init_stars = Table() init_stars['x'] = xx.astype(int) init_stars['y'] = yy.astype(int) self.init_stars = init_stars def test_extract_stars(self): size = 25 stars = extract_stars(self.nddata, self.init_stars, size=size) assert len(stars) == 81 assert isinstance(stars, EPSFStars) assert isinstance(stars[0], EPSFStars) assert stars[0].data.shape == (size, size) def test_epsf_build(self): """ This is an end-to-end test of EPSFBuilder on a simulated image. """ size = 25 oversampling = 4. stars = extract_stars(self.nddata, self.init_stars, size=size) epsf_builder = EPSFBuilder(oversampling=oversampling, maxiters=15, progress_bar=False, norm_radius=25, recentering_maxiters=15) epsf, fitted_stars = epsf_builder(stars) ref_size = (size * oversampling) + 1 assert epsf.data.shape == (ref_size, ref_size) y0 = (ref_size - 1) / 2 / oversampling y = np.arange(ref_size, dtype=float) / oversampling psf_model = IntegratedGaussianPRF(sigma=self.stddev) z = epsf.data x = psf_model.evaluate(y.reshape(-1, 1), y.reshape(1, -1), 1, y0, y0, self.stddev) assert_allclose(z, x, rtol=1e-2, atol=1e-5) resid_star = fitted_stars[0].compute_residual_image(epsf) assert_almost_equal(np.sum(resid_star)/fitted_stars[0].flux, 0, decimal=3) def test_epsf_fitting_bounds(self): size = 25 oversampling = 4. stars = extract_stars(self.nddata, self.init_stars, size=size) epsf_builder = EPSFBuilder(oversampling=oversampling, maxiters=8, progress_bar=True, norm_radius=25, recentering_maxiters=5, fitter=EPSFFitter(fit_boxsize=30), smoothing_kernel='quadratic') # With a boxsize larger than the cutout we expect the fitting to # fail for all stars, due to star._fit_error_status with pytest.raises(ValueError): epsf_builder(stars) def test_epsf_build_invalid_fitter(self): """ Test that the input fitter is an EPSFFitter instance. """ with pytest.raises(TypeError): EPSFBuilder(fitter=EPSFFitter, maxiters=3) with pytest.raises(TypeError): EPSFBuilder(fitter=LevMarLSQFitter(), maxiters=3) with pytest.raises(TypeError): EPSFBuilder(fitter=LevMarLSQFitter, maxiters=3) def test_epsfbuilder_inputs(): # invalid inputs with pytest.raises(ValueError): EPSFBuilder(oversampling=None) with pytest.raises(ValueError): EPSFBuilder(oversampling=-1) with pytest.raises(ValueError): EPSFBuilder(maxiters=-1) with pytest.raises(ValueError): EPSFBuilder(oversampling=[-1, 4]) # valid inputs EPSFBuilder(oversampling=6) EPSFBuilder(oversampling=[4, 6]) # invalid inputs for sigclip in [None, [], 'a']: with pytest.raises(ValueError): EPSFBuilder(flux_residual_sigclip=sigclip) # valid inputs EPSFBuilder(flux_residual_sigclip=SigmaClip(sigma=2.5, cenfunc='mean', maxiters=2)) def test_epsfmodel_inputs(): data = np.array([[], []]) with pytest.raises(ValueError): EPSFModel(data) data = np.ones((5, 5), dtype=float) data[2, 2] = np.inf with pytest.raises(ValueError): EPSFModel(data) data[2, 2] = np.finfo(float).max * 2 with pytest.raises(ValueError): EPSFModel(data, flux=None) data[2, 2] = 1 for oversampling in [-1, [-2, 4], (1, 4, 8), ((1, 2), (3, 4)), np.ones((2, 2, 2)), 2.1, np.nan, (1, np.inf)]: with pytest.raises(ValueError): EPSFModel(data, oversampling=oversampling) origin = (1, 2, 3) with pytest.raises(TypeError): EPSFModel(data, origin=origin) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize('oversamp', [3, 4]) def test_epsf_build_oversampling(oversamp): offsets = np.arange(oversamp) * 1./oversamp - 0.5 + 1./(2. * oversamp) xydithers = np.array(list(itertools.product(offsets, offsets))) xdithers = np.transpose(xydithers)[0] ydithers = np.transpose(xydithers)[1] nstars = oversamp**2 sigma = 3.0 sources = Table() offset = 50 size = oversamp * offset + offset y, x = np.mgrid[0:oversamp, 0:oversamp] * offset + offset sources['amplitude'] = np.full((nstars,), 100.0) sources['x_0'] = x.ravel() + xdithers sources['y_0'] = y.ravel() + ydithers sources['sigma'] = np.full((nstars,), sigma) data = make_gaussian_prf_sources_image((size, size), sources) nddata = NDData(data=data) stars_tbl = Table() stars_tbl['x'] = sources['x_0'] stars_tbl['y'] = sources['y_0'] stars = extract_stars(nddata, stars_tbl, size=25) epsf_builder = EPSFBuilder(oversampling=oversamp, maxiters=15, progress_bar=False, recentering_maxiters=20) epsf, fitted_stars = epsf_builder(stars) # input PSF shape size = epsf.data.shape[0] cen = (size - 1) / 2 sigma2 = oversamp * sigma m = IntegratedGaussianPRF(sigma2, x_0=cen, y_0=cen, flux=1) yy, xx = np.mgrid[0:size, 0:size] psf = m(xx, yy) assert_allclose(epsf.data, psf*epsf.data.sum(), atol=2.5e-4) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/tests/test_epsf_stars.py0000644000214200020070000000626100000000000021752 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the epsf_stars module. """ from astropy.modeling.models import Moffat2D from astropy.nddata import NDData from astropy.table import Table import numpy as np from numpy.testing import assert_allclose import pytest from ..epsf_stars import extract_stars, EPSFStars from ..models import EPSFModel, IntegratedGaussianPRF from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') class TestExtractStars: def setup_class(self): stars_tbl = Table() stars_tbl['x'] = [15, 15, 35, 35] stars_tbl['y'] = [15, 35, 40, 10] self.stars_tbl = stars_tbl yy, xx = np.mgrid[0:51, 0:55] self.data = np.zeros(xx.shape) for (xi, yi) in zip(stars_tbl['x'], stars_tbl['y']): m = Moffat2D(100, xi, yi, 3, 3) self.data += m(xx, yy) self.nddata = NDData(data=self.data) def test_extract_stars(self): size = 11 stars = extract_stars(self.nddata, self.stars_tbl, size=size) assert len(stars) == 4 assert isinstance(stars, EPSFStars) assert isinstance(stars[0], EPSFStars) assert stars[0].data.shape == (size, size) assert stars.n_stars == stars.n_all_stars assert stars.n_stars == stars.n_good_stars assert stars.center.shape == (len(stars), 2) def test_extract_stars_inputs(self): with pytest.raises(ValueError): extract_stars(np.ones(3), self.stars_tbl) with pytest.raises(ValueError): extract_stars(self.nddata, [(1, 1), (2, 2), (3, 3)]) with pytest.raises(ValueError): extract_stars(self.nddata, [self.stars_tbl, self.stars_tbl]) with pytest.raises(ValueError): extract_stars([self.nddata, self.nddata], self.stars_tbl) @pytest.mark.skipif('not HAS_SCIPY') def test_epsf_star_residual_image(): """ Test to ensure ``compute_residual_image`` gives correct residuals. """ size = 100 yy, xx, = np.mgrid[0:size+1, 0:size+1] / 4 gmodel = IntegratedGaussianPRF().evaluate(xx, yy, 1, 12.5, 12.5, 2.5) epsf = EPSFModel(gmodel, oversampling=4, norm_radius=100) _size = 25 data = np.zeros((_size, _size)) _yy, _xx, = np.mgrid[0:_size, 0:_size] data += epsf.evaluate(x=_xx, y=_yy, flux=16, x_0=12, y_0=12) tbl = Table() tbl['x'] = [12] tbl['y'] = [12] stars = extract_stars(NDData(data), tbl, size=23) residual = stars[0].compute_residual_image(epsf) # As current EPSFStar instances cannot accept IntegratedGaussianPRF as input, # we have to accept some loss of precision from the conversion to ePSF, and # spline fitting (twice), so assert_allclose cannot be more more precise than # 0.001 currently. assert_allclose(np.sum(residual), 0., atol=1.e-3, rtol=1e-3) def test_stars_pickleable(): """ Verify that EPSFStars can be successfully pickled/unpickled for use multiprocessing """ from multiprocessing.reduction import ForkingPickler # Doesn't need to actually contain anything useful stars = EPSFStars([1]) # This should not blow up ForkingPickler.loads(ForkingPickler.dumps(stars)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/tests/test_groupstars.py0000644000214200020070000004423700000000000022017 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the groupstars module. """ from astropy.table import Table, vstack import numpy as np from numpy.testing import assert_almost_equal import pytest from ..groupstars import DAOGroup, DBSCANGroup from ...utils._optional_deps import HAS_SKLEARN # noqa def assert_table_almost_equal(table1, table2): assert table1.colnames == table2.colnames assert table1.meta == table2.meta for colname in table1.colnames: assert_almost_equal(table1[colname], table2[colname]) class TestDAOGROUP: def test_daogroup_one(self): """ +---------+--------+---------+---------+--------+---------+ | * * * * | | | 0.2 + + | | | | | | 0 + * * + | | | | | | -0.2 + + | | | * * * * | +---------+--------+---------+---------+--------+---------+ 0 0.5 1 1.5 2 x and y axis are in pixel coordinates. Each asterisk represents the centroid of a star. """ x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) x_1 = x_0 + 2.0 first_group = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.ones(len(x_0), dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([x_1, y_0, len(x_0) + np.arange(len(x_0)) + 1, 2*np.ones(len(x_0), dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group]) daogroup = DAOGroup(crit_separation=0.6) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_daogroup_two(self): """ +--------------+--------------+-------------+--------------+ 3 + * + | * | 2.5 + * + | * | 2 + * + | | 1.5 + + | | 1 + * + | * | 0.5 + * + | * | 0 + * + +--------------+--------------+-------------+--------------+ -1 -0.5 0 0.5 1 """ first_group = Table([np.zeros(5), np.linspace(0, 1, 5), np.arange(5) + 1, np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([np.zeros(5), np.linspace(2, 3, 5), 6 + np.arange(5), 2*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group]) daogroup = DAOGroup(crit_separation=0.3) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_daogroup_three(self): """ 1 +--+-------+--------+--------+--------+-------+--------+--+ | | | | | | 0.5 + + | | | | 0 + * * * * * * * * * * + | | | | -0.5 + + | | | | | | -1 +--+-------+--------+--------+--------+-------+--------+--+ 0 0.5 1 1.5 2 2.5 3 """ first_group = Table([np.linspace(0, 1, 5), np.zeros(5), np.arange(5) + 1, np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([np.linspace(2, 3, 5), np.zeros(5), 6 + np.arange(5), 2*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group]) daogroup = DAOGroup(crit_separation=0.3) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_daogroup_four(self): """ +-+---------+---------+---------+---------+-+ 1 + * + | * * | | | | | 0.5 + + | | | | | | 0 + * * + | | | | -0.5 + + | | | | | * * | -1 + * + +-+---------+---------+---------+---------+-+ -1 -0.5 0 0.5 1 """ x = np.linspace(-1., 1., 5) y = np.sqrt(1. - x**2) xx = np.hstack((x, x)) yy = np.hstack((y, -y)) starlist = Table([xx, yy, np.arange(10) + 1, np.ones(10, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) daogroup = DAOGroup(crit_separation=2.5) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_daogroup_five(self): """ +--+--------+--------+-------+--------+--------+--------+--+ 3 + * + | * | 2.5 + * + | * | 2 + * + | | 1.5 + * * * * * * * * * * + | | 1 + * + | * | 0.5 + * + | * | 0 + * + +--+--------+--------+-------+--------+--------+--------+--+ 0 0.5 1 1.5 2 2.5 3 """ first_group = Table([1.5*np.ones(5), np.linspace(0, 1, 5), np.arange(5) + 1, np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([1.5*np.ones(5), np.linspace(2, 3, 5), 6 + np.arange(5), 2*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) third_group = Table([np.linspace(0, 1, 5), 1.5*np.ones(5), 11 + np.arange(5), 3*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) fourth_group = Table([np.linspace(2, 3, 5), 1.5*np.ones(5), 16 + np.arange(5), 4*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group, third_group, fourth_group]) daogroup = DAOGroup(crit_separation=0.3) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_daogroup_six(self): """ +------+----------+----------+----------+----------+------+ | * * * * * * | | | 0.2 + + | | | | | | 0 + * * * + | | | | | | -0.2 + + | | | * * * * * * | +------+----------+----------+----------+----------+------+ 0 1 2 3 4 """ x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) x_1 = x_0 + 2.0 x_2 = x_0 + 4.0 first_group = Table([x_0, y_0, np.arange(5) + 1, np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([x_1, y_0, 6 + np.arange(5), 2*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) third_group = Table([x_2, y_0, 11 + np.arange(5), 3*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group, third_group]) daogroup = DAOGroup(crit_separation=0.6) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_isolated_sources(self): """ Test case when all sources are isolated. """ x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) daogroup = DAOGroup(crit_separation=0.01) test_starlist = daogroup(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_id_column(self): x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) daogroup = DAOGroup(crit_separation=0.01) test_starlist = daogroup(starlist['x_0', 'y_0']) assert_table_almost_equal(starlist, test_starlist) def test_id_column_raise_error(self): x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)), np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) daogroup = DAOGroup(crit_separation=0.01) with pytest.raises(ValueError): daogroup(starlist['x_0', 'y_0', 'id']) @pytest.mark.skipif('not HAS_SKLEARN') class TestDBSCANGroup: def test_group_stars_one(object): x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) x_1 = x_0 + 2.0 first_group = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.ones(len(x_0), dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([x_1, y_0, len(x_0) + np.arange(len(x_0)) + 1, 2*np.ones(len(x_0), dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group]) dbscan = DBSCANGroup(crit_separation=0.6) test_starlist = dbscan(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_group_stars_two(object): first_group = Table([1.5*np.ones(5), np.linspace(0, 1, 5), np.arange(5) + 1, np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) second_group = Table([1.5*np.ones(5), np.linspace(2, 3, 5), 6 + np.arange(5), 2*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) third_group = Table([np.linspace(0, 1, 5), 1.5*np.ones(5), 11 + np.arange(5), 3*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) fourth_group = Table([np.linspace(2, 3, 5), 1.5*np.ones(5), 16 + np.arange(5), 4*np.ones(5, dtype=int)], names=('x_0', 'y_0', 'id', 'group_id')) starlist = vstack([first_group, second_group, third_group, fourth_group]) dbscan = DBSCANGroup(crit_separation=0.3) test_starlist = dbscan(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_isolated_sources(self): """ Test case when all sources are isolated. """ x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) dbscan = DBSCANGroup(crit_separation=0.01) test_starlist = dbscan(starlist['x_0', 'y_0', 'id']) assert_table_almost_equal(starlist, test_starlist) def test_id_column(self): x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)) + 1, np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) dbscan = DBSCANGroup(crit_separation=0.01) test_starlist = dbscan(starlist['x_0', 'y_0']) assert_table_almost_equal(starlist, test_starlist) def test_id_column_raise_error(self): x_0 = np.array([0, np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4, -np.sqrt(2)/4]) y_0 = np.array([0, np.sqrt(2)/4, -np.sqrt(2)/4, np.sqrt(2)/4, -np.sqrt(2)/4]) starlist = Table([x_0, y_0, np.arange(len(x_0)), np.arange(len(x_0)) + 1], names=('x_0', 'y_0', 'id', 'group_id')) dbscan = DBSCANGroup(crit_separation=0.01) with pytest.raises(ValueError): dbscan(starlist['x_0', 'y_0', 'id']) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639445452.0 photutils-1.3.0/photutils/psf/tests/test_models.py0000644000214200020070000003474400000000000021073 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the models module. """ from itertools import product from astropy.modeling.models import Gaussian2D, Moffat2D from astropy.nddata import NDData import numpy as np from numpy.testing import assert_allclose import pytest from ..models import (FittableImageModel, GriddedPSFModel, IntegratedGaussianPRF, PRFAdapter) from ...segmentation import detect_sources, SourceCatalog from ...utils._optional_deps import HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') class TestFittableImageModel: def setup_class(self): self.gm = Gaussian2D(x_stddev=3, y_stddev=3) def test_fittable_image_model(self): yy, xx = np.mgrid[-2:3, -2:3] model_nonorm = FittableImageModel(self.gm(xx, yy)) assert_allclose(model_nonorm(0, 0), self.gm(0, 0)) assert_allclose(model_nonorm(1, 1), self.gm(1, 1)) assert_allclose(model_nonorm(-2, 1), self.gm(-2, 1)) # subpixel should *not* match, but be reasonably close # in this case good to ~0.1% seems to be fine assert_allclose(model_nonorm(0.5, 0.5), self.gm(0.5, 0.5), rtol=.001) assert_allclose(model_nonorm(-0.5, 1.75), self.gm(-0.5, 1.75), rtol=.001) model_norm = FittableImageModel(self.gm(xx, yy), normalize=True) assert not np.allclose(model_norm(0, 0), self.gm(0, 0)) assert_allclose(np.sum(model_norm(xx, yy)), 1) model_norm2 = FittableImageModel(self.gm(xx, yy), normalize=True, normalization_correction=2) assert not np.allclose(model_norm2(0, 0), self.gm(0, 0)) assert_allclose(model_norm(0, 0), model_norm2(0, 0)*2) assert_allclose(np.sum(model_norm2(xx, yy)), 0.5) def test_fittable_image_model_oversampling(self): oversamp = 3 # oversampling factor yy, xx = np.mgrid[-3:3.00001:(1/oversamp), -3:3.00001:(1/oversamp)] im = self.gm(xx, yy) assert im.shape[0] > 7 model_oversampled = FittableImageModel(im, oversampling=oversamp) assert_allclose(model_oversampled(0, 0), self.gm(0, 0)) assert_allclose(model_oversampled(1, 1), self.gm(1, 1)) assert_allclose(model_oversampled(-2, 1), self.gm(-2, 1)) assert_allclose(model_oversampled(0.5, 0.5), self.gm(0.5, 0.5), rtol=.001) assert_allclose(model_oversampled(-0.5, 1.75), self.gm(-0.5, 1.75), rtol=.001) # without oversampling the same tests should fail except for at # the origin model_wrongsampled = FittableImageModel(im) assert_allclose(model_wrongsampled(0, 0), self.gm(0, 0)) assert not np.allclose(model_wrongsampled(1, 1), self.gm(1, 1)) assert not np.allclose(model_wrongsampled(-2, 1), self.gm(-2, 1)) assert not np.allclose(model_wrongsampled(0.5, 0.5), self.gm(0.5, 0.5), rtol=.001) assert not np.allclose(model_wrongsampled(-0.5, 1.75), self.gm(-0.5, 1.75), rtol=.001) def test_centering_oversampled(self): gm = Gaussian2D(x_stddev=2, y_stddev=3) oversamp = 3 yy, xx = np.mgrid[-3:3.00001:(1 / oversamp), -3:3.00001:(1 / oversamp)] model_oversampled = FittableImageModel(gm(xx, yy), oversampling=oversamp) valcen = gm(0, 0) val36 = gm(0.66, 0.66) assert_allclose(valcen, model_oversampled(0, 0)) assert_allclose(val36, model_oversampled(0.66, 0.66), rtol=1.e-6) model_oversampled.x_0 = 2.5 model_oversampled.y_0 = -3.5 assert_allclose(valcen, model_oversampled(2.5, -3.5)) assert_allclose(val36, model_oversampled(2.5 + 0.66, -3.5 + 0.66), rtol=1.e-6) def test_oversampling_inputs(self): data = np.arange(30).reshape(5, 6) for oversampling in [4, (3, 3), (3, 4)]: fim = FittableImageModel(data, oversampling=oversampling) if not hasattr(oversampling, '__len__'): _oversamp = float(oversampling) else: _oversamp = tuple(float(o) for o in oversampling) assert np.all(fim._oversampling == _oversamp) for oversampling in [-1, [-2, 4], (1, 4, 8), ((1, 2), (3, 4)), np.ones((2, 2, 2)), 2.1, np.nan, (1, np.inf)]: with pytest.raises(ValueError): FittableImageModel(data, oversampling=oversampling) class TestGriddedPSFModel: def setup_class(self): psfs = [] y, x = np.mgrid[0:101, 0:101] for i in range(16): theta = i * 10. * np.pi / 180. g = Gaussian2D(1, 50, 50, 10, 5, theta=theta) m = g(x, y) psfs.append(m) xgrid = [0, 40, 160, 200] ygrid = [0, 60, 140, 200] grid_xypos = list(product(xgrid, ygrid)) meta = {} meta['grid_xypos'] = grid_xypos meta['oversampling'] = 4 self.nddata = NDData(psfs, meta=meta) self.psfmodel = GriddedPSFModel(self.nddata) def test_gridded_psf_model(self): keys = ['grid_xypos', 'oversampling'] for key in keys: assert key in self.psfmodel.meta assert len(self.psfmodel.meta) == 2 assert len(self.psfmodel.meta['grid_xypos']) == 16 assert self.psfmodel.oversampling == 4 assert (self.psfmodel.meta['oversampling'] == self.psfmodel.oversampling) assert self.psfmodel.data.shape == (16, 101, 101) @pytest.mark.skipif('not HAS_SCIPY') def test_gridded_psf_model_basic_eval(self): y, x = np.mgrid[0:100, 0:100] psf = self.psfmodel.evaluate(x=x, y=y, flux=100, x_0=40, y_0=60) assert psf.shape == (100, 100) @pytest.mark.skipif('not HAS_SCIPY') def test_gridded_psf_model_eval_outside_grid(self): y, x = np.mgrid[-50:50, -50:50] psf1 = self.psfmodel.evaluate(x=x, y=y, flux=100, x_0=0, y_0=0) y, x = np.mgrid[-60:40, -60:40] psf2 = self.psfmodel.evaluate(x=x, y=y, flux=100, x_0=-10, y_0=-10) assert_allclose(psf1, psf2) y, x = np.mgrid[150:250, 150:250] psf3 = self.psfmodel.evaluate(x=x, y=y, flux=100, x_0=200, y_0=200) y, x = np.mgrid[170:270, 170:270] psf4 = self.psfmodel.evaluate(x=x, y=y, flux=100, x_0=220, y_0=220) assert_allclose(psf3, psf4) @pytest.mark.skipif('not HAS_SCIPY') def test_gridded_psf_model_interp(self): # test xyref length with pytest.raises(ValueError): self.psfmodel._bilinear_interp([1, 1], 1, 1, 1) # test zref shape with pytest.raises(ValueError): xyref = [[0, 0], [0, 1], [1, 0], [1, 1]] zref = np.ones((3, 4, 4)) self.psfmodel._bilinear_interp(xyref, zref, 1, 1) # test if refxy points form a rectangle with pytest.raises(ValueError): xyref = [[0, 0], [0, 1], [1, 0], [2, 2]] zref = np.ones((4, 4, 4)) self.psfmodel._bilinear_interp(xyref, zref, 1, 1) # test if xi and yi are outside of xyref xyref = [[0, 0], [0, 1], [1, 0], [1, 1]] zref = np.ones((4, 4, 4)) with pytest.raises(ValueError): self.psfmodel._bilinear_interp(xyref, zref, 100, 1) with pytest.raises(ValueError): self.psfmodel._bilinear_interp(xyref, zref, 1, 100) # test non-scalar xi and yi idx = [0, 1, 4, 5] xyref = np.array(self.psfmodel.grid_xypos)[idx] psfs = self.psfmodel.data[idx, :, :] val1 = self.psfmodel._bilinear_interp(xyref, psfs, 10, 20) val2 = self.psfmodel._bilinear_interp(xyref, psfs, [10], [20]) assert_allclose(val1, val2) def test_gridded_psf_model_invalid_inputs(self): data = np.ones((4, 3, 3)) # check if NDData with pytest.raises(TypeError): GriddedPSFModel(data) # check PSF data dimension with pytest.raises(ValueError): GriddedPSFModel(NDData(np.ones((3, 3)))) # check that grid_xypos is in meta meta = {'oversampling': 4} nddata = NDData(data, meta=meta) with pytest.raises(ValueError): GriddedPSFModel(nddata) # check grid_xypos length meta = {'grid_xypos': [[0, 0], [1, 0], [1, 0]], 'oversampling': 4} nddata = NDData(data, meta=meta) with pytest.raises(ValueError): GriddedPSFModel(nddata) # check if grid_xypos is a regular grid meta = {'grid_xypos': [[0, 0], [1, 0], [1, 0], [3, 4]], 'oversampling': 4} nddata = NDData(data, meta=meta) with pytest.raises(ValueError): GriddedPSFModel(nddata) # check that oversampling is in meta meta = {'grid_xypos': [[0, 0], [0, 1], [1, 0], [1, 1]]} nddata = NDData(data, meta=meta) with pytest.raises(ValueError): GriddedPSFModel(nddata) # check oversampling is a scalar meta = {'grid_xypos': [[0, 0], [0, 1], [1, 0], [1, 1]], 'oversampling': [4, 4]} nddata = NDData(data, meta=meta) with pytest.raises(ValueError): GriddedPSFModel(nddata) @pytest.mark.skipif('not HAS_SCIPY') def test_gridded_psf_model_eval(self): """ Create a simulated image using GriddedPSFModel and test the properties of the generated sources. """ shape = (200, 200) data = np.zeros(shape) eval_xshape = (np.ceil(self.psfmodel.data.shape[2] / self.psfmodel.oversampling)).astype(int) eval_yshape = (np.ceil(self.psfmodel.data.shape[1] / self.psfmodel.oversampling)).astype(int) xx = [40, 50, 160, 160] yy = [60, 150, 50, 140] zz = [100, 100, 100, 100] for xxi, yyi, zzi in zip(xx, yy, zz): x0 = np.floor(xxi - (eval_xshape - 1) / 2.).astype(int) y0 = np.floor(yyi - (eval_yshape - 1) / 2.).astype(int) x1 = x0 + eval_xshape y1 = y0 + eval_yshape if x0 < 0: x0 = 0 if y0 < 0: y0 = 0 if x1 > shape[1]: x1 = shape[1] if y1 > shape[0]: y1 = shape[0] y, x = np.mgrid[y0:y1, x0:x1] data[y, x] += self.psfmodel.evaluate(x=x, y=y, flux=zzi, x_0=xxi, y_0=yyi) segm = detect_sources(data, 0., 5) cat = SourceCatalog(data, segm) orients = cat.orientation.value assert_allclose(orients[1], 50., rtol=1.e-5) assert_allclose(orients[2], -80., rtol=1.e-5) assert 88.3 < orients[0] < 88.4 assert 64. < orients[3] < 64.2 @pytest.mark.skipif('not HAS_SCIPY') class TestIntegratedGaussianPRF: widths = [0.001, 0.01, 0.1, 1] sigmas = [0.5, 1., 2., 10., 12.34] @pytest.mark.parametrize('width', widths) def test_subpixel_gauss_psf(self, width): """ Test subpixel accuracy of IntegratedGaussianPRF by checking the sum of pixels. """ gauss_psf = IntegratedGaussianPRF(width) y, x = np.mgrid[-10:11, -10:11] assert_allclose(gauss_psf(x, y).sum(), 1) @pytest.mark.parametrize('sigma', sigmas) def test_gaussian_psf_integral(self, sigma): """ Test if IntegratedGaussianPRF integrates to unity on larger scales. """ psf = IntegratedGaussianPRF(sigma=sigma) y, x = np.mgrid[-100:101, -100:101] assert_allclose(psf(y, x).sum(), 1) @pytest.mark.skipif('not HAS_SCIPY') class TestPRFAdapter: def normalize_moffat(self, mof): # this is the analytic value needed to get a total flux of 1 mof = mof.copy() mof.amplitude = (mof.alpha-1)/(np.pi*mof.gamma**2) return mof @pytest.mark.parametrize("adapterkwargs", [ dict(xname='x_0', yname='y_0', fluxname=None, renormalize_psf=False), dict(xname=None, yname=None, fluxname=None, renormalize_psf=False), dict(xname='x_0', yname='y_0', fluxname='amplitude', renormalize_psf=False)]) def test_create_eval_prfadapter(self, adapterkwargs): mof = Moffat2D(gamma=1, alpha=4.8) prf = PRFAdapter(mof, **adapterkwargs) # test that these work without errors prf.x_0 = 0.5 prf.y_0 = -0.5 prf.flux = 1.2 prf(0, 0) @pytest.mark.parametrize("adapterkwargs", [ dict(xname='x_0', yname='y_0', fluxname=None, renormalize_psf=True), dict(xname='x_0', yname='y_0', fluxname=None, renormalize_psf=False), dict(xname=None, yname=None, fluxname=None, renormalize_psf=False) ]) def test_prfadapter_integrates(self, adapterkwargs): from scipy.integrate import dblquad mof = Moffat2D(gamma=1.5, alpha=4.8) if not adapterkwargs['renormalize_psf']: mof = self.normalize_moffat(mof) prf1 = PRFAdapter(mof, **adapterkwargs) # first check that the PRF over a central grid ends up summing to the # integrand over the whole PSF xg, yg = np.meshgrid(*([(-1, 0, 1)]*2)) evalmod = prf1(xg, yg) if adapterkwargs['renormalize_psf']: mof = self.normalize_moffat(mof) integrand, itol = dblquad(mof, -1.5, 1.5, lambda x: -1.5, lambda x: 1.5) assert_allclose(np.sum(evalmod), integrand, atol=itol * 10) @pytest.mark.parametrize("adapterkwargs", [ dict(xname='x_0', yname='y_0', fluxname=None, renormalize_psf=False), dict(xname=None, yname=None, fluxname=None, renormalize_psf=False)]) def test_prfadapter_sizematch(self, adapterkwargs): from scipy.integrate import dblquad mof1 = self.normalize_moffat(Moffat2D(gamma=1, alpha=4.8)) prf1 = PRFAdapter(mof1, **adapterkwargs) # now try integrating over differently-sampled PRFs # and check that they match mof2 = self.normalize_moffat(Moffat2D(gamma=2, alpha=4.8)) prf2 = PRFAdapter(mof2, **adapterkwargs) xg1, yg1 = np.meshgrid(*([(-0.5, 0.5)]*2)) xg2, yg2 = np.meshgrid(*([(-1.5, -0.5, 0.5, 1.5)]*2)) eval11 = prf1(xg1, yg1) eval22 = prf2(xg2, yg2) integrand, itol = dblquad(mof1, -2, 2, lambda x: -2, lambda x: 2) # it's a bit of a guess that the above itol is appropriate, but # it should be close assert_allclose(np.sum(eval11), np.sum(eval22), atol=itol*100) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/tests/test_photometry.py0000644000214200020070000010114600000000000022011 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the photometry module. """ from astropy.convolution.utils import discretize_model from astropy.modeling import Fittable2DModel, Parameter from astropy.modeling.fitting import LevMarLSQFitter, SimplexLSQFitter from astropy.modeling.models import Gaussian2D, Moffat2D from astropy.stats import SigmaClip, gaussian_sigma_to_fwhm from astropy.table import Table from astropy.tests.helper import catch_warnings from astropy.utils.exceptions import AstropyUserWarning import numpy as np from numpy.testing import assert_allclose, assert_array_equal, assert_equal import pytest from ..groupstars import DAOGroup from ..models import IntegratedGaussianPRF, FittableImageModel from ..photometry import (BasicPSFPhotometry, DAOPhotPSFPhotometry, IterativelySubtractedPSFPhotometry) from ..sandbox import DiscretePRF from ..utils import prepare_psf_model from ...background import MMMBackground, StdBackgroundRMS from ...datasets import make_gaussian_prf_sources_image, make_noise_image from ...detection import DAOStarFinder from ...utils._optional_deps import HAS_SCIPY # noqa def make_psf_photometry_objs(std=1, sigma_psf=1): """ Produces baseline photometry objects which are then modified as-needed in specific tests below """ daofind = DAOStarFinder(threshold=5.0 * std, fwhm=sigma_psf * gaussian_sigma_to_fwhm) daogroup = DAOGroup(1.5 * sigma_psf * gaussian_sigma_to_fwhm) threshold = 5. * std fwhm = sigma_psf * gaussian_sigma_to_fwhm crit_separation = 1.5 * sigma_psf * gaussian_sigma_to_fwhm daofind = DAOStarFinder(threshold=threshold, fwhm=fwhm) daogroup = DAOGroup(crit_separation) mode_bkg = MMMBackground() psf_model = IntegratedGaussianPRF(sigma=sigma_psf) fitter = LevMarLSQFitter() basic_phot_obj = BasicPSFPhotometry(finder=daofind, group_maker=daogroup, bkg_estimator=mode_bkg, psf_model=psf_model, fitter=fitter, fitshape=(11, 11)) iter_phot_obj = IterativelySubtractedPSFPhotometry(finder=daofind, group_maker=daogroup, bkg_estimator=mode_bkg, psf_model=psf_model, fitter=fitter, niters=1, fitshape=(11, 11)) dao_phot_obj = DAOPhotPSFPhotometry(crit_separation=crit_separation, threshold=threshold, fwhm=fwhm, psf_model=psf_model, fitshape=(11, 11), niters=1) return (basic_phot_obj, iter_phot_obj, dao_phot_obj) sigma_psfs = [] # A group of two overlapped stars and an isolated one sigma_psfs.append(2) sources1 = Table() sources1['flux'] = [800, 1000, 1200] sources1['x_0'] = [13, 18, 25] sources1['y_0'] = [16, 16, 25] sources1['sigma'] = [sigma_psfs[-1]] * 3 sources1['theta'] = [0] * 3 sources1['id'] = [1, 2, 3] sources1['group_id'] = [1, 1, 2] # one single group with four stars. sigma_psfs.append(2) sources2 = Table() sources2['flux'] = [700, 800, 700, 800] sources2['x_0'] = [12, 17, 12, 17] sources2['y_0'] = [15, 15, 20, 20] sources2['sigma'] = [sigma_psfs[-1]] * 4 sources2['theta'] = [0] * 4 sources2['id'] = [1, 2, 3, 4] sources2['group_id'] = [1, 1, 1, 1] # one faint star and one brither companion # although they are in the same group, the detection algorithm # is not able to detect the fainter star, hence photometry should # be performed with niters > 1 or niters=None sigma_psfs.append(2) sources3 = Table() sources3['flux'] = [10000, 1000] sources3['x_0'] = [18, 13] sources3['y_0'] = [17, 19] sources3['sigma'] = [sigma_psfs[-1]] * 2 sources3['theta'] = [0] * 2 sources3['id'] = [1] * 2 sources3['group_id'] = [1] * 2 sources3['iter_detected'] = [1, 2] @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize("sigma_psf, sources", [(sigma_psfs[2], sources3)]) def test_psf_photometry_niters(sigma_psf, sources): img_shape = (32, 32) # generate image with read-out noise (Gaussian) and # background noise (Poisson) image = (make_gaussian_prf_sources_image(img_shape, sources) + make_noise_image(img_shape, distribution='poisson', mean=6., seed=0) + make_noise_image(img_shape, distribution='gaussian', mean=0., stddev=2., seed=0)) cp_image = image.copy() sigma_clip = SigmaClip(sigma=3.) bkgrms = StdBackgroundRMS(sigma_clip) std = bkgrms(image) phot_obj = make_psf_photometry_objs(std, sigma_psf)[1:3] for iter_phot_obj in phot_obj: iter_phot_obj.niters = None result_tab = iter_phot_obj(image) residual_image = iter_phot_obj.get_residual_image() assert (result_tab['x_0_unc'] < 1.96 * sigma_psf / np.sqrt(sources['flux'])).all() assert (result_tab['y_0_unc'] < 1.96 * sigma_psf / np.sqrt(sources['flux'])).all() assert (result_tab['flux_unc'] < 1.96 * np.sqrt(sources['flux'])).all() assert_allclose(result_tab['x_fit'], sources['x_0'], rtol=1e-1) assert_allclose(result_tab['y_fit'], sources['y_0'], rtol=1e-1) assert_allclose(result_tab['flux_fit'], sources['flux'], rtol=1e-1) assert_array_equal(result_tab['id'], sources['id']) assert_array_equal(result_tab['group_id'], sources['group_id']) assert_array_equal(result_tab['iter_detected'], sources['iter_detected']) assert_allclose(np.mean(residual_image), 0.0, atol=1e1) # make sure image is note overwritten assert_array_equal(cp_image, image) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize("sigma_psf, sources", [(sigma_psfs[0], sources1), (sigma_psfs[1], sources2), # these ensure that the test *fails* if the model # PSFs are the wrong shape pytest.param(sigma_psfs[0] / 1.2, sources1, marks=pytest.mark.xfail()), pytest.param(sigma_psfs[1] * 1.2, sources2, marks=pytest.mark.xfail())]) def test_psf_photometry_oneiter(sigma_psf, sources): """ Tests in an image with a group of two overlapped stars and an isolated one. """ img_shape = (32, 32) # generate image with read-out noise (Gaussian) and # background noise (Poisson) image = (make_gaussian_prf_sources_image(img_shape, sources) + make_noise_image(img_shape, distribution='poisson', mean=6., seed=0) + make_noise_image(img_shape, distribution='gaussian', mean=0., stddev=2., seed=0)) cp_image = image.copy() sigma_clip = SigmaClip(sigma=3.) bkgrms = StdBackgroundRMS(sigma_clip) std = bkgrms(image) phot_objs = make_psf_photometry_objs(std, sigma_psf) for phot_proc in phot_objs: result_tab = phot_proc(image) residual_image = phot_proc.get_residual_image() assert (result_tab['x_0_unc'] < 1.96 * sigma_psf / np.sqrt(sources['flux'])).all() assert (result_tab['y_0_unc'] < 1.96 * sigma_psf / np.sqrt(sources['flux'])).all() assert (result_tab['flux_unc'] < 1.96 * np.sqrt(sources['flux'])).all() assert_allclose(result_tab['x_fit'], sources['x_0'], rtol=1e-1) assert_allclose(result_tab['y_fit'], sources['y_0'], rtol=1e-1) assert_allclose(result_tab['flux_fit'], sources['flux'], rtol=1e-1) assert_array_equal(result_tab['id'], sources['id']) assert_array_equal(result_tab['group_id'], sources['group_id']) assert_allclose(np.mean(residual_image), 0.0, atol=1e1) # test fixed photometry phot_proc.psf_model.x_0.fixed = True phot_proc.psf_model.y_0.fixed = True pos = Table(names=['x_0', 'y_0'], data=[sources['x_0'], sources['y_0']]) cp_pos = pos.copy() result_tab = phot_proc(image, pos) residual_image = phot_proc.get_residual_image() assert 'x_0_unc' not in result_tab.colnames assert 'y_0_unc' not in result_tab.colnames assert (result_tab['flux_unc'] < 1.96 * np.sqrt(sources['flux'])).all() assert_array_equal(result_tab['x_fit'], sources['x_0']) assert_array_equal(result_tab['y_fit'], sources['y_0']) assert_allclose(result_tab['flux_fit'], sources['flux'], rtol=1e-1) assert_array_equal(result_tab['id'], sources['id']) assert_array_equal(result_tab['group_id'], sources['group_id']) assert_allclose(np.mean(residual_image), 0.0, atol=1e1) # make sure image is not overwritten assert_array_equal(cp_image, image) # make sure initial guess table is not modified assert_array_equal(cp_pos, pos) # resets fixed positions phot_proc.psf_model.x_0.fixed = False phot_proc.psf_model.y_0.fixed = False @pytest.mark.skipif('not HAS_SCIPY') def test_niters_errors(): iter_phot_obj = make_psf_photometry_objs()[1] # tests that niters is set to an integer even if the user inputs # a float iter_phot_obj.niters = 1.1 assert_equal(iter_phot_obj.niters, 1) # test that a ValueError is raised if niters <= 0 with pytest.raises(ValueError): iter_phot_obj.niters = 0 # test that it's OK to set niters to None iter_phot_obj.niters = None @pytest.mark.skipif('not HAS_SCIPY') def test_fitshape_errors(): basic_phot_obj = make_psf_photometry_objs()[0] # first make sure setting to a scalar does the right thing (and makes # no errors) basic_phot_obj.fitshape = 11 assert np.all(basic_phot_obj.fitshape == (11, 11)) # test that a ValuError is raised if fitshape has even components with pytest.raises(ValueError): basic_phot_obj.fitshape = (2, 2) with pytest.raises(ValueError): basic_phot_obj.fitshape = 2 # test that a ValueError is raised if fitshape has non positive # components with pytest.raises(ValueError): basic_phot_obj.fitshape = (-1, 0) # test that a ValueError is raised if fitshape has more than two # dimensions with pytest.raises(ValueError): basic_phot_obj.fitshape = (3, 3, 3) @pytest.mark.skipif('not HAS_SCIPY') def test_aperture_radius_errors(): basic_phot_obj = make_psf_photometry_objs()[0] # test that aperture_radius was set to None by default assert_equal(basic_phot_obj.aperture_radius, None) # test that a ValueError is raised if aperture_radius is non positive with pytest.raises(ValueError): basic_phot_obj.aperture_radius = -3 @pytest.mark.skipif('not HAS_SCIPY') def test_finder_errors(): iter_phot_obj = make_psf_photometry_objs()[1] with pytest.raises(ValueError): iter_phot_obj.finder = None with pytest.raises(ValueError): iter_phot_obj = IterativelySubtractedPSFPhotometry( finder=None, group_maker=DAOGroup(1), bkg_estimator=MMMBackground(), psf_model=IntegratedGaussianPRF(1), fitshape=(11, 11)) @pytest.mark.skipif('not HAS_SCIPY') def test_finder_positions_warning(): basic_phot_obj = make_psf_photometry_objs(sigma_psf=2)[0] positions = Table() positions['x_0'] = [12.8, 18.2, 25.3] positions['y_0'] = [15.7, 16.5, 25.1] image = (make_gaussian_prf_sources_image((32, 32), sources1) + make_noise_image((32, 32), distribution='poisson', mean=6., seed=0)) with catch_warnings(AstropyUserWarning): result_tab = basic_phot_obj(image=image, init_guesses=positions) assert_array_equal(result_tab['x_0'], positions['x_0']) assert_array_equal(result_tab['y_0'], positions['y_0']) assert_allclose(result_tab['x_fit'], positions['x_0'], rtol=1e-1) assert_allclose(result_tab['y_fit'], positions['y_0'], rtol=1e-1) with pytest.raises(ValueError): basic_phot_obj.finder = None result_tab = basic_phot_obj(image=image) @pytest.mark.skipif('not HAS_SCIPY') def test_aperture_radius(): img_shape = (32, 32) # generate image with read-out noise (Gaussian) and # background noise (Poisson) image = (make_gaussian_prf_sources_image(img_shape, sources1) + make_noise_image(img_shape, distribution='poisson', mean=6., seed=0) + make_noise_image(img_shape, distribution='gaussian', mean=0., stddev=2., seed=0)) basic_phot_obj = make_psf_photometry_objs()[0] # test that aperture radius is properly set whenever the PSF model has # a `fwhm` attribute class PSFModelWithFWHM(Fittable2DModel): x_0 = Parameter(default=1) y_0 = Parameter(default=1) flux = Parameter(default=1) fwhm = Parameter(default=5) def __init__(self, fwhm=fwhm.default): super().__init__(fwhm=fwhm) def evaluate(self, x, y, x_0, y_0, flux, fwhm): return flux / (fwhm * (x - x_0)**2 * (y - y_0)**2) psf_model = PSFModelWithFWHM() basic_phot_obj.psf_model = psf_model basic_phot_obj(image) assert_equal(basic_phot_obj.aperture_radius, psf_model.fwhm.value) PARS_TO_SET_0 = {'x_0': 'x_0', 'y_0': 'y_0', 'flux_0': 'flux'} PARS_TO_OUTPUT_0 = {'x_fit': 'x_0', 'y_fit': 'y_0', 'flux_fit': 'flux'} PARS_TO_SET_1 = PARS_TO_SET_0.copy() PARS_TO_SET_1['sigma_0'] = 'sigma' PARS_TO_OUTPUT_1 = PARS_TO_OUTPUT_0.copy() PARS_TO_OUTPUT_1['sigma_fit'] = 'sigma' @pytest.mark.parametrize("actual_pars_to_set, actual_pars_to_output," "is_sigma_fixed", [(PARS_TO_SET_0, PARS_TO_OUTPUT_0, True), (PARS_TO_SET_1, PARS_TO_OUTPUT_1, False)]) @pytest.mark.skipif('not HAS_SCIPY') def test_define_fit_param_names(actual_pars_to_set, actual_pars_to_output, is_sigma_fixed): psf_model = IntegratedGaussianPRF() psf_model.sigma.fixed = is_sigma_fixed basic_phot_obj = make_psf_photometry_objs()[0] basic_phot_obj.psf_model = psf_model basic_phot_obj._define_fit_param_names() assert_equal(basic_phot_obj._pars_to_set, actual_pars_to_set) assert_equal(basic_phot_obj._pars_to_output, actual_pars_to_output) # tests previously written to psf_photometry PSF_SIZE = 11 GAUSSIAN_WIDTH = 1. IMAGE_SIZE = 101 # Position and FLUXES of test sources INTAB = Table([[50., 23, 12, 86], [50., 83, 80, 84], [np.pi * 10, 3.654, 20., 80 / np.sqrt(3)]], names=['x_0', 'y_0', 'flux_0']) # Create test psf psf_model = Gaussian2D(1. / (2 * np.pi * GAUSSIAN_WIDTH ** 2), PSF_SIZE // 2, PSF_SIZE // 2, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) test_psf = discretize_model(psf_model, (0, PSF_SIZE), (0, PSF_SIZE), mode='oversample') # Set up grid for test image image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) # Add sources to test image for x, y, flux in INTAB: model = Gaussian2D(flux / (2 * np.pi * GAUSSIAN_WIDTH ** 2), x, y, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) image += discretize_model(model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') # Some tests require an image with wider sources. WIDE_GAUSSIAN_WIDTH = 3. WIDE_INTAB = Table([[50, 23.2], [50.5, 1], [10, 20]], names=['x_0', 'y_0', 'flux_0']) wide_image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) # Add sources to test image for x, y, flux in WIDE_INTAB: model = Gaussian2D(flux / (2 * np.pi * WIDE_GAUSSIAN_WIDTH ** 2), x, y, WIDE_GAUSSIAN_WIDTH, WIDE_GAUSSIAN_WIDTH) wide_image += discretize_model(model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') @pytest.mark.skipif('not HAS_SCIPY') def test_psf_photometry_discrete(): """ Test psf_photometry with discrete PRF model. """ prf = DiscretePRF(test_psf, subsampling=1) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7) f = basic_phot(image=image, init_guesses=INTAB) for n in ['x', 'y', 'flux']: assert_allclose(f[n + '_0'], f[n + '_fit'], rtol=1e-6) @pytest.mark.skipif('not HAS_SCIPY') def test_tune_coordinates(): """ Test psf_photometry with discrete PRF model and coordinates that need to be adjusted in the fit. """ prf = DiscretePRF(test_psf, subsampling=1) prf.x_0.fixed = False prf.y_0.fixed = False # Shift all sources by 0.3 pixels intab = INTAB.copy() intab['x_0'] += 0.3 basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7) f = basic_phot(image=image, init_guesses=intab) for n in ['x', 'y', 'flux']: assert_allclose(f[n + '_0'], f[n + '_fit'], rtol=1e-3) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_boundary(): """ Test psf_photometry with discrete PRF model at the boundary of the data. """ prf = DiscretePRF(test_psf, subsampling=1) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7, aperture_radius=5.5) intab = Table(data=[[1], [1]], names=['x_0', 'y_0']) f = basic_phot(image=image, init_guesses=intab) assert_allclose(f['flux_fit'], 0, atol=1e-8) @pytest.mark.skipif('not HAS_SCIPY') def test_default_aperture_radius(): """ Test psf_photometry with non-Gaussian model, such that it raises a warning about aperture_radius. """ def tophatfinder(image): """ Simple top hat finder function for use with a top hat PRF""" fluxes = np.unique(image[image > 1]) table = Table(names=['id', 'xcentroid', 'ycentroid', 'flux'], dtype=[int, float, float, float]) for n, f in enumerate(fluxes): ys, xs = np.where(image == f) x = np.mean(xs) y = np.mean(ys) table.add_row([int(n+1), x, y, f*9]) table.sort(['flux']) return table prf = np.zeros((7, 7), float) prf[2:5, 2:5] = 1/9 prf = FittableImageModel(prf) img = np.zeros((50, 50), float) x0 = [38, 20, 35] y0 = [20, 5, 40] f0 = [50, 100, 200] for x, y, f in zip(x0, y0, f0): img[y-1:y+2, x-1:x+2] = f/9 intab = Table(data=[[37, 19.6, 34.9], [19.6, 4.5, 40.1]], names=['x_0', 'y_0']) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7, finder=tophatfinder) # Test for init_guesses is None with pytest.warns(AstropyUserWarning, match='aperture_radius is None and ' 'could not be determined'): results = basic_phot(image=img) assert_allclose(results['flux_fit'], f0, rtol=0.05) # Have to reset the object or it saves any updates, and we wish to # re-verify the aperture_radius assignment basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7) # Test for init_guesses is not None, but lacks a flux_0 column with pytest.warns(AstropyUserWarning, match='aperture_radius is None and ' 'could not be determined'): results = basic_phot(image=img, init_guesses=intab) assert_allclose(results['flux_fit'], f0, rtol=0.05) iter_phot = IterativelySubtractedPSFPhotometry(finder=tophatfinder, group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7, niters=2) # Test for init_guesses is not None, but lacks a flux_0 column with pytest.warns(AstropyUserWarning, match='aperture_radius is None and ' 'could not be determined'): results = iter_phot(image=img, init_guesses=intab) assert_allclose(results['flux_fit'], f0, rtol=0.05) iter_phot = IterativelySubtractedPSFPhotometry(finder=tophatfinder, group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7, niters=2) # Test for init_guesses is None with pytest.warns(AstropyUserWarning, match='aperture_radius is None and ' 'could not be determined'): results = iter_phot(image=img) assert_allclose(results['flux_fit'], f0, rtol=0.05) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_boundary_gaussian(): """ Test psf_photometry with discrete PRF model at the boundary of the data. """ psf = IntegratedGaussianPRF(GAUSSIAN_WIDTH) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf, fitshape=7) intab = Table(data=[[1], [1]], names=['x_0', 'y_0']) f = basic_phot(image=image, init_guesses=intab) assert_allclose(f['flux_fit'], 0, atol=1e-8) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_photometry_gaussian(): """ Test psf_photometry with Gaussian PSF model. """ psf = IntegratedGaussianPRF(sigma=GAUSSIAN_WIDTH) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf, fitshape=7) f = basic_phot(image=image, init_guesses=INTAB) for n in ['x', 'y', 'flux']: assert_allclose(f[n + '_0'], f[n + '_fit'], rtol=1e-3) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize("renormalize_psf", (True, False)) def test_psf_photometry_gaussian2(renormalize_psf): """ Test psf_photometry with Gaussian PSF model from Astropy. """ psf = Gaussian2D(1. / (2 * np.pi * GAUSSIAN_WIDTH ** 2), PSF_SIZE // 2, PSF_SIZE // 2, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) psf = prepare_psf_model(psf, xname='x_mean', yname='y_mean', renormalize_psf=renormalize_psf) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf, fitshape=7) f = basic_phot(image=image, init_guesses=INTAB) for n in ['x', 'y']: assert_allclose(f[n + '_0'], f[n + '_fit'], rtol=1e-1) assert_allclose(f['flux_0'], f['flux_fit'], rtol=1e-1) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_photometry_moffat(): """ Test psf_photometry with Moffat PSF model from Astropy. """ psf = Moffat2D(1. / (2 * np.pi * GAUSSIAN_WIDTH ** 2), PSF_SIZE // 2, PSF_SIZE // 2, 1, 1) psf = prepare_psf_model(psf, xname='x_0', yname='y_0', renormalize_psf=False) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf, fitshape=7) f = basic_phot(image=image, init_guesses=INTAB) f.pprint(max_width=-1) for n in ['x', 'y']: assert_allclose(f[n + '_0'], f[n + '_fit'], rtol=1e-3) # image was created with a gaussian, so flux won't match exactly assert_allclose(f['flux_0'], f['flux_fit'], rtol=1e-1) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_fitting_data_on_edge(): """ No mask is input explicitly here, but source 2 is so close to the edge that the subarray that's extracted gets a mask internally. """ psf_guess = IntegratedGaussianPRF(flux=1, sigma=WIDE_GAUSSIAN_WIDTH) psf_guess.flux.fixed = psf_guess.x_0.fixed = psf_guess.y_0.fixed = False basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf_guess, fitshape=7) outtab = basic_phot(image=wide_image, init_guesses=WIDE_INTAB) for n in ['x', 'y', 'flux']: assert_allclose(outtab[n + '_0'], outtab[n + '_fit'], rtol=0.05, atol=0.1) @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.parametrize("sigma_psf, sources", [(sigma_psfs[2], sources3)]) def test_psf_extra_output_cols(sigma_psf, sources): """ Test the handling of a non-None extra_output_cols """ psf_model = IntegratedGaussianPRF(sigma=sigma_psf) tshape = (32, 32) image = (make_gaussian_prf_sources_image(tshape, sources) + make_noise_image(tshape, distribution='poisson', mean=6., seed=0) + make_noise_image(tshape, distribution='gaussian', mean=0., stddev=2., seed=0)) init_guess1 = None init_guess2 = Table(names=['x_0', 'y_0', 'sharpness', 'roundness1', 'roundness2'], data=[[17.4], [16], [0.4], [0], [0]]) init_guess3 = Table(names=['x_0', 'y_0'], data=[[17.4], [16]]) init_guess4 = Table(names=['x_0', 'y_0', 'sharpness'], data=[[17.4], [16], [0.4]]) for i, init_guesses in enumerate([init_guess1, init_guess2, init_guess3, init_guess4]): dao_phot = DAOPhotPSFPhotometry(crit_separation=8, threshold=40, fwhm=4 * np.sqrt(2 * np.log(2)), psf_model=psf_model, fitshape=(11, 11), extra_output_cols=['sharpness', 'roundness1', 'roundness2']) phot_results = dao_phot(image, init_guesses=init_guesses) # test that the original required columns are also passed back, as well # as extra_output_cols assert np.all([name in phot_results.colnames for name in ['x_0', 'y_0']]) assert np.all([name in phot_results.colnames for name in ['sharpness', 'roundness1', 'roundness2']]) assert len(phot_results) == 2 # checks to verify that half-passing init_guesses results in NaN output # for extra_output_cols not passed as initial guesses if i == 2: # init_guess3 assert(np.all(np.all(np.isnan(phot_results[o])) for o in ['sharpness', 'roundness1', 'roundness2'])) if i == 3: # init_guess4 assert(np.all(np.all(np.isnan(phot_results[o])) for o in ['roundness1', 'roundness2'])) assert(np.all(~np.isnan(phot_results['sharpness']))) @pytest.fixture(params=[2, 3]) def overlap_image(request): if request.param == 2: close_tab = Table([[50., 53.], [50., 50.], [25., 25.]], names=['x_0', 'y_0', 'flux_0']) elif request.param == 3: close_tab = Table([[50., 55., 50.], [50., 50., 55.], [25., 25., 25.]], names=['x_0', 'y_0', 'flux_0']) else: raise ValueError # Add sources to test image close_image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) for x, y, flux in close_tab: close_model = Gaussian2D(flux / (2 * np.pi * GAUSSIAN_WIDTH ** 2), x, y, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) close_image += discretize_model(close_model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') return close_image @pytest.mark.skipif('not HAS_SCIPY') def test_psf_fitting_group(overlap_image): """ Test psf_photometry when two input stars are close and need to be fit together """ from photutils.background import MADStdBackgroundRMS # There are a few models here that fail, be it something # created by EPSFBuilder or simpler the Moffat2D one # unprepared_psf = Moffat2D(amplitude=1, gamma=2, alpha=2.8, x_0=0, y_0=0) # psf = prepare_psf_model(unprepared_psf, xname='x_0', yname='y_0', fluxname=None) psf = prepare_psf_model(Gaussian2D(), renormalize_psf=False) psf.fwhm = Parameter('fwhm', 'this is not the way to add this I think') psf.fwhm.value = 10 separation_crit = 10 # choose low threshold and fwhm to find stars no matter what basic_phot = BasicPSFPhotometry(finder=DAOStarFinder(1, 1), group_maker=DAOGroup(separation_crit), bkg_estimator=MADStdBackgroundRMS(), fitter=LevMarLSQFitter(), psf_model=psf, fitshape=31) # this should not raise AttributeError: Attribute "offset_0_0" not found basic_phot(image=overlap_image) @pytest.mark.skipif('not HAS_SCIPY') def test_finder_return_none(): """ Test psf_photometry with finder that does not return None if no sources are detected, to test Iterative PSF fitting. """ def tophatfinder(image): """ Simple top hat finder function for use with a top hat PRF""" fluxes = np.unique(image[image > 1]) table = Table(names=['id', 'xcentroid', 'ycentroid', 'flux'], dtype=[int, float, float, float]) for n, f in enumerate(fluxes): ys, xs = np.where(image == f) x = np.mean(xs) y = np.mean(ys) table.add_row([int(n + 1), x, y, f * 9]) table.sort(['flux']) return table prf = np.zeros((7, 7), float) prf[2:5, 2:5] = 1 / 9 prf = FittableImageModel(prf) img = np.zeros((50, 50), float) x0 = [38, 20, 35] y0 = [20, 5, 40] f0 = [50, 100, 200] for x, y, f in zip(x0, y0, f0): img[y - 1:y + 2, x - 1:x + 2] = f / 9 intab = Table(data=[[37, 19.6, 34.9], [19.6, 4.5, 40.1], [45, 103, 210]], names=['x_0', 'y_0', 'flux_0']) iter_phot = IterativelySubtractedPSFPhotometry(finder=tophatfinder, group_maker=DAOGroup(2), bkg_estimator=None, psf_model=prf, fitshape=7, niters=2, aperture_radius=3) results = iter_phot(image=img, init_guesses=intab) assert_allclose(results['flux_fit'], f0, rtol=0.05) @pytest.mark.skipif('not HAS_SCIPY') def test_psf_photometry_uncertainties(): """ Test an Astropy fitter that does not return a parameter covariance matrix (param_cov). The output table should not contain flux_unc, x_0_unc, and y_0_unc columns. """ psf = IntegratedGaussianPRF(sigma=GAUSSIAN_WIDTH) basic_phot = BasicPSFPhotometry(group_maker=DAOGroup(2), bkg_estimator=None, psf_model=psf, fitter=SimplexLSQFitter(), fitshape=7) phot_tbl = basic_phot(image=image, init_guesses=INTAB) columns = ('flux_unc', 'x_0_unc', 'y_0_unc') for column in columns: assert column not in phot_tbl.colnames @pytest.mark.skipif('not HAS_SCIPY') def test_re_use_result_as_initial_guess(): img_shape = (32, 32) # generate image with read-out noise (Gaussian) and # background noise (Poisson) image = (make_gaussian_prf_sources_image(img_shape, sources1) + make_noise_image(img_shape, distribution='poisson', mean=6., seed=0) + make_noise_image(img_shape, distribution='gaussian', mean=0., stddev=2., seed=0)) _, _, dao_phot_obj = make_psf_photometry_objs() result_table = dao_phot_obj(image) result_table['x'] = result_table['x_fit'] result_table['y'] = result_table['y_fit'] result_table['flux'] = result_table['flux_fit'] second_result = dao_phot_obj(image, result_table) assert second_result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/photutils/psf/tests/test_sandbox.py0000644000214200020070000000661700000000000021244 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the sandbox module. """ from astropy.convolution.utils import discretize_model from astropy.modeling.models import Gaussian2D from astropy.table import Table import numpy as np from numpy.testing import assert_allclose from ..sandbox import DiscretePRF PSF_SIZE = 11 GAUSSIAN_WIDTH = 1. IMAGE_SIZE = 101 # Position and FLUXES of test sources INTAB = Table([[50., 23, 12, 86], [50., 83, 80, 84], [np.pi * 10, 3.654, 20., 80 / np.sqrt(3)]], names=['x_0', 'y_0', 'flux_0']) # Create test psf psf_model = Gaussian2D(1. / (2 * np.pi * GAUSSIAN_WIDTH ** 2), PSF_SIZE // 2, PSF_SIZE // 2, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) test_psf = discretize_model(psf_model, (0, PSF_SIZE), (0, PSF_SIZE), mode='oversample') # Set up grid for test image image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) # Add sources to test image for x, y, flux in INTAB: model = Gaussian2D(flux / (2 * np.pi * GAUSSIAN_WIDTH ** 2), x, y, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) image += discretize_model(model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') # Some tests require an image with wider sources. WIDE_GAUSSIAN_WIDTH = 3. WIDE_INTAB = Table([[50, 23.2], [50.5, 1], [10, 20]], names=['x_0', 'y_0', 'flux_0']) wide_image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) # Add sources to test image for x, y, flux in WIDE_INTAB: model = Gaussian2D(flux / (2 * np.pi * WIDE_GAUSSIAN_WIDTH ** 2), x, y, WIDE_GAUSSIAN_WIDTH, WIDE_GAUSSIAN_WIDTH) wide_image += discretize_model(model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') def test_create_prf_mean(): """ Check if create_prf works correctly on simulated data. Position input format: list """ prf = DiscretePRF.create_from_image(image, list(INTAB['x_0', 'y_0'].as_array()), PSF_SIZE, subsampling=1, mode='mean') assert_allclose(prf._prf_array[0, 0], test_psf, atol=1E-8) def test_create_prf_median(): """ Check if create_prf works correctly on simulated data. Position input format: astropy.table.Table """ prf = DiscretePRF.create_from_image(image, np.array(INTAB['x_0', 'y_0']), PSF_SIZE, subsampling=1, mode='median') assert_allclose(prf._prf_array[0, 0], test_psf, atol=1E-8) def test_create_prf_nan(): """ Check if create_prf deals correctly with nan values. """ image_nan = image.copy() image_nan[52, 52] = np.nan image_nan[52, 48] = np.nan prf = DiscretePRF.create_from_image(image, np.array(INTAB['x_0', 'y_0']), PSF_SIZE, subsampling=1, fix_nan=True) assert not np.isnan(prf._prf_array[0, 0]).any() def test_create_prf_flux(): """ Check if create_prf works correctly when FLUXES are specified. """ prf = DiscretePRF.create_from_image(image, np.array(INTAB['x_0', 'y_0']), PSF_SIZE, subsampling=1, mode='median', fluxes=INTAB['flux_0']) assert_allclose(prf._prf_array[0, 0].sum(), 1) assert_allclose(prf._prf_array[0, 0], test_psf, atol=1E-8) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/tests/test_utils.py0000644000214200020070000002236100000000000020740 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the utils module. """ from astropy.convolution.utils import discretize_model from astropy.modeling.models import Gaussian2D from astropy.table import Table import numpy as np from numpy.testing import assert_allclose import pytest from ..groupstars import DAOGroup from ..models import IntegratedGaussianPRF from ..photometry import BasicPSFPhotometry from ..sandbox import DiscretePRF from ..utils import get_grouped_psf_model, prepare_psf_model, subtract_psf from ...utils._optional_deps import HAS_SCIPY # noqa PSF_SIZE = 11 GAUSSIAN_WIDTH = 1. IMAGE_SIZE = 101 # Position and FLUXES of test sources INTAB = Table([[50., 23, 12, 86], [50., 83, 80, 84], [np.pi * 10, 3.654, 20., 80 / np.sqrt(3)]], names=['x_0', 'y_0', 'flux_0']) # Create test psf psf_model = Gaussian2D(1. / (2 * np.pi * GAUSSIAN_WIDTH ** 2), PSF_SIZE // 2, PSF_SIZE // 2, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) test_psf = discretize_model(psf_model, (0, PSF_SIZE), (0, PSF_SIZE), mode='oversample') # Set up grid for test image image = np.zeros((IMAGE_SIZE, IMAGE_SIZE)) # Add sources to test image for x, y, flux in INTAB: model = Gaussian2D(flux / (2 * np.pi * GAUSSIAN_WIDTH ** 2), x, y, GAUSSIAN_WIDTH, GAUSSIAN_WIDTH) image += discretize_model(model, (0, IMAGE_SIZE), (0, IMAGE_SIZE), mode='oversample') @pytest.fixture(scope="module") def moffimg(): """ This fixture requires scipy so don't call it from non-scipy tests """ from scipy import integrate from astropy.modeling.models import Moffat2D mof = Moffat2D(alpha=4.8) # this is the analytic value needed to get a total flux of 1 mof.amplitude = (mof.alpha-1)/(np.pi*mof.gamma**2) # first make sure it really is normalized assert (1 - integrate.dblquad(mof, -10, 10, lambda x: -10, lambda x: 10)[0]) < 1e-6 # now create an "image" of the PSF xg, yg = np.meshgrid(*([np.linspace(-2, 2, 100)]*2)) return mof, (xg, yg, mof(xg, yg)) @pytest.mark.skipif('not HAS_SCIPY') def test_moffat_fitting(moffimg): """ Test that the Moffat to be fit in test_psf_adapter is behaving correctly """ from astropy.modeling.fitting import LevMarLSQFitter from astropy.modeling.models import Moffat2D mof, (xg, yg, img) = moffimg # a closeish-but-wrong "guessed Moffat" guess_moffat = Moffat2D(x_0=.1, y_0=-.05, gamma=1.05, amplitude=mof.amplitude*1.06, alpha=4.75) f = LevMarLSQFitter() fit_mof = f(guess_moffat, xg, yg, img) assert_allclose(fit_mof.parameters, mof.parameters, rtol=.01, atol=.0005) # we set the tolerances in flux to be 2-3% because the shape paraameters of # the guessed version are known to be wrong. @pytest.mark.parametrize("prepkwargs,tols", [ (dict(xname='x_0', yname='y_0', fluxname=None, renormalize_psf=True), (1e-3, .02)), (dict(xname=None, yname=None, fluxname=None, renormalize_psf=True), (1e-3, .02)), (dict(xname=None, yname=None, fluxname=None, renormalize_psf=False), (1e-3, .03)), (dict(xname='x_0', yname='y_0', fluxname='amplitude', renormalize_psf=False), (1e-3, None)), ]) @pytest.mark.skipif('not HAS_SCIPY') def test_prepare_psf_model(moffimg, prepkwargs, tols): """ Test that prepare_psf_model behaves as expected for fitting (don't worry about full-on psf photometry for now) """ from astropy.modeling.fitting import LevMarLSQFitter from astropy.modeling.models import Moffat2D mof, (xg, yg, img) = moffimg f = LevMarLSQFitter() # a close-but-wrong "guessed Moffat" guess_moffat = Moffat2D(x_0=.1, y_0=-.05, gamma=1.01, amplitude=mof.amplitude*1.01, alpha=4.79) if prepkwargs['renormalize_psf']: # definitely very wrong, so this ensures the re-normalization # stuff works guess_moffat.amplitude = 5. if prepkwargs['xname'] is None: guess_moffat.x_0 = 0 if prepkwargs['yname'] is None: guess_moffat.y_0 = 0 psfmod = prepare_psf_model(guess_moffat, **prepkwargs) xytol, fluxtol = tols fit_psfmod = f(psfmod, xg, yg, img) if xytol is not None: assert np.abs(getattr(fit_psfmod, fit_psfmod.xname)) < xytol assert np.abs(getattr(fit_psfmod, fit_psfmod.yname)) < xytol if fluxtol is not None: assert np.abs(1 - getattr(fit_psfmod, fit_psfmod.fluxname)) < fluxtol # ensure the amplitude and shape parameters did *not* change assert fit_psfmod.psfmodel.gamma == guess_moffat.gamma assert fit_psfmod.psfmodel.alpha == guess_moffat.alpha if prepkwargs['fluxname'] is None: assert fit_psfmod.psfmodel.amplitude == guess_moffat.amplitude @pytest.mark.skipif('not HAS_SCIPY') def test_prepare_psf_model_offset(): """ Regression test to ensure the offset is in the correct direction. """ norm = False sigma = 3.0 amplitude = 1. / (2 * np.pi * sigma ** 2) xcen = ycen = 0. psf0 = Gaussian2D(amplitude, xcen, ycen, sigma, sigma) psf1 = prepare_psf_model(psf0, xname='x_mean', yname='y_mean', renormalize_psf=norm) psf2 = prepare_psf_model(psf0, renormalize_psf=norm) psf3 = prepare_psf_model(psf0, xname='x_mean', renormalize_psf=norm) psf4 = prepare_psf_model(psf0, yname='y_mean', renormalize_psf=norm) yy, xx = np.mgrid[0:101, 0:101] psf = psf1.copy() xval = 48 yval = 52 flux = 14.51 psf.x_mean_2 = xval psf.y_mean_2 = yval data = psf(xx, yy) * flux group_maker = DAOGroup(2) bkg_estimator = None fitshape = 7 init_guesses = Table([[46.1], [57.3], [7.1]], names=['x_0', 'y_0', 'flux_0']) phot1 = BasicPSFPhotometry(group_maker=group_maker, bkg_estimator=bkg_estimator, fitshape=fitshape, psf_model=psf1) tbl1 = phot1(image=data, init_guesses=init_guesses) phot2 = BasicPSFPhotometry(group_maker=group_maker, bkg_estimator=bkg_estimator, fitshape=fitshape, psf_model=psf2) tbl2 = phot2(image=data, init_guesses=init_guesses) phot3 = BasicPSFPhotometry(group_maker=group_maker, bkg_estimator=bkg_estimator, fitshape=fitshape, psf_model=psf3) tbl3 = phot3(image=data, init_guesses=init_guesses) phot4 = BasicPSFPhotometry(group_maker=group_maker, bkg_estimator=bkg_estimator, fitshape=fitshape, psf_model=psf4) tbl4 = phot4(image=data, init_guesses=init_guesses) assert_allclose((tbl1['x_fit'][0], tbl1['y_fit'][0], tbl1['flux_fit'][0]), (xval, yval, flux)) assert_allclose((tbl2['x_fit'][0], tbl2['y_fit'][0], tbl2['flux_fit'][0]), (xval, yval, flux)) assert_allclose((tbl3['x_fit'][0], tbl3['y_fit'][0], tbl3['flux_fit'][0]), (xval, yval, flux)) assert_allclose((tbl4['x_fit'][0], tbl4['y_fit'][0], tbl4['flux_fit'][0]), (xval, yval, flux)) @pytest.mark.skipif('not HAS_SCIPY') def test_get_grouped_psf_model(): igp = IntegratedGaussianPRF(sigma=1.2) tab = Table(names=['x_0', 'y_0', 'flux_0'], data=[[1, 2], [3, 4], [0.5, 1]]) pars_to_set = {'x_0': 'x_0', 'y_0': 'y_0', 'flux_0': 'flux'} gpsf = get_grouped_psf_model(igp, tab, pars_to_set) assert gpsf.x_0_0 == 1 assert gpsf.y_0_1 == 4 assert gpsf.flux_0 == 0.5 assert gpsf.flux_1 == 1 assert gpsf.sigma_0 == gpsf.sigma_1 == 1.2 @pytest.fixture(params=[0, 1, 2]) def prf_model(request): # use this instead of pytest.mark.parameterize as we use scipy and # it still calls that even if not HAS_SCIPY is set... prfs = [IntegratedGaussianPRF(sigma=1.2), Gaussian2D(x_stddev=2), prepare_psf_model(Gaussian2D(x_stddev=2), renormalize_psf=False)] return prfs[request.param] @pytest.mark.skipif('not HAS_SCIPY') def test_get_grouped_psf_model_submodel_names(prf_model): """Verify that submodel tagging works""" tab = Table(names=['x_0', 'y_0', 'flux_0'], data=[[1, 2], [3, 4], [0.5, 1]]) pars_to_set = {'x_0': 'x_0', 'y_0': 'y_0', 'flux_0': 'flux'} gpsf = get_grouped_psf_model(prf_model, tab, pars_to_set) # There should be two submodels one named 0 and one named 1 assert len([submodel for submodel in gpsf.traverse_postorder() if submodel.name == 0]) == 1 assert len([submodel for submodel in gpsf.traverse_postorder() if submodel.name == 1]) == 1 @pytest.mark.skipif('not HAS_SCIPY') def test_subtract_psf(): """Test subtract_psf.""" prf = DiscretePRF(test_psf, subsampling=1) posflux = INTAB.copy() for n in posflux.colnames: posflux.rename_column(n, n.split('_')[0] + '_fit') residuals = subtract_psf(image, prf, posflux) assert_allclose(residuals, np.zeros_like(image), atol=1E-4) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/psf/utils.py0000644000214200020070000002277400000000000016547 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides utilities for PSF-fitting photometry. """ from astropy.table import QTable from astropy.modeling.models import Const2D, Identity, Shift from astropy.nddata.utils import add_array, extract_array import numpy as np __all__ = ['prepare_psf_model', 'get_grouped_psf_model', 'subtract_psf'] class _InverseShift(Shift): @staticmethod def evaluate(x, offset): return x - offset @staticmethod def fit_deriv(x, *params): """ One dimensional Shift model derivative with respect to parameter. """ d_offset = -np.ones_like(x) return [d_offset] def prepare_psf_model(psfmodel, xname=None, yname=None, fluxname=None, renormalize_psf=True): """ Convert a 2D PSF model to one suitable for use with `BasicPSFPhotometry` or its subclasses. .. note:: This function is needed only in special cases where the PSF model does not have ``x_0``, ``y_0``, and ``flux`` model parameters. In particular, it is not needed for any of the PSF models provided by photutils (e.g., `~photutils.psf.EPSFModel`, `~photutils.psf.IntegratedGaussianPRF`, `~photutils.psf.FittableImageModel`, `~photutils.psf.GriddedPSFModel`, etc). Parameters ---------- psfmodel : `~astropy.modeling.Fittable2DModel` The model to assume as representative of the PSF. xname : `str` or `None`, optional The name of the ``psfmodel`` parameter that corresponds to the x-axis center of the PSF. If `None`, the model will be assumed to be centered at x=0, and a new parameter will be added for the offset. yname : `str` or `None`, optional The name of the ``psfmodel`` parameter that corresponds to the y-axis center of the PSF. If `None`, the model will be assumed to be centered at y=0, and a new parameter will be added for the offset. fluxname : `str` or `None`, optional The name of the ``psfmodel`` parameter that corresponds to the total flux of the star. If `None`, a scaling factor will be added to the model. renormalize_psf : bool, optional If `True`, the model will be integrated from -inf to inf and rescaled so that the total integrates to 1. Note that this renormalization only occurs *once*, so if the total flux of ``psfmodel`` depends on position, this will *not* be correct. Returns ------- result : `~astropy.modeling.Fittable2DModel` A new model ready to be passed into `BasicPSFPhotometry` or its subclasses. """ if xname is None: xinmod = _InverseShift(0, name='x_offset') xname = 'offset_0' else: xinmod = Identity(1) xname = xname + '_2' xinmod.fittable = True if yname is None: yinmod = _InverseShift(0, name='y_offset') yname = 'offset_1' else: yinmod = Identity(1) yname = yname + '_2' yinmod.fittable = True outmod = (xinmod & yinmod) | psfmodel.copy() if fluxname is None: outmod = outmod * Const2D(1, name='flux_scaling') fluxname = 'amplitude_3' else: fluxname = fluxname + '_2' if renormalize_psf: # we do the import here because other machinery works w/o scipy from scipy import integrate integrand = integrate.dblquad(psfmodel, -np.inf, np.inf, lambda x: -np.inf, lambda x: np.inf)[0] normmod = Const2D(1./integrand, name='renormalize_scaling') outmod = outmod * normmod # final setup of the output model - fix all the non-offset/scale # parameters for pnm in outmod.param_names: outmod.fixed[pnm] = pnm not in (xname, yname, fluxname) # and set the names so that BasicPSFPhotometry knows what to do outmod.xname = xname outmod.yname = yname outmod.fluxname = fluxname # now some convenience aliases if reasonable outmod.psfmodel = outmod[2] if 'x_0' not in outmod.param_names and 'y_0' not in outmod.param_names: outmod.x_0 = getattr(outmod, xname) outmod.y_0 = getattr(outmod, yname) if 'flux' not in outmod.param_names: outmod.flux = getattr(outmod, fluxname) return outmod def get_grouped_psf_model(template_psf_model, star_group, pars_to_set): """ Construct a joint PSF model which consists of a sum of PSF's templated on a specific model, but whose parameters are given by a table of objects. Parameters ---------- template_psf_model : `astropy.modeling.Fittable2DModel` instance The model to use for *individual* objects. Must have parameters named ``x_0``, ``y_0``, and ``flux``. star_group : `~astropy.table.Table` Table of stars for which the compound PSF will be constructed. It must have columns named ``x_0``, ``y_0``, and ``flux_0``. pars_to_set : `dict` A dictionary of parameter names and values to set. Returns ------- group_psf An `astropy.modeling` ``CompoundModel`` instance which is a sum of the given PSF models. """ group_psf = None for index, star in enumerate(star_group): psf_to_add = template_psf_model.copy() # we 'tag' the model here so that later we don't have to rely # on possibly mangled names of the compound model to find # the parameters again psf_to_add.name = index for param_tab_name, param_name in pars_to_set.items(): setattr(psf_to_add, param_name, star[param_tab_name]) if group_psf is None: # this is the first one only group_psf = psf_to_add else: group_psf = group_psf + psf_to_add return group_psf def _extract_psf_fitting_names(psf): """ Determine the names of the x coordinate, y coordinate, and flux from a model. Returns (xname, yname, fluxname) """ if hasattr(psf, 'xname'): xname = psf.xname elif 'x_0' in psf.param_names: xname = 'x_0' else: raise ValueError('Could not determine x coordinate name for ' 'psf_photometry.') if hasattr(psf, 'yname'): yname = psf.yname elif 'y_0' in psf.param_names: yname = 'y_0' else: raise ValueError('Could not determine y coordinate name for ' 'psf_photometry.') if hasattr(psf, 'fluxname'): fluxname = psf.fluxname elif 'flux' in psf.param_names: fluxname = 'flux' else: raise ValueError('Could not determine flux name for psf_photometry.') return xname, yname, fluxname def _call_fitter(fitter, psf, x, y, data, weights): """ Not all fitters have to support a weight array. This function includes the weight in the fitter call only if really needed. """ if np.all(weights == 1.): return fitter(psf, x, y, data) else: return fitter(psf, x, y, data, weights=weights) def subtract_psf(data, psf, posflux, subshape=None): """ Subtract PSF/PRFs from an image. Parameters ---------- data : `~astropy.nddata.NDData` or array (must be 2D) Image data. psf : `astropy.modeling.Fittable2DModel` instance PSF/PRF model to be subtracted from the data. posflux : Array-like of shape (3, N) or `~astropy.table.Table` Positions and fluxes for the objects to subtract. If an array, it is interpreted as ``(x, y, flux)`` If a table, the columns 'x_fit', 'y_fit', and 'flux_fit' must be present. subshape : length-2 or None The shape of the region around the center of the location to subtract the PSF from. If None, subtract from the whole image. Returns ------- subdata : same shape and type as ``data`` The image with the PSF subtracted """ if data.ndim != 2: raise ValueError(f'{data.ndim}-d array not supported. Only 2-d ' 'arrays can be passed to subtract_psf.') # translate array input into table if hasattr(posflux, 'colnames'): if 'x_fit' not in posflux.colnames: raise ValueError('Input table does not have x_fit') if 'y_fit' not in posflux.colnames: raise ValueError('Input table does not have y_fit') if 'flux_fit' not in posflux.colnames: raise ValueError('Input table does not have flux_fit') else: posflux = QTable(names=['x_fit', 'y_fit', 'flux_fit'], data=posflux) # Set up constants across the loop psf = psf.copy() xname, yname, fluxname = _extract_psf_fitting_names(psf) indices = np.indices(data.shape) subbeddata = data.copy() if subshape is None: indicies_reversed = indices[::-1] for row in posflux: getattr(psf, xname).value = row['x_fit'] getattr(psf, yname).value = row['y_fit'] getattr(psf, fluxname).value = row['flux_fit'] subbeddata -= psf(*indicies_reversed) else: for row in posflux: x_0, y_0 = row['x_fit'], row['y_fit'] # float dtype needed for fill_value=np.nan y = extract_array(indices[0].astype(float), subshape, (y_0, x_0)) x = extract_array(indices[1].astype(float), subshape, (y_0, x_0)) getattr(psf, xname).value = x_0 getattr(psf, yname).value = y_0 getattr(psf, fluxname).value = row['flux_fit'] subbeddata = add_array(subbeddata, -psf(x, y), (y_0, x_0)) return subbeddata ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0196848 photutils-1.3.0/photutils/segmentation/0000755000214200020070000000000000000000000016726 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635873628.0 photutils-1.3.0/photutils/segmentation/__init__.py0000644000214200020070000000057200000000000021043 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage contains tools for detecting sources using image segmentation and measuring their centroids, photometry, and morphological properties. """ from .catalog import * # noqa from .core import * # noqa from .deblend import * # noqa from .detect import * # noqa from .properties import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/_utils.py0000644000214200020070000000507700000000000020610 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides miscellaneous segmentation utilities. """ import numpy as np def mask_to_mirrored_value(data, replace_mask, xycenter, mask=None): """ Replace masked pixels with the value of the pixel mirrored across a given center position. If the mirror pixel is unavailable (i.e., it is outside of the image or masked), then the masked pixel value is set to zero. Parameters ---------- data : `numpy.ndarray`, 2D A 2D array. replace_mask : array-like, bool A boolean mask where `True` values indicate the pixels that should be replaced, if possible, by mirrored pixel values. It must have the same shape as ``data``. xycenter : tuple of two int The (x, y) center coordinates around which masked pixels will be mirrored. mask : array-like, bool A boolean mask where `True` values indicate ``replace_mask`` *mirrored* pixels that should never be used to fix ``replace_mask`` pixels. In other words, if a pixel in ``replace_mask`` has a mirror pixel in this ``mask``, then the mirrored value is set to zero. Using this keyword prevents potential spreading of known non-finite or bad pixel values. Returns ------- result : `numpy.ndarray`, 2D A 2D array with replaced masked pixels. """ outdata = np.copy(data) ymasked, xmasked = np.nonzero(replace_mask) xmirror = 2 * int(xycenter[0] + 0.5) - xmasked ymirror = 2 * int(xycenter[1] + 0.5) - ymasked # Find mirrored pixels that are outside of the image badmask = ((xmirror < 0) | (ymirror < 0) | (xmirror >= data.shape[1]) | (ymirror >= data.shape[0])) # remove them from the set of replace_mask pixels and set them to # zero if np.any(badmask): outdata[ymasked[badmask], xmasked[badmask]] = 0. # remove the badmask pixels from pixels to be replaced goodmask = ~badmask ymasked = ymasked[goodmask] xmasked = xmasked[goodmask] xmirror = xmirror[goodmask] ymirror = ymirror[goodmask] outdata[ymasked, xmasked] = outdata[ymirror, xmirror] # Find mirrored pixels that are masked and replace_mask pixels that are # mirrored to other replace_mask pixels. Set them both to zero. mirror_mask = replace_mask[ymirror, xmirror] if mask is not None: mirror_mask |= mask[ymirror, xmirror] xbad = xmasked[mirror_mask] ybad = ymasked[mirror_mask] outdata[ybad, xbad] = 0.0 return outdata ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636688958.0 photutils-1.3.0/photutils/segmentation/catalog.py0000644000214200020070000031412100000000000020714 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for calculating the properties of sources defined by a segmentation image. """ from copy import deepcopy import functools import inspect import warnings from astropy.stats import SigmaClip from astropy.table import QTable import astropy.units as u from astropy.utils import lazyproperty from astropy.utils.decorators import deprecated import numpy as np from .core import SegmentationImage from ..aperture import (BoundingBox, CircularAperture, EllipticalAperture, RectangularAnnulus) from ..background import SExtractorBackground from ..utils._convolution import _filter_data from ..utils._misc import _get_meta from ..utils._moments import _moments, _moments_central __all__ = ['SourceCatalog'] __doctest_requires__ = {('SourceCatalog', 'SourceCatalog.*'): ['scipy']} # default table columns for `to_table()` output DEFAULT_COLUMNS = ['label', 'xcentroid', 'ycentroid', 'sky_centroid', 'bbox_xmin', 'bbox_xmax', 'bbox_ymin', 'bbox_ymax', 'area', 'semimajor_sigma', 'semiminor_sigma', 'orientation', 'eccentricity', 'min_value', 'max_value', 'local_background', 'segment_flux', 'segment_fluxerr', 'kron_flux', 'kron_fluxerr'] def as_scalar(method): """ Return a scalar value from a method if the class is scalar. """ @functools.wraps(method) def _decorator(*args, **kwargs): result = method(*args, **kwargs) try: return (result[0] if args[0].isscalar and len(result) == 1 else result) except TypeError: # if result has no len return result return _decorator class SourceCatalog: r""" Class to create a catalog of photometry and morphological properties for sources defined by a segmentation image. Parameters ---------- data : 2D `~numpy.ndarray` or `~astropy.units.Quantity`, optional The 2D array from which to calculate the source photometry and properties. If ``kernel`` is input, then a convolved version of ``data`` will be used instead of ``data`` to calculate the source centroid and morphological properties. Source photometry is always measured from ``data``. For accurate source properties and photometry, ``data`` should be background-subtracted. Non-finite ``data`` values (NaN and inf) are automatically masked. segment_img : `~photutils.segmentation.SegmentationImage` A `~photutils.segmentation.SegmentationImage` object defining the sources. error : 2D `~numpy.ndarray` or `~astropy.units.Quantity`, optional The total error array corresponding to the input ``data`` array. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`) . ``error`` must have the same shape as the input ``data``. If ``data`` is a `~astropy.units.Quantity` array then ``error`` must be a `~astropy.units.Quantity` array (and vice versa) with identical units. Non-finite ``error`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. See the Notes section below for details on the error propagation. mask : 2D `~numpy.ndarray` (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. Non-finite values (NaN and inf) in the input ``data`` are automatically masked. kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional The 2D array of the kernel used to filter the data prior to calculating the source centroid and morphological parameters. The kernel should be the same one used in defining the source segments, i.e., the detection image (e.g., see :func:`~photutils.segmentation.detect_sources`). If `None`, then the unfiltered ``data`` will be used instead. background : float, 2D `~numpy.ndarray` or `~astropy.units.Quantity`, optional The background level that was *previously* present in the input ``data``. ``background`` may either be a scalar value or a 2D image with the same shape as the input ``data``. If ``data`` is a `~astropy.units.Quantity` array then ``background`` must be a `~astropy.units.Quantity` array (and vice versa) with identical units. Inputing the ``background`` merely allows for its properties to be measured within each source segment. The input ``background`` does *not* get subtracted from the input ``data``, which should already be background-subtracted. Non-finite ``background`` values (NaN and inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. wcs : WCS object or `None`, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). If `None`, then all sky-based properties will be set to `None`. localbkg_width : int, optional The width of the rectangular annulus used to compute a local background around each source. If 0.0, then no local background subtraction is performed. The local background affects the ``min_value``, ``max_value``, ``segment_flux``, and ``kron_flux`` properties. It does not affect the moment-based morphological properties of the source. apermask_method : {'correct', 'mask', 'none'}, optional The method used to handle neighboring sources when performing aperture photometry (e.g., circular apertures or elliptical Kron apertures). This parameter also affects the Kron radius. * 'correct': replace pixels assigned to neighboring sources by replacing them with pixels on the opposite side of the source center (equivalent to MASK_TYPE=CORRECT in SourceExtractor). * 'mask': mask pixels assigned to neighboring sources (equivalent to MASK_TYPE=BLANK in SourceExtractor). * 'none': do not mask any pixels (equivalent to MASK_TYPE=NONE in SourceExtractor). kron_params : list of 2 floats, optional A list of two parameters used to determine how the Kron radius and flux are calculated. The first item is the scaling parameter of the Kron radius and the second item represents the minimum circular radius. If the Kron radius times sqrt(``semimajor_sigma`` * ``semiminor_sigma``) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. detection_cat : `SourceCatalog`, optional A `SourceCatalog` object for the detection image. The source labels in ``detection_cat`` must correspond to the labels in the input ``segment_img``. If input, this detection catalog will be used to define the source centroids for all aperture-based photometry (e.g., local background aperture, circular aperture, Kron aperture). It will also be used to define the object elliptical shape parameters when calculating the Kron radius. This keyword affects the local-background value, circular aperture photometry, Kron radius, and Kron photometry. Notes ----- ``data`` should be background-subtracted for accurate source photometry and properties. The previously-subtracted background can be passed into this class to calculate properties of the background for each source. `SourceExtractor`_'s centroid and morphological parameters are always calculated from a filtered "detection" image, i.e., the image used to define the segmentation image. The usual downside of the filtering is the sources will be made more circular than they actually are. If you wish to reproduce `SourceExtractor`_ centroid and morphology results, then input a ``kernel``. If ``kernel`` is `None`, then the unfiltered ``data`` will be used for the source centroid and morphological parameters. Negative data values within the source segment are set to zero when calculating morphological properties based on image moments. Negative values could occur, for example, if the segmentation image was defined from a different image (e.g., different bandpass) or if the background was oversubtracted. However, `~photutils.segmentation.SourceCatalog.segment_flux` always includes the contribution of negative ``data`` values. The input ``error`` array is assumed to include *all* sources of error, including the Poisson error of the sources. `~photutils.segmentation.SourceCatalog.segment_fluxerr` is simply the quadrature sum of the pixel-wise total errors over the unmasked pixels within the source segment: .. math:: \Delta F = \sqrt{\sum_{i \in S} \sigma_{\mathrm{tot}, i}^2} where :math:`\Delta F` is `~photutils.segmentation.SourceCatalog.segment_fluxerr`, :math:`S` are the unmasked pixels in the source segment, and :math:`\sigma_{\mathrm{tot}, i}` is the input ``error`` array. Custom errors for source segments can be calculated using the `~photutils.segmentation.SourceCatalog.error_ma` and `~photutils.segmentation.SourceCatalog.background_ma` properties, which are 2D `~numpy.ma.MaskedArray` cutout versions of the input ``error`` and ``background`` arrays. The mask is `True` for pixels outside of the source segment, masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ """ def __init__(self, data, segment_img, *, error=None, mask=None, kernel=None, background=None, wcs=None, localbkg_width=0, apermask_method='correct', kron_params=(2.5, 1.0), detection_cat=None): self._data_unit = None data, error, background = self._process_quantities(data, error, background) self._data = self._validate_array(data, 'data', shape=False) self._segment_img = self._validate_segment_img(segment_img) self._error = self._validate_array(error, 'error') self._mask = self._validate_array(mask, 'mask') self._kernel = kernel self._background = self._validate_array(background, 'background') self._wcs = wcs self._convolved_data = self._convolve_data() self._data_mask = self._make_data_mask() self._localbkg_width = self._validate_localbkg_width(localbkg_width) self._apermask_method = self._validate_apermask_method(apermask_method) self._kron_params = self._validate_kron_params(kron_params) # needed for ordering and isscalar self._labels = self._segment_img.labels self._slices = self._segment_img.slices self.default_columns = DEFAULT_COLUMNS self._extra_properties = [] if detection_cat is not None: if not isinstance(detection_cat, SourceCatalog): raise TypeError('detection_cat must be a SourceCatalog ' 'instance') if not np.array_equal(detection_cat.labels, self.labels): raise ValueError('detection_cat must have same source labels ' 'as the input segment_img') self._detection_cat = detection_cat self.meta = _get_meta() def _process_quantities(self, data, error, background): """ Check units of input arrays. If any of the input arrays have units then they all must have units and the units must be the same. Return unitless ndarrays with the array unit set in self._data_unit. """ inputs = (data, error, background) has_unit = [hasattr(x, 'unit') for x in inputs if x is not None] use_units = all(has_unit) if any(has_unit) and not use_units: raise ValueError('If any of data, error, or background has ' 'units, then they all must all have units.') if use_units: self._data_unit = data.unit data = data.value if error is not None: if error.unit != self._data_unit: raise ValueError('error must have the same units as data') error = error.value if background is not None: if background.unit != self._data_unit: raise ValueError('background must have the same units as ' 'data') background = background.value return data, error, background def _validate_segment_img(self, segment_img): if not isinstance(segment_img, SegmentationImage): raise TypeError('segment_img must be a SegmentationImage') if segment_img.shape != self._data.shape: raise ValueError('segment_img and data must have the same shape.') return segment_img def _validate_array(self, array, name, shape=True): if name == 'mask' and array is np.ma.nomask: array = None if array is not None: array = np.asanyarray(array) if array.ndim != 2: raise ValueError(f'{name} must be a 2D array.') if shape and array.shape != self._data.shape: raise ValueError(f'data and {name} must have the same shape.') return array @staticmethod def _validate_localbkg_width(localbkg_width): if localbkg_width < 0: raise ValueError('localbkg_width must be >= 0') localbkg_width_int = int(localbkg_width) if localbkg_width_int != localbkg_width: raise ValueError('localbkg_width must be an integer') return localbkg_width_int @staticmethod def _validate_apermask_method(apermask_method): if apermask_method not in ('none', 'mask', 'correct'): raise ValueError('Invalid apermask_method value') return apermask_method @staticmethod def _validate_kron_params(kron_params): kron_params = np.atleast_1d(kron_params) if len(kron_params) != 2: raise ValueError('kron_params must have 2 elements') if kron_params[0] <= 0: raise ValueError('kron_params[0] must be > 0') if kron_params[1] <= 0: raise ValueError('kron_params[1] must be > 0') return kron_params @property def _properties(self): """ A list of all class properties, include lazyproperties (even in superclasses). """ def isproperty(obj): return isinstance(obj, property) return [i[0] for i in inspect.getmembers(self.__class__, predicate=isproperty)] @property def properties(self): """ A list of built-in source properties. """ lazyproperties = [name for name in self._lazyproperties if not name.startswith('_')] lazyproperties.remove('isscalar') lazyproperties.remove('nlabels') lazyproperties.extend(['label', 'labels', 'slices']) lazyproperties.sort() return lazyproperties @property def _lazyproperties(self): """ A list of all class lazyproperties (even in superclasses). """ def islazyproperty(obj): return isinstance(obj, lazyproperty) return [i[0] for i in inspect.getmembers(self.__class__, predicate=islazyproperty)] def __getitem__(self, index): if self.isscalar: raise TypeError(f'A scalar {self.__class__.__name__!r} object ' 'cannot be indexed') newcls = object.__new__(self.__class__) # attributes defined in __init__ that are copied directly to the # new class init_attr = ('_data', '_segment_img', '_error', '_mask', '_kernel', '_background', '_wcs', '_data_unit', '_convolved_data', '_data_mask', '_localbkg_width', '_apermask_method', '_kron_params', 'default_columns', '_extra_properties', 'meta') for attr in init_attr: setattr(newcls, attr, getattr(self, attr)) # _labels determines ordering and isscalar attr = '_labels' setattr(newcls, attr, getattr(self, attr)[index]) # need to slice detection_cat, if input attr = '_detection_cat' if getattr(self, attr) is None: setattr(newcls, attr, None) else: setattr(newcls, attr, getattr(self, attr)[index]) attr = '_slices' # Use a numpy object array to allow for fancy and bool indices. # NOTE: None is appended to the list (and then removed) to keep # the array only on the outer level (i.e., prevents recursion). # Otherwise, the tuple of (y, x) slices are not preserved. value = np.array(getattr(self, attr) + [None], dtype=object)[:-1][index] if not newcls.isscalar: value = value.tolist() setattr(newcls, attr, value) # evaluated lazyproperty objects and extra properties keys = (set(self.__dict__.keys()) & (set(self._lazyproperties) | set(self._extra_properties))) for key in keys: value = self.__dict__[key] # do not insert attributes that are always scalar (e.g., # isscalar, nlabels), i.e., not an array/list for each # source if np.isscalar(value): continue try: # keep _ as length-1 iterables if newcls.isscalar and key.startswith('_'): if isinstance(value, np.ndarray): val = value[:, np.newaxis][index] else: val = [value[index]] else: val = value[index] except TypeError: # apply fancy indices (e.g., array/list or bool # mask) to lists val = (np.array(value + [None], dtype=object)[:-1][index]).tolist() newcls.__dict__[key] = val return newcls def __str__(self): cls_name = f'<{self.__class__.__module__}.{self.__class__.__name__}>' with np.printoptions(threshold=25, edgeitems=5): fmt = [f'Length: {self.nlabels}', f'labels: {self.labels}'] return f'{cls_name}\n' + '\n'.join(fmt) def __repr__(self): return self.__str__() def __len__(self): if self.isscalar: raise TypeError(f'Scalar {self.__class__.__name__!r} object has ' 'no len()') return self.nlabels def __iter__(self): for item in range(len(self)): yield self.__getitem__(item) @lazyproperty def isscalar(self): """ Whether the instance is scalar (e.g., a single source). """ return self._labels.shape == () @staticmethod def _has_len(value): if isinstance(value, str): return False try: # NOTE: cannot just check for __len__ attribute, because # it could exist, but raise an Exception for scalar objects len(value) except TypeError: return False return True def copy(self): """ Return a deep copy of this SourceCatalog. """ return deepcopy(self) @property def extra_properties(self): return self._extra_properties def add_extra_property(self, name, value, overwrite=False): """ Add extra properties as attributes. For example, this property ``name`` can then be included in the `to_table` ``columns`` keyword list to output the results in the table. The complete list of user-defined extra properties is stored in the ``extra_properties`` attribute. Parameters ---------- name : str The name of property. The name must not conflict with any of the built-in property names or attributes. value : array-like or float The value to assign. overwrite : bool, option If `True`, will overwrite the existing property ``name``. """ internal_attributes = ((set(self.__dict__.keys()) | set(self._properties)) - set(self.extra_properties)) if name in internal_attributes: raise ValueError(f'{name} cannot be set because it is a ' 'built-in attribute') if not overwrite: if hasattr(self, name): raise ValueError(f'{name} already exists as an attribute. ' 'Set overwrite=True to overwrite an existing ' 'attribute.') if name in self._extra_properties: raise ValueError(f'{name} already exists in the ' '"extra_properties" attribute list.') property_error = False if self.isscalar: # this allows fluxfrac_radius to add len-1 array values for # scalar self if self._has_len(value) and len(value) == 1: value = value[0] if hasattr(value, 'isscalar'): # e.g., Quantity, SkyCoord, Time if not value.isscalar: property_error = True else: if not np.isscalar(value): property_error = True else: if not self._has_len(value) or len(value) != self.nlabels: property_error = True if property_error: raise ValueError('value must have the same number of elements as ' 'the catalog in order to add it as an extra ' 'property.') setattr(self, name, value) if not overwrite: self._extra_properties.append(name) def remove_extra_property(self, name): """ Remove a user-defined extra property. The property must have been defined using `add_extra_property`. The complete list of user-defined extra properties is stored in the ``extra_properties`` attribute. Parameters ---------- name : str The name of the property to remove. """ self.remove_extra_properties(name) def remove_extra_properties(self, names): """ Remove user-defined extra properties. The properties must have been defined using `add_extra_property`. The complete list of user-defined extra properties is stored in the ``extra_properties`` attribute. Parameters ---------- name : list of str The names of the properties to remove. """ names = np.atleast_1d(names) for name in names: if name in self._extra_properties: delattr(self, name) self._extra_properties.remove(name) else: raise ValueError(f'{name} is not a defined extra property.') def rename_extra_property(self, name, new_name): """ Rename an extra property. The renamed property will remain at the same index in the ``extra_properties`` list. Parameters ---------- name : str The old attribute name. new_name : str The new attribute name. """ self.add_extra_property(new_name, getattr(self, name)) idx = self.extra_properties.index(name) self.remove_extra_property(name) # preserve the order of self.extra_properties self.extra_properties.remove(new_name) self.extra_properties.insert(idx, new_name) def _convolve_data(self): """ Convolve the input data with the input kernel. """ if self._kernel is None: return self._data return _filter_data(self._data, self._kernel, mode='constant', fill_value=0.0, check_normalization=True) def _make_data_mask(self): """ Create a mask of non-finite ``data`` values combined with the input ``mask`` array. """ mask = ~np.isfinite(self._data) if self._mask is not None: mask |= self._mask return mask @lazyproperty def _null_object(self): """ Return `None` values. For example, this is used for SkyCoord properties if ``wcs`` is `None`. """ return np.array([None] * self.nlabels) @lazyproperty def _null_value(self): """ Return np.nan values. For example, this is used for background properties if ``background`` is `None`. """ values = np.empty(self.nlabels) values.fill(np.nan) return values @lazyproperty def _cutout_segment_mask(self): """ Boolean mask for source segment. The mask is `True` for all pixels (background and from other source segments) outside of the source segment. """ return [self._segment_img.data[slc] != label for label, slc in zip(self._label_iter, self._slices_iter)] @lazyproperty def _cutout_total_mask(self): """ Boolean mask representing the combination of ``_data_mask`` and ``_cutout_segment_mask``. This mask is applied to ``data``, ``error``, and ``background`` inputs when calculating properties. """ masks = [] for mask, slc in zip(self._cutout_segment_mask, self._slices_iter): masks.append(mask | self._data_mask[slc]) return masks @as_scalar def _make_cutout(self, array, units=True, masked=False): """ Make cutouts from in the input array using the source minimal bounding box. Masks and units are optionally applied. """ cutouts = [array[slc] for slc in self._slices_iter] if units and self._data_unit is not None: cutouts = [(cutout << self._data_unit) for cutout in cutouts] if masked: return [np.ma.masked_array(cutout, mask=mask) for cutout, mask in zip(cutouts, self._cutout_total_mask)] return cutouts @lazyproperty def _cutout_moment_data(self): """ A list of 2D `~numpy.ndarray` cutouts from the (convolved) data The following pixels are set to zero in these arrays: * any masked pixels * invalid values (NaN and inf) * negative data values - negative pixels (especially at large radii) can give image moments that have negative variances. These arrays are used to derive moment-based properties. """ mask = ~np.isfinite(self._convolved_data) | (self._convolved_data < 0) if self._mask is not None: mask |= self._mask cutout = self.convdata if self.isscalar: cutout = (cutout,) cutouts = [] for slc, cutout_, mask_ in zip(self._slices_iter, cutout, self._cutout_segment_mask): try: cutout = cutout_.value.copy() # Quantity array except AttributeError: cutout = cutout_.copy() cutout[(mask[slc] | mask_)] = 0. cutouts.append(cutout) return cutouts def get_label(self, label): """ Return a new `SourceCatalog` object for the input ``label`` only. Parameters ---------- label : int The source label. Returns ------- cat : `SourceCatalog` A new `SourceCatalog` object containing only the source with the input ``label``. """ return self.get_labels(label) def get_labels(self, labels): """ Return a new `SourceCatalog` object for the input ``labels`` only. Parameters ---------- labels : list, tuple, or `~numpy.ndarray` of int The source label(s). Returns ------- cat : `SourceCatalog` A new `SourceCatalog` object containing only the sources with the input ``labels``. """ idx = np.searchsorted(self.label, labels) return self[idx] def to_table(self, columns=None): """ Create a `~astropy.table.QTable` of source properties. Parameters ---------- columns : str, list of str, `None`, optional Names of columns, in order, to include in the output `~astropy.table.QTable`. The allowed column names are any of the `SourceCatalog` properties or custom properties added using `add_extra_property`. If ``columns`` is `None`, then a default list of scalar-valued properties (as defined by the ``default_columns`` attribute) will be used. Returns ------- table : `~astropy.table.QTable` A table of sources properties with one row per source. """ if columns is None: table_columns = self.default_columns else: table_columns = np.atleast_1d(columns) tbl = QTable(meta=self.meta) for column in table_columns: values = getattr(self, column) # column assignment requires an object with a length if self.isscalar: values = (values,) tbl[column] = values return tbl @lazyproperty def nlabels(self): """ The number of source labels. """ if self.isscalar: return 1 return len(self._labels) @property @as_scalar def label(self): """ The source label number(s). This label number corresponds to the assigned pixel value in the `~photutils.segmentation.SegmentationImage`. """ return self._labels @property def labels(self): """ The source label number(s), always as an iterable `~numpy.ndarray`. This label number corresponds to the assigned pixel value in the `~photutils.segmentation.SegmentationImage`. """ return self._label_iter @property def _label_iter(self): """ The source label, always as a iterable. """ _label = self.label if self.isscalar: _label = np.array((_label,)) return _label @property @as_scalar def slices(self): """ A tuple of slice objects defining the minimal bounding box of the source. """ return self._slices @lazyproperty def _slices_iter(self): """ A tuple of slice objects defining the minimal bounding box of the source, always as an iterable. """ _slices = self.slices if self.isscalar: _slices = (_slices,) return _slices @lazyproperty @as_scalar def segment(self): """ A 2D `~numpy.ndarray` cutout of the segmentation image using the minimal bounding box of the source. """ return self._make_cutout(self._segment_img.data, units=False, masked=False) @lazyproperty @as_scalar def segment_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout of the segmentation image using the minimal bounding box of the source. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). """ return self._make_cutout(self._segment_img.data, units=False, masked=True) @lazyproperty def data(self): """ A 2D `~numpy.ndarray` cutout from the data using the minimal bounding box of the source. """ return self._make_cutout(self._data, units=True, masked=False) @lazyproperty def data_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the data using the minimal bounding box of the source. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). """ return self._make_cutout(self._data, units=False, masked=True) @lazyproperty def convdata(self): """ A 2D `~numpy.ndarray` cutout from the convolved data using the minimal bounding box of the source. """ return self._make_cutout(self._convolved_data, units=True, masked=False) @lazyproperty def convdata_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the convolved data using the minimal bounding box of the source. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). """ return self._make_cutout(self._convolved_data, units=False, masked=True) @lazyproperty @as_scalar def error(self): """ A 2D `~numpy.ndarray` cutout from the error array using the minimal bounding box of the source. """ if self._error is None: return self._null_object return self._make_cutout(self._error, units=True, masked=False) @lazyproperty @as_scalar def error_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the error array using the minimal bounding box of the source. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). """ if self._error is None: return self._null_object return self._make_cutout(self._error, units=False, masked=True) @lazyproperty @as_scalar def background(self): """ A 2D `~numpy.ndarray` cutout from the background array using the minimal bounding box of the source. """ if self._background is None: return self._null_object return self._make_cutout(self._background, units=True, masked=False) @lazyproperty @as_scalar def background_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the background array. using the minimal bounding box of the source. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and inf). """ if self._background is None: return self._null_object return self._make_cutout(self._background, units=False, masked=True) @lazyproperty def _all_masked(self): """ True if all pixels over the source segment are masked. """ return np.array([np.all(mask) for mask in self._cutout_total_mask]) def _get_values(self, array): """ Get a 1D array of unmasked values from the input array within the source segment. An array with a single NaN is returned for completely-masked sources. """ if self.isscalar: array = (array,) return [arr.compressed() if len(arr.compressed()) > 0 else np.array([np.nan]) for arr in array] @lazyproperty def _data_values(self): """ A 1D array of unmasked data values. An array with a single NaN is returned for completely-masked sources. """ return self._get_values(self.data_ma) @lazyproperty def _error_values(self): """ A 1D array of unmasked error values. An array with a single NaN is returned for completely-masked sources. """ return self._get_values(self.error_ma) @lazyproperty def _background_values(self): """ A 1D array of unmasked background values. An array with a single NaN is returned for completely-masked sources. """ return self._get_values(self.background_ma) @lazyproperty @as_scalar def moments(self): """ Spatial moments up to 3rd order of the source. """ return np.array([_moments(arr, order=3) for arr in self._cutout_moment_data]) @lazyproperty @as_scalar def moments_central(self): """ Central moments (translation invariant) of the source up to 3rd order. """ cutout_centroid = self.cutout_centroid if self.isscalar: cutout_centroid = cutout_centroid[np.newaxis, :] return np.array([_moments_central(arr, center=(xcen_, ycen_), order=3) for arr, xcen_, ycen_ in zip(self._cutout_moment_data, cutout_centroid[:, 0], cutout_centroid[:, 1])]) @lazyproperty @as_scalar def cutout_centroid(self): """ The ``(x, y)`` coordinate, relative to the cutout data, of the centroid within the source segment. """ moments = self.moments if self.isscalar: moments = moments[np.newaxis, :] # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) ycentroid = moments[:, 1, 0] / moments[:, 0, 0] xcentroid = moments[:, 0, 1] / moments[:, 0, 0] return np.transpose((xcentroid, ycentroid)) @lazyproperty @as_scalar def centroid(self): """ The ``(x, y)`` coordinate of the centroid within the source segment. """ origin = np.transpose((self.bbox_xmin, self.bbox_ymin)) return self.cutout_centroid + origin @lazyproperty def _xcentroid(self): """ The ``x`` coordinate of the centroid within the source segment, always as an iterable. """ xcentroid = np.transpose(self.centroid)[0] if self.isscalar: xcentroid = (xcentroid,) return xcentroid @lazyproperty @as_scalar def xcentroid(self): """ The ``x`` coordinate of the centroid within the source segment. """ return self._xcentroid @lazyproperty def _ycentroid(self): """ The ``y`` coordinate of the centroid within the source segment, always as an iterable. """ ycentroid = np.transpose(self.centroid)[1] if self.isscalar: ycentroid = (ycentroid,) return ycentroid @lazyproperty @as_scalar def ycentroid(self): """ The ``y`` coordinate of the centroid within the source segment. """ return self._ycentroid @lazyproperty @as_scalar def sky_centroid(self): """ The sky coordinate of the centroid within the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The output coordinate frame is the same as the input ``wcs``. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self._wcs.pixel_to_world(self.xcentroid, self.ycentroid) @lazyproperty @as_scalar def sky_centroid_icrs(self): """ The sky coordinate in the International Celestial Reference System (ICRS) frame of the centroid within the source segment, returned as a `~astropy.coordinates.SkyCoord` object. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self.sky_centroid.icrs @lazyproperty def _bbox(self): """ The `~photutils.aperture.BoundingBox` of the minimal rectangular region containing the source segment, always as an iterable. """ return [BoundingBox(ixmin=slc[1].start, ixmax=slc[1].stop, iymin=slc[0].start, iymax=slc[0].stop) for slc in self._slices_iter] @lazyproperty @as_scalar def bbox(self): """ The `~photutils.aperture.BoundingBox` of the minimal rectangular region containing the source segment. """ return self._bbox @lazyproperty @as_scalar def bbox_xmin(self): """ The minimum ``x`` pixel index within the minimal bounding box containing the source segment. """ return np.array([slc[1].start for slc in self._slices_iter]) @lazyproperty @as_scalar def bbox_xmax(self): """ The maximum ``x`` pixel index within the minimal bounding box containing the source segment. Note that this value is inclusive, unlike numpy slice indices. """ return np.array([slc[1].stop - 1 for slc in self._slices_iter]) @lazyproperty @as_scalar def bbox_ymin(self): """ The minimum ``y`` pixel index within the minimal bounding box containing the source segment. """ return np.array([slc[0].start for slc in self._slices_iter]) @lazyproperty @as_scalar def bbox_ymax(self): """ The maximum ``y`` pixel index within the minimal bounding box containing the source segment. Note that this value is inclusive, unlike numpy slice indices. """ return np.array([slc[0].stop - 1 for slc in self._slices_iter]) @lazyproperty def _bbox_corner_ll(self): """ Lower-left *outside* pixel corner location (not index). """ xypos = [] for bbox_ in self._bbox: xypos.append((bbox_.ixmin - 0.5, bbox_.iymin - 0.5)) return np.array(xypos) @lazyproperty def _bbox_corner_ul(self): """ Upper-left *outside* pixel corner location (not index). """ xypos = [] for bbox_ in self._bbox: xypos.append((bbox_.ixmin - 0.5, bbox_.iymax + 0.5)) return np.array(xypos) @lazyproperty def _bbox_corner_lr(self): """ Lower-right *outside* pixel corner location (not index). """ xypos = [] for bbox_ in self._bbox: xypos.append((bbox_.ixmax + 0.5, bbox_.iymin - 0.5)) return np.array(xypos) @lazyproperty def _bbox_corner_ur(self): """ Upper-right *outside* pixel corner location (not index). """ xypos = [] for bbox_ in self._bbox: xypos.append((bbox_.ixmax + 0.5, bbox_.iymax + 0.5)) return np.array(xypos) @lazyproperty @as_scalar def sky_bbox_ll(self): """ The sky coordinates of the lower-left corner vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*, not their centers. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self._wcs.pixel_to_world(*np.transpose(self._bbox_corner_ll)) @lazyproperty @as_scalar def sky_bbox_ul(self): """ The sky coordinates of the upper-left corner vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*, not their centers. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self._wcs.pixel_to_world(*np.transpose(self._bbox_corner_ul)) @lazyproperty @as_scalar def sky_bbox_lr(self): """ The sky coordinates of the lower-right corner vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*, not their centers. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self._wcs.pixel_to_world(*np.transpose(self._bbox_corner_lr)) @lazyproperty @as_scalar def sky_bbox_ur(self): """ The sky coordinates of the upper-right corner vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*, not their centers. `None` if ``wcs`` is not input. """ if self._wcs is None: return self._null_object return self._wcs.pixel_to_world(*np.transpose(self._bbox_corner_ur)) @lazyproperty @as_scalar def min_value(self): """ The minimum pixel value of the ``data`` within the source segment. """ values = np.array([np.min(array) for array in self._data_values]) values -= self._local_background if self._data_unit is not None: values <<= self._data_unit return values @lazyproperty @as_scalar def max_value(self): """ The maximum pixel value of the ``data`` within the source segment. """ values = np.array([np.max(array) for array in self._data_values]) values -= self._local_background if self._data_unit is not None: values <<= self._data_unit return values @lazyproperty @as_scalar def cutout_minval_index(self): """ The ``(y, x)`` coordinate, relative to the cutout data, of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ data = self.data_ma if self.isscalar: data = (data,) idx = [] for arr in data: if np.all(arr.mask): idx.append((np.nan, np.nan)) else: idx.append(np.unravel_index(np.argmin(arr), arr.shape)) return np.array(idx) @lazyproperty @as_scalar def cutout_maxval_index(self): """ The ``(y, x)`` coordinate, relative to the cutout data, of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ data = self.data_ma if self.isscalar: data = (data,) idx = [] for arr in data: if np.all(arr.mask): idx.append((np.nan, np.nan)) else: idx.append(np.unravel_index(np.argmax(arr), arr.shape)) return np.array(idx) @lazyproperty @as_scalar def minval_index(self): """ The ``(y, x)`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ index = self.cutout_minval_index if self.isscalar: index = (index,) out = [] for idx, slc in zip(index, self._slices_iter): out.append((idx[0] + slc[0].start, idx[1] + slc[1].start)) return np.array(out) @lazyproperty @as_scalar def maxval_index(self): """ The ``(y, x)`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ index = self.cutout_maxval_index if self.isscalar: index = (index,) out = [] for idx, slc in zip(index, self._slices_iter): out.append((idx[0] + slc[0].start, idx[1] + slc[1].start)) return np.array(out) @lazyproperty @as_scalar def minval_xindex(self): """ The ``x`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ return np.transpose(self.minval_index)[1] @lazyproperty @as_scalar def minval_yindex(self): """ The ``y`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ return np.transpose(self.minval_index)[0] @lazyproperty @as_scalar def maxval_xindex(self): """ The ``x`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ return np.transpose(self.maxval_index)[1] @lazyproperty @as_scalar def maxval_yindex(self): """ The ``y`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ return np.transpose(self.maxval_index)[0] @lazyproperty @as_scalar def segment_flux(self): r""" The sum of the unmasked ``data`` values within the source segment. .. math:: F = \sum_{i \in S} I_i where :math:`F` is ``segment_flux``, :math:`I_i` is the background-subtracted ``data``, and :math:`S` are the unmasked pixels in the source segment. Non-finite pixel values (NaN and inf) are excluded (automatically masked). """ localbkg = self._local_background if self.isscalar: localbkg = localbkg[0] source_sum = np.array([np.sum(arr) for arr in self._data_values]) source_sum -= self.area.value * localbkg if self._data_unit is not None: source_sum <<= self._data_unit return source_sum @lazyproperty @as_scalar def segment_fluxerr(self): r""" The uncertainty of `segment_flux` , propagated from the input ``error`` array. ``segment_fluxerr`` is the quadrature sum of the total errors over the unmasked pixels within the source segment: .. math:: \Delta F = \sqrt{\sum_{i \in S} \sigma_{\mathrm{tot}, i}^2} where :math:`\Delta F` is the `segment_flux`, :math:`\sigma_{\mathrm{tot, i}}` are the pixel-wise total errors (``error``), and :math:`S` are the unmasked pixels in the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and inf) that are automatically masked, are also masked in the error array. """ if self._error is None: err = self._null_value else: err = np.sqrt(np.array([np.sum(arr**2) for arr in self._error_values])) if self._data_unit is not None: err <<= self._data_unit return err @lazyproperty @as_scalar def background_sum(self): """ The sum of ``background`` values within the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and inf) that are automatically masked, are also masked in the background array. """ if self._background is None: bkg_sum = self._null_value else: bkg_sum = np.array([np.sum(arr) for arr in self._background_values]) if self._data_unit is not None: bkg_sum <<= self._data_unit return bkg_sum @lazyproperty @as_scalar def background_mean(self): """ The mean of ``background`` values within the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and inf) that are automatically masked, are also masked in the background array. """ if self._background is None: bkg_mean = self._null_value else: bkg_mean = np.array([np.mean(arr) for arr in self._background_values]) if self._data_unit is not None: bkg_mean <<= self._data_unit return bkg_mean @lazyproperty @as_scalar def background_centroid(self): """ The value of the ``background`` at the position of the source centroid. The background value at fractional position values are determined using bilinear interpolation. """ if self._background is None: bkg = self._null_value else: from scipy.ndimage import map_coordinates xcen = self._xcentroid ycen = self._ycentroid bkg = map_coordinates(self._background, (xcen, ycen), order=1, mode='nearest') mask = np.isfinite(xcen) & np.isfinite(ycen) bkg[~mask] = np.nan if self._data_unit is not None: bkg <<= self._data_unit return bkg @lazyproperty @as_scalar def area(self): """ The total unmasked area of the source segment in units of pixels**2. Note that the source area may be smaller than its segment area if a mask is input to `SourceCatalog` or if the ``data`` within the segment contains invalid values (NaN and inf). """ areas = np.array([arr.size for arr in self._data_values]).astype(float) areas[self._all_masked] = np.nan return areas << (u.pix ** 2) @lazyproperty @as_scalar def equivalent_radius(self): """ The radius of a circle with the same `area` as the source segment. """ return np.sqrt(self.area / np.pi) @lazyproperty @as_scalar def perimeter(self): """ The perimeter of the source segment, approximated as the total length of lines connecting the centers of the border pixels defined by a 4-pixel connectivity. If any masked pixels make holes within the source segment, then the perimeter around the inner hole (e.g., an annulus) will also contribute to the total perimeter. References ---------- .. [1] K. Benkrid, D. Crookes, and A. Benkrid. "Design and FPGA Implementation of a Perimeter Estimator". Proceedings of the Irish Machine Vision and Image Processing Conference, pp. 51-57 (2000). http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc """ from scipy.ndimage import binary_erosion, convolve selem = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) kernel = np.array([[10, 2, 10], [2, 1, 2], [10, 2, 10]]) size = 34 weights = np.zeros(size, dtype=float) weights[[5, 7, 15, 17, 25, 27]] = 1. weights[[21, 33]] = np.sqrt(2.) weights[[13, 23]] = (1 + np.sqrt(2.)) / 2. perimeter = [] for mask in self._cutout_total_mask: if np.all(mask): perimeter.append(np.nan) continue data = ~mask data_eroded = binary_erosion(data, selem, border_value=0) border = np.logical_xor(data, data_eroded).astype(int) perimeter_data = convolve(border, kernel, mode='constant', cval=0) perimeter_hist = np.bincount(perimeter_data.ravel(), minlength=size) perimeter.append(perimeter_hist[0:size] @ weights) return np.array(perimeter) * u.pix @lazyproperty @as_scalar def inertia_tensor(self): """ The inertia tensor of the source for the rotation around its center of mass. """ moments = self.moments_central if self.isscalar: moments = moments[np.newaxis, :] mu_02 = moments[:, 0, 2] mu_11 = -moments[:, 1, 1] mu_20 = moments[:, 2, 0] tensor = np.array([mu_02, mu_11, mu_11, mu_20]).swapaxes(0, 1) return tensor.reshape((tensor.shape[0], 2, 2)) * u.pix**2 @lazyproperty def _covariance(self): """ The covariance matrix of the 2D Gaussian function that has the same second-order moments as the source, always as an iterable. """ moments = self.moments_central if self.isscalar: moments = moments[np.newaxis, :] # ignore divide-by-zero RuntimeWarning with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) mu_norm = moments / moments[:, 0, 0][:, np.newaxis, np.newaxis] covar = np.array([mu_norm[:, 0, 2], mu_norm[:, 1, 1], mu_norm[:, 1, 1], mu_norm[:, 2, 0]]).swapaxes(0, 1) covar = covar.reshape((covar.shape[0], 2, 2)) # Modify the covariance matrix in the case of "infinitely" thin # detections. This follows SourceExtractor's prescription of # incrementally increasing the diagonal elements by 1/12. delta = 1. / 12 delta2 = delta**2 # ignore RuntimeWarning from NaN values in covar with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) covar_det = np.linalg.det(covar) idx = np.where(covar_det < delta2)[0] while idx.size > 0: # pragma: no cover covar[idx, 0, 0] += delta covar[idx, 1, 1] += delta covar_det = np.linalg.det(covar) idx = np.where(covar_det < delta2)[0] return covar @lazyproperty @as_scalar def covariance(self): """ The covariance matrix of the 2D Gaussian function that has the same second-order moments as the source. """ return self._covariance * (u.pix**2) @lazyproperty @as_scalar def covariance_eigvals(self): """ The two eigenvalues of the `covariance` matrix in decreasing order. """ eigvals = np.empty((self.nlabels, 2)) eigvals.fill(np.nan) # np.linalg.eivals requires finite input values idx = np.unique(np.where(np.isfinite(self._covariance))[0]) eigvals[idx] = np.linalg.eigvals(self._covariance[idx]) # check for negative variance # (just in case covariance matrix is not positive (semi)definite) idx2 = np.unique(np.where(eigvals < 0)[0]) # pragma: no cover eigvals[idx2] = (np.nan, np.nan) # pragma: no cover # sort each eigenvalue pair in descending order eigvals.sort(axis=1) eigvals = np.fliplr(eigvals) return eigvals * u.pix**2 @lazyproperty @as_scalar def semimajor_sigma(self): """ The 1-sigma standard deviation along the semimajor axis of the 2D Gaussian function that has the same second-order central moments as the source. """ eigvals = self.covariance_eigvals if self.isscalar: eigvals = eigvals[np.newaxis, :] # this matches SourceExtractor's A parameter return np.sqrt(eigvals[:, 0]) @lazyproperty @as_scalar def semiminor_sigma(self): """ The 1-sigma standard deviation along the semiminor axis of the 2D Gaussian function that has the same second-order central moments as the source. """ eigvals = self.covariance_eigvals if self.isscalar: eigvals = eigvals[np.newaxis, :] # this matches SourceExtractor's A parameter return np.sqrt(eigvals[:, 1]) @lazyproperty @as_scalar def fwhm(self): r""" The circularized full width at half maximum (FWHM) of the 2D Gaussian function that has the same second-order central moments as the source. .. math:: \mathrm{FWHM} & = 2 \sqrt{2 \ln(2)} \sqrt{0.5 (a^2 + b^2)} \\ & = 2 \sqrt{\ln(2) \ (a^2 + b^2)} where :math:`a` and :math:`b` are the 1-sigma lengths of the semimajor (`semimajor_sigma`) and semiminor (`semiminor_sigma`) axes, respectively. """ return 2.0 * np.sqrt(np.log(2.0) * (self.semimajor_sigma**2 + self.semiminor_sigma**2)) @lazyproperty @as_scalar def orientation(self): """ The angle between the ``x`` axis and the major axis of the 2D Gaussian function that has the same second-order moments as the source. The angle increases in the counter-clockwise direction. """ covar = self._covariance orient_radians = 0.5 * np.arctan2(2. * covar[:, 0, 1], (covar[:, 0, 0] - covar[:, 1, 1])) return orient_radians * 180. / np.pi * u.deg @lazyproperty @as_scalar def eccentricity(self): r""" The eccentricity of the 2D Gaussian function that has the same second-order moments as the source. The eccentricity is the fraction of the distance along the semimajor axis at which the focus lies. .. math:: e = \sqrt{1 - \frac{b^2}{a^2}} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. """ semimajor_var, semiminor_var = np.transpose(self.covariance_eigvals) return np.sqrt(1. - (semiminor_var / semimajor_var)) @lazyproperty @as_scalar def elongation(self): r""" The ratio of the lengths of the semimajor and semiminor axes: .. math:: \mathrm{elongation} = \frac{a}{b} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. """ return self.semimajor_sigma / self.semiminor_sigma @lazyproperty @as_scalar def ellipticity(self): r""" 1.0 minus the ratio of the lengths of the semimajor and semiminor axes (or 1.0 minus the `elongation`): .. math:: \mathrm{ellipticity} = 1 - \frac{b}{a} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. """ return 1.0 - (self.semiminor_sigma / self.semimajor_sigma) @lazyproperty @as_scalar def covar_sigx2(self): r""" The ``(0, 0)`` element of the `covariance` matrix, representing :math:`\sigma_x^2`, in units of pixel**2. """ return self._covariance[:, 0, 0] * u.pix**2 @lazyproperty @as_scalar def covar_sigy2(self): r""" The ``(1, 1)`` element of the `covariance` matrix, representing :math:`\sigma_y^2`, in units of pixel**2. """ return self._covariance[:, 1, 1] * u.pix**2 @lazyproperty @as_scalar def covar_sigxy(self): r""" The ``(0, 1)`` and ``(1, 0)`` elements of the `covariance` matrix, representing :math:`\sigma_x \sigma_y`, in units of pixel**2. """ return self._covariance[:, 0, 1] * u.pix**2 @lazyproperty @as_scalar def cxx(self): r""" `SourceExtractor`_'s CXX ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return ((np.cos(self.orientation) / self.semimajor_sigma)**2 + (np.sin(self.orientation) / self.semiminor_sigma)**2) @lazyproperty @as_scalar def cyy(self): r""" `SourceExtractor`_'s CYY ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return ((np.sin(self.orientation) / self.semimajor_sigma)**2 + (np.cos(self.orientation) / self.semiminor_sigma)**2) @lazyproperty @as_scalar def cxy(self): r""" `SourceExtractor`_'s CXY ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return (2. * np.cos(self.orientation) * np.sin(self.orientation) * ((1. / self.semimajor_sigma**2) - (1. / self.semiminor_sigma**2))) @lazyproperty @as_scalar def gini(self): r""" The `Gini coefficient `_ of the source. The Gini coefficient is calculated using the prescription from `Lotz et al. 2004 `_ as: .. math:: G = \frac{1}{\left | \bar{x} \right | n (n - 1)} \sum^{n}_{i} (2i - n - 1) \left | x_i \right | where :math:`\bar{x}` is the mean over pixel values :math:`x_i` within the source segment. The Gini coefficient is a way of measuring the inequality in a given set of values. In the context of galaxy morphology, it measures how the light of a galaxy image is distributed among its pixels. A Gini coefficient value of 0 corresponds to a galaxy image with the light evenly distributed over all pixels while a Gini coefficient value of 1 represents a galaxy image with all its light concentrated in just one pixel. """ gini = [] for arr in self._data_values: if np.all(np.isnan(arr)): gini.append(np.nan) continue npix = np.size(arr) normalization = np.abs(np.mean(arr)) * npix * (npix - 1) kernel = ((2. * np.arange(1, npix + 1) - npix - 1) * np.abs(np.sort(arr))) gini.append(np.sum(kernel) / normalization) return np.array(gini) @lazyproperty @as_scalar def local_background_aperture(self): """ The `~photutils.aperture.RectangularAnnulus` aperture used to estimate the local background. """ if self._detection_cat is not None: # local background aperture defined using the source # centroid and bbox defined by detection image return self._detection_cat.local_background_aperture if self._localbkg_width == 0: return self._null_object aperture = [] for bbox_ in self._bbox: xpos = 0.5 * (bbox_.ixmin + bbox_.ixmax - 1) ypos = 0.5 * (bbox_.iymin + bbox_.iymax - 1) scale = 1.5 width_bbox = bbox_.ixmax - bbox_.ixmin width_in = width_bbox * scale width_out = width_in + 2 * self._localbkg_width height_bbox = bbox_.iymax - bbox_.iymin height_in = height_bbox * scale height_out = height_in + 2 * self._localbkg_width aperture.append(RectangularAnnulus((xpos, ypos), width_in, width_out, height_out, height_in, theta=0.)) return aperture @lazyproperty def _local_background(self): """ The local background value estimated using a rectangular annulus aperture around the source. This property is always an `~numpy.ndarray` without units. """ if self._localbkg_width == 0: bkg = np.zeros(self.nlabels) else: mask = self._data_mask | self._segment_img.data.astype(bool) sigma_clip = SigmaClip(sigma=3.0, cenfunc='median', maxiters=20) bkg_func = SExtractorBackground(sigma_clip) bkg_aper = self.local_background_aperture if self.isscalar: bkg_aper = (bkg_aper,) bkg = [] for aperture in bkg_aper: aperture_mask = aperture.to_mask(method='center') values = aperture_mask.get_values(self._data, mask=mask) # check not enough unmasked pixels if len(values) < 10: # pragma: no cover bkg.append(0.) continue bkg.append(bkg_func(values)) bkg = np.array(bkg) bkg[self._all_masked] = np.nan return bkg @lazyproperty @as_scalar def local_background(self): """ The local background value estimated using a rectangular annulus aperture around the source. """ bkg = self._local_background if self._data_unit is not None: bkg <<= self._data_unit return bkg def _make_aperture_data(self, label, xcentroid, ycentroid, aperture_bbox, local_background, make_error=True): """ Make cutouts of data, error, and mask arrays for aperture photometry (e.g., circular or Kron). Neighboring sources can be included, masked, or corrected based on the ``apermask_method`` keyword. """ # make cutouts of the data based on the aperture bbox slc_lg, slc_sm = aperture_bbox.get_overlap_slices(self._data.shape) data = self._data[slc_lg] - local_background data_mask = self._data_mask[slc_lg] if make_error and self._error is not None: error = self._error[slc_lg] else: error = None # calculate cutout centroid position cutout_xycen = (xcentroid - max(0, aperture_bbox.ixmin), ycentroid - max(0, aperture_bbox.iymin)) # mask or correct neighboring sources if self._apermask_method != 'none': segment_img = self._segment_img.data[slc_lg] segm_mask = np.logical_and(segment_img != label, segment_img != 0) if self._apermask_method == 'mask': mask = data_mask | segm_mask else: mask = data_mask if self._apermask_method == 'correct': from ._utils import mask_to_mirrored_value data = mask_to_mirrored_value(data, segm_mask, cutout_xycen, mask=mask) if error is not None: error = mask_to_mirrored_value(error, segm_mask, cutout_xycen, mask=mask) return data, error, mask, cutout_xycen, slc_sm def make_circular_apertures(self, radius): """ Return a list of circular apertures with the specified radius centered at the source centroid position. If provided, the `SourceCatalog` ``detection_cat`` will be used for the source centroids. Parameters ---------- radius : float The radius of the circle in pixels. Returns ------- result : list of `~photutils.aperture.CircularAperture` A list of `~photutils.aperture.CircularAperture` instances. The aperture will be `None` where the source centroid position is not finite or where the source is completely masked. """ if self._detection_cat is not None: # use detection catalog for centroids detcat = self._detection_cat else: detcat = self if radius <= 0: raise ValueError('radius must be > 0') apertures = [] for (xcen, ycen, all_masked) in zip(detcat._xcentroid, detcat._ycentroid, self._all_masked): if all_masked or np.any(~np.isfinite((xcen, ycen))): apertures.append(None) continue apertures.append(CircularAperture((xcen, ycen), r=radius)) return apertures @as_scalar def plot_circular_apertures(self, radius, axes=None, origin=(0, 0), **kwargs): """ Plot circular apertures on a matplotlib `~matplotlib.axes.Axes` instance. The apertures are defined by the specified radius and are centered at the source centroid position. If provided, the `SourceCatalog` ``detection_cat`` will be used for the source centroids. Parameters ---------- radius : float The radius of the circle in pixels. axes : `matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : list of `~matplotlib.patches.Patch` A list of matplotlib patches for the plotted aperture. The patches can be used, for example, when adding a plot legend. """ apertures = self.make_circular_apertures(radius) patches = [] for aperture in apertures: if aperture is not None: aperture.plot(axes=axes, origin=origin, **kwargs) patches.append(aperture._to_patch(origin=origin, **kwargs)) return patches @deprecated('1.1', alternative='make_circular_apertures') def circular_aperture(self, radius): """ Return a list of circular apertures with the specified radius centered at the source centroid position. Parameters ---------- radius : float The radius of the circle in pixels. Returns ------- result : list of `~photutils.aperture.CircularAperture` A list of `~photutils.aperture.CircularAperture` instances. The aperture will be `None` where the source centroid position is not finite or where the source is completely masked. """ return self.make_circular_apertures(radius) def circular_photometry(self, radius, name=None, overwrite=False): """ Perform aperture photometry for each source using a circular aperture of the specified radius centered at the source centroid position. See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. Parameters ---------- radius : float The radius of the circle in pixels. name : str or `None`, optional The prefix name which will be used to define attribute names for the flux and flux error. The attribute names ``[name]_flux`` and ``[name]_fluxerr`` will store the photometry results. For example, these names can then be included in the `to_table` ``columns`` keyword list to output the results in the table. overwrite : bool, optional If True, overwrite the attribute ``name`` if it exists. Returns ------- flux, fluxerr : `~numpy.ndarray` of floats, floats, or `~astropy.units.Quantity` The aperture fluxes and flux errors. NaN will be returned where the circular aperture is `None` (e.g., where the source centroid position is not finite). """ if radius <= 0: raise ValueError('radius must be > 0') if self._detection_cat is not None: # use source centroid defined by detection image detcat = self._detection_cat else: detcat = self apertures = self.make_circular_apertures(radius) flux = [] fluxerr = [] for (label, aperture, xcen, ycen, bkg) in zip( self.labels, apertures, detcat._xcentroid, detcat._ycentroid, self._local_background): if aperture is None: flux.append(np.nan) fluxerr.append(np.nan) continue aperture_mask = aperture.to_mask(method='exact') data, error, mask, _, slc_sm = self._make_aperture_data( label, xcen, ycen, aperture_mask.bbox, bkg) aperture_weights = aperture_mask.data[slc_sm] pixel_mask = (aperture_weights > 0) & ~mask # good pixels # ignore RuntimeWarning for invalid data or error values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) flux.append(np.sum((aperture_weights * data)[pixel_mask])) if error is None: fluxerr.append(np.nan) else: fluxerr.append(np.sqrt(np.sum( (aperture_weights * error**2)[pixel_mask]))) flux = np.array(flux) fluxerr = np.array(fluxerr) if self._data_unit is not None: flux <<= self._data_unit fluxerr <<= self._data_unit if self.isscalar: flux = flux[0] fluxerr = fluxerr[0] if name is not None: flux_name = f'{name}_flux' fluxerr_name = f'{name}_fluxerr' self.add_extra_property(flux_name, flux, overwrite=overwrite) self.add_extra_property(fluxerr_name, fluxerr, overwrite=overwrite) return flux, fluxerr def _make_elliptical_apertures(self, scale=6.): """ Return a list of elliptical apertures based on the scaled isophotal shape of the sources. If provided, the `SourceCatalog` ``detection_cat`` will be used for the source centroids. Parameters ---------- scale : float or `~numpy.ndarray`, optional The scale factor to apply to the ellipse major and minor axes. The default value of 6.0 is roughly two times the isophotal extent of the source. A `~numpy.ndarray` input must be a 1D array of length ``nlabels``. Returns ------- result : list of `~photutils.aperture.EllipticalAperture` A list of `~photutils.aperture.EllipticalAperture` instances. The aperture will be `None` where the source centroid position or elliptical shape parameters are not finite or where the source is completely masked. """ if self._detection_cat is not None: # use detection catalog for centroids and elliptical shape # parameters detcat = self._detection_cat else: detcat = self xcen = detcat._xcentroid ycen = detcat._ycentroid major_size = detcat.semimajor_sigma.value * scale minor_size = detcat.semiminor_sigma.value * scale theta = detcat.orientation.to(u.radian).value if self.isscalar: major_size = (major_size,) minor_size = (minor_size,) theta = (theta,) aperture = [] for values in zip(xcen, ycen, major_size, minor_size, theta, self._all_masked): if values[-1] or np.any(~np.isfinite(values[:-1])): aperture.append(None) continue (xcen_, ycen_, major_, minor_, theta_) = values[:-1] aperture.append(EllipticalAperture((xcen_, ycen_), major_, minor_, theta=theta_)) return aperture @lazyproperty @as_scalar def kron_radius(self): r""" The *unscaled* first-moment Kron radius. The *unscaled* first-moment Kron radius is given by: .. math:: k_r = \frac{\sum_{i \in A} \ r_i I_i}{\sum_{i \in A} I_i} where :math:`I_i` are the data values and the sum is over pixels in an elliptical aperture whose axes are defined by six times the semimajor (`semimajor_sigma`) and semiminor axes (`semiminor_sigma`) at the calculated `orientation` (all properties derived from the central image moments of the source). :math:`r_i` is the elliptical "radius" to the pixel given by: .. math:: r_i^2 = cxx (x_i - \bar{x})^2 + cxy (x_i - \bar{x})(y_i - \bar{y}) + cyy (y_i - \bar{y})^2 where :math:`\bar{x}` and :math:`\bar{y}` represent the source centroid and the coefficients are based on image moments (`cxx`, `cxy`, and `cyy`). The scaling parameter of the `kron_radius` is defined using the `SourceCatalog` ``kron_params`` keyword. See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. If either the numerator or denominator above is less than or equal to 0, then ``np.nan`` will be returned for both the Kron radius and Kron flux. If the source is completely masked, then ``np.nan`` will be returned for both the Kron radius and Kron flux. If the `SourceCatalog` ``detection_cat`` was provided, then its ``kron_radius`` will be returned if the source is not completely masked. """ if self._detection_cat is not None: kron_radius = self._detection_cat.kron_radius if self.isscalar: kron_radius = np.atleast_1d(kron_radius) kron_radius[self._all_masked] = np.nan return kron_radius labels = self._label_iter apertures = self._make_elliptical_apertures(scale=6.0) xcen = self._xcentroid ycen = self._ycentroid cxx = self.cxx.value cxy = self.cxy.value cyy = self.cyy.value if self.isscalar: cxx = (cxx,) cxy = (cxy,) cyy = (cyy,) kron_radius = [] for (label, aperture, xcen_, ycen_, cxx_, cxy_, cyy_) in zip( labels, apertures, xcen, ycen, cxx, cxy, cyy): if aperture is None: kron_radius.append(np.nan) continue # use 'center' (whole pixels) to compute Kron radius aperture_mask = aperture.to_mask(method='center') # prepare cutouts of the data based on the aperture size # local background explicitly set to zero for SE agreement data, _, mask, xycen, slc_sm = self._make_aperture_data( label, xcen_, ycen_, aperture_mask.bbox, 0.0, make_error=False) xval = np.arange(data.shape[1]) - xycen[0] yval = np.arange(data.shape[0]) - xycen[1] xx, yy = np.meshgrid(xval, yval) rr = np.sqrt(cxx_ * xx**2 + cxy_ * xx * yy + cyy_ * yy**2) aperture_weights = aperture_mask.data[slc_sm] pixel_mask = (aperture_weights > 0) & ~mask # good pixels # ignore RuntimeWarning for invalid data values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) flux_numer = np.sum((aperture_weights * data * rr)[pixel_mask]) flux_denom = np.sum((aperture_weights * data)[pixel_mask]) if flux_numer <= 0 or flux_denom <= 0: kron_radius.append(np.nan) continue kron_radius.append(flux_numer / flux_denom) kron_radius = np.array(kron_radius) * u.pix return kron_radius def make_kron_apertures(self, kron_params): """ Return a list of Kron elliptical apertures with the specified scaling and centered at the source centroid position. If provided, the `SourceCatalog` ``detection_cat`` will be used for the source centroids and elliptical shape parameters. Parameters ---------- kron_params : list of 2 floats, optional A list of two parameters used to determine how the Kron radius and flux are calculated. The first item is the scaling parameter of the Kron radius (`kron_radius`) and the second item represents the minimum circular radius. If the Kron radius times sqrt( `semimajor_sigma` * `semiminor_sigma`) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. Returns ------- result : list of `~photutils.aperture.PixelAperture` A list of `~photutils.aperture.EllipticalAperture` or `~photutils.aperture.CircularAperture` instances. The aperture will be `None` where the source centroid position is not finite or where the source is completely masked. """ if self._detection_cat is not None: # use detection catalog for centroids and elliptical shape # parameters detcat = self._detection_cat else: detcat = self kron_radius = detcat.kron_radius.value scale = kron_radius * kron_params[0] # NOTE: if kron_radius = NaN, scale = NaN and kron_aperture = None kron_apertures = self._make_elliptical_apertures(scale=scale) # check for minimum Kron radius major_sigma = detcat.semimajor_sigma.value minor_sigma = detcat.semiminor_sigma.value circ_radius = kron_radius * np.sqrt(major_sigma * minor_sigma) min_radius = kron_params[1] mask = (circ_radius < min_radius) idx = np.atleast_1d(mask).nonzero()[0] if idx.size > 0: circ_aperture = self.make_circular_apertures(min_radius) for i in idx: kron_apertures[i] = circ_aperture[i] return kron_apertures @as_scalar def plot_kron_apertures(self, kron_params, axes=None, origin=(0, 0), **kwargs): """ Plot Kron elliptical apertures on a matplotlib `~matplotlib.axes.Axes` instance. The apertures are defined by the specified radius and are centered at the source centroid position. If provided, the `SourceCatalog` ``detection_cat`` will be used for the source centroids and elliptical shape parameters. Parameters ---------- kron_params : list of 2 floats, optional A list of two parameters used to determine how the Kron radius and flux are calculated. The first item is the scaling parameter of the Kron radius (`kron_radius`) and the second item represents the minimum circular radius. If the Kron radius times sqrt( `semimajor_sigma` * `semiminor_sigma`) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. axes : `matplotlib.axes.Axes` or `None`, optional The matplotlib axes on which to plot. If `None`, then the current `~matplotlib.axes.Axes` instance is used. origin : array_like, optional The ``(x, y)`` position of the origin of the displayed image. **kwargs : `dict` Any keyword arguments accepted by `matplotlib.patches.Patch`. Returns ------- patch : list of `~matplotlib.patches.Patch` A list of matplotlib patches for the plotted aperture. The patches can be used, for example, when adding a plot legend. """ apertures = self.make_kron_apertures(kron_params) patches = [] for aperture in apertures: if aperture is not None: aperture.plot(axes=axes, origin=origin, **kwargs) patches.append(aperture._to_patch(origin=origin, **kwargs)) return patches @lazyproperty @as_scalar def kron_aperture(self): r""" The elliptical Kron aperture. For sources where .. math:: k_r \ \sqrt{a \cdot b} < rc_{min} where :math:`k_r` is the `kron_radius`, :math:`a` and :math:`b` are the semimajor (`semimajor_sigma`) and semiminor (`semiminor_sigma`) axes, respectively, and :math:`rc_{min}` is the minimum circular radius defined by ``kron_params[1]`` (see `SourceCatalog`), then a circular aperture with a radius equal to ``kron_params[1]`` will be returned. If ``kron_params[1] <= 0``, then the Kron aperture will be `None`. If ``kron_radius = np.nan`` then a circular aperture with a radius equal to ``kron_params[1]`` will be returned if the source is not completely masked, otherwise `None` will be returned. Note that if the Kron aperture is `None`, the Kron flux will be ``np.nan``. """ if self._detection_cat is not None: return self._detection_cat.kron_aperture return self.make_kron_apertures(self._kron_params) def _calc_kron_photometry(self, kron_params): """ Calculate the flux and flux error in the Kron aperture (without units). See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. If the Kron aperture is `None`, then ``np.nan`` will be returned. Returns ------- kron_flux, kron_fluxerr : tuple of `~numpy.ndarray` The Kron flux and flux error. """ # check kron_params kron_params = self._validate_kron_params(kron_params) if self._detection_cat is not None: detcat = self._detection_cat else: detcat = self kron_flux = [] kron_fluxerr = [] kron_aperture = self.make_kron_apertures(kron_params) for label, xcen, ycen, aperture, bkg in zip(detcat._label_iter, detcat._xcentroid, detcat._ycentroid, kron_aperture, self._local_background): if aperture is None: kron_flux.append(np.nan) kron_fluxerr.append(np.nan) continue aperture_mask = aperture.to_mask(method='exact') # prepare cutouts of the data based on the aperture size data, error, mask, _, slc_sm = self._make_aperture_data( label, xcen, ycen, aperture_mask.bbox, bkg) aperture_weights = aperture_mask.data[slc_sm] pixel_mask = (aperture_weights > 0) & ~mask # good pixels # ignore RuntimeWarning for invalid data or error values with warnings.catch_warnings(): warnings.simplefilter('ignore', RuntimeWarning) kron_flux.append(np.sum((aperture_weights * data)[pixel_mask])) if error is None: kron_fluxerr.append(np.nan) else: kron_fluxerr.append( np.sqrt(np.sum((aperture_weights * error**2)[pixel_mask]))) return kron_flux, kron_fluxerr def kron_photometry(self, kron_params, name=None, overwrite=False): """ Perform photometry for each source using an elliptical Kron aperture. This method can be used to calculate the Kron photometry using different scalings of the Kron radius (`kron_radius`). See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. Parameters ---------- kron_params : list of 2 floats, optional A list of two parameters used to determine how the Kron radius and flux are calculated. The first item is the scaling parameter of the Kron radius (`kron_radius`) and the second item represents the minimum circular radius. If the Kron radius times sqrt( `semimajor_sigma` * `semiminor_sigma`) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. name : str or `None`, optional The prefix name which will be used to define attribute names for the Kron flux and flux error. The attribute names ``[name]_flux`` and ``[name]_fluxerr`` will store the photometry results. For example, these names can then be included in the `to_table` ``columns`` keyword list to output the results in the table. overwrite : bool, optional If True, overwrite the attribute ``name`` if it exists. Returns ------- flux, fluxerr : `~numpy.ndarray` of floats, floats, or `~astropy.units.Quantity` The aperture fluxes and flux errors. NaN will be returned where the circular aperture is `None` (e.g., where the source centroid position is not finite). """ kron_flux, kron_fluxerr = self._calc_kron_photometry(kron_params) if self._data_unit is not None: kron_flux <<= self._data_unit kron_fluxerr <<= self._data_unit if self.isscalar: kron_flux = kron_flux[0] kron_fluxerr = kron_fluxerr[0] if name is not None: flux_name = f'{name}_flux' fluxerr_name = f'{name}_fluxerr' self.add_extra_property(flux_name, kron_flux, overwrite=overwrite) self.add_extra_property(fluxerr_name, kron_fluxerr, overwrite=overwrite) return kron_flux, kron_fluxerr @lazyproperty def _kron_flux_fluxerr(self): """ The flux and flux error in the Kron aperture (without units). See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. If the Kron aperture is `None`, then ``np.nan`` will be returned. """ return np.transpose(self._calc_kron_photometry(self._kron_params)) @lazyproperty @as_scalar def kron_flux(self): """ The flux in the Kron aperture. See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. If the Kron aperture is `None`, then ``np.nan`` will be returned. """ kron_flux = self._kron_flux_fluxerr[:, 0] if self._data_unit is not None: kron_flux <<= self._data_unit return kron_flux @lazyproperty @as_scalar def kron_fluxerr(self): """ The flux error in the Kron aperture. See the `SourceCatalog` ``apermask_method`` keyword for options to mask neighboring sources. If the Kron aperture is `None`, then ``np.nan`` will be returned. """ kron_fluxerr = self._kron_flux_fluxerr[:, 1] if self._data_unit is not None: kron_fluxerr <<= self._data_unit return kron_fluxerr @lazyproperty def _max_circular_kron_radius(self): """ The maximum circular Kron radius used as the upper limit of fluxfrac_radius. """ if self._detection_cat is not None: detcat = self._detection_cat else: detcat = self semimajor_sig = detcat.semimajor_sigma.value kron_radius = detcat.kron_radius.value radius = semimajor_sig * kron_radius * self._kron_params[0] if self.isscalar: radius = np.array([radius]) return radius @staticmethod def _fluxfrac_radius_fcn(radius, data, mask, aperture, normflux): """ Function whose root is found to compute the fluxfrac_radius. """ aperture.r = radius flux, _ = aperture.do_photometry(data, mask=mask) return 1.0 - (flux[0] / normflux) @lazyproperty def _fluxfrac_optimizer_args(self): if self._detection_cat is not None: detcat = self._detection_cat else: detcat = self kron_flux = self._kron_flux_fluxerr[:, 0] # unitless max_radius = self._max_circular_kron_radius args = [] for label, xcen, ycen, kronflux, bkg, max_radius_ in zip( self.labels, detcat._xcentroid, detcat._ycentroid, kron_flux, self._local_background, max_radius): if (np.any(~np.isfinite((xcen, ycen, kronflux, max_radius_))) or kronflux == 0): args.append(None) continue aperture = CircularAperture((xcen, ycen), r=max_radius_) aperture_mask = aperture.to_mask(method='exact') # prepare cutouts of the data based on the maximum aperture size data, _, mask, xycen, _ = self._make_aperture_data( label, xcen, ycen, aperture_mask.bbox, bkg, make_error=False) aperture.positions = xycen args.append([data, mask, aperture, kronflux, max_radius_]) return args @as_scalar def fluxfrac_radius(self, fluxfrac, name=None, overwrite=False): """ Calculate the circular radius that encloses the specified fraction of the Kron flux. To estimate the half-light radius, use ``fluxfrac = 0.5``. Parameters ---------- fluxfrac : float The fraction of the Kron flux at which to find the circular radius. name : str or `None`, optional The attribute name which will be assigned to the value of the output array. For example, this name can then be included in the `to_table` ``columns`` keyword list to output the results in the table. overwrite : bool, optional If True, overwrite the attribute ``name`` if it exists. Returns ------- radius : 1D `~numpy.ndarray` The circular radius that encloses the specified fraction of the Kron flux. NaN is returned where no solution was found or if the Kron flux is zero. """ if fluxfrac <= 0 or fluxfrac > 1: raise ValueError('fluxfrac must be > 0 and <= 1') from scipy.optimize import root_scalar radius = [] for fluxfrac_args in self._fluxfrac_optimizer_args: if fluxfrac_args is None: radius.append(np.nan) continue max_radius = fluxfrac_args[-1] args = fluxfrac_args[:-1] args[-1] *= fluxfrac args = tuple(args) # Try to find the root of self._fluxfrac_radius_fnc, which # is bracketed by a min and max radius. A ValueError is # raised if the bracket points do not have different signs, # indicating no solution or multiple solutions (e.g., a # multi-valued function). This can happen when at some # radius, flux starts decreasing with increasing radius (due # to negative data values), resulting in multiple possible # solutions. If no solution is found, we iteratively # decrease the max radius to narrow the bracket range until # the root is found. If max radius drops below the min # radius (0.1), then no solution is possible and NaN will be # returned as the result. found = False min_radius = 0.1 max_radius_delta = 1.0 while max_radius > min_radius and found is False: try: bracket = [min_radius, max_radius] result = root_scalar(self._fluxfrac_radius_fcn, args=args, bracket=bracket, method='brentq') result = result.root found = True except ValueError: # pragma: no cover # ValueError is raised if the bracket points do not # have different signs max_radius -= max_radius_delta # no solution found between min_radius and max_radius if found is False: result = np.nan radius.append(result) result = np.array(radius) << u.pix if name is not None: self.add_extra_property(name, result, overwrite=overwrite) return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/core.py0000644000214200020070000011551100000000000020234 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides classes for a segmentation image and a single segment within a segmentation image. """ from copy import deepcopy from astropy.utils import lazyproperty import numpy as np from ..aperture import BoundingBox from ..utils.colormaps import make_random_cmap __all__ = ['SegmentationImage', 'Segment'] __doctest_requires__ = {('SegmentationImage', 'SegmentationImage.*'): ['scipy']} class SegmentationImage: """ Class for a segmentation image. Parameters ---------- data : array_like (int) A segmentation array where source regions are labeled by different positive integer values. A value of zero is reserved for the background. The segmentation image must contain at least one non-zero pixel and must not contain any non-finite values (e.g., NaN, inf). """ def __init__(self, data): self.data = data def __str__(self): cls_name = f'<{self.__class__.__module__}.{self.__class__.__name__}>' cls_info = [] params = ['shape', 'nlabels'] for param in params: cls_info.append((param, getattr(self, param))) cls_info.append(('labels', self.labels)) with np.printoptions(threshold=25, edgeitems=5): fmt = [f'{key}: {val}' for key, val in cls_info] return f'{cls_name}\n' + '\n'.join(fmt) def __repr__(self): return self.__str__() def __array__(self): """ Array representation of the segmentation array (e.g., for matplotlib). """ return self._data @lazyproperty def _cmap(self): """ A matplotlib colormap consisting of (random) muted colors. This is very useful for plotting the segmentation array. """ return self.make_cmap(background_color='#000000', seed=0) @staticmethod def _get_labels(data): """ Return a sorted array of the non-zero labels in the segmentation image. Parameters ---------- data : array_like (int) A segmentation array where source regions are labeled by different positive integer values. A value of zero is reserved for the background. Returns ------- result : `~numpy.ndarray` An array of non-zero label numbers. Notes ----- This is a static method so it can be used in :meth:`remove_masked_labels` on a masked version of the segmentation array. """ # np.unique also sorts elements return np.unique(data[data != 0]) @lazyproperty def segments(self): """ A list of `Segment` objects. The list starts with the *non-zero* label. The returned list has a length equal to the number of labels and matches the order of the ``labels`` attribute. """ segments = [] for label, slc, bbox, area in zip(self.labels, self.slices, self.bbox, self.areas): segments.append(Segment(self.data, label, slc, bbox, area)) return segments @property def data(self): """The segmentation array.""" return self._data @data.setter def data(self, value): if np.any(~np.isfinite(value)): raise ValueError('data must not contain any non-finite values ' '(e.g., NaN, inf)') value = np.asarray(value, dtype=int) if not np.any(value): raise ValueError('The segmentation image must contain at least ' 'one non-zero pixel.') if np.min(value) < 0: raise ValueError('The segmentation image cannot contain ' 'negative integers.') if '_data' in self.__dict__: # needed only when data is reassigned, not on init self.__dict__ = {} self._data = value # pylint: disable=attribute-defined-outside-init @lazyproperty def data_ma(self): """ A `~numpy.ma.MaskedArray` version of the segmentation array where the background (label = 0) has been masked. """ return np.ma.masked_where(self.data == 0, self.data) @lazyproperty def shape(self): """The shape of the segmentation array.""" return self._data.shape @lazyproperty def _ndim(self): """The number of array dimensions of the segmentation array.""" return self._data.ndim @lazyproperty def labels(self): """The sorted non-zero labels in the segmentation array.""" return self._get_labels(self.data) @lazyproperty def nlabels(self): """The number of non-zero labels in the segmentation array.""" return len(self.labels) @lazyproperty def max_label(self): """The maximum non-zero label in the segmentation array.""" return np.max(self.labels) def get_index(self, label): """ Find the index of the input ``label``. Parameters ---------- label : int The label number to find. Returns ------- index : int The array index. Raises ------ ValueError If ``label`` is invalid. """ self.check_labels(label) return np.searchsorted(self.labels, label) def get_indices(self, labels): """ Find the indices of the input ``labels``. Parameters ---------- labels : int, array-like (1D, int) The label numbers(s) to find. Returns ------- indices : int `~numpy.ndarray` An integer array of indices with the same shape as ``labels``. If ``labels`` is a scalar, then the returned index will also be a scalar. Raises ------ ValueError If any input ``labels`` are invalid. """ self.check_labels(labels) return np.searchsorted(self.labels, labels) @lazyproperty def slices(self): """ A list of tuples, where each tuple contains two slices representing the minimal box that contains the labeled region. The list starts with the *non-zero* label. The returned list has a length equal to the number of labels and matches the order of the ``labels`` attribute. """ from scipy.ndimage import find_objects return [slc for slc in find_objects(self._data) if slc is not None] @lazyproperty def bbox(self): """ A list of `~photutils.aperture.BoundingBox` of the minimal bounding boxes containing the labeled regions. """ if self._ndim != 2: raise ValueError('The "bbox" attribute requires a 2D ' 'segmentation image.') return [BoundingBox(ixmin=slc[1].start, ixmax=slc[1].stop, iymin=slc[0].start, iymax=slc[0].stop) for slc in self.slices] @lazyproperty def background_area(self): """The area (in pixel**2) of the background (label=0) region.""" return len(self.data[self.data == 0]) @lazyproperty def areas(self): """ A 1D array of areas (in pixel**2) of the non-zero labeled regions. The `~numpy.ndarray` starts with the *non-zero* label. The returned array has a length equal to the number of labels and matches the order of the ``labels`` attribute. """ return np.array([area for area in np.bincount(self.data.ravel())[1:] if area != 0]) def get_area(self, label): """ The area (in pixel**2) of the region for the input label. Parameters ---------- label : int The label whose area to return. Label must be non-zero. Returns ------- area : `~numpy.ndarray` The area of the labeled region. """ return self.get_areas(label) def get_areas(self, labels): """ The areas (in pixel**2) of the regions for the input labels. Parameters ---------- labels : int, 1D array-like (int) The label(s) for which to return areas. Label must be non-zero. Returns ------- areas : `~numpy.ndarray` The areas of the labeled regions. """ idx = self.get_indices(labels) return self.areas[idx] @lazyproperty def is_consecutive(self): """ Determine whether or not the non-zero labels in the segmentation array are consecutive and start from 1. """ return ((self.labels[-1] - self.labels[0] + 1) == self.nlabels and self.labels[0] == 1) @lazyproperty def missing_labels(self): """ A 1D `~numpy.ndarray` of the sorted non-zero labels that are missing in the consecutive sequence from one to the maximum label number. """ return np.array(sorted(set(range(0, self.max_label + 1)) .difference(np.insert(self.labels, 0, 0)))) def copy(self): """Return a deep copy of this class instance.""" return deepcopy(self) def check_label(self, label): """ Check that the input label is a valid label number within the segmentation array. Parameters ---------- label : int The label number to check. Raises ------ ValueError If the input ``label`` is invalid. """ self.check_labels(label) def check_labels(self, labels): """ Check that the input label(s) are valid label numbers within the segmentation array. Parameters ---------- labels : int, 1D array-like (int) The label(s) to check. Raises ------ ValueError If any input ``labels`` are invalid. """ labels = np.atleast_1d(labels) bad_labels = set() # check for positive label numbers idx = np.where(labels <= 0)[0] if idx.size > 0: bad_labels.update(labels[idx]) # check if label is in the segmentation array bad_labels.update(np.setdiff1d(labels, self.labels)) if bad_labels: if len(bad_labels) == 1: raise ValueError(f'label {bad_labels} is invalid') else: raise ValueError(f'labels {bad_labels} are invalid') def make_cmap(self, background_color='#000000', seed=None): """ Define a matplotlib colormap consisting of (random) muted colors. This is very useful for plotting the segmentation array. Parameters ---------- background_color : str or `None`, optional A hex string in the "#rrggbb" format defining the first color in the colormap. This color will be used as the background color (label = 0) when plotting the segmentation array. The default is black ('#000000'). seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Separate function calls with the same ``seed`` will generate the same colormap. Returns ------- cmap : `matplotlib.colors.ListedColormap` The matplotlib colormap. """ from matplotlib import colors cmap = make_random_cmap(self.max_label + 1, seed=seed) if background_color is not None: cmap.colors[0] = colors.hex2color(background_color) return cmap def reassign_label(self, label, new_label, relabel=False): """ Reassign a label number to a new number. If ``new_label`` is already present in the segmentation array, then it will be combined with the input ``label`` number. Parameters ---------- label : int The label number to reassign. new_label : int The newly assigned label number. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_label(label=1, new_label=2) >>> segm.data array([[2, 2, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_label(label=1, new_label=4) >>> segm.data array([[4, 4, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_label(label=1, new_label=4, relabel=True) >>> segm.data array([[2, 2, 0, 0, 2, 2], [0, 0, 0, 0, 0, 2], [0, 0, 1, 1, 0, 0], [4, 0, 0, 0, 0, 3], [4, 4, 0, 3, 3, 3], [4, 4, 0, 0, 3, 3]]) """ self.reassign_labels(label, new_label, relabel=relabel) def reassign_labels(self, labels, new_label, relabel=False): """ Reassign one or more label numbers. Multiple input ``labels`` will all be reassigned to the same ``new_label`` number. If ``new_label`` is already present in the segmentation array, then it will be combined with the input ``labels``. Parameters ---------- labels : int, array-like (1D, int) The label numbers(s) to reassign. new_label : int The reassigned label number. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_labels(labels=[1, 7], new_label=2) >>> segm.data array([[2, 2, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [2, 0, 0, 0, 0, 5], [2, 2, 0, 5, 5, 5], [2, 2, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_labels(labels=[1, 7], new_label=4) >>> segm.data array([[4, 4, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [4, 0, 0, 0, 0, 5], [4, 4, 0, 5, 5, 5], [4, 4, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.reassign_labels(labels=[1, 7], new_label=2, relabel=True) >>> segm.data array([[1, 1, 0, 0, 3, 3], [0, 0, 0, 0, 0, 3], [0, 0, 2, 2, 0, 0], [1, 0, 0, 0, 0, 4], [1, 1, 0, 4, 4, 4], [1, 1, 0, 0, 4, 4]]) """ self.check_labels(labels) labels = np.atleast_1d(labels) if labels.size == 0: return idx = np.zeros(self.max_label + 1, dtype=int) idx[self.labels] = self.labels idx[labels] = new_label # reassign labels if relabel: labels = np.unique(idx[idx != 0]) if not len(labels) == 0: idx2 = np.zeros(max(labels) + 1, dtype=int) idx2[labels] = np.arange(len(labels)) + 1 idx = idx2[idx] data_new = idx[self.data] self.__dict__ = {} # reset all cached properties self._data = data_new # use _data to avoid validation def relabel_consecutive(self, start_label=1): """ Reassign the label numbers consecutively starting from a given label number. Parameters ---------- start_label : int, optional The starting label number, which should be a strictly positive integer. The default is 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.relabel_consecutive() >>> segm.data array([[1, 1, 0, 0, 3, 3], [0, 0, 0, 0, 0, 3], [0, 0, 2, 2, 0, 0], [5, 0, 0, 0, 0, 4], [5, 5, 0, 4, 4, 4], [5, 5, 0, 0, 4, 4]]) """ if start_label <= 0: raise ValueError('start_label must be > 0.') if ((self.labels[0] == start_label) and (self.labels[-1] - self.labels[0] + 1) == self.nlabels): return new_labels = np.zeros(self.max_label + 1, dtype=int) new_labels[self.labels] = np.arange(self.nlabels) + start_label data_new = new_labels[self.data] self.__dict__ = {} # reset all cached properties self._data = data_new # use _data to avoid validation def keep_label(self, label, relabel=False): """ Keep only the specified label. Parameters ---------- label : int The label number to keep. relabel : bool, optional If `True`, then the single segment will be assigned a label value of 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.keep_label(label=3) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.keep_label(label=3, relabel=True) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) """ self.keep_labels(label, relabel=relabel) def keep_labels(self, labels, relabel=False): """ Keep only the specified labels. Parameters ---------- labels : int, array-like (1D, int) The label number(s) to keep. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.keep_labels(labels=[5, 3]) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [0, 0, 0, 0, 0, 5], [0, 0, 0, 5, 5, 5], [0, 0, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.keep_labels(labels=[5, 3], relabel=True) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 2], [0, 0, 0, 2, 2, 2], [0, 0, 0, 0, 2, 2]]) """ self.check_labels(labels) labels = np.atleast_1d(labels) labels_tmp = list(set(self.labels) - set(labels)) self.remove_labels(labels_tmp, relabel=relabel) def remove_label(self, label, relabel=False): """ Remove the label number. The removed label is assigned a value of zero (i.e., background). Parameters ---------- label : int The label number to remove. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_label(label=5) >>> segm.data array([[1, 1, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_label(label=5, relabel=True) >>> segm.data array([[1, 1, 0, 0, 3, 3], [0, 0, 0, 0, 0, 3], [0, 0, 2, 2, 0, 0], [4, 0, 0, 0, 0, 0], [4, 4, 0, 0, 0, 0], [4, 4, 0, 0, 0, 0]]) """ self.remove_labels(label, relabel=relabel) def remove_labels(self, labels, relabel=False): """ Remove one or more labels. Removed labels are assigned a value of zero (i.e., background). Parameters ---------- labels : int, array-like (1D, int) The label number(s) to remove. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_labels(labels=[5, 3]) >>> segm.data array([[1, 1, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 0, 0, 0, 0], [7, 0, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_labels(labels=[5, 3], relabel=True) >>> segm.data array([[1, 1, 0, 0, 2, 2], [0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0], [3, 0, 0, 0, 0, 0], [3, 3, 0, 0, 0, 0], [3, 3, 0, 0, 0, 0]]) """ self.check_labels(labels) self.reassign_labels(labels, new_label=0, relabel=relabel) def remove_border_labels(self, border_width, partial_overlap=True, relabel=False): """ Remove labeled segments near the array border. Labels within the defined border region will be removed. Parameters ---------- border_width : int The width of the border region in pixels. partial_overlap : bool, optional If this is set to `True` (the default), a segment that partially extends into the border region will be removed. Segments that are completely within the border region are always removed. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_border_labels(border_width=1) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_border_labels(border_width=1, ... partial_overlap=False) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) """ if border_width >= min(self.shape) / 2: raise ValueError('border_width must be smaller than half the ' 'array size in any dimension') border_mask = np.zeros(self.shape, dtype=bool) for i in range(border_mask.ndim): border_mask = border_mask.swapaxes(0, i) border_mask[:border_width] = True border_mask[-border_width:] = True border_mask = border_mask.swapaxes(0, i) self.remove_masked_labels(border_mask, partial_overlap=partial_overlap, relabel=relabel) def remove_masked_labels(self, mask, partial_overlap=True, relabel=False): """ Remove labeled segments located within a masked region. Parameters ---------- mask : array_like (bool) A boolean mask, with the same shape as the segmentation array, where `True` values indicate masked pixels. partial_overlap : bool, optional If this is set to `True` (default), a segment that partially extends into a masked region will also be removed. Segments that are completely within a masked region are always removed. relabel : bool, optional If `True`, then the segmentation array will be relabeled such that the labels are in consecutive order starting from 1. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> mask = np.zeros(segm.data.shape, dtype=bool) >>> mask[0, :] = True # mask the first row >>> segm.remove_masked_labels(mask) >>> segm.data array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) >>> segm = SegmentationImage([[1, 1, 0, 0, 4, 4], ... [0, 0, 0, 0, 0, 4], ... [0, 0, 3, 3, 0, 0], ... [7, 0, 0, 0, 0, 5], ... [7, 7, 0, 5, 5, 5], ... [7, 7, 0, 0, 5, 5]]) >>> segm.remove_masked_labels(mask, partial_overlap=False) >>> segm.data array([[0, 0, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) """ if mask.shape != self.shape: raise ValueError('mask must have the same shape as the ' 'segmentation array') remove_labels = self._get_labels(self.data[mask]) if not partial_overlap: interior_labels = self._get_labels(self.data[~mask]) remove_labels = list(set(remove_labels) - set(interior_labels)) self.remove_labels(remove_labels, relabel=relabel) def outline_segments(self, mask_background=False): """ Outline the labeled segments. The "outlines" represent the pixels *just inside* the segments, leaving the background pixels unmodified. Parameters ---------- mask_background : bool, optional Set to `True` to mask the background pixels (labels = 0) in the returned array. This is useful for overplotting the segment outlines. The default is `False`. Returns ------- boundaries : `~numpy.ndarray` or `~numpy.ma.MaskedArray` An array with the same shape of the segmentation array containing only the outlines of the labeled segments. The pixel values in the outlines correspond to the labels in the segmentation array. If ``mask_background`` is `True`, then a `~numpy.ma.MaskedArray` is returned. Examples -------- >>> from photutils.segmentation import SegmentationImage >>> segm = SegmentationImage([[0, 0, 0, 0, 0, 0], ... [0, 2, 2, 2, 2, 0], ... [0, 2, 2, 2, 2, 0], ... [0, 2, 2, 2, 2, 0], ... [0, 2, 2, 2, 2, 0], ... [0, 0, 0, 0, 0, 0]]) >>> segm.outline_segments() array([[0, 0, 0, 0, 0, 0], [0, 2, 2, 2, 2, 0], [0, 2, 0, 0, 2, 0], [0, 2, 0, 0, 2, 0], [0, 2, 2, 2, 2, 0], [0, 0, 0, 0, 0, 0]]) """ from scipy.ndimage import (generate_binary_structure, grey_dilation, grey_erosion) # mode='constant' ensures outline is included on the array borders selem = generate_binary_structure(self._ndim, 1) # edge connectivity eroded = grey_erosion(self.data, footprint=selem, mode='constant', cval=0.) dilated = grey_dilation(self.data, footprint=selem, mode='constant', cval=0.) outlines = ((dilated != eroded) & (self.data != 0)).astype(int) outlines *= self.data if mask_background: outlines = np.ma.masked_where(outlines == 0, outlines) return outlines class Segment: """ Class for a single labeled region (segment) within a segmentation image. Parameters ---------- segment_data : int `~numpy.ndarray` A segmentation array where source regions are labeled by different positive integer values. A value of zero is reserved for the background. label : int The segment label number. slices : tuple of two slices A tuple of two slices representing the minimal box that contains the labeled region. bbox : `~photutils.aperture.BoundingBox` The minimal bounding box that contains the labeled region. area : float The area of the segment in pixels**2. """ def __init__(self, segment_data, label, slices, bbox, area): self._segment_data = segment_data self.label = label self.slices = slices self.bbox = bbox self.area = area def __str__(self): cls_name = f'<{self.__class__.__module__}.{self.__class__.__name__}>' cls_info = [] params = ['label', 'slices', 'area'] for param in params: cls_info.append((param, getattr(self, param))) fmt = [f'{key}: {val}' for key, val in cls_info] return f'{cls_name}\n' + '\n'.join(fmt) def __repr__(self): return self.__str__() def __array__(self): """ Array representation of the labeled region (e.g., for matplotlib). """ return self.data @lazyproperty def data(self): """ A cutout array of the segment using the minimal bounding box, where pixels outside of the labeled region are set to zero (i.e., neighboring segments within the rectangular cutout array are not shown). """ cutout = np.copy(self._segment_data[self.slices]) cutout[cutout != self.label] = 0 return cutout @lazyproperty def data_ma(self): """ A `~numpy.ma.MaskedArray` cutout array of the segment using the minimal bounding box. The mask is `True` for pixels outside of the source segment (i.e., neighboring segments within the rectangular cutout array are masked). """ mask = (self._segment_data[self.slices] != self.label) return np.ma.masked_array(self._segment_data[self.slices], mask=mask) def make_cutout(self, data, masked_array=False): """ Create a (masked) cutout array from the input ``data`` using the minimal bounding box of the segment (labeled region). If ``masked_array`` is `False` (default), then the returned cutout array is simply a `~numpy.ndarray`. The returned cutout is a view (not a copy) of the input ``data``. No pixels are altered (e.g., set to zero) within the bounding box. If ``masked_array` is `True`, then the returned cutout array is a `~numpy.ma.MaskedArray`, where the mask is `True` for pixels outside of the segment (labeled region). The data part of the masked array is a view (not a copy) of the input ``data``. Parameters ---------- data : array-like The data array from which to create the masked cutout array. ``data`` must have the same shape as the segmentation array. masked_array : bool, optional If `True` then a `~numpy.ma.MaskedArray` will be created where the mask is `True` for pixels outside of the segment (labeled region). If `False`, then a `~numpy.ndarray` will be generated. Returns ------- result : `~numpy.ndarray` or `~numpy.ma.MaskedArray` The cutout array. """ data = np.asanyarray(data) if data.shape != self._segment_data.shape: raise ValueError('data must have the same shape as the ' 'segmentation array.') if masked_array: mask = (self._segment_data[self.slices] != self.label) return np.ma.masked_array(data[self.slices], mask=mask) else: return data[self.slices] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640109423.0 photutils-1.3.0/photutils/segmentation/deblend.py0000644000214200020070000003157000000000000020703 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for deblending overlapping sources labeled in a segmentation image. """ import warnings from astropy.utils.decorators import deprecated_renamed_argument from astropy.utils.exceptions import AstropyUserWarning import numpy as np from .core import SegmentationImage from .detect import _make_binary_structure, _detect_sources from ..utils._convolution import _filter_data from ..utils.exceptions import NoDetectionsWarning __all__ = ['deblend_sources'] @deprecated_renamed_argument('filter_kernel', 'kernel', '1.2') def deblend_sources(data, segment_img, npixels, kernel=None, labels=None, nlevels=32, contrast=0.001, mode='exponential', connectivity=8, relabel=True): """ Deblend overlapping sources labeled in a segmentation image. Sources are deblended using a combination of multi-thresholding and `watershed segmentation `_. In order to deblend sources, there must be a saddle between them. Parameters ---------- data : array_like The data array. segment_img : `~photutils.segmentation.SegmentationImage` or array_like (int) A segmentation image, either as a `~photutils.segmentation.SegmentationImage` object or an `~numpy.ndarray`, with the same shape as ``data`` where sources are labeled by different positive integer values. A value of zero is reserved for the background. npixels : int The number of connected pixels, each greater than ``threshold``, that an object must have to be detected. ``npixels`` must be a positive integer. kernel : array-like or `~astropy.convolution.Kernel2D`, optional The array of the kernel used to filter the image before thresholding. Filtering the image will smooth the noise and maximize detectability of objects with a shape similar to the kernel. labels : int or array-like of int, optional The label numbers to deblend. If `None` (default), then all labels in the segmentation image will be deblended. nlevels : int, optional The number of multi-thresholding levels to use. Each source will be re-thresholded at ``nlevels`` levels spaced exponentially or linearly (see the ``mode`` keyword) between its minimum and maximum values within the source segment. contrast : float, optional The fraction of the total (blended) source flux that a local peak must have (at any one of the multi-thresholds) to be considered as a separate object. ``contrast`` must be between 0 and 1, inclusive. If ``contrast = 0`` then every local peak will be made a separate object (maximum deblending). If ``contrast = 1`` then no deblending will occur. The default is 0.001, which will deblend sources with a 7.5 magnitude difference. mode : {'exponential', 'linear'}, optional The mode used in defining the spacing between the multi-thresholding levels (see the ``nlevels`` keyword). The default is 'exponential'. connectivity : {8, 4}, optional The type of pixel connectivity used in determining how pixels are grouped into a detected source. The options are 8 (default) or 4. 8-connected pixels touch along their edges or corners. 4-connected pixels touch along their edges. For reference, SourceExtractor uses 8-connected pixels. relabel : bool If `True` (default), then the segmentation image will be relabeled such that the labels are in consecutive order starting from 1. Returns ------- segment_image : `~photutils.segmentation.SegmentationImage` A segmentation image, with the same shape as ``data``, where sources are marked by different positive integer values. A value of zero is reserved for the background. See Also -------- :func:`photutils.segmentation.detect_sources` """ if not isinstance(segment_img, SegmentationImage): segment_img = SegmentationImage(segment_img) if segment_img.shape != data.shape: raise ValueError('The data and segmentation image must have ' 'the same shape') if labels is None: labels = segment_img.labels labels = np.atleast_1d(labels) segment_img.check_labels(labels) if kernel is not None: data = _filter_data(data, kernel, mode='constant', fill_value=0.0) last_label = segment_img.max_label segm_deblended = object.__new__(SegmentationImage) segm_deblended._data = np.copy(segment_img.data) for label in labels: source_slice = segment_img.slices[segment_img.get_index(label)] source_data = data[source_slice] source_segm = object.__new__(SegmentationImage) source_segm._data = np.copy(segment_img.data[source_slice]) source_segm.keep_labels(label) # include only one label source_deblended = _deblend_source( source_data, source_segm, npixels, nlevels=nlevels, contrast=contrast, mode=mode, connectivity=connectivity) if not np.array_equal(source_deblended.data.astype(bool), source_segm.data.astype(bool)): raise ValueError(f'Deblending failed for source "{label}". ' 'Please ensure you used the same pixel ' 'connectivity in detect_sources and ' 'deblend_sources. If this issue persists, ' 'then please inform the developers.') if source_deblended.nlabels > 1: source_deblended.relabel_consecutive(start_label=1) # replace the original source with the deblended source source_mask = (source_deblended.data > 0) segm_tmp = segm_deblended.data segm_tmp[source_slice][source_mask] = ( source_deblended.data[source_mask] + last_label) segm_deblended.__dict__ = {} # reset cached properties segm_deblended._data = segm_tmp last_label += source_deblended.nlabels if relabel: segm_deblended.relabel_consecutive() return segm_deblended def _deblend_source(data, segment_img, npixels, nlevels=32, contrast=0.001, mode='exponential', connectivity=8): """ Deblend a single labeled source. Parameters ---------- data : array_like The cutout data array for a single source. ``data`` should also already be smoothed by the same filter used in :func:`~photutils.segmentation.detect_sources`, if applicable. segment_img : `~photutils.segmentation.SegmentationImage` A cutout `~photutils.segmentation.SegmentationImage` object with the same shape as ``data``. ``segment_img`` should contain only *one* source label. npixels : int The number of connected pixels, each greater than ``threshold``, that an object must have to be detected. ``npixels`` must be a positive integer. nlevels : int, optional The number of multi-thresholding levels to use. Each source will be re-thresholded at ``nlevels`` levels spaced exponentially or linearly (see the ``mode`` keyword) between its minimum and maximum values within the source segment. contrast : float, optional The fraction of the total (blended) source flux that a local peak must have (at any one of the multi-thresholds) to be considered as a separate object. ``contrast`` must be between 0 and 1, inclusive. If ``contrast = 0`` then every local peak will be made a separate object (maximum deblending). If ``contrast = 1`` then no deblending will occur. The default is 0.001, which will deblend sources with a 7.5 magnitude difference. mode : {'exponential', 'linear'}, optional The mode used in defining the spacing between the multi-thresholding levels (see the ``nlevels`` keyword). The default is 'exponential'. connectivity : {8, 4}, optional The type of pixel connectivity used in determining how pixels are grouped into a detected source. The options are 8 (default) or 4. 8-connected pixels touch along their edges or corners. 4-connected pixels touch along their edges. For reference, SourceExtractor uses 8-connected pixels. Returns ------- segment_image : `~photutils.segmentation.SegmentationImage` A segmentation image, with the same shape as ``data``, where sources are marked by different positive integer values. A value of zero is reserved for the background. Note that the returned `SegmentationImage` may *not* have consecutive labels. """ from scipy.ndimage import label as ndilabel from skimage.segmentation import watershed if nlevels < 1: raise ValueError(f'nlevels must be >= 1, got "{nlevels}"') if contrast < 0 or contrast > 1: raise ValueError(f'contrast must be >= 0 and <= 1, got "{contrast}"') segm_mask = (segment_img.data > 0) source_values = data[segm_mask] source_sum = float(np.nansum(source_values)) source_min = np.nanmin(source_values) source_max = np.nanmax(source_values) if source_min == source_max: return segment_img # no deblending if mode == 'exponential' and source_min < 0: warnings.warn(f'Source "{segment_img.labels[0]}" contains negative ' 'values, setting deblending mode to "linear"', AstropyUserWarning) mode = 'linear' steps = np.arange(1., nlevels + 1) if mode == 'exponential': if source_min == 0: source_min = source_max * 0.01 thresholds = source_min * ((source_max / source_min) ** (steps / (nlevels + 1))) elif mode == 'linear': thresholds = source_min + ((source_max - source_min) / (nlevels + 1)) * steps else: raise ValueError(f'"{mode}" is an invalid mode; mode must be ' '"exponential" or "linear"') # suppress NoDetectionsWarning during deblending warnings.filterwarnings('ignore', category=NoDetectionsWarning) mask = ~segm_mask segments = _detect_sources(data, thresholds, npixels=npixels, connectivity=connectivity, mask=mask, deblend_skip=True) selem = _make_binary_structure(data.ndim, connectivity) # define the sources (markers) for the watershed algorithm nsegments = len(segments) if nsegments == 0: # no deblending return segment_img else: for i in range(nsegments - 1): segm_lower = segments[i].data segm_upper = segments[i + 1].data relabel = False # if the are more sources at the upper level, then # remove the parent source(s) from the lower level, # but keep any sources in the lower level that do not have # multiple children in the upper level for label in segments[i].labels: mask = (segm_lower == label) # checks for 1-to-1 label mapping n -> m (where m >= 0) upper_labels = segm_upper[mask] upper_labels = np.unique(upper_labels[upper_labels != 0]) if upper_labels.size >= 2: relabel = True segm_lower[mask] = segm_upper[mask] if relabel: segm_new = object.__new__(SegmentationImage) segm_new._data = ndilabel(segm_lower, structure=selem)[0] segments[i + 1] = segm_new else: segments[i + 1] = segments[i] # Deblend using watershed. If any sources do not meet the # contrast criterion, then remove the faintest such source and # repeat until all sources meet the contrast criterion. markers = segments[-1].data mask = segment_img.data.astype(bool) remove_marker = True while remove_marker: markers = watershed(-data, markers, mask=mask, connectivity=selem) labels = np.unique(markers[markers != 0]) flux_frac = np.array([np.sum(data[markers == label]) for label in labels]) / source_sum remove_marker = any(flux_frac < contrast) if remove_marker: # remove only the faintest source (one at a time) # because several faint sources could combine to meet the # contrast criterion markers[markers == labels[np.argmin(flux_frac)]] = 0. segm_new = object.__new__(SegmentationImage) segm_new._data = markers return segm_new ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640109423.0 photutils-1.3.0/photutils/segmentation/detect.py0000644000214200020070000004457400000000000020566 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for detecting sources in an image. """ import warnings from astropy.convolution import Gaussian2DKernel, convolve from astropy.stats import gaussian_fwhm_to_sigma, sigma_clipped_stats from astropy.utils.decorators import deprecated_renamed_argument from astropy.utils.exceptions import AstropyUserWarning import numpy as np from .core import SegmentationImage from ..utils.exceptions import NoDetectionsWarning __all__ = ['detect_threshold', 'detect_sources', 'make_source_mask'] def detect_threshold(data, nsigma, background=None, error=None, mask=None, mask_value=None, sigclip_sigma=3.0, sigclip_iters=None): """ Calculate a pixel-wise threshold image that can be used to detect sources. Parameters ---------- data : array_like The 2D array of the image. nsigma : float The number of standard deviations per pixel above the ``background`` for which to consider a pixel as possibly being part of a source. background : float or array_like, optional The background value(s) of the input ``data``. ``background`` may either be a scalar value or a 2D image with the same shape as the input ``data``. If the input ``data`` has been background-subtracted, then set ``background`` to ``0.0``. If `None`, then a scalar background value will be estimated using sigma-clipped statistics. error : float or array_like, optional The Gaussian 1-sigma standard deviation of the background noise in ``data``. ``error`` should include all sources of "background" error, but *exclude* the Poisson error of the sources. If ``error`` is a 2D image, then it should represent the 1-sigma background error in each pixel of ``data``. If `None`, then a scalar background rms value will be estimated using sigma-clipped statistics. mask : array_like, bool, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when computing the image background statistics. mask_value : float, optional An image data value (e.g., ``0.0``) that is ignored when computing the image background statistics. ``mask_value`` will be ignored if ``mask`` is input. sigclip_sigma : float, optional The number of standard deviations to use as the clipping limit when calculating the image background statistics. sigclip_iters : int, optional The maximum number of iterations to perform sigma clipping, or `None` to clip until convergence is achieved (i.e., continue until the last iteration clips nothing) when calculating the image background statistics. Returns ------- threshold : 2D `~numpy.ndarray` A 2D image with the same shape as ``data`` containing the pixel-wise threshold values. See Also -------- :func:`photutils.segmentation.detect_sources` Notes ----- The ``mask``, ``mask_value``, ``sigclip_sigma``, and ``sigclip_iters`` inputs are used only if it is necessary to estimate ``background`` or ``error`` using sigma-clipped background statistics. If ``background`` and ``error`` are both input, then ``mask``, ``mask_value``, ``sigclip_sigma``, and ``sigclip_iters`` are ignored. """ if background is None or error is None: data_mean, _, data_std = sigma_clipped_stats( data, mask=mask, mask_value=mask_value, sigma=sigclip_sigma, maxiters=sigclip_iters) bkgrd_image = np.zeros_like(data) + data_mean bkgrdrms_image = np.zeros_like(data) + data_std if background is None: background = bkgrd_image else: if np.isscalar(background): background = np.zeros_like(data) + background else: if background.shape != data.shape: raise ValueError('If input background is 2D, then it ' 'must have the same shape as the input ' 'data.') if error is None: error = bkgrdrms_image else: if np.isscalar(error): error = np.zeros_like(data) + error else: if error.shape != data.shape: raise ValueError('If input error is 2D, then it ' 'must have the same shape as the input ' 'data.') return background + (error * nsigma) def _make_binary_structure(ndim, connectivity): """ Make a binary structure element. Parameters ---------- ndim : int The number of array dimensions. connectivity : {4, 8} For the case of ``ndim=2``, the type of pixel connectivity used in determining how pixels are grouped into a detected source. The options are 4 or 8 (default). 4-connected pixels touch along their edges. 8-connected pixels touch along their edges or corners. For reference, SourceExtractor uses 8-connected pixels. Returns ------- array : ndarray of int or bool The binary structure element. If ``ndim <= 2`` an array of int is returned, otherwise an array of bool is returned. """ from scipy.ndimage import generate_binary_structure if ndim == 1: selem = np.array((1, 1, 1)) elif ndim == 2: if connectivity == 4: selem = np.array(((0, 1, 0), (1, 1, 1), (0, 1, 0))) elif connectivity == 8: selem = np.ones((3, 3), dtype=int) else: raise ValueError(f'Invalid connectivity={connectivity}. ' 'Options are 4 or 8.') else: selem = generate_binary_structure(ndim, 1) return selem def _detect_sources(data, thresholds, npixels, kernel=None, connectivity=8, mask=None, deblend_skip=False): """ Detect sources above a specified threshold value in an image and return a `~photutils.segmentation.SegmentationImage` object. Detected sources must have ``npixels`` connected pixels that are each greater than the ``threshold`` value. If the filtering option is used, then the ``threshold`` is applied to the filtered image. The input ``mask`` can be used to mask pixels in the input data. Masked pixels will not be included in any source. This function does not deblend overlapping sources. First use this function to detect sources followed by :func:`~photutils.segmentation.deblend_sources` to deblend sources. Parameters ---------- data : array_like The 2D array of the image. thresholds : array-like of floats or arrays The data value or pixel-wise data values to be used for the detection thresholds. A 2D ``threshold`` must have the same shape as ``data``. See `~photutils.segmentation.detect_threshold` for one way to create a ``threshold`` image. npixels : int The number of connected pixels, each greater than ``threshold``, that an object must have to be detected. ``npixels`` must be a positive integer. kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional The 2D array of the kernel used to filter the image before thresholding. Filtering the image will smooth the noise and maximize detectability of objects with a shape similar to the kernel. connectivity : {4, 8}, optional The type of pixel connectivity used in determining how pixels are grouped into a detected source. The options are 4 or 8 (default). 4-connected pixels touch along their edges. 8-connected pixels touch along their edges or corners. For reference, SourceExtractor uses 8-connected pixels. mask : array_like of bool, optional A boolean mask, with the same shape as the input ``data``, where `True` values indicate masked pixels. Masked pixels will not be included in any source. deblend_skip : bool, optional If `True` do not include the segmentation image in the output list for any threshold level where the number of detected sources is less than 2. This is useful for source deblending and improves its performance. Returns ------- segment_image : list of `~photutils.segmentation.SegmentationImage` A list of 2D segmentation images, with the same shape as ``data``, where sources are marked by different positive integer values. A value of zero is reserved for the background. If no sources are found for a given threshold, then the output list will contain `None` for that threshold. Also see the ``deblend_skip`` keyword. """ from scipy import ndimage if (npixels <= 0) or (int(npixels) != npixels): raise ValueError('npixels must be a positive integer, got ' f'"{npixels}"') if mask is not None: if mask.shape != data.shape: raise ValueError('mask must have the same shape as the input ' 'image.') if kernel is not None: with warnings.catch_warnings(): warnings.simplefilter('ignore', AstropyUserWarning) data = convolve(data, kernel, mask=mask, normalize_kernel=True) selem = _make_binary_structure(data.ndim, connectivity) segms = [] for threshold in thresholds: # ignore RuntimeWarning caused by > comparison when data contains NaNs with warnings.catch_warnings(): warnings.simplefilter('ignore', category=RuntimeWarning) data2 = data > threshold if mask is not None: data2 &= ~mask # return if threshold was too high to detect any sources if np.count_nonzero(data2) == 0: warnings.warn('No sources were found.', NoDetectionsWarning) if not deblend_skip: segms.append(None) continue segm_img, _ = ndimage.label(data2, structure=selem) # remove objects with less than npixels # NOTE: for typical data, making the cutout images is ~10x faster # than using segm_img directly segm_slices = ndimage.find_objects(segm_img) for i, slices in enumerate(segm_slices): cutout = segm_img[slices] segment_mask = (cutout == (i + 1)) if np.count_nonzero(segment_mask) < npixels: cutout[segment_mask] = 0 if np.count_nonzero(segm_img) == 0: warnings.warn('No sources were found.', NoDetectionsWarning) if not deblend_skip: segms.append(None) continue segm = object.__new__(SegmentationImage) segm._data = segm_img if deblend_skip and segm.nlabels == 1: continue segm.relabel_consecutive() segms.append(segm) return segms @deprecated_renamed_argument('filter_kernel', 'kernel', '1.2') def detect_sources(data, threshold, npixels, kernel=None, connectivity=8, mask=None): """ Detect sources above a specified threshold value in an image and return a `~photutils.segmentation.SegmentationImage` object. Detected sources must have ``npixels`` connected pixels that are each greater than the ``threshold`` value. If the filtering option is used, then the ``threshold`` is applied to the filtered image. The input ``mask`` can be used to mask pixels in the input data. Masked pixels will not be included in any source. This function does not deblend overlapping sources. First use this function to detect sources followed by :func:`~photutils.segmentation.deblend_sources` to deblend sources. Parameters ---------- data : array_like The 2D array of the image. threshold : float or array-like The data value or pixel-wise data values to be used for the detection threshold. A 2D ``threshold`` must have the same shape as ``data``. See `~photutils.segmentation.detect_threshold` for one way to create a ``threshold`` image. npixels : int The number of connected pixels, each greater than ``threshold``, that an object must have to be detected. ``npixels`` must be a positive integer. kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional The 2D array of the kernel used to filter the image before thresholding. Filtering the image will smooth the noise and maximize detectability of objects with a shape similar to the kernel. connectivity : {4, 8}, optional The type of pixel connectivity used in determining how pixels are grouped into a detected source. The options are 4 or 8 (default). 4-connected pixels touch along their edges. 8-connected pixels touch along their edges or corners. For reference, SourceExtractor uses 8-connected pixels. mask : array_like of bool, optional A boolean mask, with the same shape as the input ``data``, where `True` values indicate masked pixels. Masked pixels will not be included in any source. Returns ------- segment_image : `~photutils.segmentation.SegmentationImage` or `None` A 2D segmentation image, with the same shape as ``data``, where sources are marked by different positive integer values. A value of zero is reserved for the background. If no sources are found then `None` is returned. See Also -------- :func:`photutils.segmentation.detect_threshold` :class:`photutils.segmentation.SegmentationImage` :func:`photutils.segmentation.source_properties` :func:`photutils.segmentation.deblend_sources` Examples -------- .. plot:: :include-source: from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma from astropy.visualization import simple_norm import matplotlib.pyplot as plt from photutils.datasets import make_100gaussians_image from photutils.segmentation import detect_threshold, detect_sources # make a simulated image data = make_100gaussians_image() # detect the sources threshold = detect_threshold(data, nsigma=3) sigma = 3.0 * gaussian_fwhm_to_sigma # FWHM = 3. kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() segm = detect_sources(data, threshold, npixels=5, kernel=kernel) # plot the image and the segmentation image fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 10)) norm = simple_norm(data, 'sqrt', percent=99.) ax1.imshow(data, origin='lower', interpolation='nearest', norm=norm) ax2.imshow(segm.data, origin='lower', interpolation='nearest', cmap=segm.make_cmap(seed=1234)) plt.tight_layout() """ return _detect_sources(data, (threshold,), npixels, kernel=kernel, connectivity=connectivity, mask=mask)[0] @deprecated_renamed_argument('filter_kernel', 'kernel', '1.2') def make_source_mask(data, nsigma, npixels, mask=None, filter_fwhm=None, filter_size=3, kernel=None, sigclip_sigma=3.0, sigclip_iters=5, dilate_size=11): """ Make a source mask using source segmentation and binary dilation. Parameters ---------- data : array_like The 2D array of the image. nsigma : float The number of standard deviations per pixel above the ``background`` for which to consider a pixel as possibly being part of a source. npixels : int The number of connected pixels, each greater than ``threshold``, that an object must have to be detected. ``npixels`` must be a positive integer. mask : array_like, bool, optional A boolean mask with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Masked pixels are ignored when computing the image background statistics. filter_fwhm : float, optional The full-width at half-maximum (FWHM) of the Gaussian kernel to filter the image before thresholding. ``filter_fwhm`` and ``filter_size`` are ignored if ``kernel`` is defined. filter_size : float, optional The size of the square Gaussian kernel image. Used only if ``filter_fwhm`` is defined. ``filter_fwhm`` and ``filter_size`` are ignored if ``kernel`` is defined. kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional The 2D array of the kernel used to filter the image before thresholding. Filtering the image will smooth the noise and maximize detectability of objects with a shape similar to the kernel. ``kernel`` overrides ``filter_fwhm`` and ``filter_size``. sigclip_sigma : float, optional The number of standard deviations to use as the clipping limit when calculating the image background statistics. sigclip_iters : int, optional The maximum number of iterations to perform sigma clipping, or `None` to clip until convergence is achieved (i.e., continue until the last iteration clips nothing) when calculating the image background statistics. dilate_size : int, optional The size of the square array used to dilate the segmentation image. Returns ------- mask : 2D bool `~numpy.ndarray` A 2D boolean image containing the source mask. """ from scipy import ndimage threshold = detect_threshold(data, nsigma, background=None, error=None, mask=mask, sigclip_sigma=sigclip_sigma, sigclip_iters=sigclip_iters) if kernel is None and filter_fwhm is not None: kernel_sigma = filter_fwhm * gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(kernel_sigma, x_size=filter_size, y_size=filter_size) if kernel is not None: kernel.normalize() segm = detect_sources(data, threshold, npixels, kernel=kernel) if segm is None: return np.zeros(data.shape, dtype=bool) selem = np.ones((dilate_size, dilate_size)) return ndimage.binary_dilation(segm.data.astype(bool), selem) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/properties.py0000644000214200020070000023656500000000000021515 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for calculating the properties of sources defined by a segmentation image. """ from copy import deepcopy import warnings from astropy.coordinates import SkyCoord from astropy.stats import SigmaClip from astropy.table import QTable import astropy.units as u from astropy.utils import lazyproperty from astropy.utils.decorators import deprecated from astropy.utils.exceptions import (AstropyUserWarning, AstropyDeprecationWarning) import numpy as np from .core import SegmentationImage from ..background import SExtractorBackground from ..aperture import (BoundingBox, CircularAperture, EllipticalAperture, RectangularAnnulus) from ..utils._convolution import _filter_data from ..utils._moments import _moments, _moments_central __all__ = ['SourceProperties', 'source_properties', 'LegacySourceCatalog'] __doctest_requires__ = {('SourceProperties', 'SourceProperties.*', 'LegacySourceCatalog', 'LegacySourceCatalog.*', 'source_properties', 'properties_table'): ['scipy']} # default table columns for `to_table()` output DEFAULT_COLUMNS = ['id', 'xcentroid', 'ycentroid', 'sky_centroid', 'sky_centroid_icrs', 'source_sum', 'source_sum_err', 'background_sum', 'background_mean', 'background_at_centroid', 'bbox_xmin', 'bbox_xmax', 'bbox_ymin', 'bbox_ymax', 'min_value', 'max_value', 'minval_xpos', 'minval_ypos', 'maxval_xpos', 'maxval_ypos', 'area', 'equivalent_radius', 'perimeter', 'semimajor_axis_sigma', 'semiminor_axis_sigma', 'orientation', 'eccentricity', 'ellipticity', 'elongation', 'covar_sigx2', 'covar_sigxy', 'covar_sigy2', 'cxx', 'cxy', 'cyy', 'gini'] @deprecated('1.1', alternative='`~photutils.segmentation.SourceCatalog`') class SourceProperties: r""" Class to calculate photometry and morphological properties of a single labeled source (deprecated). Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array from which to calculate the source photometry and properties. If ``filtered_data`` is input, then it will be used instead of ``data`` to calculate the source centroid and morphological properties. Source photometry is always measured from ``data``. For accurate source properties and photometry, ``data`` should be background-subtracted. Non-finite ``data`` values (NaN and +/- inf) are automatically masked. segment_img : `SegmentationImage` or array_like (int) A 2D segmentation image, either as a `SegmentationImage` object or an `~numpy.ndarray`, with the same shape as ``data`` where sources are labeled by different positive integer values. A value of zero is reserved for the background. label : int The label number of the source whose properties are calculated. filtered_data : array-like or `~astropy.units.Quantity`, optional The filtered version of the background-subtracted ``data`` from which to calculate the source centroid and morphological properties. The kernel used to perform the filtering should be the same one used in defining the source segments (e.g., see :func:`~photutils.segmentation.detect_sources`). If ``data`` is a `~astropy.units.Quantity` array then ``filtered_data`` must be a `~astropy.units.Quantity` array (and vice versa) with identical units. Non-finite ``filtered_data`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. If `None`, then the unfiltered ``data`` will be used instead. error : array_like or `~astropy.units.Quantity`, optional The total error array corresponding to the input ``data`` array. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`) . ``error`` must have the same shape as the input ``data``. If ``data`` is a `~astropy.units.Quantity` array then ``error`` must be a `~astropy.units.Quantity` array (and vice versa) with identical units. Non-finite ``error`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. See the Notes section below for details on the error propagation. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. Non-finite values (NaN and +/- inf) in the input ``data`` are automatically masked. background : float, array_like, or `~astropy.units.Quantity`, optional The background level that was *previously* present in the input ``data``. ``background`` may either be a scalar value or a 2D image with the same shape as the input ``data``. If ``data`` is a `~astropy.units.Quantity` array then ``background`` must be a `~astropy.units.Quantity` array (and vice versa) with identical units. Inputting the ``background`` merely allows for its properties to be measured within each source segment. The input ``background`` does *not* get subtracted from the input ``data``, which should already be background-subtracted. Non-finite ``background`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. wcs : WCS object or `None`, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). If `None`, then all sky-based properties will be set to `None`. localbkg_width : `None` or positive int, optional The width of the rectangular annulus used to compute a local background around each source. If `None` then no local background subtraction is performed. The local background affects the ``source_sum``, ``max_value``, ``min_value``, and ``kron_flux`` properties. It does not affect the moment-based morphological properties of the source. kron_params : tuple of list, optional A list of five parameters used to determine how the Kron radius and flux are calculated. The first item represents how data pixels are masked around the source. It must be one of: * 'none': do not mask any pixels (equivalent to MASK_TYPE=NONE in SourceExtractor). * 'mask': mask pixels assigned to neighboring sources (equivalent to MASK_TYPE=BLANK in SourceExtractor) * 'mask_all': mask all pixels outside of the source segment (deprecated). * 'correct': replace pixels assigned to neighboring sources by replacing them with pixels on the opposite side of the source (equivalent to MASK_TYPE=CORRECT in SourceExtractor). The second item represents the scaling parameter of the Kron radius as a scalar float. The third item represents the minimum circular radius as a scalar float. If the Kron radius times sqrt(``semimajor_axis_sigma`` * ``semiminor_axis_sigma``) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. The forth and fifth items represent the :func:`~photutils.aperture.aperture_photometry` keywords ``method`` and ``subpixels``, respectively, which are used to measure the flux in the Kron aperture. Notes ----- ``data`` (and optional ``filtered_data``) should be background-subtracted for accurate source photometry and properties. `SourceExtractor`_'s centroid and morphological parameters are always calculated from a filtered "detection" image, i.e., the image used to define the segmentation image. The usual downside of the filtering is the sources will be made more circular than they actually are. If you wish to reproduce `SourceExtractor`_ centroid and morphology results, then input a filtered and background-subtracted "detection" image into the ``filtered_data`` keyword. If ``filtered_data`` is `None`, then the unfiltered ``data`` will be used for the source centroid and morphological parameters. Negative data values (``filtered_data`` or ``data``) within the source segment are set to zero when calculating morphological properties based on image moments. Negative values could occur, for example, if the segmentation image was defined from a different image (e.g., different bandpass) or if the background was oversubtracted. Note that `~photutils.segmentation.SourceProperties.source_sum` always includes the contribution of negative ``data`` values. The input ``error`` array is assumed to include *all* sources of error, including the Poisson error of the sources. `~photutils.segmentation.SourceProperties.source_sum_err` is simply the quadrature sum of the pixel-wise total errors over the non-masked pixels within the source segment: .. math:: \Delta F = \sqrt{\sum_{i \in S} \sigma_{\mathrm{tot}, i}^2} where :math:`\Delta F` is `~photutils.segmentation.SourceProperties.source_sum_err`, :math:`S` are the non-masked pixels in the source segment, and :math:`\sigma_{\mathrm{tot}, i}` is the input ``error`` array. Custom errors for source segments can be calculated using the `~photutils.segmentation.SourceProperties.error_cutout_ma` and `~photutils.segmentation.SourceProperties.background_cutout_ma` properties, which are 2D `~numpy.ma.MaskedArray` cutout versions of the input ``error`` and ``background``. The mask is `True` for pixels outside of the source segment, masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ """ def __init__(self, data, segment_img, label, filtered_data=None, error=None, mask=None, background=None, wcs=None, localbkg_width=None, kron_params=('mask', 2.5, 0.0, 'exact', 5)): if not isinstance(segment_img, SegmentationImage): segment_img = SegmentationImage(segment_img) if segment_img.shape != data.shape: raise ValueError('segment_img and data must have the same shape.') inputs = (data, filtered_data, error, background) has_unit = [hasattr(x, 'unit') for x in inputs if x is not None] use_units = all(has_unit) if any(has_unit) and not use_units: raise ValueError('If any of data, filtered_data, error, or ' 'background has units, then they all must have ' 'the same units.') if use_units: self._data_unit = data.unit else: self._data_unit = 1 if error is not None: error = np.asanyarray(error) if error.shape != data.shape: raise ValueError('error and data must have the same shape.') if use_units and error.unit != self._data_unit: raise ValueError('error and data must have the same units.') if mask is np.ma.nomask: mask = None if mask is not None: mask = np.asanyarray(mask) if mask.shape != data.shape: raise ValueError('mask and data must have the same shape.') if background is not None: background = np.atleast_1d(background) if len(background) == 1: background = np.zeros(data.shape) + background else: background = np.asanyarray(background) if background.shape != data.shape: raise ValueError('background and data must have the same ' 'shape.') if use_units and background.unit != self._data_unit: raise ValueError('background and data must have the same ' 'units.') if filtered_data is not None: filtered_data = np.asanyarray(filtered_data) if filtered_data.shape != data.shape: raise ValueError('filtered_data and data must have the same ' 'shape.') if use_units and filtered_data.unit != self._data_unit: raise ValueError('filtered_data and data must have the same ' 'units.') self._filtered_data = filtered_data else: self._filtered_data = data self._data = data self._segment_img = segment_img self._error = error self._mask = mask self._background = background # 2D array self._wcs = wcs segment_img.check_labels(label) self.label = label self.segment = segment_img.segments[segment_img.get_index(label)] self.slices = self.segment.slices if localbkg_width is not None and localbkg_width <= 0: raise ValueError('localbkg_width must be >= 0') self.localbkg_width = localbkg_width if kron_params[0] not in ('none', 'mask', 'mask_all', 'correct'): raise ValueError('Invalid value for kron_params[0]') if kron_params[0] == 'mask_all': warnings.warn('The "mask_all" option is deprecated and will ' 'be removed in a future release.', AstropyDeprecationWarning) self.kron_params = kron_params self._kron_fluxerr = None def __str__(self): cls_name = f'<{self.__class__.__module__}.{self.__class__.__name__}>' cls_info = [] params = ['label', 'sky_centroid'] for param in params: cls_info.append((param, getattr(self, param))) fmt = ([f'{key}: {val}' for key, val in cls_info]) fmt.insert(1, 'centroid (x, y): ({0:0.4f}, {1:0.4f})' .format(self.xcentroid.value, self.ycentroid.value)) return f'{cls_name}\n' + '\n'.join(fmt) def __repr__(self): return self.__str__() @lazyproperty def _segment_mask(self): """ Boolean mask for source segment. ``_segment_mask`` is `True` for all pixels outside of the source segment for this label. Pixels from other source segments within the rectangular cutout are `True`. """ return self._segment_img.data[self.slices] != self.label @lazyproperty def _input_mask(self): """ Boolean mask for the user-input mask. """ if self._mask is not None: return self._mask[self.slices] else: return None @lazyproperty def _data_mask(self): """ Boolean mask for non-finite (NaN and +/- inf) ``data`` values. """ return ~np.isfinite(self.data_cutout) @lazyproperty def _total_mask(self): """ Boolean mask representing the combination of the ``_segment_mask``, ``_input_mask``, and ``_data_mask``. This mask is applied to ``data``, ``error``, and ``background`` inputs when calculating properties. """ mask = self._segment_mask | self._data_mask if self._input_mask is not None: mask |= self._input_mask return mask @lazyproperty def _is_completely_masked(self): """ `True` if all pixels within the source segment are masked, otherwise `False`. """ return np.all(self._total_mask) @lazyproperty def _data_zeroed(self): """ A 2D `~numpy.ndarray` cutout from the input ``data`` where any masked pixels (``_segment_mask``, ``_input_mask``, or ``_data_mask``) are set to zero. Invalid values (NaN and +/- inf) are set to zero via the ``_data_mask``. Any units are dropped on the input ``data``. This is a 2D array representation (with zeros as placeholders for the masked/removed values) of the 1D ``_data_values`` property, which is used for ``source_sum``, ``area``, ``min_value``, ``max_value``, ``minval_pos``, ``maxval_pos``, etc. """ if isinstance(self.data_cutout, u.Quantity): cutout = self.data_cutout.value else: cutout = self.data_cutout # NOTE: using np.where is faster than # _data = np.copy(self.data_cutout) # self._data[self._total_mask] = 0. return np.where(self._total_mask, 0, cutout).astype(float) # copy @lazyproperty def _filtered_data_zeroed(self): """ A 2D `~numpy.ndarray` cutout from the input ``filtered_data`` (or ``data`` if ``filtered_data`` is `None`) where any masked pixels (``_segment_mask``, ``_input_mask``, or ``_data_mask``) are set to zero. Invalid values (NaN and +/- inf) are set to zero. Any units are dropped on the input ``filtered_data`` (or ``data``). Negative data values are also set to zero because negative pixels (especially at large radii) can result in image moments that result in negative variances. This array is used for moment-based properties. """ filt_data = self._filtered_data[self.slices] if isinstance(filt_data, u.Quantity): filt_data = filt_data.value filt_data = np.where(self._total_mask, 0., filt_data) # copy filt_data[filt_data < 0] = 0. return filt_data.astype(float) def make_cutout(self, data, masked_array=False): """ Create a (masked) cutout array from the input ``data`` using the minimal bounding box of the source segment. If ``masked_array`` is `False` (default), then the returned cutout array is simply a `~numpy.ndarray`. The returned cutout is a view (not a copy) of the input ``data``. No pixels are altered (e.g., set to zero) within the bounding box. If ``masked_array` is `True`, then the returned cutout array is a `~numpy.ma.MaskedArray`. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). The data part of the masked array is a view (not a copy) of the input ``data``. Parameters ---------- data : array-like (2D) The data array from which to create the masked cutout array. ``data`` must have the same shape as the segmentation image input into `SourceProperties`. masked_array : bool, optional If `True` then a `~numpy.ma.MaskedArray` will be returned, where the mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). If `False`, then a `~numpy.ndarray` will be returned. Returns ------- result : 2D `~numpy.ndarray` or `~numpy.ma.MaskedArray` The 2D cutout array. """ data = np.asanyarray(data) if data.shape != self._segment_img.shape: raise ValueError('data must have the same shape as the ' 'segmentation image input to SourceProperties') if masked_array: return np.ma.masked_array(data[self.slices], mask=self._total_mask) else: return data[self.slices] def to_table(self, columns=None, exclude_columns=None): """ Create a `~astropy.table.QTable` of properties. If ``columns`` or ``exclude_columns`` are not input, then the `~astropy.table.QTable` will include a default list of scalar-valued properties. Parameters ---------- columns : str or list of str, optional Names of columns, in order, to include in the output `~astropy.table.QTable`. The allowed column names are any of the attributes of `SourceProperties`. exclude_columns : str or list of str, optional Names of columns to exclude from the default columns in the output `~astropy.table.QTable`. The default columns are defined in the ``photutils.segmentation.properties.DEFAULT_COLUMNS`` variable. Returns ------- table : `~astropy.table.QTable` A single-row table of properties of the source. """ return _properties_table(self, columns=columns, exclude_columns=exclude_columns) @lazyproperty def data_cutout(self): """ A 2D `~numpy.ndarray` cutout from the data using the minimal bounding box of the source segment. """ return self._data[self.slices] @lazyproperty def data_cutout_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the ``data``. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). """ return np.ma.masked_array(self._data[self.slices], mask=self._total_mask) @lazyproperty def filtered_data_cutout_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the ``filtered_data``. If ``filtered_data`` was not input, then the cutout will be from the input ``data``. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). """ return np.ma.masked_array(self._filtered_data[self.slices], mask=self._total_mask) @lazyproperty def error_cutout_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the input ``error`` image. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). If ``error`` is `None`, then ``error_cutout_ma`` is also `None`. """ if self._error is None: return None else: return np.ma.masked_array(self._error[self.slices], mask=self._total_mask) @lazyproperty def background_cutout_ma(self): """ A 2D `~numpy.ma.MaskedArray` cutout from the input ``background``. The mask is `True` for pixels outside of the source segment (labeled region of interest), masked pixels from the ``mask`` input, or any non-finite ``data`` values (NaN and +/- inf). If ``background`` is `None`, then ``background_cutout_ma`` is also `None`. """ if self._background is None: return None else: return np.ma.masked_array(self._background[self.slices], mask=self._total_mask) @lazyproperty def _data_values(self): """ A 1D `~numpy.ndarray` of the unmasked ``data`` values within the source segment. Non-finite pixel values (NaN and +/- inf) are excluded (automatically masked) via the ``_data_mask``. If all pixels are masked, an empty array will be returned. This array is used for ``source_sum``, ``area``, ``min_value``, ``max_value``, ``minval_pos``, ``maxval_pos``, etc. """ return self.data_cutout_ma.compressed() @lazyproperty def _filtered_data_values(self): return self.filtered_data_cutout_ma.compressed() @lazyproperty def _error_values(self): return self.error_cutout_ma.compressed() @lazyproperty def _background_values(self): return self.background_cutout_ma.compressed() @lazyproperty def indices(self): """ A tuple of two `~numpy.ndarray` containing the ``y`` and ``x`` pixel indices, respectively, of unmasked pixels within the source segment. Non-finite ``data`` values (NaN and +/- inf) are excluded. If all ``data`` pixels are masked, a tuple of two empty arrays will be returned. """ yindices, xindices = np.nonzero(self.data_cutout_ma) return (yindices + self.slices[0].start, xindices + self.slices[1].start) @lazyproperty def moments(self): """Spatial moments up to 3rd order of the source.""" return _moments(self._filtered_data_zeroed, order=3) @lazyproperty def moments_central(self): """ Central moments (translation invariant) of the source up to 3rd order. """ ycentroid, xcentroid = self.cutout_centroid.value return _moments_central(self._filtered_data_zeroed, center=(xcentroid, ycentroid), order=3) @lazyproperty def id(self): """ The source identification number corresponding to the object label in the segmentation image. """ return self.label @lazyproperty def cutout_centroid(self): """ The ``(y, x)`` coordinate, relative to the `data_cutout`, of the centroid within the source segment. """ moments = self.moments if moments[0, 0] != 0: ycentroid = moments[1, 0] / moments[0, 0] xcentroid = moments[0, 1] / moments[0, 0] return (ycentroid, xcentroid) * u.pix else: return (np.nan, np.nan) * u.pix @lazyproperty def centroid(self): """ The ``(y, x)`` coordinate of the centroid within the source segment. """ ycen, xcen = self.cutout_centroid.value return (ycen + self.slices[0].start, xcen + self.slices[1].start) * u.pix @lazyproperty def xcentroid(self): """ The ``x`` coordinate of the centroid within the source segment. """ return self.centroid[1] @lazyproperty def ycentroid(self): """ The ``y`` coordinate of the centroid within the source segment. """ return self.centroid[0] @lazyproperty def sky_centroid(self): """ The sky coordinates of the centroid within the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The output coordinate frame is the same as the input WCS. """ if self._wcs is None: return None return self._wcs.pixel_to_world(self.xcentroid.value, self.ycentroid.value) @lazyproperty def sky_centroid_icrs(self): """ The sky coordinates, in the International Celestial Reference System (ICRS) frame, of the centroid within the source segment, returned as a `~astropy.coordinates.SkyCoord` object. """ if self._wcs is None: return None else: return self.sky_centroid.icrs @lazyproperty def bbox(self): """ The `~photutils.aperture.BoundingBox` of the minimal rectangular region containing the source segment. """ return BoundingBox(self.slices[1].start, self.slices[1].stop, self.slices[0].start, self.slices[0].stop) @lazyproperty def bbox_xmin(self): """ The minimum ``x`` pixel location within the minimal bounding box containing the source segment. """ return self.bbox.ixmin * u.pix @lazyproperty def bbox_xmax(self): """ The maximum ``x`` pixel location within the minimal bounding box containing the source segment. Note that this value is inclusive, unlike numpy slice indices. """ return (self.bbox.ixmax - 1) * u.pix @lazyproperty def bbox_ymin(self): """ The minimum ``y`` pixel location within the minimal bounding box containing the source segment. """ return self.bbox.iymin * u.pix @lazyproperty def bbox_ymax(self): """ The maximum ``y`` pixel location within the minimal bounding box containing the source segment. Note that this value is inclusive, unlike numpy slice indices. """ return (self.bbox.iymax - 1) * u.pix @lazyproperty def sky_bbox_ll(self): """ The sky coordinates of the lower-left vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*. """ return _calc_sky_bbox_corner(self.bbox, 'll', self._wcs) @lazyproperty def sky_bbox_ul(self): """ The sky coordinates of the upper-left vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*. """ return _calc_sky_bbox_corner(self.bbox, 'ul', self._wcs) @lazyproperty def sky_bbox_lr(self): """ The sky coordinates of the lower-right vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*. """ return _calc_sky_bbox_corner(self.bbox, 'lr', self._wcs) @lazyproperty def sky_bbox_ur(self): """ The sky coordinates of the upper-right vertex of the minimal bounding box of the source segment, returned as a `~astropy.coordinates.SkyCoord` object. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*. """ return _calc_sky_bbox_corner(self.bbox, 'ur', self._wcs) @lazyproperty def min_value(self): """ The minimum pixel value of the ``data`` within the source segment. """ if self._is_completely_masked: return np.nan * self._data_unit else: return np.min(self._data_values - self.local_background) @lazyproperty def max_value(self): """ The maximum pixel value of the ``data`` within the source segment. """ if self._is_completely_masked: return np.nan * self._data_unit else: return np.max(self._data_values - self.local_background) @lazyproperty def minval_cutout_pos(self): """ The ``(y, x)`` coordinate, relative to the `data_cutout`, of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ if self._is_completely_masked: return (np.nan, np.nan) * u.pix else: arr = self.data_cutout_ma # multiplying by unit converts int to float, but keep as # float in case the array contains a NaN return np.asarray(np.unravel_index(np.argmin(arr), arr.shape)) * u.pix @lazyproperty def maxval_cutout_pos(self): """ The ``(y, x)`` coordinate, relative to the `data_cutout`, of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ if self._is_completely_masked: return (np.nan, np.nan) * u.pix else: arr = self.data_cutout_ma # multiplying by unit converts int to float, but keep as # float in case the array contains a NaN return np.asarray(np.unravel_index(np.argmax(arr), arr.shape)) * u.pix @lazyproperty def minval_pos(self): """ The ``(y, x)`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ if self._is_completely_masked: return (np.nan, np.nan) * u.pix else: yposition, xposition = self.minval_cutout_pos.value return (yposition + self.slices[0].start, xposition + self.slices[1].start) * u.pix @lazyproperty def maxval_pos(self): """ The ``(y, x)`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ if self._is_completely_masked: return (np.nan, np.nan) * u.pix else: yposition, xposition = self.maxval_cutout_pos.value return (yposition + self.slices[0].start, xposition + self.slices[1].start) * u.pix @lazyproperty def minval_xpos(self): """ The ``x`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ return self.minval_pos[1] @lazyproperty def minval_ypos(self): """ The ``y`` coordinate of the minimum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the minimum value, only the first occurrence is returned. """ return self.minval_pos[0] @lazyproperty def maxval_xpos(self): """ The ``x`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ return self.maxval_pos[1] @lazyproperty def maxval_ypos(self): """ The ``y`` coordinate of the maximum pixel value of the ``data`` within the source segment. If there are multiple occurrences of the maximum value, only the first occurrence is returned. """ return self.maxval_pos[0] @lazyproperty def source_sum(self): r""" The sum of the unmasked ``data`` values within the source segment. .. math:: F = \sum_{i \in S} (I_i - B_i) where :math:`F` is ``source_sum``, :math:`(I_i - B_i)` is the ``data``, and :math:`S` are the unmasked pixels in the source segment. Non-finite pixel values (NaN and +/- inf) are excluded (automatically masked). """ if self._is_completely_masked: return np.nan * self._data_unit # table output needs unit else: return (np.sum(self._data_values) - self.local_background * self.area.value) @lazyproperty def source_sum_err(self): r""" The uncertainty of `~photutils.segmentation.SourceProperties.source_sum`, propagated from the input ``error`` array. ``source_sum_err`` is the quadrature sum of the total errors over the non-masked pixels within the source segment: .. math:: \Delta F = \sqrt{\sum_{i \in S} \sigma_{\mathrm{tot}, i}^2} where :math:`\Delta F` is ``source_sum_err``, :math:`\sigma_{\mathrm{tot, i}}` are the pixel-wise total errors, and :math:`S` are the non-masked pixels in the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and +/- inf) that are automatically masked, are also masked in the error array. """ if self._error is not None: if self._is_completely_masked: return np.nan * self._data_unit # table output needs unit else: return np.sqrt(np.sum(self._error_values ** 2)) else: return None @lazyproperty def background_sum(self): """ The sum of ``background`` values within the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and +/- inf) that are automatically masked, are also masked in the background array. """ if self._background is not None: if self._is_completely_masked: return np.nan * self._data_unit # unit for table else: return np.sum(self._background_values) else: return None @lazyproperty def background_mean(self): """ The mean of ``background`` values within the source segment. Pixel values that are masked in the input ``data``, including any non-finite pixel values (NaN and +/- inf) that are automatically masked, are also masked in the background array. """ if self._background is not None: if self._is_completely_masked: return np.nan * self._data_unit # unit for table else: return np.mean(self._background_values) else: return None @lazyproperty def background_at_centroid(self): """ The value of the ``background`` at the position of the source centroid. The background value at fractional position values are determined using bilinear interpolation. """ if self._background is not None: from scipy.ndimage import map_coordinates # centroid can be NaN if segment is completely masked or if # all data values are <= 0 if np.any(~np.isfinite(self.centroid)): return np.nan * self._data_unit # unit for table else: value = map_coordinates(self._background, [[self.ycentroid.value], [self.xcentroid.value]], order=1, mode='nearest')[0] return value * self._data_unit else: return None @lazyproperty def area(self): """ The total unmasked area of the source segment in units of pixels**2. Note that the source area may be smaller than its segment area if a mask is input to `SourceProperties` or `source_properties`, or if the ``data`` within the segment contains invalid values (NaN and +/- inf). """ if self._is_completely_masked: return np.nan * u.pix**2 else: return len(self._data_values) * u.pix**2 @lazyproperty def equivalent_radius(self): """ The radius of a circle with the same `area` as the source segment. """ return np.sqrt(self.area / np.pi) @lazyproperty def perimeter(self): """ The perimeter of the source segment, approximated as the total length of lines connecting the centers of the border pixels defined by a 4-pixel connectivity. If any masked pixels make holes within the source segment, then the perimeter around the inner hole (e.g., an annulus) will also contribute to the total perimeter. References ---------- .. [1] K. Benkrid, D. Crookes, and A. Benkrid. "Design and FPGA Implementation of a Perimeter Estimator". Proceedings of the Irish Machine Vision and Image Processing Conference, pp. 51-57 (2000). http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc """ if self._is_completely_masked: return np.nan * u.pix # unit for table else: from scipy.ndimage import binary_erosion, convolve data = ~self._total_mask selem = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) data_eroded = binary_erosion(data, selem, border_value=0) border = np.logical_xor(data, data_eroded).astype(int) kernel = np.array([[10, 2, 10], [2, 1, 2], [10, 2, 10]]) perimeter_data = convolve(border, kernel, mode='constant', cval=0) size = 34 perimeter_hist = np.bincount(perimeter_data.ravel(), minlength=size) weights = np.zeros(size, dtype=float) weights[[5, 7, 15, 17, 25, 27]] = 1. weights[[21, 33]] = np.sqrt(2.) weights[[13, 23]] = (1 + np.sqrt(2.)) / 2. return (perimeter_hist[0:size] @ weights) * u.pix @lazyproperty def inertia_tensor(self): """ The inertia tensor of the source for the rotation around its center of mass. """ moments = self.moments_central mu_02 = moments[0, 2] mu_11 = -moments[1, 1] mu_20 = moments[2, 0] return np.array([[mu_02, mu_11], [mu_11, mu_20]]) * u.pix**2 @lazyproperty def covariance(self): """ The covariance matrix of the 2D Gaussian function that has the same second-order moments as the source. """ moments = self.moments_central if moments[0, 0] != 0: mu_norm = moments / moments[0, 0] covariance = self._check_covariance( np.array([[mu_norm[0, 2], mu_norm[1, 1]], [mu_norm[1, 1], mu_norm[2, 0]]])) return covariance * u.pix**2 else: return np.empty((2, 2)) * np.nan * u.pix**2 @staticmethod def _check_covariance(covariance): """ Check and modify the covariance matrix in the case of "infinitely" thin detections. This follows SourceExtractor's prescription of incrementally increasing the diagonal elements by 1/12. """ increment = 1. / 12 # arbitrary SourceExtractor value value = (covariance[0, 0] * covariance[1, 1]) - covariance[0, 1]**2 if value >= increment**2: return covariance else: covar = np.copy(covariance) while value < increment**2: covar[0, 0] += increment covar[1, 1] += increment value = (covar[0, 0] * covar[1, 1]) - covar[0, 1]**2 return covar @lazyproperty def covariance_eigvals(self): """ The two eigenvalues of the `covariance` matrix in decreasing order. """ unit = u.pix**2 # eigvals unit if np.any(~np.isfinite(self.covariance.value)): return (np.nan, np.nan) * unit else: eigvals = np.linalg.eigvals(self.covariance.value) if np.any(eigvals < 0): # negative variance return (np.nan, np.nan) * unit # pragma: no cover return (np.max(eigvals), np.min(eigvals)) * unit @lazyproperty def semimajor_axis_sigma(self): """ The 1-sigma standard deviation along the semimajor axis of the 2D Gaussian function that has the same second-order central moments as the source. """ # this matches SourceExtractor's A parameter return np.sqrt(self.covariance_eigvals[0]) @lazyproperty def semiminor_axis_sigma(self): """ The 1-sigma standard deviation along the semiminor axis of the 2D Gaussian function that has the same second-order central moments as the source. """ # this matches SourceExtractor's B parameter return np.sqrt(self.covariance_eigvals[1]) @lazyproperty def eccentricity(self): r""" The eccentricity of the 2D Gaussian function that has the same second-order moments as the source. The eccentricity is the fraction of the distance along the semimajor axis at which the focus lies. .. math:: e = \sqrt{1 - \frac{b^2}{a^2}} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. """ semimajor_var, semiminor_var = self.covariance_eigvals if semimajor_var == 0: return 0. # pragma: no cover return np.sqrt(1. - (semiminor_var / semimajor_var)) @lazyproperty def orientation(self): """ The angle between the ``x`` axis and the major axis of the 2D Gaussian function that has the same second-order moments as the source. The angle increases in the counter-clockwise direction. """ covar_00, covar_01, _, covar_11 = self.covariance.flat if covar_00 < 0 or covar_11 < 0: # negative variance return np.nan * u.deg # pragma: no cover # Quantity output in radians because inputs are Quantities orient_radians = 0.5 * np.arctan2(2. * covar_01, (covar_00 - covar_11)) return orient_radians.to(u.deg) @lazyproperty def elongation(self): r""" The ratio of the lengths of the semimajor and semiminor axes: .. math:: \mathrm{elongation} = \frac{a}{b} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. Note that this is the same as `SourceExtractor`_'s elongation parameter. """ return self.semimajor_axis_sigma / self.semiminor_axis_sigma @lazyproperty def ellipticity(self): r""" ``1`` minus the ratio of the lengths of the semimajor and semiminor axes (or ``1`` minus the `elongation`): .. math:: \mathrm{ellipticity} = 1 - \frac{b}{a} where :math:`a` and :math:`b` are the lengths of the semimajor and semiminor axes, respectively. Note that this is the same as `SourceExtractor`_'s ellipticity parameter. """ return 1.0 - (self.semiminor_axis_sigma / self.semimajor_axis_sigma) @lazyproperty def covar_sigx2(self): r""" The ``(0, 0)`` element of the `covariance` matrix, representing :math:`\sigma_x^2`, in units of pixel**2. Note that this is the same as `SourceExtractor`_'s X2 parameter. """ return self.covariance[0, 0] @lazyproperty def covar_sigy2(self): r""" The ``(1, 1)`` element of the `covariance` matrix, representing :math:`\sigma_y^2`, in units of pixel**2. Note that this is the same as `SourceExtractor`_'s Y2 parameter. """ return self.covariance[1, 1] @lazyproperty def covar_sigxy(self): r""" The ``(0, 1)`` and ``(1, 0)`` elements of the `covariance` matrix, representing :math:`\sigma_x \sigma_y`, in units of pixel**2. Note that this is the same as `SourceExtractor`_'s XY parameter. """ return self.covariance[0, 1] @lazyproperty def cxx(self): r""" `SourceExtractor`_'s CXX ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return ((np.cos(self.orientation) / self.semimajor_axis_sigma)**2 + (np.sin(self.orientation) / self.semiminor_axis_sigma)**2) @lazyproperty def cyy(self): r""" `SourceExtractor`_'s CYY ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return ((np.sin(self.orientation) / self.semimajor_axis_sigma)**2 + (np.cos(self.orientation) / self.semiminor_axis_sigma)**2) @lazyproperty def cxy(self): r""" `SourceExtractor`_'s CXY ellipse parameter in units of pixel**(-2). The ellipse is defined as .. math:: cxx (x - \bar{x})^2 + cxy (x - \bar{x}) (y - \bar{y}) + cyy (y - \bar{y})^2 = R^2 where :math:`R` is a parameter which scales the ellipse (in units of the axes lengths). `SourceExtractor`_ reports that the isophotal limit of a source is well represented by :math:`R \approx 3`. """ return (2. * np.cos(self.orientation) * np.sin(self.orientation) * ((1. / self.semimajor_axis_sigma**2) - (1. / self.semiminor_axis_sigma**2))) @lazyproperty def local_background_aperture(self): """ The rectangular annulus aperture used to estimate the local background. """ if self.localbkg_width is None: return None xpos = 0.5 * (self.bbox.ixmin + self.bbox.ixmax - 1) ypos = 0.5 * (self.bbox.iymin + self.bbox.iymax - 1) scale = 1.5 width_bbox = self.bbox.ixmax - self.bbox.ixmin width_in = width_bbox * scale width_out = width_in + 2 * self.localbkg_width height_bbox = self.bbox.iymax - self.bbox.iymin height_in = height_bbox * scale height_out = height_in + 2 * self.localbkg_width return RectangularAnnulus((xpos, ypos), width_in, width_out, height_out, height_in, theta=0.) @lazyproperty def local_background(self): """ The local background value estimated using a rectangular annulus aperture around the source. """ if self.localbkg_width is None: local_bkg = 0. elif self._is_completely_masked: local_bkg = np.nan else: aperture = self.local_background_aperture aperture_mask = aperture.to_mask(method='center') mask = ~np.isfinite(self._data) if self._mask is not None: mask |= self._mask mask |= self._segment_img.data.astype(bool) values = aperture_mask.get_values(self._data, mask=mask) if len(values) < 10: # not enough unmasked pixels return 0. sigma_clip = SigmaClip(sigma=3.0, cenfunc='median', maxiters=20) bkg_func = SExtractorBackground(sigma_clip) if isinstance(values, u.Quantity): local_bkg = bkg_func(values.value) else: local_bkg = bkg_func(values) if self._data_unit != 1: local_bkg <<= self._data_unit return local_bkg def _elliptical_aperture(self, radius=6.): """ Parameters ---------- radius : float, optional The elliptical "radius". The default value of 6.0 is roughly two times the isophotal extent of the source. """ position = (self.xcentroid.value, self.ycentroid.value) a = self.semimajor_axis_sigma.value * radius b = self.semiminor_axis_sigma.value * radius theta = self.orientation.to(u.radian).value values = (position[0], position[1], a, b, theta) if np.any(~np.isfinite(values)): return None return EllipticalAperture(position, a, b, theta=theta) def _mask_neighbors(self, aperture_mask, method='none'): if method == 'none': return None segment_img = aperture_mask.cutout(self._segment_img.data, copy=True) # mask all pixels outside of the source segment if method in ('mask_all', ): segm_mask = (segment_img != self.id) # mask pixels *only* in neighboring segments (not including # background pixels) if method in ('mask', 'correct'): segm_mask = np.logical_and(segment_img != self.id, segment_img != 0) return segm_mask def _prepare_kron_data(self, aperture_mask, sub_bkg=False, correct=False): apply_correct = correct and (self.kron_params[0] == 'correct') mask = ~np.isfinite(self._data) if self._mask is not None: mask |= self._mask data = aperture_mask.cutout(self._data, copy=True, fill_value=np.nan) mask = aperture_mask.cutout(mask) | np.isnan(data) segm_mask = self._mask_neighbors(aperture_mask, method=self.kron_params[0]) if segm_mask is not None and not apply_correct: mask |= segm_mask if sub_bkg: data -= self.local_background data[mask] = 0. if self._error is not None: error = aperture_mask.cutout(self._error, copy=True) error[mask] = 0. else: error = None # Correct masked pixels in neighboring segments. Masked pixels # are replaced with pixels on the opposite side of the source. if apply_correct: from ._utils import mask_to_mirrored_value xycen = (self.xcentroid.value - aperture_mask.bbox.ixmin, self.ycentroid.value - aperture_mask.bbox.iymin) data = mask_to_mirrored_value(data, segm_mask, xycen) if self._error is not None: error = mask_to_mirrored_value(error, segm_mask, xycen) return data, error @lazyproperty def kron_radius(self): r""" The unscaled first-moment Kron radius. The unscaled first-moment Kron radius is given by: .. math:: k_r = \frac{\sum_{i \in A} \ r_i I_i}{\sum_{i \in A} I_i} where the sum is over all pixels in an elliptical aperture whose axes are defined by six times the ``semimajor_axis_sigma`` and ``semiminor_axis_sigma`` at the calculated ``orientation`` (all properties derived from the central image moments of the source). :math:`r_i` is the elliptical "radius" to the pixel given by: .. math:: r_i^2 = cxx(x_i - \bar{x})^2 + cxx \ cyy (x_i - \bar{x})(y_i - \bar{y}) + cyy(y_i - \bar{y})^2 where :math:`\bar{x}` and :math:`\bar{y}` represent the source centroid. If either the numerator or denominator <= 0, then ``np.nan`` will be returned. In this case, the Kron aperture will be defined as a circular aperture with a radius equal to ``kron_params[2]``. If ``kron_params[2] <= 0``, then the Kron aperture will be `None` and the Kron flux will be ``np.nan``. """ aperture = self._elliptical_aperture(radius=6.0) if aperture is None: return np.nan << u.pixel aperture_mask = aperture.to_mask() # prepare cutouts of the data and error arrays based on the # aperture size data, _ = self._prepare_kron_data(aperture_mask, sub_bkg=False, correct=False) aperture.positions -= (aperture_mask.bbox.ixmin, aperture_mask.bbox.iymin) x = (np.arange(data.shape[1]) - self.xcentroid.value + aperture_mask.bbox.ixmin) y = (np.arange(data.shape[0]) - self.ycentroid.value + aperture_mask.bbox.iymin) xx, yy = np.meshgrid(x, y) rr = np.sqrt(self.cxx.value * xx**2 + self.cxy.value * xx * yy + self.cyy.value * yy**2) method = 'center' # need whole pixel to compute Kron radius if isinstance(data, u.Quantity): data = data.value flux_numer, _ = aperture.do_photometry(data * rr, method=method) flux_denom, _ = aperture.do_photometry(data, method=method) if flux_numer <= 0 or flux_denom <= 0: return np.nan << u.pixel return (flux_numer[0] / flux_denom[0]) << u.pixel @lazyproperty def kron_aperture(self): """ The Kron aperture. If ``kron_radius = np.nan`` or ``kron_radius * np.sqrt(semimajor_axis_sigma * semiminor_axis_sigma) < kron_params[2]`` then a circular aperture with a radius equal to ``kron_params[2]`` will be returned. If ``kron_params[2] <= 0``, then the Kron aperture will be `None`, and the Kron flux will be ``np.nan``. """ a = self.semimajor_axis_sigma.value b = self.semiminor_axis_sigma.value circ_radius = self.kron_radius.value * np.sqrt(a * b) min_radius = self.kron_params[2] if np.isnan(self.kron_radius.value) or circ_radius < min_radius: if min_radius <= 0: return None # use circular aperture with radius=self.kron_params[2] xypos = (self.xcentroid.value, self.ycentroid.value) values = (xypos[0], xypos[1], self.kron_params[2]) if np.any(~np.isfinite(values)): return None aperture = CircularAperture(xypos, r=self.kron_params[2]) else: radius = self.kron_radius.value * self.kron_params[1] aperture = self._elliptical_aperture(radius=radius) return aperture @lazyproperty def kron_flux(self): """ The flux in the Kron aperture. If the Kron aperture is `None`, then ``np.nan`` will be returned. """ if self.kron_aperture is None: self._kron_fluxerr = np.nan * self._data_unit return np.nan * self._data_unit aperture = deepcopy(self.kron_aperture) aperture_mask = aperture.to_mask() data, error = self._prepare_kron_data(aperture_mask, sub_bkg=True, correct=True) aperture.positions -= (aperture_mask.bbox.ixmin, aperture_mask.bbox.iymin) method = self.kron_params[3] subpixels = self.kron_params[4] flux, fluxerr = aperture.do_photometry(data, error=error, method=method, subpixels=subpixels) if len(fluxerr) > 0: self._kron_fluxerr = fluxerr[0] else: self._kron_fluxerr = None return flux[0] @lazyproperty def kron_fluxerr(self): """ The flux error in the Kron aperture. If the Kron aperture is `None`, then ``np.nan`` will be returned. """ if self._kron_fluxerr is None: _ = self.kron_flux # run kron_flux to computer kron_fluxerr if self._error is None: return None return self._kron_fluxerr @lazyproperty def gini(self): r""" The `Gini coefficient `_ of the source. The Gini coefficient is calculated using the prescription from `Lotz et al. 2004 `_ as: .. math:: G = \frac{1}{\left | \bar{x} \right | n (n - 1)} \sum^{n}_{i} (2i - n - 1) \left | x_i \right | where :math:`\bar{x}` is the mean over all pixel values :math:`x_i`. The Gini coefficient is a way of measuring the inequality in a given set of values. In the context of galaxy morphology, it measures how the light of a galaxy image is distributed among its pixels. A Gini coefficient value of 0 corresponds to a galaxy image with the light evenly distributed over all pixels while a Gini coefficient value of 1 represents a galaxy image with all its light concentrated in just one pixel. """ npix = np.size(self._data_values) normalization = (np.abs(np.mean(self._data_values)) * npix * (npix - 1)) kernel = ((2. * np.arange(1, npix + 1) - npix - 1) * np.abs(np.sort(self._data_values))) return np.sum(kernel) / normalization @deprecated('1.1', alternative='`~photutils.segmentation.SourceCatalog`') def source_properties(data, segment_img, error=None, mask=None, background=None, filter_kernel=None, wcs=None, labels=None, localbkg_width=None, kron_params=('mask', 2.5, 0.0, 'exact', 5)): r""" Calculate photometry and morphological properties of sources defined by a labeled segmentation image (deprecated). Parameters ---------- data : array_like or `~astropy.units.Quantity` The 2D array from which to calculate the source photometry and properties. ``data`` should be background-subtracted. Non-finite ``data`` values (NaN and +/- inf) are automatically masked. segment_img : `SegmentationImage` or array_like (int) A 2D segmentation image, either as a `SegmentationImage` object or an `~numpy.ndarray`, with the same shape as ``data`` where sources are labeled by different positive integer values. A value of zero is reserved for the background. error : array_like or `~astropy.units.Quantity`, optional The total error array corresponding to the input ``data`` array. ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources (see `~photutils.utils.calc_total_error`) . ``error`` must have the same shape as the input ``data``. Non-finite ``error`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. See the Notes section below for details on the error propagation. mask : array_like (bool), optional A boolean mask with the same shape as ``data`` where a `True` value indicates the corresponding element of ``data`` is masked. Masked data are excluded from all calculations. Non-finite values (NaN and +/- inf) in the input ``data`` are automatically masked. background : float, array_like, or `~astropy.units.Quantity`, optional The background level that was *previously* present in the input ``data``. ``background`` may either be a scalar value or a 2D image with the same shape as the input ``data``. Inputting the ``background`` merely allows for its properties to be measured within each source segment. The input ``background`` does *not* get subtracted from the input ``data``, which should already be background-subtracted. Non-finite ``background`` values (NaN and +/- inf) are not automatically masked, unless they are at the same position of non-finite values in the input ``data`` array. Such pixels can be masked using the ``mask`` keyword. filter_kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional The 2D array of the kernel used to filter the data prior to calculating the source centroid and morphological parameters. The kernel should be the same one used in defining the source segments, i.e., the detection image (e.g., see :func:`~photutils.segmentation.detect_sources`). If `None`, then the unfiltered ``data`` will be used instead. wcs : `None` or WCS object, optional A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). If `None`, then all sky-based properties will be set to `None`. labels : int, array-like (1D, int) The segmentation labels for which to calculate source properties. If `None` (default), then the properties will be calculated for all labeled sources. localbkg_width : `None` or positive int, optional The width of the rectangular annulus used to compute a local background around each source. If `None` then no local background subtraction is performed. The local background affects the ``source_sum``, ``max_value``, ``min_value``, and ``kron_flux`` properties. It does not affect the moment-based morphological properties of the source. kron_params : tuple of list, optional A list of five parameters used to determine how the Kron radius and flux are calculated. The first item represents how data pixels are masked around the source. It must be one of: * 'none': do not mask any pixels (equivalent to MASK_TYPE=NONE in SourceExtractor). * 'mask': mask pixels assigned to neighboring sources (equivalent to MASK_TYPE=BLANK in SourceExtractor) * 'mask_all': mask all pixels outside of the source segment. * 'correct': replace pixels assigned to neighboring sources by replacing them with pixels on the opposite side of the source (equivalent to MASK_TYPE=CORRECT in SourceExtractor). The second item represents the scaling parameter of the Kron radius as a scalar float. The third item represents the minimum circular radius as a scalar float. If the Kron radius times sqrt(``semimajor_axis_sigma`` * ``semiminor_axis_sigma``) is less than than this radius, then the Kron flux will be measured in a circle with this minimum radius. The forth and fifth items represent the :func:`~photutils.aperture.aperture_photometry` keywords ``method`` and ``subpixels``, respectively, which are used to measure the flux in the Kron aperture. Returns ------- output : `LegacySourceCatalog` instance A `LegacySourceCatalog` instance containing the properties of each source. Notes ----- `SourceExtractor`_'s centroid and morphological parameters are always calculated from a filtered "detection" image, i.e., the image used to define the segmentation image. The usual downside of the filtering is the sources will be made more circular than they actually are. If you wish to reproduce `SourceExtractor`_ centroid and morphology results, then input a filtered and background-subtracted "detection" image into the ``filtered_data`` keyword. If ``filtered_data`` is `None`, then the unfiltered ``data`` will be used for the source centroid and morphological parameters. Negative data values (``filtered_data`` or ``data``) within the source segment are set to zero when calculating morphological properties based on image moments. Negative values could occur, for example, if the segmentation image was defined from a different image (e.g., different bandpass) or if the background was oversubtracted. Note that `~photutils.segmentation.SourceProperties.source_sum` always includes the contribution of negative ``data`` values. The input ``error`` is assumed to include *all* sources of error, including the Poisson error of the sources. `~photutils.segmentation.SourceProperties.source_sum_err` is simply the quadrature sum of the pixel-wise total errors over the non-masked pixels within the source segment: .. math:: \Delta F = \sqrt{\sum_{i \in S} \sigma_{\mathrm{tot}, i}^2} where :math:`\Delta F` is `~photutils.segmentation.SourceProperties.source_sum_err`, :math:`S` are the non-masked pixels in the source segment, and :math:`\sigma_{\mathrm{tot}, i}` is the input ``error`` array. .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ See Also -------- SegmentationImage, SourceProperties, detect_sources """ if not isinstance(segment_img, SegmentationImage): segment_img = SegmentationImage(segment_img) if segment_img.shape != data.shape: raise ValueError('segment_img and data must have the same shape.') # filter the data once, instead of repeating for each source if filter_kernel is not None: filtered_data = _filter_data(data, filter_kernel, mode='constant', fill_value=0.0, check_normalization=True) else: filtered_data = None if labels is None: labels = segment_img.labels labels = np.atleast_1d(labels) sources_props = [] for label in labels: if label not in segment_img.labels: warnings.warn('label {} is not in the segmentation image.' .format(label), AstropyUserWarning) continue # skip invalid labels sources_props.append(SourceProperties( data, segment_img, label, filtered_data=filtered_data, error=error, mask=mask, background=background, wcs=wcs, localbkg_width=localbkg_width, kron_params=kron_params)) if not sources_props: raise ValueError('No sources are defined.') return LegacySourceCatalog(sources_props, wcs=wcs) @deprecated('1.1', alternative='`~photutils.segmentation.SourceCatalog`') class LegacySourceCatalog: """ Class to hold source catalogs (deprecated). """ def __init__(self, properties_list, wcs=None): if isinstance(properties_list, SourceProperties): self._data = [properties_list] elif isinstance(properties_list, list): if not properties_list: raise ValueError('properties_list must not be an empty list.') self._data = properties_list else: raise ValueError('invalid input.') self.wcs = wcs self._cache = {} def __len__(self): return len(self._data) def __getitem__(self, index): return self._data[index] def __delitem__(self, index): del self._data[index] def __iter__(self): for i in self._data: yield i def __str__(self): cls_name = f'<{self.__class__.__module__}.{self.__class__.__name__}>' fmt = [f'Catalog length: {len(self)}'] return f'{cls_name}\n' + '\n'.join(fmt) def __repr__(self): return self.__str__() def __getattr__(self, attr): if attr not in self._cache: values = [getattr(p, attr) for p in self._data] if isinstance(values[0], u.Quantity): # turn list of Quantities into a Quantity array values = u.Quantity(values) if isinstance(values[0], SkyCoord): # pragma: no cover # failsafe: turn list of SkyCoord into a SkyCoord array values = SkyCoord(values) self._cache[attr] = values return self._cache[attr] @lazyproperty def _none_list(self): """ Return a list of `None` values, used by SkyCoord properties if ``wcs`` is `None`. """ return [None] * len(self._data) @lazyproperty def background_at_centroid(self): background = self._data[0]._background if background is None: return self._none_list else: from scipy.ndimage import map_coordinates values = map_coordinates(background, [[self.ycentroid.value], [self.xcentroid.value]], order=1, mode='nearest')[0] mask = np.isfinite(self.xcentroid) & np.isfinite(self.ycentroid) values[~mask] = np.nan return values * self._data[0]._data_unit @lazyproperty def sky_centroid(self): if self.wcs is None: return self._none_list else: # For a large catalog, it's much faster to calculate world # coordinates using the complete list of (x, y) instead of # looping through the individual (x, y). It's also much # faster to recalculate the world coordinates than to create a # SkyCoord array from a loop-generated SkyCoord list. The # assumption here is that the wcs is the same for each # SourceProperties instance. return self.wcs.pixel_to_world(self.xcentroid.value, self.ycentroid.value) @lazyproperty def sky_centroid_icrs(self): if self.wcs is None: return self._none_list else: return self.sky_centroid.icrs @lazyproperty def sky_bbox_ll(self): if self.wcs is None: return self._none_list else: return _calc_sky_bbox_corner(self.bbox, 'll', self.wcs) @lazyproperty def sky_bbox_ul(self): if self.wcs is None: return self._none_list else: return _calc_sky_bbox_corner(self.bbox, 'ul', self.wcs) @lazyproperty def sky_bbox_lr(self): if self.wcs is None: return self._none_list else: return _calc_sky_bbox_corner(self.bbox, 'lr', self.wcs) @lazyproperty def sky_bbox_ur(self): if self.wcs is None: return self._none_list else: return _calc_sky_bbox_corner(self.bbox, 'ur', self.wcs) def to_table(self, columns=None, exclude_columns=None): """ Construct a `~astropy.table.QTable` of source properties from a `LegacySourceCatalog` object. If ``columns`` or ``exclude_columns`` are not input, then the `~astropy.table.QTable` will include a default list of scalar-valued properties. Multi-dimensional properties, e.g., `~photutils.segmentation.SourceProperties.data_cutout`, can be included in the ``columns`` input, but they will not be preserved when writing the table to a file. This is a limitation of multi-dimensional columns in astropy tables. Parameters ---------- columns : str or list of str, optional Names of columns, in order, to include in the output `~astropy.table.QTable`. The allowed column names are any of the attributes of `SourceProperties`. exclude_columns : str or list of str, optional Names of columns to exclude from the default columns in the output `~astropy.table.QTable`. The default columns are defined in the ``photutils.segmentation.properties.DEFAULT_COLUMNS`` variable. Returns ------- table : `~astropy.table.QTable` A table of source properties with one row per source. See Also -------- SegmentationImage, SourceProperties, source_properties, detect_sources """ return _properties_table(self, columns=columns, exclude_columns=exclude_columns) def _properties_table(obj, columns=None, exclude_columns=None): """ Construct a `~astropy.table.QTable` of source properties from a `SourceProperties` or `LegacySourceCatalog` object. Parameters ---------- obj : `SourceProperties` or `LegacySourceCatalog` instance The object containing the source properties. columns : str or list of str, optional Names of columns, in order, to include in the output `~astropy.table.QTable`. The allowed column names are any of the attributes of `SourceProperties`. exclude_columns : str or list of str, optional Names of columns to exclude from the default columns in the output `~astropy.table.QTable`. The default columns are defined in the ``photutils.segmentation.properties.DEFAULT_COLUMNS`` variable. Returns ------- table : `~astropy.table.QTable` A table of source properties with one row per source. """ # start with the default columns columns_all = DEFAULT_COLUMNS table_columns = None if exclude_columns is not None: table_columns = [s for s in columns_all if s not in exclude_columns] if columns is not None: table_columns = np.atleast_1d(columns) if table_columns is None: table_columns = columns_all tbl = QTable() for column in table_columns: values = getattr(obj, column) if isinstance(obj, SourceProperties): # turn scalar values into length-1 arrays because QTable # column assignment requires an object with a length values = np.atleast_1d(values) # Unfortunately np.atleast_1d creates an array of SkyCoord # instead of a SkyCoord array (Quantity does work correctly # with np.atleast_1d). Here we make a SkyCoord array for # the output table column. if isinstance(values[0], SkyCoord): values = SkyCoord(values) # length-1 SkyCoord array tbl[column] = values return tbl def _calc_sky_bbox_corner(bbox, corner, wcs): """ Calculate the sky coordinates at the corner of a minimal bounding box. The bounding box encloses all of the source segment pixels in their entirety, thus the vertices are at the pixel *corners*. Parameters ---------- bbox : `~photutils.aperture.BoundingBox` The source bounding box. corner : {'ll', 'ul', 'lr', 'ur'} The desired bounding box corner: * 'll': lower left * 'ul': upper left * 'lr': lower right * 'ur': upper right wcs : `None` or WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- skycoord : `~astropy.coordinates.SkyCoord` or `None` The sky coordinate at the bounding box corner. If ``wcs`` is `None`, then `None` will be returned. """ if wcs is None: return None if corner == 'll': xpos = bbox.ixmin - 0.5 ypos = bbox.iymin - 0.5 elif corner == 'ul': xpos = bbox.ixmin - 0.5 ypos = bbox.iymax + 0.5 elif corner == 'lr': xpos = bbox.ixmax + 0.5 ypos = bbox.iymin - 0.5 elif corner == 'ur': xpos = bbox.ixmax + 0.5 ypos = bbox.iymax + 0.5 else: raise ValueError('Invalid corner name.') return wcs.pixel_to_world(xpos, ypos) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0216243 photutils-1.3.0/photutils/segmentation/tests/0000755000214200020070000000000000000000000020070 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/segmentation/tests/__init__.py0000644000214200020070000000000000000000000022167 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636688958.0 photutils-1.3.0/photutils/segmentation/tests/test_catalog.py0000644000214200020070000006435600000000000023131 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the catalog module. """ from astropy.coordinates import SkyCoord from astropy.modeling.models import Gaussian2D from astropy.table import QTable import astropy.units as u from numpy.testing import assert_allclose, assert_equal, assert_raises import numpy as np import pytest from ..catalog import SourceCatalog from ..core import SegmentationImage from ..detect import detect_sources from ...aperture import CircularAperture, EllipticalAperture from ...datasets import make_gwcs, make_wcs, make_noise_image from ...utils._optional_deps import HAS_GWCS, HAS_MATPLOTLIB, HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') class TestSourceCatalog: def setup_class(self): xcen = 51. ycen = 52.7 major_sigma = 8. minor_sigma = 3. theta = np.pi / 6. g1 = Gaussian2D(111., xcen, ycen, major_sigma, minor_sigma, theta=theta) g2 = Gaussian2D(50, 20, 80, 5.1, 4.5) g3 = Gaussian2D(70, 75, 18, 9.2, 4.5) g4 = Gaussian2D(111., 11.1, 12.2, major_sigma, minor_sigma, theta=theta) g5 = Gaussian2D(81., 61, 42.7, major_sigma, minor_sigma, theta=theta) g6 = Gaussian2D(107., 75, 61, major_sigma, minor_sigma, theta=-theta) g7 = Gaussian2D(107., 90, 90, 4, 2, theta=-theta) yy, xx = np.mgrid[0:101, 0:101] self.data = (g1(xx, yy) + g2(xx, yy) + g3(xx, yy) + g4(xx, yy) + g5(xx, yy) + g6(xx, yy) + g7(xx, yy)) threshold = 27. self.segm = detect_sources(self.data, threshold, npixels=5) self.error = make_noise_image(self.data.shape, mean=0, stddev=2., seed=123) self.background = np.ones(self.data.shape) * 5.1 self.mask = np.zeros(self.data.shape, dtype=bool) self.mask[0:30, 0:30] = True self.wcs = make_wcs(self.data.shape) self.cat = SourceCatalog(self.data, self.segm, error=self.error, background=self.background, mask=self.mask, wcs=self.wcs, localbkg_width=24) unit = u.nJy self.unit = unit self.cat_units = SourceCatalog(self.data << unit, self.segm, error=self.error << unit, background=self.background << unit, mask=self.mask, wcs=self.wcs, localbkg_width=24) @pytest.mark.parametrize('with_units', (True, False)) def test_catalog(self, with_units): props1 = ('background_centroid', 'background_mean', 'background_sum', 'bbox', 'covar_sigx2', 'covar_sigxy', 'covar_sigy2', 'cxx', 'cxy', 'cyy', 'ellipticity', 'elongation', 'fwhm', 'equivalent_radius', 'gini', 'kron_radius', 'maxval_xindex', 'maxval_yindex', 'minval_xindex', 'minval_yindex', 'perimeter', 'sky_bbox_ll', 'sky_bbox_lr', 'sky_bbox_ul', 'sky_bbox_ur', 'sky_centroid_icrs', 'local_background', 'segment_flux', 'segment_fluxerr', 'kron_flux', 'kron_fluxerr') props2 = ('centroid', 'covariance', 'covariance_eigvals', 'cutout_centroid', 'cutout_maxval_index', 'cutout_minval_index', 'inertia_tensor', 'maxval_index', 'minval_index', 'moments', 'moments_central', 'background', 'background_ma', 'convdata', 'convdata_ma', 'data', 'data_ma', 'error', 'error_ma', 'segment', 'segment_ma') props = tuple(self.cat.default_columns) + props1 + props2 if with_units: cat1 = self.cat_units.copy() cat2 = self.cat_units.copy() else: cat1 = self.cat.copy() cat2 = self.cat.copy() # test extra properties cat1.circular_photometry(5.0, name='circ5') cat1.kron_photometry((2.0, 1.0), name='kron2') cat1.fluxfrac_radius(0.5, name='r_hl') segment_snr = cat1.segment_flux / cat1.segment_fluxerr cat1.add_extra_property('segment_snr', segment_snr) props = list(props) props.extend(cat1.extra_properties) idx = 1 # evaluate (cache) catalog properties before slice obj = cat1[idx] for prop in props: assert_equal(getattr(cat1, prop)[idx], getattr(obj, prop)) # slice catalog before evaluating catalog properties obj = cat2[idx] obj.circular_photometry(5.0, name='circ5') obj.kron_photometry((2.0, 1.0), name='kron2') obj.fluxfrac_radius(0.5, name='r_hl') segment_snr = obj.segment_flux / obj.segment_fluxerr obj.add_extra_property('segment_snr', segment_snr) for prop in props: assert_equal(getattr(obj, prop), getattr(cat1, prop)[idx]) @pytest.mark.parametrize('with_units', (True, False)) def test_catalog_detection_cat(self, with_units): """ Test aperture-based properties with an input detection catalog. """ error = 2.0 * self.error data2 = self.data + error if with_units: cat1 = self.cat_units.copy() cat2 = SourceCatalog(data2 << self.unit, self.segm, error=error << self.unit, background=self.background << self.unit, mask=self.mask, wcs=self.wcs, localbkg_width=24, detection_cat=None) cat3 = SourceCatalog(data2 << self.unit, self.segm, error=error << self.unit, background=self.background << self.unit, mask=self.mask, wcs=self.wcs, localbkg_width=24, detection_cat=cat1) else: cat1 = self.cat.copy() cat2 = SourceCatalog(data2, self.segm, error=error, background=self.background, mask=self.mask, wcs=self.wcs, localbkg_width=24, detection_cat=None) cat3 = SourceCatalog(data2, self.segm, error=error, background=self.background, mask=self.mask, wcs=self.wcs, localbkg_width=24, detection_cat=cat1) assert_equal(cat1.kron_radius, cat3.kron_radius) # assert not equal with assert_raises(AssertionError): assert_equal(cat1.kron_radius, cat2.kron_radius) with assert_raises(AssertionError): assert_equal(cat2.kron_flux, cat3.kron_flux) with assert_raises(AssertionError): assert_equal(cat2.kron_fluxerr, cat3.kron_fluxerr) with assert_raises(AssertionError): assert_equal(cat1.kron_flux, cat3.kron_flux) with assert_raises(AssertionError): assert_equal(cat1.kron_fluxerr, cat3.kron_fluxerr) flux1, fluxerr1 = cat1.circular_photometry(1.0) flux2, fluxerr2 = cat2.circular_photometry(1.0) flux3, fluxerr3 = cat3.circular_photometry(1.0) with assert_raises(AssertionError): assert_equal(flux2, flux3) with assert_raises(AssertionError): assert_equal(fluxerr2, fluxerr3) with assert_raises(AssertionError): assert_equal(flux1, flux2) with assert_raises(AssertionError): assert_equal(fluxerr1, fluxerr2) flux1, fluxerr1 = cat1.kron_photometry((2.0, 1.0)) flux2, fluxerr2 = cat2.kron_photometry((2.0, 1.0)) flux3, fluxerr3 = cat3.kron_photometry((2.0, 1.0)) with assert_raises(AssertionError): assert_equal(flux2, flux3) with assert_raises(AssertionError): assert_equal(fluxerr2, fluxerr3) with assert_raises(AssertionError): assert_equal(flux1, flux2) with assert_raises(AssertionError): assert_equal(fluxerr1, fluxerr2) radius1 = cat1.fluxfrac_radius(0.5) radius2 = cat2.fluxfrac_radius(0.5) radius3 = cat3.fluxfrac_radius(0.5) with assert_raises(AssertionError): assert_equal(radius2, radius3) with assert_raises(AssertionError): assert_equal(radius1, radius2) cat4 = cat3[0:1] assert len(cat4.kron_radius) == 1 def test_minimal_catalog(self): cat = SourceCatalog(self.data, self.segm) obj = cat[4] props = ('background', 'background_ma', 'error', 'error_ma') for prop in props: assert getattr(obj, prop) is None props = ('background_mean', 'background_sum', 'background_centroid', 'segment_fluxerr', 'kron_fluxerr') for prop in props: assert np.isnan(getattr(obj, prop)) assert obj.local_background_aperture is None assert obj.local_background == 0. def test_slicing(self): self.cat.to_table() # evaluate and cache several properties obj1 = self.cat[0] assert obj1.nlabels == 1 obj1b = self.cat.get_label(1) assert obj1b.nlabels == 1 obj2 = self.cat[0:1] assert obj2.nlabels == 1 assert len(obj2) == 1 obj3 = self.cat[0:3] obj3b = self.cat.get_labels((1, 2, 3)) assert_equal(obj3.label, obj3b.label) obj4 = self.cat[[0, 1, 2]] assert obj3.nlabels == 3 assert obj3b.nlabels == 3 assert obj4.nlabels == 3 assert len(obj3) == 3 assert len(obj4) == 3 obj5 = self.cat[[3, 2, 1]] labels = [4, 3, 2] obj5b = self.cat.get_labels(labels) assert_equal(obj5.label, obj5b.label) assert obj5.nlabels == 3 assert len(obj5) == 3 assert_equal(obj5.label, labels) obj6 = obj5[0] assert obj6.label == labels[0] mask = self.cat.label > 3 obj7 = self.cat[mask] assert obj7.nlabels == 4 assert len(obj7) == 4 with pytest.raises(TypeError): obj1 = self.cat[0] obj2 = obj1[0] def test_iter(self): labels = [] for obj in self.cat: labels.append(obj.label) assert len(labels) == len(self.cat) def test_table(self): columns = ['label', 'xcentroid', 'ycentroid'] tbl = self.cat.to_table(columns=columns) assert len(tbl) == 7 assert tbl.colnames == columns def test_invalid_inputs(self): # test 1D arrays img1d = np.arange(4) segm = SegmentationImage(img1d) with pytest.raises(ValueError): SourceCatalog(img1d, segm) wrong_shape = np.ones((3, 3)) with pytest.raises(ValueError): SourceCatalog(wrong_shape, self.segm) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, error=wrong_shape) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, background=wrong_shape) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, mask=wrong_shape) with pytest.raises(ValueError): segm = SegmentationImage(wrong_shape) SourceCatalog(self.data, segm) with pytest.raises(TypeError): SourceCatalog(self.data, wrong_shape) with pytest.raises(TypeError): obj = SourceCatalog(self.data, self.segm)[0] len(obj) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, localbkg_width=-1) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, localbkg_width=3.4) with pytest.raises(ValueError): apermask_method = 'invalid' SourceCatalog(self.data, self.segm, apermask_method=apermask_method) with pytest.raises(ValueError): kron_params = (2.5, 0.0, 3.0) SourceCatalog(self.data, self.segm, kron_params=kron_params) with pytest.raises(ValueError): kron_params = (-2.5, 0.0) SourceCatalog(self.data, self.segm, kron_params=kron_params) with pytest.raises(ValueError): kron_params = (2.5, -4.0) SourceCatalog(self.data, self.segm, kron_params=kron_params) def test_invalid_units(self): unit = u.uJy wrong_unit = u.km with pytest.raises(ValueError): SourceCatalog(self.data << unit, self.segm, error=self.error << wrong_unit) with pytest.raises(ValueError): SourceCatalog(self.data << unit, self.segm, background=self.background << wrong_unit) # all array inputs must have the same unit with pytest.raises(ValueError): SourceCatalog(self.data << unit, self.segm, error=self.error) with pytest.raises(ValueError): SourceCatalog(self.data, self.segm, background=self.background << unit) def test_wcs(self): mywcs = make_wcs(self.data.shape) cat = SourceCatalog(self.data, self.segm, wcs=mywcs) obj = cat[0] assert obj.sky_centroid is not None assert obj.sky_centroid_icrs is not None assert obj.sky_bbox_ll is not None assert obj.sky_bbox_ul is not None assert obj.sky_bbox_lr is not None assert obj.sky_bbox_ur is not None @pytest.mark.skipif('not HAS_GWCS') def test_gwcs(self): mywcs = make_gwcs(self.data.shape) cat = SourceCatalog(self.data, self.segm, wcs=mywcs) obj = cat[1] assert obj.sky_centroid is not None assert obj.sky_centroid_icrs is not None assert obj.sky_bbox_ll is not None assert obj.sky_bbox_ul is not None assert obj.sky_bbox_lr is not None assert obj.sky_bbox_ur is not None def test_nowcs(self): cat = SourceCatalog(self.data, self.segm, wcs=None) obj = cat[2] assert obj.sky_centroid is None assert obj.sky_centroid_icrs is None assert obj.sky_bbox_ll is None assert obj.sky_bbox_ul is None assert obj.sky_bbox_lr is None assert obj.sky_bbox_ur is None def test_to_table(self): cat = SourceCatalog(self.data, self.segm) assert len(cat) == 7 tbl = cat.to_table() assert isinstance(tbl, QTable) assert len(tbl) == 7 obj = cat[0] assert obj.nlabels == 1 tbl = obj.to_table() assert len(tbl) == 1 def test_masks(self): """ Test masks, including automatic masking of all non-finite (e.g., NaN, inf) values in the data array. """ data = np.copy(self.data) error = np.copy(self.error) background = np.copy(self.background) data[:, 55] = np.nan data[16, :] = np.inf error[:, 55] = np.nan error[16, :] = np.inf background[:, 55] = np.nan background[16, :] = np.inf cat = SourceCatalog(data, self.segm, error=error, background=background, mask=self.mask) props = ('xcentroid', 'ycentroid', 'area', 'orientation', 'segment_flux', 'segment_fluxerr', 'kron_flux', 'kron_fluxerr', 'background_mean') obj = cat[0] for prop in props: assert np.isnan(getattr(obj, prop)) objs = cat[1:] for prop in props: assert np.all(np.isfinite(getattr(objs, prop))) # test that mask=None is the same as mask=np.ma.nomask cat1 = SourceCatalog(data, self.segm, mask=None) cat2 = SourceCatalog(data, self.segm, mask=np.ma.nomask) assert cat1[0].xcentroid == cat2[0].xcentroid def test_repr_str(self): cat = SourceCatalog(self.data, self.segm) assert repr(cat) == str(cat) lines = ('Length: 7', 'labels: [1 2 3 4 5 6 7]') for line in lines: assert line in repr(cat) def test_kernel(self): kernel = np.array([[1., 2, 1], [2, 4, 2], [1, 2, 100]]) kernel /= kernel.sum() cat1 = SourceCatalog(self.data, self.segm, kernel=None) cat2 = SourceCatalog(self.data, self.segm, kernel=kernel) assert not np.array_equal(cat1.xcentroid, cat2.xcentroid) assert not np.array_equal(cat1.ycentroid, cat2.ycentroid) def test_detection_cat(self): data2 = self.data - 5 cat1 = SourceCatalog(data2, self.segm) cat2 = SourceCatalog(data2, self.segm, detection_cat=self.cat) assert len(cat2.kron_aperture) == len(cat2) assert not np.array_equal(cat1.kron_radius, cat2.kron_radius) assert not np.array_equal(cat1.kron_flux, cat2.kron_flux) assert_allclose(cat2.kron_radius, self.cat.kron_radius) assert not np.array_equal(cat2.kron_flux, self.cat.kron_flux) with pytest.raises(TypeError): SourceCatalog(data2, self.segm, detection_cat=np.arange(4)) with pytest.raises(ValueError): segm = self.segm.copy() segm.remove_labels((6, 7)) cat = SourceCatalog(self.data, segm) SourceCatalog(self.data, self.segm, detection_cat=cat) def test_kron_minradius(self): kron_params = (2.5, 10.0) cat = SourceCatalog(self.data, self.segm, mask=self.mask, apermask_method='none', kron_params=kron_params) assert cat.kron_aperture[0] is None assert isinstance(cat.kron_aperture[2], EllipticalAperture) assert isinstance(cat.kron_aperture[4], CircularAperture) def test_kron_masking(self): apermask_method = 'none' cat1 = SourceCatalog(self.data, self.segm, apermask_method=apermask_method) apermask_method = 'mask' cat2 = SourceCatalog(self.data, self.segm, apermask_method=apermask_method) apermask_method = 'correct' cat3 = SourceCatalog(self.data, self.segm, apermask_method=apermask_method) idx = 2 # source with close neighbors assert cat1[idx].kron_flux > cat2[idx].kron_flux assert cat3[idx].kron_flux > cat2[idx].kron_flux assert cat1[idx].kron_flux > cat3[idx].kron_flux def test_kron_negative(self): cat = SourceCatalog(self.data - 10, self.segm) assert np.all(np.isnan(cat.kron_radius.value)) assert np.all(np.isnan(cat.kron_flux)) def test_kron_photometry(self): flux1, fluxerr1 = self.cat.kron_photometry((2.5, 1.0)) assert_allclose(flux1, self.cat.kron_flux) assert_allclose(fluxerr1, self.cat.kron_fluxerr) flux1, fluxerr1 = self.cat.kron_photometry((1.0, 1.0), name='kron1') flux2, fluxerr2 = self.cat.kron_photometry((2.0, 1.0), name='kron2') assert_allclose(flux1, self.cat.kron1_flux) assert_allclose(fluxerr1, self.cat.kron1_fluxerr) assert_allclose(flux2, self.cat.kron2_flux) assert_allclose(fluxerr2, self.cat.kron2_fluxerr) assert np.all((flux2 > flux1) | (np.isnan(flux2) & np.isnan(flux1))) assert np.all((fluxerr2 > fluxerr1) | (np.isnan(fluxerr2) & np.isnan(fluxerr1))) obj = self.cat[1] flux1, fluxerr1 = obj.kron_photometry((1.0, 1.0), name='kron0') assert np.isscalar(flux1) assert np.isscalar(fluxerr1) assert_allclose(flux1, obj.kron0_flux) assert_allclose(fluxerr1, obj.kron0_fluxerr) cat = SourceCatalog(self.data, self.segm) _, fluxerr = cat.kron_photometry((2.0, 1.0)) assert np.all(np.isnan(fluxerr)) with pytest.raises(ValueError): self.cat.kron_photometry(2.0) with pytest.raises(ValueError): self.cat.kron_photometry((2.0, 0.0)) with pytest.raises(ValueError): self.cat.kron_photometry((0.0, 2.0)) with pytest.raises(ValueError): self.cat.kron_photometry((2.0, 0.0, 1.5)) def test_circular_photometry(self): flux1, fluxerr1 = self.cat.circular_photometry(1.0, name='circ1') flux2, fluxerr2 = self.cat.circular_photometry(5.0, name='circ5') assert_allclose(flux1, self.cat.circ1_flux) assert_allclose(fluxerr1, self.cat.circ1_fluxerr) assert_allclose(flux2, self.cat.circ5_flux) assert_allclose(fluxerr2, self.cat.circ5_fluxerr) assert np.all((flux2 > flux1) | (np.isnan(flux2) & np.isnan(flux1))) assert np.all((fluxerr2 > fluxerr1) | (np.isnan(fluxerr2) & np.isnan(fluxerr1))) obj = self.cat[1] assert obj.isscalar flux1, fluxerr1 = obj.circular_photometry(1.0, name='circ0') assert np.isscalar(flux1) assert np.isscalar(fluxerr1) assert_allclose(flux1, obj.circ0_flux) assert_allclose(fluxerr1, obj.circ0_fluxerr) cat = SourceCatalog(self.data, self.segm) _, fluxerr = cat.circular_photometry(1.0) assert np.all(np.isnan(fluxerr)) with pytest.raises(ValueError): self.cat.circular_photometry(0.0) with pytest.raises(ValueError): self.cat.circular_photometry(-1.0) with pytest.raises(ValueError): self.cat.make_circular_apertures(0.0) with pytest.raises(ValueError): self.cat.make_circular_apertures(-1.0) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_plots(self): from matplotlib.patches import Patch patches = self.cat.plot_circular_apertures(5.0) assert isinstance(patches, list) for patch in patches: assert isinstance(patch, Patch) patches = self.cat.plot_kron_apertures((2.5, 1.0)) assert isinstance(patches, list) for patch in patches: assert isinstance(patch, Patch) def test_fluxfrac_radius(self): radius1 = self.cat.fluxfrac_radius(0.1, name='fluxfrac_r1') radius2 = self.cat.fluxfrac_radius(0.5, name='fluxfrac_r5') assert_allclose(radius1, self.cat.fluxfrac_r1) assert_allclose(radius2, self.cat.fluxfrac_r5) assert np.all((radius2 > radius1) | (np.isnan(radius2) & np.isnan(radius1))) cat = SourceCatalog(self.data, self.segm) obj = cat[1] radius = obj.fluxfrac_radius(0.5) assert radius.isscalar # Quantity radius - can't use np.isscalar assert_allclose(radius.value, 7.899648) with pytest.raises(ValueError): radius = self.cat.fluxfrac_radius(0) with pytest.raises(ValueError): radius = self.cat.fluxfrac_radius(-1) cat = SourceCatalog(self.data - 50., self.segm, error=self.error, background=self.background, mask=self.mask, wcs=self.wcs, localbkg_width=24) radius_hl = cat.fluxfrac_radius(0.5) assert np.all(np.isnan(radius_hl)) def test_cutout_units(self): obj = self.cat_units[0] quantities = (obj.data, obj.error, obj.background) ndarray = (obj.segment, obj.segment_ma, obj.data_ma, obj.error_ma, obj.background_ma) for arr in quantities: assert isinstance(arr, u.Quantity) for arr in ndarray: assert not isinstance(arr, u.Quantity) @pytest.mark.parametrize('scalar', (True, False)) def test_extra_properties(self, scalar): cat = SourceCatalog(self.data, self.segm) if scalar: cat = cat[1] segment_snr = cat.segment_flux / cat.segment_fluxerr with pytest.raises(ValueError): # built-in attribute cat.add_extra_property('_data', segment_snr) with pytest.raises(ValueError): # built-in property cat.add_extra_property('label', segment_snr) with pytest.raises(ValueError): # built-in lazyproperty cat.add_extra_property('area', segment_snr) cat.add_extra_property('segment_snr', segment_snr) with pytest.raises(ValueError): # already exists cat.add_extra_property('segment_snr', segment_snr) cat.add_extra_property('segment_snr', 2.0 * segment_snr, overwrite=True) assert len(cat.extra_properties) == 1 assert_equal(cat.segment_snr, 2.0 * segment_snr) with pytest.raises(ValueError): cat.remove_extra_property('invalid') cat.remove_extra_property(cat.extra_properties) assert len(cat.extra_properties) == 0 cat.add_extra_property('segment_snr', segment_snr) cat.add_extra_property('segment_snr2', segment_snr) cat.add_extra_property('segment_snr3', segment_snr) assert len(cat.extra_properties) == 3 cat.remove_extra_properties(cat.extra_properties) assert len(cat.extra_properties) == 0 cat.add_extra_property('segment_snr', segment_snr) new_name = 'segment_snr0' cat.rename_extra_property('segment_snr', new_name) assert new_name in cat.extra_properties # key in extra_properties, but not a defined attribute cat._extra_properties.append('invalid') with pytest.raises(ValueError): cat.add_extra_property('invalid', segment_snr) cat._extra_properties.remove('invalid') def test_extra_properties_invalid(self): cat = SourceCatalog(self.data, self.segm) with pytest.raises(ValueError): cat.add_extra_property('invalid', 1.0) with pytest.raises(ValueError): cat.add_extra_property('invalid', (1.0, 2.0)) obj = cat[1] with pytest.raises(ValueError): obj.add_extra_property('invalid', (1.0, 2.0)) with pytest.raises(ValueError): val = np.arange(2) << u.km obj.add_extra_property('invalid', val) with pytest.raises(ValueError): coord = SkyCoord([42, 43], [44, 45], unit='deg') obj.add_extra_property('invalid', coord) def test_properties(self): attrs = ('label', 'labels', 'slices', 'xcentroid', 'segment_flux', 'kron_flux') for attr in attrs: assert attr in self.cat.properties def test_copy(self): cat = SourceCatalog(self.data, self.segm) cat2 = cat.copy() cat.kron_flux assert 'kron_flux' not in cat2.__dict__ tbl = cat2.to_table() assert len(tbl) == 7 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/tests/test_core.py0000644000214200020070000003047200000000000022437 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the core module. """ import numpy as np from numpy.testing import assert_allclose import pytest from ..core import Segment, SegmentationImage from ...utils._optional_deps import HAS_MATPLOTLIB, HAS_SCIPY # noqa @pytest.mark.skipif('not HAS_SCIPY') class TestSegmentationImage: def setup_class(self): self.data = [[1, 1, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]] self.segm = SegmentationImage(self.data) def test_array(self): assert_allclose(self.segm.data, self.segm.__array__()) def test_copy(self): segm = SegmentationImage(self.data) segm2 = segm.copy() assert segm.data is not segm2.data assert segm.labels is not segm2.labels segm.data[0, 0] = 100. assert segm.data[0, 0] != segm2.data[0, 0] def test_invalid_data(self): # contains all zeros data = np.zeros((3, 3)) with pytest.raises(ValueError): SegmentationImage(data) # contains a NaN data = np.zeros((5, 5)) data[2, 2] = np.nan with pytest.raises(ValueError): SegmentationImage(data) # contains an inf data = np.zeros((5, 5)) data[2, 2] = np.inf data[0, 0] = -np.inf with pytest.raises(ValueError): SegmentationImage(data) # contains a negative value data = np.arange(-1, 8).reshape(3, 3) with pytest.raises(ValueError): SegmentationImage(data) @pytest.mark.parametrize('label', [0, -1, 2]) def test_invalid_label(self, label): # test with scalar labels with pytest.raises(ValueError): self.segm.check_label(label) self.segm.check_labels(label) def test_invalid_label_array(self): # test with array of labels with pytest.raises(ValueError): self.segm.check_labels([0, -1, 2]) def test_data_ma(self): assert isinstance(self.segm.data_ma, np.ma.MaskedArray) assert np.ma.count(self.segm.data_ma) == 18 assert np.ma.count_masked(self.segm.data_ma) == 18 def test_segments(self): assert isinstance(self.segm.segments[0], Segment) assert_allclose(self.segm.segments[0].data, self.segm.segments[0].__array__()) assert (self.segm.segments[0].data_ma.shape == self.segm.segments[0].data.shape) assert (self.segm.segments[0].data_ma.filled(0.).sum() == self.segm.segments[0].data.sum()) label = 4 idx = self.segm.get_index(label) assert self.segm.segments[idx].label == label assert self.segm.segments[idx].area == self.segm.areas[idx] assert self.segm.segments[idx].slices == self.segm.slices[idx] assert self.segm.segments[idx].bbox == self.segm.bbox[idx] def test_repr_str(self): assert repr(self.segm) == str(self.segm) props = ['shape', 'nlabels'] for prop in props: assert f'{prop}:' in repr(self.segm) def test_segment_repr_str(self): props = ['label', 'slices', 'area'] for prop in props: assert f'{prop}:' in repr(self.segm.segments[0]) def test_segment_data(self): assert_allclose(self.segm.segments[3].data.shape, (3, 3)) assert_allclose(np.unique(self.segm.segments[3].data), [0, 5]) def test_segment_make_cutout(self): cutout = self.segm.segments[3].make_cutout(self.data, masked_array=False) assert not np.ma.is_masked(cutout) assert_allclose(cutout.shape, (3, 3)) cutout = self.segm.segments[3].make_cutout(self.data, masked_array=True) assert np.ma.is_masked(cutout) assert_allclose(cutout.shape, (3, 3)) def test_segment_make_cutout_input(self): with pytest.raises(ValueError): self.segm.segments[0].make_cutout(np.arange(10)) def test_labels(self): assert_allclose(self.segm.labels, [1, 3, 4, 5, 7]) def test_nlabels(self): assert self.segm.nlabels == 5 def test_max_label(self): assert self.segm.max_label == 7 def test_areas(self): expected = np.array([2, 2, 3, 6, 5]) assert_allclose(self.segm.areas, expected) assert (self.segm.get_area(1) == self.segm.areas[self.segm.get_index(1)]) assert_allclose(self.segm.get_areas(self.segm.labels), self.segm.areas) def test_background_area(self): assert self.segm.background_area == 18 def test_is_consecutive(self): assert not self.segm.is_consecutive data = [[2, 2, 0], [0, 3, 3], [0, 0, 4]] segm = SegmentationImage(data) assert not segm.is_consecutive # does not start with label=1 segm.relabel_consecutive(start_label=1) assert segm.is_consecutive def test_missing_labels(self): assert_allclose(self.segm.missing_labels, [2, 6]) def test_check_labels(self): with pytest.raises(ValueError): self.segm.check_label(2) self.segm.check_labels([2]) with pytest.raises(ValueError): self.segm.check_labels([2, 6]) @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_make_cmap(self): cmap = self.segm.make_cmap() assert len(cmap.colors) == (self.segm.max_label + 1) assert_allclose(cmap.colors[0], [0, 0, 0]) assert_allclose(self.segm._cmap.colors, self.segm.make_cmap(background_color='#000000', seed=0).colors) def test_reassign_labels(self): segm = SegmentationImage(self.data) segm.reassign_labels(labels=[1, 7], new_label=2) ref_data = np.array([[2, 2, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [2, 0, 0, 0, 0, 5], [2, 2, 0, 5, 5, 5], [2, 2, 0, 0, 5, 5]]) assert_allclose(segm.data, ref_data) assert segm.nlabels == len(segm.slices) - segm.slices.count(None) @pytest.mark.parametrize('start_label', [1, 5]) def test_relabel_consecutive(self, start_label): segm = SegmentationImage(self.data) ref_data = np.array([[1, 1, 0, 0, 3, 3], [0, 0, 0, 0, 0, 3], [0, 0, 2, 2, 0, 0], [5, 0, 0, 0, 0, 4], [5, 5, 0, 4, 4, 4], [5, 5, 0, 0, 4, 4]]) ref_data[ref_data != 0] += (start_label - 1) segm.relabel_consecutive(start_label=start_label) assert_allclose(segm.data, ref_data) # relabel_consecutive should do nothing if already consecutive segm.relabel_consecutive(start_label=start_label) assert_allclose(segm.data, ref_data) assert segm.nlabels == len(segm.slices) - segm.slices.count(None) @pytest.mark.parametrize('start_label', [0, -1]) def test_relabel_consecutive_start_invalid(self, start_label): with pytest.raises(ValueError): segm = SegmentationImage(self.data) segm.relabel_consecutive(start_label=start_label) def test_keep_labels(self): ref_data = np.array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [0, 0, 0, 0, 0, 5], [0, 0, 0, 5, 5, 5], [0, 0, 0, 0, 5, 5]]) segm = SegmentationImage(self.data) segm.keep_labels([5, 3]) assert_allclose(segm.data, ref_data) def test_keep_labels_relabel(self): ref_data = np.array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 2], [0, 0, 0, 2, 2, 2], [0, 0, 0, 0, 2, 2]]) segm = SegmentationImage(self.data) segm.keep_labels([5, 3], relabel=True) assert_allclose(segm.data, ref_data) def test_remove_labels(self): ref_data = np.array([[1, 1, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 0, 0, 0, 0], [7, 0, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0], [7, 7, 0, 0, 0, 0]]) segm = SegmentationImage(self.data) segm.remove_labels(labels=[5, 3]) assert_allclose(segm.data, ref_data) def test_remove_labels_relabel(self): ref_data = np.array([[1, 1, 0, 0, 2, 2], [0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0], [3, 0, 0, 0, 0, 0], [3, 3, 0, 0, 0, 0], [3, 3, 0, 0, 0, 0]]) segm = SegmentationImage(self.data) segm.remove_labels(labels=[5, 3], relabel=True) assert_allclose(segm.data, ref_data) def test_remove_border_labels(self): ref_data = np.array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) segm = SegmentationImage(self.data) segm.remove_border_labels(border_width=1) assert_allclose(segm.data, ref_data) def test_remove_border_labels_border_width(self): with pytest.raises(ValueError): segm = SegmentationImage(self.data) segm.remove_border_labels(border_width=3) def test_remove_border_labels_no_remaining_segments(self): alt_data = np.copy(self.data) alt_data[alt_data == 3] = 0 segm = SegmentationImage(alt_data) segm.remove_border_labels(border_width=1, relabel=True) assert segm.nlabels == 0 def test_remove_masked_labels(self): ref_data = np.array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) segm = SegmentationImage(self.data) mask = np.zeros(segm.data.shape, dtype=bool) mask[0, :] = True segm.remove_masked_labels(mask) assert_allclose(segm.data, ref_data) def test_remove_masked_labels_without_partial_overlap(self): ref_data = np.array([[0, 0, 0, 0, 4, 4], [0, 0, 0, 0, 0, 4], [0, 0, 3, 3, 0, 0], [7, 0, 0, 0, 0, 5], [7, 7, 0, 5, 5, 5], [7, 7, 0, 0, 5, 5]]) segm = SegmentationImage(self.data) mask = np.zeros(segm.data.shape, dtype=bool) mask[0, :] = True segm.remove_masked_labels(mask, partial_overlap=False) assert_allclose(segm.data, ref_data) def test_remove_masked_segments_mask_shape(self): segm = SegmentationImage(np.ones((5, 5))) mask = np.zeros((3, 3), dtype=bool) with pytest.raises(ValueError): segm.remove_masked_labels(mask) def test_outline_segments(self): segm_array = np.zeros((5, 5)).astype(int) segm_array[1:4, 1:4] = 2 segm = SegmentationImage(segm_array) segm_array_ref = np.copy(segm_array) segm_array_ref[2, 2] = 0 assert_allclose(segm.outline_segments(), segm_array_ref) def test_outline_segments_masked_background(self): segm_array = np.zeros((5, 5)).astype(int) segm_array[1:4, 1:4] = 2 segm = SegmentationImage(segm_array) segm_array_ref = np.copy(segm_array) segm_array_ref[2, 2] = 0 segm_outlines = segm.outline_segments(mask_background=True) assert isinstance(segm_outlines, np.ma.MaskedArray) assert np.ma.count(segm_outlines) == 8 assert np.ma.count_masked(segm_outlines) == 17 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640109423.0 photutils-1.3.0/photutils/segmentation/tests/test_deblend.py0000644000214200020070000002426500000000000023107 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the deblend module. """ from astropy.modeling.models import Gaussian2D from astropy.tests.helper import catch_warnings from astropy.utils.exceptions import AstropyUserWarning import numpy as np from numpy.testing import assert_allclose import pytest from ..core import SegmentationImage from ..deblend import deblend_sources from ..detect import detect_sources from ...utils.exceptions import NoDetectionsWarning from ...utils._optional_deps import HAS_SCIPY, HAS_SKIMAGE # noqa @pytest.mark.skipif('not HAS_SCIPY') @pytest.mark.skipif('not HAS_SKIMAGE') class TestDeblendSources: def setup_class(self): g1 = Gaussian2D(100, 50, 50, 5, 5) g2 = Gaussian2D(100, 35, 50, 5, 5) g3 = Gaussian2D(30, 70, 50, 5, 5) y, x = np.mgrid[0:100, 0:100] self.x = x self.y = y self.data = g1(x, y) + g2(x, y) self.data3 = self.data + g3(x, y) self.threshold = 10 self.npixels = 5 self.segm = detect_sources(self.data, self.threshold, self.npixels) self.segm3 = detect_sources(self.data3, self.threshold, self.npixels) @pytest.mark.parametrize('mode', ['exponential', 'linear']) def test_deblend_sources(self, mode): result = deblend_sources(self.data, self.segm, self.npixels, mode=mode) assert result.nlabels == 2 assert result.nlabels == len(result.slices) mask1 = (result.data == 1) mask2 = (result.data == 2) assert_allclose(len(result.data[mask1]), len(result.data[mask2])) assert_allclose(np.sum(self.data[mask1]), np.sum(self.data[mask2])) assert_allclose(np.nonzero(self.segm), np.nonzero(result)) def test_deblend_multiple_sources(self): g4 = Gaussian2D(100, 50, 15, 5, 5) g5 = Gaussian2D(100, 35, 15, 5, 5) g6 = Gaussian2D(100, 50, 85, 5, 5) g7 = Gaussian2D(100, 35, 85, 5, 5) x = self.x y = self.y data = self.data + g4(x, y) + g5(x, y) + g6(x, y) + g7(x, y) segm = detect_sources(data, self.threshold, self.npixels) result = deblend_sources(data, segm, self.npixels) assert result.nlabels == 6 assert result.nlabels == len(result.slices) assert result.areas[0] == result.areas[1] assert result.areas[0] == result.areas[2] assert result.areas[0] == result.areas[3] assert result.areas[0] == result.areas[4] assert result.areas[0] == result.areas[5] def test_deblend_multiple_sources_with_neighbor(self): g1 = Gaussian2D(100, 50, 50, 20, 5, theta=45) g2 = Gaussian2D(100, 35, 50, 5, 5) g3 = Gaussian2D(100, 60, 20, 5, 5) x = self.x y = self.y data = (g1 + g2 + g3)(x, y) segm = detect_sources(data, self.threshold, self.npixels) result = deblend_sources(data, segm, self.npixels) assert result.nlabels == 3 @pytest.mark.parametrize('contrast, nlabels', ((0.001, 6), (0.017, 5), (0.06, 4), (0.1, 3), (0.15, 2), (0.45, 1))) def test_deblend_contrast(self, contrast, nlabels): y, x = np.mgrid[0:51, 0:151] y0 = 25 data = (Gaussian2D(9.5, 16, y0, 5, 5)(x, y) + Gaussian2D(51, 30, y0, 3, 3)(x, y) + Gaussian2D(30, 42, y0, 5, 5)(x, y) + Gaussian2D(80, 66, y0, 8, 8)(x, y) + Gaussian2D(71, 88, y0, 8, 8)(x, y) + Gaussian2D(18, 119, y0, 7, 7)(x, y)) npixels = 5 segm = detect_sources(data, 1.0, npixels) segm2 = deblend_sources(data, segm, npixels, mode='linear', nlevels=32, contrast=contrast) assert segm2.nlabels == nlabels def test_deblend_connectivity(self): data = np.zeros((51, 51)) data[15:36, 15:36] = 10. data[14, 36] = 1. data[13, 37] = 10 data[14, 14] = 5. data[13, 13] = 10. data[36, 14] = 10. data[37, 13] = 10. data[36, 36] = 10. data[37, 37] = 10. segm = detect_sources(data, 0.1, 1, connectivity=4) assert segm.nlabels == 9 segm2 = deblend_sources(data, segm, 1, mode='linear', connectivity=4) assert segm2.nlabels == 9 segm = detect_sources(data, 0.1, 1, connectivity=8) assert segm.nlabels == 1 segm2 = deblend_sources(data, segm, 1, mode='linear', connectivity=8) assert segm2.nlabels == 3 with pytest.raises(ValueError): deblend_sources(data, segm, 1, mode='linear', connectivity=4) def test_deblend_label_assignment(self): """ Regression test to ensure newly-deblended labels are unique. """ y, x = np.mgrid[0:201, 0:101] y0a = 35 y1a = 60 yshift = 100 y0b = y0a + yshift y1b = y1a + yshift data = (Gaussian2D(80, 36, y0a, 8, 8)(x, y) + Gaussian2D(71, 58, y1a, 8, 8)(x, y) + Gaussian2D(30, 36, y1a, 7, 7)(x, y) + Gaussian2D(30, 58, y0a, 7, 7)(x, y) + Gaussian2D(80, 36, y0b, 8, 8)(x, y) + Gaussian2D(71, 58, y1b, 8, 8)(x, y) + Gaussian2D(30, 36, y1b, 7, 7)(x, y) + Gaussian2D(30, 58, y0b, 7, 7)(x, y)) npixels = 5 segm1 = detect_sources(data, 5.0, npixels) segm2 = deblend_sources(data, segm1, npixels, mode='linear', nlevels=32, contrast=0.3) assert segm2.nlabels == 4 @pytest.mark.parametrize('mode', ['exponential', 'linear']) def test_deblend_sources_norelabel(self, mode): result = deblend_sources(self.data, self.segm, self.npixels, mode=mode, relabel=False) assert result.nlabels == 2 assert len(result.slices) <= result.max_label assert len(result.slices) == result.nlabels assert_allclose(np.nonzero(self.segm), np.nonzero(result)) @pytest.mark.parametrize('mode', ['exponential', 'linear']) def test_deblend_three_sources(self, mode): result = deblend_sources(self.data3, self.segm3, self.npixels, mode=mode) assert result.nlabels == 3 assert_allclose(np.nonzero(self.segm3), np.nonzero(result)) def test_deblend_sources_segm_array(self): result = deblend_sources(self.data, self.segm.data, self.npixels) assert result.nlabels == 2 def test_segment_img_badshape(self): segm_wrong = np.ones((2, 2)) with pytest.raises(ValueError): deblend_sources(self.data, segm_wrong, self.npixels) def test_invalid_nlevels(self): with pytest.raises(ValueError): deblend_sources(self.data, self.segm, self.npixels, nlevels=0) def test_invalid_contrast(self): with pytest.raises(ValueError): deblend_sources(self.data, self.segm, self.npixels, contrast=-1) def test_invalid_mode(self): with pytest.raises(ValueError): deblend_sources(self.data, self.segm, self.npixels, mode='invalid') def test_invalid_connectivity(self): with pytest.raises(ValueError): deblend_sources(self.data, self.segm, self.npixels, connectivity='invalid') def test_constant_source(self): data = self.data.copy() data[data.nonzero()] = 1. result = deblend_sources(data, self.segm, self.npixels) assert_allclose(result, self.segm) def test_source_with_negval(self): data = self.data.copy() data -= 20 with catch_warnings(AstropyUserWarning) as warning_lines: deblend_sources(data, self.segm, self.npixels) assert ('contains negative values' in str(warning_lines[0].message)) def test_source_zero_min(self): data = self.data.copy() data -= data[self.segm.data > 0].min() result1 = deblend_sources(self.data, self.segm, self.npixels) result2 = deblend_sources(data, self.segm, self.npixels) assert_allclose(result1, result2) def test_connectivity(self): """Regression test for #341.""" data = np.zeros((3, 3)) data[0, 0] = 2 data[1, 1] = 2 data[2, 2] = 1 segm = np.zeros(data.shape, dtype=int) segm[data.nonzero()] = 1 segm = SegmentationImage(segm) data = data * 100. segm_deblend = deblend_sources(data, segm, npixels=1, connectivity=8) assert segm_deblend.nlabels == 1 with pytest.raises(ValueError): deblend_sources(data, segm, npixels=1, connectivity=4) def test_data_nan(self): """ Test that deblending occurs even if the data within a segment contains one or more NaNs. Regression test for #658. """ data = self.data.copy() data[50, 50] = np.nan segm2 = deblend_sources(data, self.segm, 5) assert segm2.nlabels == 2 def test_watershed(self): """ Regression test to ensure watershed input mask is bool array. With scikit-image >= 0.13, the mask must be a bool array. In particular, if the mask array contains label 512, the watershed algorithm fails. """ segm = self.segm.copy() segm.reassign_label(1, 512) result = deblend_sources(self.data, segm, self.npixels) assert result.nlabels == 2 def test_nondetection(self): """ Test for case where no sources are detected at one of the threshold levels. For this case, a `NoDetectionsWarning` should not be raised when deblending sources. """ data = np.copy(self.data3) data[50, 50] = 1000. data[50, 70] = 500. self.segm = detect_sources(data, self.threshold, self.npixels) with catch_warnings(NoDetectionsWarning) as warning_lines: deblend_sources(data, self.segm, self.npixels) assert len(warning_lines) == 0 def test_nonconsecutive_labels(self): segm = self.segm.copy() segm.reassign_label(1, 1000) result = deblend_sources(self.data, segm, self.npixels) assert result.nlabels == 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636690896.0 photutils-1.3.0/photutils/segmentation/tests/test_detect.py0000644000214200020070000002325700000000000022762 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the detect module. """ from astropy.convolution import Gaussian2DKernel from astropy.stats import gaussian_fwhm_to_sigma from astropy.tests.helper import catch_warnings import numpy as np from numpy.testing import assert_allclose, assert_array_equal import pytest from ...utils.exceptions import NoDetectionsWarning from ..detect import detect_threshold, detect_sources, make_source_mask from ...datasets import make_4gaussians_image from ...utils._optional_deps import HAS_SCIPY # noqa DATA = np.array([[0, 1, 0], [0, 2, 0], [0, 0, 0]]).astype(float) REF1 = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) @pytest.mark.skipif('not HAS_SCIPY') class TestDetectThreshold: def test_nsigma(self): """Test basic nsigma.""" threshold = detect_threshold(DATA, nsigma=0.1) ref = 0.4 * np.ones((3, 3)) assert_allclose(threshold, ref) def test_nsigma_zero(self): """Test nsigma=0.""" threshold = detect_threshold(DATA, nsigma=0.0) ref = (1. / 3.) * np.ones((3, 3)) assert_allclose(threshold, ref) def test_background(self): threshold = detect_threshold(DATA, nsigma=1.0, background=1) ref = (5. / 3.) * np.ones((3, 3)) assert_allclose(threshold, ref) def test_background_image(self): background = np.ones((3, 3)) threshold = detect_threshold(DATA, nsigma=1.0, background=background) ref = (5. / 3.) * np.ones((3, 3)) assert_allclose(threshold, ref) def test_background_badshape(self): wrong_shape = np.zeros((2, 2)) with pytest.raises(ValueError): detect_threshold(DATA, nsigma=2., background=wrong_shape) def test_error(self): threshold = detect_threshold(DATA, nsigma=1.0, error=1) ref = (4. / 3.) * np.ones((3, 3)) assert_allclose(threshold, ref) def test_error_image(self): error = np.ones((3, 3)) threshold = detect_threshold(DATA, nsigma=1.0, error=error) ref = (4. / 3.) * np.ones((3, 3)) assert_allclose(threshold, ref) def test_error_badshape(self): wrong_shape = np.zeros((2, 2)) with pytest.raises(ValueError): detect_threshold(DATA, nsigma=2., error=wrong_shape) def test_background_error(self): threshold = detect_threshold(DATA, nsigma=2.0, background=10., error=1.) ref = 12. * np.ones((3, 3)) assert_allclose(threshold, ref) def test_background_error_images(self): background = np.ones((3, 3)) * 10. error = np.ones((3, 3)) threshold = detect_threshold(DATA, nsigma=2.0, background=background, error=error) ref = 12. * np.ones((3, 3)) assert_allclose(threshold, ref) def test_mask_value(self): """Test detection with mask_value.""" threshold = detect_threshold(DATA, nsigma=1.0, mask_value=0.0) ref = 2. * np.ones((3, 3)) assert_array_equal(threshold, ref) def test_image_mask(self): """ Test detection with image_mask. Set sigma=10 and iters=1 to prevent sigma clipping after applying the mask. """ mask = REF1.astype(bool) threshold = detect_threshold(DATA, nsigma=1., error=0, mask=mask, sigclip_sigma=10, sigclip_iters=1) ref = (1. / 8.) * np.ones((3, 3)) assert_array_equal(threshold, ref) def test_image_mask_override(self): """Test that image_mask overrides mask_value.""" mask = REF1.astype(bool) threshold = detect_threshold(DATA, nsigma=0.1, error=0, mask_value=0.0, mask=mask, sigclip_sigma=10, sigclip_iters=1) ref = np.ones((3, 3)) assert_array_equal(threshold, ref) @pytest.mark.skipif('not HAS_SCIPY') class TestDetectSources: def setup_class(self): self.data = np.array([[0, 1, 0], [0, 2, 0], [0, 0, 0]]).astype(float) self.refdata = np.array([[0, 1, 0], [0, 1, 0], [0, 0, 0]]) fwhm2sigma = 1.0 / (2.0 * np.sqrt(2.0 * np.log(2.0))) kernel = Gaussian2DKernel(2. * fwhm2sigma, x_size=3, y_size=3) kernel.normalize() self.kernel = kernel def test_detection(self): """Test basic detection.""" segm = detect_sources(self.data, threshold=0.9, npixels=2) assert_array_equal(segm.data, self.refdata) def test_small_sources(self): """Test detection where sources are smaller than npixels size.""" with catch_warnings(NoDetectionsWarning) as warning_lines: detect_sources(self.data, threshold=0.9, npixels=5) assert warning_lines[0].category == NoDetectionsWarning assert 'No sources were found.' in str(warning_lines[0].message) def test_npixels(self): """ Test removal of sources whose size is less than npixels. Regression tests for #663. """ data = np.zeros((8, 8)) data[0:4, 0] = 1 data[0, 0:4] = 1 data[3, 3:] = 2 data[3:, 3] = 2 segm = detect_sources(data, 0, npixels=8) assert segm.nlabels == 1 segm = detect_sources(data, 0, npixels=9) assert segm.nlabels == 1 data = np.zeros((8, 8)) data[0:4, 0] = 1 data[0, 0:4] = 1 data[3, 2:] = 2 data[3:, 2] = 2 data[5:, 3] = 2 npixels = np.arange(9, 14) for npixels in np.arange(9, 14): segm = detect_sources(data, 0, npixels=npixels) assert segm.nlabels == 1 assert segm.areas[0] == 13 with catch_warnings(NoDetectionsWarning) as warning_lines: detect_sources(data, 0, npixels=14) assert warning_lines[0].category == NoDetectionsWarning assert 'No sources were found.' in str(warning_lines[0].message) def test_zerothresh(self): """Test detection with zero threshold.""" segm = detect_sources(self.data, threshold=0., npixels=2) assert_array_equal(segm.data, self.refdata) def test_zerodet(self): """Test detection with large threshold giving no detections.""" with catch_warnings(NoDetectionsWarning) as warning_lines: detect_sources(self.data, threshold=7, npixels=2) assert warning_lines[0].category == NoDetectionsWarning assert 'No sources were found.' in str(warning_lines[0].message) def test_8connectivity(self): """Test detection with connectivity=8.""" data = np.eye(3) segm = detect_sources(data, threshold=0.9, npixels=1, connectivity=8) assert_array_equal(segm.data, data) def test_4connectivity(self): """Test detection with connectivity=4.""" data = np.eye(3) ref = np.diag([1, 2, 3]) segm = detect_sources(data, threshold=0.9, npixels=1, connectivity=4) assert_array_equal(segm.data, ref) def test_basic_kernel(self): """Test detection with kernel.""" kernel = np.ones((3, 3)) / 9. threshold = 0.3 expected = np.ones((3, 3)) expected[2] = 0 segm = detect_sources(self.data, threshold, npixels=1, kernel=kernel) assert_array_equal(segm.data, expected) def test_npixels_nonint(self): """Test if error raises if npixel is non-integer.""" with pytest.raises(ValueError): detect_sources(self.data, threshold=1, npixels=0.1) def test_npixels_negative(self): """Test if error raises if npixel is negative.""" with pytest.raises(ValueError): detect_sources(self.data, threshold=1, npixels=-1) def test_connectivity_invalid(self): """Test if error raises if connectivity is invalid.""" with pytest.raises(ValueError): detect_sources(self.data, threshold=1, npixels=1, connectivity=10) def test_kernel_array(self): segm = detect_sources(self.data, 0.1, npixels=1, kernel=self.kernel.array) assert_array_equal(segm.data, np.ones((3, 3))) def test_kernel(self): segm = detect_sources(self.data, 0.1, npixels=1, kernel=self.kernel) assert_array_equal(segm.data, np.ones((3, 3))) def test_mask(self): data = np.zeros((11, 11)) data[3:8, 3:8] = 5. mask = np.zeros(data.shape, dtype=bool) mask[4:6, 4:6] = True segm1 = detect_sources(data, 1., 1.) segm2 = detect_sources(data, 1., 1., mask=mask) assert segm2.areas[0] == segm1.areas[0] - mask.sum() def test_mask_shape(self): with pytest.raises(ValueError): detect_sources(self.data, 1., 1., mask=np.ones((5, 5))) @pytest.mark.skipif('not HAS_SCIPY') class TestMakeSourceMask: def setup_class(self): self.data = make_4gaussians_image() def test_dilate_size(self): mask1 = make_source_mask(self.data, 5, 10) mask2 = make_source_mask(self.data, 5, 10, dilate_size=20) assert np.count_nonzero(mask2) > np.count_nonzero(mask1) def test_kernel(self): mask1 = make_source_mask(self.data, 5, 10, filter_fwhm=2, filter_size=3) sigma = 2 * gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) mask2 = make_source_mask(self.data, 5, 10, kernel=kernel) assert_allclose(mask1, mask2) def test_no_detections(self): with catch_warnings(NoDetectionsWarning) as warning_lines: mask = make_source_mask(self.data, 100, 100) assert np.count_nonzero(mask) == 0 assert warning_lines[0].category == NoDetectionsWarning ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/tests/test_properties.py0000644000214200020070000007002700000000000023703 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the properties module. """ import itertools import astropy.units as u import astropy.wcs as WCS from astropy.modeling import models from astropy.table import QTable from astropy.tests.helper import assert_quantity_allclose from astropy.utils.exceptions import AstropyDeprecationWarning from astropy.utils.misc import isiterable from numpy.testing import assert_allclose import numpy as np import pytest from ..core import SegmentationImage from ..detect import detect_sources from ..properties import (LegacySourceCatalog, SourceProperties, source_properties) from ...datasets import make_gwcs, make_wcs from ...utils._optional_deps import HAS_GWCS, HAS_SCIPY # noqa XCEN = 51. YCEN = 52.7 MAJOR_SIG = 8. MINOR_SIG = 3. THETA = np.pi / 6. G1 = models.Gaussian2D(111., XCEN, YCEN, MAJOR_SIG, MINOR_SIG, theta=THETA) G2 = models.Gaussian2D(50, 20, 80, 5.1, 4.5) G3 = models.Gaussian2D(70, 75, 18, 9.2, 4.5) Y, X = np.mgrid[0:100, 0:100] IMAGE = G1(X, Y) + G2(X, Y) + G3(X, Y) THRESHOLD = 0.1 ERR_VALS = [0., 2.5] BACKGRD_VALS = [None, 0., 1., 3.5] FWHM2SIGMA = 1.0 / (2.0 * np.sqrt(2.0 * np.log(2.0))) @pytest.mark.skipif('not HAS_SCIPY') class TestSourceProperties: def setup_class(self): self.segm = detect_sources(IMAGE, THRESHOLD, npixels=5) def test_invalid_shapes(self): with pytest.warns(AstropyDeprecationWarning): wrong_shape = np.ones((3, 3)) with pytest.raises(ValueError): SourceProperties(IMAGE, np.eye(3, dtype=int), label=1) with pytest.raises(ValueError): SourceProperties(IMAGE, self.segm, label=1, filtered_data=wrong_shape) with pytest.raises(ValueError): SourceProperties(IMAGE, self.segm, label=1, error=wrong_shape) with pytest.raises(ValueError): SourceProperties(IMAGE, self.segm, label=1, background=wrong_shape) def test_invalid_units(self): with pytest.warns(AstropyDeprecationWarning): unit = u.uJy wrong_unit = u.km with pytest.raises(ValueError): SourceProperties(IMAGE*unit, self.segm, label=1, filtered_data=IMAGE*wrong_unit) with pytest.raises(ValueError): SourceProperties(IMAGE*unit, self.segm, label=1, error=IMAGE*wrong_unit) with pytest.raises(ValueError): SourceProperties(IMAGE*unit, self.segm, label=1, background=IMAGE*wrong_unit) # all array inputs must have the same unit with pytest.raises(ValueError): SourceProperties(IMAGE*unit, self.segm, label=1, filtered_data=IMAGE) @pytest.mark.parametrize('label', (0, -1)) def test_label_invalid(self, label): with pytest.warns(AstropyDeprecationWarning): with pytest.raises(ValueError): SourceProperties(IMAGE, self.segm, label=label) @pytest.mark.parametrize('label', (0, -1)) def test_label_missing(self, label): with pytest.warns(AstropyDeprecationWarning): segm = self.segm.copy() segm.remove_label(2) with pytest.raises(ValueError): SourceProperties(IMAGE, segm, label=2) SourceProperties(IMAGE, segm, label=label) def test_wcs(self): with pytest.warns(AstropyDeprecationWarning): mywcs = make_wcs(IMAGE.shape) props = SourceProperties(IMAGE, self.segm, wcs=mywcs, label=1) assert props.sky_centroid is not None assert props.sky_centroid_icrs is not None assert props.sky_bbox_ll is not None assert props.sky_bbox_ul is not None assert props.sky_bbox_lr is not None assert props.sky_bbox_ur is not None tbl = props.to_table() assert len(tbl) == 1 @pytest.mark.skipif('not HAS_GWCS') def test_gwcs(self): with pytest.warns(AstropyDeprecationWarning): mywcs = make_gwcs(IMAGE.shape) props = SourceProperties(IMAGE, self.segm, wcs=mywcs, label=1) assert props.sky_centroid is not None assert props.sky_centroid_icrs is not None assert props.sky_bbox_ll is not None assert props.sky_bbox_ul is not None assert props.sky_bbox_lr is not None assert props.sky_bbox_ur is not None tbl = props.to_table() assert len(tbl) == 1 def test_nowcs(self): with pytest.warns(AstropyDeprecationWarning): props = SourceProperties(IMAGE, self.segm, wcs=None, label=1) assert props.sky_centroid_icrs is None assert props.sky_bbox_ll is None assert props.sky_bbox_ul is None assert props.sky_bbox_lr is None assert props.sky_bbox_ur is None def test_to_table(self): with pytest.warns(AstropyDeprecationWarning): props = SourceProperties(IMAGE, self.segm, label=2) t1 = props.to_table() assert isinstance(t1, QTable) assert len(t1) == 1 assert_quantity_allclose(t1['area'], 1058 * u.pix**2) def test_masks(self): """ Test masks, including automatic masking of all non-finite (e.g., NaN, inf) values in the data array. """ with pytest.warns(AstropyDeprecationWarning): error = np.ones(IMAGE.shape) * 5.1 error[41, 35] = np.nan error[42, 36] = np.inf background = np.ones(IMAGE.shape) * 1.2 background[62, 55] = np.nan background[63, 56] = np.inf mask = np.zeros(IMAGE.shape).astype(bool) mask[45:55, :] = True data = np.copy(IMAGE) data[40, 40:45] = np.nan data[60, 60:65] = np.inf data[65, 65:70] = -np.inf props = SourceProperties(data, self.segm, label=2, error=error, background=background, mask=mask) # ensure mask is identical for data, error, and background assert props.data_cutout_ma.compressed().size == 677 assert (props.data_cutout_ma.compressed().size == props.filtered_data_cutout_ma.compressed().size) assert (props.data_cutout_ma.compressed().size == props.error_cutout_ma.compressed().size) assert (props.data_cutout_ma.compressed().size == props.background_cutout_ma.compressed().size) assert (len(props._filtered_data_values) == props.filtered_data_cutout_ma.compressed().size) # test for non-finite values in error and/or background outside # of the data mask tbl = props.to_table() assert np.isnan(tbl['source_sum_err']) assert np.isnan(tbl['background_sum']) assert np.isnan(tbl['background_mean']) # test that the masks are independent objects assert (np.count_nonzero(props._segment_mask) != np.count_nonzero(props._total_mask)) assert (np.count_nonzero(props._data_mask) != np.count_nonzero(props._total_mask)) assert_allclose(props._data_zeroed.sum(), props.source_sum) assert_allclose(props.data_cutout, props.make_cutout(props._data, masked_array=False)) assert_allclose(props.data_cutout_ma, props.make_cutout(props._data, masked_array=True)) assert_allclose(props.error_cutout_ma, props.make_cutout(props._error, masked_array=True)) assert_allclose(props.background_cutout_ma, props.make_cutout(props._background, masked_array=True)) def test_completely_masked(self): """Test case where a source is completely masked.""" with pytest.warns(AstropyDeprecationWarning): error = np.ones(IMAGE.shape) * 5.1 background = np.ones(IMAGE.shape) * 1.2 mask = np.ones(IMAGE.shape).astype(bool) obj = source_properties(IMAGE, self.segm, error=error, background=background, mask=mask)[0] assert np.isnan(obj.xcentroid.value) assert np.isnan(obj.ycentroid.value) assert np.isnan(obj.source_sum) assert np.isnan(obj.source_sum_err) assert np.isnan(obj.background_sum) assert np.isnan(obj.background_mean) assert np.isnan(obj.background_at_centroid) assert np.isnan(obj.area) assert np.isnan(obj.perimeter) assert np.isnan(obj.min_value) assert np.isnan(obj.max_value) assert np.isnan(obj.minval_xpos.value) assert np.isnan(obj.minval_ypos.value) assert np.isnan(obj.maxval_xpos.value) assert np.isnan(obj.maxval_ypos.value) assert np.all(np.isnan(obj.minval_cutout_pos.value)) assert np.all(np.isnan(obj.maxval_cutout_pos.value)) assert np.isnan(obj.gini) def test_repr_str(self): with pytest.warns(AstropyDeprecationWarning): props = SourceProperties(IMAGE, self.segm, label=1) assert repr(props) == str(props) attrs = ['label', 'centroid', 'sky_centroid'] for attr in attrs: assert f'{attr}:' in repr(props) @pytest.mark.skipif('not HAS_SCIPY') class TestSourcePropertiesFunctionInputs: def setup_class(self): self.segm = detect_sources(IMAGE, THRESHOLD, npixels=5) def test_segment_shape(self): with pytest.warns(AstropyDeprecationWarning): wrong_shape = np.eye(3, dtype=int) with pytest.raises(ValueError): source_properties(IMAGE, wrong_shape) def test_error_shape(self): with pytest.warns(AstropyDeprecationWarning): wrong_shape = np.ones((2, 2)) with pytest.raises(ValueError): source_properties(IMAGE, self.segm, error=wrong_shape) def test_background_shape(self): with pytest.warns(AstropyDeprecationWarning): wrong_shape = np.ones((2, 2)) with pytest.raises(ValueError): source_properties(IMAGE, self.segm, background=wrong_shape) def test_mask_shape(self): with pytest.warns(AstropyDeprecationWarning): wrong_shape = np.zeros((2, 2)).astype(bool) with pytest.raises(ValueError): source_properties(IMAGE, self.segm, mask=wrong_shape) def test_labels(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE, self.segm, labels=1) assert props[0].id == 1 def test_nosources(self): with pytest.warns(AstropyDeprecationWarning): with pytest.raises(ValueError): source_properties(IMAGE, self.segm, labels=-1) @pytest.mark.skipif('not HAS_SCIPY') class TestSourcePropertiesFunction: def setup_class(self): self.segm = detect_sources(IMAGE, THRESHOLD, npixels=5) def test_properties(self): with pytest.warns(AstropyDeprecationWarning): obj = source_properties(IMAGE, self.segm)[1] assert obj.id == 2 assert_quantity_allclose(obj.xcentroid, XCEN*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.ycentroid, YCEN*u.pix, rtol=1.e-2) assert_allclose(obj.source_sum, 16723.388) assert_quantity_allclose(obj.semimajor_axis_sigma, MAJOR_SIG*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.semiminor_axis_sigma, MINOR_SIG*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.orientation, THETA*u.rad, rtol=1.e-3) assert obj.bbox_xmin.value == 25 assert obj.bbox_xmax.value == 77 assert obj.bbox_ymin.value == 35 assert obj.bbox_ymax.value == 70 assert_quantity_allclose(obj.area, 1058.0*u.pix**2) assert_allclose(len(obj.indices), 2) assert_allclose(len(obj.indices[0]), obj.area.value) properties = ['background_at_centroid', 'background_mean', 'eccentricity', 'ellipticity', 'elongation', 'equivalent_radius', 'max_value', 'maxval_xpos', 'maxval_ypos', 'min_value', 'minval_xpos', 'minval_ypos', 'perimeter', 'cxx', 'cxy', 'cyy', 'covar_sigx2', 'covar_sigxy', 'covar_sigy2', 'bbox_xmin', 'bbox_xmax', 'bbox_ymin', 'bbox_ymax'] for propname in properties: assert not isiterable(getattr(obj, propname)) properties = ['centroid', 'covariance_eigvals', 'cutout_centroid', 'maxval_cutout_pos', 'minval_cutout_pos'] shapes = [getattr(obj, p).shape for p in properties] for shape in shapes: assert shape == (2,) properties = ['covariance', 'inertia_tensor'] shapes = [getattr(obj, p).shape for p in properties] for shape in shapes: assert shape == (2, 2) properties = ['moments', 'moments_central'] shapes = [getattr(obj, p).shape for p in properties] for shape in shapes: assert shape == (4, 4) assert obj.kron_radius.value < 1.3 assert obj.kron_flux < 16700. assert obj.kron_fluxerr is None def test_properties_background_notnone(self): with pytest.warns(AstropyDeprecationWarning): value = 1. props = source_properties(IMAGE, self.segm, background=value) assert props[0].background_mean == value assert_allclose(props[0].background_at_centroid, value) def test_properties_background_units(self): with pytest.warns(AstropyDeprecationWarning): unit = u.uJy value = 1. * unit props = source_properties(IMAGE * unit, self.segm, background=value) assert props[0].background_mean == value assert_allclose(props[0].background_at_centroid, value) def test_properties_error_background_none(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE, self.segm) assert props[0].background_cutout_ma is None assert props[0].error_cutout_ma is None def test_cutout_shapes(self): with pytest.warns(AstropyDeprecationWarning): error = np.ones(IMAGE.shape, dtype=float) props = source_properties(IMAGE, self.segm, error=error, background=1.) bbox = props[0].bbox properties = ['background_cutout_ma', 'data_cutout', 'data_cutout_ma', 'error_cutout_ma'] shapes = [getattr(props[0], p).shape for p in properties] for shape in shapes: assert shape == bbox.shape def test_make_cutout(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE, self.segm) data = np.ones((2, 2)) with pytest.raises(ValueError): props[0].make_cutout(data) @pytest.mark.parametrize(('error_value', 'background'), list(itertools.product(ERR_VALS, BACKGRD_VALS))) def test_segmentation_inputs(self, error_value, background): with pytest.warns(AstropyDeprecationWarning): error = np.ones(IMAGE.shape) * error_value props = source_properties(IMAGE, self.segm, error=error, background=background) obj = props[1] assert_quantity_allclose(obj.xcentroid, XCEN*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.ycentroid, YCEN*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.semimajor_axis_sigma, MAJOR_SIG*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.semiminor_axis_sigma, MINOR_SIG*u.pix, rtol=1.e-2) assert_quantity_allclose(obj.orientation, THETA*u.rad, rtol=1.e-3) assert obj.bbox_xmin.value == 25 assert obj.bbox_xmax.value == 77 assert obj.bbox_ymin.value == 35 assert obj.bbox_ymax.value == 70 area = obj.area.value assert_allclose(area, 1058.0) if background is not None: assert_allclose(obj.background_sum, area * background) assert_allclose(obj.source_sum, 16723.388) true_error = np.sqrt(obj.area.value) * error_value assert_allclose(obj.source_sum_err, true_error) def test_data_allzero(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE*0., self.segm) proplist = ['xcentroid', 'ycentroid', 'semimajor_axis_sigma', 'semiminor_axis_sigma', 'eccentricity', 'orientation', 'ellipticity', 'elongation', 'cxx', 'cxy', 'cyy'] for prop in proplist: assert np.isnan(getattr(props[0], prop)) def test_mask(self): with pytest.warns(AstropyDeprecationWarning): data = np.zeros((3, 3)) data[0, 1] = 1. data[1, 1] = 1. mask = np.zeros(data.shape, dtype=bool) mask[0, 1] = True segm = data.astype(int) props = source_properties(data, segm, mask=mask) assert_allclose(props[0].xcentroid.value, 1) assert_allclose(props[0].ycentroid.value, 1) assert_allclose(props[0].source_sum, 1) assert_allclose(props[0].area.value, 1) def test_mask_nomask(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE, self.segm, mask=np.ma.nomask) mask = np.zeros(IMAGE.shape).astype(bool) props2 = source_properties(IMAGE, self.segm, mask=mask) assert_allclose(props.xcentroid.value, props2.xcentroid.value) assert_allclose(props.ycentroid.value, props2.ycentroid.value) assert_quantity_allclose(props.source_sum, props2.source_sum) def test_single_pixel_segment(self): with pytest.warns(AstropyDeprecationWarning): segm = np.zeros(self.segm.shape, dtype=int) segm[50, 50] = 1 props = source_properties(IMAGE, segm) assert props[0].eccentricity == 0 def test_filtering(self): with pytest.warns(AstropyDeprecationWarning): from astropy.convolution import Gaussian2DKernel filter_kernel = Gaussian2DKernel(2.*FWHM2SIGMA, x_size=3, y_size=3) error = np.sqrt(IMAGE) props1 = source_properties(IMAGE, self.segm, error=error) props2 = source_properties(IMAGE, self.segm, error=error, filter_kernel=filter_kernel.array) p1, p2 = props1[0], props2[0] keys = ['source_sum', 'source_sum_err'] for key in keys: assert getattr(p1, key) == getattr(p2, key) keys = ['semimajor_axis_sigma', 'semiminor_axis_sigma'] for key in keys: assert getattr(p1, key) != getattr(p2, key) def test_filtering_kernel(self): with pytest.warns(AstropyDeprecationWarning): data = np.zeros((3, 3)) data[1, 1] = 1. from astropy.convolution import Gaussian2DKernel filter_kernel = Gaussian2DKernel(2.*FWHM2SIGMA, x_size=3, y_size=3) error = np.sqrt(IMAGE) props1 = source_properties(IMAGE, self.segm, error=error) props2 = source_properties(IMAGE, self.segm, error=error, filter_kernel=filter_kernel) p1, p2 = props1[0], props2[0] keys = ['source_sum', 'source_sum_err'] for key in keys: assert getattr(p1, key) == getattr(p2, key) keys = ['semimajor_axis_sigma', 'semiminor_axis_sigma'] for key in keys: assert getattr(p1, key) != getattr(p2, key) def test_data_nan(self): """Test case when data contains NaNs within a segment.""" with pytest.warns(AstropyDeprecationWarning): data = np.ones((20, 20)) data[2, 2] = np.nan segm = np.zeros((20, 20)).astype(int) segm[1:5, 1:5] = 1 segm[7:15, 7:15] = 2 segm = SegmentationImage(segm) props = source_properties(data, segm) assert_quantity_allclose(props.minval_xpos, [1, 7]*u.pix) def test_nonconsecutive_labels(self): with pytest.warns(AstropyDeprecationWarning): segm = self.segm.copy() segm.reassign_label(1, 1000) cat = source_properties(IMAGE, segm) assert len(cat) == 3 assert cat.id == [2, 3, 1000] assert cat[0].id == 2 assert cat[1].id == 3 assert cat[segm.get_index(label=1000)].id == 1000 def test_local_background(self): with pytest.warns(AstropyDeprecationWarning): props = source_properties(IMAGE, self.segm, localbkg_width=24) assert_allclose(props[0].local_background, 0., atol=1.e-7) props = source_properties(IMAGE << u.Jy, self.segm, localbkg_width=24) local_bkg = props.local_background assert isinstance(local_bkg, u.Quantity) assert_allclose(local_bkg.value, 0., atol=1.e-7) def test_wcs(self): with pytest.warns(AstropyDeprecationWarning): mywcs = make_wcs(IMAGE.shape) props = source_properties(IMAGE, self.segm, wcs=mywcs) assert props.sky_centroid is not None assert props.sky_centroid_icrs is not None assert props.sky_bbox_ll is not None assert props.sky_bbox_ul is not None assert props.sky_bbox_lr is not None assert props.sky_bbox_ur is not None tbl = props.to_table() assert len(tbl) == 3 @pytest.mark.skipif('not HAS_GWCS') def test_gwcs(self): with pytest.warns(AstropyDeprecationWarning): mywcs = make_gwcs(IMAGE.shape) props = source_properties(IMAGE, self.segm, wcs=mywcs) assert props.sky_centroid is not None assert props.sky_centroid_icrs is not None assert props.sky_bbox_ll is not None assert props.sky_bbox_ul is not None assert props.sky_bbox_lr is not None assert props.sky_bbox_ur is not None tbl = props.to_table() assert len(tbl) == 3 @pytest.mark.skipif('not HAS_SCIPY') class TestLegacySourceCatalog: def setup_class(self): self.segm = detect_sources(IMAGE, THRESHOLD, npixels=5) def test_basic(self): with pytest.warns(AstropyDeprecationWarning): segm = np.zeros(IMAGE.shape) x = y = np.arange(0, 100, 10) segm[y, x] = np.arange(10) cat = source_properties(IMAGE, segm) assert len(cat) == 9 cat2 = cat[0:5] assert len(cat2) == 5 cat3 = LegacySourceCatalog(cat2) del cat3[4] assert len(cat3) == 4 # test iteration for obj in cat: assert obj.area.value == 1 def test_inputs(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) cat2 = LegacySourceCatalog(cat[0]) assert len(cat) == 3 assert len(cat2) == 1 with pytest.raises(ValueError): LegacySourceCatalog([]) with pytest.raises(ValueError): LegacySourceCatalog('a') def test_table(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) t = cat.to_table() assert isinstance(t, QTable) assert len(t) == 3 def test_table_include(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) columns = ['id', 'xcentroid'] t = cat.to_table(columns=columns) assert isinstance(t, QTable) assert len(t) == 3 assert t.colnames == columns def test_table_include_invalidname(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) columns = ['idzz', 'xcentroidzz'] with pytest.raises(AttributeError): cat.to_table(columns=columns) def test_table_exclude(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) exclude = ['id', 'xcentroid'] t = cat.to_table(exclude_columns=exclude) assert isinstance(t, QTable) assert len(t) == 3 with pytest.raises(KeyError): t['id'] def test_table_wcs(self): with pytest.warns(AstropyDeprecationWarning): mywcs = WCS.WCS(naxis=2) rho = np.pi / 3. scale = 0.1 / 3600. mywcs.wcs.cd = [[scale*np.cos(rho), -scale*np.sin(rho)], [scale*np.sin(rho), scale*np.cos(rho)]] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] cat = source_properties(IMAGE, self.segm, wcs=mywcs) columns = ['sky_centroid', 'sky_centroid_icrs', 'sky_bbox_ll', 'sky_bbox_ul', 'sky_bbox_lr', 'sky_bbox_ur'] t = cat.to_table(columns=columns) for column in columns: assert t[0][column] is not None assert t.colnames == columns obj = cat[0] row = t[0] assert_quantity_allclose(obj.sky_bbox_ll.ra, row['sky_bbox_ll'].ra) assert_quantity_allclose(obj.sky_bbox_ll.dec, row['sky_bbox_ll'].dec) assert_quantity_allclose(obj.sky_bbox_ul.ra, row['sky_bbox_ul'].ra) assert_quantity_allclose(obj.sky_bbox_ul.dec, row['sky_bbox_ul'].dec) assert_quantity_allclose(obj.sky_bbox_lr.ra, row['sky_bbox_lr'].ra) assert_quantity_allclose(obj.sky_bbox_lr.dec, row['sky_bbox_lr'].dec) assert_quantity_allclose(obj.sky_bbox_ur.ra, row['sky_bbox_ur'].ra) assert_quantity_allclose(obj.sky_bbox_ur.dec, row['sky_bbox_ur'].dec) def test_table_no_wcs(self): with pytest.warns(AstropyDeprecationWarning): cat = source_properties(IMAGE, self.segm) columns = ['sky_centroid', 'sky_centroid_icrs', 'sky_bbox_ll', 'sky_bbox_ul', 'sky_bbox_lr', 'sky_bbox_ur'] t = cat.to_table(columns=columns) for column in columns: assert t[0][column] is None assert t.colnames == columns def test_repr_str(self): with pytest.warns(AstropyDeprecationWarning): segm = np.zeros(IMAGE.shape) x = y = np.arange(0, 100, 10) segm[y, x] = np.arange(10) cat = source_properties(IMAGE, segm) assert repr(cat) == str(cat) assert 'Catalog length:' in repr(cat) def test_kron_radius_all_masked(self): with pytest.warns(AstropyDeprecationWarning): mask = np.ones(IMAGE.shape, dtype=bool) cat = source_properties(IMAGE, self.segm, mask=mask) assert np.all(np.isnan(cat.kron_radius)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/segmentation/tests/test_utils.py0000644000214200020070000000437500000000000022652 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the _utils module. """ import numpy as np from numpy.testing import assert_allclose from .._utils import mask_to_mirrored_value def testmask_to_mirrored_value(): center = (2.0, 2.0) data = np.arange(25).reshape(5, 5) mask = np.zeros(data.shape, dtype=bool) mask[0, 0] = True mask[1, 1] = True data_ref = data.copy() data_ref[0, 0] = data[4, 4] data_ref[1, 1] = data[3, 3] mirror_data = mask_to_mirrored_value(data, mask, center) assert_allclose(mirror_data, data_ref, rtol=0, atol=1.e-6) def testmask_to_mirrored_value_range(): """ Test mask_to_mirrored_value when mirrored pixels are outside of the image. """ center = (3.0, 3.0) data = np.arange(25).reshape(5, 5) mask = np.zeros(data.shape, dtype=bool) mask[0, 0] = True mask[1, 1] = True mask[2, 2] = True data_ref = data.copy() data_ref[0, 0] = 0. data_ref[1, 1] = 0. data_ref[2, 2] = data[4, 4] mirror_data = mask_to_mirrored_value(data, mask, center) assert_allclose(mirror_data, data_ref, rtol=0, atol=1.e-6) def testmask_to_mirrored_value_masked(): """ Test mask_to_mirrored_value when mirrored pixels are also in the replace_mask. """ center = (2.0, 2.0) data = np.arange(25).reshape(5, 5) mask = np.zeros(data.shape, dtype=bool) mask[0, 0] = True mask[1, 1] = True mask[3, 3] = True mask[4, 4] = True data_ref = data.copy() data_ref[0, 0] = 0. data_ref[1, 1] = 0. data_ref[3, 3] = 0. data_ref[4, 4] = 0. mirror_data = mask_to_mirrored_value(data, mask, center) mirror_data = mask_to_mirrored_value(data, mask, center) assert_allclose(mirror_data, data_ref, rtol=0, atol=1.e-6) def testmask_to_mirrored_value_mask_keyword(): """ Test mask_to_mirrored_value when mirrored pixels are masked (via the mask keyword). """ center = (2.0, 2.0) data = np.arange(25.).reshape(5, 5) replace_mask = np.zeros(data.shape, dtype=bool) mask = np.zeros(data.shape, dtype=bool) replace_mask[0, 2] = True data[4, 2] = np.nan mask[4, 2] = True result = mask_to_mirrored_value(data, replace_mask, center, mask=mask) assert result[0, 2] == 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0218751 photutils-1.3.0/photutils/tests/0000755000214200020070000000000000000000000015373 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/tests/__init__.py0000644000214200020070000000000000000000000017472 0ustar00lbradley././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0247333 photutils-1.3.0/photutils/utils/0000755000214200020070000000000000000000000015371 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/__init__.py0000644000214200020070000000041300000000000017500 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This subpackage provides general-purpose utility functions. """ from .colormaps import * # noqa from .errors import * # noqa from .exceptions import * # noqa from .interpolation import * # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/_convolution.py0000644000214200020070000000522700000000000020467 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for convolving images with a kernel. """ import warnings from astropy.convolution import Kernel2D from astropy.units import Quantity from astropy.utils.exceptions import AstropyUserWarning import numpy as np def _filter_data(data, kernel, mode='constant', fill_value=0.0, check_normalization=False): """ Convolve a 2D image with a 2D kernel. The kernel may either be a 2D `~numpy.ndarray` or a `~astropy.convolution.Kernel2D` object. Parameters ---------- data : array_like The 2D array of the image. kernel : array-like (2D) or `~astropy.convolution.Kernel2D` The 2D kernel used to filter the input ``data``. Filtering the ``data`` will smooth the noise and maximize detectability of objects with a shape similar to the kernel. mode : {'constant', 'reflect', 'nearest', 'mirror', 'wrap'}, optional The ``mode`` determines how the array borders are handled. For the ``'constant'`` mode, values outside the array borders are set to ``fill_value``. The default is ``'constant'``. fill_value : scalar, optional Value to fill data values beyond the array borders if ``mode`` is ``'constant'``. The default is ``0.0``. check_normalization : bool, optional If `True` then a warning will be issued if the kernel is not normalized to 1. """ from scipy import ndimage if kernel is not None: if isinstance(kernel, Kernel2D): kernel_array = kernel.array else: kernel_array = kernel if check_normalization: if not np.allclose(np.sum(kernel_array), 1.0): warnings.warn('The kernel is not normalized.', AstropyUserWarning) # scipy.ndimage.convolve currently strips units, but be explicit # in case that behavior changes unit = None if isinstance(data, Quantity): unit = data.unit data = data.value # NOTE: astropy.convolution.convolve fails with zero-sum # kernels (used in findstars) (cf. astropy #1647) # NOTE: if data is int and kernel is float, ndimage.convolve # will return an int image - here we make the data float so # that a float image is always returned result = ndimage.convolve(data.astype(float), kernel_array, mode=mode, cval=fill_value) if unit is not None: result = result * unit # can't use *= with older astropy return result else: return data ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638926956.0 photutils-1.3.0/photutils/utils/_misc.py0000644000214200020070000000303100000000000017032 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools to return the installed astropy and photutils versions. """ from datetime import datetime, timezone import sys def _get_version_info(): """ Return a dictionary of the installed version numbers for photutils and its dependencies. Returns ------- result : dict A dictionary containing the version numbers for photutils and its dependencies. """ versions = {'Python': sys.version.split()[0]} packages = ('photutils', 'astropy', 'numpy', 'scipy', 'skimage', 'sklearn', 'matplotlib', 'gwcs', 'bottleneck') for package in packages: try: pkg = __import__(package) version = pkg.__version__ except ImportError: version = None versions[package] = version return versions def _get_date(utc=False): """ Return a string of the current date/time. Parameters ---------- utz : bool, optional Whether to use the UTZ timezone instead of the local timezone. Returns ------- result : str The current date/time. """ if not utc: now = datetime.now().astimezone() else: now = datetime.now(timezone.utc) return now.strftime('%Y-%m-%d %H:%M:%S %Z') def _get_meta(utc=False): """ Return a metadata dictionary with the package versions and current date/time. """ return {'date': _get_date(utc=utc), 'version': _get_version_info()} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/_moments.py0000644000214200020070000000322100000000000017562 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provide tools for calculating image moments. """ import numpy as np __all__ = ['_moments_central', '_moments'] def _moments_central(data, center=None, order=1): """ Calculate the central image moments up to the specified order. Parameters ---------- data : 2D array-like The input 2D array. center : tuple of two floats or `None`, optional The ``(x, y)`` center position. If `None` it will calculated as the "center of mass" of the input ``data``. order : int, optional The maximum order of the moments to calculate. Returns ------- moments : 2D `~numpy.ndarray` The central image moments. """ data = np.asarray(data).astype(float) if data.ndim != 2: raise ValueError('data must be a 2D array.') if center is None: from ..centroids import centroid_com center = centroid_com(data) indices = np.ogrid[[slice(0, i) for i in data.shape]] ypowers = (indices[0] - center[1]) ** np.arange(order + 1) xpowers = np.transpose(indices[1] - center[0]) ** np.arange(order + 1) return np.dot(np.dot(np.transpose(ypowers), data), xpowers) def _moments(data, order=1): """ Calculate the raw image moments up to the specified order. Parameters ---------- data : 2D array-like The input 2D array. order : int, optional The maximum order of the moments to calculate. Returns ------- moments : 2D `~numpy.ndarray` The raw image moments. """ return _moments_central(data, center=(0, 0), order=order) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/_optional_deps.py0000644000214200020070000000154300000000000020745 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """Checks for optional dependencies using lazy import from `PEP 562 `_. """ import importlib # This list is a duplicate of the dependencies in setup.cfg "all". # Note that in some cases the package names are different from the # pip-install name (e.g.k scikit-image -> skimage). optional_deps = ['scipy', 'matplotlib', 'skimage', 'sklearn', 'gwcs', 'bottleneck'] deps = {key.upper(): key for key in optional_deps} __all__ = [f'HAS_{pkg}' for pkg in deps] def __getattr__(name): if name in __all__: try: importlib.import_module(deps[name[4:]]) except (ImportError, ModuleNotFoundError): return False return True raise AttributeError(f'Module {__name__!r} has no attribute {name!r}.') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639109126.0 photutils-1.3.0/photutils/utils/_round.py0000644000214200020070000000100200000000000017222 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools to round numpy arrays. """ import numpy as np def _py2intround(a): """ Round the input to the nearest integer. If two integers are equally close, rounding is done away from 0. """ data = np.asanyarray(a) value = np.where(data >= 0, np.floor(data + 0.5), np.ceil(data - 0.5)).astype(int) if not hasattr(a, '__iter__'): value = value.item() return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/_wcs_helpers.py0000644000214200020070000000413600000000000020424 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst # (taken from photutils: should probably migrate into astropy.wcs) """ This module provides WCS helper tools. """ import astropy.units as u import numpy as np def _pixel_scale_angle_at_skycoord(skycoord, wcs, offset=1 * u.arcsec): """ Calculate the pixel coordinate and scale and WCS rotation angle at the position of a SkyCoord coordinate. Parameters ---------- skycoord : `~astropy.coordinates.SkyCoord` The SkyCoord coordinate. wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS `_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). offset : `~astropy.units.Quantity` A small angular offset to use to compute the pixel scale and position angle. Returns ------- xypos : tuple of float The (x, y) pixel coordinate. scale : `~astropy.units.Quantity` The pixel scale in arcsec/pixel. angle : `~astropy.units.Quantity` The angle (in degrees) measured counterclockwise from the positive x axis to the "North" axis of the celestial coordinate system. Notes ----- If distortions are present in the image, the x and y pixel scales likely differ. This function computes a single pixel scale along the North/South axis. """ # Convert to pixel coordinates xpos, ypos = wcs.world_to_pixel(skycoord) # We take a point directly North (i.e., latitude offset) the # input sky coordinate and convert it to pixel coordinates, # then we use the pixel deltas between the input and offset sky # coordinate to calculate the pixel scale and angle. skycoord_offset = skycoord.directional_offset_by(0.0, offset) x_offset, y_offset = wcs.world_to_pixel(skycoord_offset) dx = x_offset - xpos dy = y_offset - ypos scale = offset.to(u.arcsec) / (np.hypot(dx, dy) * u.pixel) angle = (np.arctan2(dy, dx) * u.radian).to(u.deg) return (xpos, ypos), scale, angle ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/colormaps.py0000644000214200020070000000236100000000000017744 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for generating matplotlib colormaps. """ import numpy as np __all__ = ['make_random_cmap'] def make_random_cmap(ncolors=256, seed=None): """ Make a matplotlib colormap consisting of (random) muted colors. A random colormap is very useful for plotting segmentation images. Parameters ---------- ncolors : int, optional The number of colors in the colormap. The default is 256. seed : int, optional A seed to initialize the `numpy.random.BitGenerator`. If `None`, then fresh, unpredictable entropy will be pulled from the OS. Separate function calls with the same ``seed`` will generate the same colormap. Returns ------- cmap : `matplotlib.colors.ListedColormap` The matplotlib colormap with random colors. """ from matplotlib import colors rng = np.random.default_rng(seed) hue = rng.uniform(low=0.0, high=1.0, size=ncolors) sat = rng.uniform(low=0.2, high=0.7, size=ncolors) val = rng.uniform(low=0.5, high=1.0, size=ncolors) hsv = np.dstack((hue, sat, val)) rgb = np.squeeze(colors.hsv_to_rgb(hsv)) return colors.ListedColormap(rgb) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/errors.py0000644000214200020070000001707100000000000017265 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for calculating total error arrays. """ import astropy.units as u from astropy.utils.misc import isiterable import numpy as np __all__ = ['calc_total_error'] def calc_total_error(data, bkg_error, effective_gain): r""" Calculate a total error array, combining a background-only error array with the Poisson noise of sources. Parameters ---------- data : array_like or `~astropy.units.Quantity` The background-subtracted data array. bkg_error : array_like or `~astropy.units.Quantity` The 1-sigma background-only errors of the input ``data``. ``bkg_error`` should include all sources of "background" error but *exclude* the Poisson error of the sources. ``bkg_error`` must have the same shape as ``data``. If ``data`` and ``bkg_error`` are `~astropy.units.Quantity` objects, then they must have the same units. effective_gain : float, array-like, or `~astropy.units.Quantity` Ratio of counts (e.g., electrons or photons) to the units of ``data`` used to calculate the Poisson error of the sources. If ``effective_gain`` is zero (or contains zero values in an array), then the source Poisson noise component will not be included. In other words, the returned total error value will simply be the ``bkg_error`` value for pixels where ``effective_gain`` is zero. ``effective_gain`` cannot not be negative or contain negative values. Returns ------- total_error : `~numpy.ndarray` or `~astropy.units.Quantity` The total error array. If ``data``, ``bkg_error``, and ``effective_gain`` are all `~astropy.units.Quantity` objects, then ``total_error`` will also be returned as a `~astropy.units.Quantity` object with the same units as the input ``data``. Otherwise, a `~numpy.ndarray` will be returned. Notes ----- To use units, ``data``, ``bkg_error``, and ``effective_gain`` must *all* be `~astropy.units.Quantity` objects. ``data`` and ``bkg_error`` must have the same units. A `ValueError` will be raised if only some of the inputs are `~astropy.units.Quantity` objects or if the ``data`` and ``bkg_error`` units differ. The source Poisson error in countable units (e.g., electrons or photons) is: .. math:: \sigma_{\mathrm{src}} = \sqrt{g_{\mathrm{eff}} I} where :math:`g_{\mathrm{eff}}` is the effective gain (``effective_gain``; image or scalar) and :math:`I` is the ``data`` image. The total error is the combination of the background-only error and the source Poisson error. The total error array :math:`\sigma_{\mathrm{tot}}` in countable units (e.g., electrons or photons) is therefore: .. math:: \sigma_{\mathrm{tot}} = \sqrt{g_{\mathrm{eff}}^2 \sigma_{\mathrm{bkg}}^2 + g_{\mathrm{eff}} I} where :math:`\sigma_{\mathrm{bkg}}` is the background-only error image (``bkg_error``). Converting back to the input ``data`` units gives: .. math:: \sigma_{\mathrm{tot}} = \frac{1}{g_{\mathrm{eff}}} \sqrt{g_{\mathrm{eff}}^2 \sigma_{\mathrm{bkg}}^2 + g_{\mathrm{eff}} I} .. math:: \sigma_{\mathrm{tot}} = \sqrt{\sigma_{\mathrm{bkg}}^2 + \frac{I}{g_{\mathrm{eff}}}} ``effective_gain`` can either be a scalar value or a 2D image with the same shape as the ``data``. A 2D ``effective_gain`` image is useful when the input ``data`` has variable depths across the field (e.g., a mosaic image with non-uniform exposure times). For example, if your input ``data`` are in units of electrons/s then ideally ``effective_gain`` should be an exposure-time map. The Poisson noise component is not included in the output total error for pixels where ``data`` (:math:`I_i)` is negative. For such pixels, :math:`\sigma_{\mathrm{tot}, i} = \sigma_{\mathrm{bkg}, i}`. The Poisson noise component is also not included in the output total error for pixels where the effective gain (:math:`g_{\mathrm{eff}, i}`) is zero. For such pixels, :math:`\sigma_{\mathrm{tot}, i} = \sigma_{\mathrm{bkg}, i}`. To replicate `SourceExtractor`_ errors when it is configured to consider weight maps as gain maps (i.e., 'WEIGHT_GAIN=Y'; which is the default), one should input an ``effective_gain`` calculated as: .. math:: g_{\mathrm{eff}}^{\prime} = g_{\mathrm{eff}} \left( \frac{\mathrm{RMS_{\mathrm{median}}^2}}{\sigma_{\mathrm{bkg}}^2} \right) where :math:`g_{\mathrm{eff}}` is the effective gain, :math:`\sigma_{\mathrm{bkg}}` are the background-only errors, and :math:`\mathrm{RMS_{\mathrm{median}}}` is the median value of the low-resolution background RMS map generated by `SourceExtractor`_. When running `SourceExtractor`_, this value is printed to stdout as "(M+D) RMS: ". If you are using `~photutils.background.Background2D`, the median value of the low-resolution background RMS map is returned via the `~photutils.background.Background2D.background_rms_median` attribute. In that case the total error is: .. math:: \sigma_{\mathrm{tot}} = \sqrt{\sigma_{\mathrm{bkg}}^2 + \left(\frac{I}{g_{\mathrm{eff}}}\right) \left(\frac{\sigma_{\mathrm{bkg}}^2} {\mathrm{RMS_{\mathrm{median}}^2}}\right)} .. _SourceExtractor: https://sextractor.readthedocs.io/en/latest/ """ data = np.asanyarray(data) bkg_error = np.asanyarray(bkg_error) inputs = [data, bkg_error, effective_gain] has_unit = [hasattr(x, 'unit') for x in inputs] use_units = all(has_unit) if any(has_unit) and not use_units: raise ValueError('If any of data, bkg_error, or effective_gain has ' 'units, then they all must all have units.') if use_units: if data.unit != bkg_error.unit: raise ValueError('data and bkg_error must have the same units.') count_units = [u.electron, u.photon] datagain_unit = data.unit * effective_gain.unit if datagain_unit not in count_units: raise u.UnitsError('(data * effective_gain) has units of "{0}", ' 'but it must have count units (e.g., ' 'u.electron or u.photon).' .format(datagain_unit)) if not isiterable(effective_gain): effective_gain = np.zeros(data.shape) + effective_gain else: effective_gain = np.asanyarray(effective_gain) if effective_gain.shape != data.shape: raise ValueError('If input effective_gain is 2D, then it must ' 'have the same shape as the input data.') if np.any(effective_gain < 0): raise ValueError('effective_gain must be non-zero everywhere.') if use_units: unit = data.unit data = data.value effective_gain = effective_gain.value # do not include source variance where effective_gain = 0 source_variance = np.where(effective_gain != 0, data / effective_gain, 0) # do not include source variance where data is negative (note that # effective_gain cannot be negative) source_variance = np.maximum(source_variance, 0) if use_units: # source_variance is calculated to have units of (data.unit)**2 # so that it can be added with bkg_error**2 below. The returned # total error will have units of data.unit. source_variance <<= unit**2 return np.sqrt(bkg_error**2 + source_variance) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/utils/exceptions.py0000644000214200020070000000047700000000000020134 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides custom exceptions. """ from astropy.utils.exceptions import AstropyWarning __all__ = ['NoDetectionsWarning'] class NoDetectionsWarning(AstropyWarning): """ A warning class to indicate no sources were detected. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/interpolation.py0000644000214200020070000002621000000000000020633 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module provides tools for interpolating data. """ import numpy as np __all__ = ['ShepardIDWInterpolator'] __doctest_requires__ = {('ShepardIDWInterpolator'): ['scipy']} class ShepardIDWInterpolator: """ Class to perform Inverse Distance Weighted (IDW) interpolation. This interpolator uses a modified version of `Shepard's method `_ (see the Notes section for details). Parameters ---------- coordinates : float, 1D array-like, or NxM-array-like Coordinates of the known data points. In general, it is expected that these coordinates are in a form of a NxM-like array where N is the number of points and M is dimension of the coordinate space. When M=1 (1D space), then the ``coordinates`` parameter may be entered as a 1D array or, if only one data point is available, ``coordinates`` can be a scalar number representing the 1D coordinate of the data point. .. note:: If the dimensionality of ``coordinates`` is larger than 2, e.g., if it is of the form N1 x N2 x N3 x ... x Nn x M, then it will be flattened to form an array of size NxM where N = N1 * N2 * ... * Nn. values : float or 1D array-like Values of the data points corresponding to each coordinate provided in ``coordinates``. In general a 1D array is expected. When a single data point is available, then ``values`` can be a scalar number. .. note:: If the dimensionality of ``values`` is larger than 1 then it will be flattened. weights : float or 1D array-like, optional Weights to be associated with each data value. These weights, if provided, will be combined with inverse distance weights (see the Notes section for details). When ``weights`` is `None` (default), then only inverse distance weights will be used. When provided, this input parameter must have the same form as ``values``. leafsize : float, optional The number of points at which the k-d tree algorithm switches over to brute-force. ``leafsize`` must be positive. See `scipy.spatial.cKDTree` for further information. Notes ----- This interpolator uses a slightly modified version of `Shepard's method `_. The essential difference is the introduction of a "regularization" parameter (``reg``) that is used when computing the inverse distance weights: .. math:: w_i = 1 / (d(x, x_i)^{power} + r) By supplying a positive regularization parameter one can avoid singularities at the locations of the data points as well as control the "smoothness" of the interpolation (e.g., make the weights of the neighbors less varied). The "smoothness" of interpolation can also be controlled by the power parameter (``power``). Examples -------- This class can can be instantiated using the following syntax:: >>> from photutils.utils import ShepardIDWInterpolator as idw Example of interpolating 1D data:: >>> import numpy as np >>> rng = np.random.default_rng(0) >>> x = rng.random(100) # 100 random values >>> y = np.sin(x) >>> f = idw(x, y) >>> f(0.4) # doctest: +FLOAT_CMP 0.38937843420912366 >>> np.sin(0.4) # doctest: +FLOAT_CMP 0.3894183423086505 >>> xi = rng.random(4) # 4 random values >>> xi # doctest: +FLOAT_CMP array([0.47998792, 0.23237292, 0.80188058, 0.92353016]) >>> f(xi) # doctest: +FLOAT_CMP array([0.46577097, 0.22837422, 0.71856662, 0.80125391]) >>> np.sin(xi) # doctest: +FLOAT_CMP array([0.46176846, 0.23028731, 0.71866503, 0.7977353 ]) NOTE: In the last example, ``xi`` may be a ``Nx1`` array instead of a 1D vector. Example of interpolating 2D data:: >>> rng = np.random.default_rng(0) >>> pos = rng.random((1000, 2)) >>> val = np.sin(pos[:, 0] + pos[:, 1]) >>> f = idw(pos, val) >>> f([0.5, 0.6]) # doctest: +FLOAT_CMP 0.8948257014687874 >>> np.sin(0.5 + 0.6) # doctest: +FLOAT_CMP 0.8912073600614354 """ def __init__(self, coordinates, values, weights=None, leafsize=10): from scipy.spatial import cKDTree coordinates = np.asarray(coordinates) if coordinates.ndim == 0: # scalar coordinate coordinates = np.atleast_2d(coordinates) if coordinates.ndim == 1: coordinates = np.transpose(np.atleast_2d(coordinates)) if coordinates.ndim > 2: coordinates = np.reshape(coordinates, (-1, coordinates.shape[-1])) values = np.asanyarray(values).ravel() ncoords = coordinates.shape[0] if ncoords < 1: raise ValueError('You must enter at least one data point.') if values.shape[0] != ncoords: raise ValueError('The number of values must match the number ' 'of coordinates.') if weights is not None: weights = np.asanyarray(weights).ravel() if weights.shape[0] != ncoords: raise ValueError('The number of weights must match the ' 'number of coordinates.') if np.any(weights < 0.0): raise ValueError('All weight values must be non-negative ' 'numbers.') self.coordinates = coordinates self.ncoords = ncoords self.coords_ndim = coordinates.shape[1] self.values = values self.weights = weights self.kdtree = cKDTree(coordinates, leafsize=leafsize) def __call__(self, positions, n_neighbors=8, eps=0.0, power=1.0, reg=0.0, conf_dist=1e-12, dtype=float): """ Evaluate the interpolator at the given positions. Parameters ---------- positions : float, 1D array-like, or NxM-array-like Coordinates of the position(s) at which the interpolator should be evaluated. In general, it is expected that these coordinates are in a form of a NxM-like array where N is the number of points and M is dimension of the coordinate space. When M=1 (1D space), then the ``positions`` parameter may be input as a 1D-like array or, if only one data point is available, ``positions`` can be a scalar number representing the 1D coordinate of the data point. .. note:: If the dimensionality of the ``positions`` argument is larger than 2, e.g., if it is of the form N1 x N2 x N3 x ... x Nn x M, then it will be flattened to form an array of size NxM where N = N1 * N2 * ... * Nn. .. warning:: The dimensionality of ``positions`` must match the dimensionality of the ``coordinates`` used during the initialization of the interpolator. n_neighbors : int, optional The maximum number of nearest neighbors to use during the interpolation. eps : float, optional Set to use approximate nearest neighbors; the kth neighbor is guaranteed to be no further than (1 + ``eps``) times the distance to the real *k*-th nearest neighbor. See `scipy.spatial.cKDTree.query` for further information. power : float, optional The power of the inverse distance used for the interpolation weights. See the Notes section for more details. reg : float, optional The regularization parameter. It may be used to control the smoothness of the interpolator. See the Notes section for more details. conf_dist : float, optional The confusion distance below which the interpolator should use the value of the closest data point instead of attempting to interpolate. This is used to avoid singularities at the known data points, especially if ``reg`` is 0.0. dtype : data-type, optional The data type of the output interpolated values. If `None` then the type will be inferred from the type of the ``values`` parameter used during the initialization of the interpolator. """ n_neighbors = int(n_neighbors) if n_neighbors < 1: raise ValueError('n_neighbors must be a positive integer') if conf_dist is not None and conf_dist <= 0.0: conf_dist = None positions = np.asanyarray(positions) if positions.ndim == 0: # assume we have a single 1D coordinate if self.coords_ndim != 1: raise ValueError('The dimensionality of the input position ' 'does not match the dimensionality of the ' 'coordinates used to initialize the ' 'interpolator.') elif positions.ndim == 1: # assume we have a single point if (self.coords_ndim != 1 and (positions.shape[-1] != self.coords_ndim)): raise ValueError('The input position was provided as a 1D ' 'array, but its length does not match the ' 'dimensionality of the coordinates used ' 'to initialize the interpolator.') elif positions.ndim != 2: raise ValueError('The input positions must be an array-like ' 'object of dimensionality no larger than 2.') positions = np.reshape(positions, (-1, self.coords_ndim)) npositions = positions.shape[0] distances, idx = self.kdtree.query(positions, k=n_neighbors, eps=eps) if n_neighbors == 1: return self.values[idx] if dtype is None: dtype = self.values.dtype interp_values = np.zeros(npositions, dtype=dtype) for k in range(npositions): valid_idx = np.isfinite(distances[k]) idk = idx[k][valid_idx] dk = distances[k][valid_idx] if dk.shape[0] == 0: interp_values[k] = np.nan continue if conf_dist is not None: # check if we are close to a known data point confused = (dk <= conf_dist) if np.any(confused): interp_values[k] = self.values[idk[confused][0]] continue w = 1.0 / ((dk ** power) + reg) if self.weights is not None: w *= self.weights[idk] wtot = np.sum(w) if wtot > 0.0: interp_values[k] = np.dot(w, self.values[idk]) / wtot else: interp_values[k] = np.nan if len(interp_values) == 1: return interp_values[0] else: return interp_values ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0262332 photutils-1.3.0/photutils/utils/tests/0000755000214200020070000000000000000000000016533 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/utils/tests/__init__.py0000644000214200020070000000000000000000000020632 0ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/tests/test_colormaps.py0000644000214200020070000000076000000000000022146 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the colormaps module. """ from numpy.testing import assert_allclose import pytest from ..colormaps import make_random_cmap from .._optional_deps import HAS_MATPLOTLIB # noqa @pytest.mark.skipif('not HAS_MATPLOTLIB') def test_colormap(): ncolors = 100 cmap = make_random_cmap(ncolors, seed=0) assert len(cmap.colors) == ncolors assert_allclose(cmap.colors[0], [0.36951484, 0.42125961, 0.65984082]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/tests/test_convolution.py0000644000214200020070000000451300000000000022526 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the convolution module. """ from astropy.convolution import Gaussian2DKernel from astropy.tests.helper import catch_warnings import astropy.units as u from astropy.utils.exceptions import AstropyUserWarning from numpy.testing import assert_allclose import pytest from .._convolution import _filter_data from .._optional_deps import HAS_SCIPY # noqa from ...datasets import make_100gaussians_image @pytest.mark.skipif('not HAS_SCIPY') class TestFilterData: def setup_class(self): self.data = make_100gaussians_image() self.kernel = Gaussian2DKernel(3., x_size=3, y_size=3) def test_filter_data(self): filt_data1 = _filter_data(self.data, self.kernel) filt_data2 = _filter_data(self.data, self.kernel.array) assert_allclose(filt_data1, filt_data2) def test_filter_data_units(self): unit = u.electron filt_data = _filter_data(self.data * unit, self.kernel) assert isinstance(filt_data, u.Quantity) assert filt_data.unit == unit def test_filter_data_types(self): """ Test to ensure output is a float array for integer input data. """ filt_data = _filter_data(self.data.astype(int), self.kernel.array.astype(int)) assert filt_data.dtype == float filt_data = _filter_data(self.data.astype(int), self.kernel.array.astype(float)) assert filt_data.dtype == float filt_data = _filter_data(self.data.astype(float), self.kernel.array.astype(int)) assert filt_data.dtype == float filt_data = _filter_data(self.data.astype(float), self.kernel.array.astype(float)) assert filt_data.dtype == float def test_filter_data_kernel_none(self): """ Test for kernel=None. """ kernel = None filt_data = _filter_data(self.data, kernel) assert_allclose(filt_data, self.data) def test_filter_data_check_normalization(self): """ Test kernel normalization check. """ with catch_warnings(AstropyUserWarning) as w: _filter_data(self.data, self.kernel, check_normalization=True) assert len(w) == 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623768821.0 photutils-1.3.0/photutils/utils/tests/test_errors.py0000644000214200020070000000475500000000000021473 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the errors module. """ import astropy.units as u import numpy as np from numpy.testing import assert_allclose import pytest from ..errors import calc_total_error SHAPE = (5, 5) DATAVAL = 2. DATA = np.ones(SHAPE) * DATAVAL BKG_ERROR = np.ones(SHAPE) EFFGAIN = np.ones(SHAPE) * DATAVAL BACKGROUND = np.ones(SHAPE) WRONG_SHAPE = np.ones((2, 2)) def test_error_shape(): with pytest.raises(ValueError): calc_total_error(DATA, WRONG_SHAPE, EFFGAIN) def test_gain_shape(): with pytest.raises(ValueError): calc_total_error(DATA, BKG_ERROR, WRONG_SHAPE) @pytest.mark.parametrize('effective_gain', (-1, -100)) def test_gain_negative(effective_gain): with pytest.raises(ValueError): calc_total_error(DATA, BKG_ERROR, effective_gain) def test_gain_scalar(): error_tot = calc_total_error(DATA, BKG_ERROR, 2.) assert_allclose(error_tot, np.sqrt(2.) * BKG_ERROR) def test_gain_array(): error_tot = calc_total_error(DATA, BKG_ERROR, EFFGAIN) assert_allclose(error_tot, np.sqrt(2.) * BKG_ERROR) def test_gain_zero(): error_tot = calc_total_error(DATA, BKG_ERROR, 0.) assert_allclose(error_tot, BKG_ERROR) effgain = np.copy(EFFGAIN) effgain[0, 0] = 0 effgain[1, 1] = 0 mask = (effgain == 0) error_tot = calc_total_error(DATA, BKG_ERROR, effgain) assert_allclose(error_tot[mask], BKG_ERROR[mask]) assert_allclose(error_tot[~mask], np.sqrt(2)) def test_units(): units = u.electron / u.s error_tot1 = calc_total_error(DATA * units, BKG_ERROR * units, EFFGAIN * u.s) assert error_tot1.unit == units error_tot2 = calc_total_error(DATA, BKG_ERROR, EFFGAIN) assert_allclose(error_tot1.value, error_tot2) def test_error_units(): units = u.electron / u.s with pytest.raises(ValueError): calc_total_error(DATA * units, BKG_ERROR * u.electron, EFFGAIN * u.s) def test_effgain_units(): units = u.electron / u.s with pytest.raises(u.UnitsError): calc_total_error(DATA * units, BKG_ERROR * units, EFFGAIN * u.km) def test_missing_bkgerror_units(): units = u.electron / u.s with pytest.raises(ValueError): calc_total_error(DATA * units, BKG_ERROR, EFFGAIN * u.s) def test_missing_effgain_units(): units = u.electron / u.s with pytest.raises(ValueError): calc_total_error(DATA * units, BKG_ERROR * units, EFFGAIN) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/photutils/utils/tests/test_interpolation.py0000644000214200020070000000736400000000000023045 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the interpolation module. """ import numpy as np from numpy.testing import assert_allclose import pytest from .. import ShepardIDWInterpolator as idw from .._optional_deps import HAS_SCIPY # noqa SHAPE = (5, 5) DATA = np.ones(SHAPE) * 2.0 MASK = np.zeros(DATA.shape, dtype=bool) MASK[2, 2] = True ERROR = np.ones(SHAPE) BACKGROUND = np.ones(SHAPE) WRONG_SHAPE = np.ones((2, 2)) @pytest.mark.skipif('not HAS_SCIPY') class TestShepardIDWInterpolator: def setup_class(self): self.rng = np.random.default_rng(0) self.x = self.rng.random(100) self.y = np.sin(self.x) self.f = idw(self.x, self.y) @pytest.mark.parametrize('positions', [0.4, np.arange(2, 5) * 0.1]) def test_idw_1d(self, positions): f = idw(self.x, self.y) assert_allclose(f(positions), np.sin(positions), atol=1e-2) def test_idw_weights(self): weights = self.y * 0.1 f = idw(self.x, self.y, weights=weights) pos = 0.4 assert_allclose(f(pos), np.sin(pos), atol=1e-2) def test_idw_2d(self): pos = self.rng.random((1000, 2)) val = np.sin(pos[:, 0] + pos[:, 1]) f = idw(pos, val) x = 0.5 y = 0.6 assert_allclose(f([x, y]), np.sin(x + y), atol=1e-2) def test_idw_3d(self): val = np.ones((3, 3, 3)) pos = np.indices(val.shape) f = idw(pos, val) assert_allclose(f([0.5, 0.5, 0.5]), 1.0) def test_no_coordinates(self): with pytest.raises(ValueError): idw([], 0) def test_values_invalid_shape(self): with pytest.raises(ValueError): idw(self.x, 0) def test_weights_invalid_shape(self): with pytest.raises(ValueError): idw(self.x, self.y, weights=10) def test_weights_negative(self): with pytest.raises(ValueError): idw(self.x, self.y, weights=-self.y) def test_n_neighbors_one(self): assert_allclose(self.f(0.5, n_neighbors=1), [0.479334], rtol=3e-7) def test_n_neighbors_negative(self): with pytest.raises(ValueError): self.f(0.5, n_neighbors=-1) def test_conf_dist_negative(self): assert_allclose(self.f(0.5, conf_dist=-1), self.f(0.5, conf_dist=None)) def test_dtype_none(self): result = self.f(0.5, dtype=None) assert result.dtype == float def test_positions_0d_nomatch(self): """test when position ndim doesn't match coordinates ndim""" pos = self.rng.random((10, 2)) val = np.sin(pos[:, 0] + pos[:, 1]) f = idw(pos, val) with pytest.raises(ValueError): f(0.5) def test_positions_1d_nomatch(self): """test when position ndim doesn't match coordinates ndim""" pos = self.rng.random((10, 2)) val = np.sin(pos[:, 0] + pos[:, 1]) f = idw(pos, val) with pytest.raises(ValueError): f([0.5]) def test_positions_3d(self): with pytest.raises(ValueError): self.f(np.ones((3, 3, 3))) def test_scalar_values_1d(self): value = 10. f = idw(2, value) assert_allclose(f(2), value) assert_allclose(f(-1), value) assert_allclose(f(0), value) assert_allclose(f(142), value) def test_scalar_values_2d(self): value = 10. f = idw([[1, 2]], value) assert_allclose(f([1, 2]), value) assert_allclose(f([-1, 0]), value) assert_allclose(f([142, 213]), value) def test_scalar_values_3d(self): value = 10. f = idw([[7, 4, 1]], value) assert_allclose(f([7, 4, 1]), value) assert_allclose(f([-1, 0, 7]), value) assert_allclose(f([142, 213, 5]), value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1610665576.0 photutils-1.3.0/photutils/utils/tests/test_moments.py0000644000214200020070000000220600000000000021626 0ustar00lbradley# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests for the moments module. """ import numpy as np from numpy.testing import assert_equal, assert_allclose import pytest from .._moments import _moments, _moments_central def test_moments(): data = np.array([[0, 1], [0, 1]]) moments = _moments(data, order=2) result = np.array([[2, 2, 2], [1, 1, 1], [1, 1, 1]]) assert_equal(moments, result) assert_allclose(moments[0, 1] / moments[0, 0], 1.0) assert_allclose(moments[1, 0] / moments[0, 0], 0.5) def test_moments_central(): data = np.array([[0, 1], [0, 1]]) moments = _moments_central(data, order=2) result = np.array([[2., 0., 0.], [0., 0., 0.], [0.5, 0., 0.]]) assert_allclose(moments, result) def test_moments_central_nonsquare(): data = np.array([[0, 1], [0, 1], [0, 1]]) moments = _moments_central(data, order=2) result = np.array([[3., 0., 0.], [0., 0., 0.], [2., 0., 0.]]) assert_allclose(moments, result) def test_moments_central_invalid_dim(): data = np.arange(27).reshape(3, 3, 3) with pytest.raises(ValueError): _moments_central(data, order=3) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils/version.py0000644000214200020070000000052100000000000016266 0ustar00lbradley# Note that we need to fall back to the hard-coded version if either # setuptools_scm can't be imported or setuptools_scm can't determine the # version, so we catch the generic 'Exception'. try: from setuptools_scm import get_version version = get_version(root='..', relative_to=__file__) except Exception: version = '1.3.0' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123871.9777358 photutils-1.3.0/photutils.egg-info/0000755000214200020070000000000000000000000015723 5ustar00lbradley././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/PKG-INFO0000644000214200020070000001047200000000000017024 0ustar00lbradleyMetadata-Version: 2.1 Name: photutils Version: 1.3.0 Summary: An Astropy package for source detection and photometry Home-page: https://github.com/astropy/photutils Author: Photutils Developers Author-email: photutils.team@gmail.com License: BSD 3-Clause Keywords: astronomy,astrophysics,photometry,aperture,psf,source detection,background,segmentation,centroids,isophote,morphology Platform: UNKNOWN Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Cython Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Scientific/Engineering :: Astronomy Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: all Provides-Extra: test Provides-Extra: docs License-File: LICENSE.rst ========= Photutils ========= |PyPI Version| |Conda Version| |PyPI Downloads| |Astropy| |CI Status| |CircleCI Status| |Codecov Status| |Latest RTD Status| |LGTM Grade| Photutils is an `Astropy`_ package for detection and photometry of astronomical sources. Please see the `online documentation `_ for `installation instructions `_ and usage information. Citing Photutils ---------------- |Zenodo| If you use Photutils for a project that leads to a publication, whether directly or as a dependency of another package, please include the following acknowledgment:: This research made use of Photutils, an Astropy package for detection and photometry of astronomical sources (Bradley et al. 20XX). where (Bradley et al. 20XX) is a citation to the `Zenodo record `_ of the Photutils version that was used. We also encourage citations in the main text wherever appropriate. Please see the `CITATION `_ file for details and an example BibTeX entry. License ------- Photutils is licensed under a 3-clause BSD license. Please see the `LICENSE `_ file for details. .. |PyPI Version| image:: https://img.shields.io/pypi/v/photutils.svg?logo=pypi&logoColor=white&label=PyPI :target: https://pypi.org/project/photutils/ :alt: PyPI Latest Release .. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/photutils :target: https://anaconda.org/conda-forge/photutils :alt: Conda Latest Release .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/photutils?label=PyPI%20Downloads :target: https://pypistats.org/packages/photutils :alt: PyPI Downloads .. |Astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: https://www.astropy.org/ :alt: Powered by Astropy .. |Zenodo| image:: https://zenodo.org/badge/2640766.svg :target: https://zenodo.org/badge/latestdoi/2640766 :alt: Zenodo Latest DOI .. |CI Status| image:: https://github.com/astropy/photutils/workflows/CI%20Tests/badge.svg# :target: https://github.com/astropy/photutils/actions :alt: CI Status .. |Codecov Status| image:: https://img.shields.io/codecov/c/github/astropy/photutils?logo=codecov :target: https://codecov.io/gh/astropy/photutils :alt: Coverage Status .. |CircleCI Status| image:: https://img.shields.io/circleci/build/github/astropy/photutils/main?logo=circleci&label=CircleCI :target: https://circleci.com/gh/astropy/photutils :alt: CircleCI Status .. |Stable RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=stable :target: https://photutils.readthedocs.io/en/stable/ :alt: Stable Documentation Status .. |Latest RTD Status| image:: https://img.shields.io/readthedocs/photutils/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=latest :target: https://photutils.readthedocs.io/en/latest/ :alt: Latest Documentation Status .. |LGTM Grade| image:: https://img.shields.io/lgtm/grade/python/g/astropy/photutils.svg?logo=lgtm&logoWidth=18 :target: https://lgtm.com/projects/g/astropy/photutils/context:python :alt: LGTM Grade .. _Astropy: https://www.astropy.org/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/SOURCES.txt0000644000214200020070000002045000000000000017610 0ustar00lbradley.bandit.yaml .gitignore .lgtm.yml .pep8speaks.yml .readthedocs.yml CHANGES.rst CITATION.rst CODE_OF_CONDUCT.rst CONTRIBUTING.rst LICENSE.rst MANIFEST.in README.rst codecov.yml pyproject.toml setup.cfg setup.py tox.ini ./photutils/geometry/circular_overlap.pyx ./photutils/geometry/core.pyx ./photutils/geometry/elliptical_overlap.pyx ./photutils/geometry/rectangular_overlap.pyx .circleci/config.yml .github/workflows/ci_tests.yml .github/workflows/cron_tests.yml docs/Makefile docs/aperture.rst docs/background.rst docs/centroids.rst docs/changelog.rst docs/citation.rst docs/conf.py docs/contributing.rst docs/datasets.rst docs/detection.rst docs/epsf.rst docs/geometry.rst docs/getting_started.rst docs/grouping.rst docs/index.rst docs/install.rst docs/isophote.rst docs/isophote_faq.rst docs/license.rst docs/make.bat docs/morphology.rst docs/overview.rst docs/pixel_conventions.rst docs/psf.rst docs/psf_matching.rst docs/rtd_requirements.txt docs/segmentation.rst docs/test_function.rst docs/utils.rst docs/_static/favicon.ico docs/_static/photutils.css docs/_static/photutils_banner-475x120.png docs/_static/photutils_banner.pdf docs/_static/photutils_banner.svg docs/_static/photutils_banner_original.svg docs/_static/photutils_logo-32x32.png docs/_static/photutils_logo.svg docs/dev/releasing.rst docs/psf_spec/background_estimator.rst docs/psf_spec/block_diagram.png docs/psf_spec/block_template.rst docs/psf_spec/culler_and_ender.rst docs/psf_spec/finder.rst docs/psf_spec/fitter.rst docs/psf_spec/group_maker.rst docs/psf_spec/index.rst docs/psf_spec/noise_data.rst docs/psf_spec/psf_model.rst docs/psf_spec/scene_maker.rst docs/psf_spec/single_object_model.rst docs/whats_new/1.1.rst docs/whats_new/1.2.rst docs/whats_new/index.rst photutils/CITATION.rst photutils/__init__.py photutils/_astropy_init.py photutils/_compiler.c photutils/conftest.py photutils/version.py photutils.egg-info/PKG-INFO photutils.egg-info/SOURCES.txt photutils.egg-info/dependency_links.txt photutils.egg-info/not-zip-safe photutils.egg-info/requires.txt photutils.egg-info/top_level.txt photutils/aperture/__init__.py photutils/aperture/_photometry_utils.py photutils/aperture/attributes.py photutils/aperture/bounding_box.py photutils/aperture/circle.py photutils/aperture/core.py photutils/aperture/ellipse.py photutils/aperture/mask.py photutils/aperture/photometry.py photutils/aperture/rectangle.py photutils/aperture/tests/__init__.py photutils/aperture/tests/test_aperture_common.py photutils/aperture/tests/test_bounding_box.py photutils/aperture/tests/test_circle.py photutils/aperture/tests/test_ellipse.py photutils/aperture/tests/test_mask.py photutils/aperture/tests/test_photometry.py photutils/aperture/tests/test_rectangle.py photutils/background/__init__.py photutils/background/_utils.py photutils/background/background_2d.py photutils/background/core.py photutils/background/interpolators.py photutils/background/tests/__init__.py photutils/background/tests/test_background_2d.py photutils/background/tests/test_core.py photutils/centroids/__init__.py photutils/centroids/core.py photutils/centroids/gaussian.py photutils/centroids/tests/__init__.py photutils/centroids/tests/test_core.py photutils/centroids/tests/test_gaussian.py photutils/datasets/__init__.py photutils/datasets/load.py photutils/datasets/make.py photutils/datasets/data/README.rst photutils/datasets/data/fermi_counts.fits.gz photutils/datasets/tests/__init__.py photutils/datasets/tests/test_load.py photutils/datasets/tests/test_make.py photutils/detection/__init__.py photutils/detection/core.py photutils/detection/daofinder.py photutils/detection/irafstarfinder.py photutils/detection/peakfinder.py photutils/detection/starfinder.py photutils/detection/tests/__init__.py photutils/detection/tests/test_daofinder.py photutils/detection/tests/test_irafstarfinder.py photutils/detection/tests/test_peakfinder.py photutils/detection/tests/test_starfinder.py photutils/detection/tests/data/daofind_test_thresh08.0_fwhm01.0.txt photutils/detection/tests/data/daofind_test_thresh08.0_fwhm01.5.txt photutils/detection/tests/data/daofind_test_thresh08.0_fwhm02.0.txt photutils/detection/tests/data/daofind_test_thresh10.0_fwhm01.0.txt photutils/detection/tests/data/daofind_test_thresh10.0_fwhm01.5.txt photutils/detection/tests/data/daofind_test_thresh10.0_fwhm02.0.txt photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm01.0.txt photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm01.5.txt photutils/detection/tests/data/irafstarfind_test_thresh08.0_fwhm02.0.txt photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm01.0.txt photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm01.5.txt photutils/detection/tests/data/irafstarfind_test_thresh10.0_fwhm02.0.txt photutils/extern/__init__.py photutils/geometry/__init__.py photutils/geometry/circular_overlap.pyx photutils/geometry/core.pxd photutils/geometry/core.pyx photutils/geometry/elliptical_overlap.pyx photutils/geometry/rectangular_overlap.pyx photutils/geometry/tests/__init__.py photutils/geometry/tests/test_circular_overlap_grid.py photutils/geometry/tests/test_elliptical_overlap_grid.py photutils/geometry/tests/test_rectangular_overlap_grid.py photutils/isophote/__init__.py photutils/isophote/ellipse.py photutils/isophote/fitter.py photutils/isophote/geometry.py photutils/isophote/harmonics.py photutils/isophote/integrator.py photutils/isophote/isophote.py photutils/isophote/model.py photutils/isophote/sample.py photutils/isophote/tests/__init__.py photutils/isophote/tests/make_test_data.py photutils/isophote/tests/test_angles.py photutils/isophote/tests/test_ellipse.py photutils/isophote/tests/test_fitter.py photutils/isophote/tests/test_geometry.py photutils/isophote/tests/test_harmonics.py photutils/isophote/tests/test_integrator.py photutils/isophote/tests/test_isophote.py photutils/isophote/tests/test_model.py photutils/isophote/tests/test_regression.py photutils/isophote/tests/test_sample.py photutils/isophote/tests/data/M51_table.fits photutils/isophote/tests/data/README.rst photutils/isophote/tests/data/minimum_radius_test.fits photutils/isophote/tests/data/synth_highsnr_table.fits photutils/isophote/tests/data/synth_lowsnr_table.fits photutils/isophote/tests/data/synth_table.fits photutils/isophote/tests/data/synth_table_mean.fits photutils/isophote/tests/data/synth_table_mean.txt photutils/morphology/__init__.py photutils/morphology/core.py photutils/morphology/non_parametric.py photutils/morphology/tests/__init__.py photutils/morphology/tests/test_core.py photutils/morphology/tests/test_non_parametric.py photutils/psf/__init__.py photutils/psf/epsf.py photutils/psf/epsf_stars.py photutils/psf/groupstars.py photutils/psf/models.py photutils/psf/photometry.py photutils/psf/sandbox.py photutils/psf/utils.py photutils/psf/matching/__init__.py photutils/psf/matching/fourier.py photutils/psf/matching/windows.py photutils/psf/matching/tests/__init__.py photutils/psf/matching/tests/test_fourier.py photutils/psf/matching/tests/test_windows.py photutils/psf/tests/__init__.py photutils/psf/tests/test_epsf.py photutils/psf/tests/test_epsf_stars.py photutils/psf/tests/test_groupstars.py photutils/psf/tests/test_models.py photutils/psf/tests/test_photometry.py photutils/psf/tests/test_sandbox.py photutils/psf/tests/test_utils.py photutils/segmentation/__init__.py photutils/segmentation/_utils.py photutils/segmentation/catalog.py photutils/segmentation/core.py photutils/segmentation/deblend.py photutils/segmentation/detect.py photutils/segmentation/properties.py photutils/segmentation/tests/__init__.py photutils/segmentation/tests/test_catalog.py photutils/segmentation/tests/test_core.py photutils/segmentation/tests/test_deblend.py photutils/segmentation/tests/test_detect.py photutils/segmentation/tests/test_properties.py photutils/segmentation/tests/test_utils.py photutils/tests/__init__.py photutils/utils/__init__.py photutils/utils/_convolution.py photutils/utils/_misc.py photutils/utils/_moments.py photutils/utils/_optional_deps.py photutils/utils/_round.py photutils/utils/_wcs_helpers.py photutils/utils/colormaps.py photutils/utils/errors.py photutils/utils/exceptions.py photutils/utils/interpolation.py photutils/utils/tests/__init__.py photutils/utils/tests/test_colormaps.py photutils/utils/tests/test_convolution.py photutils/utils/tests/test_errors.py photutils/utils/tests/test_interpolation.py photutils/utils/tests/test_moments.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/dependency_links.txt0000644000214200020070000000000100000000000021771 0ustar00lbradley ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/not-zip-safe0000644000214200020070000000000100000000000020151 0ustar00lbradley ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/requires.txt0000644000214200020070000000036600000000000020330 0ustar00lbradleynumpy>=1.17 astropy>=4.0 [all] scipy>=1.6.0 matplotlib>=2.2 scikit-image>=0.14.2 scikit-learn gwcs>=0.12 bottleneck [docs] scipy>=1.6.0 sphinx<4 sphinx-astropy matplotlib>=2.2 scikit-image>=0.14.2 scikit-learn gwcs>=0.12 [test] pytest-astropy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1640123871.0 photutils-1.3.0/photutils.egg-info/top_level.txt0000644000214200020070000000001200000000000020446 0ustar00lbradleyphotutils ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635887455.0 photutils-1.3.0/pyproject.toml0000644000214200020070000000047500000000000015120 0ustar00lbradley[build-system] requires = ['setuptools', 'setuptools_scm', 'wheel', 'cython>=0.29.22', 'oldest-supported-numpy', 'extension-helpers'] build-backend = 'setuptools.build_meta' [tool.astropy-bot] [tool.astropy-bot.changelog_checker] filename = "CHANGES.rst" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1640123872.0275912 photutils-1.3.0/setup.cfg0000644000214200020070000000433700000000000014026 0ustar00lbradley[metadata] name = photutils author = Photutils Developers author_email = photutils.team@gmail.com license = BSD 3-Clause license_file = LICENSE.rst url = https://github.com/astropy/photutils github_project = astropy/photutils edit_on_github = False description = An Astropy package for source detection and photometry long_description = file: README.rst long_description_content_type = text/x-rst keywords = astronomy, astrophysics, photometry, aperture, psf, source detection, background, segmentation, centroids, isophote, morphology classifiers = Intended Audience :: Science/Research License :: OSI Approved :: BSD License Natural Language :: English Operating System :: OS Independent Programming Language :: Cython Programming Language :: Python Programming Language :: Python :: 3 Topic :: Scientific/Engineering :: Astronomy [options] zip_safe = False packages = find: python_requires = >=3.7 setup_requires = setuptools_scm install_requires = numpy>=1.17 astropy>=4.0 [options.extras_require] all = scipy>=1.6.0 matplotlib>=2.2 scikit-image>=0.14.2 scikit-learn gwcs>=0.12 bottleneck test = pytest-astropy docs = scipy>=1.6.0 sphinx<4 sphinx-astropy matplotlib>=2.2 scikit-image>=0.14.2 scikit-learn gwcs>=0.12 [options.package_data] photutils = CITATION.rst photutils.datasets = data/* photutils.detection.tests = data/* photutils.isophote.tests = data/* [tool:pytest] testpaths = "photutils" "docs" norecursedirs = "docs[\/]_build" "docs[\/]generated" "photutils[\/]extern" astropy_header = true doctest_plus = enabled text_file_format = rst addopts = --doctest-rst filterwarnings = ignore:numpy.ufunc size changed:RuntimeWarning [coverage:run] omit = photutils/_astropy_init* photutils/conftest.py photutils/*setup_package* photutils/tests/* photutils/*/tests/* photutils/extern/* photutils/version* */photutils/_astropy_init* */photutils/conftest.py */photutils/*setup_package* */photutils/tests/* */photutils/*/tests/* */photutils/extern/* */photutils/version* [coverage:report] exclude_lines = pragma: no cover except ImportError raise AssertionError raise NotImplementedError def main\(.*\): pragma: py{ignore_python_version} def _ipython_key_completions_ [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629473289.0 photutils-1.3.0/setup.py0000755000214200020070000000417600000000000013723 0ustar00lbradley#!/usr/bin/env python # Licensed under a 3-clause BSD style license - see LICENSE.rst # NOTE: The configuration for the package, including the name, version, and # other information are set in the setup.cfg file. import sys # First provide helpful messages if contributors try and run legacy commands # for tests or docs. TEST_HELP = """ Note: running tests is no longer done using 'python setup.py test'. Instead you will need to run: tox -e test If you don't already have tox installed, you can install it with: pip install tox If you only want to run part of the test suite, you can also use pytest directly with:: pip install -e .[test] pytest For more information, see: http://docs.astropy.org/en/latest/development/testguide.html#running-tests """ if 'test' in sys.argv: print(TEST_HELP) sys.exit(1) DOCS_HELP = """ Note: building the documentation is no longer done using 'python setup.py build_docs'. Instead you will need to run: tox -e build_docs If you don't already have tox installed, you can install it with: pip install tox You can also build the documentation with Sphinx directly using:: pip install -e .[docs] cd docs make html For more information, see: http://docs.astropy.org/en/latest/install.html#builddocs """ if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: print(DOCS_HELP) sys.exit(1) VERSION_TEMPLATE = """ # Note that we need to fall back to the hard-coded version if either # setuptools_scm can't be imported or setuptools_scm can't determine the # version, so we catch the generic 'Exception'. try: from setuptools_scm import get_version version = get_version(root='..', relative_to=__file__) except Exception: version = '{version}' """.lstrip() # Import these after the above checks to ensure they are printed even if # extensions_helpers is not installed import os # noqa from setuptools import setup # noqa from extension_helpers import get_extensions # noqa setup(use_scm_version={'write_to': os.path.join('photutils', 'version.py'), 'write_to_template': VERSION_TEMPLATE}, ext_modules=get_extensions()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638927624.0 photutils-1.3.0/tox.ini0000644000214200020070000000626300000000000013520 0ustar00lbradley[tox] envlist = py{37,38,39,310}-test{,-alldeps,-devdeps}{,-cov} py{37,38,39,310}-test-numpy{117,118,119,120,121} py{37,38,39,310}-test-astropy{40,50,lts} build_docs linkcheck codestyle bandit requires = setuptools >= 30.3.0 pip >= 19.3.1 isolated_build = true indexserver = NIGHTLY = https://pypi.anaconda.org/scipy-wheels-nightly/simple [testenv] # Pass through the following environment variables which may be needed # for the CI passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI # Run the tests in a temporary directory to make sure that we don't # import this package from the source tree changedir = .tmp/{envname} # tox environments are constructed with so-called 'factors' (or terms) # separated by hyphens, e.g., test-devdeps-cov. Lines below starting # with factor: will only take effect if that factor is included in the # environment name. To see a list of example environments that can be run, # along with a description, run: # # tox -l -v # description = run tests alldeps: with all optional dependencies devdeps: with the latest developer version of key dependencies oldestdeps: with the oldest supported version of key dependencies cov: and test coverage numpy117: with numpy 1.17.* numpy118: with numpy 1.18.* numpy119: with numpy 1.19.* numpy120: with numpy 1.20.* numpy121: with numpy 1.21.* astropy40: with astropy 4.0.* astropy50: with astropy 5.0.* astropylts: with the latest astropy LTS # The following provides some specific pinnings for key packages deps = cov: coverage numpy117: numpy==1.17.* numpy118: numpy==1.18.* numpy119: numpy==1.19.* numpy120: numpy==1.20.* numpy121: numpy==1.21.* astropy40: astropy==4.0.* astropy50: astropy==5.0.* astropylts: astropy==4.0.* devdeps: :NIGHTLY:numpy devdeps: git+https://github.com/astropy/astropy.git#egg=astropy oldestdeps: numpy==1.17 oldestdeps: astropy==4.0 oldestdeps: scipy==0.19 oldestdeps: matplotlib==2.2 oldestdeps: scikit-image==0.14.2 oldestdeps: scikit-learn==0.19 oldestdeps: gwcs==0.12 oldestdeps: pytest-astropy==0.4 # The following indicates which extras_require from setup.cfg will be # installed extras = test: test alldeps: all build_docs: docs commands = pip freeze !cov: pytest --pyargs photutils {toxinidir}/docs {posargs} cov: pytest --pyargs photutils {toxinidir}/docs --cov photutils --cov-config={toxinidir}/setup.cfg {posargs} cov: coverage xml -o {toxinidir}/coverage.xml [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs extras = docs commands = pip freeze sphinx-build -W -b html . _build/html [testenv:linkcheck] changedir = docs description = check the links in the HTML docs extras = docs commands = pip freeze sphinx-build -W -b linkcheck . _build/html [testenv:codestyle] skip_install = true changedir = . description = check code style with flake8 deps = flake8 commands = flake8 photutils --count --max-line-length=100 [testenv:bandit] skip_install = true changedir = . description = security check with bandit deps = bandit commands = bandit -r photutils -c .bandit.yaml