NIGHTSCRIBE

Precision photometry in NightScribe: how it is achieved

Versión en español

This document answers one question: what separates a 0.05 mag measurement from a 0.005 mag one. It serves two readers: the observer, who wants the concepts and to get the most out of their equipment (part one, no mathematics); and an AI or developer extending the implementation (technical appendix at the end). Almost everything described here already exists in NightScribe (phases G and H, 2026-09-23); phase I (same date) added the precision centroid and the aperture suggestion (suggest_apertures plus the Suggest button on the Measure section, right under the three apertures); phase I.5 raised the centroid to the gaussian matched filter (gaussian_centroid: the seeing template on a 0.1 px lattice with parabolic refinement, ~0.01 px with decent signal, honest guards on the faint) with the reticle snapping to the centroid on hover; the exoplanet-transit regime now runs on the signed ADR-015 reopening (2026-09-27), directed by docs/PLANS/series-photometry.md (pieces T1–T8, thirteen phases).

Documentation of the base photometric process: PHOTOMETRY.md. This document is its quality sequel.


Part I: for the observer

1. The two error budgets

Every photometric measurement carries two errors of different nature, and mixing them up is the source of almost every disappointment:

  • Noise is the needle's jitter. Photons arriving at random, a glowing sky, humming electronics. It goes down by collecting photons: longer exposures, more telescope aperture, or averaging measurements. It is honest and predictable.

  • Systematics are a badly tared kitchen scale. They do not jitter: they lie always in the same direction. Optics that illuminate the corners a little less (a badly corrected flat), a filter whose response is not exactly the standard one, a comparison star that turns out to be variable. No amount of averaging fixes them: they are fought only by calibrating against known weights (the comparison stars) and by careful technique.

Precision photometry is, above all, the systematic hunt of the systematics.

2. The quality ladder

From the free to the heroic; each rung "buys" a typical improvement, and almost all of them already live in the FITS editor (Photometry tab, phase H):

RungWhere it livesWhat it buys
Well-reduced plate (bias, darks, flats)you, when stackinga flat field: without it, 1–5 % error depending on position
Comparisons of similar colour to the targetyou, Photometry tabremoves most of the colour term
Aperture that follows the night's seeingPhotometry tab (H3)optimal SNR and night-to-night consistency
Well-estimated sky (robust median or plane)Photometry tab (H2a)the SN stops being measured "galaxy included"
Your camera's real saturation levelPhotometry tab (H4: SATURATE card or ccd_saturate setting)no clipped star sneaks in as a good one
Your camera's real gain (measured on your own frames)Photometry tab + Settings (2026-10-08, ADR-072)the photon term of the error bar: without it the error falls back to the comparison stars' scatter
Colour term fitted with the compsPhotometry tab (H1)your equipment's response stops biasing the zero
Host-galaxy subtraction (SNe)Photometry tab (H2b)on galactic cores: from 0.05–0.15 to 0.03–0.05 mag
Per-frame normalization + detrending (series)Done (2026-09-27): T1–T7 and the fit (T8) in core/series_measure.py, core/transit_fit.py and core/exoclock_export.py; model parity D27 closed (1e-5 vs batman) and the EXOTIC end-to-end gate open on the real photometry. See docs/SEQUENCES.md and docs/PLANS/series-photometry.mdthe transits' requirement: 0.001–0.005 mag relative

3. Three scenarios, honest figures

  • Variable star on a good night, reduced plate: the series flow gives Δmag with 0.02–0.05 mag total precision; the Photometry tab (adaptive aperture, colour term and the check star watching) brings it to 0.01–0.02 mag. Beyond that the colour calibration rules, and going further means measuring your own camera's transformation coefficients on standard fields (another league, noted as v2).

  • Supernova against a galactic core: the enemy is not noise, it is the galaxy. The Photometry tab does both things: the plane-fitted sky already improves the measurement, and with host subtraction (the PS1 reference aligned by the blink is subtracted, scaled so the stars vanish and only the SN remains) you go from 0.05–0.15 to 0.03–0.05 mag.

  • Exoplanet transit (hot Jupiter): the signal is a 0.01–0.02 mag dip lasting hours. Absolute accuracy is not needed; extreme relative stability is: every frame is normalized with its own comparison stars (a thin cloud must not fake a transit), the ensemble is weighted by each star's error, the aperture is chosen to minimize the check star's scatter, and the curve is detrended against air mass always showing the raw one beside it. With that and good practices, an amateur reaches 0.001–0.005 mag per binned point: enough for publishable transit curves. Today EXOTIC does that reduction (original ADR-015); since the signed reopening (2026-09-27) it is done in-house, with EXOTIC as the quality mirror: that is the major work item of docs/PLANS/series-photometry.md.

4. When to trust a number

  • The check star is the traffic light: the Photometry tab measures it as if it were the target and compares it with its catalog value. If it moves, the variable is not to blame: the night, the plate or the sequence is not trustworthy. If only the target moves, that is astrophysics.

  • Saturation with a real level: "almost saturated" does not exist. The ceiling comes from the SATURATE card or the ccd_saturate setting, not estimated from the frame itself.

  • The reported error vs. the total: the panel distinguishes "internal" (photons) from "total" (plus comps' scatter, scintillation, colour and flats). Trusting the first number as if it were the second is the most common mistake.

5. What is what today

PieceStatus
Sub-pixel centroid, aperture, median sky, guardsExists (core/series.py)
Comparison sequences with catalog magnitudes (Gaia/APASS, VSX veto)Exists (the Sequence half of the FITS editor's Photometry tab)
Δmag series with automatic ensemble + SN quick-lookExists (core/series.py, Follow-up flow)
CSV / AAVSO EFF exportExists (core/photometry_export.py)
Calibrated measurement on one plate (zero point from comps)Exists (the Measure half of the FITS editor's Photometry tab, phase G)
Colour term, gradient sky, FWHM aperture, real saturation, total error, check semaphoreExists (phase H, 2026-09-23: core/photometry.py + Photometry tab)
Host-galaxy subtractionExists (phase H: the blink's aligned PS1 reference, comp-scaled)
Per-frame normalized series + detrending for transitsIn progress: pieces T1–T8 in docs/PLANS/series-photometry.md (ADR-015 reopened 2026-09-27; ADR-048 to ADR-051)

6. What really limits a faint series (measured)

On a short exposure the limit is usually not the software, and it is worth knowing which of the two you are fighting. Measured on the author's own 1 s series (2025 FG18, a 0.43 m telescope):

  • the differential scatter between two stars 6 arcmin apart is 19 mmag, and between two stars 30 arcmin apart it is 38 mmag;

  • that growth with separation is the signature of spatially correlated noise: scintillation and atmospheric transparency move whole patches of sky together, so two nearby stars see the same atmosphere and two distant ones do not;

  • the app's own scintillation model (Young, 1967) predicts 131 mmag for 0.43 m and 1 s, which is the same order as what is measured.

Two consequences, and the second is the important one:

  1. No amount of algorithm fixes this. The engine was measured against a photutils-based one on this same data (ADR-067): same precision on single measurements, and on the series a median of 1.8 % of improvement, with one dataset at 9.7 %. The atmosphere was not in the difference.

  2. What does fix it is collecting more light per unit of time: a longer exposure (scintillation falls as 1/sqrt(2t)), a larger aperture (as D^(-2/3)), a lower airmass, or binning the series in time, which trades cadence for precision. A transit that needs 1 mmag per binned point wants the longest exposure that does not smear the ingress.

The practical way to read your own limit: measure two stars of similar brightness at different separations and compare their scatter. If it grows with distance, you are atmosphere-limited; if it does not, you are photon-limited and the exposure or the aperture is what is missing.


Appendix: the implementation's technical reference

Status: pieces H1–H7 are implemented (2026-09-23) in core/photometry.py and gui/ufe_measure_tab.py, with tests in tests/unit/test_photometry.py and test_ufe_measure_tab.py; phase G (calibrated single-plate measurement) lives in docs/PLANS/ufe-photometry.md. Pieces T1–T8 (transits) are in progress in docs/PLANS/series-photometry.md (ADR-015 reopened and signed 2026-09-27). This appendix stays as the reference specification for future extensions.

House rules: the project header on every .py; code in English with # @args:/# @return: comments; every visible string through self.tr(); network only from core/sources/ via core/db.py; offscreen tests with a fixed np.random.default_rng(<seed>); the legacy dialogs are never touched; docs in natural language (the usual style rule: colons, commas and semicolons; the en dash only for numeric ranges).

A. Single-shot improvements (variables, SNe): pieces H1–H7

Existing context, verified 2026-09-22 (do not re-explore):

  • core/series.py: _centroid(data, x, y, half=5), aperture_flux(data, x, y, r_ap, r_ann_in, r_ann_out) -> (flux, sky_pp), instrumental_mag, _is_unsaturated (estimated ceiling: 99.9th percentile × 1.5, or absolute sat_adu), R_AP=6.0, R_ANN_IN=10.0, R_ANN_OUT=15.0, _SAT_FRAC=0.85.

  • core/compstars.py: stars carry bands (APASS: direct B, V; Gaia: G plus derived B, V, Rc, Ic) and star["bv"] (direct or estimated; color_origin says which).

  • core/blink.py:prepare_pair returns the PS1-g reference aligned to the plate's geometry (same centre/scale/rotation, up to WORK_MAX=2048): the base of the subtraction.

  • core/phototrans.py, core/photometry_export.py (EFF with CNAME/CMAG/KNAME/KMAG).

The pieces, in impact-per-effort order:

  • H1. Colour term: weighted fit of (V_cat − v_inst) = ZP + k·(B−V) over the comps (≥6 with colour spread; MAD-clip the residuals before quoting ZP±err); apply with the target's B−V (VSX for variables; SN: B−V≈0 assumed with a visible warning, or report untransformed). Pure numpy. Test: synthetic comps with a known slope are recovered; without colour spread the slope reports as undetermined and nothing breaks.

  • H2. Sky on a galactic core, two levels: (a) a plane fitted to the annulus pixels (after 2.5σ sigma-clipping) evaluated at the star's position, instead of the flat median; (b) host subtraction: take the pair from core/blink.prepare_pair, register the reference onto the frame's pixel grid as a similarity (scale + rotation + translation) on the stars the two share (core/register: the WCS alone is not enough, because the SIP terms are ignored and, above all, the cutout's scale differs from the real one by a fraction of a percent, leaving a dipole at every star), exclude the survey's masked pixels (they arrive as NaN: filled so the resampling can cross them, then excluded again from the difference), match the PSF (core/difference, ADR-073: the observation is broader than the survey, so the reference is broadened with the difference Gaussian or with a regularized optimal kernel of the Alard-Lupton kind, and the one that leaves the least residual on the stars wins), scale the reference by the flux ratio of the comps present in both images (the comps must vanish in the difference; their mean residual is the scaling criterion), subtract, and measure the SN on the difference image. The PS1-g reference is not calibrated to the user's filter: the comp fit absorbs that, and the docs must say so. When the registration is not reliable the subtraction still runs and the panel warns. Tests: a shifted synthetic pair + a NaN hole; the registration lowers the residual, the hole is not painted and the measurement recovers the target's flux.

  • H3. FWHM-based aperture: FWHM measured from bright unsaturated comps (second-order moments after sky subtraction); r_ap = k·FWHM with k ∈ [1.2, 1.6], default 1.35; the annulus scales in proportion. Test: two synthetic seeings give different apertures and the measured SNR improves over the fixed one.

  • H4. Real saturation: ceiling from the SATURATE card or a ccd_saturate settings key; fallback to the current estimator with a warning. Test: a star at 95 % of the ceiling is flagged and not measured.

  • H5. Honest total error: CCD equation (gain/RON resolved in the order of ADR-072: Settings → measurement on the frames → remembered → header) + scintillation (Young 1967: σ ∝ D^(−2/3) · X^1.75 · t^(−1/2) · e^(−h/8 km); site parameters in Settings with sensible defaults) + covariance of the ZP+colour fit + a configurable flat-residual floor (flat_resid_mag, default 0.007). The panel distinguishes "internal error" from "total error" and says where the gain came from. Test: the total is never below the internal one; without gain it degrades with a warning.

  • H6. Check semaphore: measure the check star on the same plate and compare with the catalog; |Δ| > 2.5·σ_total flags the measurement as "unreliable" with a plain-language explanation. Test: a plate with a simulated cloud (global flux scale) trips the semaphore.

  • H7. Outlier rejection in the ZP: MAD-clip of residuals (already cited in H1; its own piece if H1 is skipped). Test: one corrupted comp does not move the median.

Everything lands in core/photometry.py (the phase-G module) and in the Photometry tab's panel; nothing touches the legacy flows.

B. Exoplanet transits: pieces T1–T8

Decision note (read first): signed 2026-09-27. ADR-015 is reopened: transit reduction happens in-house (pure numpy; no scipy, no astropy per ADR-004), with EXOTIC as the quality mirror and its inits.json handoff untouched as the expert path for the heavy stack. T1–T8 live in docs/PLANS/series-photometry.md (register D1–D39, thirteen phases, EXOTIC parity gate in phase 7, fixed thresholds).

  • T1. Per-frame zero-point series: every image is normalized with the comps measured on THAT image (extinction and thin clouds stop faking signal). Reuses core/series.load_series/measure_series, swapping the ensemble's global mean for a per-frame ZP.

  • T2. Weighted ensemble: error-weighted mean per comp (or flux sum, AIJ's "superstar"), with a per-frame outlier veto (per-frame MAD).

  • T3. Optimal aperture per night: sweep k in [1.0, 2.0]×FWHM keeping the one that minimizes the check star's rms; per-frame FWHM (small guiding wobbles no longer break the curve).

  • T4. Full noise: H5 plus cadence; the per-point error must include scintillation (dominant in fast-cadence short exposures).

  • T5. Honest detrending: against air mass at minimum; optionally against FWHM, sky level, centroid x/y (flat residuals). Golden rule: the raw curve always visible next to the detrended one, and the user docs explain that detrending can eat signal.

  • T6. Mid-exposure HJD: hjd_of already corrects to the Sun; add EXPTIME/2 to the DATE-OBS card instant (a clearly documented convention).

  • T7. Per-frame quality gates: saturation (H4), guiding jump (centroid shifted beyond a threshold), cosmic ray in the aperture (local sigma-clip), outlier ZP (cloud) flagged, not deleted.

  • T8. Observing practices: a user-doc section (PHOTOMETRY): flats mandatory at mmag level, dithering, deliberate slight defocus, constant cadence, nothing saturated.

Validation: a seeded synthetic series with a known 0.01 mag dip recovered to ±0.001 mag; and, once real data exists, a known transit (e.g. HD 209458 b) with the depth recovered within 10 % and the check star's rms inside what the noise model predicts.

C. Global acceptance criteria

  1. Every piece arrives with seeded unit tests and the full suite green (.venv/bin/python -m pytest tests/unit).

  2. The user docs (PHOTOMETRY + PRECISION + SEQUENCES, both languages) explain the new piece with an example; i18n with no unfinished strings.

  3. The legacy flows (series quick-look, blink, legacy chart, EXOTIC handoff) stay green untouched.

  4. The reported total error is never smaller than the internal one; when a datum is missing (gain, saturation, B−V), the panel says so in plain language instead of staying silent.