Skip to content

nirwals.physics.exposure

Functions for signal-to-noise ratio calculations.

detection_rates(area, grating_angle, grating_constant, observation)

Return the count rates of an observation, binned for the wavelength resolution.

The function bins the observation's wavelength bins together, so that the new bins cover the wavelength resolution element as tightly as possible. For example, if each observation wavelength bin covers 5 A and the wavelength resolution element is 23 A, then the new bins will consist of 5 of the original bins each. The first five original bins become the first new bin, the sixth to tenth original bin become the second new bin, and so on.

The wavelengths corresponding to the new bins are taken to be the midpoints of the new bins.

The flux in each new bin is integrated and multiplied by the given area. These rates are returned together with the new wavelengths.

This function assumes that the bin set of the observation is equidistant, and that each wavelength bin corresponds to the wavelength range covered by a pixel.

The rates of the first and last bin should be considered to have arbitrary values, as they are affected by boundary effects.

Parameters:

Name Type Description Default
area Quantity

Effective mirror area.

required
grating_angle deg

Grating angle.

required
grating_constant Quantity

The grating constant, i.e. the spacing between grooves.

required
observation Observation

Observation.

required

Returns:

Type Description
tuple

A tuple of wavelengths and corresponding rates.

Source code in nirwals/physics/exposure.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def detection_rates(
    area: Quantity,
    grating_angle: u.deg,
    grating_constant: Quantity,
    observation: Observation,
) -> tuple[Quantity, Quantity]:
    """
    Return the count rates of an observation, binned for the wavelength resolution.

    The function bins the observation's wavelength bins together, so that the new bins
    cover the wavelength resolution element as tightly as possible. For example, if each
    observation wavelength bin covers 5 A and the wavelength resolution element is
    23 A, then the new bins will consist of 5 of the original bins each. The first five
    original bins become the first new bin, the sixth to tenth original bin become the
    second new bin, and so on.

    The wavelengths corresponding to the new bins are taken to be the midpoints of the
    new bins.

    The flux in each new bin is integrated and multiplied by the given area. These rates
    are returned together with the new wavelengths.

    This function assumes that the bin set of the observation is equidistant, and that
    each wavelength bin corresponds to the wavelength range covered by a pixel.

    The rates of the first and last bin should be considered to have arbitrary values,
    as they are affected by boundary effects.

    Parameters
    ----------
    area: Quantity
        Effective mirror area.
    grating_angle: Angle
        Grating angle.
    grating_constant: u.micron
        The grating constant, i.e. the spacing between grooves.
    observation: Observation
        Observation.

    Returns
    -------
    tuple
        A tuple of wavelengths and corresponding rates.
    """

    wavelengths = observation.binset
    wavelength_values = wavelengths.to(u.AA).value
    flux_values = observation(wavelengths).to(units.PHOTLAM).value
    delta_lambda = wavelengths[1] - wavelengths[0]
    delta_lambda_value = delta_lambda.to(u.AA).value
    pixel_flux_values = _bin_integrals(wavelength_values, flux_values)

    # Find the binning so that each bin covers the wavelength resolution element as
    # tightly as possible.
    binning = 1
    wre = wavelength_resolution_element(
        grating_angle=grating_angle, grating_constant=grating_constant
    )
    while binning * delta_lambda < wre:
        binning += 1

    # Get the wavelengths for the bin wavelengths and the fluxes in the bins.
    bin_wavelength_values: list[float] = []
    bin_flux_values: list[float] = []
    pixel = 0
    while pixel < len(wavelengths):
        # Get the wavelength in the middle of the bin. The current pixel is the first in
        # the bin and the distance between the midpoints of the bin's outer pixels is
        # the binsize less one pixel.
        bin_wavelength_value = (
            wavelength_values[0] + (pixel + 0.5 * (binning - 1)) * delta_lambda_value
        )
        bin_wavelength_values.append(bin_wavelength_value)

        # Add up the fluxes of the pixels in the bin. If we are running out of pixels
        # for the last bin, we assume a flux of 0 for the "missing" pixels.
        bin_flux_value = 0
        for _ in range(binning):
            pixel_flux = (
                pixel_flux_values[pixel] if pixel < len(pixel_flux_values) else 0
            )
            bin_flux_value += pixel_flux
            pixel += 1
        bin_flux_values.append(bin_flux_value)

    # Add the units, and convert fluxes to rates.
    bin_wavelengths = np.array(bin_wavelength_values) * u.AA
    rates = np.array(bin_flux_values) * area * u.AA * units.PHOTLAM

    # Returns wavelengths and rates.
    return bin_wavelengths, rates

electrons(area, exposures, exposure_time, grating_angle, grating_constant, observation)

Return the number of electrons accumulated for an observation.

The electron counts are calculated for the same bins which are used by the detection_rates function. They should not be confused with the detector counts, for which you still would have to divide by the gain.

Parameters:

Name Type Description Default
area Quantity

Effective mirror area.

required
exposures int

Number of exposures.

required
exposure_time s

Exposure time per exposure.

required
grating_angle deg

Grating angle.

required
grating_constant Quantity

The grating constant, i.e. the spacing between grooves.

required
observation Observation

Observation.

required

Returns:

Type Description
tuple[Quantity, Quantity]

A tuple with an array of wavelengths and an array of the corresponding electron counts.

Source code in nirwals/physics/exposure.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def electrons(
    area: Quantity,
    exposures: int,
    exposure_time: u.s,
    grating_angle: u.deg,
    grating_constant: Quantity,
    observation: Observation,
) -> tuple[Quantity, Quantity]:
    """
    Return the number of electrons accumulated for an observation.

    The electron counts are calculated for the same bins which are used by the
    detection_rates function. They should not be confused with the detector counts,
    for which you still would have to divide by the gain.

    Parameters
    ----------
    area: Quantity
        Effective mirror area.
    exposures: int
        Number of exposures.
    exposure_time: Quantity
        Exposure time per exposure.
    grating_angle: Angle
        Grating angle.
    grating_constant: u.micron
        The grating constant, i.e. the spacing between grooves.
    observation: Observation
        Observation.

    Returns
    -------
    tuple[Quantity, Quantity]
        A tuple with an array of wavelengths and an array of the corresponding electron
        counts.
    """
    wavelengths, rates = detection_rates(
        area=area,
        grating_angle=grating_angle,
        grating_constant=grating_constant,
        observation=observation,
    )
    return wavelengths, exposures * exposure_time * rates

exposure_time(configuration)

Calculate the exposure time as a function of the signal-to-noise ratio.

The given configuration must include a wavelength lambda and a signal-to-noise ratio snr, and the exposure time are calculated at lambda for 101 equidistant SNR values in the interval [0, 2 * snr]. The choice of 101 (rather than 100) values is deliberate as it means that the given snr is one of the values. It has the index 50.

The function returns a tuple of the signal-to-noise ratios and corresponding exposure times.

Parameters:

Name Type Description Default
configuration Configuration

The configuration.

required

Returns:

Type Description
tuple

A tuple of 101 signal-to-noise ratios and corresponding exposure times.

Source code in nirwals/physics/exposure.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def exposure_time(configuration: Configuration) -> tuple[Quantity, Quantity]:
    """
    Calculate the exposure time as a function of the signal-to-noise ratio.

    The given configuration must include a wavelength lambda and a signal-to-noise ratio
    snr, and the exposure time are calculated at lambda for 101 equidistant SNR values
    in the interval [0, 2 * snr]. The choice of 101 (rather than 100) values is
    deliberate as it means that the given snr is one of the values. It has the index 50.

    The function returns a tuple of the signal-to-noise ratios and corresponding
    exposure times.

    Parameters
    ----------
    configuration: Configuration
        The configuration.

    Returns
    -------
    tuple
        A tuple of 101 signal-to-noise ratios and corresponding exposure times.
    """
    # Get the source and sky observation.
    source = source_observation(configuration)
    sky = sky_observation(configuration)

    # Get the source and sky detection rates,
    grating = cast(Grating, configuration.telescope.grating)
    wavelengths_source, rates_source = detection_rates(
        area=configuration.telescope.effective_mirror_area,
        grating_angle=grating.grating_angle,
        grating_constant=grating.grating_constant,
        observation=source,
    )
    wavelengths_sky, rates_sky = detection_rates(
        area=configuration.telescope.effective_mirror_area,
        grating_angle=grating.grating_angle,
        grating_constant=grating.grating_constant,
        observation=sky,
    )

    # Collect the remaining relevant configuration parameters.
    exposure = cast(Exposure, configuration.exposure)
    e = exposure.exposures
    snr_ = cast(SNR, exposure.snr)
    requested_wavelength = snr_.wavelength
    detector = cast(Detector, configuration.detector)
    read_noise = detector.read_noise
    samplings = detector.samplings
    sampling_mode = detector.sampling_mode
    r = readout_noise(
        read_noise=read_noise, samplings=samplings, sampling_mode=sampling_mode
    )

    # Define the SNR values for which to calculate the exposure time.
    sigma = np.linspace(0, 2 * float(snr_.snr), 101)

    # Find the source and sky rate for the requested wavelength.
    rates_source_spectrum = SpectralElement(
        Empirical1D,
        points=wavelengths_source,
        lookup_table=rates_source.to(units.PHOTLAM * u.cm**2 * u.AA).value,
    )
    rate_source = rates_source_spectrum(requested_wavelength)
    rates_sky_spectrum = SpectralElement(
        Empirical1D,
        points=wavelengths_sky,
        lookup_table=rates_sky.to(units.PHOTLAM * u.cm**2 * u.AA).value,
    )
    rate_sky = rates_sky_spectrum(requested_wavelength)

    # The exposure time t is defined by a quadratic equation r^2 + p t + q = 0.
    p = -(sigma**2) * (rate_source + rate_sky) / (e * rate_source**2)
    q = -(sigma**2) * r / (e * rate_source**2)
    t = -(p / 2) + np.sqrt((p / 2) ** 2 - q)

    # Add units and return the result.
    return sigma * u.dimensionless_unscaled, t * u.s

pixel_wavelength_range(grating_angle, grating_constant)

Return the wavelength range covered by a single CCD pixel.

Parameters:

Name Type Description Default
grating_angle deg

The grating angle, i.e. the angle of the incoming rays to the grating normal.

required
grating_constant micron

The grating constant, i.e. the groove spacing.

required

Returns:

Type Description
Quantity

The wavelength range covered by a single pixel.

Source code in nirwals/physics/exposure.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def pixel_wavelength_range(grating_angle: u.deg, grating_constant: u.micron) -> u.AA:
    """
    Return the wavelength range covered by a single CCD pixel.

    Parameters
    ----------
    grating_angle: Angle
        The grating angle, i.e. the angle of the incoming rays to the grating normal.
    grating_constant: Quantity
        The grating constant, i.e. the groove spacing.

    Returns
    -------
    Quantity
        The wavelength range covered by a single pixel.
    """
    return (
        grating_constant
        * CCD_PIXEL_SIZE
        * math.cos(grating_angle.to(u.rad).value)
        / CAMERA_FOCAL_LENGTH
    )

readout_noise(read_noise, samplings, sampling_mode)

Return the readout noise for a single exposure.

Parameters:

Name Type Description Default
read_noise float

Read noise.

required
samplings int

Number of samplings (per exposure).

required
sampling_mode SamplingMode

Sampling mode, such as "Fowler".

required

Returns:

Type Description
float

The readout noise.

Source code in nirwals/physics/exposure.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def readout_noise(
    read_noise: float, samplings: int, sampling_mode: SamplingMode
) -> float:
    """
    Return the readout noise for a single exposure.

    Parameters
    ----------
    read_noise: float
        Read noise.
    samplings: int
        Number of samplings (per exposure).
    sampling_mode: SamplingMode
        Sampling mode, such as "Fowler".

    Returns
    -------
    float
        The readout noise.
    """
    match sampling_mode:
        case "Fowler":
            return read_noise**2 / (samplings / 2)
        case "Up-the-Ramp":
            return read_noise**2 / (samplings / 12)

sky_observation(configuration)

Returns the sky background observation for a given configuration.

The observation, represented by a synphot Observation object, is the photon rate detected, which means it is the sky background flux with the following losses applied:

  • Telescope throughput
  • Fibre throughput
  • Filter transmission
  • Grating efficiency
  • Detector quantum efficiency

No atmospheric extinction is applied, as it is assumed that it is included in the background spectrum already.

The bin set of the Observation object is chosen so that the bin just size is equal to the wavelength range covered by a single pixel.

The bin set covers the wavelength range from lambda_min - 100 A to at least lambda_max + 100 A, where lambda_min and lambda_max are the minimum and maximum requested wavelengths. The extra 100 A are added to avoid artifacts at the boundaries of the requested wavelength range.

Parameters:

Name Type Description Default
configuration Configuration

Simulator configuration.

required

Returns:

Type Description
Observation

The observation for the sky background.

Source code in nirwals/physics/exposure.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def sky_observation(configuration: Configuration) -> Observation:
    """
    Returns the sky background observation for a given configuration.

    The observation, represented by a synphot Observation object, is the photon rate
    detected, which means it is the sky background flux with the following losses
    applied:

    - Telescope throughput
    - Fibre throughput
    - Filter transmission
    - Grating efficiency
    - Detector quantum efficiency

    No atmospheric extinction is applied, as it is assumed that it is included in the
    background spectrum already.

    The bin set of the Observation object is chosen so that the bin just size is equal
    to the wavelength range covered by a single pixel.

    The bin set covers the wavelength range from lambda_min - 100 A to at least
    lambda_max + 100 A, where lambda_min and lambda_max are the minimum and maximum
    requested wavelengths. The extra 100 A are added to avoid artifacts at the
    boundaries of the requested wavelength range.

    Parameters
    ----------
    configuration: Configuration
        Simulator configuration.

    Returns
    -------
    Observation
        The observation for the sky background.
    """
    sky = sky_spectrum()
    grating = cast(Grating, configuration.telescope.grating)
    bandpass = (
        telescope_throughput()
        * fibre_throughput(
            seeing=configuration.seeing,
            source_extension="Diffuse",
            zenith_distance=configuration.zenith_distance,
        )
        * filter_transmission(filter_name=cast(Filter, configuration.telescope.filter))
        * grating_efficiency(
            grating_angle=grating.grating_angle,
            grating_name=grating.name,
        )
        * detector_quantum_efficiency()
    )
    return Observation(
        sky,
        bandpass,
        binset=_binset(grating.grating_angle, grating.grating_constant),
    )

snr(configuration)

Source code in nirwals/physics/exposure.py
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
def snr(configuration: Configuration) -> tuple[Quantity, Quantity]:
    # Get the source and sky observation.
    source = source_observation(configuration)
    sky = sky_observation(configuration)

    # Get the source and sky detection rates,
    grating = cast(Grating, configuration.telescope.grating)
    wavelengths_source, rates_source = detection_rates(
        area=configuration.telescope.effective_mirror_area,
        grating_angle=grating.grating_angle,
        grating_constant=grating.grating_constant,
        observation=source,
    )
    wavelengths_sky, rates_sky = detection_rates(
        area=configuration.telescope.effective_mirror_area,
        grating_angle=grating.grating_angle,
        grating_constant=grating.grating_constant,
        observation=sky,
    )

    # Collect the remaining relevant configuration parameters.
    exposure = cast(Exposure, configuration.exposure)
    e = exposure.exposures
    t = exposure.exposure_time
    detector = cast(Detector, configuration.detector)
    read_noise = detector.read_noise
    samplings = detector.samplings
    sampling_mode = detector.sampling_mode
    r = readout_noise(
        read_noise=read_noise, samplings=samplings, sampling_mode=sampling_mode
    )

    # Calculate the SNR.
    source_counts = (
        (rates_source * e * t).to(units.PHOTLAM * u.AA * u.cm**2 * u.s).value
    )
    sky_counts = (rates_sky * e * t).to(units.PHOTLAM * u.AA * u.cm**2 * u.s).value
    snr_values = source_counts / np.sqrt(source_counts + sky_counts + r * e)

    # Return the wavelengths and SNR values.
    return wavelengths_source, snr_values * u.dimensionless_unscaled

source_electrons(configuration)

Return the number of electrons accumulated due to source photons.

The electron counts are calculated for the same bins which are used by the detection_rates function. They should not be confused with the detector counts, for which you still would have to divide by the gain.

Parameters:

Name Type Description Default
configuration Configuration

Simulator configuration.

required

Returns:

Type Description
tuple[Quantity, Quantity]

A tuple with an array of wavelengths and an array of the corresponding electron counts.

Source code in nirwals/physics/exposure.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def source_electrons(configuration: Configuration) -> tuple[Quantity, Quantity]:
    """
    Return the number of electrons accumulated due to source photons.

    The electron counts are calculated for the same bins which are used by the
    detection_rates function. They should not be confused with the detector counts,
    for which you still would have to divide by the gain.

    Parameters
    ----------
    configuration: Configuration
        Simulator configuration.

    Returns
    -------
    tuple[Quantity, Quantity]
        A tuple with an array of wavelengths and an array of the corresponding electron
        counts.
    """
    # Collect the required parameters.
    area = configuration.telescope.effective_mirror_area
    exposure = cast(Exposure, configuration.exposure)
    grating = cast(Grating, configuration.telescope.grating)
    observation = source_observation(configuration)

    return electrons(
        area=area,
        exposures=exposure.exposures,
        exposure_time=exposure.exposure_time,
        grating_angle=grating.grating_angle,
        grating_constant=grating.grating_constant,
        observation=observation,
    )

source_observation(configuration)

Returns the source observation for a given configuration.

The observation, represented by a synphot Observation object, is the photon flux detected, which means it is the source spectrum with the following losses applied:

  • Atmospheric extinction
  • Telescope throughput
  • Fibre throughput
  • Filter transmission
  • Grating efficiency
  • Detector quantum efficiency

The bin set of the Observation object is chosen so that the bin just size is equal to the wavelength range covered by a single pixel.

The bin set covers the wavelength range from lambda_min - 100 A to at least lambda_max + 100 A, where lambda_min and lambda_max are the minimum and maximum requested wavelengths. The extra 100 A are added to avoid artifacts at the boundaries of the requested wavelength range.

Parameters:

Name Type Description Default
configuration Configuration

Simulator configuration.

required

Returns:

Type Description
Observation

The observation for the source spectrum.

Source code in nirwals/physics/exposure.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def source_observation(configuration: Configuration) -> Observation:
    """
    Returns the source observation for a given configuration.

    The observation, represented by a synphot Observation object, is the photon flux
    detected, which means it is the source spectrum with the following losses applied:

    - Atmospheric extinction
    - Telescope throughput
    - Fibre throughput
    - Filter transmission
    - Grating efficiency
    - Detector quantum efficiency

    The bin set of the Observation object is chosen so that the bin just size is equal
    to the wavelength range covered by a single pixel.

    The bin set covers the wavelength range from lambda_min - 100 A to at least
    lambda_max + 100 A, where lambda_min and lambda_max are the minimum and maximum
    requested wavelengths. The extra 100 A are added to avoid artifacts at the
    boundaries of the requested wavelength range.

    Parameters
    ----------
    configuration: Configuration
        Simulator configuration.

    Returns
    -------
    Observation
        The observation for the source spectrum.
    """
    source = source_spectrum(configuration=configuration)
    grating = cast(Grating, configuration.telescope.grating)
    configuration_source = cast(Source, configuration.source)
    bandpass = (
        atmospheric_transmission(zenith_distance=configuration.zenith_distance)
        * telescope_throughput()
        * fibre_throughput(
            seeing=configuration.seeing,
            source_extension=configuration_source.extension,
            zenith_distance=configuration.zenith_distance,
        )
        * filter_transmission(filter_name=cast(Filter, configuration.telescope.filter))
        * grating_efficiency(
            grating_angle=grating.grating_angle,
            grating_name=grating.name,
        )
        * detector_quantum_efficiency()
    )
    return Observation(
        source,
        bandpass,
        binset=_binset(
            grating_angle=grating.grating_angle,
            grating_constant=grating.grating_constant,
        ),
        force="taper"
    )

wavelength_resolution_element(grating_angle, grating_constant)

Return the wavelength resolution element for a grating setup.

Parameters:

Name Type Description Default
grating_constant micron

The grating constant, i.e. the groove spacing.

required
grating_angle deg

The grating angle, i.e. the angle of the incoming rays to the grating normal.

required

Returns:

Type Description
Quantity

The wavelength resolution element.

Source code in nirwals/physics/exposure.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def wavelength_resolution_element(
    grating_angle: u.deg, grating_constant: u.micron
) -> u.AA:
    """
    Return the wavelength resolution element for a grating setup.

    Parameters
    ----------
    grating_constant: Quantity
        The grating constant, i.e. the groove spacing.
    grating_angle: Angle
        The grating angle, i.e. the angle of the incoming rays to the grating normal.

    Returns
    -------
    Quantity
        The wavelength resolution element.
    """
    # Angle of a fibre on the sky, in radians
    phi_fibre = 2 * FIBRE_RADIUS.to(u.rad).value

    return (
        phi_fibre
        * (TELESCOPE_FOCAL_LENGTH / COLLIMATOR_FOCAL_LENGTH)
        * grating_constant
        * math.cos(grating_angle.to(u.rad).value)
    )