feat(dosages): added library function to calculate optimal combination of medicines for desired dosage

This commit is contained in:
2026-08-02 17:47:53 +10:00
committed by professor
commit e1d47d6a28
6 changed files with 330 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
/target
# Added by cargo
#
# already existing elements were commented out
#/target
Generated
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "lsdp"
version = "0.1.0"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "lsdp"
version = "0.1.0"
edition = "2024"
[dependencies]
+137
View File
@@ -0,0 +1,137 @@
use std::{cmp::max, collections::HashMap, ops::Index};
pub struct ComboNode {
combo: Vec<u32>,
weight: u32,
}
impl Index<usize> for ComboNode {
type Output = u32;
fn index(&self, index: usize) -> &Self::Output {
self.combo.index(index)
}
}
impl ComboNode {
pub fn weight(&self) -> u32 {
self.weight
}
pub fn new(combo: Vec<u32>) -> Self {
let sum = combo.iter().sum();
Self { combo, weight: sum }
}
pub fn get_neighbours(&self) -> impl Iterator<Item = ComboNode> {
(0..self.combo.len()).map(|i| {
let mut combo = self.combo.clone();
combo[i] += 1;
Self {
combo,
weight: self.weight + 1,
}
})
}
}
pub struct ComboGraph {
nodes: HashMap<u32, ComboNode>,
values: Vec<u32>,
target: u32,
queue: Vec<ComboNode>,
}
impl ComboGraph {
pub fn new(mut values: Vec<u32>, target: u32) -> Self {
// ensure values is sorted
values.sort_unstable();
let num_values = values.len();
Self {
nodes: HashMap::new(),
values,
target,
queue: vec![ComboNode::new(vec![0; num_values])],
}
}
pub fn closest_node_target(&self) -> Option<&ComboNode> {
let closest = self
.nodes
.iter()
// Treat a zero difference as a difference of 1 to accomodate for rounding
.map(|(pos, combo)| (max(self.target.abs_diff(*pos), 1), combo.weight(), pos))
.min()?;
self.nodes.get(&closest.2)
}
/// Visit a combo and calculate its position and weight
pub fn visit(&mut self, node: ComboNode) -> bool {
let mut pos = 0;
for i in 0..self.values.len() {
pos += self.values[i] * node[i];
}
// Check if pos already found with less weight
let current = self.nodes.get(&pos);
if let Some(combo) = current {
if combo.weight() > node.weight() {
// should never happen
panic!("Found better path second");
}
return false;
} else if pos == self.target {
self.nodes.insert(pos, node);
return true;
} else if pos < self.target {
node.get_neighbours().for_each(|n| {
self.queue.push(n);
});
}
self.nodes.insert(pos, node);
false
}
/// grows the graph to find the optimal path
pub fn grow(&mut self) -> bool {
let mut next = self.queue.pop();
while let Some(node) = next {
if self.visit(node) {
return true;
}
next = self.queue.pop();
}
false
}
pub fn target_combo(&self) -> Option<Vec<u32>> {
let combo = self.nodes.get(&self.target)?;
Some(
(0..self.values.len())
.map(|i| vec![self.values[i]; combo[i] as usize])
.flatten()
.collect(),
)
}
pub fn closest_combo(&self) -> Option<Vec<u32>> {
let combo = self.closest_node_target()?;
Some(
(0..self.values.len())
.map(|i| vec![self.values[i]; combo[i] as usize])
.flatten()
.collect(),
)
}
}
pub fn find_optimal_combo(strengths: Vec<u32>, target: u32, rounded: bool) -> Vec<u32> {
let mut graph = ComboGraph::new(strengths, target);
if graph.grow() && !rounded {
graph.target_combo().expect("grow signalled target hit")
} else {
graph.closest_combo().expect("non empty graph")
}
}
+101
View File
@@ -0,0 +1,101 @@
/// calculate greatest common factor
/// requires a > b
fn gcf(mut a: u32, mut b: u32) -> u32 {
let mut rem = a % b;
while rem != 0 {
a = b;
b = rem;
rem = a % b;
}
b
}
/// Calculate gcf of vector
fn gcfv(v: &Vec<u32>) -> u32 {
let mut i = 1;
let mut cgcf = v[0]; // start with first item
let mut a;
let mut b;
while i < v.len() {
if cgcf >= v[i] {
a = cgcf;
b = v[i];
} else {
a = v[i];
b = cgcf;
}
cgcf = gcf(a, b);
i += 1;
}
cgcf
}
pub fn factorise(strengths: &mut Vec<u32>) -> u32 {
let factor = gcfv(strengths);
if factor == 1 {
return factor;
}
for s in strengths.iter_mut() {
*s = *s / factor;
}
factor
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gcf_base_case() {
assert_eq!(gcf(1, 1), 1);
}
#[test]
fn gcf_standard_case() {
assert_eq!(gcf(1071, 462), 21)
}
#[test]
fn gcf_coprime_case() {
assert_eq!(gcf(1071, 461), 1)
}
#[test]
fn gcfv_base_case() {
assert_eq!(gcfv(&vec![1]), 1);
}
#[test]
fn gcfv_standard_case() {
assert_eq!(gcfv(&vec![1071, 462, 42, 105]), 21)
}
#[test]
fn gcfv_coprime_case() {
assert_eq!(gcfv(&vec![1071, 105, 42, 461]), 1)
}
#[test]
fn factorise_base_case() {
let mut s = vec![1];
assert_eq!(factorise(&mut s), 1);
assert_eq!(s, vec![1]);
}
#[test]
fn factorise_standard_case() {
let mut s = vec![1071, 462, 42, 105];
assert_eq!(factorise(&mut s), 21);
assert_eq!(s, vec![51, 22, 2, 5]);
}
#[test]
fn factorise_coprime_case() {
let mut s = vec![1071, 105, 42, 461];
assert_eq!(factorise(&mut s), 1);
assert_eq!(s, vec![1071, 105, 42, 461]);
}
}
+71
View File
@@ -0,0 +1,71 @@
mod combo_graph;
mod factor;
use crate::combo_graph::find_optimal_combo;
use crate::factor::factorise;
/// Algorithm:
/// 1. factorise strengths by GCF
/// 2. Perform DFS search on strength combos starting from full largest strength
/// 3. use efficiencies from dynamic programming to prune cases already calculated
///
/// ```
/// use lsdp::calculate_combo;
/// let combo = calculate_combo(173, vec![20, 57]);
/// assert_eq!(combo, vec![20, 20, 20, 57, 57]);
/// ```
///
pub fn calculate_combo(mut target: u32, mut strengths: Vec<u32>) -> Vec<u32> {
if strengths.len() == 0 {
return vec![];
}
let factor = factorise(&mut strengths);
let mut rounded = false;
if factor != 1 {
if target % factor != 0 {
let f_target: f64 = target as f64 / factor as f64;
target = f_target.round() as u32;
rounded = true;
} else {
target /= factor;
rounded = false;
}
}
let combo = find_optimal_combo(strengths, target, rounded);
combo.iter().map(|s| s * factor).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_case() {
assert_eq!(calculate_combo(1, vec![1]), vec![1]);
}
#[test]
fn test_basic() {
assert_eq!(
calculate_combo(245, vec![50, 20, 5]),
vec![5, 20, 20, 50, 50, 50, 50]
);
}
#[test]
fn test_basic_close() {
assert_eq!(
calculate_combo(245, vec![50, 20, 4]),
vec![4, 20, 20, 50, 50, 50, 50]
);
}
#[test]
fn test_no_exact() {
assert_eq!(calculate_combo(173, vec![20, 57]), vec![20, 20, 20, 57, 57]);
}
}