argumentation/aspic/
postulates.rs1use super::argument::{Argument, ArgumentId};
23use super::language::Literal;
24use super::rules::{Rule, RuleKind};
25use std::collections::HashSet;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PostulateReport {
31 pub violations: Vec<PostulateViolation>,
33}
34
35impl PostulateReport {
36 pub fn is_clean(&self) -> bool {
38 self.violations.is_empty()
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum PostulateViolation {
45 SubArgumentNotInExtension {
48 parent: ArgumentId,
50 sub: ArgumentId,
52 },
53 StrictClosureViolation {
57 missing: Literal,
60 },
61 DirectInconsistency {
64 literal: Literal,
67 },
68 IndirectInconsistency {
71 literal: Literal,
73 },
74}
75
76pub fn check_postulates(
78 arguments: &[Argument],
79 rules: &[Rule],
80 extension: &HashSet<ArgumentId>,
81) -> PostulateReport {
82 let mut violations = Vec::new();
83
84 for arg in arguments {
86 if !extension.contains(&arg.id) {
87 continue;
88 }
89 for sub_id in &arg.sub_arguments {
90 if !extension.contains(sub_id) {
91 violations.push(PostulateViolation::SubArgumentNotInExtension {
92 parent: arg.id,
93 sub: *sub_id,
94 });
95 }
96 }
97 }
98
99 let conclusions: HashSet<Literal> = arguments
101 .iter()
102 .filter(|a| extension.contains(&a.id))
103 .map(|a| a.conclusion.clone())
104 .collect();
105
106 for lit in &conclusions {
108 if conclusions.contains(&lit.contrary()) && !matches!(lit, Literal::Neg(_)) {
109 violations.push(PostulateViolation::DirectInconsistency {
110 literal: lit.clone(),
111 });
112 }
113 }
114
115 let closure = strict_closure(&conclusions, rules);
117 for lit in &closure {
118 if !conclusions.contains(lit) {
119 violations.push(PostulateViolation::StrictClosureViolation {
120 missing: lit.clone(),
121 });
122 }
123 }
124
125 for lit in &closure {
127 if closure.contains(&lit.contrary()) && !matches!(lit, Literal::Neg(_)) {
128 violations.push(PostulateViolation::IndirectInconsistency {
129 literal: lit.clone(),
130 });
131 }
132 }
133
134 PostulateReport { violations }
135}
136
137fn strict_closure(initial: &HashSet<Literal>, rules: &[Rule]) -> HashSet<Literal> {
139 let mut closure = initial.clone();
140 loop {
141 let before = closure.len();
142 for rule in rules.iter().filter(|r| r.kind == RuleKind::Strict) {
143 if rule.premises.iter().all(|p| closure.contains(p)) {
144 closure.insert(rule.conclusion.clone());
145 }
146 }
147 if closure.len() == before {
148 return closure;
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::aspic::argument::construct_arguments;
157 use crate::aspic::kb::KnowledgeBase;
158 use crate::aspic::rules::{Rule, RuleId};
159
160 #[test]
161 fn clean_system_has_no_violations() {
162 let mut kb = KnowledgeBase::new();
163 kb.add_ordinary(Literal::atom("p"));
164 let rules = vec![Rule::defeasible(
165 RuleId(0),
166 vec![Literal::atom("p")],
167 Literal::atom("q"),
168 )];
169 let args = construct_arguments(&kb, &rules).unwrap();
170 let extension: HashSet<ArgumentId> = args.iter().map(|a| a.id).collect();
171 let report = check_postulates(&args, &rules, &extension);
172 assert!(
173 report.is_clean(),
174 "expected clean report, got {:?}",
175 report.violations
176 );
177 }
178
179 #[test]
180 fn direct_inconsistency_is_detected() {
181 let mut kb = KnowledgeBase::new();
182 kb.add_ordinary(Literal::atom("p"));
183 kb.add_ordinary(Literal::atom("r"));
184 let rules = vec![Rule::defeasible(
185 RuleId(0),
186 vec![Literal::atom("r")],
187 Literal::neg("p"),
188 )];
189 let args = construct_arguments(&kb, &rules).unwrap();
190 let extension: HashSet<ArgumentId> = args.iter().map(|a| a.id).collect();
191 let report = check_postulates(&args, &rules, &extension);
192 assert!(
193 report
194 .violations
195 .iter()
196 .any(|v| matches!(v, PostulateViolation::DirectInconsistency { .. })),
197 "expected direct inconsistency, got {:?}",
198 report.violations
199 );
200 }
201}