stdpipe 0.4.0__py3-none-any.whl
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.
- stdpipe/__init__.py +9 -0
- stdpipe/artefacts.py +370 -0
- stdpipe/astrometry.py +2001 -0
- stdpipe/astrometry_quad.py +1643 -0
- stdpipe/astrometry_wcs.py +1141 -0
- stdpipe/catalogs.py +942 -0
- stdpipe/cutouts.py +570 -0
- stdpipe/data/legacysurvey_bricks.fits.gz +0 -0
- stdpipe/data/ps1skycells.txt +199708 -0
- stdpipe/db.py +130 -0
- stdpipe/fringe_removal.py +1086 -0
- stdpipe/lcs.py +440 -0
- stdpipe/photometry.py +2340 -0
- stdpipe/photometry_background.py +998 -0
- stdpipe/photometry_iraf.py +913 -0
- stdpipe/photometry_measure.py +2268 -0
- stdpipe/photometry_model.py +931 -0
- stdpipe/photometry_psf.py +1124 -0
- stdpipe/photometry_quality.py +185 -0
- stdpipe/pipeline.py +1303 -0
- stdpipe/plots.py +1582 -0
- stdpipe/psf.py +1542 -0
- stdpipe/realbogus.py +971 -0
- stdpipe/realbogus_features.py +1657 -0
- stdpipe/reproject.py +943 -0
- stdpipe/resolve.py +267 -0
- stdpipe/sfft.py +1077 -0
- stdpipe/simulation.py +2131 -0
- stdpipe/smoothing.py +488 -0
- stdpipe/subtraction.py +1324 -0
- stdpipe/templates.py +929 -0
- stdpipe/utils.py +365 -0
- stdpipe-0.4.0.dist-info/METADATA +171 -0
- stdpipe-0.4.0.dist-info/RECORD +37 -0
- stdpipe-0.4.0.dist-info/WHEEL +5 -0
- stdpipe-0.4.0.dist-info/licenses/LICENSE.md +21 -0
- stdpipe-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""PSF-based per-source quality metrics borrowed from the ``crowdsource``
|
|
2
|
+
photometry pipeline (Schlafly et al., MIT-licensed).
|
|
3
|
+
|
|
4
|
+
The metrics operate on stacks of postage stamps:
|
|
5
|
+
|
|
6
|
+
``impsf`` — neighbour-subtracted data stamps (data minus the model of every
|
|
7
|
+
*other* source). For an isolated source this is just the data stamp.
|
|
8
|
+
|
|
9
|
+
``im`` — raw data stamps (no neighbour subtraction).
|
|
10
|
+
|
|
11
|
+
``psf`` — model PSF stamps for each source, normalized to integrate to one.
|
|
12
|
+
|
|
13
|
+
``weight`` — inverse-sigma stamps; pixels with ``weight == 0`` are treated as
|
|
14
|
+
masked.
|
|
15
|
+
|
|
16
|
+
All inputs are ``(N, S, S)`` arrays. Outputs are length-``N`` 1D arrays.
|
|
17
|
+
|
|
18
|
+
The functions are deliberately decoupled from any particular photometry
|
|
19
|
+
backend so they can be reused from :func:`stdpipe.photometry_psf.measure_objects_psf`,
|
|
20
|
+
:func:`stdpipe.photometry_measure.measure_objects_sep`, or external code.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Mixture-of-Gaussians representation of the exponential galaxy profile from
|
|
27
|
+
# Hogg & Lang's ``the-tractor`` (also used by ``crowdsource.galconv``). Six
|
|
28
|
+
# isotropic Gaussians whose amplitudes / variances reproduce a 1-pixel-scale
|
|
29
|
+
# exponential.
|
|
30
|
+
_EXP_GAL_AMP = np.array(
|
|
31
|
+
[1.99485977e-04, 2.61612679e-03, 1.89726655e-02,
|
|
32
|
+
1.00186544e-01, 3.68534484e-01, 5.09490694e-01]
|
|
33
|
+
)
|
|
34
|
+
_EXP_GAL_VAR = np.array(
|
|
35
|
+
[1.20078965e-03, 8.84526493e-03, 3.91463084e-02,
|
|
36
|
+
1.39976817e-01, 4.60962500e-01, 1.50159566e+00]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def neff_fwhm(stamp):
|
|
41
|
+
"""FWHM-like quantity of a 2D PSF stamp, equivalent to
|
|
42
|
+
``crowdsource.psf.neff_fwhm``.
|
|
43
|
+
|
|
44
|
+
Returns ``1.18 / sqrt(pi * sum((p/norm)^2))``. The ``1.18`` calibrates the
|
|
45
|
+
n_eff-derived width to the actual FWHM of a Gaussian PSF.
|
|
46
|
+
"""
|
|
47
|
+
stamp = np.asarray(stamp)
|
|
48
|
+
norm = np.sum(stamp, axis=(-1, -2), keepdims=True)
|
|
49
|
+
norm = norm + (norm == 0)
|
|
50
|
+
return 1.18 * (np.pi * np.sum((stamp / norm) ** 2, axis=(-1, -2))) ** -0.5
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def convolve_with_exp_galaxy(psf_stack, re):
|
|
54
|
+
"""Convolve each ``(S, S)`` PSF in ``psf_stack`` with a circular exponential
|
|
55
|
+
galaxy of effective radius ``re`` (in pixels).
|
|
56
|
+
|
|
57
|
+
Uses an FFT-based 6-Gaussian mixture approximation of the exponential, so
|
|
58
|
+
the result is the same shape as the input. ``re`` may be a scalar or a
|
|
59
|
+
length-N array (one per source).
|
|
60
|
+
"""
|
|
61
|
+
psf_stack = np.asarray(psf_stack, dtype='f4')
|
|
62
|
+
one_d = psf_stack.ndim == 2
|
|
63
|
+
if one_d:
|
|
64
|
+
psf_stack = psf_stack[None, ...]
|
|
65
|
+
N, H, W = psf_stack.shape
|
|
66
|
+
re = np.atleast_1d(re).astype('f4')
|
|
67
|
+
if re.size == 1:
|
|
68
|
+
re = np.full(N, re[0], dtype='f4')
|
|
69
|
+
|
|
70
|
+
u = np.fft.rfftfreq(W).astype('f4')
|
|
71
|
+
v = np.fft.fftfreq(H).astype('f4')
|
|
72
|
+
rr2 = (u[None, :] ** 2 + v[:, None] ** 2) # (H, W//2+1)
|
|
73
|
+
|
|
74
|
+
# F_gal[n, v, u] = sum_k amp_k exp(-2 pi^2 re_n^2 var_k (u^2+v^2))
|
|
75
|
+
coeff = -2.0 * np.pi ** 2 * (re ** 2)[:, None] # (N, K)
|
|
76
|
+
coeff = coeff * _EXP_GAL_VAR[None, :].astype('f4') # (N, K)
|
|
77
|
+
# Build Fgal per source by summing the K Gaussians.
|
|
78
|
+
Fgal = np.zeros((N, rr2.shape[0], rr2.shape[1]), dtype='c8')
|
|
79
|
+
for k in range(_EXP_GAL_AMP.size):
|
|
80
|
+
Fgal += _EXP_GAL_AMP[k] * np.exp(coeff[:, k][:, None, None] * rr2[None, :, :])
|
|
81
|
+
|
|
82
|
+
P = np.fft.rfft2(psf_stack)
|
|
83
|
+
G = np.fft.irfft2(Fgal * P, s=(H, W)).astype('f4')
|
|
84
|
+
|
|
85
|
+
if one_d:
|
|
86
|
+
G = G[0]
|
|
87
|
+
return G
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _normalize_psf(psf_stack):
|
|
91
|
+
"""Return PSF stacks scaled so each stamp integrates to 1 (in-place safe)."""
|
|
92
|
+
psf = np.asarray(psf_stack, dtype='f4')
|
|
93
|
+
norm = psf.sum(axis=(-1, -2), keepdims=True)
|
|
94
|
+
norm = norm + (norm == 0) * 1e-30
|
|
95
|
+
return psf / norm
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def compute_qf(psf_stack, weight_stack):
|
|
99
|
+
"""Per-source PSF quality factor.
|
|
100
|
+
|
|
101
|
+
``qf = sum(P_normed * (W > 0))`` — the fraction of the PSF footprint that
|
|
102
|
+
overlaps unmasked pixels. ``1.0`` for a fully unmasked source, ``< 1`` if
|
|
103
|
+
pixels are masked or off-image.
|
|
104
|
+
"""
|
|
105
|
+
P = _normalize_psf(psf_stack)
|
|
106
|
+
return np.sum(P * (np.asarray(weight_stack) > 0), axis=(-1, -2)).astype('f4')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compute_fracflux(impsf_stack, im_stack, psf_stack, weight_stack):
|
|
110
|
+
"""Per-source fraction-of-flux metric.
|
|
111
|
+
|
|
112
|
+
``fracflux = sum(impsf * (W > 0) * P_normed) / sum(im * (W > 0) * P_normed)``
|
|
113
|
+
|
|
114
|
+
Approaches 1 for an isolated source whose neighbours have been cleanly
|
|
115
|
+
subtracted, and drops below 1 when nearby sources contribute flux at the
|
|
116
|
+
target's PSF footprint. Useful as a crowding / blendiness indicator.
|
|
117
|
+
"""
|
|
118
|
+
P = _normalize_psf(psf_stack)
|
|
119
|
+
w = (np.asarray(weight_stack) > 0).astype('f4')
|
|
120
|
+
num = np.sum(impsf_stack * w * P, axis=(-1, -2))
|
|
121
|
+
den = np.sum(im_stack * w * P, axis=(-1, -2))
|
|
122
|
+
den_safe = den + (den == 0) * 1e-30
|
|
123
|
+
return (num / den_safe).astype('f4')
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def compute_spread_model(impsf_stack, psf_stack, weight_stack, fwhm=None):
|
|
127
|
+
"""Per-source ``spread_model`` star/galaxy classifier (and its uncertainty).
|
|
128
|
+
|
|
129
|
+
Equivalent to SExtractor's spread_model and to ``crowdsource_base.spread_model``.
|
|
130
|
+
Convolves each PSF with a tiny exponential galaxy (re = fwhm / 16 * 1.6783)
|
|
131
|
+
and compares the data's similarity to PSF vs PSF-convolved-galaxy. Returns
|
|
132
|
+
``(spread, dspread)``: a star yields ``spread ~ 0``, an extended source
|
|
133
|
+
``spread > 0``.
|
|
134
|
+
|
|
135
|
+
If ``fwhm`` is None it is estimated per-source from the PSF stamp via
|
|
136
|
+
:func:`neff_fwhm`.
|
|
137
|
+
"""
|
|
138
|
+
psf = np.asarray(psf_stack, dtype='f4')
|
|
139
|
+
impsf = np.asarray(impsf_stack, dtype='f4')
|
|
140
|
+
w2 = np.asarray(weight_stack, dtype='f4') ** 2
|
|
141
|
+
|
|
142
|
+
if fwhm is None:
|
|
143
|
+
fwhm = neff_fwhm(psf)
|
|
144
|
+
fwhm = np.atleast_1d(np.asarray(fwhm, dtype='f4'))
|
|
145
|
+
sigma = fwhm / 16.0
|
|
146
|
+
re = sigma * 1.67834699 # exponential scale length matching that sigma
|
|
147
|
+
|
|
148
|
+
G = convolve_with_exp_galaxy(psf, re)
|
|
149
|
+
|
|
150
|
+
# Compute dot products in float64 — for bright sources the cross-terms in
|
|
151
|
+
# the dspread numerator (PWp**2 * GWG, etc.) easily overflow float32.
|
|
152
|
+
psf64 = psf.astype('f8')
|
|
153
|
+
impsf64 = impsf.astype('f8')
|
|
154
|
+
G64 = G.astype('f8')
|
|
155
|
+
w2_64 = w2.astype('f8')
|
|
156
|
+
|
|
157
|
+
GWp = np.sum(G64 * w2_64 * impsf64, axis=(-1, -2))
|
|
158
|
+
PWp = np.sum(psf64 * w2_64 * impsf64, axis=(-1, -2))
|
|
159
|
+
GWP = np.sum(G64 * w2_64 * psf64, axis=(-1, -2))
|
|
160
|
+
PWP = np.sum(psf64 * psf64 * w2_64, axis=(-1, -2))
|
|
161
|
+
GWG = np.sum(G64 * G64 * w2_64, axis=(-1, -2))
|
|
162
|
+
|
|
163
|
+
PWp_safe = PWp + (PWp == 0) * 1e-30
|
|
164
|
+
PWP_safe = PWP + (PWP == 0) * 1e-30
|
|
165
|
+
|
|
166
|
+
spread = (GWp / PWp_safe - GWP / PWP_safe).astype('f4')
|
|
167
|
+
dspread = np.sqrt(
|
|
168
|
+
np.clip(PWp ** 2 * GWG + GWp ** 2 * PWP - 2 * GWp * PWp * GWP, 0.0, np.inf)
|
|
169
|
+
/ (PWp_safe ** 4)
|
|
170
|
+
).astype('f4')
|
|
171
|
+
|
|
172
|
+
return spread, dspread
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def compute_psf_quality(impsf_stack, im_stack, psf_stack, weight_stack, fwhm=None):
|
|
176
|
+
"""Convenience wrapper: returns dict with ``qf``, ``fracflux``, ``spread_model``,
|
|
177
|
+
and ``dspread_model`` for each source in the input stacks.
|
|
178
|
+
"""
|
|
179
|
+
spread, dspread = compute_spread_model(impsf_stack, psf_stack, weight_stack, fwhm=fwhm)
|
|
180
|
+
return {
|
|
181
|
+
'qf': compute_qf(psf_stack, weight_stack),
|
|
182
|
+
'fracflux': compute_fracflux(impsf_stack, im_stack, psf_stack, weight_stack),
|
|
183
|
+
'spread_model': spread,
|
|
184
|
+
'dspread_model': dspread,
|
|
185
|
+
}
|