Skip to content

Commit

Permalink
Prototyping Updates
Browse files Browse the repository at this point in the history
Update to README framework, early days.

Implementation of a Builder for small Piecewise functions.

Some ability to query a Piecewise (at function), and display it.
  • Loading branch information
electronjoe committed Apr 16, 2019
1 parent 5827ab3 commit 6e2434b
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ edition = "2018"

[dependencies]
intervals-general = "0.1.0"
itertools = "0.8.0"
smallvec = "0.6.9"

[patch.crates-io]
intervals-general = {path = "../intervals-general" }
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,70 @@
# piecewise
A Rust crate with support for representation of Piecewise functions (values over Intervals) and common operations on such functions.

A Rust crate with supporting representation of Piecewise constant functions (values over Intervals) and common operations.

## Summary

Addition of a new Crate named piecewise which supports rigorous piecewise constant function representation and common piecewise function operations, all while operating over generic bound data types provided required traits are met (e.g. can use [units of measure](https://crates.io/crates/uom) in defining piecewise segments).

## Motivation

In working to write a simulation tool with support for e.g. piecewise function representations that enforce units of measure - I was unable to find a suitable candidate.

### Requirements

The Requirements for the library are then:

1. Support for Piecewise segments defined over [general intervals](https://github.com/electronjoe/intervals-general) with bound data types provied via generic
1. Support for values over Piecewise segments having data types provided via generic
1. Support for SmallPiecewise functions, with focus on performance for low-segment-count representations
1. Support for common operations on piecewise functions, e.g. multipliciation, convolution, value lookup, etc

Additional desires:

1. no_std support
1. No use of of panic, assert
1. Minimize error handling by design
1. Make the library hard to use incorrectly

### Requirement Reasoning

As a motivating case consider the use of this Intervals library to build a step function library for physics simulations. Lets omit for now the design decision to use Interval representations to back the step function library (versus say "change point" annotations). Commonly in such a setting, you may want:

One may want to be capable of expressing signal contents:

* Over a domain that (optionally) stretched to +/- infinity
* With arbitrary bounds on segements (e.g. Closed here, Left-Half-Open there)
* Use of [Units of Measure](https://crates.io/crates/uom) types to detect user error

Why some of these characteristics? If you cannot specify interval ranges out to +/- infinity, then one must determine in advance just how far out a signal may matter - which is truly a function of your processing chain. It is awkward and error prone to simply say to oneself "the signal is primarily in 40-50 Ghz, I'll add sidelobe behavior out an additional 5 Ghz and should be fine" - only to have a mixer place contents well outside of this defined 35-55 Ghz in an important band down the chain. Additionally, if one can support operations on only domain-complete piecewise functions, then error handling and signaling to the user is greatly reduced (one need not consider how to handle operations in which one of the inputs is undefined).

As to arbitrary bounds on segments - it is confusing and awkward to have a signal that spans e.g. 40-50 Ghz but have a library that uses Intervals that are exclusively e.g. [LeftHalfOpen](https://proofwiki.org/wiki/Definition:Real_Interval/Half-Open) Intervals. Because query of value_at(40 Ghz) -> undefined while value_at(50 Ghz) -> defined. This leaves sharp edges. Meanwhile one cannot use exclusively closed intervals (which alleivate this confusion) because then one cannot define piecewise functions with abbuting intervals (one would double-define some domain point/s). Additinally, representations of signals may very well want to omit a DC component (i.e. open bound at zero). The motivation for general Interval specification capabilities is quite broad and will crop up in many mathematics and physics contexts.

Finally, supporting (but not requiring) Units of Measure type enforcement of our units seems pretty non-contentious.

## Detailed Design

### Terminology Selection

* Propose use of **Segment** as the building blocks of Piecewise
* Domain of a Segement is an **[Interval](https://github.com/electronjoe/intervals-general)**
* Value of a Segemnt is a generic **value**

### SmallPiecewise vs Piecewise

For small piecewise functions, it is likely that simple operations over a primarily stack allocated array will be more performant. Benchmarks for comparison and validation of this claim will be provided. The crate will also support large Piecewise functions using appropriate heap centric allocation and logarithmic operations.

### Constructing Piecewise

Describe builder

## Open Design Questions

### Complete Domain Coverage

Should there be Piecewise variants that are guaranteed to contain the entire Domain?
E.g. this would ensure that value_at would always return a value, no need for Optional.

### Support for broader Piecewise functions

Can support for a braoder class of Piecewise functions be engeineered? E.g. Piecewise linear, etc? The impact of this extension seems to lie in how well the value generic of Segment can be extended to arbitrary functions yet provide lookup semantics that do not complexify our base case of constant use.
186 changes: 185 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use intervals_general::interval::Interval;
use itertools::iproduct;
use smallvec::smallvec;

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ValueOverInterval<T, U> {
Expand Down Expand Up @@ -93,14 +95,174 @@ where
}
}

/// SmallPiecewise
///
/// The SmallPiecewise variant is for use when the number of Intervals
/// over which the function is defined is relatively small. For these
/// small entities, benchmarking backs the intuition that we benefit from:
///
/// * Stack storage with heap overflow (via SmallVec)
/// * Linear search instead of binary search
///
/// The step function is ensured to be well-defined. Specifically this means:
///
/// * All intervals are pairwise disjoint (non-overlapping)
/// * The union of the intervals is the entire real line (TODO: drop this?)
///
/// These guarantees are ensured by the StepFunctionBuilder at build() time, and
/// by operations over piecewise functions.
#[derive(Clone, Debug)]
pub struct SmallPiecewise<T, U> {
values_over_intervals: smallvec::SmallVec<[ValueOverInterval<T, U>; 8]>,
}

impl<T, U> SmallPiecewise<T, U>
where
T: std::cmp::PartialOrd,
T: std::marker::Copy,
T: std::ops::Sub,
{
/// Retrieves the value of the piecewise function at a specific point
///
/// If the Domain does not contain the value specified by at: - Optional
/// returns None
///
/// # Runtime
///
/// Using a simple learning search - runtime is linear in Segment count
///
/// # Examples
///
/// ```
/// use intervals_general::interval::Interval;
/// use piecewise::SmallPiecewiseBuilder;
/// use piecewise::ValueOverInterval;
///
/// let mut builder: SmallPiecewiseBuilder<u32, f32> = SmallPiecewiseBuilder::new();
/// builder.add(ValueOverInterval::new(
/// Interval::UnboundedClosedLeft { left: 230 },
/// 2.0,
/// ));
/// builder.add(ValueOverInterval::new(
/// Interval::UnboundedOpenRight { right: 200 },
/// 1.0,
/// ));
/// let small_piecewise = builder.build();
///
/// assert_eq!(small_piecewise.value_at(1), Some(&1.0));
/// assert_eq!(small_piecewise.value_at(200), None);
/// assert_eq!(small_piecewise.value_at(230), Some(&2.0));
/// ```
pub fn value_at(&self, at: T) -> Option<&U> {
self.values_over_intervals
.iter()
.find(|voi| voi.interval().contains(&Interval::Singleton { at }))
.and_then(|voi| Some(voi.value()))
}
}

impl<T, U> std::fmt::Display for SmallPiecewise<T, U>
where
T: std::fmt::Debug,
U: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut output = String::new();
for i in self.values_over_intervals.iter() {
output.push_str(&format!("{}{: >7?}\n", i.interval(), i.value()));
}
write!(f, "{}", output)
}
}

#[derive(Default)]
pub struct SmallPiecewiseBuilder<T, U>
where
T: Copy,
T: PartialOrd,
{
values_over_intervals: smallvec::SmallVec<[ValueOverInterval<T, U>; 8]>,
}

impl<T, U> SmallPiecewiseBuilder<T, U>
where
T: std::cmp::PartialOrd,
T: std::marker::Copy,
T: std::ops::Sub,
U: std::marker::Copy,
{
pub fn new() -> SmallPiecewiseBuilder<T, U> {
SmallPiecewiseBuilder {
values_over_intervals: smallvec::SmallVec::new(),
}
}

/// Consume the builder and produce a SmallPiecewise output
pub fn build(self) -> SmallPiecewise<T, U> {
SmallPiecewise {
values_over_intervals: self.values_over_intervals,
}
}

/// Add a Segment to the Builder, overlay on top of existing
///
/// When adding a new Segment, if portions of the existing Segments
/// overlap in the domain, the new segment is applied and existing
/// segments are modified to deconflict (newest addition wins).
///
/// Additionally, the segments are sorted and duplicates are removed.
///
/// # Example
///
/// ```
/// use intervals_general::interval::Interval;
/// use piecewise::SmallPiecewiseBuilder;
/// use piecewise::ValueOverInterval;
///
/// let mut builder: SmallPiecewiseBuilder<u32, f32> = SmallPiecewiseBuilder::new();
/// builder.add(ValueOverInterval::new(Interval::Unbounded, 5.0));
/// builder.add(ValueOverInterval::new(
/// Interval::UnboundedClosedLeft { left: 230 },
/// 2.0,
/// ));
/// builder.add(ValueOverInterval::new(
/// Interval::UnboundedOpenRight { right: 200 },
/// 1.0,
/// ));
/// let small_piecewise = builder.build();
///
/// println!("{}", small_piecewise);
///
/// assert_eq!(small_piecewise.value_at(1), Some(&1.0));
/// assert_eq!(small_piecewise.value_at(210), Some(&5.0));
/// assert_eq!(small_piecewise.value_at(230), Some(&2.0));
/// assert_eq!(small_piecewise.value_at(231), Some(&2.0));
/// ```
pub fn add(&mut self, element: ValueOverInterval<T, U>) -> &mut Self {
let mut new_voi: smallvec::SmallVec<[ValueOverInterval<T, U>; 8]> = smallvec![];
for (self_voi, complement_interval) in
iproduct!(&self.values_over_intervals, element.interval().complement())
{
new_voi.push(ValueOverInterval {
interval: self_voi.interval().intersect(&complement_interval),
value: *self_voi.value(),
});
}
self.values_over_intervals = new_voi;
self.values_over_intervals.push(element);
self
}
}

#[cfg(test)]
mod tests {
use crate::SmallPiecewiseBuilder;
use crate::ValueOverInterval;
use intervals_general::bound_pair::BoundPair;
use intervals_general::interval::Interval;

#[test]
fn mul() {
fn value_over_interval_mul() {
let value_over_interval = ValueOverInterval::new(
Interval::Closed {
bound_pair: BoundPair::new(1.0, 2.0).unwrap(),
Expand All @@ -118,4 +280,26 @@ mod tests {
);
assert_eq!(*scaled_value.value(), 48);
}

#[test]
fn builder_add() {
let mut builder: SmallPiecewiseBuilder<u32, f32> = SmallPiecewiseBuilder::new();
builder.add(ValueOverInterval::new(Interval::Unbounded, 5.0));
builder.add(ValueOverInterval::new(
Interval::UnboundedClosedLeft { left: 230 },
2.0,
));
builder.add(ValueOverInterval::new(
Interval::UnboundedOpenRight { right: 200 },
1.0,
));
let small_piecewise = builder.build();

println!("{}", small_piecewise);

assert_eq!(small_piecewise.value_at(1), Some(&1.0));
assert_eq!(small_piecewise.value_at(210), Some(&5.0));
assert_eq!(small_piecewise.value_at(230), Some(&2.0));
assert_eq!(small_piecewise.value_at(231), Some(&2.0));
}
}

0 comments on commit 6e2434b

Please sign in to comment.