Scattered-light halo (wcc_sim.scatter)

Scattered-light halo as an azimuthally averaged radial profile.

The Lazuli stray-light budget comes from a FRED ray trace of the telescope at 450 nm, which wcc_etc ships and combines with a diffraction core in wcc_etc.scatter_psf.make_total_psf:

total = (1 - f_scat) * core + f_scat * scatter / P_full

f_scat is the instrument-wide scattered fraction – a property of the optics, not of any stamp. FRED quotes it two ways: P_FULL = 5.542e-3 per watt launched at the aperture, and F_FOCAL = 7.13e-3 per watt reaching the image plane; they differ by the mirror throughput (0.777). A unit-sum PSF describes detected photons, so wcc-sim uses F_FOCAL by default and records the choice as SCATNORM in the FITS header. wcc_etc.scatter_psf.make_total_psf still uses P_FULL, which understates the halo by 22%; pass frac_total=table.meta["P_FULL"] to reproduce it.

Why a 1D profile and not a 2D kernel

The FRED grid has 0.4 mm cells, which at the WCC’s 3.76 um pixels is 106 detector pixels per cell, and the map is azimuthally symmetric to 0.3-2% rms at every radius from 1 mm to 130 mm. So the halo carries no azimuthal information worth keeping and is intrinsically smooth on a ~100 px scale: a radial profile reproduces it to within its own symmetry scatter, in ~10 kB instead of the ~10^9 pixels a full-plane 2D kernel would need. Convolving with such a kernel would also cost more than the rest of the simulation put together, and would forfeit the sub-pixel star placement that wcc_sim.render.add_star() gets from the oversampled core stamp.

The halo is therefore added as a second, additive term rather than spliced onto the core: no join radius, no crossfade, and no amplitude step to reconcile. It reaches to 138 mm = 36,800 detector pixels, well beyond the 11,500 px chip diagonal, so a star in one corner of the array still deposits a correct halo in the opposite corner.

The profile keeps FRED’s absolute surface brightness and is truncated at that reach rather than renormalized to unit energy. Renormalizing would inflate the halo by ~1/0.875, corrupting the far-field brightness that is the whole reason for using the map; and extrapolating a power law past the edge invents 41% of a unit of flux the map does not contain. The 12.5% of the scattered power that lies outside the modelled disc sits in the FRED map’s long-axis corners, 37,000+ px off-axis – off any detector, so it is not deposited and not counted.

The FRED run is monochromatic at 450 nm and the halo is treated as achromatic here. Real surface-scatter BRDF falls roughly as lambda^-2, so the halo is over-predicted in redder bands; WAVELEN is recorded in the profile metadata so that can be revisited.

wcc_sim.scatter.PROFILE_PATH = '/home/docs/checkouts/readthedocs.org/user_builds/schmidt-observatories-wcc-sim/checkouts/latest/src/wcc_sim/data/psfs/lazuli_scatter_450nm_profile.ecsv'

Packaged azimuthal profile, built from the FRED map by scripts/build_scatter_profile.py. The 24 MB .fgd and the 75 MB combined-PSF FITS are deliberately not in the repo: runtime needs neither.

class wcc_sim.scatter.ScatterHalo(r_px, frac_per_px, frac_total, meta=<factory>)[source]

Bases: object

Azimuthally averaged scattered-light halo on detector pixels.

Parameters:
  • r_px (ndarray) – Ascending radii [detector px] at which the profile is tabulated.

  • frac_per_px (ndarray) – PSF fraction per detector pixel at r_px – already carrying the scattered fraction, so it is directly comparable with wcc_sim.wings.WingModel.profile().

  • frac_total (float) – The instrument-wide scattered fraction this profile represents. Stored as metadata; energy_beyond() integrates the table itself rather than trusting it.

  • flat (Inside r_px[0] the profile is held)

  • across (detector pixels)

  • halo (divergence. It does not matter -- the diffraction core outruns the)

  • core (near the)

  • a (and extrapolating a power law inward would invent)

  • halo

  • there. (by ~10^9)

  • meta (dict)

r_px: ndarray
frac_per_px: ndarray
frac_total: float
meta: dict
profile(r)[source]

PSF fraction per detector pixel at radius r [px]; 0 past the edge.

energy_beyond(r)[source]

PSF fraction of the halo lying outside radius r [px].

r_out(flux_e, floor_e)[source]

Radius [px] where flux_e * profile(r) falls to floor_e.

Returns 0.0 when even the halo’s peak is below the floor, so the caller simply draws nothing.

scaled(frac_total)[source]

Same halo shape, renormalized to a different scattered fraction.

The result’s meta["SCATNORM"] is "USER": the fraction no longer matches the F_FOCAL / P_FULL value the table quoted.

wcc_sim.scatter.build_scatter_table(fgd_path=None, n_bins=300)[source]

Azimuthally average a FRED .fgd stray-light map into a radial table.

Reads the map through wcc_etc’s own read_fgd / fgd_grid so the two packages cannot drift apart on the grid convention (FRED’s axis limits are cell edges, so samples sit at cell centers).

Only annuli that lie entirely inside the map are used. The map is rectangular (+/-335 x +/-140 mm), so averaging over partial annuli beyond the short half-axis and then multiplying by a full 2*pi*r biases the integral high – by 13% when measured against FRED’s exact 2D integral.

Returns:

Columns r_mm and frac_per_mm2 (halo fraction per mm^2, normalized so the profile integrates to 1 over the plane), with the provenance and the pre-normalization diagnostics in .meta.

Return type:

astropy.table.Table

wcc_sim.scatter.load_scatter_table(path=None)[source]

Read the packaged azimuthal scatter profile.

wcc_sim.scatter.scatter_halo(mm_per_pixel, frac_total=None, table=None)[source]

A ScatterHalo on the pixel grid of a detector.

Parameters:
  • mm_per_pixel (float) – Detector pixel pitch at the focal plane [mm]; sets the conversion from the profile’s physical units to fraction per detector pixel.

  • frac_total (float, optional) – Instrument-wide scattered fraction. Defaults to F_FOCAL from the profile metadata (7.13e-3 for the packaged profile): scattered power as a fraction of the power reaching the image plane, which is what a unit-sum PSF of detected photons needs. table.meta["P_FULL"] (5.542e-3, per watt launched at the aperture) is the wcc_etc convention. meta["SCATNORM"] records which one was used (F_FOCAL, P_FULL or USER).

  • table (astropy.table.Table, optional) – A profile table; defaults to the packaged one.

wcc_sim.scatter.halo_for_sensorfilter(sensorfilter, frac_total=None, table=None, sim=None)[source]

A ScatterHalo on the pixel grid of a wcc_etc sensorfilter.