qualia_client_core/wellfair/
bodyparts3d_ontology.rs1use 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#[derive(Debug, Clone)]
29pub struct OntologyConcept {
30 pub id: String,
31 pub compiled_digest: u32,
32 pub systems: Vec<String>,
33}
34
35const OBO_FMA_PREFIX: &str = "http://purl.obolibrary.org/obo/FMA_";
37const DATASET_IRI: &str = "urn:qualia:bodyparts3d:ontology";
38const 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"; const 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"; const C_DATASET: &str = "http://www.w3.org/ns/dcat#Dataset";
50const 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
57fn 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
67struct 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 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 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 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
106pub 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 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 g.edge_iri(s, P_RDF_TYPE, C_ANATOMICAL_STRUCTURE);
137 g.edge_iri(s, P_RDF_TYPE, C_Q42_CONCEPT);
138 if let Some(name) = hier.name(&c.id) {
140 g.edge_lit(s, P_RDFS_LABEL, name);
141 }
142 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 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 for sys in &c.systems {
158 g.edge_lit(s, P_GEO_SYSTEM, sys);
159 }
160 g.edge(s, P_GEO_DIGEST, c.compiled_digest as u64);
162 g.edge(s, P_DCT_IS_PART_OF, ds);
164 }
165
166 (g.quins, g.lex)
167}
168
169pub 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()); 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 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 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 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 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 assert!(
265 quins.iter().any(|q| q.object == 0xDEAD_BEEF),
266 "hasMesh digest present"
267 );
268 }
269}