use dualis::prelude::*;
use dualis_optics::diffraction::{airy_intensity, mtf_ideal, FIRST_AIRY_ZERO, SECOND_AIRY_ZERO};
mod common;
use common::svg::{document, heat, rgb, ticks, Plot};
use common::{check, check_between, heading};
const LAMBDA: f64 = 550e-9;
const F_NUMBER: f64 = 4.0;
const NA: f64 = 0.5 / F_NUMBER;
fn main() {
let lambda = Length::from_si(LAMBDA);
heading("A perfect f/4 lens in the green");
let airy = airy_radius(lambda, NA);
let exact = FIRST_AIRY_ZERO / std::f64::consts::TAU;
check(
"Airy radius (J1 zero / 2pi) lambda / NA",
airy.in_um(),
exact * LAMBDA / NA * 1e6,
1e-12,
"um",
);
check(
" against the textbook 1.22 lambda F",
airy.in_um() / (1.22 * LAMBDA * F_NUMBER * 1e6),
1.0,
4e-4,
"x",
);
check(
"Rayleigh limit (the same distance)",
rayleigh_limit(lambda, NA).in_um(),
airy.in_um(),
1e-12,
"um",
);
let cutoff = cutoff_frequency(lambda, NA);
check(
"cutoff 2 NA / lambda",
cutoff / 1000.0,
1.0 / (LAMBDA * F_NUMBER) / 1000.0,
1e-12,
"cy/mm",
);
heading("Where the light goes");
check(
"energy inside the 1st dark ring",
encircled_energy(FIRST_AIRY_ZERO) * 100.0,
83.8,
1e-3,
"%",
);
check(
"energy inside the 2nd",
encircled_energy(SECOND_AIRY_ZERO) * 100.0,
91.0,
1e-3,
"%",
);
check(
"energy in the rings, therefore",
100.0 - encircled_energy(FIRST_AIRY_ZERO) * 100.0,
16.2,
6e-3,
"%",
);
let (ring_v, ring_peak) = (0..2000)
.map(|i| {
let v =
FIRST_AIRY_ZERO + (SECOND_AIRY_ZERO - FIRST_AIRY_ZERO) * (i as f64 + 0.5) / 2000.0;
(v, airy_intensity(v))
})
.fold((0.0, 0.0), |best, c| if c.1 > best.1 { c } else { best });
check_between(
"brightest point of the 1st ring",
ring_peak * 100.0,
1.5,
2.0,
"%",
);
check_between(
" which sits at, in Airy radii",
ring_v / FIRST_AIRY_ZERO,
1.3,
1.5,
"",
);
heading("Contrast against spatial frequency");
for (fraction, expected) in [(0.0, 1.0), (0.25, 0.685), (0.5, 0.391), (0.75, 0.1443)] {
check(
&format!("MTF at {:>3.0}% of cutoff", fraction * 100.0),
mtf_at(fraction * cutoff, lambda, NA),
expected,
2e-3,
"",
);
}
check(
"MTF at cutoff, and beyond",
mtf_at(1.5 * cutoff, lambda, NA),
0.0,
1e-12,
"",
);
heading("And what a quarter wave of error costs");
let strehl = strehl_from_wavefront_error(1.0 / 14.0);
check_between("Strehl at lambda/14 RMS", strehl, 0.78, 0.84, "");
let dof = depth_of_focus(lambda, NA, 1.0);
let half_na = depth_of_focus(lambda, NA * 2.0, 1.0);
check(
"depth of focus at f/4",
dof.in_um(),
LAMBDA / (NA * NA) * 1e6,
1e-12,
"um",
);
check(
" and at f/2, four times shallower",
dof.in_um() / half_na.in_um(),
4.0,
1e-12,
"x",
);
let Some(path) = common::output_path() else {
println!("\npass a path to write an SVG, e.g. `cargo run --example airy_pattern out.svg`");
return;
};
common::write(&path, &draw(airy.in_um(), cutoff));
}
fn draw(airy_um: f64, cutoff: f64) -> String {
let (w, h) = (900.0, 330.0);
let extent = 3.2;
let mut pattern =
Plot::new(w, h, (-extent, extent), (-extent, extent)).viewport(56.0, 54.0, 216.0, 216.0);
let n = 96;
pattern.raster(n, n, (-extent, extent), (-extent, extent), |ix, iy| {
let centre = |k: usize| -extent + 2.0 * extent * (k as f64 + 0.5) / n as f64;
let (x, y) = (centre(ix), centre(iy));
let intensity = airy_intensity((x * x + y * y).sqrt() * FIRST_AIRY_ZERO);
heat(((intensity.max(1e-6).log10() + 4.0) / 4.0).clamp(0.0, 1.0))
});
pattern.axes(
&[-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
&[],
|v| format!("{v:.0}"),
|_| String::new(),
);
pattern.title("A point source through a perfect f/4 lens");
pattern.caption(&format!(
"Airy radius {airy_um:.2} um cutoff {:.0} cy/mm",
cutoff / 1000.0
));
pattern.footnote("the pattern, log scale, in Airy radii");
let mut energy = Plot::new(w, h, (0.0, 4.0), (0.0, 1.02)).viewport(340.0, 54.0, 216.0, 216.0);
energy.axes(
&ticks(0.0, 4.0, 4),
&ticks(0.0, 1.0, 5),
|v| format!("{v:.0}"),
|v| format!("{:.0}%", v * 100.0),
);
energy.polyline(
(0..=320).map(|i| {
let r = i as f64 / 80.0;
(r, encircled_energy(r * FIRST_AIRY_ZERO))
}),
&rgb(158, 40, 32),
2.0,
);
for (radii, v) in [(1.0, FIRST_AIRY_ZERO), (2.0, SECOND_AIRY_ZERO)] {
let e = encircled_energy(v);
energy.polyline([(radii, 0.0), (radii, e)], "#00000038", 1.0);
energy.text(
radii + 0.12,
e - 0.09,
&format!("{:.1}%", e * 100.0),
11.5,
"#7a3a34",
"start",
);
}
energy.footnote("energy inside a radius, in Airy radii");
let mut mtf = Plot::new(w, h, (0.0, 1.0), (0.0, 1.02)).viewport(624.0, 54.0, 216.0, 216.0);
mtf.axes(
&ticks(0.0, 1.0, 4),
&ticks(0.0, 1.0, 5),
|v| format!("{v:.2}"),
|v| format!("{v:.1}"),
);
mtf.polyline(
(0..=240).map(|i| {
let s = i as f64 / 240.0;
(s, mtf_ideal(s))
}),
&rgb(48, 108, 186),
2.0,
);
mtf.polyline([(0.5, 0.0), (0.5, mtf_ideal(0.5))], "#00000038", 1.0);
mtf.text(
0.54,
mtf_ideal(0.5) - 0.08,
"0.391",
11.5,
"#2a4a78",
"start",
);
mtf.footnote("contrast, as a fraction of cutoff");
document(
w,
h,
[pattern.into_body(), energy.into_body(), mtf.into_body()],
)
}