fts_solver/types/
submission.rs

1use super::{DemandCurve, Segment, demand::Point, disaggregate};
2use crate::{HashMap, HashSet};
3use std::hash::Hash;
4use thiserror::Error;
5
6/// The fundamental input to a `Solver` implementation, containing an
7/// independent collection of portfolios and demand curves.
8#[derive(Debug)]
9#[cfg_attr(
10    feature = "serde",
11    derive(serde::Serialize, serde::Deserialize),
12    serde(bound = "
13        PortfolioId: Clone + Hash + Eq + serde::Serialize + serde::de::DeserializeOwned,
14        ProductId: Hash + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned
15    ")
16)]
17pub struct Submission<PortfolioId, ProductId> {
18    /// The portfolios that are defined by the submission.
19    pub portfolios: HashMap<PortfolioId, HashMap<ProductId, f64>>,
20
21    /// The demand curves in the submission
22    pub demand_curves: Vec<(HashMap<PortfolioId, f64>, Vec<Segment>)>,
23}
24
25impl<PortfolioId: Clone + Hash + Eq, ProductId: Hash + Eq + Ord>
26    Submission<PortfolioId, ProductId>
27{
28    /// Construct a canonicalized submission from the inputs
29    pub fn new<T, U, V>(
30        portfolios: impl IntoIterator<Item = (PortfolioId, T)>,
31        curves: impl IntoIterator<Item = DemandCurve<PortfolioId, U, V>>,
32    ) -> Result<Submission<PortfolioId, ProductId>, SubmissionError>
33    where
34        T: IntoIterator<Item = (ProductId, f64)>,
35        T::IntoIter: ExactSizeIterator,
36        U: IntoIterator<Item = (PortfolioId, f64)>,
37        V: IntoIterator<Item = Point>,
38    {
39        // Step 1: Canonicalize the portfolios
40        let portfolios = portfolios
41            .into_iter()
42            .map(|(id, weights)| {
43                let weights = weights.into_iter();
44                let mut portfolio = HashMap::<ProductId, f64>::with_capacity_and_hasher(
45                    weights.len(),
46                    Default::default(),
47                );
48
49                // If product entries are repeated, sum them
50                for (product, weight) in weights {
51                    *portfolio.entry(product).or_default() += weight;
52                }
53
54                // For maximal sparsity, keep only non-zero terms
55                portfolio.retain(|_, v| *v != 0.0);
56
57                // Sort the portfolio by product id, so that subsequent matrix
58                // construction can rely on this invariant.
59                // (Note: we can enforce this invariant elsewhere, if we want to remove this for performance.)
60                portfolio.sort_unstable_keys();
61
62                (id, portfolio)
63            })
64            .collect::<HashMap<_, _>>();
65
66        // Each portfolio creates a decision variable, but if the variable is
67        // not referenced by a cost, it becomes unconstrained. We *define* the
68        // behavior in this case to force unconstrained portfolios to zero.
69        // To start, we assert every portfolio is unused, then remove entries
70        // as we iterate through the demand curves.
71        let mut unused: HashSet<&PortfolioId> = portfolios.keys().collect();
72
73        // Step 2: Canonicalize the demand curves
74        let mut curves = curves
75            .into_iter()
76            .filter_map(
77                |DemandCurve {
78                     domain: (min, max),
79                     group,
80                     points,
81                 }| {
82                    // Collect the weights into a sparse vector
83                    let group = {
84                        // Aggreggate any repeated entries
85                        let mut group2 = HashMap::default();
86                        for (id, weight) in group {
87                            *group2.entry(id).or_default() += weight;
88                        }
89
90                        // Maximize the sparsity of the vector
91                        group2.retain(|id, weight| {
92                            // We only keep this pair if the associated portfolio
93                            // (1) exists and (2) has at least one product to trade
94                            // and the weight is nonzero.
95                            let portfolio_size = portfolios
96                                .get(id)
97                                .map(|portfolio| portfolio.len())
98                                .unwrap_or(0);
99
100                            if portfolio_size != 0 && *weight != 0.0 {
101                                // If we keep the pair, make sure we accredit `unused`
102                                unused.swap_remove(id);
103                                true
104                            } else {
105                                false
106                            }
107                        });
108
109                        // If the group is empty, just ignore this curve entirely
110                        if group2.len() == 0 {
111                            return None;
112                        }
113
114                        group2
115                    };
116
117                    // Decompose the curve into its constituent segments
118                    let segments = disaggregate(points.into_iter(), min, max)
119                        .map(|iter| iter.collect::<Result<Vec<_>, _>>());
120
121                    match segments {
122                        Some(Ok(segments)) => Some(Ok((group, segments))),
123                        Some(Err(error)) => Some(Err(SubmissionError::InvalidDemandCurve(error))),
124                        None => Some(Err(SubmissionError::InvalidDomain(min, max))),
125                    }
126                },
127            )
128            .collect::<Result<Vec<_>, _>>()?;
129
130        // Now, anything left in `unused` was not referenced by any demand curve.
131        // Accordingly, we inject demand curves that force them to zero.
132        for portfolio in unused {
133            let mut group = HashMap::with_capacity_and_hasher(1, Default::default());
134            group.insert(portfolio.clone(), 1.0);
135            curves.push((group, Vec::new()))
136        }
137
138        // Note: there are opportunities for additional gains here. For example,
139        // subsetting the demand curves (group, segments) on segments.len() == 0
140        // gives us a presolve step where we can solve Ax = 0 (rows of A are the
141        // groups) and determine which x are necessarily zero, which can be
142        // propagated upwards to further sparsify the groups as well as remove the
143        // associated portfolio.
144        //
145        // Since the underlying QP solver does its own linear algebra, we do not
146        // exploit this presolve step in this constructor. However, we intend to
147        // make a Submission::presolve(&mut self) function that does this and other
148        // such calculations.
149        Ok(Self {
150            portfolios,
151            demand_curves: curves,
152        })
153    }
154
155    // /// Computes which portfolios are necessarily zero and removes them from the submission
156    // pub fn presolve(&mut self) -> HashSet<PortfolioId> {
157    //     unimplemented!("TODO");
158    // }
159}
160
161/// The error type for when preparing a submission fails
162#[derive(Error, Debug)]
163pub enum SubmissionError {
164    /// When a demand curve cannot be disaggregated
165    #[error("invalid demand curve")]
166    InvalidDemandCurve(Segment),
167
168    /// When an invalid truncation is specified for a demand curve
169    #[error("invalid domain")]
170    InvalidDomain(f64, f64),
171}
172
173#[cfg(test)]
174mod tests {
175    //use super::*;
176
177    // TODO
178}