ask_bayes/
lib.rs

1//! This library contains the core functionality of the `ask-bayes` crate.
2#![warn(
3    clippy::all,
4    clippy::restriction,
5    clippy::pedantic,
6    clippy::nursery,
7    clippy::cargo,
8    rust_2018_idioms,
9    missing_debug_implementations,
10    missing_docs
11)]
12#![allow(clippy::module_inception)]
13#![allow(clippy::implicit_return)]
14#![allow(clippy::blanket_clippy_restriction_lints)]
15#![allow(clippy::shadow_same)]
16#![allow(clippy::module_name_repetitions)]
17#![allow(clippy::cargo_common_metadata)]
18#![allow(clippy::separated_literal_suffix)]
19#![allow(clippy::float_arithmetic)]
20#![allow(clippy::struct_excessive_bools)]
21
22use anyhow::{anyhow, Error, Result};
23use clap::Parser;
24use dialoguer::Input;
25use dirs::home_dir;
26use log::info;
27use prettytable::{format, Cell, Row, Table};
28use serde_json::json;
29use sled::Db;
30use std::fmt::{Display, Formatter};
31use std::str::FromStr;
32
33/// The prelude for the `ask-bayes` crate.
34pub mod prelude {
35    pub use crate::{
36        calculate_posterior_probability, get_prior, remove_prior, report_posterior_probability,
37        set_prior, wizard, Args, Evidence, UpdateHypothesis,
38    };
39}
40
41/// Whether or not evidence supporting the hypothesis was observed
42#[derive(Debug, Clone, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum Evidence {
45    /// Evidence supporting the hypothesis was observed
46    Observed,
47    /// Evidence supporting the hypothesis was not observed
48    NotObserved,
49}
50
51impl FromStr for Evidence {
52    type Err = Error;
53
54    #[inline]
55    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
56        match s {
57            "o" | "observed" | "Observed" | "y" | "Y" => Ok(Self::Observed),
58            "n" | "not-observed" | "NotObserved" | "N" | "not observed" => Ok(Self::NotObserved),
59            _ => Err(anyhow!("Invalid evidence: {}", s)),
60        }
61    }
62}
63
64impl Display for Evidence {
65    #[inline]
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        match *self {
68            Self::Observed => write!(f, "Observed"),
69            Self::NotObserved => write!(f, "NotObserved"),
70        }
71    }
72}
73
74/// Whether or not the hypothesis should be updated in the database
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum UpdateHypothesis {
78    /// The hypothesis should be updated
79    Update,
80    /// The hypothesis should not be updated
81    NoUpdate,
82}
83
84impl FromStr for UpdateHypothesis {
85    type Err = Error;
86
87    #[inline]
88    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
89        match s {
90            "u" | "update" | "Update" | "y" | "Y" => Ok(Self::Update),
91            "n" | "no-update" | "NoUpdate" | "N" => Ok(Self::NoUpdate),
92            _ => Err(anyhow!("Invalid update hypothesis: {}", s)),
93        }
94    }
95}
96
97impl Display for UpdateHypothesis {
98    #[inline]
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        match *self {
101            Self::Update => write!(f, "Update"),
102            Self::NoUpdate => write!(f, "NoUpdate"),
103        }
104    }
105}
106
107/// The output format to use when displaying results to the user
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[non_exhaustive]
110pub enum OutputFormat {
111    /// Output in a table format
112    Table,
113    /// Output in a JSON format
114    Json,
115    /// Output in a formatted string
116    Simple,
117}
118
119impl FromStr for OutputFormat {
120    type Err = Error;
121
122    #[inline]
123    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
124        match s {
125            "table" | "Table" | "t" | "T" => Ok(Self::Table),
126            "json" | "Json" | "j" | "J" => Ok(Self::Json),
127            "simple" | "Simple" | "s" | "S" => Ok(Self::Simple),
128            _ => Err(anyhow!("Invalid output format: {}", s)),
129        }
130    }
131}
132
133impl Display for OutputFormat {
134    #[inline]
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        match *self {
137            Self::Table => write!(f, "Table"),
138            Self::Json => write!(f, "Json"),
139            Self::Simple => write!(f, "Simple"),
140        }
141    }
142}
143
144/// Arguments for the `ask-bayes` command
145#[derive(Parser, Debug)]
146#[non_exhaustive]
147#[clap(author, version, about, long_about = None)]
148pub struct Args {
149    /// Name of the Hypothesis to update
150    #[clap(
151        short,
152        long,
153        forbid_empty_values = true,
154        required_unless_present("wizard")
155    )]
156    pub name: Option<String>,
157    /// The prior probability of the hypothesis P(H)
158    #[clap(
159        short,
160        long,
161        default_value_if("name", None, Some("0.5")),
162        validator = parse_validate_probability,
163        forbid_empty_values = true,
164        required_unless_present("wizard")
165    )]
166    pub prior: Option<f64>,
167    /// The likelihood of the evidence P(E|H)
168    #[clap(
169        short,
170        long,
171        default_value_if("name", None, Some("0.5")),
172        validator = parse_validate_probability,
173        forbid_empty_values = true,
174        required_unless_present("wizard"))]
175    pub likelihood: Option<f64>,
176    /// The likelihood of the evidence P(E|¬H)
177    #[clap(
178        long,
179        default_value_if("name", None, Some("0.5")),
180        validator = parse_validate_probability,
181        forbid_empty_values = true,
182        required_unless_present("wizard"))]
183    pub likelihood_null: Option<f64>,
184    /// Indicates whether supporting evidence is observed
185    #[clap(
186        short,
187        long,
188        default_value_if("name", None, Some("Observed")),
189        default_missing_value = "Observed",
190        possible_values = ["o", "observed", "Observed", "n", "not-observed", "NotObserved"],
191        required_unless_present("wizard"))]
192    pub evidence: Option<Evidence>,
193    /// Updates the prior probability of the hypothesis P(H) to the new posterior probability, saving it to the database
194    #[clap(
195        short,
196        long,
197        default_value_if("name", None, Some("NoUpdate")),
198        default_missing_value = "Update",
199        possible_values = ["u", "update", "Update", "n", "no-update", "NoUpdate"])]
200    pub update_prior: Option<UpdateHypothesis>,
201    /// Returns the saved value of the prior probability of the hypothesis P(H).
202    /// Incompatible with other flags aside from `--name`
203    #[clap(
204        short,
205        long,
206        conflicts_with = "prior",
207        conflicts_with = "likelihood",
208        conflicts_with = "likelihood-null",
209        conflicts_with = "evidence",
210        conflicts_with = "update-prior"
211    )]
212    pub get_prior: bool,
213    /// Sets the prior probability of the hypothesis P(H) to the new value, saving it to the database.
214    /// Incompatible with other flags aside from `--name` and `--prior`
215    #[clap(
216        short,
217        long,
218        validator = parse_validate_probability,
219        conflicts_with = "prior",
220        conflicts_with = "likelihood",
221        conflicts_with = "likelihood-null",
222        conflicts_with = "evidence",
223        conflicts_with = "update-prior",
224        conflicts_with = "get-prior"
225    )]
226    pub set_prior: Option<f64>,
227    /// Removes the prior probability of the hypothesis P(H) from the database.
228    /// Incompatible with other flags aside from `--name`
229    #[clap(
230        short,
231        long,
232        conflicts_with = "prior",
233        conflicts_with = "likelihood",
234        conflicts_with = "likelihood-null",
235        conflicts_with = "evidence",
236        conflicts_with = "update-prior",
237        conflicts_with = "set-prior",
238        conflicts_with = "get-prior"
239    )]
240    pub remove_prior: bool,
241    /// Runs the wizard to help guide you through the process of updating a hypothesis
242    #[clap(short, long, exclusive = true, takes_value = false)]
243    pub wizard: bool,
244    /// The type of output to display
245    #[clap(
246        short,
247        long,
248        default_value_if("name", None, Some("Table")),
249        possible_values = ["t", "table", "Table", "T", "j", "json", "Json", "J", "s", "simple", "Simple", "S"],
250        required_unless_present("wizard")
251    )]
252    pub output: Option<OutputFormat>,
253}
254
255/// The posterior probability of the hypothesis P(H|E) if the evidence is observed, or P(H|¬E) if the evidence is not observed
256/// # Errors
257/// - If the P(E) is 0
258#[inline]
259pub fn calculate_posterior_probability(
260    prior: f64,
261    likelihood: f64,
262    likelihood_null: f64,
263    evidence: &Evidence,
264    name: &str,
265) -> Result<f64> {
266    validate_likelihoods_and_prior(prior, likelihood, likelihood_null, evidence, name)?;
267    let p_e = marginal_likelihood(prior, likelihood, likelihood_null);
268    match *evidence {
269        Evidence::Observed => {
270            // P(H|E) = P(H) * P(E|H) / P(E)
271            Ok(likelihood * prior / p_e)
272        }
273        Evidence::NotObserved => {
274            // P(H|¬E) = P(H) * P(¬E|H) / P(¬E)
275            Ok(negate(likelihood) * prior / negate(p_e))
276        }
277    }
278}
279
280/// Gets the prior probability of the hypothesis P(H) from the database.
281/// # Errors
282/// - If the prior probability of the hypothesis is not in the database
283/// - If the database cannot be opened
284/// - If the prior value is not a valid float  
285#[inline]
286#[cfg(not(tarpaulin_include))]
287pub fn get_prior(name: &str) -> Result<f64> {
288    let db = open_db()?;
289    let prior = db.get(&name)?;
290    match prior {
291        Some(prior_serialized) => {
292            let bytes = prior_serialized.as_ref();
293            Ok(f64::from_be_bytes(bytes.try_into()?))
294        }
295        None => return Err(anyhow!("Could not find hypothesis {name}")),
296    }
297}
298
299/// Sets the prior probability of the hypothesis P(H) to the new value, saving it to the database.
300/// # Errors
301/// - If the database cannot be opened
302/// - If the prior cannot be inserted into the database
303#[inline]
304#[cfg(not(tarpaulin_include))]
305pub fn set_prior(name: &str, prior: f64) -> Result<()> {
306    let db = open_db()?;
307    db.insert(name, &prior.to_be_bytes())?;
308    Ok(())
309}
310
311/// Removes the prior probability of the hypothesis P(H) from the database
312/// # Errors
313/// - If the database cannot be opened
314/// - If the prior cannot be removed from the database
315#[inline]
316#[cfg(not(tarpaulin_include))]
317pub fn remove_prior(name: &str) -> Result<()> {
318    let db = open_db()?;
319    db.remove(name)?;
320    Ok(())
321}
322
323/// Opens the hypotheses database
324/// # Errors
325/// - If the database cannot be opened
326#[inline]
327#[cfg(not(tarpaulin_include))]
328fn open_db() -> Result<Db> {
329    let hd = match home_dir() {
330        Some(hd) => hd,
331        None => return Err(anyhow!("Could not find home directory")),
332    };
333    let db_path = hd.join(".ask-bayes").join("hypotheses.db");
334    Ok(sled::open(db_path)?)
335}
336
337/// Parses and validates a probability
338fn parse_validate_probability(value: &str) -> Result<f64> {
339    let float = value.parse::<f64>()?;
340    validate_probability(float)?;
341    Ok(float)
342}
343
344/// Validates a probability.  Probabilities should be valid floats between 0 and 1.
345fn validate_probability(value: f64) -> Result<()> {
346    if !(0.0_f64..=1.0_f64).contains(&value) {
347        return Err(anyhow!("Probability must be between 0 and 1"));
348    }
349    Ok(())
350}
351
352/// Negates a probability.  Ex. P(H) -> P(¬H)
353fn negate(value: f64) -> f64 {
354    1.0_f64 - value
355}
356
357/// Checks that P(E) is not 0
358fn validate_likelihoods_and_prior(
359    prior: f64,
360    likelihood: f64,
361    likelihood_null: f64,
362    evidence: &Evidence,
363    name: &str,
364) -> Result<()> {
365    match *evidence {
366        Evidence::Observed => {
367            if marginal_likelihood(prior, likelihood, likelihood_null) <= 0.0_f64 {
368                return Err(anyhow!("The total probability of observing evidence P(E) must be greater than 0 if evidence is observed.  \r\nP(E) = P({name})[{prior}] * P(E|{name})[{likelihood}] + P(\u{ac}{name})[{}] * P(E|\u{ac}{name})[{}] = 0", negate(prior), likelihood_null));
369            }
370        }
371        Evidence::NotObserved => {
372            if negate(marginal_likelihood(prior, likelihood, likelihood_null)) <= 0.0_f64 {
373                return Err(anyhow!("The total probability of not observing evidence P(\u{ac}E) must be greater than 0 if evidence is not observed.  \r\nP(\u{ac}E) = P(\u{ac}E|{name})[{}] * P({name})[{prior}] + P(\u{ac}{name})[{}] * P(\u{ac}E|\u{ac}{name})[{}] = 0", negate(likelihood), negate(prior), negate(likelihood_null)));
374            }
375        }
376    }
377
378    Ok(())
379}
380
381///  P(H) * P(E|H) + P(¬H) * P(E|¬H), otherwise known as P(E)
382fn marginal_likelihood(prior: f64, likelihood: f64, likelihood_null: f64) -> f64 {
383    likelihood.mul_add(prior, likelihood_null * negate(prior))
384}
385
386/// Runs the wizard to guide the update of the prior probability of the hypothesis
387/// # Errors
388/// - If the prompt cannot be displayed
389#[inline]
390#[cfg(not(tarpaulin_include))]
391pub fn wizard() -> Result<()> {
392    let name = Input::<String>::new()
393        .with_prompt("Enter the name of the hypothesis")
394        .allow_empty(false)
395        .interact_text()?;
396
397    let prior = Input::<f64>::new()
398        .with_prompt(format!(
399            "Enter the prior probability of the hypothesis P({name})"
400        ))
401        .allow_empty(false)
402        .default(0.5_f64)
403        .validate_with(|v: &f64| validate_probability(*v))
404        .interact_text()?;
405
406    let likelihood = Input::<f64>::new()
407        .with_prompt(format!(
408            "Enter the likelihood of observing evidence given {name} is true P(E|{name})"
409        ))
410        .allow_empty(false)
411        .default(0.5_f64)
412        .validate_with(|v: &f64| validate_probability(*v))
413        .interact_text()?;
414
415    let likelihood_null = Input::<f64>::new()
416        .with_prompt(format!(
417            "Enter the likelihood of observing evidence given {name} is false P(E|\u{ac}{name})"
418        ))
419        .allow_empty(false)
420        .default(0.5_f64)
421        .validate_with(|v: &f64| validate_probability(*v))
422        .interact_text()?;
423
424    let evidence = Input::<Evidence>::new()
425        .with_prompt("Is evidence observed or not observed?".to_owned())
426        .allow_empty(false)
427        .default(Evidence::Observed)
428        .interact_text()?;
429
430    let posterior_probability =
431        calculate_posterior_probability(prior, likelihood, likelihood_null, &evidence, &name)?;
432
433    let output_format = Input::<OutputFormat>::new()
434        .with_prompt("How would you like the output?".to_owned())
435        .allow_empty(false)
436        .default(OutputFormat::Table)
437        .interact_text()?;
438
439    report_posterior_probability(
440        prior,
441        likelihood,
442        likelihood_null,
443        &evidence,
444        posterior_probability,
445        &name,
446        &output_format,
447    );
448
449    let update = Input::<UpdateHypothesis>::new()
450        .with_prompt("Would you like to update the prior probability?".to_owned())
451        .allow_empty(false)
452        .default(UpdateHypothesis::NoUpdate)
453        .interact_text()?;
454
455    if update == UpdateHypothesis::Update {
456        set_prior(&name, posterior_probability)?;
457        info!("P({name}) has been updated to {}", posterior_probability);
458    }
459
460    Ok(())
461}
462
463/// Reports the posterior probability of the hypothesis given the evidence.  Also reports the values of the `prior`, `likelihood`, and `likelihood_null`.
464#[inline]
465#[cfg(not(tarpaulin_include))]
466pub fn report_posterior_probability(
467    prior: f64,
468    likelihood: f64,
469    likelihood_null: f64,
470    evidence: &Evidence,
471    posterior_probability: f64,
472    name: &str,
473    output_format: &OutputFormat,
474) {
475    match *output_format {
476        OutputFormat::Table => {
477            report_table(
478                name,
479                prior,
480                likelihood,
481                likelihood_null,
482                evidence,
483                posterior_probability,
484            );
485        }
486        OutputFormat::Json => {
487            report_json(
488                name,
489                prior,
490                likelihood,
491                likelihood_null,
492                evidence,
493                posterior_probability,
494            );
495        }
496        OutputFormat::Simple => {
497            let output = format!(
498                "
499                P({name}) = {prior}
500                P(E|{name}) = {likelihood}
501                P(E|\u{ac}{name}) = {likelihood_null}
502                P({name}|E) = {posterior_probability}
503                "
504            );
505            info!("{output}");
506        }
507    }
508}
509
510/// Reports the posterior probability of the hypothesis given the evidence in a table format.
511#[cfg(not(tarpaulin_include))]
512fn report_table(
513    name: &str,
514    prior: f64,
515    likelihood: f64,
516    likelihood_null: f64,
517    evidence: &Evidence,
518    posterior_probability: f64,
519) {
520    let marginal_likelihood = marginal_likelihood(prior, likelihood, likelihood_null);
521    let mut table = Table::new();
522    table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
523    table.set_titles(Row::new(vec![
524        Cell::new("Name"),
525        Cell::new("Probability"),
526        Cell::new("Value"),
527    ]));
528    table.add_row(Row::new(vec![
529        Cell::new("Prior"),
530        Cell::new(&format!("P({name})")),
531        Cell::new(&format!("{prior}")),
532    ]));
533    table.add_row(Row::new(vec![
534        Cell::new("Likelihood"),
535        Cell::new(&format!("P(E|{name})")),
536        Cell::new(&format!("{likelihood}")),
537    ]));
538    table.add_row(Row::new(vec![
539        Cell::new("Likelihood Null"),
540        Cell::new(&format!("P(E|\u{ac}{name})")),
541        Cell::new(&format!("{likelihood_null}")),
542    ]));
543    table.add_row(Row::new(vec![
544        Cell::new("Marginal Likelihood"),
545        Cell::new("P(E)"),
546        Cell::new(&format!("{marginal_likelihood}")),
547    ]));
548
549    match *evidence {
550        Evidence::Observed => table.add_row(Row::new(vec![
551            Cell::new("Posterior Probability"),
552            Cell::new(&format!("P({name}|E)")),
553            Cell::new(&format!("{posterior_probability}")),
554        ])),
555        Evidence::NotObserved => table.add_row(Row::new(vec![
556            Cell::new(&format!("P({name}|\u{ac}E)")),
557            Cell::new(&format!("{posterior_probability}")),
558        ])),
559    };
560
561    table.printstd();
562}
563
564/// Reports the posterior probability of the hypothesis given the evidence in a JSON format.
565#[cfg(not(tarpaulin_include))]
566fn report_json(
567    name: &str,
568    prior: f64,
569    likelihood: f64,
570    likelihood_null: f64,
571    evidence: &Evidence,
572    posterior_probability: f64,
573) {
574    let json = json!({
575        "name": name,
576        "prior": prior,
577        "likelihood": likelihood,
578        "likelihood_null": likelihood_null,
579        "evidence": match *evidence {
580            Evidence::Observed => "observed",
581            Evidence::NotObserved => "not observed",
582        },
583        "posterior_probability": posterior_probability,
584    });
585
586    info!("{}", json.to_string());
587}
588
589#[cfg(test)]
590#[allow(clippy::panic_in_result_fn)]
591mod tests {
592    use super::*;
593
594    fn epsilon_compare(a: f64, b: f64) -> bool {
595        (a - b).abs() < f64::EPSILON
596    }
597
598    #[test]
599    fn it_validates_a_valid_probability() -> Result<()> {
600        let prob = "0.75";
601        let result = parse_validate_probability(prob)?;
602        assert!(epsilon_compare(result, 0.75_f64));
603        Ok(())
604    }
605
606    #[test]
607    fn it_fails_to_validate_a_probability_greater_than_1() {
608        let prob = "1.1";
609        let result = parse_validate_probability(prob);
610        assert!(result.is_err());
611    }
612
613    #[test]
614    fn it_fails_to_validate_a_probability_less_than_0() {
615        let prob = "-0.1";
616        let result = parse_validate_probability(prob);
617        assert!(result.is_err());
618    }
619
620    #[test]
621    fn it_fails_to_validate_an_invalid_float() {
622        let prob = "invalid";
623        let result = parse_validate_probability(prob);
624        assert!(result.is_err());
625    }
626
627    #[test]
628    fn it_validates_a_valid_pair_of_likelihoods() -> Result<()> {
629        let likelihood = 0.75_f64;
630        let likelihood_null = 0.25_f64;
631        let prior = 0.5_f64;
632        let evidence = Evidence::Observed;
633        let name = "test";
634        validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name)
635    }
636
637    #[test]
638    fn it_validates_a_valid_pair_of_negated_likelihoods() -> Result<()> {
639        let prior = 0.5_f64;
640        let likelihood = 0.75_f64;
641        let likelihood_null = 0.25_f64;
642        let evidence = Evidence::NotObserved;
643        let name = "test";
644        validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name)
645    }
646
647    #[test]
648    fn it_fails_to_validate_a_pair_of_likelihoods_with_evidence_observed_when_the_sum_is_less_than_or_equal_to_0(
649    ) {
650        let prior = 0.5_f64;
651        let likelihood = 0.0_f64;
652        let likelihood_null = 0.0_f64;
653        let evidence = Evidence::Observed;
654        let name = "test";
655        let result =
656            validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name);
657        assert!(result.is_err());
658    }
659
660    #[test]
661    fn it_fails_to_validate_a_pair_of_negated_likelihoods_with_evidence_not_observed_when_the_negated_sum_is_less_than_or_equal_to_0(
662    ) {
663        let prior = 0.5_f64;
664        let likelihood = 1.0_f64;
665        let likelihood_null = 1.0_f64;
666        let evidence = Evidence::NotObserved;
667        let name = "test";
668        let result =
669            validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name);
670        assert!(result.is_err());
671    }
672
673    #[test]
674    fn it_parses_a_valid_evidence_string() -> Result<()> {
675        {
676            let evidence = "observed";
677            let result = Evidence::from_str(evidence)?;
678            assert_eq!(result, Evidence::Observed);
679        }
680        {
681            let evidence = "o";
682            let result = Evidence::from_str(evidence)?;
683            assert_eq!(result, Evidence::Observed);
684        }
685        {
686            let evidence = "Observed";
687            let result = Evidence::from_str(evidence)?;
688            assert_eq!(result, Evidence::Observed);
689        }
690        {
691            let evidence = "not-observed";
692            let result = Evidence::from_str(evidence)?;
693            assert_eq!(result, Evidence::NotObserved);
694        }
695        {
696            let evidence = "n";
697            let result = Evidence::from_str(evidence)?;
698            assert_eq!(result, Evidence::NotObserved);
699        }
700        {
701            let evidence = "NotObserved";
702            let result = Evidence::from_str(evidence)?;
703            assert_eq!(result, Evidence::NotObserved);
704        }
705        Ok(())
706    }
707
708    #[test]
709    fn it_fails_to_parse_an_invalid_evidence_string() {
710        let evidence = "invalid";
711        let result = Evidence::from_str(evidence);
712        assert!(result.is_err());
713    }
714
715    #[test]
716    fn it_displays_a_valid_evidence_string() {
717        {
718            let evidence = Evidence::Observed;
719            let result = evidence.to_string();
720            assert_eq!(result, "Observed");
721        }
722        {
723            let evidence = Evidence::NotObserved;
724            let result = evidence.to_string();
725            assert_eq!(result, "NotObserved");
726        }
727    }
728
729    #[test]
730    fn it_parses_a_valid_update_string() -> Result<()> {
731        {
732            let update = "update";
733            let result = UpdateHypothesis::from_str(update)?;
734            assert_eq!(result, UpdateHypothesis::Update);
735        }
736        {
737            let update = "u";
738            let result = UpdateHypothesis::from_str(update)?;
739            assert_eq!(result, UpdateHypothesis::Update);
740        }
741        {
742            let update = "Update";
743            let result = UpdateHypothesis::from_str(update)?;
744            assert_eq!(result, UpdateHypothesis::Update);
745        }
746        {
747            let update = "no-update";
748            let result = UpdateHypothesis::from_str(update)?;
749            assert_eq!(result, UpdateHypothesis::NoUpdate);
750        }
751        {
752            let update = "n";
753            let result = UpdateHypothesis::from_str(update)?;
754            assert_eq!(result, UpdateHypothesis::NoUpdate);
755        }
756        {
757            let update = "NoUpdate";
758            let result = UpdateHypothesis::from_str(update)?;
759            assert_eq!(result, UpdateHypothesis::NoUpdate);
760        }
761        Ok(())
762    }
763
764    #[test]
765    fn it_fails_to_parse_an_invalid_update_string() {
766        let update = "invalid";
767        let result = UpdateHypothesis::from_str(update);
768        assert!(result.is_err());
769    }
770
771    #[test]
772    fn it_displays_a_valid_update_string() {
773        {
774            let update = UpdateHypothesis::Update;
775            let result = update.to_string();
776            assert_eq!(result, "Update");
777        }
778        {
779            let update = UpdateHypothesis::NoUpdate;
780            let result = update.to_string();
781            assert_eq!(result, "NoUpdate");
782        }
783    }
784
785    #[test]
786    fn it_calculates_the_posterior_probability_when_evidence_is_observed() -> Result<()> {
787        let prior = 0.75_f64;
788        let likelihood = 0.75_f64;
789        let likelihood_null = 0.5_f64;
790        let evidence = Evidence::Observed;
791        let name = "test";
792        let result =
793            calculate_posterior_probability(prior, likelihood, likelihood_null, &evidence, name)?;
794        assert!(epsilon_compare(result, 0.818_181_818_181_818_2_f64));
795        Ok(())
796    }
797
798    #[test]
799    fn it_calculates_the_posterior_probability_when_evidence_is_not_observed() -> Result<()> {
800        let prior = 0.75_f64;
801        let likelihood = 0.75_f64;
802        let likelihood_null = 0.5_f64;
803        let evidence = Evidence::NotObserved;
804        let name = "test";
805        let result =
806            calculate_posterior_probability(prior, likelihood, likelihood_null, &evidence, name)?;
807        assert!(epsilon_compare(result, 0.6));
808        Ok(())
809    }
810
811    #[test]
812    fn it_fails_to_validate_likelihoods_and_hypothesis_when_the_negated_prior_is_zero() {
813        let name = "test";
814        let prior = 1.0_f64;
815        let likelihood = 1.0_f64;
816        let likelihood_null = 0.5_f64;
817        let evidence = Evidence::NotObserved;
818        let result =
819            validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name);
820        assert!(result.is_err());
821    }
822
823    #[test]
824    fn it_fails_to_validate_likelihoods_and_hypothesis_when_the_prior_is_zero() {
825        let name = "test";
826        let prior = 0.0_f64;
827        let likelihood = 0.5_f64;
828        let likelihood_null = 0.0_f64;
829        let evidence = Evidence::Observed;
830        let result =
831            validate_likelihoods_and_prior(prior, likelihood, likelihood_null, &evidence, name);
832        assert!(result.is_err());
833    }
834
835    #[test]
836    fn it_parses_a_valid_output_format() -> Result<()> {
837        {
838            let format = "json";
839            let result = OutputFormat::from_str(format)?;
840            assert_eq!(result, OutputFormat::Json);
841        }
842        {
843            let format = "j";
844            let result = OutputFormat::from_str(format)?;
845            assert_eq!(result, OutputFormat::Json);
846        }
847        {
848            let format = "Json";
849            let result = OutputFormat::from_str(format)?;
850            assert_eq!(result, OutputFormat::Json);
851        }
852        {
853            let format = "J";
854            let result = OutputFormat::from_str(format)?;
855            assert_eq!(result, OutputFormat::Json);
856        }
857        {
858            let format = "t";
859            let result = OutputFormat::from_str(format)?;
860            assert_eq!(result, OutputFormat::Table);
861        }
862        {
863            let format = "Table";
864            let result = OutputFormat::from_str(format)?;
865            assert_eq!(result, OutputFormat::Table);
866        }
867        {
868            let format = "table";
869            let result = OutputFormat::from_str(format)?;
870            assert_eq!(result, OutputFormat::Table);
871        }
872        {
873            let format = "T";
874            let result = OutputFormat::from_str(format)?;
875            assert_eq!(result, OutputFormat::Table);
876        }
877        {
878            let format = "simple";
879            let result = OutputFormat::from_str(format)?;
880            assert_eq!(result, OutputFormat::Simple);
881        }
882        {
883            let format = "s";
884            let result = OutputFormat::from_str(format)?;
885            assert_eq!(result, OutputFormat::Simple);
886        }
887        {
888            let format = "S";
889            let result = OutputFormat::from_str(format)?;
890            assert_eq!(result, OutputFormat::Simple);
891        }
892        {
893            let format = "simple";
894            let result = OutputFormat::from_str(format)?;
895            assert_eq!(result, OutputFormat::Simple);
896        }
897        {
898            let format = "Simple";
899            let result = OutputFormat::from_str(format)?;
900            assert_eq!(result, OutputFormat::Simple);
901        }
902
903        Ok(())
904    }
905
906    #[test]
907    fn it_fails_to_parse_an_invalid_output_format() {
908        let format = "invalid";
909        let result = OutputFormat::from_str(format);
910        assert!(result.is_err());
911    }
912
913    #[test]
914    fn it_displays_a_valid_output_format() {
915        {
916            let format = OutputFormat::Json;
917            let result = format.to_string();
918            assert_eq!(result, "Json");
919        }
920        {
921            let format = OutputFormat::Table;
922            let result = format.to_string();
923            assert_eq!(result, "Table");
924        }
925        {
926            let format = OutputFormat::Simple;
927            let result = format.to_string();
928            assert_eq!(result, "Simple");
929        }
930    }
931}