jwst-psfmc 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mingyang Zhuang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: jwst-psfmc
3
+ Version: 0.1.0
4
+ Summary: PSF photometry with MCMC for JWST (and HST) drizzled difference images, with correlated-noise likelihood
5
+ Author: Mingyang Zhuang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mingyangzhuang/jwst_psfmc
8
+ Project-URL: Bug Tracker, https://github.com/mingyangzhuang/jwst_psfmc/issues
9
+ Keywords: astronomy,JWST,PSF,photometry,MCMC,drizzle,transients,supernovae
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy>=1.23
22
+ Requires-Dist: scipy>=1.9
23
+ Requires-Dist: astropy>=5.2
24
+ Requires-Dist: photutils>=1.7
25
+ Requires-Dist: emcee>=3.1
26
+ Requires-Dist: corner>=2.2
27
+ Requires-Dist: matplotlib>=3.6
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+ Requires-Dist: pytest-cov; extra == "dev"
31
+ Provides-Extra: docs
32
+ Requires-Dist: sphinx; extra == "docs"
33
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
34
+ Requires-Dist: nbsphinx; extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # jwst_psfmc
38
+
39
+ **PSF photometry with MCMC for JWST (and HST) drizzled difference images.**
40
+
41
+ [![CI](https://github.com/mingyangzhuang/jwst_psfmc/actions/workflows/ci.yml/badge.svg)](https://github.com/mingyangzhuang/jwst_psfmc/actions)
42
+ [![PyPI](https://img.shields.io/pypi/v/jwst-psfmc)](https://pypi.org/project/jwst-psfmc/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/jwst-psfmc)](https://pypi.org/project/jwst-psfmc/)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
45
+
46
+ ---
47
+
48
+ ## Scientific Background
49
+
50
+ JWST NIRCam images processed with the [drizzle algorithm](https://drizzlepac.readthedocs.io)
51
+ exhibit **spatially correlated pixel-to-pixel noise**: photons detected in a single detector
52
+ pixel are spread across several output pixels during co-addition. If these correlations are
53
+ ignored in a standard χ² PSF fit, the flux uncertainties are systematically *underestimated*.
54
+ Even in full MCMC fitting with correlated-noise likelihoods, neglecting the covariance kernel
55
+ underestimates flux uncertainties by ~30% in F200W (Zhuang et al., *NEXUS: Transient Searches
56
+ and First Results from Year One Observations*, in prep.).
57
+
58
+ `jwst_psfmc` solves this by:
59
+
60
+ 1. **Measuring the covariance structure** directly from source-free sky regions of the
61
+ difference image using an autocorrelation estimator.
62
+ 2. **Encoding the covariance as a Fourier-space power spectrum**, enabling an exact
63
+ correlated-noise log-likelihood that is *O(N log N)* per MCMC step.
64
+ 3. **Running an ensemble MCMC sampler** ([`emcee`](https://emcee.readthedocs.io)) to obtain
65
+ full posterior distributions for the four model parameters:
66
+
67
+ | Parameter | Description |
68
+ |-----------|-------------|
69
+ | `flux` | Total source flux (same units as the image) |
70
+ | `dx` | Sub-pixel x-shift of the PSF centroid (native pixels) |
71
+ | `dy` | Sub-pixel y-shift of the PSF centroid (native pixels) |
72
+ | `bkg` | Flat residual background level |
73
+
74
+ The PSF is shifted with **exact Fourier-space interpolation** (no interpolation kernel
75
+ artefacts) and downsampled from the 4× oversampled PSF model by block-summing.
76
+
77
+ ---
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install jwst-psfmc
83
+ ```
84
+
85
+ Or from source:
86
+
87
+ ```bash
88
+ git clone https://github.com/mingyangzhuang/jwst_psfmc.git
89
+ cd jwst_psfmc
90
+ pip install -e ".[dev]"
91
+ ```
92
+
93
+ **Dependencies:** `numpy`, `scipy`, `astropy`, `photutils`, `emcee`, `corner`, `matplotlib`
94
+
95
+ ---
96
+
97
+ ## Example Data
98
+
99
+ The example FITS files (difference images, error maps, covariance kernels, and PSF models)
100
+ live in `examples/data/` and `examples/PSF/` inside the repository. They are **not**
101
+ bundled with the PyPI package — clone the repo to use them:
102
+
103
+ ```bash
104
+ git clone https://github.com/mingyangzhuang/jwst_psfmc.git
105
+ cd jwst_psfmc/examples
106
+ jupyter notebook demo_psf_photometry.ipynb
107
+ ```
108
+
109
+ | File | Description |
110
+ |------|-------------|
111
+ | `examples/data/example1_f200w_diff.fits` | Difference image — non-detection |
112
+ | `examples/data/example1_f200w_diff_error.fits` | Uncertainty map — non-detection |
113
+ | `examples/data/example1_f200w_cov_kernel.fits` | Covariance kernel — non-detection |
114
+ | `examples/data/example2_f200w_diff.fits` | Difference image — detection |
115
+ | `examples/data/example2_f200w_diff_error.fits` | Uncertainty map — detection |
116
+ | `examples/data/example2_f200w_cov_kernel.fits` | Covariance kernel — detection |
117
+ | `examples/PSF/example1_f200w_PSF_4_c.fits` | 4× oversampled PSF model — non-detection |
118
+ | `examples/PSF/example2_f200w_PSF_4_c.fits` | 4× oversampled PSF model — detection |
119
+
120
+ ---
121
+
122
+ ## Quick Start
123
+
124
+ The core API accepts NumPy arrays directly — load your own FITS files however you prefer:
125
+
126
+ ```python
127
+ from astropy.io import fits
128
+ import numpy as np
129
+ import jwst_psfmc as jpm
130
+
131
+ # ── 1. Load your data ──────────────────────────────────────────────────────
132
+ data = fits.getdata("examples/data/example2_f200w_diff.fits")
133
+ err = fits.getdata("examples/data/example2_f200w_diff_error.fits")
134
+ kernel = fits.getdata("examples/data/example2_f200w_cov_kernel.fits")
135
+ psf_raw = fits.getdata("examples/PSF/example2_f200w_PSF_4_c.fits")
136
+
137
+ # ── 2. Extract a 9×9 stamp centred on the source ───────────────────────────
138
+ cy, cx = data.shape[0] // 2, data.shape[1] // 2
139
+ stamp = data[cy-4:cy+5, cx-4:cx+5]
140
+ stamp_err = err[cy-4:cy+5, cx-4:cx+5]
141
+
142
+ # ── 3. Prepare MCMC inputs ─────────────────────────────────────────────────
143
+ fit_args = jpm.prepare_for_fitting(
144
+ data=stamp, err=stamp_err,
145
+ psf_os=psf_raw, cov_kernel=kernel,
146
+ dx_init=0.0, dy_init=0.0,
147
+ oversamp=4, native_shape=(11, 11), nwalkers=48,
148
+ )
149
+
150
+ # ── 4. Run MCMC ────────────────────────────────────────────────────────────
151
+ sampler = jpm.run_mcmc(**fit_args, nsteps=2000, ncores=4, progress=True)
152
+
153
+ # ── 5. Summarise posterior ─────────────────────────────────────────────────
154
+ summary = jpm.summarize_emcee(sampler, burnin=500, thin=4)
155
+ print(f"flux = {summary['flux']['median']:.4f} "
156
+ f"+{summary['flux']['plus_1sigma']:.4f} / "
157
+ f"-{summary['flux']['minus_1sigma']:.4f}")
158
+
159
+ # ── 6. Visualise ───────────────────────────────────────────────────────────
160
+ fig_trip, _ = jpm.plot_psf_fit_triptych(
161
+ stamp, stamp_err, fit_args["psf_os"], oversamp=4,
162
+ native_shape=fit_args["native_shape"],
163
+ psf_prepare_fraction=fit_args["psf_prepare_fraction"],
164
+ summary=summary, fig_title="Detection example — F200W",
165
+ )
166
+ fig_trip.savefig("triptych.pdf", bbox_inches="tight")
167
+
168
+ fig_chains, _ = jpm.plot_chains(sampler, burnin=500)
169
+ fig_corner, _ = jpm.plot_corner(sampler, burnin=500, thin=4)
170
+
171
+ # ── 7. Save / reload results ───────────────────────────────────────────────
172
+ jpm.save_emcee_results("example2_f200w_mcmc.npz", sampler)
173
+ res = jpm.load_emcee_results("example2_f200w_mcmc.npz")
174
+ print(res["summary"])
175
+ ```
176
+
177
+ ### Deriving a flux upper limit from a non-detection
178
+
179
+ Residual small-scale background fluctuations can mimic low-level source
180
+ emission at 1–2σ. We use a **two-run workflow**:
181
+
182
+ 1. **Detection check** — broad priors (±1 pix on dx/dy) to confirm the
183
+ source is genuinely absent.
184
+ 2. **Upper limit** — tight priors (±0.1 pix) to derive the official 3σ
185
+ bound from the 99.7th percentile of the posterior flux.
186
+
187
+ ```python
188
+ data_nd = fits.getdata("examples/data/example1_f200w_diff.fits")
189
+ err_nd = fits.getdata("examples/data/example1_f200w_diff_error.fits")
190
+ kernel_nd = fits.getdata("examples/data/example1_f200w_cov_kernel.fits")
191
+ psf_nd = fits.getdata("examples/PSF/example1_f200w_PSF_4_c.fits")
192
+
193
+ stamp_nd = data_nd[cy-4:cy+5, cx-4:cx+5]
194
+ stamp_err_nd = err_nd[cy-4:cy+5, cx-4:cx+5]
195
+
196
+ # ── Run 1: broad priors (detection check) ─────────────────────────────
197
+ fit_nd_broad = jpm.prepare_for_fitting(data=stamp_nd, err=stamp_err_nd,
198
+ psf_os=psf_nd, cov_kernel=kernel_nd)
199
+ sampler_nd_broad = jpm.run_mcmc(**fit_nd_broad, nsteps=2000, ncores=4,
200
+ progress=True)
201
+
202
+ # Confirm non-detection: flux median consistent with zero within 1σ
203
+ summary_broad = jpm.summarize_emcee(sampler_nd_broad, burnin=500, thin=4)
204
+ is_absent = abs(summary_broad['flux']['median']) < summary_broad['flux']['plus_1sigma']
205
+ print(f'Non-detection confirmed: {is_absent}')
206
+
207
+ # ── Run 2: tight priors (upper limit) ─────────────────────────────────
208
+ fit_nd_tight = jpm.prepare_for_fitting(data=stamp_nd, err=stamp_err_nd,
209
+ psf_os=psf_nd, cov_kernel=kernel_nd,
210
+ pos_prior_half_width=0.1) # ±0.1 pix
211
+ sampler_nd_tight = jpm.run_mcmc(**fit_nd_tight, nsteps=2000, ncores=4,
212
+ progress=True)
213
+
214
+ flat_flux = sampler_nd_tight.get_chain(discard=500, thin=4, flat=True)[:, 0]
215
+ upper_limit_3sigma = float(np.percentile(flat_flux, 99.7))
216
+ print(f"3-sigma upper limit on flux: {upper_limit_3sigma:.4f}")
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Full Worked Examples
222
+
223
+ ### Covariance kernel estimation
224
+
225
+ See **[`examples/demo_covariance_kernel.ipynb`](examples/demo_covariance_kernel.ipynb)** for
226
+ a step-by-step demonstration of estimating the pixel-to-pixel covariance kernel
227
+ from JWST difference images:
228
+
229
+ - Source masking and finding source-free sky regions
230
+ - Autocorrelation-based kernel estimation
231
+ - Cosine-bell windowing and power spectrum computation
232
+ - Pixel correlation whitening demonstration
233
+
234
+ ### PSF photometry with MCMC
235
+
236
+ See **[`examples/demo_psf_photometry.ipynb`](examples/demo_psf_photometry.ipynb)** for a
237
+ step-by-step notebook covering:
238
+
239
+ - Loading and inspecting the JWST cutout data
240
+ - Estimating and visualising the covariance kernel
241
+ - Running MCMC for a **non-detection** (example1) and deriving a rigorous 3-σ upper limit
242
+ - Running MCMC for a clear **detection** (example2) and extracting flux posteriors
243
+ - Diagnosing convergence via chain traces and autocorrelation times
244
+ - Producing publication-quality corner plots and residual triptychs
245
+
246
+ ---
247
+
248
+ ## Module Overview
249
+
250
+ | Module | Contents |
251
+ |--------|----------|
252
+ | `jwst_psfmc.psf` | PSF shifting, downsampling, model evaluation |
253
+ | `jwst_psfmc.covariance` | Covariance kernel estimation, Fourier-space pre-computation |
254
+ | `jwst_psfmc.mcmc` | MCMC preparation, log-prob, `run_mcmc`, posterior summaries |
255
+ | `jwst_psfmc.io` | Save/load `.npz` results |
256
+ | `jwst_psfmc.plot` | Triptych, chain traces, corner plot |
257
+
258
+ ---
259
+
260
+ ## Citation
261
+
262
+ If you use `jwst_psfmc` in your research, please cite:
263
+
264
+ > Zhuang et al. (in prep.), *NEXUS JWST Transient Survey Year 1*
265
+
266
+ ---
267
+
268
+ ## License
269
+
270
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,234 @@
1
+ # jwst_psfmc
2
+
3
+ **PSF photometry with MCMC for JWST (and HST) drizzled difference images.**
4
+
5
+ [![CI](https://github.com/mingyangzhuang/jwst_psfmc/actions/workflows/ci.yml/badge.svg)](https://github.com/mingyangzhuang/jwst_psfmc/actions)
6
+ [![PyPI](https://img.shields.io/pypi/v/jwst-psfmc)](https://pypi.org/project/jwst-psfmc/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/jwst-psfmc)](https://pypi.org/project/jwst-psfmc/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ ---
11
+
12
+ ## Scientific Background
13
+
14
+ JWST NIRCam images processed with the [drizzle algorithm](https://drizzlepac.readthedocs.io)
15
+ exhibit **spatially correlated pixel-to-pixel noise**: photons detected in a single detector
16
+ pixel are spread across several output pixels during co-addition. If these correlations are
17
+ ignored in a standard χ² PSF fit, the flux uncertainties are systematically *underestimated*.
18
+ Even in full MCMC fitting with correlated-noise likelihoods, neglecting the covariance kernel
19
+ underestimates flux uncertainties by ~30% in F200W (Zhuang et al., *NEXUS: Transient Searches
20
+ and First Results from Year One Observations*, in prep.).
21
+
22
+ `jwst_psfmc` solves this by:
23
+
24
+ 1. **Measuring the covariance structure** directly from source-free sky regions of the
25
+ difference image using an autocorrelation estimator.
26
+ 2. **Encoding the covariance as a Fourier-space power spectrum**, enabling an exact
27
+ correlated-noise log-likelihood that is *O(N log N)* per MCMC step.
28
+ 3. **Running an ensemble MCMC sampler** ([`emcee`](https://emcee.readthedocs.io)) to obtain
29
+ full posterior distributions for the four model parameters:
30
+
31
+ | Parameter | Description |
32
+ |-----------|-------------|
33
+ | `flux` | Total source flux (same units as the image) |
34
+ | `dx` | Sub-pixel x-shift of the PSF centroid (native pixels) |
35
+ | `dy` | Sub-pixel y-shift of the PSF centroid (native pixels) |
36
+ | `bkg` | Flat residual background level |
37
+
38
+ The PSF is shifted with **exact Fourier-space interpolation** (no interpolation kernel
39
+ artefacts) and downsampled from the 4× oversampled PSF model by block-summing.
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install jwst-psfmc
47
+ ```
48
+
49
+ Or from source:
50
+
51
+ ```bash
52
+ git clone https://github.com/mingyangzhuang/jwst_psfmc.git
53
+ cd jwst_psfmc
54
+ pip install -e ".[dev]"
55
+ ```
56
+
57
+ **Dependencies:** `numpy`, `scipy`, `astropy`, `photutils`, `emcee`, `corner`, `matplotlib`
58
+
59
+ ---
60
+
61
+ ## Example Data
62
+
63
+ The example FITS files (difference images, error maps, covariance kernels, and PSF models)
64
+ live in `examples/data/` and `examples/PSF/` inside the repository. They are **not**
65
+ bundled with the PyPI package — clone the repo to use them:
66
+
67
+ ```bash
68
+ git clone https://github.com/mingyangzhuang/jwst_psfmc.git
69
+ cd jwst_psfmc/examples
70
+ jupyter notebook demo_psf_photometry.ipynb
71
+ ```
72
+
73
+ | File | Description |
74
+ |------|-------------|
75
+ | `examples/data/example1_f200w_diff.fits` | Difference image — non-detection |
76
+ | `examples/data/example1_f200w_diff_error.fits` | Uncertainty map — non-detection |
77
+ | `examples/data/example1_f200w_cov_kernel.fits` | Covariance kernel — non-detection |
78
+ | `examples/data/example2_f200w_diff.fits` | Difference image — detection |
79
+ | `examples/data/example2_f200w_diff_error.fits` | Uncertainty map — detection |
80
+ | `examples/data/example2_f200w_cov_kernel.fits` | Covariance kernel — detection |
81
+ | `examples/PSF/example1_f200w_PSF_4_c.fits` | 4× oversampled PSF model — non-detection |
82
+ | `examples/PSF/example2_f200w_PSF_4_c.fits` | 4× oversampled PSF model — detection |
83
+
84
+ ---
85
+
86
+ ## Quick Start
87
+
88
+ The core API accepts NumPy arrays directly — load your own FITS files however you prefer:
89
+
90
+ ```python
91
+ from astropy.io import fits
92
+ import numpy as np
93
+ import jwst_psfmc as jpm
94
+
95
+ # ── 1. Load your data ──────────────────────────────────────────────────────
96
+ data = fits.getdata("examples/data/example2_f200w_diff.fits")
97
+ err = fits.getdata("examples/data/example2_f200w_diff_error.fits")
98
+ kernel = fits.getdata("examples/data/example2_f200w_cov_kernel.fits")
99
+ psf_raw = fits.getdata("examples/PSF/example2_f200w_PSF_4_c.fits")
100
+
101
+ # ── 2. Extract a 9×9 stamp centred on the source ───────────────────────────
102
+ cy, cx = data.shape[0] // 2, data.shape[1] // 2
103
+ stamp = data[cy-4:cy+5, cx-4:cx+5]
104
+ stamp_err = err[cy-4:cy+5, cx-4:cx+5]
105
+
106
+ # ── 3. Prepare MCMC inputs ─────────────────────────────────────────────────
107
+ fit_args = jpm.prepare_for_fitting(
108
+ data=stamp, err=stamp_err,
109
+ psf_os=psf_raw, cov_kernel=kernel,
110
+ dx_init=0.0, dy_init=0.0,
111
+ oversamp=4, native_shape=(11, 11), nwalkers=48,
112
+ )
113
+
114
+ # ── 4. Run MCMC ────────────────────────────────────────────────────────────
115
+ sampler = jpm.run_mcmc(**fit_args, nsteps=2000, ncores=4, progress=True)
116
+
117
+ # ── 5. Summarise posterior ─────────────────────────────────────────────────
118
+ summary = jpm.summarize_emcee(sampler, burnin=500, thin=4)
119
+ print(f"flux = {summary['flux']['median']:.4f} "
120
+ f"+{summary['flux']['plus_1sigma']:.4f} / "
121
+ f"-{summary['flux']['minus_1sigma']:.4f}")
122
+
123
+ # ── 6. Visualise ───────────────────────────────────────────────────────────
124
+ fig_trip, _ = jpm.plot_psf_fit_triptych(
125
+ stamp, stamp_err, fit_args["psf_os"], oversamp=4,
126
+ native_shape=fit_args["native_shape"],
127
+ psf_prepare_fraction=fit_args["psf_prepare_fraction"],
128
+ summary=summary, fig_title="Detection example — F200W",
129
+ )
130
+ fig_trip.savefig("triptych.pdf", bbox_inches="tight")
131
+
132
+ fig_chains, _ = jpm.plot_chains(sampler, burnin=500)
133
+ fig_corner, _ = jpm.plot_corner(sampler, burnin=500, thin=4)
134
+
135
+ # ── 7. Save / reload results ───────────────────────────────────────────────
136
+ jpm.save_emcee_results("example2_f200w_mcmc.npz", sampler)
137
+ res = jpm.load_emcee_results("example2_f200w_mcmc.npz")
138
+ print(res["summary"])
139
+ ```
140
+
141
+ ### Deriving a flux upper limit from a non-detection
142
+
143
+ Residual small-scale background fluctuations can mimic low-level source
144
+ emission at 1–2σ. We use a **two-run workflow**:
145
+
146
+ 1. **Detection check** — broad priors (±1 pix on dx/dy) to confirm the
147
+ source is genuinely absent.
148
+ 2. **Upper limit** — tight priors (±0.1 pix) to derive the official 3σ
149
+ bound from the 99.7th percentile of the posterior flux.
150
+
151
+ ```python
152
+ data_nd = fits.getdata("examples/data/example1_f200w_diff.fits")
153
+ err_nd = fits.getdata("examples/data/example1_f200w_diff_error.fits")
154
+ kernel_nd = fits.getdata("examples/data/example1_f200w_cov_kernel.fits")
155
+ psf_nd = fits.getdata("examples/PSF/example1_f200w_PSF_4_c.fits")
156
+
157
+ stamp_nd = data_nd[cy-4:cy+5, cx-4:cx+5]
158
+ stamp_err_nd = err_nd[cy-4:cy+5, cx-4:cx+5]
159
+
160
+ # ── Run 1: broad priors (detection check) ─────────────────────────────
161
+ fit_nd_broad = jpm.prepare_for_fitting(data=stamp_nd, err=stamp_err_nd,
162
+ psf_os=psf_nd, cov_kernel=kernel_nd)
163
+ sampler_nd_broad = jpm.run_mcmc(**fit_nd_broad, nsteps=2000, ncores=4,
164
+ progress=True)
165
+
166
+ # Confirm non-detection: flux median consistent with zero within 1σ
167
+ summary_broad = jpm.summarize_emcee(sampler_nd_broad, burnin=500, thin=4)
168
+ is_absent = abs(summary_broad['flux']['median']) < summary_broad['flux']['plus_1sigma']
169
+ print(f'Non-detection confirmed: {is_absent}')
170
+
171
+ # ── Run 2: tight priors (upper limit) ─────────────────────────────────
172
+ fit_nd_tight = jpm.prepare_for_fitting(data=stamp_nd, err=stamp_err_nd,
173
+ psf_os=psf_nd, cov_kernel=kernel_nd,
174
+ pos_prior_half_width=0.1) # ±0.1 pix
175
+ sampler_nd_tight = jpm.run_mcmc(**fit_nd_tight, nsteps=2000, ncores=4,
176
+ progress=True)
177
+
178
+ flat_flux = sampler_nd_tight.get_chain(discard=500, thin=4, flat=True)[:, 0]
179
+ upper_limit_3sigma = float(np.percentile(flat_flux, 99.7))
180
+ print(f"3-sigma upper limit on flux: {upper_limit_3sigma:.4f}")
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Full Worked Examples
186
+
187
+ ### Covariance kernel estimation
188
+
189
+ See **[`examples/demo_covariance_kernel.ipynb`](examples/demo_covariance_kernel.ipynb)** for
190
+ a step-by-step demonstration of estimating the pixel-to-pixel covariance kernel
191
+ from JWST difference images:
192
+
193
+ - Source masking and finding source-free sky regions
194
+ - Autocorrelation-based kernel estimation
195
+ - Cosine-bell windowing and power spectrum computation
196
+ - Pixel correlation whitening demonstration
197
+
198
+ ### PSF photometry with MCMC
199
+
200
+ See **[`examples/demo_psf_photometry.ipynb`](examples/demo_psf_photometry.ipynb)** for a
201
+ step-by-step notebook covering:
202
+
203
+ - Loading and inspecting the JWST cutout data
204
+ - Estimating and visualising the covariance kernel
205
+ - Running MCMC for a **non-detection** (example1) and deriving a rigorous 3-σ upper limit
206
+ - Running MCMC for a clear **detection** (example2) and extracting flux posteriors
207
+ - Diagnosing convergence via chain traces and autocorrelation times
208
+ - Producing publication-quality corner plots and residual triptychs
209
+
210
+ ---
211
+
212
+ ## Module Overview
213
+
214
+ | Module | Contents |
215
+ |--------|----------|
216
+ | `jwst_psfmc.psf` | PSF shifting, downsampling, model evaluation |
217
+ | `jwst_psfmc.covariance` | Covariance kernel estimation, Fourier-space pre-computation |
218
+ | `jwst_psfmc.mcmc` | MCMC preparation, log-prob, `run_mcmc`, posterior summaries |
219
+ | `jwst_psfmc.io` | Save/load `.npz` results |
220
+ | `jwst_psfmc.plot` | Triptych, chain traces, corner plot |
221
+
222
+ ---
223
+
224
+ ## Citation
225
+
226
+ If you use `jwst_psfmc` in your research, please cite:
227
+
228
+ > Zhuang et al. (in prep.), *NEXUS JWST Transient Survey Year 1*
229
+
230
+ ---
231
+
232
+ ## License
233
+
234
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,104 @@
1
+ """
2
+ jwst_psfmc
3
+ ==========
4
+
5
+ PSF photometry with MCMC for JWST (and HST) drizzled difference images.
6
+
7
+ Accounts for correlated pixel noise introduced by the drizzle algorithm via
8
+ a Fourier-space covariance likelihood. The four fitted parameters are:
9
+
10
+ flux – total source flux in the same units as the image
11
+ dx – sub-pixel x-shift of the PSF centre (native pixels)
12
+ dy – sub-pixel y-shift of the PSF centre (native pixels)
13
+ bkg – flat residual background
14
+
15
+ Public API
16
+ ----------
17
+ PSF manipulation
18
+ shift_psf_fourier, downsample_psf, prepare_psf_for_oversamp,
19
+ psf_model, match_shape_center, inspect_psf_shift
20
+
21
+ Covariance estimation
22
+ estimate_cov_kernel, prepare_covariance_terms,
23
+ SplitCosineBellWindow, get_source_mask, find_zero_squares
24
+
25
+ MCMC fitting
26
+ prepare_for_fitting, run_mcmc,
27
+ log_prior, log_likelihood_cov_prepared, log_prob_prepared,
28
+ summarize_emcee, summarize_flux_from_chain
29
+
30
+ I/O
31
+ save_emcee_results, load_emcee_results
32
+
33
+ Visualisation
34
+ plot_psf_fit_triptych, plot_chains, plot_corner
35
+ """
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ from .psf import (
40
+ shift_psf_fourier,
41
+ downsample_psf,
42
+ match_shape_center,
43
+ prepare_psf_for_oversamp,
44
+ psf_model,
45
+ inspect_psf_shift,
46
+ )
47
+ from .covariance import (
48
+ SplitCosineBellWindow,
49
+ distance_grid,
50
+ estimate_cov_kernel,
51
+ prepare_covariance_terms,
52
+ get_source_mask,
53
+ find_zero_squares,
54
+ )
55
+ from .mcmc import (
56
+ log_prior,
57
+ log_likelihood_cov_prepared,
58
+ log_prob_prepared,
59
+ prepare_for_fitting,
60
+ run_mcmc,
61
+ summarize_emcee,
62
+ summarize_flux_from_chain,
63
+ )
64
+ from .io import (
65
+ save_emcee_results,
66
+ load_emcee_results,
67
+ )
68
+ from .plot import (
69
+ plot_psf_fit_triptych,
70
+ plot_chains,
71
+ plot_corner,
72
+ )
73
+
74
+ __all__ = [
75
+ # psf
76
+ "shift_psf_fourier",
77
+ "downsample_psf",
78
+ "match_shape_center",
79
+ "prepare_psf_for_oversamp",
80
+ "psf_model",
81
+ "inspect_psf_shift",
82
+ # covariance
83
+ "SplitCosineBellWindow",
84
+ "distance_grid",
85
+ "estimate_cov_kernel",
86
+ "prepare_covariance_terms",
87
+ "get_source_mask",
88
+ "find_zero_squares",
89
+ # mcmc
90
+ "log_prior",
91
+ "log_likelihood_cov_prepared",
92
+ "log_prob_prepared",
93
+ "prepare_for_fitting",
94
+ "run_mcmc",
95
+ "summarize_emcee",
96
+ "summarize_flux_from_chain",
97
+ # io
98
+ "save_emcee_results",
99
+ "load_emcee_results",
100
+ # plot
101
+ "plot_psf_fit_triptych",
102
+ "plot_chains",
103
+ "plot_corner",
104
+ ]