From 08433d6ea6b15000668ee71b2fedc605322d7ae9 Mon Sep 17 00:00:00 2001 From: piv <> Date: Sat, 18 Jun 2022 14:07:56 +0930 Subject: [PATCH] Rework reciprocal allocation to be closer to final algorithm, perform calculations off heap --- src/lib.rs | 120 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 15177e3..ff7a547 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ extern crate nalgebra as na; -use na::DMatrix; -use std::{collections::HashMap, ops::Mul}; +use core::slice; +use na::{DMatrix, Dynamic, LU}; +use std::{collections::HashMap, ops::Mul, error::Error}; // TODO: Look into serde for serialisation, can also use it to serialise/deserialise // records from a csv file using the csv crate @@ -132,22 +133,39 @@ pub struct TotalDepartmentCost { value: f64, } +pub struct AccountCost { + account: String, + summed_department_costs: Vec, +} + +// TODO: Also need a way to dictate the order of the departments? +pub trait ReciprocalAllocationSolver { + fn solve(&self, costs: &DMatrix) -> DMatrix; +} + +impl ReciprocalAllocationSolver for LU { + fn solve(&self, costs: &DMatrix) -> DMatrix { + self.solve(costs).unwrap() + } +} + +impl ReciprocalAllocationSolver for DMatrix { + fn solve(&self, costs: &DMatrix) -> DMatrix { + self.mul(costs) + } +} + + // 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 // matrix is singular -// TODO: Could also reduce memory by just calculating overhead costs in a first step (service departments), then -// calculating operating department costs in a second step using the output from the service departments (multiply -// by service department output rather than original). The second step can be a vector multiply or a loop, basically -// same as move money step, might bven be able to just repeat it -// Note: PPM currently does the invert for the cost centres only (so can be up to 6000 ccs), as the cost centres are the actual departments, -// and a previous step calculates the percentages for overhead areas using their allocation statistics. Then for each account, -// it will use the overhead allocation matrix to calculate the moved/overhead allocations from the line items calculated from the previous -// cost definiteions/reclass rules steps. Really we'd want to batch this out so we multiple a couple hundred or so accounts at a time (maybe -// with a batch size property) -pub fn get_reciprocal_allocation_matrix( +pub fn reciprocal_allocation( allocations: Vec, - total_costs: Vec, -) -> DMatrix { + 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(); @@ -160,39 +178,63 @@ pub fn get_reciprocal_allocation_matrix( .or_insert(map_size); } - let mut slice_allocations = vec![0.; department_mappings.len()]; - - // TODO: This needs to be passed in another time - let mut slice_costs = vec![0.; department_mappings.len()]; - for allocation in allocations { - // TODO: Is there a more idiomatic way to do this? - let elem = &mut slice_allocations[*department_mappings - .get(&allocation.from_department) - .unwrap()]; - *elem = allocation.percent; - } - - for cost in total_costs { - let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()]; - *elem = cost.value; - } - let mat: DMatrix = DMatrix::from_row_slice( department_mappings.len(), department_mappings.len(), &slice_allocations, ); - let costs_vec: DMatrix = - DMatrix::from_row_slice(department_mappings.len(), 1, &slice_costs); - - // TODO: Only calculate lu/pseudoinverse once. We then do the solve for the overhead department totals for each account, and use this to - // calculate the final totals. if mat.determinant() == 0. { let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001); - pseudo_inverse.unwrap().mul(&costs_vec) + do_solve_reciprocal( + pseudo_inverse.unwrap(), + account_costs, + department_mappings, + allocations, + ) } else { - let lup = mat.lu(); - lup.solve(&costs_vec).unwrap() + do_solve_reciprocal( + mat.lu(), + account_costs, + department_mappings, + allocations, + ) } } + +fn do_solve_reciprocal( + solver: T, + account_costs: Vec, + department_mappings: HashMap, + allocations: Vec, +) -> 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()]; + *elem = cost.value; + } + + let costs_vec: DMatrix = + DMatrix::from_row_slice(department_mappings.len(), 1, &slice_costs); + + let calculated_overheads = solver.solve(&costs_vec); + + // Calculation: operating_overhead_usage . calculated_overheads + initial_totals + // 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![]) +}