1pub mod analytic;
5pub mod builtins;
6pub mod chain;
7pub mod complex;
8pub mod conjugate;
9pub mod continuous;
10pub mod data;
11pub mod dates;
12pub mod dist;
13pub mod error;
14pub mod failure;
15pub mod interp;
16pub mod joint;
17pub mod likelihood;
18mod math;
19pub mod ops;
20mod ordering;
21pub mod quadrature;
22pub mod report;
23mod stats;
24pub mod symbolic;
25mod text;
26mod type_name;
27pub mod value;
28pub mod weight;
29pub mod world;
30
31pub use error::{ErrorKind, Fault, RuntimeError};
32pub use failure::{Failure, Failures};
33pub use interp::{Stats, Updates};
34pub use probl_sema::ir::FailureMode;
35pub use symbolic::{Diagrams, Symbolic};
36pub use weight::Weight;
37
38use continuous::Rng;
39use dist::Budget;
40use error::OpError;
41use probl_sema::Liveness;
42use probl_sema::conjugate::{Conjugacy, Variable};
43use probl_sema::ir::{Mode, Program};
44use probl_syntax::Span;
45use report::{Format, Sink};
46use std::any::Any;
47use std::collections::BTreeMap;
48use std::panic::AssertUnwindSafe;
49use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
50use std::sync::{Arc, Condvar, Mutex, PoisonError, mpsc};
51use value::Value;
52
53#[derive(Clone, Debug)]
56pub struct Limits {
57 pub max_integer_bits: u64,
59 pub max_integer_bytes: u64,
61 pub max_string_bytes: usize,
63 pub max_string_alloc_bytes: u64,
65 pub max_worlds: usize,
67 pub max_outcomes: usize,
69 pub max_collection: usize,
71 pub max_work: u64,
73 pub max_iterations: u64,
75 pub max_call_depth: usize,
77 pub max_cached_calls: usize,
79 pub max_output: usize,
81 pub stack_size: usize,
84 pub max_threads: usize,
87 pub max_chain_states: usize,
90 pub max_nodes: usize,
93}
94
95impl Default for Limits {
96 fn default() -> Limits {
97 Limits {
98 max_integer_bits: probl_number::MAX_INTEGER_BITS,
99 max_integer_bytes: 256 * 1024 * 1024,
100 max_string_bytes: 16 * 1024 * 1024,
101 max_string_alloc_bytes: 256 * 1024 * 1024,
102 max_worlds: 10_000_000,
103 max_outcomes: 2_000_000,
104 max_collection: 10_000_000,
105 max_work: 20_000_000_000,
106 max_iterations: 10_000_000,
107 max_call_depth: 500,
108 max_cached_calls: 1_000_000,
109 max_output: 64 * 1024 * 1024,
110 stack_size: 64 * 1024 * 1024,
111 max_threads: std::thread::available_parallelism().map_or(1, |n| n.get()),
112 max_chain_states: 50_000,
113 max_nodes: 10_000_000,
114 }
115 }
116}
117
118#[derive(Clone, Debug)]
121pub struct Options {
122 pub today: Option<i32>,
126 pub merge: bool,
129 pub memoize: bool,
131 pub epsilon: Option<f64>,
133 pub fractions: bool,
135 pub limits: Limits,
136 pub cancel: Option<Arc<AtomicBool>>,
138 pub mode: Option<Mode>,
140 pub inputs: Option<Arc<data::Inputs>>,
143 pub conjugate: bool,
147 pub solve: bool,
151 pub progress: Option<Progress>,
154 pub on_error: Option<FailureMode>,
158 pub integrate: Option<f64>,
161 pub symbolic: Symbolic,
164}
165
166#[derive(Clone)]
168pub struct Progress(pub Arc<dyn Fn(u64, u64) + Send + Sync>);
169
170impl std::fmt::Debug for Progress {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.write_str("Progress")
173 }
174}
175
176impl Default for Options {
177 fn default() -> Options {
178 Options {
179 today: None,
180 merge: true,
181 memoize: true,
182 epsilon: None,
183 fractions: false,
184 limits: Limits::default(),
185 cancel: None,
186 mode: None,
187 inputs: None,
188 conjugate: true,
189 solve: true,
190 progress: None,
191 on_error: None,
192 integrate: None,
193 symbolic: Symbolic::Off,
194 }
195 }
196}
197
198#[derive(Clone, Debug)]
199pub struct Outcome {
200 pub today: Option<i32>,
202 pub output: String,
204 pub stats: Stats,
205 pub unresolved: Weight,
207 pub evidence: Option<Weight>,
209 pub integration: Option<f64>,
213 pub reports: Vec<Sink>,
215 pub results: Vec<report::ReportResult>,
217 pub format: Format,
220 pub sample: Option<Sampled>,
222 pub data: Vec<data::SourceInfo>,
224 pub updates: Vec<(Variable, Updates)>,
227 pub on_error: FailureMode,
230 pub failures: Failures,
233 pub finished: Weight,
236 pub densities: bool,
239}
240
241#[derive(Clone, Debug)]
243pub struct Sampled {
244 pub runs: u64,
245 pub seed: u64,
246 pub effective: f64,
248 pub weight: Weight,
250 pub squares: Weight,
252 pub evidence_se: f64,
255 pub densities: bool,
258}
259
260pub fn run(
264 program: &Program,
265 options: &Options,
266 print: &mut (dyn FnMut(&str) + Send),
267) -> Result<Outcome, RuntimeError> {
268 std::thread::scope(|scope| {
269 let handle = std::thread::Builder::new()
270 .name("probl-engine".into())
271 .stack_size(options.limits.stack_size)
272 .spawn_scoped(scope, || run_here(program, options, print));
273 let handle = match handle {
274 Ok(h) => h,
275 Err(e) => {
276 return Err(internal(format!("couldn't start the engine: {e}")));
277 }
278 };
279 handle
280 .join()
281 .unwrap_or_else(|panic| Err(internal(panic_detail(&*panic))))
282 })
283}
284
285pub fn run_on_this_thread(
292 program: &Program,
293 options: &Options,
294 print: &mut (dyn FnMut(&str) + Send),
295) -> Result<Outcome, RuntimeError> {
296 run_here(program, options, print)
297}
298
299fn panic_detail(panic: &(dyn Any + Send)) -> String {
301 panic
302 .downcast_ref::<String>()
303 .cloned()
304 .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
305 .unwrap_or_else(|| "no details".to_string())
306}
307
308fn internal(detail: String) -> RuntimeError {
309 OpError::internal("internal error: the engine crashed")
310 .at(Span::default())
311 .with_note(detail)
312}
313
314fn run_here(
315 program: &Program,
316 options: &Options,
317 print: &mut (dyn FnMut(&str) + Send),
318) -> Result<Outcome, RuntimeError> {
319 if options.today.is_some_and(|d| !dates::valid(d)) {
320 return Err(RuntimeError::new(
321 Default::default(),
322 "execution date must be within 0001-01-01..9999-12-31",
323 ));
324 }
325 let settings = &program.settings;
326 let mode = options.mode.clone().unwrap_or_else(|| settings.mode.clone());
327 let sample = match mode {
328 Mode::Auto | Mode::Enumerate => None,
329 Mode::Sample { runs, seed } => Some((runs, seed)),
330 ref other => {
331 return Err(
332 OpError::unsupported(format!("`{}` mode isn't implemented yet", other.name()))
333 .help("enumeration and `@mode sample(runs: 10_000)` are; particles and beam search come later")
334 .at(settings.mode_span.unwrap_or_default()),
335 );
336 }
337 };
338 if let Some((runs, _)) = sample {
339 if runs == 0 || runs > u32::MAX as u64 {
340 return Err(OpError::new(format!(
341 "sample mode needs between 1 and {} runs",
342 report::thousands(u32::MAX as i64)
343 ))
344 .at(settings.mode_span.unwrap_or_default()));
345 }
346 }
347 let on_error = options.on_error.or(settings.on_error).unwrap_or(match sample {
350 Some(_) => FailureMode::Partial,
351 None => FailureMode::Total,
352 });
353 let limits = &options.limits;
354 let config = interp::Config {
355 today: options.today,
356 epsilon: options.epsilon.unwrap_or(settings.epsilon),
357 merging: options.merge,
358 memoizing: options.memoize,
359 max_worlds: limits.max_worlds.min(settings.max_worlds),
360 max_iterations: limits.max_iterations.min(settings.max_iterations),
361 max_call_depth: limits.max_call_depth,
362 max_cached_calls: limits.max_cached_calls,
363 max_output: limits.max_output,
364 budget: Budget {
365 max_integer_bits: limits.max_integer_bits,
366 integer_bytes_left: Arc::new(AtomicU64::new(limits.max_integer_bytes)),
367 max_string_bytes: limits.max_string_bytes,
368 string_bytes_left: Arc::new(AtomicU64::new(limits.max_string_alloc_bytes)),
369 cancel: options.cancel.clone(),
370 max_outcomes: limits.max_outcomes,
371 max_collection: limits.max_collection,
372 work_left: limits.max_work,
373 shared: None,
374 },
375 cancel: options.cancel.clone(),
376 sample_seed: sample.map(|(_, seed)| seed),
377 conjugate: options.conjugate,
378 solving: options.solve,
379 max_chain_states: limits.max_chain_states,
380 partial: on_error == FailureMode::Partial,
381 integrate: options.integrate.or(settings.integrate),
382 };
383 let epsilon = config.epsilon;
384 let inputs = inputs(program, options)?;
385 if let Some((runs, seed)) = sample {
386 let live = probl_sema::analyze(program);
387 let conj = probl_sema::conjugate::analyze(program);
388 return sampled(program, &live, &conj, config, options, print, runs, seed, on_error);
389 }
390 let integrate = config.integrate;
391 if let Some(t) = integrate.filter(|t| !(*t > 0.0 && *t <= 1e-3)) {
392 return Err(RuntimeError::new(
393 Default::default(),
394 format!("integration needs a relative tolerance above 0 and at most 0.001, not {t}"),
395 ));
396 }
397 if options.symbolic != Symbolic::Off {
398 let budget = config.budget.clone();
399 match symbolic::run(program, budget, limits.max_nodes, options.cancel.clone()) {
400 Ok(answer) => {
401 let run = Enumerated {
402 sinks: answer.sinks,
403 stats: Stats {
404 diagrams: Some(answer.diagrams),
405 ..Stats::default()
406 },
407 finished: answer.finished,
408 unresolved: Weight::ZERO,
409 failures: Failures::default(),
410 observed: answer.observed,
411 densities: false,
412 integrated: None,
413 last_ruling_out: answer.last_ruling_out,
414 };
415 return enumerated(program, options, run, on_error, epsilon, integrate);
416 }
417 Err(symbolic::Fallback::Stopped(e)) => return Err(e),
418 Err(symbolic::Fallback::Unsupported(e) | symbolic::Fallback::TooLarge(e))
419 if options.symbolic == Symbolic::Only =>
420 {
421 return Err(e);
422 }
423 Err(_) => {}
425 }
426 }
427 let live = probl_sema::analyze(program);
428 let conj = probl_sema::conjugate::analyze(program);
429 let mut engine = interp::Engine::new(program, &live, &conj, config, inputs, print);
430 let finished = engine.run_main()?;
431 let unresolved = engine.unresolved;
432 let mut failures = std::mem::take(&mut engine.failures);
433 failures.other_units = failures.mixed_densities
434 || matches!((failures.densities, engine.reported_densities), (Some(a), Some(b)) if a != b);
435 if let Some(span) = engine.density_at.filter(|_| !unresolved.is_zero()) {
436 return Err(OpError::unsupported(
437 "observing a value from a continuous distribution while some probability is unresolved isn't supported when enumerating",
438 )
439 .help("the unresolved worlds could have any density at the value, so nothing would bound the evidence; sample the model with `@mode sample(runs: 10_000)`")
440 .at(span));
441 }
442 let run = Enumerated {
443 sinks: std::mem::take(&mut engine.sinks),
444 stats: engine.stats.clone(),
445 finished,
446 unresolved,
447 failures,
448 observed: engine.observed,
449 densities: engine.densities,
450 integrated: engine.integrated,
451 last_ruling_out: engine.last_ruling_out,
452 };
453 enumerated(program, options, run, on_error, epsilon, integrate)
454}
455
456struct Enumerated {
458 sinks: Vec<Sink>,
459 stats: Stats,
460 finished: Weight,
462 unresolved: Weight,
463 failures: Failures,
464 observed: bool,
466 densities: bool,
468 integrated: Option<f64>,
470 last_ruling_out: Option<Span>,
472}
473
474fn enumerated(
477 program: &Program,
478 options: &Options,
479 run: Enumerated,
480 on_error: FailureMode,
481 epsilon: f64,
482 integrate: Option<f64>,
483) -> Result<Outcome, RuntimeError> {
484 let Enumerated {
485 sinks,
486 stats,
487 finished,
488 unresolved,
489 failures,
490 observed,
491 densities,
492 integrated,
493 last_ruling_out,
494 } = run;
495 if observed && finished.is_zero() && failures.is_empty() {
496 let span = last_ruling_out.unwrap_or_default();
497 return Err(if unresolved.is_zero() {
498 RuntimeError::new(
499 span,
500 "the evidence is impossible: every world was ruled out by `observe`",
501 )
502 .with_help("check the observations; there's no answer to condition on")
503 } else {
504 RuntimeError::new(span, "every world that fits the evidence was left unresolved")
505 .with_help("lower `@epsilon` so that loops run longer")
506 });
507 }
508
509 let comparable = failures.comparable();
513 let total = if comparable {
514 finished + failures.weight
515 } else {
516 finished
517 };
518 let format = Format {
519 fractions: options.fractions,
520 weighted: program.main().effects.observes,
521 unresolved,
522 program_total: total,
523 run_squares: None,
524 reach_known: comparable,
525 integrate,
526 };
527 let plain = Format {
528 fractions: false,
529 ..format
530 };
531 let results = report::results(&program.reports, &sinks, format, unresolved);
532 let integration = integrated.or_else(|| {
535 let numeric = |g: &report::GroupResult| g.numeric.as_ref().is_some_and(|n| n.integrated);
536 results
537 .iter()
538 .flat_map(|r| &r.groups)
539 .any(|g| numeric(g) || g.fields.iter().flat_map(|f| &f.0).any(|(_, g)| numeric(g)))
540 .then_some(integrate)
541 .flatten()
542 });
543 let mut header = String::from("enumerated");
544 if let Some(tolerance) = integration {
545 header.push_str(&format!(" · integrated to {tolerance:e}"));
546 }
547 if !failures.is_empty() {
548 header.push_str(" · partial result");
549 if comparable && !total.is_zero() {
550 header.push_str(&format!(
551 " · {} failed",
552 report::pct(failures.weight.ratio(total), plain)
553 ));
554 }
555 }
556 let evidence = observed.then_some(total);
559 let of = if comparable { "" } else { " of the finished worlds" };
560 if let Some(z) = evidence {
561 let (lo, hi) = (total.to_f64(), (total + unresolved).to_f64());
562 if densities {
563 header.push_str(&format!(" · log evidence{of} {:.4}", z.ln()));
564 } else if hi - lo >= 0.00005 {
565 header.push_str(&format!(
566 " · evidence{of} {}–{}",
567 report::pct(lo, plain),
568 report::pct(hi, plain)
569 ));
570 } else if z.to_f64() < 0.0001 {
571 header.push_str(&format!(" · evidence{of} {}", scientific(z)));
572 } else {
573 header.push_str(&format!(" · evidence{of} {}", report::pct(z.to_f64(), plain)));
574 }
575 }
576 if !unresolved.is_zero() {
577 if unresolved.to_f64() <= epsilon {
578 header.push_str(&format!(" · unresolved < {epsilon:e}"));
579 } else {
580 header.push_str(&format!(" · unresolved {:.1e}", unresolved.to_f64()));
581 }
582 }
583 let body = report::render_results(&program.reports, &results, format);
584 let output = if body.is_empty() {
585 header
586 } else {
587 format!("{header}\n\n{body}")
588 };
589 Ok(Outcome {
590 today: options.today,
591 output,
592 stats,
593 unresolved,
594 evidence,
595 integration,
596 reports: sinks,
597 results,
598 format,
599 sample: None,
600 data: sources(options),
601 updates: Vec::new(),
602 on_error,
603 failures,
604 finished,
605 densities,
606 })
607}
608
609fn scientific(w: Weight) -> String {
611 let l = w.log10();
612 let exponent = l.floor();
613 let mantissa = libm::pow(10.0, l - exponent);
614 let (mantissa, exponent) = if format!("{mantissa:.2}") == "10.00" {
616 (1.0, exponent + 1.0)
617 } else {
618 (mantissa, exponent)
619 };
620 format!("{mantissa:.2}e{exponent}")
621}
622
623fn evidence_estimate(z: Weight, relative_se: f64, densities: bool) -> String {
626 let known = relative_se.is_finite();
627 if densities {
628 let ln = z.log10() * std::f64::consts::LN_10;
629 if !known {
630 return format!("log evidence {ln:.2}");
631 }
632 let decimals = if relative_se > 0.0 {
633 (-libm::log10(relative_se).floor()).clamp(2.0, 6.0) as usize
634 } else {
635 2
636 };
637 return format!("log evidence {ln:.decimals$} ± {relative_se:.decimals$}");
638 }
639 let x = z.to_f64();
640 if x >= 1e-4 {
641 return match known {
642 true => format!("evidence {}", report::estimate(x, x * relative_se)),
643 false => format!("evidence {}", value::fmt_prob(x)),
644 };
645 }
646 if !known {
647 return format!("evidence {}", scientific(z));
648 }
649 let pct = relative_se * 100.0;
650 let decimals = if pct >= 10.0 {
651 0
652 } else if pct >= 1.0 {
653 1
654 } else {
655 2
656 };
657 format!("evidence {} (± {pct:.decimals$}%)", scientific(z))
658}
659
660fn inputs<'a>(program: &Program, options: &'a Options) -> Result<&'a [Value], RuntimeError> {
662 let Some(first) = program.inputs.first() else {
663 return Ok(&[]);
664 };
665 match &options.inputs {
666 Some(inputs) if inputs.fit(program) => {
667 if inputs.max_string_bytes() > options.limits.max_string_bytes {
668 return Err(RuntimeError::limit(
669 first.span,
670 "an input string exceeds the runtime string size limit",
671 ));
672 }
673 if inputs.max_integer_bits() > options.limits.max_integer_bits {
674 return Err(RuntimeError::limit(
675 first.span,
676 "an input integer exceeds the runtime integer size limit",
677 ));
678 }
679 Ok(inputs.values())
680 }
681 Some(_) => Err(internal("the data was loaded for a different program".to_string())),
682 None => Err(
683 RuntimeError::new(first.span, "the program reads data, which wasn't loaded")
684 .with_help("load it with `probl_engine::data::load` before running the program"),
685 ),
686 }
687}
688
689fn sources(options: &Options) -> Vec<data::SourceInfo> {
690 options.inputs.as_ref().map_or_else(Vec::new, |i| i.sources().to_vec())
691}
692
693struct Combined {
695 sinks: Vec<Sink>,
696 totals: interp::SampleTotals,
697 observed: bool,
698 densities: bool,
699 unresolved: Weight,
700 last_ruling_out: Option<Span>,
701 stats: Stats,
702 failures: Failures,
703}
704
705impl Combined {
706 fn new(reports: usize) -> Combined {
707 Combined {
708 sinks: vec![Sink::default(); reports],
709 totals: interp::SampleTotals {
710 weight: Weight::ZERO,
711 squares: Weight::ZERO,
712 },
713 observed: false,
714 densities: false,
715 unresolved: Weight::ZERO,
716 last_ruling_out: None,
717 stats: Stats::default(),
718 failures: Failures::default(),
719 }
720 }
721
722 fn absorb(&mut self, batch: interp::Batch) {
723 for (sink, theirs) in self.sinks.iter_mut().zip(batch.sinks) {
724 sink.absorb(theirs);
725 }
726 self.totals.weight += batch.totals.weight;
727 self.totals.squares += batch.totals.squares;
728 self.observed |= batch.observed;
729 self.densities |= batch.densities;
730 self.unresolved += batch.unresolved;
731 self.last_ruling_out = batch.last_ruling_out.or(self.last_ruling_out);
732 self.stats.absorb(&batch.stats);
733 self.failures.append(batch.failures);
734 }
735}
736
737type Sent = (u64, Result<interp::Batch, RuntimeError>, interp::Printed);
740
741#[allow(clippy::too_many_arguments)]
746fn run_batches(
747 program: &Program,
748 live: &Liveness,
749 conj: &Conjugacy,
750 config: &interp::Config,
751 options: &Options,
752 print: &mut (dyn FnMut(&str) + Send),
753 runs: u64,
754 seed: u64,
755) -> Result<Combined, RuntimeError> {
756 let batches = runs.div_ceil(interp::BATCH);
757 let threads = (options.limits.max_threads.max(1) as u64).min(batches);
758 let inputs = inputs(program, options)?;
759 let ahead = 2 * threads;
762 let mut config = config.clone();
763 config.budget.shared = Some(Arc::new(AtomicU64::new(config.budget.work_left)));
764 config.budget.work_left = 0;
765 let config = &config;
766 if threads == 1 {
767 return run_batches_here(program, live, conj, config, inputs, options, print, runs, seed);
768 }
769
770 let next = AtomicU64::new(0);
771 let failed = AtomicU64::new(u64::MAX);
773 let printed = AtomicUsize::new(0);
775 let combined_upto = Mutex::new(0u64);
777 let caught_up = Condvar::new();
778 let stop = |error: RuntimeError| {
779 let _upto = combined_upto.lock().unwrap_or_else(PoisonError::into_inner);
781 failed.store(0, Ordering::Relaxed);
782 caught_up.notify_all();
783 Err(error)
784 };
785 let (tx, rx) = mpsc::channel::<Sent>();
786 std::thread::scope(|scope| {
787 for t in 0..threads {
788 let tx = tx.clone();
789 let (next, failed, printed) = (&next, &failed, &printed);
790 let (combined_upto, caught_up) = (&combined_upto, &caught_up);
791 let worker = move || {
792 let mut ignore = |_: &str| {};
793 let copied: Vec<Value> = inputs.iter().map(Value::unshared).collect();
796 let mut engine = interp::Engine::new(program, live, conj, config.clone(), &copied, &mut ignore);
797 loop {
798 let index = next.fetch_add(1, Ordering::Relaxed);
799 if index >= batches {
800 break;
801 }
802 let mut upto = combined_upto.lock().unwrap_or_else(PoisonError::into_inner);
803 while index >= *upto + ahead && index <= failed.load(Ordering::Relaxed) {
804 upto = caught_up.wait(upto).unwrap_or_else(PoisonError::into_inner);
805 }
806 drop(upto);
807 if index > failed.load(Ordering::Relaxed) {
808 break;
809 }
810 let first = index * interp::BATCH;
811 let n = (runs - first).min(interp::BATCH);
812 let before = printed.load(Ordering::Relaxed);
813 let (result, lines) = std::panic::catch_unwind(AssertUnwindSafe(|| {
814 engine.run_batch(Rng::stream(seed, index), first, n, before)
815 }))
816 .unwrap_or_else(|panic| (Err(internal(panic_detail(&*panic))), Vec::new()));
817 let stopped = result.is_err();
818 if stopped {
819 failed.fetch_min(index, Ordering::Relaxed);
820 }
821 if tx.send((index, result, lines)).is_err() || stopped {
822 break;
823 }
824 }
825 };
826 let spawned = std::thread::Builder::new()
827 .name("probl-sampler".into())
828 .stack_size(options.limits.stack_size)
829 .spawn_scoped(scope, worker);
830 if let Err(e) = spawned {
831 if t == 0 {
833 return Err(internal(format!("couldn't start a sampling thread: {e}")));
834 }
835 break;
836 }
837 }
838 drop(tx);
839
840 let mut combined = Combined::new(program.reports.len());
841 let mut pending = BTreeMap::new();
842 let mut bytes = 0;
843 for index in 0..batches {
844 let (result, lines) = loop {
845 if let Some(sent) = pending.remove(&index) {
846 break sent;
847 }
848 match rx.recv() {
849 Ok((i, result, lines)) => {
850 pending.insert(i, (result, lines));
851 }
852 Err(_) => return stop(internal("a sampling thread stopped before its batch was done".into())),
853 }
854 };
855 for (span, line) in lines {
856 bytes += line.len() + 1;
857 if bytes > options.limits.max_output {
858 return stop(interp::too_much_output(span));
859 }
860 print(&line);
861 }
862 printed.store(bytes, Ordering::Relaxed);
863 match result {
864 Ok(batch) => combined.absorb(batch),
865 Err(e) => return stop(e),
866 }
867 if let Some(progress) = &options.progress {
868 (progress.0)(((index + 1) * interp::BATCH).min(runs), runs);
869 }
870 *combined_upto.lock().unwrap_or_else(PoisonError::into_inner) = index + 1;
871 caught_up.notify_all();
872 }
873 Ok(combined)
874 })
875}
876
877#[allow(clippy::too_many_arguments)]
880fn run_batches_here(
881 program: &Program,
882 live: &Liveness,
883 conj: &Conjugacy,
884 config: &interp::Config,
885 inputs: &[Value],
886 options: &Options,
887 print: &mut (dyn FnMut(&str) + Send),
888 runs: u64,
889 seed: u64,
890) -> Result<Combined, RuntimeError> {
891 let mut ignore = |_: &str| {};
892 let mut engine = interp::Engine::new(program, live, conj, config.clone(), inputs, &mut ignore);
893 let mut combined = Combined::new(program.reports.len());
894 let mut bytes = 0;
895 for index in 0..runs.div_ceil(interp::BATCH) {
896 let first = index * interp::BATCH;
897 let n = (runs - first).min(interp::BATCH);
898 let (result, lines) = engine.run_batch(Rng::stream(seed, index), first, n, bytes);
899 for (span, line) in lines {
900 bytes += line.len() + 1;
901 if bytes > options.limits.max_output {
902 return Err(interp::too_much_output(span));
903 }
904 print(&line);
905 }
906 combined.absorb(result?);
907 if let Some(progress) = &options.progress {
908 (progress.0)(first + n, runs);
909 }
910 }
911 Ok(combined)
912}
913
914#[allow(clippy::too_many_arguments)]
916fn sampled(
917 program: &Program,
918 live: &Liveness,
919 conj: &Conjugacy,
920 config: interp::Config,
921 options: &Options,
922 print: &mut (dyn FnMut(&str) + Send),
923 runs: u64,
924 seed: u64,
925 on_error: FailureMode,
926) -> Result<Outcome, RuntimeError> {
927 let mut engine = run_batches(program, live, conj, &config, options, print, runs, seed)?;
928 let totals = engine.totals;
929 let failures = std::mem::take(&mut engine.failures);
930 if totals.weight.is_zero() && failures.is_empty() {
931 let span = engine.last_ruling_out.unwrap_or_default();
932 return Err(RuntimeError::new(span, "every run was ruled out by `observe`")
933 .with_note(format!("{} runs were tried", report::thousands(runs as i64)))
934 .with_help(
935 "the evidence may be impossible, or too unlikely for this many runs: enumerate the model, or add runs",
936 ));
937 }
938 let effective = (totals.weight * totals.weight).ratio(totals.squares);
939 let mut header = format!("sample · {} runs · seed {seed}", report::thousands(runs as i64));
940 if !failures.is_empty() {
941 let failed = match failures.runs {
942 1 => "1 run".to_string(),
943 n => format!("{} runs", report::thousands(n as i64)),
944 };
945 header.push_str(&format!(" · partial result · {failed} failed"));
946 }
947 let comparable = failures.comparable();
950 let (total, total_squares) = match comparable {
951 true => (totals.weight + failures.weight, totals.squares + failures.squares),
952 false => (totals.weight, totals.squares),
953 };
954 let evidence = total.scale(1.0 / runs as f64);
957 let n = runs as f64;
958 let evidence_se = if runs > 1 {
959 let effective = (total * total).ratio(total_squares);
960 ((n / effective - 1.0).max(0.0) / (n - 1.0)).sqrt()
961 } else {
962 f64::NAN
963 };
964 if engine.observed {
965 let of = if comparable { "" } else { " of the finished runs" };
966 header.push_str(&format!(
967 " · {}{of}",
968 evidence_estimate(evidence, evidence_se, engine.densities)
969 ));
970 header.push_str(&format!(
971 " · effective sample size {}",
972 report::thousands(effective.round() as i64)
973 ));
974 }
975 let unresolved = engine.unresolved;
976 if !unresolved.is_zero() {
977 header.push_str(&format!(
978 " · unresolved {:.1e}",
979 unresolved.ratio(totals.weight + unresolved)
980 ));
981 }
982 let format = Format {
983 fractions: false,
984 unresolved: Weight::ZERO,
985 program_total: total,
986 run_squares: Some(total_squares),
987 weighted: program.main().effects.observes,
988 reach_known: comparable,
989 integrate: None,
990 };
991 let results = report::results(&program.reports, &engine.sinks, format, unresolved);
994 let body = report::render_results(&program.reports, &results, format);
995 let output = if body.is_empty() {
996 header
997 } else {
998 format!("{header}\n\n{body}")
999 };
1000 Ok(Outcome {
1001 today: options.today,
1002 output,
1003 stats: engine.stats.clone(),
1004 unresolved,
1005 evidence: engine.observed.then_some(evidence),
1006 integration: None,
1007 densities: engine.densities,
1008 reports: std::mem::take(&mut engine.sinks),
1009 results,
1010 format,
1011 sample: Some(Sampled {
1012 runs,
1013 seed,
1014 effective,
1015 weight: totals.weight,
1016 squares: totals.squares,
1017 evidence_se,
1018 densities: engine.densities,
1019 }),
1020 data: sources(options),
1021 updates: conj
1022 .variables
1023 .iter()
1024 .enumerate()
1025 .map(|(i, v)| (v.clone(), engine.stats.updates.get(i).copied().unwrap_or_default()))
1026 .collect(),
1027 on_error,
1028 failures,
1029 finished: totals.weight,
1030 })
1031}