#[cfg(test)]
use super::composition::Evaluation;
use super::composition::Proposition;
use std::fmt::Debug;
#[derive(Debug)]
pub struct Deduction<Ctx: Debug> {
pub premises: Vec<Box<dyn Proposition<Context = Ctx>>>,
pub rule: Box<dyn Proposition<Context = Ctx>>,
pub conclusion: Box<dyn Proposition<Context = Ctx>>,
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DeductionResult {
Valid { conclusion: String },
Unsound { conclusion: String, reason: String },
Inapplicable {
failed_premise: String,
reason: String,
},
}
impl<Ctx: Debug> Deduction<Ctx> {
pub fn new(
name: impl Into<String>,
premises: Vec<Box<dyn Proposition<Context = Ctx>>>,
rule: Box<dyn Proposition<Context = Ctx>>,
conclusion: Box<dyn Proposition<Context = Ctx>>,
) -> Self {
Self {
premises,
rule,
conclusion,
name: name.into(),
}
}
pub fn apply(&self, context: &Ctx) -> DeductionResult {
for premise in &self.premises {
let result = premise.evaluate(context);
if !result.is_satisfied() {
return DeductionResult::Inapplicable {
failed_premise: premise.describe(),
reason: result.reason().to_string(),
};
}
}
let rule_result = self.rule.evaluate(context);
if !rule_result.is_satisfied() {
return DeductionResult::Unsound {
conclusion: self.conclusion.describe(),
reason: rule_result.reason().to_string(),
};
}
let conclusion_result = self.conclusion.evaluate(context);
if conclusion_result.is_satisfied() {
DeductionResult::Valid {
conclusion: self.conclusion.describe(),
}
} else {
DeductionResult::Unsound {
conclusion: self.conclusion.describe(),
reason: format!(
"rule '{}' passed but conclusion failed: {}",
self.rule.describe(),
conclusion_result.reason()
),
}
}
}
}
#[derive(Debug)]
pub struct Induction<Ctx: Debug> {
pub hypothesis: Box<dyn Proposition<Context = Ctx>>,
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InductionResult {
pub supported: bool,
pub instances_checked: usize,
pub counterexamples: Vec<String>,
}
impl<Ctx: Debug> Induction<Ctx> {
pub fn new(name: impl Into<String>, hypothesis: Box<dyn Proposition<Context = Ctx>>) -> Self {
Self {
hypothesis,
name: name.into(),
}
}
pub fn test(&self, instances: &[Ctx]) -> InductionResult {
let mut counterexamples = Vec::new();
for instance in instances {
let result = self.hypothesis.evaluate(instance);
if !result.is_satisfied() {
counterexamples.push(format!("{:?}: {}", instance, result.reason()));
}
}
InductionResult {
supported: counterexamples.is_empty(),
instances_checked: instances.len(),
counterexamples,
}
}
}
#[derive(Debug)]
pub struct Abduction<Ctx: Debug> {
pub observation: Box<dyn Proposition<Context = Ctx>>,
pub explanations: Vec<Box<dyn Proposition<Context = Ctx>>>,
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AbductionResult {
Explained {
first_consistent: String,
all_consistent: Vec<String>,
},
Unexplained { observation: String },
NoObservation { reason: String },
}
impl<Ctx: Debug> Abduction<Ctx> {
pub fn new(
name: impl Into<String>,
observation: Box<dyn Proposition<Context = Ctx>>,
explanations: Vec<Box<dyn Proposition<Context = Ctx>>>,
) -> Self {
Self {
observation,
explanations,
name: name.into(),
}
}
pub fn infer(&self, context: &Ctx) -> AbductionResult {
let obs_result = self.observation.evaluate(context);
if !obs_result.is_satisfied() {
return AbductionResult::NoObservation {
reason: obs_result.reason().to_string(),
};
}
let consistent: Vec<String> = self
.explanations
.iter()
.filter(|e| e.evaluate(context).is_satisfied())
.map(|e| e.describe())
.collect();
if consistent.is_empty() {
AbductionResult::Unexplained {
observation: self.observation.describe(),
}
} else {
AbductionResult::Explained {
first_consistent: consistent[0].clone(),
all_consistent: consistent,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::logic::composition::Implies;
use proptest::prelude::*;
#[derive(Debug)]
struct IsPositive;
impl Proposition for IsPositive {
type Context = i32;
fn evaluate(&self, n: &i32) -> Evaluation {
if *n > 0 {
Evaluation::Satisfied {
reason: format!("{} > 0", n),
}
} else {
Evaluation::Violated {
reason: format!("{} <= 0", n),
}
}
}
fn describe(&self) -> String {
"is positive".into()
}
}
#[derive(Debug)]
struct IsEven;
impl Proposition for IsEven {
type Context = i32;
fn evaluate(&self, n: &i32) -> Evaluation {
if n % 2 == 0 {
Evaluation::Satisfied {
reason: format!("{} is even", n),
}
} else {
Evaluation::Violated {
reason: format!("{} is odd", n),
}
}
}
fn describe(&self) -> String {
"is even".into()
}
}
#[derive(Debug)]
struct LessThan(i32);
impl Proposition for LessThan {
type Context = i32;
fn evaluate(&self, n: &i32) -> Evaluation {
if *n < self.0 {
Evaluation::Satisfied {
reason: format!("{} < {}", n, self.0),
}
} else {
Evaluation::Violated {
reason: format!("{} >= {}", n, self.0),
}
}
}
fn describe(&self) -> String {
format!("less than {}", self.0)
}
}
#[test]
fn test_deduction_valid() {
let d = Deduction::new(
"positive implies positive",
vec![Box::new(IsPositive)],
Box::new(Implies::new(IsPositive, IsPositive)),
Box::new(IsPositive),
);
let result = d.apply(&5);
assert!(matches!(result, DeductionResult::Valid { .. }));
}
#[test]
fn test_deduction_inapplicable() {
let d = Deduction::new(
"positive implies even",
vec![Box::new(IsPositive)],
Box::new(Implies::new(IsPositive, IsEven)),
Box::new(IsEven),
);
let result = d.apply(&-4);
assert!(matches!(result, DeductionResult::Inapplicable { .. }));
}
#[test]
fn test_deduction_unsound() {
let d = Deduction::new(
"positive implies even",
vec![Box::new(IsPositive)],
Box::new(Implies::new(IsPositive, IsEven)),
Box::new(IsEven),
);
let result = d.apply(&3);
assert!(matches!(result, DeductionResult::Unsound { .. }));
}
#[test]
fn test_deduction_multiple_premises() {
let d = Deduction::new(
"small positive even",
vec![Box::new(IsPositive), Box::new(IsEven)],
Box::new(LessThan(100)),
Box::new(LessThan(100)),
);
assert!(matches!(d.apply(&4), DeductionResult::Valid { .. }));
assert!(matches!(d.apply(&3), DeductionResult::Inapplicable { .. }));
}
#[test]
fn test_induction_supported() {
let ind = Induction::new("all positive", Box::new(IsPositive));
let instances = vec![1, 2, 3, 4, 5];
let result = ind.test(&instances);
assert!(result.supported);
assert_eq!(result.instances_checked, 5);
assert!(result.counterexamples.is_empty());
}
#[test]
fn test_induction_refuted() {
let ind = Induction::new("all even", Box::new(IsEven));
let instances = vec![2, 4, 5, 6];
let result = ind.test(&instances);
assert!(!result.supported);
assert_eq!(result.counterexamples.len(), 1);
}
#[test]
fn test_induction_empty() {
let ind = Induction::new("no evidence", Box::new(IsPositive));
let result = ind.test(&[]);
assert!(result.supported); assert_eq!(result.instances_checked, 0);
assert!(result.counterexamples.is_empty());
}
#[test]
fn test_abduction_explained() {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![Box::new(IsEven), Box::new(LessThan(100))],
);
let result = abd.infer(&4);
match result {
AbductionResult::Explained { all_consistent, .. } => {
assert_eq!(all_consistent.len(), 2);
}
_ => panic!("expected Explained"),
}
}
#[test]
fn test_abduction_partial() {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![Box::new(IsEven), Box::new(LessThan(100))],
);
let result = abd.infer(&3);
match result {
AbductionResult::Explained { all_consistent, .. } => {
assert_eq!(all_consistent.len(), 1);
}
_ => panic!("expected Explained"),
}
}
#[test]
fn test_abduction_unexplained() {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![Box::new(IsEven), Box::new(LessThan(5))],
);
let result = abd.infer(&7);
assert!(matches!(result, AbductionResult::Unexplained { .. }));
}
#[test]
fn test_abduction_no_observation() {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![Box::new(IsEven)],
);
let result = abd.infer(&-4);
assert!(matches!(result, AbductionResult::NoObservation { .. }));
}
proptest! {
#[test]
fn prop_deduction_tautology(n in -100..100i32) {
let d = Deduction::new(
"tautology",
vec![Box::new(IsPositive)],
Box::new(Implies::new(IsPositive, IsPositive)),
Box::new(IsPositive),
);
let result = d.apply(&n);
let is_unsound = matches!(result, DeductionResult::Unsound { .. });
prop_assert!(!is_unsound);
}
#[test]
fn prop_induction_positive_instances(n in 1..100i32) {
let instances: Vec<i32> = (1..=n).collect();
let ind = Induction::new("all positive", Box::new(IsPositive));
let result = ind.test(&instances);
prop_assert!(result.supported);
}
#[test]
fn prop_induction_counterexample_count(n in 1..50i32) {
let instances: Vec<i32> = (-n..=n).collect();
let ind = Induction::new("all positive", Box::new(IsPositive));
let result = ind.test(&instances);
let expected_failures = (n + 1) as usize; prop_assert_eq!(result.counterexamples.len(), expected_failures);
}
#[test]
fn prop_abduction_no_observation(n in -100..-1i32) {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![Box::new(IsEven)],
);
let result = abd.infer(&n);
let is_no_obs = matches!(result, AbductionResult::NoObservation { .. });
prop_assert!(is_no_obs);
}
#[test]
fn prop_abduction_consistent_bounded(n in 1..100i32) {
let abd = Abduction::new(
"why positive?",
Box::new(IsPositive),
vec![
Box::new(IsEven),
Box::new(LessThan(50)),
Box::new(LessThan(100)),
],
);
if let AbductionResult::Explained { all_consistent, .. } = abd.infer(&n) {
prop_assert!(all_consistent.len() <= 3);
}
}
}
}