From e0c09c1683a45d1a37c246bf9ce7540cd939e6ee Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Tue, 3 Oct 2023 18:58:08 +0200 Subject: [PATCH 01/37] WIP --- Cargo.toml | 2 ++ src/entrypoints.rs | 1 + src/logger.rs | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5014716..971e5ca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ repository = "https://github.com/LAPKB/NPcore" exclude = [".github/*", ".vscode/*"] [dependencies] +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } dashmap = "5.5.3" #cached = "0.26.2" lazy_static = "1.4.0" diff --git a/src/entrypoints.rs b/src/entrypoints.rs index f5841a36..c33d9d79 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -59,6 +59,7 @@ where let now = Instant::now(); let settings = settings::run::read(settings_path); logger::setup_log(&settings); + tracing::info!("Starting NPcore"); let (tx, rx) = mpsc::unbounded_channel::(); let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), tx); // Spawn new thread for TUI diff --git a/src/logger.rs b/src/logger.rs index 5ddbea7a..e9e1b016 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,23 +1,23 @@ use crate::prelude::settings::run::Data; -use log::LevelFilter; -use log4rs::append::file::FileAppender; -use log4rs::config::{Appender, Config, Root}; -use log4rs::encode::pattern::PatternEncoder; -use std::fs; +use tracing::Level; +use tracing_subscriber::fmt; pub fn setup_log(settings: &Data) { if let Some(log_path) = &settings.parsed.paths.log_out { - if fs::remove_file(log_path).is_ok() {}; - let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{l}: {m}\n"))) - .build(log_path) - .unwrap(); + if std::fs::remove_file(log_path).is_ok() {}; - let config = Config::builder() - .appender(Appender::builder().build("logfile", Box::new(logfile))) - .build(Root::builder().appender("logfile").build(LevelFilter::Info)) - .unwrap(); + let subscriber = fmt::Subscriber::builder() + .with_max_level(Level::INFO) + .with_writer(std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(log_path) + .unwrap()) + .with_ansi(false) + .finish(); - log4rs::init_config(config).unwrap(); - }; + tracing::subscriber::set_global_default(subscriber) + .expect("Setting default subscriber failed"); + } } From 45e88c082dac1889759586f050af7474782bfa92 Mon Sep 17 00:00:00 2001 From: "Julian D. Otalvaro" Date: Thu, 5 Oct 2023 22:29:04 -0500 Subject: [PATCH 02/37] alternate way to define the execution file, tried to move as much as I could to the lib --- .gitignore | 1 - examples/bimodal_ke/main.rs | 91 +++++++++++++++--------------- src/algorithms.rs | 2 +- src/algorithms/npag.rs | 8 +-- src/algorithms/postprob.rs | 8 +-- src/entrypoints.rs | 4 +- src/routines/output.rs | 8 +-- src/routines/simulation/predict.rs | 90 ++++++++++++++++++++++++----- 8 files changed, 136 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index caffd081..e9036c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,4 @@ meta*.csv /.idea stop .vscode - *.f90 \ No newline at end of file diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 542572d8..4b55a8e2 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -12,18 +12,18 @@ const ATOL: f64 = 1e-4; const RTOL: f64 = 1e-4; #[derive(Debug, Clone)] -struct Model<'a> { +struct Model { ke: f64, - _v: f64, - _scenario: &'a Scenario, + v: f64, + _scenario: Scenario, infusions: Vec, - cov: Option<&'a HashMap>, + cov: Option>, } type State = Vector1; type Time = f64; -impl ode_solvers::System for Model<'_> { +impl ode_solvers::System for Model { fn system(&self, t: Time, y: &State, dy: &mut State) { let ke = self.ke; @@ -47,51 +47,52 @@ impl ode_solvers::System for Model<'_> { #[derive(Debug, Clone)] struct Ode {} -impl Predict for Ode { - fn predict(&self, params: Vec, scenario: &Scenario) -> Vec { - let mut system = Model { +impl<'a> Predict<'a> for Ode { + type Model = Model; + type State = State; + fn initial_system(&self, params: &Vec, scenario: Scenario) -> Self::Model { + Model { ke: params[0], - _v: params[1], + v: params[1], _scenario: scenario, infusions: vec![], cov: None, - }; - // let scenario = scenario.reorder_with_lag(vec![]); - let mut yout = vec![]; - let mut x = State::new(0.0); - let mut index: usize = 0; - for block in &scenario.blocks { - system.cov = Some(&block.covs); - for event in &block.events { - if event.evid == 1 { - if event.dur.unwrap_or(0.0) > 0.0 { - //infusion - system.infusions.push(Infusion { - time: event.time, - dur: event.dur.unwrap(), - amount: event.dose.unwrap(), - compartment: event.input.unwrap() - 1, - }); - } else { - //dose - x[event.input.unwrap() - 1] += event.dose.unwrap(); - } - } else if event.evid == 0 { - //obs - yout.push(x[event.outeq.unwrap() - 1] / params[1]); - } - if let Some(next_time) = scenario.times.get(index + 1) { - //TODO: use the last dx as the initial one for the next simulation. - let mut stepper = - Dopri5::new(system.clone(), event.time, *next_time, 1e-3, x, RTOL, ATOL); - let _res = stepper.integrate(); - let y = stepper.y_out(); - x = *y.last().unwrap(); - } - index += 1; - } } - yout + } + fn get_output(&self, x: &Self::State, system: &Self::Model, outeq: usize) -> f64 { + let v = system.v; + match outeq { + 1 => x[0] / v, + _ => panic!("Invalid output equation"), + } + } + fn initial_state(&self) -> State { + State::default() + } + fn add_infusion(&self, mut system: Self::Model, infusion: Infusion) -> Model { + system.infusions.push(infusion); + system + } + fn add_covs(&self, mut system: Self::Model, cov: Option>) -> Model { + system.cov = cov; + system + } + fn add_dose(&self, mut state: Self::State, dose: f64, compartment: usize) -> Self::State { + state[compartment] += dose; + state + } + fn state_step( + &self, + mut x: Self::State, + system: Self::Model, + time: f64, + next_time: f64, + ) -> State { + let mut stepper = Dopri5::new(system, time, next_time, 1e-3, x, RTOL, ATOL); + let _res = stepper.integrate(); + let y = stepper.y_out(); + x = *y.last().unwrap(); + x } } diff --git a/src/algorithms.rs b/src/algorithms.rs index aacb057f..3155988c 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -24,7 +24,7 @@ pub fn initialize_algorithm( tx: mpsc::UnboundedSender, ) -> Box where - S: Predict + std::marker::Sync + Clone + 'static, + S: Predict<'static> + std::marker::Sync + Clone + 'static, { if std::path::Path::new("stop").exists() { match std::fs::remove_file("stop") { diff --git a/src/algorithms/npag.rs b/src/algorithms/npag.rs index b8fb6029..d233b359 100644 --- a/src/algorithms/npag.rs +++ b/src/algorithms/npag.rs @@ -23,7 +23,7 @@ const THETA_D: f64 = 1e-4; pub struct NPAG where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { engine: Engine, ranges: Vec<(f64, f64)>, @@ -51,7 +51,7 @@ where impl Algorithm for NPAG where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { fn fit(&mut self) -> NPResult { self.run() @@ -72,7 +72,7 @@ where impl NPAG where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { /// Creates a new NPAG instance. /// @@ -99,7 +99,7 @@ where settings: Data, ) -> Self where - S: Predict + std::marker::Sync, + S: Predict<'static> + std::marker::Sync, { Self { engine: sim_eng, diff --git a/src/algorithms/postprob.rs b/src/algorithms/postprob.rs index 44f31618..1b171200 100644 --- a/src/algorithms/postprob.rs +++ b/src/algorithms/postprob.rs @@ -18,7 +18,7 @@ use tokio::sync::mpsc::UnboundedSender; /// Reweights the prior probabilities to the observed data and error model pub struct POSTPROB where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { engine: Engine, psi: Array2, @@ -38,7 +38,7 @@ where impl Algorithm for POSTPROB where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { fn fit(&mut self) -> NPResult { self.run() @@ -59,7 +59,7 @@ where impl POSTPROB where - S: Predict + std::marker::Sync + Clone, + S: Predict<'static> + std::marker::Sync + Clone, { pub fn new( sim_eng: Engine, @@ -70,7 +70,7 @@ where settings: Data, ) -> Self where - S: Predict + std::marker::Sync, + S: Predict<'static> + std::marker::Sync, { Self { engine: sim_eng, diff --git a/src/entrypoints.rs b/src/entrypoints.rs index f5841a36..4fde5b4a 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -18,7 +18,7 @@ use tokio::sync::mpsc::{self}; pub fn simulate(engine: Engine, settings_path: String) -> Result<()> where - S: Predict + std::marker::Sync + std::marker::Send + 'static + Clone, + S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { let settings = settings::simulator::read(settings_path); let theta_file = File::open(settings.paths.theta).unwrap(); @@ -54,7 +54,7 @@ where } pub fn start(engine: Engine, settings_path: String) -> Result where - S: Predict + std::marker::Sync + std::marker::Send + 'static + Clone, + S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { let now = Instant::now(); let settings = settings::run::read(settings_path); diff --git a/src/routines/output.rs b/src/routines/output.rs index 368d24b1..8b30fca8 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -55,9 +55,9 @@ impl NPResult { } } - pub fn write_outputs(&self, write: bool, engine: &Engine) + pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine) where - S: Predict + std::marker::Sync + 'static + Clone + std::marker::Send, + S: Predict<'static> + std::marker::Sync + 'static + Clone + std::marker::Send, { if write { self.write_theta(); @@ -178,9 +178,9 @@ impl NPResult { } /// Writes the predictions - pub fn write_pred(&self, engine: &Engine) + pub fn write_pred<'a, S>(&self, engine: &Engine) where - S: Predict + std::marker::Sync + std::marker::Send + 'static + Clone, + S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { let result = (|| { let scenarios = self.scenarios.clone(); diff --git a/src/routines/simulation/predict.rs b/src/routines/simulation/predict.rs index 62152f80..81c10e03 100644 --- a/src/routines/simulation/predict.rs +++ b/src/routines/simulation/predict.rs @@ -1,3 +1,5 @@ +use crate::routines::datafile::CovLine; +use crate::routines::datafile::Infusion; use crate::routines::datafile::Scenario; use dashmap::mapref::entry::Entry; use dashmap::DashMap; @@ -6,6 +8,7 @@ use ndarray::parallel::prelude::*; use ndarray::prelude::*; use ndarray::Array1; use ndarray::{Array, Array2, Axis}; +use std::collections::HashMap; use std::error; use std::hash::{Hash, Hasher}; @@ -15,27 +18,79 @@ const CACHE_SIZE: usize = 1000000; /// where the second element of the tuple is the predicted values /// one per observation time in scenario and in the same order /// it is not relevant the outeq of the specific event. -pub trait Predict { - fn predict(&self, params: Vec, scenario: &Scenario) -> Vec; +pub trait Predict<'a> { + type Model: 'a + Clone; + type State; + fn initial_system(&self, params: &Vec, scenario: Scenario) -> Self::Model; + fn initial_state(&self) -> Self::State; + fn add_covs(&self, system: Self::Model, cov: Option>) -> Self::Model; + fn add_infusion(&self, system: Self::Model, infusion: Infusion) -> Self::Model; + fn add_dose(&self, state: Self::State, dose: f64, compartment: usize) -> Self::State; + fn get_output(&self, state: &Self::State, system: &Self::Model, outeq: usize) -> f64; + fn state_step( + &self, + state: Self::State, + system: Self::Model, + time: f64, + next_time: f64, + ) -> Self::State; } #[derive(Clone, Debug)] pub struct Engine where - S: Predict + Clone, + S: Predict<'static> + Clone, { ode: S, } impl Engine where - S: Predict + Clone, + S: Predict<'static> + Clone, { pub fn new(ode: S) -> Self { Self { ode } } - pub fn pred(&self, scenario: &Scenario, params: Vec) -> Vec { - self.ode.predict(params, scenario) + pub fn pred(&self, scenario: Scenario, params: Vec) -> Vec { + let system = self.ode.initial_system(¶ms, scenario.clone()); + let mut yout = vec![]; + let mut x = self.ode.initial_state(); + let mut index: usize = 0; + for block in scenario.blocks { + let mut system = self.ode.add_covs(system.clone(), Some(block.covs)); + for event in &block.events { + if event.evid == 1 { + if event.dur.unwrap_or(0.0) > 0.0 { + //infusion + system = self.ode.add_infusion( + system.clone(), + Infusion { + time: event.time, + dur: event.dur.unwrap(), + amount: event.dose.unwrap(), + compartment: event.input.unwrap() - 1, + }, + ); + } else { + // //dose + x = self + .ode + .add_dose(x, event.dose.unwrap(), event.input.unwrap() - 1); + } + } else if event.evid == 0 { + //obs + yout.push(self.ode.get_output(&x, &system, event.outeq.unwrap())) + } + if let Some(next_time) = scenario.times.get(index + 1) { + // TODO: use the last dx as the initial one for the next simulation. + x = self + .ode + .state_step(x, system.clone(), event.time, *next_time); + } + index += 1; + } + } + yout } } @@ -61,9 +116,9 @@ lazy_static! { DashMap::with_capacity(CACHE_SIZE); // Adjust cache size as needed } -pub fn get_ypred( +pub fn get_ypred + Sync + Clone>( sim_eng: &Engine, - scenario: &Scenario, + scenario: Scenario, support_point: Vec, i: usize, cache: bool, @@ -124,7 +179,7 @@ pub fn sim_obs( cache: bool, ) -> Array2> where - S: Predict + Sync + Clone, + S: Predict<'static> + Sync + Clone, { let mut pred: Array2> = Array2::default((scenarios.len(), support_points.nrows()).f()); @@ -137,8 +192,13 @@ where .enumerate() .for_each(|(j, mut element)| { let scenario = scenarios.get(i).unwrap(); - let ypred = - get_ypred(sim_eng, scenario, support_points.row(j).to_vec(), i, cache); + let ypred = get_ypred( + sim_eng, + scenario.clone(), + support_points.row(j).to_vec(), + i, + cache, + ); element.fill(ypred); }); }); @@ -147,11 +207,11 @@ where pub fn simple_sim( sim_eng: &Engine, - scenario: &Scenario, + scenario: Scenario, support_point: &Array1, ) -> Vec where - S: Predict + Sync + Clone, + S: Predict<'static> + Sync + Clone, { sim_eng.pred(scenario, support_point.to_vec()) } @@ -162,7 +222,7 @@ pub fn post_predictions( scenarios: &Vec, ) -> Result>, Box> where - S: Predict + Sync + Clone, + S: Predict<'static> + Sync + Clone, { if post.nrows() != scenarios.len() { return Err("Error calculating the posterior predictions, size mismatch.".into()); @@ -176,7 +236,7 @@ where .for_each(|(i, mut pred)| { let scenario = scenarios.get(i).unwrap(); let support_point = post.row(i).to_owned(); - pred.fill(simple_sim(sim_engine, scenario, &support_point)) + pred.fill(simple_sim(sim_engine, scenario.clone(), &support_point)) }); Ok(predictions) From 05ac2de70a4e71e2d7b5618a32c61964c07c8082 Mon Sep 17 00:00:00 2001 From: "Julian D. Otalvaro" Date: Fri, 6 Oct 2023 15:04:18 -0500 Subject: [PATCH 03/37] params into hashmap --- examples/bimodal_ke/main.rs | 23 ++++++++++++----------- src/routines/simulation/predict.rs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 4b55a8e2..54831275 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -11,23 +11,24 @@ use ode_solvers::*; const ATOL: f64 = 1e-4; const RTOL: f64 = 1e-4; +type State = Vector1; +type Time = f64; #[derive(Debug, Clone)] struct Model { - ke: f64, - v: f64, + params: HashMap, _scenario: Scenario, infusions: Vec, cov: Option>, } - -type State = Vector1; -type Time = f64; +impl Model { + pub fn get_param(&self, str: &str) -> f64 { + *self.params.get(str).unwrap() + } +} impl ode_solvers::System for Model { fn system(&self, t: Time, y: &State, dy: &mut State) { - let ke = self.ke; - - let _lag = 0.0; + let ke = self.get_param("ke"); let mut rateiv = [0.0]; for infusion in &self.infusions { @@ -51,16 +52,16 @@ impl<'a> Predict<'a> for Ode { type Model = Model; type State = State; fn initial_system(&self, params: &Vec, scenario: Scenario) -> Self::Model { + let params = HashMap::from([("ke".to_string(), params[0]), ("v".to_string(), params[1])]); Model { - ke: params[0], - v: params[1], + params, _scenario: scenario, infusions: vec![], cov: None, } } fn get_output(&self, x: &Self::State, system: &Self::Model, outeq: usize) -> f64 { - let v = system.v; + let v = system.get_param("v"); match outeq { 1 => x[0] / v, _ => panic!("Invalid output equation"), diff --git a/src/routines/simulation/predict.rs b/src/routines/simulation/predict.rs index 81c10e03..2567982d 100644 --- a/src/routines/simulation/predict.rs +++ b/src/routines/simulation/predict.rs @@ -14,6 +14,19 @@ use std::hash::{Hash, Hasher}; const CACHE_SIZE: usize = 1000000; +#[derive(Debug, Clone)] +pub struct Model { + params: HashMap, + _scenario: Scenario, + infusions: Vec, + cov: Option>, +} +impl Model { + pub fn get_param(&self, str: &str) -> f64 { + *self.params.get(str).unwrap() + } +} + /// return the predicted values for the given scenario and parameters /// where the second element of the tuple is the predicted values /// one per observation time in scenario and in the same order From fb76477463233d8d5a549c8533c91d1c9d2ee80e Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 8 Oct 2023 12:49:02 +0200 Subject: [PATCH 04/37] Setting for more predictions Implements add_event_interval for Scenario, which will add new events for predictions. Currently used in the write_pred function, with new argument idelta, which is read from the setting of the same name, --- examples/bimodal_ke/config.toml | 1 + examples/bimodal_ke/main.rs | 3 ++ src/entrypoints.rs | 3 +- src/routines/datafile.rs | 69 +++++++++++++++++++++++++++++++++ src/routines/output.rs | 15 +++++-- src/routines/settings/run.rs | 1 + src/tui/ui.rs | 4 +- 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/examples/bimodal_ke/config.toml b/examples/bimodal_ke/config.toml index a2857d0a..cd3f5d08 100644 --- a/examples/bimodal_ke/config.toml +++ b/examples/bimodal_ke/config.toml @@ -11,6 +11,7 @@ seed = 347 tui = true pmetrics_outputs = true cache = true +idelta = 0.1 [random] Ke = [0.001, 3.0] diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 54831275..38446863 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -89,6 +89,9 @@ impl<'a> Predict<'a> for Ode { time: f64, next_time: f64, ) -> State { + if time == next_time { + return x; + } let mut stepper = Dopri5::new(system, time, next_time, 1e-3, x, RTOL, ATOL); let _res = stepper.integrate(); let y = stepper.y_out(); diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 4fde5b4a..20072f6f 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -72,8 +72,9 @@ where let result = algorithm.fit(); log::info!("Total time: {:.2?}", now.elapsed()); + let idelta = settings.parsed.config.idelta.unwrap_or(0.0); if let Some(write) = &settings.parsed.config.pmetrics_outputs { - result.write_outputs(*write, &engine); + result.write_outputs(*write, &engine, idelta); } Ok(result) diff --git a/src/routines/datafile.rs b/src/routines/datafile.rs index 872cb891..0900f1be 100644 --- a/src/routines/datafile.rs +++ b/src/routines/datafile.rs @@ -22,6 +22,70 @@ impl Scenario { Ok(scenario) } + pub fn add_event_interval(&self, interval: f64) -> Self { + // Clone the underlying Event data instead of the references + let all_events = self + .clone() + .blocks + .iter() + .flat_map(|block| block.events.iter().cloned()) + .collect::>(); + + // Determine the start and end times + let start_time = all_events.first().unwrap().time; + let end_time = all_events.last().unwrap().time; + + // Determine the unique output equations in the events + // TODO: This should be read from the model / engine + let mut outeqs: Vec<_> = all_events.iter().filter_map(|event| event.outeq).collect(); + outeqs.sort_unstable(); + outeqs.dedup(); + + // Generate dummy events + let mut new_events = vec![]; + let mut current_time = start_time + interval; // Start from the first interval after the start time + while current_time < end_time { + current_time = (current_time / interval).round() * interval; // Round to the nearest interval + current_time = decimals(current_time, 4); // Round to 4 decimal places + for outeq in &outeqs { + new_events.push(Event { + id: self.id.clone(), + evid: 0, + time: current_time, + dur: None, + dose: None, + _addl: None, + _ii: None, + input: None, + out: Some(-99.0), + outeq: Some(*outeq), + _c0: None, + _c1: None, + _c2: None, + _c3: None, + covs: HashMap::new(), + }); + } + current_time += interval; + } + + // Combine all_events with new_events + let mut combined_events = all_events; + combined_events.extend(new_events.iter().cloned()); + + // Sort the events by time + combined_events.sort_by(|a, b| a.cmp_by_id_then_time(b)); + // Remove duplicate events based on time and outeq + // In essence, no need to have two predictions at the same time for the same outeq + let time_tolerance = 1e-4; + combined_events.sort_by(|a, b| a.cmp_by_id_then_time(b)); + combined_events.dedup_by(|a, b| { + (a.time - b.time).abs() < time_tolerance && a.outeq == b.outeq && a.evid == b.evid + }); + + Scenario::new(combined_events).unwrap() + } + pub fn reorder_with_lag(&self, lag_inputs: Vec<(f64, usize)>) -> Self { if lag_inputs.is_empty() { return self.clone(); @@ -301,3 +365,8 @@ fn check_obs(event: &Event) -> Result<(), Box> { } Ok(()) } + +fn decimals(value: f64, places: u32) -> f64 { + let multiplier = 10f64.powi(places as i32); + (value * multiplier).round() / multiplier +} diff --git a/src/routines/output.rs b/src/routines/output.rs index 8b30fca8..762651df 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -55,7 +55,7 @@ impl NPResult { } } - pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine) + pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine, idelta: f64) where S: Predict<'static> + std::marker::Sync + 'static + Clone + std::marker::Send, { @@ -63,7 +63,7 @@ impl NPResult { self.write_theta(); self.write_posterior(); self.write_obs(); - self.write_pred(&engine); + self.write_pred(&engine, idelta); self.write_meta(); } } @@ -178,12 +178,19 @@ impl NPResult { } /// Writes the predictions - pub fn write_pred<'a, S>(&self, engine: &Engine) + pub fn write_pred<'a, S>(&self, engine: &Engine, idelta: f64) where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { let result = (|| { - let scenarios = self.scenarios.clone(); + let mut scenarios = self.scenarios.clone(); + // Add an event interval to each scenario + if idelta > 0.0 { + scenarios.iter_mut().for_each(|scenario| { + *scenario = scenario.add_event_interval(idelta); + }); + } + let theta: Array2 = self.theta.clone(); let w: Array1 = self.w.clone(); let psi: Array2 = self.psi.clone(); diff --git a/src/routines/settings/run.rs b/src/routines/settings/run.rs index 52ce15ac..196eaf64 100644 --- a/src/routines/settings/run.rs +++ b/src/routines/settings/run.rs @@ -63,6 +63,7 @@ pub struct Config { pub pmetrics_outputs: Option, pub exclude: Option, pub cache: Option, + pub idelta: Option, } pub fn read(filename: String) -> Data { diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 786bbd6f..d0953067 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -93,8 +93,8 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; terminal - .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) - .unwrap(); + .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) + .unwrap(); Ok(()) } From 8fbabd224cc3208aad36f278398d876ffa4d7d04 Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:57:39 +0200 Subject: [PATCH 05/37] Simulate to time after dose This commit modifies add_event_interval() to use the new setting, tad (time after dose). This will ensure that simulations are made until a given time after dose, thus not requiring mock observations (-99), which are currently not supported. Both idelta and tad should possible be Option instead for better control flow. --- src/entrypoints.rs | 3 ++- src/routines/datafile.rs | 22 +++++++++++++++++++--- src/routines/output.rs | 8 ++++---- src/routines/settings/run.rs | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 20072f6f..03c50d01 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -73,8 +73,9 @@ where log::info!("Total time: {:.2?}", now.elapsed()); let idelta = settings.parsed.config.idelta.unwrap_or(0.0); + let tad = settings.parsed.config.tad.unwrap_or(0.0); if let Some(write) = &settings.parsed.config.pmetrics_outputs { - result.write_outputs(*write, &engine, idelta); + result.write_outputs(*write, &engine, idelta, tad); } Ok(result) diff --git a/src/routines/datafile.rs b/src/routines/datafile.rs index 0900f1be..63389149 100644 --- a/src/routines/datafile.rs +++ b/src/routines/datafile.rs @@ -22,7 +22,10 @@ impl Scenario { Ok(scenario) } - pub fn add_event_interval(&self, interval: f64) -> Self { + /// Adds "mock" events to a Scenario in order to generate predictions at those times + /// The interval is mapped to the `idelta`-setting in the configuration time + /// Time after dose (tad) will ensure that predictions are made until the last dose + tad + pub fn add_event_interval(&self, interval: f64, tad: f64) -> Self { // Clone the underlying Event data instead of the references let all_events = self .clone() @@ -33,7 +36,20 @@ impl Scenario { // Determine the start and end times let start_time = all_events.first().unwrap().time; - let end_time = all_events.last().unwrap().time; + let mut end_time = all_events.last().unwrap().time; + + // Pad end time to accomodate time after dose + if tad > 0.0{ + let last_dose_time = all_events + .iter() + .filter(|event| event.evid == 1) + .map(|event| event.time) + .fold(std::f64::NEG_INFINITY, f64::max); + + if end_time < last_dose_time + tad { + end_time = last_dose_time + tad + } + } // Determine the unique output equations in the events // TODO: This should be read from the model / engine @@ -44,7 +60,7 @@ impl Scenario { // Generate dummy events let mut new_events = vec![]; let mut current_time = start_time + interval; // Start from the first interval after the start time - while current_time < end_time { + while current_time <= end_time { current_time = (current_time / interval).round() * interval; // Round to the nearest interval current_time = decimals(current_time, 4); // Round to 4 decimal places for outeq in &outeqs { diff --git a/src/routines/output.rs b/src/routines/output.rs index 762651df..f33b7386 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -55,7 +55,7 @@ impl NPResult { } } - pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine, idelta: f64) + pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine, idelta: f64, tad: f64) where S: Predict<'static> + std::marker::Sync + 'static + Clone + std::marker::Send, { @@ -63,7 +63,7 @@ impl NPResult { self.write_theta(); self.write_posterior(); self.write_obs(); - self.write_pred(&engine, idelta); + self.write_pred(&engine, idelta, tad); self.write_meta(); } } @@ -178,7 +178,7 @@ impl NPResult { } /// Writes the predictions - pub fn write_pred<'a, S>(&self, engine: &Engine, idelta: f64) + pub fn write_pred<'a, S>(&self, engine: &Engine, idelta: f64, tad: f64) where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { @@ -187,7 +187,7 @@ impl NPResult { // Add an event interval to each scenario if idelta > 0.0 { scenarios.iter_mut().for_each(|scenario| { - *scenario = scenario.add_event_interval(idelta); + *scenario = scenario.add_event_interval(idelta, tad); }); } diff --git a/src/routines/settings/run.rs b/src/routines/settings/run.rs index 196eaf64..6789a4ff 100644 --- a/src/routines/settings/run.rs +++ b/src/routines/settings/run.rs @@ -64,6 +64,7 @@ pub struct Config { pub exclude: Option, pub cache: Option, pub idelta: Option, + pub tad: Option, } pub fn read(filename: String) -> Data { From b78f783f35aa3b0f2e6b8539f666088c5db4c60a Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:57:39 +0200 Subject: [PATCH 06/37] Simulate to time after dose Simulate to time after dose This commit modifies add_event_interval() to use the new setting, tad (time after dose). This will ensure that simulations are made until a given time after dose, thus not requiring mock observations (-99), which are currently not supported. Both idelta and tad should possible be Option instead for better control flow. --- src/entrypoints.rs | 3 ++- src/routines/datafile.rs | 22 +++++++++++++++++++--- src/routines/output.rs | 8 ++++---- src/routines/settings/run.rs | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 20072f6f..03c50d01 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -73,8 +73,9 @@ where log::info!("Total time: {:.2?}", now.elapsed()); let idelta = settings.parsed.config.idelta.unwrap_or(0.0); + let tad = settings.parsed.config.tad.unwrap_or(0.0); if let Some(write) = &settings.parsed.config.pmetrics_outputs { - result.write_outputs(*write, &engine, idelta); + result.write_outputs(*write, &engine, idelta, tad); } Ok(result) diff --git a/src/routines/datafile.rs b/src/routines/datafile.rs index 0900f1be..9d483d66 100644 --- a/src/routines/datafile.rs +++ b/src/routines/datafile.rs @@ -22,7 +22,10 @@ impl Scenario { Ok(scenario) } - pub fn add_event_interval(&self, interval: f64) -> Self { + /// Adds "mock" events to a Scenario in order to generate predictions at those times + /// The interval is mapped to the `idelta`-setting in the configuration time + /// Time after dose (tad) will ensure that predictions are made until the last dose + tad + pub fn add_event_interval(&self, interval: f64, tad: f64) -> Self { // Clone the underlying Event data instead of the references let all_events = self .clone() @@ -33,7 +36,20 @@ impl Scenario { // Determine the start and end times let start_time = all_events.first().unwrap().time; - let end_time = all_events.last().unwrap().time; + let mut end_time = all_events.last().unwrap().time; + + // Pad end time to accomodate time after dose + if tad > 0.0 { + let last_dose_time = all_events + .iter() + .filter(|event| event.evid == 1) + .map(|event| event.time) + .fold(std::f64::NEG_INFINITY, f64::max); + + if end_time < last_dose_time + tad { + end_time = last_dose_time + tad + } + } // Determine the unique output equations in the events // TODO: This should be read from the model / engine @@ -44,7 +60,7 @@ impl Scenario { // Generate dummy events let mut new_events = vec![]; let mut current_time = start_time + interval; // Start from the first interval after the start time - while current_time < end_time { + while current_time <= end_time { current_time = (current_time / interval).round() * interval; // Round to the nearest interval current_time = decimals(current_time, 4); // Round to 4 decimal places for outeq in &outeqs { diff --git a/src/routines/output.rs b/src/routines/output.rs index 762651df..f33b7386 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -55,7 +55,7 @@ impl NPResult { } } - pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine, idelta: f64) + pub fn write_outputs<'a, S>(&self, write: bool, engine: &Engine, idelta: f64, tad: f64) where S: Predict<'static> + std::marker::Sync + 'static + Clone + std::marker::Send, { @@ -63,7 +63,7 @@ impl NPResult { self.write_theta(); self.write_posterior(); self.write_obs(); - self.write_pred(&engine, idelta); + self.write_pred(&engine, idelta, tad); self.write_meta(); } } @@ -178,7 +178,7 @@ impl NPResult { } /// Writes the predictions - pub fn write_pred<'a, S>(&self, engine: &Engine, idelta: f64) + pub fn write_pred<'a, S>(&self, engine: &Engine, idelta: f64, tad: f64) where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { @@ -187,7 +187,7 @@ impl NPResult { // Add an event interval to each scenario if idelta > 0.0 { scenarios.iter_mut().for_each(|scenario| { - *scenario = scenario.add_event_interval(idelta); + *scenario = scenario.add_event_interval(idelta, tad); }); } diff --git a/src/routines/settings/run.rs b/src/routines/settings/run.rs index 196eaf64..6789a4ff 100644 --- a/src/routines/settings/run.rs +++ b/src/routines/settings/run.rs @@ -64,6 +64,7 @@ pub struct Config { pub exclude: Option, pub cache: Option, pub idelta: Option, + pub tad: Option, } pub fn read(filename: String) -> Data { From ad198ec1aaf909ac8bdba5d24c653a1249617ccd Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:03:35 +0200 Subject: [PATCH 07/37] Move datafile read from algorithms to entrypoint --- src/algorithms.rs | 10 +++------- src/entrypoints.rs | 11 +++++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 3155988c..581c88f4 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,7 +1,7 @@ use crate::prelude::{self, output::NPCycle, settings::run::Data}; use output::NPResult; -use prelude::*; +use prelude::{datafile::Scenario, *}; use simulation::predict::{Engine, Predict}; use tokio::sync::mpsc; @@ -21,6 +21,7 @@ pub trait Algorithm { pub fn initialize_algorithm( engine: Engine, settings: Data, + scenarios: Vec, tx: mpsc::UnboundedSender, ) -> Box where @@ -34,12 +35,7 @@ where } let ranges = settings.computed.random.ranges.clone(); let theta = initialization::sample_space(&settings, &ranges); - let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); - if let Some(exclude) = &settings.parsed.config.exclude { - for val in exclude { - scenarios.remove(val.as_integer().unwrap() as usize); - } - } + //This should be a macro, so it can automatically expands as soon as we add a new option in the Type Enum match settings.parsed.config.engine.as_str() { "NPAG" => Box::new(npag::NPAG::new( diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 4fde5b4a..3409b770 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -5,6 +5,7 @@ use crate::prelude::{ predict::{Engine, Predict}, *, }; +use crate::routines::datafile::Scenario; use csv::{ReaderBuilder, WriterBuilder}; use eyre::Result; @@ -60,7 +61,13 @@ where let settings = settings::run::read(settings_path); logger::setup_log(&settings); let (tx, rx) = mpsc::unbounded_channel::(); - let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), tx); + let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); + if let Some(exclude) = &settings.parsed.config.exclude { + for val in exclude { + scenarios.remove(val.as_integer().unwrap() as usize); + } + } + let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); // Spawn new thread for TUI let settings_tui = settings.clone(); if settings.parsed.config.tui { @@ -77,4 +84,4 @@ where } Ok(result) -} +} \ No newline at end of file From e4fab6b3410e59dbe82e1695767271a0e3ac7f9a Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:03:57 +0200 Subject: [PATCH 08/37] Update entrypoints.rs --- src/entrypoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 3409b770..0e60b125 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -84,4 +84,4 @@ where } Ok(result) -} \ No newline at end of file +} From 27257cc081f0c570d9ab44f048e0cfbfb0c8904b Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:04:22 +0200 Subject: [PATCH 09/37] Revert "Update entrypoints.rs" This reverts commit e4fab6b3410e59dbe82e1695767271a0e3ac7f9a. --- src/entrypoints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 0e60b125..3409b770 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -84,4 +84,4 @@ where } Ok(result) -} +} \ No newline at end of file From c8dc0fa07d837ea6e4f0682fc4ff2bece067fbb5 Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:27:55 +0200 Subject: [PATCH 10/37] New entrypoint with data argument Mainly exists to provide support for APIs which provide the data directly, or when data is provided synthetically --- src/entrypoints.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 3409b770..21bb1d63 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -6,6 +6,7 @@ use crate::prelude::{ *, }; use crate::routines::datafile::Scenario; + use csv::{ReaderBuilder, WriterBuilder}; use eyre::Result; @@ -83,5 +84,34 @@ where result.write_outputs(*write, &engine); } + Ok(result) +} + +pub fn start_with_data(engine: Engine, settings_path: String, scenarios: Vec) -> Result +where + S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, +{ + let now = Instant::now(); + let settings = settings::run::read(settings_path); + logger::setup_log(&settings); + let (tx, rx) = mpsc::unbounded_channel::(); + let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); + + let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); + // Spawn new thread for TUI + let settings_tui = settings.clone(); + if settings.parsed.config.tui { + let _ui_handle = spawn(move || { + start_ui(rx, settings_tui).expect("Failed to start TUI"); + }); + } + + let result = algorithm.fit(); + log::info!("Total time: {:.2?}", now.elapsed()); + + if let Some(write) = &settings.parsed.config.pmetrics_outputs { + result.write_outputs(*write, &engine); + } + Ok(result) } \ No newline at end of file From bb686e4b49be054348a0c7e7eb966674be979dce Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:31:52 +0200 Subject: [PATCH 11/37] Fix start_with_data entrypoint Scenarios should be mutable, and not overwritten in function --- src/entrypoints.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 21bb1d63..0c683982 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -87,7 +87,7 @@ where Ok(result) } -pub fn start_with_data(engine: Engine, settings_path: String, scenarios: Vec) -> Result +pub fn start_with_data(engine: Engine, settings_path: String, mut scenarios: Vec) -> Result where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { @@ -95,7 +95,6 @@ where let settings = settings::run::read(settings_path); logger::setup_log(&settings); let (tx, rx) = mpsc::unbounded_channel::(); - let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); // Spawn new thread for TUI From 4700f6f4c503935bfcd3be178ea127a9e2e7965d Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Thu, 26 Oct 2023 14:37:52 +0200 Subject: [PATCH 12/37] Update prelude and provide example for bimodal_ke --- examples/bimodal_ke/main.rs | 23 +++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 24 insertions(+) diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 54831275..a0eeb277 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -5,7 +5,9 @@ use npcore::prelude::{ datafile::{CovLine, Infusion, Scenario}, predict::{Engine, Predict}, start, + start_with_data }; +use npcore::prelude::*; use ode_solvers::*; const ATOL: f64 = 1e-4; @@ -105,3 +107,24 @@ fn main() -> Result<()> { Ok(()) } + +#[allow(dead_code)] +fn new_entry_test() -> Result<()> { + + let settings_path = "examples/bimodal_ke/config.toml".to_string(); + let settings = settings::run::read(settings_path); + let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); + if let Some(exclude) = &settings.parsed.config.exclude { + for val in exclude { + scenarios.remove(val.as_integer().unwrap() as usize); + } + } + + let _result = start_with_data( + Engine::new(Ode {}), + "examples/bimodal_ke/config.toml".to_string(), + scenarios, + )?; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 93613688..91f5365a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub mod prelude { pub use crate::algorithms; pub use crate::entrypoints::simulate; pub use crate::entrypoints::start; + pub use crate::entrypoints::start_with_data; pub use crate::logger; pub use crate::prelude::evaluation::{prob, sigma, *}; pub use crate::routines::initialization::*; From 76b34c3d979b4839a13817308a0264bfec111712 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 4 Nov 2023 18:09:02 +0100 Subject: [PATCH 13/37] Update dependencies --- Cargo.toml | 10 +++++----- src/entrypoints.rs | 12 +++++++++--- src/tui/ui.rs | 8 +++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5014716..11f1a84b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,20 +31,20 @@ log = "0.4.20" log4rs = "1.2.0" rayon = "1.8.0" eyre = "0.6.8" -ratatui = { version = "0.23.0", features = ["crossterm"] } +ratatui = { version = "0.24.0", features = ["crossterm"] } crossterm = "0.27.0" tokio = { version = "1.32.0", features = ["sync", "rt"] } ndarray-csv = "0.5.2" rawpointer = "0.2.1" argmin = "0.8.1" itertools = "0.11.0" -faer-core = { version = "0.11.0", features = [] } +faer-core = { version = "0.14.0", features = [] } # faer-lu = "0.9" -faer-qr = "0.11.0" +faer-qr = "0.14.0" # faer-cholesky = "0.9" # faer-svd = "0.9" -dyn-stack = "0.9.0" -faer = { version = "0.11.0", features = ["nalgebra", "ndarray"] } +dyn-stack = "0.10.0" +faer = { version = "0.14.0", features = ["nalgebra", "ndarray"] } [profile.release] diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 587c89df..af011a84 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -89,7 +89,11 @@ where Ok(result) } -pub fn start_with_data(engine: Engine, settings_path: String, mut scenarios: Vec) -> Result +pub fn start_with_data( + engine: Engine, + settings_path: String, + scenarios: Vec, +) -> Result where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { @@ -110,9 +114,11 @@ where let result = algorithm.fit(); log::info!("Total time: {:.2?}", now.elapsed()); + let idelta = settings.parsed.config.idelta.unwrap_or(0.0); + let tad = settings.parsed.config.tad.unwrap_or(0.0); if let Some(write) = &settings.parsed.config.pmetrics_outputs { - result.write_outputs(*write, &engine); + result.write_outputs(*write, &engine, idelta, tad); } Ok(result) -} \ No newline at end of file +} diff --git a/src/tui/ui.rs b/src/tui/ui.rs index d0953067..e77dd98d 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -98,15 +98,13 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() Ok(()) } -pub fn draw( - rect: &mut Frame, +pub fn draw( + rect: &mut Frame, app: &App, app_history: &AppHistory, elapsed_time: Duration, settings: &Data, -) where - B: Backend, -{ +) { let size = rect.size(); check_size(&size); From 83b4248796f741c4cdfe26d9fd5c561b9f38416d Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 4 Nov 2023 18:12:29 +0100 Subject: [PATCH 14/37] Restore bimodal_ke --- examples/bimodal_ke/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 1440842d..82a75cae 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -4,10 +4,10 @@ use eyre::Result; use npcore::prelude::{ datafile::{CovLine, Infusion, Scenario}, predict::{Engine, Predict}, - start, - start_with_data + settings, + datafile, + start, start_with_data, }; -use npcore::prelude::*; use ode_solvers::*; const ATOL: f64 = 1e-4; @@ -113,7 +113,6 @@ fn main() -> Result<()> { #[allow(dead_code)] fn new_entry_test() -> Result<()> { - let settings_path = "examples/bimodal_ke/config.toml".to_string(); let settings = settings::run::read(settings_path); let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); From 9c8dbd19732d0e52d3f317ffea5d10d18b23cd0b Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 12:30:48 +0100 Subject: [PATCH 15/37] Use tracing for logging --- Cargo.toml | 18 +++++----- examples/bimodal_ke/config.toml | 1 + src/logger.rs | 62 ++++++++++++++++++++++++--------- src/routines/settings/run.rs | 1 + src/tui/ui.rs | 14 ++++---- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 971e5ca3..786fa850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,13 @@ authors = [ "Michael Neely", "Walter Yamada", ] -description = "Rust Library with the building blocks needed to create new Non-Parametric algorithms and its integration with Pmetrics." +description = "Rust library with the building blocks needed to create new Non-Parametric algorithms and its integration with Pmetrics." license = "GPL-3.0" documentation = "https://lapkb.github.io/NPcore/npcore/" repository = "https://github.com/LAPKB/NPcore" exclude = [".github/*", ".vscode/*"] [dependencies] -tracing = "0.1.37" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } dashmap = "5.5.3" #cached = "0.26.2" lazy_static = "1.4.0" @@ -33,21 +31,23 @@ log = "0.4.20" log4rs = "1.2.0" rayon = "1.8.0" eyre = "0.6.8" -ratatui = { version = "0.23.0", features = ["crossterm"] } +ratatui = { version = "0.24.0", features = ["crossterm"] } crossterm = "0.27.0" tokio = { version = "1.32.0", features = ["sync", "rt"] } ndarray-csv = "0.5.2" rawpointer = "0.2.1" argmin = "0.8.1" itertools = "0.11.0" -faer-core = { version = "0.11.0", features = [] } +faer-core = { version = "0.14.0", features = [] } # faer-lu = "0.9" -faer-qr = "0.11.0" +faer-qr = "0.14.0" # faer-cholesky = "0.9" # faer-svd = "0.9" -dyn-stack = "0.9.0" -faer = { version = "0.11.0", features = ["nalgebra", "ndarray"] } - +dyn-stack = "0.10.0" +faer = { version = "0.14.0", features = ["nalgebra", "ndarray"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt", "time"] } +chrono = "0.4.19" [profile.release] codegen-units = 1 diff --git a/examples/bimodal_ke/config.toml b/examples/bimodal_ke/config.toml index a2857d0a..223c5ba9 100644 --- a/examples/bimodal_ke/config.toml +++ b/examples/bimodal_ke/config.toml @@ -11,6 +11,7 @@ seed = 347 tui = true pmetrics_outputs = true cache = true +log_level = "info" [random] Ke = [0.001, 3.0] diff --git a/src/logger.rs b/src/logger.rs index e9e1b016..8d2e4056 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,23 +1,53 @@ -use crate::prelude::settings::run::Data; -use tracing::Level; -use tracing_subscriber::fmt; +use tracing_subscriber::fmt::{self, format::Format}; +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::registry::Registry; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +use crate::routines::settings::run::Data; pub fn setup_log(settings: &Data) { + let log_level = settings + .parsed + .config + .log_level + .as_ref() + .map(|level| level.as_str()) + .unwrap_or("info"); // Default to 'info' if not set + + let env_filter = EnvFilter::new(log_level); + + let stdout_log = Format::default().compact(); + + // Start with a base subscriber from the registry + let subscriber = Registry::default().with(env_filter); + + // Check if a log file path is provided if let Some(log_path) = &settings.parsed.paths.log_out { - if std::fs::remove_file(log_path).is_ok() {}; - - let subscriber = fmt::Subscriber::builder() - .with_max_level(Level::INFO) - .with_writer(std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(log_path) - .unwrap()) + // Ensure the log file is created or truncated + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(log_path) + .expect("Failed to open log file"); + + let file_layer = fmt::layer() + .with_writer(file) .with_ansi(false) - .finish(); + .event_format(stdout_log); - tracing::subscriber::set_global_default(subscriber) - .expect("Setting default subscriber failed"); + // Add the file layer to the subscriber + subscriber.with(file_layer).init(); + } else { + // Add stdout layer only if no log file is specified + let stdout_layer = fmt::layer() + .event_format(stdout_log) + .with_writer(std::io::stdout); + + // Add the stdout layer to the subscriber + subscriber.with(stdout_layer).init(); } + + tracing::info!("Logging is configured with level: {}", log_level); } diff --git a/src/routines/settings/run.rs b/src/routines/settings/run.rs index 52ce15ac..25314ff0 100644 --- a/src/routines/settings/run.rs +++ b/src/routines/settings/run.rs @@ -63,6 +63,7 @@ pub struct Config { pub pmetrics_outputs: Option, pub exclude: Option, pub cache: Option, + pub log_level: Option, } pub fn read(filename: String) -> Data { diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 786bbd6f..f8359389 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -2,7 +2,7 @@ use eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, + backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, symbols, @@ -93,20 +93,18 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; terminal - .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) - .unwrap(); + .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) + .unwrap(); Ok(()) } -pub fn draw( - rect: &mut Frame, +pub fn draw( + rect: &mut Frame, app: &App, app_history: &AppHistory, elapsed_time: Duration, settings: &Data, -) where - B: Backend, -{ +) { let size = rect.size(); check_size(&size); From 1516447eae1a21ee0e2a36eca375f97d164a9e10 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 12:37:53 +0100 Subject: [PATCH 16/37] Format timestamp compactly --- Cargo.toml | 2 +- src/logger.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 786fa850..0b1534c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ dyn-stack = "0.10.0" faer = { version = "0.14.0", features = ["nalgebra", "ndarray"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt", "time"] } -chrono = "0.4.19" +chrono = "0.4" [profile.release] codegen-units = 1 diff --git a/src/logger.rs b/src/logger.rs index 8d2e4056..f3f8d0ab 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -3,6 +3,8 @@ use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; +use tracing_subscriber::fmt::time::{self, FormatTime}; +use std::io; use crate::routines::settings::run::Data; @@ -17,7 +19,7 @@ pub fn setup_log(settings: &Data) { let env_filter = EnvFilter::new(log_level); - let stdout_log = Format::default().compact(); + let stdout_log = Format::default().compact().with_timer(CompactTimestamp); // Start with a base subscriber from the registry let subscriber = Registry::default().with(env_filter); @@ -35,7 +37,7 @@ pub fn setup_log(settings: &Data) { let file_layer = fmt::layer() .with_writer(file) .with_ansi(false) - .event_format(stdout_log); + .event_format(stdout_log.clone()); // Add the file layer to the subscriber subscriber.with(file_layer).init(); @@ -51,3 +53,12 @@ pub fn setup_log(settings: &Data) { tracing::info!("Logging is configured with level: {}", log_level); } + +#[derive(Clone)] +struct CompactTimestamp; + +impl FormatTime for CompactTimestamp { + fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> Result<(), std::fmt::Error> { + write!(w, "{}", chrono::Local::now().format("%H:%M:%S")) + } +} \ No newline at end of file From ade2639d95ca842cc5ab21265c422f7f62004804 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 13:05:51 +0100 Subject: [PATCH 17/37] Transition to tracing --- Cargo.toml | 3 --- examples/debug.rs | 8 ++++---- examples/vori/main.rs | 7 +++++-- src/algorithms.rs | 2 +- src/algorithms/npag.rs | 35 ++++++++++++++++++++------------ src/entrypoints.rs | 2 +- src/logger.rs | 10 +++++---- src/routines/datafile.rs | 16 +++++++-------- src/routines/evaluation/prob.rs | 2 +- src/routines/evaluation/sigma.rs | 2 +- src/routines/output.rs | 8 ++++---- src/tui/mod.rs | 7 +++---- 12 files changed, 56 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b1534c1..2d08c093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ exclude = [".github/*", ".vscode/*"] [dependencies] dashmap = "5.5.3" -#cached = "0.26.2" lazy_static = "1.4.0" csv = "1.2.1" ndarray = { version = "0.15.6", features = ["rayon"] } @@ -27,8 +26,6 @@ toml = { version = "0.8.1", features = ["preserve_order"] } ode_solvers = "0.3.7" ndarray-stats = "0.5.1" linfa-linalg = "0.1.0" -log = "0.4.20" -log4rs = "1.2.0" rayon = "1.8.0" eyre = "0.6.8" ratatui = { version = "0.24.0", features = ["crossterm"] } diff --git a/examples/debug.rs b/examples/debug.rs index ff5597ba..ba06f7a4 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -92,7 +92,7 @@ impl Predict for Ode { let y = stepper.y_out(); x = *y.last().unwrap(); } else if *next_time < event.time { - log::error!("Panic: Next event's time is in the past!"); + tracing::error!("Panic: Next event's time is in the past!"); panic!("Panic: Next event's time is in the past!"); } } @@ -103,7 +103,7 @@ impl Predict for Ode { // Rk4::new(system.clone(), event.time, x, lag_time, 0.1); if let Some(next_time) = scenario.times.get(index + 1) { if *next_time < lag_time { - log::error!("Panic: lag time overpasses next observation, not implemented. Stopping."); + tracing::error!("Panic: lag time overpasses next observation, not implemented. Stopping."); panic!("Panic: lag time overpasses next observation, not implemented. Stopping."); } let mut stepper = Dopri5::new( @@ -139,7 +139,7 @@ impl Predict for Ode { let y = stepper.y_out(); x = *y.last().unwrap(); } else if *next_time > event.time { - log::error!("Panic: Next event's time is in the past!"); + tracing::error!("Panic: Next event's time is in the past!"); panic!("Panic: Next event's time is in the past!"); } } @@ -164,7 +164,7 @@ impl Predict for Ode { let y = stepper.y_out(); x = *y.last().unwrap(); } else if *next_time < event.time { - log::error!("Panic: Next event's time is in the past!"); + tracing::error!("Panic: Next event's time is in the past!"); panic!("Panic: Next event's time is in the past!"); } } diff --git a/examples/vori/main.rs b/examples/vori/main.rs index 9483446e..782c55f8 100644 --- a/examples/vori/main.rs +++ b/examples/vori/main.rs @@ -127,8 +127,11 @@ impl Predict for Ode { let y = stepper.y_out(); x = *y.last().unwrap(); } else if event.time > *next_time { - log::error!("next time is in the past!"); - log::error!("event_time: {}\nnext_time: {}", event.time, *next_time); + tracing::error!( + "The next event before the current: event_time: {}, next_time: {}", + event.time, + *next_time + ); } } index += 1; diff --git a/src/algorithms.rs b/src/algorithms.rs index aacb057f..50c98c45 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -28,7 +28,7 @@ where { if std::path::Path::new("stop").exists() { match std::fs::remove_file("stop") { - Ok(_) => log::info!("Removed previous stop file"), + Ok(_) => tracing::info!("Removed previous stop file"), Err(err) => panic!("Unable to remove previous stop file: {}", err), } } diff --git a/src/algorithms/npag.rs b/src/algorithms/npag.rs index b8fb6029..dcb1f344 100644 --- a/src/algorithms/npag.rs +++ b/src/algorithms/npag.rs @@ -195,7 +195,9 @@ where pub fn run(&mut self) -> NPResult { while self.eps > THETA_E { - // log::info!("Cycle: {}", cycle); + // Enter a span for each cycle, provding context for further errors + let cycle_span = tracing::span!(tracing::Level::INFO, "Cycle", cycle = self.cycle); + let _enter = cycle_span.enter(); // psi n_sub rows, nspp columns let cache = if self.cycle == 1 { false } else { self.cache }; let ypred = sim_obs(&self.engine, &self.scenarios, &self.theta, cache); @@ -240,12 +242,16 @@ where keep.push(*perm.get(i).unwrap()); } } - log::info!( - "QR decomp, cycle {}, kept: {}, thrown {}", - self.cycle, - keep.len(), - self.psi.ncols() - keep.len() - ); + + // If a support point is dropped, log it + if self.psi.ncols() != keep.len() { + tracing::info!( + "QR decomposition dropped {} SPP, kept {}", + self.psi.ncols() - keep.len(), + keep.len(), + ); + } + self.theta = self.theta.select(Axis(0), &keep); self.psi = self.psi.select(Axis(1), &keep); @@ -270,10 +276,13 @@ where }; self.tx.send(state.clone()).unwrap(); - // If the objective function decreased, log an error. - // Increasing objf signals instability of model misspecification. + // Increasing objf signals instability or model misspecification. if self.last_objf > self.objf { - log::error!("Objective function decreased"); + tracing::error!( + "Objective function decreased from {} to {}", + self.last_objf, + self.objf + ); } self.w = self.lambda.clone(); @@ -285,7 +294,7 @@ where if self.eps <= THETA_E { self.f1 = pyl.mapv(|x| x.ln()).sum(); if (self.f1 - self.f0).abs() <= THETA_F { - log::info!("Likelihood criteria convergence"); + tracing::info!("Likelihood criteria convergence, -2LL: {:.1}", self.objf); self.converged = true; state.stop_text = "The run converged!".to_string(); self.tx.send(state).unwrap(); @@ -299,7 +308,7 @@ where // Stop if we have reached maximum number of cycles if self.cycle >= self.settings.parsed.config.cycles { - log::info!("Maximum number of cycles reached"); + tracing::info!("Maximum number of cycles reached"); state.stop_text = "No (max cycle)".to_string(); self.tx.send(state).unwrap(); break; @@ -307,7 +316,7 @@ where // Stop if stopfile exists if std::path::Path::new("stop").exists() { - log::info!("Stopfile detected - breaking"); + tracing::info!("Stopfile detected - breaking"); state.stop_text = "No (stopped)".to_string(); self.tx.send(state).unwrap(); break; diff --git a/src/entrypoints.rs b/src/entrypoints.rs index c33d9d79..7dcd5f3e 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -71,7 +71,7 @@ where } let result = algorithm.fit(); - log::info!("Total time: {:.2?}", now.elapsed()); + tracing::info!("Total time: {:.2?}", now.elapsed()); if let Some(write) = &settings.parsed.config.pmetrics_outputs { result.write_outputs(*write, &engine); diff --git a/src/logger.rs b/src/logger.rs index f3f8d0ab..aa5a0026 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,10 +1,9 @@ +use tracing_subscriber::fmt::time::FormatTime; use tracing_subscriber::fmt::{self, format::Format}; use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; -use tracing_subscriber::fmt::time::{self, FormatTime}; -use std::io; use crate::routines::settings::run::Data; @@ -58,7 +57,10 @@ pub fn setup_log(settings: &Data) { struct CompactTimestamp; impl FormatTime for CompactTimestamp { - fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> Result<(), std::fmt::Error> { + fn format_time( + &self, + w: &mut tracing_subscriber::fmt::format::Writer<'_>, + ) -> Result<(), std::fmt::Error> { write!(w, "{}", chrono::Local::now().format("%H:%M:%S")) } -} \ No newline at end of file +} diff --git a/src/routines/datafile.rs b/src/routines/datafile.rs index 872cb891..891383df 100644 --- a/src/routines/datafile.rs +++ b/src/routines/datafile.rs @@ -86,7 +86,7 @@ impl Scenario { obs_times.push(event.time); obs.push(event.out.unwrap()); } else { - log::error!("Error: Unsupported evid"); + tracing::error!("Error: Unsupported evid: {evid}", evid = event.evid); exit(-1); } block.events.push(event); @@ -259,12 +259,12 @@ pub fn parse(path: &String) -> Result, Box> { fn check_dose(event: &Event) -> Result<(), Box> { if event.dose.is_none() { - log::error!("Error: Dose event without dose"); + tracing::error!("Error: Dose event without dose"); //return Err("Error: Dose event without dose".into()); exit(-1); } if event.input.is_none() { - log::error!("Error: Dose event without input"); + tracing::error!("Error: Dose event without input"); //return Err("Error: Dose event without input".into()); exit(-1); } @@ -272,17 +272,17 @@ fn check_dose(event: &Event) -> Result<(), Box> { } fn check_infusion(event: &Event) -> Result<(), Box> { if event.dose.is_none() { - log::error!("Error: Infusion event without dose"); + tracing::error!("Error: Infusion event without dose"); //return Err("Error: Infusion event without dose".into()); exit(-1); } if event.dur.is_none() { - log::error!("Error: Infusion event without duration"); + tracing::error!("Error: Infusion event without duration"); //return Err("Error: Infusion event without duration".into()); exit(-1); } if event.input.is_none() { - log::error!("Error: Infusion event without input"); + tracing::error!("Error: Infusion event without input"); //return Err("Error: Infusion event without input".into()); exit(-1); } @@ -290,12 +290,12 @@ fn check_infusion(event: &Event) -> Result<(), Box> { } fn check_obs(event: &Event) -> Result<(), Box> { if event.out.is_none() { - log::error!("Error: Obs event without out"); + tracing::error!("Error: Obs event without out"); //return Err("Error: Obs event without out".into()); exit(-1); } if event.outeq.is_none() { - log::error!("Error: Obs event without outeq"); + tracing::error!("Error: Obs event without outeq"); //return Err("Error: Obs event without outeq".into()); exit(-1); } diff --git a/src/routines/evaluation/prob.rs b/src/routines/evaluation/prob.rs index bf94daf6..38a9dccd 100644 --- a/src/routines/evaluation/prob.rs +++ b/src/routines/evaluation/prob.rs @@ -32,7 +32,7 @@ where let sigma = sig.sigma(&yobs); let ll = normal_likelihood(ypred.get((i, j)).unwrap(), &yobs, &sigma); if ll.is_nan() || ll.is_infinite() { - log::info!( + tracing::info!( "NaN or Inf Likelihood detected!\nLL:{:?}\nypred: {:?}\nsubject: {}\nSpp: {}", ll, &ypred.get((i, j)), diff --git a/src/routines/evaluation/sigma.rs b/src/routines/evaluation/sigma.rs index 490f92fb..8b3525de 100644 --- a/src/routines/evaluation/sigma.rs +++ b/src/routines/evaluation/sigma.rs @@ -54,7 +54,7 @@ impl<'a> Sigma for ErrorPoly<'a> { res.mapv(|x| { if x.is_nan() || x < 0.0 { - log::error!( + tracing::error!( "The computed standard deviation is either NaN or negative (SD = {}), coercing to 0", x ); diff --git a/src/routines/output.rs b/src/routines/output.rs index 368d24b1..cc9c89e2 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -99,7 +99,7 @@ impl NPResult { })(); if let Err(e) = result { - log::error!("Error while writing theta: {}", e); + tracing::error!("Error while writing theta: {}", e); } } @@ -143,7 +143,7 @@ impl NPResult { })(); if let Err(e) = result { - log::error!("Error while writing posterior: {}", e); + tracing::error!("Error while writing posterior: {}", e); } } @@ -173,7 +173,7 @@ impl NPResult { })(); if let Err(e) = result { - log::error!("Error while writing observations: {}", e); + tracing::error!("Error while writing observations: {}", e); } } @@ -252,7 +252,7 @@ impl NPResult { })(); if let Err(e) = result { - log::error!("Error while writing predictions: {}", e); + tracing::error!("Error while writing predictions: {}", e); } } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index aed565d1..45a964e4 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -4,7 +4,6 @@ pub mod state; pub mod ui; use crate::prelude::output::NPCycle; -use log::{debug, trace}; use self::actions::{Action, Actions}; use self::inputs::key::Key; @@ -36,19 +35,19 @@ impl App { /// Handle a user action pub fn do_action(&mut self, key: Key) -> AppReturn { if let Some(action) = self.actions.find(key) { - debug!("Run action [{:?}]", action); + tracing::debug!("Run action [{:?}]", action); match action { Action::Quit => AppReturn::Exit, Action::Stop => { // Write the "stop.txt" file - log::info!("Stop signal received - writing stopfile"); + tracing::info!("Stop signal received - writing stopfile"); let filename = "stop"; File::create(filename).unwrap(); AppReturn::Continue } } } else { - trace!("{} was registered, but it has no associated action", key); + tracing::trace!("{} was registered, but it has no associated action", key); AppReturn::Continue } } From 40dba8d77109b902465597d0f01e8754d14d5dd8 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 13:19:30 +0100 Subject: [PATCH 18/37] Use tracing in entrypoints --- examples/bimodal_ke/main.rs | 5 ++--- src/entrypoints.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 82a75cae..8a5626b2 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -2,11 +2,10 @@ use std::collections::HashMap; use eyre::Result; use npcore::prelude::{ + datafile, datafile::{CovLine, Infusion, Scenario}, predict::{Engine, Predict}, - settings, - datafile, - start, start_with_data, + settings, start, start_with_data, }; use ode_solvers::*; diff --git a/src/entrypoints.rs b/src/entrypoints.rs index efb5941b..91c8d40d 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -79,7 +79,7 @@ where } let result = algorithm.fit(); - log::info!("Total time: {:.2?}", now.elapsed()); + tracing::info!("Total time: {:.2?}", now.elapsed()); let idelta = settings.parsed.config.idelta.unwrap_or(0.0); let tad = settings.parsed.config.tad.unwrap_or(0.0); From 14eab52d38ed2d836b52218343a118d6aebce98b Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 15:01:44 +0100 Subject: [PATCH 19/37] Move UI code to components --- src/tui/components.rs | 30 ++++++ src/tui/mod.rs | 1 + src/tui/ui.rs | 234 +----------------------------------------- 3 files changed, 33 insertions(+), 232 deletions(-) diff --git a/src/tui/components.rs b/src/tui/components.rs index 8a6becf0..2972bdf8 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -1,6 +1,21 @@ +use std::time::Duration; + /// This file contains the different components of the TUI /// The purpose is to create common components with generic methods +use ratatui::{ + layout::{Alignment, Constraint}, + style::{Color, Modifier, Style}, + symbols, + text::Span, + widgets::{ + Axis, Block, BorderType, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Table, + }, +}; + +use super::App; + +use crate::prelude::settings::run::Data; pub fn draw_title<'a>() -> Paragraph<'a> { Paragraph::new("NPcore Execution") @@ -200,4 +215,19 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { .title(" Objective function ") .borders(Borders::ALL), ) +} + +fn format_time(elapsed_time: std::time::Duration) -> String { + let elapsed_seconds = elapsed_time.as_secs(); + let (elapsed, unit) = if elapsed_seconds < 60 { + (elapsed_seconds, "s") + } else if elapsed_seconds < 3600 { + let elapsed_minutes = elapsed_seconds / 60; + (elapsed_minutes, "m") + } else { + let elapsed_hours = elapsed_seconds / 3600; + (elapsed_hours, "h") + }; + let time_text = format!("{}{}", elapsed, unit); + time_text } \ No newline at end of file diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 45a964e4..0eef276f 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -2,6 +2,7 @@ pub mod actions; pub mod inputs; pub mod state; pub mod ui; +pub mod components; use crate::prelude::output::NPCycle; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index f8359389..eb621192 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -3,13 +3,7 @@ use eyre::Result; use ratatui::{ backend::CrosstermBackend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::Span, - widgets::{ - Axis, Block, BorderType, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Table, - }, + layout::{Constraint, Direction, Layout}, Frame, Terminal, }; use std::{ @@ -25,6 +19,7 @@ use super::{ }; use crate::prelude::{output::NPCycle, settings::run::Data}; +use crate::tui::components::*; pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { let stdout = stdout(); @@ -106,7 +101,6 @@ pub fn draw( settings: &Data, ) { let size = rect.size(); - check_size(&size); // Vertical layout (overall) let chunks = Layout::default() @@ -172,227 +166,3 @@ pub fn draw( let plot = draw_plot(&mut norm_data); rect.render_widget(plot, chunks[2]); } - -fn draw_title<'a>() -> Paragraph<'a> { - Paragraph::new("NPcore Execution") - .style(Style::default().fg(Color::LightCyan)) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)) - .border_type(BorderType::Plain), - ) -} - -fn draw_status<'a>(app: &App, elapsed_time: Duration) -> Table<'a> { - // Define (formatted) texts - let cycle_text = format!("{}", app.state.cycle); - let objf_text = format!("{:.5}", app.state.objf); - let delta_objf_text = format!("{:.5}", app.state.delta_objf); - let gamma_text = format!("{:.5}", app.state.gamlam); - let spp_text = format!("{}", app.state.nspp); - let time_text = format_time(elapsed_time); - let stop_text = app.state.stop_text.to_string(); - - // Define the table data - let data = vec![ - ("Current cycle", cycle_text), - ("Objective function", objf_text), - ("Δ Objective function", delta_objf_text), - ("Gamma/Lambda", gamma_text), - ("Support points", spp_text), - ("Elapsed time", time_text), - ("Convergence", stop_text), - // Add more rows as needed - ]; - - // Populate the table rows - let rows: Vec = data - .iter() - .map(|(key, value)| { - let title_style = Style::default().add_modifier(Modifier::BOLD); - let title_cell = Cell::from(Span::styled(format!("{}:", key), title_style)); - let value_cell = Cell::from(value.to_string()); - Row::new(vec![title_cell, value_cell]) - }) - .collect(); - - // Create the table widget - Table::new(rows) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .title(" Status "), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) // Set percentage widths for columns - .column_spacing(1) -} - -fn draw_options<'a>(settings: &Data) -> Table<'a> { - // Define the table data - - let cycles = settings.parsed.config.cycles.to_string(); - let engine = settings.parsed.config.engine.to_string(); - let conv_crit = "Placeholder".to_string(); - let indpts = settings.parsed.config.init_points.to_string(); - let error = settings.parsed.error.class.to_string(); - let cache = match settings.parsed.config.cache { - Some(true) => "Yes".to_string(), - Some(false) => "No".to_string(), - None => "Not set".to_string(), - }; - let seed = settings.parsed.config.seed.to_string(); - - let data = vec![ - ("Maximum cycles", cycles), - ("Engine", engine), - ("Convergence criteria", conv_crit), - ("Initial gridpoints", indpts), - ("Error model", error), - ("Cache", cache), - ("Random seed", seed), - // Add more rows as needed - ]; - - // Populate the table rows - let rows: Vec = data - .iter() - .map(|(key, value)| { - let title_style = Style::default().add_modifier(Modifier::BOLD); - let title_cell = Cell::from(Span::styled(format!("{}:", key), title_style)); - let value_cell = Cell::from(value.to_string()); - Row::new(vec![title_cell, value_cell]) - }) - .collect(); - - // Create the table widget - Table::new(rows) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .title(" Options "), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) // Set percentage widths for columns - .column_spacing(1) -} - -fn draw_commands(app: &App) -> Table { - let key_style = Style::default().fg(Color::LightCyan); - let help_style = Style::default().fg(Color::Gray); - - let mut rows = vec![]; - for action in app.actions.actions().iter() { - let mut first = true; - for key in action.keys() { - let help = if first { - first = false; - action.to_string() - } else { - String::from("") - }; - let row = Row::new(vec![ - Cell::from(Span::styled(key.to_string(), key_style)), - Cell::from(Span::styled(help, help_style)), - ]); - rows.push(row); - } - } - - Table::new(rows) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .title(" Commands "), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) // Set percentage widths for columns - .column_spacing(1) -} - -fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { - // Find min and max values - let (x_min, x_max) = norm_data - .iter() - .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), (x, _)| { - (min.min(*x), max.max(*x)) - }); - - let (y_min, y_max) = norm_data - .iter() - .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), (_, y)| { - (min.min(*y), max.max(*y)) - }); - - // Compute the dynamic step size for the X-labels - let step_size = ((x_max - x_min) / 10.0).max(1.0).ceil(); - - // Generate X-labels using the dynamic step size - let x_labels: Vec = ((x_min as i64)..=(x_max as i64)) - .step_by(step_size as usize) - .map(|x| Span::from(x.to_string())) - .collect(); - - // Generate four Y-labels, evenly from y_min to y_max - let y_step = (y_max - y_min) / 5.0; // To get 4 labels, we need 3 steps - let y_labels: Vec = (0..=3) - .map(|i| { - let y = y_min + y_step * (i as f64); - Span::from(format!("{:.0}", y)) - }) - .collect(); - - // Prepare the dataset - let dataset = vec![Dataset::default() - .name("-2LL") - .marker(symbols::Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .graph_type(GraphType::Scatter) - .data(norm_data)]; - - // Return the plot - Chart::new(dataset) - .x_axis( - Axis::default() - .title("Cycle") - .bounds([x_min, x_max]) - .labels(x_labels), - ) - .y_axis( - Axis::default() - .title("-2LL") - .bounds([y_min, y_max]) - .labels(y_labels), - ) - .block( - Block::default() - .title(" Objective function ") - .borders(Borders::ALL), - ) -} - -fn check_size(rect: &Rect) { - if rect.width < 52 { - // panic!("Require width >= 52, (got {})", rect.width); - } - if rect.height < 12 { - // panic!("Require height >= 12, (got {})", rect.height); - } -} - -fn format_time(elapsed_time: std::time::Duration) -> String { - let elapsed_seconds = elapsed_time.as_secs(); - let (elapsed, unit) = if elapsed_seconds < 60 { - (elapsed_seconds, "s") - } else if elapsed_seconds < 3600 { - let elapsed_minutes = elapsed_seconds / 60; - (elapsed_minutes, "m") - } else { - let elapsed_hours = elapsed_seconds / 3600; - (elapsed_hours, "h") - }; - let time_text = format!("{}{}", elapsed, unit); - time_text -} From b583de997cfa44aba90baca619e96ae96fcc3a66 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sun, 5 Nov 2023 15:36:20 +0100 Subject: [PATCH 20/37] Prototype reading app_history --- src/tui/components.rs | 16 ++++++++++++---- src/tui/mod.rs | 2 +- src/tui/ui.rs | 26 +++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/tui/components.rs b/src/tui/components.rs index 2972bdf8..9b567f38 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -2,14 +2,14 @@ use std::time::Duration; /// This file contains the different components of the TUI /// The purpose is to create common components with generic methods - use ratatui::{ layout::{Alignment, Constraint}, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, symbols, - text::Span, + text::{Line, Span}, widgets::{ Axis, Block, BorderType, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Table, + Wrap, }, }; @@ -217,6 +217,14 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { ) } +pub fn draw_logs<'a>(text: &'a Vec) -> Paragraph<'a> { + Paragraph::new(text.clone()) + .block(Block::new().title(" Logs ").borders(Borders::ALL)) + .style(Style::new().white().on_black()) + .alignment(Alignment::Left) + .wrap(Wrap { trim: true }) +} + fn format_time(elapsed_time: std::time::Duration) -> String { let elapsed_seconds = elapsed_time.as_secs(); let (elapsed, unit) = if elapsed_seconds < 60 { @@ -230,4 +238,4 @@ fn format_time(elapsed_time: std::time::Duration) -> String { }; let time_text = format!("{}{}", elapsed, unit); time_text -} \ No newline at end of file +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 0eef276f..e80b2fc6 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,8 +1,8 @@ pub mod actions; +pub mod components; pub mod inputs; pub mod state; pub mod ui; -pub mod components; use crate::prelude::output::NPCycle; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index eb621192..5a64bec9 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -4,6 +4,7 @@ use eyre::Result; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, + text::Line, Frame, Terminal, }; use std::{ @@ -145,6 +146,14 @@ pub fn draw( let commands = draw_commands(app); rect.render_widget(commands, body_layout[2]); + // Bottom chunk (plot and logs) + let bottom_chunk = chunks[2]; + let bottom_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(bottom_chunk); + + // Plot // Prepare the data let data: Vec<(f64, f64)> = app_history .cycles @@ -164,5 +173,20 @@ pub fn draw( .collect(); let plot = draw_plot(&mut norm_data); - rect.render_widget(plot, chunks[2]); + rect.render_widget(plot, bottom_layout[0]); + + // Logs + // Iterate through app_history and get cycle and objf + let logtext: Vec = app_history + .cycles + .iter() + .map(|entry| { + let cycle = entry.cycle.to_string(); + let objf = entry.objf.to_string(); + Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) + }) + .collect(); + + let logs = draw_logs(&logtext); + rect.render_widget(logs, bottom_layout[1]) } From 0416cb8687c8127dcee83c254a2ecf5b8d5059ab Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Wed, 8 Nov 2023 11:31:53 +0100 Subject: [PATCH 21/37] Remove black background --- src/tui/components.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/components.rs b/src/tui/components.rs index 9b567f38..aebc7eff 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -220,7 +220,7 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { pub fn draw_logs<'a>(text: &'a Vec) -> Paragraph<'a> { Paragraph::new(text.clone()) .block(Block::new().title(" Logs ").borders(Borders::ALL)) - .style(Style::new().white().on_black()) + .style(Style::new().white()) .alignment(Alignment::Left) .wrap(Wrap { trim: true }) } From c17b9a66d1a2b73764d2cb8d755f03d23c947fc4 Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Wed, 8 Nov 2023 19:32:19 +0100 Subject: [PATCH 22/37] Working prototype of tabs Currently only "logs" are supported --- src/tui/actions.rs | 7 +++++-- src/tui/components.rs | 31 ++++++++++++++++++++++++++++--- src/tui/mod.rs | 19 ++++++++++++++++--- src/tui/ui.rs | 39 +++++++++++++++++++-------------------- 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/tui/actions.rs b/src/tui/actions.rs index 7bf3d330..68eafb25 100644 --- a/src/tui/actions.rs +++ b/src/tui/actions.rs @@ -9,20 +9,22 @@ use super::inputs::key::Key; pub enum Action { Quit, Stop, + Next, } impl Action { /// All available actions pub fn iterator() -> Iter<'static, Action> { - static ACTIONS: [Action; 2] = [Action::Quit, Action::Stop]; + static ACTIONS: [Action; 3] = [Action::Quit, Action::Stop, Action::Next]; ACTIONS.iter() } /// List of key associated to action pub fn keys(&self) -> &[Key] { match self { - Action::Quit => &[Key::Ctrl('c'), Key::Char('q')], + Action::Quit => &[Key::Char('q')], Action::Stop => &[Key::Ctrl('d')], + Action::Next => &[Key::Char('n')] } } } @@ -31,6 +33,7 @@ impl Action { impl Display for Action { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { + Action::Next => "Next", Action::Quit => "Quit", Action::Stop => "Stop", }; diff --git a/src/tui/components.rs b/src/tui/components.rs index aebc7eff..d2eb741b 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -9,11 +9,11 @@ use ratatui::{ text::{Line, Span}, widgets::{ Axis, Block, BorderType, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Table, - Wrap, + Wrap, Tabs, }, }; -use super::App; +use super::{App, state::AppHistory}; use crate::prelude::settings::run::Data; @@ -217,7 +217,17 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { ) } -pub fn draw_logs<'a>(text: &'a Vec) -> Paragraph<'a> { +pub fn draw_logs<'a>(app_history: &AppHistory) -> Paragraph<'a> { + let text: Vec = app_history + .cycles + .iter() + .map(|entry| { + let cycle = entry.cycle.to_string(); + let objf = entry.objf.to_string(); + Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) + }) + .collect(); + Paragraph::new(text.clone()) .block(Block::new().title(" Logs ").borders(Borders::ALL)) .style(Style::new().white()) @@ -225,6 +235,21 @@ pub fn draw_logs<'a>(text: &'a Vec) -> Paragraph<'a> { .wrap(Wrap { trim: true }) } + +pub fn draw_tabs<'a>(app: &App) -> Tabs<'a> { + + let titles = app.tab_titles.clone(); + let index = app.tab_index.clone(); + let tabs = Tabs::new(titles.clone()) + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)) + .divider(Span::raw("|")) + .select(index); + + tabs +} + fn format_time(elapsed_time: std::time::Duration) -> String { let elapsed_seconds = elapsed_time.as_secs(); let (elapsed, unit) = if elapsed_seconds < 60 { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index e80b2fc6..cb381666 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -22,15 +22,21 @@ pub struct App { actions: Actions, /// State state: NPCycle, + /// Index for tab + tab_index: usize, + /// Tab titles + tab_titles: Vec<&'static str>, } impl App { #[allow(clippy::new_without_default)] pub fn new() -> Self { - let actions = vec![Action::Quit, Action::Stop].into(); + let actions = vec![Action::Quit, Action::Stop, Action::Next].into(); let state = NPCycle::new(); + let tab_index = 0; + let tab_titles = vec!["Logs", "Plot", "Settings"]; - Self { actions, state } + Self { actions, state , tab_index, tab_titles} } /// Handle a user action @@ -45,10 +51,17 @@ impl App { let filename = "stop"; File::create(filename).unwrap(); AppReturn::Continue + }, + Action::Next => { + self.tab_index = self.tab_index + 1; + if self.tab_index >= self.tab_titles.len() { + self.tab_index = 0; + } + AppReturn::Continue } } } else { - tracing::trace!("{} was registered, but it has no associated action", key); + tracing::trace!("The {} key was registered, but it has no associated action", key); AppReturn::Continue } } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 5a64bec9..b509f9de 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -146,13 +146,25 @@ pub fn draw( let commands = draw_commands(app); rect.render_widget(commands, body_layout[2]); - // Bottom chunk (plot and logs) + // Bottom chunk (tabs) let bottom_chunk = chunks[2]; - let bottom_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + let tab_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(10), Constraint::Percentage(90)].as_ref()) .split(bottom_chunk); + let tabs = draw_tabs(&app); + rect.render_widget(tabs, tab_layout[0]); + + // Tab content + let tab_content = match app.tab_index { + 0 => draw_logs(app_history), + 1 => draw_logs(app_history), + 2 => draw_logs(app_history), + _ => unreachable!(), + }; + rect.render_widget(tab_content, tab_layout[1]); + // Plot // Prepare the data let data: Vec<(f64, f64)> = app_history @@ -165,28 +177,15 @@ pub fn draw( let start_index = (data.len() as f64 * 0.1) as usize; // Calculate data points and remove infinities - let mut norm_data: Vec<(f64, f64)> = data + let mut _norm_data: Vec<(f64, f64)> = data .iter() .filter(|&(_, y)| !y.is_infinite()) .skip(start_index) .map(|&(x, y)| (x, y)) .collect(); - let plot = draw_plot(&mut norm_data); - rect.render_widget(plot, bottom_layout[0]); + //let plot = draw_plot(&mut norm_data); + //rect.render_widget(plot, bottom_layout[0]); - // Logs - // Iterate through app_history and get cycle and objf - let logtext: Vec = app_history - .cycles - .iter() - .map(|entry| { - let cycle = entry.cycle.to_string(); - let objf = entry.objf.to_string(); - Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) - }) - .collect(); - let logs = draw_logs(&logtext); - rect.render_widget(logs, bottom_layout[1]) } From 64eb81d29499cf5515690d76583888003c4a0d4c Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Thu, 9 Nov 2023 08:59:05 +0100 Subject: [PATCH 23/37] Working on tabbed content Also renamed AppHistory to CycleHistory for more intuitive naming, and to support different communications over tx to the TUI through enumeration in the future. --- src/tui/components.rs | 39 +++++++++++++++++++++++++-------------- src/tui/state.rs | 8 ++++---- src/tui/ui.rs | 42 ++++++++++++++++++++---------------------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/tui/components.rs b/src/tui/components.rs index d2eb741b..13d0df13 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -13,7 +13,7 @@ use ratatui::{ }, }; -use super::{App, state::AppHistory}; +use super::{App, state::CycleHistory}; use crate::prelude::settings::run::Data; @@ -217,25 +217,36 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { ) } -pub fn draw_logs<'a>(app_history: &AppHistory) -> Paragraph<'a> { +pub fn draw_logs<'a>(app_history: &CycleHistory, height: u16) -> Paragraph<'a> { let text: Vec = app_history - .cycles - .iter() - .map(|entry| { - let cycle = entry.cycle.to_string(); - let objf = entry.objf.to_string(); - Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) - }) - .collect(); - - Paragraph::new(text.clone()) - .block(Block::new().title(" Logs ").borders(Borders::ALL)) - .style(Style::new().white()) + .cycles + .iter() + .map(|entry| { + let cycle = entry.cycle.to_string(); + let objf = entry.objf.to_string(); + Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) + }) + .collect(); + + let to_text = text.len(); + // Prevent underflow with saturating_sub + let from_text = to_text.saturating_sub(height as usize); + + let show_text = if from_text < to_text { + text[from_text..to_text].to_vec() + } else { + Vec::new() + }; + + Paragraph::new(show_text) + .block(Block::default().title(" Logs ").borders(Borders::ALL)) + .style(Style::default().fg(Color::White)) .alignment(Alignment::Left) .wrap(Wrap { trim: true }) } + pub fn draw_tabs<'a>(app: &App) -> Tabs<'a> { let titles = app.tab_titles.clone(); diff --git a/src/tui/state.rs b/src/tui/state.rs index 50a60312..54dff4f5 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -1,20 +1,20 @@ use crate::prelude::output::NPCycle; #[derive(Debug, Clone)] -pub struct AppHistory { +pub struct CycleHistory { pub cycles: Vec, } -impl AppHistory { +impl CycleHistory { pub fn new() -> Self { - AppHistory { cycles: Vec::new() } + CycleHistory { cycles: Vec::new() } } pub fn add_cycle(&mut self, cycle: NPCycle) { self.cycles.push(cycle); } } -impl Default for AppHistory { +impl Default for CycleHistory { fn default() -> Self { Self::new() } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index b509f9de..e3254bfc 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -5,7 +5,7 @@ use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, text::Line, - Frame, Terminal, + Frame, Terminal, widgets::Paragraph, }; use std::{ io::stdout, @@ -15,7 +15,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use super::{ inputs::{events::Events, InputEvent}, - state::AppHistory, + state::CycleHistory, App, AppReturn, }; @@ -28,7 +28,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut app = App::new(); - let mut app_history = AppHistory::new(); + let mut cycle_history = CycleHistory::new(); terminal.clear()?; @@ -61,17 +61,17 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() } // If we receive a new NPCycle, add it to the app_history - if !app_history + if !cycle_history .cycles .iter() .any(|state| state.cycle == app.state.cycle) { - app_history.add_cycle(app.state.clone()); + cycle_history.add_cycle(app.state.clone()); } // Draw the terminal terminal - .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) + .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) .unwrap(); // Handle inputs @@ -89,7 +89,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; terminal - .draw(|rect| draw(rect, &app, &app_history, elapsed_time, &settings)) + .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) .unwrap(); Ok(()) } @@ -97,7 +97,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() pub fn draw( rect: &mut Frame, app: &App, - app_history: &AppHistory, + cycle_history: &CycleHistory, elapsed_time: Duration, settings: &Data, ) { @@ -156,18 +156,9 @@ pub fn draw( let tabs = draw_tabs(&app); rect.render_widget(tabs, tab_layout[0]); - // Tab content - let tab_content = match app.tab_index { - 0 => draw_logs(app_history), - 1 => draw_logs(app_history), - 2 => draw_logs(app_history), - _ => unreachable!(), - }; - rect.render_widget(tab_content, tab_layout[1]); - // Plot // Prepare the data - let data: Vec<(f64, f64)> = app_history + let data: Vec<(f64, f64)> = cycle_history .cycles .iter() .enumerate() @@ -177,15 +168,22 @@ pub fn draw( let start_index = (data.len() as f64 * 0.1) as usize; // Calculate data points and remove infinities - let mut _norm_data: Vec<(f64, f64)> = data + let mut norm_data: Vec<(f64, f64)> = data .iter() .filter(|&(_, y)| !y.is_infinite()) .skip(start_index) .map(|&(x, y)| (x, y)) .collect(); - //let plot = draw_plot(&mut norm_data); - //rect.render_widget(plot, bottom_layout[0]); - + // Tab content + let tab_content_height = tab_layout[1].height; + let tab_content = match app.tab_index { + 0 => draw_logs(cycle_history, tab_content_height), + 0 => draw_logs(cycle_history, tab_content_height), + 2 => draw_logs(cycle_history, tab_content_height), + _ => unreachable!(), + }; + rect.render_widget(tab_content, tab_layout[1]); + } From fb900c48f7f9110bb7ff1e9dd0e3c98360f57051 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Thu, 9 Nov 2023 14:42:53 +0100 Subject: [PATCH 24/37] Plot implemented for tabs --- src/tui/actions.rs | 2 +- src/tui/components.rs | 7 ++----- src/tui/mod.rs | 14 +++++++++++--- src/tui/ui.rs | 26 +++++++++++++++++--------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/tui/actions.rs b/src/tui/actions.rs index 68eafb25..4413b2f6 100644 --- a/src/tui/actions.rs +++ b/src/tui/actions.rs @@ -24,7 +24,7 @@ impl Action { match self { Action::Quit => &[Key::Char('q')], Action::Stop => &[Key::Ctrl('d')], - Action::Next => &[Key::Char('n')] + Action::Next => &[Key::Char('n')], } } } diff --git a/src/tui/components.rs b/src/tui/components.rs index 13d0df13..05c0432e 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -9,11 +9,11 @@ use ratatui::{ text::{Line, Span}, widgets::{ Axis, Block, BorderType, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Table, - Wrap, Tabs, + Tabs, Wrap, }, }; -use super::{App, state::CycleHistory}; +use super::{state::CycleHistory, App}; use crate::prelude::settings::run::Data; @@ -245,10 +245,7 @@ pub fn draw_logs<'a>(app_history: &CycleHistory, height: u16) -> Paragraph<'a> { .wrap(Wrap { trim: true }) } - - pub fn draw_tabs<'a>(app: &App) -> Tabs<'a> { - let titles = app.tab_titles.clone(); let index = app.tab_index.clone(); let tabs = Tabs::new(titles.clone()) diff --git a/src/tui/mod.rs b/src/tui/mod.rs index cb381666..bf7dda18 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -36,7 +36,12 @@ impl App { let tab_index = 0; let tab_titles = vec!["Logs", "Plot", "Settings"]; - Self { actions, state , tab_index, tab_titles} + Self { + actions, + state, + tab_index, + tab_titles, + } } /// Handle a user action @@ -51,7 +56,7 @@ impl App { let filename = "stop"; File::create(filename).unwrap(); AppReturn::Continue - }, + } Action::Next => { self.tab_index = self.tab_index + 1; if self.tab_index >= self.tab_titles.len() { @@ -61,7 +66,10 @@ impl App { } } } else { - tracing::trace!("The {} key was registered, but it has no associated action", key); + tracing::trace!( + "The {} key was registered, but it has no associated action", + key + ); AppReturn::Continue } } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index e3254bfc..dd2010d8 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -4,8 +4,10 @@ use eyre::Result; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, + prelude::Rect, text::Line, - Frame, Terminal, widgets::Paragraph, + widgets::Paragraph, + Frame, Terminal, }; use std::{ io::stdout, @@ -176,14 +178,20 @@ pub fn draw( .collect(); // Tab content - let tab_content_height = tab_layout[1].height; - let tab_content = match app.tab_index { - 0 => draw_logs(cycle_history, tab_content_height), - 0 => draw_logs(cycle_history, tab_content_height), - 2 => draw_logs(cycle_history, tab_content_height), + let inner_height = tab_layout[1].height; + match app.tab_index { + 0 => { + let logs = draw_logs(cycle_history, inner_height); + rect.render_widget(logs, tab_layout[1]); + } + 1 => { + let plot = draw_plot(&mut norm_data); + rect.render_widget(plot, tab_layout[1]); + } + 2 => { + let logs = draw_logs(cycle_history, inner_height); + rect.render_widget(logs, tab_layout[1]); + } _ => unreachable!(), }; - rect.render_widget(tab_content, tab_layout[1]); - - } From 3974386fb0788efb2e4cd875fa519771ff09cd9e Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Thu, 9 Nov 2023 15:14:14 +0100 Subject: [PATCH 25/37] Added parameter boundaries tab --- src/tui/components.rs | 59 ++++++++++++++++++++++++++++++++++++++++++- src/tui/mod.rs | 2 +- src/tui/ui.rs | 7 ++--- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/tui/components.rs b/src/tui/components.rs index 05c0432e..62373c48 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -4,7 +4,7 @@ use std::time::Duration; /// The purpose is to create common components with generic methods use ratatui::{ layout::{Alignment, Constraint}, - style::{Color, Modifier, Style, Stylize}, + style::{Color, Modifier, Style}, symbols, text::{Line, Span}, widgets::{ @@ -258,6 +258,63 @@ pub fn draw_tabs<'a>(app: &App) -> Tabs<'a> { tabs } +fn get_computed_settings(settings: &Data) -> Vec { + let computed = settings.computed.clone(); + let mut rows = Vec::new(); + let key_style = Style::default().fg(Color::LightCyan); + let help_style = Style::default().fg(Color::Gray); + + // Iterate over the random ranges + for (name, &(start, end)) in computed.random.names.iter().zip(&computed.random.ranges) { + let row = Row::new(vec![ + Cell::from(Span::styled(name.to_string(), key_style)), + Cell::from(Span::styled( + format!("{:.2} - {:.2}", start, end), + help_style, + )), + ]); + rows.push(row); + } + + // Iterate over the constant values + for (name, &value) in computed + .constant + .names + .iter() + .zip(&computed.constant.values) + { + let row = Row::new(vec![ + Cell::from(Span::styled(name.to_string(), key_style)), + Cell::from(Span::styled(format!("{:.2} (Constant)", value), help_style)), + ]); + rows.push(row); + } + + // Iterate over the fixed values + for (name, &value) in computed.fixed.names.iter().zip(&computed.fixed.values) { + let row = Row::new(vec![ + Cell::from(Span::styled(name.to_string(), key_style)), + Cell::from(Span::styled(format!("{:.2} (Fixed)", value), help_style)), + ]); + rows.push(row); + } + + rows +} + +pub fn draw_parameter_bounds(settings: &Data) -> Table { + let rows = get_computed_settings(&settings); + Table::new(rows) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .title(" Parameters "), + ) + .widths(&[Constraint::Percentage(20), Constraint::Percentage(80)]) // Set percentage widths for columns + .column_spacing(1) +} + fn format_time(elapsed_time: std::time::Duration) -> String { let elapsed_seconds = elapsed_time.as_secs(); let (elapsed, unit) = if elapsed_seconds < 60 { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index bf7dda18..41c49541 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -34,7 +34,7 @@ impl App { let actions = vec![Action::Quit, Action::Stop, Action::Next].into(); let state = NPCycle::new(); let tab_index = 0; - let tab_titles = vec!["Logs", "Plot", "Settings"]; + let tab_titles = vec!["Logs", "Plot", "Parameters"]; Self { actions, diff --git a/src/tui/ui.rs b/src/tui/ui.rs index dd2010d8..d919ea58 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -4,9 +4,6 @@ use eyre::Result; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, - prelude::Rect, - text::Line, - widgets::Paragraph, Frame, Terminal, }; use std::{ @@ -189,8 +186,8 @@ pub fn draw( rect.render_widget(plot, tab_layout[1]); } 2 => { - let logs = draw_logs(cycle_history, inner_height); - rect.render_widget(logs, tab_layout[1]); + let par_bounds = draw_parameter_bounds(&settings); + rect.render_widget(par_bounds, tab_layout[1]); } _ => unreachable!(), }; From ca3ef75f3d2f104ef9527af37fbc02ab0312c75d Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Thu, 9 Nov 2023 20:32:51 +0100 Subject: [PATCH 26/37] Refactor communcation to TUI Also removes (now) extraneous stop_text from NPcycle. Instead, the logs will provide that information. --- src/algorithms.rs | 4 +-- src/algorithms/npag.rs | 47 +++++++++++++++---------------- src/algorithms/postprob.rs | 29 ++++++++++--------- src/entrypoints.rs | 4 +-- src/routines/output.rs | 2 -- src/tui/components.rs | 4 +-- src/tui/ui.rs | 57 +++++++++++++++++++------------------- 7 files changed, 72 insertions(+), 75 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 05af352e..872fcaa4 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,4 +1,4 @@ -use crate::prelude::{self, output::NPCycle, settings::run::Data}; +use crate::prelude::{self, settings::run::Data}; use output::NPResult; use prelude::{datafile::Scenario, *}; @@ -22,7 +22,7 @@ pub fn initialize_algorithm( engine: Engine, settings: Data, scenarios: Vec, - tx: mpsc::UnboundedSender, + tx: mpsc::UnboundedSender, ) -> Box where S: Predict<'static> + std::marker::Sync + Clone + 'static, diff --git a/src/algorithms/npag.rs b/src/algorithms/npag.rs index 08c58d13..aae45c27 100644 --- a/src/algorithms/npag.rs +++ b/src/algorithms/npag.rs @@ -1,15 +1,18 @@ -use crate::prelude::{ - algorithms::Algorithm, - datafile::Scenario, - evaluation::sigma::{ErrorPoly, ErrorType}, - ipm, - optimization::expansion::adaptative_grid, - output::NPResult, - output::{CycleLog, NPCycle}, - prob, qr, - settings::run::Data, - simulation::predict::Engine, - simulation::predict::{sim_obs, Predict}, +use crate::{ + prelude::{ + algorithms::Algorithm, + datafile::Scenario, + evaluation::sigma::{ErrorPoly, ErrorType}, + ipm, + optimization::expansion::adaptative_grid, + output::NPResult, + output::{CycleLog, NPCycle}, + prob, qr, + settings::run::Data, + simulation::predict::Engine, + simulation::predict::{sim_obs, Predict}, + }, + tui::ui::Comm, }; use ndarray::{Array, Array1, Array2, Axis}; @@ -45,7 +48,7 @@ where cache: bool, scenarios: Vec, c: (f64, f64, f64, f64), - tx: UnboundedSender, + tx: UnboundedSender, settings: Data, } @@ -95,7 +98,7 @@ where theta: Array2, scenarios: Vec, c: (f64, f64, f64, f64), - tx: UnboundedSender, + tx: UnboundedSender, settings: Data, ) -> Self where @@ -265,16 +268,15 @@ where self.optim_gamma(); - let mut state = NPCycle { + let state = NPCycle { cycle: self.cycle, objf: -2. * self.objf, delta_objf: (self.last_objf - self.objf).abs(), nspp: self.theta.shape()[0], - stop_text: "".to_string(), theta: self.theta.clone(), gamlam: self.gamma, }; - self.tx.send(state.clone()).unwrap(); + self.tx.send(Comm::NPCycle(state.clone())).unwrap(); // Increasing objf signals instability or model misspecification. if self.last_objf > self.objf { @@ -296,8 +298,6 @@ where if (self.f1 - self.f0).abs() <= THETA_F { tracing::info!("Likelihood criteria convergence, -2LL: {:.1}", self.objf); self.converged = true; - state.stop_text = "The run converged!".to_string(); - self.tx.send(state).unwrap(); break; } else { self.f0 = self.f1; @@ -308,17 +308,13 @@ where // Stop if we have reached maximum number of cycles if self.cycle >= self.settings.parsed.config.cycles { - tracing::info!("Maximum number of cycles reached"); - state.stop_text = "No (max cycle)".to_string(); - self.tx.send(state).unwrap(); + tracing::warn!("Maximum number of cycles reached"); break; } // Stop if stopfile exists if std::path::Path::new("stop").exists() { - tracing::info!("Stopfile detected - breaking"); - state.stop_text = "No (stopped)".to_string(); - self.tx.send(state).unwrap(); + tracing::warn!("Stopfile detected - breaking"); break; } self.cycle_log @@ -329,6 +325,7 @@ where self.last_objf = self.objf; } + self.tx.send(Comm::Stop(true)).unwrap(); self.to_npresult() } } diff --git a/src/algorithms/postprob.rs b/src/algorithms/postprob.rs index 1b171200..a88bf795 100644 --- a/src/algorithms/postprob.rs +++ b/src/algorithms/postprob.rs @@ -1,14 +1,17 @@ -use crate::prelude::{ - algorithms::Algorithm, - datafile::Scenario, - evaluation::sigma::{ErrorPoly, ErrorType}, - ipm, - output::NPCycle, - output::NPResult, - prob, - settings::run::Data, - simulation::predict::Engine, - simulation::predict::{sim_obs, Predict}, +use crate::{ + prelude::{ + algorithms::Algorithm, + datafile::Scenario, + evaluation::sigma::{ErrorPoly, ErrorType}, + ipm, + output::NPCycle, + output::NPResult, + prob, + settings::run::Data, + simulation::predict::Engine, + simulation::predict::{sim_obs, Predict}, + }, + tui::ui::Comm, }; use ndarray::{Array1, Array2}; @@ -32,7 +35,7 @@ where scenarios: Vec, c: (f64, f64, f64, f64), #[allow(dead_code)] - tx: UnboundedSender, + tx: UnboundedSender, settings: Data, } @@ -66,7 +69,7 @@ where theta: Array2, scenarios: Vec, c: (f64, f64, f64, f64), - tx: UnboundedSender, + tx: UnboundedSender, settings: Data, ) -> Self where diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 91c8d40d..1371f8fd 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -62,7 +62,7 @@ where let settings = settings::run::read(settings_path); logger::setup_log(&settings); tracing::info!("Starting NPcore"); - let (tx, rx) = mpsc::unbounded_channel::(); + let (tx, rx) = mpsc::unbounded_channel::(); let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); if let Some(exclude) = &settings.parsed.config.exclude { for val in exclude { @@ -101,7 +101,7 @@ where let now = Instant::now(); let settings = settings::run::read(settings_path); logger::setup_log(&settings); - let (tx, rx) = mpsc::unbounded_channel::(); + let (tx, rx) = mpsc::unbounded_channel::(); let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); // Spawn new thread for TUI diff --git a/src/routines/output.rs b/src/routines/output.rs index 9e35298d..c19aab4b 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -292,7 +292,6 @@ pub struct NPCycle { pub objf: f64, pub gamlam: f64, pub theta: Array2, - pub stop_text: String, pub nspp: usize, pub delta_objf: f64, } @@ -303,7 +302,6 @@ impl NPCycle { objf: 0.0, gamlam: 0.0, theta: Array2::default((0, 0)), - stop_text: "".to_string(), nspp: 0, delta_objf: 0.0, } diff --git a/src/tui/components.rs b/src/tui/components.rs index 62373c48..bfffb35b 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -37,7 +37,7 @@ pub fn draw_status<'a>(app: &App, elapsed_time: Duration) -> Table<'a> { let gamma_text = format!("{:.5}", app.state.gamlam); let spp_text = format!("{}", app.state.nspp); let time_text = format_time(elapsed_time); - let stop_text = app.state.stop_text.to_string(); + let conv_text = "Placeholder".to_string(); // Define the table data let data = vec![ @@ -47,7 +47,7 @@ pub fn draw_status<'a>(app: &App, elapsed_time: Duration) -> Table<'a> { ("Gamma/Lambda", gamma_text), ("Support points", spp_text), ("Elapsed time", time_text), - ("Convergence", stop_text), + ("Convergence", conv_text), // Add more rows as needed ]; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index d919ea58..c7bcda2c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -18,10 +18,16 @@ use super::{ App, AppReturn, }; +pub enum Comm { + NPCycle(NPCycle), + Message(String), + Stop(bool), +} + use crate::prelude::{output::NPCycle, settings::run::Data}; use crate::tui::components::*; -pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { +pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { let stdout = stdout(); crossterm::terminal::enable_raw_mode()?; let backend = CrosstermBackend::new(stdout); @@ -40,34 +46,29 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() // Main UI loop loop { - app.state = match rx.try_recv() { - Ok(state) => state, - Err(_) => app.state, + let _ = match rx.try_recv() { + Ok(comm) => match comm { + Comm::NPCycle(cycle) => { + app.state = cycle.clone(); + cycle_history.add_cycle(cycle); + } + Comm::Message(_msg) => {} + Comm::Stop(stop) => { + if stop { + break; //TODO: Replace with graceful exit from TUI + } + } + }, + Err(_) => {} }; - // Stop incrementing elapsed time if conv is true - if app.state.stop_text.is_empty() { - let now = Instant::now(); - if now.duration_since(start_time) > tick_rate { - elapsed_time += now.duration_since(start_time); - start_time = now; - } - } - - // Break if we receive a stop text - if !app.state.stop_text.is_empty() { - break; - } - - // If we receive a new NPCycle, add it to the app_history - if !cycle_history - .cycles - .iter() - .any(|state| state.cycle == app.state.cycle) - { - cycle_history.add_cycle(app.state.clone()); + // Update elapsed time + let now = Instant::now(); + if now.duration_since(start_time) > tick_rate { + elapsed_time += now.duration_since(start_time); + start_time = now; } - + // Draw the terminal terminal .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) @@ -87,9 +88,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<() terminal.clear()?; terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; - terminal - .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) - .unwrap(); + //terminal.draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)).unwrap(); Ok(()) } From 924762dd66222c3d4b3f948f675da57ea90ba573 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Thu, 9 Nov 2023 20:46:27 +0100 Subject: [PATCH 27/37] Minor cleanup --- src/algorithms/postprob.rs | 1 - src/entrypoints.rs | 1 - src/tui/ui.rs | 17 ++++++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/algorithms/postprob.rs b/src/algorithms/postprob.rs index a88bf795..c0d1ee94 100644 --- a/src/algorithms/postprob.rs +++ b/src/algorithms/postprob.rs @@ -4,7 +4,6 @@ use crate::{ datafile::Scenario, evaluation::sigma::{ErrorPoly, ErrorType}, ipm, - output::NPCycle, output::NPResult, prob, settings::run::Data, diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 1371f8fd..145cd0b6 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -1,5 +1,4 @@ use crate::algorithms::initialize_algorithm; -use crate::prelude::output::NPCycle; use crate::prelude::{ output::NPResult, predict::{Engine, Predict}, diff --git a/src/tui/ui.rs b/src/tui/ui.rs index c7bcda2c..33f80f8c 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -41,9 +41,8 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { let tick_rate = Duration::from_millis(200); let mut events = Events::new(tick_rate); - let mut start_time = Instant::now(); + let start_time = Instant::now(); let mut elapsed_time = Duration::from_secs(0); - // Main UI loop loop { let _ = match rx.try_recv() { @@ -64,11 +63,8 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { // Update elapsed time let now = Instant::now(); - if now.duration_since(start_time) > tick_rate { - elapsed_time += now.duration_since(start_time); - start_time = now; - } - + elapsed_time = now.duration_since(start_time); + // Draw the terminal terminal .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) @@ -85,10 +81,13 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { } } - terminal.clear()?; + terminal + .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) + .unwrap(); + println!(); terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; - //terminal.draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)).unwrap(); + Ok(()) } From e9805726178cc045d256ae612a9a308e8c7f2617 Mon Sep 17 00:00:00 2001 From: Markus Herberg Hovd Date: Sat, 11 Nov 2023 13:42:33 +0100 Subject: [PATCH 28/37] Working on getting logs to TUI (BROKEN) --- src/entrypoints.rs | 7 +++-- src/logger.rs | 77 +++++++++++++++++++--------------------------- src/tui/ui.rs | 3 ++ 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 145cd0b6..ef981144 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -59,9 +59,9 @@ where { let now = Instant::now(); let settings = settings::run::read(settings_path); - logger::setup_log(&settings); - tracing::info!("Starting NPcore"); let (tx, rx) = mpsc::unbounded_channel::(); + logger::setup_log(&settings, tx.clone()); + tracing::info!("Starting NPcore"); let mut scenarios = datafile::parse(&settings.parsed.paths.data).unwrap(); if let Some(exclude) = &settings.parsed.config.exclude { for val in exclude { @@ -99,8 +99,9 @@ where { let now = Instant::now(); let settings = settings::run::read(settings_path); - logger::setup_log(&settings); let (tx, rx) = mpsc::unbounded_channel::(); + logger::setup_log(&settings, tx.clone()); + let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); // Spawn new thread for TUI diff --git a/src/logger.rs b/src/logger.rs index aa5a0026..d4714120 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,66 +1,53 @@ -use tracing_subscriber::fmt::time::FormatTime; -use tracing_subscriber::fmt::{self, format::Format}; -use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; -use tracing_subscriber::registry::Registry; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::EnvFilter; +use tracing::Subscriber; +use tracing_subscriber::{fmt, layer::Context, prelude::*, registry::Registry, Layer, EnvFilter}; +use tokio::sync::mpsc::UnboundedSender; use crate::routines::settings::run::Data; +use crate::tui::ui::Comm; -pub fn setup_log(settings: &Data) { +pub fn setup_log(settings: &Data, tx: UnboundedSender) { let log_level = settings .parsed .config .log_level .as_ref() .map(|level| level.as_str()) - .unwrap_or("info"); // Default to 'info' if not set + .unwrap_or("info"); let env_filter = EnvFilter::new(log_level); + let format_layer = fmt::layer().compact(); - let stdout_log = Format::default().compact().with_timer(CompactTimestamp); + // Custom RatatuiLogLayer + let ratatui_log_layer = RatatuiLogLayer::new(tx); - // Start with a base subscriber from the registry - let subscriber = Registry::default().with(env_filter); + // Combine layers + let subscriber = Registry::default() + .with(env_filter) + .with(format_layer) + .with(ratatui_log_layer); - // Check if a log file path is provided - if let Some(log_path) = &settings.parsed.paths.log_out { - // Ensure the log file is created or truncated - let file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(log_path) - .expect("Failed to open log file"); - - let file_layer = fmt::layer() - .with_writer(file) - .with_ansi(false) - .event_format(stdout_log.clone()); + subscriber.init(); +} - // Add the file layer to the subscriber - subscriber.with(file_layer).init(); - } else { - // Add stdout layer only if no log file is specified - let stdout_layer = fmt::layer() - .event_format(stdout_log) - .with_writer(std::io::stdout); +// Custom Layer for sending log messages +struct RatatuiLogLayer { + sender: UnboundedSender, +} - // Add the stdout layer to the subscriber - subscriber.with(stdout_layer).init(); +impl RatatuiLogLayer { + pub fn new(sender: UnboundedSender) -> Self { + RatatuiLogLayer { + sender + } } - - tracing::info!("Logging is configured with level: {}", log_level); } -#[derive(Clone)] -struct CompactTimestamp; - -impl FormatTime for CompactTimestamp { - fn format_time( - &self, - w: &mut tracing_subscriber::fmt::format::Writer<'_>, - ) -> Result<(), std::fmt::Error> { - write!(w, "{}", chrono::Local::now().format("%H:%M:%S")) +impl Layer for RatatuiLogLayer { + fn on_event(&self, event: &tracing::Event<'_>, ctx: Context) { + let mut buffer = String::new(); + if ctx.event_format(event, &mut buffer).is_ok() { + // Send the formatted message through Comm + let _ = self.sender.send(Comm::LogMessage(buffer)); + } } } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 33f80f8c..7011ac87 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -22,6 +22,7 @@ pub enum Comm { NPCycle(NPCycle), Message(String), Stop(bool), + LogMessage(String), } use crate::prelude::{output::NPCycle, settings::run::Data}; @@ -34,6 +35,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { let mut terminal = Terminal::new(backend)?; let mut app = App::new(); let mut cycle_history = CycleHistory::new(); + let mut log_history: Vec = Vec::new(); terminal.clear()?; @@ -57,6 +59,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { break; //TODO: Replace with graceful exit from TUI } } + Comm::LogMessage(msg) => log_history.push(msg), }, Err(_) => {} }; From 7e4ceb3d8cb76098afa888cc11ba00fc353e599f Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 11 Nov 2023 16:30:51 +0100 Subject: [PATCH 29/37] Working example log_out is no longer an option, but this can be reverted --- src/entrypoints.rs | 1 - src/logger.rs | 110 +++++++++++++++++++++++++---------- src/routines/settings/run.rs | 2 +- src/tui/components.rs | 16 ++--- src/tui/ui.rs | 25 +++++++- 5 files changed, 107 insertions(+), 47 deletions(-) diff --git a/src/entrypoints.rs b/src/entrypoints.rs index ef981144..37eb3056 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -101,7 +101,6 @@ where let settings = settings::run::read(settings_path); let (tx, rx) = mpsc::unbounded_channel::(); logger::setup_log(&settings, tx.clone()); - let mut algorithm = initialize_algorithm(engine.clone(), settings.clone(), scenarios, tx); // Spawn new thread for TUI diff --git a/src/logger.rs b/src/logger.rs index d4714120..348b3469 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,11 +1,16 @@ -use tracing::Subscriber; -use tracing_subscriber::{fmt, layer::Context, prelude::*, registry::Registry, Layer, EnvFilter}; -use tokio::sync::mpsc::UnboundedSender; - use crate::routines::settings::run::Data; use crate::tui::ui::Comm; +use tokio::sync::mpsc::UnboundedSender; +use tracing_subscriber::fmt::time::FormatTime; +use tracing_subscriber::fmt::{self}; +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::registry::Registry; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; +use std::io::{Write, self}; -pub fn setup_log(settings: &Data, tx: UnboundedSender) { +pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { + // Use the log level defined in configuration file, or default to info let log_level = settings .parsed .config @@ -15,39 +20,82 @@ pub fn setup_log(settings: &Data, tx: UnboundedSender) { .unwrap_or("info"); let env_filter = EnvFilter::new(log_level); - let format_layer = fmt::layer().compact(); - // Custom RatatuiLogLayer - let ratatui_log_layer = RatatuiLogLayer::new(tx); + // Define a registry with that level as an environment filter + let subscriber = Registry::default().with(env_filter); - // Combine layers - let subscriber = Registry::default() - .with(env_filter) - .with(format_layer) - .with(ratatui_log_layer); + // Define a layer for the log file + let file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&settings.parsed.paths.log_out) + .expect("Failed to open log file"); - subscriber.init(); -} + let file_layer = fmt::layer().with_writer(file).with_ansi(false); -// Custom Layer for sending log messages -struct RatatuiLogLayer { - sender: UnboundedSender, -} + // Define layer for stdout + let stdout_layer = if !settings.parsed.config.tui { + let layer = fmt::layer() + .with_writer(std::io::stdout) + .with_ansi(true) + .with_target(false) + .with_timer(CompactTimestamp); + Some(layer) + } else { + None + }; -impl RatatuiLogLayer { - pub fn new(sender: UnboundedSender) -> Self { - RatatuiLogLayer { - sender + // Define layer for TUI + let tui_writer_closure = move || { + TuiWriter { + ui_tx: ui_tx.clone(), // Ensure this clone is okay with your design (consider the lifetime of _ui_tx) } - } + }; + + let tui_layer = if settings.parsed.config.tui { + let layer = fmt::layer() + .with_writer(tui_writer_closure) + .with_ansi(true) + .with_target(false) + .with_timer(CompactTimestamp); + Some(layer) + } else { + None + }; + + // Combine layers with subscriber + subscriber.with(file_layer).with(stdout_layer).with(tui_layer).init(); + tracing::debug!("Logging is configured with level: {}", log_level); } -impl Layer for RatatuiLogLayer { - fn on_event(&self, event: &tracing::Event<'_>, ctx: Context) { - let mut buffer = String::new(); - if ctx.event_format(event, &mut buffer).is_ok() { - // Send the formatted message through Comm - let _ = self.sender.send(Comm::LogMessage(buffer)); - } +#[derive(Clone)] +struct CompactTimestamp; + +impl FormatTime for CompactTimestamp { + fn format_time( + &self, + w: &mut tracing_subscriber::fmt::format::Writer<'_>, + ) -> Result<(), std::fmt::Error> { + write!(w, "{}", chrono::Local::now().format("%H:%M:%S")) } } + +struct TuiWriter { + ui_tx: UnboundedSender, +} + +impl Write for TuiWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let msg = String::from_utf8_lossy(buf); + // Send the message through the channel + self.ui_tx.send(Comm::LogMessage(msg.to_string())) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to send log message"))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + // Flushing is not required for this use case + Ok(()) + } +} \ No newline at end of file diff --git a/src/routines/settings/run.rs b/src/routines/settings/run.rs index 6051b7bc..72a2ee96 100644 --- a/src/routines/settings/run.rs +++ b/src/routines/settings/run.rs @@ -49,7 +49,7 @@ pub struct Parsed { #[derive(Deserialize, Clone, Debug)] pub struct Paths { pub data: String, - pub log_out: Option, + pub log_out: String, pub prior_dist: Option, } diff --git a/src/tui/components.rs b/src/tui/components.rs index bfffb35b..cf40ee90 100644 --- a/src/tui/components.rs +++ b/src/tui/components.rs @@ -13,7 +13,7 @@ use ratatui::{ }, }; -use super::{state::CycleHistory, App}; +use super::App; use crate::prelude::settings::run::Data; @@ -217,21 +217,15 @@ pub fn draw_plot(norm_data: &mut [(f64, f64)]) -> Chart { ) } -pub fn draw_logs<'a>(app_history: &CycleHistory, height: u16) -> Paragraph<'a> { - let text: Vec = app_history - .cycles - .iter() - .map(|entry| { - let cycle = entry.cycle.to_string(); - let objf = entry.objf.to_string(); - Line::from(format!("Cycle {} has -2LL {}", cycle, objf)) - }) - .collect(); +pub fn draw_logs<'a>(log_history: &'a Vec, height: u16) -> Paragraph<'a> { + // Convert each String in log_history to a Line + let text: Vec = log_history.iter().map(|s| Line::from(s.as_str())).collect(); let to_text = text.len(); // Prevent underflow with saturating_sub let from_text = to_text.saturating_sub(height as usize); + // Create a slice of the text to be displayed let show_text = if from_text < to_text { text[from_text..to_text].to_vec() } else { diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 7011ac87..46931921 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -70,7 +70,16 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { // Draw the terminal terminal - .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) + .draw(|rect| { + draw( + rect, + &app, + &cycle_history, + elapsed_time, + &settings, + &log_history, + ) + }) .unwrap(); // Handle inputs @@ -85,7 +94,16 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { } terminal - .draw(|rect| draw(rect, &app, &cycle_history, elapsed_time, &settings)) + .draw(|rect| { + draw( + rect, + &app, + &cycle_history, + elapsed_time, + &settings, + &log_history, + ) + }) .unwrap(); println!(); terminal.show_cursor()?; @@ -100,6 +118,7 @@ pub fn draw( cycle_history: &CycleHistory, elapsed_time: Duration, settings: &Data, + log_history: &Vec, ) { let size = rect.size(); @@ -179,7 +198,7 @@ pub fn draw( let inner_height = tab_layout[1].height; match app.tab_index { 0 => { - let logs = draw_logs(cycle_history, inner_height); + let logs = draw_logs(log_history, inner_height); rect.render_widget(logs, tab_layout[1]); } 1 => { From 02b70bf792ab06d2460eefe17303c719fd7d0911 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 11 Nov 2023 17:14:49 +0100 Subject: [PATCH 30/37] Nicer formatting --- src/logger.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/logger.rs b/src/logger.rs index 348b3469..6bbdfdc0 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,5 +1,6 @@ use crate::routines::settings::run::Data; use crate::tui::ui::Comm; +use std::io::{self, Write}; use tokio::sync::mpsc::UnboundedSender; use tracing_subscriber::fmt::time::FormatTime; use tracing_subscriber::fmt::{self}; @@ -7,7 +8,6 @@ use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; -use std::io::{Write, self}; pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { // Use the log level defined in configuration file, or default to info @@ -32,7 +32,10 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { .open(&settings.parsed.paths.log_out) .expect("Failed to open log file"); - let file_layer = fmt::layer().with_writer(file).with_ansi(false); + let file_layer = fmt::layer() + .with_writer(file) + .with_ansi(false) + .with_timer(CompactTimestamp); // Define layer for stdout let stdout_layer = if !settings.parsed.config.tui { @@ -56,7 +59,7 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { let tui_layer = if settings.parsed.config.tui { let layer = fmt::layer() .with_writer(tui_writer_closure) - .with_ansi(true) + .with_ansi(false) .with_target(false) .with_timer(CompactTimestamp); Some(layer) @@ -65,7 +68,11 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { }; // Combine layers with subscriber - subscriber.with(file_layer).with(stdout_layer).with(tui_layer).init(); + subscriber + .with(file_layer) + .with(stdout_layer) + .with(tui_layer) + .init(); tracing::debug!("Logging is configured with level: {}", log_level); } @@ -89,7 +96,8 @@ impl Write for TuiWriter { fn write(&mut self, buf: &[u8]) -> io::Result { let msg = String::from_utf8_lossy(buf); // Send the message through the channel - self.ui_tx.send(Comm::LogMessage(msg.to_string())) + self.ui_tx + .send(Comm::LogMessage(msg.to_string())) .map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to send log message"))?; Ok(buf.len()) } @@ -98,4 +106,4 @@ impl Write for TuiWriter { // Flushing is not required for this use case Ok(()) } -} \ No newline at end of file +} From bdf8ae2d895b191db1c4dba91373855911ada045 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 11 Nov 2023 18:01:50 +0100 Subject: [PATCH 31/37] Added more logging --- examples/bimodal_ke/config.toml | 2 +- src/algorithms/npag.rs | 5 ++--- src/routines/output.rs | 4 ++++ src/tui/ui.rs | 6 ++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/bimodal_ke/config.toml b/examples/bimodal_ke/config.toml index e193872c..4c5ed00a 100644 --- a/examples/bimodal_ke/config.toml +++ b/examples/bimodal_ke/config.toml @@ -8,7 +8,7 @@ cycles = 1024 engine = "NPAG" init_points = 2129 seed = 347 -tui = true +tui = false pmetrics_outputs = true cache = true idelta = 0.1 diff --git a/src/algorithms/npag.rs b/src/algorithms/npag.rs index aae45c27..de8ef190 100644 --- a/src/algorithms/npag.rs +++ b/src/algorithms/npag.rs @@ -249,9 +249,8 @@ where // If a support point is dropped, log it if self.psi.ncols() != keep.len() { tracing::info!( - "QR decomposition dropped {} SPP, kept {}", + "QRD dropped {} support point(s)", self.psi.ncols() - keep.len(), - keep.len(), ); } @@ -296,7 +295,7 @@ where if self.eps <= THETA_E { self.f1 = pyl.mapv(|x| x.ln()).sum(); if (self.f1 - self.f0).abs() <= THETA_F { - tracing::info!("Likelihood criteria convergence, -2LL: {:.1}", self.objf); + tracing::info!("The run converged"); self.converged = true; break; } else { diff --git a/src/routines/output.rs b/src/routines/output.rs index c19aab4b..6d290431 100644 --- a/src/routines/output.rs +++ b/src/routines/output.rs @@ -77,6 +77,7 @@ impl NPResult { /// Writes theta, which containts the population support points and their associated probabilities /// Each row is one support point, the last column being probability pub fn write_theta(&self) { + tracing::info!("Writing final parameter distribution..."); let result = (|| { let theta: Array2 = self.theta.clone(); let w: Array1 = self.w.clone(); @@ -105,6 +106,7 @@ impl NPResult { /// Writes the posterior support points for each individual pub fn write_posterior(&self) { + tracing::info!("Writing posterior parameter probabilities..."); let result = (|| { let theta: Array2 = self.theta.clone(); let w: Array1 = self.w.clone(); @@ -149,6 +151,7 @@ impl NPResult { /// Write the observations, which is the reformatted input data pub fn write_obs(&self) { + tracing::info!("Writing (expanded) observations..."); let result = (|| { let scenarios = self.scenarios.clone(); @@ -182,6 +185,7 @@ impl NPResult { where S: Predict<'static> + std::marker::Sync + std::marker::Send + 'static + Clone, { + tracing::info!("Writing individual predictions..."); let result = (|| { let mut scenarios = self.scenarios.clone(); // Add an event interval to each scenario diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 46931921..1f7ed044 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -44,7 +44,9 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { let mut events = Events::new(tick_rate); let start_time = Instant::now(); + #[allow(unused_assignments)] let mut elapsed_time = Duration::from_secs(0); + // Main UI loop loop { let _ = match rx.try_recv() { @@ -56,7 +58,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { Comm::Message(_msg) => {} Comm::Stop(stop) => { if stop { - break; //TODO: Replace with graceful exit from TUI + //exit(-1); //TODO: Replace with graceful exit from TUI } } Comm::LogMessage(msg) => log_history.push(msg), @@ -93,6 +95,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { } } + // Draw one last image terminal .draw(|rect| { draw( @@ -105,7 +108,6 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { ) }) .unwrap(); - println!(); terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; From 1d575f934cff5725fa5069ae5f1712f7bdb4be4b Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Sat, 11 Nov 2023 18:33:18 +0100 Subject: [PATCH 32/37] Minor adjustments --- examples/bimodal_ke/config.toml | 2 +- src/algorithms/npag.rs | 3 ++- src/entrypoints.rs | 1 + src/tui/ui.rs | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/bimodal_ke/config.toml b/examples/bimodal_ke/config.toml index 4c5ed00a..e193872c 100644 --- a/examples/bimodal_ke/config.toml +++ b/examples/bimodal_ke/config.toml @@ -8,7 +8,7 @@ cycles = 1024 engine = "NPAG" init_points = 2129 seed = 347 -tui = false +tui = true pmetrics_outputs = true cache = true idelta = 0.1 diff --git a/src/algorithms/npag.rs b/src/algorithms/npag.rs index de8ef190..ef95234a 100644 --- a/src/algorithms/npag.rs +++ b/src/algorithms/npag.rs @@ -199,8 +199,9 @@ where pub fn run(&mut self) -> NPResult { while self.eps > THETA_E { // Enter a span for each cycle, provding context for further errors - let cycle_span = tracing::span!(tracing::Level::INFO, "Cycle", cycle = self.cycle); + let cycle_span = tracing::span!(tracing::Level::INFO, "Cycle", cycle = self.cycle); let _enter = cycle_span.enter(); + // psi n_sub rows, nspp columns let cache = if self.cycle == 1 { false } else { self.cache }; let ypred = sim_obs(&self.engine, &self.scenarios, &self.theta, cache); diff --git a/src/entrypoints.rs b/src/entrypoints.rs index 37eb3056..655c7a2f 100644 --- a/src/entrypoints.rs +++ b/src/entrypoints.rs @@ -85,6 +85,7 @@ where if let Some(write) = &settings.parsed.config.pmetrics_outputs { result.write_outputs(*write, &engine, idelta, tad); } + tracing::info!("Program complete"); Ok(result) } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 1f7ed044..2964a1c2 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -110,6 +110,7 @@ pub fn start_ui(mut rx: UnboundedReceiver, settings: Data) -> Result<()> { .unwrap(); terminal.show_cursor()?; crossterm::terminal::disable_raw_mode()?; + println!(); Ok(()) } From 4cf8db81aa847effb81ddc8f4df73c4dd74ff8c3 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Mon, 13 Nov 2023 18:03:54 +0100 Subject: [PATCH 33/37] Minor change of log text --- src/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.rs b/src/logger.rs index 6bbdfdc0..8d68c4fd 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -30,7 +30,7 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { .write(true) .truncate(true) .open(&settings.parsed.paths.log_out) - .expect("Failed to open log file"); + .expect("Failed to open log file - does the directory exist?"); let file_layer = fmt::layer() .with_writer(file) From 0d7b12ad542e675656fb1f1a398c61a71e7ad321 Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Mon, 13 Nov 2023 18:10:54 +0100 Subject: [PATCH 34/37] Convert desired log level from settings to lowercase --- src/logger.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/logger.rs b/src/logger.rs index 8d68c4fd..726fcd42 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -17,7 +17,8 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { .log_level .as_ref() .map(|level| level.as_str()) - .unwrap_or("info"); + .unwrap_or("info") + .to_lowercase(); let env_filter = EnvFilter::new(log_level); From 39ebd6d528b17c22858b9bb1e7b84cf1cbf11c8a Mon Sep 17 00:00:00 2001 From: Markus Hovd Date: Mon, 13 Nov 2023 18:20:39 +0100 Subject: [PATCH 35/37] Temporarily exclude non-working models and files --- Cargo.toml | 2 +- src/logger.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d08c093..2c673fa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ description = "Rust library with the building blocks needed to create new Non-Pa license = "GPL-3.0" documentation = "https://lapkb.github.io/NPcore/npcore/" repository = "https://github.com/LAPKB/NPcore" -exclude = [".github/*", ".vscode/*"] +exclude = [".github/*", ".vscode/*", "debug.rs", "examples/faer_qr.rs", "examples/drusano/*", "examples/two_eq_lag/*", "examples/vori/*"] [dependencies] dashmap = "5.5.3" diff --git a/src/logger.rs b/src/logger.rs index 726fcd42..2b30e883 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -20,7 +20,7 @@ pub fn setup_log(settings: &Data, ui_tx: UnboundedSender) { .unwrap_or("info") .to_lowercase(); - let env_filter = EnvFilter::new(log_level); + let env_filter = EnvFilter::new(&log_level); // Define a registry with that level as an environment filter let subscriber = Registry::default().with(env_filter); From b873ec9397636cde0e57b1a2292ef113fc24d2e9 Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:01:24 +0100 Subject: [PATCH 36/37] Update README.md Added description of current and future algorithms, and their respective references --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d43bd140..45bfaf35 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,29 @@ [![Build](https://github.com/LAPKB/NPcore/actions/workflows/rust.yml/badge.svg)](https://github.com/LAPKB/NPcore/actions/workflows/rust.yml) [![Security Audit](https://github.com/LAPKB/NPcore/actions/workflows/security_audit.yml/badge.svg)](https://github.com/LAPKB/NPcore/actions/workflows/security_audit.yml) -Rust Library with the building blocks needed to create new Non-Parametric algorithms and its integration with [Pmetrics]([https://link-url-here.org](https://github.com/LAPKB/Pmetrics)). +Rust library with the building blocks to create and implement new non-parametric algorithms and their integration with [Pmetrics](https://github.com/LAPKB/Pmetrics). ## Implemented functionality * Solver for ODE-based population pharmacokinetic models * Supports the Pmetrics data format for seamless integration -* Basic NPAG implementation for parameter estimation * Covariate support, carry-forward or linear interpolation * Option to cache results for improved speed * Powerful simulation engine * Informative Terminal User Interface (TUI) +## Available algoritms + +This project aims to implement several algorithms for non-parametric population pharmacokinetic modelling. + +- [x] Non Parametric Adaptive Grid (NPAG) + - [Yamada et al (2021)](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7823953/) + - [Neely et al (2012)](https://pubmed.ncbi.nlm.nih.gov/22722776/) +- [ ] Non Parametric Optimal Design (NPOD) + - [Otalvaro et al (2023)](https://pubmed.ncbi.nlm.nih.gov/36478350/) + - [Leary et al (2003)](https://www.page-meeting.org/default.asp?abstract=421) +- [ ] Non Parametric Simulated Annealing (NPSA) + - [Chen et al (2023)](https://arxiv.org/abs/2301.12656) ## Examples From db56cb2e400986a9879f5aa48971e9a6b10cfc15 Mon Sep 17 00:00:00 2001 From: "Julian D. Otalvaro" Date: Thu, 23 Nov 2023 17:00:30 +0000 Subject: [PATCH 37/37] Lag re-implemented --- examples/bimodal_ke/main.rs | 11 +++++++---- src/routines/simulation/predict.rs | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/bimodal_ke/main.rs b/examples/bimodal_ke/main.rs index 8a5626b2..7ea4acdb 100644 --- a/examples/bimodal_ke/main.rs +++ b/examples/bimodal_ke/main.rs @@ -52,14 +52,17 @@ struct Ode {} impl<'a> Predict<'a> for Ode { type Model = Model; type State = State; - fn initial_system(&self, params: &Vec, scenario: Scenario) -> Self::Model { + fn initial_system(&self, params: &Vec, scenario: Scenario) -> (Self::Model, Scenario) { let params = HashMap::from([("ke".to_string(), params[0]), ("v".to_string(), params[1])]); - Model { + (Model { params, - _scenario: scenario, + _scenario: scenario.clone(),//TODO remove infusions: vec![], cov: None, - } + }, + scenario.reorder_with_lag(vec![(0.0, 1)]) + ) + } fn get_output(&self, x: &Self::State, system: &Self::Model, outeq: usize) -> f64 { let v = system.get_param("v"); diff --git a/src/routines/simulation/predict.rs b/src/routines/simulation/predict.rs index 2567982d..63d39d0a 100644 --- a/src/routines/simulation/predict.rs +++ b/src/routines/simulation/predict.rs @@ -34,7 +34,7 @@ impl Model { pub trait Predict<'a> { type Model: 'a + Clone; type State; - fn initial_system(&self, params: &Vec, scenario: Scenario) -> Self::Model; + fn initial_system(&self, params: &Vec, scenario: Scenario) -> (Self::Model, Scenario); fn initial_state(&self) -> Self::State; fn add_covs(&self, system: Self::Model, cov: Option>) -> Self::Model; fn add_infusion(&self, system: Self::Model, infusion: Infusion) -> Self::Model; @@ -65,7 +65,7 @@ where Self { ode } } pub fn pred(&self, scenario: Scenario, params: Vec) -> Vec { - let system = self.ode.initial_system(¶ms, scenario.clone()); + let (system, scenario) = self.ode.initial_system(¶ms, scenario.clone()); let mut yout = vec![]; let mut x = self.ode.initial_state(); let mut index: usize = 0;