roman-snpit-snappl 0.3.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 (68) hide show
  1. {roman_snpit_snappl-0.3.0/roman_snpit_snappl.egg-info → roman_snpit_snappl-0.5.0}/PKG-INFO +1 -1
  2. roman_snpit_snappl-0.5.0/changes/18.feature.rst +1 -0
  3. roman_snpit_snappl-0.5.0/changes/20.bugfix.rst +1 -0
  4. roman_snpit_snappl-0.5.0/changes/23.snappl.rst +1 -0
  5. roman_snpit_snappl-0.5.0/changes/26.feature.rst +3 -0
  6. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0/roman_snpit_snappl.egg-info}/PKG-INFO +1 -1
  7. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/SOURCES.txt +4 -0
  8. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/_version.py +2 -2
  9. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/image.py +51 -1
  10. roman_snpit_snappl-0.5.0/snappl/psf.py +438 -0
  11. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/wcs.py +3 -2
  12. roman_snpit_snappl-0.3.0/snappl/psf.py +0 -207
  13. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.cruft.json +0 -0
  14. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/CODEOWNERS +0 -0
  15. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +0 -0
  16. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +0 -0
  17. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/ISSUE_TEMPLATE/PR_TEMPLATE.md +0 -0
  18. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/dependabot.yml +0 -0
  19. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/labeler.yml +0 -0
  20. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/workflows/changelog.yml +0 -0
  21. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/workflows/run_labeler.yml +0 -0
  22. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/workflows/run_snappl_tests.yml +0 -0
  23. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/workflows/sphinx-deploy.yml +0 -0
  24. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.github/workflows/sub_package_update.yml +0 -0
  25. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.gitignore +0 -0
  26. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/.pre-commit-config.yaml +0 -0
  27. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/CHANGES.rst +0 -0
  28. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/CITATION.cff +0 -0
  29. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  30. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/CONTRIBUTING.md +0 -0
  31. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/LICENSE +0 -0
  32. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/MANIFEST.in +0 -0
  33. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/README.rst +0 -0
  34. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/.gitkeep +0 -0
  35. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/10.snappl.rst +0 -0
  36. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/13.bugfix.rst +0 -0
  37. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/14.snappl.rst +0 -0
  38. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/15.feature.rst +0 -0
  39. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/16.feature.rst +0 -0
  40. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/3.snappl.rst +0 -0
  41. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/5.snappl.rst +0 -0
  42. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/8.snappl.rst +0 -0
  43. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/changes/9.snappl.rst +0 -0
  44. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/codespell-ignore.txt +0 -0
  45. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/Makefile +0 -0
  46. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/_static/logo_black_filled.png +0 -0
  47. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/changes.rst +0 -0
  48. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/conf.py +0 -0
  49. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/index.rst +0 -0
  50. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/installation.rst +0 -0
  51. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/make.bat +0 -0
  52. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/docs/usage.rst +0 -0
  53. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/licenses/.DS_Store +0 -0
  54. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/licenses/LICENSE.rst +0 -0
  55. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/licenses/README.rst +0 -0
  56. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/licenses/TEMPLATE_LICENSE.rst +0 -0
  57. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/pyproject.toml +0 -0
  58. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/dependency_links.txt +0 -0
  59. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/not-zip-safe +0 -0
  60. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/requires.txt +0 -0
  61. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/roman_snpit_snappl.egg-info/top_level.txt +0 -0
  62. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/setup.cfg +0 -0
  63. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/setup.py +0 -0
  64. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/__init__.py +0 -0
  65. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/_dev/__init__.py +0 -0
  66. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/_dev/scm_version.py +0 -0
  67. {roman_snpit_snappl-0.3.0 → roman_snpit_snappl-0.5.0}/snappl/data/README.rst +0 -0
  68. {roman_snpit_snappl-0.3.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.3.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
+ Add a few properties (sca, zeropoint, etc.) to Image ; add ou2024PSF.
@@ -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.3.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>
@@ -29,6 +29,10 @@ changes/13.bugfix.rst
29
29
  changes/14.snappl.rst
30
30
  changes/15.feature.rst
31
31
  changes/16.feature.rst
32
+ changes/18.feature.rst
33
+ changes/20.bugfix.rst
34
+ changes/23.snappl.rst
35
+ changes/26.feature.rst
32
36
  changes/3.snappl.rst
33
37
  changes/5.snappl.rst
34
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.3.0'
21
- __version_tuple__ = version_tuple = (0, 3, 0)
20
+ __version__ = version = '0.5.0'
21
+ __version_tuple__ = version_tuple = (0, 5, 0)
@@ -1,10 +1,13 @@
1
1
  import types
2
+ import pathlib
2
3
 
3
4
  import numpy as np
4
5
  from astropy.io import fits
5
6
  from astropy.nddata.utils import Cutout2D
6
7
  # from astropy.coordinates import SkyCoord
7
8
 
9
+ import galsim.roman
10
+
8
11
  from snpit_utils.logger import SNLogger
9
12
  from snappl.wcs import AstropyWCS, GalsimWCS
10
13
 
@@ -63,11 +66,12 @@ class Image:
63
66
 
64
67
  """
65
68
  self.inputs = types.SimpleNamespace()
66
- self.inputs.path = path
69
+ self.inputs.path = pathlib.Path( path )
67
70
  self.inputs.exposure = exposure
68
71
  self.inputs.sca = sca
69
72
  self._wcs = None # a BaseWCS object (in wcs.py)
70
73
  self._is_cutout = False
74
+ self._zeropoint = None
71
75
 
72
76
  @property
73
77
  def data( self ):
@@ -101,6 +105,18 @@ class Image:
101
105
  """Tuple: (ny, nx) pixel size of image."""
102
106
  raise NotImplementedError( f"{self.__class__.__name__} needs to implement image_shape" )
103
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
+
104
120
  @property
105
121
  def sky_level( self ):
106
122
  """Estimate of the sky level in ADU."""
@@ -116,6 +132,24 @@ class Image:
116
132
  """Band (str)"""
117
133
  raise NotImplementedError( f"{self.__class__.__name__} needs to implement band" )
118
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
+
119
153
  @property
120
154
  def mjd( self ):
121
155
  """MJD of the start of the image (defined how? TAI?)"""
@@ -192,7 +226,12 @@ class Image:
192
226
  """
193
227
  raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_wcs" )
194
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
+
195
233
  def get_cutout(self, ra, dec, size):
234
+
196
235
  """Make a cutout of the image at the given RA and DEC.
197
236
 
198
237
  Returns
@@ -504,3 +543,14 @@ class OpenUniverse2024FITSImage( FITSImage ):
504
543
  """The band the image is taken in (str)."""
505
544
  header = self._get_header()
506
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']
@@ -0,0 +1,438 @@
1
+ # IMPORTS Standard
2
+ import base64
3
+ import numpy as np
4
+ import pathlib
5
+ import yaml
6
+
7
+ # IMPORTS Astro
8
+ import galsim
9
+
10
+ # IMPORTS Internal
11
+ from roman_imsim.utils import roman_utils
12
+ from snpit_utils.config import Config
13
+ from snpit_utils.logger import SNLogger
14
+
15
+
16
+ class PSF:
17
+ # Thought required: how to deal with oversampling.
18
+
19
+ def __init__( self, *args, **kwargs ):
20
+ pass
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
+
33
+ def get_stamp( self, x, y, flux=1. ):
34
+ """Return a 2d numpy image of the PSF at the image resolution.
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
+
61
+ Parameters
62
+ ----------
63
+ x: float
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.
69
+
70
+ y: float
71
+ Position on the image of the center of the psf. Same kind
72
+ of default as x.
73
+
74
+ flux: float, default 1.
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.
78
+
79
+ Returns
80
+ -------
81
+ 2d numpy array
82
+
83
+ """
84
+ raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_stamp" )
85
+
86
+ @classmethod
87
+ def get_psf_object( cls, psfclass, **kwargs ):
88
+ """Return a PSF object whose type is specified by psfclass.
89
+
90
+ Parameters
91
+ ----------
92
+ psfclass : str
93
+ The name of the class of the PSF to instantiate.
94
+
95
+ **kwargs : further keyword arguments
96
+ TODO : we need to standardize on these so that things can
97
+ just call PSF.get_psf_object() without having to have their
98
+ own if statements on the type to figure out what kwargs to
99
+ pass!
100
+
101
+ """
102
+ if psfclass == "OversampledImagePSF":
103
+ return OversampledImagePSF.create( **kwargs )
104
+
105
+ if psfclass == "YamlSerialized_OversampledImagePSF":
106
+ return YamlSerialized_OversampledImagePSF( **kwargs )
107
+
108
+ if psfclass == "A25ePSF":
109
+ return A25ePSF( **kwargs )
110
+
111
+ if psfclass == "ou24PSF":
112
+ return ou24PSF( **kwargs )
113
+
114
+ raise ValueError( f"Unknown PSF class {psfclass}" )
115
+
116
+
117
+ class OversampledImagePSF( PSF ):
118
+ @classmethod
119
+ def create( cls, data=None, x=None, y=None, oversample_factor=1., enforce_odd=True, normalize=True, **kwargs ):
120
+
121
+ """Parameters
122
+ ----------
123
+ data: 2d numpy array
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).
152
+
153
+ oversample_factor: float, default 1.
154
+ There are this many pixels along one axis in data for one pixel in the original image.
155
+
156
+ enforce_odd: bool, default True
157
+ Enforce x_edges and y_edges having an odd width.
158
+
159
+ normalize: bool, default True
160
+ Make sure internally stored PSF sums to 1 ; you usually don't want to change this.
161
+
162
+ Returns
163
+ -------
164
+ object of type cls
165
+
166
+ """
167
+
168
+ if len(kwargs) > 0:
169
+ SNLogger.warning( f"Unused arguments to OversampledImagePSF.create: {[k for k in kwargs]}" )
170
+
171
+ # TODO : implement enforce_odd
172
+ # TODO : enforce square
173
+
174
+ if not isinstance( data, np.ndarray ) or ( len(data.shape) != 2 ):
175
+ raise TypeError( "data must be a 2d numpy array" )
176
+
177
+ x = float( x )
178
+ y = float( y )
179
+
180
+ psf = cls()
181
+ psf._data = data
182
+ if normalize:
183
+ psf._data /= psf._data.sum()
184
+ psf._x = x
185
+ psf._y = y
186
+ psf._oversamp = oversample_factor
187
+ return psf
188
+
189
+ @property
190
+ def x( self ):
191
+ return self._x
192
+
193
+ @property
194
+ def y( self ):
195
+ return self._y
196
+
197
+ @property
198
+ def x0( self ):
199
+ return self._x0
200
+
201
+ @property
202
+ def y0( self ):
203
+ return self._x0
204
+
205
+ @property
206
+ def oversample_factor( self ):
207
+ return self._oversamp
208
+
209
+ @property
210
+ def oversampled_data( self ):
211
+ return self._data
212
+
213
+ @property
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
+
220
+
221
+ def __init__( self, *args, **kwargs ):
222
+ super().__init__( *args, **kwargs )
223
+ self._data = None
224
+ self._x = None
225
+ self._y = None
226
+ self._x0 = None
227
+ self._y0 = None
228
+ self._oversamp = None
229
+
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
234
+
235
+ # (xc, yc) is the closest pixel center to (x, y) on the image--
236
+ #
237
+ # round() isn't the right thing to use here, because it will
238
+ # behave differently when x - round(x) = 0.5 based on whether
239
+ # floor(x) is even or odd. What we *want* is for the psf to
240
+ # be as close to the center of the clip as possible. In the
241
+ # case where the fractional part of x is exactly 0.5, it's
242
+ # ambiguous what that means-- there are four places you could
243
+ # stick the PSF to statisfy that criterion. By using
244
+ # floor(x+0.5), we will consistently have the psf leaning down
245
+ # and to the left when the fractional part of x (and y) is
246
+ # exactly 0.5, whereas using round would give different
247
+ # results based on the integer part of x (and y).
248
+ xc = int( np.floor( x + 0.5 ) )
249
+ yc = int( np.floor( y + 0.5 ) )
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
+
264
+ # See Chapter 5, "How PSFEx Works", of the PSFEx manual
265
+ # https://psfex.readthedocs.io/en/latest/Working.html
266
+ # We're using this method for both image and psfex PSFs,
267
+ # as the interpolation is more general than PSFEx:
268
+ # https://en.wikipedia.org/wiki/Lanczos_resampling
269
+ # ...though of course, the choice of a=4 comes from PSFEx.
270
+
271
+
272
+ psfwid = self._data.shape[0]
273
+ stampwid = self.clip_size
274
+
275
+ psfdex1d = np.arange( -( psfwid//2), psfwid//2+1, dtype=int )
276
+
277
+ # If the returned clip is to be added to the image, it should
278
+ # be added to image[ymin:ymax, xmin:xmax].
279
+ xmin = xc - stampwid // 2
280
+ xmax = xc + stampwid // 2 + 1
281
+ ymin = yc - stampwid // 2
282
+ ymax = yc + stampwid // 2 + 1
283
+
284
+ psfsamp = 1. / self._oversamp
285
+ xs = np.arange( xmin, xmax )
286
+ ys = np.arange( ymin, ymax )
287
+ xsincarg = psfdex1d[:, np.newaxis] - ( xs - natxfrac - x ) / psfsamp
288
+ xsincvals = np.sinc( xsincarg ) * np.sinc( xsincarg/4. )
289
+ xsincvals[ ( xsincarg > 4 ) | ( xsincarg < -4 ) ] = 0.
290
+ ysincarg = psfdex1d[:, np.newaxis] - ( ys - natyfrac - y ) / psfsamp
291
+ ysincvals = np.sinc( ysincarg ) * np.sinc( ysincarg/4. )
292
+ ysincvals[ ( ysincarg > 4 ) | ( ysincarg < -4 ) ] = 0.
293
+ tenpro = np.tensordot( ysincvals[:, :, np.newaxis], xsincvals[:, :, np.newaxis], axes=0 )[ :, :, 0, :, :, 0 ]
294
+ clip = ( self._data[:, np.newaxis, :, np.newaxis ] * tenpro ).sum( axis=0 ).sum( axis=1 )
295
+
296
+ # Keeping the code below, because the code above is inpenetrable, and it's trying to
297
+ # do the same thing as the code below.
298
+ # (I did emprically test it using the PSFs from the test_psf.py::test_psfex_rendering,
299
+ # and it worked. In particular, there is not a transposition error in the "tenpro=" line;
300
+ # if you swap the order of yxincvals and xsincvals in the test, then the values of clip
301
+ # do not match the code below very well. As is, they match to within a few times 1e-17,
302
+ # which is good enough as the minimum non-zero value in either one is of order 1e-12.)
303
+ # clip = np.empty( ( stampwid, stampwid ), dtype=dtype )
304
+ # for xi in range( xmin, xmax ):
305
+ # for yi in range( ymin, ymax ):
306
+ # xsincarg = psfdex1d - (xi-x) / psfsamp
307
+ # xsincvals = np.sinc( xsincarg ) * np.sinc( xsincarg/4. )
308
+ # xsincvals[ ( xsincarg > 4 ) | ( xsincarg < -4 ) ] = 0
309
+ # ysincarg = psfdex1d - (yi-y) / psfsamp
310
+ # ysincvals = np.sinc( ysincarg ) * np.sinc( ysincarg/4. )
311
+ # ysincvals[ ( ysincarg > 4 ) | ( ysincarg < -4 ) ] = 0
312
+ # clip[ yi-ymin, xi-xmin ] = ( xsincvals[np.newaxis, :]
313
+ # * ysincvals[:, np.newaxis]
314
+ # * psfbase ).sum()
315
+
316
+ clip *= flux / clip.sum()
317
+
318
+ return clip
319
+
320
+
321
+ class YamlSerialized_OversampledImagePSF( OversampledImagePSF ):
322
+
323
+ def __init__( self, *args, **kwargs ):
324
+ super().__init__( *args, **kwargs )
325
+
326
+ def read( self, filepath ):
327
+ y = yaml.safe_load( open( filepath ) )
328
+ self._x = y['x0']
329
+ self._y = y['y0']
330
+ self._oversamp = y['oversamp']
331
+ self._data = np.frombuffer( base64.b64decode( y['data'] ), dtype=y['dtype'] )
332
+ self._data = self._data.reshape( ( y['shape0'], y['shape1'] ) )
333
+
334
+ def write( self, filepath ):
335
+ out = { 'x0': float( self._x ),
336
+ 'y0': float( self._y ),
337
+ 'oversamp': self._oversamp,
338
+ 'shape0': self._data.shape[0],
339
+ 'shape1': self._data.shape[1],
340
+ 'dtype': str( self._data.dtype ),
341
+ # TODO : make this right, think about endian-ness, etc.
342
+ 'data': base64.b64encode( self._data.tobytes() ).decode( 'utf-8' ) }
343
+ # TODO : check overwriting etc.
344
+ yaml.dump( out, open( filepath, 'w' ) )
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)
386
+
387
+ class ou24PSF( PSF ):
388
+ # Currently, does not support any oversampling, because SFFT doesn't
389
+ # TODO: support oversampling!
390
+
391
+ def __init__( self, pointing=None, sca=None, config_file=None, size=201, include_photonOps=True, **kwargs ):
392
+ if len(kwargs) > 0:
393
+ SNLogger.warning( f"Unused arguments to ou24PSF.__init__: {[k for k in kwargs]}" )
394
+
395
+ if ( pointing is None ) or ( sca is None ):
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
+
401
+ if config_file is None:
402
+ config_file = Config.get().value( 'ou24psf.config_file' )
403
+ self.config_file = config_file
404
+ self.pointing = pointing
405
+ self.sca = sca
406
+ self.size = size
407
+ self.include_photonOps = include_photonOps
408
+ self._stamps = {}
409
+
410
+
411
+ @property
412
+ def stamp_size( self ):
413
+ return self.size
414
+
415
+
416
+ def get_stamp( self, x, y, flux=1., seed=None ):
417
+ """Return a 2d numpy image of the PSF at the image resolution.
418
+
419
+ Parameters are as in PSF.get_stamp, plus:
420
+
421
+ Parameters
422
+ ----------
423
+ seed : int
424
+ A random seed to pass to galsim.BaseDeviate for photonOps.
425
+ NOTE: this is not part of the base PSF interface (at least,
426
+ as of yet), so don't use it in production pipeline code.
427
+ However, it will be useful in tests for purposes of testing
428
+ reproducibility.
429
+
430
+ """
431
+ if (x, y) not in self._stamps:
432
+ rmutils = roman_utils( self.config_file, self.pointing, self.sca )
433
+ if seed is not None:
434
+ rmutils.rng = galsim.BaseDeviate( seed )
435
+ self._stamps[(x, y)] = rmutils.getPSF_Image( self.size, x, y,
436
+ include_photonOps=self.include_photonOps ).array
437
+ self._stamps[(x, y)] *= flux / self._stamps[(x, y)].sum()
438
+ return self._stamps[(x, y)]
@@ -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 )
@@ -1,207 +0,0 @@
1
- import yaml
2
- import base64
3
-
4
- import numpy as np
5
-
6
-
7
- class PSF:
8
- def __init__( self, *args, **kwargs ):
9
- # Will define a PSF with a nominal position
10
- pass
11
-
12
- def get_stamp( self, x, y, flux=1. ):
13
- """Return a 2d numpy image of the PSF at the image resolution.
14
-
15
- Parameters
16
- ----------
17
- x: float
18
- Position on the image of the center of the psf
19
-
20
- y: float
21
- Position on the image of the center of the psf
22
-
23
- x0: float or None
24
- Image position of the center of the stamp; defaults to FIGURE THIS OUT
25
-
26
- y0: float or None
27
-
28
- flux: float, default 1.
29
- Make the sum of the clip this
30
-
31
- Returns
32
- -------
33
- 2d numpy array
34
-
35
- """
36
- raise NotImplementedError( f"{self.__class__.__name__} needs to implement get_stamp" )
37
-
38
-
39
-
40
- class OversampledImagePSF( PSF ):
41
- @classmethod
42
- def create( cls, data, x0, y0, oversample_factor=1., enforce_odd=True, normalize=True ):
43
- """Parameters
44
- ----------
45
- data: 2d numpy array
46
-
47
- x0, y0: float
48
- Position on the source image where this PSF is evaluated
49
-
50
- oversample_factor: float, default 1.
51
- There are this many pixels along one axis in data for one pixel in the original image
52
-
53
- enforce_odd: bool, default True
54
- Enforce x_edges and y_edges having an odd width.
55
-
56
- normalize: bool, default True
57
- Make sure internally stored PSF sums to 1 ; you usually don't want to change this.
58
-
59
- Returns
60
- -------
61
- object of type cls
62
-
63
- """
64
- # TODO : implement enforce_odd
65
- # TODO : enforce square
66
-
67
- psf = cls()
68
- psf._data = data
69
- if normalize:
70
- psf._data /= psf._data.sum()
71
- psf._x0 = x0
72
- psf._y0 = y0
73
- psf._oversamp = oversample_factor
74
- return psf
75
-
76
- @property
77
- def x0( self ):
78
- return self._x0
79
-
80
- @property
81
- def y0( self ):
82
- return self._x0
83
-
84
- @property
85
- def oversample_factor( self ):
86
- return self._oversamp
87
-
88
- @property
89
- def oversampled_data( self ):
90
- return self._data
91
-
92
- @property
93
- def clip_size( self ):
94
- """The size of the PSF image clip at image resolution."""
95
- return int( np.floor( self._data.shape[0] / self._oversamp ) )
96
-
97
- def __init__( self, *args, **kwargs ):
98
- super().__init__( *args, **kwargs )
99
- self._data = None
100
- self._x0 = None
101
- self._y0 = None
102
- self._oversamp = None
103
-
104
- def get_stamp( self, x=None, y=None, normalize=True ):
105
- x = float(x) if x is not None else self._x0
106
- y = float(y) if y is not None else self._y0
107
-
108
- # round() isn't the right thing to use here, because it will
109
- # behave differently when x - round(x) = 0.5 based on whether
110
- # floor(x) is even or odd. What we *want* is for the psf to
111
- # be as close to the center of the clip as possible. In the
112
- # case where the fractional part of x is exactly 0.5, it's
113
- # ambiguous what that means-- there are four places you could
114
- # stick the PSF to statisfy that criterion. By using
115
- # floor(x+0.5), we will consistently have the psf leaning down
116
- # and to the left when the fractional part of x (and y) is
117
- # exactly 0.5, whereas using round would give different
118
- # results based on the integer part of x (and y).
119
-
120
- xc = int( np.floor( x + 0.5 ) )
121
- yc = int( np.floor( y + 0.5 ) )
122
-
123
- # See Chapter 5, "How PSFEx Works", of the PSFEx manual
124
- # https://psfex.readthedocs.io/en/latest/Working.html
125
- # We're using this method for both image and psfex PSFs,
126
- # as the interpolation is more general than PSFEx:
127
- # https://en.wikipedia.org/wiki/Lanczos_resampling
128
- # ...though of course, the choice of a=4 comes from PSFEx.
129
-
130
-
131
- psfwid = self._data.shape[0]
132
- stampwid = self.clip_size
133
- stampwid += 1 if stampwid % 2 == 0 else 0
134
-
135
- psfdex1d = np.arange( -( psfwid//2), psfwid//2+1, dtype=int )
136
-
137
- xmin = xc - stampwid // 2
138
- xmax = xc + stampwid // 2 + 1
139
- ymin = yc - stampwid // 2
140
- ymax = yc + stampwid // 2 + 1
141
-
142
- psfsamp = 1. / self._oversamp
143
- xs = np.array( range( xmin, xmax ) )
144
- ys = np.array( range( ymin, ymax ) )
145
- xsincarg = psfdex1d[:, np.newaxis] - ( xs - x ) / psfsamp
146
- xsincvals = np.sinc( xsincarg ) * np.sinc( xsincarg/4. )
147
- xsincvals[ ( xsincarg > 4 ) | ( xsincarg < -4 ) ] = 0.
148
- ysincarg = psfdex1d[:, np.newaxis] - ( ys - y ) / psfsamp
149
- ysincvals = np.sinc( ysincarg ) * np.sinc( ysincarg/4. )
150
- ysincvals[ ( ysincarg > 4 ) | ( ysincarg < -4 ) ] = 0.
151
- tenpro = np.tensordot( ysincvals[:, :, np.newaxis], xsincvals[:, :, np.newaxis], axes=0 )[ :, :, 0, :, :, 0 ]
152
- clip = ( self._data[:, np.newaxis, :, np.newaxis ] * tenpro ).sum( axis=0 ).sum( axis=1 )
153
-
154
- # Keeping the code below, because the code above is inpenetrable, and it's trying to
155
- # do the same thing as the code below.
156
- # (I did emprically test it using the PSFs from the test_psf.py::test_psfex_rendering,
157
- # and it worked. In particular, there is not a transposition error in the "tenpro=" line;
158
- # if you swap the order of yxincvals and xsincvals in the test, then the values of clip
159
- # do not match the code below very well. As is, they match to within a few times 1e-17,
160
- # which is good enough as the minimum non-zero value in either one is of order 1e-12.)
161
- # clip = np.empty( ( stampwid, stampwid ), dtype=dtype )
162
- # for xi in range( xmin, xmax ):
163
- # for yi in range( ymin, ymax ):
164
- # xsincarg = psfdex1d - (xi-x) / psfsamp
165
- # xsincvals = np.sinc( xsincarg ) * np.sinc( xsincarg/4. )
166
- # xsincvals[ ( xsincarg > 4 ) | ( xsincarg < -4 ) ] = 0
167
- # ysincarg = psfdex1d - (yi-y) / psfsamp
168
- # ysincvals = np.sinc( ysincarg ) * np.sinc( ysincarg/4. )
169
- # ysincvals[ ( ysincarg > 4 ) | ( ysincarg < -4 ) ] = 0
170
- # clip[ yi-ymin, xi-xmin ] = ( xsincvals[np.newaxis, :]
171
- # * ysincvals[:, np.newaxis]
172
- # * psfbase ).sum()
173
-
174
- if normalize:
175
- clip /= clip.sum()
176
-
177
- return clip
178
-
179
-
180
- class YamlSerialized_OversampledImagePSF( OversampledImagePSF ):
181
-
182
- def __init__( self, *args, **kwargs ):
183
- super().__init__( *args, **kwargs )
184
-
185
- def read( self, filepath ):
186
- y = yaml.safe_load( open( filepath ) )
187
- self._x0 = y['x0']
188
- self._y0 = y['y0']
189
- self._oversamp = y['oversamp']
190
- self._data = np.frombuffer( base64.b64decode( y['data'] ), dtype=y['dtype'] )
191
- self._data = self._data.reshape( ( y['shape0'], y['shape1'] ) )
192
-
193
- def write( self, filepath ):
194
- out = { 'x0': float( self._x0 ),
195
- 'y0': float( self._y0 ),
196
- 'oversamp': self._oversamp,
197
- 'shape0': self._data.shape[0],
198
- 'shape1': self._data.shape[1],
199
- 'dtype': str( self._data.dtype ),
200
- # TODO : make this right, think about endian-ness, etc.
201
- 'data': base64.b64encode( self._data.tobytes() ).decode( 'utf-8' ) }
202
- # TODO : check overwriting etc.
203
- yaml.dump( out, open( filepath, 'w' ) )
204
-
205
-
206
- class galsimPSF( PSF ):
207
- pass