diff --git a/.travis.yml b/.travis.yml
index afe2328a..0a3caaaf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,11 @@
language: c
+# Cache can be cleared from the travis settings menu, see docs currently at
+# https://docs.travis-ci.com/user/caching#Clearing-Caches
+cache:
+ - ccache
+
+
os:
- linux
@@ -16,7 +22,6 @@ addons:
- texlive-latex-extra
- dvipng
-
env:
global:
# SET DEFAULTS TO AVOID REPEATING IN MOST CASES
@@ -31,6 +36,23 @@ env:
- SETUP_XVFB=True
- EVENT_TYPE='push pull_request'
+ # PEP8 errors/warnings:
+ # E101 - mix of tabs and spaces
+ # W191 - use of tabs
+ # W291 - trailing whitespace
+ # W292 - no newline at end of file
+ # W293 - trailing whitespace
+ # W391 - blank line at end of file
+ # E111 - 4 spaces per indentation level
+ # E112 - 4 spaces per indentation level
+ # E113 - 4 spaces per indentation level
+ # E502 - the backslash is redundant between brackets
+ # E722 - do not use bare except
+ # E901 - SyntaxError or IndentationError
+ # E902 - IOError
+ - FLAKE8_OPT="--select=E101,W191,W291,W292,W293,W391,E111,E112,E113,E502,E722,E901,E902"
+
+
matrix:
# make sure that egg_info works without dependencies
- PYTHON_VERSION=2.7 SETUP_CMD='egg_info'
@@ -59,7 +81,6 @@ matrix:
SPHINX_VERSION=1.5.6
- python: 3.5
env: SETUP_CMD='build_sphinx -w' CONDA_DEPENDENCIES='Cython ipython scipy matplotlib ginga'
- SPHINX_VERSION=1.5.6
# Try Astropy development and LTS versions
- python: 2.7
@@ -68,12 +89,16 @@ matrix:
env: ASTROPY_VERSION=lts
- python: 3.5
env: ASTROPY_VERSION=development
+ - python: 3.6
+ env: ASTROPY_VERSION=development
# Try with optional dependencies disabled
- python: 2.7
env: PIP_DEPENDENCIES=''
- python: 3.5
env: PIP_DEPENDENCIES=''
+ - python: 3.6
+ env: PIP_DEPENDENCIES=''
# Try older numpy versions
@@ -81,20 +106,24 @@ matrix:
env: NUMPY_VERSION=1.11
- python: 3.5
env: NUMPY_VERSION=1.12
+ - python: 3.5
+ env: NUMPY_VERSION=1.13
# Try numpy pre-release
- python: 3.5
env: NUMPY_VERSION=prerelease
+ - python: 3.6
+ env: NUMPY_VERSION=prerelease
- # Do a coverage test in Python 2.
- - python: 2.7
+ # Do a coverage tests
+ - python: 3.5
env: SETUP_CMD='test --coverage'
# Do a pep8 test
- python: 3.5
- env: SETUP_CMD='test --pep8 -k pep8
+ env: MAIN_CMD="flake8 imexam --count $FLAKE8_OPT" SETUP_CMD=''
allow_failures:
# The build with numpy pre-release halts in the middle without
@@ -102,17 +131,13 @@ matrix:
- python: 3.5
env: NUMPY_VERSION=prerelease
- python: 3.6
- env: SETUP_CMD='build_sphinx -w' CONDA_DEPENDENCIES='Cython ipython scipy matplotlib ginga'
+ env: NUMPY_VERSION=prerelease
before_install:
-
- # CONFIGURE A HEADLESS DISPLAY TO TEST PLOT GENERATION
- - export DISPLAY=:99.0
- - sh -e /etc/init.d/xvfb start
- - sleep 3
- uname -a
- python --version
+ - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ( sudo Xvfb :99 -ac -screen 0 1024x768x8; sleep 3; echo ok )& fi
# We now use the ci-helpers package to set up our testing environment.
# This is done by using Miniconda and then using conda and pip to install
@@ -142,7 +167,8 @@ install:
script:
- - $MAIN_CMD $SETUP_CMD
+ - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ( xvfb-run -a $MAIN_CMD $SETUP_CMD ) fi
+ - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ( $MAIN_CMD $SETUP_CMD ) fi
after_success:
diff --git a/CHANGES.rst b/CHANGES.rst
index a786c4cb..0d2d7f3d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,26 @@
-version 0.8.0 (released)
------------------------------
+version 0.8.1 (2018-12-14)
+--------------------------
+** THIS WILL BE THE LAST VERSION THAT SUPPORT Python 2.7 **
+
+- travis and appveyor testing updates
+- radial profile plot centering fixed to more correctly calculate the fractional center offsets
+- cumulative radial profile flux calculation should now be correct
+- the fit_gauss_1d function call was changed to accept the radius and flux array so that they
+ could be constructed correctly for multiple circumstances
+- documentation updated and new simple walkthrough added
+- MEF fits images with IMAGE arrays in the primary HDU should be detected correctly now
+- now possible to give load_fits an in-memory fits object
+- code cleanup and minor bug fixes
+- background fit added to 1D and 2D Gaussian fits
+- plotting AiryDisk2D fit is now possible
+- unit test updates
+- new options added to the aperture phot parameter set that allow users to plot the used apertures
+- ZScaleInterval added from astropy.virtualization to set the color range on the data for aperture photometry plot
+- replaced the sigma to fwhm lambda with the astropy constant for conversion
+- added cursor move recognition using the arrow keys during the imexam loop, however, depending how the user has their windowing focus set, the DS9 window may loose focus, forcing them to move the cursor manually back to the window. Cursor moves are only implemented for DS9, not Ginga.
+
+version 0.8.0 (2017-11-06)
+--------------------------
- fixed show_xpa_commands bug sending None instead of empty string
to the xpa library
- fixed logic of connect method. When a target is given, do not look
@@ -8,14 +29,14 @@ version 0.8.0 (released)
- logic bug in ds9 class init updated to warn when user specified target doesn't exist
-version 0.7.1 (released)
------------------------------
+version 0.7.1 (2017-02-06)
+--------------------------
- fixed xpa bug holdout from updating for windows specific code
- changed default connection type from local to inet when XPA_METHOD not specified in users environment
-version 0.7.0 (released)
------------------------------
+version 0.7.0 (2017-01-19)
+--------------------------
- fixed a text error in the display_help() so that now the correct version loads the documentation
- Windows users can now install from source. The setup will ignore the cython and xpa necessary to build the DS9 interaction, and users will only be able to use the Ginga HTML5 window, they can also use the Imexamine() functions without any graphical interface.
- Documentation updates, mostly specific information for Windows users
@@ -37,8 +58,8 @@ version 0.6.4dev (unreleased)
- fixed bug in fits loader for ds9 multi-extension FITS files, made load_fits() prefer the extension specified in the key rather than the image name
-version 0.6.3 (released)
-------------------------
+version 0.6.3 (2017-01-01)
+--------------------------
- Logging was updated to fix bugs as well allow for more user control of the log files. Additionally, most prints were moved to the stdout stream handler so that users could also shut off messages to the screen
- The imexamine class was updated so that analysis functions could be more easily called by external entities. This was primarily to support ginga plugins, and a new imexam plugin for ginga.
- A dictionary is now returned to the user when they request information on the active DS9 windows which are available.
@@ -47,14 +68,14 @@ version 0.6.3 (released)
- Fixed bug with loading user specified fits extensions for both ginga and ds9
-version 0.6.2 (released)
-------------------------
+version 0.6.2 (2016-08-10)
+--------------------------
- Unbinned radial plots were added, bins are still an available option
- documentation updates
-version 0.6dev (unreleased)
----------------------------
+version 0.6.1 (2016-07-16)
+--------------------------
- Ginga viewer support for images in matplotlib and QT backend removed, but replaced with HTML5 canvas viewer which is faster and simpler for users to both use and install.
- replaced custom fits with astropy.modeling, enabling Gaussian2d, Gaussian1d, Moffat1D and MexicanHat1D fits for lines and centering
- General bug fixes and documentation updates, including example jupyter notebooks
@@ -74,18 +95,18 @@ version 0.5.3dev (unreleased)
- added a radial profile plot under the r key, the curve of growth plot was moved to g
-version 0.5.2 (released)
-------------------------
+version 0.5.2 (2016-01-29)
+--------------------------
- windows build change
-version 0.5.1 (released)
------------------------
+version 0.5.1 (2016-01-29)
+--------------------------
- version upgraded needed for the release on pypi so it would accept the upload
-version 0.5 (released)
-----------------------
+version 0.5 (2015-05-01)
+------------------------
- Ginga viewer with matplotlib backend fully flushed out,
this uses an event driven examination which is activated by key-press
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..ebfc6acc
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,24 @@
+# Spacetelescope Open Source Code of Conduct
+
+We expect all "spacetelescope" organization projects to adopt a code of conduct that ensures a productive, respectful environment for all open source contributors and participants. We are committed to providing a strong and enforced code of conduct and expect everyone in our community to follow these guidelines when interacting with others in all forums. Our goal is to keep ours a positive, inclusive, successful, and growing community. The community of participants in open source Astronomy projects is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences success and continued growth.
+
+
+As members of the community,
+
+- We pledge to treat all people with respect and provide a harassment- and bullying-free environment, regardless of sex, sexual orientation and/or gender identity, disability, physical appearance, body size, race, nationality, ethnicity, and religion. In particular, sexual language and imagery, sexist, racist, or otherwise exclusionary jokes are not appropriate.
+
+- We pledge to respect the work of others by recognizing acknowledgment/citation requests of original authors. As authors, we pledge to be explicit about how we want our own work to be cited or acknowledged.
+
+- We pledge to welcome those interested in joining the community, and realize that including people with a variety of opinions and backgrounds will only serve to enrich our community. In particular, discussions relating to pros/cons of various technologies, programming languages, and so on are welcome, but these should be done with respect, taking proactive measure to ensure that all participants are heard and feel confident that they can freely express their opinions.
+
+- We pledge to welcome questions and answer them respectfully, paying particular attention to those new to the community. We pledge to provide respectful criticisms and feedback in forums, especially in discussion threads resulting from code contributions.
+
+- We pledge to be conscientious of the perceptions of the wider community and to respond to criticism respectfully. We will strive to model behaviors that encourage productive debate and disagreement, both within our community and where we are criticized. We will treat those outside our community with the same respect as people within our community.
+
+- We pledge to help the entire community follow the code of conduct, and to not remain silent when we see violations of the code of conduct. We will take action when members of our community violate this code such as such as contacting conduct@stsci.edu (all emails sent to this address will be treated with the strictest confidence) or talking privately with the person.
+
+This code of conduct applies to all community situations online and offline, including mailing lists, forums, social media, conferences, meetings, associated social events, and one-to-one interactions.
+
+Parts of this code of conduct have been adapted from the Astropy and Numfocus codes of conduct.
+http://www.astropy.org/code_of_conduct.html
+https://www.numfocus.org/about/code-of-conduct/
diff --git a/README.rst b/README.rst
index c5b5f450..d85d000e 100644
--- a/README.rst
+++ b/README.rst
@@ -21,6 +21,9 @@ imexam
:target: https://ci.appveyor.com/project/spacetelescope/imexam/branch/master
:alt: Appveyor
+.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.1042809.svg
+ :target: https://doi.org/10.5281/zenodo.1042809
+
imexam is an affiliated package of `AstroPy`_. It was designed to be a lightweight library which enables users to explore data from a command line interface, through a Jupyter notebook or through a Jupyter console. It can be used with multiple viewers, such as DS9 or Ginga, or without a viewer as a simple library to make plots and grab quick photometry information. It has been designed so that other viewers may be easily attached in the future.
For more information please see the `online documentation `_
@@ -38,7 +41,7 @@ after you have cloned the repository and before you "python setup.py install"
git submodule update --init -- cextern/xpa
-If you are cloneing the repository for the first time, you can do both steps at once using a recursive clone:
+If you are cloning the repository for the first time, you can do both steps at once using a recursive clone:
::
@@ -75,25 +78,59 @@ Try turning off the resume state:
defaults write org.python.python ApplePersistenceIgnoreState NO
-If you are having display issues, some build problems may exist with the dependency packages which deal with backend graphics, try setting your matplotlib backend to Qt4Agg. You can set this in your .matplotlib/matplotlibrc file. You may also want to switch your matplotlib backend to Qt if you have a mac with the default MacOS backend specified. If you don't already have matplotlibrc file in your home directory, you can download one from their documentation: http://matplotlib.org/_static/matplotlibrc
+
+Contributing
+============
+Please open a new issue or new pull request for bugs, feedback, or new features
+you would like to see. If there is an issue you would like to work on, please
+leave a comment and we will be happy to assist. New contributions and
+contributors are very welcome!
+
+New to github or open source projects? If you are unsure about where to start
+or haven't used github before, please feel free to contact `@sosey`.
+Want more information about how to make a contribution? Take a look at
+the astropy `contributing`_ and `developer`_ documentation.
+
+Feedback and feature requests? Is there something missing you would like
+to see? Please open an issue or send an email to `@sosey`. imexam follows the `Astropy Code of Conduct`_ and strives to provide a
+welcoming community to all of our users and contributors.
+
+
+License
+=======
+imexam is licensed under a 3-clause BSD style license (see the
+``licenses/LICENSE.rst`` file).
+
+.. _AstroPy: http://www.astropy.org/
+.. _contributing: http://docs.astropy.org/en/stable/index.html#contributing
+.. _developer: http://docs.astropy.org/en/stable/index.html#developer-documentation
+.. _Astropy Code of Conduct: http://www.astropy.org/about.html#codeofconduct
+
+
+
+Quick Instructions
+==================
+If you are having display issues, and you are using TkAgg, try setting your matplotlib backend to Qt4Agg or Qt5Agg. You can set this in your .matplotlib/matplotlibrc file. You may also want to switch your matplotlib backend to Qt if you have a mac with the default MacOS backend specified. If you don't already have matplotlibrc file in your home directory, you can download one from their documentation: http://matplotlib.org/_static/matplotlibrc
+
::
-inside ~/.matplotlib/matplotlibrc:
+ inside ~/.matplotlib/matplotlibrc:
backend: Qt4Agg
-
Using the Ginga HTML5 Viewer
----------------------------
-If you have installed Ginga, you can use the HTML5 viewer for image display with either a python terminal, jupyter console, qtconsole or Jypyter notebook session. If you are using a Windows machine you should install ginga to use as the viewer with this package. Make sure that you have installed the latest version, or you can download the development code here: https://github.com/ejeschke/ginga. There's also a new ginga plugin for imexam which is in the ginga repository in the experimental directory. This will load the imexam plotting and analysis library into the ginga gui framework.
+If you have installed Ginga, you can use the HTML5 viewer for image display with either a jupyter console, qtconsole or Jupyter notebook session. If you are using a Windows machine you should install ginga to use as the viewer with this package. Make sure that you have installed the latest version, or you can download the development code here: https://github.com/ejeschke/ginga.
+
+There is also a ginga plugin for imexam which is in the ginga repository in the experimental directory. This will load the imexam plotting and analysis library into the ginga gui framework.
Starting a connection to a Ginga HTML5 canvas backend for browser and Jupyter viewing:
::
- a=imexam.connect(viewer='ginga')
+ a = imexam.connect(viewer='ginga')
You can optionally provide a port number to which the viewer is connected as well:
@@ -102,10 +139,39 @@ You can optionally provide a port number to which the viewer is connected as wel
a=imexam.connect(viewer='ginga', port=9856)
+Using imexam with DS9
+---------------------
+From a python terminal: using either the TkAGG or QT4Agg/QT5Agg backends:
+
+
+::
+
+ import imexam
+ a = imexam.connect()
+ a.imexam()
+
+From an ipython terminal: using either the TkAgg or QT4Agg/QT5Agg backends.
+
+::
+
+ import imexam
+ a = imexam.connect()
+
+If you are using TkAGG as the backend, from an ipython terminal, you may need to ctrl-D, then select n, to closeout the plotting window. This should not happen if you are running TkAgg and running from a regular python terminal. Looking into the closeout issue with TkAgg now.
+
+From jupyter console/qtconsole: startup with the matplotlib magics to use the backend you specified for display:
+
+::
+
+ In [1]: %matplotlib
+ import imexam
+ a = imexam.connect()
+
+If you are using the Qt4Agg/Qt5Agg backend with ginga, the plots will display in the console window
+
Launching multiple DS9 windows
------------------------------
-
You can launch multiple ds9 windows either from this package or the command line. DS9 can be used to view images and arrays from any of the python terminals, consoles or the Jupyter notebook.
If you launch ds9 from outside the imexam package, you need supply the name of the window to imexam, this can be done in one of 2 ways:
@@ -153,7 +219,10 @@ Connecting to a DS9 window which was started from the system prompt:
Examples can be found in the package documentation, online documentation, and imexam.display_help() will pull up the installed package documentation in a web browser. You can also download the examply Jupyter notebooks available in the example_notebooks directory above.
-You can also just load the plotting library and NOT connect to any viewer:
+You can also just load the plotting library for use without a viewer:
+---------------------------------------------------------------------
+This is useful when you want to make batch plots or return information from scripts.
+You can also save the lotting data returned and use it futher, or design your own plot.
::
@@ -169,32 +238,3 @@ You can also just load the plotting library and NOT connect to any viewer:
plots.set_data(data)
plots.plot_line(35,45)
-
-Contributing
-------------
-
-Please open a new issue or new pull request for bugs, feedback, or new features
-you would like to see. If there is an issue you would like to work on, please
-leave a comment and we will be happy to assist. New contributions and
-contributors are very welcome!
-
-New to github or open source projects? If you are unsure about where to start
-or haven't used github before, please feel free to contact `@sosey`.
-Want more information about how to make a contribution? Take a look at
-the astropy `contributing`_ and `developer`_ documentation.
-
-Feedback and feature requests? Is there something missing you would like
-to see? Please open an issue or send an email to `@sosey`. imexam follows the `Astropy Code of Conduct`_ and strives to provide a
-welcoming community to all of our users and contributors.
-
-
-License
--------
-
-imexam is licensed under a 3-clause BSD style license (see the
-``licenses/LICENSE.rst`` file).
-
-.. _AstroPy: http://www.astropy.org/
-.. _contributing: http://docs.astropy.org/en/stable/index.html#contributing
-.. _developer: http://docs.astropy.org/en/stable/index.html#developer-documentation
-.. _Astropy Code of Conduct: http://www.astropy.org/about.html#codeofconduct
diff --git a/ah_bootstrap.py b/ah_bootstrap.py
index 786b8b14..2cea5bd7 100644
--- a/ah_bootstrap.py
+++ b/ah_bootstrap.py
@@ -19,9 +19,14 @@
contains an option called ``auto_use`` with a value of ``True``, it will
automatically call the main function of this module called
`use_astropy_helpers` (see that function's docstring for full details).
-Otherwise no further action is taken (however,
-``ah_bootstrap.use_astropy_helpers`` may be called manually from within the
-setup.py script).
+Otherwise no further action is taken and by default the system-installed version
+of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers``
+may be called manually from within the setup.py script).
+
+This behavior can also be controlled using the ``--auto-use`` and
+``--no-auto-use`` command-line flags. For clarity, an alias for
+``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using
+the latter if needed.
Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same
names as the arguments to `use_astropy_helpers`, and can be used to configure
@@ -33,7 +38,6 @@
import contextlib
import errno
-import imp
import io
import locale
import os
@@ -41,6 +45,13 @@
import subprocess as sp
import sys
+__minimum_python_version__ = (2, 7)
+
+if sys.version_info < __minimum_python_version__:
+ print("ERROR: Python {} or later is required by astropy-helpers".format(
+ __minimum_python_version__))
+ sys.exit(1)
+
try:
from ConfigParser import ConfigParser, RawConfigParser
except ImportError:
@@ -61,35 +72,15 @@
# issues with either missing or misbehaving pacakges (including making sure
# setuptools itself is installed):
+# Check that setuptools 1.0 or later is present
+from distutils.version import LooseVersion
-# Some pre-setuptools checks to ensure that either distribute or setuptools >=
-# 0.7 is used (over pre-distribute setuptools) if it is available on the path;
-# otherwise the latest setuptools will be downloaded and bootstrapped with
-# ``ez_setup.py``. This used to be included in a separate file called
-# setuptools_bootstrap.py; but it was combined into ah_bootstrap.py
try:
- import pkg_resources
- _setuptools_req = pkg_resources.Requirement.parse('setuptools>=0.7')
- # This may raise a DistributionNotFound in which case no version of
- # setuptools or distribute is properly installed
- _setuptools = pkg_resources.get_distribution('setuptools')
- if _setuptools not in _setuptools_req:
- # Older version of setuptools; check if we have distribute; again if
- # this results in DistributionNotFound we want to give up
- _distribute = pkg_resources.get_distribution('distribute')
- if _setuptools != _distribute:
- # It's possible on some pathological systems to have an old version
- # of setuptools and distribute on sys.path simultaneously; make
- # sure distribute is the one that's used
- sys.path.insert(1, _distribute.location)
- _distribute.activate()
- imp.reload(pkg_resources)
-except:
- # There are several types of exceptions that can occur here; if all else
- # fails bootstrap and use the bootstrapped version
- from ez_setup import use_setuptools
- use_setuptools()
-
+ import setuptools
+ assert LooseVersion(setuptools.__version__) >= LooseVersion('1.0')
+except (ImportError, AssertionError):
+ print("ERROR: setuptools 1.0 or later is required by astropy-helpers")
+ sys.exit(1)
# typing as a dependency for 1.6.1+ Sphinx causes issues when imported after
# initializing submodule with ah_boostrap.py
@@ -137,7 +128,6 @@
from setuptools import Distribution
from setuptools.package_index import PackageIndex
-from setuptools.sandbox import run_setup
from distutils import log
from distutils.debug import DEBUG
@@ -147,6 +137,11 @@
DIST_NAME = 'astropy-helpers'
PACKAGE_NAME = 'astropy_helpers'
+if PY3:
+ UPPER_VERSION_EXCLUSIVE = None
+else:
+ UPPER_VERSION_EXCLUSIVE = '3'
+
# Defaults for other options
DOWNLOAD_IF_NEEDED = True
INDEX_URL = 'https://pypi.python.org/simple'
@@ -287,6 +282,18 @@ def parse_command_line(cls, argv=None):
config['offline'] = True
argv.remove('--offline')
+ if '--auto-use' in argv:
+ config['auto_use'] = True
+ argv.remove('--auto-use')
+
+ if '--no-auto-use' in argv:
+ config['auto_use'] = False
+ argv.remove('--no-auto-use')
+
+ if '--use-system-astropy-helpers' in argv:
+ config['auto_use'] = False
+ argv.remove('--use-system-astropy-helpers')
+
return config
def run(self):
@@ -464,9 +471,10 @@ def _directory_import(self):
# setup.py exists we can generate it
setup_py = os.path.join(path, 'setup.py')
if os.path.isfile(setup_py):
- with _silence():
- run_setup(os.path.join(path, 'setup.py'),
- ['egg_info'])
+ # We use subprocess instead of run_setup from setuptools to
+ # avoid segmentation faults - see the following for more details:
+ # https://github.com/cython/cython/issues/2104
+ sp.check_output([sys.executable, 'setup.py', 'egg_info'], cwd=path)
for dist in pkg_resources.find_distributions(path, True):
# There should be only one...
@@ -501,16 +509,32 @@ def get_option_dict(self, command_name):
if version:
req = '{0}=={1}'.format(DIST_NAME, version)
else:
- req = DIST_NAME
+ if UPPER_VERSION_EXCLUSIVE is None:
+ req = DIST_NAME
+ else:
+ req = '{0}<{1}'.format(DIST_NAME, UPPER_VERSION_EXCLUSIVE)
attrs = {'setup_requires': [req]}
+ # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure
+ # it honours the options set in the [easy_install] section, and we need
+ # to explicitly fetch the requirement eggs as setup_requires does not
+ # get honored in recent versions of setuptools:
+ # https://github.com/pypa/setuptools/issues/1273
+
try:
- if DEBUG:
- _Distribution(attrs=attrs)
- else:
- with _silence():
- _Distribution(attrs=attrs)
+
+ context = _verbose if DEBUG else _silence
+ with context():
+ dist = _Distribution(attrs=attrs)
+ try:
+ dist.parse_config_files(ignore_option_errors=True)
+ dist.fetch_build_eggs(req)
+ except TypeError:
+ # On older versions of setuptools, ignore_option_errors
+ # doesn't exist, and the above two lines are not needed
+ # so we can just continue
+ pass
# If the setup_requires succeeded it will have added the new dist to
# the main working_set
@@ -846,6 +870,10 @@ def flush(self):
pass
+@contextlib.contextmanager
+def _verbose():
+ yield
+
@contextlib.contextmanager
def _silence():
"""A context manager that silences sys.stdout and sys.stderr."""
diff --git a/appveyor.yml b/appveyor.yml
index c3476bfb..4408f6f4 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -32,6 +32,7 @@ platform:
# os: Visual Studio 2015 Update 2
install:
+ - "git submodule update --init --recursive"
- "git clone git://github.com/astropy/ci-helpers.git"
- "powershell ci-helpers/appveyor/install-miniconda.ps1"
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
diff --git a/astropy_helpers b/astropy_helpers
index d23a53f4..231c409a 160000
--- a/astropy_helpers
+++ b/astropy_helpers
@@ -1 +1 @@
-Subproject commit d23a53f46dd1c3703e5eee63dca3f53bd18a4e8b
+Subproject commit 231c409a632dcbf2beae1c2dea5b843d81ede511
diff --git a/docs/_static/ap_phot_plot.png b/docs/_static/ap_phot_plot.png
new file mode 100644
index 00000000..eedf39de
Binary files /dev/null and b/docs/_static/ap_phot_plot.png differ
diff --git a/docs/_static/walkthrough-array.png b/docs/_static/walkthrough-array.png
new file mode 100644
index 00000000..485d7ce8
Binary files /dev/null and b/docs/_static/walkthrough-array.png differ
diff --git a/docs/_static/walkthrough-ginga.png b/docs/_static/walkthrough-ginga.png
new file mode 100644
index 00000000..6ed44188
Binary files /dev/null and b/docs/_static/walkthrough-ginga.png differ
diff --git a/docs/_static/walkthrough-imexam.png b/docs/_static/walkthrough-imexam.png
new file mode 100644
index 00000000..ed4b235e
Binary files /dev/null and b/docs/_static/walkthrough-imexam.png differ
diff --git a/docs/conf.py b/docs/conf.py
index c3f84084..872c6624 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,9 +18,7 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-
-sys.path.insert(0, os.path.abspath('../'))
-print(sys.path)
+# sys.path.insert(0, os.path.abspath('../'))
# IMPORTANT: the above commented section was generated by sphinx-quickstart,but
# is *NOT* appropriate for astropy or Astropy affiliated packages. It is left
diff --git a/docs/imexam/current_capability.rst b/docs/imexam/current_capability.rst
index b3f4f793..fcb96fb6 100644
--- a/docs/imexam/current_capability.rst
+++ b/docs/imexam/current_capability.rst
@@ -85,7 +85,7 @@ but not yet fully implemented should return an error to that affect.
::
- In [1]: viewer.get_data_filename()
+ In [1]: viewer.get_filename()
Out[2]: '/Users/sosey/ssb/imexam/iabf01bzq_flt.fits'
@@ -151,7 +151,8 @@ but not yet fully implemented should return an error to that affect.
**load_fits**\ (fname="", extver=1, extname=None):
- Load a fits image into the current frame
+ Load a fits image into the current frame.
+ fname can be a filename or a fits HDU
**load_mef_as_cube**\ (filename=None):
diff --git a/docs/imexam/description.rst b/docs/imexam/description.rst
index a7e40754..82089584 100644
--- a/docs/imexam/description.rst
+++ b/docs/imexam/description.rst
@@ -143,6 +143,7 @@ If you wish to open multiple DS9 windows outside of ``imexam``, then it's recomm
imexam.connect(target="",path=None,viewer="ds9",wait_time=10)
+
Where target is the name of the ds9 window that is already running, path is the location of the ds9 executable, viewer is the name of the viewer to use (ds9 is the only one which is currently activated), and wait_time is the time to wait to establish a connection to the socket before exiting the process.
If it seems like the ds9 window is opening or hanging, there could be few things going on:
@@ -267,7 +268,7 @@ If you are having display issues, some build problems may exist with the depende
backend: Qt4Agg
-The package works with the Qt5Agg and notebook backends, but on occasion I've seen the matplotlib window take two cycles to update, especially inside the Jupyter notebook with inline plots, meaning you may have to hit the exam key twice for the plot to appear. This issue still needs to be worked out, if you're running into it try using the Qt4Agg backend or plotting outside the notebook and saving the figures through the imexam grab or save calls.
+The package works with the Qt5Agg and notebook backends, but on occasion I've seen the matplotlib window take two cycles to update, especially inside the Jupyter notebook with inline plots, meaning you may have to hit the exam key twice for the plot to appear. This issue still needs to be worked out, if you're running into it try using the Qt4Agg backend or plotting outside the notebook and saving the figures through the imexam grab or save calls. More information about the backends for matplotlib can be found here: https://matplotlib.org/users/shell.html
If you get an error about not finding the file "import" when you use the grab() function to save a copy of the DS9 window.
diff --git a/docs/imexam/example1.rst b/docs/imexam/example1.rst
index 92dc79af..7b28afff 100644
--- a/docs/imexam/example1.rst
+++ b/docs/imexam/example1.rst
@@ -17,7 +17,7 @@ First you need to import the package
Usage with D9 (the current default viewer)
------------------------------------------
-If you are on a windows system, DS9 may not be available, so move on to the Ginga specification.
+If you are on a windows system, and DS9 is not be available, move on to the Ginga specification.
Start up a ``DS9`` window (by default), a new ``DS9`` window will be opened, open a fits image, and scale it::
@@ -48,13 +48,13 @@ If you already have a window running, you can ask for a list of windows; windows
DS9 ds9 gs 82a7e75f:57222 sosey
-You can attach to a current ``DS9`` window be specifying its unique name
+You can attach to a current ``DS9`` window by specifying its unique name
::
viewer1=imexam.connect('ds9')
-If you haven't given your windows unique names using the ``-t `` option from the commandline, then you must use the ip:port address::
+If you haven't given your windows unique names using the ``-title `` option from the commandline, then you must use the ip:port address::
viewer=imexam.connect('82a7e75f:57222')
diff --git a/docs/imexam/example3.rst b/docs/imexam/example3.rst
index 95dc5a3d..d0afdcb9 100644
--- a/docs/imexam/example3.rst
+++ b/docs/imexam/example3.rst
@@ -288,7 +288,7 @@ If you haven't already, start DS9 and load your image into the viewer. I'll assu
#A little unsure this is the correct window? Let's check by asking what image is loaded. The image I'm working with is iabf01bzq_flt.fits
- viewer.get_data_filename()
+ viewer.get_filename()
'/Users/sosey/ssb/sosey/testme/iabf01bzq_flt.fits' <-- notice it returned the full pathname to the file
diff --git a/docs/imexam/example5.rst b/docs/imexam/example5.rst
index d4f4667b..e702c09f 100644
--- a/docs/imexam/example5.rst
+++ b/docs/imexam/example5.rst
@@ -25,13 +25,13 @@ These are the functions you now have access to:
plots.aper_phot plots.contour_plot plots.histogram_plot plots.plot_line plots.set_colplot_pars plots.set_surface_pars
plots.aperphot_def_pars plots.curve_of_growth_def_pars plots.imexam_option_funcs plots.plot_name plots.set_column_fit_pars plots.show_xy_coords
- plots.aperphot_pars plots.curve_of_growth_pars plots.line_fit plots.print_options plots.set_contour_pars plots.showplt
+ plots.aperphot_pars plots.curve_of_growth_pars plots.line_fit plots.print_options plots.set_contour_pars plots.set_plot_name
plots.colplot_def_pars plots.curve_of_growth_plot plots.line_fit_def_pars plots.register plots.set_data plots.sleep_time
plots.colplot_pars plots.do_option plots.line_fit_pars plots.report_stat plots.set_histogram_pars plots.surface_def_pars
plots.column_fit plots.gauss_center plots.lineplot_def_pars plots.report_stat_def_pars plots.set_line_fit_pars plots.surface_pars
plots.column_fit_def_pars plots.get_options plots.lineplot_pars plots.report_stat_pars plots.set_lineplot_pars plots.surface_plot
plots.column_fit_pars plots.get_plot_name plots.new_plot_window plots.reset_defpars plots.set_option_funcs plots.unlearn_all
- plots.contour_def_pars plots.histogram_def_pars plots.option_descrip plots.save_figure plots.set_plot_name
+ plots.contour_def_pars plots.histogram_def_pars plots.option_descrip plots.save_figure
plots.contour_pars plots.histogram_pars plots.plot_column plots.set_aperphot_pars plots.set_radial_pars
diff --git a/docs/imexam/imexam_command.rst b/docs/imexam/imexam_command.rst
index c32a7f45..624e3178 100644
--- a/docs/imexam/imexam_command.rst
+++ b/docs/imexam/imexam_command.rst
@@ -9,24 +9,23 @@ This is the main method which allows live interaction with the image display whe
**Current recognized keys available during imexam are:** ::
- 2 Make the next plot in a new window
- a Aperture sum, with radius region_size
- b Return the 2D gauss fit center of the object
- c Return column plot
- e Return a contour plot in a region around the cursor
- g Return curve of growth plot
- h Return a histogram in the region around the cursor
- j 1D [Gaussian1D default] line fit
- k 1D [Gaussian1D default] column fit
- l Return line plot
- m Square region stats, in [region_size],default is median
- r Return the radial profile plot
- s Save current figure to disk as [plot_name]
- t Make a fits image cutout using pointer location
- w Display a surface plot around the cursor location
- x Return x,y,value of pixel
- y Return x,y,value of pixel
-
+ 2 Make the next plot in a new window
+ a Aperture sum, with radius region_size
+ b Return the 2D gauss fit center of the object
+ c Return column plot
+ e Return a contour plot in a region around the cursor
+ g Return curve of growth plot
+ h Return a histogram in the region around the cursor
+ j 1D [Gaussian1D default] line fit
+ k 1D [Gaussian1D default] column fit
+ l Return line plot
+ m Square region stats, in [region_size],default is median
+ r Return the radial profile plot
+ s Save current figure to disk as [plot_name]
+ t Make a fits image cutout using pointer location
+ w Display a surface plot around the cursor location
+ x Return x,y,value of pixel
+ y Return x,y,value of pixel
aimexam(): return a dict of current parameters for aperture photometery
@@ -61,13 +60,13 @@ The ``imexam`` key dictionary is stored inside the user object as
However, you can access the same dictionary and customize the plotting parameters using ``set_plot_pars``. In the following example, I'm setting three of the parameters for the contour map, whose imexam key is "e"::
#customize the plotting parameters (or any function in the imexam loop)
- a.set_plot_pars('e','title','This is my favorite galaxy')
- a.set_plot_pars('e','ncontours',4)
- a.set_plot_pars('e','cmap','YlOrRd') #see http://matplotlib.org/users/colormaps.html
+ viewer.set_plot_pars('e','title','This is my favorite galaxy')
+ viewer.set_plot_pars('e','ncontours',4)
+ viewer.set_plot_pars('e','cmap','YlOrRd') #see http://matplotlib.org/users/colormaps.html
where the full dictionary of available values can be found using the ``eimexam()`` function described above.::
- In [1]: a.eimexam()
+ In [1]: viewer.eimexam()
Out[2]:
{'ceiling': [None, 'Maximum value to be contoured'],
'cmap': ['RdBu', 'Colormap (matplotlib style) for image'],
@@ -133,25 +132,37 @@ These are the default parameters for aperture photometry. They live in a diction
The direct access:
- viewer.exam.aperphot_pars= {"function":["aperphot",],
- "center":[True,"Center the object location using a Gaussian2D fit"],
- "width":[5,"Width of sky annulus in pixels"],
- "subsky":[True,"Subtract a sky background?"],
- "skyrad":[15,"Distance to start sky annulus is pixels"],
- "radius":[5,"Radius of aperture for star flux"],
- "zmag":[25.,"zeropoint for the magnitude calculation"],
+ viewer.exam.aper_phot_pars= {'function':["aperphot",],
+ 'center':[True,"Center the object location using a Gaussian2D fit"],
+ 'width':[5,"Width of sky annulus in pixels"],
+ 'subsky':[True,"Subtract a sky background?"],
+ 'skyrad':[15,"Distance to start sky annulus is pixels"],
+ 'radius':[5,"Radius of aperture for star flux"],
+ 'zmag':[25.,"zeropoint for the magnitude calculation"],
+ 'genplot': [True, 'Plot the apertures'],
+ 'title': [None, 'Title of the plot'],
+ 'scale': ['zscale', 'How to scale the image'],
+ 'color_min': [None, 'Minimum color value'],
+ 'color_max': [None, 'Maximum color value'],
+ 'cmap': ['Greys', 'Matplotlib colormap to use']
}
Using the convenience function:
- In [1]: a.aimexam()
+ In [1]: viewer.aimexam()
Out[2]:
{'center': [True, 'Center the object location using a 2d gaussian fit'],
- 'function': ['aper_phot'],
+ 'function': ['aper_phot'],
'radius': [5, 'Radius of aperture for star flux'],
'skyrad': [15, 'Distance to start sky annulus is pixels'],
'subsky': [True, 'Subtract a sky background?'],
'width': [5, 'Width of sky annulus in pixels'],
- 'zmag': [25.0, 'zeropoint for the magnitude calculation']}
+ 'zmag': [25.0, 'zeropoint for the magnitude calculation'],
+ 'genplot': [True, 'Plot the apertures'],
+ 'title': [None, 'Title of the plot'],
+ 'scale': ['zscale', 'How to scale the image'],
+ 'color_min': [None, 'Minimum color value'],
+ 'color_max': [None, 'Maximum color value'],
+ 'cmap': ['Greys', 'Matplotlib colormap to use']}
In order to change the width of the photometry aperture around the object you would do this:::
@@ -161,49 +172,58 @@ This is what the return looks like when you do photometry, where I've asked for
viewer.imexam()
- xc=576.855763 yc=634.911425
- x y radius flux mag(zpt=25.00) sky fwhm
- 576.86 634.91 10 2191284.53 9.15 10998.89 5.58
+ xc=574.988523 yc=632.680333
+ x y radius flux mag(zpt=25.00) sky/pix fwhm(pix)
+ 574.99 632.68 10 2178054.09 9.15 11005.40 5.72
xc = xcenter, yc=ycenter; these were found using a Gaussian2D fit centered on the pixel location of the mouse. You can turn the fit off by setting the "center" parameter to "False".
+This is the resulting plot:
+
+.. image:: ../_static/ap_phot_plot.png
+ :height: 400
+ :width: 400
+ :alt: Plot of aperture photometry apertures
+
+
+Available 1D profiles
+---------------------
+These include Gaussian1D, Moffat1D, MexicanHat1D, AiryDisk2D, and Polynomial1D.
-Gaussian1D, Moffat1D, MexicanHat1D profiles
--------------------------------------------
If you press the "j" or "k" keys, a 1D profile is fit to the data in either the line or column of the current pointer location. An option to use a Polynomial1D fit is also available, although not something of use for looking at stellar profiles. A plot of both the data and the fit + parameters is displayed. If the centering option is True, then the center of the flux is computed by fitting a 2d Gaussian to the data. ::
line_fit_pars={"function":["line_fit",],
- "func":["gaussian","function for fitting [see available]"],
+ "func":["gaussian"," function for fitting [see available]"],
"title":["Fit 1D line plot","Title of the plot"],
- "xlabel":["Line","The string for the xaxis label"],
- "ylabel":["Flux","The string for the yaxis label"],
- "background":[False,"Solve for background? [bool]"],
- "width":[10.0,"Background width in pixels"],
- "xorder":[0,"Background terms to fit, 0=median"],
- "rplot":[20.,"Plotting radius in pixels"],
- "pointmode":[True,"plot points instead of lines? [bool]"],
- "logx":[False,"log scale x-axis?"],
- "logy":[False,"log scale y-axis?"],
- "center":[True,"Recenter around the local max"],
+ "xlabel":["Line", "The string for the xaxis label"],
+ "ylabel":["Flux", "The string for the yaxis label"],
+ "background":[False, "Solve for background? [bool]"],
+ "width":[10.0, "Background width in pixels"],
+ "xorder":[0, "Background terms to fit, 0=median"],
+ "rplot":[20., "Plotting radius in pixels"],
+ "pointmode":[True, "plot points instead of lines? [bool]"],
+ "logx":[False, "log scale x-axis?"],
+ "logy":[False, "log scale y-axis?"],
+ "center":[True, "Recenter around the local max"],
}
The column fit parameters are similar::
column_fit_pars={"function":["column_fit",],
- "func":["Gaussian1D","function for fitting [see available]"],
- "title":["Fit 1D column plot","Title of the plot"],
- "xlabel":["Column","The string for the xaxis label"],
- "ylabel":["Flux","The string for the yaxis label"],
- "background":[False,"Solve for background? [bool]"],
- "width":[10.0,"Background width in pixels"],
- "xorder":[0,"Background terms to fit, 0=median"],
- "rplot":[20.,"Plotting radius in pixels"],
+ "func":["Gaussian1D", "function for fitting [see available]"],
+ "title":["Fit 1D column plot", "Title of the plot"],
+ "xlabel":["Column", "The string for the xaxis label"],
+ "ylabel":["Flux", "The string for the yaxis label"],
+ "background":[False, "Solve for background? [bool]"],
+ "width":[10.0, "Background width in pixels"],
+ "xorder":[0, "Background terms to fit, 0=median"],
+ "rplot":[20., "Plo tting radius in pixels"],
"pointmode":[True,"plot points instead of lines? [bool]"],
- "logx":[False,"log scale x-axis?"],
- "logy":[False,"log scale y-axis?"],
- "center":[True,"Recenter around the local max"],
+ "logx":[False, "log scale x-axis?"],
+ "logy":[False, "log scale y-axis?"],
+ "center":[True, "Recenter around the local max"],
}
This is the resulting line fit:
@@ -230,8 +250,8 @@ If you press the "m" key, the pixel values around the pointer location are calc
The user can map the function to any reasonable numpy function, it's set to numpy.median by default::
report_stat_pars= {"function":["report_stat",],
- "stat":["median","numpy stat name or describe for scipy.stats"],
- "region_size":[5,"region size in pixels to use"],
+ "stat":["median", "numpy stat name or describe for scipy.stats"],
+ "region_size":[5, "region size in pixels to use"],
}
@@ -239,7 +259,7 @@ The user can map the function to any reasonable numpy function, it's set to nump
You can change the statistic reported by changing the "stat" parameter::
- viewer.set_plot_pars('m',"stat","max")
+ viewer.set_plot_pars('m', "stat", "max")
[572:577,629:634] amax: 55271.000000
diff --git a/docs/imexam/walkthrough.rst b/docs/imexam/walkthrough.rst
new file mode 100644
index 00000000..71fd578e
--- /dev/null
+++ b/docs/imexam/walkthrough.rst
@@ -0,0 +1,279 @@
+==================
+Simple Walkthrough
+==================
+
+This is intended as a basic example of using the imexam package as a quicklook
+for image examination. If you are new to python or to the python version of imexam,
+start here to get your feet wet.
+
+First you need to import the package
+::
+
+ import imexam
+
+
+Usage with D9 (the current default viewer)
+------------------------------------------
+Start up a ``DS9`` window (DS9 is the default viewer):
+
+* a new ``DS9`` window will be opened
+* open a fits image
+* scale the image using zscale()::
+
+ viewer=imexam.connect() # startup a new DS9 window
+ viewer.load_fits('iacs01t4q_flt.fits') # load a fits image into it
+ viewer.scale() # run default zscaling on the image
+
+
+.. image:: ../_static/simple_ds9_open.png
+ :height: 600
+ :width: 400
+ :alt: imexam with DS9 window and loaded fits image
+
+If you already have a DS9 gui running, you can ask for a list of available windows:
+
+::
+
+ # This will display if you've used the default command above and have no other DS9 windows open
+ In [1]: imexam.list_active_ds9()
+ DS9 imexam1522943947.288667 gs a825364:62436 sosey
+ Out[2]: {'a825364:62436': ('imexam1522943947.288667', 'sosey', 'DS9', 'gs')}
+
+ ## imexam puts its own unique name on the window
+
+ # open a window in another process from the shell
+ # you should see it use the default name, 'ds9'
+ In [3]: !ds9&
+ In [4]: imexam.list_active_ds9()
+ Out[7]:
+ {'a825364:62436': ('imexam1522943947.288667', 'sosey', 'DS9', 'gs'),
+ 'a825364:62459': ('ds9', 'sosey', 'DS9', 'gs')}
+
+
+You can attach to a current ``DS9`` window by specifying its unique name,
+this is the first name listed in the dictionary item values tuple:
+::
+
+ viewer1 = imexam.connect('ds9')
+
+
+If you haven't given your windows unique names using the ``-title `` option from the commandline, then you must use the ip:port address. This address is also the key that is
+returned in the dictionary of active DS9 windows. In order to attached to the window
+we stared in the shell, use : `a825364:62459`
+
+ viewer1 = imexam.connect('a825364:62459')
+
+
+Load a fits image into the new DS9 window:
+
+ viewer1.load_fits('n8q624e8q_cal.fits')
+
+
+You may have noticed that the information from `list_active_ds9()` is returned in a python dictionary structure, this is to enable quick cycling or picking of available DS9 windows
+by asking for the keys in the dictionary. This following is just for instruction
+purposes, the code below asks for the list of windows and then successively
+displays the same image to each one::
+
+ ds9_windows = imexam.list_active_ds9()
+ for window in ds9_windows:
+ temp=imexam.connect(window)
+ temp.load_fits('n8q624e8q_cal.fits')
+
+
+It's also possible to load a FITS image object that you already have opened in your
+python session, if no extension is given, then the first IMAGE exension that is found
+will be loaded as a numpy array::
+
+ from astropy.io import fits
+ image = fits.open('n8q624e8q_cal.fits')
+ viewer1.load_fits(image)
+
+
+Using `get_viewer_info()` returns information about what is contained
+inside the DS9 window. There could be many uses for the returned
+dictionary, here I'm just listing the information to show you
+how the display of the FITS file versus the FITS object changes
+the information that `imexam` stores::
+
+
+ In [23]: viewer1.get_viewer_info()
+ Out[23]:
+ {'1': {'extname': 'SCI',
+ 'extver': 1,
+ 'filename': '/Users/sosey/test_images/n8q624e8q_cal.fits',
+ 'iscube': False,
+ 'mef': True,
+ 'naxis': 0,
+ 'numaxis': 2,
+ 'user_array': None}}
+
+ # Above, you can see there is only 1 frame, named 1, that
+ # contains a multi-extension fits file
+
+ In [24]: from astropy.io import fits
+ In [25]: image = fits.open('n8q624e8q_cal.fits')
+ In [26]: viewer1.load_fits(image)
+ In [27]: viewer1.get_viewer_info()
+ Out[27]:
+ {'1': {'extname': None,
+ 'extver': None,
+ 'filename': None,
+ 'iscube': False,
+ 'mef': False,
+ 'naxis': 0,
+ 'numaxis': 2,
+ 'user_array': array([[ 0. , 0. , 0.73420113, ..., 2.29928851,
+ 1.13779497, 0.40814143],
+ [ 0. , 0.76415622, 0. , ..., 2.02307796,
+ 1.07565212, 0.44265628],
+ [ 0. , 0.76297635, 0.65969932, ..., 0.61184824,
+ 0.48248726, 0.41064522],
+ ...,
+ [ 0.5144701 , 0.38698068, 0.31468284, ..., 1.57044649,
+ 0.42518842, 0.50868863],
+ [ 0.44805121, 0.34715804, 0.33939072, ..., 0.67747742,
+ 0.46475834, 0.51104462],
+ [ 0.53063494, 0.54570055, 0.53724855, ..., 0.4361479 ,
+ 0.58057427, 0.45152891]], dtype=float32)}}
+
+ # Above you can see that there is only 1 frame, but it contains
+ # a numpy array and no filename reference.
+
+
+
+You can also load a numpy array directly, we'll create an example array
+and display it to our viewer::
+
+ import numpy as np
+ array = np.ones((100,100), dtype=np.float) * np.random.rand(100)
+ viewer.view(array)
+ viewer.zoom() # by default, zoom-to-fit, or give it a scale factor
+
+.. image:: ../_static/walkthrough-array.png
+ :height: 500
+ :width: 400
+ :alt: imexam with DS9 window and loaded numpy array
+
+
+Now lets use `imexam()` to create a couple plots::
+
+ viewer.load_fits('n8q624e8q_cal.fits')
+ viewer.imexam()
+
+The available key mappings should be printed to your terminal::
+
+ In [7]: viewer.imexam()
+
+ Press 'q' to quit
+
+ 2 Make the next plot in a new window
+ a Aperture sum, with radius region_size
+ b Return the 2D gauss fit center of the object
+ c Return column plot
+ e Return a contour plot in a region around the cursor
+ g Return curve of growth plot
+ h Return a histogram in the region around the cursor
+ j 1D [Gaussian1D default] line fit
+ k 1D [Gaussian1D default] column fit
+ l Return line plot
+ m Square region stats, in [region_size],default is median
+ r Return the radial profile plot
+ s Save current figure to disk as [plot_name]
+ t Make a fits image cutout using pointer location
+ w Display a surface plot around the cursor location
+ x Return x,y,value of pixel
+ y Return x,y,value of pixel
+
+
+Look at the window below, I've started the imexam loop
+and then pressed the 'a' key to create an aperture photometry
+plot (which also printed information about the photometry to
+the terminal), then I pressed the '2' key in order to keep the
+current plot open and direct the next plot to a new window,
+where I've asked for a line plot of the same star, using the 'l' key.
+
+.. image:: ../_static/walkthrough-imexam.png
+ :height: 500
+ :width: 400
+ :alt: imexam plotting functionality
+
+You should see the printed information in your terminal::
+
+ Current image /Users/sosey/test_images/n8q624e8q_cal.fits
+ xc=104.757598 yc=131.706727
+ x y radius flux mag(zpt=25.00) sky/pix fwhm(pix)
+ 104.76 131.71 5 33.84 21.18 0.87 1.73
+ Plots now directed towards imexam2
+ Line at 104.75 131.625
+
+
+Users may change the default settings for each of the imexamine recognized keys by
+editing the associated dictionary. You can edit it directly, by accessing each of
+the values by their keyname and then reset mydict to values you prefer. You can
+also create a new dictionary of functions which map to your own analysis functions.
+
+However, you can access the same dictionary and customize the plotting parameters using ``set_plot_pars``. In the following example, I'm setting three of the parameters for the contour map, whose imexam key is "e"::
+
+ #customize the plotting parameters (or any function in the imexam loop)
+ viewer.set_plot_pars('e','title','This is my favorite galaxy')
+ viewer.set_plot_pars('e','ncontours',4)
+ viewer.set_plot_pars('e','cmap','YlOrRd') #see http://matplotlib.org/users/colormaps.html
+
+where the full dictionary of available values can be found using the ``eimexam()`` function described above.::
+
+ In [1]: viewer.eimexam()
+ Out[2]:
+ {'ceiling': [None, 'Maximum value to be contoured'],
+ 'cmap': ['RdBu', 'Colormap (matplotlib style) for image'],
+ 'floor': [None, 'Minimum value to be contoured'],
+ 'function': ['contour'],
+ 'label': [True, 'Label major contours with their values? [bool]'],
+ 'linestyle': ['--', 'matplotlib linestyle'],
+ 'ncolumns': [15, 'Number of columns'],
+ 'ncontours': [8, 'Number of contours to be drawn'],
+ 'nlines': [15, 'Number of lines'],
+ 'title': [None, 'Title of the plot'],
+ 'xlabel': ['x', 'The string for the xaxis label'],
+ 'ylabel': ['y', 'The string for the yaxis label']}
+
+Users may also add their own ``imexam`` keys and associated functions by registering them with the register(user_funct=dict()) method. The new binding will be added to the dictionary of imexamine functions as long as the key is unique. The new functions do not have to have default dictionaries association with them, but users are free to create them.
+
+
+
+Usage with Ginga viewer
+-----------------------
+
+Start up a ginga window using the HTML5 backend and display an image. Make sure that you have installed the most recent version of ginga, ``imexam`` may return an error that the viewer cannot be found otherwise.::
+
+ # since we've already used the viewer object
+ # to point to a DS9 window in the example
+ # above, we'll first cleanly close that down
+ viewer.close()
+
+ # now connect to a ginga window
+ viewer=imexam.connect(viewer='ginga')
+ viewer.load_fits('n8q624e8q_cal.fits')
+
+
+.. note:: All commands after your chosen viewer is opened are the same. Each viewer may also have it's own set of commands which you can additionally use.
+
+Scale the image to the default scaling, which is a zscale algorithm, but the viewers other scaling options are also available::
+
+ viewer.scale()
+ viewer.scale('asinh') # <-- uses asinh
+
+
+.. image:: ../_static/walkthrough-ginga.png
+ :height: 500
+ :width: 400
+ :alt: imexam with ginga window and loaded FITS array
+
+
+.. note:: When using the Ginga interface, the `imexam` plotting and analysis functions are used by pressing the 'i' key to enter imexam mode. Inside this mode the key mappings are as listed by `imexam`, outside of this mode (pressing 'q') the Ginga key mappings are in effect.
+
+
+When you are using the HTML5 Ginga viewer, the `close()` method will stop the HTTP server, but you must close the window manually.
+
+ In [34]: viewer.close()
+ Stopped http server
+
diff --git a/docs/index.rst b/docs/index.rst
index c4be8e40..9f3c7e17 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -4,26 +4,41 @@
:width: 600
:alt: Example imexam workspace
-``imexam`` is an affiliated package of AstroPy. It was designed to be a lightweight library which enables users to explore data using common methods which are consistant across viewers. It can be used from a command line interface, through a Jupyter notebook or through a Jupyter console. It can be used with multiple viewers, such as DS9 or Ginga, or without a viewer as a simple library to make plots and grab quick photometry information. The above image is an example desktop interfacing with DS9. Below, is another example desktop using the Jupyter notebook and the Ginga HTML5 viewer.
+The above image is an example desktop interfacing with DS9.
-.. image:: _static/ginga_desktop_html5.png
- :height: 400
- :width: 600
- :alt: Example imexam workspace
+``imexam`` is an affiliated package of AstroPy. It was designed to be a lightweight library that enables users to explore data using common methods which are consistant across viewers.
The power of this python tool is that it is essentially a library of plotting
-and analysis routines which can be directed towards any viewer. It attempts to
-standardize the interface into these functions so that no matter
+and analysis routines that can be directed towards any viewer. It attempts to
+standardize the analysis interface so that no matter
what viewer is in use the calls and results are the same. It can also be used
without connecting to any viewer since the calls take only data and location
information. This means that given a data array and a list of x,y positions
you can create plots and return information without having to interact with
-the viewers, just by calling the functions directly either from a shell or a
-private script.
+the viewers, just by calling the functions directly either from a a command line
+shell or from a private script.
+
+`imexam` can be used:
+
+* from a command line interface
+* through a Jupyter notebook or through a Jupyter console
+* with multiple viewers, such as DS9 or Ginga (submit a github issue or PR to add others)
+* without a viewer as a simple library to make plots and grab quick photometry information.
+
``imexam`` may be used as a replacement for the IRAF imexamine task. You should be able
to perform all of the most used functions that ``imexamine`` provided in IRAF, but
you also gain the flexibility of python and the ability to add your own analysis functions.
+The standalone library has also been used as a replacement for `psfmeasure`.
+
+.. image:: _static/ginga_desktop_html5.png
+ :height: 400
+ :width: 600
+ :alt: Example imexam workspace
+
+The above image is an example desktop using the Jupyter notebook and the Ginga HTML5 viewer.
+
+
Installation
============
@@ -34,6 +49,15 @@ Installation
Description
+Simple Walkthrough
+==================
+.. toctree::
+ :maxdepth: 2
+
+
+ Getting started with a basic walk though a simple use case
+
+
User documentation
==================
.. toctree::
@@ -47,6 +71,7 @@ User documentation
Dependencies
IRAF-imexamine Capabilites
Comparison with IRAF
+
Reporting Issues
================
diff --git a/ez_setup.py b/ez_setup.py
deleted file mode 100644
index 800c31ef..00000000
--- a/ez_setup.py
+++ /dev/null
@@ -1,414 +0,0 @@
-#!/usr/bin/env python
-
-"""
-Setuptools bootstrapping installer.
-
-Maintained at https://github.com/pypa/setuptools/tree/bootstrap.
-
-Run this script to install or upgrade setuptools.
-
-This method is DEPRECATED. Check https://github.com/pypa/setuptools/issues/581 for more details.
-"""
-
-import os
-import shutil
-import sys
-import tempfile
-import zipfile
-import optparse
-import subprocess
-import platform
-import textwrap
-import contextlib
-
-from distutils import log
-
-try:
- from urllib.request import urlopen
-except ImportError:
- from urllib2 import urlopen
-
-try:
- from site import USER_SITE
-except ImportError:
- USER_SITE = None
-
-# 33.1.1 is the last version that supports setuptools self upgrade/installation.
-DEFAULT_VERSION = "33.1.1"
-DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/"
-DEFAULT_SAVE_DIR = os.curdir
-DEFAULT_DEPRECATION_MESSAGE = "ez_setup.py is deprecated and when using it setuptools will be pinned to {0} since it's the last version that supports setuptools self upgrade/installation, check https://github.com/pypa/setuptools/issues/581 for more info; use pip to install setuptools"
-
-MEANINGFUL_INVALID_ZIP_ERR_MSG = 'Maybe {0} is corrupted, delete it and try again.'
-
-log.warn(DEFAULT_DEPRECATION_MESSAGE.format(DEFAULT_VERSION))
-
-
-def _python_cmd(*args):
- """
- Execute a command.
-
- Return True if the command succeeded.
- """
- args = (sys.executable,) + args
- return subprocess.call(args) == 0
-
-
-def _install(archive_filename, install_args=()):
- """Install Setuptools."""
- with archive_context(archive_filename):
- # installing
- log.warn('Installing Setuptools')
- if not _python_cmd('setup.py', 'install', *install_args):
- log.warn('Something went wrong during the installation.')
- log.warn('See the error message above.')
- # exitcode will be 2
- return 2
-
-
-def _build_egg(egg, archive_filename, to_dir):
- """Build Setuptools egg."""
- with archive_context(archive_filename):
- # building an egg
- log.warn('Building a Setuptools egg in %s', to_dir)
- _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
- # returning the result
- log.warn(egg)
- if not os.path.exists(egg):
- raise IOError('Could not build the egg.')
-
-
-class ContextualZipFile(zipfile.ZipFile):
-
- """Supplement ZipFile class to support context manager for Python 2.6."""
-
- def __enter__(self):
- return self
-
- def __exit__(self, type, value, traceback):
- self.close()
-
- def __new__(cls, *args, **kwargs):
- """Construct a ZipFile or ContextualZipFile as appropriate."""
- if hasattr(zipfile.ZipFile, '__exit__'):
- return zipfile.ZipFile(*args, **kwargs)
- return super(ContextualZipFile, cls).__new__(cls)
-
-
-@contextlib.contextmanager
-def archive_context(filename):
- """
- Unzip filename to a temporary directory, set to the cwd.
-
- The unzipped target is cleaned up after.
- """
- tmpdir = tempfile.mkdtemp()
- log.warn('Extracting in %s', tmpdir)
- old_wd = os.getcwd()
- try:
- os.chdir(tmpdir)
- try:
- with ContextualZipFile(filename) as archive:
- archive.extractall()
- except zipfile.BadZipfile as err:
- if not err.args:
- err.args = ('', )
- err.args = err.args + (
- MEANINGFUL_INVALID_ZIP_ERR_MSG.format(filename),
- )
- raise
-
- # going in the directory
- subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
- os.chdir(subdir)
- log.warn('Now working in %s', subdir)
- yield
-
- finally:
- os.chdir(old_wd)
- shutil.rmtree(tmpdir)
-
-
-def _do_download(version, download_base, to_dir, download_delay):
- """Download Setuptools."""
- py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys)
- tp = 'setuptools-{version}-{py_desig}.egg'
- egg = os.path.join(to_dir, tp.format(**locals()))
- if not os.path.exists(egg):
- archive = download_setuptools(version, download_base,
- to_dir, download_delay)
- _build_egg(egg, archive, to_dir)
- sys.path.insert(0, egg)
-
- # Remove previously-imported pkg_resources if present (see
- # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
- if 'pkg_resources' in sys.modules:
- _unload_pkg_resources()
-
- import setuptools
- setuptools.bootstrap_install_from = egg
-
-
-def use_setuptools(
- version=DEFAULT_VERSION, download_base=DEFAULT_URL,
- to_dir=DEFAULT_SAVE_DIR, download_delay=15):
- """
- Ensure that a setuptools version is installed.
-
- Return None. Raise SystemExit if the requested version
- or later cannot be installed.
- """
- to_dir = os.path.abspath(to_dir)
-
- # prior to importing, capture the module state for
- # representative modules.
- rep_modules = 'pkg_resources', 'setuptools'
- imported = set(sys.modules).intersection(rep_modules)
-
- try:
- import pkg_resources
- pkg_resources.require("setuptools>=" + version)
- # a suitable version is already installed
- return
- except ImportError:
- # pkg_resources not available; setuptools is not installed; download
- pass
- except pkg_resources.DistributionNotFound:
- # no version of setuptools was found; allow download
- pass
- except pkg_resources.VersionConflict as VC_err:
- if imported:
- _conflict_bail(VC_err, version)
-
- # otherwise, unload pkg_resources to allow the downloaded version to
- # take precedence.
- del pkg_resources
- _unload_pkg_resources()
-
- return _do_download(version, download_base, to_dir, download_delay)
-
-
-def _conflict_bail(VC_err, version):
- """
- Setuptools was imported prior to invocation, so it is
- unsafe to unload it. Bail out.
- """
- conflict_tmpl = textwrap.dedent("""
- The required version of setuptools (>={version}) is not available,
- and can't be installed while this script is running. Please
- install a more recent version first, using
- 'easy_install -U setuptools'.
-
- (Currently using {VC_err.args[0]!r})
- """)
- msg = conflict_tmpl.format(**locals())
- sys.stderr.write(msg)
- sys.exit(2)
-
-
-def _unload_pkg_resources():
- sys.meta_path = [
- importer
- for importer in sys.meta_path
- if importer.__class__.__module__ != 'pkg_resources.extern'
- ]
- del_modules = [
- name for name in sys.modules
- if name.startswith('pkg_resources')
- ]
- for mod_name in del_modules:
- del sys.modules[mod_name]
-
-
-def _clean_check(cmd, target):
- """
- Run the command to download target.
-
- If the command fails, clean up before re-raising the error.
- """
- try:
- subprocess.check_call(cmd)
- except subprocess.CalledProcessError:
- if os.access(target, os.F_OK):
- os.unlink(target)
- raise
-
-
-def download_file_powershell(url, target):
- """
- Download the file at url to target using Powershell.
-
- Powershell will validate trust.
- Raise an exception if the command cannot complete.
- """
- target = os.path.abspath(target)
- ps_cmd = (
- "[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
- "[System.Net.CredentialCache]::DefaultCredentials; "
- '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")'
- % locals()
- )
- cmd = [
- 'powershell',
- '-Command',
- ps_cmd,
- ]
- _clean_check(cmd, target)
-
-
-def has_powershell():
- """Determine if Powershell is available."""
- if platform.system() != 'Windows':
- return False
- cmd = ['powershell', '-Command', 'echo test']
- with open(os.path.devnull, 'wb') as devnull:
- try:
- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
- except Exception:
- return False
- return True
-download_file_powershell.viable = has_powershell
-
-
-def download_file_curl(url, target):
- cmd = ['curl', url, '--location', '--silent', '--output', target]
- _clean_check(cmd, target)
-
-
-def has_curl():
- cmd = ['curl', '--version']
- with open(os.path.devnull, 'wb') as devnull:
- try:
- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
- except Exception:
- return False
- return True
-download_file_curl.viable = has_curl
-
-
-def download_file_wget(url, target):
- cmd = ['wget', url, '--quiet', '--output-document', target]
- _clean_check(cmd, target)
-
-
-def has_wget():
- cmd = ['wget', '--version']
- with open(os.path.devnull, 'wb') as devnull:
- try:
- subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
- except Exception:
- return False
- return True
-download_file_wget.viable = has_wget
-
-
-def download_file_insecure(url, target):
- """Use Python to download the file, without connection authentication."""
- src = urlopen(url)
- try:
- # Read all the data in one block.
- data = src.read()
- finally:
- src.close()
-
- # Write all the data in one block to avoid creating a partial file.
- with open(target, "wb") as dst:
- dst.write(data)
-download_file_insecure.viable = lambda: True
-
-
-def get_best_downloader():
- downloaders = (
- download_file_powershell,
- download_file_curl,
- download_file_wget,
- download_file_insecure,
- )
- viable_downloaders = (dl for dl in downloaders if dl.viable())
- return next(viable_downloaders, None)
-
-
-def download_setuptools(
- version=DEFAULT_VERSION, download_base=DEFAULT_URL,
- to_dir=DEFAULT_SAVE_DIR, delay=15,
- downloader_factory=get_best_downloader):
- """
- Download setuptools from a specified location and return its filename.
-
- `version` should be a valid setuptools version number that is available
- as an sdist for download under the `download_base` URL (which should end
- with a '/'). `to_dir` is the directory where the egg will be downloaded.
- `delay` is the number of seconds to pause before an actual download
- attempt.
-
- ``downloader_factory`` should be a function taking no arguments and
- returning a function for downloading a URL to a target.
- """
- # making sure we use the absolute path
- to_dir = os.path.abspath(to_dir)
- zip_name = "setuptools-%s.zip" % version
- url = download_base + zip_name
- saveto = os.path.join(to_dir, zip_name)
- if not os.path.exists(saveto): # Avoid repeated downloads
- log.warn("Downloading %s", url)
- downloader = downloader_factory()
- downloader(url, saveto)
- return os.path.realpath(saveto)
-
-
-def _build_install_args(options):
- """
- Build the arguments to 'python setup.py install' on the setuptools package.
-
- Returns list of command line arguments.
- """
- return ['--user'] if options.user_install else []
-
-
-def _parse_args():
- """Parse the command line for options."""
- parser = optparse.OptionParser()
- parser.add_option(
- '--user', dest='user_install', action='store_true', default=False,
- help='install in user site package')
- parser.add_option(
- '--download-base', dest='download_base', metavar="URL",
- default=DEFAULT_URL,
- help='alternative URL from where to download the setuptools package')
- parser.add_option(
- '--insecure', dest='downloader_factory', action='store_const',
- const=lambda: download_file_insecure, default=get_best_downloader,
- help='Use internal, non-validating downloader'
- )
- parser.add_option(
- '--version', help="Specify which version to download",
- default=DEFAULT_VERSION,
- )
- parser.add_option(
- '--to-dir',
- help="Directory to save (and re-use) package",
- default=DEFAULT_SAVE_DIR,
- )
- options, args = parser.parse_args()
- # positional arguments are ignored
- return options
-
-
-def _download_args(options):
- """Return args for download_setuptools function from cmdline args."""
- return dict(
- version=options.version,
- download_base=options.download_base,
- downloader_factory=options.downloader_factory,
- to_dir=options.to_dir,
- )
-
-
-def main():
- """Install or upgrade setuptools and EasyInstall."""
- options = _parse_args()
- archive = download_setuptools(**_download_args(options))
- return _install(archive, _build_install_args(options))
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/imexam/__init__.py b/imexam/__init__.py
index b29efd97..2e837325 100644
--- a/imexam/__init__.py
+++ b/imexam/__init__.py
@@ -19,8 +19,8 @@
if not _ASTROPY_SETUP_:
# import high level functions into the imexam namespace
if _have_xpa:
- from .util import list_active_ds9, find_xpans
- from .util import display_help, display_xpa_help, find_ds9
+ from .util import list_active_ds9, find_path
+ from .util import display_help, display_xpa_help
from .util import set_logging
from . import connect as _connect
diff --git a/imexam/_astropy_init.py b/imexam/_astropy_init.py
index e45aef7b..7f90e383 100644
--- a/imexam/_astropy_init.py
+++ b/imexam/_astropy_init.py
@@ -119,5 +119,5 @@ def test(package=None, test_path=None, args=None, plugins=None,
"importing from source, this is expected.")
warn(config.configuration.ConfigurationDefaultMissingWarning(wmsg))
del e
- except:
+ except Exception:
raise orig_error
diff --git a/imexam/connect.py b/imexam/connect.py
index d7e17092..39b130c8 100644
--- a/imexam/connect.py
+++ b/imexam/connect.py
@@ -170,7 +170,7 @@ def grab(self):
"""Display a snapshop of the current image in the browser window."""
return self.window.grab()
- def get_data_filename(self):
+ def get_filename(self):
"""Return the filename for the data in the current window."""
return self.window.get_filename()
@@ -214,7 +214,7 @@ def _run_imexam(self):
print("\nPress 'q' to quit\n")
keys = self.exam.get_options() # possible commands
self.exam.print_options()
- cstring = "Current image {0}".format(self.get_data_filename(),)
+ cstring = "Current image {0}".format(self.get_filename(),)
print(cstring)
# set defaults
@@ -232,22 +232,40 @@ def _run_imexam(self):
self._check_frame()
if self.window.iscube():
self._check_slice()
+
+ # This loop now recognizes the arrow keys
+ # for moving the cursor in the window, it calls
+ # the windows cursor function. However, depending
+ # on how the use has their windowing focus setup
+ # they might loose focus on the DS9 window and have to
+ # move the cursor to gain focus again, is there a way
+ # around this? Cursor is not implemented in the ginga
+ # interface.
try:
x, y, current_key = self.readcursor()
- self._check_frame()
- if self.window.iscube():
- self._check_slice()
- if current_key not in keys and 'q' not in current_key:
- print("Invalid key")
- self.exam._close_plots()
+ if current_key in ["Left", "Right", "Up", "Down"]:
+ if current_key == "Left":
+ x, y = x - 1, y
+ if current_key == "Right":
+ x, y = x + 1, y
+ if current_key == "Down":
+ x, y = x, y - 1
+ if current_key == "Up":
+ x, y = x, y + 1
+ self.cursor(x=x, y=y)
else:
- if 'q' in current_key:
- current_key = None
+ if current_key not in keys and 'q' not in current_key:
self.exam._close_plots()
else:
- self.exam.do_option(
- x, y, current_key)
-
+ if 'q' in current_key:
+ current_key = None
+ self.exam._close_plots()
+ else:
+ self._check_frame()
+ if self.window.iscube():
+ self._check_slice()
+ self.exam.do_option(
+ x, y, current_key)
except KeyError:
print(
"Invalid key, use\n: {0}".format(
@@ -346,7 +364,7 @@ def embed(self, **kwargs):
return self.window.embed(**kwargs)
def frame(self, *args, **kwargs):
- """Move to a different frame."""
+ """Move to a different frame, or add a new one"""
return self.window.frame(*args, **kwargs)
def get_data(self):
diff --git a/imexam/ds9_viewer.py b/imexam/ds9_viewer.py
index 8ac1c7c2..6abcddbe 100644
--- a/imexam/ds9_viewer.py
+++ b/imexam/ds9_viewer.py
@@ -225,7 +225,7 @@ def __init__(self, target='', path='', wait_time=5,
else:
if not path:
- self._ds9_path = util.find_ds9()
+ self._ds9_path = util.find_path('ds9')
if not self._ds9_path:
raise OSError("Could not find ds9 executable on your path")
@@ -326,7 +326,6 @@ def _set_frameinfo(self):
load_header = True
else:
filename_string = ""
-
except XpaException:
filename_string = ""
@@ -343,9 +342,7 @@ def _set_frameinfo(self):
naxis.append("0")
naxis.reverse() # for astropy.fits row-major ordering
naxis = map(int, naxis)
- naxis = [
- axis -
- 1 if axis > 0 else 0 for axis in naxis]
+ naxis = [axis - 1 if axis > 0 else 0 for axis in naxis]
naxis = tuple(naxis)
except ValueError:
raise ValueError("Problem parsing filename")
@@ -357,14 +354,14 @@ def _set_frameinfo(self):
header_cards = fits.Header.fromstring(
self.get_header(),
sep='\n')
- mef_file = util.check_filetype(filename)
+ mef_file, nextend, first_image = util.check_valid(filename)
if mef_file:
try:
extver = int(header_cards['EXTVER'])
except KeyError:
# fits doesn't require extver if there is only 1
# extension
- extver = 1
+ extver = first_image
try:
extname = str(header_cards['EXTNAME'])
@@ -409,10 +406,8 @@ def valid_data_in_viewer(self):
if frame:
self._set_frameinfo()
-
if self._viewer[frame]['filename']:
return True
-
else:
try:
if self._viewer[frame]['user_array'].any():
@@ -1058,7 +1053,7 @@ def get_data(self):
if self._viewer[frame]['iscube']:
data = filedata[extname, extver].section[naxis]
else:
- data = filedata[extname, extver].data
+ data = filedata[extver].data
return data
else:
with fits.open(filename) as filedata:
@@ -1125,15 +1120,16 @@ def embed(self):
"""Embed the viewer in a notebook."""
print("Not Implemented for DS9")
- def load_fits(self, fname=None, extver=None, mecube=False):
+ def load_fits(self, fname, extver=None, mecube=False):
"""convenience function to load fits image to current frame.
Parameters
----------
- fname: string, optional
+ fname: string, FITS object
The name of the file to be loaded. You can specify the full
extension in the name, such as
filename_flt.fits or filename_flt.fits[1]
+ You can also pass it an in-memory FITS object
extver: int, optional
The extension to load (EXTVER in the header)
@@ -1154,41 +1150,50 @@ def load_fits(self, fname=None, extver=None, mecube=False):
XPA needs to have the absolute path to the filename so that if the
DS9 window was started in another directory it can still find the
- file to load.
+ file to load. The pathname also needs to be stripped of spaces.
"""
- if fname is None:
- raise ValueError("No filename provided")
-
- frame = self.frame() # for the viewer reference
+ # for the viewer reference
+ frame = self.frame()
if frame is None:
frame = 1 # load into first frame
- shortname, extn, extv = util.verify_filename(fname)
+ if isinstance(fname, fits.hdu.image.PrimaryHDU):
+ shortname = fname
+ extn = None
+ extv = 0
+ if extver is None:
+ extver = extv
+ if isinstance(fname, fits.hdu.hdulist.HDUList):
+ shortname = fname
+ extn = None
+ extv = extver
+ elif isinstance(fname, str):
+ shortname, extn, extv = util.verify_filename(fname)
+ if extn is not None:
+ raise ValueError("Extension name given, must "
+ "specify the absolute extension you want")
+ # prefer the keyword value over the extension in the name
+ if extver is None:
+ extver = extv
+ else:
+ raise TypeError("Expected FITS data as input")
- if extn is not None:
- raise ValueError("Extension name given, must "
- "specify the absolute extension you want")
- # prefer the keyword value over the extension in the name
- if extver is None:
- extver = extv
+ # safety for a valid imexam file
+ if ((extv is None) and (extver is None)):
+ mef_file, nextend, first_image = util.check_valid(shortname)
+ extver = first_image # the extension of the first IMAGE
- # safety for simple vs mef fits
- if (extv is None and extver is None):
- mef = util.check_filetype(shortname)
- if mef:
- extver = 1 # MEF fits
+ if isinstance(fname, str):
+ if mecube:
+ cstring = "mecube {0:s}".format(shortname)
else:
- extver = 0 # simple fits
-
- if mecube:
- cstring = "mecube {0:s}".format(shortname)
+ cstring = ('fits {0:s}[{1:d}]'.format(shortname, extver))
+ self.set(cstring)
+ # make sure any previous reference is reset
+ self._set_frameinfo()
+ self._viewer[frame]['user_array'] = None
else:
- cstring = ('fits {0:s}[{1:d}]'.format(shortname, extver))
-
- self.set(cstring)
- self._set_frameinfo()
- # make sure any previous reference is reset
- self._viewer[frame]['user_array'] = None
+ self.view(fname[extver].data)
def load_region(self, filename):
"""Load regions from a file which uses ds9 standard formatting.
@@ -1288,23 +1293,23 @@ def mark_region_from_array(
elif isinstance(input_points, str):
input_points = [tuple(input_points.split())]
- X = 0
- Y = 1
- COMMENT = 2
+ x = 0
+ y = 1
+ comment = 2
rtype = "circle" # only one supported right now
for location in input_points:
if rtype == "circle":
pline = rtype + " " + \
- str(location[X]) + " " + str(location[Y]) + " " + str(size)
+ str(location[x]) + " " + str(location[y]) + " " + str(size)
print(pline)
self.set_region(pline)
try:
- if(len(str(location[COMMENT])) > 0):
- pline = "text " + str(float(location[X]) + textoff) +\
- " " + str(float(location[Y]) + textoff) + " '" +\
- str(location[COMMENT]) + "' #font=times"
+ if(len(str(location[comment])) > 0):
+ pline = "text " + str(float(location[x]) + textoff) +\
+ " " + str(float(location[y]) + textoff) + " '" +\
+ str(location[comment]) + "' #font=times"
print(pline)
self.set_region(pline)
except IndexError:
@@ -1793,7 +1798,6 @@ def zoom(self, par="to fit"):
def show_xpa_commands(self):
"""Print the available XPA commands."""
print(self.get('')) # With empty string, all commands are returned
-
def reopen(self):
"""Reopen a closed window."""
diff --git a/imexam/ginga_viewer.py b/imexam/ginga_viewer.py
index cc153490..d2012962 100644
--- a/imexam/ginga_viewer.py
+++ b/imexam/ginga_viewer.py
@@ -563,12 +563,12 @@ def embed(self, width=600, height=650):
return self.ginga_view.embed(width, height)
- def load_fits(self, fname="", extver=None):
+ def load_fits(self, fname=None, extver=None):
"""Load fits image to current frame.
Parameters
----------
- fname: string, optional
+ fname: string, FITS HDU
The name of the file to be loaded. You can specify the full
extension in the name, such as
filename_flt.fits[sci,1] or filename_flt.fits[1]
@@ -582,37 +582,57 @@ def load_fits(self, fname="", extver=None):
number, not the version number associated with a name
"""
if fname is None:
- raise ValueError("No filename provided")
+ raise ValueError("No filename or HDU provided")
- try:
- shortname, extn, extv = util.verify_filename(fname)
- image = AstroImage(logger=self.logger)
+ if isinstance(fname, (fits.hdu.image.PrimaryHDU, fits.hdu.image.ImageHDU)):
+ # Simple fits, data + header
+ shortname = fname
+ extn = None
+ if extver is None:
+ extv = None
+ extver = 0
+
+ elif isinstance(fname, fits.hdu.hdulist.HDUList):
+ shortname = fname
+ extn = None
+ extv = extver
+ elif isinstance(fname, str):
+ shortname, extn, extv = util.verify_filename(fname)
if extn is not None:
- raise ValueError("Extension name given, must specify "
- "the absolute extension you want")
- # prefer specified over filename
+ raise ValueError("Extension name given, must "
+ "specify the absolute extension you want")
+ # prefer the keyword value over the extension in the name
if extver is None:
extver = extv
- if (extv is None and extver is None):
- mef = util.check_filetype(shortname)
- if mef:
- extver = 1 # MEF fits
- else:
- extver = 0 # simple fits
+ else:
+ raise TypeError("Expected FITS data as input")
- with fits.open(shortname) as filedata:
- hdu = filedata[extver]
- image.load_hdu(hdu)
+ # safety for a valid imexam file
+ if ((extv is None) and (extver is None)):
+ mef_file, nextend, first_image = util.check_valid(shortname)
+ extver = first_image # the extension of the first IMAGE
+ image = AstroImage(logger=self.logger)
+
+ try:
+ if isinstance(fname, str):
+ with fits.open(shortname) as filedata:
+ hdu = filedata[extver]
+ image.load_hdu(hdu)
+ else:
+ if extver:
+ hdu = shortname[extver]
+ else:
+ hdu = shortname
+ image.load_hdu(hdu)
+ self._set_frameinfo(fname=shortname, hdu=hdu, image=image)
+ self.ginga_view.set_image(image)
except Exception as e:
- self.logger.error("Exception opening file: {0}".format(repr(e)))
+ self.logger.error("Exception loading image: {0}".format(repr(e)))
raise Exception(repr(e))
- self._set_frameinfo(fname=shortname, hdu=hdu, image=image)
- self.ginga_view.set_image(image)
-
def panto_image(self, x, y):
"""Change to x,y physical image coordinates.
diff --git a/imexam/imexam_defpars.py b/imexam/imexam_defpars.py
index 7b992ac7..64466c57 100644
--- a/imexam/imexam_defpars.py
+++ b/imexam/imexam_defpars.py
@@ -1,22 +1,26 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""These are default parameters for some of the plotting functions in Imexam
-Maybe it would be better to put these along with the imexam functions into
-their own class which the connect class can import? Users could edit this file
-to set their own defaults before installation, they could script something that
-resets the dictionaries themselves, or we could create a method to let them set
-from a json or text file maybe
+Users can edit this file to set their own defaults before installation,
+they could script something that resets the dictionaries themselves,
+or we could create a method to let them set from a json or text file maybe
"""
# aperture photometry parameters
aper_phot_pars = {"function": ["aper_phot", ],
- "center": [True, "Center the object location using a 2d gaussian fit"],
- "width": [5, "Width of sky annulus in pixels"],
- "subsky": [True, "Subtract a sky background?"],
- "skyrad": [15, "Distance to start sky annulus is pixels"],
- "radius": [5, "Radius of aperture for star flux"],
- "zmag": [25., "zeropoint for the magnitude calculation"],
- }
+ "center": [True, "Center the object location using a 2d gaussian fit"],
+ "width": [5, "Width of sky annulus in pixels"],
+ "subsky": [True, "Subtract a sky background?"],
+ "skyrad": [15, "Distance to start sky annulus is pixels"],
+ "radius": [5, "Radius of aperture for star flux"],
+ "zmag": [25., "zeropoint for the magnitude calculation"],
+ "genplot": [True, "Plot the apertures"],
+ "title": [None, "Title of the plot"],
+ "scale": ['zscale', "How to scale the image"],
+ "color_min": [None, "Minimum color value"],
+ "color_max": [None, "Maximum color value"],
+ "cmap": ['Greys', "Matplotlib colormap to use"],
+ }
# box statistics
@@ -27,22 +31,24 @@
# radial profile plots
radial_profile_pars = {"function": ["radial_profile", ],
- "title": [None, "Title of the plot"],
- "xlabel": ["Radius", "The string for the xaxis label"],
- "ylabel": ["Flux", "The string for the yaxis label"],
- "pixels": [True, "Plot all pixels at each radius? (False bins the data)"],
- "fitplot": [False, "Overplot profile fit?"],
- "fittype": ["Gaussian2D", "Profile type to fit (Gaussian2D)"],
- "center": [True, "Solve for center using 2d Gaussian? [bool]"],
- "background": [False, "Subtract background? [bool]"],
- "skyrad": [10., "Background inner radius in pixels, from center of object"],
- "width": [5., "Background annulus width in pixels"],
- "magzero": [25., "magnitude zero point"],
- "rplot": [8., "Plotting radius in pixels"],
- "pointmode": [True, "plot points instead of lines? [bool]"],
- "marker": ["o", "The marker character to use, matplotlib style"],
- "getdata": [False, "print the plotted data values"]
- }
+ "title": [None, "Title of the plot"],
+ "xlabel": ["Radius", "The string for the xaxis label"],
+ "ylabel": ["Flux", "The string for the yaxis label"],
+ "pixels": [True, "Plot all pixels at each radius? (False bins the data)"],
+ "fitplot": [True, "Overplot model fit?"],
+ "func": ["Gaussian1D", "Model form to fit"],
+ "center": [True, "Solve for center using 2d Gaussian? [bool]"],
+ "background": [True, "Subtract background? [bool]"],
+ "skyrad": [10., "Background inner radius in pixels, from center of object"],
+ "width": [5., "Background annulus width in pixels"],
+ "magzero": [25., "magnitude zero point"],
+ "rplot": [8., "Plotting radius in pixels"],
+ "pointmode": [True, "plot points instead of lines? [bool]"],
+ "marker": ["o", "The marker character to use, matplotlib style"],
+ "getdata": [False, "print the plotted data values"],
+ "clip": [False, "Sigma clip by np.mean before center fitting?"],
+ "sigma": [4.0, "sigma value for clipping during model fit"],
+ }
# curve of growth
curve_of_growth_pars = {"function": ["curve_of_growth", ],
@@ -93,6 +99,8 @@
"logx": [False, "log scale x-axis?"],
"logy": [False, "log scale y-axis?"],
"center": [True, "Recenter around the local max"],
+ "clip": [False, "Sigma clip by the mean before fitting?"],
+ "sigma": [3.0, "sigma value for clipping"],
}
# fit of column in image using model
@@ -104,11 +112,13 @@
"background": [False, "Solve for background? [bool]"],
"width": [10.0, "Background width in pixels"],
"order": [1, "Polynomial order to fit, 1=line"],
- "rplot": [20., "Plotting radius in pixels"],
+ "rplot": [15., "Plotting radius in pixels"],
"pointmode": [True, "plot points instead of lines? [bool]"],
"logx": [False, "log scale x-axis?"],
"logy": [False, "log scale y-axis?"],
"center": [True, "Recenter around the local max"],
+ "clip": [False, "Sigma clip by the mean before fitting?"],
+ "sigma": [3.0, "sigma value for clipping"],
}
# contour plots
@@ -121,7 +131,7 @@
"floor": [None, "Minimum value to be contoured"],
"ceiling": [None, "Maximum value to be contoured"],
"ncontours": [8, "Number of contours to be drawn"],
- "linestyle": ["--", "matplotlib linestyle"],
+ "linestyles": ["--", "matplotlib linestyle"],
"label": [True, "Label major contours with their values? [bool]"],
"cmap": ["viridis", "Colormap (matplotlib style) for image"],
}
diff --git a/imexam/imexamine.py b/imexam/imexamine.py
index 2ca1e3f6..80458465 100644
--- a/imexam/imexamine.py
+++ b/imexam/imexamine.py
@@ -19,6 +19,12 @@
from __future__ import print_function, division, absolute_import
+import matplotlib.pyplot as plt
+# turn on interactive mode for plotting
+# so that plt.show becomes non-blocking
+if not plt.isinteractive():
+ plt.ion()
+
import warnings
import numpy as np
import sys
@@ -26,15 +32,18 @@
import tempfile
from copy import deepcopy
-import matplotlib.pyplot as plt
+
from matplotlib import get_backend
-from IPython.display import Image
from astropy.io import fits
from astropy.modeling import models
+from astropy.visualization import ZScaleInterval
+
try:
from scipy import stats
+ scipy_installed = True
except ImportError:
+ scipy_installed = False
print("Scipy not installed, describe stat unavailable")
from . import math_helper
@@ -46,13 +55,10 @@
else:
PY3 = True
-# turn on interactive mode for plotting
-plt.ion()
-
# enable display plot in iPython notebook
try:
from io import StringIO
-except:
+except ImportError:
from cString import StringIO
try:
@@ -89,7 +95,11 @@ def __init__(self):
self._figure_name = "imexam"
self._plot_windows.append(self._figure_name)
self._reserved_keys = ['q', '2'] # not to be changed with user funcs
- self._fit_models = ["Gaussian1D", "Moffat1D", "MexicanHat1D"]
+ self._fit_models = ["Gaussian1D",
+ "Moffat1D",
+ "MexicanHat1D",
+ "AiryDisk2D",
+ "Polynomial1D"]
# see if the package logger was already started
self.log = logging.getLogger(__name__)
@@ -308,7 +318,7 @@ def plot_line(self, x, y, data=None, fig=None):
ax.set_ylabel(self.lineplot_pars["ylabel"][0])
if not self.lineplot_pars["xmax"][0]:
- xmax = len(data[y, :])
+ xmax = len(data[int(y), :])
else:
xmax = self.lineplot_pars["xmax"][0]
ax.set_xlim(self.lineplot_pars["xmin"][0], xmax)
@@ -362,7 +372,7 @@ def plot_column(self, x, y, data=None, fig=None):
ax.set_ylabel(self.colplot_pars["ylabel"][0])
if not self.colplot_pars["xmax"][0]:
- xmax = len(data[:, x])
+ xmax = len(data[:, int(x)])
else:
xmax = self.colplot_pars["xmax"][0]
ax.set_xlim(self.colplot_pars["xmin"][0], xmax)
@@ -425,7 +435,7 @@ def report_stat(self, x, y, data=None):
ymin = int(y - dist)
ymax = int(y + dist)
- if "describe" in name:
+ if (("describe" in name) and (scipy_installed)):
try:
stat = getattr(stats, "describe")
nobs, minmax, mean, var, skew, kurt = stat(data[ymin:ymax,
@@ -499,7 +509,7 @@ def save(self, filename=None, fig=None):
pstr = "plot saved to {0:s}".format(self.plot_name)
self.log.info(pstr)
- def aper_phot(self, x, y, data=None):
+ def aper_phot(self, x, y, data=None, fig=None):
"""Perform aperture photometry.
uses photutils functions, photutils must be available
@@ -512,11 +522,13 @@ def aper_phot(self, x, y, data=None):
The y location of the object
data: numpy array
The data array to work on
-
+ fig: figure object for redirect
+ Used for interaction with the ginga GUI
"""
if data is None:
data = self._data
+ center = False
if not photutils_installed:
self.log.warning("Install photutils to enable")
else:
@@ -526,6 +538,7 @@ def aper_phot(self, x, y, data=None):
amp, x, y, sigma, sigmay = self.gauss_center(x, y, data,
delta=delta)
+ # XXX TODO: Do I beleive that these all have to be ints?
radius = int(self.aper_phot_pars["radius"][0])
width = int(self.aper_phot_pars["width"][0])
inner = int(self.aper_phot_pars["skyrad"][0])
@@ -540,6 +553,7 @@ def aper_phot(self, x, y, data=None):
subpixels=1,
method="center")
+ sky_per_pix = None
if subsky:
annulus_apertures = photutils.CircularAnnulus(
(x, y), r_in=inner, r_out=outer)
@@ -570,26 +584,73 @@ def aper_phot(self, x, y, data=None):
magzero = float(self.aper_phot_pars["zmag"][0])
mag = magzero - 2.5 * (np.log10(total_flux))
- pheader = (
- "x\ty\tradius\tflux\tmag(zpt={0:0.2f})"
- "sky\t".format(magzero)).expandtabs(15)
+ # Construct the output strings (header and parameter values)
+ pheader = "x\ty\tradius\tflux\tmag(zpt={0:0.2f})\t".format(magzero)
+ pstr = "\n{0:.2f}\t{1:0.2f}\t{2:d}\t{3:0.2f}\t{4:0.2f}\t".format(x,
+ y,
+ radius,
+ total_flux,
+ mag)
+
+ if sky_per_pix is not None:
+ pheader += "sky/pix\t"
+ pstr += "{0:0.2f}\t".format(sky_per_pix)
if center:
- pheader += ("fwhm")
- pstr = "\n{0:.2f}\t{1:0.2f}\t{2:d}\t{3:0.2f}\t{4:0.2f}\
- \t{5:0.2f}\t{6:0.2f}".format(x, y, radius,
- total_flux, mag,
- sky_per_pix,
- math_helper.gfwhm(sigma)[0]).expandtabs(15)
- else:
- pstr = "\n{0:0.2f}\t{1:0.2f}\t{2:d}\t{3:0.2f}\
- \t{4:0.2f}\t{5:0.2f}".format(x, y, radius,
- total_flux, mag,
- sky_per_pix,).expandtabs(15)
+ pheader += "fwhm(pix)"
+ pstr += "{0:0.2f}".format(math_helper.gfwhm(sigma)[0])
+
+ pheader = pheader.expandtabs(15)
+ pstr = pstr.expandtabs(15)
+
+ # Save the total flux for unit testing things later
+ self.total_flux = total_flux
- # print(pheader + pstr)
self.log.info(pheader + pstr)
+ if self.aper_phot_pars["genplot"][0]:
+ pfig = fig
+ if fig is None:
+ # Make sure figure is square so round stars look round
+ fig = plt.figure(self._figure_name, figsize=[5, 5])
+ fig.clf()
+ fig.add_subplot(111)
+ ax = plt.gca()
+
+ if self.aper_phot_pars["title"][0] is None:
+ title = "x=%.2f, y=%.2f, flux=%.1f, \nmag=%.1f, sky=%.1f" %(x, y,
+ total_flux, mag,
+ sky_per_pix)
+ if center:
+ title += ", fwhm=%.2f" % math_helper.gfwhm(sigma)[0]
+ ax.set_title(title)
+ else:
+ ax.set_title(self.aper_phot_pars["title"][0])
+
+ if self.aper_phot_pars['scale'][0] == 'zscale':
+ zs = ZScaleInterval()
+ color_range = zs.get_limits(data)
+ else:
+ color_range = [self.aper_phot_pars['color_min'][0],
+ self.aper_phot_pars['color_max'][0]]
+
+ pad = outer * 1.2 # XXX TODO: Bad magic number
+ ax.imshow(data[int(y - pad):int(y + pad),
+ int(x - pad):int(x + pad)],
+ vmin=color_range[0], vmax=color_range[1],
+ extent=[int(x - pad), int(x + pad),
+ int(y - pad), int(y + pad)], origin='lower',
+ cmap=self.aper_phot_pars['cmap'][0])
+
+ apertures.plot(ax=ax, color='green', alpha=0.75, lw=3)
+ if subsky:
+ annulus_apertures.plot(ax=ax, color='red', alpha=0.75, lw=3)
- def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
+ if pfig is None and 'nbagg' not in get_backend().lower():
+ plt.draw()
+ plt.pause(0.001)
+ else:
+ fig.canvas.draw()
+
+ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None, col=False):
"""compute the 1D fit to the line of data using the specified form.
Parameters
@@ -602,11 +663,13 @@ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
The data array to work on
form: string
This is the functional form specified in the line fit parameters
- Currently Gaussian1D, Moffat1D, MexicanHat1D, Polynomial1D
+ see show_fit_models()
genplot: bool
produce the plot or return the fit
fig: figure for redirect
Used for interaction with the ginga GUI
+ col: bool (False)
+ Plot column instead of line
Notes
-----
@@ -615,29 +678,44 @@ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
If centering is True in the parameter set, then the center
is fit with a 2d gaussian, not performed for Polynomial1D
"""
+
+ # Set which parameters to use
+ if col:
+ pars = self.column_fit_pars
+ else:
+ pars = self.line_fit_pars
+
if data is None:
data = self._data
if form is None:
- fitform = getattr(models, self.line_fit_pars["func"][0])
+ fitform = getattr(models, pars["func"][0])
else:
- fitform = getattr(models, form)
+ if form in self._fit_models:
+ fitform = getattr(models, form)
+ else:
+ raise ValueError("Functional form not in available: {}"
+ .format(self._fit_models))
+
self.log.info("using model: {0}".format(fitform))
# Used for Polynomial1D fitting
- degree = int(self.line_fit_pars["order"][0])
+ degree = int(pars["order"][0])
- delta = int(self.line_fit_pars["rplot"][0])
- if delta >= len(data)/4: # help with small data arrays and defaults
- delta = delta/2
+ delta = int(pars["rplot"][0])
+ if delta >= len(data) / 4: # help with small data arrays and defaults
+ delta = delta / 2
delta = int(delta)
xx = int(x)
yy = int(y)
+
# fit the center with a 2d gaussian
- if self.line_fit_pars["center"][0]:
+ if pars["center"][0]:
if fitform.name is not "Polynomial1D":
- amp, xout, yout, sigma, sigmay = self.gauss_center(xx, yy, data,
+ amp, xout, yout, sigma, sigmay = self.gauss_center(xx,
+ yy,
+ data,
delta=delta)
if (xout < 0 or yout < 0 or xout > data.shape[1] or
yout > data.shape[0]):
@@ -646,43 +724,56 @@ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
else:
xx = int(xout)
yy = int(yout)
+ if col:
+ line = data[:, xx]
+ chunk = line[yy - delta:yy + delta]
+ delta_add = yy - delta
+ else:
+ line = data[yy, :]
+ chunk = line[xx - delta: xx + delta]
+ delta_add = xx - delta
- line = data[yy, :]
- chunk = line[xx - delta: xx + delta]
+ # This factor is passed to the fitter
+ if pars["clip"][0]:
+ sig_factor = pars["sigma"][0]
+ else:
+ sig_factor = 0
# fit model to data
if fitform.name is "Gaussian1D":
- fitted = math_helper.fit_gauss_1d(chunk)
- fitted.mean.value += (xx-delta)
+ xr = np.arange(len(chunk))
+ fitted = math_helper.fit_gauss_1d(xr, chunk, sigma_factor=sig_factor)
+ fitted.mean_0.value += delta_add
elif fitform.name is "Moffat1D":
- fitted = math_helper.fit_moffat_1d(chunk)
- fitted.x_0.value += (xx-delta)
+ fitted = math_helper.fit_moffat_1d(chunk, sigma_factor=sig_factor)
+ fitted.x_0_0.value += delta_add
elif fitform.name is "MexicanHat1D":
- fitted = math_helper.fit_mex_hat_1d(chunk)
- fitted.x_0.value += (xx-delta)
+ fitted = math_helper.fit_mex_hat_1d(chunk, sigma_factor=sig_factor)
+ fitted.x_0_0.value += delta_add
elif fitform.name is "Polynomial1D":
- fitted = math_helper.fit_poly_n(chunk, deg=degree)
+ fitted = math_helper.fit_poly_n(chunk, deg=degree, sigma_factor=sig_factor)
if fitted is None:
raise ValueError("Problem with the Poly1D fit")
elif fitform.name is "AiryDisk2D":
- fitted = math_helper.fit_airy_2d(chunk)
+ fitted = math_helper.fit_airy_2d(chunk, sigma_factor=sig_factor)
if fitted is None:
raise ValueError("Problem with the AiryDisk2D fit")
- fitted.x_0.value += (xx-delta)
- fitted.y_0.value += (yy-delta)
- else:
- self.log.info("{0:s} not implemented".format(fitform.name))
- return
+ fitted.x_0_0.value += (xx - delta)
+ fitted.y_0_0.value += (yy - delta)
- xline = np.arange(len(chunk)) + xx - delta
+ xline = np.arange(len(chunk)) + delta_add
fline = np.linspace(xline[0], xline[-1], 100) # finer sample
- yfit = fitted(fline)
+
+ if fitform.name is "AiryDisk2D":
+ yfit = fitted(fline, fline * 0 + fitted.y_0_0.value)
+ else:
+ yfit = fitted(fline)
# make a plot
- if self.line_fit_pars["title"][0] is None:
+ if pars["title"][0] is None:
title = "{0}: {1} {2}".format(self._datafile, int(x), int(y))
else:
- title = self.line_fit_pars["title"][0]
+ title = pars["title"][0]
if genplot:
pfig = fig
@@ -692,50 +783,55 @@ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
fig.add_subplot(111)
ax = fig.gca()
- ax.set_xlabel(self.line_fit_pars["xlabel"][0])
- ax.set_ylabel(self.line_fit_pars["ylabel"][0])
- if self.line_fit_pars["logx"][0]:
+ ax.set_xlabel(pars["xlabel"][0])
+ ax.set_ylabel(pars["ylabel"][0])
+ if pars["logx"][0]:
ax.set_xscale("log")
- if self.line_fit_pars["logy"][0]:
+ if pars["logy"][0]:
ax.set_yscale("log")
- if bool(self.line_fit_pars["pointmode"][0]):
+ if bool(pars["pointmode"][0]):
ax.plot(xline, chunk, 'o', label="data")
else:
ax.plot(xline, chunk, label="data", linestyle='-')
if fitform.name is "Gaussian1D":
- fwhmx, fwhmy = math_helper.gfwhm(fitted.stddev.value)
+ fwhmx, fwhmy = math_helper.gfwhm(fitted.stddev_0.value)
ax.set_title("{0:s} amp={1:8.2f} mean={2:9.2f},"
"fwhm={3:9.2f}".format(title,
- fitted.amplitude.value,
- fitted.mean.value,
+ fitted.amplitude_0.value,
+ fitted.mean_0.value,
fwhmx))
pstr = "({0:d},{1:d}) mean={2:9.2f}, fwhm={3:9.2f}".format(
- int(x), int(y), fitted.mean.value, fwhmx)
+ int(x), int(y), fitted.mean_0.value, fwhmx)
self.log.info(pstr)
elif fitform.name is "Moffat1D":
- mfwhm = math_helper.mfwhm(fitted.alpha.value,
- fitted.gamma.value)
+ mfwhm = math_helper.mfwhm(fitted.alpha_0.value,
+ fitted.gamma_0.value)
ax.set_title("{0:s} amp={1:8.2f} fwhm={2:9.2f}".format(
- title, fitted.amplitude.value, mfwhm))
+ title, fitted.amplitude_0.value, mfwhm))
pstr = "({0:d},{1:d}) amp={2:8.2f} fwhm={3:9.2f}".format(
- int(x), int(y), fitted.amplitude.value, mfwhm)
+ int(x), int(y), fitted.amplitude_0.value, mfwhm)
self.log.info(pstr)
elif fitform.name is "MexicanHat1D":
ax.set_title("{0:s} amp={1:8.2f} sigma={2:8.2f}".format(
- title, fitted.amplitude.value, fitted.sigma.value))
+ title, fitted.amplitude_0.value, fitted.sigma_0.value))
pstr = "({0:d},{1:d}) amp={2:8.2f} sigma={3:9.2f}".format(
- int(x), int(y), fitted.amplitude.value, fitted.sigma.value)
+ int(x), int(y), fitted.amplitude_0.value, fitted.sigma_0.value)
self.log.info(pstr)
elif fitform.name is "Polynomial1D":
ax.set_title("{0:s} degree={1:d}".format(title, degree))
- pstr = "({0:d},{1:d}) degree={2:d}".format(
- int(x), int(y), degree)
+ pstr = "({0:d},{1:d}) degree={2:d}".format(int(x), int(y), degree)
self.log.info(fitted.parameters)
self.log.info(pstr)
+ elif fitform.name is "AiryDisk2D":
+ ax.set_title("{0:s} amp={1:8.2f} radius={2:8.2f}".format(
+ title, fitted.amplitude_0.value, fitted.radius_0.value))
+ pstr = "({0:d},{1:d}) amp={2:8.2f} radius={3:9.2f}".format(
+ int(x), int(y), fitted.amplitude_0.value, fitted.radius_0.value)
+ self.log.info(pstr)
else:
- warnings.warn("Unsupported functional form used in line_fit")
+ warnings.warn("Unsupported functional form specified for fit")
raise ValueError
ax.plot(fline, yfit, c='r', label=str(fitform.__name__) + " fit")
ax.legend()
@@ -750,7 +846,7 @@ def line_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
return fitted
def column_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
- """Compute the 1d fit to the column of data.
+ """Compute the 1d fit to the column of data.
Parameters
----------
@@ -776,135 +872,13 @@ def column_fit(self, x, y, data=None, form=None, genplot=True, fig=None):
if centering is True, then the center is fit with a 2d gaussian,
but this is currently not done for Polynomial1D
"""
- if data is None:
- data = self._data
- if form is None:
- fitform = getattr(models, self.column_fit_pars["func"][0])
- else:
- fitform = getattr(models, form)
- self.log.info("using model: {0}".format(fitform))
- # Used for Polynomial1D fitting
- degree = int(self.column_fit_pars["order"][0])
- delta = int(self.column_fit_pars["rplot"][0])
- if delta >= len(data)/4:
- delta = int(delta/2)
-
- # fit the center with a 2d gaussian
- xx = int(x)
- yy = int(y)
- # fit the center with a 2d gaussian
- if self.column_fit_pars["center"][0]:
- if fitform.name is not "Polynomial1D":
- amp, xout, yout, sigma, sigmay = self.gauss_center(x, y, data,
- delta=delta)
- if (xout < 0 or yout < 0 or xout > data.shape[1] or
- yout > data.shape[0]):
- self.log.info("Problem with centering, "
- "using pixel coords")
- else:
- xx = int(xout)
- yy = int(yout)
-
- line = data[:, xx]
- chunk = line[yy - delta:yy + delta]
-
- # fit model to data
- if fitform.name is "Gaussian1D":
- fitted = math_helper.fit_gauss_1d(chunk)
- fitted.mean.value += (yy-delta)
- elif fitform.name is "Moffat1D":
- fitted = math_helper.fit_moffat_1d(chunk)
- fitted.x_0.value += (yy-delta)
- elif fitform.name is "MexicanHat1D":
- fitted = math_helper.fit_mex_hat_1d(chunk)
- fitted.x_0.value += (yy-delta)
- elif fitform.name is "Polynomial1D":
- fitted = math_helper.fit_poly_n(chunk, deg=degree)
- if fitted is None:
- raise ValueError("Problem with the Poly1D fit")
- else:
- self.log.info("{0:s} not implemented".format(fitform.name))
- return
-
- yline = np.arange(len(chunk)) + yy - delta
- fline = np.linspace(yline[0], yline[-1], 100) # finer sample
- yfit = fitted(fline)
-
- if self.column_fit_pars["title"][0] is None:
- title = "{0}: {1} {2}".format(self._datafile, int(x), int(y))
- else:
- title = self.column_fit_pars["title"][0]
-
- # make a plot
- if genplot:
- pfig = fig
- if fig is None:
- fig = plt.figure(self._figure_name)
- fig.clf()
- fig.add_subplot(111)
- ax = fig.gca()
-
- ax.set_xlabel(self.column_fit_pars["xlabel"][0])
- ax.set_ylabel(self.column_fit_pars["ylabel"][0])
- if self.column_fit_pars["logx"][0]:
- ax.set_xscale("log")
- if self.column_fit_pars["logy"][0]:
- ax.set_yscale("log")
-
- if bool(self.column_fit_pars["pointmode"][0]):
- ax.plot(yline, chunk, 'o', label="data")
- else:
- ax.plot(yline, chunk, linestyle='-', label="data")
-
- if fitform.name == "Gaussian1D":
- fwhmx, fwhmy = math_helper.gfwhm(fitted.stddev.value)
- ax.set_title("{0:s} amp={1:8.2f} mean={2:9.2f}, fwhm={3:9.2f}".format(
- title, fitted.amplitude.value, fitted.mean.value, fwhmy))
- pstr = "({0:d},{1:d}) mean={2:0.3f}, fwhm={3:0.2f}".format(
- int(x), int(y), fitted.mean.value, fwhmy)
- self.log.info(pstr)
- self.log.info(fitted.parameters)
-
- elif fitform.name is "Moffat1D":
- mfwhm = math_helper.mfwhm(fitted.alpha.value,
- fitted.gamma.value)
- ax.set_title("{0:s} amp={1:8.2f} fwhm={2:9.2f}".format(
- title, fitted.amplitude.value, mfwhm))
- pstr = "({0:d},{1:d}) amp={2:8.2f} fwhm={3:9.2f}".format(
- int(x), int(y), fitted.amplitude.value, mfwhm)
- self.log.info(pstr)
- self.log.info(fitted.parameters)
+ result = self.line_fit(x, y, data=data, form=form,
+ genplot=genplot, fig=fig, col=True)
+ if not genplot:
+ return result
- elif fitform.name is "MexicanHat1D":
- ax.set_title("{0:s} amp={1:8.2f} sigma={2:8.2f}".format(
- title, fitted.amplitude.value, fitted.sigma.value))
- pstr = "({0:d},{1:d}) amp={2:8.2f} sigma={3:9.2f}".format(
- int(x), int(y), fitted.amplitude.value, fitted.sigma.value)
- self.log.info(pstr)
- elif fitform.name is "Polynomial1D":
- ax.set_title("{0:s} degree={1:d}".format(title, degree))
- pstr = "({0:d},{1:d}) degree={2:d}".format(
- int(x), int(y), degree)
- self.log.info(pstr)
- self.log.info(fitted.parameters)
- else:
- warnings.warn("Unsupported functional form used in column_fit")
- raise ValueError
-
- ax.plot(fline, yfit, c='r', label=str(fitform.__name__) + " fit")
- ax.legend()
-
- if pfig is None and 'nbagg' not in get_backend().lower():
- plt.draw()
- plt.pause(0.001)
- else:
- fig.canvas.draw()
-
- else:
- return fitted
-
- def gauss_center(self, x, y, data=None, delta=10):
+ def gauss_center(self, x, y, data=None, delta=10, sigma_factor=0):
"""Return the 2d gaussian fit center of the data.
Parameters
@@ -918,27 +892,30 @@ def gauss_center(self, x, y, data=None, delta=10):
delta: int
The range of data values (bounding box) to use around the x,y
location for calculating the center
+ sigma_factor: float, optional
+ The sigma clipping factor to use on the data fit
"""
if data is None:
data = self._data
# reset delta for small arrays
- if delta >= len(data)/4:
- delta = delta/2
+ if delta >= len(data) / 4:
+ delta = delta / 2
delta = int(delta)
- xx=int(x)
- yy=int(y)
+ xx = int(x)
+ yy = int(y)
+
# flipped from xpa
chunk = data[yy - delta:yy + delta, xx - delta:xx + delta]
try:
- fit = math_helper.gauss_center(chunk)
- amp = fit.amplitude.value
- xcenter = fit.x_mean.value
- ycenter = fit.y_mean.value
- xsigma = fit.x_stddev.value
- ysigma = fit.y_stddev.value
+ fit = math_helper.fit_gaussian_2d(chunk, sigma_factor=sigma_factor)
+ amp = fit.amplitude_0.value
+ xcenter = fit.x_mean_0.value
+ ycenter = fit.y_mean_0.value
+ xsigma = fit.x_stddev_0.value
+ ysigma = fit.y_stddev_0.value
pstr = "xc={0:4f}\tyc={1:4f}".format(
(xcenter + xx - delta),
@@ -961,7 +938,7 @@ def radial_profile(self, x, y, data=None, form=None,
From the parameters Dictionary:
If pixel is True, then every pixel at each radius is plotted.
- If pixel is False, then the sum of all pixels at each radius is plotted.
+ If pixel is False, then the sum of all pixels in integer bins is plotted
Background may be subtracted and centering can be done with a
2D Gaussian fit. These options are read from the plot parameters dict.
@@ -977,125 +954,231 @@ def radial_profile(self, x, y, data=None, form=None,
form: string
The string name of the form of the fit to use
genplot: bool
- Generate the plot if True, else return the fit data
+ Generate the plot if True, else retfurn the fit data
"""
- subtract_background = bool(self.radial_profile_pars["background"][0])
+ pars = self.radial_profile_pars
+
+ subtract_background = bool(pars["background"][0])
if not photutils_installed and subtract_background:
self.log.warning("Install photutils to enable "
"background subtraction")
subtract_background = False
+
+ if data is None:
+ data = self._data
+
+ getdata = bool(pars["getdata"][0])
+ center = pars["center"][0]
+
+ # be careful with the clipping since most
+ # of the data will be near the low value
+ clip_on = pars["clip"][0]
+ if clip_on:
+ sig_factor = pars["sigma"][0]
else:
+ sig_factor = 0
- if data is None:
- data = self._data
+ fitplot = bool(pars["fitplot"][0])
+ if fitplot:
if form is None:
- form = getattr(models,
- self.radial_profile_pars["fittype"][0])
+ fitform = getattr(models, pars["func"][0])
+ else:
+ if form not in self._fit_models:
+ msg = "{0:s} not supported for fitting".format(form)
+ self.log.info(msg)
+ raise ValueError(msg)
+ else:
+ fitform = getattr(models, form)
+
+ # cut the data down to size and center cutout
+ datasize = int(pars["rplot"][0])
+ if datasize < 3:
+ self.log.info("Insufficient pixels, resetting chunk size to 3.")
+ datasize = 3
+
+ if center:
+ # reset delta for small arrays
+ # make it odd if it's even
+ if ((datasize % 2) == 0):
+ datasize = datasize + 1
+ xx = int(x)
+ yy = int(y)
+ # flipped from xpa
+ data_chunk = data[yy - datasize:yy + datasize,
+ xx - datasize:xx + datasize]
+ amp, centerx, centery, sigmax, sigmay = self.gauss_center(xx, yy, data, delta=datasize)
- getdata = bool(self.radial_profile_pars["getdata"][0])
+ else:
+ centery = y
+ centerx = x
+
+ icenterx = int(centerx)
+ icentery = int(centery)
+
+ # fractional center, help with precision errors to 1000th pixel
+ xfrac = round(centerx - icenterx, 2)
+ yfrac = round(centery - icentery, 2)
+
+ # just grab the data box centered on the object
+ data_chunk = data[icentery - datasize:icentery + datasize,
+ icenterx - datasize:icenterx + datasize]
+
+ y, x = np.indices(data_chunk.shape) # index of all pixels
+ y = y - datasize
+ x = x - datasize
+ r = np.sqrt((x-xfrac)**2 + (y-yfrac)**2)
+
+ indices = np.argsort(r.flat) # sorted indices
+
+ if pars["pixels"][0]:
+ flux = data_chunk.ravel()[indices]
+ radius = r.ravel()[indices]
+
+ else: # sum the flux in integer bins
+ radius = r.ravel()[indices].astype(np.int)
+ flux = np.bincount(radius, data_chunk.ravel()[indices])
+ radbc = np.bincount(radius)
+ flux = flux / radbc
+ radius = np.arange(len(flux))
+
+ # Get a background measurement
+ if subtract_background:
+ inner = pars["skyrad"][0]
+ width = pars["width"][0]
+ annulus_apertures = photutils.CircularAnnulus((centerx, centery),
+ r_in=inner,
+ r_out=inner+width)
+ bkgflux_table = photutils.aperture_photometry(data,
+ annulus_apertures)
- # cut the data down to size
- datasize = int(self.radial_profile_pars["rplot"][0])-1
- delta = 10 # chunk size in pixels to find center
+ # to calculate the mean local background, divide the circular
+ # annulus aperture sums by the area of the circular annulus.
+ # The bkg sum with the circular aperture is then
+ # the mean local background times the circular apreture area.
+ annulus_area = annulus_apertures.area()
+ sky_per_pix = float(bkgflux_table['aperture_sum'] /
+ annulus_area)
- # center on image using a 2d gaussian
- if self.radial_profile_pars["center"][0]:
- # pull out a small chunk to get the center defined
- amp, centerx, centery, sigma, sigmay = \
- self.gauss_center(x, y, data, delta=delta)
- else:
- centery = y
- centerx = x
- icenterx = int(centerx)
- icentery = int(centery)
+ # don't add flux
+ if sky_per_pix < 0:
+ sky_per_pix = 0
+ self.log.info("Sky background negative, setting to zero")
+ self.log.info("Background per pixel: {0:f}".format(sky_per_pix))
- # just grab the data box we want from the image
- data_chunk = data[icentery-datasize:icentery+datasize,
- icenterx-datasize:icenterx+datasize]
+ flux -= sky_per_pix
- y, x = np.indices((data_chunk.shape)) # radii of all pixels
+ if getdata:
+ self.log.info("Sky per pixel: {0} using "
+ "(rad={1}->{2})".format(sky_per_pix,
+ inner, inner + width))
+ if getdata:
+ info = "\nat (x,y)={0:f},{1:f}\n".format(centerx, centery)
+ self.log.info(info)
+ self.log.info(radius, flux)
+
+ # Fit the functional form to the radial profile flux
+ # TODO: Ignore sky subtracted pixels that push flux
+ # below zero?
+ if fitplot:
+ fline = np.linspace(0, datasize, 100) # finer sample
+ # fit model to data
+ if fitform.name is "Gaussian1D":
+ fitted = math_helper.fit_gauss_1d(radius, flux,
+ sigma_factor=sig_factor,
+ center_at=0,
+ weighted=True)
+
+ fwhmx, fwhmy = math_helper.gfwhm(fitted.stddev_0.value)
+ legend = ("Max. pix. flux = {0:9.3f}\n"
+ "amp = {1:9.3f}\n"
+ "fwhm = {2:9.3f}".format(np.max(flux),
+ fitted.amplitude_0.value,
+ fwhmx))
+ self.log.info(legend)
+ legendx = datasize / 2
+ legendy = np.max(flux) / 2
- if self.radial_profile_pars["pixels"][0]:
- r = np.sqrt((x - datasize+(centerx-icenterx))**2 +
- (y - datasize + (centery-icentery))**2)
- indices = np.argsort(r.flat) # sorted indices
- radius = r.flat[indices]
- flux = data_chunk.flat[indices]
+ elif fitform.name is "Moffat1D":
+ fitted = math_helper.fit_moffat_1d(flux,
+ sigma_factor=sig_factor,
+ center_at=0,
+ weighted=True)
+ mfwhm = math_helper.mfwhm(fitted.alpha_0.value,
+ fitted.gamma_0.value)
+ legend = ("Max. pix. flux = {0:9.3f}\n"
+ "amp = {1:9.3f}\n"
+ "fwhm = {2:9.3f}".format(np.max(flux),
+ fitted.amplitude_0.value,
+ mfwhm))
+ legendx = datasize / 2
+ legendy = np.max(flux) / 2
- else: # sum the flux in integer bins
- r = np.sqrt((x - datasize)**2 + (y - datasize)**2)
- r = r.astype(np.int)
- flux = np.bincount(r.ravel(), data_chunk.ravel())
- radius = np.arange(len(flux))
+ elif fitform.name is "MexicanHat1D":
+ fitted = math_helper.fit_mex_hat_1d(flux,
+ sigma_factor=sig_factor,
+ center_at=0,
+ weighted=True)
+ legend = ("Max. pix. flux = {0:9.3f}\n".format(np.max(flux)))
+ legendx = datasize / 2
+ legendy = np.max(flux) / 2
- # Get a background measurement
- if subtract_background:
- inner = self.radial_profile_pars["skyrad"][0]
- width = self.radial_profile_pars["width"][0]
- annulus_apertures = photutils.CircularAnnulus(
- (centerx, centery), r_in=inner, r_out=inner+width)
- bkgflux_table = photutils.aperture_photometry(data,
- annulus_apertures)
+ if fitted is None:
+ msg = "Problem with the {0:s} fit".format(fitform.name)
+ self.log.info(msg)
+ raise ValueError(msg)
- # to calculate the mean local background, divide the circular
- # annulus aperture sums by the area of the circular annulus.
- # The bkg sum with the circular aperture is then
- # then mean local background times the circular apreture area.
- annulus_area = annulus_apertures.area()
- sky_per_pix = float(bkgflux_table['aperture_sum'] /
- annulus_area)
- self.log.info("Background per pixel: {0:f}".format(sky_per_pix))
- if self.radial_profile_pars["pixels"][0]:
- flux -= sky_per_pix
- else:
- flux -= np.bincount(r.ravel()) * sky_per_pix
- if getdata:
- self.log.info("Sky per pixel: {0} using "
- "(rad={1}->{2})".format(sky_per_pix,
- inner, inner+width))
- if getdata:
- info = "\nat (x,y)={0:f},{1:f}\n".format(centerx, centery)
- self.log.info(info)
- self.log.info(radius, flux)
+ yfit = fitted(fline)
- # finish the plot
- if genplot:
- pfig = fig
- if fig is None:
- fig = plt.figure(self._figure_name)
- fig.clf()
- fig.add_subplot(111)
- ax = fig.gca()
+ # finish the plot
+ # TODO: users may get error if they use this without a display
+ # and request data back but forget to set genplot=False
+ if genplot:
+ pfig = fig
+ if fig is None:
+ fig = plt.figure(self._figure_name)
+ fig.clf()
+ fig.add_subplot(111)
+ ax = fig.gca()
- if self.radial_profile_pars["title"][0] is None:
- title = "{0}: {1} {2}".format(self._datafile,
- icenterx, icentery)
- else:
- title = self.radial_profile_pars["title"][0]
+ if subtract_background:
+ ytitle = ("Flux ( sky/pix = {0:8.2f} )".format(sky_per_pix))
+ else:
+ ytitle = pars["ylabel"][0]
+ ax.set_xlabel(pars["xlabel"][0])
+ ax.set_ylabel(ytitle)
- ax.set_xlabel(self.radial_profile_pars["xlabel"][0])
- ax.set_ylabel(self.radial_profile_pars["ylabel"][0])
+ if bool(pars["pointmode"][0]):
+ ax.plot(radius, flux, pars["marker"][0])
+ else:
+ ax.plot(radius, flux)
+ ax.set_ylim(0,)
- if bool(self.radial_profile_pars["pointmode"][0]):
- ax.plot(radius, flux, self.radial_profile_pars["marker"][0])
+ if pars["title"][0] is None:
+ if fitplot:
+ title = ("Radial Profile at ({0:d},{1:d}) with {2:s}"
+ .format(icenterx, icentery, fitform.name))
else:
- ax.plot(radius, flux)
- ax.set_title(title)
- ax.set_ylim(0,)
+ title = "Radial Profile for {0} {1}".format(icenterx,
+ icentery)
+ else:
+ title = pars["title"][0]
- # over plot a gaussian fit to the data
- if bool(self.radial_profile_pars["fitplot"][0]):
- self.log.info("Fit overlay not yet implemented")
+ if fitplot:
+ ax.plot(fline, yfit, linestyle='-', c='r', label=fitform.name)
+ ax.set_xlim(0, datasize, 0.5)
+ ax.text(legendx, legendy, legend)
- if pfig is None and 'nbagg' not in get_backend().lower():
- plt.draw()
- plt.pause(0.001)
- else:
- fig.canvas.draw()
+ ax.set_title(title)
+
+ if pfig is None and 'nbagg' not in get_backend().lower():
+ plt.draw()
+ plt.pause(0.001)
else:
- return radius, flux
+ fig.canvas.draw()
+ else:
+ return radius, flux
def curve_of_growth(self, x, y, data=None, genplot=True, fig=None):
"""Display a curve of growth plot.
@@ -1341,9 +1424,12 @@ def histogram(self, x, y, data=None, genplot=True, fig=None):
ax.set_xscale("log")
if self.histogram_pars["logy"][0]:
ax.set_yscale("log")
- n, bins, patches = ax.hist(
- flat_data, num_bins, range=[mini, maxi], normed=False,
- facecolor='green', alpha=0.5, histtype='bar')
+ n, bins, patches = ax.hist(flat_data, num_bins,
+ range=[mini, maxi],
+ normed=False,
+ facecolor='green',
+ alpha=0.5,
+ histtype='bar')
self.log.info("{0} bins "
"range:[{1},{2}]".format(num_bins, mini, maxi))
@@ -1393,7 +1479,7 @@ def contour(self, x, y, data=None, fig=None):
ax.set_ylabel(self.contour_pars["ylabel"][0])
ncont = self.contour_pars["ncontours"][0]
colormap = self.contour_pars["cmap"][0]
- lsty = self.contour_pars["linestyle"][0]
+ lsty = self.contour_pars["linestyles"][0]
self.log.info("contour centered at: {0} {1}".format(x, y))
deltax = int(self.contour_pars["ncolumns"][0] / 2.)
@@ -1412,9 +1498,9 @@ def contour(self, x, y, data=None, fig=None):
Y,
data_cut,
ncont,
- linewidth=.5,
+ linewidths=.5,
colors='black',
- linestyle=lsty)
+ linestyles=lsty)
# make the filled contour
ax.contourf(X, Y, data_cut, ncont, alpha=.75, cmap=colormap)
if self.contour_pars["label"][0]:
@@ -1554,14 +1640,16 @@ def cutout(self, x, y, data=None, size=None, fig=None):
if size is None:
size = self.cutout_pars["size"][0]
- prefix = "cutout_{0}_{1}_".format(int(x), int(y))
+ xx = int(x)
+ yy = int(y)
+ prefix = "cutout_{0}_{1}_".format(xx, yy)
fname = tempfile.mkstemp(prefix=prefix, suffix=".fits", dir="./")[-1]
- cutout = data[y-size:y+size, x-size:x+size]
+ cutout = data[yy - size:yy + size, xx - size:xx + size]
hdu = fits.PrimaryHDU(cutout)
hdulist = fits.HDUList([hdu])
hdulist[0].header['EXTEND'] = False
hdulist.writeto(fname)
- self.log.info("Cutout at ({0},{1}) saved to {2:s}".format(x, y, fname))
+ self.log.info("Cutout at ({0},{1}) saved to {2:s}".format(xx, yy, fname))
def register(self, user_funcs):
"""register a new imexamine function made by the user as an option.
@@ -1607,15 +1695,6 @@ def _add_user_function(cls, func):
return setattr(cls, func.__name__,
types.MethodType(func, None, cls))
- def showplt(self):
- """Show the plot."""
- buf = StringIO.StringIO()
- plt.savefig(buf, bbox_inches=0)
- img = Image(data=bytes(buf.getvalue()),
- format='png', embed=True)
- buf.close()
- return img
-
def set_aper_phot_pars(self, user_dict=None):
"""the user may supply a dictionary of par settings."""
if not user_dict:
diff --git a/imexam/math_helper.py b/imexam/math_helper.py
index 5757a4e5..1b9ee0ec 100644
--- a/imexam/math_helper.py
+++ b/imexam/math_helper.py
@@ -6,13 +6,13 @@
import numpy as np
import warnings
-from astropy.modeling import models, fitting
-
+from astropy.modeling import models, fitting
+from astropy.stats import gaussian_sigma_to_fwhm, sigma_clip
def gfwhm(sigmax, sigmay=None):
- """Compute the gaussian full width half max.
+ """Compute the Gaussian full width half max.
Parameters
----------
@@ -25,19 +25,20 @@ def gfwhm(sigmax, sigmay=None):
Returns
-------
- The FWHM tuple for the gaussian
+ The FWHM tuple for the Gaussian
"""
if sigmax is None:
- print("Need at least one sigma value")
+ print("Need at least one sigma value for Gaussian FWHM")
return (None, None)
- g = lambda x: x * np.sqrt(8.0 * np.log(2.))
-
+ fwhmx = gaussian_sigma_to_fwhm * sigmax
if sigmay is None:
- return (g(sigmax), g(sigmax)) # assume circular where sigmax = sigmay
+ fwhmy = fwhmx
else:
- return (g(sigmax), g(sigmay))
+ fwhmy = gaussian_sigma_to_fwhm * sigmay
+
+ return (fwhmx, fwhmy)
def mfwhm(alpha=0, gamma=0):
@@ -58,132 +59,357 @@ def mfwhm(alpha=0, gamma=0):
fwhm = 2* alpha * sqrt(2^(1/gamma) -1 ))
"""
if alpha == 0 or gamma == 0:
- print("Need alpha AND gamma values")
+ print("Need alpha AND gamma values for moffat FWHM")
return None
- g = lambda x, y: 2 * x * np.sqrt(2 ** (1/y) - 1)
+ return 2 * alpha * np.sqrt(2 ** (1 / gamma) - 1)
- return g(alpha, gamma)
+def fit_moffat_1d(data, gamma=2., alpha=1., sigma_factor=0.,
+ center_at=None, weighted=False):
+ """Fit a 1D moffat profile to the data and return the fit.
-def fit_moffat_1d(data, gamma=2., alpha=1.):
- """Fit a 1D moffat profile to the data and return the fit."""
+ Parameters
+ ----------
+ data: 2D data array
+ The input sigma to use
+ gamma: float (optional)
+ The input gamma to use
+ alpha: float (optional)
+ The input alpha to use
+ sigma_factor: float (optional)
+ If sigma>0 then sigma clipping of the data is performed
+ at that level
+ center_at: float or None
+ None by default, set to value to use as center
+ weighted: bool
+ if weighted is True, then weight the values by basic
+ uncertainty hueristic
+
+ Returns
+ -------
+ The fitted 1D moffat model for the data
+
+ """
# data is assumed to already be chunked to a reasonable size
ldata = len(data)
+
+ # guesstimate mean
+ if center_at:
+ x0 = 0
+ else:
+ x0 = int(ldata / 2.)
+
+ # assumes negligable background
+ if weighted:
+ z = np.nan_to_num(1. / np.sqrt(data)) # use as weight
+
x = np.arange(ldata)
- # Fit model to data
- fit = fitting.LevMarLSQFitter()
+ # Initialize the fitter
+ fitter = fitting.LevMarLSQFitter()
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ niter=3,
+ sigma=sigma_factor)
+ else:
+ fit = fitter
+
+ # Moffat1D + constant
+ model = (models.Moffat1D(amplitude=max(data),
+ x_0=x0,
+ gamma=gamma,
+ alpha=alpha) +
+ models.Polynomial1D(c0=data.min(), degree=0))
- # Moffat1D
- model = models.Moffat1D(amplitude=max(data), x_0=ldata/2, gamma=gamma, alpha=alpha)
with warnings.catch_warnings():
# Ignore model linearity warning from the fitter
warnings.simplefilter('ignore')
- results = fit(model, x, data)
+ if weighted:
+ results = fit(model, x, data, weights=z)
+ else:
+ results = fit(model, x, data)
# previous yield amp, ycenter, xcenter, sigma, offset
- return results
+ # if sigma clipping is used, results is a tuple of data, model
+ if sigma_factor > 0:
+ return results[1]
+ else:
+ return results
-def fit_gauss_1d(data):
- """Fit a 1D gaussian to the data and return the fit."""
- # data is assumed to already be chunked to a reasonable size
- ldata = len(data)
- x = np.arange(ldata)
+def fit_gauss_1d(radius, flux, sigma_factor=0, center_at=None, weighted=False):
+ """Fit a 1D gaussian to the data and return the fit.
- # Fit model to data
- fit = fitting.LevMarLSQFitter()
+ Parameters
+ ----------
+ radius: array of float
+ set center_at and the center will be taken there
+ flux: array of float
+ values should correspond to radius array
+ sigma_factor: float (optional)
+ If sigma>0 then sigma clipping of the data is performed
+ at that level
+ center_at: float or None
+ None by default, set to value to use as center
+ If the value is None, center will be set at half the
+ array size.
+ weighted: bool
+ if weighted is True, then weight the values by basic
+ uncertainty hueristic
+
+ Returns
+ -------
+ The fitted 1D gaussian model for the data.
+ """
+
+ if radius.shape != flux.shape:
+ raise ValueError("Expected same sizes for radius and flux")
+
+ # guesstimate the mean
+ # assumes ordered radius
+ if center_at is None:
+ delta = int(len(radius) / 2.)
+ else:
+ delta = center_at
+
+ # assumes negligable background
+ if weighted:
+ z = np.nan_to_num(np.log(flux))
+
+ # Initialize the fitter
+ fitter = fitting.LevMarLSQFitter()
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ niter=3,
+ sigma=sigma_factor)
+ else:
+ fit = fitter
+
+ # Gaussian1D + a constant
+ model = (models.Gaussian1D(amplitude=flux.max() - flux.min(),
+ mean=delta, stddev=1.) +
+ models.Polynomial1D(c0=flux.min(), degree=0))
- # Gaussian1D
- model = models.Gaussian1D(amplitude=1, mean=0, stddev=1.)
with warnings.catch_warnings():
# Ignore model linearity warning from the fitter
warnings.simplefilter('ignore')
- results = fit(model, x, data)
+ if weighted:
+ results = fit(model, radius, flux, weights=z)
+ else:
+ results = fit(model, radius, flux)
# previous yield amp, ycenter, xcenter, sigma, offset
- return results
+ # if sigma clipping is used, results is a tuple of data, model
+ if sigma_factor > 0:
+ return results[1]
+ else:
+ return results
-def gauss_center(data, sigma=3., theta=0.):
+def fit_gaussian_2d(data, sigma=3., theta=0., sigma_factor=0):
"""center the data by fitting a 2d gaussian to the region.
Parameters
----------
- data: float
+ data: 2D float array
should be a 2d array, the initial center is used to estimate
the fit center
+ sigma: float (optional)
+ The sigma value for the starting gaussian model
+ theta: float(optional)
+ The theta value for the starting gaussian model
+ sigma_factor: float (optional)
+ If sigma_factor > 0 then clipping will be performed
+ on the data during the model fit
+
+ Returns
+ -------
+ The full gaussian fit model, from which the center can be extracted
+
"""
# use a smaller bounding box so that we are only fitting the local data
delta = int(len(data) / 2) # guess the center
+ amp = data.max() - data.min() # guess the amplitude
ldata = len(data)
yy, xx = np.mgrid[:ldata, :ldata]
- # Fit model to data
- fit = fitting.LevMarLSQFitter()
+ # Initialize the fitter
+ fitter = fitting.LevMarLSQFitter()
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ niter=3,
+ sigma=sigma_factor)
+ else:
+ fit = fitter
- # Gaussian2D(amp,xmean,ymean,xstd,ystd,theta)
- model = models.Gaussian2D(1, delta, delta, sigma, sigma, theta)
+ # Gaussian2D(amp,xmean,ymean,xstd,ystd,theta) + a constant
+ model = (models.Gaussian2D(amp, delta, delta, sigma, sigma, theta) +
+ models.Polynomial2D(c0_0=data.min(), degree=0))
with warnings.catch_warnings():
# Ignore model linearity warning from the fitter
warnings.simplefilter('ignore')
results = fit(model, xx, yy, data)
# previous yield amp, ycenter, xcenter, sigma, offset
- return results
+ # if sigma clipping is used, results is a tuple of data, model
+ if sigma_factor > 0:
+ return results[1]
+ else:
+ return results
+
+def fit_mex_hat_1d(data, sigma_factor=0, center_at=None, weighted=False):
+ """Fit a 1D Mexican Hat function to the data.
-def fit_mex_hat_1d(data):
- """Fit a 1D Mexican Hat function to the data."""
+ Parameters
+ ----------
+
+ data: 2D float array
+ should be a 2d array, the initial center is used to estimate
+ the fit center
+ sigma_factor: float (optional)
+ If sigma_factor > 0 then clipping will be performed
+ on the data during the model fit
+ center_at: float or None
+ None by default, set to value to use as center
+ weighted: bool
+ if weighted is True, then weight the values by basic
+ uncertainty hueristic
+
+ Returns
+ -------
+ The the fit model for mexican hat 1D function
+ """
ldata = len(data)
+ if center_at:
+ x0 = 0
+ else:
+ x0 = int(ldata / 2.)
+
+ # assumes negligable background
+ if weighted:
+ z = np.nan_to_num(1. / np.sqrt(data)) # use as weight
+
x = np.arange(ldata)
fixed_pars = {"x_0": True}
- # Fit model to data
- fit = fitting.LevMarLSQFitter()
+ # Initialize the fitter
+ fitter = fitting.LevMarLSQFitter()
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ niter=3,
+ sigma=sigma_factor)
+ else:
+ fit = fitter
+
+ # Mexican Hat 1D + constant
+ model = (models.MexicanHat1D(amplitude=np.max(data),
+ x_0=x0,
+ sigma=2.,
+ fixed=fixed_pars) +
+ models.Polynomial1D(c0=data.min(), degree=0))
- # Mexican Hat 1D
- model = models.MexicanHat1D(amplitude=np.max(data),
- x_0=ldata/2, sigma=2., fixed=fixed_pars)
with warnings.catch_warnings():
# Ignore model linearity warning from the fitter
warnings.simplefilter('ignore')
- results = fit(model, x, data)
+ if weighted:
+ results = fit(model, x, data, weights=z)
+ else:
+ results = fit(model, x, data)
# previous yield amp, ycenter, xcenter, sigma, offset
- return results
+ if sigma_factor > 0:
+ return results[1]
+ else:
+ return results
+
+
+def fit_airy_2d(data, x=None, y=None, sigma_factor=0):
+ """Fit an AiryDisk2D model to the data.
+
+ Parameters
+ ----------
+
+ data: 2D float array
+ should be a 2d array, the initial center is used to estimate
+ the fit center
+ x: float (optional)
+ xcenter location
+ y: float (optional)
+ ycenter location
+ sigma_factor: float (optional)
+ If sigma_factor > 0 then clipping will be performed
+ on the data during the model fit
+ Returns
+ -------
+ The the fit model for Airy2D function
-def fit_airy_2d(data, x=None, y=None):
- """Fit an AiryDisk2D model to the data."""
+ """
delta = int(len(data) / 2) # guess the center
ldata = len(data)
- if not x:
+ if x is None:
x = delta
- if not y:
+ if y is None:
y = delta
fixed_pars = {"x_0": True, "y_0": True} # hold these constant
yy, xx = np.mgrid[:ldata, :ldata]
- # fit model to the data
- fit = fitting.LevMarLSQFitter()
-
- # AiryDisk2D(amplitude, x_0, y_0, radius)
- model = models.AiryDisk2D(np.max(data), x_0=x, y_0=y, radius=delta,
- fixed=fixed_pars)
+ # Initialize the fitter
+ fitter = fitting.LevMarLSQFitter()
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ niter=3,
+ sigma=sigma_factor)
+ else:
+ fit = fitter
+
+ # AiryDisk2D(amplitude, x_0, y_0, radius) + constant
+ model = (models.AiryDisk2D(np.max(data),
+ x_0=x,
+ y_0=y,
+ radius=delta,
+ fixed=fixed_pars) +
+ models.Polynomial2D(c0_0=data.min(), degree=0))
with warnings.catch_warnings():
# Ignore model warnings for new_plot_window
warnings.simplefilter('ignore')
results = fit(model, xx, yy, data)
- return results
+ if sigma_factor > 0:
+ return results[1]
+ else:
+ return results
+
+def fit_poly_n(data, deg=1, sigma_factor=0):
+ """Fit a Polynomial 1D model to the data.
-def fit_poly_n(data, x=None, y=None, deg=1):
- """Fit a Polynomial 1D model to the data."""
+ Parameters
+ ----------
+
+ data: float array
+ should be a 1d or 2d array
+ deg: int
+ The degree of polynomial to fit
+ sigma_factor: float (optional)
+ If sigma_factor > 0 then clipping will be performed
+ on the data during the model fit
+
+ Returns
+ -------
+ The the polynomial fit model for the function
+ """
+ if len(data) < deg + 1:
+ raise ValueError("fit_poly_n: Need more data for fit")
# define the model
poly = models.Polynomial1D(deg)
@@ -192,10 +418,22 @@ def fit_poly_n(data, x=None, y=None, deg=1):
ax = np.arange(len(data))
# define the fitter
- fit = fitting.LinearLSQFitter()
+ fitter = fitting.LinearLSQFitter()
+
+ if sigma_factor > 0:
+ fit = fitting.FittingWithOutlierRemoval(fitter,
+ sigma_clip,
+ sigma=sigma_factor,
+ niter=3)
+ else:
+ fit = fitter
+
try:
result = fit(poly, ax, data)
- except:
- ValueError
+ except ValueError:
result = None
+
+ if sigma_factor > 0:
+ result = result[1]
+
return result
diff --git a/imexam/tests/coveragerc b/imexam/tests/coveragerc
index 86afa43d..9e6074a0 100644
--- a/imexam/tests/coveragerc
+++ b/imexam/tests/coveragerc
@@ -26,3 +26,6 @@ exclude_lines =
# Ignore branches that don't pertain to this version of Python
pragma: py{ignore_python_version}
+
+ # Don't complain about IPython completion helper
+ def _ipython_key_completions_
diff --git a/imexam/tests/test_imexamine.py b/imexam/tests/test_imexamine.py
index 8a25f8d0..4723d1b1 100644
--- a/imexam/tests/test_imexamine.py
+++ b/imexam/tests/test_imexamine.py
@@ -24,9 +24,8 @@
# make some data to test with
test_data = np.zeros((100, 100), dtype=np.float)
-test_data[25:75, 25:75] = 1.0
-test_data[35:65, 35:65] = 2.0
test_data[45:55, 45:55] = 3.0
+xx, yy = np.meshgrid(np.arange(100), np.arange(100))
@pytest.mark.skipif('not HAS_MATPLOTLIB')
@@ -56,275 +55,180 @@ def test_line_plot():
@pytest.mark.skipif('not HAS_PHOTUTILS')
def test_aper_phot(capsys):
"""Check that apertures are as expected from photutils"""
- apertures = photutils.CircularAperture((50, 50), 5)
+ radius = 5
+ apertures = photutils.CircularAperture((50, 50), radius)
aperture_area = apertures.area()
- assert_equal(aperture_area, 78.53981633974483)
+ assert_equal(aperture_area, np.pi * radius**2)
rawflux_table = photutils.aperture_photometry(
test_data,
apertures,
subpixels=1,
method="center")
total_flux = float(rawflux_table['aperture_sum'][0])
- assert_equal(total_flux, 207.)
+ # Verify the expected circular area sum
+ assert_equal(total_flux, 207.0)
def test_line_fit():
"""Fit a Gaussian1D line to the data."""
plots = Imexamine()
- plots.set_data(test_data)
+ in_amp = 3.
+ in_mean = 50.
+ in_stddev = 2.
+ in_const = 20.
+
+ # Set all the lines to be Gaussians
+ line_gauss = in_const + in_amp * np.exp(-0.5 * ((xx - in_mean) / in_stddev)**2)
+ plots.set_data(line_gauss)
fit = plots.line_fit(50, 50, form='Gaussian1D', genplot=False)
- amp = 2.8152269683542137
- mean = 49.45671107821953
- stddev = 13.051081779478146
- assert_allclose(amp, fit.amplitude, 1e-6)
- assert_allclose(mean, fit.mean, 1e-6)
- assert_allclose(stddev, fit.stddev, 1e-6)
+ assert_allclose(in_amp, fit.amplitude_0, 1e-6)
+ assert_allclose(in_mean, fit.mean_0, 1e-6)
+ assert_allclose(in_stddev, fit.stddev_0, 1e-6)
+ assert_allclose(in_const, fit.c0_1, 1e-6)
def test_column_fit():
"""Fit a Gaussian1D column to the data."""
plots = Imexamine()
- plots.set_data(test_data)
+ in_amp = 3.
+ in_mean = 50.
+ in_stddev = 2.
+ in_const = 20.
+ # Set all the columns to be Gaussians
+ col_gauss = in_const + in_amp * np.exp(-0.5 * ((yy - in_mean) / in_stddev)**2)
+ plots.set_data(col_gauss)
fit = plots.column_fit(50, 50, form='Gaussian1D', genplot=False)
- amp = 2.8285560281694115
- mean = 49.42625526973088
- stddev = 12.791137635400535
- assert_allclose(amp, fit.amplitude, 1e-6)
- assert_allclose(mean, fit.mean, 1e-6)
- assert_allclose(stddev, fit.stddev, 1e-6)
+ assert_allclose(in_amp, fit.amplitude_0, 1e-6)
+ assert_allclose(in_mean, fit.mean_0, 1e-6)
+ assert_allclose(in_stddev, fit.stddev_0, 1e-6)
+ assert_allclose(in_const, fit.c0_1, 1e-6)
def test_gauss_center():
"""Check the gaussian center fitting."""
- # make a 2d dataset with a gaussian at the center
+
from astropy.convolution import Gaussian2DKernel
- gaussian_2D_kernel = Gaussian2DKernel(10)
+
+ # This creates a 2D normalized gaussian kernal with
+ # a set amplitude. Guess off-center
+ amp = 0.0015915494309189533
+ size = 81.0
+ sigma = 10.0
+
+ gaussian_2D_kernel = Gaussian2DKernel(sigma, x_size=size, y_size=size)
plots = Imexamine()
plots.set_data(gaussian_2D_kernel.array)
a, xx, yy, xs, ys = plots.gauss_center(37, 37)
- amp = 0.0015915494309189533
- xc = 40.0
- yc = 40.0
- xsig = 10.0
- ysig = 10.0
-
assert_allclose(amp, a, 1e-6)
- assert_allclose(xc, xx, 1e-4)
- assert_allclose(yc, yy, 1e-4)
- assert_allclose(xsig, xs, 0.01)
- assert_allclose(ysig, ys, 0.01)
+ assert_allclose(size // 2, xx, 1e-6)
+ assert_allclose(size // 2, yy, 1e-6)
+ assert_allclose(sigma, xs, 0.01)
+ assert_allclose(sigma, ys, 0.01)
def test_radial_profile():
- """Test the radial profile function without background subtraction"""
+ """Test the radial profile function
+ No background subtraction
+ individual pixel results used
+ """
from astropy.convolution import Gaussian2DKernel
data = Gaussian2DKernel(1.5, x_size=25, y_size=25)
- plots = Imexamine()
- plots.set_data(data.array)
- # check the binned results
- plots.radial_profile_pars['pixels'][0] = False
- x, y = plots.radial_profile(12, 12, genplot=False)
+ xx, yy = np.meshgrid(np.arange(25), np.arange(25))
+ x0, y0 = np.where(data.array == data.array.max())
- rad = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- flux = [4.53542348e-02, 3.00703719e-01, 3.54889792e-01,
- 1.95806071e-01, 7.56662018e-02, 2.46976310e-02,
- 2.54073324e-03, 1.51802506e-04, 1.08323362e-06,
- 3.65945812e-10]
+ rad_in = np.sqrt((xx - x0)**2 + (yy - y0)**2)
+ rad_in = rad_in.ravel()
+ flux_in = data.array.ravel()
- assert_array_equal(rad, x)
- assert_allclose(flux, y, 1e-7)
+ order = np.argsort(rad_in)
+ rad_in = rad_in[order]
+ flux_in = flux_in[order]
-
-@pytest.mark.skipif('not HAS_PHOTUTILS')
-def test_radial_profile_background():
- """Test the radial profile function with background subtraction"""
- from astropy.convolution import Gaussian2DKernel
- data = Gaussian2DKernel(1.5, x_size=25, y_size=25)
plots = Imexamine()
plots.set_data(data.array)
- # check the binned results
- plots.radial_profile_pars['pixels'][0] = False
- plots.radial_profile_pars['background'][0] = True
- x, y = plots.radial_profile(12, 12, genplot=False)
- rad = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- flux = [4.535423e-02, 3.007037e-01, 3.548898e-01, 1.958061e-01,
- 7.566620e-02, 2.469763e-02, 2.540733e-03, 1.518025e-04,
- 1.083221e-06, 3.605551e-10]
+ plots.radial_profile_pars['pixels'][0] = True
+ plots.radial_profile_pars['background'][0] = False
+ plots.radial_profile_pars['clip'][0] = False
+ rad_out, flux_out = plots.radial_profile(x0, y0, genplot=False)
+
+ order2 = np.argsort(rad_out)
+ rad_out = rad_out[order2]
+ flux_out = flux_out[order2]
+
+ # the radial profile is done on a smaller cutout by default
+ # and may have a fractional center radius calculation. This
+ # looks at the first few hundred data points in both arrays
+ assert (len(rad_out) < len(rad_in))
+ good = 150
+ assert_allclose(rad_in[:good], rad_out[:good], atol=1e-14)
+ assert_allclose(flux_in[:good], flux_out[:good], atol=1e-14)
+
+
+def test_radial_profile_cumulative():
+ """Test the radial profile function
+ without background subtraction
+ with each pixel integer binned
+ """
+ from astropy.convolution import Gaussian2DKernel
+ ksize = 25
+ data = Gaussian2DKernel(1.5, x_size=ksize, y_size=ksize)
+ xx, yy = np.meshgrid(np.arange(ksize), np.arange(ksize))
+ x0, y0 = np.where(data.array == data.array.max())
+ rad_in = np.sqrt((xx - x0)**2 + (yy - y0)**2)
+
+ rad_in = rad_in.ravel()
+ flux_in = data.array.ravel()
- assert_array_equal(rad, x)
- assert_allclose(flux, y, 1e-6)
+ indices = np.argsort(rad_in)
+ rad_in = rad_in[indices]
+ flux_in = flux_in[indices]
+ # now bin the radflux like we expect
+ rad_in = rad_in.astype(np.int)
+ flux_in = np.bincount(rad_in, flux_in) / np.bincount(rad_in)
+ rad_in = np.arange(len(flux_in))
+ assert (data.array[x0, y0] == flux_in[0])
-def test_radial_profile_pixels():
- """Test the radial profile function without background subtraction"""
- from astropy.convolution import Gaussian2DKernel
- data = Gaussian2DKernel(1.5, x_size=25, y_size=25)
+ # check the binned results
plots = Imexamine()
plots.set_data(data.array)
- # check the unbinned results
- plots.radial_profile_pars['pixels'][0] = True
- x, y = plots.radial_profile(12, 12, genplot=False)
-
- rad = [1.00485917e-14, 1.00000000e+00, 1.00000000e+00,
- 1.00000000e+00, 1.00000000e+00, 1.41421356e+00,
- 1.41421356e+00, 1.41421356e+00, 1.41421356e+00,
- 2.00000000e+00, 2.00000000e+00, 2.00000000e+00,
- 2.00000000e+00, 2.23606798e+00, 2.23606798e+00,
- 2.23606798e+00, 2.23606798e+00, 2.23606798e+00,
- 2.23606798e+00, 2.23606798e+00, 2.23606798e+00,
- 2.82842712e+00, 2.82842712e+00, 2.82842712e+00,
- 2.82842712e+00, 3.00000000e+00, 3.00000000e+00,
- 3.00000000e+00, 3.00000000e+00, 3.16227766e+00,
- 3.16227766e+00, 3.16227766e+00, 3.16227766e+00,
- 3.16227766e+00, 3.16227766e+00, 3.16227766e+00,
- 3.16227766e+00, 3.60555128e+00, 3.60555128e+00,
- 3.60555128e+00, 3.60555128e+00, 3.60555128e+00,
- 3.60555128e+00, 3.60555128e+00, 3.60555128e+00,
- 4.00000000e+00, 4.00000000e+00, 4.00000000e+00,
- 4.00000000e+00, 4.12310563e+00, 4.12310563e+00,
- 4.12310563e+00, 4.12310563e+00, 4.12310563e+00,
- 4.12310563e+00, 4.12310563e+00, 4.12310563e+00,
- 4.24264069e+00, 4.24264069e+00, 4.24264069e+00,
- 4.24264069e+00, 4.47213595e+00, 4.47213595e+00,
- 4.47213595e+00, 4.47213595e+00, 4.47213595e+00,
- 4.47213595e+00, 4.47213595e+00, 4.47213595e+00,
- 5.00000000e+00, 5.00000000e+00, 5.00000000e+00,
- 5.00000000e+00, 5.00000000e+00, 5.00000000e+00,
- 5.00000000e+00, 5.00000000e+00, 5.00000000e+00,
- 5.00000000e+00, 5.00000000e+00, 5.00000000e+00,
- 5.09901951e+00, 5.09901951e+00, 5.09901951e+00,
- 5.09901951e+00, 5.09901951e+00, 5.09901951e+00,
- 5.09901951e+00, 5.09901951e+00, 5.38516481e+00,
- 5.38516481e+00, 5.38516481e+00, 5.38516481e+00,
- 5.38516481e+00, 5.38516481e+00, 5.38516481e+00,
- 5.38516481e+00, 5.65685425e+00, 5.65685425e+00,
- 5.65685425e+00, 5.65685425e+00, 5.83095189e+00,
- 5.83095189e+00, 5.83095189e+00, 5.83095189e+00,
- 5.83095189e+00, 5.83095189e+00, 5.83095189e+00,
- 5.83095189e+00, 6.00000000e+00, 6.00000000e+00,
- 6.00000000e+00, 6.00000000e+00, 6.08276253e+00,
- 6.08276253e+00, 6.08276253e+00, 6.08276253e+00,
- 6.08276253e+00, 6.08276253e+00, 6.08276253e+00,
- 6.08276253e+00, 6.32455532e+00, 6.32455532e+00,
- 6.32455532e+00, 6.32455532e+00, 6.32455532e+00,
- 6.32455532e+00, 6.32455532e+00, 6.32455532e+00,
- 6.40312424e+00, 6.40312424e+00, 6.40312424e+00,
- 6.40312424e+00, 6.40312424e+00, 6.40312424e+00,
- 6.40312424e+00, 6.40312424e+00, 6.70820393e+00,
- 6.70820393e+00, 6.70820393e+00, 6.70820393e+00,
- 6.70820393e+00, 6.70820393e+00, 6.70820393e+00,
- 6.70820393e+00, 7.00000000e+00, 7.00000000e+00,
- 7.07106781e+00, 7.07106781e+00, 7.07106781e+00,
- 7.07106781e+00, 7.07106781e+00, 7.07106781e+00,
- 7.07106781e+00, 7.07106781e+00, 7.21110255e+00,
- 7.21110255e+00, 7.21110255e+00, 7.21110255e+00,
- 7.21110255e+00, 7.21110255e+00, 7.21110255e+00,
- 7.21110255e+00, 7.28010989e+00, 7.28010989e+00,
- 7.28010989e+00, 7.28010989e+00, 7.61577311e+00,
- 7.61577311e+00, 7.61577311e+00, 7.61577311e+00,
- 7.81024968e+00, 7.81024968e+00, 7.81024968e+00,
- 7.81024968e+00, 7.81024968e+00, 7.81024968e+00,
- 7.81024968e+00, 7.81024968e+00, 8.06225775e+00,
- 8.06225775e+00, 8.06225775e+00, 8.06225775e+00,
- 8.48528137e+00, 8.48528137e+00, 8.48528137e+00,
- 8.48528137e+00, 8.60232527e+00, 8.60232527e+00,
- 8.60232527e+00, 8.60232527e+00, 9.21954446e+00,
- 9.21954446e+00, 9.21954446e+00, 9.21954446e+00,
- 9.89949494e+00]
-
- flux = [1.19552465e-02, 2.32856406e-02, 2.32856406e-02,
- 3.93558331e-03, 3.93558331e-03, 4.53542348e-02,
- 7.66546959e-03, 7.66546959e-03, 1.29556643e-03,
- 2.90802459e-02, 2.90802459e-02, 8.30691786e-04,
- 8.30691786e-04, 5.66405848e-02, 5.66405848e-02,
- 9.57301302e-03, 9.57301302e-03, 1.61796667e-03,
- 1.61796667e-03, 2.73457911e-04, 2.73457911e-04,
- 7.07355303e-02, 2.02059585e-03, 2.02059585e-03,
- 5.77193322e-05, 2.32856406e-02, 2.32856406e-02,
- 1.12421908e-04, 1.12421908e-04, 4.53542348e-02,
- 4.53542348e-02, 7.66546959e-03, 7.66546959e-03,
- 2.18967977e-04, 2.18967977e-04, 3.70085038e-05,
- 3.70085038e-05, 5.66405848e-02, 5.66405848e-02,
- 1.61796667e-03, 1.61796667e-03, 2.73457911e-04,
- 2.73457911e-04, 7.81146217e-06, 7.81146217e-06,
- 1.19552465e-02, 1.19552465e-02, 9.75533570e-06,
- 9.75533570e-06, 2.32856406e-02, 2.32856406e-02,
- 3.93558331e-03, 3.93558331e-03, 1.90007994e-05,
- 1.90007994e-05, 3.21138811e-06, 3.21138811e-06,
- 4.53542348e-02, 2.18967977e-04, 2.18967977e-04,
- 1.05716645e-06, 2.90802459e-02, 2.90802459e-02,
- 8.30691786e-04, 8.30691786e-04, 2.37291269e-05,
- 2.37291269e-05, 6.77834392e-07, 6.77834392e-07,
- 2.32856406e-02, 2.32856406e-02, 3.93558331e-03,
- 3.93558331e-03, 1.12421908e-04, 1.12421908e-04,
- 1.90007994e-05, 1.90007994e-05, 5.42767351e-07,
- 5.42767351e-07, 9.17349095e-08, 9.17349095e-08,
- 7.66546959e-03, 7.66546959e-03, 1.29556643e-03,
- 1.29556643e-03, 1.05716645e-06, 1.05716645e-06,
- 1.78675206e-07, 1.78675206e-07, 9.57301302e-03,
- 9.57301302e-03, 2.73457911e-04, 2.73457911e-04,
- 1.32024112e-06, 1.32024112e-06, 3.77133487e-08,
- 3.77133487e-08, 1.19552465e-02, 9.75533570e-06,
- 9.75533570e-06, 7.96023526e-09, 7.66546959e-03,
- 7.66546959e-03, 3.70085038e-05, 3.70085038e-05,
- 1.05716645e-06, 1.05716645e-06, 5.10394673e-09,
- 5.10394673e-09, 8.30691786e-04, 8.30691786e-04,
- 1.93626789e-08, 1.93626789e-08, 1.61796667e-03,
- 1.61796667e-03, 2.73457911e-04, 2.73457911e-04,
- 3.77133487e-08, 3.77133487e-08, 6.37405811e-09,
- 6.37405811e-09, 2.02059585e-03, 2.02059585e-03,
- 5.77193322e-05, 5.77193322e-05, 4.70982729e-08,
- 4.70982729e-08, 1.34538575e-09, 1.34538575e-09,
- 3.93558331e-03, 3.93558331e-03, 3.21138811e-06,
- 3.21138811e-06, 5.42767351e-07, 5.42767351e-07,
- 4.42891556e-10, 4.42891556e-10, 1.61796667e-03,
- 1.61796667e-03, 7.81146217e-06, 7.81146217e-06,
- 3.77133487e-08, 3.77133487e-08, 1.82078162e-10,
- 1.82078162e-10, 1.12421908e-04, 1.12421908e-04,
- 1.29556643e-03, 2.18967977e-04, 2.18967977e-04,
- 3.70085038e-05, 3.70085038e-05, 1.78675206e-07,
- 1.78675206e-07, 2.46415996e-11, 8.30691786e-04,
- 8.30691786e-04, 6.77834392e-07, 6.77834392e-07,
- 1.93626789e-08, 1.93626789e-08, 1.57997104e-11,
- 1.57997104e-11, 2.73457911e-04, 2.73457911e-04,
- 7.81146217e-06, 7.81146217e-06, 2.18967977e-04,
- 2.18967977e-04, 1.05716645e-06, 1.05716645e-06,
- 2.73457911e-04, 2.73457911e-04, 3.77133487e-08,
- 3.77133487e-08, 6.37405811e-09, 6.37405811e-09,
- 8.79064260e-13, 8.79064260e-13, 1.12421908e-04,
- 1.12421908e-04, 9.17349095e-08, 9.17349095e-08,
- 5.77193322e-05, 1.34538575e-09, 1.34538575e-09,
- 3.13597326e-14, 3.70085038e-05, 3.70085038e-05,
- 5.10394673e-09, 5.10394673e-09, 7.81146217e-06,
- 7.81146217e-06, 1.82078162e-10, 1.82078162e-10,
- 1.05716645e-06]
-
- assert_allclose(rad, x, 1e-7)
- assert_allclose(flux, y, 1e-7)
+ plots.radial_profile_pars['pixels'][0] = False
+ plots.radial_profile_pars['background'][0] = False
+ plots.radial_profile_pars['clip'][0] = False
+ rad_out, flux_out = plots.radial_profile(x0, y0, genplot=False)
+
+ # The default measurement size is not equal
+ assert (len(rad_in) >= len(rad_out))
+ good = [rad_in[i] for i in rad_out if rad_out[i] == rad_in[i]]
+
+ assert_array_equal(rad_in[good], rad_out[good])
+ assert_allclose(flux_in[good], flux_out[good], atol=1e-7)
@pytest.mark.skipif('not HAS_PHOTUTILS')
def test_curve_of_growth():
- """Test the cog functionality."""
+ """Test the curve of growth functionality."""
from astropy.convolution import Gaussian2DKernel
data = Gaussian2DKernel(1.5, x_size=25, y_size=25)
plots = Imexamine()
plots.set_data(data.array)
- x, y = plots.curve_of_growth(12, 12, genplot=False)
-
- rad = [1, 2, 3, 4, 5, 6, 7, 8]
- flux = [0.04535423476987057,
- 0.34605795394960859,
- 0.70094774639729907,
- 0.89675381769455764,
- 0.97242001951216395,
- 0.99711765053819645,
- 0.99965838382174854,
- 0.99998044756724924]
-
- assert_array_equal(rad, x)
- assert_allclose(flux, y, 1e-6)
+ rad_out, flux_out = plots.curve_of_growth(12, 12, genplot=False)
+
+ rads = [1, 2, 3, 4, 5, 6, 7, 8]
+ flux = []
+
+ # Run the aperture phot on this to build up the expected fluxes
+ plots.aper_phot_pars['genplot'][0] = False
+ plots.aper_phot_pars['subsky'][0] = False
+
+ for rad in rads:
+ plots.aper_phot_pars['radius'][0] = rad
+ plots.aper_phot(12, 12)
+ flux.append(plots.total_flux)
+
+ assert_array_equal(rads, rad_out)
+ assert_allclose(flux, flux_out, 1e-6)
diff --git a/imexam/tests/test_util.py b/imexam/tests/test_util.py
new file mode 100644
index 00000000..1ec38fa0
--- /dev/null
+++ b/imexam/tests/test_util.py
@@ -0,0 +1,212 @@
+"""Licensed under a 3-clause BSD style license - see LICENSE.rst.
+
+Make sure that the functions in util are behaving as expected
+"""
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+import numpy as np
+from numpy.testing import assert_equal
+from astropy.io import fits
+from imexam import util
+
+# testing data
+test_data_zeros = np.zeros((100, 100), dtype=np.float)
+
+
+def test_invalid_simple_fits():
+ """Test for an invalid simple FITS hdu with no data in
+ the primary HDU."""
+
+ simple_fits_hdu = fits.PrimaryHDU()
+ valid_file, nextend, first_image = util.check_valid(simple_fits_hdu)
+ assert_equal(valid_file, False)
+ assert_equal(nextend, 0)
+ assert_equal(first_image, None)
+
+
+def test_invalid_MEF_table():
+ """Test an MEF FITS hdu that only has table data."""
+
+ # create some binary table data
+ data = np.arange(0, 1, 0.01) * np.random.rand(100)
+ col = np.arange(100) + 1
+ d1 = fits.Column(name="data", format='d', array=data)
+ c1 = fits.Column(name="item", format='d', array=col)
+ cols = fits.ColDefs([c1, d1])
+ tbhdu = fits.BinTableHDU.from_columns(cols, name='TAB1')
+
+ mef_fits_hdu = fits.HDUList()
+ extension = fits.PrimaryHDU()
+ mef_fits_hdu.append(tbhdu)
+
+ mef_file, nextend, first_image = util.check_valid(mef_fits_hdu)
+ assert_equal(mef_file, True)
+ assert_equal(nextend, 1)
+ assert_equal(first_image, None)
+
+def test_image_and_table_extensions():
+ """Validate an MEF with an image in the first and
+ a table in the second extension."""
+
+ data = np.arange(0, 1, 0.01) * np.random.rand(100)
+ col = np.arange(100) + 1
+ d1 = fits.Column(name="data", format='d', array=data)
+ c1 = fits.Column(name="item", format='d', array=col)
+ cols = fits.ColDefs([c1, d1])
+ tbhdu = fits.BinTableHDU.from_columns(cols, name='TAB1')
+
+ mef_fits_hdu = fits.HDUList()
+ mef_fits_hdu.append(fits.PrimaryHDU())
+ extension = fits.PrimaryHDU()
+
+ mef_fits_hdu.append(tbhdu)
+ mef_fits_hdu.append(fits.ImageHDU(test_data_zeros,
+ header=extension.header,
+ name='SCI1'))
+
+ mef_file, nextend, first_image = util.check_valid(mef_fits_hdu)
+ assert_equal(mef_file, True)
+ assert_equal(nextend, 2)
+ assert_equal(first_image, 2)
+
+
+def test_drizzled_image():
+ """Validate a drizzle style output, with an image in the
+ primary HDU and a table in the first."""
+
+ data = np.arange(0, 1, 0.01) * np.random.rand(100)
+ col = np.arange(100) + 1
+ d1 = fits.Column(name="data", format='d', array=data)
+ c1 = fits.Column(name="item", format='d', array=col)
+ cols = fits.ColDefs([c1, d1])
+ tbhdu = fits.BinTableHDU.from_columns(cols, name='TAB1')
+
+ mef_fits_hdu = fits.HDUList()
+ extension = fits.PrimaryHDU()
+ mef_fits_hdu.append(fits.ImageHDU(test_data_zeros,
+ header=extension.header))
+ mef_fits_hdu.data = test_data_zeros
+ mef_fits_hdu.append(tbhdu)
+ mef_file, nextend, first_image = util.check_valid(mef_fits_hdu)
+ assert_equal(mef_file, True)
+ assert_equal(nextend, 1)
+ assert_equal(first_image, 0)
+
+
+def test_mef_2_image_extensions():
+ """Check the file to see if it is a multi-extension FITS file
+ or a simple fits image where the data and header are stored in
+ the primary hdr.
+
+ testa valid MEF file with 2 image extensions
+ and the first image in the first extension
+ not the primary HDU
+ """
+
+ mef_fits_hdu = fits.HDUList()
+ mef_fits_hdu.append(fits.PrimaryHDU())
+ extension = fits.PrimaryHDU()
+ extension.header['EXTVER'] = 1
+ mef_fits_hdu.append(fits.ImageHDU(test_data_zeros,
+ header=extension.header,
+ name='SCI1'))
+ extension.header['EXTVER'] = 2
+ mef_fits_hdu.append(fits.ImageHDU(test_data_zeros,
+ header=extension.header,
+ name='SCI2'))
+
+ mef_file, nextend, first_image = util.check_valid(mef_fits_hdu)
+ assert_equal(mef_file, True)
+ assert_equal(nextend, 2)
+ assert_equal(first_image, 1)
+
+
+def test_simple_image_in_primary():
+ """Check the file to see if it is a multi-extension FITS file
+ or a simple fits image where the data and header are stored in
+ the primary hdr.
+
+ This will try and find an image data unit in the primary HDU
+ """
+ simple_fits_hdu = fits.PrimaryHDU()
+ simple_fits_hdu.data = test_data_zeros
+
+ mef_file, nextend, first_image = util.check_valid(simple_fits_hdu)
+ assert_equal(mef_file, False)
+ assert_equal(nextend, 0)
+ assert_equal(first_image, 0)
+
+
+def test_hst_filename():
+ """Verify split of a standard hst filename."""
+
+ hst_name = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name)
+ short_compare = shortname.split("/")[-1]
+ assert_equal(hst_name, short_compare)
+ assert_equal(extname, None)
+ assert_equal(extver, None)
+
+
+def test_ext_ver_filename():
+ """Verify split of a filname given with ext and ver."""
+
+ hst_name_ext_ver = "hstimagex_cal.fits[sci,1]"
+ rootname = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name_ext_ver)
+ short_compare = shortname.split("/")[-1]
+ assert_equal(rootname, short_compare)
+ assert_equal(extname, "sci")
+ assert_equal(extver, 1)
+
+
+def test_name_ver_filename():
+ """Verify split of a filename given name and ver."""
+
+ hst_name_ver = "hstimagex_cal.fits[1]"
+ rootname = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name_ver)
+ short_compare = shortname.split("/")[-1]
+ assert_equal(rootname, short_compare)
+ assert_equal(extname, None)
+ assert_equal(extver, 1)
+
+
+def test_extver():
+ """Verify that specifying an extver gets recorded correctly.
+
+ extver is the extension number explicitly"""
+
+ hst_name = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name,
+ extver=1)
+ short_compare = shortname.split("/")[-1]
+ assert_equal(hst_name, short_compare)
+ assert_equal(extname, None)
+ assert_equal(extver, 1)
+
+
+def test_extname():
+ """Verify that specifying an extname gets recorded correctly."""
+
+ hst_name = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name,
+ extname='sci')
+ short_compare = shortname.split("/")[-1]
+ assert_equal(hst_name, short_compare)
+ assert_equal(extname, 'sci')
+ assert_equal(extver, None)
+
+
+def test_name_ver():
+ """Verify that specifying both name and ver gets recorded correctly."""
+
+ hst_name = "hstimagex_cal.fits"
+ shortname, extname, extver = util.verify_filename(filename=hst_name,
+ extname='sci',
+ extver=1)
+ short_compare = shortname.split("/")[-1]
+ assert_equal(hst_name, short_compare)
+ assert_equal(extname, 'sci')
+ assert_equal(extver, 1)
diff --git a/imexam/util.py b/imexam/util.py
index a2e82c90..80a23dcd 100644
--- a/imexam/util.py
+++ b/imexam/util.py
@@ -20,22 +20,14 @@
__all__ = ["display_help", "set_logging"]
-def find_ds9():
- """Find the local path to the DS9 executable"""
- path = "ds9"
- for dirname in os.getenv("PATH").split(":"):
- possible = os.path.join(dirname, path)
- if os.path.isfile(possible):
- return possible
- return None
+def find_path(target=None):
+ """Find the local path to the target executable"""
+ if target is None:
+ raise TypeError("Expected name of executable")
-
-def find_xpans():
- """Find the local path to the xpans executable"""
- path = "xpans"
for dirname in os.getenv("PATH").split(":"):
- possible = os.path.join(dirname, path)
- if os.path.exists(possible):
+ possible = os.path.join(dirname, target)
+ if os.path.isfile(possible):
return possible
return None
@@ -69,7 +61,7 @@ def list_active_ds9(verbose=True):
session_dict = {}
# only run if XPA/xpans is installed on the machine
- if find_xpans():
+ if find_path('xpans'):
sessions = None
try:
sessions = xpa.get(b"xpans").decode().strip().split("\n")
@@ -82,7 +74,7 @@ def list_active_ds9(verbose=True):
if verbose:
for line in sessions:
print(line)
- except xpa.XpaException:
+ except (ValueError, xpa.XpaException):
print("No active sessions registered")
else:
@@ -185,47 +177,101 @@ def set_logging(filename=None, on=True, level=logging.INFO):
# set stream logging level
if isinstance(handler, logging.StreamHandler):
handler.setLevel(level)
-
return root
-def check_filetype(filename=None):
+def check_valid(fits_data=None):
"""Check the file to see if it is a multi-extension FITS file
or a simple fits image where the data and header are stored in
- the global unit.
+ the primary header unit.
Parameters
----------
- filename: string
- The name of the file to check
+ fits_data: None, FITS object
+ Set to an in-memory FITS object if passing FITS HDUList;
+ Otherwise set to the name of the file to check
+ Returns
+ -------
+ mef_file: bool
+ Returns True if the file is a multi-extension fits file
+ nextend: int
+ The number of extension in the file
+ first_image: int, None
+ The extension that contains the first image data.
+ None will be returned when no IMAGE xtension is found
+ Notes
+ -----
+ Drizzled images put a table in the first extension and an image in
+ the zero extension, so this function checks for the first occurrance
+ of 'IMAGE' in 'XTENSION', which is a required keyword.
"""
log = logging.getLogger(__name__)
-
- if filename is None:
- raise ValueError("No filename provided")
- else:
+ found_image = False # Does it contain an IMAGE XTENSION
+ nextend = 0 # how many extenions does it have
+ first_image = None # what extension has the first image?
+ fits_file = False
+ mef_file = False
+
+ if fits_data is None:
+ raise ValueError("No filename or FITS object provided")
+ if isinstance(fits_data, fits.hdu.hdulist.HDUList):
+ mef_file = True
+ fits_image = fits_data
+ elif isinstance(fits_data, fits.hdu.image.PrimaryHDU):
+ if fits_data.header['NAXIS'] > 0:
+ first_image = 0
+ fits_image = fits_data
+ elif isinstance(fits_data, str):
+ fits_file = True
try:
- mef_file = fits.getval(filename, ext=0, keyword='EXTEND')
- except KeyError:
- mef_file = False
+ fits_image = fits.open(fits_data)
except IOError:
- log.warning("Problem opening file {0:s}".format(repr(filename)))
- raise IOError("Error opening {0:s}".format(filename))
-
- # double check for lying liars, should at least have 1 extension
- # and XTENSION is a required keyword
- if mef_file:
+ msg = "Error opening file {0:s}".format(repr(fits_data))
+ log.warning(msg)
+ raise IOError(msg)
+ try:
+ # EXTEND is required for MEF FITS files
+ mef_file = fits_image[0].header['EXTEND']
+ if not mef_file:
+ if fits_image[0].header['NAXIS'] > 0:
+ first_image = 0
+ except KeyError:
+ if fits_image[0].header['NAXIS'] > 0:
+ first_image = 0
+
+ # double check for lying liars, should at least have 1 extension
+ # if it's MEF and XTENSION is a required keyword in each extension
+ # after the primary hdu
+ if mef_file:
+ for extn in fits_image:
try:
- fits.getval(filename, ext=1, keyword='XTENSION')
- except (KeyError, IndexError):
- mef_file = False
- return mef_file
+ if ((extn.header['XTENSION'] == 'IMAGE') and (not found_image)):
+ first_image = nextend # The number of the extension
+ found_image = True
+ except KeyError:
+ # There doens't have to be an 'XTENSION' keyword in the global(0) if
+ # the MEF has data there, so check for naxis if its an image
+ # and tfields if it's table data
+ if nextend == 0:
+ try:
+ tfields = extn.header['TFIELDS'] # table?
+ except KeyError:
+ if extn.header['NAXIS'] > 0:
+ found_image = True
+ first_image = nextend
+ else:
+ mef_file = False
+ nextend += 1
+ nextend -= 1 # account for overcounting in return value
+ if fits_file:
+ fits_image.close()
+ return (mef_file, nextend, first_image)
def verify_filename(filename=None, extver=None, extname=None):
- """Verify the filename exist and split it for extension information.
+ """Verify the filename exists and split it for extension information.
If the user has given an extension, extension name or some combination of
those, return the full filename, extension and version tuple.
@@ -239,23 +285,29 @@ def verify_filename(filename=None, extver=None, extname=None):
extname: string
the name of the extension
+
+ Returns
+ -------
+ shortname: str
+ The name of the file with full path
+ extname: str, None
+ The extension name, or None
+ extver: int, None
+ The extension version number or None
"""
if filename is None:
print("No filename provided")
+ elif not isinstance(filename, str):
+ raise TypeError("Expected filename to be a string")
else:
- try:
- if "[" in filename:
- splitstr = filename.split("[")
- shortname = splitstr[0]
- if "," in splitstr[1]:
- extname = splitstr[1].split(",")[0]
- extver = int(splitstr[1].split(",")[1][0])
- else:
- extver = int(filename.split("[")[1][0])
+ if "[" in filename:
+ splitstr = filename.split("[")
+ shortname = os.path.abspath(splitstr[0])
+ if "," in splitstr[1]:
+ extname = splitstr[1].split(",")[0]
+ extver = int(splitstr[1].split(",")[1][0])
else:
- shortname = os.path.abspath(filename)
- except IndexError as e:
- print("Exception: {0}".format(repr(e)))
- raise IndexError
-
- return shortname, extname, extver
+ extver = int(filename.split("[")[1][0])
+ else:
+ shortname = os.path.abspath(filename)
+ return shortname, extname, extver
diff --git a/setup.py b/setup.py
index eec5072d..cf4ed5b2 100644
--- a/setup.py
+++ b/setup.py
@@ -65,7 +65,7 @@
# VERSION should be PEP386 compatible (http://www.python.org/dev/peps/pep-0386)
-VERSION = '0.8.0'
+VERSION = '0.8.1'
# Indicates if this version is a release version
RELEASE = 'dev' not in VERSION