1use 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#[derive(Debug, Clone)]
20pub struct MinimalPresentationComputationSummary {
21 pub timers: MinimalPresentationComputationTime,
22 pub output: ParsedMpfreeOutput,
23}
24
25#[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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub struct ParsedMpfreeOutput {
36 pub parameters: usize,
37 pub sizes: [usize; 3],
38}
39
40pub 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
78pub 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 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 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 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#[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}