use super::material_lookup::{EditorMaterialLookup, resolved_gem_material};
use indicatrix::optics::materials::GemMaterial;
use indicatrix_cut_core::{
Design, DesignSolveError, MaterialSelection, OptimizeConfig, OptimizeOptions, OptimizeResult,
SearchHooks, free_tier_indices_with,
optimize::{SearchStage, StartProgress, inclusive_max_evaluations_for},
optimize_design_with,
};
use indicatrix_editor::{
material_lookup::sized_material_for_optimize, optimize_view::measure_anchor_hinges,
solve_policy::design_to_gpu_planes_from_solved,
};
use slint::{ComponentHandle, Weak};
use std::{
collections::BTreeSet,
sync::{
Arc,
atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicUsize, Ordering},
},
thread,
time::{Duration, Instant},
};
const TICK_INTERVAL: Duration = Duration::from_millis(250);
pub struct OptimizeSolveHandle {
cancel: Arc<AtomicBool>,
}
impl OptimizeSolveHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy)]
pub struct OptimizeSolveProgress {
pub evaluations: usize,
pub max_evaluations: usize,
pub stage: SearchStage,
pub start: Option<StartProgress>,
pub elapsed: Duration,
}
#[derive(Default)]
struct StartCell {
index: AtomicUsize,
count: AtomicUsize,
best_bits: AtomicU32,
}
impl StartCell {
fn store(&self, progress: StartProgress) {
self.index.store(progress.index, Ordering::Relaxed);
self.best_bits
.store(progress.best_fast_score.to_bits(), Ordering::Relaxed);
self.count.store(progress.count, Ordering::Relaxed);
}
fn load(&self) -> Option<StartProgress> {
let count = self.count.load(Ordering::Relaxed);
(count > 0).then(|| StartProgress {
index: self.index.load(Ordering::Relaxed),
count,
best_fast_score: f32::from_bits(self.best_bits.load(Ordering::Relaxed)),
})
}
}
fn start_for_stage(stage: SearchStage, start: Option<StartProgress>) -> Option<StartProgress> {
match stage {
SearchStage::Polish | SearchStage::FinalFull => None,
_ => start,
}
}
pub enum OptimizeRunOutcome {
Completed { result: OptimizeResult },
Cancelled { result: OptimizeResult },
Failed { error: DesignSolveError },
}
pub struct OptimizeRunRequest {
pub design: Design,
pub material_selection: MaterialSelection,
pub custom_materials: Vec<GemMaterial>,
pub config: OptimizeConfig,
pub options: OptimizeOptions,
pub only_tiers: Option<BTreeSet<usize>>,
}
pub fn spawn_optimize_run<T, P, D>(
ui_weak: Weak<T>,
request: OptimizeRunRequest,
on_progress: P,
on_done: D,
) -> OptimizeSolveHandle
where
T: ComponentHandle + 'static,
P: Fn(&T, OptimizeSolveProgress) + Send + 'static + Clone,
D: FnOnce(&T, OptimizeRunOutcome) + Send + 'static,
{
let cancel = Arc::new(AtomicBool::new(false));
let cancel_ticker = Arc::clone(&cancel);
let cancel_worker = Arc::clone(&cancel);
let done_flag = Arc::new(AtomicBool::new(false));
let done_ticker = Arc::clone(&done_flag);
let evaluations_done = Arc::new(AtomicUsize::new(0));
let evaluations_ticker = Arc::clone(&evaluations_done);
let stage_done = Arc::new(AtomicU8::new(SearchStage::BaselineFull.to_code()));
let stage_ticker = Arc::clone(&stage_done);
let start_done = Arc::new(StartCell::default());
let start_ticker = Arc::clone(&start_done);
let ticker_ui = ui_weak.clone();
let max_evaluations = Arc::new(AtomicUsize::new(inclusive_max_evaluations_for(
&request.config,
request.options.keep_candidates,
free_tier_indices_with(&request.design, &request.options).len(),
)));
let max_evaluations_ticker = Arc::clone(&max_evaluations);
thread::spawn(move || {
let start = Instant::now();
loop {
thread::sleep(TICK_INTERVAL);
if done_ticker.load(Ordering::Relaxed) || cancel_ticker.load(Ordering::Relaxed) {
break;
}
let stage = SearchStage::from_code(stage_ticker.load(Ordering::Relaxed));
let progress = OptimizeSolveProgress {
evaluations: evaluations_ticker.load(Ordering::Relaxed),
max_evaluations: max_evaluations_ticker.load(Ordering::Relaxed),
stage,
start: start_for_stage(stage, start_ticker.load()),
elapsed: start.elapsed(),
};
let on_progress = on_progress.clone();
let _ = ticker_ui.upgrade_in_event_loop(move |ui| {
on_progress(&ui, progress);
});
}
});
thread::spawn(move || {
let OptimizeRunRequest {
design,
material_selection,
custom_materials,
config,
mut options,
only_tiers,
} = request;
let lookup = EditorMaterialLookup::new(&custom_materials);
let material = resolved_gem_material(&material_selection, &lookup);
let hooks = SearchHooks {
cancel: Some(&cancel_worker),
on_progress: Some(&|evaluations: usize, stage: SearchStage| {
evaluations_done.store(evaluations, Ordering::Relaxed);
stage_done.store(stage.to_code(), Ordering::Relaxed);
}),
on_start: Some(&|progress: StartProgress| start_done.store(progress)),
};
let result = prepare_run_inputs(&design, material, &mut options, only_tiers.as_ref())
.and_then(|material| {
max_evaluations.store(
inclusive_max_evaluations_for(
&config,
options.keep_candidates,
free_tier_indices_with(&design, &options).len(),
),
Ordering::Relaxed,
);
optimize_design_with(&design, &material, &config, &options, &hooks)
});
done_flag.store(true, Ordering::Relaxed);
let cancelled = cancel_worker.load(Ordering::Relaxed);
let outcome = run_outcome_for(cancelled, result);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
on_done(&ui, outcome);
});
});
OptimizeSolveHandle { cancel }
}
fn prepare_run_inputs(
design: &Design,
material: GemMaterial,
options: &mut OptimizeOptions,
only: Option<&BTreeSet<usize>>,
) -> Result<GemMaterial, DesignSolveError> {
let wants_size = design
.girdle_diameter_mm
.is_some_and(|mm| mm > 0.0 && mm.is_finite());
if !options.vary_anchored && !wants_size {
if !indicatrix::render_setup::needs_model_width(&material) {
return Ok(sized_material_for_optimize(material, design, &[]));
}
}
let solved = design.solve()?;
if options.vary_anchored {
measure_anchor_hinges(options, design, &solved, only);
}
let planes = design_to_gpu_planes_from_solved(design, &solved);
Ok(sized_material_for_optimize(material, design, &planes))
}
fn run_outcome_for(
cancelled: bool,
result: Result<OptimizeResult, DesignSolveError>,
) -> OptimizeRunOutcome {
match result {
Err(error) => OptimizeRunOutcome::Failed { error },
Ok(result) if cancelled => OptimizeRunOutcome::Cancelled { result },
Ok(result) => OptimizeRunOutcome::Completed { result },
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_cut_core::{AngleChange, MissingAnchor, ObjectiveComponents, OptimizeOutcome};
#[test]
fn the_start_prefix_is_dropped_during_polish_and_kept_for_coordinate() {
let start = Some(StartProgress {
index: 2,
count: 5,
best_fast_score: 1.0,
});
assert!(start_for_stage(SearchStage::Coordinate, start).is_some());
assert!(start_for_stage(SearchStage::Screening, start).is_some());
assert!(start_for_stage(SearchStage::Polish, start).is_none());
assert!(start_for_stage(SearchStage::FinalFull, start).is_none());
}
fn dummy_outcome(changed: bool) -> OptimizeOutcome {
let components = ObjectiveComponents {
windowing_pct: 10.0,
extinction_pct: 10.0,
tilt_brilliance_pct: 80.0,
};
OptimizeOutcome {
before: components,
before_score: 10.0,
before_yield_loss_pct: 0.0,
after: components,
after_score: 10.0,
after_yield_loss_pct: 0.0,
evaluations: 4,
changes: if changed {
vec![AngleChange {
index: 0,
from_deg: 30.0,
to_deg: 31.0,
}]
} else {
Vec::new()
},
cancelled: false,
polish_evaluations: 0,
polish_improvement: 0.0,
}
}
fn dummy_result(changed: bool) -> OptimizeResult {
OptimizeResult {
outcome: dummy_outcome(changed),
mast_changes: Vec::new(),
candidates: Vec::new(),
tone_before: None,
tone_goal: None,
lighting: indicatrix_cut_core::CANONICAL_LIGHTING_PRESET,
starts_run: 1,
best_start: 0,
}
}
#[test]
fn cancel_sets_the_flag_the_worker_and_ticker_threads_poll() {
let flag = Arc::new(AtomicBool::new(false));
let handle = OptimizeSolveHandle {
cancel: Arc::clone(&flag),
};
assert!(!flag.load(Ordering::Relaxed));
handle.cancel();
assert!(flag.load(Ordering::Relaxed));
}
#[test]
fn run_outcome_for_reports_failed_regardless_of_cancelled_when_the_design_does_not_solve() {
let error = DesignSolveError::MissingAnchor(MissingAnchor {
blocks: vec![indicatrix::geometry::meet_solver::Block::Crown],
});
for cancelled in [true, false] {
let outcome = run_outcome_for(cancelled, Err(error.clone()));
assert!(matches!(outcome, OptimizeRunOutcome::Failed { .. }));
}
}
#[test]
fn run_outcome_for_preserves_the_real_partial_result_when_cancelled() {
let outcome = run_outcome_for(true, Ok(dummy_result(true)));
match outcome {
OptimizeRunOutcome::Cancelled { result } => {
assert_eq!(result.outcome.changes.len(), 1);
assert_eq!(result.outcome.evaluations, 4);
}
_ => panic!("expected Cancelled"),
}
}
#[test]
fn run_outcome_for_reports_completed_when_not_cancelled() {
let outcome = run_outcome_for(false, Ok(dummy_result(false)));
assert!(matches!(outcome, OptimizeRunOutcome::Completed { .. }));
}
#[test]
fn optimize_job_sizes_the_material_by_the_girdle_diameter() {
let mut design = indicatrix_editor::EditorSession::fresh().design;
let coloured = || {
GemMaterial::sapphire().with_body_color(
indicatrix::optics::materials::body_color::BODY_COLOR_PRESETS[1].absorption_rgb,
)
};
let mut options = OptimizeOptions::default();
let unsized_material = prepare_run_inputs(&design, coloured(), &mut options, None)
.expect("the fresh design solves");
let face_up = indicatrix::render_setup::MODEL_UNIT_FACE_UP_PATH;
assert!((unsized_material.absorption_path_scale - 1.0 / face_up).abs() < 1e-7);
design.girdle_diameter_mm = Some(6.5);
let sized = prepare_run_inputs(&design, coloured(), &mut options, None)
.expect("the fresh design solves");
assert!(
(sized.absorption_path_scale - 6.5 / 7.0 / face_up).abs() < 1e-6,
"{}",
sized.absorption_path_scale
);
}
#[test]
fn progress_is_copy_not_just_clone() {
let progress = OptimizeSolveProgress {
evaluations: 12,
max_evaluations: 200,
stage: SearchStage::Coordinate,
start: None,
elapsed: Duration::from_secs(1),
};
let copied = progress;
assert_eq!(copied.evaluations, progress.evaluations);
}
#[test]
fn optimize_job_resolves_no_material_selection_to_diamond() {
let lookup = EditorMaterialLookup::new(&[]);
let material = resolved_gem_material(&MaterialSelection::none(), &lookup);
assert_eq!(material.name, GemMaterial::diamond().name);
}
}