├── PySpectrograph ├── Spectrograph │ ├── tests │ │ ├── __init__.py │ │ └── test_optics.py │ ├── __init__.py │ ├── Grating.py │ ├── Slit.py │ ├── Optics.py │ ├── CCD.py │ ├── SpectrographEquations.py │ ├── CreateImage.py │ ├── Spectrograph.py │ └── Detector.py ├── unit_tests │ ├── one.line │ ├── ut_LineSolution.py │ ├── ut_Functions.py │ ├── ut_Identify.py │ ├── ut_RssModel.py │ ├── ut_ImageSolution.py │ ├── ut_CreateImage.py │ ├── ut_Spectrum.py │ ├── ut_detectlines.py │ ├── ut_ModelSolution.py │ ├── ut_LineFit.py │ ├── Xe.dat │ ├── ut_WavelengthSolution.py │ └── ut_ModelFit.py ├── Spectra │ ├── __init__.py │ ├── apext.py │ ├── detectlines.py │ ├── findobj.py │ └── Spectrum.py ├── tests │ ├── __init__.py │ ├── setup_package.py │ └── coveragerc ├── Identify │ ├── __init__.py │ ├── Identify.py │ ├── AutoIdentify.py │ └── specidentify.py ├── Models │ ├── __init__.py │ └── RSSModel.py ├── WavelengthSolution │ ├── __init__.py │ ├── LineSolution.py │ ├── LineFit.py │ ├── WavelengthSolution.py │ ├── ImageSolution.py │ └── ModelSolution.py ├── Utilities │ ├── __init__.py │ ├── degreemath.py │ ├── makeplots.py │ ├── Functions.py │ └── fit.py ├── __init__.py ├── conftest.py └── _astropy_init.py ├── .gitmodules ├── RELEASE.txt ├── doc ├── Spectrograph.rst ├── Models.rst ├── WavelengthSolution.rst ├── index.rst ├── Utilities.rst ├── Spectra.rst ├── make.bat ├── Makefile └── conf.py ├── docs ├── _templates │ └── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ └── module.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── README.txt ├── README.md ├── .gitignore ├── MANIFEST.in ├── tests ├── Ne.txt └── ut_RSSModel.py ├── LICENSE ├── setup.cfg ├── INSTALL.txt ├── setup.py └── .travis.yml /PySpectrograph/Spectrograph/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/one.line: -------------------------------------------------------------------------------- 1 | 4671.226 2000 2 | -------------------------------------------------------------------------------- /PySpectrograph/Spectra/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Spectra includes models for a spectrum 3 | """ 4 | 5 | from . import Spectrum 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "astropy_helpers"] 2 | path = astropy_helpers 3 | url = https://github.com/astropy/astropy-helpers.git 4 | -------------------------------------------------------------------------------- /PySpectrograph/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 2 | """ 3 | This module contains package tests. 4 | """ 5 | -------------------------------------------------------------------------------- /PySpectrograph/Identify/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Identify is a set of tasks that provide fitting and determine the 3 | wavelength calibration of a set of data 4 | """ 5 | 6 | from . import Identify 7 | -------------------------------------------------------------------------------- /PySpectrograph/Models/__init__.py: -------------------------------------------------------------------------------- 1 | """Models contains different models for spectrographs. These current 2 | include the following spectrographs: 3 | 4 | RSSModel--RSS Spectrograph on SALT 5 | 6 | """ 7 | -------------------------------------------------------------------------------- /PySpectrograph/Identify/Identify.py: -------------------------------------------------------------------------------- 1 | """IDENTIFY is a task that determines the wavelength calibration for an 2 | array. The information that needs to be supplied to identify is the 3 | 1- or 2-D data array""" 4 | -------------------------------------------------------------------------------- /RELEASE.txt: -------------------------------------------------------------------------------- 1 | 2 | 0.3 3 | -Move to github 4 | 5 | 0.23 6 | -Updated the RSS model 7 | 8 | 0.211 9 | -Bug fix in findobj so it always returns valid coordinates for objects 10 | 11 | 0.21 12 | 13 | 0.20 14 | -------------------------------------------------------------------------------- /doc/Spectrograph.rst: -------------------------------------------------------------------------------- 1 | 2 | ======================================== 3 | Spectrograph 4 | ======================================== 5 | 6 | .. autoclass:: PySpectrograph.Spectrograph 7 | :members: 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/__init__.py: -------------------------------------------------------------------------------- 1 | """WavelengthSolutions includes tasks for determining a relationship between 2 | pixel position and wavelength in an observed 1- or 2-D spectrum 3 | 4 | 5 | """ 6 | 7 | from . import WavelengthSolution 8 | -------------------------------------------------------------------------------- /doc/Models.rst: -------------------------------------------------------------------------------- 1 | 2 | ======================================== 3 | Models 4 | ======================================== 5 | 6 | .. autoclass:: PySpectrograph.Models.RSSModel.RSSModel 7 | :members: 8 | :inherited-members: 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc/WavelengthSolution.rst: -------------------------------------------------------------------------------- 1 | 2 | ======================================== 3 | Wavelength Solution 4 | ======================================== 5 | 6 | .. autoclass:: PySpectrograph.WavelengthSolution.WavelengthSolution 7 | :members: 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/base.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/class.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {% extends "autosummary_core/module.rst" %} 2 | {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #} -------------------------------------------------------------------------------- /PySpectrograph/tests/setup_package.py: -------------------------------------------------------------------------------- 1 | # import os 2 | 3 | # If this package has tests data in the tests/data directory, add them to 4 | # the paths here, see commented example 5 | paths = ['coveragerc', 6 | # os.path.join('data', '*fits') 7 | ] 8 | 9 | def get_package_data(): 10 | return { 11 | _ASTROPY_PACKAGE_NAME_ + '.tests': paths} 12 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/tests/test_optics.py: -------------------------------------------------------------------------------- 1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 2 | 3 | import numpy as np 4 | 5 | import pytest 6 | 7 | from ..Optics import Optics 8 | 9 | 10 | def test_make_optic(): 11 | op = Optics(diameter=100, focallength=100, width=100, zpos=0, focus=0) 12 | 13 | assert op.diameter == 100 14 | assert op.focallength == 100 15 | assert op.width == 100 16 | assert op.focus == 0 17 | assert op.zpos == 0 18 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | The PySpectrograph package holds the tasks to model and analyze spectroscopic 2 | data from grating spectrographs. 3 | 4 | These tasks include: 5 | 6 | Spectrograph--The basic equations and components of a spectrograph 7 | Models--models for different spectrographs 8 | Spectra--classes for the handling of spectra 9 | WavelengthSolutions--Methods for calculating the wavelength solution 10 | Identify--Tasks to measure the wavelength solution 11 | Utilities--General useful tasks 12 | -------------------------------------------------------------------------------- /PySpectrograph/Utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities is part of The PySpectrograph package and holds general tasks 2 | that are useful in the reduction and analysis of spectroscopic data 3 | 4 | * degremath--trigometric functions for arrays and scalars in degrees 5 | * fit--A general fitting routine to emply least squares fitting 6 | * Functions--create n-dimension distributions 7 | * makeplots--random plotting package 8 | 9 | """ 10 | 11 | from . import degreemath 12 | from . import fit 13 | from . import Functions 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyspectrograph 2 | ============== 3 | 4 | The PySpectrograph package holds the tasks to model and analyze spectroscopic 5 | data from grating spectrographs. 6 | 7 | These tasks include: 8 | 9 | Spectrograph--The basic equations and components of a spectrograph 10 | Models--models for different spectrographs 11 | Spectra--classes for the handling of spectra 12 | WavelengthSolutions--Methods for calculating the wavelength solution 13 | Identify--Tasks to measure the wavelength solution 14 | Utilities--General useful tasks 15 | 16 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. PySpectrograph documentation master file, created by 2 | sphinx-quickstart on Tue Apr 10 18:14:51 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PySpectrograph's documentation! 7 | ========================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Spectra 15 | Spectrograph 16 | Models 17 | Utilities 18 | WavelengthSolution 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PySpectrograph documentation master file, created by 2 | sphinx-quickstart on Tue Apr 10 18:14:51 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PySpectrograph's documentation! 7 | ========================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | .. Spectra 15 | Spectrograph 16 | Models 17 | Utilities 18 | WavelengthSolution 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /PySpectrograph/Utilities/degreemath.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy 3 | 4 | 5 | def sind(x): 6 | """Return the sin of x where x is in degrees""" 7 | if isinstance(x, numpy.ndarray): 8 | return numpy.sin(math.pi * x / 180.0) 9 | return math.sin(math.radians(x)) 10 | 11 | 12 | def cosd(x): 13 | """Return the cos of x where x is in degrees""" 14 | if isinstance(x, numpy.ndarray): 15 | return numpy.cos(math.pi * x / 180.0) 16 | return math.cos(math.radians(x)) 17 | 18 | 19 | def tand(x): 20 | """Return the cos of x where x is in degrees""" 21 | if isinstance(x, numpy.ndarray): 22 | return numpy.tan(math.pi * x / 180.0) 23 | return math.cos(math.radians(x)) 24 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/__init__.py: -------------------------------------------------------------------------------- 1 | """Spectrograph package is part of PySpectrograph and provides 2 | a model for a grating spectrograph. 3 | 4 | This task includes: 5 | 6 | Spectrograph--The basic equations and components of a spectrograph 7 | Models--models for different spectrographs 8 | Spectra--classes for the handling of spectra 9 | WavelengthSolutions--Methods for calculating the wavelength solution 10 | Identify--Tasks to measure the wavelength solution 11 | Utilities--General useful tasks 12 | 13 | """ 14 | 15 | from .Spectrograph import Spectrograph 16 | from .Grating import Grating 17 | from .Optics import Optics 18 | from .Slit import Slit 19 | from .Detector import Detector 20 | from .CCD import CCD 21 | -------------------------------------------------------------------------------- /doc/Utilities.rst: -------------------------------------------------------------------------------- 1 | 2 | Utilities 3 | ======================================== 4 | 5 | .. automodule:: PySpectrograph.Utilities 6 | :members: 7 | 8 | 9 | ======================================== 10 | Fit 11 | ======================================== 12 | 13 | .. autoclass:: PySpectrograph.Utilities.fit.interfit 14 | :members: 15 | 16 | ======================================== 17 | Functions 18 | ======================================== 19 | 20 | .. automodule:: PySpectrograph.Utilities.Functions 21 | :members: 22 | 23 | ======================================== 24 | degreemath 25 | ======================================== 26 | 27 | .. automodule:: PySpectrograph.Utilities.degreemath 28 | :members: 29 | 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | #vi 56 | *.swp 57 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/Grating.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Grating: 4 | 5 | """A class that describing gratings. Sigma should be in lines/mm and the 6 | units of the dimensions should be mm. 7 | """ 8 | 9 | def __init__(self, name='', spacing=600, order=1, height=100, width=100, 10 | thickness=100, blaze=0, type='transmission'): 11 | # define the variables that describe the grating 12 | self.order = order 13 | self.height = height 14 | self.width = width 15 | self.thickness = thickness 16 | self.sigma = 1.0 / spacing 17 | self.blaze = blaze 18 | self.name = name 19 | self.type = type 20 | # set the sign for the grating equation 21 | self.sign = 1 22 | if self.type == 'transmission': 23 | self.sign = -1 24 | -------------------------------------------------------------------------------- /doc/Spectra.rst: -------------------------------------------------------------------------------- 1 | 2 | Spectra 3 | ======================================== 4 | 5 | ======================================== 6 | Spectrum 7 | ======================================== 8 | 9 | .. autoclass:: PySpectrograph.Spectra.Spectrum.Spectrum 10 | :members: 11 | 12 | ======================================== 13 | findobj 14 | ======================================== 15 | 16 | .. automodule:: PySpectrograph.Spectra.findobj 17 | :members: 18 | 19 | ======================================== 20 | detectlines 21 | ======================================== 22 | 23 | .. automodule:: PySpectrograph.Spectra.detectlines 24 | :members: 25 | 26 | ======================================== 27 | apext 28 | ======================================== 29 | 30 | .. automodule:: PySpectrograph.Spectra.apext 31 | :members: 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # the next few stanzas are for astropy_helpers. It's derived from the 2 | # astropy_helpers/MANIFEST.in, but requires additional includes for the actual 3 | # package directory and egg-info. 4 | 5 | include astropy_helpers/README.rst 6 | include astropy_helpers/CHANGES.rst 7 | include astropy_helpers/LICENSE.rst 8 | recursive-include astropy_helpers/licenses * 9 | 10 | include astropy_helpers/ez_setup.py 11 | include astropy_helpers/ah_bootstrap.py 12 | 13 | recursive-include astropy_helpers/astropy_helpers *.py *.pyx *.c *.h *.rst 14 | recursive-include astropy_helpers/astropy_helpers.egg-info * 15 | # include the sphinx stuff with "*" because there are css/html/rst/etc. 16 | recursive-include astropy_helpers/astropy_helpers/sphinx * 17 | 18 | prune astropy_helpers/build 19 | prune astropy_helpers/astropy_helpers/tests 20 | 21 | 22 | global-exclude *.pyc *.o 23 | 24 | -------------------------------------------------------------------------------- /PySpectrograph/tests/coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = {packagename} 3 | omit = 4 | {packagename}/_astropy_init* 5 | {packagename}/conftest* 6 | {packagename}/cython_version* 7 | {packagename}/setup_package* 8 | {packagename}/*/setup_package* 9 | {packagename}/*/*/setup_package* 10 | {packagename}/tests/* 11 | {packagename}/*/tests/* 12 | {packagename}/*/*/tests/* 13 | {packagename}/version* 14 | 15 | [report] 16 | exclude_lines = 17 | # Have to re-enable the standard pragma 18 | pragma: no cover 19 | 20 | # Don't complain about packages we have installed 21 | except ImportError 22 | 23 | # Don't complain if tests don't hit assertions 24 | raise AssertionError 25 | raise NotImplementedError 26 | 27 | # Don't complain about script hooks 28 | def main\(.*\): 29 | 30 | # Ignore branches that don't pertain to this version of Python 31 | pragma: py{ignore_python_version} -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_LineSolution.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pylab as pl 4 | 5 | from PySpectrograph.WavelengthSolution import LineSolution as LS 6 | 7 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 8 | wp = np.array([4500.9772, 9 | 4524.6805, 10 | 4582.7474, 11 | 4624.2757, 12 | 4671.226, 13 | 4690.9711, 14 | 4697.02, 15 | 4734.1524, 16 | 4807.019, 17 | 4829.709, 18 | 4916.51]) 19 | 20 | 21 | def test_LineSolution(): 22 | ls = LS.LineSolution(xp, wp, function='legendre') 23 | ls.interfit() 24 | print(ls.coef) 25 | print(ls.sigma(ls.x, ls.y)) 26 | print(ls.value(2000)) 27 | 28 | pl.figure() 29 | pl.plot(xp, wp - ls(xp), ls='', marker='o') 30 | pl.plot(ls.x, ls.y - ls(ls.x), ls='', marker='o') 31 | pl.show() 32 | return 33 | 34 | 35 | test_LineSolution() 36 | -------------------------------------------------------------------------------- /tests/Ne.txt: -------------------------------------------------------------------------------- 1 | #Wavelength Intensity 2 | 5852.488 12386 3 | 5881.895 8539 4 | 5944.834 15257 5 | 5965.471 92 6 | 6029.997 4652 7 | 6074.338 13396 8 | 6128.450 1230 9 | 6143.063 33330 10 | 6163.594 11344 11 | 6217.281 13705 12 | 6266.495 21747 13 | 6304.789 10226 14 | 6334.428 36235 15 | 6382.991 35824 16 | 6402.248 70163 17 | 6506.528 46165 18 | 6532.882 21413 19 | 6598.953 26396 20 | 6652.093 535 21 | 6678.277 51338 22 | 6717.043 36780 23 | 6929.467 55590 24 | 7032.413 100000 25 | 7059.108 839 26 | 7173.938 19713 27 | 7245.167 73545 28 | 7438.898 33715 29 | 7472.438 521 30 | 7488.871 5207 31 | 7535.774 4852 32 | 7544.044 2559 33 | 7943.181 651 34 | 8082.458 1484 35 | 8118.550 251 36 | 8136.406 1621 37 | 8266.077 537 38 | 8300.325 3531 39 | 8365.746 422 40 | 8377.607 14543 41 | 8418.427 2842 42 | 8463.357 378 43 | 8495.359 8765 44 | 8544.695 186 45 | 8571.354 233 46 | 8591.258 2935 47 | 8634.647 3059 48 | 8654.383 5458 49 | 8704.112 291 50 | 8853.867 1820 51 | 8919.501 447 52 | 8988.556 173 53 | 9148.672 529 54 | 9201.759 382 55 | 9275.519 35 56 | 9300.853 244 57 | 9326.507 213 58 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/Slit.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class Slit: 5 | 6 | """A class that describing the slit. Only assuming a single slit. All sizes are in mm. 7 | All positions assume the center of the slit. Phi is in arcseconds 8 | """ 9 | 10 | def __init__(self, name='', height=100, width=100, zpos=0, xpos=0, ypos=0, phi=1): 11 | 12 | # define the variables that describe the grating 13 | self.height = height 14 | self.width = width 15 | self.name = name 16 | self.zpos = zpos 17 | self.xpos = xpos 18 | self.ypos = ypos 19 | self.phi = phi 20 | 21 | def set_phi(self, phi): 22 | self.phi = phi 23 | 24 | def calc_phi(self, ftel): 25 | """Calculate phi(angle on sky) assuming w/ftel 26 | 27 | returns phi in arcseconds 28 | """ 29 | return 3600.0 * math.degrees(self.width / ftel) 30 | 31 | def calc_width(self, ftel): 32 | """Calculate the width assuming ftel*phi(rad). 33 | 34 | returns the slit width in mm 35 | """ 36 | return ftel * math.radians(self.phi / 3600.0) 37 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/Optics.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Optics: 4 | 5 | """A class that describing optics. All dimensions should in mm. This assumes all optics 6 | can be desribed by a diameter and focal length. zpos is the distance in mm that the center 7 | of the optic is from the primary mirror. focas is the offset from that position 8 | """ 9 | 10 | def __init__(self, name='', diameter=100, focallength=100, width=100, 11 | zpos=0, focus=0): 12 | # define the variables that describe the opticsg 13 | self.diameter = diameter 14 | self.focallength = focallength 15 | self.width = width 16 | self.name = name 17 | # set distances of the optics 18 | self.zpos = zpos 19 | self.focus = focus 20 | 21 | def speed(self): 22 | """Camera Speed f/ = d/f 23 | """ 24 | return self.diameter / self.focallength 25 | 26 | def platescale(self): 27 | """Plate scale for a given optic in arcsec/mm 28 | """ 29 | return 206265 / self.focallength 30 | 31 | def position(self): 32 | """Current position along the optical path of the element 33 | """ 34 | return self.zpos + self.focus 35 | -------------------------------------------------------------------------------- /tests/ut_RSSModel.py: -------------------------------------------------------------------------------- 1 | import pylab as pl 2 | 3 | from PySpectrograph.Models import RSSModel 4 | from PySpectrograph.Spectra import Spectrum 5 | 6 | # create the spectrograph model 7 | rss = RSSModel.RSSModel(grating_name="PG0900", gratang=15.875, camang=31.76496, 8 | slit=1.50, xbin=2, ybin=2) 9 | 10 | 11 | # print out some basic statistics 12 | print(1e7 * rss.calc_bluewavelength(), 1e7 * rss.calc_centralwavelength(), 1e7 * rss.calc_redwavelength()) 13 | R = rss.calc_resolution(rss.calc_centralwavelength(), rss.alpha(), -rss.beta()) 14 | res = 1e7 * rss.calc_resolelement(rss.alpha(), -rss.beta()) 15 | print(R, res) 16 | 17 | # set up the detector 18 | ycen = rss.detector.get_ypixcenter() 19 | d_arr = rss.detector.make_detector()[ycen, :] 20 | w = 1e7 * rss.get_wavelength(d_arr) 21 | 22 | # set up the artificial spectrum 23 | sw, sf = pl.loadtxt('Ne.txt', usecols=(0, 1), unpack=True) 24 | wrange = [1e7 * rss.calc_bluewavelength(), 1e7 * rss.calc_redwavelength()] 25 | spec = Spectrum.Spectrum(sw, sf, wrange=wrange, dw=res / 10, stype='line', sigma=res) 26 | 27 | # interpolate it over the same range as the detector 28 | spec.interp(w) 29 | 30 | 31 | # plot it 32 | pl.figure() 33 | pl.plot(spec.wavelength, d_arr * ((spec.flux) / spec.flux.max())) 34 | pl.show() 35 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/LineSolution.py: -------------------------------------------------------------------------------- 1 | """LineSolution is a task describing the functional form for transforming 2 | pixel position to wavelength. The inputs for this task are the given pixel position 3 | and the corresponding wavelength. The user selects an input functional form and 4 | order for that form. The task then calculates the coefficients for that form. 5 | Possible options for the wavelength solution include polynomial, legendre, spline. 6 | 7 | HISTORY 8 | 20090915 SMC Initially Written by SM Crawford 9 | 20101018 SMC Updated to use interfit 10 | 11 | LIMITATIONS 12 | 13 | """ 14 | 15 | from PySpectrograph.Utilities.fit import interfit 16 | 17 | 18 | class LineSolution(interfit): 19 | 20 | """LineSolution is a task describing the functional form for transforming 21 | pixel position to wavelength. 22 | 23 | * x - list or array of x data 24 | * y - list or array of y data 25 | * yerr - error on y data 26 | * coef - Initial coefficients for fit 27 | * function - function to be fit to the data: 28 | options include polynomial, legendre, chebyshev, or spline 29 | * order - order of the function that is fit 30 | * thresh - threshold for rejection 31 | * niter - number of times to iterate 32 | 33 | """ 34 | 35 | def value(self, x): 36 | """Calculate the value of the array 37 | """ 38 | return self(x) 39 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_Functions.py: -------------------------------------------------------------------------------- 1 | 2 | from Functions import Normal, FunctionsError 3 | import numpy as np 4 | import pylab as pl 5 | 6 | 7 | def test_normal(): 8 | 9 | xarr = np.arange(-25, 25, 0.1) 10 | arr = np.indices([50, 50]) - 25 11 | d3arr = np.indices([50, 50, 50]) - 25 12 | # create a 1-D gaussian 13 | g1d = Normal(xarr, 10, 5, 15) 14 | pl.figure(figsize=(10, 10)) 15 | pl.axes([0.1, 0.6, 0.8, 0.3]) 16 | pl.plot(xarr, g1d) 17 | 18 | # create a 2-D gaussian 19 | g2d = Normal(arr, np.array([10, 0]), np.array([5, 10]), 15) 20 | pl.axes([0.1, 0.1, 0.3, 0.3]) 21 | zmap = np.product(g2d, axis=0) 22 | pl.imshow(zmap) 23 | 24 | # create a 3-D gaussian 25 | g3d = Normal(d3arr, 10, 5, 15) 26 | z3map = np.product(g3d, axis=0) 27 | m3d = z3map[35].max() 28 | pl.axes([0.5, 0.1, 0.2, 0.2]) 29 | pl.imshow(z3map[10], vmin=0, vmax=m3d) 30 | pl.axes([0.5, 0.35, 0.2, 0.2]) 31 | pl.imshow(z3map[33], vmin=0, vmax=m3d) 32 | pl.axes([0.75, 0.1, 0.2, 0.2]) 33 | pl.imshow(z3map[35], vmin=0, vmax=m3d) 34 | pl.axes([0.75, 0.35, 0.2, 0.2]) 35 | pl.imshow(z3map[37], vmin=0, vmax=m3d) 36 | # test error that mean is the wrong number of dimensions 37 | try: 38 | g2d = Normal(arr, np.array([10, 10, 0]), np.array([5, 10]), 15) 39 | except FunctionsError: 40 | pass 41 | 42 | pl.show() 43 | 44 | 45 | test_normal() 46 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_Identify.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from astropy.io import fits 3 | 4 | from PySpectrograph.Models import RSSModel 5 | 6 | inimage = 'fmbxpP200610180009.fits' 7 | 8 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 9 | wp = np.array([4500.9772, 10 | 4524.6805, 11 | 4582.7474, 12 | 4624.2757, 13 | 4671.226, 14 | 4690.9711, 15 | 4697.02, 16 | 4734.1524, 17 | 4807.019, 18 | 4829.709, 19 | 4916.51]) 20 | 21 | 22 | def test_Identify(): 23 | hdu = fits.open(inimage) 24 | 25 | # create the data arra 26 | data = hdu[1].data 27 | 28 | # create the header information 29 | instrume = hdu[1].header['INSTRUME'].strip() 30 | grating = hdu[1].header['GRATING'].strip() 31 | grang = hdu[1].header['GR-ANGLE'] 32 | arang = hdu[1].header['AR-ANGLE'] 33 | filter = hdu[1].header['FILTER'].strip() 34 | slit = float(hdu[1].header['MASKID']) 35 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 36 | 37 | print(instrume, grating, grang, arang, filter) 38 | print(xbin, ybin) 39 | print(len(data), len(data[0])) 40 | 41 | # create the RSS Model 42 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 43 | camang=arang, slit=slit, xbin=int(xbin), 44 | ybin=int(ybin)) 45 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_RssModel.py: -------------------------------------------------------------------------------- 1 | 2 | """Unit Test for LineSolution. LineSolution will calculate the 3 | best spectrograph design given an input spectrograph and spectrum 4 | 5 | """ 6 | from astropy.io import fits 7 | 8 | from PySpectrograph.Models import RSSModel 9 | 10 | inimage = 'fmbxpP200610180009.fits' 11 | inspectra = 'Xe.dat' 12 | 13 | 14 | def test_rssmodel(): 15 | # load the image and determine its spectrograph parameters 16 | hdu = fits.open(inimage) 17 | 18 | # create the header information 19 | grating = hdu[1].header['GRATING'].strip() 20 | grang = hdu[1].header['GR-ANGLE'] 21 | arang = hdu[1].header['AR-ANGLE'] 22 | slit = float(hdu[1].header['MASKID']) 23 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 24 | 25 | # create the RSS Model 26 | rss = RSSModel.RSSModel(grating_name=grating, gratang=grang, 27 | camang=arang, slit=slit, xbin=int(xbin), 28 | ybin=int(ybin)) 29 | alpha = rss.alpha() 30 | beta = rss.beta() 31 | 32 | sigma = 1e7 * rss.calc_resolelement(alpha, beta) 33 | print("SIGMA: ", sigma) 34 | 35 | # test to see if giving the wrong name it will raise an error 36 | try: 37 | rss = RSSModel.RSSModel(grating_name="not a grating", gratang=grang, 38 | camang=arang, slit=slit, xbin=int(xbin), 39 | ybin=int(ybin)) 40 | except RSSModel.RSSError as e: 41 | pass 42 | 43 | 44 | test_rssmodel() 45 | -------------------------------------------------------------------------------- /PySpectrograph/Spectra/apext.py: -------------------------------------------------------------------------------- 1 | # 2 | # APEXT--Extract a 1-D spectra from a 2-D image 3 | # 4 | # 5 | # 6 | 7 | 8 | def makeflat(arr, y1, y2): 9 | """For a 2-D array, compress it along the y-dimension to make a one dimensional 10 | array 11 | """ 12 | y1 = max(0, y1) 13 | y2 = min(len(arr), y2) 14 | # return the 1-D array if only a single line is requested 15 | if abs(y1 - y2) <= 1: 16 | return arr[y1, :] 17 | 18 | # sum up the array along for the full value 19 | return arr[y1:y2, :].sum(axis=0) 20 | 21 | 22 | class apext: 23 | 24 | """A class for extracting a 1-D spectra from a 2-D image""" 25 | 26 | def __init__(self, wave, data, ivar=None): 27 | self.data = data 28 | self.wave = wave 29 | self.nwave = len(wave) 30 | if ivar is None: 31 | self.ivar = None 32 | else: 33 | self.ivar = ivar 34 | 35 | def flatten(self, y1, y2): 36 | """Compress the 2-D array down to 1-D. This is just either done by 37 | a straight summation or by a weighted summation if IVAR is present 38 | """ 39 | if self.ivar is None: 40 | self.ldata = makeflat(self.data, y1, y2) 41 | # self.lvar=self.data[y1:y2,:].std(axis=0) 42 | self.lvar = self.ldata 43 | else: 44 | self.lvar = ((self.ivar[y1:y2, :] ** 2).sum(axis=0)) 45 | # self.ldata=(self.data[y1:y2,:]*self.ivar[y1:y2,:]).sum(axis=0)/self.lvar 46 | self.ldata = makeflat(self.data, y1, y2) 47 | return 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Steve Crawford 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /PySpectrograph/__init__.py: -------------------------------------------------------------------------------- 1 | """The PySpectrograph package holds the tasks to model and analyze spectroscopic 2 | data from grating spectrographs. 3 | 4 | These tasks include: 5 | 6 | Spectrograph--The basic equations and components of a spectrograph 7 | Models--models for different spectrographs 8 | Spectra--classes for the handling of spectra 9 | WavelengthSolutions--Methods for calculating the wavelength solution 10 | Identify--Tasks to measure the wavelength solution 11 | Utilities--General useful tasks 12 | 13 | """ 14 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 15 | 16 | # Packages may add whatever they like to this file, but 17 | # should keep this content at the top. 18 | # ---------------------------------------------------------------------------- 19 | from ._astropy_init import * 20 | # ---------------------------------------------------------------------------- 21 | 22 | if not _ASTROPY_SETUP_: 23 | # For egg_info test builds to pass, put package imports here. 24 | from . import Utilities 25 | from .Spectrograph import * 26 | from . import Models 27 | from . import Spectra 28 | from .Spectra import * 29 | from . import WavelengthSolution 30 | 31 | __all__ = ['Identify', 'Models', 'Spectra', 'Spectrograph', 'Utilities', 'WavelengthSolution'] 32 | 33 | 34 | # general class for errors 35 | class SpectrographError(Exception): 36 | 37 | """Exception Raised for Spectrograph errors""" 38 | pass 39 | 40 | 41 | class PySpectrographError(Exception): 42 | 43 | """Exception Raised for PySpectrograph errors""" 44 | pass 45 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/CCD.py: -------------------------------------------------------------------------------- 1 | class CCD: 2 | 3 | """Defines a CCD by x and y position, size, and pixel size. The x and y position are 4 | set such that they are zero relative to the detector position. This assumes that 5 | the x and y positions are in the center of the pixels and that the ccd is symmetric. 6 | 7 | pix_size is in mm 8 | """ 9 | 10 | def __init__(self, name='', height=0, width=0, xpos=0, ypos=0, 11 | pix_size=0.015, xpix=2048, ypix=2048): 12 | # set the variables 13 | self.xpos = xpos 14 | self.ypos = ypos 15 | self.pix_size = pix_size 16 | self.xpix = xpix 17 | self.ypix = ypix 18 | self.height = self.set_height(height) 19 | self.width = self.set_width(width) 20 | 21 | def set_width(self, w): 22 | """If the width is less than the number of pixels, then the width is 23 | given by the number of pixels 24 | """ 25 | min = self.xpix * self.pix_size 26 | return max(w, min) 27 | 28 | def set_height(self, h): 29 | """If the height is less than the number of pixels, then the height is 30 | given by the number of pixels 31 | """ 32 | min = self.ypix * self.pix_size 33 | return max(h, min) 34 | 35 | def find_corners(self): 36 | """Return the corners of the ccd""" 37 | x1 = self.xpos - 0.5 * self.width 38 | x2 = self.xpos + 0.5 * self.width 39 | y1 = self.ypos - 0.5 * self.height 40 | y2 = self.ypos + 0.5 * self.height 41 | return x1, x2, y1, y2 42 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [build_docs] 7 | source-dir = docs 8 | build-dir = docs/_build 9 | all_files = 1 10 | 11 | [upload_docs] 12 | upload-dir = docs/_build/html 13 | show-response = 1 14 | 15 | [tool:pytest] 16 | minversion = 3.0 17 | norecursedirs = build docs/_build 18 | doctest_plus = enabled 19 | addopts = -p no:warnings 20 | 21 | [ah_bootstrap] 22 | auto_use = True 23 | 24 | [pycodestyle] 25 | # E101 - mix of tabs and spaces 26 | # W191 - use of tabs 27 | # W291 - trailing whitespace 28 | # W292 - no newline at end of file 29 | # W293 - trailing whitespace 30 | # W391 - blank line at end of file 31 | # E111 - 4 spaces per indentation level 32 | # E112 - 4 spaces per indentation level 33 | # E113 - 4 spaces per indentation level 34 | # E901 - SyntaxError or IndentationError 35 | # E902 - IOError 36 | select = E101,W191,W291,W292,W293,W391,E111,E112,E113,E901,E902 37 | exclude = extern,sphinx,*parsetab.py 38 | 39 | [metadata] 40 | package_name = PySpectrograph 41 | description = pyspectrograph 42 | long_description = Software for modelling spectrographs Edit 43 | author = Steve Crawford 44 | author_email = crawfordsm@gmail.com 45 | license = BSD 3-Clause 46 | url = https://github.com/crawfordsm/pyspectrograph 47 | edit_on_github = False 48 | github_project = crawfordsm/pyspectrograph 49 | # install_requires should be formatted as a comma-separated list, e.g.: 50 | # install_requires = astropy, scipy, matplotlib 51 | install_requires = astropy, numpy, scipy 52 | # version should be PEP386 compatible (http://www.python.org/dev/peps/pep-0386) 53 | version = 0.4.dev0 54 | 55 | [entry_points] 56 | 57 | astropy-package-template-example = PySpectrograph.example_mod:main 58 | 59 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_ImageSolution.py: -------------------------------------------------------------------------------- 1 | 2 | """Unit Test for ImageSolution. ImageSolution will calculate the 3 | best spectrograph design given an input spectrograph and data array 4 | 5 | """ 6 | import numpy as np 7 | from astropy.io import fits 8 | 9 | from ImageSolution import ImageSolution 10 | import RSSModel 11 | import Spectrum 12 | 13 | inimage = 'fmbxpP200610180009.fits' 14 | inspectra = 'Xe.dat' 15 | 16 | 17 | def test_imagesolution(): 18 | # load the image and determine its spectrograph parameters 19 | hdu = fits.open(inimage) 20 | 21 | # create the data arra 22 | data = hdu[1].data 23 | 24 | # create the header information 25 | instrume = hdu[1].header['INSTRUME'].strip() 26 | grating = hdu[1].header['GRATING'].strip() 27 | grang = hdu[1].header['GR-ANGLE'] 28 | arang = hdu[1].header['AR-ANGLE'] 29 | filter = hdu[1].header['FILTER'].strip() 30 | slit = float(hdu[1].header['MASKID']) 31 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 32 | 33 | print(instrume, grating, grang, arang, filter) 34 | print(xbin, ybin) 35 | 36 | # create the RSS Model 37 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 38 | camang=arang, slit=slit, xbin=int(xbin), 39 | ybin=int(ybin)) 40 | 41 | # create the spectrum 42 | stype = 'line' 43 | w, s = np.loadtxt(inspectra, usecols=(0, 1), unpack=True) 44 | spec = Spectrum.Spectrum(w, s, wrange=[4000, 5000], dw=0.1, stype=stype) 45 | 46 | # Now having the model and the data, set up the variables 47 | imsol = ImageSolution(data, rssmodel.rss, spec) 48 | # imsol.fit() 49 | # for i in imsol.coef: 50 | # print imsol.coef[i]() 51 | 52 | 53 | test_imagesolution() 54 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_CreateImage.py: -------------------------------------------------------------------------------- 1 | """Test CreateImage is a task to produce a 2D image generated by a spectrograph 2 | 3 | HISTORY 4 | 20100601 SMC First written by SM Crawford 5 | 6 | Limitations: 7 | 8 | """ 9 | import os 10 | 11 | import math 12 | import numpy as np 13 | from astropy.io import fits 14 | 15 | from PySpectrograph.Spectrograph.CreateImage import * 16 | from PySpectrograph.Spectra.Spectrum import * 17 | from PySpectrograph.Models import RSSModel 18 | 19 | 20 | from pylab import * 21 | 22 | inimg = 'fmbxpP200610180009.fits' 23 | infile = 'Xe.dat' 24 | outfile = 'out.fits' 25 | 26 | 27 | def test_CreateImage(): 28 | 29 | # read in the data 30 | hdu = fits.open(inimg) 31 | im_arr = hdu[1].data 32 | hdu.close() 33 | 34 | # set up the spectrum 35 | stype = 'line' 36 | w, s = np.loadtxt('Xe.dat', usecols=(0, 1), unpack=True) 37 | spec = Spectrum(w, s, wrange=[4000, 5000], dw=0.1, stype=stype) 38 | 39 | # set up the spectrograph 40 | dx = 2 * 0.015 * 8.169 41 | dy = 2 * 0.015 * 0.101 42 | # set up the spectrograph 43 | # rssmodel=RSSModel.RSSModel(grating_name='PG0900', gratang=13.625, camang=27.25, slit=1.0, xbin=2, ybin=2, xpos=dx, ypos=dy) 44 | rssmodel = RSSModel.RSSModel( 45 | grating_name='PG3000', 46 | gratang=43.625, 47 | camang=87.25, 48 | slit=2.0, 49 | xbin=2, 50 | ybin=2, 51 | xpos=dx, 52 | ypos=dy) 53 | 54 | rssmodel.set_camera(name='RSS', focallength=330.0) 55 | rss = rssmodel.rss 56 | 57 | # set up the outfile 58 | if os.path.isfile(outfile): 59 | os.remove(outfile) 60 | 61 | arr = im_arr.copy() 62 | arr = CreateImage(spec, rss) 63 | arr = arr * im_arr.max() / spec.flux.max() 64 | writeout(arr, outfile) 65 | 66 | 67 | test_CreateImage() 68 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_Spectrum.py: -------------------------------------------------------------------------------- 1 | from PySpectrograph import Spectrum 2 | import numpy as np 3 | import pylab as pl 4 | 5 | infile = 'Xe.10.spec' 6 | inlist = 'Xe.dat' 7 | oneline = 'one.line' 8 | 9 | 10 | def test_spectrum(): 11 | print('testing spectrum...') 12 | 13 | # create a spectrum from a single line 14 | w, f = np.loadtxt(oneline, usecols=(0, 1), unpack=True) 15 | sp1 = Spectrum.Spectrum([w], [f], wrange=[4500, 4900], sigma=1) 16 | pl.figure(figsize=(10, 10)) 17 | pl.axes([0.1, 0.1, 0.8, 0.8]) 18 | pl.plot(sp1.wavelength, sp1.flux) 19 | 20 | # create a spectrum from a line list 21 | w, f = np.loadtxt(inlist, usecols=(0, 1), unpack=True) 22 | spl = Spectrum.Spectrum(w, f, sigma=5) 23 | pl.plot(spl.wavelength, spl.flux) 24 | 25 | # create a spectrum from a spectrum 26 | w, f = np.loadtxt(infile, usecols=(0, 1), unpack=True) 27 | spf = Spectrum.Spectrum(w, f, sigma=5, stype='continuum') 28 | pl.plot(spf.wavelength, spf.flux) 29 | 30 | # test error that mean is the wrong number of dimensions 31 | pl.savefig('out.png') 32 | pl.show() 33 | 34 | 35 | def test_vacuum(): 36 | # test code to change vacuum to air wavlengths and back again 37 | # test case form IDL script 38 | w_air = 6056.125 39 | w_vac = 6057.8019 40 | if abs(Spectrum.air2vac(w_air, mode='Morton') - w_vac) > 0.01: 41 | print("ERROR in MORTON AIR2VAC calculation") 42 | if abs(Spectrum.air2vac(w_air, mode='Ciddor') - w_vac) > 0.01: 43 | print("ERROR in CIDDOR AIR2VAC calculation") 44 | print(w_air, w_vac) 45 | if abs(Spectrum.vac2air(w_vac, mode='Ciddor') - w_air) > 0.01: 46 | print("ERROR in CIDDOR VAC2AIR calculation") 47 | if abs(Spectrum.vac2air(w_vac, mode='Morton') - w_air) > 0.01: 48 | print("ERROR in MORTON VAC2AIR calculation") 49 | 50 | # check that it works with a 51 | 52 | 53 | test_spectrum() 54 | test_vacuum() 55 | -------------------------------------------------------------------------------- /PySpectrograph/Utilities/makeplots.py: -------------------------------------------------------------------------------- 1 | # 2 | # MAKEPLOTS--A library for making plots for demaniacs 3 | # 4 | # 5 | # 6 | 7 | from pylab import * 8 | import numpy 9 | 10 | 11 | def plotframe(data): 12 | """Plot the entire data array 13 | returns a figure 14 | """ 15 | nimg = 10 16 | ywidth = 0.08 17 | xlen = len(data[0]) / nimg 18 | for i in range(nimg): 19 | yax = 0.90 - ywidth * 1.1 * i 20 | x1 = xlen * i 21 | x2 = xlen * (1 + i) 22 | f = axes([0.1, yax, 0.8, ywidth]) 23 | f.imshow(data[:, x1:x2], cmap=cm.gray, aspect='auto', vmin=-5, vmax=50) 24 | f.axis('off') 25 | return f 26 | 27 | 28 | def plotfeature(f, wave, data, w1, w2, z): 29 | """Plot a section of the data array 30 | as indicated by w1 and w2 31 | """ 32 | w1 = w1 * (1 + z) 33 | w2 = w2 * (1 + z) 34 | if w1 > wave.max(): 35 | return f 36 | mask = (w1 < wave) * (wave < w2) 37 | mdata = data[:, mask] 38 | f.imshow(mdata, cmap=cm.gray, aspect='auto', vmin=-5, vmax=50) 39 | # set up the axis labels 40 | x1 = wave[mask][0] 41 | x2 = wave[mask][-1] 42 | dw = (x2 - x1) / 5 43 | xtarr = arange(x1, x2 + dw, dw) 44 | xtlab = [] 45 | for x in xticks()[0]: 46 | if x >= 0 and x < len(wave[mask]): 47 | x = wave[mask][x] 48 | xtlab.append('%4.2f' % x) 49 | else: 50 | xtlab.append('0') 51 | f.set_yticklabels([]) 52 | f.set_xticklabels([]) 53 | return f 54 | 55 | 56 | def plotlinefeature(f, wave, flux, w1, w2, z): 57 | w1 = w1 * (1 + z) 58 | w2 = w2 * (1 + z) 59 | mask = (w1 < wave) * (wave < w2) 60 | f = plotline(f, wave[mask], flux[mask]) 61 | return f 62 | 63 | 64 | def plotline(f, wave, flux, color=None): 65 | if color: 66 | f.plot(wave, flux, ls='-', color=color, lw=1.55) 67 | else: 68 | f.plot(wave, flux, ls='-', lw=1.55) 69 | f.set_xlim((wave[0], wave[-1])) 70 | # f.set_yticklabels([]) 71 | return f 72 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_detectlines.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from detectlines import * 3 | 4 | infile = 'Xe.01.spec' 5 | inlist = 'Xe.dat' 6 | plist = [4500.98500001, 4624.28500002, 4671.23500002, 4734.16500002, 4807.02500002, 4916.51500002, 4923.16500002] 7 | wlist = [4500.9772, 4624.2757, 4671.226, 4734.1524, 4807.019, 4916.51, 4923.152] 8 | 9 | 10 | def centroid2(warr, farr, mask): 11 | return (warr[mask] * farr[mask]).sum() / farr[mask].sum() 12 | 13 | 14 | def test_centroid(): 15 | xc = 30.2 16 | 17 | def gaussian(x): return 3 * np.exp(-0.5 * (xc - x) ** 2 / (3.16 ** 2.)) 18 | x = np.arange(-50, 50) 19 | f = gaussian(x) 20 | 21 | xc1 = centroid(x, f) 22 | 23 | xdiff = 10 24 | mask = (abs(x - xc) < xdiff) 25 | 26 | xcm = centroid(x, f, mask=mask) 27 | 28 | if abs(xc - xc1) > 0.01: 29 | print("FAIL", xc, xc1) 30 | if abs(xc - xcm) > 0.01: 31 | print("FAIL", xc, xcm) 32 | 33 | 34 | def test_find_backstats(): 35 | w, f = np.loadtxt(infile, unpack=True) 36 | ave, std = find_backstats(f, sigma=3, niter=5) 37 | if abs(ave - 50) > 1 or (std - 10) > 1: 38 | print("FAIL") 39 | 40 | 41 | def test_find_peaks(): 42 | warr, farr = np.loadtxt(infile, unpack=True) 43 | xp = find_peaks(farr, 10, 5) 44 | for x, w in zip(xp, plist): 45 | if warr[x] != w: 46 | print("FAIL", warr[x], w) 47 | 48 | 49 | def test_detectlines(): 50 | wl, fl = np.loadtxt(inlist, usecols=[0, 1], unpack=True) 51 | warr, farr = np.loadtxt(infile, unpack=True) 52 | wp = detectlines(warr, farr, sigma=10, niter=5, bsigma=3, mask=None, kern=default_kernal, center=False) 53 | cwp = detectlines(warr, farr, sigma=10, niter=5, bsigma=3, mask=None, kern=default_kernal, center=True) 54 | if (np.array(wlist, dtype=float) - wp).mean() > 0.01: 55 | print('FAIL center=False') 56 | if (np.array(wlist, dtype=float) - cwp).mean() > 0.01: 57 | print('FAIL center=True') 58 | 59 | 60 | test_centroid() 61 | test_find_backstats() 62 | test_find_peaks() 63 | test_detectlines() 64 | -------------------------------------------------------------------------------- /PySpectrograph/Utilities/Functions.py: -------------------------------------------------------------------------------- 1 | """Functions supports the creation of general 1 and n-dim profiles including 2 | normal and sersic profiles 3 | 4 | TODO: 5 | 6 | Limitations: 7 | 1. The sersic profile assumes radial symmetry 8 | 9 | """ 10 | import numpy as np 11 | 12 | 13 | class FunctionsError(Exception): 14 | 15 | """Exception Raised for Function errors""" 16 | pass 17 | 18 | 19 | def Normal(arr, mean, sigma, scale=1): 20 | """Return the normal distribution of N dimensions 21 | 22 | arr--The input array of N dimensions 23 | mean--the mean of the distribution--either a scalor or N-dimensional array 24 | sigma--the std deviation of the distribution--either a scalor or N-dimensial array 25 | scale--the scale of the distrubion 26 | 27 | """ 28 | arr = arr 29 | mean = mean 30 | sigma = sigma 31 | scale = scale 32 | dim = np.ndim(arr) - 1 33 | 34 | # check to make sure that mean and sigma have dimensions that match dim 35 | # and create the arrays to calculate the distribution 36 | if isinstance(mean, np.ndarray): 37 | if len(mean) != dim: 38 | raise FunctionsError('Mean and input array are different number of dimensions') 39 | mean = np.reshape(mean, (dim, 1, 1)) 40 | if isinstance(sigma, np.ndarray): 41 | if len(sigma) != dim: 42 | raise FunctionsError('Sigma and input array are different number of dimensions') 43 | sigma = np.reshape(sigma, (dim, 1, 1)) 44 | 45 | # calculate the gaussian 46 | z = scale * np.exp(-0.5 * (arr - mean) ** 2 / sigma) 47 | return z 48 | 49 | 50 | def sersic(arr, deg, r_e, ell=1, scale=1): 51 | """Produce a light distribution given by a sersic profile 52 | I=I_e exp(-b [ (R/R_E)^(1/n)-1]) 53 | 54 | where: 55 | Gamma(2n)=gamma(2n, b) 56 | """ 57 | 58 | dim = np.ndim(arr) - 1 59 | # assume radial symmetry 60 | if dim == 0: 61 | r = abs(arr / r_e) 62 | else: 63 | pass 64 | 65 | z = scale * np.exp(-ell * (r ** (1 / deg) - 1)) 66 | return z 67 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_ModelSolution.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pylab as pl 4 | from astropy.io import fits 5 | from PySpectrograph.Models import RSSModel 6 | from PySpectrograph.WavelengthSolution import ModelSolution as MS 7 | 8 | inimage = 'fmbxpP200610180009.fits' 9 | 10 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 11 | wp = np.array([4500.9772, 12 | 4524.6805, 13 | 4582.7474, 14 | 4624.2757, 15 | 4671.226, 16 | 4690.9711, 17 | 4697.02, 18 | 4734.1524, 19 | 4807.019, 20 | 4829.709, 21 | 4916.51]) 22 | 23 | 24 | def test_ModelSolution(): 25 | 26 | hdu = fits.open(inimage) 27 | 28 | # create the data arra 29 | data = hdu[1].data 30 | 31 | # create the header information 32 | instrume = hdu[1].header['INSTRUME'].strip() 33 | grating = hdu[1].header['GRATING'].strip() 34 | grang = hdu[1].header['GR-ANGLE'] 35 | arang = hdu[1].header['AR-ANGLE'] 36 | filter = hdu[1].header['FILTER'].strip() 37 | slit = float(hdu[1].header['MASKID']) 38 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 39 | 40 | print(instrume, grating, grang, arang, filter) 41 | print(xbin, ybin) 42 | print(len(data), len(data[0])) 43 | 44 | # create the RSS Model 45 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 46 | camang=arang, slit=slit, xbin=int(xbin), 47 | ybin=int(ybin)) 48 | 49 | err = xp * 0.0 + 0.1 50 | ls = MS.ModelSolution(xp, wp, rssmodel.rss, xlen=len(data[0]), order=4) 51 | ls.fit(cfit='all') 52 | ls.fit(ls.ndcoef) 53 | for c in ls.coef: 54 | print(c(), end=' ') 55 | print() 56 | print(ls.sigma(ls.x, ls.y)) 57 | print(ls.chisq(ls.x, ls.y, err)) 58 | print(ls.value(2000)) 59 | 60 | pl.figure() 61 | pl.plot(xp, wp - ls.value(xp), ls='', marker='o') 62 | # pl.plot(ls.x, ls.y-ls(ls.x), ls='', marker='o') 63 | pl.show() 64 | return 65 | 66 | 67 | test_ModelSolution() 68 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/LineFit.py: -------------------------------------------------------------------------------- 1 | """LineFit is a task describing the functional form for transforming 2 | pixel position to wavelength by fitting a model spectrum. The inputs for this task 3 | are an observed spectrum and a calibrated spectrum 4 | and the corresponding wavelength. The user selects an input functional form and 5 | order for that form. The task then calculates the coefficients for that form. 6 | Possible options for the wavelength solution include polynomial, legendre, spline. 7 | 8 | HISTORY 9 | 20090915 SMC Initially Written by SM Crawford 10 | 20101018 SMC Updated to use interfit 11 | 12 | LIMITATIONS 13 | 14 | """ 15 | 16 | from scipy import optimize 17 | 18 | from PySpectrograph.Utilities.fit import interfit 19 | 20 | 21 | class LineFit(interfit): 22 | 23 | """LineSolution is a task describing the functional form for transforming 24 | pixel position to wavelength. 25 | 26 | * obsspec --observed spectrum 27 | * calspec --calibrated spectrum 28 | * function - function to be fit to the data: 29 | options include polynomial, legendre, chebyshev, or spline 30 | * order - order of the function that is fit 31 | 32 | """ 33 | 34 | def __init__(self, obsspec, calspec, function='poly', order=3, coef=None): 35 | 36 | # set up the spectrum 37 | self.obs_spec = obsspec 38 | self.cal_spec = calspec 39 | 40 | # set up the function 41 | self.order = order 42 | self.set_func(function) 43 | 44 | # set up the coef 45 | self.set_coef(coef) 46 | 47 | def flux(self, x): 48 | """Return the calibrated flux at the transformed position x""" 49 | return self.cal_spec.get_flux(self(x)) 50 | 51 | def value(self, x): 52 | """Calculate the value of the array 53 | """ 54 | return self(x) 55 | 56 | def errfit(self, coef, x, y, err=1): 57 | self.set_coe(coef) 58 | return (y - self.flux(x)) 59 | 60 | def lfit(self, task=0, s=None, t=None, full_output=1, warn=False): 61 | self.results = optimize.leastsq(self.errfit, self.coef, 62 | args=(self.obs_spec.wavelength, self.obs_spec.flux), full_output=full_output) 63 | self.set_coef(self.results[0]) 64 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/SpectrographEquations.py: -------------------------------------------------------------------------------- 1 | 2 | from PySpectrograph.Utilities.degreemath import cosd, sind 3 | 4 | 5 | def n_index(): 6 | return 1.0000 7 | 8 | 9 | def gratingequation(sigma, order, sign, alpha, beta, gamma=0, nd=n_index): 10 | """Apply the grating equation to determine the wavelength 11 | w = sigma/m cos (gamma) * n_ind *(sin alpha +- sin beta) 12 | 13 | returns wavelength in mm 14 | """ 15 | angle = cosd(gamma) * nd() * (sind(alpha) + sign * sind(beta)) 16 | return sigma / order * angle 17 | 18 | 19 | def calc_angdisp(sigma, order, beta, gamma=0): 20 | """Calculate the angular dispersion according to m/sigma/cos beta 21 | 22 | returns angular dispersion in 1/mm 23 | """ 24 | return order / sigma / cosd(beta) / cosd(gamma) 25 | 26 | 27 | def calc_lindisp(f, sigma, order, beta, gamma=0.0): 28 | """Calculate the linear dispersion according to f_cam * A 29 | 30 | return linear dispersion in mm/mm 31 | 32 | """ 33 | return f * calc_angdisp(sigma, order, beta, gamma=gamma) 34 | 35 | 36 | def calc_anamorph(alpha, beta): 37 | """Calculates the anamorphic magnification 38 | 39 | returns the anamorpic magnification 40 | """ 41 | return cosd(alpha) / cosd(beta) 42 | 43 | 44 | def calc_demagspatial(fcol, fcam): 45 | """Calculate the spatial demagnification 46 | 47 | returns the spatial demagnification 48 | """ 49 | return fcol / fcam 50 | 51 | 52 | def calc_demagspectral(fcol, fcam, alpha, beta): 53 | """Calculate the spectral demagnification 54 | 55 | returns the spectral demagnification 56 | """ 57 | return calc_demagspatial(fcol, fcam) / calc_anamorph(alpha, beta) 58 | 59 | 60 | def calc_resolelement(w, fcol, sigma, order, alpha, beta, gamma=0.0): 61 | """Calculate the resolution element using dw=r*w/A/fcol 62 | 63 | returns resolution element in mm 64 | """ 65 | r = calc_anamorph(alpha, beta) 66 | A = calc_angdisp(sigma, order, beta, gamma=gamma) 67 | return _calc_resolelement(w, fcol, r, A) 68 | 69 | 70 | def _calc_resolelement(w, fcol, r, A): 71 | """Calculate the resolution element using dw=r*w/A/fcol 72 | 73 | returns resolution element in mm 74 | """ 75 | return r * w / A / fcol 76 | 77 | 78 | def calc_resolution(w, dw): 79 | """Calcualte the resolution R=w/dw 80 | 81 | returns the resolution 82 | """ 83 | return w / dw 84 | -------------------------------------------------------------------------------- /PySpectrograph/conftest.py: -------------------------------------------------------------------------------- 1 | # This file is used to configure the behavior of pytest when using the Astropy 2 | # test infrastructure. 3 | 4 | from astropy.version import version as astropy_version 5 | if astropy_version < '3.0': 6 | # With older versions of Astropy, we actually need to import the pytest 7 | # plugins themselves in order to make them discoverable by pytest. 8 | from astropy.tests.pytest_plugins import * 9 | else: 10 | # As of Astropy 3.0, the pytest plugins provided by Astropy are 11 | # automatically made available when Astropy is installed. This means it's 12 | # not necessary to import them here, but we still need to import global 13 | # variables that are used for configuration. 14 | from astropy.tests.plugins.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS 15 | 16 | from astropy.tests.helper import enable_deprecations_as_exceptions 17 | 18 | ## Uncomment the following line to treat all DeprecationWarnings as 19 | ## exceptions. For Astropy v2.0 or later, there are 2 additional keywords, 20 | ## as follow (although default should work for most cases). 21 | ## To ignore some packages that produce deprecation warnings on import 22 | ## (in addition to 'compiler', 'scipy', 'pygments', 'ipykernel', and 23 | ## 'setuptools'), add: 24 | ## modules_to_ignore_on_import=['module_1', 'module_2'] 25 | ## To ignore some specific deprecation warning messages for Python version 26 | ## MAJOR.MINOR or later, add: 27 | ## warnings_to_ignore_by_pyver={(MAJOR, MINOR): ['Message to ignore']} 28 | # enable_deprecations_as_exceptions() 29 | 30 | ## Uncomment and customize the following lines to add/remove entries from 31 | ## the list of packages for which version numbers are displayed when running 32 | ## the tests. Making it pass for KeyError is essential in some cases when 33 | ## the package uses other astropy affiliated packages. 34 | # try: 35 | # PYTEST_HEADER_MODULES['Astropy'] = 'astropy' 36 | # PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' 37 | # del PYTEST_HEADER_MODULES['h5py'] 38 | # except (NameError, KeyError): # NameError is needed to support Astropy < 1.0 39 | # pass 40 | 41 | ## Uncomment the following lines to display the version number of the 42 | ## package rather than the version number of Astropy in the top line when 43 | ## running the tests. 44 | # import os 45 | # 46 | ## This is to figure out the package version, rather than 47 | ## using Astropy's 48 | # try: 49 | # from .version import version 50 | # except ImportError: 51 | # version = 'dev' 52 | # 53 | # try: 54 | # packagename = os.path.basename(os.path.dirname(__file__)) 55 | # TESTED_VERSIONS[packagename] = version 56 | # except NameError: # Needed to support Astropy <= 1.0.0 57 | # pass 58 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/CreateImage.py: -------------------------------------------------------------------------------- 1 | """CreateImage is a task to produce a 2D image generated by a spectrograph 2 | 3 | HISTORY 4 | 20100601 SMC First written by SM Crawford 5 | 6 | Limitations: 7 | 8 | """ 9 | 10 | import numpy as np 11 | from astropy.io import fits 12 | 13 | from PySpectrograph.Spectrograph import * 14 | from PySpectrograph.Spectra import * 15 | 16 | 17 | def writeout(arr, outfile): 18 | # make and output the image 19 | hdu = fits.PrimaryHDU(arr) 20 | hdu.writeto(outfile) 21 | 22 | 23 | def CreateImage(spectrum, spec, xlen=None, ylen=None): 24 | """CreateImage is a task for creating a 2D spectrum of a source. 25 | It creates a fits image of the source given pertinent information about 26 | the spectrograph and spectrum 27 | 28 | spectrum--an Spectrum object with information about the spectrum to plot 29 | 30 | spec--a Spectrograph object describing a spectrograph 31 | 32 | xlen--Optional length for the spetrograph detector. If not, takes the information from spec 33 | 34 | ylen--Optional height for the spectrograph detector. If not, take the information from spec 35 | 36 | """ 37 | 38 | # Set up the detector array--this is the output image 39 | if xlen is None: 40 | xlen = int(spec.detector.width / spec.detector.xbin / spec.detector.pix_size) 41 | else: 42 | xlen = xlen 43 | 44 | if ylen is None: 45 | ylen = int(spec.detector.height / spec.detector.ybin / spec.detector.pix_size) 46 | else: 47 | ylen = ylen 48 | 49 | # set up the arrays 50 | x_arr = np.arange(xlen) 51 | y_arr = np.arange(ylen) 52 | arr = np.zeros((ylen, xlen)) 53 | 54 | # Convert the spectrum into pixel space 55 | alpha = spec.gratang 56 | beta = spec.gratang - spec.camang 57 | dw = 0.5 * 1e7 * spec.calc_resolelement(alpha, beta) 58 | dx = spec.detector.xpos / (spec.detector.xbin * spec.detector.pix_scale) 59 | dy = spec.detector.ypos / (spec.detector.ybin * spec.detector.pix_scale) 60 | dbeta = np.degrees(np.arctan(spec.detector.xbin * 61 | spec.detector.pix_size * 62 | (x_arr - 63 | 0.5 * 64 | x_arr.max() + 65 | dx) / 66 | spec.camera.focallength)) 67 | 68 | for j in y_arr: 69 | gamma = np.degrees(np.arctan(spec.detector.ybin * 70 | spec.detector.pix_size * 71 | (j - 72 | 0.5 * 73 | y_arr.max() + 74 | dy) / 75 | spec.camera.focallength)) 76 | w_arr = 1e7 * spec.calc_wavelength(alpha, beta - dbeta, gamma=gamma) 77 | arr[j, x_arr] = np.interp(w_arr, spectrum.wavelength, spectrum.set_dispersion(dw, nkern=50)) 78 | 79 | return arr 80 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | Building and installing PySpectrograph 2 | +++++++++++++++++++++++++++++ 3 | 4 | See http://code.google.com/p/pyspectrograph/ 5 | for updates of this document. 6 | 7 | .. Contents:: 8 | 9 | INTRODUCTION 10 | ============ 11 | 12 | These installation instructions have only been tested on Mac OSX and Ubuntu 13 | Linux. For the prerequisites, the software has been tested at these version of 14 | the software to work, but it may operate at earlier versions that have not been 15 | tested. 16 | 17 | PREREQUISITES 18 | ============= 19 | 20 | PySpectrograph requires the following software installed for your platform: 21 | 22 | 1) Python__ 2.5.x or newer 23 | 24 | __ http://www.python.org 25 | 26 | 2) NumPy__ 1.4.1 or newer 27 | 28 | __ http://www.numpy.org/ 29 | 30 | 3) SciPy__ 0.8.0 or newer 31 | 32 | __ http://www.scipy.org/ 33 | 34 | 4) PyFits__ 1.3 or newer 35 | 36 | __ http://www.stsci.edu/resources/software_hardware/pyfits 37 | 38 | 5) Matplotlib__ 1.0 or newer 39 | 40 | __ http://matplotlib.sourceforge.net/ 41 | 42 | 43 | GETTING PySpectrograph 44 | ====================== 45 | 46 | For the latest information, see the web site: 47 | 48 | http://code.google.com/p/pyspectrograph/ 49 | 50 | Download the current stable version (Download): 51 | ----------------------------------------------- 52 | 53 | http://code.google.com/p/pyspectrograph/ 54 | 55 | Development version from Subversion (SVN) 56 | ----------------------------------------- 57 | Use the command:: 58 | 59 | svn checkout http://pyspectrograph.googlecode.com/svn/trunk/ pyspectrograph-read-only 60 | 61 | Then type:: 62 | 63 | cd pyspectrograph 64 | rm -rf build 65 | python setup.py install 66 | 67 | INSTALLATION 68 | ============ 69 | 70 | First make sure that all PySpectrograph prerequisites are installed and working 71 | properly. 72 | 73 | From tarballs 74 | ------------- 75 | Unpack ``PySpectrograph-.tar.gz``, change to the ``PySpectrograph-/`` 76 | directory, and run 77 | :: 78 | 79 | python setup.py install 80 | 81 | You may need to be root in order to this and if so, make sure that the root python 82 | will install the package in the same directory as the user python. 83 | To install to a user-specific location instead, run:: 84 | 85 | python setup.py install --prefix=$MYDIR 86 | 87 | where $MYDIR is, for example, $HOME or $HOME/usr. 88 | 89 | ** Note 1: On Unix, you should avoid installing in /usr, but rather in 90 | /usr/local or somewhere else. /usr is generally 'owned' by your package 91 | manager, and you may overwrite a packaged PySpectrograph this way. 92 | 93 | TESTING 94 | ======= 95 | 96 | There are no tests currently available with this release, but you should be able 97 | to import the package after installation. 98 | 99 | KNOWN INSTALLATION PROBLEMS 100 | =========================== 101 | 102 | Currently None. 103 | 104 | TROUBLESHOOTING 105 | =============== 106 | 107 | If you experience problems when building/installing/testing PySpectrograph, 108 | please ask help from crawfordsm@gmail.com. 109 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_LineFit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pylab as pl 3 | from astropy.io import fits 4 | from PySpectrograph.WavelengthSolution import LineFit as LF 5 | from PySpectrograph.Models import RSSModel 6 | from PySpectrograph.Spectra import Spectrum 7 | 8 | inimage = 'fmbxpP200610180009.fits' 9 | inspectra = 'Xe.dat' 10 | 11 | 12 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 13 | wp = np.array([4500.9772, 14 | 4524.6805, 15 | 4582.7474, 16 | 4624.2757, 17 | 4671.226, 18 | 4690.9711, 19 | 4697.02, 20 | 4734.1524, 21 | 4807.019, 22 | 4829.709, 23 | 4916.51]) 24 | ep = xp * 0.0 + 0.1 25 | 26 | 27 | def test_Linefit(): 28 | 29 | hdu = fits.open(inimage) 30 | 31 | # create the data arra 32 | data = hdu[1].data 33 | 34 | # create the header information 35 | grating = hdu[1].header['GRATING'].strip() 36 | grang = hdu[1].header['GR-ANGLE'] 37 | arang = hdu[1].header['AR-ANGLE'] 38 | slit = float(hdu[1].header['MASKID']) 39 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 40 | 41 | # print instrume, grating, grang, arang, filter 42 | # print xbin, ybin 43 | # print len(data), len(data[0]) 44 | 45 | # create the RSS Model 46 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 47 | camang=arang, slit=slit, xbin=int(xbin), 48 | ybin=int(ybin)) 49 | alpha = rssmodel.rss.gratang 50 | beta = rssmodel.rss.gratang - rssmodel.rss.camang 51 | 52 | sigma = 1e7 * rssmodel.rss.calc_resolelement(alpha, beta) 53 | 54 | # create the observed spectrum 55 | midpoint = int(0.5 * len(data)) 56 | xarr = np.arange(len(data[0]), dtype='float') 57 | farr = data[midpoint, :] 58 | obs_spec = Spectrum(xarr, farr, stype='continuum') 59 | 60 | # create artificial spectrum 61 | stype = 'line' 62 | w, s = np.loadtxt(inspectra, usecols=(0, 1), unpack=True) 63 | cal_spec = Spectrum(w, s, wrange=[4000, 5000], dw=0.1, stype=stype, sigma=sigma) 64 | cal_spec.flux = cal_spec.set_dispersion(sigma=sigma) 65 | cal_spec.flux = cal_spec.flux * obs_spec.flux.max() / cal_spec.flux.max() + 1 66 | 67 | lf = LF.LineFit(obs_spec, cal_spec, function='legendre', order=3) 68 | lf.set_coef([4.23180070e+03, 2.45517852e-01, -4.46931562e-06, -2.22067766e-10]) 69 | print(lf(2000)) 70 | print(lf.obs_spec.get_flux(2000), lf.flux(2000)) 71 | print('chisq ', (lf.errfit(lf.coef, xarr, farr) ** 2).sum() / 1e7) 72 | lf.set_coef([4.23280070e+03, 2.45517852e-01, -4.46931562e-06, -2.22067766e-10]) 73 | print(lf(2000)) 74 | print(lf.obs_spec.get_flux(2000), lf.flux(2000)) 75 | print('chisq ', (lf.errfit(lf.coef, xarr, farr) ** 2).sum() / 1e7) 76 | # print lf.lfit(xarr) 77 | # print lf.coef 78 | # print lf(2000) 79 | # print lf.results 80 | 81 | pl.figure() 82 | 83 | pl.plot(lf(lf.obs_spec.wavelength), lf.obs_spec.get_flux(xarr)) 84 | pl.plot(lf.cal_spec.wavelength, lf.cal_spec.flux) 85 | pl.show() 86 | 87 | 88 | # test_LineSolution() 89 | # test_ModelSolution() 90 | test_Linefit() 91 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/WavelengthSolution.py: -------------------------------------------------------------------------------- 1 | """Wavelength Solution is a task describing the functional form for transforming 2 | pixel position to wavelength. The inputs for this task are the given pixel position 3 | and the corresponding wavelength. The user selects an input functional form and 4 | order for that form. The task then calculates the coefficients for that form. 5 | Possible options for the wavelength solution include polynomial, legendre, spline. 6 | 7 | HISTORY 8 | 20090915 SMC Initially Written by SM Crawford 9 | 10 | LIMITATIONS 11 | 20090915 SMC Need to add legendre, spline functions 12 | 13 | """ 14 | 15 | import numpy as np 16 | 17 | 18 | from .LineSolution import LineSolution 19 | from .ModelSolution import ModelSolution 20 | 21 | 22 | class WavelengthSolution: 23 | 24 | """Wavelength Solution is a task describing the functional form for transforming 25 | pixel position to wavelength. 26 | """ 27 | func_options = ['poly', 'polynomial', 'spline', 'legendre', 'chebyshev', 'model'] 28 | 29 | def __init__(self, x, w, function='poly', order=3, niter=5, thresh=3, 30 | sgraph=None, cfit='both', xlen=3162, yval=0): 31 | self.sgraph = sgraph 32 | self.function = function 33 | self.order = order 34 | self.niter = niter 35 | self.thresh = thresh 36 | self.cfit = cfit 37 | self.xlen = xlen 38 | self.yval = yval 39 | 40 | self.set_array(x, w) 41 | self.set_func() 42 | 43 | def set_array(self, x, w): 44 | self.x_arr = x 45 | self.w_arr = w 46 | 47 | def set_thresh(self, thresh): 48 | self.thresh = thresh 49 | 50 | def set_niter(self, niter): 51 | self.niter = niter 52 | 53 | def set_func(self): 54 | if self.function in ['poly', 'polynomial', 'spline', 'legendre', 'chebyshev']: 55 | self.func = LineSolution(self.x_arr, self.w_arr, function=self.function, 56 | order=self.order, niter=self.niter, thresh=self.thresh) 57 | if self.function == 'model': 58 | self.func = ModelSolution(self.x_arr, self.w_arr, sgraph=self.sgraph, 59 | xlen=self.xlen, yval=self.yval, order=self.order) 60 | 61 | def fit(self): 62 | if self.function in ['poly', 'polynomial', 'spline', 'legendre', 'chebyshev']: 63 | self.func.interfit() 64 | self.coef = self.func.coef 65 | if self.function in ['model']: 66 | self.func.fit(cfit=self.cfit) 67 | self.coef = np.array([c() for c in self.func.coef]) 68 | # self.set_coef(coef) 69 | 70 | def set_coef(self, coef): 71 | if self.function in ['poly', 'polynomial', 'spline', 'legendre', 'chebyshev']: 72 | self.func.coef = coef 73 | self.coef = self.func.coef 74 | if self.function in ['model']: 75 | for i in range(len(self.func.coef)): 76 | self.func.coef[i].set(coef[i]) 77 | self.coef = np.array([c() for c in self.func.coef]) 78 | 79 | def value(self, x): 80 | return self.func.value(x) 81 | 82 | def invvalue(self, w): 83 | """Given a wavelength, return the pixel position 84 | 85 | """ 86 | return w 87 | 88 | def sigma(self, x, y): 89 | """Return the RMS of the fit """ 90 | return (((y - self.value(x)) ** 2).mean()) ** 0.5 91 | 92 | def chisq(self, x, y, err): 93 | """Return the chi^2 of the fit""" 94 | return (((y - self.value(x)) / err) ** 2).sum() 95 | -------------------------------------------------------------------------------- /PySpectrograph/Spectra/detectlines.py: -------------------------------------------------------------------------------- 1 | """detectlines includes tasks and tools for handling 1-d spectra 2 | 3 | 4 | """ 5 | 6 | import numpy as np 7 | from PySpectrograph import SpectrographError 8 | 9 | default_kernal = [0, -1, -2, -3, -2, -1, 0, 1, 2, 3, 2, 1, 0] 10 | 11 | 12 | def centroid(xarr, yarr, kern=default_kernal, mask=None, mode='same'): 13 | """Find the centroid of a line following a similar algorithm as 14 | the center1d algorithm in IRAF. xarr and yarr should be an area 15 | around the desired feature to be centroided. The default kernal 16 | is used if the user does not specific one. 17 | 18 | The algorithm solves for the solution to the equation 19 | 20 | ..math:: \int (I-I_0) f(x-x_0) dx = 0 21 | 22 | returns xc 23 | """ 24 | if len(yarr) < len(kern): 25 | raise SpectrographError('Array has to be larger than kernal') 26 | 27 | if mask is not None: 28 | # catch the fact that at the edges it 29 | if mask.sum() < len(default_kernal): 30 | warr = np.convolve(yarr, kern, mode=mode) 31 | xc = np.interp(0, warr[mask], xarr[mask]) 32 | return xc 33 | else: 34 | yarr = yarr[mask] 35 | xarr = xarr[mask] 36 | 37 | # convle the input array with the default kernal 38 | warr = np.convolve(yarr, kern, mode=mode) 39 | 40 | # interpolate the results 41 | xc = np.interp(0, warr, xarr) 42 | 43 | return xc 44 | 45 | 46 | def detectlines(w_arr, f_arr, sigma=3, bsigma=None, niter=5, mask=None, kern=default_kernal, center=False): 47 | """Detect lines goes through a 1-D spectra and detect peaks 48 | 49 | w_arr--xaxis array (pixels, wavelength, etc) 50 | f_arr--yaxis array (flux, counts, etc) 51 | sigma--Threshold for detecting sources 52 | bsigma--Threshold for determining background statistics 53 | niter--iterations to determine background 54 | center--return centroids and not pixels 55 | mask--Pixels not to use 56 | """ 57 | # set up the variables 58 | if bsigma is None: 59 | bsigma = sigma 60 | 61 | if mask: 62 | f_arr = f_arr[mask] 63 | w_arr = w_arr[mask] 64 | 65 | # find all peaks 66 | peaks = find_peaks(f_arr, sigma, niter, bsigma=bsigma) 67 | 68 | # set the output values 69 | xp = w_arr[peaks] 70 | 71 | if center: 72 | xdiff = int(0.5 * len(kern) + 1) 73 | x_arr = np.arange(len(w_arr)) 74 | xp = xp * 1.0 75 | for i in range(len(peaks)): 76 | cmask = (abs(x_arr - peaks[i]) < xdiff) 77 | xp[i] = centroid(w_arr, f_arr, kern=kern, mask=cmask) 78 | 79 | return xp 80 | 81 | 82 | def find_peaks(f_arr, sigma, niter, bsigma=None): 83 | """Go through an ordered array and find any element which is a peak""" 84 | # set up the variables 85 | if bsigma is None: 86 | bsigma = sigma 87 | 88 | # determine the background statistics 89 | back_ave, back_std = find_backstats(f_arr, sigma, niter) 90 | 91 | # calculate the differences between the pixels 92 | dfh = f_arr[1:-1] - f_arr[:-2] 93 | dfl = f_arr[1:-1] - f_arr[2:] 94 | 95 | # find the objects 96 | mask = (dfh > 0) * (dfl > 0) * (abs(f_arr[1:-1] - back_ave) > back_std * sigma) 97 | t = np.where(mask)[0] 98 | return t + 1 99 | 100 | 101 | def find_backstats(f_arr, sigma, niter): 102 | """Iteratively calculate the statistics of an array""" 103 | ave = f_arr.mean() 104 | std = f_arr.std() 105 | for i in range(niter): 106 | mask = (abs(f_arr - ave) < sigma * std) 107 | ave = f_arr[mask].mean() 108 | std = f_arr[mask].std() 109 | return ave, std 110 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/Xe.dat: -------------------------------------------------------------------------------- 1 | # Xenon penray linelist 2 | # khn 26 March, 2005 3 | # NIST and Striganov & Sventitskii (1968) (SS68) 4 | # lines actually seen in PFIS Xenon penray 5 | #----------------------------------------------------------------------- 6 | # 3400.07 2 NIST 7 | # 3418.37 2 NIST 8 | # 3420.00 2 NIST 9 | # 3442.66 3 NIST 10 | # 3469.81 4 NIST 11 | # 3472.36 4 NIST 12 | # 3506.74 5 NIST 13 | # 3549.86 10 NIST 14 | # 3554.04 10 NIST 15 | # 3610.32 15 NIST 16 | # 3613.06 8 NIST 17 | # 3633.06 6 NIST 18 | # 3669.91 10 NIST 19 | # 3685.90 40 NIST 20 | # 3693.49 40 NIST 21 | #3950.925 120 SS68 22 | #3967.541 200 SS68 23 | #4078.8207 100 SS68 24 | #4109.7093 60 SS68 25 | #4116.1151 80 SS68 26 | #4193.5296 150 SS68 27 | #4203.6945 50 SS68 28 | 4383.9092 100 SS68 29 | 4385.7693 70 SS68 30 | 4500.9772 500 SS68 31 | # 4501.0 3 NIST 32 | 4524.6805 400 SS68 33 | # 4524.7 5 NIST 34 | 4582.7474 300 SS68 35 | #4611.8896 100 SS68 36 | 4624.2757 1000 SS68 37 | # 4624.3 5 NIST 38 | 4671.226 2000 SS68 39 | # 4671.2 7 NIST 40 | 4690.9711 100 SS68 41 | 4697.020 300 SS68 42 | 4734.1524 600 SS68 43 | # 4734.152 600 NIST 44 | 4792.6192 150 SS68 45 | # 4792.619 150 NIST 46 | 4807.019 500 SS68 47 | # 4807.02 500 NIST 48 | 4829.709 400 SS68 49 | # 4829.71 400 NIST 50 | 4843.294 300 SS68 51 | # 4843.29 300 NIST 52 | 4916.51 500 NIST 53 | 4923.152 500 NIST 54 | #5028.280 200 NIST 55 | # 5392.80 100 NIST 56 | # 5566.62 100 NIST 57 | # 5695.75 100 NIST 58 | # 5823.89 300 NIST 59 | # 5824.80 150 NIST 60 | # 5875.02 100 NIST 61 | # 5894.99 100 NIST 62 | # 5934.17 100 NIST 63 | # 6178.30 150 NIST 64 | # 6179.66 120 NIST 65 | # 6182.42 300 NIST 66 | # 6198.26 100 NIST 67 | # 6286.01 100 NIST 68 | # 6318.06 500 NIST 69 | # 6469.70 300 NIST 70 | # 6472.84 150 NIST 71 | # 6487.76 120 NIST 72 | # 6498.72 100 NIST 73 | # 6504.18 200 NIST 74 | # 6533.16 100 NIST 75 | # 6595.56 100 NIST 76 | # 6668.92 150 NIST 77 | # 6728.01 200 NIST 78 | # 6827.32 200 NIST 79 | # 6872.11 100 NIST 80 | # 6882.16 300 NIST 81 | # 6925.53 100 NIST 82 | # 6976.18 100 NIST 83 | # 7119.60 500 NIST 84 | # 7386.00 100 NIST 85 | # 7393.79 150 NIST 86 | # 7584.68 200 NIST 87 | # 7642.02 500 NIST 88 | # 7643.91 100 NIST 89 | # 7802.65 100 NIST 90 | # 7881.32 100 NIST 91 | # 7887.40 300 NIST 92 | # 7967.34 500 NIST 93 | # 8029.67 100 NIST 94 | # 8057.26 200 NIST 95 | # 8061.34 150 NIST 96 | # 8101.98 100 NIST 97 | # 8171.02 100 NIST 98 | # 8206.34 700 NIST 99 | # 8231.635 10000 NIST 100 | # 8266.52 500 NIST 101 | # 8280.116 7000 NIST 102 | # 8346.82 2000 NIST 103 | # 8409.19 2000 NIST 104 | # 8576.01 200 NIST 105 | # 8648.54 250 NIST 106 | # 8692.20 100 NIST 107 | # 8696.86 200 NIST 108 | # 8739.39 300 NIST 109 | # 8758.20 100 NIST 110 | # 8819.41 5000 NIST 111 | # 8862.32 300 NIST 112 | # 8908.73 200 NIST 113 | # 8930.83 200 NIST 114 | # 8952.25 1000 NIST 115 | # 8981.05 100 NIST 116 | # 8987.57 200 NIST 117 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_WavelengthSolution.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pylab as pl 3 | 4 | from astropy.io import fits 5 | 6 | from PySpectrograph.WavelengthSolution import WavelengthSolution as WS 7 | from PySpectrograph.Models import RSSModel 8 | from PySpectrograph.Spectra import Spectrum 9 | 10 | inimage = 'fmbxpP200610180009.fits' 11 | inspectra = 'Xe.dat' 12 | 13 | 14 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 15 | wp = np.array([4500.9772, 16 | 4524.6805, 17 | 4582.7474, 18 | 4624.2757, 19 | 4671.226, 20 | 4690.9711, 21 | 4697.02, 22 | 4734.1524, 23 | 4807.019, 24 | 4829.709, 25 | 4916.51]) 26 | ep = xp * 0.0 + 0.1 27 | 28 | 29 | def test_LineSolution(): 30 | ws = WS.WavelengthSolution(xp, wp, function='poly') 31 | ws.fit() 32 | print(ws.func.coef) 33 | print(ws.value(2000)) 34 | print(ws.sigma(ws.func.x, ws.func.y)) 35 | print(ws.chisq(ws.func.x, ws.func.y, ws.func.yerr)) 36 | 37 | pl.figure() 38 | pl.plot(xp, wp - ws.value(xp), ls='', marker='o') 39 | # pl.plot(ls.x, ls.y-ls(ls.x), ls='', marker='o') 40 | pl.show() 41 | return 42 | 43 | 44 | def test_ModelSolution(): 45 | 46 | hdu = fits.open(inimage) 47 | 48 | # create the data arra 49 | data = hdu[1].data 50 | 51 | # create the header information 52 | grating = hdu[1].header['GRATING'].strip() 53 | grang = hdu[1].header['GR-ANGLE'] 54 | arang = hdu[1].header['AR-ANGLE'] 55 | slit = float(hdu[1].header['MASKID']) 56 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 57 | 58 | # print instrume, grating, grang, arang, filter 59 | # print xbin, ybin 60 | # print len(data), len(data[0]) 61 | 62 | # create the RSS Model 63 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 64 | camang=arang, slit=slit, xbin=int(xbin), 65 | ybin=int(ybin)) 66 | 67 | xarr = np.arange(len(data[0]), dtype='int64') 68 | rss = rssmodel.rss 69 | alpha = rss.gratang 70 | beta = rss.gratang - rss.camang 71 | d = rss.detector.xbin * rss.detector.pix_size * (xarr - 0.5 * len(xarr)) 72 | dbeta = np.degrees(np.arctan(d / rss.camera.focallength)) 73 | y = 1e7 * rss.calc_wavelength(alpha, beta - dbeta) 74 | 75 | # ws=WS.WavelengthSolution(xp, wp, function='model', sgraph=rssmodel.rss, xlen=len(data[0]), order=4, cfit='all') 76 | ws = WS.WavelengthSolution( 77 | xarr, 78 | y, 79 | function='model', 80 | sgraph=rssmodel.rss, 81 | xlen=len( 82 | data[0]), 83 | order=4, 84 | cfit='ndcoef') 85 | 86 | # ws=WS.WavelengthSolution(xarr, y, function='poly', order=3) 87 | # ws=WS.WavelengthSolution(xp, wp, function='poly', order=3) 88 | 89 | ws.fit() 90 | print(ws.coef) 91 | print(ws.func.result) 92 | for c in ws.func.spcoef: 93 | print(c()) 94 | for c in ws.func.ndcoef: 95 | print(c()) 96 | print(ws.value(2000)) 97 | print(ws.sigma(xp, wp)) 98 | print(ws.chisq(xp, wp, ep)) 99 | 100 | pl.figure() 101 | # pl.plot(xarr,y) 102 | # pl.plot(xarr, ws.value(xarr)) 103 | # pl.plot(xp, wp-ws.value(xp), ls='', marker='o') 104 | pl.plot(xarr, y - ws.value(xarr)) 105 | # pl.plot(ls.x, ls.y-ls(ls.x), ls='', marker='o') 106 | pl.show() 107 | 108 | 109 | def test_Linefit(): 110 | 111 | hdu = fits.open(inimage) 112 | 113 | # create the header information 114 | grating = hdu[1].header['GRATING'].strip() 115 | grang = hdu[1].header['GR-ANGLE'] 116 | arang = hdu[1].header['AR-ANGLE'] 117 | slit = float(hdu[1].header['MASKID']) 118 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 119 | 120 | # print instrume, grating, grang, arang, filter 121 | # print xbin, ybin 122 | # print len(data), len(data[0]) 123 | 124 | # create the RSS Model 125 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 126 | camang=arang, slit=slit, xbin=int(xbin), 127 | ybin=int(ybin)) 128 | alpha = rssmodel.rss.gratang 129 | beta = rssmodel.rss.gratang - rssmodel.rss.camang 130 | 131 | sigma = 1e7 * rssmodel.rss.calc_resolelement(alpha, beta) 132 | 133 | # create artificial spectrum 134 | # create the spectrum 135 | stype = 'line' 136 | w, s = np.loadtxt(inspectra, usecols=(0, 1), unpack=True) 137 | spec = Spectrum(w, s, wrange=[4000, 5000], dw=0.1, stype=stype, sigma=sigma) 138 | spec.flux = spec.set_dispersion(sigma=sigma) 139 | 140 | 141 | # test_LineSolution() 142 | # test_ModelSolution() 143 | test_Linefit() 144 | -------------------------------------------------------------------------------- /PySpectrograph/unit_tests/ut_ModelFit.py: -------------------------------------------------------------------------------- 1 | """Unit Test for LineSolution. LineSolution will calculate the 2 | best spectrograph design given an input spectrograph and spectrum 3 | 4 | """ 5 | import numpy as np 6 | from astropy.io import fits 7 | from LineSolution import LineSolution 8 | import RSSModel 9 | import Spectrum 10 | 11 | import pylab as pl 12 | 13 | inimage = 'fmbxpP200610180009.fits' 14 | inspectra = 'Xe.dat' 15 | 16 | xp = np.array([1134.87, 1239.11, 1498.22, 1687.21, 1904.49, 1997.15, 2025.77, 2202.59, 2559.72, 2673.74, 3124.34]) 17 | wp = np.array([4500.9772, 18 | 4524.6805, 19 | 4582.7474, 20 | 4624.2757, 21 | 4671.226, 22 | 4690.9711, 23 | 4697.02, 24 | 4734.1524, 25 | 4807.019, 26 | 4829.709, 27 | 4916.51]) 28 | 29 | 30 | def test_imagesolution(): 31 | # load the image and determine its spectrograph parameters 32 | hdu = fits.open(inimage) 33 | 34 | # create the data arra 35 | data = hdu[1].data 36 | 37 | # create the header information 38 | instrume = hdu[1].header['INSTRUME'].strip() 39 | grating = hdu[1].header['GRATING'].strip() 40 | grang = hdu[1].header['GR-ANGLE'] 41 | arang = hdu[1].header['AR-ANGLE'] 42 | filter = hdu[1].header['FILTER'].strip() 43 | slit = float(hdu[1].header['MASKID']) 44 | xbin, ybin = hdu[1].header['CCDSUM'].strip().split() 45 | 46 | print(instrume, grating, grang, arang, filter) 47 | print(xbin, ybin) 48 | 49 | # create the RSS Model 50 | rssmodel = RSSModel.RSSModel(grating_name=grating, gratang=grang, 51 | camang=arang, slit=slit, xbin=int(xbin), 52 | ybin=int(ybin)) 53 | alpha = rssmodel.rss.gratang 54 | beta = rssmodel.rss.gratang - rssmodel.rss.camang 55 | 56 | sigma = 1e7 * rssmodel.rss.calc_resolelement(alpha, beta) 57 | 58 | # create the spectrum 59 | stype = 'line' 60 | w, s = np.loadtxt(inspectra, usecols=(0, 1), unpack=True) 61 | spec = Spectrum.Spectrum(w, s, wrange=[4000, 5000], dw=0.1, stype=stype, sigma=sigma) 62 | # spec.flux=spec.set_dispersion(sigma=sigma) 63 | 64 | # Now having the model and the data, set up the variables 65 | j = int(len(data) / 2) 66 | xlen = len(data[0]) 67 | xarr = np.arange(len(data[0])) 68 | farr = data[j, :] 69 | var = abs(farr) + farr.mean() 70 | var = var * (farr > 1000) + 1 71 | 72 | if 1: 73 | imsol = LineSolution(rssmodel.rss, xarr=xarr, farr=farr, spectrum=spec, yval=0, var=var, order=2) 74 | output = imsol.fit(imsol.makeflux, imsol.coef, imsol.xarr, imsol.farr, imsol.var) 75 | # imsol.fit(imsol.makeflux, imsol.ndcoef, imsol.xarr, imsol.farr, imsol.var) 76 | # imsol.fit(imsol.makeflux, imsol.coef, imsol.xarr, imsol.farr, imsol.var) 77 | # for i in range(len(imsol.coef)): 78 | # print imsol.coef[i]() 79 | # for i in range(len(imsol.ndcoef)): 80 | # print imsol.ndcoef[i]() 81 | 82 | print(output.beta) 83 | print(imsol.value(output.beta, 500)) 84 | 85 | # check the results 86 | warr = imsol.value(output.beta, imsol.xarr) 87 | print((wp - imsol.value(output.beta, xp)).mean(), (wp - imsol.value(output.beta, xp)).std()) 88 | pl.figure() 89 | pl.plot(imsol.spectrum.wavelength, imsol.spectrum.flux * imsol.farr.max() / imsol.spectrum.flux.max()) 90 | pl.plot(warr, imsol.farr) 91 | # pl.plot(xp, wp-imsol.value(xp), ls='', marker='o') 92 | pl.show() 93 | 94 | # okay now test the results for a purely matched lines 95 | fp = xp * 0.0 + 2.0 96 | var = xp * 0.0 + 1.0 97 | xspec = Spectrum.Spectrum(xp, fp, dw=0.1, stype='line', sigma=sigma) 98 | wspec = Spectrum.Spectrum(wp, fp, wrange=[4000, 5000], dw=0.1, stype='line', sigma=sigma) 99 | 100 | imsol = LineSolution(rssmodel.rss, xarr=xspec.wavelength, farr=xspec.flux, spectrum=wspec, yval=0, var=var, order=3) 101 | imsol.xlen = xlen 102 | output = imsol.fit(imsol.value, imsol.coef, xp, wp, var) 103 | print(output.beta) 104 | # imsol.fit(imsol.value, imsol.ndcoef, xp, wp, var) 105 | # for i in range(len(imsol.coef)): 106 | # print imsol.coef[i]) 107 | # for i in range(len(imsol.ndcoef)): 108 | # print imsol.ndcoef[i]() 109 | 110 | print(imsol.value(output.beta, 500)) 111 | print((wp - imsol.value(output.beta, xp)).mean(), (wp - imsol.value(output.beta, xp)).std()) 112 | 113 | # check the results 114 | warr = imsol.value(output.beta, xarr) 115 | pl.figure() 116 | pl.plot(spec.wavelength, spec.flux * farr.max() / spec.flux.max()) 117 | pl.plot(warr, farr) 118 | # pl.plot(xp, wp-imsol.value(output.beta, xp), ls='', marker='o') 119 | pl.show() 120 | 121 | 122 | test_imagesolution() 123 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/ImageSolution.py: -------------------------------------------------------------------------------- 1 | """ImageSolution is a task describing the functional form for fitting an 2 | spectrograph model to an 2-D spectroscopic data. The model will output 3 | the best fit spectrograph to the observed data 4 | 5 | In the Spectrograph model, the three things that we can alter are the 6 | x position, the y position, and focus. 7 | 8 | HISTORY 9 | 20100614 SMC Initially Written by SM Crawford 10 | 11 | LIMITATIONSa 12 | 20101001 Add the ability to alter n--the index of refraction 13 | 14 | """ 15 | 16 | import numpy as np 17 | 18 | import fit as ft 19 | 20 | import pylab as pl 21 | 22 | 23 | class fitimage: 24 | 25 | """Fit legendre polynomials to a function""" 26 | 27 | def __init__(self, order=3): 28 | self.order = order 29 | self.setcoef() 30 | 31 | def setcoef(self, x=1): 32 | self.coef = [] 33 | for i in range(self.order): 34 | try: 35 | self.coef.append(ft.Parameter(x[i])) 36 | except TypeError: 37 | self.coef.append(ft.Parameter(x)) 38 | 39 | def legendre(self, x): 40 | v = 0 41 | for i in range(self.order): 42 | v += self.coef[i]() * self.legendre(i)(x) 43 | return v 44 | 45 | def fit(self, data, x=None, var=None): 46 | return ft.fit(self.legendre, self.coef, data, x=x, var=var) 47 | 48 | 49 | class ImageSolution: 50 | 51 | """ImageSolution is a task describing the functional form for transforming 52 | pixel position to wavelength for a 2-D image using a model for a 53 | spectrograph. 54 | 55 | data--2-D array to be fit 56 | """ 57 | 58 | def __init__(self, data, sgraph, spectrum): 59 | 60 | # set up the variables 61 | self.data = data 62 | self.sgraph = sgraph 63 | self.spectrum = spectrum 64 | 65 | # set up the sizes 66 | self.ylen = len(self.data) 67 | self.xlen = len(self.data[0]) 68 | 69 | # set the parameters 70 | self.xpos = ft.Parameter(self.sgraph.detector.xpos) 71 | self.ypos = ft.Parameter(self.sgraph.detector.ypos) 72 | self.fcam = ft.Parameter(self.sgraph.camera.focallength) 73 | self.nd0 = ft.Parameter(1) 74 | self.nd1 = ft.Parameter(0.00) 75 | self.nd2 = ft.Parameter(0.00) 76 | # self.xpos=ft.Parameter(2*0.015*8.169) 77 | # self.fcam=ft.Parameter(327.85) 78 | # self.coef=[self.xpos, self.ypos, self.fcam] 79 | self.coef = [self.nd0, self.nd1, self.nd2] 80 | # print self.sgraph.gratingequation 81 | 82 | j = int(self.ylen / 2) 83 | xarr = np.arange(self.xlen) 84 | yarr = np.arange(self.ylen) 85 | self.data1 = self.data[j, :] 86 | self.err1 = abs(self.data1) + self.data1.mean() 87 | # print self.makewave(500) 88 | # print self.xpos(), self.ypos(), self.fcam(), self.nd0() 89 | # print self.nd0(), self.nd1(), self.nd2() 90 | # print (self.data1-self.makeflux(xarr)).sum() 91 | self.fit(self.makeflux, self.coef, self.data1, var=self.err1) 92 | 93 | warr = self.makewave(xarr) 94 | # print self.makewave(500) 95 | # print self.xpos(), self.ypos(), self.fcam() 96 | # print self.nd0(), self.nd1(), self.nd2() 97 | # print (self.data1-self.makeflux(xarr)).sum() 98 | 99 | # check the results 100 | pl.figure() 101 | pl.plot(self.spectrum.wavelength, self.spectrum.flux * self.data.max() / self.spectrum.flux.max()) 102 | pl.plot(warr, self.data[j, :]) 103 | pl.show() 104 | 105 | def makend(self, x): 106 | return self.nd0() + self.nd1() * x + self.nd2() * x ** 2 107 | 108 | def makewave(self, x): 109 | dx = self.xpos() 110 | dy = self.ypos() 111 | alpha = self.sgraph.gratang 112 | beta = self.sgraph.gratang - self.sgraph.camang 113 | dw = 0.5 * 1e7 * self.sgraph.calc_resolelement(alpha, beta) 114 | # dx=self.sgraph.detector.xpos/(self.sgraph.detector.xbin*self.sgraph.detector.pix_scale) 115 | # dy=self.sgraph.detector.ypos/(self.sgraph.detector.ybin*self.sgraph.detector.pix_scale) 116 | dbeta = np.degrees(np.arctan(self.sgraph.detector.xbin * 117 | self.sgraph.detector.pix_size * 118 | (x - 119 | 0.5 * 120 | self.xlen + 121 | dx) / 122 | self.fcam())) 123 | gamma = np.degrees(np.arctan(self.sgraph.detector.ybin * self.sgraph.detector.pix_size * (dy) / self.fcam())) 124 | return 1e7 * self.sgraph.calc_wavelength(alpha, beta - dbeta, gamma=gamma) * self.makend(x) 125 | 126 | def makeflux(self, x): 127 | return self.data.max() / self.spectrum.flux.max() * self.spectrum.get_flux(self.makewave(x)) 128 | 129 | def fit(self, func, coef, data, var): 130 | return ft.fit(func, coef, data, var=var) 131 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | #This is needed with git because git doesn't create a dir if it's empty 18 | $(shell [ -d "_static" ] || mkdir -p _static) 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " changes to make an overview of all changed/added/deprecated items" 36 | @echo " linkcheck to check all external links for integrity" 37 | 38 | clean: 39 | -rm -rf $(BUILDDIR) 40 | -rm -rf api 41 | -rm -rf generated 42 | 43 | html: 44 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 45 | @echo 46 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 47 | 48 | dirhtml: 49 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 50 | @echo 51 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 52 | 53 | singlehtml: 54 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 55 | @echo 56 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 57 | 58 | pickle: 59 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 60 | @echo 61 | @echo "Build finished; now you can process the pickle files." 62 | 63 | json: 64 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 65 | @echo 66 | @echo "Build finished; now you can process the JSON files." 67 | 68 | htmlhelp: 69 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 70 | @echo 71 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 72 | ".hhp project file in $(BUILDDIR)/htmlhelp." 73 | 74 | qthelp: 75 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 76 | @echo 77 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 78 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 79 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp" 80 | @echo "To view the help file:" 81 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc" 82 | 83 | devhelp: 84 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 85 | @echo 86 | @echo "Build finished." 87 | @echo "To view the help file:" 88 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy" 89 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy" 90 | @echo "# devhelp" 91 | 92 | epub: 93 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 94 | @echo 95 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 96 | 97 | latex: 98 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 99 | @echo 100 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 101 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 102 | "(use \`make latexpdf' here to do that automatically)." 103 | 104 | latexpdf: 105 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 106 | @echo "Running LaTeX files through pdflatex..." 107 | make -C $(BUILDDIR)/latex all-pdf 108 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 109 | 110 | text: 111 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 112 | @echo 113 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 114 | 115 | man: 116 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 117 | @echo 118 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 119 | 120 | changes: 121 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 122 | @echo 123 | @echo "The overview file is in $(BUILDDIR)/changes." 124 | 125 | linkcheck: 126 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 127 | @echo 128 | @echo "Link check complete; look for any errors in the above output " \ 129 | "or in $(BUILDDIR)/linkcheck/output.txt." 130 | 131 | doctest: 132 | @echo "Run 'python setup.py test' in the root directory to run doctests " \ 133 | @echo "in the documentation." 134 | -------------------------------------------------------------------------------- /PySpectrograph/WavelengthSolution/ModelSolution.py: -------------------------------------------------------------------------------- 1 | """ModelSolution is a task describing the functional form for fitting an 2 | spectrograph model to an 1-D spectroscopic data. The model will output 3 | the best fit spectrograph to the observed data 4 | 5 | In the Spectrograph model, the three things that we can alter are the 6 | x position, the y position, and focus. 7 | 8 | HISTORY 9 | 20100614 SMC Initially Written by SM Crawford 10 | 11 | LIMITATIONSa 12 | 20101001 Add the ability to alter n--the index of refraction 13 | 14 | """ 15 | 16 | import numpy as np 17 | from PySpectrograph.Utilities import fit as ft 18 | 19 | 20 | class ModelSolution: 21 | 22 | """ModelSolution is a task describing the functional form for transforming 23 | pixel position to wavelength for a 2-D image using a model for a 24 | spectrograph. 25 | 26 | xarr--pixel position of the spectrum 27 | farr--the flux values for the spectrum 28 | sgraph--model of a spectrograph 29 | spectrum--model spectrum 30 | yval--offset on the ccd in the y-direction 31 | var--array with the variance values 32 | """ 33 | 34 | def __init__(self, x, y, sgraph, xlen=3162, yval=0, order=3): 35 | 36 | # set up the variables 37 | self.x = x 38 | self.y = y 39 | self.sgraph = sgraph 40 | self.yval = yval 41 | self.xlen = xlen 42 | self.order = order 43 | self.function = 'model' 44 | 45 | # set the parameters 46 | self.reset_coef() 47 | 48 | def reset_coef(self): 49 | xpos = self.sgraph.detector.xpos 50 | ypos = self.yval + self.sgraph.detector.ypos 51 | fcam = self.sgraph.camera.focallength 52 | self.set_spcoef(fcam, xpos, ypos) 53 | self.spcoef = [self.fcam, self.xpos, self.ypos] 54 | self.set_ndcoef() 55 | 56 | # the total coefficient 57 | self.set_coef() 58 | 59 | def set_spcoef(self, fcam, xpos, ypos): 60 | self.fcam = ft.Parameter(fcam) 61 | self.xpos = ft.Parameter(xpos) 62 | self.ypos = ft.Parameter(ypos) 63 | self.spcoef = [self.fcam, self.xpos, self.ypos] 64 | 65 | def set_coef(self): 66 | self.coef = self.spcoef + self.ndcoef 67 | 68 | def set_ndcoef(self, x=[1.00, 0.00, 0.00]): 69 | self.ndcoef = [] 70 | for i in range(self.order): 71 | try: 72 | self.ndcoef.append(ft.Parameter(x[i])) 73 | except BaseException: 74 | self.ndcoef.append(ft.Parameter(0.0)) 75 | 76 | def set_xarr(self, xarr): 77 | self.xarr = xarr 78 | if self.xarr is None: 79 | npix = self.sgraph.detector.width / (self.sgraph.detector.pix_size * self.sgraph.detector.xbin) 80 | self.xarr = np.arange(npix) 81 | self.xlen = len(self.xarr) 82 | else: 83 | self.xlen = len(self.xarr) 84 | 85 | def nd(self, x): 86 | v = 0 87 | for i in range(self.order): 88 | v += self.ndcoef[i]() * x ** i 89 | return v 90 | 91 | def value(self, x): 92 | """Return the wavelength value at x due to the model and current values for the model""" 93 | # these are just left here for the record 94 | # dx=self.sgraph.detector.xpos/(self.sgraph.detector.xbin*self.sgraph.detector.pix_scale) 95 | # dy=self.sgraph.detector.ypos/(self.sgraph.detector.ybin*self.sgraph.detector.pix_scale) 96 | fcam = self.spcoef[0]() 97 | dx = self.spcoef[1]() 98 | dy = self.spcoef[2]() 99 | alpha = self.sgraph.gratang 100 | beta = self.sgraph.gratang - self.sgraph.camang 101 | dw = 0.5 * 1e7 * self.sgraph.calc_resolelement(alpha, beta) 102 | dbeta = np.degrees( 103 | np.arctan(self.sgraph.detector.xbin * self.sgraph.detector.pix_size * (x - 0.5 * self.xlen + dx) / fcam)) 104 | gamma = np.degrees(np.arctan(self.sgraph.detector.ybin * self.sgraph.detector.pix_size * (dy) / fcam)) 105 | return 1e7 * self.sgraph.calc_wavelength(alpha, beta - dbeta, gamma=gamma) * self.nd(x) 106 | 107 | def erf(self, x, y): 108 | return(y - self.value(x)) 109 | 110 | def fit_coef(self, coef): 111 | self.result = ft.fit(self.value, coef, self.y, x=self.x) 112 | 113 | def fit(self, cfit='all'): 114 | """Fit can be set to be several different possibilites: 115 | 116 | pscoef--only fit xpos, ypos, fcam 117 | ndcoef--only fit the index of refraction 118 | all--fit coef and then fit ndcoef 119 | both--fit coef and ndcoef 120 | """ 121 | if cfit in ['pscoef', 'all']: 122 | self.fit_coef(self.spcoef) 123 | if cfit in ['ndcoef', 'all']: 124 | self.fit_coef(self.ndcoef) 125 | if cfit in ['both']: 126 | self.fit_coef(self.coef) 127 | # update the total coefficient 128 | self.set_coef() 129 | 130 | def sigma(self, x, y): 131 | """Return the RMS of the fit """ 132 | return (((y - self.value(x)) ** 2).mean()) ** 0.5 133 | 134 | def chisq(self, x, y, err): 135 | """Return the chi^2 of the fit""" 136 | return (((y - self.value(x)) / err) ** 2).sum() 137 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Astropy.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Astropy.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 3 | 4 | import glob 5 | import os 6 | import sys 7 | 8 | import ah_bootstrap 9 | from setuptools import setup 10 | 11 | # A dirty hack to get around some early import/configurations ambiguities 12 | if sys.version_info[0] >= 3: 13 | import builtins 14 | else: 15 | import __builtin__ as builtins 16 | builtins._ASTROPY_SETUP_ = True 17 | 18 | from astropy_helpers.setup_helpers import (register_commands, get_debug_option, 19 | get_package_info) 20 | from astropy_helpers.git_helpers import get_git_devstr 21 | from astropy_helpers.version_helpers import generate_version_py 22 | 23 | # Get some values from the setup.cfg 24 | try: 25 | from ConfigParser import ConfigParser 26 | except ImportError: 27 | from configparser import ConfigParser 28 | 29 | conf = ConfigParser() 30 | conf.read(['setup.cfg']) 31 | metadata = dict(conf.items('metadata')) 32 | 33 | PACKAGENAME = metadata.get('package_name', 'packagename') 34 | DESCRIPTION = metadata.get('description', 'packagename') 35 | AUTHOR = metadata.get('author', 'Astropy Developers') 36 | AUTHOR_EMAIL = metadata.get('author_email', '') 37 | LICENSE = metadata.get('license', 'unknown') 38 | URL = metadata.get('url', 'http://astropy.org') 39 | 40 | # order of priority for long_description: 41 | # (1) set in setup.cfg, 42 | # (2) load LONG_DESCRIPTION.rst, 43 | # (3) load README.rst, 44 | # (4) package docstring 45 | readme_glob = 'README*' 46 | _cfg_long_description = metadata.get('long_description', '') 47 | if _cfg_long_description: 48 | LONG_DESCRIPTION = _cfg_long_description 49 | 50 | elif os.path.exists('LONG_DESCRIPTION.rst'): 51 | with open('LONG_DESCRIPTION.rst') as f: 52 | LONG_DESCRIPTION = f.read() 53 | 54 | elif len(glob.glob(readme_glob)) > 0: 55 | with open(glob.glob(readme_glob)[0]) as f: 56 | LONG_DESCRIPTION = f.read() 57 | 58 | else: 59 | # Get the long description from the package's docstring 60 | __import__(PACKAGENAME) 61 | package = sys.modules[PACKAGENAME] 62 | LONG_DESCRIPTION = package.__doc__ 63 | 64 | # Store the package name in a built-in variable so it's easy 65 | # to get from other parts of the setup infrastructure 66 | builtins._ASTROPY_PACKAGE_NAME_ = PACKAGENAME 67 | 68 | # VERSION should be PEP440 compatible (http://www.python.org/dev/peps/pep-0440) 69 | VERSION = metadata.get('version', '0.4.dev0') 70 | 71 | # Indicates if this version is a release version 72 | RELEASE = 'dev' not in VERSION 73 | 74 | if not RELEASE: 75 | VERSION += get_git_devstr(False) 76 | 77 | # Populate the dict of setup command overrides; this should be done before 78 | # invoking any other functionality from distutils since it can potentially 79 | # modify distutils' behavior. 80 | cmdclassd = register_commands(PACKAGENAME, VERSION, RELEASE) 81 | 82 | # Freeze build information in version.py 83 | generate_version_py(PACKAGENAME, VERSION, RELEASE, 84 | get_debug_option(PACKAGENAME)) 85 | 86 | # Treat everything in scripts except README* as a script to be installed 87 | scripts = [fname for fname in glob.glob(os.path.join('scripts', '*')) 88 | if not os.path.basename(fname).startswith('README')] 89 | 90 | 91 | # Get configuration information from all of the various subpackages. 92 | # See the docstring for setup_helpers.update_package_files for more 93 | # details. 94 | package_info = get_package_info() 95 | 96 | # Add the project-global data 97 | package_info['package_data'].setdefault(PACKAGENAME, []) 98 | package_info['package_data'][PACKAGENAME].append('data/*') 99 | 100 | # Define entry points for command-line scripts 101 | entry_points = {'console_scripts': []} 102 | 103 | if conf.has_section('entry_points'): 104 | entry_point_list = conf.items('entry_points') 105 | for entry_point in entry_point_list: 106 | entry_points['console_scripts'].append('{0} = {1}'.format( 107 | entry_point[0], entry_point[1])) 108 | 109 | # Include all .c files, recursively, including those generated by 110 | # Cython, since we can not do this in MANIFEST.in with a "dynamic" 111 | # directory name. 112 | c_files = [] 113 | for root, dirs, files in os.walk(PACKAGENAME): 114 | for filename in files: 115 | if filename.endswith('.c'): 116 | c_files.append( 117 | os.path.join( 118 | os.path.relpath(root, PACKAGENAME), filename)) 119 | package_info['package_data'][PACKAGENAME].extend(c_files) 120 | 121 | # Note that requires and provides should not be included in the call to 122 | # ``setup``, since these are now deprecated. See this link for more details: 123 | # https://groups.google.com/forum/#!topic/astropy-dev/urYO8ckB2uM 124 | 125 | setup(name=PACKAGENAME, 126 | version=VERSION, 127 | description=DESCRIPTION, 128 | scripts=scripts, 129 | install_requires=[s.strip() for s in metadata.get('install_requires', 'astropy').split(',')], 130 | author=AUTHOR, 131 | author_email=AUTHOR_EMAIL, 132 | license=LICENSE, 133 | url=URL, 134 | long_description=LONG_DESCRIPTION, 135 | cmdclass=cmdclassd, 136 | zip_safe=False, 137 | use_2to3=False, 138 | entry_points=entry_points, 139 | **package_info 140 | ) 141 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/Spectrograph.py: -------------------------------------------------------------------------------- 1 | """Spectrograph is a class that general describes a spectrograph. This includes 2 | describing the telescope, slit, collimator, grating, camera, and detector. 3 | 4 | HISTORY 5 | 20090912 SMC First written by SM Crawford 6 | 7 | Limitations: 8 | -Still need to verify how alpha, grating angle, beta, and camera angle 9 | to see if I can hardwire some of the tasks 10 | 11 | """ 12 | 13 | 14 | import math 15 | 16 | from .SpectrographEquations import * 17 | from .Grating import Grating 18 | from .Optics import Optics 19 | from .Slit import Slit 20 | from .Detector import Detector 21 | 22 | 23 | class Spectrograph(Grating, Optics, Slit, Detector): 24 | 25 | """A class describing a spectrograph and functions 26 | related to a spectrograph. All angles are in degrees. 27 | """ 28 | 29 | def __init__(self, camang=45, gratang=45, grating=Grating(), camera=Optics(), 30 | collimator=Optics(), telescope=Optics(), slit=Slit(), 31 | detector=Detector()): 32 | 33 | # initiate the grating 34 | self.grating = grating 35 | 36 | # initiate the telescope 37 | self.telescope = telescope 38 | 39 | # initiate the collimator 40 | self.collimator = collimator 41 | 42 | # initiate the camera 43 | self.camera = camera 44 | 45 | # initiate the slit 46 | self.slit = slit 47 | 48 | # initiate the detector 49 | self.detector = detector 50 | 51 | # set up the angles in the system 52 | self.gratang = gratang 53 | self.camang = camang 54 | 55 | return 56 | 57 | def alpha(self): 58 | return self.gratang 59 | 60 | def beta(self): 61 | return self.camang - self.gratang 62 | 63 | def gamma(self): 64 | return self.gamma 65 | 66 | def calc_wavelength(self, alpha, beta, gamma=0.0, nd=n_index): 67 | """Apply the grating equation to determine the wavelength 68 | returns wavelength in mm 69 | """ 70 | w = gratingequation(self.grating.sigma, self.grating.order, self.grating.sign, alpha, beta, gamma=gamma, nd=nd) 71 | return w 72 | 73 | def calc_angdisp(self, beta): 74 | """Calculate the angular dispersion according to m/sigma/cos beta 75 | 76 | returns angular dispersion in 1/mm 77 | """ 78 | A = calc_angdisp(self.grating.sigma, self.grating.order, beta) 79 | return A 80 | 81 | def calc_lindisp(self, beta): 82 | """Calculate the linear dispersion according to f_cam * A 83 | 84 | return linear dispersion in mm/mm 85 | 86 | """ 87 | return calc_lindisp(self.camera.focallength, self.grating.sigma, self.grating.order, beta) 88 | 89 | def calc_demagspatial(self): 90 | """Calculate the spatial demagnification 91 | 92 | returns the spatial demagnification 93 | """ 94 | return calc_demagspatial(self.collimator.focallength, self.camera.focallength) 95 | 96 | def calc_demagspectral(self, alpha, beta): 97 | """Calculate the spectral demagnification 98 | 99 | returns the spectral demagnification 100 | """ 101 | return self.calc_demagspatial() / se.calc_anamorph(alpha, beta) 102 | 103 | def calc_spatslitimage(self): 104 | """Calculate the spatial extant of the slit image 105 | 106 | return in mm 107 | """ 108 | return self.slit.width / self.calc_demagspatial() 109 | 110 | def calc_specslitimage(self, beta): 111 | """Calculate the spectral extant of the slit image 112 | 113 | return in mm 114 | """ 115 | return self.slit.width * self.calc_lindisp(beta) 116 | 117 | def calc_resolelement(self, alpha, beta): 118 | """Calculate the resolution of a single element for a filled slit 119 | 120 | return the wavelength resolution in mm 121 | """ 122 | dw = calc_resolelement(self.slit.width, self.collimator.focallength, 123 | self.grating.sigma, self.grating.order, 124 | alpha, beta) 125 | return dw 126 | 127 | def calc_resolution(self, w, alpha, beta): 128 | """Calculate the resolution at a given wavelength. w/dw 129 | 130 | returns resolution 131 | """ 132 | return w / self.calc_resolelement(alpha, beta) 133 | 134 | def calc_centralwavelength(self): 135 | """Calculate the central wavlength 136 | 137 | return waveleng in mm 138 | """ 139 | return self.calc_wavelength(self.alpha(), -self.beta()) 140 | 141 | def calc_redwavelength(self): 142 | """For the detector, calculate the maximum red wavelength 143 | Assume just the width of the detector 144 | 145 | return waveleng in mm 146 | """ 147 | dbeta = math.degrees(math.atan(0.5 * self.detector.width / self.camera.focallength)) 148 | return self.calc_wavelength(self.alpha(), -self.beta() - dbeta) 149 | 150 | def calc_bluewavelength(self): 151 | """For the detector, calculate the maximum blue wavelength 152 | Assume just the width of the detector 153 | 154 | return waveleng in mm 155 | """ 156 | dbeta = math.degrees(math.atan(0.5 * self.detector.width / self.camera.focallength)) 157 | return self.calc_wavelength(self.alpha(), -self.beta() + dbeta) 158 | -------------------------------------------------------------------------------- /PySpectrograph/_astropy_init.py: -------------------------------------------------------------------------------- 1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 2 | 3 | __all__ = ['__version__', '__githash__', 'test'] 4 | 5 | # this indicates whether or not we are in the package's setup.py 6 | try: 7 | _ASTROPY_SETUP_ 8 | except NameError: 9 | from sys import version_info 10 | if version_info[0] >= 3: 11 | import builtins 12 | else: 13 | import __builtin__ as builtins 14 | builtins._ASTROPY_SETUP_ = False 15 | 16 | try: 17 | from .version import version as __version__ 18 | except ImportError: 19 | __version__ = '' 20 | try: 21 | from .version import githash as __githash__ 22 | except ImportError: 23 | __githash__ = '' 24 | 25 | 26 | # set up the test command 27 | def _get_test_runner(): 28 | import os 29 | from astropy.tests.helper import TestRunner 30 | return TestRunner(os.path.dirname(__file__)) 31 | 32 | 33 | def test(package=None, test_path=None, args=None, plugins=None, 34 | verbose=False, pastebin=None, remote_data=False, pep8=False, 35 | pdb=False, coverage=False, open_files=False, **kwargs): 36 | """ 37 | Run the tests using `py.test `__. A proper set 38 | of arguments is constructed and passed to `pytest.main`_. 39 | 40 | .. _py.test: http://pytest.org/latest/ 41 | .. _pytest.main: http://pytest.org/latest/builtin.html#pytest.main 42 | 43 | Parameters 44 | ---------- 45 | package : str, optional 46 | The name of a specific package to test, e.g. 'io.fits' or 'utils'. 47 | If nothing is specified all default tests are run. 48 | 49 | test_path : str, optional 50 | Specify location to test by path. May be a single file or 51 | directory. Must be specified absolutely or relative to the 52 | calling directory. 53 | 54 | args : str, optional 55 | Additional arguments to be passed to pytest.main_ in the ``args`` 56 | keyword argument. 57 | 58 | plugins : list, optional 59 | Plugins to be passed to pytest.main_ in the ``plugins`` keyword 60 | argument. 61 | 62 | verbose : bool, optional 63 | Convenience option to turn on verbose output from py.test_. Passing 64 | True is the same as specifying ``'-v'`` in ``args``. 65 | 66 | pastebin : {'failed','all',None}, optional 67 | Convenience option for turning on py.test_ pastebin output. Set to 68 | ``'failed'`` to upload info for failed tests, or ``'all'`` to upload 69 | info for all tests. 70 | 71 | remote_data : bool, optional 72 | Controls whether to run tests marked with @remote_data. These 73 | tests use online data and are not run by default. Set to True to 74 | run these tests. 75 | 76 | pep8 : bool, optional 77 | Turn on PEP8 checking via the `pytest-pep8 plugin 78 | `_ and disable normal 79 | tests. Same as specifying ``'--pep8 -k pep8'`` in ``args``. 80 | 81 | pdb : bool, optional 82 | Turn on PDB post-mortem analysis for failing tests. Same as 83 | specifying ``'--pdb'`` in ``args``. 84 | 85 | coverage : bool, optional 86 | Generate a test coverage report. The result will be placed in 87 | the directory htmlcov. 88 | 89 | open_files : bool, optional 90 | Fail when any tests leave files open. Off by default, because 91 | this adds extra run time to the test suite. Requires the 92 | `psutil `_ package. 93 | 94 | parallel : int, optional 95 | When provided, run the tests in parallel on the specified 96 | number of CPUs. If parallel is negative, it will use the all 97 | the cores on the machine. Requires the 98 | `pytest-xdist `_ plugin 99 | installed. Only available when using Astropy 0.3 or later. 100 | 101 | kwargs 102 | Any additional keywords passed into this function will be passed 103 | on to the astropy test runner. This allows use of test-related 104 | functionality implemented in later versions of astropy without 105 | explicitly updating the package template. 106 | 107 | """ 108 | test_runner = _get_test_runner() 109 | return test_runner.run_tests( 110 | package=package, test_path=test_path, args=args, 111 | plugins=plugins, verbose=verbose, pastebin=pastebin, 112 | remote_data=remote_data, pep8=pep8, pdb=pdb, 113 | coverage=coverage, open_files=open_files, **kwargs) 114 | 115 | if not _ASTROPY_SETUP_: # noqa 116 | import os 117 | from warnings import warn 118 | from astropy.config.configuration import ( 119 | update_default_config, 120 | ConfigurationDefaultMissingError, 121 | ConfigurationDefaultMissingWarning) 122 | 123 | # add these here so we only need to cleanup the namespace at the end 124 | config_dir = None 125 | 126 | if not os.environ.get('ASTROPY_SKIP_CONFIG_UPDATE', False): 127 | config_dir = os.path.dirname(__file__) 128 | config_template = os.path.join(config_dir, __package__ + ".cfg") 129 | if os.path.isfile(config_template): 130 | try: 131 | update_default_config( 132 | __package__, config_dir, version=__version__) 133 | except TypeError as orig_error: 134 | try: 135 | update_default_config(__package__, config_dir) 136 | except ConfigurationDefaultMissingError as e: 137 | wmsg = (e.args[0] + 138 | " Cannot install default profile. If you are " 139 | "importing from source, this is expected.") 140 | warn(ConfigurationDefaultMissingWarning(wmsg)) 141 | del e 142 | except Exception: 143 | raise orig_error 144 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PySpectrograph.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PySpectrograph.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /PySpectrograph/Spectrograph/Detector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .CCD import CCD 3 | 4 | 5 | class Detector(CCD): 6 | 7 | """A class that describing the Detector. It inherets from the CCD class as there could be 8 | multiple ccds at each position. 9 | 10 | name--Name of the detector 11 | ccd--a CCD class or list describing the CCDs in the detecfor 12 | xpos--Offset of the x center of the ccd from the central ray in mm 13 | ypos--Offset of the y center of the ccd from the central ray in mm 14 | zpos--Offset of the z center of the ccd from the central ray in mm 15 | xbin--ccd binning in x-direction 16 | ybin--ccd binning in y-direction 17 | plate_scale--plate scale in mm/" 18 | """ 19 | 20 | def __init__(self, name='', ccd=CCD(), zpos=0, xpos=0, ypos=0, xbin=2, ybin=2, plate_scale=0.224): 21 | 22 | # Set the detector up as a list of CCDs. 23 | self.detector = [] 24 | self.pix_size = None 25 | if isinstance(ccd, CCD): 26 | self.detector = [ccd] 27 | self.pix_size = ccd.pix_size 28 | elif isinstance(ccd, list): 29 | for c in ccd: 30 | if isinstance(c, CCD): 31 | self.detector.append(c) 32 | if self.pix_size: 33 | self.pix_size = min(self.pix_size, c.pix_size) 34 | else: 35 | self.pix_size = c.pix_size 36 | else: 37 | return 38 | 39 | self.nccd = len(self.detector) 40 | 41 | # set up the zero points for the detector 42 | self.name = name 43 | self.zpos = zpos 44 | self.xpos = xpos 45 | self.ypos = ypos 46 | self.xbin = xbin 47 | self.ybin = ybin 48 | self.plate_scale = plate_scale 49 | self.pix_scale = self.plate_scale / self.pix_size 50 | 51 | # check to make sure that the ccds don't overlap 52 | self.real = self.check_ccds() 53 | 54 | # determine the max width and height for the detector 55 | self.width = self.find_width() 56 | 57 | # determine the max width and height for the detector 58 | self.height = self.find_height() 59 | 60 | def check_ccds(self): 61 | """Check to make sure none of the ccds overlap""" 62 | if self.nccd <= 1: 63 | return True 64 | 65 | # loop over each ccd and check to see if any of the ccd 66 | # overlaps with the coordinates of another ccd 67 | for i in range(self.nccd): 68 | ax1, ax2, ay1, ay2 = self.detector[i].find_corners() 69 | for j in range(i + 1, self.nccd): 70 | bx1, bx2, by1, by2 = self.detector[j].find_corners() 71 | if ax1 <= bx1 < ax2 or ax1 < bx2 < ax2: 72 | if ay1 <= by1 < ay2 or ay1 < by2 < ay2: 73 | return False 74 | 75 | return True 76 | 77 | def get_xpixcenter(self): 78 | """Return the xpixel center based on the x and y position""" 79 | return int((0.5 * self.find_width() - self.xpos) / self.pix_size / self.xbin) 80 | 81 | def get_ypixcenter(self): 82 | """Return the xpixel center based on the x and y position""" 83 | return int((0.5 * self.find_height() - self.ypos) / self.pix_size / self.ybin) 84 | 85 | def find_width(self): 86 | """Loop over all the ccds in detector and find the width""" 87 | width = 0 88 | # return zero if no detector 89 | if self.nccd < 1: 90 | return width 91 | 92 | # handle a single detector 93 | width = self.detector[0].width 94 | if self.nccd == 1: 95 | return width 96 | 97 | # Loop over multipe CCDs to find the width 98 | ax1, ax2, ay1, ay2 = self.detector[0].find_corners() 99 | xmin = min(ax1, ax2) 100 | xmax = max(ax1, ax2) 101 | for ccd in self.detector[1:]: 102 | ax1, ax2, ay1, ay2 = ccd.find_corners() 103 | xmin = min(xmin, ax1, ax2) 104 | xmax = max(xmax, ax1, ax2) 105 | return xmax - xmin 106 | 107 | def find_height(self): 108 | """Loop over all the ccds in detector and find the height""" 109 | height = 0 110 | # return zero if no detector 111 | if self.nccd < 1: 112 | return height 113 | 114 | # handle a single detector 115 | height = self.detector[0].height 116 | if self.nccd == 1: 117 | return height 118 | 119 | # Loop over multipe CCDs to find the width 120 | ax1, ax2, ay1, ay2 = self.detector[0].find_corners() 121 | ymin = min(ay1, ay2) 122 | ymax = max(ay1, ay2) 123 | for ccd in self.detector[1:]: 124 | ax1, ax2, ay1, ay2 = ccd.find_corners() 125 | ymin = min(ymin, ay1, ay2) 126 | ymax = max(ymax, ay1, ay2) 127 | height = ymax - ymin 128 | return height 129 | 130 | def make_detector(self): 131 | """Given the information about the detector, return an array with values of 132 | either 1 or 0 for where the CCDs are 133 | """ 134 | 135 | # find the minimum pixel scale and set the number of pixels 136 | xps = self.xbin * self.pix_size 137 | yps = self.ybin * self.pix_size 138 | pw = round(self.width / xps) 139 | ph = round(self.height / yps) 140 | 141 | # create the array 142 | arr = np.zeros((ph, pw), dtype=float) 143 | y, x = np.indices((ph, pw)) 144 | 145 | # set up where the detectors are 146 | for ccd in self.detector: 147 | x1, x2, y1, y2 = ccd.find_corners() 148 | x1 = (x1 + 0.5 * self.width) / xps 149 | x2 = (x2 + 0.5 * self.width) / xps 150 | y1 = (y1 + 0.5 * self.height) / yps 151 | y2 = (y2 + 0.5 * self.height) / yps 152 | mask = (x1 <= x) * (x < x2) * (y1 <= y) * (y < y2) 153 | arr[mask] = 1 154 | 155 | return arr 156 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # We set the language to c because python isn't supported on the MacOS X nodes 2 | # on Travis. However, the language ends up being irrelevant anyway, since we 3 | # install Python ourselves using conda. 4 | language: c 5 | 6 | os: 7 | - linux 8 | 9 | # Setting sudo to false opts in to Travis-CI container-based builds. 10 | sudo: false 11 | 12 | # The apt packages below are needed for sphinx builds. A full list of packages 13 | # that can be included can be found here: 14 | # 15 | # https://github.com/travis-ci/apt-package-whitelist/blob/master/ubuntu-precise 16 | 17 | addons: 18 | apt: 19 | packages: 20 | - graphviz 21 | - texlive-latex-extra 22 | - dvipng 23 | 24 | env: 25 | global: 26 | 27 | # The following versions are the 'default' for tests, unless 28 | # overridden underneath. They are defined here in order to save having 29 | # to repeat them for all configurations. 30 | - PYTHON_VERSION=3.6 31 | - NUMPY_VERSION=stable 32 | - ASTROPY_VERSION=stable 33 | - MAIN_CMD='python setup.py' 34 | - SETUP_CMD='test' 35 | - PIP_DEPENDENCIES='' 36 | - EVENT_TYPE='pull_request push' 37 | 38 | 39 | # For this package-template, we include examples of Cython modules, 40 | # so Cython is required for testing. If your package does not include 41 | # Cython code, you can set CONDA_DEPENDENCIES='' 42 | - CONDA_DEPENDENCIES='scipy' 43 | 44 | # List other runtime dependencies for the package that are available as 45 | # pip packages here. 46 | # - PIP_DEPENDENCIES='' 47 | 48 | # Conda packages for affiliated packages are hosted in channel 49 | # "astropy" while builds for astropy LTS with recent numpy versions 50 | # are in astropy-ci-extras. If your package uses either of these, 51 | # add the channels to CONDA_CHANNELS along with any other channels 52 | # you want to use. 53 | - CONDA_CHANNELS='astropy-ci-extras astropy' 54 | 55 | # If there are matplotlib or other GUI tests, uncomment the following 56 | # line to use the X virtual framebuffer. 57 | # - SETUP_XVFB=True 58 | 59 | matrix: 60 | # Make sure that egg_info works without dependencies 61 | #- PYTHON_VERSION=2.7 SETUP_CMD='egg_info' 62 | #- PYTHON_VERSION=3.4 SETUP_CMD='egg_info' 63 | #- PYTHON_VERSION=3.5 SETUP_CMD='egg_info' 64 | - PYTHON_VERSION=3.6 SETUP_CMD='egg_info' 65 | 66 | matrix: 67 | 68 | # Don't wait for allowed failures 69 | fast_finish: true 70 | 71 | include: 72 | # Try MacOS X 73 | - os: osx 74 | env: SETUP_CMD='test' 75 | 76 | # Do a coverage test. 77 | - os: linux 78 | env: SETUP_CMD='test --coverage' 79 | 80 | # Check for sphinx doc build warnings - we do this first because it 81 | # may run for a long time 82 | - os: linux 83 | env: SETUP_CMD='build_docs -w' 84 | 85 | # Now try Astropy dev with the latest Python and LTS with Python 2.7 and 3.x. 86 | - os: linux 87 | env: ASTROPY_VERSION=development 88 | EVENT_TYPE='pull_request push cron' 89 | #- os: linux 90 | # env: PYTHON_VERSION=2.7 ASTROPY_VERSION=lts 91 | - os: linux 92 | env: ASTROPY_VERSION=lts 93 | 94 | # Try all python versions and Numpy versions. Since we can assume that 95 | # the Numpy developers have taken care of testing Numpy with different 96 | # versions of Python, we can vary Python and Numpy versions at the same 97 | # time. 98 | 99 | - os: linux 100 | env: NUMPY_VERSION=1.12 101 | 102 | # Try numpy pre-release 103 | - os: linux 104 | env: NUMPY_VERSION=prerelease 105 | EVENT_TYPE='pull_request push cron' 106 | 107 | # Do a PEP8 test with pycodestyle 108 | - os: linux 109 | env: MAIN_CMD='pycodestyle PySpectrograph --count' SETUP_CMD='' 110 | 111 | allow_failures: 112 | # Do a PEP8 test with pycodestyle 113 | # (allow to fail unless your code completely compliant) 114 | - os: linux 115 | env: MAIN_CMD='pycodestyle packagename --count' SETUP_CMD='' 116 | 117 | install: 118 | 119 | # We now use the ci-helpers package to set up our testing environment. 120 | # This is done by using Miniconda and then using conda and pip to install 121 | # dependencies. Which dependencies are installed using conda and pip is 122 | # determined by the CONDA_DEPENDENCIES and PIP_DEPENDENCIES variables, 123 | # which should be space-delimited lists of package names. See the README 124 | # in https://github.com/astropy/ci-helpers for information about the full 125 | # list of environment variables that can be used to customize your 126 | # environment. In some cases, ci-helpers may not offer enough flexibility 127 | # in how to install a package, in which case you can have additional 128 | # commands in the install: section below. 129 | 130 | - git clone --depth 1 git://github.com/astropy/ci-helpers.git 131 | - source ci-helpers/travis/setup_conda.sh 132 | 133 | # As described above, using ci-helpers, you should be able to set up an 134 | # environment with dependencies installed using conda and pip, but in some 135 | # cases this may not provide enough flexibility in how to install a 136 | # specific dependency (and it will not be able to install non-Python 137 | # dependencies). Therefore, you can also include commands below (as 138 | # well as at the start of the install section or in the before_install 139 | # section if they are needed before setting up conda) to install any 140 | # other dependencies. 141 | 142 | script: 143 | - $MAIN_CMD $SETUP_CMD 144 | 145 | after_success: 146 | # If coveralls.io is set up for this package, uncomment the line below. 147 | # The coveragerc file may be customized as needed for your package. 148 | # - if [[ $SETUP_CMD == *coverage* ]]; then coveralls --rcfile='packagename/tests/coveragerc'; fi 149 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PySpectrograph.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PySpectrograph.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PySpectrograph" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PySpectrograph" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /PySpectrograph/Models/RSSModel.py: -------------------------------------------------------------------------------- 1 | """RSSmodel is a class that describes the optical arm of the Robert Stobie 2 | Spectrograph. This model inherents from Spectragraph. The RSS is currently 3 | described by the grating, grating angle, and camera angle. All other components 4 | are fixed. 5 | 6 | 20090913 SMC First version 7 | 20120325 SMC -Update to include deviations in the as built spectrograph in the grating angle 8 | -include the most up to date chip geometry 9 | 20120511 SMC -Updated to inherit directly from Spectrograph 10 | -Included get_wavelength 11 | 12 | Limitations: 13 | -Set the geometry of the CCDs using the current geometry file instead of the 14 | default given here 15 | 16 | """ 17 | 18 | import numpy as np 19 | 20 | from PySpectrograph.Spectrograph import Spectrograph, Grating, Optics, CCD, Detector, Slit 21 | 22 | 23 | class RSSError(Exception): 24 | pass 25 | 26 | 27 | class RSSModel (Spectrograph): 28 | 29 | """A model describing the RSS spectrograph""" 30 | 31 | def __init__(self, grating_name='None', gratang=45, camang=45, slit=1.0, 32 | xbin=2, ybin=2, xpos=-0.2666, ypos=0.0117, wavelength=None): 33 | 34 | # set up the parts of the grating 35 | self.grating_name = grating_name 36 | self.slitang = slit 37 | 38 | # set the telescope 39 | self.set_telescope('RSS') 40 | 41 | # set the collimator 42 | self.set_collimator('RSS') 43 | 44 | # set the camera 45 | self.set_camera('RSS', wavelength=wavelength) 46 | 47 | # set the detector 48 | self.set_detector('RSS', xbin=xbin, ybin=ybin, xpos=xpos, ypos=ypos) 49 | 50 | # set up the grating 51 | self.set_grating(self.grating_name) 52 | 53 | # set up the slit 54 | self.set_slit(self.slitang) 55 | 56 | # set up the grating angle 57 | self.gratang = gratang 58 | self.camang = camang 59 | 60 | def alpha(self, da=0.23): 61 | """Return the value of alpha for the spectrograph""" 62 | return self.gratang + da 63 | 64 | def beta(self, mF=4.2e-5, db=0.00): 65 | """Return the value of beta for the spectrograph 66 | 67 | Beta_o=(1+fA)*(camang)-gratang+beta_o 68 | """ 69 | return (1 + mF) * self.camang - self.alpha() + db 70 | 71 | def focallength(self, focallength=328.0, wavelength=None): 72 | """The camera focal lenght set oaccording to the following 73 | Wavelength should be provided in angstroms 74 | 75 | """ 76 | if wavelength is None: 77 | return focallength 78 | 79 | L = (wavelength - 4000.0) / 1000.0 80 | return 327.66 + -0.1861 * L + 0.5061 * L ** 2 + -0.2100 * L ** 3 + 0.0365 * L ** 4 + -0.0023 * L ** 5 81 | 82 | def get_wavelength(self, xarr, gamma=0.0): 83 | """For a given spectrograph configuration, return the wavelength coordinate 84 | associated with a pixel coordinate. 85 | 86 | xarr: 1-D Array of pixel coordinates 87 | gamma: Value of gamma for the row being analyzed 88 | 89 | returns an array of wavelengths in mm 90 | """ 91 | d = self.detector.xbin * self.detector.pix_size * (xarr - self.detector.get_xpixcenter()) 92 | dbeta = -np.degrees(np.arctan(d / self.camera.focallength)) 93 | return self.calc_wavelength(self.alpha(), -self.beta() + dbeta, gamma=gamma) 94 | 95 | def set_telescope(self, name='RSS'): 96 | if name == 'RSS': 97 | self.telescope = Optics(name=name, focallength=46200.0) 98 | elif name == 'SALT': 99 | self.telescope = Optics(name=name, focallength=46200.0) 100 | else: 101 | raise RSSError('%s is not a supported Telescope' % name) 102 | 103 | def set_collimator(self, name='RSS', focallength=630.0): 104 | if name == 'RSS': 105 | self.collimator = Optics(name=name, focallength=focallength) 106 | else: 107 | raise RSSError('%s is not a supported collimator' % name) 108 | 109 | def set_camera(self, name='RSS', wavelength=None): 110 | if name == 'RSS': 111 | self.camera = Optics(name=name, focallength=self.focallength()) 112 | else: 113 | raise RSSError('%s is not a supported camera' % name) 114 | 115 | def set_detector(self, name='RSS', geom=None, xbin=2, ybin=2, xpos=0, ypos=0): 116 | if name == 'RSS': 117 | if geom: 118 | pass 119 | else: 120 | # version 1 121 | # ccd1=Spectrograph.CCD(name='CCD1', xpix=2032, ypix=4102, pix_size=0.015, xpos=-32.19, ypos=0) 122 | # updated on 20120325 123 | ccd1 = CCD(name='CCD1', xpix=2032, ypix=4102, pix_size=0.015, xpos=-32.40, ypos=0.0486) 124 | ccd2 = CCD(name='CCD1', xpix=2032, ypix=4102, pix_size=0.015, xpos=0, ypos=0) 125 | ccd3 = CCD(name='CCD1', xpix=2032, ypix=4102, pix_size=0.015, xpos=32.218, ypos=0.0196) 126 | self.detector = Detector(name=name, ccd=[ccd1, ccd2, ccd3], xbin=xbin, ybin=ybin, 127 | xpos=xpos, ypos=ypos, plate_scale=0.224) 128 | else: 129 | raise RSSError('%s is not a supported detector' % name) 130 | 131 | def set_grating(self, name=None): 132 | if name == 'PG0300': 133 | self.grating = Grating(name='PG0300', spacing=300) 134 | elif name == 'PG0900': 135 | self.grating = Grating(name='PG0900', spacing=903.20) 136 | elif name == 'PG1300': 137 | self.grating = Grating(name='PG1300', spacing=1301.20) 138 | elif name == 'PG1800': 139 | self.grating = Grating(name='PG1800', spacing=1801.65) 140 | elif name == 'PG2300': 141 | self.grating = Grating(name='PG2300', spacing=2302.15) 142 | elif name == 'PG3000': 143 | self.grating = Grating(name='PG3000', spacing=2999.98) 144 | else: 145 | raise RSSError('%s is not a supported RSS grating' % name) 146 | 147 | def set_slit(self, slitang=1.0): 148 | self.slit = Slit(name='LongSlit', phi=slitang) 149 | self.slit.width = self.slit.calc_width(self.telescope.focallength) 150 | -------------------------------------------------------------------------------- /PySpectrograph/Spectra/findobj.py: -------------------------------------------------------------------------------- 1 | # 2 | # detect objects in a 2-D spectra 3 | # 4 | # This is adopted from apextract 5 | # 6 | import numpy as np 7 | import scipy.ndimage as nd 8 | 9 | from PySpectrograph import PySpectrographError 10 | 11 | 12 | def makeflat(data, method='median', specaxis=1): 13 | """Comparess a 2-D array along the spectral axis 14 | 15 | data 16 | 17 | """ 18 | if method == 'median': 19 | return np.median(data, axis=specaxis) 20 | elif method == 'average': 21 | return np.average(data, axis=specaxis) 22 | elif method == 'sum': 23 | return np.sum(data, axis=specaxis) 24 | else: 25 | msg = '%s is not method to flatten array' 26 | raise PySpectrographError(msg) 27 | 28 | 29 | def calc_ave_cont(data, method='sigclip', thresh=3.0, niter=5): 30 | """Calculate the average level of the continuum with sig-clip rejection or via median absolute deviation. 31 | If method is set to sigclip, it will return the sigma-clipped rejected mean and stardard deviation 32 | given using the threshold and number of iterations 33 | 34 | If method is mad, it will return the median and median absolute deviation for the data 35 | 36 | Returns mean, std 37 | """ 38 | if method == 'mad': 39 | return np.median(data), np.median(abs(data - np.median(data))) 40 | 41 | ave = data.mean() 42 | std = data.std() 43 | for i in range(niter): 44 | mask = abs(data - ave) < std * thresh 45 | ave = data[mask].mean() 46 | std = data[mask].std() 47 | return ave, std 48 | 49 | 50 | def calc_med_cont(data): 51 | """Calculate the median level of the continuum. Std is based on MAD 52 | 53 | Returns median, std 54 | """ 55 | med = np.median(data) 56 | std = 1.4826 * np.median(abs(data - med)) 57 | return med, std 58 | 59 | 60 | def findObjects(data, specaxis=1, method='median', thresh=3.0, niter=5, minsize=3): 61 | """Detect objects in the 2-D spectra 62 | data: 2D array of a spectral observations 63 | specaxis: Axis of the dispersion 64 | method: method for combining the array. It can either be median, average, or sum 65 | thresh: Threshhold for object detection 66 | niter: Number of iterations 67 | 68 | return list of tuples 69 | """ 70 | 71 | # compress the data 72 | ldata = makeflat(data, method=method, specaxis=specaxis) 73 | 74 | # median the data 75 | ldata = nd.filters.median_filter(ldata, size=minsize) 76 | 77 | # determine the continuum values 78 | cont_mean, cont_std = calc_ave_cont(ldata, method='mad') 79 | 80 | return findLines(ldata, method=method, cont_mean=cont_mean, cont_std=cont_std, 81 | thresh=thresh, niter=niter, minsize=minsize) 82 | 83 | 84 | def findLines(ldata, method='median', cont_mean=None, cont_std=None, thresh=3.0, niter=5, minsize=3): 85 | """Detect objects in 1-D spectra 86 | ldatl: 1D array of a spectral observations 87 | specaxis: Axis of the dispersion 88 | method: method for combining the array. It can either be median, average, or sum 89 | thresh: Threshhold for object detection 90 | niter: Number of iterations 91 | 92 | return list of tuples 93 | """ 94 | 95 | # set the levels of the continuum 96 | if cont_mean is None or cont_std is None: 97 | mean, std = calc_ave_cont(ldata) 98 | if cont_mean is None: 99 | cont_mean = mean 100 | if cont_std is None: 101 | cont_std = std 102 | 103 | # detect the peakc in the distribution 104 | obj_arr, obj_num = nd.label((abs(ldata - cont_mean) > thresh * cont_std)) 105 | 106 | # determine the distributions 107 | obj_list = [] 108 | mag_list = [] 109 | 110 | # determine the boundries for all objects 111 | for i in range(1, obj_num + 1): 112 | ind = np.where(obj_arr == i)[0] 113 | my1 = ind.min() 114 | my2 = ind.max() 115 | if my2 - my1 > minsize and my1 < my2: 116 | objs = deblendObjects(ldata, my1, my2, thresh, niter, minsize) 117 | for y1, y2 in objs: 118 | if 0 < y1 < len(ldata) and 0 < y2 < len(ldata): 119 | if y2 < y1: 120 | y1, y2 = y2, y1 121 | if y2 == y1: 122 | y2 = y1 + 1 123 | obj_list.append((y1, y2)) 124 | mag_list.append(ldata[y1:y2].max()) 125 | 126 | # sort the objects in magnitude order 127 | ord_obj_list = [] 128 | mag_arr = np.array(mag_list) 129 | mag_id = mag_arr.argsort() 130 | for i in mag_id[::-1]: 131 | ord_obj_list.append(obj_list[i]) 132 | 133 | return ord_obj_list 134 | 135 | 136 | def deblendObjects(ldata, y1, y2, thresh=3.0, niter=5, minsize=3): 137 | """Deblend a set of objects. Deblend produces a list of y1,y2 for 138 | a an array created by scip.ndimages.label based on a set of data 139 | 140 | """ 141 | 142 | # take the gradient of the data 143 | gdata = np.gradient(ldata[y1:y2]) 144 | 145 | # determine if there is more than one object 146 | try: 147 | pos_ind = np.where(gdata >= 0)[0].max() 148 | neg_ind = np.where(gdata <= 0)[0].min() 149 | except BaseException: 150 | return [(y1, y2)] 151 | 152 | # If this is true, then there is only a single object to extract 153 | if abs(pos_ind - neg_ind) < minsize: 154 | return [(y1, y2)] 155 | 156 | # manually go through the points and determine where it starts and stops 157 | obj_list = [] 158 | dy1 = y1 159 | neg = False 160 | for i in range(len(gdata)): 161 | if gdata[i] <= 0 and neg is False: 162 | neg = True 163 | if gdata[i] > 0 and neg is True: 164 | dy2 = dy1 + i 165 | obj_list.append((dy1, dy2)) 166 | dy1 = dy2 + 1 167 | neg = False 168 | obj_list.append((dy1, y2)) 169 | 170 | return obj_list 171 | 172 | 173 | def plotdata(ldata, obj_arr): 174 | """Just for debuggin purposes""" 175 | from PySpectrograph.Utilities import makeplots 176 | 177 | nlen = len(ldata) 178 | x = np.arange(nlen) 179 | makeplots.figure(figsize=(6, 6), dpi=72) 180 | ay = makeplots.axes([0.15, 0.10, 0.8, 0.8]) 181 | # y.imshow(med_data, cmap=makeplots.cm.gray, aspect='equal', vmin=-5, vmax=50 ) 182 | makeplots.plotline(ay, x, np.gradient(ldata)) 183 | # makeplots.plotline(ay, x, gdata) 184 | makeplots.plotline(ay, x, obj_arr * 100) 185 | makeplots.show() 186 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 3 | # 4 | # Astropy documentation build configuration file. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this file. 9 | # 10 | # All configuration values have a default. Some values are defined in 11 | # the global Astropy configuration which is loaded here before anything else. 12 | # See astropy.sphinx.conf for which values are set there. 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | # sys.path.insert(0, os.path.abspath('..')) 18 | # IMPORTANT: the above commented section was generated by sphinx-quickstart, but 19 | # is *NOT* appropriate for astropy or Astropy affiliated packages. It is left 20 | # commented out with this explanation to make it clear why this should not be 21 | # done. If the sys.path entry above is added, when the astropy.sphinx.conf 22 | # import occurs, it will import the *source* version of astropy instead of the 23 | # version installed (if invoked as "make html" or directly with sphinx), or the 24 | # version in the build directory (if "python setup.py build_sphinx" is used). 25 | # Thus, any C-extensions that are needed to build the documentation will *not* 26 | # be accessible, and the documentation will not build correctly. 27 | 28 | import datetime 29 | import os 30 | import sys 31 | 32 | try: 33 | import astropy_helpers 34 | except ImportError: 35 | # Building from inside the docs/ directory? 36 | if os.path.basename(os.getcwd()) == 'docs': 37 | a_h_path = os.path.abspath(os.path.join('..', 'astropy_helpers')) 38 | if os.path.isdir(a_h_path): 39 | sys.path.insert(1, a_h_path) 40 | 41 | # Load all of the global Astropy configuration 42 | from astropy_helpers.sphinx.conf import * 43 | 44 | # Get configuration information from setup.cfg 45 | try: 46 | from ConfigParser import ConfigParser 47 | except ImportError: 48 | from configparser import ConfigParser 49 | conf = ConfigParser() 50 | 51 | conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) 52 | setup_cfg = dict(conf.items('metadata')) 53 | 54 | # -- General configuration ---------------------------------------------------- 55 | 56 | # By default, highlight as Python 3. 57 | highlight_language = 'python3' 58 | 59 | # If your documentation needs a minimal Sphinx version, state it here. 60 | #needs_sphinx = '1.2' 61 | 62 | # To perform a Sphinx version check that needs to be more specific than 63 | # major.minor, call `check_sphinx_version("x.y.z")` here. 64 | # check_sphinx_version("1.2.1") 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns.append('_templates') 69 | 70 | # This is added to the end of RST files - a good place to put substitutions to 71 | # be used globally. 72 | rst_epilog += """ 73 | """ 74 | 75 | # -- Project information ------------------------------------------------------ 76 | 77 | # This does not *have* to match the package name, but typically does 78 | project = setup_cfg['package_name'] 79 | author = setup_cfg['author'] 80 | copyright = '{0}, {1}'.format( 81 | datetime.datetime.now().year, setup_cfg['author']) 82 | 83 | # The version info for the project you're documenting, acts as replacement for 84 | # |version| and |release|, also used in various other places throughout the 85 | # built documents. 86 | 87 | __import__(setup_cfg['package_name']) 88 | package = sys.modules[setup_cfg['package_name']] 89 | 90 | # The short X.Y version. 91 | version = package.__version__.split('-', 1)[0] 92 | # The full version, including alpha/beta/rc tags. 93 | release = package.__version__ 94 | 95 | 96 | # -- Options for HTML output -------------------------------------------------- 97 | 98 | # A NOTE ON HTML THEMES 99 | # The global astropy configuration uses a custom theme, 'bootstrap-astropy', 100 | # which is installed along with astropy. A different theme can be used or 101 | # the options for this theme can be modified by overriding some of the 102 | # variables set in the global configuration. The variables set in the 103 | # global configuration are listed below, commented out. 104 | 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | # To use a different custom theme, add the directory containing the theme. 108 | #html_theme_path = [] 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. To override the custom theme, set this to the 112 | # name of a builtin theme or the name of a custom theme in html_theme_path. 113 | #html_theme = None 114 | 115 | # Please update these texts to match the name of your package. 116 | html_theme_options = { 117 | 'logotext1': 'package', # white, semi-bold 118 | 'logotext2': '-template', # orange, light 119 | 'logotext3': ':docs' # white, light 120 | } 121 | 122 | 123 | 124 | # Custom sidebar templates, maps document names to template names. 125 | #html_sidebars = {} 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | #html_logo = '' 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = '' 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '' 139 | 140 | # The name for this set of Sphinx documents. If None, it defaults to 141 | # " v documentation". 142 | html_title = '{0} v{1}'.format(project, release) 143 | 144 | # Output file base name for HTML help builder. 145 | htmlhelp_basename = project + 'doc' 146 | 147 | 148 | # -- Options for LaTeX output ------------------------------------------------- 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, author, documentclass [howto/manual]). 152 | latex_documents = [('index', project + '.tex', project + u' Documentation', 153 | author, 'manual')] 154 | 155 | 156 | # -- Options for manual page output ------------------------------------------- 157 | 158 | # One entry per manual page. List of tuples 159 | # (source start file, name, description, authors, manual section). 160 | man_pages = [('index', project.lower(), project + u' Documentation', 161 | [author], 1)] 162 | 163 | 164 | # -- Options for the edit_on_github extension --------------------------------- 165 | 166 | if eval(setup_cfg.get('edit_on_github')): 167 | extensions += ['astropy_helpers.sphinx.ext.edit_on_github'] 168 | 169 | versionmod = __import__(setup_cfg['package_name'] + '.version') 170 | edit_on_github_project = setup_cfg['github_project'] 171 | if versionmod.version.release: 172 | edit_on_github_branch = "v" + versionmod.version.version 173 | else: 174 | edit_on_github_branch = "master" 175 | 176 | edit_on_github_source_root = "" 177 | edit_on_github_doc_root = "docs" 178 | 179 | # -- Resolving issue number to links in changelog ----------------------------- 180 | github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) 181 | 182 | -------------------------------------------------------------------------------- /PySpectrograph/Utilities/fit.py: -------------------------------------------------------------------------------- 1 | 2 | """FIT.PY--General fitting routines 3 | 4 | fit--fit is a general fitting routine using the scipy.optimize.leastsq 5 | technique to fit a function. This example has been taken from the scipy 6 | cookbook. 7 | 8 | """ 9 | 10 | import numpy as np 11 | from scipy import optimize, interpolate 12 | from scipy.special import legendre, chebyt 13 | 14 | 15 | class power: 16 | 17 | """A class to produce a polynomial term of power n. 18 | 19 | This has similar behavior to scipy.special.legendre 20 | 21 | """ 22 | 23 | def __init__(self, n): 24 | self.n = n 25 | 26 | def __call__(self, x): 27 | return x ** self.n 28 | 29 | 30 | class Parameter: 31 | 32 | def __init__(self, value): 33 | self.value = value 34 | 35 | def set(self, value): 36 | self.value = value 37 | 38 | def __call__(self): 39 | return self.value 40 | 41 | 42 | def fit(function, parameters, y, x=None, var=1, warn=False): 43 | def f(params): 44 | i = 0 45 | for p in parameters: 46 | p.set(params[i]) 47 | i += 1 48 | return (y - function(x)) / var 49 | 50 | if x is None: 51 | x = np.arange(y.shape[0]) 52 | p = [param() for param in parameters] 53 | return optimize.leastsq(f, p, full_output=1, warning=warn) 54 | 55 | 56 | class curfit: 57 | 58 | """Given an x and y data arrays, find the best fitting curve 59 | 60 | * x - list or array of x data 61 | * y - list or array of y data 62 | * yerr - error on y data 63 | * coef - Initial coefficients for fit 64 | * function - function to be fit to the data: 65 | options include polynomial, legendre, chebyshev, or spline 66 | * order - order of the function that is fit 67 | 68 | 69 | """ 70 | 71 | def __init__(self, x, y, yerr=None, coef=None, function='poly', order=3): 72 | 73 | # set up the variables 74 | self.x = x 75 | self.y = y 76 | if yerr is None: 77 | self.yerr = 1 78 | else: 79 | self.yerr = yerr 80 | self.order = order 81 | 82 | self.set_func(function) 83 | self.set_coef(coef) 84 | 85 | def set_coef(self, coef=None): 86 | """Set the coefficients for the fits for poly, legendre, and chebyshev""" 87 | if coef is None: 88 | coef = np.ones(self.order + 1) 89 | if isinstance(coef, np.ndarray): 90 | self.coef = coef 91 | elif isinstance(coef, list): 92 | self.coef = np.array(coef) 93 | else: 94 | self.coef = np.array([coef]) 95 | 96 | def set_func(self, function): 97 | """Set the function that will be used. 98 | * function - name of function to be used 99 | 100 | It will throw an error if an inappropriate function is given 101 | """ 102 | self.function = function 103 | if self.function == 'poly' or self.function == 'polynomial' or self.function == 'power': 104 | self.func = power 105 | elif self.function == 'legendre': 106 | self.func = legendre 107 | elif self.function == 'chebyshev': 108 | self.func = chebyt 109 | elif self.function == 'spline': 110 | self.func = None 111 | else: 112 | msg = '%s is not a valid function' % self.function 113 | raise Exception(msg) 114 | 115 | def set_weight(self, err): 116 | """Set the weighting for spline fitting """ 117 | if isinstance(err, np.ndarray): 118 | if err.any() != 0: 119 | self.weight = 1 / err 120 | return 121 | self.weight = None 122 | 123 | def __call__(self, x): 124 | """Return the value of the function evaluated at x""" 125 | if self.function == 'spline': 126 | return interpolate.splev(x, self.coef, der=0) 127 | v = x * 0.0 128 | for i in range(self.order + 1): 129 | v += self.coef[i] * self.func(i)(x) 130 | return v 131 | 132 | def erf(self, coef, x, y, v): 133 | """Error function to be minimized in least-squares fit""" 134 | self.set_coef(coef) 135 | return (y - self.__call__(x)) / v 136 | 137 | def sigma(self, x, y): 138 | """Return the RMS of the fit """ 139 | return (((y - self(x)) ** 2).mean()) ** 0.5 140 | 141 | def chisq(self, x, y, err): 142 | """Return the chi^2 of the fit""" 143 | return (((y - self(x)) / err) ** 2).sum() 144 | 145 | def fit(self, task=0, s=None, t=None, full_output=1, warn=False): 146 | """Fit the function to the data""" 147 | if self.function == 'spline': 148 | self.set_weight(self.yerr) 149 | self.results = interpolate.splrep( 150 | self.x, 151 | self.y, 152 | w=self.weight, 153 | task=0, 154 | s=None, 155 | t=None, 156 | k=self.order, 157 | full_output=full_output) 158 | # w=None, k=self.order, s=s, t=t, task=task, 159 | # full_output=full_output) 160 | self.set_coef(self.results[0]) 161 | 162 | else: 163 | self.results = optimize.leastsq(self.erf, self.coef, 164 | args=(self.x, self.y, self.yerr), 165 | full_output=full_output) 166 | self.set_coef(self.results[0]) 167 | 168 | 169 | class interfit(curfit): 170 | 171 | """Given an x and y data arrays, find the best fitting curve. 172 | After the initial fit, iterate on the solution to reject any 173 | points which are away from the solution 174 | 175 | * x - list or array of x data 176 | * y - list or array of y data 177 | * yerr - error on y data 178 | * coef - Initial coefficients for fit 179 | * function - function to be fit to the data: 180 | options include polynomial, legendre, chebyshev, or spline 181 | * order - order of the function that is fit 182 | * thresh - threshold for rejection 183 | * niter - number of times to iterate 184 | 185 | 186 | """ 187 | 188 | def __init__(self, x, y, yerr=None, coef=None, function='poly', order=3, 189 | thresh=3, niter=5): 190 | # set up the variables 191 | self.x_orig = x 192 | self.y_orig = y 193 | self.npts = len(self.x_orig) 194 | if yerr is None: 195 | self.yerr_orig = np.ones(self.npts) 196 | else: 197 | self.yerr_orig = yerr 198 | 199 | self.order = order 200 | self.thresh = thresh 201 | self.niter = niter 202 | 203 | self.set_func(function) 204 | self.set_coef(coef) 205 | self.set_mask(init=True) 206 | self.set_arrays(self.x_orig, self.y_orig, self.mask, err=self.yerr_orig) 207 | 208 | def set_mask(self, init=False): 209 | """Set the mask according to the values for rejecting points""" 210 | self.mask = np.ones(self.npts, dtype=bool) 211 | if init: 212 | return 213 | 214 | # difference the arrays 215 | diff = self.y_orig - self(self.x_orig) 216 | sigma = self.sigma(self.x, self.y) 217 | self.mask = (abs(diff) < self.thresh * sigma) 218 | 219 | def set_arrays(self, x, y, mask, err=None): 220 | """set the arrays using a mask""" 221 | self.x = x[mask] 222 | self.y = y[mask] 223 | if err is not None: 224 | self.yerr = err[mask] 225 | 226 | def interfit(self): 227 | """Fit a function and then iterate it to reject possible outlyiers""" 228 | self.fit() 229 | for i in range(self.niter): 230 | self.set_mask() 231 | self.set_arrays(self.x_orig, self.y_orig, self.mask, err=self.yerr_orig) 232 | self.fit() 233 | -------------------------------------------------------------------------------- /PySpectrograph/Spectra/Spectrum.py: -------------------------------------------------------------------------------- 1 | """Spectrum is a class to describe and generate a spectrum. 2 | 3 | HISTORY 4 | 20100601 SMC First written by SM Crawford 5 | 6 | Limitations: 7 | 8 | """ 9 | 10 | import numpy as np 11 | from PySpectrograph.Utilities.Functions import Normal 12 | 13 | 14 | class SpectrumError(Exception): 15 | 16 | """Exception Raised for Spectrograph errors""" 17 | pass 18 | 19 | 20 | class Spectrum: 21 | 22 | """Spectrum is a class for handling and creating spectra. It can either 23 | be created from a line list or a continuum flux. If no inputs are given, 24 | it creates an empty object that has a wavelength range from 3000-9000 and 25 | a sampling of 0.1. 26 | 27 | Parameters 28 | ---------- 29 | wavelength: An array of wavelengths for values in flux 30 | 31 | flux: An array of fluxes. It can either be lines or continuum 32 | 33 | wrange: wavelenght range of flux 34 | 35 | dw: sampling of the spectrum 36 | 37 | stype: line--the input spectrum is a list of lines 38 | continuum--the input spectrum is continuum values 39 | 40 | wavelength_units: Units of the wavelength array 41 | 42 | flux_units: Units of the wavelength array 43 | """ 44 | 45 | def __init__(self, wavelength=None, flux=None, var=None, wrange=None, dw=0.1, 46 | sigma=1e-1, stype='line', wavelength_units=None, flux_units=None): 47 | 48 | if wavelength is None and wrange is None: 49 | raise SpectrumError('Please specify either wavelength or wrange') 50 | 51 | # set the variables 52 | self.wrange = wrange 53 | self.dw = dw 54 | self.wavelength_units = wavelength_units 55 | self.stype = stype 56 | self.flux_units = flux_units 57 | self.sigma = sigma 58 | self.var = var 59 | 60 | # set the wavelength 61 | self.set_wavelength(wavelength) 62 | 63 | # set the flux 64 | self.set_flux(wavelength, flux) 65 | 66 | return 67 | 68 | def set_wavelength(self, wavelength, new=False): 69 | """Set the wavelength scale 70 | 71 | If new is True, then it will create a new wavelength 72 | using wrange and dw 73 | 74 | """ 75 | 76 | if self.wrange is None and wavelength is not None: 77 | self.wrange = [wavelength.min(), wavelength.max()] 78 | 79 | if self.stype == 'line' or new: 80 | self.wavelength = np.arange(self.wrange[0], self.wrange[1], self.dw) 81 | elif self.stype == 'continuum': 82 | self.wavelength = wavelength 83 | else: 84 | self.wavelength = wavelength 85 | 86 | self.nwave = len(self.wavelength) 87 | 88 | def set_flux(self, wavelength, flux): 89 | """Set the flux levels""" 90 | 91 | if flux is not None and len(flux) == self.nwave: 92 | self.flux = flux 93 | 94 | elif flux is not None and wavelength is not None: 95 | if self.stype == 'line': 96 | self.flux = np.zeros(self.nwave, dtype=float) 97 | for w, f in zip(wavelength, flux): 98 | self.flux += Normal(self.wavelength, w, self.sigma * self.dw, f) 99 | elif self.stype == 'continuum': 100 | self.flux = np.interp(self.wavelength, wavelength, flux) 101 | else: 102 | raise SpectrumError(f'{self.stype} is not an acceptable stype option') 103 | else: 104 | self.flux = np.zeros(self.nwave, dtype=float) 105 | 106 | def set_dispersion(self, sigma=1.0, nkern=20, ktype='Gaussian', func=None): 107 | """Applies an dispersion to the spectrum. 108 | 109 | sigma: Dispersion to apply if assuming a Gaussian in units of wavelength 110 | 111 | nkern: sampling of kernal 112 | 113 | ktype: 'Gaussian'--Apply a Gaussian function 114 | 'User' -- Use a user supplied kernal 115 | 116 | func: User supplied kernal 117 | 118 | """ 119 | if ktype == 'Gaussian': 120 | xkern = np.arange(nkern) 121 | kern = np.exp(-(xkern - 0.5 * nkern) ** 2 / (sigma / self.dw) ** 2) 122 | elif ktype == 'User': 123 | kern = func 124 | else: 125 | raise SpectrumError('%s is not an acceptable kernal option' % ktype) 126 | 127 | return np.convolve(self.flux, kern, mode='same') 128 | 129 | def get_flux(self, w): 130 | """Given a wavelength w, return what the flux value is""" 131 | return np.interp(w, self.wavelength, self.flux) 132 | 133 | def interp(self, warr): 134 | """Re-interpolate the spectrum such that the wavelength sampling is given by warr""" 135 | self.flux = np.interp(warr, self.wavelength, self.flux) 136 | self.wavelength = warr 137 | 138 | 139 | """ 140 | TODO: 141 | Here's the full text from Morton 1991 ApJS 77, 119 and it is a 142 | good question whether we should adopt the data from Peck and 143 | Reeder as IR data will be important for us. 144 | 145 | The IAU standard for conversion between air and vacuum wavelengths is, 146 | according to Oosterhoff (1957) and Edlen (1953), 147 | 148 | \begin{equation} 149 | \frac{\lambda_{vac}-\lambda_{air}}{\lambda_{air}}=(n-1) = 150 | 6.4328\times10^{-5}+ \frac{2.94981 times10^{-2}}{146-\sigma^2} 151 | +\frac{2.5540\times10^{-4}}{41-\sigma^2} 152 | \end{equation} 153 | where $\sigma=10^4/$, wiht $\lambda$ in angstroms. Edlen (1966) and Peck 154 | & Reeder (1972) have proposed improvmeents that primarily affect infrared 155 | wavelengths, but equation (3) was used here. 156 | 157 | More recently, this has been updated in IDLASTRO with a 158 | formula from Ciddor 1996, Applied Optics 62, 958 159 | See http://idlastro.gsfc.nasa.gov/ftp/pro/astro/airtovac.pro 160 | """ 161 | 162 | 163 | def air2vac(w_air, mode='Morton'): 164 | """Given an wavelength in units of 165 | 166 | w--wavelength in air in units of angstrom 167 | 168 | mode--method to use for conversion 169 | Morton--Morton 1991 ApJS 77, 119 170 | Ciddor--Ciddor 1996, Applied Optics 62, 958 171 | 172 | """ 173 | if mode == 'Morton': 174 | sigmasq = (1e4 / w_air) ** 2 175 | w_vac = w_air * (1 + 6.4328e-5 + 2.94981e-2 / (146.0 - sigmasq) + 2.5540e-4 / (41.0 - sigmasq)) 176 | elif mode == 'Ciddor': 177 | sigmasq = (1e4 / w_air) ** 2 178 | w_vac = w_air * (1 + 5.792105e-2 / (238.0185 - sigmasq) + 1.67917e-3 / (57.362 - sigmasq)) 179 | else: 180 | raise SpectrumError('%s is an invalid mode' % mode) 181 | 182 | return w_vac 183 | 184 | 185 | def vac2air(w_vac, mode='Morton'): 186 | """Given an wavelength in units of 187 | 188 | w--wavelength in air in units of angstrom 189 | 190 | mode--method to use for conversion 191 | Morton--Morton 1991 ApJS 77, 119 192 | Ciddor--Ciddor 1996, Applied Optics 62, 958 193 | 194 | """ 195 | if mode == 'Morton': 196 | sigmasq = (1e4 / w_vac) ** 2 197 | w_air = w_vac / (1 + 6.4328e-5 + 2.94981e-2 / (146.0 - sigmasq) + 2.5540e-4 / (41.0 - sigmasq)) 198 | elif mode == 'Ciddor': 199 | sigmasq = (1e4 / w_vac) ** 2 200 | w_air = w_vac / (1 + 5.792105e-2 / (238.0185 - sigmasq) + 1.67917e-3 / (57.362 - sigmasq)) 201 | else: 202 | raise SpectrumError('%s is an invalid mode' % mode) 203 | 204 | return w_air 205 | 206 | 207 | def fnutofwave(warr, farr): 208 | """Converts farr in ergs/s/cm2/Hz to ergs/s/cm2/A""" 209 | c = 2.99792458e18 # spped of light in Angstroms/s 210 | return farr * c / warr ** 2 211 | 212 | 213 | def magtoflux(marr, fzero): 214 | """Convert from magnitude to flux. 215 | marr--input array in mags 216 | fzero--zero point for the conversion 217 | """ 218 | return fzero * 10 ** (-0.4 * marr) 219 | 220 | 221 | def fluxtomag(farr, fzero): 222 | """"Convert from flux to magnitudes 223 | farr--input array in flux units 224 | fzero--zero point for the converion 225 | """ 226 | return -2.5 * np.log10(farr / fzero) 227 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PySpectrograph documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Apr 10 18:14:51 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys 15 | import os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'PySpectrograph' 45 | copyright = u'2012, S. M. Crawford' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.20' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.20' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'PySpectrographdoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | latex_elements = { 174 | # The paper size ('letterpaper' or 'a4paper'). 175 | #'papersize': 'letterpaper', 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | #'pointsize': '10pt', 179 | 180 | # Additional stuff for the LaTeX preamble. 181 | #'preamble': '', 182 | } 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'PySpectrograph.tex', u'PySpectrograph Documentation', 188 | u'S. M. Crawford', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # If true, show page references after internal links. 200 | #latex_show_pagerefs = False 201 | 202 | # If true, show URL addresses after external links. 203 | #latex_show_urls = False 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'pyspectrograph', u'PySpectrograph Documentation', 218 | [u'S. M. Crawford'], 1) 219 | ] 220 | 221 | # If true, show URL addresses after external links. 222 | #man_show_urls = False 223 | 224 | 225 | # -- Options for Texinfo output ------------------------------------------------ 226 | 227 | # Grouping the document tree into Texinfo files. List of tuples 228 | # (source start file, target name, title, author, 229 | # dir menu entry, description, category) 230 | texinfo_documents = [ 231 | ('index', 'PySpectrograph', u'PySpectrograph Documentation', 232 | u'S. M. Crawford', 'PySpectrograph', 'One line description of project.', 233 | 'Miscellaneous'), 234 | ] 235 | 236 | # Documents to append as an appendix to all manuals. 237 | #texinfo_appendices = [] 238 | 239 | # If false, no module index is generated. 240 | #texinfo_domain_indices = True 241 | 242 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 243 | #texinfo_show_urls = 'footnote' 244 | -------------------------------------------------------------------------------- /PySpectrograph/Identify/AutoIdentify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ############################# LICENSE ############################### 3 | # Copyright (c) 2009, South African Astronomical Observatory (SAAO) # 4 | # All rights reserved. # 5 | # # 6 | # Redistribution and use in source and binary forms, with or without # 7 | # modification, are permitted provided that the following conditions # 8 | # are met: # 9 | # # 10 | # * Redistributions of source code must retain the above copyright # 11 | # notice, this list of conditions and the following disclaimer. # 12 | # * Redistributions in binary form must reproduce the above copyright # 13 | # notice, this list of conditions and the following disclaimer # 14 | # in the documentation and/or other materials provided with the # 15 | # distribution. # 16 | # * Neither the name of the South African Astronomical Observatory # 17 | # (SAAO) nor the names of its contributors may be used to endorse # 18 | # or promote products derived from this software without specific # 19 | # prior written permission. # 20 | # # 21 | # THIS SOFTWARE IS PROVIDED BY THE SAAO ''AS IS'' AND ANY EXPRESS OR # 22 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # 24 | # DISCLAIMED. IN NO EVENT SHALL THE SAAO BE LIABLE FOR ANY # 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # 27 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # 28 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # 29 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # 31 | # POSSIBILITY OF SUCH DAMAGE. # 32 | # ##################################################################### 33 | """ 34 | AutoIDENTIFY is a program to automatically identify spectral lines in 35 | an arc image. 36 | 37 | Author Version Date 38 | ----------------------------------------------- 39 | S. M. Crawford (SAAO) 1.0 21 Aug 2010 40 | 41 | TODO 42 | ---- 43 | 44 | 45 | LIMITATIONS 46 | ----------- 47 | 48 | """ 49 | 50 | import sys 51 | import time 52 | import numpy as np 53 | 54 | from PySpectrograph import apext 55 | 56 | from . import spectools as st 57 | 58 | debug = True 59 | 60 | 61 | def AutoIdentify(xarr, specarr, slines, sfluxes, ws, method='Zeropoint', 62 | rstep=1, icenter=None, nrows=1, res=2, dres=0.1, 63 | sigma=5, niter=5, 64 | dc=20, nstep=20, 65 | verbose=True): 66 | """Automatically find the wavlength solution for the entire image. The following 67 | methods are used: 68 | 69 | zeropoint--Assume that the form for the initial guess of the wavelength solution 70 | is correct 71 | """ 72 | ImageSolution = {} 73 | 74 | # run it if only the zeropoint needs to be calculated 75 | if method == 'Zeropoint': 76 | func = st.findzeropoint 77 | ImageSolution = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=False, oneline=False, 78 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 79 | dsigma=sigma, dniter=niter, verbose=verbose, dc=dc, nstep=nstep) 80 | print(method) 81 | 82 | # use a line matching algorithm to match the lines 83 | # in the image with those in the line list 84 | if method == 'Matchlines': 85 | func = st.findwavelengthsolution 86 | ImageSolution = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=True, oneline=False, 87 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 88 | dsigma=sigma, dniter=niter, verbose=verbose, sigma=sigma, niter=niter) 89 | 90 | # first fit a zeropoint, then match the lines, and then 91 | # find the rest of the points by using only the zeropoint 92 | if method == 'MatchZero': 93 | print(ws.coef) 94 | func = st.findzeropoint 95 | ws = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=False, oneline=True, 96 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 97 | dsigma=sigma, dniter=niter, verbose=verbose, dc=10, nstep=20) 98 | 99 | func = st.findwavelengthsolution 100 | ws = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=True, oneline=True, 101 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 102 | dsigma=sigma, dniter=niter, verbose=verbose, sigma=sigma, niter=niter) 103 | print('Running zero now') 104 | func = st.findzeropoint 105 | ImageSolution = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=False, oneline=False, 106 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 107 | dsigma=sigma, dniter=niter, verbose=verbose, dc=dc, nstep=nstep) 108 | 109 | print(method) 110 | 111 | if method == 'FullXcor': 112 | func = st.findxcor 113 | dcoef = ws.coef * 0.1 114 | dcoef[-1] = dc 115 | print(dcoef) 116 | ws = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=True, oneline=True, 117 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 118 | dsigma=sigma, dniter=niter, verbose=verbose, dcoef=dcoef) 119 | print('Running zero now') 120 | func = st.findzeropoint 121 | ImageSolution = runsolution(xarr, specarr, slines, sfluxes, ws, func, fline=False, oneline=False, 122 | rstep=rstep, icenter=icenter, nrows=nrows, res=res, dres=dres, 123 | dsigma=sigma, dniter=niter, verbose=verbose, dc=dc, nstep=nstep) 124 | 125 | print(method) 126 | 127 | return ImageSolution 128 | 129 | 130 | def runsolution(xarr, specarr, slines, sfluxes, ws, func, ivar=None, 131 | fline=True, oneline=False, 132 | rstep=20, 133 | icenter=None, nrows=1, dsigma=5, dniter=5, res=2.0, dres=0.1, verbose=True, **kwargs): 134 | """Starting in the middle of the image, it will determine the solution 135 | by working its way out to either edge and compiling all the results into 136 | ImageSolution 137 | 138 | xarr--Full range in x of pixels to solve for 139 | 140 | specarr--Input 2D flux 141 | 142 | func--function to use for the solution 143 | 144 | 145 | """ 146 | # set up the variables 147 | ImageSolution = {} 148 | 149 | # Setup the central line if it isn't specified 150 | if icenter is None: 151 | icenter = int(0.5 * len(specarr)) 152 | 153 | # set up the flux from the central line (or the line specified by the user in icenter) 154 | farr = apext.makeflat(specarr, icenter, icenter + nrows) 155 | farr = st.flatspectrum(xarr, farr, mode='poly', order=2) 156 | cxp = st.detectlines(xarr, farr, dsigma, dniter) 157 | 158 | # first set up the artificial spectrum 159 | swarr, sfarr = st.makeartificial(slines, sfluxes, farr.max(), res, dres) 160 | 161 | # find the solution for the central wavelegnth 162 | k = icenter 163 | min_lines = 0.1 * len(cxp) 164 | if fline: 165 | mws = solution(xarr, specarr, slines, sfluxes, ws, func, k, k + nrows, 166 | min_lines=min_lines, dsigma=dsigma, dniter=dniter, **kwargs) 167 | else: 168 | mws = solution(xarr, specarr, swarr, sfarr, ws, func, k, k + nrows, 169 | min_lines=min_lines, dsigma=dsigma, dniter=dniter, **kwargs) 170 | 171 | print('runsolution:', mws.coef) 172 | if oneline: 173 | return mws 174 | 175 | ImageSolution[k] = mws 176 | 177 | # now loop through each step, and calculate the wavelengths for the given 178 | for i in range(rstep, int(0.5 * len(specarr)), rstep): 179 | for k in [icenter - i, icenter + i]: 180 | lws = getwsfromIS(k, ImageSolution) 181 | if fline: 182 | fws = solution(xarr, specarr, slines, sfluxes, lws, func, k, k + nrows, 183 | min_lines=min_lines, dsigma=dsigma, dniter=dniter, **kwargs) 184 | else: 185 | fws = solution(xarr, specarr, swarr, sfarr, lws, func, k, k + nrows, 186 | min_lines=min_lines, dsigma=dsigma, dniter=dniter, **kwargs) 187 | ImageSolution[k] = fws 188 | 189 | if verbose: 190 | p_new = i * 100.0 / (0.5 * len(specarr)) 191 | ctext = 'Percentage Complete: %d %d %f\r' % (i, p_new, time.clock()) # p_new 192 | sys.stdout.write(ctext) 193 | sys.stdout.flush() 194 | 195 | return ImageSolution 196 | 197 | 198 | def solution(xarr, specarr, sl, sf, ws, func, y1, y2, min_lines=2, dsigma=5, dniter=3, pad=50, **kwargs): 199 | """Extract a single line and calculate the wavelneght solution""" 200 | 201 | # set up the flux from the set of lines 202 | farr = apext.makeflat(specarr, y1, y2) 203 | farr = st.flatspectrum(xarr, farr, mode='poly', order=2) 204 | 205 | # check to see if there are any points 206 | xp = st.detectlines(xarr, farr, dsigma, dniter) 207 | 208 | if len(xp) > min_lines and ws: 209 | # make the artificial list 210 | wmin = ws.value(xarr.min()) 211 | wmax = ws.value(xarr.max()) 212 | smask = (sl > wmin - pad) * (sl < wmax + pad) 213 | 214 | # fit the function 215 | fws = func(xarr, farr, sl[smask], sf[smask], ws, **kwargs) 216 | return fws 217 | 218 | return None 219 | 220 | 221 | def getwsfromIS(k, ImageSolution): 222 | """From the imageSolution dictionary, find the ws which is nearest to the value k 223 | 224 | """ 225 | ISkeys = np.array(list(ImageSolution.keys())) 226 | ws = ImageSolution[ISkeys[abs(ISkeys - k).argmin()]] 227 | if ws is None: 228 | dist = abs(ISkeys[0] - k) 229 | ws = ImageSolution[ISkeys[0]] 230 | for i in ISkeys: 231 | if ImageSolution[i] and abs(i - k) < dist: 232 | dist = abs(i - k) 233 | ws = ImageSolution[i] 234 | return ws 235 | -------------------------------------------------------------------------------- /PySpectrograph/Identify/specidentify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ############################# LICENSE ############################### 3 | # Copyright (c) 2009, South African Astronomical Observatory (SAAO) # 4 | # All rights reserved. # 5 | # # 6 | # Redistribution and use in source and binary forms, with or without # 7 | # modification, are permitted provided that the following conditions # 8 | # are met: # 9 | # # 10 | # * Redistributions of source code must retain the above copyright # 11 | # notice, this list of conditions and the following disclaimer. # 12 | # * Redistributions in binary form must reproduce the above copyright # 13 | # notice, this list of conditions and the following disclaimer # 14 | # in the documentation and/or other materials provided with the # 15 | # distribution. # 16 | # * Neither the name of the South African Astronomical Observatory # 17 | # (SAAO) nor the names of its contributors may be used to endorse # 18 | # or promote products derived from this software without specific # 19 | # prior written permission. # 20 | # # 21 | # THIS SOFTWARE IS PROVIDED BY THE SAAO ''AS IS'' AND ANY EXPRESS OR # 22 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # 24 | # DISCLAIMED. IN NO EVENT SHALL THE SAAO BE LIABLE FOR ANY # 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # 27 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # 28 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # 29 | # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # 31 | # POSSIBILITY OF SUCH DAMAGE. # 32 | # ##################################################################### 33 | """ 34 | SPECIDENTIFY is a program to read in SALT RSS spectroscopic arc lamps and 35 | determine the wavelength solution for that data. The input data should be 36 | a SALT arc lamp and a line list or another arc lamp image with high quality 37 | wavelength solution. The lamp list can be either wavelengths, wavelengths 38 | and fluxes, or an arc image with a high quality solution. The line lamp 39 | can also be left unspecified and the user will manually enter the data. 40 | 41 | From there, the user has several different possible choices. They can provide 42 | a first guess of the coefficients for the wavelength solution or transformation, 43 | indicate the model for the spectragraph for the first guess, or provide an 44 | image with a solution already as the first guess. 45 | 46 | Author Version Date 47 | ----------------------------------------------- 48 | S. M. Crawford (SAAO) 1.0 10 Oct 2009 49 | 50 | TODO 51 | ---- 52 | 53 | 54 | LIMITATIONS 55 | ----------- 56 | 1. Currently assumes that the linelist is of the form of an ascii file with 57 | either appropriate information in either one or two columns 58 | 59 | """ 60 | # Ensure python 2.5 compatibility 61 | 62 | 63 | import os 64 | import numpy as np 65 | 66 | from pyraf import iraf 67 | import saltsafekey 68 | import saltsafeio 69 | from saltsafelog import logging 70 | 71 | 72 | from PySpectrograph import RSSModel 73 | from PySpectrograph import WavelengthSolution 74 | from PySpectrograph import LineSolution 75 | 76 | 77 | from . import spectools as st 78 | from .spectools import SALTSpecError 79 | from .InterIdentify import InterIdentify 80 | from .AutoIdentify import AutoIdentify 81 | 82 | debug = True 83 | 84 | 85 | # ----------------------------------------------------------- 86 | # core routine 87 | 88 | def specidentify(images, linelist, outfile, guesstype, guessfile, function, 89 | order, rstep, interact, clobber, logfile, verbose, status): 90 | 91 | with logging(logfile, debug) as log: 92 | 93 | # set up the variables 94 | infiles = [] 95 | outfiles = [] 96 | 97 | # Check the input images 98 | infiles = saltsafeio.argunpack('Input', images) 99 | print(infiles) 100 | 101 | # create list of output files 102 | outfiles = saltsafeio.argunpack('Input', outfile) 103 | 104 | # if outfiles is a single image, turn it into a list 105 | # of the same length as infiles 106 | if len(outfiles) != len(infiles): 107 | if len(outfiles) == 1: 108 | outfiles = outfiles * len(infiles) 109 | elif len(outfiles) == 0: 110 | outfiles = [None] * len(infiles) 111 | else: 112 | msg = 'Please enter an appropriate number of outfiles' 113 | raise SALTSpecError(msg) 114 | 115 | # open the line lists 116 | slines, sfluxes = st.readlinelist(linelist) 117 | 118 | # Identify the lines in each file 119 | for img, oimg in zip(infiles, outfiles): 120 | log.message('Proccessing image %s' % img) 121 | identify(img, oimg, slines, sfluxes, guesstype, guessfile, function, 122 | order, rstep, interact, clobber, log, verbose) 123 | 124 | 125 | # ------------------------------------------------------------------ 126 | # Find the solution for lines in a file 127 | 128 | def identify(img, oimg, slines, sfluxes, guesstype, guessfile, function, order, 129 | rstep, interact, clobber, log, verbose, outfile=None): 130 | """For a given image, find the solution for each row in the file. Use the appropriate first guess and 131 | guess type along with the appropriate function and order for the fit. 132 | 133 | Write out the new image with the solution in the headers and/or as a table in the multi-extension 134 | fits file 135 | 136 | returns the status 137 | """ 138 | ImageSolution = {} 139 | dcstep = 3 140 | nstep = 50 141 | res = 2.0 142 | dres = 0.1 143 | centerrow = None 144 | nrows = 1 145 | sigma = 3 146 | niter = 5 147 | xdiff = 2 * res 148 | method = 'MatchZero' 149 | 150 | # Open up the image 151 | hdu = saltsafeio.openfits(img) 152 | 153 | # Read in important keywords 154 | 155 | # determine the central row and read it in 156 | try: 157 | data = hdu[1].data 158 | midline = int(0.5 * len(data)) 159 | xarr = np.arange(len(data[midline])) 160 | specarr = data 161 | except Exception as e: 162 | message = 'Unable to read in data array in %s because %s' % (img, e) 163 | raise SALTSpecError(message) 164 | 165 | # determine the type of first guess. Assumes none 166 | if guesstype == 'user': 167 | pass 168 | elif guesstype == 'rss': 169 | dateobs = saltsafekey.get('DATE-OBS', hdu[0], img) 170 | utctime = saltsafekey.get('UTC-OBS', hdu[0], img) 171 | instrume = saltsafekey.get('INSTRUME', hdu[0], img) 172 | grating = saltsafekey.get('GRATING', hdu[0], img) 173 | grang = saltsafekey.get('GR-ANGLE', hdu[0], img) 174 | arang = saltsafekey.get('AR-ANGLE', hdu[0], img) 175 | filter = saltsafekey.get('FILTER', hdu[0], img) 176 | slit = float(saltsafekey.get('MASKID', hdu[0], img)) 177 | xbin, ybin = saltsafekey.ccdbin(hdu[0], img) 178 | # set up the rss model 179 | rssmodel = RSSModel.RSSModel(grating_name=grating.strip(), gratang=grang, 180 | camang=arang, slit=slit, xbin=xbin, ybin=ybin) 181 | rss = rssmodel.rss 182 | res = 1e7 * rss.calc_resolelement(rss.gratang, rss.gratang - rss.camang) 183 | 184 | if instrume not in ['PFIS', 'RSS']: 185 | msg = '%s is not a currently supported instrument' % instrume 186 | raise SALTSpecError(msg) 187 | ws = useRSSModel(xarr, rss, function=function, order=order) 188 | elif guesstype == 'image': 189 | pass 190 | else: 191 | ws = None 192 | 193 | # run in either interactive or non-interactive mode 194 | if interact: 195 | ImageSolution = InterIdentify(xarr, specarr, slines, sfluxes, ws, xdiff=xdiff, function=function, 196 | order=order, verbose=True) 197 | else: 198 | ImageSolution = AutoIdentify(xarr, specarr, slines, sfluxes, ws, 199 | rstep=rstep, method=method, icenter=centerrow, nrows=nrows, 200 | res=res, dres=dres, dc=dcstep, nstep=nstep, sigma=sigma, niter=niter, 201 | verbose=verbose) 202 | 203 | # set up the list of solutions to into an array 204 | key_arr = np.array(list(ImageSolution.keys())) 205 | arg_arr = key_arr.argsort() 206 | ws_arr = np.zeros((len(arg_arr), len(ws.coef) + 1), dtype=float) 207 | 208 | # write the solution to an array 209 | for j, i in enumerate(arg_arr): 210 | if isinstance(ImageSolution[key_arr[i]], WavelengthSolution.WavelengthSolution): 211 | ws_arr[j, 0] = key_arr[i] 212 | ws_arr[j, 1:] = ImageSolution[key_arr[i]].coef 213 | 214 | # write the solution as an file 215 | if outfile is not None: 216 | # write header to the file that should include the order and function 217 | if os.path.isfile(outfile) and not clobber: 218 | dout = open(outfile, 'a') 219 | else: 220 | dout = open(outfile, 'w') 221 | 222 | msg = '#WS: Wavelength solution for image %s\n' % img 223 | msg += '#The following parameters were used in determining the solution:\n' 224 | msg += '#name=%s\n' % img 225 | msg += '#time-obs=%s %s\n' % (dateobs, utctime) 226 | msg += '#instrument=%s\n' % instrume 227 | msg += '#grating=%s\n' % grating.strip() 228 | msg += '#graang=%s\n' % grang 229 | msg += '#arang=%s\n' % arang 230 | msg += '#filter=%s\n' % filter.strip() 231 | msg += '#Function=%s\n' % function 232 | msg += '#Order=%s\n' % order 233 | msg += '#Starting Data\n' 234 | dout.write(msg) 235 | 236 | for i in range(len(ws_arr)): 237 | if ws_arr[i, 0]: 238 | msg = '%5.2f ' % ws_arr[i, 0] 239 | msg += ' '.join(['%e' % k for k in ws_arr[i, 1:]]) 240 | dout.write(msg + '\n') 241 | dout.write('\n') 242 | dout.close() 243 | 244 | # write the solution as an extension 245 | hdu.close() 246 | 247 | return 248 | 249 | 250 | def useRSSModel(xarr, rss, function='poly', order=3): 251 | """Returns the wavelength solution using the RSS model for the spectrograph 252 | 253 | 254 | """ 255 | 256 | # now for each position on the detector, calculate the wavelength at that position 257 | if function == 'poly' or function == 'legendre': 258 | d = rss.detector.xbin * rss.detector.pix_size * (xarr - 0.5 * len(xarr)) 259 | alpha = rss.gratang 260 | beta = rss.gratang - rss.camang 261 | dbeta = -np.degrees(np.arctan(d / rss.camera.focallength)) 262 | y = 1e7 * rss.calc_wavelength(alpha, beta + dbeta) 263 | 264 | # for these models, calculate the wavelength solution 265 | ws = WavelengthSolution.WavelengthSolution(xarr, y, order=order, function=function) 266 | ws.fit() 267 | elif function == 'line': 268 | ws = LineSolution.LineSolution(rss) 269 | else: 270 | message = '%s is not an acceptable form for the function' % function 271 | raise SALTSpecError(message) 272 | 273 | return ws 274 | 275 | 276 | # main code 277 | 278 | parfile = iraf.osfn("saltspec$specidentify.par") 279 | t = iraf.IrafTaskFactory(taskname="specidentify", value=parfile, function=specidentify, pkgname='saltspec') 280 | --------------------------------------------------------------------------------