Lazuli telescope (prysm)#

The Lazuli space telescope (UASAL; its configuration packages call it “Ultramarine”, UM), computed with prysm.

[[coronagraph]]
type = "lazuli"
charge = 0                  # 0: the telescope alone; even >= 2: a vortex mask and a Lyot stop
wavelengths = ["632 nm"]    # "auto": [observation] wavelength
throughput = "auto"         # weights from the mirror, filter and detector curves of the configs

The telescope and its wide-field coronagraph camera (WCC) come from the UASAL configuration packages, read once (lazuli_parameters): config_um (focal length f_eff, mirror coatings) and config_um_wcc (pupil sampling, beam size on M1, detector, filter). The pupil (pupil()) is a circle of diameter D = the beam size on M1 (3.0 m) with an anti-aliased (gray) edge, filling pupil_samples x pupil_samples samples (x along the columns, y along the rows, centred on the array). OPD: an optional wavefront error map (opd) adds Lazuli’s aberrations. Without one (the default) the optics are ideal: a perfect telescope and, with a charge, an ideal coronagraph. The mirror OPD maps of the configs ($UASAL_ARCHIVE) are not bundled.

A point source at angle (theta_x, theta_y), one wavelength at a time (LazuliBackend.source):

pupil field   A exp(2 pi i (OPD + theta_x x + theta_y y) / lambda);
coronagraph   charge >= 2: an ideal vortex (prysm's multi-resolution focal-plane propagation)
              then a circular Lyot stop of lyot_mask_size D; charge 0: nothing;
image         prysm's matrix DFT (MDFT) onto the image pixels; a pixel is pixel_scale scene
              lambda/D, i.e. pixel_scale (wavelength0 / diameter0) f_eff in the focal plane
              (only the angle matters).

The image is the weighted sum over the wavelengths. The image size must be odd: the star then sits on the central pixel, which is prysm’s centre n//2 as well as toycoronagraph’s (n-1)/2, so the images compare with prysm’s pixel by pixel. Intensities follow toycoronagraph.optics: each pixel holds |field|^2 / sum(A^2), the fraction of the source’s light entering the aperture (prysm’s executor is unitary), so an unocculted source sums to 1 over the whole focal plane at every wavelength. The focal field of P pupil samples repeats every P lambda/D (Lazuli’s, at each wavelength): every source is repeated P lambda/D away along x and y. optics_from_table refuses a sampling whose period at the shortest wavelength does not clear the PSF frames (pupil samples are cheap; aliasing is not).

The Lazuli team’s get_psf is here too (get_psf, on prysm 0.22, its inputs from tele_data), with the wavelengths in um, prysm’s unit (their version multiplies them by 1e-3, so it needs nm). It is charge 0 of the backend:

tele_data['Aperture'], ['dx'], ['fnum']   pupil(), its sample D / pupil_samples, f_eff / D
tele_data['OPD']                          opd (a map in nm)
tele_data['Pixsize'] / oversample         the image pixel: [grid] pixel_scale (detector_pixel_scale()
                                          gives the WCC detector's)
tele_data['Wavelengths']                  wavelengths: um there; m here, or text ("0.632 um", "632 nm")
tele_data['Throughput']                   throughput ("auto": spectral_weights)
shift                                     moves get_psf's focal grid by +shift; here the source moves
                                          instead, to -shift

get_psf normalises each wavelength’s PSF and the weighted sum to 1 over its own image; the backend normalises each wavelength to the light entering the aperture, which keeps the light falling outside the image (and the coronagraph’s throughput).

This is the only module importing prysm and the config packages; toycoronagraph imports it when a “lazuli” coronagraph is used (Optics.lazuli names it as its module).

toycoronagraph.lazuli.lazuli_parameters()[source]#

The Lazuli telescope and WCC parameters, read once from config_um and config_um_wcc.

Returns:

  • diameter (float): pupil diameter D in m: the beam size on M1 (config_um_wcc E2ES_default.optics.m1.beam_size; M1’s clear aperture is larger, 3.065 m).

  • focal_length (float): effective focal length in m (config_um telescope.general.f_eff, at the centre of the field; config_um’s f_number = 15 is a provisional design requirement).

  • pupil_samples (int): pupil samples across D (config_um_wcc E2ES_default.simulation.beam_sampling).

  • pixel_size (float): WCC detector pixel size in m (config_um_wcc common_params.sensor.pixel_size).

  • curves (dict): {name: (wavelengths (n,) float64 in m, values (n,) float64)} for “m1” (aluminium reflectance), “m2”, “m3”, “m4” (silver reflectance), “filter” (SDSS r’) and “qe” (detector).

Return type:

SimpleNamespace

toycoronagraph.lazuli.detector_pixel_scale()[source]#

The angular size of a WCC detector pixel, pixel_size / focal_length (config_um’s f_eff = 42.047 m: 0.018445 arcsec): e.g. for [grid] pixel_scale. Inside the backend the focal length cancels (only angles matter).

Returns:

arcsec per detector pixel.

Return type:

float

toycoronagraph.lazuli.spectral_weights(wavelengths)[source]#

The relative weight of each wavelength through Lazuli and the WCC: M1 aluminium x M2-M4 silver reflectance x SDSS r’ filter x detector QE, interpolated at the wavelengths (0 outside a curve), times the width of the band each wavelength stands for (halfway to its neighbours, as far again beyond the first and last: np.gradient of the sorted wavelengths). The weighted sum is then the band integral however the band is sampled; evenly spaced wavelengths get the curves’ product alone.

Parameters:

wavelengths (sequence of float) – wavelengths in m, in any order (one wavelength: weight 1).

Returns:

(n,) float64 weights, summing to 1.

Return type:

np.ndarray

Raises:

ValueError – no light at any of the wavelengths (all outside the r’ band, about 540-730 nm).

toycoronagraph.lazuli.pupil(samples)[source]#

The Lazuli pupil: a circle of diameter D (the beam size on M1) with an anti-aliased (gray) edge, centred on the array (so the circle touching its edges is symmetric; prysm’s grids put 0 at n//2).

Parameters:

samples (int) – Pupil samples across D.

Returns:

x, y ((P, P) float64 pupil coordinates in mm; x along the columns, y along the rows) and the aperture ((P, P) float64 amplitude, 0 to 1).

Return type:

tuple

toycoronagraph.lazuli.read_opd(path)[source]#

An OPD (wavefront error) map over the pupil diameter.

Parameters:

path (str) – a FITS file (primary HDU, in its BUNIT, converted with astropy; nm without BUNIT) or a .npy file (nm). The map is square, its rows along y and its columns along x.

Returns:

(n, n) float64 OPD in nm (NaN, e.g. outside the pupil, set to 0).

Return type:

np.ndarray

Raises:

ValueError – the map is not square and 2-D, or its BUNIT is not a length.

toycoronagraph.lazuli.pupil_samples(opd, requested='auto')[source]#

The number of pupil samples across the aperture.

Parameters:
  • opd (str) – the OPD map file; “” for none.

  • requested (int or str) – [[coronagraph]] pupil_samples; “auto”: the OPD map’s size, else the config’s.

Returns:

the OPD map’s size if there is one, else requested, else config_um_wcc’s beam_sampling.

Return type:

int

Raises:
  • ValueError – requested differs from the OPD map’s size, or the map is not valid (see read_opd).

  • OSError – the OPD map cannot be read.

toycoronagraph.lazuli.optics_from_table(values, img_pixel, psf_range, wavelength, diameter, size_fix)[source]#

The Optics of a “lazuli” [[coronagraph]] table, its “auto” settings resolved.

Parameters:
  • values (dict) – the table’s values, every key of params.CORONAGRAPHS[“lazuli”] present (defaults filled in): charge, lyot_mask_size, wavelengths (list in m or “auto”), throughput (list or “auto”), opd (str), pupil_samples (int or “auto”).

  • img_pixel (int) – Number of pixels on one edge of the image (odd).

  • psf_range (float) – Half-width of the image, in scene lambda/D.

  • wavelength (float) – The scene’s wavelength in m ([observation] wavelength; “auto” wavelengths use it).

  • diameter (float) – The scene’s telescope diameter in m ([observation] diameter).

  • size_fix (str) – How the user makes the image size odd, with two %d for the odd sizes around img_pixel, e.g. “set [grid] size = %d or %d (the image is resampled onto it)”.

Returns:

see Optics.lazuli.

Return type:

Optics

Raises:

ParamError – img_pixel is even, or the focal field’s period (pupil samples x Lazuli’s lambda/D at the shortest wavelength) does not clear the PSF frames: the farthest pixel of a star-centred radial frame (psf.extended_frame_size) from a source at the image edge, (N-1)/2 + (M-1)/2 pixels.

Warns:
  • SamplingWarning – fewer than MIN_SAMPLING x 8/3 pupil samples per lambda/D of the image’s half-width (the PSFs’ far wings come out too low near the image edge; see optics.MIN_SAMPLING).

  • ValueError – “auto” throughput with no light at the wavelengths (spectral_weights), an OPD map that is not square and 2-D or whose BUNIT is not a length (read_opd), or pupil_samples differing from the OPD map’s size.

  • OSError – the OPD map cannot be read (e.g. FileNotFoundError).

class toycoronagraph.lazuli.LazuliBackend(optics)[source]#

Bases: object

The Lazuli telescope, alone (charge 0) or with an ideal vortex coronagraph (see the module docstring).

A source enters only through the tilt across the pupil, whose width is D: for a fixed image pixel the field is a Fourier integral over the pupil, so the intensity is band-limited in the source angle at D / lambda cycles per radian, i.e. (D / diameter0) (wavelength0 / lambda) cycles per scene lambda/D. Without an OPD map, the pupil and the stops are circles: rotationally symmetric PSFs.

Variables:
  • optics (Optics) – kind ‘lazuli’ (see Optics.lazuli).

  • rotationally_symmetric (bool) – True without an OPD map.

  • bandlimit (float) – (D / diameter0) (wavelength0 / shortest wavelength), cycles per scene lambda/D.

  • lambda_d (float) – the scene’s lambda/D, wavelength0 / diameter0, in radians.

  • pupil_dx (float) – pupil sample, in mm.

  • efl (float) – focal length (config_um’s f_eff), in mm.

  • um_per_lambda_d (float) – focal-plane microns per scene lambda/D.

  • x,​y (np.ndarray) – (P, P) float64 pupil coordinates in mm (x along the columns, y along the rows).

  • aperture (np.ndarray) – (P, P) float64 circle of diameter D (anti-aliased edge).

  • opd (np.ndarray) – (P, P) float64 OPD in nm (zeros without a map).

  • lyot_stop (np.ndarray or None) – (P, P) float64 circle of diameter lyot_mask_size D; None for charge 0.

  • entering (float) – sum(aperture^2), the light entering the aperture (the intensity unit).

  • lyot_light (float) – sum((aperture lyot_stop)^2), a source’s light after the Lyot stop without the vortex (the throughput’s reference; for charge 0, entering).

  • vortex (callable or None) – prysm’s vortex_phase_mask(charge); None for charge 0.

  • channels (list) – per wavelength, (wavelength in um, weight, multi-resolution vortex executor or None).

  • frame_executors (list) – per wavelength, the MDFT onto the whole image.

Parameters:

optics (Optics) – An Optics of kind ‘lazuli’ (see Optics.lazuli).

executor(wavelength, center, shape)[source]#

prysm’s MDFT from the pupil onto a block of image pixels.

Parameters:
  • wavelength (float) – in um.

  • center (tuple) – (x, y) of the block’s centre, in scene lambda/D from the star.

  • shape (tuple) – (ny, nx) size of the block, in pixels.

Returns:

its output pixel (row, col) is at x = center_x + (col - (nx-1)/2) pixel_scale, y = center_y + (row - (ny-1)/2) pixel_scale.

Return type:

prysm.fttools.MDFT

source(x, y)[source]#

A point source at (x, y) scene lambda/D, through the telescope and the coronagraph.

Parameters:
  • x (float) – Source position, in scene lambda/D from the star (x along columns, y along rows).

  • y (float) – Source position, in scene lambda/D from the star (x along columns, y along rows).

Returns:

Its exit fields (after the Lyot stop with a charge), their total light and the throughput.

Return type:

LazuliSource

unocculted()[source]#

The star without the vortex (the Lyot stop kept when there is a charge): the reference of the contrast_peak unit.

Returns:

The star through the telescope (and the Lyot stop); throughput 1.

Return type:

LazuliSource

class toycoronagraph.lazuli.LazuliSource(backend, fields)[source]#

Bases: object

A point source through the Lazuli telescope: its fields leaving the last pupil, one per wavelength, focused onto any block of the image pixel grid.

Variables:
  • total (float) – Total light over the whole focal plane (Parseval: the exit fields’ light, weighted over the wavelengths), in the package’s units.

  • throughput (float) – The source’s light leaving the system relative to the same source without the vortex (1 for charge 0).

Parameters:
  • backend (LazuliBackend) – The backend (channels, entering, lyot_light, executor()).

  • fields (list) – Per wavelength, the (P, P) complex128 field leaving the last pupil.

block(center, shape)[source]#

Intensity on a block of the image pixel grid (pixel size optics.pixel_scale).

Parameters:
  • center (tuple) – (x, y) of the block’s centre, in scene lambda/D from the star.

  • shape (tuple) – (ny, nx) size of the block, in pixels.

Returns:

(ny, nx) float64 intensity (rows = y, columns = x), in the package’s units.

Return type:

np.ndarray

frame()[source]#

np.ndarray: (img_pixel, img_pixel) float64 intensity on the whole image.

toycoronagraph.lazuli.tele_data(wavelengths, opd='', samples='auto')[source]#

The inputs of get_psf for Lazuli, from the same configuration values as the backend.

Parameters:
  • wavelengths (sequence of float) – Wavelengths in um (prysm’s unit), e.g. [0.632].

  • opd (str) – An OPD map file (see read_opd); “” for none (ideal optics).

  • samples (int or str) – Pupil samples across D; “auto”: the OPD map’s size, else the configuration’s.

Returns:

Aperture ((P, P) float64 amplitude, pupil()), OPD ((P, P) float64, nm), Wavelengths (list of float, um), dx (float, pupil sample in mm), fnum (float, f_eff / D), Pixsize (float, WCC detector pixel in um), Throughput ((n,) float64 weights, spectral_weights).

Return type:

dict

toycoronagraph.lazuli.get_psf(tele_data, oversample=1, shift=(0, 0), samples=256)[source]#

The Lazuli team’s get_psf on prysm 0.22 (prepare_executor + focus_dft replace focus_fixed_sampling; numpy instead of cupy), with tele_data[‘Wavelengths’] in um (their version multiplies the wavelengths by 1e-3, so it needs them in nm).

Parameters:
  • tele_data (dict) – Aperture ((P, P) amplitude), OPD ((P, P) nm), Wavelengths (list, um), dx (pupil sample, mm), fnum (focal length / (dx P)), Pixsize (focal-plane pixel, um), Throughput (a float, or one weight per wavelength); see tele_data().

  • oversample (int) – Pixels per Pixsize.

  • shift (tuple) – (x, y) shift of the focal grid, um.

  • samples (int) – Pixels on one edge of the image.

Returns:

(samples, samples) float64 PSF (rows y, columns x; the star, for shift (0, 0), at pixel samples // 2): each wavelength’s PSF normalised to 1 over the image, weighted by the throughput, and the sum normalised to 1.

Return type:

np.ndarray