filtration_domination/
mpfree.rs

1//! Interface with mpfree that allows to compute minimal presentations.
2use std::fs::File;
3use std::io::{BufRead, BufReader, BufWriter};
4use std::path::Path;
5use std::process::{Command, ExitStatus, Stdio};
6use std::time::Duration;
7use std::{fs, io};
8use thiserror::Error;
9
10use crate::chain_complex::ToFreeImplicitRepresentation;
11use crate::edges::{EdgeList, FilteredEdge};
12use crate::filtration::{build_flag_filtration_with_check, Filtration};
13use crate::simplicial_complex::MapSimplicialComplex;
14use crate::{CriticalGrade, Value};
15
16const TMP_DIRECTORY: &str = "tmp";
17
18/// The time taken to run mpfree, and the parsed output.
19#[derive(Debug, Clone)]
20pub struct MinimalPresentationComputationSummary {
21    pub timers: MinimalPresentationComputationTime,
22    pub output: ParsedMpfreeOutput,
23}
24
25/// Timers related to minimal presentation computation.
26#[derive(Debug, Clone, Copy, Default)]
27pub struct MinimalPresentationComputationTime {
28    pub build_filtration: Duration,
29    pub write_bifiltration: Duration,
30    pub mpfree: Duration,
31}
32
33/// Summaries of the minimal presentation computed by mpfree.
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub struct ParsedMpfreeOutput {
36    pub parameters: usize,
37    pub sizes: [usize; 3],
38}
39
40/// Compute a minimal presentation of the homology at the given dimension of the clique bifiltration
41/// of the given bifiltered edge list.
42///
43/// The `name` parameter is used to name and identify temporary files.
44pub fn compute_minimal_presentation<VF: Value, G: CriticalGrade>(
45    name: &str,
46    homology: usize,
47    edge_list: &EdgeList<FilteredEdge<G>>,
48) -> Result<MinimalPresentationComputationSummary, MpfreeError>
49where
50    Filtration<G, MapSimplicialComplex>: ToFreeImplicitRepresentation<VF, 2>,
51{
52    let result = compute_minimal_presentation_with_check::<
53        _,
54        _,
55        std::io::Error,
56        fn(usize) -> Result<(), io::Error>,
57    >(name, homology, edge_list, None);
58    match result {
59        Ok(summary) => Ok(summary),
60        Err(err) => match err {
61            CheckedMpfreeError::CheckFailed(_) => {
62                panic!("Programming error: we didn't specify a check.")
63            }
64            CheckedMpfreeError::Mpfree(err) => Err(err),
65        },
66    }
67}
68
69#[derive(Error, Debug)]
70pub enum CheckedMpfreeError<E> {
71    #[error(transparent)]
72    CheckFailed(E),
73
74    #[error(transparent)]
75    Mpfree(#[from] MpfreeError),
76}
77
78/// Compute a minimal presentation of the homology at the given dimension of the clique bifiltration
79/// of the given bifiltered edge list.
80///
81/// The `name` parameter is used to name and identify temporary files.
82pub fn compute_minimal_presentation_with_check<
83    VF: Value,
84    G: CriticalGrade,
85    E: std::error::Error,
86    F: Fn(usize) -> Result<(), E>,
87>(
88    name: &str,
89    homology: usize,
90    edge_list: &EdgeList<FilteredEdge<G>>,
91    memory_check_fn: Option<F>,
92) -> Result<MinimalPresentationComputationSummary, CheckedMpfreeError<E>>
93where
94    Filtration<G, MapSimplicialComplex>: ToFreeImplicitRepresentation<VF, 2>,
95{
96    let mut timers = MinimalPresentationComputationTime::default();
97
98    // Build filtration.
99    let start_filtration = std::time::Instant::now();
100    let filtration: Filtration<_, MapSimplicialComplex> = build_flag_filtration_with_check(
101        edge_list.n_vertices,
102        homology + 1,
103        edge_list.edge_iter().cloned(),
104        memory_check_fn,
105    )
106    .map_err(CheckedMpfreeError::CheckFailed)?;
107    timers.build_filtration = start_filtration.elapsed();
108
109    // Save filtration to disk.
110    let start_io = std::time::Instant::now();
111    let directory = Path::new(TMP_DIRECTORY);
112    fs::create_dir_all(&directory).map_err(MpfreeError::CreateTmpDirectory)?;
113    let filepath_mpfree_input = directory.join(format!("{}_scc2020", name));
114    let filepath_out = filepath_mpfree_input.with_extension("out");
115    write_bifiltration(&filepath_mpfree_input, homology, &filtration).map_err(MpfreeError::Io)?;
116    timers.write_bifiltration = start_io.elapsed();
117
118    // Compute minimal presentation.
119    let start_mpfree = std::time::Instant::now();
120    let output = run_mpfree(filepath_mpfree_input, filepath_out)?;
121    timers.mpfree = start_mpfree.elapsed();
122
123    Ok(MinimalPresentationComputationSummary { timers, output })
124}
125
126fn write_bifiltration<
127    VF: Value,
128    F: ToFreeImplicitRepresentation<VF, N>,
129    P: AsRef<Path>,
130    const N: usize,
131>(
132    filepath: P,
133    homology: usize,
134    f: &F,
135) -> io::Result<()> {
136    let file = std::fs::File::create(&filepath)?;
137    let mut writer = BufWriter::new(&file);
138    f.write_scc2020(homology, &mut writer)?;
139    Ok(())
140}
141
142/// A error that happened when executing mpfree.
143#[derive(Error, Debug)]
144pub enum MpfreeError {
145    #[error("Mpfree ended with a non-okay exit code: {0}")]
146    ExitStatus(ExitStatus),
147
148    #[error("Error parsing output header")]
149    BadOutputHeader,
150
151    #[error("Error parsing Betti numbers")]
152    ParsingBettiNumbers,
153
154    #[error("Spawning mpfree process. Is mpfree installed?")]
155    SpawnMpfree(#[source] io::Error),
156
157    #[error("Creating tmp directory")]
158    CreateTmpDirectory(#[source] io::Error),
159
160    #[error("Processing mpfree output file")]
161    OutputFile(#[source] io::Error),
162
163    #[error("A unknown IO error happened")]
164    Io(#[from] io::Error),
165
166    #[error("Error parsing number: {0}")]
167    WrongNumberFormat(#[from] std::num::ParseIntError),
168}
169
170pub fn run_mpfree<P: AsRef<Path>>(
171    filepath_in: P,
172    filepath_out: P,
173) -> Result<ParsedMpfreeOutput, MpfreeError> {
174    let mut child = Command::new("mpfree")
175        .args([
176            filepath_in.as_ref().as_os_str(),
177            filepath_out.as_ref().as_os_str(),
178        ])
179        .stdout(Stdio::null())
180        .spawn()
181        .map_err(MpfreeError::SpawnMpfree)?;
182    let exit_code = child.wait()?;
183    if !exit_code.success() {
184        return Err(MpfreeError::ExitStatus(exit_code));
185    }
186
187    let output_file = File::open(filepath_out.as_ref()).map_err(MpfreeError::OutputFile)?;
188    let mut child_stdout = BufReader::new(output_file);
189    let mut buffer = String::new();
190    child_stdout.read_line(&mut buffer)?;
191    if buffer != "scc2020\n" {
192        return Err(MpfreeError::BadOutputHeader);
193    }
194    buffer.clear();
195    child_stdout.read_line(&mut buffer)?;
196    let parameters: usize = buffer.trim().parse()?;
197    buffer.clear();
198    child_stdout.read_line(&mut buffer)?;
199    let mut sizes_raw = buffer.split_whitespace();
200    let mut sizes: [usize; 3] = [0, 0, 0];
201    for s in sizes.iter_mut() {
202        *s = sizes_raw
203            .next()
204            .ok_or(MpfreeError::ParsingBettiNumbers)?
205            .parse()?;
206    }
207
208    Ok(ParsedMpfreeOutput { parameters, sizes })
209}