diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 00000000..02b915b8 --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/apps/cli/src/cli/dashboard/mod.rs b/apps/cli/src/cli/dashboard/mod.rs index ec7a48fc..9d025c1b 100644 --- a/apps/cli/src/cli/dashboard/mod.rs +++ b/apps/cli/src/cli/dashboard/mod.rs @@ -10,8 +10,8 @@ use futures::stream::StreamExt; use humantime::format_duration; use tabled::Table; -use db::sea_orm::*; use db::sea_orm::DatabaseConnection; +use db::sea_orm::*; use crate::core::dosage::Dosage; use crate::core::ingestion::Ingestion; @@ -20,28 +20,36 @@ use crate::service::ingestion::get_ingestion_by_id; pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { // Fetch and map all ingestions from database - let ingestion = db::ingestion::Entity::find().all(database_connection).await.unwrap(); + let ingestion = db::ingestion::Entity::find() + .all(database_connection) + .await + .unwrap(); let ingestions_stream = stream::iter(ingestion.into_iter()); - let ingestions = futures::future::join_all(ingestions_stream - .map(|ingestion| { - let ingestion_id = ingestion.id; - async move { - get_ingestion_by_id(ingestion_id).await.unwrap() - } - }).collect::>().await).await; + let ingestions = futures::future::join_all( + ingestions_stream + .map(|ingestion| { + let ingestion_id = ingestion.id; + async move { get_ingestion_by_id(ingestion_id).await.unwrap() } + }) + .collect::>() + .await, + ) + .await; // Group ingestion by substance name - let ingestion_by_substance = ingestions.clone() - .into_iter() - .fold(HashMap::>::new(), |mut acc, ingestion| { + let ingestion_by_substance = ingestions.clone().into_iter().fold( + HashMap::>::new(), + |mut acc, ingestion| { let substance_name = ingestion.substance_name.clone(); let ingestion_entry = acc.entry(substance_name).or_insert(vec![]); ingestion_entry.push(ingestion); acc - }); + }, + ); // Calculate total dosage per substance for last 7 days - let total_dosage_per_substance_for_last_7_days = ingestion_by_substance.clone() + let total_dosage_per_substance_for_last_7_days = ingestion_by_substance + .clone() .into_iter() .map(|(substance_name, ingestions)| { let total_dosage: Dosage = ingestions @@ -51,15 +59,16 @@ pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { .collect::>() .into_iter() // Use custom summing implementation - .fold(Dosage::from_str( - "0.0 mg" - ).unwrap(), |acc, dosage| acc + dosage); + .fold(Dosage::from_str("0.0 mg").unwrap(), |acc, dosage| { + acc + dosage + }); (substance_name, total_dosage) }) .collect::>(); - - let average_dosage_per_day_per_substance_for_last_7_days = ingestion_by_substance.clone() + + let average_dosage_per_day_per_substance_for_last_7_days = ingestion_by_substance + .clone() .into_iter() .map(|(substance_name, ingestions)| { let total_dosage: Dosage = ingestions @@ -69,9 +78,9 @@ pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { .collect::>() .into_iter() // Use custom summing implementation - .fold(Dosage::from_str( - "0.0 mg" - ).unwrap(), |acc, dosage| acc + dosage); + .fold(Dosage::from_str("0.0 mg").unwrap(), |acc, dosage| { + acc + dosage + }); let average_dosage = total_dosage / 7.0; (substance_name, average_dosage) @@ -81,27 +90,46 @@ pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { println!("\n"); println!("Substance Name, Total Dosage, Average Dosage per Day"); - let table = Table::from_iter(total_dosage_per_substance_for_last_7_days.into_iter() - .map(|(substance_name, total_dosage)| { - vec![ - substance_name.clone(), - format!("{:.0}", total_dosage).to_string(), - format!("{:.0}", average_dosage_per_day_per_substance_for_last_7_days.get(&substance_name).unwrap()).to_string(), - ] - }) - .collect::>().iter().cloned()); - + let table = Table::from_iter( + total_dosage_per_substance_for_last_7_days + .into_iter() + .map(|(substance_name, total_dosage)| { + vec![ + substance_name.clone(), + format!("{:.0}", total_dosage).to_string(), + format!( + "{:.0}", + average_dosage_per_day_per_substance_for_last_7_days + .get(&substance_name) + .unwrap() + ) + .to_string(), + ] + }) + .collect::>() + .iter() + .cloned(), + ); + println!("{}", table.to_string()); println!("\n"); - + // Filter all ingestions to find those which are active (onset, comeup, peak, offset) let active_ingestions = ingestions .into_iter() .filter(|ingestion| { - let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; - let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; + let ingestion_start = ingestion + .phases + .get(&PhaseClassification::Onset) + .unwrap() + .start_time; + let ingestion_end = ingestion + .phases + .get(&PhaseClassification::Offset) + .unwrap() + .end_time; let daterange = ingestion_start..ingestion_end; - daterange.contains( &Local::now() ) + daterange.contains(&Local::now()) }) .collect::>(); @@ -111,32 +139,27 @@ pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { // time left and other useful information such as ingestion id and substance name. active_ingestions.into_iter().for_each(|ingestion| { - let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; - let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; + let ingestion_start = ingestion + .phases + .get(&PhaseClassification::Onset) + .unwrap() + .start_time; + let ingestion_end = ingestion + .phases + .get(&PhaseClassification::Offset) + .unwrap() + .end_time; let now = Local::now(); let total_duration = ingestion_end - ingestion_start; let elapsed_duration = now - ingestion_start; let remaining_duration = total_duration - elapsed_duration; - println!("#{} {} ({:.0}) | {}", ingestion.id, ingestion.substance_name, ingestion.dosage, format_duration(remaining_duration.to_std().unwrap())); + println!( + "#{} {} ({:.0}) | {}", + ingestion.id, + ingestion.substance_name, + ingestion.dosage, + format_duration(remaining_duration.to_std().unwrap()) + ); }); - - // We need to create progress bar for each active ingestion and - // put them all into easy-readable table allowing end-user to - // quickly see how long each ingestion will last. - - // active_ingestions.into_iter().for_each(|ingestion| { - // let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; - // let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; - // let now = Local::now(); - // let total_duration = (ingestion_end - ingestion_start).num_seconds() as u64; - // let elapsed_duration = (now - ingestion_start).num_seconds() as u64; - // - // println!("{}:", ingestion.substance_name); - // let pb = ProgressBar::new(total_duration).with_prefix(Cow::from(format!("{}:", ingestion.substance_name))); - // pb.set_style(ProgressStyle::default_bar()); - // pb.set_position(elapsed_duration); - // - // pb.finish_and_clear(); - // }); -} \ No newline at end of file +} diff --git a/apps/cli/src/core/ingestion.rs b/apps/cli/src/core/ingestion.rs index 2a99b116..ae5cb9d2 100644 --- a/apps/cli/src/core/ingestion.rs +++ b/apps/cli/src/core/ingestion.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; @@ -17,7 +18,7 @@ pub struct IngestionPhase { pub type IngestionPhases = HashMap; -#[derive( Serialize, Debug, Deserialize, Clone)] +#[derive(Serialize, Debug, Deserialize, Clone)] pub struct Ingestion { pub(crate) id: i32, pub(crate) substance_name: String, @@ -45,4 +46,25 @@ impl Ingestion { phases, } } -} \ No newline at end of file +} + +impl From for Ingestion { + fn from(ingestion: db::ingestion::Model) -> Self { + Ingestion { + id: ingestion.id, + substance_name: ingestion.substance_name.unwrap(), + administration_route: RouteOfAdministrationClassification::from_str( + ingestion.administration_route.unwrap().as_str(), + ) + .unwrap(), + ingested_at: ingestion.ingestion_date.unwrap().and_utc(), + dosage: Dosage::from_str(&format!( + "{} {}", + ingestion.dosage_amount.unwrap(), + ingestion.dosage_unit.unwrap() + )) + .unwrap(), + phases: HashMap::new(), + } + } +} diff --git a/apps/cli/src/ingestion_intelligence.rs b/apps/cli/src/ingestion_intelligence.rs new file mode 100644 index 00000000..92501de1 --- /dev/null +++ b/apps/cli/src/ingestion_intelligence.rs @@ -0,0 +1,156 @@ +// Ingestion Intelligence is a tool that helps to calculate all of the metrics +// related to an ingestions in a cache-able format to be later used for ingestion +// analysis. This is a ciritial tool for Neuronek project. + +use crate::core::dosage::Dosage; +use crate::orm::DB_CONNECTION; +use chrono::Utc; +use db::*; +use std::str::FromStr; + +pub(super) async fn get_total_dosage_of_substance( + substance_name: String, + date_range: Option<(chrono::DateTime, chrono::DateTime)>, +) -> Result> { + // Fetch all ingestions of substance + let mut database_ingestions = db::ingestion::Entity::find() + .filter(db::ingestion::Column::SubstanceName.contains(substance_name)) + .all(&DB_CONNECTION as &DatabaseConnection) + .await?; + + // If a date range is provided, filter the ingestions to include only those that fall within the date range. + if let Some((start_date, end_date)) = date_range { + database_ingestions = database_ingestions + .into_iter() + .filter(|ingestion| { + let ingestion_date: chrono::DateTime = + ingestion.ingestion_date.unwrap().and_utc(); + let dr: (chrono::DateTime, chrono::DateTime) = (start_date, end_date); + + // If the ingestion date is within the date range, include it in the filtered list. + let is_within_date_range = dr.0.le(&ingestion_date) && dr.1.ge(&ingestion_date); + is_within_date_range + }) + .collect(); + } + + // Calculate the total dosage by summing up the dosage of each ingestion. + let total_dosage = database_ingestions + .iter() + .map(|ingestion| { + let cloned_ingestion = ingestion.clone(); + return Dosage::from_str( + format!( + "{} {}", + cloned_ingestion.dosage_amount.unwrap(), + cloned_ingestion.dosage_unit.unwrap(), + ) + .as_str(), + ) + .unwrap() + .to_owned(); + }) + .fold(Dosage::from_str("0.0 mg").unwrap(), |acc, dosage| { + acc + dosage + }); + + Ok(total_dosage) +} + +pub(crate) async fn get_average_dosage_of_substance( + substance_name: String, + date_range: Option<(chrono::DateTime, chrono::DateTime)>, +) -> Result> { + // Fetch all ingestions of substance + let mut database_ingestions = db::ingestion::Entity::find() + .filter(db::ingestion::Column::SubstanceName.contains(substance_name)) + .all(&DB_CONNECTION as &DatabaseConnection) + .await?; + + // If a date range is provided, filter the ingestions to include only those that fall within the date range. + if let Some((start_date, end_date)) = date_range { + database_ingestions = database_ingestions + .into_iter() + .filter(|ingestion| { + let ingestion_date: chrono::DateTime = + ingestion.ingestion_date.unwrap().and_utc(); + let dr: (chrono::DateTime, chrono::DateTime) = (start_date, end_date); + + // If the ingestion date is within the date range, include it in the filtered list. + let is_within_date_range = dr.0.le(&ingestion_date) && dr.1.ge(&ingestion_date); + is_within_date_range + }) + .collect(); + } + + // Calculate the average dosage by summing up the dosage of each ingestion. + let average_dosage = database_ingestions + .iter() + .map(|ingestion| { + let cloned_ingestion = ingestion.clone(); + return Dosage::from_str( + format!( + "{} {}", + cloned_ingestion.dosage_amount.unwrap(), + cloned_ingestion.dosage_unit.unwrap(), + ) + .as_str(), + ) + .unwrap() + .to_owned(); + }) + .fold(Dosage::from_str("0.0 mg").unwrap(), |acc, dosage| { + acc + dosage + }); + + Ok(average_dosage) +} + +pub(crate) async fn get_average_daily_dosage_of_substance( + substance_name: String, + date_range: Option<(chrono::DateTime, chrono::DateTime)>, +) -> Result> { + // Fetch all ingestions of substance + let mut database_ingestions = db::ingestion::Entity::find() + .filter(db::ingestion::Column::SubstanceName.contains(substance_name)) + .all(&DB_CONNECTION as &DatabaseConnection) + .await?; + + // If a date range is provided, filter the ingestions to include only those that fall within the date range. + if let Some((start_date, end_date)) = date_range { + database_ingestions = database_ingestions + .into_iter() + .filter(|ingestion| { + let ingestion_date: chrono::DateTime = + ingestion.ingestion_date.unwrap().and_utc(); + let dr: (chrono::DateTime, chrono::DateTime) = (start_date, end_date); + + // If the ingestion date is within the date range, include it in the filtered list. + let is_within_date_range = dr.0.le(&ingestion_date) && dr.1.ge(&ingestion_date); + is_within_date_range + }) + .collect(); + } + + // Calculate the average dosage by summing up the dosage of each ingestion. + let average_daily_dosage = database_ingestions + .iter() + .map(|ingestion| { + let cloned_ingestion = ingestion.clone(); + return Dosage::from_str( + format!( + "{} {}", + cloned_ingestion.dosage_amount.unwrap(), + cloned_ingestion.dosage_unit.unwrap(), + ) + .as_str(), + ) + .unwrap() + .to_owned(); + }) + .fold(Dosage::from_str("0.0 mg").unwrap(), |acc, dosage| { + acc + dosage + }); + + Ok(average_daily_dosage) +} diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs index 1f844e7f..c64c7621 100644 --- a/apps/cli/src/main.rs +++ b/apps/cli/src/main.rs @@ -1,15 +1,11 @@ #![feature(duration_constructors)] -use async_std::task; - -use crate::cli::main::cli; -use crate::core::dosage::test_measurements; - mod cli; mod core; mod ingestion_analyzer; mod orm; mod service; +mod ingestion_intelligence; fn main() { cli::bin::main() diff --git a/docs/Architecture/storming.d2 b/docs/Architecture/storming.d2 index 313db401..dae91f53 100644 --- a/docs/Architecture/storming.d2 +++ b/docs/Architecture/storming.d2 @@ -37,21 +37,21 @@ classes: { style.stroke: ${colors.command} } actor: { - shape: person - style.stroke-dash: 3 + shape: person + style.stroke-dash: 3 + } + event: { + style.stroke: ${colors.event} + } + aggregate: { + style.stroke: ${colors.aggregate} + } + bounded_context: { + style.stroke: ${colors.bounded_context} + } + timeline: { + style.stroke: ${colors.timeline} } - event: { - style.stroke: ${colors.event} - } - aggregate: { - style.stroke: ${colors.aggregate} - } - bounded_context: { - style.stroke: ${colors.bounded_context} - } - timeline: { - style.stroke: ${colors.timeline} - } med: { width: 200 height: 200 @@ -73,130 +73,152 @@ classes: { } } -identity_and_access_management { - label: "Identity and Access Management" - style.stroke: "#E91E63" +identity_and_access_management: { + label: "Identity and Access Management" + style.stroke: "#E91E63" - # Commands - create_account { - label: "Create Account" - class: command - } - - # Events - account_created { - label: "Account Created" - style.stroke: "#f4a261" - } - - account_created - account_created.style.stroke: "#f4a261" - - create_account - create_account.style.stroke: "#1976D2" + # Commands + create_account: { + label: "Create Account" + class: command + } - account - account.style.stroke: "#FFB703" + # Events + account_created: { + label: "Account Created" + style.stroke: "#f4a261" + } - account_vm - account_vm.style.stroke: "#4CAF50" + account_created + account_created.style.stroke: "#f4a261" - account_registration { - label: "Account Registration" - style.stroke: "#E91E63" + create_account + create_account.style.stroke: "#1976D2" - register_account { - label: "Register Account" - class: command - } + account + account.style.stroke: "#FFB703" - account_registered { - label: "Account Registered" - style.stroke: "#f4a261" - } + account_vm + account_vm.style.stroke: "#4CAF50" - account { - label: "Account" - class: aggregate - } + account_registration: { + label: "Account Registration" + style.stroke: "#E91E63" - unknown_user: { - label: "Unknown User" - class: actor - } + register_account: { + label: "Register Account" + class: command + } - unknown_user -> register_account -> account -> account_registered -> account_vm + account_registered: { + label: "Account Registered" + style.stroke: "#f4a261" } - account_confirmation { - label: "Account Confirmation" - style.stroke: "#E91E63" + account: { + label: "Account" + class: aggregate + } - confirm_account { - label: "Confirm Account" - class: command - } + unknown_user: { + label: "Unknown User" + class: actor + } - account_confirmed { - label: "Account Confirmed" - style.stroke: "#f4a261" - } + unknown_user -> register_account -> account -> account_registered -> account_vm + } - account { - label: "Account" - class: aggregate - } + account_confirmation: { + label: "Account Confirmation" + style.stroke: "#E91E63" - account_view_model { - label: "Account View Model" - class: read_model - } + confirm_account: { + label: "Confirm Account" + class: command + } - account_registration -> confirm_account -> account -> account_confirmed -> account_view_model + account_confirmed: { + label: "Account Confirmed" + style.stroke: "#f4a261" } - account_deletion { - label: "Account Deletion" - style.stroke: "#E91E63" + account: { + label: "Account" + class: aggregate } - password_reset { - label: "Password Reset" - style.stroke: "#E91E63" + account_view_model: { + label: "Account View Model" + class: read_model } - account_update { - label: "Account Update" - style.stroke: "#E91E63" + account_registration -> confirm_account -> account -> account_confirmed -> account_view_model + } + + account_deletion: { + label: "Account Deletion" + style.stroke: "#E91E63" + } + + password_reset: { + label: "Password Reset" + style.stroke: "#E91E63" + } - update_account { - label: "Update Account" - class: command - } + account_update: { + label: "Account Update" + style.stroke: "#E91E63" - account_updated { - label: "Account Updated" - style.stroke: "#f4a261" - } + update_account: { + label: "Update Account" + class: command + } - account { - label: "Account" - class: aggregate - } + account_updated: { + label: "Account Updated" + style.stroke: "#f4a261" + } - account_view_model { - label: "Account View Model" - class: read_model - } + account: { + label: "Account" + class: aggregate + } - account_registration -> update_account -> account -> account_updated -> account_view_model + account_view_model: { + label: "Account View Model" + class: read_model } + account_registration -> update_account -> account -> account_updated -> account_view_model + } + + account_registration -> account_confirmation + account_registration -> account_deletion + account_confirmation -> password_reset + account_registration -> account_update +} +ingestion_journal: { + label: "Ingestion Journal" + style.stroke: "#E91E63" - account_registration -> account_confirmation - account_registration -> account_deletion - account_confirmation -> password_reset - account_registration -> account_update + create_ingestion + delete_ingestion + update_ingestion + plan_ingestion + + plan_ingestion -> ingestion -> ingestions_fetched -> statistics_calculated -> interactions_checked -> duration_calculated -> rules_checked -> ingestion_plan_view_model } +# Ingestion Intelligence is a component dedicated to the analysis of the data +# contained in the ingestions journals to predict and avoid harm potential. +# This is also intented to calculate and visualize statistics about the +# interactions between the users and the system. +ingestion_intelligence: { + label: "Ingestion Intelligence" + + # TODO: Average ingestions + # TODO: Average dosages + # TODO: Most common effects + # TODO: Most common side-effects +}