Rework reciprocal allocation to be closer to final algorithm, perform calculations off heap

This commit is contained in:
piv
2022-06-18 14:07:56 +09:30
parent efdf4af2de
commit 08433d6ea6

View File

@@ -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<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
// 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<OverheadAllocationRule>,
total_costs: Vec<TotalDepartmentCost>,
) -> DMatrix<f64> {
account_costs: Vec<AccountCost>,
// 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();
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<f64> = DMatrix::from_row_slice(
department_mappings.len(),
department_mappings.len(),
&slice_allocations,
);
if mat.determinant() == 0. {
let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
do_solve_reciprocal(
pseudo_inverse.unwrap(),
account_costs,
department_mappings,
allocations,
)
} else {
do_solve_reciprocal(
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);
// 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)
} else {
let lup = mat.lu();
lup.solve(&costs_vec).unwrap()
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![])
}