From 28e3c87b989583e625724461f596bc6c36203730 Mon Sep 17 00:00:00 2001 From: piv <> Date: Mon, 20 Jun 2022 12:22:07 +0930 Subject: [PATCH] More progress towards reciprocal allocation algorithm --- Cargo.lock | 16 +++++++++++ Cargo.toml | 4 ++- src/lib.rs | 83 ++++++++++++++++++++++++++++++++++++------------------ 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 231e671..5388f8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,7 @@ version = "0.1.0" dependencies = [ "clap", "csv", + "itertools", "nalgebra", ] @@ -122,6 +123,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "hashbrown" version = "0.12.1" @@ -153,6 +160,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" diff --git a/Cargo.toml b/Cargo.toml index 7106bfb..d3c8ea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,6 @@ csv = "1.1" # simba = { version = "0.7.1", features = ["partial_fixed_point_support"] } # num = "0.4" -clap = { version = "3.1.18", features = ["derive"] } \ No newline at end of file +clap = { version = "3.1.18", features = ["derive"] } + +itertools = "0.10.3" diff --git a/src/lib.rs b/src/lib.rs index ff7a547..592d72a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ extern crate nalgebra as na; -use core::slice; +use itertools::Itertools; use na::{DMatrix, Dynamic, LU}; -use std::{collections::HashMap, ops::Mul, error::Error}; +use std::{collections::HashMap, error::Error, ops::Mul}; // TODO: Look into serde for serialisation, can also use it to serialise/deserialise // records from a csv file using the csv crate @@ -117,15 +117,22 @@ pub fn move_money_2( running_total } +#[derive(Debug, PartialEq, Eq)] +pub enum DepartmentType { + Operating, + Overhead, +} + // TODO: Could also look at BigDecimal rather than f64 for higher precision (even i64 might be fine if we don't need to divide...) // Note: remember these are overhead departments only when calculating the lu decomposition or pseudoinverse, and for each department, // you either need -1 or rest negative for a row to subtract the initial amounts so we end up effectively 0 (simultaneous equations end // up with negative there so yes this is expected) // Also, we could potentially use this same struct for non-overhead departments when mapping from overhead to pub struct OverheadAllocationRule { - from_department: String, + from_overhead_department: String, to_department: String, percent: f64, + to_department_type: DepartmentType, } pub struct TotalDepartmentCost { @@ -155,6 +162,24 @@ impl ReciprocalAllocationSolver for DMatrix { } } +fn get_rules_indexes( + allocations: &Vec, + department_type: DepartmentType, +) -> HashMap { + allocations + .iter() + .filter(|allocation| allocation.to_department_type == department_type) + .flat_map(|department| { + [ + department.from_overhead_department.clone(), + department.to_department.clone(), + ] + }) + .unique() + .enumerate() + .map(|(index, department)| (department, index)) + .collect() +} // Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs // to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when @@ -164,23 +189,35 @@ pub fn reciprocal_allocation( account_costs: Vec, // TODO: Throw an appropriate error ) -> Result, Box> { - // TODO: Need to split up the rules so that we only pass overhead departments into the getreciprocal matrix method, - // and - let mut department_mappings: HashMap = HashMap::new(); - for allocation in allocations.iter() { - let map_size = department_mappings.len(); - department_mappings - .entry(allocation.from_department.clone()) - .or_insert(map_size); - let map_size = department_mappings.len(); - department_mappings - .entry(allocation.to_department.clone()) - .or_insert(map_size); + let overhead_department_mappings: HashMap = + get_rules_indexes(&allocations, DepartmentType::Overhead); + let operating_department_mappings: HashMap = + get_rules_indexes(&allocations, DepartmentType::Operating); + + let mut slice_allocations = + vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()]; + + for allocation in allocations + .iter() + .filter(|allocation| allocation.to_department_type == DepartmentType::Overhead) + { + // TODO: Check if we need to flp this around + let from_index = overhead_department_mappings + .get(&allocation.from_overhead_department) + .unwrap(); + let to_index = operating_department_mappings + .get(&allocation.to_department) + .unwrap(); + let elem = &mut slice_allocations + [(*from_index) + (overhead_department_mappings.len() * (*to_index))]; + *elem = allocation.percent; } + // TODO: Also need ones along the diagonal, and negatives in some places... + let mat: DMatrix = DMatrix::from_row_slice( - department_mappings.len(), - department_mappings.len(), + overhead_department_mappings.len(), + overhead_department_mappings.len(), &slice_allocations, ); @@ -189,14 +226,14 @@ pub fn reciprocal_allocation( do_solve_reciprocal( pseudo_inverse.unwrap(), account_costs, - department_mappings, + overhead_department_mappings, allocations, ) } else { do_solve_reciprocal( mat.lu(), account_costs, - department_mappings, + overhead_department_mappings, allocations, ) } @@ -210,14 +247,7 @@ fn do_solve_reciprocal( ) -> Result, Box> { // TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out for total_costs in account_costs { - let mut slice_allocations = vec![0.; department_mappings.len()]; let mut slice_costs = vec![0.; department_mappings.len()]; - for allocation in allocations { - let elem = &mut slice_allocations[*department_mappings - .get(&allocation.from_department) - .unwrap()]; - *elem = allocation.percent; - } for cost in total_costs.summed_department_costs { let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()]; @@ -233,7 +263,6 @@ fn do_solve_reciprocal( // Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the // solved overheads usages after taking into account usage between departments, and initial_totals is the initial values // for the operating departments. - } // TODO: return something appropriate Ok(vec![])