Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Cherry might be for you if:
- [ ] **Help Wanted** 3D views
- [ ] **Help Wanted** Fuzzy search for materials
- [ ] [Cardinal lens](https://opg.optica.org/ao/fulltext.cfm?uri=ao-63-4-1110) surface type for ideal objective models
- [ ] Cross-validate integration test results with Optiland

### Done

Expand Down
4 changes: 2 additions & 2 deletions crates/cherry-rs/src/core/sequential_model/solves/fno.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ mod tests {
let target = 4.0;
let (_, pv) = build_with_fno_solve(target);
let sub = pv.get(0, 0).unwrap();
assert_abs_diff_eq!(sub.paraxial_fno(), target, epsilon = 1e-4);
assert_abs_diff_eq!(sub.paraxial_fno().abs(), target, epsilon = 1e-4);
}

#[test]
Expand Down Expand Up @@ -315,7 +315,7 @@ mod tests {
let sub = pv.get(0, 0).unwrap();

// F/# constraint satisfied.
assert_abs_diff_eq!(sub.paraxial_fno(), target_fno, epsilon = 1e-3);
assert_abs_diff_eq!(sub.paraxial_fno().abs(), target_fno, epsilon = 1e-3);

// MarginalRaySolve constraint: marginal ray height at image ≈ 0.
let bundle = marginal_ray_bundle(&model, 0).unwrap();
Expand Down
6 changes: 6 additions & 0 deletions crates/cherry-rs/src/gui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,19 @@ impl eframe::App for CherryApp {
self.load_specs(examples::f_theta_scan_lens());
ui.close();
}
ui.separator();
ui.label("Systems");
if ui
.button("Galvo and Scan Lens (Negrean and Mansvelder)")
.clicked()
{
self.load_specs(examples::galvo_scan_lens_negrean_mansvelder());
ui.close();
}
if ui.button("Widefield Epifluorescence Excitation").clicked() {
self.load_specs(examples::wf_epi_excitation());
ui.close();
}
});
ui.add_space(16.0);

Expand Down
65 changes: 65 additions & 0 deletions crates/cherry-rs/src/gui/examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,59 @@ pub fn galvo_scan_lens_negrean_mansvelder() -> SystemSpecs {
}
}

/// Reduced widefield epifluorescence microscope excitation path.
///
/// An object, a thin relay lens (f = 40 mm), a flat fold mirror at 45°, and a
/// second thin lens (f = 3.3333 mm) that acts as the aperture stop, modelling
/// a microscope objective back aperture immersed in oil (n = 1.5). The fold
/// mirror contributes no power; only the unfolded track length of 150 mm
/// between the two lenses matters for paraxial calculations.
pub fn wf_epi_excitation() -> SystemSpecs {
SystemSpecs {
surfaces: vec![
SurfaceRow::new_object("40.0"),
SurfaceRow::new_thin_lens("25.0", "40.0", "100.0", "1.0"),
SurfaceRow {
variant: SurfaceVariant::Sphere,
boundary_variant: BoundaryVariant::Reflecting,
refractive_index: "1.0".into(),
thickness: "50.0".into(),
semi_diameter: "36.0".into(),
radius_of_curvature: "Infinity".into(),
conic_constant: String::new(),
focal_length: String::new(),
theta: "45".into(),
psi: "0".into(),
material_key: None,
},
SurfaceRow::new_thin_lens("4.25", "3.3333", "1.0", "1.5"),
SurfaceRow::new_image(),
],
fields: vec![FieldRow {
chi: "1.5".into(),
phi: "90.0".into(),
x: "0.0".into(),
}],
aperture_semi_diameter: "1.5454".into(),
wavelengths: vec!["0.5876".into()],
field_mode: FieldMode::PointSource,
use_materials: false,
selected_materials: Vec::new(),
cross_section_n_rays: 11,
full_pupil_spacing: "0.1".into(),
n_fan_rays: 65,
background_n: "1.0".into(),
background_material_key: None,
stop_surface: Some(3),
solves: vec![SolveSpec::MarginalRayHeight {
gap_index: 3,
target_height: 0.0,
wavelength_id: 0,
}],
lens_groups: Vec::new(),
}
}

/// f = +100 mm concave mirror.
pub fn concave_mirror() -> SystemSpecs {
SystemSpecs {
Expand Down Expand Up @@ -625,6 +678,18 @@ mod tests {
.expect("model");
}

#[test]
fn wf_epi_excitation_example_converts_to_valid_model() {
let specs = wf_epi_excitation();
let parsed = parse(&specs);
SequentialModelBuilder::new()
.gap_specs(parsed.gaps)
.surface_specs(parsed.surfaces)
.wavelengths(parsed.wavelengths)
.build()
.expect("model");
}

/// The M solve on the lens-to-image gap must resolve to the lens's back
/// focal distance (== focal length, for a thin lens in air with an
/// object at infinity), demonstrating that the image plane tracks the
Expand Down
89 changes: 53 additions & 36 deletions crates/cherry-rs/src/gui/windows/paraxial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,59 +130,65 @@ fn render_table_body(
tb_sep(body, n_cols);
}

tb_row(body, row_h, "EFL", ids, pv, |sv| {
tb_row(body, row_h, "Effective focal length", ids, pv, |sv| {
*sv.effective_focal_length()
});
tb_row(body, row_h, "BFD", ids, pv, |sv| *sv.back_focal_distance());
tb_row(body, row_h, "FFD", ids, pv, |sv| *sv.front_focal_distance());
tb_row(body, row_h, "Paraxial F/#", ids, pv, |sv| sv.paraxial_fno());
tb_row(body, row_h, "Image space F/#", ids, pv, |sv| {
sv.image_space_fno()
tb_row(body, row_h, "Back focal distance", ids, pv, |sv| {
*sv.back_focal_distance()
});

tb_sep(body, n_cols);

tb_row(
body,
row_h,
"Entrance pupil dist. from first surface",
"Back principal plane location",
ids,
pv,
|sv| sv.entrance_pupil().location,
|sv| *sv.back_principal_plane(),
);
tb_row(body, row_h, "Entrance pupil semi-diameter", ids, pv, |sv| {
sv.entrance_pupil().semi_diameter
tb_row(body, row_h, "Front focal length", ids, pv, |sv| {
*sv.front_focal_length()
});
tb_row(body, row_h, "Front focal distance", ids, pv, |sv| {
*sv.front_focal_distance()
});
tb_row(
body,
row_h,
"Exit pupil dist. from last surface",
"Front principal plane location",
ids,
pv,
|sv| sv.exit_pupil().location,
|sv| *sv.front_principal_plane(),
);
tb_row(body, row_h, "Exit pupil semi-diameter", ids, pv, |sv| {
sv.exit_pupil().semi_diameter

tb_sep(body, n_cols);

tb_row(body, row_h, "Paraxial F/#", ids, pv, |sv| sv.paraxial_fno());
tb_row(body, row_h, "Image space F/#", ids, pv, |sv| {
sv.image_space_fno()
});

tb_row(body, row_h, "Lagrange invariant", ids, pv, |sv| {
let stop = *sv.aperture_stop();
sv.lagrange_invariants()
.get(stop)
.copied()
.unwrap_or(f64::NAN)
.abs()
});

tb_sep(body, n_cols);

tb_row(
body,
row_h,
"Front principal plane dist. from first surface",
ids,
pv,
|sv| *sv.front_principal_plane(),
);
tb_row(
body,
row_h,
"Back principal plane dist. from last surface",
ids,
pv,
|sv| *sv.back_principal_plane(),
);
tb_row(body, row_h, "Entrance pupil location", ids, pv, |sv| {
sv.entrance_pupil().location
});
tb_row(body, row_h, "Entrance pupil semi-diameter", ids, pv, |sv| {
sv.entrance_pupil().semi_diameter
});
tb_row(body, row_h, "Exit pupil location", ids, pv, |sv| {
sv.exit_pupil().location
});
tb_row(body, row_h, "Exit pupil semi-diameter", ids, pv, |sv| {
sv.exit_pupil().semi_diameter
});

tb_sep(body, n_cols);

Expand Down Expand Up @@ -331,9 +337,9 @@ mod tests {
ParaxialWindow::show(ctx, &mut open, Some(&result));
});
harness.step();
harness.get_by_label("EFL");
harness.get_by_label("BFD");
harness.get_by_label("FFD");
harness.get_by_label("Effective focal length");
harness.get_by_label("Back focal distance");
harness.get_by_label("Front focal distance");
}

#[test]
Expand Down Expand Up @@ -388,6 +394,17 @@ mod tests {
harness.get_by_label("Image space F/#");
}

#[test]
fn paraxial_data_shows_lagrange_invariant_row() {
let result = make_result(&["0.567"]);
let mut harness = Harness::new(|ctx| {
let mut open = true;
ParaxialWindow::show(ctx, &mut open, Some(&result));
});
harness.step();
harness.get_by_label("Lagrange invariant");
}

#[test]
fn single_wavelength_no_primary_axial_color() {
let result = make_result(&["0.567"]);
Expand Down
Loading
Loading