wcc-sim 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.
Files changed (75) hide show
  1. wcc_sim-0.1.0/LICENSE +28 -0
  2. wcc_sim-0.1.0/PKG-INFO +220 -0
  3. wcc_sim-0.1.0/README.md +188 -0
  4. wcc_sim-0.1.0/pyproject.toml +65 -0
  5. wcc_sim-0.1.0/setup.cfg +4 -0
  6. wcc_sim-0.1.0/src/wcc_phot/__init__.py +6 -0
  7. wcc_sim-0.1.0/src/wcc_phot/apphot.py +69 -0
  8. wcc_sim-0.1.0/src/wcc_phot/centroid.py +41 -0
  9. wcc_sim-0.1.0/src/wcc_phot/cli.py +110 -0
  10. wcc_sim-0.1.0/src/wcc_phot/flags.py +7 -0
  11. wcc_sim-0.1.0/src/wcc_phot/geometry.py +93 -0
  12. wcc_sim-0.1.0/src/wcc_phot/io.py +110 -0
  13. wcc_sim-0.1.0/src/wcc_phot/lightcurve.py +103 -0
  14. wcc_sim-0.1.0/src/wcc_phot/live.py +141 -0
  15. wcc_sim-0.1.0/src/wcc_phot/pipeline.py +315 -0
  16. wcc_sim-0.1.0/src/wcc_phot/psfphot.py +78 -0
  17. wcc_sim-0.1.0/src/wcc_phot/report.py +454 -0
  18. wcc_sim-0.1.0/src/wcc_phot/select.py +83 -0
  19. wcc_sim-0.1.0/src/wcc_phot/validation.py +243 -0
  20. wcc_sim-0.1.0/src/wcc_sim/__init__.py +29 -0
  21. wcc_sim-0.1.0/src/wcc_sim/astrometry.py +136 -0
  22. wcc_sim-0.1.0/src/wcc_sim/brightcat.py +292 -0
  23. wcc_sim-0.1.0/src/wcc_sim/catalog.py +133 -0
  24. wcc_sim-0.1.0/src/wcc_sim/chromatic.py +179 -0
  25. wcc_sim-0.1.0/src/wcc_sim/cli.py +92 -0
  26. wcc_sim-0.1.0/src/wcc_sim/data/gaia_dr3_bp.dat +426 -0
  27. wcc_sim-0.1.0/src/wcc_sim/data/gaia_dr3_g.dat +731 -0
  28. wcc_sim-0.1.0/src/wcc_sim/data/gaia_dr3_rp.dat +471 -0
  29. wcc_sim-0.1.0/src/wcc_sim/data/plot_default.mplstyle +38 -0
  30. wcc_sim-0.1.0/src/wcc_sim/data/psfs/lazuli_scatter_450nm_profile.ecsv +237 -0
  31. wcc_sim-0.1.0/src/wcc_sim/data/spt_synthetic_colors.csv +28 -0
  32. wcc_sim-0.1.0/src/wcc_sim/detectors.py +82 -0
  33. wcc_sim-0.1.0/src/wcc_sim/extended.py +178 -0
  34. wcc_sim-0.1.0/src/wcc_sim/fitswriter.py +102 -0
  35. wcc_sim-0.1.0/src/wcc_sim/pipeline.py +514 -0
  36. wcc_sim-0.1.0/src/wcc_sim/plotting.py +283 -0
  37. wcc_sim-0.1.0/src/wcc_sim/psf.py +95 -0
  38. wcc_sim-0.1.0/src/wcc_sim/psfreport.py +719 -0
  39. wcc_sim-0.1.0/src/wcc_sim/render.py +208 -0
  40. wcc_sim-0.1.0/src/wcc_sim/scatter.py +422 -0
  41. wcc_sim-0.1.0/src/wcc_sim/starflux.py +289 -0
  42. wcc_sim-0.1.0/src/wcc_sim/wcsutil.py +24 -0
  43. wcc_sim-0.1.0/src/wcc_sim/wings.py +157 -0
  44. wcc_sim-0.1.0/src/wcc_sim.egg-info/PKG-INFO +220 -0
  45. wcc_sim-0.1.0/src/wcc_sim.egg-info/SOURCES.txt +73 -0
  46. wcc_sim-0.1.0/src/wcc_sim.egg-info/dependency_links.txt +1 -0
  47. wcc_sim-0.1.0/src/wcc_sim.egg-info/entry_points.txt +3 -0
  48. wcc_sim-0.1.0/src/wcc_sim.egg-info/requires.txt +20 -0
  49. wcc_sim-0.1.0/src/wcc_sim.egg-info/top_level.txt +2 -0
  50. wcc_sim-0.1.0/tests/test_astrometry.py +226 -0
  51. wcc_sim-0.1.0/tests/test_brightcat.py +519 -0
  52. wcc_sim-0.1.0/tests/test_catalog.py +154 -0
  53. wcc_sim-0.1.0/tests/test_chromatic.py +138 -0
  54. wcc_sim-0.1.0/tests/test_cli.py +54 -0
  55. wcc_sim-0.1.0/tests/test_detectors.py +71 -0
  56. wcc_sim-0.1.0/tests/test_edge_cases.py +131 -0
  57. wcc_sim-0.1.0/tests/test_etc_crosscheck.py +108 -0
  58. wcc_sim-0.1.0/tests/test_extended.py +218 -0
  59. wcc_sim-0.1.0/tests/test_fitswriter.py +63 -0
  60. wcc_sim-0.1.0/tests/test_phot_io.py +100 -0
  61. wcc_sim-0.1.0/tests/test_phot_lightcurve.py +87 -0
  62. wcc_sim-0.1.0/tests/test_phot_live.py +67 -0
  63. wcc_sim-0.1.0/tests/test_phot_measure.py +103 -0
  64. wcc_sim-0.1.0/tests/test_phot_pipeline.py +108 -0
  65. wcc_sim-0.1.0/tests/test_phot_report.py +70 -0
  66. wcc_sim-0.1.0/tests/test_phot_select.py +73 -0
  67. wcc_sim-0.1.0/tests/test_pipeline.py +235 -0
  68. wcc_sim-0.1.0/tests/test_plotting.py +182 -0
  69. wcc_sim-0.1.0/tests/test_psf.py +154 -0
  70. wcc_sim-0.1.0/tests/test_psfreport.py +462 -0
  71. wcc_sim-0.1.0/tests/test_render.py +129 -0
  72. wcc_sim-0.1.0/tests/test_scatter.py +708 -0
  73. wcc_sim-0.1.0/tests/test_starflux.py +340 -0
  74. wcc_sim-0.1.0/tests/test_wcsutil.py +41 -0
  75. wcc_sim-0.1.0/tests/test_wings.py +196 -0
wcc_sim-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Schmidt Observatories
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
wcc_sim-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: wcc-sim
3
+ Version: 0.1.0
4
+ Summary: End-to-end photometric image simulator for the Lazuli Wide-field Context Camera (WCC)
5
+ Author-email: Gudmundur Stefansson <gstefansson@schmidtsciences.org>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/schmidt-observatories/wcc-sim
8
+ Project-URL: Documentation, https://wcc-sim.readthedocs.io/
9
+ Project-URL: Issues, https://github.com/schmidt-observatories/wcc-sim/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: wcc_etc>=0.1.0
14
+ Requires-Dist: numpy>=1.24
15
+ Requires-Dist: scipy>=1.10
16
+ Requires-Dist: astropy>=5.3
17
+ Requires-Dist: astroquery>=0.4.7
18
+ Requires-Dist: synphot>=1.2
19
+ Requires-Dist: photutils>=2.0
20
+ Requires-Dist: dust_extinction>=1.2
21
+ Requires-Dist: matplotlib>=3.7
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Requires-Dist: ruff; extra == "test"
25
+ Provides-Extra: docs
26
+ Requires-Dist: sphinx; extra == "docs"
27
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
28
+ Requires-Dist: nbsphinx; extra == "docs"
29
+ Requires-Dist: myst-parser; extra == "docs"
30
+ Requires-Dist: ipykernel; extra == "docs"
31
+ Dynamic: license-file
32
+
33
+ ![Logo](docs/logo_schmidt_observatory_system.png)
34
+
35
+ [![Tests](https://github.com/schmidt-observatories/wcc-sim/actions/workflows/tests.yml/badge.svg)](https://github.com/schmidt-observatories/wcc-sim/actions/workflows/tests.yml)
36
+
37
+ # wcc-sim
38
+ End-to-end image simulator and differential-photometry pipeline for the
39
+ Wide-field Context Camera (WCC) on Lazuli.
40
+
41
+ `wcc-sim` queries Gaia DR3 for a pointing, converts Gaia photometry to detector
42
+ count rates through the [wcc-etc](https://github.com/schmidt-observatories/wcc-etc)
43
+ instrument model, renders every star with the in-focus (Airy) or +1/+2-wave
44
+ defocus (Zemax Huygens) PSF plus its diffraction wings and the scattered-light
45
+ halo, adds photon/sky/dark/read noise with full-well and ADC saturation, and
46
+ writes a multi-extension FITS image (SCI + SATMASK + CAT + CLEAN) with a TAN
47
+ WCS. The sibling `wcc-phot` package extracts a relative light curve from a
48
+ series of such frames.
49
+
50
+ # Installation
51
+
52
+ ```sh
53
+ pip install wcc-sim
54
+ ```
55
+
56
+ [`wcc_etc`](https://pypi.org/project/wcc-etc/) provides the instrument model
57
+ and is pulled in from PyPI as a dependency. From source:
58
+ ```sh
59
+ git clone https://github.com/schmidt-observatories/wcc-sim.git
60
+ cd wcc-sim
61
+ pip install -e .
62
+ ```
63
+
64
+ With optional extras:
65
+ ```sh
66
+ pip install -e ".[test]" # pytest
67
+ pip install -e ".[docs]" # documentation build (Sphinx, nbsphinx, etc.)
68
+ ```
69
+
70
+ Python 3.11 or newer is required. A default run queries the Gaia archive, so
71
+ it needs network access. Pass `cache_dir=` (CLI: `--cache-dir`) to keep the
72
+ Gaia result on disk so repeat runs work offline.
73
+
74
+ # Quick Start
75
+
76
+ ```python
77
+ from wcc_sim import simulate_field
78
+
79
+ # Simulate one 90 s frame of the field at (RA, Dec) = (150.1, 2.2) deg on the
80
+ # Sony IMX455 detector in the r band, +1 wave defocused, and save it as FITS.
81
+ field = simulate_field(150.1, 2.2, sensorfilter="zwo:r", focus=1,
82
+ exptime=90, seed=42, output="field_1wave.fits")
83
+
84
+ field.image_adu # the detector image (ADU)
85
+ field.catalog # the Gaia table with pixel positions and count rates
86
+ field.params # everything the run used (rates, noise, PSF, wing floor)
87
+ field.plot(catalog=True) # the image with the catalog circled (ax=None -> new figure)
88
+ field.plot_psf_profile() # Airy core, scatter halo and the rendered radial profile
89
+ ```
90
+
91
+ The same from the command line:
92
+ ```sh
93
+ wcc-sim --ra 150.1 --dec 2.2 --sensorfilter zwo:r --focus 1 \
94
+ --exptime 90 --seed 42 -o field_1wave.fits
95
+ ```
96
+
97
+ Then recover a light curve from a series of frames of the same field:
98
+ ```python
99
+ from wcc_phot import run_photometry
100
+
101
+ result = run_photometry(["f0.fits", "f1.fits", "f2.fits"], target=(150.1, 2.2),
102
+ method="aperture", n_ref=10)
103
+ result.lightcurve # per frame: rel_flux_norm, errors, flags
104
+ result.plot() # the relative light curve; result.plot_field(frame), .report(path)
105
+ ```
106
+ ```sh
107
+ wcc-phot f*.fits --ra 150.1 --dec 2.2 --method psf -o phot.fits --report report.pdf
108
+ ```
109
+
110
+ `sensorfilter` is `kind:band` as in wcc-etc: `zwo:*` is the Sony IMX455
111
+ (9568 x 6380 px, 16.87 mas/pix) and `qcmos:*` the Hamamatsu HWK4123
112
+ (4096 x 2304 px, 20.64 mas/pix). `focus=0|1|2` selects in-focus, +1 wave or
113
+ +2 waves of defocus. `extended_sources=[SersicComponent(...)]` adds analytic
114
+ galaxy light and `chromatic=True` switches to spectrum-weighted PSFs (in focus
115
+ only).
116
+
117
+ # Tutorial
118
+ See the `notebooks/` directory for example tutorials (executed outputs
119
+ included; the Gaia queries they need are cached in `notebooks/gaia_cache/`,
120
+ so they run offline):
121
+
122
+ | Notebook | What it covers |
123
+ |---|---|
124
+ | `01_getting_started.ipynb` | Simulate a full frame, overlay the Gaia catalog, compare the three PSF modes on one star, and write the FITS file. |
125
+ | `02_verification.ipynb` | The checks: PSF peak ordering, astrometric residuals, photometric closure on the ETC rates, noise statistics, saturation flags, and the FITS round trip. |
126
+ | `03_psf_focus_modes.ipynb` | In-focus Airy vs +1/+2-wave defocus PSFs and what they do to peak pixel and saturation. |
127
+ | `04_detectors_and_filters.ipynb` | The two detectors, the filter sets, and Gaia-to-count-rate conversion per spectral type. |
128
+ | `05_noise_and_saturation.ipynb` | Photon, sky, dark and read noise; full well and ADC clipping; the SATMASK extension. |
129
+ | `06_astrometry_and_catalogs.ipynb` | The TAN WCS and its round trip, where stars land, position angle, edges and user-supplied catalogs. |
130
+ | `07_cli_and_fits.ipynb` | The `wcc-sim` command line and the layout of the output FITS file. |
131
+ | `08_psf_wings.ipynb` | Diffraction wings beyond the PSF stamp: the wing model, seam smoothness, encircled energy and how far stars extend. |
132
+ | `09_wcc_phot_photometry.ipynb` | `wcc-phot`: simulate a dithered series, pick a target, aperture and PSF photometry, centroid drift, the live viewer. |
133
+ | `10_plotting.ipynb` | Every `.plot_*` method once: images with catalog and saturation overlays, cutouts, the PSF radial profile, stamps, encircled energy, the photometry panels. |
134
+
135
+ Ready-to-run CLI examples are in `scripts/example_*.sh`. Two end-to-end
136
+ validations live next to them: `scripts/transit_trappist1b.py` injects the
137
+ TRAPPIST-1b transit into the Gaia catalog, simulates 36 x 300 s defocused
138
+ frames, runs `wcc-phot` and checks the recovered depth; `scripts/cepheids_m101.py`
139
+ places synthetic Cepheids on the M101 disk (Sersic galaxy light plus the real
140
+ Gaia foreground) and recovers a period-luminosity relation.
141
+
142
+ # Notes
143
+ Instrument configuration (detectors, filters, throughput, PSFs, the stray-light
144
+ model) comes from wcc-etc and is not duplicated here. The one data product
145
+ wcc-sim ships is the azimuthally averaged scattered-light profile in
146
+ `src/wcc_sim/data/psfs/`, regenerated by `scripts/build_scatter_profile.py`.
147
+
148
+ ## Model assumptions and validation status
149
+
150
+ wcc-sim takes its instrument model from wcc-etc. Some choices in that model
151
+ are deliberate margins or simplifications rather than best estimates, and
152
+ they move the numbers you get:
153
+
154
+ - **Read noise carries a x2 margin.** Both detector configurations multiply
155
+ the datasheet read noise by `read_noise_margin = 2` to cover unmodelled
156
+ noise sources (RTS, non-linearity), and every simulated frame inherits it.
157
+ In a 90 s `zwo:r` frame most of the per-pixel variance is read noise, so
158
+ faint-source depth is about 0.6 mag shallower than the datasheet value
159
+ would give. There is no switch for it yet (issue #23).
160
+ - **Unobscured pupil.** The collecting area is pi (D/2)^2 and the in-focus
161
+ Airy PSF has no central obscuration or spiders. The defocused PSFs are
162
+ Zemax Huygens images of the real system.
163
+ - **Constant zodiacal background** at V = 22.5 mag/arcsec^2, with no
164
+ dependence on ecliptic latitude or Sun angle.
165
+ - **Gaia G normalisation** uses a synthetic Vega-system zero point about 1%
166
+ below the published DR3 value, so all count rates are about 1% low.
167
+ - **The scattered-light halo is monochromatic.** It comes from a single
168
+ 450 nm stray-light run and is applied with the same shape and fraction in
169
+ every band; surface scatter falls with wavelength, so the halo is
170
+ over-predicted in the red bands.
171
+ - **"N waves" of defocus** is a Fringe Zernike Z4 coefficient of N waves
172
+ (2N waves peak-to-valley), the convention of the Zemax runs.
173
+
174
+ Not modelled: cosmic rays, PRNU, non-linearity, random telegraph noise,
175
+ charge diffusion and intrapixel response, Earthshine, the pointing dependence
176
+ of the zodiacal light, and giant or reddened-star templates (every star gets
177
+ a dwarf template).
178
+
179
+ **Validation status.** The count-rate chain, PSF energy, astrometry and
180
+ noise model have been checked against first-principles calculations and
181
+ against the wcc-etc exposure-time calculator, including a Monte Carlo
182
+ cross-check of the ETC's signal and noise predictions on simulated frames.
183
+ Nothing has yet been compared with measured data; issue #25 holds the plan.
184
+
185
+ ## Documentation
186
+
187
+ The full documentation site (installation, a user guide, rendered tutorial
188
+ notebooks, a CLI reference and an auto-generated API reference) is built with
189
+ **Sphinx** and the *Read the Docs* theme. It lives under `docs/sphinx/`:
190
+
191
+ ```bash
192
+ pip install -e ".[docs]" # Sphinx + theme + nbsphinx (+ the package itself)
193
+ brew install pandoc # or: conda install pandoc / apt-get install pandoc
194
+ cd docs/sphinx
195
+ make html # output in _build/html/index.html
196
+ ```
197
+
198
+ The rendered site is published on Read the Docs at
199
+ <https://wcc-sim.readthedocs.io/>; `.readthedocs.yaml` configures the build.
200
+
201
+ ## Tests
202
+
203
+ ```sh
204
+ pip install -e ".[test]"
205
+ python -m pytest
206
+ ```
207
+
208
+ ## Acknowledgements
209
+ This work has made use of data from the European Space Agency (ESA) mission
210
+ *Gaia* (https://www.cosmos.esa.int/gaia), processed by the Gaia Data
211
+ Processing and Analysis Consortium (DPAC); the Gaia tables redistributed in
212
+ `notebooks/gaia_cache/` are subject to the
213
+ [Gaia data credits](https://www.cosmos.esa.int/web/gaia-users/credits).
214
+ Stellar spectra are the Pickles (1998, PASP 110, 863) library; the Gaia
215
+ passbands come from the SVO Filter Profile Service (Rodrigo & Solano 2020,
216
+ http://svo2.cab.inta-csic.es/theory/fps/); reddening uses the Fitzpatrick
217
+ (1999) curve via [dust_extinction](https://dust-extinction.readthedocs.io).
218
+ Simulations are built on [Astropy](https://www.astropy.org),
219
+ [photutils](https://photutils.readthedocs.io) and
220
+ [synphot](https://synphot.readthedocs.io).
@@ -0,0 +1,188 @@
1
+ ![Logo](docs/logo_schmidt_observatory_system.png)
2
+
3
+ [![Tests](https://github.com/schmidt-observatories/wcc-sim/actions/workflows/tests.yml/badge.svg)](https://github.com/schmidt-observatories/wcc-sim/actions/workflows/tests.yml)
4
+
5
+ # wcc-sim
6
+ End-to-end image simulator and differential-photometry pipeline for the
7
+ Wide-field Context Camera (WCC) on Lazuli.
8
+
9
+ `wcc-sim` queries Gaia DR3 for a pointing, converts Gaia photometry to detector
10
+ count rates through the [wcc-etc](https://github.com/schmidt-observatories/wcc-etc)
11
+ instrument model, renders every star with the in-focus (Airy) or +1/+2-wave
12
+ defocus (Zemax Huygens) PSF plus its diffraction wings and the scattered-light
13
+ halo, adds photon/sky/dark/read noise with full-well and ADC saturation, and
14
+ writes a multi-extension FITS image (SCI + SATMASK + CAT + CLEAN) with a TAN
15
+ WCS. The sibling `wcc-phot` package extracts a relative light curve from a
16
+ series of such frames.
17
+
18
+ # Installation
19
+
20
+ ```sh
21
+ pip install wcc-sim
22
+ ```
23
+
24
+ [`wcc_etc`](https://pypi.org/project/wcc-etc/) provides the instrument model
25
+ and is pulled in from PyPI as a dependency. From source:
26
+ ```sh
27
+ git clone https://github.com/schmidt-observatories/wcc-sim.git
28
+ cd wcc-sim
29
+ pip install -e .
30
+ ```
31
+
32
+ With optional extras:
33
+ ```sh
34
+ pip install -e ".[test]" # pytest
35
+ pip install -e ".[docs]" # documentation build (Sphinx, nbsphinx, etc.)
36
+ ```
37
+
38
+ Python 3.11 or newer is required. A default run queries the Gaia archive, so
39
+ it needs network access. Pass `cache_dir=` (CLI: `--cache-dir`) to keep the
40
+ Gaia result on disk so repeat runs work offline.
41
+
42
+ # Quick Start
43
+
44
+ ```python
45
+ from wcc_sim import simulate_field
46
+
47
+ # Simulate one 90 s frame of the field at (RA, Dec) = (150.1, 2.2) deg on the
48
+ # Sony IMX455 detector in the r band, +1 wave defocused, and save it as FITS.
49
+ field = simulate_field(150.1, 2.2, sensorfilter="zwo:r", focus=1,
50
+ exptime=90, seed=42, output="field_1wave.fits")
51
+
52
+ field.image_adu # the detector image (ADU)
53
+ field.catalog # the Gaia table with pixel positions and count rates
54
+ field.params # everything the run used (rates, noise, PSF, wing floor)
55
+ field.plot(catalog=True) # the image with the catalog circled (ax=None -> new figure)
56
+ field.plot_psf_profile() # Airy core, scatter halo and the rendered radial profile
57
+ ```
58
+
59
+ The same from the command line:
60
+ ```sh
61
+ wcc-sim --ra 150.1 --dec 2.2 --sensorfilter zwo:r --focus 1 \
62
+ --exptime 90 --seed 42 -o field_1wave.fits
63
+ ```
64
+
65
+ Then recover a light curve from a series of frames of the same field:
66
+ ```python
67
+ from wcc_phot import run_photometry
68
+
69
+ result = run_photometry(["f0.fits", "f1.fits", "f2.fits"], target=(150.1, 2.2),
70
+ method="aperture", n_ref=10)
71
+ result.lightcurve # per frame: rel_flux_norm, errors, flags
72
+ result.plot() # the relative light curve; result.plot_field(frame), .report(path)
73
+ ```
74
+ ```sh
75
+ wcc-phot f*.fits --ra 150.1 --dec 2.2 --method psf -o phot.fits --report report.pdf
76
+ ```
77
+
78
+ `sensorfilter` is `kind:band` as in wcc-etc: `zwo:*` is the Sony IMX455
79
+ (9568 x 6380 px, 16.87 mas/pix) and `qcmos:*` the Hamamatsu HWK4123
80
+ (4096 x 2304 px, 20.64 mas/pix). `focus=0|1|2` selects in-focus, +1 wave or
81
+ +2 waves of defocus. `extended_sources=[SersicComponent(...)]` adds analytic
82
+ galaxy light and `chromatic=True` switches to spectrum-weighted PSFs (in focus
83
+ only).
84
+
85
+ # Tutorial
86
+ See the `notebooks/` directory for example tutorials (executed outputs
87
+ included; the Gaia queries they need are cached in `notebooks/gaia_cache/`,
88
+ so they run offline):
89
+
90
+ | Notebook | What it covers |
91
+ |---|---|
92
+ | `01_getting_started.ipynb` | Simulate a full frame, overlay the Gaia catalog, compare the three PSF modes on one star, and write the FITS file. |
93
+ | `02_verification.ipynb` | The checks: PSF peak ordering, astrometric residuals, photometric closure on the ETC rates, noise statistics, saturation flags, and the FITS round trip. |
94
+ | `03_psf_focus_modes.ipynb` | In-focus Airy vs +1/+2-wave defocus PSFs and what they do to peak pixel and saturation. |
95
+ | `04_detectors_and_filters.ipynb` | The two detectors, the filter sets, and Gaia-to-count-rate conversion per spectral type. |
96
+ | `05_noise_and_saturation.ipynb` | Photon, sky, dark and read noise; full well and ADC clipping; the SATMASK extension. |
97
+ | `06_astrometry_and_catalogs.ipynb` | The TAN WCS and its round trip, where stars land, position angle, edges and user-supplied catalogs. |
98
+ | `07_cli_and_fits.ipynb` | The `wcc-sim` command line and the layout of the output FITS file. |
99
+ | `08_psf_wings.ipynb` | Diffraction wings beyond the PSF stamp: the wing model, seam smoothness, encircled energy and how far stars extend. |
100
+ | `09_wcc_phot_photometry.ipynb` | `wcc-phot`: simulate a dithered series, pick a target, aperture and PSF photometry, centroid drift, the live viewer. |
101
+ | `10_plotting.ipynb` | Every `.plot_*` method once: images with catalog and saturation overlays, cutouts, the PSF radial profile, stamps, encircled energy, the photometry panels. |
102
+
103
+ Ready-to-run CLI examples are in `scripts/example_*.sh`. Two end-to-end
104
+ validations live next to them: `scripts/transit_trappist1b.py` injects the
105
+ TRAPPIST-1b transit into the Gaia catalog, simulates 36 x 300 s defocused
106
+ frames, runs `wcc-phot` and checks the recovered depth; `scripts/cepheids_m101.py`
107
+ places synthetic Cepheids on the M101 disk (Sersic galaxy light plus the real
108
+ Gaia foreground) and recovers a period-luminosity relation.
109
+
110
+ # Notes
111
+ Instrument configuration (detectors, filters, throughput, PSFs, the stray-light
112
+ model) comes from wcc-etc and is not duplicated here. The one data product
113
+ wcc-sim ships is the azimuthally averaged scattered-light profile in
114
+ `src/wcc_sim/data/psfs/`, regenerated by `scripts/build_scatter_profile.py`.
115
+
116
+ ## Model assumptions and validation status
117
+
118
+ wcc-sim takes its instrument model from wcc-etc. Some choices in that model
119
+ are deliberate margins or simplifications rather than best estimates, and
120
+ they move the numbers you get:
121
+
122
+ - **Read noise carries a x2 margin.** Both detector configurations multiply
123
+ the datasheet read noise by `read_noise_margin = 2` to cover unmodelled
124
+ noise sources (RTS, non-linearity), and every simulated frame inherits it.
125
+ In a 90 s `zwo:r` frame most of the per-pixel variance is read noise, so
126
+ faint-source depth is about 0.6 mag shallower than the datasheet value
127
+ would give. There is no switch for it yet (issue #23).
128
+ - **Unobscured pupil.** The collecting area is pi (D/2)^2 and the in-focus
129
+ Airy PSF has no central obscuration or spiders. The defocused PSFs are
130
+ Zemax Huygens images of the real system.
131
+ - **Constant zodiacal background** at V = 22.5 mag/arcsec^2, with no
132
+ dependence on ecliptic latitude or Sun angle.
133
+ - **Gaia G normalisation** uses a synthetic Vega-system zero point about 1%
134
+ below the published DR3 value, so all count rates are about 1% low.
135
+ - **The scattered-light halo is monochromatic.** It comes from a single
136
+ 450 nm stray-light run and is applied with the same shape and fraction in
137
+ every band; surface scatter falls with wavelength, so the halo is
138
+ over-predicted in the red bands.
139
+ - **"N waves" of defocus** is a Fringe Zernike Z4 coefficient of N waves
140
+ (2N waves peak-to-valley), the convention of the Zemax runs.
141
+
142
+ Not modelled: cosmic rays, PRNU, non-linearity, random telegraph noise,
143
+ charge diffusion and intrapixel response, Earthshine, the pointing dependence
144
+ of the zodiacal light, and giant or reddened-star templates (every star gets
145
+ a dwarf template).
146
+
147
+ **Validation status.** The count-rate chain, PSF energy, astrometry and
148
+ noise model have been checked against first-principles calculations and
149
+ against the wcc-etc exposure-time calculator, including a Monte Carlo
150
+ cross-check of the ETC's signal and noise predictions on simulated frames.
151
+ Nothing has yet been compared with measured data; issue #25 holds the plan.
152
+
153
+ ## Documentation
154
+
155
+ The full documentation site (installation, a user guide, rendered tutorial
156
+ notebooks, a CLI reference and an auto-generated API reference) is built with
157
+ **Sphinx** and the *Read the Docs* theme. It lives under `docs/sphinx/`:
158
+
159
+ ```bash
160
+ pip install -e ".[docs]" # Sphinx + theme + nbsphinx (+ the package itself)
161
+ brew install pandoc # or: conda install pandoc / apt-get install pandoc
162
+ cd docs/sphinx
163
+ make html # output in _build/html/index.html
164
+ ```
165
+
166
+ The rendered site is published on Read the Docs at
167
+ <https://wcc-sim.readthedocs.io/>; `.readthedocs.yaml` configures the build.
168
+
169
+ ## Tests
170
+
171
+ ```sh
172
+ pip install -e ".[test]"
173
+ python -m pytest
174
+ ```
175
+
176
+ ## Acknowledgements
177
+ This work has made use of data from the European Space Agency (ESA) mission
178
+ *Gaia* (https://www.cosmos.esa.int/gaia), processed by the Gaia Data
179
+ Processing and Analysis Consortium (DPAC); the Gaia tables redistributed in
180
+ `notebooks/gaia_cache/` are subject to the
181
+ [Gaia data credits](https://www.cosmos.esa.int/web/gaia-users/credits).
182
+ Stellar spectra are the Pickles (1998, PASP 110, 863) library; the Gaia
183
+ passbands come from the SVO Filter Profile Service (Rodrigo & Solano 2020,
184
+ http://svo2.cab.inta-csic.es/theory/fps/); reddening uses the Fitzpatrick
185
+ (1999) curve via [dust_extinction](https://dust-extinction.readthedocs.io).
186
+ Simulations are built on [Astropy](https://www.astropy.org),
187
+ [photutils](https://photutils.readthedocs.io) and
188
+ [synphot](https://synphot.readthedocs.io).
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wcc-sim"
7
+ version = "0.1.0"
8
+ description = "End-to-end photometric image simulator for the Lazuli Wide-field Context Camera (WCC)"
9
+ readme = "README.md"
10
+ license = "BSD-3-Clause"
11
+ license-files = ["LICENSE"]
12
+ authors = [
13
+ { name = "Gudmundur Stefansson", email = "gstefansson@schmidtsciences.org" },
14
+ ]
15
+ requires-python = ">=3.11"
16
+ dependencies = [
17
+ "wcc_etc>=0.1.0", # instrument model; public io names since 0.1.0
18
+ "numpy>=1.24",
19
+ "scipy>=1.10",
20
+ "astropy>=5.3",
21
+ "astroquery>=0.4.7",
22
+ "synphot>=1.2",
23
+ "photutils>=2.0", # ImagePSF
24
+ "dust_extinction>=1.2",
25
+ "matplotlib>=3.7", # wcc_phot report/live view, wcc_sim.psfreport
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/schmidt-observatories/wcc-sim"
30
+ Documentation = "https://wcc-sim.readthedocs.io/"
31
+ Issues = "https://github.com/schmidt-observatories/wcc-sim/issues"
32
+
33
+ [project.scripts]
34
+ wcc-sim = "wcc_sim.cli:main"
35
+ wcc-phot = "wcc_phot.cli:main"
36
+
37
+ [project.optional-dependencies]
38
+ test = ["pytest", "ruff"]
39
+ docs = [
40
+ "sphinx",
41
+ "sphinx-rtd-theme",
42
+ "nbsphinx",
43
+ "myst-parser",
44
+ "ipykernel",
45
+ ]
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.setuptools.package-data]
51
+ wcc_sim = ["data/*", "data/psfs/*.ecsv"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ # `pytest -m "not slow"` skips the Monte Carlo cross-check (~30 s); nothing
56
+ # in the suite needs the network, `network` is reserved for tests that will.
57
+ markers = [
58
+ "slow: takes more than ~10 s (Monte Carlo cross-checks)",
59
+ "network: needs the Gaia archive or VizieR",
60
+ ]
61
+
62
+ [tool.ruff.lint]
63
+ # Pin the rule set so local runs and CI agree whatever ruff defaults do:
64
+ # pyflakes plus the pycodestyle error classes ruff enables by default.
65
+ select = ["E4", "E7", "E9", "F"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .geometry import PhotGeometry # noqa: E402,F401
4
+ from .live import LiveViewer # noqa: E402,F401
5
+ from .pipeline import PhotometryResult, run_photometry # noqa: E402,F401
6
+ from .report import make_report # noqa: E402,F401
@@ -0,0 +1,69 @@
1
+ """Aperture photometry with CCD-equation errors from the frame header rates."""
2
+
3
+ import numpy as np
4
+ from astropy.stats import SigmaClip
5
+ from photutils.aperture import ApertureStats, CircularAnnulus, CircularAperture
6
+
7
+ from .flags import FLAG_EDGE, FLAG_SATURATED
8
+
9
+
10
+ def _saturated_within(satmask, x, y, radius):
11
+ """True if any saturated pixel lies within `radius` px of (x, y)."""
12
+ ny, nx = satmask.shape
13
+ r = int(np.ceil(radius))
14
+ x0, y0 = int(round(x)), int(round(y))
15
+ y_lo, y_hi = max(y0 - r, 0), min(y0 + r + 1, ny)
16
+ x_lo, x_hi = max(x0 - r, 0), min(x0 + r + 1, nx)
17
+ if y_lo >= y_hi or x_lo >= x_hi:
18
+ return False
19
+ return bool(satmask[y_lo:y_hi, x_lo:x_hi].any())
20
+
21
+
22
+ def aperture_photometry_frame(frame, x, y, geom):
23
+ """Background-subtracted aperture fluxes at (x, y) on the electron image.
24
+
25
+ Returns (flux_e, flux_err_e, bkg_e_pix, flags). Background is the
26
+ sigma-clipped annulus median; the variance is Poisson of the star plus
27
+ the per-pixel background variance from the header rates
28
+ ((sky+dark)*exptime + n_reads*read_noise^2), including the
29
+ background-estimate term (area^2/n_annulus).
30
+ """
31
+ x = np.atleast_1d(np.asarray(x, dtype=float))
32
+ y = np.atleast_1d(np.asarray(y, dtype=float))
33
+ positions = np.column_stack([x, y])
34
+ aper = CircularAperture(positions, r=geom.r_ap)
35
+ annulus = CircularAnnulus(positions, r_in=geom.r_in, r_out=geom.r_out)
36
+
37
+ bkg = np.asarray(
38
+ ApertureStats(
39
+ frame.image_e, annulus, sigma_clip=SigmaClip(sigma=3.0)
40
+ ).median,
41
+ dtype=float,
42
+ )
43
+ raw = np.asarray(ApertureStats(frame.image_e, aper).sum, dtype=float)
44
+ area = aper.area
45
+ flux = raw - bkg * area
46
+
47
+ m = frame.meta
48
+ var_pix = (
49
+ (float(m["sky_e_s"]) + float(m["dark_e_s"])) * float(m["exptime"])
50
+ + int(m["n_reads"]) * float(m["read_noise"]) ** 2
51
+ )
52
+ variance = np.clip(flux, 0, None) + area * var_pix * (
53
+ 1.0 + area / annulus.area
54
+ )
55
+ flux_err = np.sqrt(variance)
56
+
57
+ ny, nx = frame.image_e.shape
58
+ flags = np.zeros(x.size, dtype=int)
59
+ edge = (
60
+ (x < geom.r_out)
61
+ | (x > nx - 1 - geom.r_out)
62
+ | (y < geom.r_out)
63
+ | (y > ny - 1 - geom.r_out)
64
+ )
65
+ flags[edge] |= FLAG_EDGE
66
+ for i in range(x.size):
67
+ if _saturated_within(frame.satmask, x[i], y[i], geom.r_ap):
68
+ flags[i] |= FLAG_SATURATED
69
+ return flux, flux_err, bkg, flags
@@ -0,0 +1,41 @@
1
+ """Per-frame re-centroiding of WCS-predicted star positions.
2
+
3
+ Center-of-mass (not a Gaussian fit) because the +1/+2-wave defocus PSFs are
4
+ centrally-depressed donuts; COM in a box sized to the PSF works for both.
5
+ """
6
+
7
+ import warnings
8
+
9
+ import numpy as np
10
+ from photutils.centroids import centroid_com, centroid_sources
11
+
12
+ from .flags import FLAG_CENTROID
13
+
14
+
15
+ def centroid_stars(frame, x0, y0, box):
16
+ """Refine (x0, y0) with a background-subtracted COM in a `box` px window.
17
+
18
+ Returns (x, y, flags); a failed or runaway centroid (>box px from the
19
+ start) falls back to the input position with FLAG_CENTROID set.
20
+ """
21
+ x0 = np.atleast_1d(np.asarray(x0, dtype=float))
22
+ y0 = np.atleast_1d(np.asarray(y0, dtype=float))
23
+ data = frame.image_e - np.median(frame.image_e)
24
+ ny, nx = data.shape
25
+ x_start = np.clip(x0, 0, nx - 1)
26
+ y_start = np.clip(y0, 0, ny - 1)
27
+ with warnings.catch_warnings():
28
+ warnings.simplefilter("ignore") # partial-overlap warnings at edges
29
+ x, y = centroid_sources(
30
+ data, x_start, y_start, box_size=box, centroid_func=centroid_com
31
+ )
32
+ flags = np.zeros(x0.size, dtype=int)
33
+ bad = (
34
+ ~np.isfinite(x)
35
+ | ~np.isfinite(y)
36
+ | (np.hypot(x - x_start, y - y_start) > box)
37
+ )
38
+ flags[bad] |= FLAG_CENTROID
39
+ x[bad] = x0[bad]
40
+ y[bad] = y0[bad]
41
+ return x, y, flags