Skip to main content

qualia_core_db/modalities/
modal.rs

1use crate::NQuin;
2
3/// General modal logic over a Kripke frame — the shared modal substrate (□/◇) that
4/// the *specific* modalities (deontic, epistemic) specialise. Accessibility is
5/// `(world →accesses→ world')` (predicate == `accesses`); a world satisfies a
6/// proposition when `(world, holds, prop)` is present. Zero-heap (linear scans).
7///
8/// With S4 (reflexive+transitive) or S5 (equivalence) accessibility supplied as
9/// edges, this yields the corresponding modal system; the evaluator itself is
10/// frame-agnostic (it reads whatever accessibility edges are asserted).
11
12/// **◇φ (possible)** — SOME world accessible from `world` satisfies `prop`.
13pub fn possible(graph: &[NQuin], world: u64, prop: u64, accesses: u64, holds: u64) -> bool {
14    for q in graph {
15        if q.subject == world && q.predicate == accesses {
16            let w2 = q.object;
17            if graph
18                .iter()
19                .any(|r| r.subject == w2 && r.predicate == holds && r.object == prop)
20            {
21                return true;
22            }
23        }
24    }
25    false
26}
27
28/// **□φ (necessary)** — ALL worlds accessible from `world` satisfy `prop`.
29/// Vacuously true when no worlds are accessible (standard modal semantics).
30pub fn necessary(graph: &[NQuin], world: u64, prop: u64, accesses: u64, holds: u64) -> bool {
31    for q in graph {
32        if q.subject == world && q.predicate == accesses {
33            let w2 = q.object;
34            if !graph
35                .iter()
36                .any(|r| r.subject == w2 && r.predicate == holds && r.object == prop)
37            {
38                return false; // an accessible world fails the proposition
39            }
40        }
41    }
42    true
43}
44
45// ─── Kripke frame properties → normal modal axiom systems (K, T, D, B, S4, S5) ──────
46//
47// Each characteristic axiom of a normal modal logic corresponds to a structural property
48// of the accessibility relation `R`. The frame's domain `worlds` is supplied explicitly
49// (zero-heap; no allocation, just nested slice scans).
50
51/// Is `(from → to)` an accessibility edge?
52#[inline]
53fn edge(graph: &[NQuin], from: u64, to: u64, accesses: u64) -> bool {
54    graph
55        .iter()
56        .any(|q| q.subject == from && q.predicate == accesses && q.object == to)
57}
58
59/// **Reflexive** (axiom T: □φ→φ): every world accesses itself.
60pub fn is_reflexive(graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
61    worlds.iter().all(|&w| edge(graph, w, w, accesses))
62}
63
64/// **Serial** (axiom D: □φ→◇φ): every world accesses at least one world.
65pub fn is_serial(graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
66    worlds
67        .iter()
68        .all(|&w| worlds.iter().any(|&v| edge(graph, w, v, accesses)))
69}
70
71/// **Symmetric** (axiom B: φ→□◇φ): `wRv ⇒ vRw`.
72pub fn is_symmetric(graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
73    for &w in worlds {
74        for &v in worlds {
75            if edge(graph, w, v, accesses) && !edge(graph, v, w, accesses) {
76                return false;
77            }
78        }
79    }
80    true
81}
82
83/// **Transitive** (axiom 4: □φ→□□φ): `wRv ∧ vRu ⇒ wRu`.
84pub fn is_transitive(graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
85    for &w in worlds {
86        for &v in worlds {
87            if !edge(graph, w, v, accesses) {
88                continue;
89            }
90            for &u in worlds {
91                if edge(graph, v, u, accesses) && !edge(graph, w, u, accesses) {
92                    return false;
93                }
94            }
95        }
96    }
97    true
98}
99
100/// **Euclidean** (axiom 5: ◇φ→□◇φ): `wRv ∧ wRu ⇒ vRu`.
101pub fn is_euclidean(graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
102    for &w in worlds {
103        for &v in worlds {
104            if !edge(graph, w, v, accesses) {
105                continue;
106            }
107            for &u in worlds {
108                if edge(graph, w, u, accesses) && !edge(graph, v, u, accesses) {
109                    return false;
110                }
111            }
112        }
113    }
114    true
115}
116
117/// The normal modal axiom systems this engine recognises.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum ModalSystem {
120    /// The base system — every frame validates K.
121    K,
122    /// Reflexive frames.
123    T,
124    /// Serial frames.
125    D,
126    /// Symmetric frames.
127    B,
128    /// Reflexive + transitive frames.
129    S4,
130    /// Equivalence frames (reflexive + symmetric + transitive).
131    S5,
132}
133
134/// Does the frame validate the characteristic axiom(s) of `system` over `worlds`?
135pub fn validates(system: ModalSystem, graph: &[NQuin], accesses: u64, worlds: &[u64]) -> bool {
136    match system {
137        ModalSystem::K => true,
138        ModalSystem::T => is_reflexive(graph, accesses, worlds),
139        ModalSystem::D => is_serial(graph, accesses, worlds),
140        ModalSystem::B => is_symmetric(graph, accesses, worlds),
141        ModalSystem::S4 => {
142            is_reflexive(graph, accesses, worlds) && is_transitive(graph, accesses, worlds)
143        }
144        ModalSystem::S5 => {
145            is_reflexive(graph, accesses, worlds)
146                && is_symmetric(graph, accesses, worlds)
147                && is_transitive(graph, accesses, worlds)
148        }
149    }
150}
151
152// ─── Multi-agent epistemic modality (K_i) ───────────────────────────────────────────
153//
154// Each agent `i` carries its OWN accessibility relation, supplied as a distinct `accesses_i`
155// predicate (e.g. `q_hash("agent:alice:accesses")`). "Agent i knows φ at w" (K_i φ) is then just
156// □ over i's relation — so the frame-agnostic `necessary` specialises per agent at no extra cost.
157
158/// `K_i φ` — agent `i` (via its `accesses_i` relation) **knows** `prop` at `world`: every
159/// world i-accessible from `world` satisfies it.
160#[inline]
161pub fn knows(graph: &[NQuin], accesses_i: u64, world: u64, prop: u64, holds: u64) -> bool {
162    necessary(graph, world, prop, accesses_i, holds)
163}
164
165/// "Everybody knows" `prop` at `world`: K_i φ holds for **every** agent in `agent_accesses`.
166pub fn everyone_knows(
167    graph: &[NQuin],
168    agent_accesses: &[u64],
169    world: u64,
170    prop: u64,
171    holds: u64,
172) -> bool {
173    agent_accesses
174        .iter()
175        .all(|&acc| knows(graph, acc, world, prop, holds))
176}
177
178// ─── AGM belief revision ────────────────────────────────────────────────────────────
179//
180// A finite belief base over signed literals, with the three AGM operations. Revision uses the
181// Levi identity (`K*φ = (K−¬φ)+φ`), so a revised set is always consistent in `φ`: you can never
182// believe an atom both ways. Zero-heap (caller-supplied `out`, sized ≥ the result).
183
184/// A signed belief: `atom` held positively (`positive == true`) or negatively.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct Belief {
187    pub atom: u64,
188    pub positive: bool,
189}
190
191impl Belief {
192    /// The contrary belief (same atom, flipped polarity).
193    #[inline]
194    pub fn negate(self) -> Belief {
195        Belief {
196            atom: self.atom,
197            positive: !self.positive,
198        }
199    }
200}
201
202/// A belief base is **consistent** iff no atom is held both positively and negatively.
203pub fn is_consistent(set: &[Belief]) -> bool {
204    for (i, a) in set.iter().enumerate() {
205        for b in &set[i + 1..] {
206            if a.atom == b.atom && a.positive != b.positive {
207                return false;
208            }
209        }
210    }
211    true
212}
213
214/// **AGM expansion** `K + φ`: add `belief` if absent. Writes the result into `out`, returns its
215/// length (no deductive closure beyond the explicit literals — a finite base model).
216pub fn expand(set: &[Belief], belief: Belief, out: &mut [Belief]) -> usize {
217    let mut n = 0usize;
218    for &x in set {
219        if n < out.len() {
220            out[n] = x;
221            n += 1;
222        }
223    }
224    if !set.contains(&belief) && n < out.len() {
225        out[n] = belief;
226        n += 1;
227    }
228    n
229}
230
231/// **AGM contraction** `K − φ`: remove `belief` if present (vacuous if absent). Writes the
232/// result into `out`, returns its length.
233pub fn contract(set: &[Belief], belief: Belief, out: &mut [Belief]) -> usize {
234    let mut n = 0usize;
235    for &x in set {
236        if x == belief {
237            continue;
238        }
239        if n < out.len() {
240            out[n] = x;
241            n += 1;
242        }
243    }
244    n
245}
246
247/// **AGM revision** `K * φ` via the Levi identity: drop any belief about `belief.atom` (so the
248/// contrary `¬φ` is contracted), then add `φ`. The result satisfies *success* (`φ ∈ K*φ`) and
249/// *consistency* (never both `φ` and `¬φ`). Writes into `out`, returns its length.
250pub fn revise(set: &[Belief], belief: Belief, out: &mut [Belief]) -> usize {
251    let mut n = 0usize;
252    for &x in set {
253        if x.atom == belief.atom {
254            continue; // contracts both the old φ and ¬φ on this atom
255        }
256        if n < out.len() {
257            out[n] = x;
258            n += 1;
259        }
260    }
261    if n < out.len() {
262        out[n] = belief;
263        n += 1;
264    }
265    n
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    fn acc(from: u64, to: u64) -> NQuin {
273        let mut q = NQuin {
274            subject: from,
275            predicate: crate::q_hash("modal:accesses"),
276            object: to,
277            context: 0,
278            metadata: 0,
279            parity: 0,
280        };
281        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
282        q
283    }
284    fn label(world: u64, prop: u64) -> NQuin {
285        let mut q = NQuin {
286            subject: world,
287            predicate: crate::q_hash("modal:holds"),
288            object: prop,
289            context: 0,
290            metadata: 0,
291            parity: 0,
292        };
293        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
294        q
295    }
296
297    #[test]
298    fn box_and_diamond() {
299        let accesses = crate::q_hash("modal:accesses");
300        let holds = crate::q_hash("modal:holds");
301        let p = 100u64;
302        // w0 accesses w1, w2. p holds at w1 only.
303        let g = [acc(0, 1), acc(0, 2), label(1, p)];
304        assert!(
305            possible(&g, 0, p, accesses, holds),
306            "◇p: w1 (accessible) satisfies p"
307        );
308        assert!(
309            !necessary(&g, 0, p, accesses, holds),
310            "□p fails: w2 (accessible) does not satisfy p"
311        );
312        // Now p holds at both accessible worlds.
313        let g2 = [acc(0, 1), acc(0, 2), label(1, p), label(2, p)];
314        assert!(
315            necessary(&g2, 0, p, accesses, holds),
316            "□p: all accessible worlds satisfy p"
317        );
318    }
319
320    #[test]
321    fn frame_properties_and_axiom_systems() {
322        let accesses = crate::q_hash("modal:accesses");
323        let worlds = [0u64, 1u64, 2u64];
324
325        // An equivalence frame (S5): reflexive + symmetric + transitive — here the total
326        // relation over {0,1,2}.
327        let mut s5 = Vec::new();
328        for &w in &worlds {
329            for &v in &worlds {
330                s5.push(acc(w, v));
331            }
332        }
333        assert!(is_reflexive(&s5, accesses, &worlds));
334        assert!(is_symmetric(&s5, accesses, &worlds));
335        assert!(is_transitive(&s5, accesses, &worlds));
336        assert!(is_euclidean(&s5, accesses, &worlds));
337        assert!(validates(ModalSystem::S5, &s5, accesses, &worlds));
338        assert!(validates(ModalSystem::S4, &s5, accesses, &worlds));
339        assert!(validates(ModalSystem::K, &s5, accesses, &worlds));
340
341        // A bare chain 0→1→2 (irreflexive, asymmetric): validates neither T nor B; not transitive
342        // (0→1, 1→2, but no 0→2). It is serial only if every world has a successor — world 2 has
343        // none, so D fails too.
344        let chain = [acc(0, 1), acc(1, 2)];
345        assert!(!is_reflexive(&chain, accesses, &worlds));
346        assert!(!is_symmetric(&chain, accesses, &worlds));
347        assert!(!is_transitive(&chain, accesses, &worlds));
348        assert!(!validates(ModalSystem::T, &chain, accesses, &worlds));
349        assert!(!validates(ModalSystem::B, &chain, accesses, &worlds));
350        assert!(!validates(ModalSystem::D, &chain, accesses, &worlds));
351        assert!(validates(ModalSystem::K, &chain, accesses, &worlds)); // every frame validates K
352
353        // Add 0→2 and reflexive loops → reflexive + transitive (S4) but NOT symmetric.
354        let s4 = [
355            acc(0, 0),
356            acc(1, 1),
357            acc(2, 2),
358            acc(0, 1),
359            acc(1, 2),
360            acc(0, 2),
361        ];
362        assert!(is_reflexive(&s4, accesses, &worlds));
363        assert!(is_transitive(&s4, accesses, &worlds));
364        assert!(!is_symmetric(&s4, accesses, &worlds));
365        assert!(validates(ModalSystem::S4, &s4, accesses, &worlds));
366        assert!(!validates(ModalSystem::S5, &s4, accesses, &worlds));
367        assert!(validates(ModalSystem::T, &s4, accesses, &worlds));
368        assert!(validates(ModalSystem::D, &s4, accesses, &worlds)); // reflexive ⇒ serial
369    }
370
371    #[test]
372    fn multi_agent_knowledge_is_per_relation() {
373        let holds = crate::q_hash("modal:holds");
374        let alice = crate::q_hash("agent:alice:accesses");
375        let bob = crate::q_hash("agent:bob:accesses");
376        let p = 100u64;
377        let mk = |from: u64, to: u64, acc: u64| {
378            let mut q = NQuin {
379                subject: from,
380                predicate: acc,
381                object: to,
382                context: 0,
383                metadata: 0,
384                parity: 0,
385            };
386            q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
387            q
388        };
389        // From w0: Alice accesses only w1 (where p holds); Bob accesses w1 and w2 (p fails at w2).
390        let g = [mk(0, 1, alice), mk(0, 1, bob), mk(0, 2, bob), label(1, p)];
391        assert!(
392            knows(&g, alice, 0, p, holds),
393            "Alice knows p (all her worlds satisfy it)"
394        );
395        assert!(
396            !knows(&g, bob, 0, p, holds),
397            "Bob does not know p (w2 fails)"
398        );
399        assert!(!everyone_knows(&g, &[alice, bob], 0, p, holds));
400    }
401
402    #[test]
403    fn agm_revision_is_consistent_and_satisfies_success() {
404        let p = 1u64;
405        let bel_p = Belief {
406            atom: p,
407            positive: true,
408        };
409        let bel_not_p = Belief {
410            atom: p,
411            positive: false,
412        };
413        let q = Belief {
414            atom: 2,
415            positive: true,
416        };
417        let mut out = [Belief {
418            atom: 0,
419            positive: true,
420        }; 8];
421
422        // Start believing ¬p and q. Revise by p.
423        let base = [bel_not_p, q];
424        let n = revise(&base, bel_p, &mut out);
425        let result = &out[..n];
426        // Success: p ∈ K*p. Consistency: ¬p ∉ K*p. Minimal change: q retained.
427        assert!(result.contains(&bel_p), "success postulate: φ ∈ K*φ");
428        assert!(!result.contains(&bel_not_p), "consistency: ¬φ removed");
429        assert!(
430            result.contains(&q),
431            "minimal change: unrelated beliefs kept"
432        );
433        assert!(is_consistent(result));
434
435        // Expansion adds; idempotent if already present.
436        let n2 = expand(&base, q, &mut out);
437        assert_eq!(n2, 2, "q already present → no growth");
438        let n3 = expand(&base, bel_p, &mut out);
439        assert_eq!(
440            n3, 3,
441            "p added by expansion (may be inconsistent — that's expansion, not revision)"
442        );
443        assert!(
444            !is_consistent(&out[..n3]),
445            "expansion does NOT guarantee consistency"
446        );
447
448        // Contraction removes; vacuous when absent.
449        let n4 = contract(&base, bel_not_p, &mut out);
450        assert!(!out[..n4].contains(&bel_not_p));
451        assert_eq!(
452            contract(&base, bel_p, &mut out),
453            2,
454            "contracting an absent belief is vacuous"
455        );
456    }
457}