Noise Floor Conventions#

The noise floor represents the systematic limit on coronagraphic PSF subtraction – the residual stellar speckle noise that cannot be removed with longer integration time. Different exposure time calculators package this quantity differently.

Yippy provides both conventions so it can serve as a data source for any downstream ETC.

Two Conventions, Same Physics#

All ETCs produce the same final noise-floor count rate. The question is how the intermediate value is stored:

\[C_{\text{nf}} = \text{SNR} \cdot C_\star \cdot \bar{I}_\star \cdot \frac{\Omega}{\theta_{\text{pix}}^2 \cdot \text{ppf}}\]

Whether the \(\Omega / (\theta_{\text{pix}}^2 \cdot \eta_p)\) factor is baked into the stored value or applied by the consumer distinguishes the two conventions.

AYO / pyEDITH Convention (per-pixel)#

AYO and pyEDITH store the noise floor in per-pixel intensity units:

\[\text{NF}_{\text{AYO}} = \frac{\bar{I}_\star}{\text{ppf}}\]

The ETC scales to the full aperture:

\[CR_{\text{nf}} = \text{SNR} \cdot \left(\frac{F_0 F_\star A T \Delta\lambda\, n_{\text{chan}}}{\theta_{\text{pix}}^2}\right) \cdot \text{NF}_{\text{AYO}} \cdot \Omega\]

This convention keeps the noise floor in the same units as \(\bar{I}_\star\), making it natural for 2D map indexing.

EXOSIMS Convention (contrast-normalized)#

EXOSIMS has two code paths for computing stellar residuals in OpticalSystem.Cp_Cb_Csp_helper:

  1. Standard path (when core_mean_intensity is provided): Uses the 2D stellar intensity map, supporting stellar-diameter-dependent leakage:

    \[\text{core\_intensity} = \bar{I}_\star(r, d_\star) \cdot \frac{\Omega}{\theta_{\text{pix}}^2}\]
  2. Fallback path (when core_mean_intensity is None): Uses the simpler core_contrast curve, which has no stellar diameter dependence:

    \[\text{core\_intensity} = C_{\text{raw}}(r) \cdot \eta_p(r)\]

For a point source these are algebraically equivalent, since \(C_{\text{raw}} = \bar{I}_\star \Omega / (\theta_{\text{pix}}^2 \eta_p)\). The advantage of the standard path is that it can model how stellar leakage increases with stellar angular diameter.

Yippy’s noise_floor_exosims computes \(C_{\text{raw}} / \text{ppf}\), which corresponds to the fallback convention:

\[\text{NF}_{\text{EXOSIMS}} = \frac{C_{\text{raw}}}{\text{ppf}}\]

The ETC recovers the count rate by multiplying by throughput:

\[C_{\text{sr}} = C_\star \cdot \text{NF}_{\text{EXOSIMS}} \cdot \eta_p\]

This convention is natural for 1D radial curves where throughput is a separate interpolated quantity.

Algebraic Equivalence#

The two conventions differ by a geometric factor:

\[\text{NF}_{\text{EXOSIMS}} = \text{NF}_{\text{AYO}} \cdot \frac{\Omega}{\theta_{\text{pix}}^2 \cdot \eta_p}\]

When plugged into their respective ETCs, both produce identical noise-floor count rates.

import matplotlib.pyplot as plt
import numpy as np
from yippy import fetch_yip
from yippy import Coronagraph
import logging; logging.getLogger("yippy").setLevel(logging.ERROR)

yip_path = fetch_yip("eac1_aavc_2d")
coro = Coronagraph(yip_path)
print(f"Coronagraph: {coro.name}")
print(f"Pixel scale: {coro.pixel_scale_arcsec}")
print(f"IWA: {coro.IWA:.2f}, OWA: {coro.OWA:.2f}")
/home/docs/checkouts/readthedocs.org/user_builds/yippy/envs/stable/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Coronagraph: eac1_aavc_2d
Pixel scale: 0.25 λ/D / pix
IWA: 4.26 λ/D, OWA: 32.00 λ/D

Comparing the Two Conventions#

API:

nf_exosims = coro.noise_floor_exosims(separation, ppf=30.0)
nf_ayo     = coro.noise_floor_ayo(separation, ppf=30.0)

Hide code cell source

seps = np.linspace(coro.IWA.value, coro.OWA.value, 200)
ppf = 30.0

# Use contrast_floor=0 to see the true relationship;
# the default contrast_floor=1e-10 clamps values in the working region
nf_exosims = coro.noise_floor_exosims(seps, ppf=ppf, contrast_floor=0)
nf_ayo = coro.noise_floor_ayo(seps, ppf=ppf)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))

ax1.semilogy(seps, nf_exosims, lw=2, label='EXOSIMS ($C_{raw}$ / PPF)')
ax1.semilogy(seps, nf_ayo, lw=2, label='AYO ($\\bar{I}_\\star$ / PPF)')
ax1.set_xlabel('Separation [$\\lambda/D$]')
ax1.set_ylabel('Noise Floor Value')
ax1.set_title('Both Conventions (no contrast floor)')
ax1.legend(fontsize=9)
ax1.grid(True, alpha=0.3)

ratio = nf_exosims / nf_ayo
ax2.plot(seps, ratio, lw=2, color='#9C27B0')
ax2.set_xlabel('Separation [$\\lambda/D$]')
ax2.set_ylabel('EXOSIMS / AYO')
ax2.set_title('Ratio = raw\_contrast / CMI')
ax2.grid(True, alpha=0.3)

fig.suptitle(f'{coro.name} -- Noise Floor Conventions (PPF = {ppf:.0f})',
             fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()
../_images/4c736948d8b63e990c8a95492394507e580121763d4d267d80d168dc8a14e5ab.png

The ratio between the two conventions is not constant because raw_contrast integrates stellar flux within a circular aperture while core_mean_intensity is the azimuthal mean at each separation. The relationship depends on the PSF structure.

Contrast Floor

By default, noise_floor_exosims clamps raw_contrast to a minimum of 1e-10 (the contrast_floor parameter). In the working region of most coronagraphs, the actual raw contrast may be far below this floor, making the EXOSIMS noise floor appear constant. The plots above use contrast_floor=0 to show the true underlying values.


Verifying Algebraic Equivalence#

We can verify the relationship by checking that the noise floor ratio equals raw_contrast / core_mean_intensity, computed from the individual yippy API calls:

Hide code cell source

# The ratio is: noise_floor_exosims / noise_floor_ayo
#              = raw_contrast / core_mean_intensity
# Verify by computing both sides independently:

raw_con = coro.raw_contrast(seps)
cmi = np.asarray([float(coro.core_mean_intensity(s)) for s in seps])
expected_ratio = raw_con / cmi

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))

ax1.plot(seps, ratio, lw=2, label='nf$_{EXOSIMS}$ / nf$_{AYO}$')
ax1.plot(seps, expected_ratio, '--', lw=2,
         label='raw\_contrast / CMI')
ax1.set_xlabel('Separation [$\\lambda/D$]')
ax1.set_ylabel('Ratio')
ax1.set_title('Predicted vs Measured Ratio')
ax1.legend(fontsize=9)
ax1.grid(True, alpha=0.3)

residual = np.abs(ratio - expected_ratio) / np.abs(ratio)
ax2.semilogy(seps, residual, lw=2, color='#E91E63')
ax2.set_xlabel('Separation [$\\lambda/D$]')
ax2.set_ylabel('Fractional Residual')
ax2.set_title('Equivalence Error')
ax2.grid(True, alpha=0.3)

fig.suptitle('Verifying: nf$_{EXOSIMS}$ / nf$_{AYO}$ = raw\_contrast / CMI',
             fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()

print(f'Max fractional residual: {np.nanmax(residual):.2e}')
../_images/4f20fae9cb39f0ead03afe8f77e4fd54cd324c1a6392562762da0bbfcdeba1a2.png
Max fractional residual: 2.30e-16

Effect of Post-Processing Factor#

The PPF sets the systematic noise floor level. A higher PPF means better speckle subtraction and a lower noise floor:

Hide code cell source

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))

ppfs = [10, 30, 100, 300]
colors = ['#E91E63', '#4CAF50', '#2196F3', '#FF9800']

for ppf_val, c in zip(ppfs, colors, strict=True):
    nf_e = coro.noise_floor_exosims(seps, ppf=ppf_val)
    nf_a = coro.noise_floor_ayo(seps, ppf=ppf_val)
    ax1.semilogy(seps, nf_e, color=c, lw=2, label=f'PPF = {ppf_val}')
    ax2.semilogy(seps, nf_a, color=c, lw=2, label=f'PPF = {ppf_val}')

ax1.set_xlabel('Separation [$\\lambda/D$]')
ax1.set_ylabel('Noise Floor (EXOSIMS)')
ax1.set_title('EXOSIMS Convention')
ax1.legend(fontsize=9)
ax1.grid(True, alpha=0.3)

ax2.set_xlabel('Separation [$\\lambda/D$]')
ax2.set_ylabel('Noise Floor (AYO)')
ax2.set_title('AYO Convention')
ax2.legend(fontsize=9)
ax2.grid(True, alpha=0.3)

fig.suptitle(f'{coro.name} -- PPF Effect on Noise Floor',
             fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()
../_images/0cba1698bbc387a714f45bfc13871e4bb5f80cd56cec2d12e68318156c411358.png

Which Convention to Use#

Consumer Code

API Call

Why

EXOSIMS

coro.noise_floor_exosims(sep)

Matches core_contrast convention; consumer multiplies by \(\eta_p\)

pyEDITH

coro.noise_floor_ayo(sep)

Matches per-pixel Istar/ppf; consumer multiplies by \(\Omega / \theta_{\text{pix}}^2\)

AYO benchmarking

coro.noise_floor_ayo(sep)

Direct comparison with AYO CSV values

Quick SNR estimate

coro.noise_floor_exosims(sep)

Contrast-level value is more intuitive


References#

  • AYO noise floor: load_coronagraph.pro L582-584 (noisefloor = Istar / noisefloor_PPF)

  • pyEDITH noise floor: coronagraphs.py L754-755 (Istar / PPF)

  • EXOSIMS C_sr (Prototype): OpticalSystem.py L2143-2145 (core_mean_intensity * Omega / platescale**2)

  • EXOSIMS C_sp (Prototype): L2028-2030 (C_sr * ppFact * stabilityFact)

  • EXOSIMS C_b (Nemati): Nemati.py L184-196 – adds RDI factors k_SZ and k_det that scale backgrounds based on reference star differential imaging parameters

  • Stark et al. (2025) – Cross-Model Validation of Coronagraphic ETCs