From 22a2a35c71b6eb4410edab5f516ddfdb47ff9925 Mon Sep 17 00:00:00 2001 From: Bruno Borges Paschoalinoto Date: Thu, 2 May 2024 12:31:58 -0300 Subject: [PATCH] running decks and flagging values work --- nastester/src/app.rs | 109 +++++++++++------------ nastester/src/gui.rs | 188 +++++++++++++++++++++++++++------------ nastester/src/results.rs | 49 +++++++++- nastester/src/running.rs | 16 ++-- 4 files changed, 243 insertions(+), 119 deletions(-) diff --git a/nastester/src/app.rs b/nastester/src/app.rs index 70068fb..6583a7e 100644 --- a/nastester/src/app.rs +++ b/nastester/src/app.rs @@ -3,7 +3,6 @@ //! fully-interactive (like the GUI). use std::collections::BTreeMap; -use std::collections::BTreeSet; use std::collections::VecDeque; use std::path::PathBuf; use std::sync::atomic::AtomicUsize; @@ -12,8 +11,6 @@ use std::sync::Mutex; use std::thread; use f06::prelude::*; -use rayon::iter::IntoParallelRefIterator; -use rayon::iter::ParallelIterator; use serde::Deserialize; use serde::Serialize; use uuid::Uuid; @@ -110,6 +107,24 @@ impl AppState { )) } + /// Returns the names of solvers, in order. + pub(crate) fn solvers_names(&self) -> impl Iterator { + let ordering: BTreeMap<&str, Uuid> = self.solvers.iter() + .map(|(u, d)| (d.nickname.as_str(), *u)) + .collect(); + return ordering.into_iter(); + } + + /// Iterates over solvers by name. + pub(crate) fn solvers_by_name( + &self + ) -> impl Iterator { + return self.solvers_names().map(|(_, u)| ( + u, + self.solvers.get(&u).expect("invalid solver UUID") + )) + } + /// Returns a deck and its results. pub(crate) fn get_deck( &mut self, @@ -181,22 +196,45 @@ impl AppState { return self.runner.get_solver(pick).and_then(|u| self.solvers.get(&u)); } - /// Enqueues a run for a single deck. Does nothing if there isn't a solver - /// picked yet. - pub(crate) fn enqueue_deck(&mut self, deck_uuid: Uuid, pick: SolverPick) { + /// Generates a job for a deck and a solver pick. + pub(crate) fn gen_job( + &mut self, + deck_uuid: Uuid, + pick: SolverPick + ) -> Option { if let Some(solver) = self.get_solver(pick).cloned() { if let Some((deck, res)) = self.get_deck(deck_uuid) { - let job = Job { + return Some(Job { deck: deck.clone(), pick, target: res, solver: solver.clone(), - }; - self.runner.job_queue.lock().expect("mutex poisoned").push_back(job); - self.set_run_state(deck_uuid, pick, RunState::Enqueued); + crit_sets: self.suite.criteria_sets.clone() + }); } } + return None; + } + /// Enqueues a run for a single deck. Does nothing if there isn't a solver + /// picked yet. This might lock, use enqueue_deck safe if in doubt. + pub(crate) fn enqueue_deck(&mut self, deck_uuid: Uuid, pick: SolverPick) { + if let Some(job) = self.gen_job(deck_uuid, pick) { + self.runner.job_queue.lock().expect("mutex poisoned").push_back(job); + self.set_run_state(deck_uuid, pick, RunState::Enqueued); + } + } + + /// Enqueues a deck in a separate thread to prevent UI locking. + pub(crate) fn enqueue_deck_safe(&mut self, deck: Uuid, pick: SolverPick) { + let queue = self.runner.job_queue.clone(); + let state = self.get_run_state(deck); + if let Some(job) = self.gen_job(deck, pick) { + thread::spawn(move || { + queue.lock().unwrap().push_back(job); + *state.lock().unwrap().get_mut(pick) = RunState::Enqueued; + }); + } } /// Enqueues all jobs for a solver pick. @@ -258,54 +296,15 @@ impl AppState { let critsets = self.suite.criteria_sets.clone(); if let Some((deck, results_mtx)) = self.get_deck(deck) { let mut results = results_mtx.lock().expect("mutex poisoned"); - results.flagged.clear(); - let pair = (&results.ref_f06, &results.test_f06); - if let (RunState::Finished(r), RunState::Finished(t)) = pair { - for (exn, crit_uuid) in deck.extractions.iter() { - if let Some(critset) = crit_uuid.and_then(|u| critsets.get(&u)) { - let in_ref = exn.lookup(r).collect::>(); - let in_test = exn.lookup(t).collect::>(); - let in_either = in_ref.union(&in_test).collect::>(); - let dxn = in_ref.symmetric_difference(&in_test) - .collect::>(); - let mut flagged: BTreeSet = BTreeSet::new(); - if exn.dxn == DisjunctionBehaviour::Flag { - flagged.extend(dxn); - } - let get = |f: &F06File, ix: &DatumIndex| -> Option { - let v = ix.get_from(f); - if v.is_err() && exn.dxn == DisjunctionBehaviour::AssumeZeroes { - return Some(0.0.into()); - } else { - return Some(v.unwrap()); - } - }; - for ix in in_either { - let val_ref = get(r, ix); - let val_test = get(t, ix); - if let (Some(rv), Some(tv)) = (val_ref, val_test) { - if critset.criteria.check(rv.into(), tv.into()).is_some() { - flagged.insert(*ix); - } - } - } - results_mtx.lock().expect("poisoned").flagged.push(Some(flagged)); - } else { - results_mtx.lock().expect("mutex poisoned").flagged.push(None); - } - } - } + results.recompute_flagged(deck, &critsets); } } - /// Re-computes all flagged values in a background thread. + /// Re-computes all flagged values in the UI thread. pub(crate) fn recompute_all_flagged(&mut self) { - let decks = self.suite.decks.keys().cloned().collect::>(); - let mref = Arc::new(Mutex::new(self)); - decks.par_iter() - .map(|u| (u, mref.clone())) - .for_each(|(u, s)| { - if let Ok(mut s) = s.lock() { s.recompute_flagged(*u); } - }) + let uuids = self.suite.decks.keys().copied().collect::>(); + for deck in uuids { + self.recompute_flagged(deck); + } } } diff --git a/nastester/src/gui.rs b/nastester/src/gui.rs index acc6755..adfaa33 100644 --- a/nastester/src/gui.rs +++ b/nastester/src/gui.rs @@ -9,18 +9,19 @@ use std::path::PathBuf; use std::str::FromStr; use egui::{ - Align, Color32, ComboBox, Context, DragValue, Id, Layout, TextStyle, Ui, Visuals, WidgetText + Align, Color32, ComboBox, Context, DragValue, Id, Layout, RichText, + TextStyle, Ui, Visuals, WidgetText }; use egui_extras::{Column, TableBuilder}; use f06::blocks::types::BlockType; use f06::prelude::*; -use log::info; +use log::*; use native_dialog::{MessageDialog, MessageType}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::app::*; -use crate::results::RunState; +use crate::results::*; use crate::running::*; use crate::suite::*; @@ -188,7 +189,7 @@ impl Gui { } let binary = dialog.pick_file(); if let Some(bin) = binary { - log::info!("Added deck from file {}.", bin.display()); + log::info!("Added solver binary {}.", bin.display()); self.state.add_solver_bin(bin); return Ok(true); } else { @@ -213,6 +214,34 @@ impl Gui { } } + /// Change a deck's file path. Returns whether it's been changed. + fn change_deck(&mut self, deck: Uuid) -> Result> { + let deck_file = rfd::FileDialog::new() + .add_filter("NASTRAN input files", DECK_EXTENSIONS) + .add_filter("All files", &["*"]) + .set_title("Choose input files...") + .set_can_create_directories(true) + .pick_file(); + if let Some(v) = deck_file { + if let Some(d) = self.state.suite.decks.get_mut(&deck) { + log::info!( + "Deck {} path changed from {} to {}.", + deck, + d.in_file.display(), + v.display() + ); + d.in_file = v; + self.suite_clean = false; + return Ok(true); + } + log::warn!("Tried to change path for non-existing deck!"); + return Ok(false); + } else { + log::info!("Deck addition cancelled by user or no file(s) selected."); + return Ok(false); + } + } + /// Run a function and, if an error happens, do a pop-up. fn show_error(err: Box) { MessageDialog::new() @@ -415,6 +444,9 @@ impl Gui { egui::menu::bar(ui, |ui| { // suite menu ui.menu_button("Suite", |ui| { + if ui.button("New").clicked() { + self.state = AppState::default(); + } if ui.button("Save").clicked() { self.try_run(ui, Gui::save_suite); } @@ -441,12 +473,9 @@ impl Gui { lbl: &str, tgt: &mut Option | { - let label = format!( - "{}{}", - lbl, - if opt == *tgt { " (selected)" } else { "" }, - ); - if ui.button(label).clicked() { + let mut rt = RichText::new(lbl); + if opt == *tgt { rt = rt.strong(); } + if ui.button(rt).clicked() { *tgt = opt } }; @@ -461,23 +490,27 @@ impl Gui { if ui.button("Add F06 directory...").clicked() { self.try_run(ui, Gui::add_solver_dir); } + let snames = self.state.solvers_names() + .map(|(s, u)| (s.to_owned(), u)) + .collect::>(); ui.menu_button("Set reference solver", |ui| { btn(ui, None, "", &mut self.state.runner.ref_solver); - for (u, s) in self.state.solvers.iter() { + for (s, u) in snames.iter() { btn( ui, Some(*u), - s.nickname.as_str(), &mut self.state.runner.ref_solver + s.as_str(), + &mut self.state.runner.ref_solver ); } }); ui.menu_button("Set solver under test", |ui| { btn(ui, None, "", &mut self.state.runner.test_solver); - for (u, s) in self.state.solvers.iter() { + for (s, u) in snames.iter() { btn( ui, Some(*u), - s.nickname.as_str(), + s.as_str(), &mut self.state.runner.test_solver ); } @@ -491,6 +524,11 @@ impl Gui { }); // run menu ui.menu_button("Run!", |ui| { + if ui.button("Run all on both solvers").clicked() { + self.state.enqueue_solver(SolverPick::Reference); + self.state.enqueue_solver(SolverPick::Testing); + self.state.run_queue(); + } if ui.button("Run all on reference solver").clicked() { self.state.enqueue_solver(SolverPick::Reference); self.state.run_queue(); @@ -575,7 +613,7 @@ impl Gui { .auto_shrink(true) .striped(true) .cell_layout(cells) - .column(Column::remainder().resizable(true)) + .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) .column(Column::auto().resizable(true)) @@ -599,66 +637,106 @@ impl Gui { ui.label("Ready"); } else { ui.add(egui::Label::new( - WidgetText::from("Missing").strong().color(Color32::RED)) + WidgetText::from("Missing!").strong().color(Color32::RED)) ); + if ui.button("Locate...").clicked() { + self.change_deck(*uuid).ok(); + } }); // results - if let Some(res) = results { - let lblres = |ui: &mut Ui, res: &RunState| { - let (text, color) = match res { - RunState::Ready => { - ("Not yet run".to_owned(), Color32::LIGHT_YELLOW) - }, - RunState::Enqueued => { - ("In queue".to_owned(), Color32::LIGHT_YELLOW) - }, - RunState::Running => { - ("Running".to_owned(), Color32::YELLOW) - }, - RunState::Finished(_) => { - ("Finished".to_owned(), Color32::DARK_GREEN) - }, - RunState::Error(e) => { - (format!("Error: {}", e), Color32::RED) - }, - }; - ui.add(egui::Label::new( - WidgetText::from(text).color(color)) - ); + let mut lblres = |ui: &mut Ui, res: &RunState, p: SolverPick| { + let (text, color) = match res { + RunState::Ready => { + if ui.button("Run").clicked() { + self.state.enqueue_deck_safe(*uuid, p); + self.state.run_queue(); + } + return; + }, + RunState::Enqueued => { + ("In queue".to_owned(), Color32::LIGHT_YELLOW) + }, + RunState::Running => { + ("Running".to_owned(), Color32::YELLOW) + }, + RunState::Finished(_) => { + ("Finished".to_owned(), Color32::DARK_GREEN) + }, + RunState::Error(e) => { + (format!("Error: {}", e), Color32::RED) + }, }; - if let Ok(handle) = res.try_lock() { + ui.add(egui::Label::new( + WidgetText::from(text).color(color)) + ); + }; + if let Some(res) = results { + if let Ok(h) = res.try_lock() { + // got lock on results // reference run - row.col(|ui| lblres(ui, &handle.ref_f06)); + row.col(|ui| lblres(ui, &h.ref_f06, SolverPick::Reference)); // test run - row.col(|ui| lblres(ui, &handle.test_f06)); + row.col(|ui| lblres(ui, &h.test_f06, SolverPick::Testing)); // flags - row.col(|ui| { - let nflags: usize = handle.flagged - .iter() - .map(|v| match v { - Some(ref m) => m.len(), - None => 0, - }).sum(); - ui.label(format!("{} values", nflags)); - }); + match (&h.ref_f06, &h.test_f06) { + (RunState::Finished(_), RunState::Finished(_)) => { + row.col(|ui| { + let nflags: usize = h.flagged + .iter() + .map(|v| match v { + Some(ref m) => m.len(), + None => 0, + }).sum(); + ui.label(format!("{} values", nflags)); + }); + }, + _ => { + row.col(|ui| { ui.label("(requires both runs)"); }); + } + }; } else { + // no lock on results // reference run - row.col(|ui| lblres(ui, &RunState::Running)); + row.col(|ui| lblres( + ui, + &RunState::Running, + SolverPick::Reference + )); // test run - row.col(|ui| lblres(ui, &RunState::Running)); + row.col(|ui| lblres( + ui, + &RunState::Running, + SolverPick::Testing + )); + // flags + row.col(|ui| { ui.label("(running)"); }); } } else { + // no results, so it's just ready // reference run - row.col(|ui| { ui.label("Not yet run"); }); + row.col(|ui| lblres( + ui, + &RunState::Ready, + SolverPick::Reference + )); // test run - row.col(|ui| { ui.label("Not yet run"); }); + row.col(|ui| lblres( + ui, + &RunState::Ready, + SolverPick::Testing + )); + // flags + row.col(|ui| { ui.label("(requires both runs)"); }); } // actions row.col(|ui| { ui.horizontal(|ui| { - if ui.button("Extractions...").clicked() { + if ui.button("Edit extractions").clicked() { self.switch_to(View::Extractions(*uuid)); } + if ui.button("Change file path").clicked() { + self.change_deck(*uuid).ok(); + } if ui.button("Remove").clicked() { self.state.suite.decks.remove(uuid); self.suite_clean = false; diff --git a/nastester/src/results.rs b/nastester/src/results.rs index c74c8d4..caecaa0 100644 --- a/nastester/src/results.rs +++ b/nastester/src/results.rs @@ -1,11 +1,13 @@ //! This submodule defines structures for testing results. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use f06::prelude::*; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::running::SolverPick; +use crate::suite::*; /// Enum containing a deck status. #[derive(Clone, Debug, Serialize, Deserialize, Default)] @@ -59,4 +61,49 @@ impl DeckResults { SolverPick::Testing => &mut self.test_f06, }; } + + /// Recomputes the flagged values. + pub(crate) fn recompute_flagged( + &mut self, + deck: &Deck, + crit_sets: &BTreeMap + ) { + self.flagged.clear(); + let pair = (&self.ref_f06, &self.test_f06); + if let (RunState::Finished(r), RunState::Finished(t)) = pair { + for (exn, crit_uuid) in deck.extractions.iter() { + if let Some(critset) = crit_uuid.and_then(|u| crit_sets.get(&u)) { + let in_ref = exn.lookup(r).collect::>(); + let in_test = exn.lookup(t).collect::>(); + let in_either = in_ref.union(&in_test).collect::>(); + let dxn = in_ref.symmetric_difference(&in_test) + .collect::>(); + let mut flagged: BTreeSet = BTreeSet::new(); + if exn.dxn == DisjunctionBehaviour::Flag { + flagged.extend(dxn); + } + let get = |f: &F06File, ix: &DatumIndex| -> Option { + let v = ix.get_from(f); + if v.is_err() && exn.dxn == DisjunctionBehaviour::AssumeZeroes { + return Some(0.0.into()); + } else { + return Some(v.unwrap()); + } + }; + for ix in in_either { + let val_ref = get(r, ix); + let val_test = get(t, ix); + if let (Some(rv), Some(tv)) = (val_ref, val_test) { + if critset.criteria.check(rv.into(), tv.into()).is_some() { + flagged.insert(*ix); + } + } + } + self.flagged.push(Some(flagged)); + } else { + self.flagged.push(None); + } + } + } + } } diff --git a/nastester/src/running.rs b/nastester/src/running.rs index ca2badb..cd717c0 100644 --- a/nastester/src/running.rs +++ b/nastester/src/running.rs @@ -172,7 +172,7 @@ impl RunnableSolver { let stdout = File::create(file_in_tmp("stdout.log".as_ref()))?; let stderr = File::create(file_in_tmp("stderr.log".as_ref()))?; let pc = PopenConfig { - stdin: subprocess::Redirection::None, + stdin: subprocess::Redirection::Pipe, stdout: subprocess::Redirection::File(stdout), stderr: subprocess::Redirection::File(stderr), executable: Some(bin.clone().into_os_string()), @@ -223,19 +223,19 @@ pub(crate) struct Job { /// The pick of solver for the job. pub(crate) pick: SolverPick, /// The target to write results to. - pub(crate) target: Arc> + pub(crate) target: Arc>, + /// A copy of the crit-sets at the instant of job creation. + pub(crate) crit_sets: BTreeMap } impl Job { /// Runs this job. This blocks! Careful. pub(crate) fn run(&self) { - let set = |state: RunState| { - let mut s = self.target.lock().expect("mutex poisoned"); - *s.get_mut(self.pick) = state; - }; - set(RunState::Running); + let mut h = self.target.lock().expect("mutex poisoned"); + *h.get_mut(self.pick) = RunState::Running; let res = self.solver.make_f06(&self.deck).map_err(|e| e.to_string()); - set(res.into()); + *h.get_mut(self.pick) = res.into(); + h.recompute_flagged(&self.deck, &self.crit_sets) } }