roman-snpit-snappl 0.2.4__tar.gz → 0.4.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.

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