fts_solver/impls/
clarabel.rs

1use crate::{AuctionOutcome, HashMap, PortfolioOutcome, ProductOutcome, Solver, Submission};
2use clarabel::{algebra::*, solver::*};
3use std::hash::Hash;
4
5/// A solver implementation that uses the Clarabel interior point method
6/// for quadratic programming to solve the market clearing problem.
7///
8/// This solver is generally more accurate but can be slower than ADMM-based
9/// solvers for large problems. It's a good choice when high precision is needed.
10pub struct ClarabelSolver(DefaultSettings<f64>);
11
12impl Default for ClarabelSolver {
13    fn default() -> Self {
14        let mut settings = DefaultSettings::default();
15        settings.verbose = false;
16        Self(settings)
17    }
18}
19
20impl Solver for ClarabelSolver {
21    type Settings = DefaultSettings<f64>;
22
23    fn new(settings: Self::Settings) -> Self {
24        Self(settings)
25    }
26
27    fn solve<
28        T,
29        BidderId: Eq + Hash + Clone + Ord,
30        PortfolioId: Eq + Hash + Clone + Ord,
31        ProductId: Eq + Hash + Clone + Ord,
32    >(
33        &self,
34        auction: &T,
35        // TODO: warm-starts with the prices
36    ) -> AuctionOutcome<BidderId, PortfolioId, ProductId>
37    where
38        for<'t> &'t T: IntoIterator<Item = (&'t BidderId, &'t Submission<PortfolioId, ProductId>)>,
39    {
40        let (products, ncosts) = super::prepare(auction);
41
42        if products.len() == 0 {
43            return AuctionOutcome::default();
44        }
45
46        // The trade and bid constraints are all (something) = 0, we need to
47        // know how many of these there are in order to handle the box
48        // constraints for each decision variable
49        let nzero = products.len() + ncosts;
50
51        // Our quadratic term is diagonal, so we build the matrix by defining its diagonal
52        let mut p = Vec::new();
53        // and these are the linear terms
54        let mut q = Vec::new();
55
56        // Clarabel handles constraints via a cone specification, e.g. Ax + s = b, where s is a cone.
57        // The first `nzero` of b and s are just =0, so we do that work upfront.
58        let mut b = vec![0.0; nzero];
59        let mut s = vec![ZeroConeT(nzero)];
60
61        // Clarabel's matrix input is in the form of CSC, so we handle the memory representation
62        // carefully.
63        let mut a_nzval = Vec::new();
64        let mut a_rowval = Vec::new();
65        let mut a_colptr = Vec::new();
66
67        // These will help us figure out the correct row index as we iterate.
68        let mut group_offset = products.len();
69
70        // We begin by setting up the portfolio variables
71        for (_, submission) in auction.into_iter() {
72            for (id, portfolio) in submission.portfolios.iter() {
73                // portfolio variables contribute nothing to the objective
74                p.push(0.0);
75                q.push(0.0);
76
77                // start a new column in the constraint matrix
78                a_colptr.push(a_nzval.len());
79
80                // We copy the portfolio definition into the matrix
81                for (product, &weight) in portfolio.iter() {
82                    a_nzval.push(weight);
83                    a_rowval.push(products.get_index_of(product).unwrap());
84                }
85
86                // Now we embed the weights from each group. This loop is a little wonky;
87                // in matrix terms, the representation is transposed in the wrong way.
88                // However, we expect the number of groups to be fairly small, so simply
89                // searching every group for every portfolio in the submission is not so bad.
90                for (offset, (group, _)) in submission.demand_curves.iter().enumerate() {
91                    if let Some(&weight) = group.get(id) {
92                        a_nzval.push(weight);
93                        a_rowval.push(group_offset + offset);
94                    }
95                }
96            }
97
98            group_offset += submission.demand_curves.len();
99        }
100
101        // Now we setup the segment variables
102        group_offset = products.len();
103        for (_, submission) in auction.into_iter() {
104            for (_, curve) in submission.demand_curves.iter() {
105                for segment in curve.iter() {
106                    let (m, pzero) = segment.slope_intercept();
107
108                    // Setup the contributions to the objective
109                    p.push(-m);
110                    q.push(-pzero);
111
112                    // Insert a new column
113                    a_colptr.push(a_nzval.len());
114
115                    // Ensure it counts towards the group
116                    a_nzval.push(-1.0);
117                    a_rowval.push(group_offset);
118
119                    // Setup the box constraints
120                    // x0 <= y <= x1 ==> -y + s == -x0 and y + s == x1
121                    if segment.q0.is_finite() {
122                        a_nzval.push(-1.0);
123                        a_rowval.push(b.len());
124                        b.push(-segment.q0);
125                    }
126                    if segment.q1.is_finite() {
127                        a_nzval.push(1.0);
128                        a_rowval.push(b.len());
129                        b.push(segment.q1);
130                    }
131                }
132
133                // Advance the group offset for the next bid/constraint
134                group_offset += 1;
135            }
136        }
137
138        // We need to polish off the CSC matrix
139        a_colptr.push(a_nzval.len());
140
141        let a_matrix = CscMatrix {
142            m: b.len(),
143            n: p.len(),
144            colptr: a_colptr,
145            rowval: a_rowval,
146            nzval: a_nzval,
147        };
148
149        assert!(a_matrix.check_format().is_ok()); // TODO: maybe remove this
150
151        // We also need to cleanup the cone specification
152        s.push(NonnegativeConeT(b.len() - nzero));
153
154        // Finally, we need to convert our p spec into a csc matrix
155        let p_matrix = {
156            let m = p.len();
157            let n = p.len();
158
159            CscMatrix {
160                m,
161                n,
162                colptr: (0..=n).collect(),
163                rowval: (0..n).collect(),
164                nzval: p,
165            }
166        };
167
168        // Now we can solve!
169        let mut solver = DefaultSolver::new(&p_matrix, &q, &a_matrix, &b, &s, self.0.clone());
170        solver.solve();
171        assert_eq!(solver.solution.status, SolverStatus::Solved);
172
173        // We get the raw optimization output
174
175        let mut product_outcomes: HashMap<ProductId, ProductOutcome> = products
176            .into_iter()
177            .zip(solver.solution.z.iter())
178            .map(|(product, &price)| (product, ProductOutcome { price, trade: 0.0 }))
179            .collect();
180
181        let mut trades = solver.solution.x.iter();
182
183        let submission_outcomes = auction
184            .into_iter()
185            .map(|(bidder_id, submission)| {
186                let outcome = submission
187                    .portfolios
188                    .iter()
189                    .map(|(id, portfolio)| {
190                        // Safe, because we necessarily have every portfolio represented in the solution
191                        let trade = *trades.next().unwrap();
192
193                        let mut price = 0.0;
194
195                        for (product_id, weight) in portfolio.iter() {
196                            // Safe, because product outcomes contains all referenced products
197                            let product_outcome = product_outcomes.get_mut(product_id).unwrap();
198
199                            // We report the trade (in an absolute sense), to be halved later
200                            product_outcome.trade += (weight * trade).abs();
201
202                            // We also compute the effective price of the portfolio
203                            price += weight * product_outcome.price;
204
205                            // TODO: consider special summation algorithms (i.e. Kahan) for the above dot products
206                        }
207
208                        (id.clone(), PortfolioOutcome { price, trade })
209                    })
210                    .collect::<HashMap<_, _>>();
211
212                (bidder_id.clone(), outcome)
213            })
214            .collect::<HashMap<_, _>>();
215
216        // We have double-counted the trade for each product, so we halve it
217        for outcome in product_outcomes.values_mut() {
218            outcome.trade *= 0.5;
219        }
220
221        // TODO:
222        // We have assigned the products prices straight from the solver
223        // (and computed the portfolio prices from those).
224        // Under pathological circumstances, the price may not be unique
225        // (either when there is no trade, or the supply exactly matches the demand).
226        // We should think about injecting an auxiliary solve for choosing a canonical
227        // price and/or for detecting when there is such a degeneracy.
228
229        // TODO:
230        // When there are "flat" demand curves, it is possible for nonuniqueness
231        // in the traded outcomes. The convex regularization is to minimize the L2 norm
232        // of the trades as a tie-break. We should think about the best way to regularize
233        // the solve accordingly.
234
235        AuctionOutcome {
236            submissions: submission_outcomes,
237            products: product_outcomes,
238        }
239    }
240}