BTree with Monotone Map

I've got a monotone (strictly increasing) map that I will need to evaluate and invert (motivating problem is from implementing a CDF for an Empirical distribution)
Since it's monotone, ordinality is the same with both key and value and that means the sorting key can be something that is from both values. So I figured I can implement this with a BTreeSet

The benefit I expected was being able to range over the tree from either the key or value. However, I'm getting this error that requires I call something with RangeBounds<T> where T is the key / element for the BTreeSet, there's not a meaningful conversion from the RangeTo<KV<_,_>> which has one but not both key and value.

My questions are

  1. What are my options here?
  2. Why isn't range instead of impl RangeBounds<T: Ord<K> + Eq<K>> where K is the type for the key in BTreeMap/BTreeSet?
use ordered_float::NotNan;
use rand::distributions::Distribution;
use std::collections::BTreeSet;

#[derive(PartialEq, Ord, Eq)]
struct MonotoneMap<K, V> {
    key: K,
    val: V,
}

impl<K, V> PartialOrd for MonotoneMap<K, V>
where
    K: PartialOrd,
    V: PartialOrd,
{
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        let k_ord = self.key.partial_cmp(&other.key);
        let v_ord = self.val.partial_cmp(&other.val);
        if k_ord != v_ord {
            panic!("Found violation of monotonicity comparing MonotoneMap")
        } else {
            k_ord
        }
    }
}


#[derive(PartialEq, PartialOrd, Ord, Eq)]
enum KV<K, V> {
    K(K),
    V(V),
}

impl<K, V> PartialEq<KV<K, V>> for MonotoneMap<K, V>
where
    K: PartialEq,
    V: PartialEq,
{
    fn eq(&self, other: &KV<K, V>) -> bool {
        match other {
            KV::K(k) => self.key.eq(k),
            KV::V(v) => self.val.eq(v),
        }
    }
}

impl<K, V> PartialOrd<KV<K, V>> for MonotoneMap<K, V>
where
    K: PartialOrd,
    V: PartialOrd,
{
    fn partial_cmp(&self, other: &KV<K, V>) -> Option<std::cmp::Ordering> {
        match other {
            KV::K(k) => self.key.partial_cmp(k),
            KV::V(v) => self.val.partial_cmp(v),
        }
    }
}

pub struct Empirical {
    count: u64,
    mode: Option<NotNan<f64>>,
    mean_and_var: Option<(NotNan<f64>, NotNan<f64>)>,
    data: BTreeSet<MonotoneMap<NotNan<f64>, NotNan<f64>>>,
}

impl Empirical {
    fn new() -> Self {
        Empirical {
            count: 0,
            mode: None,
            mean_and_var: None,
            data: BTreeSet::new(),
        }
    }
    
    fn cdf(&self, x: f64) -> f64 {
        if let Ok(not_nan_data) = NotNan::new(x) {
            if let Some(last) = self.data.range(..=KV::K(not_nan_data)).next_back() {
                return last.val.into_inner();
            }
        }
        f64::NAN
    }

    fn inverse_cdf(&self, p: f64) -> f64 {
        if !(0.0..=1.0).contains(&p) {
            return f64::NAN;
        }
        if let Ok(not_nan_data) = NotNan::new(p) {
            if let Some(last) = self.data.range(KV::V(p)..).next() {
                return last.key.into_inner();
            }
        }
        f64::NAN
    }
}

impl Distribution<f64> for Empirical {
    fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> f64 {
        let uniform = rand::distributions::uniform::Uniform::new(0.0, 1.0);
        self.inverse_cdf(uniform.sample(rng))
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `RangeToInclusive<KV<NotNan<f64>, _>>: RangeBounds<MonotoneMap<NotNan<f64>, NotNan<f64>>>` is not satisfied
   --> src/lib.rs:62:49
    |
62  |             if let Some(last) = self.data.range(..=KV::K(not_nan_data)).next_back() {
    |                                           ----- ^^^^^^^^^^^^^^^^^^^^^^ the trait `RangeBounds<MonotoneMap<NotNan<f64>, NotNan<f64>>>` is not implemented for `RangeToInclusive<KV<NotNan<f64>, _>>`
    |                                           |
    |                                           required by a bound introduced by this call
    |
    = help: the following other types implement trait `RangeBounds<T>`:
              RangeToInclusive<&T>
              RangeToInclusive<T>
note: required by a bound in `BTreeSet::<T, A>::range`
   --> /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/collections/btree/set.rs:397:12
    |
393 |     pub fn range<K: ?Sized, R>(&self, range: R) -> Range<'_, T>
    |            ----- required by a bound in this associated function
...
397 |         R: RangeBounds<K>,
    |            ^^^^^^^^^^^^^^ required by this bound in `BTreeSet::<T, A>::range`

error[E0277]: the trait bound `RangeFrom<KV<_, f64>>: RangeBounds<MonotoneMap<NotNan<f64>, NotNan<f64>>>` is not satisfied
   --> src/lib.rs:74:49
    |
74  |             if let Some(last) = self.data.range(KV::V(p)..).next() {
    |                                           ----- ^^^^^^^^^^ the trait `RangeBounds<MonotoneMap<NotNan<f64>, NotNan<f64>>>` is not implemented for `RangeFrom<KV<_, f64>>`
    |                                           |
    |                                           required by a bound introduced by this call
    |
    = help: the following other types implement trait `RangeBounds<T>`:
              RangeFrom<&T>
              RangeFrom<T>
note: required by a bound in `BTreeSet::<T, A>::range`
   --> /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/collections/btree/set.rs:397:12
    |
393 |     pub fn range<K: ?Sized, R>(&self, range: R) -> Range<'_, T>
    |            ----- required by a bound in this associated function
...
397 |         R: RangeBounds<K>,
    |            ^^^^^^^^^^^^^^ required by this bound in `BTreeSet::<T, A>::range`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (lib) due to 2 previous errors

1 Like

You might be able to make this work with some trait-object tricks. Unfortunately, I don’t have time ATM to write an example for your usecase.

If you don't need dynamic insertion (ie, construct CDF once then only query it), then you can just use a slice of tuples and binary_search_by.

Demo without validity checks (eg. non-empty iterators and non-negative PMFs).

Ord<K> and Eq<K> are not valid traits, only Ord and Eq are. They require the implementation to be reflexive, so they can't allow comparing heterogeneous data.

But you don't need this, as the implementation allows you pass an arbitrary type for which the actual key in the BTreeMap/BTreeSet can be borrowed as. This means you can create two types MonotoneMapKey<K> and MonotoneMapValue<V> and have MonotoneMap<K, V> implement Borrow<MonotoneMapKey<K>> and Borrow<MonotoneMapValue<V>>. Implementing Borrow might or might not be a problem depending on what you're trying to do, for this simple case you can either have MonotoneMap<K, V> store a MonotoneMapKey<K> and MonotoneMapValue<V> as key/value or you can use bytemuck::TransparentWrapper like I did to transmute a reference to a K/V to one of the wrappers.

By the way not that this is not enough, it has to be strictly incresing (i.e. having equal elements is not allowed), otherwise you'll violate BTreeSet's and Borrow's requirements.

Oh, I should pay more attention to deriving traits. Didn't know Eq wouldn't simply unwrap PartialEq and be a signal trait for "things that are eq are the same"

Also, :exploding_head:

Thanks for this. I hadn't realized that

This seems very smart.

Is this because MonotoneMapKey::cmp / MonotoneMapValue::cmp will be called on a borrow of the actual key in the BTreeMap / BTreeSet? Or is there another/additional reasons from the BTree implementation?

Aside:
Lucky for me the problem will have actual monotonicity, so thankfully this will still work - edited the parenthetical from "non-decreasing" to "strictly increasing".

It is kinda flexible, but it has its own problems. For example a classic problem is when your key is a (String, String) and you want to use a (&str, &str) to access some element, then it falls apart because there are two things to borrow rather than one.

Yes, that's how it works.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.