uvex_transients.simulation.event.Event.simulate_photometry#

Event.simulate_photometry(mission: Mission, bands: list | None = None, n_sigma: float | None = None) → QTable[source]#

Evaluate this event’s detectability at every observation in observations.

Builds one ~synphot.SourceSpectrum for this whole event – batched over every candidate observation’s own time since explosion, via ~uvex_transients.models.core.base.SpectralModel.as_source_spectrum (dust folded in through its log_attenuation, from this event’s own cached ebv) – and reuses it, unmodified, for every requested band: a ~synphot.SourceSpectrum is purely a function of wavelength/time, so “band” only enters once it’s integrated against a bandpass, via ~m4opt.synphot.Detector.get_snr. That turns what used to be a Python loop over every (observation, band) pair, each doing its own scalar SpectralModel.flux/astropy.stats.signal_to_noise_oir_ccd call, into one vectorized get_snr call per band, regardless of how many observations there are.

The reported flux/magnitude at each (observation, band) is then a Gaussian realization of the true (noiseless) flux – evaluated from that same SourceSpectrum at the band’s pivot wavelength – at that SNR’s implied uncertainty; a synthetic measurement, not the ground truth. Physical SED parameters are sampled once, from seed; every band’s noise draws are one vectorized call over all observations at once, in bands order, on the same stream that draw consumed – so the whole event still replays identically given the same seed, but not row-for-row identically to an older, unbatched implementation, since the draws are now grouped per band across every observation rather than interleaved observation-by-observation.

mag_err is the usual linearized (first-order) propagation of flux_err through the magnitude log transform – a good description of the uncertainty only while it’s small relative to flux, i.e. at high SNR. It is not a substitute for a real confidence interval: because magnitude is a nonlinear (logarithmic) function of flux, a symmetric interval in flux is an asymmetric one in magnitude, and that asymmetry grows as SNR drops – comparing against ab_mag/mag_err as if they were a plain Gaussian pull systematically reads as biased at low SNR even when the underlying flux draw has no bias at all. flux_upper/flux_lower and mag_upper/mag_lower are the actual n_sigma interval, built the correct way around: bound flux symmetrically first (where the noise is actually Gaussian), then transform each bound to magnitude separately, rather than propagating one linearized width through the transform.

Parameters:
  • mission (m4opt.missions.Mission) – Supplies the ~m4opt.synphot.Detector (bandpasses, background, …) evaluated against.

  • bands (list of str, optional) – Which of mission.detector’s bandpasses to evaluate. Defaults to every bandpass the detector has.

  • n_sigma (float, optional) – Width, in multiples of flux_err, of the flux_upper/flux_lower/ mag_upper/mag_lower interval. If None (the default), uses config["simulation.detection_n_sigma"] (5 out of the box).

Returns:

One row per (observation, band), sorted by obs_time then band, with columns event_id, obs_time, exptime, band, snr, flux/flux_err (Jy), flux_upper/flux_lower (Jy, flux ± n_sigma*flux_err), ab_mag/mag_err, and mag_upper/mag_lower – the n_sigma interval transformed to magnitude, brighter bound first: mag_lower (from flux_upper) is always finite when flux_upper>0; mag_upper (from flux_lower) is nan whenever flux_lower<=0, i.e. whenever the source isn’t securely distinguished from zero flux at n_sigma – the correct behavior is a one-sided (no faint bound) result there, not a spuriously finite one. flux/flux_err/ab_mag/ mag_err are nan wherever snr is non-positive or non-finite (ab_mag/mag_err are also nan wherever the noisy flux realization itself came out non-positive). Empty (but correctly typed) if observations is empty.

Return type:

astropy.table.QTable