Skip to main content

qualia_core_db/medical/
comorbidity_eval.rs

1//! Defeasible comorbidity evaluation for the Core 1 Prolog Sentinel.
2//!
3//! Traverses nested RDF-Star Quins (subject bit 63) and compounding `q42:exacerbates`
4//! edges without heap allocation. Contradictory diagnoses route through paraconsistent
5//! isolation rather than halting evaluation.
6
7use crate::modalities::paraconsistent::route_paraconsistent;
8use crate::q_hash;
9use crate::NQuin;
10
11pub const MAX_CONDITION_SLOTS: usize = 32;
12pub const MAX_COMORBIDITY_VERDICTS: usize = 64;
13pub const NESTED_SUBJECT_MASK: u64 = 1u64 << 63;
14pub const NESTED_PAYLOAD_MASK: u64 = 0x0FFF_FFFF_FFFF_FFFF;
15
16pub const PRED_EXACERBATES: u64 = q_hash("q42:exacerbates");
17pub const PRED_HAS_CONDITION: u64 = q_hash("q42:hasCondition");
18pub const PRED_HAS_SEVERITY: u64 = q_hash("q42:hasSeverity");
19
20const INLINE_TAG_DECIMAL: u64 = 0b010u64 << 60;
21const INLINE_TAG_MASK: u64 = 0b111u64 << 60;
22const INLINE_VALUE_MASK: u64 = 0x0FFF_FFFF_FFFF_FFFF;
23
24#[repr(u8)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ComorbidityStatus {
27    Active = 0,
28    Isolated = 1,
29    Defeated = 2,
30}
31
32#[repr(C, align(8))]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct ComorbidityVerdict {
35    pub condition_hash: u64,
36    pub compounded_risk_milli: u32,
37    pub status: ComorbidityStatus,
38    pub _pad: [u8; 3],
39}
40
41#[derive(Debug, PartialEq, Eq)]
42pub enum ComorbidityError {
43    OutputBufferFull,
44}
45
46/// Fingerprint for a nested RDF-Star claim: `<< ante pred cons >>`.
47#[inline]
48pub fn nested_claim_fingerprint(ante: u64, pred: u64, cons: u64) -> u64 {
49    let body = (ante ^ pred ^ cons) & NESTED_PAYLOAD_MASK;
50    body | NESTED_SUBJECT_MASK
51}
52
53#[inline]
54pub fn is_nested_subject(subject: u64) -> bool {
55    (subject & NESTED_SUBJECT_MASK) != 0
56}
57
58#[inline]
59fn decode_severity_milli(object: u64) -> u32 {
60    if (object & INLINE_TAG_MASK) == INLINE_TAG_DECIMAL {
61        let scaled = (object & INLINE_VALUE_MASK) as u32;
62        // Object stores severity × 10⁶; map to 0–1000 milli scale.
63        ((scaled as u64 * 1000) / 1_000_000).min(1000) as u32
64    } else {
65        500
66    }
67}
68
69#[inline]
70fn encode_severity_object(severity: f32) -> u64 {
71    let clamped = severity.clamp(0.0, 1.0);
72    let scaled = (clamped * 1_000_000.0).round() as u64;
73    (scaled & INLINE_VALUE_MASK) | INLINE_TAG_DECIMAL
74}
75
76/// Compile a nested exacerbation Quin pair (relationship + severity).
77pub fn compile_exacerbation_quins(
78    ante_condition: u64,
79    cons_condition: u64,
80    patient_context: u64,
81    severity: f32,
82    out: &mut [NQuin; 2],
83) -> usize {
84    let nested = nested_claim_fingerprint(ante_condition, PRED_EXACERBATES, cons_condition);
85
86    let mut edge = NQuin::default();
87    edge.subject = ante_condition;
88    edge.predicate = PRED_EXACERBATES;
89    edge.object = cons_condition;
90    edge.context = patient_context;
91    edge.parity = edge.subject ^ edge.predicate ^ edge.object ^ edge.context;
92    out[0] = edge;
93
94    let mut severity_quin = NQuin::default();
95    severity_quin.subject = nested;
96    severity_quin.predicate = PRED_HAS_SEVERITY;
97    severity_quin.object = encode_severity_object(severity);
98    severity_quin.context = patient_context;
99    severity_quin.parity = severity_quin.subject
100        ^ severity_quin.predicate
101        ^ severity_quin.object
102        ^ severity_quin.context;
103    out[1] = severity_quin;
104
105    2
106}
107
108/// Returns `true` when `condition_hash` plausibly intersects the target organ hash.
109#[inline]
110fn condition_intersects_organ(condition_hash: u64, target_organ_hash: u64) -> bool {
111    if target_organ_hash == 0 {
112        return true;
113    }
114    if condition_hash == target_organ_hash {
115        return true;
116    }
117    // Lightweight organ–system intersection via hashed clinical tokens.
118    let cardio = q_hash("Heart");
119    let diabetes = q_hash("Type 2 Diabetes Mellitus");
120    let neuropathy = q_hash("Diabetic Neuropathy");
121    let hypertension = q_hash("Hypertension");
122
123    if target_organ_hash == cardio {
124        return matches!(
125            condition_hash,
126            x if x == diabetes || x == hypertension || x == cardio || x == neuropathy
127        );
128    }
129
130    condition_hash == target_organ_hash
131}
132
133/// Core 1 evaluator — zero heap allocation; caller supplies output buffers.
134pub fn eval_comorbidity(
135    patient_did_hash: u64,
136    target_organ_hash: u64,
137    quins: &[NQuin],
138    out: &mut [ComorbidityVerdict],
139) -> Result<usize, ComorbidityError> {
140    let mut consistent_buf = [NQuin::default(); 128];
141    let mut isolated_buf = [NQuin::default(); 32];
142    let (consistent_count, isolated_count) =
143        route_paraconsistent(quins, &mut consistent_buf, &mut isolated_buf)
144            .map_err(|_| ComorbidityError::OutputBufferFull)?;
145    let graph = &consistent_buf[..consistent_count];
146
147    let mut conditions = [0u64; MAX_CONDITION_SLOTS];
148    let mut condition_count = 0usize;
149    let mut severities = [(0u64, 0u32); MAX_CONDITION_SLOTS];
150    let mut severity_count = 0usize;
151
152    for quin in graph {
153        if quin.context != patient_did_hash && quin.subject != patient_did_hash {
154            continue;
155        }
156
157        if quin.predicate == PRED_HAS_CONDITION && quin.subject == patient_did_hash {
158            if condition_count < MAX_CONDITION_SLOTS {
159                conditions[condition_count] = quin.object;
160                condition_count += 1;
161            }
162        }
163
164        if is_nested_subject(quin.subject) && quin.predicate == PRED_HAS_SEVERITY {
165            if severity_count < MAX_CONDITION_SLOTS {
166                severities[severity_count] = (quin.subject, decode_severity_milli(quin.object));
167                severity_count += 1;
168            }
169        }
170    }
171
172    let mut emitted = 0usize;
173
174    for i in 0..condition_count {
175        let condition = conditions[i];
176        if !condition_intersects_organ(condition, target_organ_hash) {
177            continue;
178        }
179
180        let mut risk_milli: u32 = 400;
181
182        for quin in graph {
183            if quin.predicate != PRED_EXACERBATES || quin.context != patient_did_hash {
184                continue;
185            }
186            if quin.subject != condition && quin.object != condition {
187                continue;
188            }
189            let nested_fp = nested_claim_fingerprint(quin.subject, quin.predicate, quin.object);
190            let mut matched_severity = 0u32;
191            for j in 0..severity_count {
192                if severities[j].0 == nested_fp {
193                    matched_severity = severities[j].1;
194                    break;
195                }
196            }
197            if matched_severity > 0 {
198                risk_milli = risk_milli.saturating_add(matched_severity / 2);
199                risk_milli = ((risk_milli as u64 * 14) / 10).min(1000) as u32;
200            } else {
201                risk_milli = ((risk_milli as u64 * 12) / 10).min(1000) as u32;
202            }
203        }
204
205        if emitted >= out.len() {
206            return Err(ComorbidityError::OutputBufferFull);
207        }
208
209        out[emitted] = ComorbidityVerdict {
210            condition_hash: condition,
211            compounded_risk_milli: risk_milli.min(1000),
212            status: ComorbidityStatus::Active,
213            _pad: [0; 3],
214        };
215        emitted += 1;
216    }
217
218    for quin in &isolated_buf[..isolated_count] {
219        if quin.subject != patient_did_hash && quin.context != patient_did_hash {
220            continue;
221        }
222        if emitted >= out.len() {
223            return Err(ComorbidityError::OutputBufferFull);
224        }
225        out[emitted] = ComorbidityVerdict {
226            condition_hash: quin.object,
227            compounded_risk_milli: 0,
228            status: ComorbidityStatus::Isolated,
229            _pad: [0; 3],
230        };
231        emitted += 1;
232    }
233
234    Ok(emitted)
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    fn patient_ctx() -> u64 {
242        q_hash("did:patient:comorb-test")
243    }
244
245    fn compile_demo_graph(patient: u64, out: &mut [NQuin; 16]) -> usize {
246        let diabetes = q_hash("Type 2 Diabetes Mellitus");
247        let neuropathy = q_hash("Diabetic Neuropathy");
248        let heart = q_hash("Heart");
249
250        let mut idx = 0usize;
251
252        let mut has_diabetes = NQuin::default();
253        has_diabetes.subject = patient;
254        has_diabetes.predicate = PRED_HAS_CONDITION;
255        has_diabetes.object = diabetes;
256        has_diabetes.context = patient;
257        has_diabetes.parity = has_diabetes.subject
258            ^ has_diabetes.predicate
259            ^ has_diabetes.object
260            ^ has_diabetes.context;
261        out[idx] = has_diabetes;
262        idx += 1;
263
264        let mut has_neuropathy = NQuin::default();
265        has_neuropathy.subject = patient;
266        has_neuropathy.predicate = PRED_HAS_CONDITION;
267        has_neuropathy.object = neuropathy;
268        has_neuropathy.context = patient;
269        has_neuropathy.parity = has_neuropathy.subject
270            ^ has_neuropathy.predicate
271            ^ has_neuropathy.object
272            ^ has_neuropathy.context;
273        out[idx] = has_neuropathy;
274        idx += 1;
275
276        let mut pair = [NQuin::default(); 2];
277        let wrote = compile_exacerbation_quins(diabetes, neuropathy, patient, 0.85, &mut pair);
278        out[idx..idx + wrote].copy_from_slice(&pair[..wrote]);
279        idx += wrote;
280
281        let mut cardio = NQuin::default();
282        cardio.subject = patient;
283        cardio.predicate = PRED_HAS_CONDITION;
284        cardio.object = heart;
285        cardio.context = patient;
286        cardio.parity = cardio.subject ^ cardio.predicate ^ cardio.object ^ cardio.context;
287        out[idx] = cardio;
288        idx += 1;
289
290        idx
291    }
292
293    #[test]
294    fn nested_fingerprint_sets_msb() {
295        let fp = nested_claim_fingerprint(1, 2, 3);
296        assert!(is_nested_subject(fp));
297    }
298
299    #[test]
300    fn eval_finds_compounded_diabetes_neuropathy_risk() {
301        let patient = patient_ctx();
302        let mut graph = [NQuin::default(); 16];
303        let n = compile_demo_graph(patient, &mut graph);
304
305        let mut verdicts = [ComorbidityVerdict {
306            condition_hash: 0,
307            compounded_risk_milli: 0,
308            status: ComorbidityStatus::Defeated,
309            _pad: [0; 3],
310        }; 8];
311
312        let count = eval_comorbidity(patient, q_hash("Heart"), &graph[..n], &mut verdicts).unwrap();
313        assert!(count >= 2);
314        assert!(verdicts[0].compounded_risk_milli > 400);
315    }
316
317    #[test]
318    fn zero_heap_eval_comorbidity() {
319        let patient = patient_ctx();
320        let mut graph = [NQuin::default(); 16];
321        let n = compile_demo_graph(patient, &mut graph);
322
323        let _profiler = dhat::Profiler::builder().testing().build();
324        let mut verdicts = [ComorbidityVerdict {
325            condition_hash: 0,
326            compounded_risk_milli: 0,
327            status: ComorbidityStatus::Active,
328            _pad: [0; 3],
329        }; MAX_COMORBIDITY_VERDICTS];
330
331        let result = eval_comorbidity(patient, q_hash("Heart"), &graph[..n], &mut verdicts);
332        assert!(result.is_ok());
333
334        let stats = dhat::HeapStats::get();
335        assert_eq!(
336            stats.curr_blocks, 0,
337            "eval_comorbidity must not allocate on the heap"
338        );
339        assert_eq!(stats.curr_bytes, 0);
340    }
341}