roman-snpit-snappl 0.2.3__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.

Files changed (75) hide show
  1. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/workflows/run_snappl_tests.yml +1 -1
  2. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/MANIFEST.in +2 -1
  3. {roman_snpit_snappl-0.2.3/roman_snpit_snappl.egg-info → roman_snpit_snappl-0.3.0}/PKG-INFO +1 -1
  4. roman_snpit_snappl-0.3.0/changes/14.snappl.rst +1 -0
  5. roman_snpit_snappl-0.3.0/changes/15.feature.rst +1 -0
  6. roman_snpit_snappl-0.3.0/changes/16.feature.rst +1 -0
  7. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0/roman_snpit_snappl.egg-info}/PKG-INFO +1 -1
  8. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/SOURCES.txt +4 -11
  9. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/_version.py +2 -2
  10. roman_snpit_snappl-0.3.0/snappl/image.py +506 -0
  11. roman_snpit_snappl-0.3.0/snappl/wcs.py +179 -0
  12. roman_snpit_snappl-0.2.3/snappl/image.py +0 -396
  13. roman_snpit_snappl-0.2.3/snappl/tests/__init__.py +0 -1
  14. roman_snpit_snappl-0.2.3/snappl/tests/conftest.py +0 -5
  15. roman_snpit_snappl-0.2.3/snappl/tests/docker-compose.yaml +0 -44
  16. roman_snpit_snappl-0.2.3/snappl/tests/image_test_data/Roman_TDS_simple_model_F184_662_11.fits.gz +0 -0
  17. roman_snpit_snappl-0.2.3/snappl/tests/image_test_data/test_cutout.npy +0 -0
  18. roman_snpit_snappl-0.2.3/snappl/tests/psf_test_data/testpsfarray.npz +0 -0
  19. roman_snpit_snappl-0.2.3/snappl/tests/test_opensim2024fitsimage.py +0 -56
  20. roman_snpit_snappl-0.2.3/snappl/tests/test_oversampled_image_psf.py +0 -24
  21. roman_snpit_snappl-0.2.3/snappl/tests/test_version_string.py +0 -3
  22. roman_snpit_snappl-0.2.3/snappl/tests/test_yaml_serialized_oversampled_image_psf.py +0 -49
  23. roman_snpit_snappl-0.2.3/snappl/wcs.py +0 -37
  24. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.cruft.json +0 -0
  25. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/CODEOWNERS +0 -0
  26. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +0 -0
  27. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +0 -0
  28. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/ISSUE_TEMPLATE/PR_TEMPLATE.md +0 -0
  29. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/dependabot.yml +0 -0
  30. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/labeler.yml +0 -0
  31. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/workflows/changelog.yml +0 -0
  32. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/workflows/run_labeler.yml +0 -0
  33. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/workflows/sphinx-deploy.yml +0 -0
  34. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.github/workflows/sub_package_update.yml +0 -0
  35. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.gitignore +0 -0
  36. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/.pre-commit-config.yaml +0 -0
  37. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/CHANGES.rst +0 -0
  38. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/CITATION.cff +0 -0
  39. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  40. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/CONTRIBUTING.md +0 -0
  41. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/LICENSE +0 -0
  42. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/README.rst +0 -0
  43. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/.gitkeep +0 -0
  44. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/10.snappl.rst +0 -0
  45. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/13.bugfix.rst +0 -0
  46. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/3.snappl.rst +0 -0
  47. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/5.snappl.rst +0 -0
  48. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/8.snappl.rst +0 -0
  49. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/changes/9.snappl.rst +0 -0
  50. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/codespell-ignore.txt +0 -0
  51. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/Makefile +0 -0
  52. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/_static/logo_black_filled.png +0 -0
  53. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/changes.rst +0 -0
  54. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/conf.py +0 -0
  55. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/index.rst +0 -0
  56. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/installation.rst +0 -0
  57. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/make.bat +0 -0
  58. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/docs/usage.rst +0 -0
  59. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/licenses/.DS_Store +0 -0
  60. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/licenses/LICENSE.rst +0 -0
  61. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/licenses/README.rst +0 -0
  62. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/licenses/TEMPLATE_LICENSE.rst +0 -0
  63. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/pyproject.toml +0 -0
  64. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/dependency_links.txt +0 -0
  65. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/not-zip-safe +0 -0
  66. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/requires.txt +0 -0
  67. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/roman_snpit_snappl.egg-info/top_level.txt +0 -0
  68. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/setup.cfg +0 -0
  69. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/setup.py +0 -0
  70. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/__init__.py +0 -0
  71. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/_dev/__init__.py +0 -0
  72. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/_dev/scm_version.py +0 -0
  73. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/data/README.rst +0 -0
  74. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/snappl/psf.py +0 -0
  75. {roman_snpit_snappl-0.2.3 → roman_snpit_snappl-0.3.0}/tox.ini +0 -0
@@ -42,7 +42,7 @@ jobs:
42
42
 
43
43
  - name: pull latest SNPIT cpu image
44
44
  run: |
45
- docker pull rknop/roman-snpit-env:cpu
45
+ docker pull rknop/roman-snpit-env:cpu-dev
46
46
 
47
47
  - name: run test
48
48
  run: |
@@ -8,4 +8,5 @@ global-exclude *.pyc *.o
8
8
  prune snappl/_version.py
9
9
  include LICENSE
10
10
  include README.rst
11
- include pyproject.toml
11
+ include pyproject.toml
12
+ prune snappl/tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.2.3
3
+ Version: 0.3.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
+ Remove tests from what gets packaged for distribution
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.2.3
3
+ Version: 0.3.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>
@@ -26,6 +26,9 @@ tox.ini
26
26
  changes/.gitkeep
27
27
  changes/10.snappl.rst
28
28
  changes/13.bugfix.rst
29
+ changes/14.snappl.rst
30
+ changes/15.feature.rst
31
+ changes/16.feature.rst
29
32
  changes/3.snappl.rst
30
33
  changes/5.snappl.rst
31
34
  changes/8.snappl.rst
@@ -55,14 +58,4 @@ snappl/psf.py
55
58
  snappl/wcs.py
56
59
  snappl/_dev/__init__.py
57
60
  snappl/_dev/scm_version.py
58
- snappl/data/README.rst
59
- snappl/tests/__init__.py
60
- snappl/tests/conftest.py
61
- snappl/tests/docker-compose.yaml
62
- snappl/tests/test_opensim2024fitsimage.py
63
- snappl/tests/test_oversampled_image_psf.py
64
- snappl/tests/test_version_string.py
65
- snappl/tests/test_yaml_serialized_oversampled_image_psf.py
66
- snappl/tests/image_test_data/Roman_TDS_simple_model_F184_662_11.fits.gz
67
- snappl/tests/image_test_data/test_cutout.npy
68
- snappl/tests/psf_test_data/testpsfarray.npz
61
+ snappl/data/README.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.3'
21
- __version_tuple__ = version_tuple = (0, 2, 3)
20
+ __version__ = version = '0.3.0'
21
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -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()