flag_algebra/tools/
sdpa_problem.rs

1use std::fs::File;
2use std::io;
3use std::io::{BufRead, BufReader, BufWriter, Write};
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
6use std::result::Result;
7
8use log::*;
9
10use crate::sdpa::{Error, SdpaCoeff};
11
12const CS_COST: f64 = 100.;
13const INEQ_COST: f64 = 1.;
14
15pub const CERTIFICATE_FILE: &str = "certificate";
16pub const CERTIFICATE_MINIMIZE_FILE: &str = "certificate_minimize";
17
18// SDPA format for problems
19// 1. dimension ( =b.len() )
20// 2. n_blocks ( =block_sizes.len() )
21// 3. block_sizes of length nblock
22// 4. b of length dim
23// 5+. list of coefficients
24#[derive(Debug, Clone)]
25pub struct SdpaProblem {
26    block_sizes: Vec<i32>,
27    b: Vec<f64>,
28    coeffs: Vec<SdpaCoeff>,
29}
30
31impl SdpaProblem {
32    pub fn write(&self, filename: &str) -> Result<(), io::Error> {
33        let mut w = BufWriter::new(File::create(filename)?);
34        writeln!(w, "{}", self.b.len())?;
35        writeln!(w, "{}", self.block_sizes.len())?;
36        for i in &self.block_sizes {
37            write!(w, "{i} ")?;
38        }
39        writeln!(w)?;
40        for x in &self.b {
41            write!(w, "{x} ")?;
42        }
43        writeln!(w)?;
44        for coeff in &self.coeffs {
45            writeln!(w, "{coeff} ")?;
46        }
47        Ok(())
48    }
49    pub fn load(filename: &str) -> Result<Self, Error> {
50        let mut filename = PathBuf::from(filename);
51        let _ = filename.set_extension("sdpa");
52        let buf = BufReader::new(File::open(filename)?);
53        let mut lines = buf.lines().map(|line| line.unwrap()).filter(|line| {
54            let l = line.trim_start();
55            !l.starts_with('*') && !l.is_empty()
56        });
57        let dim: usize = lines.next().unwrap().parse().unwrap();
58        let nblock: usize = lines.next().unwrap().parse()?;
59        let block_sizes: Vec<i32> = lines
60            .next()
61            .unwrap()
62            .split_whitespace()
63            .map(|x| x.parse().unwrap())
64            .collect();
65        assert_eq!(block_sizes.len(), nblock);
66        let b: Vec<f64> = lines
67            .next()
68            .unwrap()
69            .split_whitespace()
70            .map(|x| x.parse().unwrap())
71            .collect();
72        assert_eq!(b.len(), dim);
73        let mut coeffs: Vec<SdpaCoeff> = Vec::new();
74        for line in lines {
75            coeffs.push(line.parse().expect(&line))
76        }
77        Ok(SdpaProblem {
78            block_sizes,
79            b,
80            coeffs,
81        })
82    }
83    // Write the problem of minimizing the cetificate, under the constrint obj=target_value
84    pub fn to_certificate_minimization(mut self, target_value: f64) -> Self {
85        self.b.push(target_value);
86        let new_mat = self.b.len();
87        // The old objective becomes the new matrix
88        for coeff in &mut self.coeffs {
89            if coeff.mat == 0 {
90                coeff.mat = new_mat
91            }
92        }
93        // The new objective is the weight of the trace
94        push_identities(&mut self.coeffs, 0, &self.block_sizes, -INEQ_COST, -CS_COST);
95        self
96    }
97}
98
99// SDPA format for certificates (as given by csdp)
100// 1. vector y
101// 2+. list of coefficients for Z and X
102// matrix 1: Z, matrix 2: X
103#[derive(Debug, Clone)]
104pub struct SdpaCertificate {
105    pub y: Vec<f64>,
106    pub coeffs: Vec<SdpaCoeff>,
107}
108
109impl SdpaCertificate {
110    pub fn write(&self, filename: &str) -> Result<(), io::Error> {
111        let filename = PathBuf::from(filename);
112        //let _ = filename.set_extension("cert.sdpa");
113        let mut w = BufWriter::new(File::create(filename)?);
114        for v in &self.y {
115            write!(w, "{v} ")?;
116        }
117        writeln!(w)?;
118        for coeff in &self.coeffs {
119            writeln!(w, "{coeff} ")?;
120        }
121        Ok(())
122    }
123    pub fn load(filename: &str) -> Result<Self, io::Error> {
124        let buf = BufReader::new(File::open(filename)?);
125        let mut lines = buf
126            .lines()
127            .map(|line| line.unwrap())
128            .filter(|line| !line.trim_start().is_empty());
129        let y: Vec<f64> = lines
130            .next()
131            .unwrap()
132            .split_whitespace()
133            .map(|x| x.parse().unwrap())
134            .collect();
135        let mut coeffs: Vec<SdpaCoeff> = Vec::new();
136        for line in lines {
137            coeffs.push(line.parse().expect(&line))
138        }
139        Ok(SdpaCertificate { y, coeffs })
140    }
141    pub fn to_certificate_minimization(mut self, problem: &SdpaProblem) -> Self {
142        // Add a dimension to y and set y to 0
143        self.y.push(-1.);
144        //      self.y.push(0.);
145        //     for v in &mut self.y {
146        //         *v = 0.
147        //     }
148        // Discard Z
149        //        self.coeffs.retain(|coeff|{ coeff.mat == 2 });
150        // Write a new Z as -C from the problem
151        for coeff in &problem.coeffs {
152            if coeff.mat == 0 {
153                let new_coeff = SdpaCoeff {
154                    mat: 1,
155                    val: -coeff.val,
156                    ..*coeff
157                };
158                self.coeffs.push(new_coeff)
159            }
160        }
161        sum_duplicates(&mut self.coeffs);
162        self
163    }
164    // Discard the dual values as they are now invalid
165    pub fn from_certificate_minimization(mut self) -> Self {
166        // Remove the extra dimension of y and set y to 0
167        let _ = self.y.pop().unwrap();
168        self.y.push(0.);
169        for v in &mut self.y {
170            *v = 0.
171        }
172        // Discard Z
173        self.coeffs.retain(|coeff| coeff.mat == 2);
174        self
175    }
176}
177
178/// Run csdp and parse its output
179pub fn csdp(filename: &str, initial_solution: Option<&str>) -> Result<f64, Error> {
180    let mut command = Command::new("csdp");
181    let _ = command.arg(filename).arg(CERTIFICATE_FILE);
182    if let Some(sol) = initial_solution {
183        let _ = command.arg(sol);
184    };
185    info!("Calling CSDP");
186    debug!("command: {:?}", command);
187    let mut child = command
188        .stdout(Stdio::piped())
189        .spawn()
190        .expect("Failed to call csdp");
191    let output = BufReader::new(child.stdout.take().unwrap());
192    //    let output = child.wait_with_output().unwrap();
193    //let output = command.output().expect("Failed to call csdp");
194    //    eprint!("{}", String::from_utf8(output.stderr).unwrap());
195    use std::time::{Duration, Instant};
196    let time_start = Instant::now();
197    let mut lines = output.lines();
198    let mut stream = false;
199    let time_before_stream = Duration::from_secs(2);
200    while let Some(line_result) = lines.next() {
201        let line = line_result.unwrap();
202        if line.starts_with("CSDP") {
203            continue;
204        };
205        if line.starts_with("Iter") {
206            if !stream && time_start.elapsed() > time_before_stream {
207                stream = true;
208                info!(
209                    "csdp is taking more than {}s, start streaming output",
210                    time_before_stream.as_secs_f32()
211                )
212            }
213            if stream {
214                info!("{}", line)
215            } else {
216                debug!("{}", line)
217            }
218        } else {
219            let code = child
220                .wait()
221                .expect("csdp wasn't running")
222                .code()
223                .expect("No exit code");
224            if code == 0 {
225                let value: f64 = lines
226                    .next()
227                    .unwrap()
228                    .unwrap()
229                    .split_whitespace()
230                    .nth(3)
231                    .unwrap()
232                    .parse()
233                    .unwrap();
234                info!("{} with primal value {}", line, value);
235                return Ok(value);
236            } else {
237                info!("{}", line);
238                assert!(code <= 10, "{command:?} aborted with code {code}");
239                return Err(Error::SdpNotSolved(code));
240            }
241        }
242    }
243    panic!("CSDP output incorrectly parsed")
244}
245
246pub fn csdp_minimize_certificate(
247    filename: &str,
248    initial_solution: Option<&str>,
249) -> Result<f64, Error> {
250    let val = csdp(filename, initial_solution)?;
251    let problem = SdpaProblem::load(filename)?.to_certificate_minimization(val);
252    let cert = SdpaCertificate::load(CERTIFICATE_FILE)?.to_certificate_minimization(&problem);
253    let filename_minimize = format!("{filename}.minimize");
254    problem.write(&filename_minimize)?;
255    cert.write(CERTIFICATE_MINIMIZE_FILE)?;
256    info!("Try to minimize certificate");
257    match csdp(&filename_minimize, Some(CERTIFICATE_MINIMIZE_FILE)) {
258        Ok(_) => {
259            info!("Certificate minimized");
260            SdpaCertificate::load(CERTIFICATE_MINIMIZE_FILE)?
261                .from_certificate_minimization()
262                .write(CERTIFICATE_FILE)?;
263        }
264        Err(_) => {
265            warn!("Cannot minimize certificate");
266        }
267    }
268    Ok(val)
269}
270
271impl SdpaCoeff {
272    fn indices(&self) -> (usize, usize, usize, usize) {
273        (self.mat, self.block, self.i, self.j)
274    }
275}
276
277fn sum_duplicates(coeffs: &mut Vec<SdpaCoeff>) {
278    if coeffs.len() >= 2 {
279        coeffs.sort_by_key(SdpaCoeff::indices);
280        let mut write = 0;
281        for read in 1..coeffs.len() {
282            if coeffs[read].indices() == coeffs[write].indices() {
283                coeffs[write].val += coeffs[read].val
284            } else {
285                write += 1;
286                coeffs.swap(write, read);
287            }
288        }
289        coeffs.truncate(write + 1)
290    }
291}
292
293fn push_identities(
294    coeffs: &mut Vec<SdpaCoeff>,
295    matrix_number: usize,
296    block_sizes: &[i32],
297    scale_diag: f64,
298    scale_nondiag: f64,
299) {
300    for (block, &blocksize) in block_sizes.iter().enumerate() {
301        let val = if blocksize < 0 {
302            scale_diag
303        } else {
304            scale_nondiag
305        };
306        for i in 0..blocksize.unsigned_abs() as usize {
307            coeffs.push(SdpaCoeff {
308                mat: matrix_number,
309                block: block + 1,
310                i: i + 1,
311                j: i + 1,
312                val,
313            })
314        }
315    }
316}