fts_solver/
export.rs

1use crate::{HashSet, Submission};
2use std::fmt::Display;
3use std::hash::Hash;
4use std::io::Write;
5
6/// Convert a set of flow trading submissions to a quadratic program and export
7/// this program to `.mps` format.
8pub fn export_mps<
9    T,
10    BidderId: Display + Eq + Hash + Clone + Ord,
11    PortfolioId: Display + Eq + Hash + Clone + Ord,
12    ProductId: Display + Eq + Hash + Clone + Ord,
13>(
14    auction: &T,
15    buffer: &mut impl Write,
16) -> Result<(), std::io::Error>
17where
18    for<'t> &'t T: IntoIterator<Item = (&'t BidderId, &'t Submission<PortfolioId, ProductId>)>,
19{
20    // MPS is a somewhat archaic format, but is easy enough to generate.
21    // https://www.ibm.com/docs/en/icos/22.1.2?topic=standard-records-in-mps-format
22    // is a good reference.
23
24    writeln!(buffer, "NAME flow_trade_qp")?;
25    writeln!(buffer, "ROWS")?;
26
27    // Our objective is gains-from-trade ("gft")
28    writeln!(buffer, " N    gft")?;
29
30    // We will have one row per product, set equal to zero, which will give us the shadow prices
31    let mut products = auction
32        .into_iter()
33        .flat_map(|(_, submission)| submission.portfolios.values())
34        .flat_map(|portfolio| portfolio.keys())
35        .collect::<HashSet<_>>();
36    products.sort_unstable();
37    for product_id in products.iter() {
38        // The product dual variables are named `p_{product_id}`
39        writeln!(buffer, " E    p_{product_id}")?;
40    }
41
42    // We will also have one row per demand curve, set equal to zero.
43    for (bidder_id, submission) in auction.into_iter() {
44        // The group dual variables are named `g_{bidder_id}_{offset}`
45        for (offset, _) in submission.demand_curves.iter().enumerate() {
46            writeln!(buffer, " E    g_{bidder_id}_{offset}")?;
47        }
48    }
49
50    // We have two sets of variables: the per-product allocations `x`, and
51    // the demand curve segment fills `y`, related such that Ax - Σy = 0,
52    // where the rows of Σ are disjoint 1 vectors.
53    writeln!(buffer, "COLUMNS")?;
54
55    // We begin with this first set x. Notably, these variables *do not* appear in the objective.
56    for (bidder_id, submission) in auction.into_iter() {
57        for (portfolio_id, portfolio) in submission.portfolios.iter() {
58            for (product_id, weight) in portfolio.iter() {
59                writeln!(
60                    buffer,
61                    "    x_{bidder_id}_{portfolio_id}    p_{product_id}    {weight}",
62                )?;
63            }
64
65            for (offset, (group, _)) in submission.demand_curves.iter().enumerate() {
66                if let Some(weight) = group.get(portfolio_id) {
67                    writeln!(
68                        buffer,
69                        "    x_{bidder_id}_{portfolio_id}    g_{bidder_id}_{offset}    {weight}",
70                    )?;
71                }
72            }
73        }
74    }
75
76    // Now the second set y.
77    for (bidder_id, submission) in auction.into_iter() {
78        for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
79            for (idx, segment) in curve.iter().enumerate() {
80                // MPS defaults to minimization. Further, the quadratic terms are specified in a
81                // well-supported extension, so we only do the linear terms here.
82                let (_, b) = segment.slope_intercept();
83                writeln!(
84                    buffer,
85                    "    y_{bidder_id}_{offset}_{idx}    gft    {term}    g_{bidder_id}_{offset}    -1",
86                    term = -b
87                )?;
88            }
89        }
90    }
91
92    // Now we specify the domains for each variable.
93    writeln!(buffer, "BOUNDS")?;
94    for (bidder_id, submission) in auction.into_iter() {
95        for portfolio_id in submission.portfolios.keys() {
96            // The allocation variables are unconstrained.
97            writeln!(buffer, " FR BND x_{bidder_id}_{portfolio_id}")?;
98        }
99    }
100    for (bidder_id, submission) in auction.into_iter() {
101        for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
102            for (idx, segment) in curve.iter().enumerate() {
103                // The segment variables are bounded above and below (unless infinite)
104                if segment.q0.is_finite() {
105                    writeln!(
106                        buffer,
107                        " LO BND    y_{bidder_id}_{offset}_{idx}    {min}",
108                        min = segment.q0,
109                    )?;
110                } else {
111                    writeln!(buffer, " MI BND    y_{bidder_id}_{offset}_{idx}",)?;
112                }
113                if segment.q1.is_finite() {
114                    writeln!(
115                        buffer,
116                        " UP BND    y_{bidder_id}_{offset}_{idx}    {max}",
117                        max = segment.q1,
118                    )?;
119                } else {
120                    writeln!(buffer, " PL BND    y_{bidder_id}_{offset}_{idx}",)?;
121                }
122            }
123        }
124    }
125
126    // Finally, we leverage the quadratic extension
127    writeln!(buffer, "QUADOBJ")?;
128    for (bidder_id, submission) in auction.into_iter() {
129        for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
130            for (idx, segment) in curve.iter().enumerate() {
131                let (m, _) = segment.slope_intercept();
132                writeln!(
133                    buffer,
134                    "    y_{bidder_id}_{offset}_{idx}    y_{bidder_id}_{offset}_{idx}    {term}",
135                    term = -m
136                )?;
137            }
138        }
139    }
140
141    writeln!(buffer, "ENDATA")?;
142    Ok(())
143}
144
145/// Convert a set of flow trading submissions to a quadratic program and export
146/// this program to `.lp` format.
147pub fn export_lp<
148    T,
149    BidderId: Display + Eq + Hash + Clone + Ord,
150    PortfolioId: Display + Eq + Hash + Clone + Ord,
151    ProductId: Display + Eq + Hash + Clone + Ord,
152>(
153    auction: &T,
154    buffer: &mut impl Write,
155) -> Result<(), std::io::Error>
156where
157    for<'t> &'t T: IntoIterator<Item = (&'t BidderId, &'t Submission<PortfolioId, ProductId>)>,
158{
159    // Start with the objective section - maximize gains from trade
160    writeln!(buffer, "Maximize")?;
161
162    // Start the objective function (gft)
163    write!(buffer, "  gft: ")?;
164
165    // Flag to track if we've written any terms yet
166    let mut first_term = true;
167    let mut has_quadratic_terms = false;
168    // Add linear terms from the y variables
169    for (bidder_id, submission) in auction.into_iter() {
170        for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
171            for (idx, segment) in curve.iter().enumerate() {
172                let (m, b) = segment.slope_intercept();
173                has_quadratic_terms = has_quadratic_terms || m != 0.0;
174                if first_term {
175                    write!(buffer, "{b} y_{bidder_id}_{offset}_{idx}")?;
176                    first_term = false;
177                } else {
178                    write!(buffer, " + {b} y_{bidder_id}_{offset}_{idx}")?;
179                }
180            }
181        }
182    }
183
184    // If no linear terms were written, add a 0
185    if first_term {
186        write!(buffer, "0")?;
187    }
188
189    if has_quadratic_terms {
190        write!(buffer, " + [ ")?;
191
192        // Reset first_term flag for quadratic terms
193        first_term = true;
194
195        for (bidder_id, submission) in auction.into_iter() {
196            for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
197                for (idx, segment) in curve.iter().enumerate() {
198                    let (m, _) = segment.slope_intercept();
199                    if first_term {
200                        write!(buffer, "{m} y_{bidder_id}_{offset}_{idx} ^ 2")?;
201                        first_term = false;
202                    } else {
203                        write!(buffer, " + {m} y_{bidder_id}_{offset}_{idx} ^ 2",)?;
204                    }
205                }
206            }
207        }
208
209        write!(buffer, " ] / 2")?;
210    }
211
212    // End the objective line
213    writeln!(buffer)?;
214
215    // Constraints section
216    writeln!(buffer, "Subject To")?;
217
218    // Product constraints (all products must sum to zero)
219    let mut products = auction
220        .into_iter()
221        .flat_map(|(_, submission)| submission.portfolios.values())
222        .flat_map(|portfolio| portfolio.keys())
223        .collect::<HashSet<_>>();
224    products.sort_unstable();
225
226    for product_id in products {
227        // Start the constraint
228        write!(buffer, "  p_{product_id}: ")?;
229
230        // Flag to track if we've written any terms
231        let mut first_term = true;
232
233        // Collect all terms related to this product
234        for (bidder_id, submission) in auction.into_iter() {
235            for (portfolio_id, portfolio) in submission.portfolios.iter() {
236                if let Some(&weight) = portfolio.get(product_id) {
237                    if first_term {
238                        write!(buffer, "{weight} x_{bidder_id}_{portfolio_id}")?;
239                        first_term = false;
240                    } else {
241                        write!(buffer, " + {weight} x_{bidder_id}_{portfolio_id}",)?;
242                    }
243                }
244            }
245        }
246
247        // If no terms were written, add a 0
248        if first_term {
249            write!(buffer, "0")?;
250        }
251
252        // Finish the constraint: = 0
253        writeln!(buffer, " = 0")?;
254    }
255
256    // Demand curve constraints
257    for (bidder_id, submission) in auction.into_iter() {
258        for (offset, (group, curve)) in submission.demand_curves.iter().enumerate() {
259            // Start the constraint
260            write!(buffer, "  g_{bidder_id}_{offset}: ")?;
261
262            // Flag to track if we've written any terms
263            let mut first_term = true;
264
265            // Add terms for the x variables
266            for (portfolio_id, &weight) in group.iter() {
267                if first_term {
268                    write!(buffer, "{weight} x_{bidder_id}_{portfolio_id}")?;
269                    first_term = false;
270                } else {
271                    write!(buffer, " + {weight} x_{bidder_id}_{portfolio_id}",)?;
272                }
273            }
274
275            // Assertion: first_term = false, since groups are non empty
276
277            // Add terms for the y variables (with negative coefficients)
278            for (idx, _) in curve.iter().enumerate() {
279                write!(buffer, " - y_{bidder_id}_{offset}_{idx}")?;
280            }
281
282            // Finish the constraint: = 0
283            writeln!(buffer, " = 0")?;
284        }
285    }
286
287    // Bounds section
288    writeln!(buffer, "Bounds")?;
289
290    // The x variables are unconstrained (free)
291    for (bidder_id, submission) in auction.into_iter() {
292        for portfolio_id in submission.portfolios.keys() {
293            writeln!(buffer, "  x_{bidder_id}_{portfolio_id} free")?;
294        }
295    }
296
297    // The y variables have specific bounds
298    for (bidder_id, submission) in auction.into_iter() {
299        for (offset, (_, curve)) in submission.demand_curves.iter().enumerate() {
300            for (idx, segment) in curve.iter().enumerate() {
301                match (segment.q0.is_finite(), segment.q1.is_finite()) {
302                    (true, true) => {
303                        writeln!(
304                            buffer,
305                            "  {min} <= y_{bidder_id}_{offset}_{idx} <= {max}",
306                            min = segment.q0,
307                            max = segment.q1
308                        )?;
309                    }
310                    (true, false) => {
311                        writeln!(
312                            buffer,
313                            "  y_{bidder_id}_{offset}_{idx} >= {min}",
314                            min = segment.q0
315                        )?;
316                    }
317                    (false, true) => {
318                        writeln!(
319                            buffer,
320                            "  y_{bidder_id}_{offset}_{idx} <= {max}",
321                            max = segment.q1
322                        )?;
323                    }
324                    (false, false) => {
325                        writeln!(buffer, "  y_{bidder_id}_{offset}_{idx} free")?;
326                    }
327                }
328            }
329        }
330    }
331
332    // End the LP file
333    writeln!(buffer, "End")?;
334
335    Ok(())
336}