roman-snpit-snappl 0.4.0__tar.gz → 0.5.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 (67) hide show
  1. {roman_snpit_snappl-0.4.0/roman_snpit_snappl.egg-info → roman_snpit_snappl-0.5.0}/PKG-INFO +1 -1
  2. roman_snpit_snappl-0.5.0/changes/20.bugfix.rst +1 -0
  3. roman_snpit_snappl-0.5.0/changes/23.snappl.rst +1 -0
  4. roman_snpit_snappl-0.5.0/changes/26.feature.rst +3 -0
  5. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0/roman_snpit_snappl.egg-info}/PKG-INFO +1 -1
  6. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/SOURCES.txt +3 -0
  7. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/_version.py +2 -2
  8. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/psf.py +185 -39
  9. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/wcs.py +3 -2
  10. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.cruft.json +0 -0
  11. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/CODEOWNERS +0 -0
  12. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +0 -0
  13. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +0 -0
  14. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/PR_TEMPLATE.md +0 -0
  15. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/dependabot.yml +0 -0
  16. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/labeler.yml +0 -0
  17. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/workflows/changelog.yml +0 -0
  18. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/workflows/run_labeler.yml +0 -0
  19. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/workflows/run_snappl_tests.yml +0 -0
  20. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/workflows/sphinx-deploy.yml +0 -0
  21. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.github/workflows/sub_package_update.yml +0 -0
  22. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.gitignore +0 -0
  23. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/.pre-commit-config.yaml +0 -0
  24. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/CHANGES.rst +0 -0
  25. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/CITATION.cff +0 -0
  26. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  27. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/CONTRIBUTING.md +0 -0
  28. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/LICENSE +0 -0
  29. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/MANIFEST.in +0 -0
  30. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/README.rst +0 -0
  31. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/.gitkeep +0 -0
  32. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/10.snappl.rst +0 -0
  33. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/13.bugfix.rst +0 -0
  34. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/14.snappl.rst +0 -0
  35. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/15.feature.rst +0 -0
  36. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/16.feature.rst +0 -0
  37. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/18.feature.rst +0 -0
  38. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/3.snappl.rst +0 -0
  39. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/5.snappl.rst +0 -0
  40. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/8.snappl.rst +0 -0
  41. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/changes/9.snappl.rst +0 -0
  42. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/codespell-ignore.txt +0 -0
  43. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/Makefile +0 -0
  44. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/_static/logo_black_filled.png +0 -0
  45. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/changes.rst +0 -0
  46. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/conf.py +0 -0
  47. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/index.rst +0 -0
  48. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/installation.rst +0 -0
  49. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/make.bat +0 -0
  50. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/docs/usage.rst +0 -0
  51. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/licenses/.DS_Store +0 -0
  52. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/licenses/LICENSE.rst +0 -0
  53. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/licenses/README.rst +0 -0
  54. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/licenses/TEMPLATE_LICENSE.rst +0 -0
  55. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/pyproject.toml +0 -0
  56. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/dependency_links.txt +0 -0
  57. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/not-zip-safe +0 -0
  58. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/requires.txt +0 -0
  59. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/top_level.txt +0 -0
  60. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/setup.cfg +0 -0
  61. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/setup.py +0 -0
  62. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/__init__.py +0 -0
  63. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/_dev/__init__.py +0 -0
  64. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/_dev/scm_version.py +0 -0
  65. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/data/README.rst +0 -0
  66. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/snappl/image.py +0 -0
  67. {roman_snpit_snappl-0.4.0 → roman_snpit_snappl-0.5.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.4.0
3
+ Version: 0.5.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
+ Properly support creation of PSFs at non-integral pixel positions in OversampledImagePSF
@@ -0,0 +1 @@
1
+ AstropyWCS now automatically determines appropriate frame.
@@ -0,0 +1,3 @@
1
+ Added A25ePSF class, made tests, updated yaml save format for YamlSerialized_OversampledImagePSF.
2
+ Added clarifying variable names, blocked and alphabetized imports.
3
+ Added comment about the gridsize int() conversion in A25ePSF.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roman_snpit_snappl
3
- Version: 0.4.0
3
+ Version: 0.5.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>
@@ -30,6 +30,9 @@ changes/14.snappl.rst
30
30
  changes/15.feature.rst
31
31
  changes/16.feature.rst
32
32
  changes/18.feature.rst
33
+ changes/20.bugfix.rst
34
+ changes/23.snappl.rst
35
+ changes/26.feature.rst
33
36
  changes/3.snappl.rst
34
37
  changes/5.snappl.rst
35
38
  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.4.0'
21
- __version_tuple__ = version_tuple = (0, 4, 0)
20
+ __version__ = version = '0.5.0'
21
+ __version_tuple__ = version_tuple = (0, 5, 0)
@@ -1,11 +1,14 @@
1
- import yaml
1
+ # IMPORTS Standard
2
2
  import base64
3
-
4
3
  import numpy as np
4
+ import pathlib
5
+ import yaml
5
6
 
6
- from roman_imsim.utils import roman_utils
7
+ # IMPORTS Astro
7
8
  import galsim
8
9
 
10
+ # IMPORTS Internal
11
+ from roman_imsim.utils import roman_utils
9
12
  from snpit_utils.config import Config
10
13
  from snpit_utils.logger import SNLogger
11
14
 
@@ -16,24 +19,62 @@ class PSF:
16
19
  def __init__( self, *args, **kwargs ):
17
20
  pass
18
21
 
22
+ # This is here for backwards compatibility
23
+ @property
24
+ def clip_size( self ):
25
+ return self.stamp_size
26
+
27
+ @property
28
+ def stamp_size( self ):
29
+ """The size of the one side of a PSF image stamp at image resolution. Is always odd."""
30
+ raise NotImplementedError( f"{self.__class__.__name__} needs to implement stamp_size." )
31
+
32
+
19
33
  def get_stamp( self, x, y, flux=1. ):
20
34
  """Return a 2d numpy image of the PSF at the image resolution.
21
35
 
36
+ The PSF will be centered as best possible on the stamp*. So, if
37
+ x ends in 0.8, it will be left of center, and if x ends in 0.2,
38
+ it will be right of center. If the fractional part of x or y is
39
+ exactly 0.5, there's an ambituity as to where on the image you
40
+ should place the stamp of the PSF. The position of the PSF on
41
+ the returned stamp will always round *down* in this case. (The
42
+ pixel on the image that corresponds to the center pixel on the
43
+ clip is at floor(x+0.5),floor(y+0.5), *not*
44
+ round(x+0.5),round(y+0.5). Those two things are different, and
45
+ round is not consistent; see the comment in
46
+ OversampledImagePSF.get_stamp for more if you care.)
47
+
48
+ So, for example, assuming the PSF is intrinsically centered*,
49
+ if the stamp size is 5×5 and you ask for the PSF at x=1023,
50
+ y=1023, then you're going to want to put the stamp on to the
51
+ image at image[1021:1026,1021:1026]. However, if you ask for
52
+ the PSF at x=1023.5,y=1023., you'll want to put the stamp on the
53
+ image at image[1021:1026,1022:1027]. (Remember that default
54
+ numpy arrays of astronomy images are indexed [y,x].)
55
+
56
+ * "The PSF will be centered as best possible on the stamp": this
57
+ is only true if the PSF itself is intrinsically centered. See
58
+ OversampledImagePSF.create for a discussion of
59
+ non-intrinsically-centered PSFs.
60
+
22
61
  Parameters
23
62
  ----------
24
63
  x: float
25
- Position on the image of the center of the psf
64
+ Position on the image of the center of the psf. If not
65
+ given, defaults to something sensible that was defined when
66
+ the object was constructed. If you want to do sub-pixel
67
+ shifts, then the fractional part of x will (usually) not be
68
+ 0.
26
69
 
27
70
  y: float
28
- Position on the image of the center of the psf
29
-
30
- x0: float or None
31
- Image position of the center of the stamp; defaults to FIGURE THIS OUT
32
-
33
- y0: float or None
71
+ Position on the image of the center of the psf. Same kind
72
+ of default as x.
34
73
 
35
74
  flux: float, default 1.
36
- Make the sum of the clip this
75
+ Make the sum of the clip this. If None, just let the clip
76
+ be scaled however it's naturally scaled. For some
77
+ subclasses, that may be what you actually want.
37
78
 
38
79
  Returns
39
80
  -------
@@ -64,6 +105,9 @@ class PSF:
64
105
  if psfclass == "YamlSerialized_OversampledImagePSF":
65
106
  return YamlSerialized_OversampledImagePSF( **kwargs )
66
107
 
108
+ if psfclass == "A25ePSF":
109
+ return A25ePSF( **kwargs )
110
+
67
111
  if psfclass == "ou24PSF":
68
112
  return ou24PSF( **kwargs )
69
113
 
@@ -72,18 +116,42 @@ class PSF:
72
116
 
73
117
  class OversampledImagePSF( PSF ):
74
118
  @classmethod
75
- def create( cls, data=None, x0=None, y0=None, oversample_factor=1., enforce_odd=True, normalize=True,
76
- **kwargs ):
119
+ def create( cls, data=None, x=None, y=None, oversample_factor=1., enforce_odd=True, normalize=True, **kwargs ):
77
120
 
78
121
  """Parameters
79
122
  ----------
80
123
  data: 2d numpy array
81
-
82
- x0, y0: float
83
- Position on the source image where this PSF is evaluated
124
+ The image data of the oversampled PSF. Required.
125
+
126
+ x, y: float
127
+ Position on the source image where this PSF is evaluated.
128
+ Required. Most of the time, but not always, you probably
129
+ want x and y to be integer values. (As in, not integer
130
+ typed, but floats that satisfy x-floor(x)=0.) These are
131
+ also the defaults that get_stamp will use if x and y are not
132
+ passed to get_stamp.
133
+
134
+ If x and/or y have nonzero fractional parts, then the data
135
+ array must be consistent. First consider non-oversampled
136
+ data. Suppose you pass a 11×11 array with x=1022.5 and
137
+ y=1023.25. In this case, the peak of a perfectly symmetric
138
+ PSF image on data would be at (4.5, 5.25). (Not (5.5,
139
+ 5.25)! If something's at *exactly* .5, always round down
140
+ here regardless of wheter the integer part is even or odd.)
141
+ The center pixel and the one to the right of it should have
142
+ the same brightness, and the pixel just below center should
143
+ be dimmer than the pixel just above center.
144
+
145
+ For oversampled psfs, the data array must be properly
146
+ shifted to account for non-integral x and y. The shift will
147
+ be as in non-oversampled data, only multiplied by the
148
+ oversampling factor. So, in the same example, if you
149
+ specify a peak of (4.5, 5.25), and you have an oversampling
150
+ factor of 3, you should pass a 33×33 array with the peak of
151
+ the PSF (assuming a symmetric PSF) at (14.5, 16.75).
84
152
 
85
153
  oversample_factor: float, default 1.
86
- There are this many pixels along one axis in data for one pixel in the original image
154
+ There are this many pixels along one axis in data for one pixel in the original image.
87
155
 
88
156
  enforce_odd: bool, default True
89
157
  Enforce x_edges and y_edges having an odd width.
@@ -106,18 +174,26 @@ class OversampledImagePSF( PSF ):
106
174
  if not isinstance( data, np.ndarray ) or ( len(data.shape) != 2 ):
107
175
  raise TypeError( "data must be a 2d numpy array" )
108
176
 
109
- x0 = float( x0 )
110
- y0 = float( y0 )
177
+ x = float( x )
178
+ y = float( y )
111
179
 
112
180
  psf = cls()
113
181
  psf._data = data
114
182
  if normalize:
115
183
  psf._data /= psf._data.sum()
116
- psf._x0 = x0
117
- psf._y0 = y0
184
+ psf._x = x
185
+ psf._y = y
118
186
  psf._oversamp = oversample_factor
119
187
  return psf
120
188
 
189
+ @property
190
+ def x( self ):
191
+ return self._x
192
+
193
+ @property
194
+ def y( self ):
195
+ return self._y
196
+
121
197
  @property
122
198
  def x0( self ):
123
199
  return self._x0
@@ -135,21 +211,29 @@ class OversampledImagePSF( PSF ):
135
211
  return self._data
136
212
 
137
213
  @property
138
- def clip_size( self ):
139
- """The size of the PSF image clip at image resolution."""
140
- return int( np.floor( self._data.shape[0] / self._oversamp ) )
214
+ def stamp_size( self ):
215
+ """The size of the PSF image clip at image resolution. Is always odd."""
216
+ sz = int( np.floor( self._data.shape[0] / self._oversamp ) )
217
+ sz += 1 if sz % 2 == 0 else 0
218
+ return sz
219
+
141
220
 
142
221
  def __init__( self, *args, **kwargs ):
143
222
  super().__init__( *args, **kwargs )
144
223
  self._data = None
224
+ self._x = None
225
+ self._y = None
145
226
  self._x0 = None
146
227
  self._y0 = None
147
228
  self._oversamp = None
148
229
 
149
- def get_stamp( self, x=None, y=None, normalize=True ):
150
- x = float(x) if x is not None else self._x0
151
- y = float(y) if y is not None else self._y0
230
+ def get_stamp( self, x=None, y=None, flux=1. ):
231
+ # (x, y) is the position on the image for which we want to render the PSF.
232
+ x = float(x) if x is not None else self._x
233
+ y = float(y) if y is not None else self._y
152
234
 
235
+ # (xc, yc) is the closest pixel center to (x, y) on the image--
236
+ #
153
237
  # round() isn't the right thing to use here, because it will
154
238
  # behave differently when x - round(x) = 0.5 based on whether
155
239
  # floor(x) is even or odd. What we *want* is for the psf to
@@ -161,10 +245,22 @@ class OversampledImagePSF( PSF ):
161
245
  # and to the left when the fractional part of x (and y) is
162
246
  # exactly 0.5, whereas using round would give different
163
247
  # results based on the integer part of x (and y).
164
-
165
248
  xc = int( np.floor( x + 0.5 ) )
166
249
  yc = int( np.floor( y + 0.5 ) )
167
250
 
251
+ # (natx, naty) is the "natural position" on the image for the
252
+ # psf. This is simply (int(x), int(y)) if the fractional part
253
+ # of x and y are zero. Otherwise, it rounds to the closest
254
+ # pixel... unless the fractional part is exactly 0.5, in which
255
+ # case we do floor(x+0.5) instead of round(x) as described above.
256
+ natx = int( np.floor( self._x + 0.5 ) )
257
+ naty = int( np.floor( self._y + 0.5 ) )
258
+ # natxfrac and natyfrac kinda the negative of the fractional
259
+ # part of natx and naty. They will be in the range (-0.5,
260
+ # 0.5]
261
+ natxfrac = natx - self._x
262
+ natyfrac = naty - self._y
263
+
168
264
  # See Chapter 5, "How PSFEx Works", of the PSFEx manual
169
265
  # https://psfex.readthedocs.io/en/latest/Working.html
170
266
  # We're using this method for both image and psfex PSFs,
@@ -175,22 +271,23 @@ class OversampledImagePSF( PSF ):
175
271
 
176
272
  psfwid = self._data.shape[0]
177
273
  stampwid = self.clip_size
178
- stampwid += 1 if stampwid % 2 == 0 else 0
179
274
 
180
275
  psfdex1d = np.arange( -( psfwid//2), psfwid//2+1, dtype=int )
181
276
 
277
+ # If the returned clip is to be added to the image, it should
278
+ # be added to image[ymin:ymax, xmin:xmax].
182
279
  xmin = xc - stampwid // 2
183
280
  xmax = xc + stampwid // 2 + 1
184
281
  ymin = yc - stampwid // 2
185
282
  ymax = yc + stampwid // 2 + 1
186
283
 
187
284
  psfsamp = 1. / self._oversamp
188
- xs = np.array( range( xmin, xmax ) )
189
- ys = np.array( range( ymin, ymax ) )
190
- xsincarg = psfdex1d[:, np.newaxis] - ( xs - x ) / psfsamp
285
+ xs = np.arange( xmin, xmax )
286
+ ys = np.arange( ymin, ymax )
287
+ xsincarg = psfdex1d[:, np.newaxis] - ( xs - natxfrac - x ) / psfsamp
191
288
  xsincvals = np.sinc( xsincarg ) * np.sinc( xsincarg/4. )
192
289
  xsincvals[ ( xsincarg > 4 ) | ( xsincarg < -4 ) ] = 0.
193
- ysincarg = psfdex1d[:, np.newaxis] - ( ys - y ) / psfsamp
290
+ ysincarg = psfdex1d[:, np.newaxis] - ( ys - natyfrac - y ) / psfsamp
194
291
  ysincvals = np.sinc( ysincarg ) * np.sinc( ysincarg/4. )
195
292
  ysincvals[ ( ysincarg > 4 ) | ( ysincarg < -4 ) ] = 0.
196
293
  tenpro = np.tensordot( ysincvals[:, :, np.newaxis], xsincvals[:, :, np.newaxis], axes=0 )[ :, :, 0, :, :, 0 ]
@@ -216,8 +313,7 @@ class OversampledImagePSF( PSF ):
216
313
  # * ysincvals[:, np.newaxis]
217
314
  # * psfbase ).sum()
218
315
 
219
- if normalize:
220
- clip /= clip.sum()
316
+ clip *= flux / clip.sum()
221
317
 
222
318
  return clip
223
319
 
@@ -229,15 +325,15 @@ class YamlSerialized_OversampledImagePSF( OversampledImagePSF ):
229
325
 
230
326
  def read( self, filepath ):
231
327
  y = yaml.safe_load( open( filepath ) )
232
- self._x0 = y['x0']
233
- self._y0 = y['y0']
328
+ self._x = y['x0']
329
+ self._y = y['y0']
234
330
  self._oversamp = y['oversamp']
235
331
  self._data = np.frombuffer( base64.b64decode( y['data'] ), dtype=y['dtype'] )
236
332
  self._data = self._data.reshape( ( y['shape0'], y['shape1'] ) )
237
333
 
238
334
  def write( self, filepath ):
239
- out = { 'x0': float( self._x0 ),
240
- 'y0': float( self._y0 ),
335
+ out = { 'x0': float( self._x ),
336
+ 'y0': float( self._y ),
241
337
  'oversamp': self._oversamp,
242
338
  'shape0': self._data.shape[0],
243
339
  'shape1': self._data.shape[1],
@@ -247,6 +343,46 @@ class YamlSerialized_OversampledImagePSF( OversampledImagePSF ):
247
343
  # TODO : check overwriting etc.
248
344
  yaml.dump( out, open( filepath, 'w' ) )
249
345
 
346
+ class A25ePSF( YamlSerialized_OversampledImagePSF ):
347
+
348
+ def __init__( self, band, sca, x, y, *args, **kwargs ):
349
+
350
+ super().__init__( *args, **kwargs )
351
+
352
+ cfg = Config.get()
353
+ basepath = pathlib.Path( cfg.value( 'photometry.snappl.A25ePSF_path' ) )
354
+
355
+ """
356
+ The array size is the size of one image (nx, ny).
357
+ The grid size is the number of times we divide that image
358
+ into smaller parts for the purposes of assigning the
359
+ correct ePSF (8 x 8 = 64 ePSFs).
360
+
361
+ 4088 px/8 = 511 px. So, int(arr_size/gridsize) is just a type
362
+ conversion. In the future, we may have a class where these things
363
+ are variable, but for now, we are using only the 8 x 8 grid of
364
+ ePSFs from Aldoroty et al. 2025a. So, it's hardcoded.
365
+
366
+ """
367
+ arr_size = 4088
368
+ gridsize = 8
369
+ cutoutsize = int(arr_size/gridsize)
370
+ grid_centers = np.linspace(0.5 * cutoutsize, arr_size - 0.5 * cutoutsize, gridsize)
371
+
372
+ dist_x = np.abs(grid_centers - x)
373
+ dist_y = np.abs(grid_centers - y)
374
+
375
+ x_idx = np.argmin(dist_x)
376
+ y_idx = np.argmin(dist_y)
377
+
378
+ x_cen = grid_centers[x_idx]
379
+ y_cen = grid_centers[y_idx]
380
+
381
+ min_mag = 19.0
382
+ max_mag = 21.5
383
+ psfpath = basepath / band / str(sca) / f'{cutoutsize}_{x_cen:.1f}_{y_cen:.1f}_-_{min_mag}_{max_mag}_-_{band}_{sca}.psf'
384
+
385
+ self.read(psfpath)
250
386
 
251
387
  class ou24PSF( PSF ):
252
388
  # Currently, does not support any oversampling, because SFFT doesn't
@@ -258,6 +394,10 @@ class ou24PSF( PSF ):
258
394
 
259
395
  if ( pointing is None ) or ( sca is None ):
260
396
  raise ValueError( "Need a pointing and an sca to make an ou24PSF" )
397
+ if ( size % 2 == 0 ) or ( int(size) != size ):
398
+ raise ValueError( "Size must be an odd integer." )
399
+ size = int( size )
400
+
261
401
  if config_file is None:
262
402
  config_file = Config.get().value( 'ou24psf.config_file' )
263
403
  self.config_file = config_file
@@ -267,6 +407,12 @@ class ou24PSF( PSF ):
267
407
  self.include_photonOps = include_photonOps
268
408
  self._stamps = {}
269
409
 
410
+
411
+ @property
412
+ def stamp_size( self ):
413
+ return self.size
414
+
415
+
270
416
  def get_stamp( self, x, y, flux=1., seed=None ):
271
417
  """Return a 2d numpy image of the PSF at the image resolution.
272
418
 
@@ -118,8 +118,9 @@ class AstropyWCS(BaseWCS):
118
118
  dec = float( dec )
119
119
  return ra, dec
120
120
 
121
- def world_to_pixel( self, ra, dec ):
122
- scs = SkyCoord( ra, dec, unit=(u.deg, u.deg) )
121
+ def world_to_pixel( self, ra, dec):
122
+ frame = self._wcs.wcs.radesys.lower() # Needs to be lowercase for SkyCoord
123
+ scs = SkyCoord( ra, dec, unit=(u.deg, u.deg), frame = frame)
123
124
  x, y = self._wcs.world_to_pixel( scs )
124
125
  if not ( isinstance( ra, collections.abc.Sequence )
125
126
  or ( isinstance( ra, np.ndarray ) and y.size > 1 )