Skip to content

Commit

Permalink
dirty: implement ingestion analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
keinsell committed Feb 10, 2025
1 parent f76bf04 commit a312570
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 131 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pretty_env_logger = "0.5.0"
textplots = "0.8.6"
valuable = "0.1.1"
derive = "1.0.0"
console = "0.15"
terminal-link = "0.1"

[features]
default = ["tui"]
Expand Down
21 changes: 0 additions & 21 deletions docs/architecture.md

This file was deleted.

48 changes: 48 additions & 0 deletions docs/cli/message-formatting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Message Formatting

Application offers non-interactive functionality which would modify the way of formatting information piped to `stdout`,
meaning you can use application's data that you would normally see in a table by other software programs.

Non-interactive mode would activate automatically when the environment in which the application is running is not
interactive (with use of a cargo library `atty` which contains complete specification on which conditions shell are
considered interactive or not).

Non-interactive mode is de-facto modifying underlying logic for stdout formatting, application expose `--format`
argument which forces usage of specified formatter for every of the following commands that is run by the application.

```bash
neuronek -f json ingestion list
```

```json
[
{
"id": 1,
"substance_name": "caffeine",
"route": "Oral",
"dosage": "10.0 mg",
"ingested_at": "2025-01-06 04:40:27.253301"
}
]
```

## Examples

### Pipe command output to another program

Non-interactive mode would automatically activate when you are piping output, which allows you to use any program that
can ingest JSON and make use of it. Example shows how the application is used to list ingestion's and then use Nushell's
JSON parser to build a pretty table, which is the default way nushell shows information.

```nu
> neuronek ingestion list | from json
╭───┬────┬────────────────┬───────┬─────────┬────────────────────────────╮
│ # │ id │ substance_name │ route │ dosage │ ingested_at │
├───┼────┼────────────────┼───────┼─────────┼────────────────────────────┤
│ 0 │ 1 │ caffeine │ Oral │ 10.0 mg │ 2025-01-06 04:40:27.253301 │
╰───┴────┴────────────────┴───────┴─────────┴────────────────────────────╯
```

## References

- [clig.dev](https://clig.dev/#output)
17 changes: 16 additions & 1 deletion docs/composite.md
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
# Composite
# Composite

Composite is a collection of various dosages of substances typically ingested through same route of administration,
think about it as pills that you might have which contain few supplement. This entity would represent a container with
multiple things inside, could be actually useful to avoid necessary scripting for making a set of ingestions.

- `Composite`
- id
- name
- description
- route_of_administration
- form
- items (`CompositeItems`)
- id
- substance_name
- substance_dosage
5 changes: 5 additions & 0 deletions docs/dosage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Dosage

**Dosage** is treated as a core concept for capturing the mass (or amount) of a substance ingested. This dosage is
stored in the database for each ingestion record, along with other relevant data such as substance name, route of
administration, and ingestion timestamps.
4 changes: 4 additions & 0 deletions docs/ingestion-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ingestion Analyzer

Ingestion Analyzer is a internal engine to use set of predefined rules and user's ingestion data to disadvise or provide
additional insight for ingestion that one is trying to ingest or actually ingested.
36 changes: 36 additions & 0 deletions docs/ingestion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Ingestion

The **Ingestion** entity represents a single record of consuming a specific [substance](./substance.md). It captures
details such as the substance name, dosage, route of administration, and the date/time at which the ingestion occurred.
The entity also supports optional classification of the dosage and calculation of various time-based phases (e.g.,
onset, peak) related to the ingestion experience.

## Data Model

- Is uniuqely identified.
- Must contain minimal amount of data to identify substance.
- Must contain minimal amount of data about dosage.
- Might support dosage ranges and unknown dosages.
- Must contain time when was ingested
- Could support ranges and unknowns probably.
- Can contain dosage classification
- Can contain reference to substance in database
- Can contain multiple `IngestionPhase` entities which define calendar entries of specific phases of ingestion, it's
created when ingestion analysis was possible and application had enough infomration.

## Flow

1. **Create/Update**
When a new ingestion is recorded, the application stores the substance details, dosage, and route. If dosage
information matches known reference ranges, a dosage_classification can be assigned. Any known phases for this
ingestion are also stored.
2. **Retrieval**
When retrieving an ingestion, relevant ingestion-phase records are also fetched, providing a comprehensive view of
both the ingestion event and its associated timeline of effects.
3. **Analysis**
Systems or modules that analyze ingestion data (e.g., generating reports, running calculations) can use the dosage,
route, and phase data to present meaningful insights to end users.
4. **Intended Usage**
This data structure is intended to help track ingestions reliably while linking them to known durations and
intensities, and to assist in historical lookups or analytics across ingestion events.

61 changes: 40 additions & 21 deletions src/cli/ingestion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::core::QueryHandler;
use crate::database::entities::ingestion;
use crate::database::entities::ingestion::Entity as Ingestion;
use crate::database::entities::ingestion::Model;
use crate::database::entities::ingestion_phase;
use crate::database::entities::ingestion_phase::Entity as IngestionPhase;
use crate::ingestion::command::LogIngestion;
use crate::ingestion::query::AnalyzeIngestion;
use crate::substance::repository::get_substance;
Expand All @@ -24,6 +26,7 @@ use clap::Subcommand;
use log::info;
use miette::IntoDiagnostic;
use miette::miette;
use owo_colors::style;
use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue;
use sea_orm::EntityTrait;
Expand All @@ -32,26 +35,27 @@ use sea_orm::QuerySelect;
use sea_orm_migration::IntoSchemaManagerConnection;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Debug;
use std::str::FromStr;
use tabled::Tabled;
use tracing::Level;
use tracing::event;
use typed_builder::TypedBuilder;
use uuid::Uuid;

#[async_trait]
impl CommandHandler for LogIngestion
{
async fn handle<'a>(&self, context: AppContext<'a>) -> miette::Result<()>
{
// Substance name powered by PubChem API, fallback to user input
let substance_name = pubchem::Compound::with_name(&self.substance_name)
.title()
.into_diagnostic()
.unwrap_or(self.substance_name.clone());

let ingestion = Ingestion::insert(ingestion::ActiveModel {
id: ActiveValue::default(),
substance_name: ActiveValue::Set(substance_name.to_lowercase()),
substance_name: ActiveValue::Set(substance_name.to_lowercase().clone()),
route_of_administration: ActiveValue::Set(
serde_json::to_value(self.route_of_administration)
.unwrap()
Expand All @@ -76,28 +80,17 @@ impl CommandHandler for LogIngestion
IngestionViewModel::from(ingestion.clone()).format(context.stdout_format)
);

// Perform ingestion analysis
let analysis_query = AnalyzeIngestion::builder()
.ingestion_id(Some(ingestion.id))
.substance(None)
.date(None)
.dosage(None)
.roa(None)
.substance(self.substance_name.clone())
.date(self.ingestion_date)
.dosage(self.dosage)
.roa(self.route_of_administration)
.build();

match analysis_query.query().await
{
| Ok(analysis) =>
{
println!("\nIngestion Analysis:");
if let Some(dosage_class) = analysis.dosage_classification
{
println!("Dosage Classification: {:?}", dosage_class);
}

println!("Start Time: {}", HumanTime::from(analysis.ingestion_date));

// Update ingestion with analysis results if needed
if analysis.dosage_classification.is_some()
{
let update_model = ingestion::ActiveModel {
Expand All @@ -112,6 +105,35 @@ impl CommandHandler for LogIngestion
.update(context.database_connection)
.await
.into_diagnostic()?;

if !analysis.phases.is_empty()
{
let phase_models = analysis
.phases
.into_iter()
.map(|phase| ingestion_phase::ActiveModel {
id: ActiveValue::Set(Uuid::new_v4().to_string()),
ingestion_id: ActiveValue::Set(ingestion.id),
classification: ActiveValue::Set(phase.class.to_string()),
duration_lower: ActiveValue::Set(
phase.duration.start.num_minutes().to_string(),
),
duration_upper: ActiveValue::Set(
phase.duration.end.num_minutes().to_string(),
),
intensity: ActiveValue::NotSet,
notes: ActiveValue::NotSet,
description: ActiveValue::NotSet,
created_at: ActiveValue::Set(phase.start_time.naive_utc()),
updated_at: ActiveValue::Set(phase.end_time.naive_utc()),
})
.collect::<Vec<_>>();

IngestionPhase::insert_many(phase_models)
.exec(context.database_connection)
.await
.into_diagnostic()?;
}
}
}
| Err(e) =>
Expand All @@ -122,10 +144,6 @@ impl CommandHandler for LogIngestion
error = ?e,
ingestion_id = ingestion.id
);
println!(
"\nWarning: Could not perform full ingestion analysis: {}",
e
);
}
}

Expand Down Expand Up @@ -346,6 +364,7 @@ pub struct IngestionViewModel
}

impl Formatter for IngestionViewModel {}

impl From<Model> for IngestionViewModel
{
fn from(model: Model) -> Self
Expand Down
7 changes: 2 additions & 5 deletions src/cli/substance.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use async_trait::async_trait;
use crate::core::CommandHandler;
use crate::cli::formatter::Formatter;
use crate::substance::error::SubstanceError;
use crate::substance::SubstanceTable;
use crate::substance::error::SubstanceError;
use crate::utils::AppContext;
use async_trait::async_trait;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
Expand Down Expand Up @@ -150,8 +149,6 @@ pub struct ViewModel
pub common_names: String,
}

impl Formatter for ViewModel {}

impl From<SubstanceTable> for ViewModel
{
fn from(model: SubstanceTable) -> Self
Expand Down
Loading

0 comments on commit a312570

Please sign in to comment.