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}