use super::solve_service::{
SolveKind, SolveOutcome, SolveRequest, SolveResult, SolveService, VerifiedSolve,
};
use indicatrix::geometry::{
meet_solver::{SolveError, SolvedTier, VerifiedSolveReport},
stone_metrics::ExternalProportions,
};
use indicatrix_cut_core::Design;
use slint::{ComponentHandle, Weak};
use std::{
sync::{Arc, Mutex, PoisonError},
time::{Duration, Instant},
};
pub struct DeepSolveHandle {
handle: super::solve_service::SolveHandle,
_service: SolveService,
}
impl DeepSolveHandle {
pub fn cancel(&self) {
self.handle.cancel();
}
}
#[derive(Debug, Clone, Copy)]
pub struct DeepSolveProgress {
pub elapsed: Duration,
}
pub enum DeepSolveOutcome {
Completed {
solved: Vec<SolvedTier>,
report: VerifiedSolveReport,
baseline: Option<Vec<SolvedTier>>,
},
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TierMastDelta {
pub tier_index: usize,
pub before_mast: f64,
pub after_mast: f64,
}
impl TierMastDelta {
#[must_use]
pub fn delta(&self) -> f64 {
self.after_mast - self.before_mast
}
}
const MAST_DELTA_EPSILON: f64 = 1e-6;
#[must_use]
pub fn tier_mast_deltas(current: &[SolvedTier], verified: &[SolvedTier]) -> Vec<TierMastDelta> {
current
.iter()
.zip(verified.iter())
.enumerate()
.filter_map(|(tier_index, (before, after))| {
let delta = after.mast - before.mast;
(delta.abs() > MAST_DELTA_EPSILON).then_some(TierMastDelta {
tier_index,
before_mast: before.mast,
after_mast: after.mast,
})
})
.collect()
}
#[must_use]
pub fn format_tier_mast_deltas(deltas: &[TierMastDelta]) -> String {
if deltas.is_empty() {
return String::new();
}
let tiers: Vec<String> = deltas
.iter()
.map(|d| format!("tier {} ({:+.4})", d.tier_index, d.delta()))
.collect();
format!(" Disagrees with the last solve on: {}.", tiers.join(", "))
}
#[must_use]
pub fn format_verification_targets(targets: &ExternalProportions, source: Option<&str>) -> String {
let mut parts = Vec::new();
if let Some(v) = targets.vol_w3 {
parts.push(format!("Vol/W3 {v:.4}"));
}
if let Some(v) = targets.lw {
parts.push(format!("L/W {v:.4}"));
}
if let Some(v) = targets.cw {
parts.push(format!("C/W {v:.4}"));
}
if let Some(v) = targets.pw {
parts.push(format!("P/W {v:.4}"));
}
if let Some(v) = targets.hw {
parts.push(format!("H/W {v:.4}"));
}
if parts.is_empty() {
return String::new();
}
let source_clause = source.map_or_else(String::new, |name| format!(" (from \"{name}\")"));
format!(
" Verified against the printed proportions{source_clause}: {}.",
parts.join(", ")
)
}
#[must_use]
pub const fn edited_since_load_caveat(edited_since_load: bool) -> &'static str {
if edited_since_load {
" Note: this design has been edited since it was loaded -- these targets \
may no longer describe the design you are holding."
} else {
""
}
}
pub fn spawn_deep_solve<T, P, D>(
ui_weak: Weak<T>,
design: Design,
targets: ExternalProportions,
need_baseline: bool,
on_progress: P,
on_done: D,
) -> DeepSolveHandle
where
T: ComponentHandle + 'static,
P: Fn(&T, DeepSolveProgress) + Send + 'static + Clone,
D: FnOnce(&T, DeepSolveOutcome) + Send + 'static,
{
let start = Instant::now();
let on_done = Arc::new(Mutex::new(Some(on_done)));
let service = SolveService::new(
ui_weak,
move |ui: &T, report: super::solve_service::SolveProgressReport| {
debug_assert_eq!(
report.generation, 0,
"this module always submits generation 0"
);
tracing::trace!(
phase = ?report.progress.phase,
sweep = report.progress.sweep,
"deep solve progress"
);
on_progress(
ui,
DeepSolveProgress {
elapsed: start.elapsed(),
},
);
},
move |ui: &T, result: SolveResult| {
debug_assert_eq!(
result.generation, 0,
"this module always submits generation 0"
);
tracing::info!(
elapsed_ms = result.elapsed.as_millis(),
"deep solve finished"
);
let SolveOutcome::Verified(verified) = result.outcome else {
unreachable!("spawn_deep_solve only ever submits SolveKind::Verified");
};
let outcome = outcome_for(verified);
let done = on_done
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
if let Some(done) = done {
done(ui, outcome);
}
},
);
let handle = service.submit(SolveRequest {
design: Arc::new(design),
generation: 0,
kind: SolveKind::Verified {
targets,
compute_baseline: need_baseline,
},
});
debug_assert_eq!(
handle.generation, 0,
"this module always submits generation 0"
);
DeepSolveHandle {
handle,
_service: service,
}
}
fn outcome_for(result: Result<VerifiedSolve, SolveError>) -> DeepSolveOutcome {
match result {
Ok(VerifiedSolve {
solved,
report,
baseline,
}) => DeepSolveOutcome::Completed {
solved,
report,
baseline,
},
Err(
SolveError::Cancelled
| SolveError::TooManyPlanes { .. }
| SolveError::NonFiniteInput { .. },
) => DeepSolveOutcome::Cancelled,
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::SolveStrategy;
fn dummy_report(accepted: bool) -> VerifiedSolveReport {
VerifiedSolveReport {
initial_score: 0.5,
score_after_calibration: 0.3,
final_score: 0.1,
accepted,
overrides_applied: 2,
anchor_moves_applied: 1,
pipeline_runs: 12,
}
}
fn dummy_solved_tier(mast: f64) -> SolvedTier {
SolvedTier {
mast,
strategy: SolveStrategy::DependencyOrder,
detail: String::new(),
}
}
#[test]
fn outcome_for_reports_completed_with_the_report_when_ok() {
let report = dummy_report(false);
let solved = vec![dummy_solved_tier(1.0)];
let outcome = outcome_for(Ok(VerifiedSolve {
solved,
report,
baseline: None,
}));
match outcome {
DeepSolveOutcome::Completed {
solved: s,
report: r,
baseline,
} => {
assert_eq!(r.accepted, report.accepted);
assert_eq!(r.pipeline_runs, report.pipeline_runs);
assert_eq!(r.final_score, report.final_score);
assert_eq!(s.len(), 1);
assert!(baseline.is_none());
}
DeepSolveOutcome::Cancelled => panic!("expected Completed, got Cancelled"),
}
}
#[test]
fn outcome_for_carries_a_worker_computed_baseline_through_to_completed() {
let report = dummy_report(true);
let solved = vec![dummy_solved_tier(1.0)];
let baseline = vec![dummy_solved_tier(0.9)];
let outcome = outcome_for(Ok(VerifiedSolve {
solved,
report,
baseline: Some(baseline.clone()),
}));
match outcome {
DeepSolveOutcome::Completed {
baseline: Some(b), ..
} => {
assert_eq!(b.len(), baseline.len());
assert!((b[0].mast - baseline[0].mast).abs() < 1e-9);
}
DeepSolveOutcome::Completed { baseline: None, .. } => {
panic!("expected the worker-computed baseline to survive");
}
DeepSolveOutcome::Cancelled => panic!("expected Completed, got Cancelled"),
}
}
#[test]
fn outcome_for_reports_cancelled_on_a_cancelled_solve_error() {
let outcome = outcome_for(Err(SolveError::Cancelled));
assert!(matches!(outcome, DeepSolveOutcome::Cancelled));
}
#[test]
fn outcome_for_reports_cancelled_on_the_unreachable_in_practice_too_many_planes_case() {
let outcome = outcome_for(Err(SolveError::TooManyPlanes {
planes: 999,
max: 400,
}));
assert!(matches!(outcome, DeepSolveOutcome::Cancelled));
}
#[test]
fn tier_mast_deltas_keeps_only_tiers_that_actually_moved() {
let current = vec![
dummy_solved_tier(1.0),
dummy_solved_tier(2.0),
dummy_solved_tier(3.0),
];
let verified = vec![
dummy_solved_tier(1.0),
dummy_solved_tier(2.5),
dummy_solved_tier(3.0),
];
let deltas = tier_mast_deltas(¤t, &verified);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].tier_index, 1);
assert_eq!(deltas[0].before_mast, 2.0);
assert_eq!(deltas[0].after_mast, 2.5);
assert!((deltas[0].delta() - 0.5).abs() < 1e-9);
}
#[test]
fn tier_mast_deltas_is_empty_when_every_tier_matches() {
let masts = vec![dummy_solved_tier(1.0), dummy_solved_tier(2.0)];
let deltas = tier_mast_deltas(&masts, &masts.clone());
assert_eq!(
deltas.len(),
0,
"identical solves must report no disagreement"
);
}
#[test]
fn tier_mast_deltas_stops_at_the_shorter_slice_instead_of_panicking() {
let current = vec![dummy_solved_tier(1.0), dummy_solved_tier(2.0)];
let verified = vec![dummy_solved_tier(9.0)];
let deltas = tier_mast_deltas(¤t, &verified);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].tier_index, 0);
}
#[test]
fn format_tier_mast_deltas_is_empty_string_when_no_tiers_disagree() {
assert_eq!(format_tier_mast_deltas(&[]), "");
}
#[test]
fn format_tier_mast_deltas_names_the_tier_and_signed_movement() {
let deltas = vec![TierMastDelta {
tier_index: 3,
before_mast: 1.0,
after_mast: 1.25,
}];
let text = format_tier_mast_deltas(&deltas);
assert!(text.contains("tier 3"));
assert!(text.contains("+0.2500"));
}
#[test]
fn format_verification_targets_names_only_the_fields_that_are_set() {
let targets = ExternalProportions {
vol_w3: Some(0.6013),
lw: None,
cw: Some(0.1489),
pw: None,
hw: None,
};
let text = format_verification_targets(&targets, None);
assert!(text.contains("Vol/W3 0.6013"));
assert!(text.contains("C/W 0.1489"));
assert!(!text.contains("L/W"));
assert!(!text.contains("P/W"));
assert!(!text.contains("H/W"));
}
#[test]
fn format_verification_targets_is_empty_when_nothing_is_set() {
assert_eq!(
format_verification_targets(&ExternalProportions::default(), Some("RBC-445.asc")),
"",
"no source clause should print when there are no targets to report at all"
);
}
#[test]
fn format_verification_targets_names_the_source_when_given_one() {
let targets = ExternalProportions {
vol_w3: Some(0.6013),
..ExternalProportions::default()
};
let text = format_verification_targets(&targets, Some("RBC-445.asc"));
assert!(text.contains("RBC-445.asc"), "got: {text}");
}
#[test]
fn format_verification_targets_omits_the_source_clause_when_none() {
let targets = ExternalProportions {
vol_w3: Some(0.6013),
..ExternalProportions::default()
};
let text = format_verification_targets(&targets, None);
assert!(!text.contains("(from"), "got: {text}");
}
#[test]
fn edited_since_load_caveat_is_empty_when_not_edited() {
assert_eq!(edited_since_load_caveat(false), "");
}
#[test]
fn edited_since_load_caveat_warns_when_edited() {
let text = edited_since_load_caveat(true);
assert!(text.contains("edited since it was loaded"));
}
#[test]
fn progress_carries_the_elapsed_duration_it_was_built_with() {
let progress = DeepSolveProgress {
elapsed: Duration::from_millis(1500),
};
assert_eq!(progress.elapsed, Duration::from_millis(1500));
}
#[test]
fn progress_is_copy_not_just_clone() {
let progress = DeepSolveProgress {
elapsed: Duration::from_secs(3),
};
let copied = progress;
assert_eq!(copied.elapsed, progress.elapsed);
}
}