Rework reciprocal allocation to be closer to final algorithm, perform calculations off heap
This commit is contained in:
120
src/lib.rs
120
src/lib.rs
@@ -1,7 +1,8 @@
|
|||||||
extern crate nalgebra as na;
|
extern crate nalgebra as na;
|
||||||
|
|
||||||
use na::DMatrix;
|
use core::slice;
|
||||||
use std::{collections::HashMap, ops::Mul};
|
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
|
// TODO: Look into serde for serialisation, can also use it to serialise/deserialise
|
||||||
// records from a csv file using the csv crate
|
// records from a csv file using the csv crate
|
||||||
@@ -132,22 +133,39 @@ pub struct TotalDepartmentCost {
|
|||||||
value: f64,
|
value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AccountCost {
|
||||||
|
account: String,
|
||||||
|
summed_department_costs: Vec<TotalDepartmentCost>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Also need a way to dictate the order of the departments?
|
||||||
|
pub trait ReciprocalAllocationSolver {
|
||||||
|
fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReciprocalAllocationSolver for LU<f64, Dynamic, Dynamic> {
|
||||||
|
fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
|
self.solve(costs).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReciprocalAllocationSolver for DMatrix<f64> {
|
||||||
|
fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64> {
|
||||||
|
self.mul(costs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) 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
|
// to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when
|
||||||
// matrix is singular
|
// matrix is singular
|
||||||
// TODO: Could also reduce memory by just calculating overhead costs in a first step (service departments), then
|
pub fn reciprocal_allocation(
|
||||||
// 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(
|
|
||||||
allocations: Vec<OverheadAllocationRule>,
|
allocations: Vec<OverheadAllocationRule>,
|
||||||
total_costs: Vec<TotalDepartmentCost>,
|
account_costs: Vec<AccountCost>,
|
||||||
) -> DMatrix<f64> {
|
// TODO: Throw an appropriate error
|
||||||
|
) -> Result<Vec<AccountCost>, Box<dyn Error>> {
|
||||||
|
// 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<String, usize> = HashMap::new();
|
let mut department_mappings: HashMap<String, usize> = HashMap::new();
|
||||||
for allocation in allocations.iter() {
|
for allocation in allocations.iter() {
|
||||||
let map_size = department_mappings.len();
|
let map_size = department_mappings.len();
|
||||||
@@ -160,39 +178,63 @@ pub fn get_reciprocal_allocation_matrix(
|
|||||||
.or_insert(map_size);
|
.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<f64> = DMatrix::from_row_slice(
|
let mat: DMatrix<f64> = DMatrix::from_row_slice(
|
||||||
department_mappings.len(),
|
department_mappings.len(),
|
||||||
department_mappings.len(),
|
department_mappings.len(),
|
||||||
&slice_allocations,
|
&slice_allocations,
|
||||||
);
|
);
|
||||||
|
|
||||||
let costs_vec: DMatrix<f64> =
|
|
||||||
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. {
|
if mat.determinant() == 0. {
|
||||||
let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
|
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 {
|
} else {
|
||||||
let lup = mat.lu();
|
do_solve_reciprocal(
|
||||||
lup.solve(&costs_vec).unwrap()
|
mat.lu(),
|
||||||
|
account_costs,
|
||||||
|
department_mappings,
|
||||||
|
allocations,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
|
||||||
|
solver: T,
|
||||||
|
account_costs: Vec<AccountCost>,
|
||||||
|
department_mappings: HashMap<String, usize>,
|
||||||
|
allocations: Vec<OverheadAllocationRule>,
|
||||||
|
) -> Result<Vec<AccountCost>, Box<dyn Error>> {
|
||||||
|
// 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<f64> =
|
||||||
|
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![])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user