Skip to main content

qualia_client_core/wellfair/
bodyparts3d_ontology.rs

1//! Define the BodyParts3D anatomy as an addressable **ontology** and serialise it to a `.q42` graph — the
2//! semantic backbone the `.10d` mesh library is addressed by (Timothy 2026-07-12: "define an ontology …
3//! store the info in a q42 file, with a library of d10 files produced via converting the files").
4//!
5//! Each FMA concept is keyed by its **canonical OBO IRI** (`http://purl.obolibrary.org/obo/FMA_<id>`), so
6//! it joins *directly* to Monarch / UBERON / MONDO disease linked-data — which is what makes reasoning
7//! over the **implications of comorbidities** a graph walk rather than a lookup table. Alongside the
8//! canonical OBO triples it emits **house `q42:`/`geo:` aliases** for native reading (Timothy's "both"
9//! choice). Per concept: `rdfs:label`, `rdfs:subClassOf` (**is-a**), `obo:BFO_0000050` (**part-of**),
10//! `geo:bodySystem` (membership), `geo:compiledDigest` (the `.10d` it *hasMesh*), and a link to a dataset
11//! node carrying the CC-BY-SA provenance + citation once.
12//!
13//! Pure (graph construction + `.q42` serialisation); unit-tested. The producer supplies the concepts +
14//! their compiled digests + the parsed is-a / part-of tables.
15
16use std::collections::HashMap;
17
18use qualia_core_db::hypermedia::fnv60;
19use qualia_core_db::q42_volume::UnifiedVolumeBuilder;
20use qualia_core_db::{NQuin, QUINS_PER_BLOCK};
21
22use super::bodyparts3d_resolver::{
23    Bp3dHierarchy, BP3D_ATTRIBUTION, BP3D_CITATION, BP3D_DATA_DOI, BP3D_LICENCE, BP3D_SOURCE_URL,
24};
25
26/// A meshed anatomical concept to place in the ontology: its BodyParts3D id, the whole-file digest of its
27/// compiled `.10d` (the geometry it `hasMesh`), and its resolved body-system membership(s).
28#[derive(Debug, Clone)]
29pub struct OntologyConcept {
30    pub id: String,
31    pub compiled_digest: u32,
32    pub systems: Vec<String>,
33}
34
35// ── Vocabulary: canonical OBO / standards, plus house aliases ────────────────────────────────────
36const OBO_FMA_PREFIX: &str = "http://purl.obolibrary.org/obo/FMA_";
37const DATASET_IRI: &str = "urn:qualia:bodyparts3d:ontology";
38// canonical predicates / classes
39const P_RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
40const P_RDFS_LABEL: &str = "http://www.w3.org/2000/01/rdf-schema#label";
41const P_RDFS_SUBCLASSOF: &str = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
42const P_BFO_PART_OF: &str = "http://purl.obolibrary.org/obo/BFO_0000050"; // "part of"
43const P_DCT_LICENSE: &str = "http://purl.org/dc/terms/license";
44const P_DCT_CREATOR: &str = "http://purl.org/dc/terms/creator";
45const P_DCT_SOURCE: &str = "http://purl.org/dc/terms/source";
46const P_DCT_CITATION: &str = "http://purl.org/dc/terms/bibliographicCitation";
47const P_DCT_IS_PART_OF: &str = "http://purl.org/dc/terms/isPartOf";
48const C_ANATOMICAL_STRUCTURE: &str = "http://purl.obolibrary.org/obo/FMA_62955"; // "anatomical structure"
49const C_DATASET: &str = "http://www.w3.org/ns/dcat#Dataset";
50// house aliases (readable / native — Timothy's "both" choice)
51const P_GEO_SYSTEM: &str = "geo:bodySystem";
52const P_GEO_DIGEST: &str = "geo:compiledDigest";
53const P_Q42_PART_OF: &str = "q42:partOf";
54const P_Q42_IS_A: &str = "q42:isA";
55const C_Q42_CONCEPT: &str = "q42:AnatomicalConcept";
56
57/// The canonical IRI for a BodyParts3D id — an OBO FMA IRI for `FMA<n>`, else a namespaced URN.
58fn concept_iri(id: &str) -> String {
59    match id.strip_prefix("FMA") {
60        Some(n) if !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => {
61            format!("{OBO_FMA_PREFIX}{n}")
62        }
63        _ => format!("urn:bodyparts3d:{id}"),
64    }
65}
66
67/// Small quin-graph builder: interns strings into a lexicon (60-bit FNV, the codebase's subject-identity
68/// space) and pushes parity-valid quins in one named-graph context.
69struct GraphBuilder {
70    quins: Vec<NQuin>,
71    lex: HashMap<u64, String>,
72    context: u64,
73}
74
75impl GraphBuilder {
76    fn intern(&mut self, s: &str) -> u64 {
77        let h = fnv60(s.as_bytes());
78        self.lex.entry(h).or_insert_with(|| s.to_string());
79        h
80    }
81    /// Push `(subject, predicate, object)` with valid ECC parity.
82    fn edge(&mut self, subject: u64, predicate: &str, object: u64) {
83        let p = self.intern(predicate);
84        let (ctx, md) = (self.context, 0u64);
85        self.quins.push(NQuin {
86            subject,
87            predicate: p,
88            object,
89            context: ctx,
90            metadata: md,
91            parity: NQuin::calculate_parity(subject, p, object, ctx, md),
92        });
93    }
94    /// An edge whose object is an IRI (interned).
95    fn edge_iri(&mut self, subject: u64, predicate: &str, object_iri: &str) {
96        let o = self.intern(object_iri);
97        self.edge(subject, predicate, o);
98    }
99    /// An edge whose object is a string literal (interned).
100    fn edge_lit(&mut self, subject: u64, predicate: &str, literal: &str) {
101        let o = self.intern(literal);
102        self.edge(subject, predicate, o);
103    }
104}
105
106/// Emit the ontology graph (quins + object-lexicon) for a set of meshed concepts. The concepts' is-a
107/// (`isa`) and part-of (`hier`) come from the BodyParts3D tables; each concept's OBO IRI is the canonical
108/// identity, with `q42:`/`geo:` aliases emitted alongside.
109pub fn emit_ontology(
110    concepts: &[OntologyConcept],
111    hier: &Bp3dHierarchy,
112    isa: &HashMap<String, String>,
113) -> (Vec<NQuin>, HashMap<u64, String>) {
114    let mut g = GraphBuilder {
115        quins: Vec::new(),
116        lex: HashMap::new(),
117        context: fnv60(DATASET_IRI.as_bytes()),
118    };
119
120    // The dataset node carries the CC-BY-SA provenance + citation ONCE (concepts link to it).
121    let ds = g.intern(DATASET_IRI);
122    g.edge_iri(ds, P_RDF_TYPE, C_DATASET);
123    g.edge_lit(ds, P_RDFS_LABEL, "BodyParts3D anatomy ontology");
124    g.edge_lit(ds, P_DCT_LICENSE, BP3D_LICENCE);
125    g.edge_lit(ds, P_DCT_CREATOR, BP3D_ATTRIBUTION);
126    g.edge_lit(ds, P_DCT_SOURCE, BP3D_SOURCE_URL);
127    g.edge_lit(ds, P_DCT_CITATION, BP3D_CITATION);
128    g.edge_lit(ds, P_DCT_CITATION, BP3D_DATA_DOI);
129
130    for c in concepts {
131        let s = {
132            let iri = concept_iri(&c.id);
133            g.intern(&iri)
134        };
135        // type — canonical anatomical-structure class + house alias.
136        g.edge_iri(s, P_RDF_TYPE, C_ANATOMICAL_STRUCTURE);
137        g.edge_iri(s, P_RDF_TYPE, C_Q42_CONCEPT);
138        // label
139        if let Some(name) = hier.name(&c.id) {
140            g.edge_lit(s, P_RDFS_LABEL, name);
141        }
142        // is-a (canonical rdfs:subClassOf + house q42:isA), skipping self-loops.
143        if let Some(parent) = isa.get(&c.id) {
144            if parent != &c.id {
145                let piri = concept_iri(parent);
146                g.edge_iri(s, P_RDFS_SUBCLASSOF, &piri);
147                g.edge_iri(s, P_Q42_IS_A, &piri);
148            }
149        }
150        // part-of (canonical BFO_0000050 + house q42:partOf) — direct wholes.
151        for whole in hier.wholes_of(&c.id) {
152            let wiri = concept_iri(whole);
153            g.edge_iri(s, P_BFO_PART_OF, &wiri);
154            g.edge_iri(s, P_Q42_PART_OF, &wiri);
155        }
156        // body-system membership (house geo:bodySystem, literal system id).
157        for sys in &c.systems {
158            g.edge_lit(s, P_GEO_SYSTEM, sys);
159        }
160        // geometry — the concept hasMesh the .10d whose whole-file digest is this (numeric object).
161        g.edge(s, P_GEO_DIGEST, c.compiled_digest as u64);
162        // provenance link to the dataset node.
163        g.edge(s, P_DCT_IS_PART_OF, ds);
164    }
165
166    (g.quins, g.lex)
167}
168
169/// Serialise the ontology of `concepts` into unified v3 `.q42` bytes (object-sorted blocks). Returns the
170/// bytes and the number of quins in the graph.
171pub fn ontology_q42_bytes(
172    concepts: &[OntologyConcept],
173    hier: &Bp3dHierarchy,
174    isa: &HashMap<String, String>,
175) -> (Vec<u8>, usize) {
176    let (quins, lex) = emit_ontology(concepts, hier, isa);
177    let count = quins.len();
178    let mut sorted = quins;
179    sorted.sort_by_key(|q| q.object);
180    let mut builder = UnifiedVolumeBuilder::with_lex_map(&lex)
181        .expect("ontology Q42 lexicon entries fit the current Q42LEX format");
182    for (seq, chunk) in sorted.chunks(QUINS_PER_BLOCK).enumerate() {
183        builder
184            .push_block(seq as u64, chunk)
185            .expect("ontology Q42 graph is object-sorted");
186    }
187    (builder.finish_to_bytes(), count)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::super::bodyparts3d_resolver::Bp3dHierarchy;
193    use super::*;
194
195    const PARTS: &str = "\"id\"\ten\n\
196        FMA72954\tmuscular system\n\
197        FMA7158\trespiratory system\n\
198        FMA13295\tdiaphragm\n";
199    const PART_OF: &str = "\"id\"\tname\tpart id\tpart name\n\
200        FMA72954\tmuscular system\tFMA13295\tdiaphragm\n\
201        FMA7158\trespiratory system\tFMA13295\tdiaphragm\n";
202
203    #[test]
204    fn emits_an_addressable_ontology_q42_with_obo_iris_and_aliases() {
205        let hier = Bp3dHierarchy::from_mapping(PARTS, PART_OF);
206        let mut isa = HashMap::new();
207        isa.insert("FMA13295".to_string(), "FMA9909".to_string()); // diaphragm is-a some muscle class
208        let concepts = vec![OntologyConcept {
209            id: "FMA13295".to_string(),
210            compiled_digest: 0xDEAD_BEEF,
211            systems: vec!["muscular".to_string(), "respiratory".to_string()],
212        }];
213
214        let (bytes, n) = ontology_q42_bytes(&concepts, &hier, &isa);
215        assert!(n > 0);
216        assert!(bytes.starts_with(&qualia_core_db::q42_volume::Q42_MAGIC));
217
218        // Round-trip through a real unified volume.
219        let tmp = tempfile::NamedTempFile::new().unwrap();
220        std::fs::write(tmp.path(), &bytes).unwrap();
221        let vol = qualia_core_db::q42_volume::Q42Volume::open(tmp.path()).unwrap();
222        let quins = vol.read_all_quins().unwrap();
223        assert_eq!(quins.len(), n, "every fact recoverable");
224        let lex = vol.lex_view().unwrap();
225        let objs: Vec<String> = quins
226            .iter()
227            .filter_map(|q| lex.lookup_hash(q.object).map(str::to_string))
228            .collect();
229
230        // Canonical OBO IRI IDENTITY (the concept is addressable + joins to disease data).
231        let iri = "http://purl.obolibrary.org/obo/FMA_13295";
232        assert_eq!(
233            lex.lookup_hash(fnv60(iri.as_bytes())),
234            Some(iri),
235            "concept keyed by OBO IRI"
236        );
237        // is-a parent + part-of parents as OBO IRIs (objects).
238        assert!(
239            objs.iter()
240                .any(|v| v == "http://purl.obolibrary.org/obo/FMA_9909"),
241            "is-a parent IRI"
242        );
243        assert!(
244            objs.iter()
245                .any(|v| v == "http://purl.obolibrary.org/obo/FMA_72954"),
246            "part-of muscular sys"
247        );
248        assert!(
249            objs.iter()
250                .any(|v| v == "http://purl.obolibrary.org/obo/FMA_7158"),
251            "part-of respiratory sys"
252        );
253        // Label + BOTH system memberships + the CC-BY-SA licence (dataset node).
254        assert!(objs.iter().any(|v| v == "diaphragm"), "label");
255        assert!(
256            objs.iter().any(|v| v == "muscular") && objs.iter().any(|v| v == "respiratory"),
257            "systems"
258        );
259        assert!(
260            objs.iter().any(|v| v == BP3D_LICENCE),
261            "CC-BY-SA licence on the dataset node"
262        );
263        // The compiled `.10d` digest is addressable as a numeric object (geo:compiledDigest).
264        assert!(
265            quins.iter().any(|q| q.object == 0xDEAD_BEEF),
266            "hasMesh digest present"
267        );
268    }
269}