synth op• Data kinds: none → image2d (an op determined by its arguments alone — it takes no image or data input)
• Call: import fullseye as fs; fs.ledger.synth_starfield(shape=(128, 128), n_stars=30, flux_min=400.0, flux_max=9000.0, fwhm_px=3.2, psf='gaussian', moffat_beta=2.5, sky=60.0, read_sigma=6.0, shift_row=0.0, shift_col=0.0, n_cosmic=0, cosmic_flux=4000.0, margin_px=6.0, seed=0, field_seed=None, noise=True) (to call the implementation directly, import astrostack; astrostack.synth_starfield(shape=(128, 128), n_stars=30, flux_min=400.0, flux_max=9000.0, fwhm_px=3.2, psf='gaussian', moffat_beta=2.5, sky=60.0, read_sigma=6.0, shift_row=0.0, shift_col=0.0, n_cosmic=0, cosmic_flux=4000.0, margin_px=6.0, seed=0, field_seed=None, noise=True); from the registry, opsastrostack.get("synth_starfield"))
• Return value through the ledger: fullseye.ledger.synth_starfield(...) returns only the declared out type image2d (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.synth_starfield.raw(...) or call astrostack.synth_starfield directly.
• Underlying return: (frame, truth) -> image2d
Synthesise one known star field — the source of ground truth for this family.
Each star draws its total flux (electrons) from a log-uniform distribution over flux_min–flux_max and is placed uniformly at least margin_px inside the image border. When *psf* is "gaussian", it is an exact integral over each pixel via scipy.special.erf, so the sum of 1 star matches the given flux exactly (apart from the part falling outside the image). "moffat" is (1 + (r/alpha)^2)^(-beta) from Moffat, *A Theoretical Investigation of Focal Stellar Images*, A&A 3, 455 (1969), the standard model of star images dominated by the ground-based atmosphere (its diffraction-limited sibling is optics.airy_pattern).
The noise follows just 1 theory —— Poisson(star + sky) (calling photoncount.photon_sample with photons_per_unit=1.0), after which cosmic rays are added, and finally additive Gaussian read noise read_sigma. The order follows the physics:
cosmic rays are not photons (they do not go through Poisson sampling). The positions of cosmic rays are left to the uniform point process of defectgen.defect_pits —— "rare, small, sharp deposits at random positions" has the same stochastic geometry as pitting corrosion, so there is no reason to write a second model.
*shift_row* / *shift_col* move the whole star field by a subpixel amount (dithering). The synthesized ground-truth coordinates move by the same amount, so it can be used to cross-check registration and drizzle.
Returns (frame, truth):
• frame —— (H, W) float64, in units of electrons.
• truth —— dict. rows / cols (N,) are the true star centres (convention: integer coordinates are pixel centres), fluxes (N,) the true total fluxes, fwhm_px / sigma_px / alpha_px / beta the PSF, sky / read_sigma the noise, cosmic_mask (H, W) bool the cosmic-ray pixels, noiseless (H, W) the expectation before noise is added (for cross-checks).
There are 2 seeds. field_seed determines the star field (coordinates and fluxes), seed determines that particular observation (shot noise, read noise, cosmic-ray positions). With field_seed=None it takes the same value as seed. The reason for separating them is an accident found by measurement: in the first version, which had 1 seed, synth_frame_series changed seed per frame, with the result that the star field itself was different in every frame, registration found only 1 correspondence (frame_align correctly failed closed), and cross-frame cosmic-ray removal returned "every pixel is an outlier". Re-imaging the same sky and imaging a different sky can be confused by 1 argument —— so they are separated by name, not by type. Both random generators are numpy.random.default_rng, so the same pair of seeds gives the same frame on any machine.
Ground truth it reproduces (pinned in tests/test_astrostack.py):
for a 1-star frame with noise=False, sky=0, fwhm_px=3.0 (64x64, flux 5000 e-), the relative error between the sum over the whole image and the given flux is 1.8e-16 —— 1 float64 rounding's worth: not "nearly conserved" but conserved. The fraction collected by a circular aperture of radius r is 1 - exp(-r^2/(2 sigma^2)):
0.8647 for r = 2 sigma, 0.98889 for r = 3 sigma.
Raises ValueError: *shape* too small / *n_stars* not a non-negative integer / flux_min > flux_max / *psf* not in PSF_MODELS / moffat_beta <= 1 (the integral diverges) / margin_px larger than the image / *seed* not a non-negative integer.
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• The canonical algorithm (author, year) and its uses are named in the family usage guide above.
• astro_stacking — py -3.11 examples/astro_stacking.py
• poc_astro_photometry — py -3.11 examples/poc_astro_photometry.py
image2d as input)frame_quality · noise_sigma · cosmic_ray_reject · star_detect · psf_fit · aperture_photometry · frame_align
synth)*Provenance: astrostack.py — ASTROSTACK operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.