#![allow(clippy::needless_range_loop)]
use anyhow::anyhow;
use num_traits::float::Float;
use ordered_float::OrderedFloat;
use num_traits::FromPrimitive;
use std::fmt::Debug;
use indxvec::Vecops;
use std::cell::RefCell;
const EPSIL: f64 = 1.0E-6;
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Direction {
Ascending,
Descending,
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct Point<T: Float + Debug> {
x: T,
y: T,
weight: T,
}
impl<T> Default for Point<T>
where
T: Float + Debug,
{
fn default() -> Self {
Point {
x: T::zero(),
y: T::zero(),
weight: T::zero(),
}
}
}
impl<T> Point<T>
where
T: Float + Debug + std::ops::AddAssign,
{
pub fn new(x: T, y: T) -> Point<T> {
Point {
x,
y,
weight: T::from(1.0).unwrap(),
}
}
pub fn new_with_weight(x: T, y: T, weight: T) -> Point<T> {
Point { x, y, weight }
}
pub fn x(&self) -> T {
self.x
}
pub fn y(&self) -> T {
self.y
}
pub fn weight(&self) -> T {
self.weight
}
fn merge(&mut self, other: &Point<T>) {
self.x = ((self.x * self.weight) + (other.x * other.weight)) / (self.weight + other.weight);
self.y = ((self.y * self.weight) + (other.y * other.weight)) / (self.weight + other.weight);
self.weight += other.weight;
}
}
impl<T: Float + Debug> PartialOrd for Point<T> {
fn partial_cmp(&self, other: &Point<T>) -> Option<std::cmp::Ordering> {
self.x.partial_cmp(&other.x)
}
}
fn interpolate_two_points<T>(a: &Point<T>, b: &Point<T>, at_x: &T) -> T
where
T: Float + Debug,
{
let prop = (*at_x - (a.x)) / (b.x - a.x);
(b.y - a.y) * prop + a.y
}
#[derive(Debug, Copy, Clone)]
pub struct BlockPoint<'a, T: Float + Debug> {
direction: Direction,
points: &'a [Point<T>],
index: &'a [usize],
first: usize,
last: usize,
centroid: Point<T>,
}
impl<'a, T> BlockPoint<'a, T>
where
T: Float + std::ops::DivAssign + std::ops::AddAssign + Debug,
{
pub fn new(
direction: Direction,
points: &'a Vec<Point<T>>,
index: &'a [usize],
first: usize,
last: usize,
) -> Self {
BlockPoint {
direction,
points,
index,
first,
last,
centroid: Point::<T>::default(),
}
}
fn new_from_point(
direction: Direction,
points: &'a [Point<T>],
index: &'a [usize],
idx: usize,
) -> Self {
let centroid = points[index[idx]];
BlockPoint {
direction,
points,
index,
first: idx,
last: idx + 1,
centroid,
}
}
pub(crate) fn merge(&mut self, other: &BlockPoint<'a, T>) -> Result<(), anyhow::Error> {
log::debug!(
"entering block merge self, {} {} other : {} {} ",
self.first,
self.last,
other.first,
other.last
);
if self.last == other.first {
self.last = other.last;
} else if self.first == other.last {
self.first = other.first;
} else {
log::error!("not contiguous blocks");
return Err(anyhow!("not contiguous blocks"));
}
self.centroid.merge(&other.centroid);
log::debug!(
"exiting block merge self, {} {}, centroid : {:?}",
self.first,
self.last,
self.centroid
);
Ok(())
}
pub fn get_centroid(&self) -> Point<T> {
self.centroid
}
pub(crate) fn get_first_index(&self) -> usize {
self.first
}
pub(crate) fn get_last_index(&self) -> usize {
self.last
}
#[allow(unused)]
fn get_point_index_unsorted(&self) -> &[usize] {
&self.index[self.first..self.last]
}
fn get_point(&self, idx: usize) -> Option<&'a Point<T>> {
if idx < self.first || idx >= self.last {
None
} else {
Some(&self.points[self.index[idx]])
}
}
pub fn get_nb_points(&self) -> usize {
self.last - self.first
}
fn is_ordered(&self, other: &BlockPoint<T>) -> bool {
assert_eq!(self.direction, other.direction);
match self.direction {
Direction::Ascending => self.centroid.y < other.centroid.y,
Direction::Descending => self.centroid.y > other.centroid.y,
}
}
#[allow(unused)]
pub fn dump(&self) {
log::debug!("\n \n block dump");
println!(
"first last centroid : {} {} {:?}",
self.first, self.last, self.centroid
);
println!(" first points :");
let nbd = (self.last - self.first).min(3);
for i in 0..nbd {
println!(
" point i : {} : {:?}",
i,
self.points[self.index[self.first + i]]
);
}
println!(" last points :");
let nbd = (self.last - self.first).min(3).min(self.index.len());
for i in (1..nbd).rev() {
println!(
" point i : {} : {:?}",
i,
self.points[self.index[self.last - i]]
);
}
}
pub fn get_point_iter(&'a self) -> PointIterator<'a, T> {
PointIterator::new(self, self.index)
}
}
impl<'a, T: Float + Debug> PartialEq for BlockPoint<'a, T> {
fn eq(&self, other: &BlockPoint<T>) -> bool {
self.centroid.eq(&other.centroid)
}
}
impl<'a, T: Float + Debug> PartialOrd for BlockPoint<'a, T> {
fn partial_cmp(&self, other: &BlockPoint<T>) -> Option<std::cmp::Ordering> {
self.centroid.x.partial_cmp(&other.centroid.x)
}
}
pub struct PointIterator<'a, T: Float + Debug> {
block: BlockPoint<'a, T>,
index: &'a [usize],
pt_index: usize,
}
impl<'a, T> PointIterator<'a, T>
where
T: Float + std::ops::DivAssign + std::ops::AddAssign + std::ops::DivAssign + Debug,
{
pub fn new(block: &'a BlockPoint<'a, T>, index: &'a [usize]) -> Self {
PointIterator {
block: *block,
index,
pt_index: block.get_first_index(),
}
}
}
impl<'a, T> Iterator for PointIterator<'a, T>
where
T: Float + std::ops::DivAssign + std::ops::AddAssign + std::ops::DivAssign + Debug,
{
type Item = (&'a Point<T>, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.pt_index >= self.block.get_last_index()
|| self.pt_index < self.block.get_first_index()
{
None
} else {
let point = self.block.get_point(self.pt_index).unwrap();
let idx = self.pt_index;
self.pt_index += 1;
Some((point, self.index[idx]))
}
} }
pub(crate) fn get_point_blocnum<T>(regression: &IsotonicRegression<T>) -> Vec<u32>
where
T: Float + std::iter::Sum + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + Debug,
{
let index = regression.get_point_index();
let mut rank: Vec<usize> = (0..index.len()).map(|_| 0).collect();
for i in 0..index.len() {
let k = index[i];
rank[k] = i;
}
let nb_blocks = regression.get_nb_block();
let mut nb_found = 0;
let mut blocknum: Vec<u32> = (0..index.len()).map(|_| 0).collect();
let mut last_b_found: usize = 0;
let mut found: bool;
for i in 0..index.len() {
let k = index[i];
found = false;
for b in last_b_found..nb_blocks {
let block = regression.get_block(b).unwrap();
if i >= block.get_first_index() && i < block.get_last_index() {
blocknum[k] = b as u32;
nb_found += 1;
log::debug!(
"PointBlockLocator setting point index: {}, rank : {}, set to blocnum : {}",
i,
k,
b
);
last_b_found = b;
found = true;
break;
}
}
if !found {
log::error!(" point cannot be located in any block");
regression.check_blocks();
}
}
assert_eq!(nb_found, index.len());
blocknum
}
pub struct PointBlockLocator {
blocknum: Vec<u32>,
}
impl PointBlockLocator {
pub fn new<T>(regression: &IsotonicRegression<T>) -> Self
where
T: Float
+ std::iter::Sum
+ FromPrimitive
+ std::ops::AddAssign
+ std::ops::DivAssign
+ Debug,
{
PointBlockLocator {
blocknum: get_point_blocnum(regression),
}
}
pub fn get_point_block_num(&self, k: usize) -> Result<usize, anyhow::Error> {
if k < self.blocknum.len() {
Ok(self.blocknum[k] as usize)
} else {
Err(anyhow!("too large arg, not so many blocks"))
}
}
pub fn get_point_blocnum(&mut self) -> &mut Vec<u32> {
&mut self.blocknum
}
}
#[derive(Debug)]
pub struct IsotonicRegression<'a, T: Float + Debug> {
direction: Direction,
points: &'a [Point<T>],
index: Vec<usize>,
blocks: RefCell<Vec<BlockPoint<'a, T>>>,
centroid_point: Point<T>,
}
impl<'a, T> IsotonicRegression<'a, T>
where
T: Float + std::iter::Sum + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + Debug,
{
pub fn new_ascending(points: &[Point<T>]) -> IsotonicRegression<T> {
IsotonicRegression::new(points, Direction::Ascending)
}
pub fn new_descending(points: &[Point<T>]) -> IsotonicRegression<T> {
IsotonicRegression::new(points, Direction::Descending)
}
fn new(points: &'a [Point<T>], direction: Direction) -> IsotonicRegression<'a, T> {
let point_count: T = points.iter().map(|p| p.weight).sum();
let mut sum_x: T = T::from(0.0).unwrap();
let mut sum_y: T = T::from(0.0).unwrap();
for point in points {
sum_x += point.x * point.weight;
sum_y += point.y * point.weight;
}
let index = if !points.is_empty() {
points.mergesort_indexed()
} else {
Vec::<usize>::new()
};
let blocks = Vec::<BlockPoint<'a, T>>::new();
log::debug!("initializing IsotonicRegression");
IsotonicRegression {
direction,
points,
index,
blocks: RefCell::new(blocks),
centroid_point: Point::new(sum_x / point_count, sum_y / point_count),
}
}
pub fn get_point_index(&self) -> &[usize] {
&self.index
}
pub fn get_block_point_index<'b: 'a>(&'b self, blocknum: usize) -> Option<Vec<usize>> {
let blocks = self.get_blocks();
if blocknum >= blocks.borrow().len() {
return None;
}
let indexes = Vec::from(blocks.borrow()[blocknum].get_point_index_unsorted());
Some(indexes)
}
pub fn interpolate<'b: 'a>(&'b self, at_x: T) -> T
where
T: Float + ordered_float::FloatCore,
{
log::debug!("interpolate nb blocks = {}", self.blocks.borrow().len());
if self.blocks.borrow().is_empty() {
log::info!("uninitialized regression, running do_isotonic");
let _res = self.do_isotonic();
}
let blocks = self.blocks.borrow();
if blocks.len() == 1 {
blocks[0].centroid.y
} else {
let pos =
blocks.binary_search_by_key(&OrderedFloat(at_x), |p| OrderedFloat(p.centroid.x));
match pos {
Ok(ix) => blocks[ix].centroid.y,
Err(ix) => {
if ix < 1 {
interpolate_two_points(
&blocks.first().unwrap().centroid,
&self.centroid_point,
&at_x,
)
} else if ix >= self.points.len() {
interpolate_two_points(
&self.centroid_point,
&blocks.last().unwrap().centroid,
&at_x,
)
} else {
interpolate_two_points(
&blocks[ix - 1].centroid,
&blocks[ix].centroid,
&at_x,
)
}
}
}
}
}
pub fn get_points(&self) -> &'a [Point<T>] {
self.points
}
pub(crate) fn get_blocks<'b: 'a>(&'b self) -> &'b RefCell<Vec<BlockPoint<'b, T>>> {
&self.blocks
}
pub fn get_block(&self, rank: usize) -> Option<BlockPoint<T>> {
if rank < self.blocks.borrow().len() {
Some(self.blocks.borrow()[rank])
} else {
None
}
}
pub fn get_centroid(&self) -> &Point<T> {
&self.centroid_point
}
pub fn do_isotonic<'b: 'a>(&'b self) -> Result<(), anyhow::Error> {
log::debug!("do_isotonic , nb points : {:?}", self.points.len());
if self.points.is_empty() {
log::info!("no points to do regression");
return Err(anyhow!("no points to do regression"));
}
if !self.blocks.borrow().is_empty() {
return Err(anyhow!("regression already done!"));
}
let epsil = T::from(EPSIL).unwrap();
let mut blocks: Vec<RefCell<BlockPoint<T>>> = Vec::new();
for i in 0..self.points.len() {
let new_block =
BlockPoint::<T>::new_from_point(self.direction, self.points, &self.index, i);
if !(i == 0
|| (i > 0 && self.points[self.index[i]].x >= self.points[self.index[i - 1]].x))
{
log::warn!("i : {}, point : {:?}", i, self.points[self.index[i]].x);
if i > 0 {
log::warn!(
"point i-1 : {:?}, point i : {:?}",
self.points[self.index[i - 1]].x,
self.points[self.index[i]].x
);
}
}
if i == 0
|| (i > 0
&& self.points[self.index[i]].x - self.points[self.index[i - 1]].x > epsil)
{
blocks.push(RefCell::new(new_block));
} else {
log::debug!(
"merging two equal points {:#?} {:#?}",
self.points[self.index[i - 1]].x,
self.points[self.index[i]].x
);
let last_block = blocks.pop().unwrap();
last_block.borrow_mut().merge(&new_block).unwrap();
blocks.push(last_block);
}
}
log::info!("nb blocks before ordering violation : {}", blocks.len());
let mut iso_blocks: Vec<RefCell<BlockPoint<T>>> = Vec::new();
for b in blocks {
let mut inserted = false;
let new_iso = b.clone();
while !inserted {
if iso_blocks.is_empty()
|| iso_blocks
.last()
.unwrap()
.borrow()
.is_ordered(&new_iso.borrow())
{
log::debug!(
"\n inserting new block, centroid : {:?} ",
new_iso.borrow().centroid
);
iso_blocks.push(new_iso.clone());
inserted = true;
} else {
let previous = iso_blocks.pop().unwrap();
new_iso.borrow_mut().merge(&previous.borrow()).unwrap();
}
}
} log::info!("\n after final merge nb blocks = {}", iso_blocks.len());
let final_blocks: Vec<BlockPoint<T>> =
iso_blocks.into_iter().map(|obj| obj.into_inner()).collect();
*self.blocks.borrow_mut() = final_blocks;
if log::log_enabled!(log::Level::Debug) {
self.print_blocks();
self.check_blocks();
}
Ok(())
}
pub fn get_nb_block(&self) -> usize {
self.blocks.borrow().len()
}
pub fn get_block_centroid(&self, bloc: usize) -> Result<Point<T>, ()> {
if bloc > self.get_nb_block() {
return Err(());
}
Ok(self.blocks.borrow()[bloc].centroid)
}
pub(crate) fn check_blocks(&self) -> bool {
let blocks = self.blocks.borrow();
for i in 0..blocks.len() {
if i == 0 {
assert_eq!(blocks[0].first, 0);
} else {
assert_eq!(blocks[i - 1].last, blocks[i].first);
}
}
assert_eq!(blocks.last().unwrap().last, self.points.len());
true
}
pub(crate) fn print_blocks(&self) {
log::debug!("dump of blocks");
let blocks = self.blocks.borrow();
for i in 0..blocks.len() {
blocks[i].dump();
}
} }
#[cfg(test)]
mod tests {
use super::*;
fn log_init_test() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn usage_example() {
log_init_test();
let points = &[
Point::<f64>::new(0.0, 1.0),
Point::<f64>::new(1.0, 2.0),
Point::<f64>::new(2.0, 1.5),
];
let regression = IsotonicRegression::new_ascending(points);
regression.do_isotonic().unwrap();
assert_eq!(regression.interpolate(1.5), 1.75);
}
#[test]
fn isotonic_no_points() {
log_init_test();
let points: &[Point<f64>; 0] = &[];
let regression = IsotonicRegression::new_ascending(points);
let res = regression.do_isotonic();
if res.is_err() {
println!("{:?}", res.as_ref().err().unwrap());
}
assert!(&res.is_err());
}
#[test]
fn isotonic_one_point() {
log_init_test();
let points = &[Point::<f64>::new(1.0, 2.0)];
let regression = IsotonicRegression::new(points, Direction::Ascending);
let res = regression.do_isotonic();
assert!(res.is_ok());
assert_eq!(regression.get_centroid(), &Point::<f64>::new(1.0, 2.0));
assert_eq!(regression.get_nb_block(), 1);
}
#[test]
fn isotonic_point_merge() {
log_init_test();
let mut point = Point::<f64>::new(1.0, 2.0);
point.merge(&Point::<f64>::new(2.0, 0.0));
assert_eq!(point, Point::new_with_weight(1.5, 1.0, 2.0));
}
#[test]
fn isotonic_one_not_merged() {
log_init_test();
let points = &[
Point::new(0.5, -0.5),
Point::new(2.0, 0.0),
Point::new(1.0, 2.0),
];
let regression = IsotonicRegression::new(points, Direction::Ascending);
let res = regression.do_isotonic();
assert!(res.is_ok());
assert_eq!(regression.get_nb_block(), 2);
let blocks = regression.get_blocks();
let centroid_1 = blocks.borrow()[0].get_centroid();
let centroid_2 = blocks.borrow()[1].get_centroid();
assert_eq!(centroid_1, Point::new(0.5, -0.5));
assert_eq!(centroid_2, Point::new_with_weight(1.5, 1.0, 2.0));
let locator = PointBlockLocator::new(®ression);
assert_eq!(locator.get_point_block_num(0).unwrap(), 0);
assert_eq!(locator.get_point_block_num(1).unwrap(), 1);
assert_eq!(locator.get_point_block_num(2).unwrap(), 1);
}
#[test]
fn isotonic_merge_three() {
log_init_test();
let points = &[
Point::new(0.0, 1.0),
Point::new(1.0, 2.0),
Point::new(2.0, -1.0),
];
let regression = IsotonicRegression::new(points, Direction::Ascending);
let res = regression.do_isotonic();
assert!(res.is_ok());
assert_eq!(regression.get_nb_block(), 1);
let blocks = regression.get_blocks();
let centroid_1 = blocks.borrow()[0].get_centroid();
assert_eq!(centroid_1, Point::new_with_weight(1.0, 2.0 / 3.0, 3.0));
}
#[test]
fn test_interpolate() {
log_init_test();
let points = [Point::new(1.0, 5.0), Point::new(2.0, 7.0)];
let regression = IsotonicRegression::new_ascending(&points);
assert!((regression.interpolate(1.5) - 6.0).abs() < f64::EPSILON);
}
#[test]
fn test_isotonic_ascending() {
log_init_test();
let points = &[
Point::new(0.0, 1.0),
Point::new(1.0, 2.0),
Point::new(2.0, -1.0),
];
let regression = IsotonicRegression::new_ascending(points);
let _res = regression.do_isotonic();
assert_eq!(regression.get_nb_block(), 1);
assert_eq!(
regression.get_block_centroid(0).unwrap(),
Point::new_with_weight((0.0 + 1.0 + 2.0) / 3.0, (1.0 + 2.0 - 1.0) / 3.0, 3.0)
)
}
#[test]
fn test_isotonic_descending() {
log_init_test();
let points = &[
Point::new(0.0, -1.0),
Point::new(1.0, 2.0),
Point::new(2.0, 1.0),
];
let regression = IsotonicRegression::new_descending(points);
let _res = regression.do_isotonic();
assert_eq!(regression.get_nb_block(), 1);
assert_eq!(
regression.get_block_centroid(0).unwrap(),
Point::new_with_weight(1.0, 2.0 / 3.0, 3.0)
)
}
#[test]
fn test_descending_interpolation() {
log_init_test();
let points = [
Point::new(0.0, 3.0),
Point::new(1.0, 2.0),
Point::new(2.0, 1.0),
];
let regression = IsotonicRegression::new_descending(&points);
assert_eq!(regression.interpolate(0.5), 2.5);
}
#[test]
fn test_single_point_regression() {
log_init_test();
let points = [Point::new(1.0, 3.0)];
let regression = IsotonicRegression::new_ascending(&points);
assert_eq!(regression.interpolate(0.0), 3.0);
}
}