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