tididi_cnf/output.rs
1//! What the run answers, and what the command line prints of it.
2//!
3//! Each mode's runner returns what it answered as a typed value — a model
4//! count with its attribution, a weighted count, a projected count with the
5//! fields its stages measured, the compiled diagram — and the driver records
6//! it once, here: the verdict and the count, and the run's record, [`Stats`]:
7//! sizes, timings, which stage answered, the preprocessing and SAT-pruning
8//! tallies, how the run ended. `driver::solve` hands both back as an
9//! [`Answer`], which is what [`crate::count`] returns. `eprintln!` diagnostics
10//! stay with the stage that emits them.
11//!
12//! The command line's half is [`finish`]: stdout carries the answer — the MCC
13//! answer lines under `--output-format mcc`, and otherwise a plainly worded
14//! count and, with `--save-tdd`, the line naming the written file — and
15//! `--stats` writes the record as one JSON object, to stdout after the answer
16//! when its path is `-`.
17//!
18//! `docs/stats.md` is the record's field table.
19
20use std::collections::BTreeMap;
21use std::path::{Path, PathBuf};
22
23use serde::Serialize;
24
25use crate::cli::{Cli, OutputFormat};
26use crate::config::{Config, Mode};
27use crate::problem::WeightedCount;
28use crate::run::{Answer, Count, Failure};
29use crate::Error;
30
31/// Which stage of a model count or a plain compile produced the answer: the
32/// record's `answered_by`, and the word the stage's stderr lines carry.
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub(crate) enum AnsweredBy {
35 /// The compile answered: on the cascade, the first compile, of the Arjun
36 /// reduction when one was kept; on the streaming route, its one compile;
37 /// in the plain compile, the compile on the frontend's vtree.
38 Compile,
39 /// Preprocessing settled the count, so nothing was compiled: the formula
40 /// was refuted, or no variable was left and the count is the lift.
41 Preprocessing,
42 /// The model count's second compile of the Arjun reduction, on a
43 /// `goatd-primal` tree, after one step of the first compile ran past its
44 /// own wall.
45 StallRecompile,
46 /// Support enumeration, the fallback that counts the Arjun-reduced formula
47 /// by enumerating assignments to its independent support.
48 SupportEnumeration,
49 /// The fallback that re-reduces without SBVA, then compiles again.
50 NoSbvaRetry,
51 /// The fallback that compiles the formula Arjun was given, unreduced.
52 UnreducedRetry,
53 /// The fallback that asks Arjun for the reduction it skipped on a
54 /// monotone formula, then compiles the reduction.
55 MonotoneReduction,
56 /// Shannon split assembled the count from cofactors.
57 ShannonSplit,
58 /// The plain compile's second compile, on the hypergraph bisection, after
59 /// the compile on the portfolio's vtree ran out of memory.
60 BisectRetry,
61}
62
63impl AnsweredBy {
64 /// The token recorded in `answered_by`. Treat these as a wire format:
65 /// programs that read the record match on them.
66 pub(crate) fn token(self) -> &'static str {
67 match self {
68 AnsweredBy::Compile => "compile",
69 AnsweredBy::Preprocessing => "preprocessing",
70 AnsweredBy::StallRecompile => "stall-recompile",
71 AnsweredBy::SupportEnumeration => "support-enumeration",
72 AnsweredBy::NoSbvaRetry => "no-sbva-retry",
73 AnsweredBy::UnreducedRetry => "unreduced-retry",
74 AnsweredBy::MonotoneReduction => "monotone-reduction",
75 AnsweredBy::ShannonSplit => "shannon-split",
76 AnsweredBy::BisectRetry => "bisect-retry",
77 }
78 }
79}
80
81/// What became of Arjun's reduction on a run that ran Arjun: the record's
82/// `reduction`, absent when Arjun did not run.
83#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub(crate) enum Reduction {
85 /// Arjun's reduction was kept, and the count went through it.
86 Used,
87 /// Arjun ran and no reduction was kept — it gave up inside its budget, or
88 /// its result was discarded — so the count went through the formula it was
89 /// given.
90 Unavailable,
91}
92
93impl Reduction {
94 /// The token recorded in `reduction`.
95 pub(crate) fn token(self) -> &'static str {
96 match self {
97 Reduction::Used => "used",
98 Reduction::Unavailable => "unavailable",
99 }
100 }
101}
102
103// ── The record ───────────────────────────────────────────────────────────────
104
105/// One run, as a harness or a researcher reads it back: the record the
106/// binary's `--stats` writes, as a value.
107///
108/// Field order here is the order the JSON object carries, and the names are a
109/// wire format, the keys of that object. A field is `None` when the run has
110/// nothing to say about it — no nulls in the JSON, and never a `0` standing for
111/// "not measured". Counts are decimal strings because they exceed 64 bits;
112/// everything else is a number, a boolean or a token. Serialize it with
113/// `serde_json` to get the object the binary writes.
114///
115/// The [record reference](https://github.com/Tractables/tididi-cnf/blob/main/docs/stats.md)
116/// tabulates the fields and which modes carry each.
117#[derive(Clone, Debug, Default, Serialize)]
118#[non_exhaustive]
119pub struct Stats {
120 /// `mc`, `wmc`, `pmc`, `pwmc`, or `compile` for a plain compile: the
121 /// [`Mode`] the run resolved to.
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub mode: Option<&'static str>,
124 /// How the run ended: `solved`, or the class of the error that stopped it
125 /// (`usage`, `environment`, `stage`, `vtree-spec`, `input`, `artifact`,
126 /// `incomplete`).
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub exit_reason: Option<&'static str>,
129 /// Variables in the parsed formula.
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub variables: Option<u32>,
132 /// Clauses in the parsed formula.
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub clauses: Option<u64>,
135 /// The vtree construction the run compiles on: the binary's `--vtree`, or
136 /// `portfolio`. `None` when the input is a vitri bundle, which brings its
137 /// vtree.
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub vtree_spec: Option<String>,
140 /// The portfolio candidate that won selection, when a portfolio ran. In
141 /// the plain compile it is the vtree the answering compile ran on: the
142 /// portfolio's winner, or the retry's hypergraph bisection. In the model
143 /// count the primary compile and the stall recompile name theirs as they
144 /// start, so a run that gives up names the last of them.
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub vtree_candidate: Option<String>,
147
148 /// Whether the formula has a model, as the run found it.
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub satisfiable: Option<bool>,
151 /// The integer count, as a decimal string.
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub model_count: Option<String>,
154 /// The weighted count, spelled as the answer line spells it: an exact
155 /// rational `<n>/<d>` in the exact domain, `<mantissa>e<exponent>` in the
156 /// bounded log domain.
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub weighted_count: Option<String>,
159 /// log10 of the weighted count's magnitude, in the log domain only, and
160 /// only when it is finite — a zero count has no logarithm and says so
161 /// through `weighted_count_sign`.
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub weighted_count_log10: Option<f64>,
164 /// Sign of the weighted count in the log domain: `-1`, `0` or `1`.
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub weighted_count_sign: Option<i8>,
167
168 /// Nodes in the compiled diagram. Only the plain compile path materializes
169 /// one; the counting paths stream and never build it.
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub tdd_nodes: Option<u64>,
172 /// Node-pairs in the compiled diagram.
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub tdd_pairs: Option<u64>,
175 /// Widest vtree level of the compiled diagram.
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub tdd_width: Option<u64>,
178 /// Largest diagram the compile held at once, in node-pairs.
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub peak_pairs: Option<u64>,
181
182 /// Which stage produced the answer: of a model count, `compile`,
183 /// `preprocessing`, `stall-recompile`, `support-enumeration`,
184 /// `no-sbva-retry`, `unreduced-retry`, `monotone-reduction` or
185 /// `shannon-split`; of a plain compile, `compile`, `preprocessing` or
186 /// `bisect-retry`.
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub answered_by: Option<&'static str>,
189 /// What became of Arjun's reduction on a model count: `used`, or
190 /// `unavailable` when Arjun ran and no reduction was kept. `None` when
191 /// Arjun did not run.
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub reduction: Option<&'static str>,
194
195 /// Wall-clock time in preprocessing, in milliseconds. On the model
196 /// count's cascade it is the simplify chain alone, since Arjun has its
197 /// own key.
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub preprocess_ms: Option<u64>,
200 /// Wall-clock time in Arjun, in milliseconds.
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub arjun_ms: Option<u64>,
203 /// Wall-clock time building vtrees, over the whole run, in milliseconds.
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub vtree_ms: Option<u64>,
206 /// Wall-clock time compiling, in milliseconds, less every phase with its
207 /// own key: preprocessing, Arjun and vtree construction.
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub compile_ms: Option<u64>,
210 /// Wall-clock time in the model count's cascade on a kept reduction, in
211 /// milliseconds: the first compile, then any fallbacks and Shannon split,
212 /// vtree construction inside it included.
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub cascade_ms: Option<u64>,
215 /// Wall-clock time in backbone detection, in milliseconds.
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub backbone_ms: Option<u64>,
218 /// Backbone literals found.
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub backbone_found: Option<u64>,
221 /// SAT probes backbone detection spent.
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub backbone_probes: Option<u64>,
224 /// Variables in the show set of the formula the projected count was
225 /// answered on, after preprocessing; `None` without a show set.
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub show_vars: Option<u64>,
228
229 /// `off` when `TIDIDI_SAT_PRUNE=off` switched the SAT-pruning pass off for
230 /// the run, a diagnostic for paired on/off measurements. Absent when the
231 /// pass is on, the default.
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub sat_prune: Option<&'static str>,
234 /// Why the SAT-pruning pass stood down, when it did.
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub sat_prune_disabled_by: Option<&'static str>,
237 /// The SAT-pruning tallies, keyed `sat_prune_<tally>`, each present only when
238 /// it is nonzero. Times end `_us` and are microseconds, the unit the pass
239 /// accumulates them in. The JSON object carries them as top-level keys.
240 #[serde(flatten)]
241 pub sat_prune_tallies: BTreeMap<&'static str, u64>,
242}
243
244/// What a mode answered: whether the formula has a model, its count, and in
245/// the compile mode the diagram the count was read off.
246pub(crate) struct Verdict {
247 pub(crate) satisfiable: bool,
248 pub(crate) count: Count,
249 pub(crate) diagram: Option<tididi::Tdd>,
250}
251
252/// Open the run's record: the facts fixed before the mode runs.
253///
254/// Called once, from the driver, at the point the mode and the vtree
255/// construction are both resolved — which is the last point every mode passes
256/// through. A run refused before then (bad argv, an instance the build will not
257/// answer) has no record; its stderr diagnostic is the whole story.
258pub(crate) fn open_record(config: &Config, formula: &vitri::cnf::CnfFormula) -> Stats {
259 Stats {
260 mode: Some(config.mode.token()),
261 variables: Some(formula.num_vars()),
262 clauses: Some(formula.clauses().len() as u64),
263 // A bundle's run builds no vtree for its main compile.
264 vtree_spec: config.input_vtree.is_none().then(|| config.vtree_spec().to_owned()),
265 sat_prune: (!config.sat_prune).then_some("off"),
266 ..Stats::default()
267 }
268}
269
270/// How the run ended, as the record names it: `solved`, or the error's class.
271pub(crate) fn exit_reason<T>(outcome: &Result<T, Error>) -> &'static str {
272 match outcome {
273 Ok(_) => "solved",
274 Err(error) => error.kind(),
275 }
276}
277
278/// Close the run's record once its mode has returned.
279///
280/// The last facts land here rather than with the answer, because they are
281/// the run's and not any one stage's: how the run ended, the time its vtree
282/// constructions took (`vtree_time`), the tree its main compile last started
283/// on (`started_on`), which names the run's `vtree_candidate` when no answer
284/// named one, and the SAT-pruning tallies of its compiles (`sat_prune`).
285/// Recording each in one place is also what keeps the record of a run that
286/// gave up carrying them.
287pub(crate) fn close_record(
288 stats: &mut Stats,
289 outcome: &Result<Verdict, Error>,
290 vtree_time: Option<std::time::Duration>,
291 started_on: Option<String>,
292 sat_prune: Option<crate::compile::sat_prune::SatPruneStats>,
293) {
294 stats.exit_reason = Some(exit_reason(outcome));
295 // A stage can finish after the whole-run deadline, and its answer reach
296 // the record before the driver rejects it. Keep diagnostics, never that
297 // answer.
298 if outcome.is_err() {
299 stats.satisfiable = None;
300 stats.model_count = None;
301 stats.weighted_count = None;
302 stats.weighted_count_log10 = None;
303 stats.weighted_count_sign = None;
304 stats.answered_by = None;
305 }
306 stats.vtree_ms = vtree_time.map(|time| time.as_millis() as u64);
307 if stats.vtree_candidate.is_none() {
308 stats.vtree_candidate = started_on;
309 }
310 // Each tally is recorded only when it is nonzero: presence is the "this
311 // happened" signal. None is there when no pass ran to its end.
312 if let Some(prune) = sat_prune {
313 stats.sat_prune_tallies = prune.tallies().filter(|(_, v)| *v > 0).collect();
314 stats.sat_prune_disabled_by = prune.disabled_by.map(crate::compile::sat_prune::PruneStop::key);
315 }
316}
317
318// ── The answers ──────────────────────────────────────────────────────────────
319
320/// Write the verdict and the count into the record, as the count's domain
321/// spells it, and hand the verdict back.
322fn recorded(stats: &mut Stats, satisfiable: bool, count: Count, diagram: Option<tididi::Tdd>) -> Verdict {
323 stats.satisfiable = Some(satisfiable);
324 match &count {
325 Count::Integer(n) => stats.model_count = Some(n.to_string()),
326 Count::Rational(_) => stats.weighted_count = Some(count.to_string()),
327 Count::Log(s) => {
328 // A zero count has no logarithm; the sign says so instead.
329 let log10 = s.log10_abs();
330 stats.weighted_count_log10 = log10.is_finite().then_some(log10);
331 stats.weighted_count_sign = Some(s.sign);
332 stats.weighted_count = Some(count.to_string());
333 }
334 }
335 Verdict { satisfiable, count, diagram }
336}
337
338/// [`recorded`] for an integer count: a model count is zero exactly when the
339/// formula has no model, so the count is the verdict.
340fn recorded_integer(stats: &mut Stats, count: num_bigint::BigUint, diagram: Option<tididi::Tdd>) -> Verdict {
341 let satisfiable = count != num_bigint::BigUint::ZERO;
342 recorded(stats, satisfiable, Count::Integer(count), diagram)
343}
344
345/// The portfolio candidate that won the run's last selection, recorded with
346/// the model and weighted counts' answers, since only by then has selection
347/// run. The plain compile names the tree its answering compile ran on
348/// instead, the last one it started on ([`close_record`]). The spec itself is
349/// settled before the mode dispatches and is recorded once, in
350/// [`open_record`].
351fn record_vtree_candidate(stats: &mut Stats, config: &Config) {
352 if let Some(candidate) = config.portfolio_history.last_winning_spec() {
353 stats.vtree_candidate = Some(candidate);
354 }
355}
356
357/// Record a model count with what the model count and the compile report
358/// alongside it.
359fn record_integer(
360 stats: &mut Stats,
361 count: num_bigint::BigUint,
362 peak_pairs: usize,
363 preprocessing: PreprocessReport,
364 compile_ms: u128,
365 diagram: Option<tididi::Tdd>,
366) -> Verdict {
367 if peak_pairs > 0 {
368 stats.peak_pairs = Some(peak_pairs as u64);
369 }
370 preprocessing.write(stats);
371 stats.compile_ms = Some(compile_ms as u64);
372 recorded_integer(stats, count, diagram)
373}
374
375/// What preprocessing did, in the four numbers the result record keeps. The
376/// modes carry them and read none of them: they travel from the frontend that
377/// measured them to the record.
378#[derive(Clone, Copy)]
379pub(crate) struct PreprocessReport {
380 pub(crate) preprocess_ms: u64,
381 pub(crate) backbone_ms: u64,
382 pub(crate) backbone_found: u64,
383 pub(crate) backbone_probes: u64,
384}
385
386impl PreprocessReport {
387 /// The report of one vitri run, its whole wall counted as preprocessing.
388 pub(crate) fn of(telemetry: &vitri::bundle::PreprocessTelemetry) -> Self {
389 PreprocessReport {
390 preprocess_ms: telemetry.total_ms,
391 backbone_ms: telemetry.backbone_ms.unwrap_or(0),
392 backbone_found: telemetry.backbone_found as u64,
393 backbone_probes: telemetry.backbone_probes as u64,
394 }
395 }
396
397 fn write(self, stats: &mut Stats) {
398 stats.preprocess_ms = Some(self.preprocess_ms);
399 stats.backbone_ms = Some(self.backbone_ms);
400 stats.backbone_found = Some(self.backbone_found);
401 stats.backbone_probes = Some(self.backbone_probes);
402 }
403}
404
405/// Which stage produced a model count, with the Arjun and cascade walls the
406/// record keeps beside it.
407#[derive(Clone, Copy)]
408pub(crate) struct Attribution {
409 /// Not an `Option`: every caller must name the stage, so a new answer
410 /// site cannot be added without saying which stage answers there.
411 pub(crate) answered_by: AnsweredBy,
412 /// `None` when Arjun did not run.
413 pub(crate) reduction: Option<Reduction>,
414 pub(crate) arjun_ms: Option<u128>,
415 pub(crate) cascade_ms: Option<u128>,
416}
417
418/// A model count, the cascade's or the streaming route's, with what the record
419/// keeps beside it. Nothing is materialized as a diagram, so there are no
420/// diagram sizes beyond the compile peak.
421pub(crate) struct McAnswer {
422 pub(crate) count: num_bigint::BigUint,
423 pub(crate) peak_pairs: usize,
424 pub(crate) preprocessing: PreprocessReport,
425 pub(crate) compile_ms: u128,
426 pub(crate) attribution: Attribution,
427}
428
429/// The verdict and the record of a model count.
430pub(crate) fn record_mc(stats: &mut Stats, config: &Config, answer: McAnswer) -> Verdict {
431 let McAnswer { count, peak_pairs, preprocessing, compile_ms, attribution } = answer;
432 stats.answered_by = Some(attribution.answered_by.token());
433 stats.reduction = attribution.reduction.map(Reduction::token);
434 stats.arjun_ms = attribution.arjun_ms.map(|ms| ms as u64);
435 stats.cascade_ms = attribution.cascade_ms.map(|ms| ms as u64);
436 record_vtree_candidate(stats, config);
437 record_integer(stats, count, peak_pairs, preprocessing, compile_ms, None)
438}
439
440/// The verdict and the record of a weighted count.
441///
442/// The count is `sign·10^(log10_abs)` in the bounded log domain, which every
443/// weighted count runs in (`src/stage/wmc.rs`), spelled in scientific notation
444/// ([`Count::Log`]), because the magnitude can be astronomically large or
445/// small and a plain f64 would overflow or underflow.
446pub(crate) fn record_wmc(
447 stats: &mut Stats,
448 config: &Config,
449 count: WeightedCount<tididi::diagram::WeightValue>,
450) -> Verdict {
451 use tididi::diagram::SignedLog;
452 record_vtree_candidate(stats, config);
453 let (satisfiable, s) = match count {
454 WeightedCount::Unsatisfiable => (false, SignedLog::zero()),
455 WeightedCount::Satisfiable(value) => {
456 (true, *value.as_log().expect("the weighted count runs in the log domain"))
457 }
458 };
459 recorded(stats, satisfiable, Count::Log(s), None)
460}
461
462/// A projected count, PMC's or PWMC's, with the two record fields its stages
463/// measure; [`crate::stage::run_stages`] fills it at the answer.
464pub(crate) struct Projected<V> {
465 pub(crate) value: V,
466 /// Variables in the show set of the problem the answering stage was
467 /// handed; `None` when there is no show set.
468 pub(crate) show_vars: Option<usize>,
469 /// The stages' wall, less the rewrites and the vtree construction.
470 pub(crate) compile_ms: u128,
471}
472
473impl<V> Projected<V> {
474 fn record(self, stats: &mut Stats) -> V {
475 stats.compile_ms = Some(self.compile_ms as u64);
476 stats.show_vars = self.show_vars.map(|n| n as u64);
477 self.value
478 }
479}
480
481/// The verdict and the record of a projected integer (PMC) count.
482pub(crate) fn record_pmc(stats: &mut Stats, answer: Projected<num_bigint::BigUint>) -> Verdict {
483 let count = answer.record(stats);
484 recorded_integer(stats, count, None)
485}
486
487/// The verdict and the record of a projected weighted (PWMC) count, an exact
488/// rational.
489pub(crate) fn record_pwmc(
490 stats: &mut Stats,
491 answer: Projected<WeightedCount<num_rational::BigRational>>,
492) -> Verdict {
493 use num_traits::Zero;
494 let (satisfiable, value) = match answer.record(stats) {
495 WeightedCount::Unsatisfiable => (false, num_rational::BigRational::zero()),
496 WeightedCount::Satisfiable(value) => (true, value),
497 };
498 recorded(stats, satisfiable, Count::Rational(value), None)
499}
500
501/// The plain compile's diagram, its count, and what the record keeps beside
502/// them.
503pub(crate) struct CompileAnswer {
504 pub(crate) tdd: tididi::Tdd,
505 /// The diagram's model count, over every original variable.
506 pub(crate) count: num_bigint::BigUint,
507 /// The largest diagram the answering compile held, in pairs.
508 pub(crate) peak_pairs: usize,
509 pub(crate) preprocessing: PreprocessReport,
510 pub(crate) compile_ms: u128,
511 pub(crate) answered_by: AnsweredBy,
512}
513
514/// The verdict and the record of the plain compile: the count, the diagram it
515/// was read off, which the answer carries, and the compile that answered.
516pub(crate) fn record_compile(stats: &mut Stats, answer: CompileAnswer) -> Verdict {
517 let CompileAnswer { tdd, count, peak_pairs, preprocessing, compile_ms, answered_by } = answer;
518 stats.answered_by = Some(answered_by.token());
519 stats.tdd_width = Some(tdd.max_width() as u64);
520 stats.tdd_pairs = Some(tdd.pair_count() as u64);
521 stats.tdd_nodes = Some(tdd.node_count() as u64);
522 record_integer(stats, count, peak_pairs, preprocessing, compile_ms, Some(tdd))
523}
524
525// ── The command line ─────────────────────────────────────────────────────────
526
527/// Print what the run answered as the command line asked, write the circuit
528/// under `--save-tdd` and the record under `--stats`, and return how the run
529/// ended.
530///
531/// Stdout, in order: the answer, or `s UNKNOWN` for a give-up under
532/// `--output-format mcc`; the `tdd written to` line; the record, when
533/// `--stats -`. A run that failed before opening its record writes none. An
534/// answer, a circuit or a record that cannot be written fails a run that
535/// otherwise succeeded, because the operator asked for it; a reader that
536/// closed stdout early is the exception ([`print`]).
537pub(crate) fn finish(solved: Result<Answer, Failure>, cli: &Cli) -> Result<(), Error> {
538 let mcc = cli.output_format == OutputFormat::Mcc;
539 let (outcome, stats) = match solved {
540 Ok(answer) => {
541 let delivered = print_answer(&answer, mcc).and_then(|()| match &cli.save_tdd_path {
542 Some(path) => {
543 // The driver refuses `--save-tdd` on every mode but the
544 // compile, and the compile always answers with its diagram.
545 let tdd = answer.diagram.as_ref().expect("a compile answers with its diagram");
546 save_diagram(tdd, path, mcc)
547 }
548 None => Ok(()),
549 });
550 let mut stats = answer.stats;
551 // An answer or a circuit that could not be written ends the run
552 // that computed it.
553 stats.exit_reason = Some(exit_reason(&delivered));
554 (delivered, Some(stats))
555 }
556 Err(Failure { error, stats }) => {
557 // The give-up is the run's outcome, whether or not its status
558 // line reaches stdout.
559 let _ = report_give_up(&error, mcc);
560 (Err(error), stats.map(|stats| *stats))
561 }
562 };
563 let written = match (&cli.stats_path, stats) {
564 (Some(path), Some(stats)) => write_record(&stats, path),
565 _ => Ok(()),
566 };
567 outcome.and(written)
568}
569
570/// Write the record as one JSON object at `path`, or on stdout for `-`.
571fn write_record(stats: &Stats, path: &Path) -> Result<(), Error> {
572 let json = serde_json::to_string(stats)
573 .map_err(|error| Error::Artifact(format!("could not build the stats record: {error}")))?;
574 if path.as_os_str() == "-" {
575 return print(&format!("{json}\n"));
576 }
577 std::fs::write(path, format!("{json}\n"))
578 .map_err(|error| Error::Artifact(format!("could not write {}: {error}", path.display())))
579}
580
581/// Write the compiled circuit to `path` and its vtree to `<path>.vtree`, and
582/// say so on stdout under the plain output format.
583fn save_diagram(tdd: &tididi::diagram::Tdd, path: &Path, mcc: bool) -> Result<(), Error> {
584 // The `.tdd` format labels vtree nodes but does not describe the tree,
585 // and `tididi::io::load_tdd` takes the vtree as an argument, so the
586 // vtree goes beside the circuit in its own text format.
587 let mut vtree_path = path.as_os_str().to_owned();
588 vtree_path.push(".vtree");
589 let vtree_path = PathBuf::from(vtree_path);
590 let (shown, vtree_shown) = (path.display(), vtree_path.display());
591 tididi::io::save_tdd(tdd, path)
592 .map_err(|error| Error::Artifact(format!("could not write the diagram to {shown}: {error}")))?;
593 std::fs::write(&vtree_path, tdd.vtree().to_text())
594 .map_err(|error| Error::Artifact(format!("could not write the vtree to {vtree_shown}: {error}")))?;
595 if mcc {
596 return Ok(());
597 }
598 print(&format!(
599 "tdd written to {shown}, vtree to {vtree_shown}: {} nodes, {} pairs\n",
600 tdd.node_count(),
601 tdd.pair_count(),
602 ))
603}
604
605/// Write `text` to stdout, the one writer of everything the command line
606/// prints there. A reader that stops early, such as `head` or a pager that
607/// quit, closes the pipe: that is not an error, and the run goes on without
608/// stdout. Any other failed write is [`Error::Artifact`].
609pub(crate) fn print(text: &str) -> Result<(), Error> {
610 use std::io::Write as _;
611 let mut stdout = std::io::stdout().lock();
612 match stdout.write_all(text.as_bytes()).and_then(|()| stdout.flush()) {
613 Err(error) if error.kind() != std::io::ErrorKind::BrokenPipe => {
614 Err(Error::Artifact(format!("could not write to stdout: {error}")))
615 }
616 _ => Ok(()),
617 }
618}
619
620/// log10 of a non-negative big integer to ~double precision, for the MCC
621/// `c s log10-estimate` line. Returns `-inf` for zero, exactly as the
622/// MC-Competition output spec (v1.2 §3) requires when the count is 0.
623fn log10_biguint(n: &num_bigint::BigUint) -> f64 {
624 if *n == num_bigint::BigUint::ZERO {
625 return f64::NEG_INFINITY;
626 }
627 // Keep the top ~53 bits (one f64 mantissa) and account for the rest via
628 // the binary exponent: log10(n) = log10(n >> s) + s*log10(2). This stays
629 // accurate for counts far larger than f64's range (2^1024).
630 let bits = n.bits(); // index of the MSB + 1, >= 1
631 let shift = (bits as usize).saturating_sub(53);
632 let high = (n >> shift).to_u64_digits().first().copied().unwrap_or(1) as f64;
633 high.log10() + (shift as f64) * std::f64::consts::LOG10_2
634}
635
636/// The competition's status line. "The solver may output `UNKNOWN`, but is not
637/// allowed to output any other value than these three": `None` is a run that
638/// gave up.
639fn status_line(satisfiable: Option<bool>) -> &'static str {
640 match satisfiable {
641 Some(true) => "s SATISFIABLE",
642 Some(false) => "s UNSATISFIABLE",
643 None => "s UNKNOWN",
644 }
645}
646
647/// The MCC answer to a run that gave up: `s UNKNOWN` under
648/// `--output-format mcc`, the one status line the format allows without a
649/// count.
650///
651/// Every task's give-up reaches the driver as [`Error::Incomplete`], after the
652/// stage that gave up said why on stderr, so [`finish`] prints this once
653/// rather than each stage printing it. A run stopped by any other error has
654/// refused the instance or failed, and prints nothing on stdout.
655fn report_give_up(error: &Error, mcc: bool) -> Result<(), Error> {
656 if matches!(error, Error::Incomplete(_)) && mcc {
657 return print(&format!("{}\n", status_line(None)));
658 }
659 Ok(())
660}
661
662/// Print one answer.
663///
664/// Under `--output-format mcc` it is the four lines of the Model Counting
665/// Competition's output format (data format v1.2 §3), in the format's order,
666/// for every task:
667///
668/// ```text
669/// s SATISFIABLE | s UNSATISFIABLE
670/// c s type mc | wmc | pmc | pwmc
671/// c s log10-estimate <log10 of the count> | c s neglog10-estimate <log10 of -count>
672/// c s <solver type> <precision> <notation> <count>
673/// ```
674///
675/// and otherwise one plainly worded line. This is the only printer of either,
676/// so no task can drift from the order or the spelling. The status line is the
677/// solver's verdict, not the count's: a zero weight, or weights that cancel,
678/// make the weighted count of a satisfiable formula zero.
679fn print_answer(answer: &Answer, mcc: bool) -> Result<(), Error> {
680 let count = &answer.count;
681 if !mcc {
682 return print(&format!("{}: {count}\n", plain_label(answer.mode)));
683 }
684 let (log10, negative) = magnitude(count);
685 let estimate = if negative { "neglog10-estimate" } else { "log10-estimate" };
686 print(&format!(
687 "{}\nc s type {}\nc s {estimate} {log10}\nc s {}\n",
688 status_line(Some(answer.satisfiable)),
689 task(answer.mode),
690 exact(count),
691 ))
692}
693
694/// The task as `c s type` names it. The plain compile's count is a model count
695/// too, read off the diagram over every original variable, so it answers as
696/// `mc`.
697fn task(mode: Mode) -> &'static str {
698 match mode {
699 Mode::Compile => Mode::Mc.token(),
700 mode => mode.token(),
701 }
702}
703
704/// The words before the count on the plain answer line.
705fn plain_label(mode: Mode) -> &'static str {
706 match mode {
707 Mode::Compile | Mode::Mc => "model count",
708 Mode::Wmc => "weighted count",
709 Mode::Pmc => "projected model count",
710 Mode::Pwmc => "projected weighted count",
711 }
712}
713
714/// log10 of the count's magnitude, `-inf` for a zero count, printed in full
715/// double precision; and whether the count is below zero, which only negative
716/// weights make it.
717fn magnitude(count: &Count) -> (f64, bool) {
718 match count {
719 Count::Integer(n) => (log10_biguint(n), false),
720 Count::Rational(r) => (
721 log10_biguint(r.numer().magnitude()) - log10_biguint(r.denom().magnitude()),
722 r.numer().sign() == num_bigint::Sign::Minus,
723 ),
724 Count::Log(s) => (s.log10_abs(), s.sign < 0),
725 }
726}
727
728/// The last MCC answer line after its `c s `: solver type, precision, notation
729/// and the count, in each task's spelling.
730fn exact(count: &Count) -> String {
731 match count {
732 Count::Integer(n) => format!("exact arb int {n}"),
733 // The standard MCC weighted-float answer: "exact" is the solver type,
734 // and `double` says the value is bounded-precision f64.
735 Count::Log(_) => format!("exact double prec-sci {count}"),
736 Count::Rational(_) => format!("exact arb frac {count}"),
737 }
738}
739
740#[cfg(test)]
741#[path = "tests/output.rs"]
742mod tests;