Skip to main content

qualia_core_db/q42/
design_encode.rs

1//! Natural-language design documents → NQuin graph + Tensor10D layout.
2//!
3//! General-purpose product/assembly representation for the Qualia Portal demo.
4//! Geometry stays as tensor coordinates; semantics live in parts, relations, and quins.
5
6use crate::tensor::Tensor10D;
7use crate::{q_hash, NQuin};
8use serde::{Deserialize, Serialize};
9
10pub const DESIGN_TYPE: &str = "qualia.design";
11pub const DESIGN_VERSION: &str = "1.0.0";
12pub const MAX_DESIGN_PARTS: usize = 64;
13pub const MAX_DESIGN_RELATIONS: usize = 128;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct DesignPart {
17    pub id: String,
18    #[serde(default)]
19    pub label: String,
20    #[serde(default)]
21    pub role: String,
22    #[serde(default)]
23    pub installer: String,
24    #[serde(default)]
25    pub components: Vec<String>,
26    #[serde(default)]
27    pub pos: Option<[f32; 3]>,
28    #[serde(default = "default_state")]
29    pub state: String,
30    #[serde(default = "default_intensity")]
31    pub intensity: f32,
32    #[serde(default)]
33    pub reasons: Vec<String>,
34}
35
36fn default_state() -> String {
37    "default".to_string()
38}
39
40fn default_intensity() -> f32 {
41    0.65
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct DesignRelation {
46    pub from: String,
47    pub to: String,
48    #[serde(rename = "type")]
49    pub relation_type: String,
50    #[serde(default)]
51    pub label: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct SparqlContextHit {
56    pub endpoint: String,
57    #[serde(default)]
58    pub query: String,
59    #[serde(default)]
60    pub bindings: serde_json::Value,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct DesignDocument {
65    #[serde(rename = "type", default = "default_design_type")]
66    pub doc_type: String,
67    #[serde(default = "default_design_version")]
68    pub version: String,
69    #[serde(default)]
70    pub title: String,
71    #[serde(default)]
72    pub summary: String,
73    #[serde(default)]
74    pub prompt: String,
75    #[serde(default)]
76    pub parts: Vec<DesignPart>,
77    #[serde(default)]
78    pub relations: Vec<DesignRelation>,
79    #[serde(default)]
80    pub explanations: Vec<String>,
81    #[serde(default)]
82    pub sparql_context: Vec<SparqlContextHit>,
83}
84
85fn default_design_type() -> String {
86    DESIGN_TYPE.to_string()
87}
88
89fn default_design_version() -> String {
90    DESIGN_VERSION.to_string()
91}
92
93#[derive(Debug, PartialEq, Eq)]
94pub enum DesignEncodeError {
95    TooManyParts,
96    TooManyRelations,
97    UnknownPartId,
98}
99
100#[derive(Debug, Clone, Serialize)]
101pub struct DesignEncodeStats {
102    pub part_count: usize,
103    pub relation_count: usize,
104    pub tensor_count: usize,
105    pub quin_count: usize,
106    pub design_hash: String,
107}
108
109fn manifold_w(label: &str, role: &str) -> f32 {
110    let key = format!("{label}:{role}").to_lowercase();
111    let h = q_hash(&key);
112    (h % 5) as f32
113}
114
115fn topology_v(role: &str, relation_count: usize) -> f32 {
116    let r = role.to_lowercase();
117    if r.contains("interface") || r.contains("mate") || r.contains("connector") {
118        return 3.2;
119    }
120    if relation_count > 2 {
121        return 2.5;
122    }
123    if r.contains("sensor") || r.contains("compute") || r.contains("smart") {
124        return 1.5;
125    }
126    0.0
127}
128
129fn epistemic_q(state: &str, installer: &str) -> f32 {
130    let s = state.to_lowercase();
131    if s == "alert" || s == "uncertain" {
132        return 0.42;
133    }
134    if !installer.is_empty() && installer != "user" && installer != "owner" {
135        return 0.0;
136    }
137    if s == "highlighted" || s == "active" {
138        return 0.12;
139    }
140    0.08
141}
142
143fn spectral_sigma(id: &str, idx: usize, total: usize) -> f32 {
144    let base = (q_hash(id) % 10_000) as f32 / 10_000.0;
145    base + (idx as f32 / total.max(1) as f32) * 0.15
146}
147
148fn auto_position(index: usize, total: usize) -> [f32; 3] {
149    if total <= 1 {
150        return [0.0, 0.0, 0.0];
151    }
152    let t = index as f32 / total as f32;
153    let angle = t * std::f32::consts::TAU;
154    let radius = 4.0 + (total as f32 * 0.15);
155    [
156        radius * angle.cos(),
157        (index as f32 * 0.35) - (total as f32 * 0.15),
158        radius * angle.sin(),
159    ]
160}
161
162fn relation_midpoint(from: &[f32; 3], to: &[f32; 3]) -> [f32; 3] {
163    [
164        (from[0] + to[0]) * 0.5,
165        (from[1] + to[1]) * 0.5 + 0.25,
166        (from[2] + to[2]) * 0.5,
167    ]
168}
169
170/// Lay out parts in 10D tensor space and emit optional relation anchor tensors.
171pub fn design_to_tensors(doc: &DesignDocument) -> Result<Vec<Tensor10D>, DesignEncodeError> {
172    if doc.parts.len() > MAX_DESIGN_PARTS {
173        return Err(DesignEncodeError::TooManyParts);
174    }
175    if doc.relations.len() > MAX_DESIGN_RELATIONS {
176        return Err(DesignEncodeError::TooManyRelations);
177    }
178
179    let total = doc.parts.len();
180    let mut positions: Vec<[f32; 3]> = Vec::with_capacity(total);
181    for (i, part) in doc.parts.iter().enumerate() {
182        positions.push(part.pos.unwrap_or_else(|| auto_position(i, total)));
183    }
184
185    let mut id_to_index = std::collections::BTreeMap::new();
186    for (i, part) in doc.parts.iter().enumerate() {
187        id_to_index.insert(part.id.clone(), i);
188    }
189
190    let mut out = Vec::new();
191    for (i, part) in doc.parts.iter().enumerate() {
192        let [x, y, z] = positions[i];
193        let rels = doc
194            .relations
195            .iter()
196            .filter(|r| r.from == part.id || r.to == part.id)
197            .count();
198        let nx = (x / 10.0).clamp(-1.0, 1.0);
199        let ny = (y / 10.0).clamp(-1.0, 1.0);
200        let nz = (z / 10.0).clamp(-1.0, 1.0);
201        let label = if part.label.is_empty() {
202            &part.id
203        } else {
204            &part.label
205        };
206        out.push(Tensor10D::new(
207            epistemic_q(&part.state, &part.installer),
208            topology_v(&part.role, rels),
209            manifold_w(label, &part.role),
210            nx,
211            ny,
212            nz,
213            i as f32 / total.max(1) as f32,
214            part.intensity.clamp(0.0, 1.0),
215            if part.installer.is_empty() { 0.0 } else { 2.0 },
216            spectral_sigma(&part.id, i, total),
217        ));
218    }
219
220    for (ri, rel) in doc.relations.iter().enumerate() {
221        let Some(&fi) = id_to_index.get(&rel.from) else {
222            return Err(DesignEncodeError::UnknownPartId);
223        };
224        let Some(&ti) = id_to_index.get(&rel.to) else {
225            return Err(DesignEncodeError::UnknownPartId);
226        };
227        let mid = relation_midpoint(&positions[fi], &positions[ti]);
228        let nx = (mid[0] / 10.0).clamp(-1.0, 1.0);
229        let ny = (mid[1] / 10.0).clamp(-1.0, 1.0);
230        let nz = (mid[2] / 10.0).clamp(-1.0, 1.0);
231        out.push(Tensor10D::new(
232            0.18,
233            3.2,
234            manifold_w(&rel.relation_type, "relation"),
235            nx,
236            ny,
237            nz,
238            0.5 + (ri as f32 * 0.01),
239            0.55,
240            1.0,
241            spectral_sigma(&rel.relation_type, ri, doc.relations.len()),
242        ));
243    }
244
245    Ok(out)
246}
247
248/// Lower design semantics to NQuin triples (parts + relations + design root).
249pub fn design_to_quins(doc: &DesignDocument) -> Result<Vec<NQuin>, DesignEncodeError> {
250    if doc.parts.len() > MAX_DESIGN_PARTS {
251        return Err(DesignEncodeError::TooManyParts);
252    }
253    if doc.relations.len() > MAX_DESIGN_RELATIONS {
254        return Err(DesignEncodeError::TooManyRelations);
255    }
256
257    let design_id = if doc.title.is_empty() {
258        q_hash(&doc.prompt)
259    } else {
260        q_hash(&doc.title)
261    };
262    let ctx = q_hash("ctx:qualia-design");
263    let pred_has_part = q_hash("q42:hasPart");
264    let pred_relation = q_hash("q42:designRelation");
265    let pred_type = q_hash("rdf:type");
266    let type_design = q_hash("q42:Design");
267
268    let mut quins = Vec::new();
269
270    let mut root = NQuin::default();
271    root.subject = design_id;
272    root.predicate = pred_type;
273    root.object = type_design;
274    root.context = ctx;
275    root.parity = root.subject ^ root.predicate ^ root.object ^ root.context;
276    quins.push(root);
277
278    for part in &doc.parts {
279        let part_hash = q_hash(&part.id);
280        let mut q = NQuin::default();
281        q.subject = design_id;
282        q.predicate = pred_has_part;
283        q.object = part_hash;
284        q.context = ctx;
285        q.metadata = (part.intensity.clamp(0.0, 1.0) * 255.0) as u64;
286        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
287        quins.push(q);
288    }
289
290    for rel in &doc.relations {
291        let _ = doc
292            .parts
293            .iter()
294            .find(|p| p.id == rel.from)
295            .ok_or(DesignEncodeError::UnknownPartId)?;
296        let _ = doc
297            .parts
298            .iter()
299            .find(|p| p.id == rel.to)
300            .ok_or(DesignEncodeError::UnknownPartId)?;
301
302        let from_hash = q_hash(&rel.from);
303        let to_hash = q_hash(&rel.to);
304        let rel_hash = q_hash(&rel.relation_type);
305        let packed = (from_hash & 0xFFFF_FFFF) | ((to_hash & 0xFFFF) << 32);
306
307        let mut q = NQuin::default();
308        q.subject = design_id;
309        q.predicate = pred_relation;
310        q.object = packed ^ rel_hash;
311        q.context = ctx;
312        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
313        quins.push(q);
314    }
315
316    Ok(quins)
317}
318
319pub fn design_context_hash(doc: &DesignDocument) -> u64 {
320    if doc.title.is_empty() {
321        q_hash(&doc.prompt)
322    } else {
323        q_hash(&doc.title)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    fn sample_switch() -> DesignDocument {
332        DesignDocument {
333            doc_type: DESIGN_TYPE.to_string(),
334            version: DESIGN_VERSION.to_string(),
335            title: "Smart switch".to_string(),
336            summary: "Two-part assembly".to_string(),
337            prompt: "smart switch".to_string(),
338            parts: vec![
339                DesignPart {
340                    id: "base".into(),
341                    label: "Wall base".into(),
342                    role: "housing".into(),
343                    installer: "electrician".into(),
344                    components: vec![],
345                    pos: Some([0.0, 0.0, 0.0]),
346                    state: "active".into(),
347                    intensity: 0.9,
348                    reasons: vec!["mains wiring".into()],
349                },
350                DesignPart {
351                    id: "face".into(),
352                    label: "Smart face".into(),
353                    role: "smart-module".into(),
354                    installer: "user".into(),
355                    components: vec!["mcu".into(), "motion-sensor".into()],
356                    pos: Some([0.0, 0.4, 0.0]),
357                    state: "highlighted".into(),
358                    intensity: 0.75,
359                    reasons: vec![],
360                },
361            ],
362            relations: vec![DesignRelation {
363                from: "face".into(),
364                to: "base".into(),
365                relation_type: "matesWith".into(),
366                label: String::new(),
367            }],
368            explanations: vec!["Electrician installs base first".into()],
369            sparql_context: vec![],
370        }
371    }
372
373    #[test]
374    fn design_to_tensors_includes_relation_anchor() {
375        let doc = sample_switch();
376        let tensors = design_to_tensors(&doc).unwrap();
377        assert_eq!(tensors.len(), 3, "2 parts + 1 relation");
378    }
379
380    #[test]
381    fn design_to_quins_emits_root_and_relations() {
382        let doc = sample_switch();
383        let quins = design_to_quins(&doc).unwrap();
384        assert!(quins.len() >= 4);
385    }
386
387    #[test]
388    fn rejects_unknown_relation_endpoint() {
389        let mut doc = sample_switch();
390        doc.relations.push(DesignRelation {
391            from: "ghost".into(),
392            to: "base".into(),
393            relation_type: "matesWith".into(),
394            label: String::new(),
395        });
396        assert_eq!(
397            design_to_tensors(&doc),
398            Err(DesignEncodeError::UnknownPartId)
399        );
400    }
401}