roman-snpit-snappl 0.2.4__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of roman-snpit-snappl might be problematic. Click here for more details.
- {roman_snpit_snappl-0.2.4/roman_snpit_snappl.egg-info → roman_snpit_snappl-0.3.0}/PKG-INFO +1 -1
- roman_snpit_snappl-0.3.0/changes/15.feature.rst +1 -0
- roman_snpit_snappl-0.3.0/changes/16.feature.rst +1 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0/roman_snpit_snappl.egg-info}/PKG-INFO +1 -1
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/SOURCES.txt +2 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/_version.py +2 -2
- roman_snpit_snappl-0.3.0/snappl/image.py +506 -0
- roman_snpit_snappl-0.3.0/snappl/wcs.py +179 -0
- roman_snpit_snappl-0.2.4/snappl/image.py +0 -396
- roman_snpit_snappl-0.2.4/snappl/wcs.py +0 -37
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.cruft.json +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/CODEOWNERS +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/PR_TEMPLATE.md +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/dependabot.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/labeler.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/workflows/changelog.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/workflows/run_labeler.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/workflows/run_snappl_tests.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/workflows/sphinx-deploy.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.github/workflows/sub_package_update.yml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.gitignore +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/.pre-commit-config.yaml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/CHANGES.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/CITATION.cff +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/CONTRIBUTING.md +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/LICENSE +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/MANIFEST.in +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/README.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/.gitkeep +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/10.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/13.bugfix.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/14.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/3.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/5.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/8.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/changes/9.snappl.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/codespell-ignore.txt +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/Makefile +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/_static/logo_black_filled.png +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/changes.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/conf.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/index.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/installation.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/make.bat +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/docs/usage.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/licenses/.DS_Store +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/licenses/LICENSE.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/licenses/README.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/licenses/TEMPLATE_LICENSE.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/pyproject.toml +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/dependency_links.txt +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/not-zip-safe +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/requires.txt +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/top_level.txt +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/setup.cfg +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/setup.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/__init__.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/_dev/__init__.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/_dev/scm_version.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/data/README.rst +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/snappl/psf.py +0 -0
- {roman_snpit_snappl-0.2.4 → roman_snpit_snappl-0.3.0}/tox.ini +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Write tests for wcs.py and image.py, and in so doing fix a bunch of problems. Bit of image.py refactoring.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Add support for galsim wCSes
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from astropy.io import fits
|
|
5
|
+
from astropy.nddata.utils import Cutout2D
|
|
6
|
+
# from astropy.coordinates import SkyCoord
|
|
7
|
+
|
|
8
|
+
from snpit_utils.logger import SNLogger
|
|
9
|
+
from snappl.wcs import AstropyWCS, GalsimWCS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Exposure:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OpenUniverse2024Exposure:
|
|
17
|
+
def __init__( self, pointing ):
|
|
18
|
+
self.pointing = pointing
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ======================================================================
|
|
22
|
+
# The base class for all images. This is not useful by itself, you need
|
|
23
|
+
# to instantiate a subclass. However, everything that you call on an
|
|
24
|
+
# object you instantiate should have its interface defined in this
|
|
25
|
+
# class.
|
|
26
|
+
|
|
27
|
+
class Image:
|
|
28
|
+
"""Encapsulates a single 2d image."""
|
|
29
|
+
|
|
30
|
+
data_array_list = [ 'all', 'data', 'noise', 'flags' ]
|
|
31
|
+
|
|
32
|
+
def __init__( self, path, exposure, sca ):
|
|
33
|
+
"""Instantiate an image. You probably don't want to do that.
|
|
34
|
+
|
|
35
|
+
This is an abstract base class that has limited functionality.
|
|
36
|
+
You probably want to instantiate a subclass.
|
|
37
|
+
|
|
38
|
+
For all implementations, the properties data, noise, and flags
|
|
39
|
+
are lazy-loaded. That is, they start empty, but when you access
|
|
40
|
+
them, an internal buffer gets loaded with that data. This means
|
|
41
|
+
it can be very easy for lots of memory to get used without your
|
|
42
|
+
realizing it. There are a couple of solutions. The first, is
|
|
43
|
+
to call Image.free() when you're sure you don't need the data
|
|
44
|
+
any more, or if you know you want to get rid of it for a while
|
|
45
|
+
and re-read it from disk later. The second is just not to
|
|
46
|
+
access the data, noise, and flags properties, instead use
|
|
47
|
+
Image.get_data(), and manage the data object lifetime yourself.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
path : str
|
|
52
|
+
Path to image file, or otherwise some kind of indentifier
|
|
53
|
+
that allows the class to find the image.
|
|
54
|
+
|
|
55
|
+
exposure : Exposure (or instance of Exposure subclass)
|
|
56
|
+
The exposure this image is associated with, or None if it's
|
|
57
|
+
not associated with an Exposure (or youdon't care)
|
|
58
|
+
|
|
59
|
+
sca : int
|
|
60
|
+
The Sensor Chip Assembly that would be called the
|
|
61
|
+
chip number for any other telescope but is called SCA for
|
|
62
|
+
Roman.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
self.inputs = types.SimpleNamespace()
|
|
66
|
+
self.inputs.path = path
|
|
67
|
+
self.inputs.exposure = exposure
|
|
68
|
+
self.inputs.sca = sca
|
|
69
|
+
self._wcs = None # a BaseWCS object (in wcs.py)
|
|
70
|
+
self._is_cutout = False
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def data( self ):
|
|
74
|
+
"""The image data, a 2d numpy array."""
|
|
75
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement data" )
|
|
76
|
+
|
|
77
|
+
@data.setter
|
|
78
|
+
def data( self, new_value ):
|
|
79
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement data setter" )
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def noise( self ):
|
|
83
|
+
"""The 1σ pixel noise, a 2d numpy array."""
|
|
84
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement noise" )
|
|
85
|
+
|
|
86
|
+
@noise.setter
|
|
87
|
+
def noise( self, new_value ):
|
|
88
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement noise setter" )
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def flags( self ):
|
|
92
|
+
"""An integer 2d numpy array of pixel masks / flags TBD"""
|
|
93
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement flags" )
|
|
94
|
+
|
|
95
|
+
@flags.setter
|
|
96
|
+
def flags( self, new_value ):
|
|
97
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement flags setter" )
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def image_shape( self ):
|
|
101
|
+
"""Tuple: (ny, nx) pixel size of image."""
|
|
102
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement image_shape" )
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def sky_level( self ):
|
|
106
|
+
"""Estimate of the sky level in ADU."""
|
|
107
|
+
raise NotImplementedError( "Do.")
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def exptime( self ):
|
|
111
|
+
"""Exposure time in seconds."""
|
|
112
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement exptime" )
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def band( self ):
|
|
116
|
+
"""Band (str)"""
|
|
117
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement band" )
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def mjd( self ):
|
|
121
|
+
"""MJD of the start of the image (defined how? TAI?)"""
|
|
122
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement mjd" )
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def position_angle( self ):
|
|
126
|
+
"""Position angle in degrees east of north (or what)?"""
|
|
127
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement position_angle" )
|
|
128
|
+
|
|
129
|
+
def fraction_masked( self ):
|
|
130
|
+
"""Fraction of pixels that are masked."""
|
|
131
|
+
raise NotImplementedError( "Do.")
|
|
132
|
+
|
|
133
|
+
def get_data( self, which='all', always_reload=False, cache=False ):
|
|
134
|
+
"""Read the data from disk and return one or more 2d numpy arrays of data.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
which : str
|
|
139
|
+
What to read:
|
|
140
|
+
'data' : just the image data
|
|
141
|
+
'noise' : just the noise data
|
|
142
|
+
'flags' : just the flags data
|
|
143
|
+
'all' : data, noise, and flags
|
|
144
|
+
|
|
145
|
+
always_reload: bool, default False
|
|
146
|
+
Whether this is supported depends on the subclass. If this
|
|
147
|
+
is false, then get_data() has the option of returning the
|
|
148
|
+
values of self.data, self.noise, and/or self.flags instead
|
|
149
|
+
of always loading the data. If this is True, then
|
|
150
|
+
get_data() will ignore the self._data et al. properties.
|
|
151
|
+
|
|
152
|
+
cache: bool, default False
|
|
153
|
+
Normally, get_data() just reads the data and does not do any
|
|
154
|
+
internal caching. If this is True, and the subclass
|
|
155
|
+
supports it, then the object will cache the loaded data so
|
|
156
|
+
that future calls with always_reload will not need to reread
|
|
157
|
+
the data, nor will accessing the data, noise, and flags
|
|
158
|
+
properties.
|
|
159
|
+
|
|
160
|
+
The data read not stored in the class, so when the caller goes
|
|
161
|
+
out of scope, the data will be freed (unless the caller saved it
|
|
162
|
+
somewhere. This does mean it's read from disk every time.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
list (length 1 or 3 ) of 2d numpy arrays
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_data" )
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def free( self ):
|
|
173
|
+
"""Try to free memory."""
|
|
174
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement free" )
|
|
175
|
+
|
|
176
|
+
def get_wcs( self, wcsclass=None ):
|
|
177
|
+
"""Get image WCS. Will be an object of type BaseWCS (from wcs.py) (really likely a subclass).
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
wcsclass : str or None
|
|
182
|
+
By default, the subclass of BaseWCS you get back will be
|
|
183
|
+
defined by the Image subclass of the object you call this
|
|
184
|
+
on. If you want a specific subclass of BaseWCS, you can put
|
|
185
|
+
the name of that class here. It may not always work; not
|
|
186
|
+
all types of images are able to return all types of wcses.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
object of a subclass of snappl.wcs.BaseWCS
|
|
191
|
+
|
|
192
|
+
"""
|
|
193
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_wcs" )
|
|
194
|
+
|
|
195
|
+
def get_cutout(self, ra, dec, size):
|
|
196
|
+
"""Make a cutout of the image at the given RA and DEC.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
snappl.image.Image
|
|
201
|
+
"""
|
|
202
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_cutout" )
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def coord_center(self):
|
|
207
|
+
"""[RA, DEC] (both floats) in degrees at the center of the image"""
|
|
208
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement coord_center" )
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ======================================================================
|
|
212
|
+
# Lots of classes will probably internally store all of data, noise, and
|
|
213
|
+
# flags as 2d numpy arrays. Common code for those classes is here.
|
|
214
|
+
|
|
215
|
+
class Numpy2DImage( Image ):
|
|
216
|
+
def __init__( self, *args, **kwargs ):
|
|
217
|
+
super().__init__( *args, **kwargs )
|
|
218
|
+
|
|
219
|
+
self._data = None
|
|
220
|
+
self._noise = None
|
|
221
|
+
self._flags = None
|
|
222
|
+
self._image_shape = None
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def data( self ):
|
|
226
|
+
if self._data is None:
|
|
227
|
+
self._load_data()
|
|
228
|
+
return self._data
|
|
229
|
+
|
|
230
|
+
@data.setter
|
|
231
|
+
def data(self, new_value):
|
|
232
|
+
if ( isinstance(new_value, np.ndarray)
|
|
233
|
+
and np.issubdtype(new_value.dtype, np.floating)
|
|
234
|
+
and len(new_value.shape) ==2
|
|
235
|
+
):
|
|
236
|
+
self._data = new_value
|
|
237
|
+
else:
|
|
238
|
+
raise TypeError( "Data must be a 2d numpy array of floats." )
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def noise( self ):
|
|
242
|
+
if self._noise is None:
|
|
243
|
+
self._load_data()
|
|
244
|
+
return self._noise
|
|
245
|
+
|
|
246
|
+
@noise.setter
|
|
247
|
+
def noise( self, new_value ):
|
|
248
|
+
if ( isinstance( new_value, np.ndarray )
|
|
249
|
+
and np.issubdtype( new_value.dtype, np.floating )
|
|
250
|
+
and len( new_value.shape ) == 2
|
|
251
|
+
):
|
|
252
|
+
self._noise = new_value
|
|
253
|
+
else:
|
|
254
|
+
raise TypeError( "Noise must be a 2d numpy array of floats." )
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def flags( self ):
|
|
258
|
+
if self._flags is None:
|
|
259
|
+
self._load_data()
|
|
260
|
+
return self._flags
|
|
261
|
+
|
|
262
|
+
@flags.setter
|
|
263
|
+
def flags( self, new_value ):
|
|
264
|
+
if ( isinstance( new_value, np.ndarray )
|
|
265
|
+
and np.issubdtype( new_value.dtype, np.integer )
|
|
266
|
+
and len( new_value.shape ) == 2
|
|
267
|
+
):
|
|
268
|
+
self._flags = new_value
|
|
269
|
+
else:
|
|
270
|
+
raise TypeError( "Flags must be a 2d numpy array of integers." )
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def image_shape( self ):
|
|
274
|
+
"""Subclasses probably want to override this!
|
|
275
|
+
|
|
276
|
+
This implementation accesses the .data property, which will load the data
|
|
277
|
+
from disk if it hasn't been already. Actual images are likely to have
|
|
278
|
+
that information availble in a manner that doesn't require loading all
|
|
279
|
+
the image data (e.g. in a header), so subclasses should do that.
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
if self._image_shape is None:
|
|
283
|
+
self._image_shape = self.data.shape
|
|
284
|
+
return self._image_shape
|
|
285
|
+
|
|
286
|
+
def _load_data( self ):
|
|
287
|
+
"""Loads (or reloads) the data from disk."""
|
|
288
|
+
imgs = self.get_data()
|
|
289
|
+
self._data = imgs[0]
|
|
290
|
+
self._noise = imgs[1]
|
|
291
|
+
self._flags = imgs[2]
|
|
292
|
+
|
|
293
|
+
def free( self ):
|
|
294
|
+
self._data = None
|
|
295
|
+
self._noise = None
|
|
296
|
+
self._flags = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ======================================================================
|
|
300
|
+
# A base class for FITSImages which use an AstropyWCS wcs. Not useful
|
|
301
|
+
# by itself, because which image you load will have different
|
|
302
|
+
# assumptions about which HDU holds image, weight, flags, plus header
|
|
303
|
+
# information will be different etc. However, there will be some
|
|
304
|
+
# shared code between all FITS implementations, so that's here.
|
|
305
|
+
|
|
306
|
+
class FITSImage( Numpy2DImage ):
|
|
307
|
+
def __init__( self, *args, **kwargs ):
|
|
308
|
+
super().__init__( *args, **kwargs )
|
|
309
|
+
|
|
310
|
+
self._data = None
|
|
311
|
+
self._noise = None
|
|
312
|
+
self._flags = None
|
|
313
|
+
self._header = None
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def image_shape(self):
|
|
317
|
+
"""tuple: (ny, nx) shape of image"""
|
|
318
|
+
|
|
319
|
+
if not self._is_cutout:
|
|
320
|
+
hdr = self._get_header()
|
|
321
|
+
self._image_shape = ( hdr['NAXIS1'], hdr['NAXIS2'] )
|
|
322
|
+
return self._image_shape
|
|
323
|
+
|
|
324
|
+
if self._image_shape is None:
|
|
325
|
+
self._image_shape = self.data.shape
|
|
326
|
+
|
|
327
|
+
return self._image_shape
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def coord_center(self):
|
|
331
|
+
"""[ RA and Dec ] at the center of the image."""
|
|
332
|
+
|
|
333
|
+
wcs = self.get_wcs()
|
|
334
|
+
return wcs.pixel_to_world( self.image_shape[1] //2, self.image_shape[0] //2 )
|
|
335
|
+
|
|
336
|
+
def _get_header( self ):
|
|
337
|
+
raise NotImplementedError( f"{self.__class__.__name__} needs to implement _get_header()" )
|
|
338
|
+
|
|
339
|
+
def get_wcs( self, wcsclass=None ):
|
|
340
|
+
wcsclass = "AstropyWCS" if wcsclass is None else wcsclass
|
|
341
|
+
if ( self._wcs is None ) or ( self._wcs.__class__.__name__ != wcsclass ):
|
|
342
|
+
if wcsclass == "AstropyWCS":
|
|
343
|
+
hdr = self._get_header()
|
|
344
|
+
self._wcs = AstropyWCS.from_header( hdr )
|
|
345
|
+
elif wcsclass == "GalsimWCS":
|
|
346
|
+
hdr = self._get_header()
|
|
347
|
+
self._wcs = GalsimWCS.from_header( hdr )
|
|
348
|
+
return self._wcs
|
|
349
|
+
|
|
350
|
+
def get_cutout(self, x, y, xsize, ysize=None):
|
|
351
|
+
"""Creates a new snappl image object that is a cutout of the original image, at a location in pixel-space.
|
|
352
|
+
|
|
353
|
+
This implementation (in FITSImage) assumes that the image WCS is an AstropyWCS.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
x : int
|
|
358
|
+
x pixel coordinate of the center of the cutout.
|
|
359
|
+
y : int
|
|
360
|
+
y pixel coordinate of the center of the cutout.
|
|
361
|
+
xsize : int
|
|
362
|
+
Width of the cutout in pixels.
|
|
363
|
+
ysize : int
|
|
364
|
+
Height of the cutout in pixels. If None, set to xsize.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
cutout : snappl.image.Image
|
|
369
|
+
A new snappl image object that is a cutout of the original image.
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
if not all( [ isinstance( x, (int, np.integer) ),
|
|
373
|
+
isinstance( y, (int, np.integer) ),
|
|
374
|
+
isinstance( xsize, (int, np.integer) ),
|
|
375
|
+
( ysize is None or isinstance( ysize, (int, np.integer) ) )
|
|
376
|
+
] ):
|
|
377
|
+
raise TypeError( "All of x, y, xsize, and ysize must be integers." )
|
|
378
|
+
|
|
379
|
+
if ysize is None:
|
|
380
|
+
ysize = xsize
|
|
381
|
+
if xsize % 2 != 1 or ysize % 2 != 1:
|
|
382
|
+
raise ValueError( f"Size must be odd for a well defined central "
|
|
383
|
+
f"pixel, you tried to pass a size of {xsize, ysize}.")
|
|
384
|
+
|
|
385
|
+
SNLogger.debug(f'Cutting out at {x , y}')
|
|
386
|
+
data, noise, flags = self.get_data( 'all', always_reload=False )
|
|
387
|
+
|
|
388
|
+
wcs = self.get_wcs()
|
|
389
|
+
if ( wcs is not None ) and ( not isinstance( wcs, AstropyWCS ) ):
|
|
390
|
+
raise TypeError( "Error, FITSImage.get_cutout only works with AstropyWCS wcses" )
|
|
391
|
+
apwcs = None if wcs is None else wcs._wcs
|
|
392
|
+
|
|
393
|
+
# Remember that numpy arrays are indexed [y, x] (at least if they're read with astropy.io.fits)
|
|
394
|
+
astropy_cutout = Cutout2D(data, (x, y), size=(ysize, xsize), mode='strict', wcs=apwcs)
|
|
395
|
+
astropy_noise = Cutout2D(noise, (x, y), size=(ysize, xsize), mode='strict', wcs=apwcs)
|
|
396
|
+
astropy_flags = Cutout2D(flags, (x, y), size=(ysize, xsize), mode='strict', wcs=apwcs)
|
|
397
|
+
|
|
398
|
+
snappl_cutout = self.__class__(self.inputs.path, self.inputs.exposure, self.inputs.sca)
|
|
399
|
+
snappl_cutout._data = astropy_cutout.data
|
|
400
|
+
snappl_cutout._wcs = None if wcs is None else AstropyWCS( astropy_cutout.wcs )
|
|
401
|
+
snappl_cutout._noise = astropy_noise.data
|
|
402
|
+
snappl_cutout._flags = astropy_flags.data
|
|
403
|
+
snappl_cutout._is_cutout = True
|
|
404
|
+
|
|
405
|
+
return snappl_cutout
|
|
406
|
+
|
|
407
|
+
def get_ra_dec_cutout(self, ra, dec, xsize, ysize=None):
|
|
408
|
+
"""Creates a new snappl image object that is a cutout of the original image, at a location in pixel-space.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
ra : float
|
|
413
|
+
RA coordinate of the center of the cutout, in degrees.
|
|
414
|
+
dec : float
|
|
415
|
+
DEC coordinate of the center of the cutout, in degrees.
|
|
416
|
+
xsize : int
|
|
417
|
+
Width of the cutout in pixels.
|
|
418
|
+
ysize : int
|
|
419
|
+
Height of the cutout in pixels. If None, set to xsize.
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
cutout : snappl.image.Image
|
|
424
|
+
A new snappl image object that is a cutout of the original image.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
wcs = self.get_wcs()
|
|
428
|
+
x, y = wcs.world_to_pixel( ra, dec )
|
|
429
|
+
x = int( np.floor( x + 0.5 ) )
|
|
430
|
+
y = int( np.floor( y + 0.5 ) )
|
|
431
|
+
return self.get_cutout( x, y, xsize, ysize )
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ======================================================================
|
|
435
|
+
# OpenUniverse 2024 Images are gzipped FITS files
|
|
436
|
+
# HDU 0 : (something, no data)
|
|
437
|
+
# HDU 1 : SCI (32-bit float)
|
|
438
|
+
# HDU 2 : ERR (32-bit float)
|
|
439
|
+
# HDU 3 : DQ (32-bit integer)
|
|
440
|
+
|
|
441
|
+
class OpenUniverse2024FITSImage( FITSImage ):
|
|
442
|
+
def __init__( self, *args, **kwargs ):
|
|
443
|
+
super().__init__( *args, **kwargs )
|
|
444
|
+
|
|
445
|
+
def get_data( self, which='all', always_reload=False, cache=False ):
|
|
446
|
+
if self._is_cutout:
|
|
447
|
+
raise RuntimeError( "get_data called on a cutout image, this will return the ORIGINAL UNCUT image. "
|
|
448
|
+
"Currently not supported.")
|
|
449
|
+
if which not in Image.data_array_list:
|
|
450
|
+
raise ValueError( f"Unknown which {which}, must be all, data, noise, or flags" )
|
|
451
|
+
|
|
452
|
+
if not always_reload:
|
|
453
|
+
if ( ( which == 'all' )
|
|
454
|
+
and ( self._data is not None )
|
|
455
|
+
and ( self._noise is not None )
|
|
456
|
+
and ( self._flags is not None )
|
|
457
|
+
):
|
|
458
|
+
return [ self._data, self._noise, self._flags ]
|
|
459
|
+
|
|
460
|
+
if ( which == 'data' ) and ( self._data is not None ):
|
|
461
|
+
return [ self._data ]
|
|
462
|
+
|
|
463
|
+
if ( which == 'noise' ) and ( self._noise is not None ):
|
|
464
|
+
return [ self._noise ]
|
|
465
|
+
|
|
466
|
+
if ( which == 'flags' ) and ( self._flags is not None ):
|
|
467
|
+
return [ self._flags ]
|
|
468
|
+
|
|
469
|
+
SNLogger.info( f"Reading FITS file {self.inputs.path}" )
|
|
470
|
+
with fits.open( self.inputs.path ) as hdul:
|
|
471
|
+
if cache:
|
|
472
|
+
self._header = hdul[1].header
|
|
473
|
+
if which == 'all':
|
|
474
|
+
imgs = [ hdul[1].data, hdul[2].data, hdul[3].data ]
|
|
475
|
+
if cache:
|
|
476
|
+
self._data = imgs[0]
|
|
477
|
+
self._noise = imgs[1]
|
|
478
|
+
self._flags = imgs[2]
|
|
479
|
+
return imgs
|
|
480
|
+
elif which == 'data':
|
|
481
|
+
if cache:
|
|
482
|
+
self._data = hdul[1].data
|
|
483
|
+
return [ hdul[1].data ]
|
|
484
|
+
elif which == 'noise':
|
|
485
|
+
if cache:
|
|
486
|
+
self._noise = hdul[2].data
|
|
487
|
+
return [ hdul[2].data ]
|
|
488
|
+
elif which == 'flags':
|
|
489
|
+
if cache:
|
|
490
|
+
self._flags = hdul[3].data
|
|
491
|
+
return [ hdul[3].data ]
|
|
492
|
+
else:
|
|
493
|
+
raise RuntimeError( f"{self.__class__.__name__} doesn't understand data plane {which}" )
|
|
494
|
+
|
|
495
|
+
def _get_header(self):
|
|
496
|
+
"""Get the header of the image."""
|
|
497
|
+
if self._header is None:
|
|
498
|
+
with fits.open(self.inputs.path) as hdul:
|
|
499
|
+
self._header = hdul[1].header
|
|
500
|
+
return self._header
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def band(self):
|
|
504
|
+
"""The band the image is taken in (str)."""
|
|
505
|
+
header = self._get_header()
|
|
506
|
+
return header['FILTER'].strip()
|