Skip to main content

oxilean_std/modal_logic/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{HashMap, HashSet};
6
7use super::functions::PropVar;
8use super::functions::*;
9
10/// A PDL model: finite Kripke frame with named atomic programs.
11#[derive(Debug, Clone)]
12pub struct PdlModel {
13    /// The Kripke model underlying the PDL model.
14    pub kripke: KripkeModel,
15    /// Number of atomic programs.
16    pub n_programs: usize,
17}
18impl PdlModel {
19    /// Create a new PDL model.
20    pub fn new(kripke: KripkeModel, n_programs: usize) -> Self {
21        PdlModel { kripke, n_programs }
22    }
23    /// Compute the set of worlds reachable from `start` by executing program `prog`.
24    pub fn reachable(&self, start: usize, prog: &PdlProgram) -> HashSet<usize> {
25        match prog {
26            PdlProgram::Atomic(rel) => self
27                .kripke
28                .frame
29                .successors(*rel, start)
30                .into_iter()
31                .collect(),
32            PdlProgram::Test(phi) => {
33                if self.kripke.satisfies(start, phi) {
34                    let mut s = HashSet::new();
35                    s.insert(start);
36                    s
37                } else {
38                    HashSet::new()
39                }
40            }
41            PdlProgram::Sequence(alpha, beta) => {
42                let after_alpha = self.reachable(start, alpha);
43                let mut result = HashSet::new();
44                for w in after_alpha {
45                    result.extend(self.reachable(w, beta));
46                }
47                result
48            }
49            PdlProgram::Choice(alpha, beta) => {
50                let mut r = self.reachable(start, alpha);
51                r.extend(self.reachable(start, beta));
52                r
53            }
54            PdlProgram::Star(alpha) => {
55                let mut reachable = HashSet::new();
56                reachable.insert(start);
57                loop {
58                    let current: Vec<usize> = reachable.iter().cloned().collect();
59                    let mut added = false;
60                    for w in current {
61                        for v in self.reachable(w, alpha) {
62                            if reachable.insert(v) {
63                                added = true;
64                            }
65                        }
66                    }
67                    if !added {
68                        break;
69                    }
70                }
71                reachable
72            }
73        }
74    }
75    /// Evaluate [α]φ at world `w`: all successors via α satisfy φ.
76    pub fn box_program(&self, w: usize, prog: &PdlProgram, phi: &ModalFormula) -> bool {
77        self.reachable(w, prog)
78            .iter()
79            .all(|&v| self.kripke.satisfies(v, phi))
80    }
81    /// Evaluate ⟨α⟩φ at world `w`: some successor via α satisfies φ.
82    pub fn diamond_program(&self, w: usize, prog: &PdlProgram, phi: &ModalFormula) -> bool {
83        self.reachable(w, prog)
84            .iter()
85            .any(|&v| self.kripke.satisfies(v, phi))
86    }
87}
88/// A simple mu-calculus fixed-point evaluator on finite Kripke models.
89#[derive(Debug, Clone)]
90pub struct MuCalculusEval {
91    /// The underlying Kripke model.
92    pub model: KripkeModel,
93    /// Variable environment: propositional variable -> set of worlds.
94    pub env: HashMap<PropVar, HashSet<usize>>,
95}
96impl MuCalculusEval {
97    /// Create a new evaluator with an empty environment.
98    pub fn new(model: KripkeModel) -> Self {
99        MuCalculusEval {
100            model,
101            env: HashMap::new(),
102        }
103    }
104    /// Compute the least fixed point: iterate from empty set upward until stable.
105    pub fn least_fixed_point<F>(&self, step: F) -> HashSet<usize>
106    where
107        F: Fn(&HashSet<usize>) -> HashSet<usize>,
108    {
109        let mut current: HashSet<usize> = HashSet::new();
110        loop {
111            let next = step(&current);
112            if next == current {
113                return current;
114            }
115            current = next;
116        }
117    }
118    /// Compute the greatest fixed point: iterate from all worlds downward until stable.
119    pub fn greatest_fixed_point<F>(&self, step: F) -> HashSet<usize>
120    where
121        F: Fn(&HashSet<usize>) -> HashSet<usize>,
122    {
123        let all_worlds: HashSet<usize> = (0..self.model.frame.n_worlds).collect();
124        let mut current = all_worlds;
125        loop {
126            let next = step(&current);
127            if next == current {
128                return current;
129            }
130            current = next;
131        }
132    }
133    /// Evaluate reachability: least fixed point of (φ ∨ ◇X).
134    pub fn reachability(&self, phi: &ModalFormula) -> HashSet<usize> {
135        let model = &self.model;
136        self.least_fixed_point(|x| {
137            (0..model.frame.n_worlds)
138                .filter(|&w| {
139                    model.satisfies(w, phi)
140                        || model.frame.successors(0, w).iter().any(|&v| x.contains(&v))
141                })
142                .collect()
143        })
144    }
145    /// Evaluate safety: greatest fixed point of (φ ∧ □X).
146    pub fn safety(&self, phi: &ModalFormula) -> HashSet<usize> {
147        let model = &self.model;
148        self.greatest_fixed_point(|x| {
149            (0..model.frame.n_worlds)
150                .filter(|&w| {
151                    model.satisfies(w, phi)
152                        && model.frame.successors(0, w).iter().all(|&v| x.contains(&v))
153                })
154                .collect()
155        })
156    }
157}
158/// A simple tableau prover for modal logic K.
159#[derive(Debug)]
160pub struct TableauProver {
161    /// Current set of tableau nodes (worlds).
162    pub nodes: Vec<TableauNode>,
163    /// Next available world index.
164    pub next_world: usize,
165    /// Accessibility edges created during proof search.
166    pub edges: Vec<(usize, usize)>,
167}
168impl TableauProver {
169    /// Create a new tableau prover with an initial node.
170    pub fn new() -> Self {
171        let root = TableauNode::new(0);
172        TableauProver {
173            nodes: vec![root],
174            next_world: 1,
175            edges: Vec::new(),
176        }
177    }
178    /// Attempt to prove φ is valid (i.e., ¬φ is unsatisfiable) using a simple check.
179    /// Returns true if every branch closes.
180    pub fn is_tautology(&mut self, phi: &ModalFormula) -> bool {
181        let neg = ModalFormula::not(phi.clone());
182        self.nodes[0].add_positive(neg);
183        self.nodes[0].detect_closure();
184        self.nodes[0].closed
185    }
186}
187/// The canonical model for a modal logic, built from maximal consistent sets.
188#[derive(Debug)]
189pub struct CanonicalModel {
190    /// The worlds = maximal consistent sets.
191    pub worlds: Vec<MaximalConsistentSet>,
192    /// Canonical accessibility: w R v iff {φ | □φ ∈ w} ⊆ v.
193    pub accessibility: HashSet<(usize, usize)>,
194}
195impl CanonicalModel {
196    /// Create an empty canonical model.
197    pub fn new() -> Self {
198        CanonicalModel {
199            worlds: Vec::new(),
200            accessibility: HashSet::new(),
201        }
202    }
203    /// Add a world (MCS) to the canonical model.
204    pub fn add_world(&mut self, mcs: MaximalConsistentSet) {
205        self.worlds.push(mcs);
206    }
207    /// Build accessibility relation: w R v if all □φ ∈ w have φ ∈ v.
208    pub fn build_accessibility(&mut self) {
209        let n = self.worlds.len();
210        for i in 0..n {
211            let box_formulas: Vec<ModalFormula> = self.worlds[i]
212                .formulas
213                .iter()
214                .filter_map(|f| {
215                    if let ModalFormula::Box(0, phi) = f {
216                        Some(*phi.clone())
217                    } else {
218                        None
219                    }
220                })
221                .collect();
222            for j in 0..n {
223                let all_contained = box_formulas.iter().all(|phi| self.worlds[j].contains(phi));
224                if all_contained {
225                    self.accessibility.insert((i, j));
226                }
227            }
228        }
229    }
230    /// Number of worlds in the canonical model.
231    pub fn size(&self) -> usize {
232        self.worlds.len()
233    }
234}
235/// A multi-agent epistemic model (for up to n agents).
236#[derive(Debug, Clone)]
237pub struct EpistemicModel {
238    /// Number of possible worlds.
239    pub n_worlds: usize,
240    /// Number of agents.
241    pub n_agents: usize,
242    /// Accessibility relations per agent: agent → frame relation.
243    pub agent_relations: Vec<HashSet<(usize, usize)>>,
244    /// Valuation.
245    pub valuation: HashMap<PropVar, HashSet<usize>>,
246}
247impl EpistemicModel {
248    /// Create a new epistemic model.
249    pub fn new(n_worlds: usize, n_agents: usize) -> Self {
250        EpistemicModel {
251            n_worlds,
252            n_agents,
253            agent_relations: vec![HashSet::new(); n_agents],
254            valuation: HashMap::new(),
255        }
256    }
257    /// Add an edge to agent `i`'s accessibility relation.
258    pub fn add_edge(&mut self, agent: usize, from: usize, to: usize) {
259        if agent < self.n_agents {
260            self.agent_relations[agent].insert((from, to));
261        }
262    }
263    /// Make all agent relations equivalence relations (for S5 / knowledge).
264    pub fn make_equivalence_relations(&mut self) {
265        for agent in 0..self.n_agents {
266            for w in 0..self.n_worlds {
267                self.agent_relations[agent].insert((w, w));
268            }
269            let edges: Vec<(usize, usize)> = self.agent_relations[agent].iter().cloned().collect();
270            for &(a, b) in &edges {
271                self.agent_relations[agent].insert((b, a));
272            }
273            loop {
274                let edges: Vec<(usize, usize)> =
275                    self.agent_relations[agent].iter().cloned().collect();
276                let mut changed = false;
277                for &(a, b) in &edges {
278                    for &(c, d) in &edges {
279                        if b == c && !self.agent_relations[agent].contains(&(a, d)) {
280                            self.agent_relations[agent].insert((a, d));
281                            changed = true;
282                        }
283                    }
284                }
285                if !changed {
286                    break;
287                }
288            }
289        }
290    }
291    /// Check whether agent `i` knows φ at world `w`.
292    pub fn knows(&self, agent: usize, w: usize, phi: &ModalFormula) -> bool {
293        if agent >= self.n_agents {
294            return false;
295        }
296        let successors: Vec<usize> = self.agent_relations[agent]
297            .iter()
298            .filter(|&&(f, _)| f == w)
299            .map(|&(_, t)| t)
300            .collect();
301        successors
302            .iter()
303            .all(|&v| self.satisfies_with_agent(v, phi))
304    }
305    /// Simple satisfaction check (treats modalities as agent 0).
306    fn satisfies_with_agent(&self, w: usize, phi: &ModalFormula) -> bool {
307        match phi {
308            ModalFormula::Top => true,
309            ModalFormula::Bot => false,
310            ModalFormula::Atom(p) => self.valuation.get(p).is_some_and(|s| s.contains(&w)),
311            ModalFormula::Not(psi) => !self.satisfies_with_agent(w, psi),
312            ModalFormula::And(a, b) => {
313                self.satisfies_with_agent(w, a) && self.satisfies_with_agent(w, b)
314            }
315            ModalFormula::Or(a, b) => {
316                self.satisfies_with_agent(w, a) || self.satisfies_with_agent(w, b)
317            }
318            ModalFormula::Implies(a, b) => {
319                !self.satisfies_with_agent(w, a) || self.satisfies_with_agent(w, b)
320            }
321            ModalFormula::Box(rel, psi) => {
322                let agent = *rel;
323                self.knows(agent, w, psi)
324            }
325            ModalFormula::Diamond(rel, psi) => {
326                let agent = *rel;
327                if agent >= self.n_agents {
328                    return false;
329                }
330                self.agent_relations[agent]
331                    .iter()
332                    .filter(|&&(f, _)| f == w)
333                    .any(|&(_, v)| self.satisfies_with_agent(v, psi))
334            }
335        }
336    }
337}
338/// Describes the Sahlqvist syntactic class membership (simplified).
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub enum SahlqvistClass {
341    /// Sahlqvist antecedent: positive (possibly boxed atoms)
342    Antecedent,
343    /// Sahlqvist consequent: positive formula
344    Consequent,
345    /// Full Sahlqvist formula: antecedent → consequent
346    Full,
347    /// Not Sahlqvist
348    NotSahlqvist,
349}
350/// A propositional dynamic logic program over a finite transition system.
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub enum PdlProgram {
353    /// Atomic action (index into transition table)
354    Atomic(usize),
355    /// Test: φ? — succeeds at worlds satisfying φ
356    Test(Box<ModalFormula>),
357    /// Sequential composition: α;β
358    Sequence(Box<PdlProgram>, Box<PdlProgram>),
359    /// Non-deterministic choice: α ∪ β
360    Choice(Box<PdlProgram>, Box<PdlProgram>),
361    /// Kleene star (finite iteration): α*
362    Star(Box<PdlProgram>),
363}
364/// A finite trace: a sequence of propositional valuations.
365#[derive(Debug, Clone)]
366pub struct FiniteTrace {
367    /// Each step maps propositional variables to their truth values.
368    pub steps: Vec<HashMap<PropVar, bool>>,
369}
370impl FiniteTrace {
371    /// Create a new empty trace.
372    pub fn new() -> Self {
373        FiniteTrace { steps: Vec::new() }
374    }
375    /// Append a step to the trace.
376    pub fn push(&mut self, valuation: HashMap<PropVar, bool>) {
377        self.steps.push(valuation);
378    }
379    /// Length of the trace.
380    pub fn len(&self) -> usize {
381        self.steps.len()
382    }
383    /// Check if the trace is empty.
384    pub fn is_empty(&self) -> bool {
385        self.steps.is_empty()
386    }
387    /// Check if proposition p holds at step i.
388    pub fn prop_at(&self, p: PropVar, i: usize) -> bool {
389        self.steps
390            .get(i)
391            .and_then(|s| s.get(&p))
392            .copied()
393            .unwrap_or(false)
394    }
395    /// Evaluate a temporal formula at position `i`.
396    /// Box(0,_)=Globally, Diamond(0,_)=Finally, Box(1,_)=Next.
397    pub fn satisfies(&self, i: usize, phi: &ModalFormula) -> bool {
398        if i >= self.steps.len() {
399            return false;
400        }
401        match phi {
402            ModalFormula::Top => true,
403            ModalFormula::Bot => false,
404            ModalFormula::Atom(p) => self.prop_at(*p, i),
405            ModalFormula::Not(psi) => !self.satisfies(i, psi),
406            ModalFormula::And(a, b) => self.satisfies(i, a) && self.satisfies(i, b),
407            ModalFormula::Or(a, b) => self.satisfies(i, a) || self.satisfies(i, b),
408            ModalFormula::Implies(a, b) => !self.satisfies(i, a) || self.satisfies(i, b),
409            ModalFormula::Box(0, psi) => (i..self.steps.len()).all(|j| self.satisfies(j, psi)),
410            ModalFormula::Diamond(0, psi) => (i..self.steps.len()).any(|j| self.satisfies(j, psi)),
411            ModalFormula::Box(1, psi) => i + 1 < self.steps.len() && self.satisfies(i + 1, psi),
412            ModalFormula::Diamond(1, psi) => {
413                i + 1 >= self.steps.len() || self.satisfies(i + 1, psi)
414            }
415            _ => false,
416        }
417    }
418    /// Check whether a formula holds at position 0.
419    pub fn check(&self, phi: &ModalFormula) -> bool {
420        self.satisfies(0, phi)
421    }
422}
423/// A belief revision operator implementing a simplified AGM framework.
424#[derive(Debug, Clone)]
425pub struct BeliefRevisionOp {
426    /// The current belief set K.
427    pub beliefs: Vec<ModalFormula>,
428    /// An ordering on formulas (entrenched beliefs have higher values).
429    pub entrenchment: HashMap<ModalFormula, u32>,
430}
431impl BeliefRevisionOp {
432    /// Create a new belief revision operator with an empty belief set.
433    pub fn new() -> Self {
434        BeliefRevisionOp {
435            beliefs: Vec::new(),
436            entrenchment: HashMap::new(),
437        }
438    }
439    /// Add a belief with a given entrenchment level.
440    pub fn add_belief(&mut self, phi: ModalFormula, level: u32) {
441        if !self.beliefs.contains(&phi) {
442            self.beliefs.push(phi.clone());
443        }
444        self.entrenchment.insert(phi, level);
445    }
446    /// Check if φ is believed.
447    pub fn believes(&self, phi: &ModalFormula) -> bool {
448        self.beliefs.contains(phi)
449    }
450    /// Revision K * φ: add φ, remove lower-entrenched negations of φ.
451    pub fn revise(&self, phi: &ModalFormula) -> BeliefRevisionOp {
452        let mut new_op = self.clone();
453        let neg_phi = ModalFormula::not(phi.clone());
454        let phi_level = self.entrenchment.get(phi).copied().unwrap_or(0);
455        new_op.beliefs.retain(|b| {
456            if b == &neg_phi {
457                let b_level = self.entrenchment.get(b).copied().unwrap_or(0);
458                b_level > phi_level
459            } else {
460                true
461            }
462        });
463        new_op.add_belief(phi.clone(), phi_level.max(1));
464        new_op
465    }
466    /// Contraction K ÷ φ: remove φ from the belief set.
467    pub fn contract(&self, phi: &ModalFormula) -> BeliefRevisionOp {
468        let mut new_op = self.clone();
469        new_op.beliefs.retain(|b| b != phi);
470        new_op.entrenchment.remove(phi);
471        new_op
472    }
473    /// Size of the belief set.
474    pub fn size(&self) -> usize {
475        self.beliefs.len()
476    }
477}
478/// A Kripke model: frame + valuation.
479#[derive(Debug, Clone)]
480pub struct KripkeModel {
481    /// The underlying frame.
482    pub frame: KripkeFrame,
483    /// Valuation: prop_var → set of worlds where it is true.
484    pub valuation: HashMap<PropVar, HashSet<usize>>,
485}
486impl KripkeModel {
487    /// Create a new model with no valuation.
488    pub fn new(frame: KripkeFrame) -> Self {
489        KripkeModel {
490            frame,
491            valuation: HashMap::new(),
492        }
493    }
494    /// Set proposition `p` to true at world `w`.
495    pub fn set_true(&mut self, p: PropVar, w: usize) {
496        self.valuation.entry(p).or_default().insert(w);
497    }
498    /// Check whether proposition `p` is true at world `w`.
499    pub fn prop_true(&self, p: PropVar, w: usize) -> bool {
500        self.valuation.get(&p).is_some_and(|s| s.contains(&w))
501    }
502    /// Evaluate a modal formula at world `w` (modality index = 0).
503    pub fn satisfies(&self, w: usize, phi: &ModalFormula) -> bool {
504        match phi {
505            ModalFormula::Top => true,
506            ModalFormula::Bot => false,
507            ModalFormula::Atom(p) => self.prop_true(*p, w),
508            ModalFormula::Not(psi) => !self.satisfies(w, psi),
509            ModalFormula::And(a, b) => self.satisfies(w, a) && self.satisfies(w, b),
510            ModalFormula::Or(a, b) => self.satisfies(w, a) || self.satisfies(w, b),
511            ModalFormula::Implies(a, b) => !self.satisfies(w, a) || self.satisfies(w, b),
512            ModalFormula::Box(rel, psi) => self
513                .frame
514                .successors(*rel, w)
515                .iter()
516                .all(|&v| self.satisfies(v, psi)),
517            ModalFormula::Diamond(rel, psi) => self
518                .frame
519                .successors(*rel, w)
520                .iter()
521                .any(|&v| self.satisfies(v, psi)),
522        }
523    }
524    /// Check whether φ is valid in this model (true at all worlds).
525    pub fn model_valid(&self, phi: &ModalFormula) -> bool {
526        (0..self.frame.n_worlds).all(|w| self.satisfies(w, phi))
527    }
528    /// Compute the extension [[φ]] — the set of worlds satisfying φ.
529    pub fn extension(&self, phi: &ModalFormula) -> HashSet<usize> {
530        (0..self.frame.n_worlds)
531            .filter(|&w| self.satisfies(w, phi))
532            .collect()
533    }
534}
535/// A maximally consistent set (MCS) represented as a set of formula indices.
536#[derive(Debug, Clone, PartialEq, Eq, Hash)]
537pub struct MaximalConsistentSet {
538    /// Index identifying this MCS (world in the canonical model).
539    pub id: usize,
540    /// Representative formulas in this MCS (for finite approximation).
541    pub formulas: Vec<ModalFormula>,
542}
543impl MaximalConsistentSet {
544    /// Create a new MCS with the given id and formulas.
545    pub fn new(id: usize, formulas: Vec<ModalFormula>) -> Self {
546        MaximalConsistentSet { id, formulas }
547    }
548    /// Check if a formula is a member of this MCS.
549    pub fn contains(&self, phi: &ModalFormula) -> bool {
550        self.formulas.contains(phi)
551    }
552    /// Add a formula to this MCS (used during construction).
553    pub fn add(&mut self, phi: ModalFormula) {
554        if !self.contains(&phi) {
555            self.formulas.push(phi);
556        }
557    }
558}
559/// Represents a public announcement update.
560#[derive(Debug, Clone)]
561pub struct PublicAnnouncement {
562    /// The announced formula (must be true at actual world).
563    pub announcement: ModalFormula,
564}
565impl PublicAnnouncement {
566    /// Create a public announcement.
567    pub fn new(phi: ModalFormula) -> Self {
568        PublicAnnouncement { announcement: phi }
569    }
570    /// Update an epistemic model by a public announcement:
571    /// keep only worlds where the announcement is satisfied.
572    pub fn update(&self, model: &EpistemicModel) -> EpistemicModel {
573        let surviving: HashSet<usize> = (0..model.n_worlds)
574            .filter(|&w| model.satisfies_with_agent(w, &self.announcement))
575            .collect();
576        let old_to_new: HashMap<usize, usize> = surviving
577            .iter()
578            .enumerate()
579            .map(|(new, &old)| (old, new))
580            .collect();
581        let n_new = surviving.len();
582        let mut new_model = EpistemicModel::new(n_new, model.n_agents);
583        for agent in 0..model.n_agents {
584            for &(from, to) in &model.agent_relations[agent] {
585                if let (Some(&nf), Some(&nt)) = (old_to_new.get(&from), old_to_new.get(&to)) {
586                    new_model.agent_relations[agent].insert((nf, nt));
587                }
588            }
589        }
590        for (&p, worlds) in &model.valuation {
591            for &w in worlds {
592                if let Some(&nw) = old_to_new.get(&w) {
593                    new_model.valuation.entry(p).or_default().insert(nw);
594                }
595            }
596        }
597        new_model
598    }
599}
600/// Identifies a normal modal logic by its axiom schema name.
601#[derive(Debug, Clone, PartialEq, Eq, Hash)]
602pub enum ModalSystem {
603    /// K: the minimal normal modal logic
604    K,
605    /// T (= M): K + Axiom T (reflexivity)
606    T,
607    /// S4: T + Axiom 4 (transitivity)
608    S4,
609    /// S5: S4 + Axiom B (symmetry)
610    S5,
611    /// D: K + Axiom D (seriality, deontic)
612    D,
613    /// KD45: D + Axiom 4 + Axiom 5 (doxastic belief)
614    KD45,
615    /// GL: K + Axiom Löb (provability)
616    GL,
617    /// B: K + T + B (Brouwer)
618    B,
619}
620impl ModalSystem {
621    /// Return a human-readable name for the logic.
622    pub fn name(&self) -> &'static str {
623        match self {
624            ModalSystem::K => "K",
625            ModalSystem::T => "T",
626            ModalSystem::S4 => "S4",
627            ModalSystem::S5 => "S5",
628            ModalSystem::D => "D",
629            ModalSystem::KD45 => "KD45",
630            ModalSystem::GL => "GL",
631            ModalSystem::B => "B",
632        }
633    }
634    /// Check whether the given frame satisfies the characteristic frame condition.
635    pub fn frame_validates(&self, frame: &KripkeFrame, rel: usize) -> bool {
636        match self {
637            ModalSystem::K => true,
638            ModalSystem::T => frame.is_reflexive(rel),
639            ModalSystem::S4 => frame.is_reflexive(rel) && frame.is_transitive(rel),
640            ModalSystem::S5 => {
641                frame.is_reflexive(rel) && frame.is_transitive(rel) && frame.is_symmetric(rel)
642            }
643            ModalSystem::D => frame.is_serial(rel),
644            ModalSystem::KD45 => {
645                frame.is_serial(rel) && frame.is_transitive(rel) && frame.is_euclidean(rel)
646            }
647            ModalSystem::GL => frame.is_transitive(rel),
648            ModalSystem::B => frame.is_reflexive(rel) && frame.is_symmetric(rel),
649        }
650    }
651}
652/// A finite Kripke frame with named accessibility relations.
653#[derive(Debug, Clone)]
654pub struct KripkeFrame {
655    /// Number of worlds (worlds are labeled 0..n_worlds).
656    pub n_worlds: usize,
657    /// Accessibility relations: relation_index → set of (from, to) pairs.
658    pub relations: Vec<HashSet<(usize, usize)>>,
659}
660impl KripkeFrame {
661    /// Create a new frame with `n_worlds` worlds and `n_relations` accessibility relations.
662    pub fn new(n_worlds: usize, n_relations: usize) -> Self {
663        KripkeFrame {
664            n_worlds,
665            relations: vec![HashSet::new(); n_relations],
666        }
667    }
668    /// Add an accessibility edge w → v for relation `rel`.
669    pub fn add_edge(&mut self, rel: usize, from: usize, to: usize) {
670        if rel < self.relations.len() {
671            self.relations[rel].insert((from, to));
672        }
673    }
674    /// Check whether wRv holds for relation `rel`.
675    pub fn accessible(&self, rel: usize, w: usize, v: usize) -> bool {
676        self.relations.get(rel).is_some_and(|r| r.contains(&(w, v)))
677    }
678    /// Return all worlds accessible from `w` via relation `rel`.
679    pub fn successors(&self, rel: usize, w: usize) -> Vec<usize> {
680        self.relations
681            .get(rel)
682            .map(|r| r.iter().filter(|(f, _)| *f == w).map(|(_, t)| *t).collect())
683            .unwrap_or_default()
684    }
685    /// Check if relation `rel` is reflexive.
686    pub fn is_reflexive(&self, rel: usize) -> bool {
687        (0..self.n_worlds).all(|w| self.accessible(rel, w, w))
688    }
689    /// Check if relation `rel` is transitive.
690    pub fn is_transitive(&self, rel: usize) -> bool {
691        let r = match self.relations.get(rel) {
692            Some(r) => r,
693            None => return true,
694        };
695        let pairs: Vec<(usize, usize)> = r.iter().cloned().collect();
696        for &(a, b) in &pairs {
697            for &(c, d) in &pairs {
698                if b == c && !r.contains(&(a, d)) {
699                    return false;
700                }
701            }
702        }
703        true
704    }
705    /// Check if relation `rel` is symmetric.
706    pub fn is_symmetric(&self, rel: usize) -> bool {
707        let r = match self.relations.get(rel) {
708            Some(r) => r,
709            None => return true,
710        };
711        r.iter().all(|&(a, b)| r.contains(&(b, a)))
712    }
713    /// Check if relation `rel` is serial (every world has a successor).
714    pub fn is_serial(&self, rel: usize) -> bool {
715        (0..self.n_worlds).all(|w| !self.successors(rel, w).is_empty())
716    }
717    /// Check if relation `rel` is Euclidean: wRv ∧ wRu → vRu.
718    pub fn is_euclidean(&self, rel: usize) -> bool {
719        let r = match self.relations.get(rel) {
720            Some(r) => r,
721            None => return true,
722        };
723        let pairs: Vec<(usize, usize)> = r.iter().cloned().collect();
724        for &(w, v) in &pairs {
725            for &(w2, u) in &pairs {
726                if w == w2 && !r.contains(&(v, u)) {
727                    return false;
728                }
729            }
730        }
731        true
732    }
733    /// Make relation `rel` reflexive (add all (w,w) edges).
734    pub fn make_reflexive(&mut self, rel: usize) {
735        for w in 0..self.n_worlds {
736            self.add_edge(rel, w, w);
737        }
738    }
739    /// Make relation `rel` transitive (Floyd-Warshall closure).
740    pub fn make_transitive(&mut self, rel: usize) {
741        if rel >= self.relations.len() {
742            return;
743        }
744        loop {
745            let current: Vec<(usize, usize)> = self.relations[rel].iter().cloned().collect();
746            let mut added = false;
747            for &(a, b) in &current {
748                for &(c, d) in &current {
749                    if b == c && !self.relations[rel].contains(&(a, d)) {
750                        self.relations[rel].insert((a, d));
751                        added = true;
752                    }
753                }
754            }
755            if !added {
756                break;
757            }
758        }
759    }
760}
761/// Modal formula over propositional variables.
762#[derive(Debug, Clone, PartialEq, Eq, Hash)]
763pub enum ModalFormula {
764    /// Atomic proposition p_i
765    Atom(PropVar),
766    /// Propositional constant ⊤
767    Top,
768    /// Propositional constant ⊥
769    Bot,
770    /// Negation ¬φ
771    Not(Box<ModalFormula>),
772    /// Conjunction φ ∧ ψ
773    And(Box<ModalFormula>, Box<ModalFormula>),
774    /// Disjunction φ ∨ ψ
775    Or(Box<ModalFormula>, Box<ModalFormula>),
776    /// Implication φ → ψ
777    Implies(Box<ModalFormula>, Box<ModalFormula>),
778    /// Box (necessity) □_i φ
779    Box(usize, Box<ModalFormula>),
780    /// Diamond (possibility) ◇_i φ
781    Diamond(usize, Box<ModalFormula>),
782}
783impl ModalFormula {
784    /// Create an atomic proposition.
785    pub fn atom(p: PropVar) -> Self {
786        ModalFormula::Atom(p)
787    }
788    /// Create the necessity □φ (modality 0).
789    pub fn necessity(phi: ModalFormula) -> Self {
790        ModalFormula::Box(0, Box::new(phi))
791    }
792    /// Create the possibility ◇φ (modality 0).
793    pub fn possibility(phi: ModalFormula) -> Self {
794        ModalFormula::Diamond(0, Box::new(phi))
795    }
796    /// Create implication.
797    pub fn implies(a: ModalFormula, b: ModalFormula) -> Self {
798        ModalFormula::Implies(Box::new(a), Box::new(b))
799    }
800    /// Create conjunction.
801    pub fn and(a: ModalFormula, b: ModalFormula) -> Self {
802        ModalFormula::And(Box::new(a), Box::new(b))
803    }
804    /// Create disjunction.
805    pub fn or(a: ModalFormula, b: ModalFormula) -> Self {
806        ModalFormula::Or(Box::new(a), Box::new(b))
807    }
808    /// Create negation.
809    pub fn not(a: ModalFormula) -> Self {
810        ModalFormula::Not(Box::new(a))
811    }
812    /// Collect all subformulas (including self).
813    pub fn subformulas(&self) -> Vec<ModalFormula> {
814        let mut result = vec![self.clone()];
815        match self {
816            ModalFormula::Not(phi) => result.extend(phi.subformulas()),
817            ModalFormula::And(a, b) | ModalFormula::Or(a, b) | ModalFormula::Implies(a, b) => {
818                result.extend(a.subformulas());
819                result.extend(b.subformulas());
820            }
821            ModalFormula::Box(_, phi) | ModalFormula::Diamond(_, phi) => {
822                result.extend(phi.subformulas())
823            }
824            _ => {}
825        }
826        result
827    }
828    /// Return the set of propositional variables appearing in the formula.
829    pub fn prop_vars(&self) -> HashSet<PropVar> {
830        let mut vars = HashSet::new();
831        self.collect_vars(&mut vars);
832        vars
833    }
834    fn collect_vars(&self, vars: &mut HashSet<PropVar>) {
835        match self {
836            ModalFormula::Atom(p) => {
837                vars.insert(*p);
838            }
839            ModalFormula::Not(phi) => phi.collect_vars(vars),
840            ModalFormula::And(a, b) | ModalFormula::Or(a, b) | ModalFormula::Implies(a, b) => {
841                a.collect_vars(vars);
842                b.collect_vars(vars);
843            }
844            ModalFormula::Box(_, phi) | ModalFormula::Diamond(_, phi) => phi.collect_vars(vars),
845            _ => {}
846        }
847    }
848    /// Modal depth of the formula.
849    pub fn modal_depth(&self) -> usize {
850        match self {
851            ModalFormula::Atom(_) | ModalFormula::Top | ModalFormula::Bot => 0,
852            ModalFormula::Not(phi) => phi.modal_depth(),
853            ModalFormula::And(a, b) | ModalFormula::Or(a, b) | ModalFormula::Implies(a, b) => {
854                a.modal_depth().max(b.modal_depth())
855            }
856            ModalFormula::Box(_, phi) | ModalFormula::Diamond(_, phi) => 1 + phi.modal_depth(),
857        }
858    }
859}
860/// A node in a tableau for modal logic K.
861#[derive(Debug, Clone)]
862pub struct TableauNode {
863    /// World label.
864    pub world: usize,
865    /// Set of formulas true at this world.
866    pub positive: Vec<ModalFormula>,
867    /// Set of formulas false at this world.
868    pub negative: Vec<ModalFormula>,
869    /// Whether this node is closed (contradiction found).
870    pub closed: bool,
871}
872impl TableauNode {
873    /// Create a new open tableau node.
874    pub fn new(world: usize) -> Self {
875        TableauNode {
876            world,
877            positive: Vec::new(),
878            negative: Vec::new(),
879            closed: false,
880        }
881    }
882    /// Add a formula to the positive (true) set.
883    pub fn add_positive(&mut self, phi: ModalFormula) {
884        self.positive.push(phi);
885    }
886    /// Add a formula to the negative (false) set.
887    pub fn add_negative(&mut self, phi: ModalFormula) {
888        self.negative.push(phi);
889    }
890    /// Detect closure: a formula appears both positive and negative.
891    pub fn detect_closure(&mut self) {
892        for p in &self.positive {
893            if self.negative.contains(p) {
894                self.closed = true;
895                return;
896            }
897        }
898        if self.positive.contains(&ModalFormula::Bot) || self.negative.contains(&ModalFormula::Top)
899        {
900            self.closed = true;
901        }
902    }
903}
904/// A Kripke model equipped with graded modality evaluation.
905#[derive(Debug, Clone)]
906pub struct GradedModel {
907    /// Underlying Kripke model.
908    pub model: KripkeModel,
909}
910impl GradedModel {
911    /// Create a new graded model.
912    pub fn new(model: KripkeModel) -> Self {
913        GradedModel { model }
914    }
915    /// Evaluate ◇^{≥n} φ at world w: at least n successors satisfy φ.
916    pub fn graded_diamond(&self, w: usize, n: usize, phi: &ModalFormula) -> bool {
917        let count = self
918            .model
919            .frame
920            .successors(0, w)
921            .iter()
922            .filter(|&&v| self.model.satisfies(v, phi))
923            .count();
924        count >= n
925    }
926    /// Evaluate □^{≤n} φ at world w: at most n successors fail to satisfy φ.
927    pub fn graded_box(&self, w: usize, n: usize, phi: &ModalFormula) -> bool {
928        let failures = self
929            .model
930            .frame
931            .successors(0, w)
932            .iter()
933            .filter(|&&v| !self.model.satisfies(v, phi))
934            .count();
935        failures <= n
936    }
937    /// Count the number of successors of world w satisfying φ.
938    pub fn count_satisfying(&self, w: usize, phi: &ModalFormula) -> usize {
939        self.model
940            .frame
941            .successors(0, w)
942            .iter()
943            .filter(|&&v| self.model.satisfies(v, phi))
944            .count()
945    }
946}
947/// A bisimulation between two Kripke models.
948#[derive(Debug, Clone)]
949pub struct Bisimulation {
950    /// Pairs (w, v) where w is in model 1 and v is in model 2.
951    pub pairs: HashSet<(usize, usize)>,
952}
953impl Bisimulation {
954    /// Create an empty bisimulation.
955    pub fn new() -> Self {
956        Bisimulation {
957            pairs: HashSet::new(),
958        }
959    }
960    /// Add a bisimulation pair.
961    pub fn add_pair(&mut self, w: usize, v: usize) {
962        self.pairs.insert((w, v));
963    }
964    /// Check whether two models are bisimilar at (w1, w2) given this relation.
965    /// (Simplified: only checks atom agreement, not the full forth/back conditions.)
966    pub fn is_bisimulation_pair(
967        &self,
968        _m1: &KripkeModel,
969        _m2: &KripkeModel,
970        w1: usize,
971        w2: usize,
972    ) -> bool {
973        self.pairs.contains(&(w1, w2))
974    }
975    /// Size of the bisimulation.
976    pub fn size(&self) -> usize {
977        self.pairs.len()
978    }
979}