Skip to main content

oxirs_embed/models/
quatd.rs

1//! QuatE: Quaternion Embeddings for Knowledge Graph Completion
2//!
3//! QuatE models entities and relations as quaternions in a 4D space,
4//! using quaternion algebra for knowledge graph completion.
5//!
6//! Reference: Zhang et al. "Quaternion Knowledge Graph Embeddings" (2019)
7
8use crate::models::serialization::{BaseModelSnapshot, MatrixF64};
9use crate::models::BaseModel;
10use crate::{EmbeddingModel, ModelConfig, ModelStats, TrainingStats, Triple, Vector};
11use anyhow::{anyhow, Result};
12use async_trait::async_trait;
13use scirs2_core::ndarray_ext::Array2;
14use scirs2_core::random::{Random, SliceRandom};
15use serde::{Deserialize, Serialize};
16use std::fs::File;
17use std::io::{BufReader, BufWriter};
18use std::path::Path;
19use std::time::Instant;
20use tracing::{debug, info};
21use uuid::Uuid;
22
23/// Serializable representation of a QuatD model for persistence.
24#[derive(Debug, Serialize, Deserialize)]
25struct QuatDSerializable {
26    base: BaseModelSnapshot,
27    entity_embeddings: MatrixF64,
28    relation_embeddings: MatrixF64,
29    embeddings_initialized: bool,
30    scoring_function: QuatDScoringFunction,
31    quaternion_regularization: f64,
32}
33
34/// Quaternion representation for embeddings
35#[derive(Debug, Clone, Copy)]
36pub struct Quaternion {
37    /// Real component
38    pub w: f64,
39    /// i component
40    pub x: f64,
41    /// j component
42    pub y: f64,
43    /// k component
44    pub z: f64,
45}
46
47impl Quaternion {
48    /// Create a new quaternion
49    pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
50        Self { w, x, y, z }
51    }
52
53    /// Create a quaternion from a 4-element array
54    pub fn from_array(arr: &[f64]) -> Self {
55        assert_eq!(arr.len(), 4);
56        Self::new(arr[0], arr[1], arr[2], arr[3])
57    }
58
59    /// Convert quaternion to array
60    pub fn to_array(&self) -> [f64; 4] {
61        [self.w, self.x, self.y, self.z]
62    }
63
64    /// Quaternion multiplication (Hamilton product)
65    pub fn multiply(&self, other: &Quaternion) -> Quaternion {
66        Quaternion {
67            w: self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z,
68            x: self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y,
69            y: self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x,
70            z: self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w,
71        }
72    }
73
74    /// Quaternion conjugate
75    pub fn conjugate(&self) -> Quaternion {
76        Quaternion {
77            w: self.w,
78            x: -self.x,
79            y: -self.y,
80            z: -self.z,
81        }
82    }
83
84    /// Quaternion norm (magnitude)
85    pub fn norm(&self) -> f64 {
86        (self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
87    }
88
89    /// Normalize quaternion to unit length
90    pub fn normalize(&mut self) {
91        let norm = self.norm();
92        if norm > 1e-12 {
93            self.w /= norm;
94            self.x /= norm;
95            self.y /= norm;
96            self.z /= norm;
97        }
98    }
99
100    /// Quaternion dot product
101    pub fn dot(&self, other: &Quaternion) -> f64 {
102        self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z
103    }
104
105    /// Element-wise addition
106    pub fn add(&self, other: &Quaternion) -> Quaternion {
107        Quaternion {
108            w: self.w + other.w,
109            x: self.x + other.x,
110            y: self.y + other.y,
111            z: self.z + other.z,
112        }
113    }
114
115    /// Element-wise subtraction
116    pub fn subtract(&self, other: &Quaternion) -> Quaternion {
117        Quaternion {
118            w: self.w - other.w,
119            x: self.x - other.x,
120            y: self.y - other.y,
121            z: self.z - other.z,
122        }
123    }
124
125    /// Scalar multiplication
126    pub fn scale(&self, scalar: f64) -> Quaternion {
127        Quaternion {
128            w: self.w * scalar,
129            x: self.x * scalar,
130            y: self.y * scalar,
131            z: self.z * scalar,
132        }
133    }
134}
135
136/// QuatD embedding model
137#[derive(Debug)]
138pub struct QuatD {
139    /// Base model functionality
140    base: BaseModel,
141    /// Entity embeddings as quaternions (num_entities × 4)
142    entity_embeddings: Array2<f64>,
143    /// Relation embeddings as quaternions (num_relations × 4)
144    relation_embeddings: Array2<f64>,
145    /// Whether embeddings have been initialized
146    embeddings_initialized: bool,
147    /// Scoring function variant
148    scoring_function: QuatDScoringFunction,
149    /// Regularization parameters
150    quaternion_regularization: f64,
151}
152
153/// Scoring function variants for QuatD
154#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
155pub enum QuatDScoringFunction {
156    /// Original QuatD scoring function
157    Standard,
158    /// QuatD with L2 distance
159    L2Distance,
160    /// QuatD with cosine similarity
161    CosineSimilarity,
162}
163
164impl QuatD {
165    /// Create a new QuatD model
166    pub fn new(config: ModelConfig) -> Self {
167        let base = BaseModel::new(config.clone());
168
169        // Get QuatD-specific parameters
170        let scoring_function = match config.model_params.get("scoring_function") {
171            Some(0.0) => QuatDScoringFunction::Standard,
172            Some(1.0) => QuatDScoringFunction::L2Distance,
173            Some(2.0) => QuatDScoringFunction::CosineSimilarity,
174            _ => QuatDScoringFunction::Standard,
175        };
176
177        let quaternion_regularization = config
178            .model_params
179            .get("quaternion_regularization")
180            .copied()
181            .unwrap_or(0.05);
182
183        Self {
184            base,
185            entity_embeddings: Array2::zeros((0, 4)), // 4D quaternions
186            relation_embeddings: Array2::zeros((0, 4)), // 4D quaternions
187            embeddings_initialized: false,
188            scoring_function,
189            quaternion_regularization,
190        }
191    }
192
193    /// Initialize embeddings after entities and relations are known
194    fn initialize_embeddings(&mut self) {
195        if self.embeddings_initialized {
196            return;
197        }
198
199        let num_entities = self.base.num_entities();
200        let num_relations = self.base.num_relations();
201
202        if num_entities == 0 || num_relations == 0 {
203            return;
204        }
205
206        let mut rng = Random::seed(self.base.config.seed.unwrap_or_else(|| {
207            use std::time::{SystemTime, UNIX_EPOCH};
208            SystemTime::now()
209                .duration_since(UNIX_EPOCH)
210                .expect("system time should be after UNIX_EPOCH")
211                .as_secs()
212        }));
213
214        // Initialize entity embeddings as quaternions
215        self.entity_embeddings =
216            Array2::from_shape_fn((num_entities, 4), |_| rng.random_range(-0.1..0.1));
217
218        // Initialize relation embeddings as quaternions
219        self.relation_embeddings =
220            Array2::from_shape_fn((num_relations, 4), |_| rng.random_range(-0.1..0.1));
221
222        // Normalize quaternions to unit length
223        self.normalize_all_quaternions();
224
225        self.embeddings_initialized = true;
226        debug!(
227            "Initialized QuatD embeddings: {} entities, {} relations (4D quaternions)",
228            num_entities, num_relations
229        );
230    }
231
232    /// Normalize all quaternion embeddings to unit length
233    fn normalize_all_quaternions(&mut self) {
234        // Normalize entity embeddings
235        for mut row in self.entity_embeddings.rows_mut() {
236            let mut quat =
237                Quaternion::from_array(row.as_slice().expect("row should be contiguous"));
238            quat.normalize();
239            let normalized = quat.to_array();
240            for (i, &val) in normalized.iter().enumerate() {
241                row[i] = val;
242            }
243        }
244
245        // Normalize relation embeddings
246        for mut row in self.relation_embeddings.rows_mut() {
247            let mut quat =
248                Quaternion::from_array(row.as_slice().expect("row should be contiguous"));
249            quat.normalize();
250            let normalized = quat.to_array();
251            for (i, &val) in normalized.iter().enumerate() {
252                row[i] = val;
253            }
254        }
255    }
256
257    /// Get quaternion from entity embeddings
258    fn get_entity_quaternion(&self, entity_id: usize) -> Quaternion {
259        let row = self.entity_embeddings.row(entity_id);
260        Quaternion::from_array(row.as_slice().expect("row should be contiguous"))
261    }
262
263    /// Get quaternion from relation embeddings
264    fn get_relation_quaternion(&self, relation_id: usize) -> Quaternion {
265        let row = self.relation_embeddings.row(relation_id);
266        Quaternion::from_array(row.as_slice().expect("row should be contiguous"))
267    }
268
269    /// Score a triple using QuatD scoring function
270    fn score_triple_ids(
271        &self,
272        subject_id: usize,
273        predicate_id: usize,
274        object_id: usize,
275    ) -> Result<f64> {
276        if !self.embeddings_initialized {
277            return Err(anyhow!("Model not trained"));
278        }
279
280        let h = self.get_entity_quaternion(subject_id);
281        let r = self.get_relation_quaternion(predicate_id);
282        let t = self.get_entity_quaternion(object_id);
283
284        match self.scoring_function {
285            QuatDScoringFunction::Standard => {
286                // QuatD scoring: σ(h ∘ r · t)
287                let hr = h.multiply(&r);
288                Ok(hr.dot(&t))
289            }
290            QuatDScoringFunction::L2Distance => {
291                // L2 distance: -||h ∘ r - t||₂
292                let hr = h.multiply(&r);
293                let diff = hr.subtract(&t);
294                Ok(-diff.norm())
295            }
296            QuatDScoringFunction::CosineSimilarity => {
297                // Cosine similarity between h ∘ r and t
298                let hr = h.multiply(&r);
299                let dot_product = hr.dot(&t);
300                let magnitude_product = hr.norm() * t.norm();
301                if magnitude_product > 1e-12 {
302                    Ok(dot_product / magnitude_product)
303                } else {
304                    Ok(0.0)
305                }
306            }
307        }
308    }
309
310    /// Compute gradients for QuatD
311    fn compute_gradients(
312        &self,
313        pos_triple: (usize, usize, usize),
314        neg_triple: (usize, usize, usize),
315    ) -> Result<(Array2<f64>, Array2<f64>)> {
316        let (pos_s, pos_p, pos_o) = pos_triple;
317        let (neg_s, neg_p, neg_o) = neg_triple;
318
319        let mut entity_grads = Array2::zeros(self.entity_embeddings.raw_dim());
320        let mut relation_grads = Array2::zeros(self.relation_embeddings.raw_dim());
321
322        // Compute scores
323        let pos_score = self.score_triple_ids(pos_s, pos_p, pos_o)?;
324        let neg_score = self.score_triple_ids(neg_s, neg_p, neg_o)?;
325
326        // Sigmoid derivatives
327        let pos_sigmoid = 1.0 / (1.0 + (-pos_score).exp());
328        let neg_sigmoid = 1.0 / (1.0 + (-neg_score).exp());
329
330        let pos_grad = pos_sigmoid - 1.0;
331        let neg_grad = neg_sigmoid;
332
333        // Compute gradients for positive triple
334        self.compute_triple_gradients(pos_triple, pos_grad, &mut entity_grads, &mut relation_grads);
335
336        // Compute gradients for negative triple
337        self.compute_triple_gradients(neg_triple, neg_grad, &mut entity_grads, &mut relation_grads);
338
339        Ok((entity_grads, relation_grads))
340    }
341
342    /// Compute gradients for a single triple
343    fn compute_triple_gradients(
344        &self,
345        triple: (usize, usize, usize),
346        loss_grad: f64,
347        entity_grads: &mut Array2<f64>,
348        relation_grads: &mut Array2<f64>,
349    ) {
350        let (s, p, o) = triple;
351
352        let h = self.get_entity_quaternion(s);
353        let r = self.get_relation_quaternion(p);
354        let t = self.get_entity_quaternion(o);
355
356        match self.scoring_function {
357            QuatDScoringFunction::Standard => {
358                // Gradients for h ∘ r · t scoring
359                let hr = h.multiply(&r);
360
361                // ∂score/∂h = (r · t) where · is quaternion multiplication with t
362                let r_conj = r.conjugate();
363                let grad_h = r_conj.multiply(&t).scale(loss_grad);
364
365                // ∂score/∂r = (h^* · t) where ^* is conjugate
366                let h_conj = h.conjugate();
367                let grad_r = h_conj.multiply(&t).scale(loss_grad);
368
369                // ∂score/∂t = (h ∘ r)
370                let grad_t = hr.scale(loss_grad);
371
372                // Add gradients
373                let grad_h_arr = grad_h.to_array();
374                let grad_r_arr = grad_r.to_array();
375                let grad_t_arr = grad_t.to_array();
376
377                for i in 0..4 {
378                    entity_grads[[s, i]] += grad_h_arr[i];
379                    relation_grads[[p, i]] += grad_r_arr[i];
380                    entity_grads[[o, i]] += grad_t_arr[i];
381                }
382            }
383            QuatDScoringFunction::L2Distance => {
384                // Gradients for -||h ∘ r - t||₂ scoring
385                let hr = h.multiply(&r);
386                let diff = hr.subtract(&t);
387                let norm = diff.norm();
388
389                if norm > 1e-12 {
390                    let scale = -loss_grad / norm;
391
392                    // Similar quaternion gradient computation but scaled by norm
393                    let r_conj = r.conjugate();
394                    let grad_h = r_conj.scale(scale);
395
396                    let h_conj = h.conjugate();
397                    let grad_r = h_conj.scale(scale);
398
399                    let grad_t = diff.scale(-scale);
400
401                    let grad_h_arr = grad_h.to_array();
402                    let grad_r_arr = grad_r.to_array();
403                    let grad_t_arr = grad_t.to_array();
404
405                    for i in 0..4 {
406                        entity_grads[[s, i]] += grad_h_arr[i];
407                        relation_grads[[p, i]] += grad_r_arr[i];
408                        entity_grads[[o, i]] += grad_t_arr[i];
409                    }
410                }
411            }
412            QuatDScoringFunction::CosineSimilarity => {
413                // Gradients for cosine similarity
414                let hr = h.multiply(&r);
415                let dot_product = hr.dot(&t);
416                let hr_norm = hr.norm();
417                let t_norm = t.norm();
418                let magnitude_product = hr_norm * t_norm;
419
420                if magnitude_product > 1e-12 {
421                    let cos_sim = dot_product / magnitude_product;
422
423                    // Complex gradients for cosine similarity - simplified version
424                    let scale = loss_grad / magnitude_product;
425
426                    let grad_hr = t
427                        .subtract(&hr.scale(cos_sim / (hr_norm * hr_norm)))
428                        .scale(scale);
429                    let grad_t = hr
430                        .subtract(&t.scale(cos_sim / (t_norm * t_norm)))
431                        .scale(scale);
432
433                    // Backpropagate through quaternion multiplication for grad_hr
434                    let r_conj = r.conjugate();
435                    let grad_h = r_conj.multiply(&grad_hr);
436
437                    let h_conj = h.conjugate();
438                    let grad_r = h_conj.multiply(&grad_hr);
439
440                    let grad_h_arr = grad_h.to_array();
441                    let grad_r_arr = grad_r.to_array();
442                    let grad_t_arr = grad_t.to_array();
443
444                    for i in 0..4 {
445                        entity_grads[[s, i]] += grad_h_arr[i];
446                        relation_grads[[p, i]] += grad_r_arr[i];
447                        entity_grads[[o, i]] += grad_t_arr[i];
448                    }
449                }
450            }
451        }
452    }
453
454    /// Perform one training epoch
455    async fn train_epoch(&mut self, learning_rate: f64) -> Result<f64> {
456        let mut rng = Random::seed(self.base.config.seed.unwrap_or_else(|| {
457            use std::time::{SystemTime, UNIX_EPOCH};
458            SystemTime::now()
459                .duration_since(UNIX_EPOCH)
460                .expect("system time should be after UNIX_EPOCH")
461                .as_secs()
462        }));
463
464        let mut total_loss = 0.0;
465        let num_batches = (self.base.triples.len() + self.base.config.batch_size - 1)
466            / self.base.config.batch_size;
467
468        // Create shuffled batches
469        let mut shuffled_triples = self.base.triples.clone();
470        shuffled_triples.shuffle(&mut rng);
471
472        for batch_triples in shuffled_triples.chunks(self.base.config.batch_size) {
473            let mut batch_entity_grads = Array2::zeros(self.entity_embeddings.raw_dim());
474            let mut batch_relation_grads = Array2::zeros(self.relation_embeddings.raw_dim());
475            let mut batch_loss = 0.0;
476
477            for &pos_triple in batch_triples {
478                // Generate negative samples
479                let neg_samples = self
480                    .base
481                    .generate_negative_samples(self.base.config.negative_samples, &mut rng);
482
483                for neg_triple in neg_samples {
484                    // Compute scores
485                    let pos_score =
486                        self.score_triple_ids(pos_triple.0, pos_triple.1, pos_triple.2)?;
487                    let neg_score =
488                        self.score_triple_ids(neg_triple.0, neg_triple.1, neg_triple.2)?;
489
490                    // Logistic loss
491                    let pos_loss = -(1.0 / (1.0 + (-pos_score).exp())).ln();
492                    let neg_loss = -(1.0 / (1.0 + neg_score.exp())).ln();
493                    let loss = pos_loss + neg_loss;
494                    batch_loss += loss;
495
496                    // Compute and accumulate gradients
497                    let (entity_grads, relation_grads) =
498                        self.compute_gradients(pos_triple, neg_triple)?;
499
500                    batch_entity_grads += &entity_grads;
501                    batch_relation_grads += &relation_grads;
502                }
503            }
504
505            // Apply gradients with quaternion regularization
506            if batch_loss > 0.0 {
507                // Update entity embeddings
508                for (((_i, _j), embedding_val), grad_val) in self
509                    .entity_embeddings
510                    .indexed_iter_mut()
511                    .zip(batch_entity_grads.iter())
512                {
513                    let reg_term = self.quaternion_regularization * *embedding_val;
514                    *embedding_val -= learning_rate * (grad_val + reg_term);
515                }
516
517                // Update relation embeddings
518                for (((_i, _j), embedding_val), grad_val) in self
519                    .relation_embeddings
520                    .indexed_iter_mut()
521                    .zip(batch_relation_grads.iter())
522                {
523                    let reg_term = self.quaternion_regularization * *embedding_val;
524                    *embedding_val -= learning_rate * (grad_val + reg_term);
525                }
526
527                // Normalize quaternions after update
528                self.normalize_all_quaternions();
529            }
530
531            total_loss += batch_loss;
532        }
533
534        Ok(total_loss / num_batches as f64)
535    }
536}
537
538#[async_trait]
539impl EmbeddingModel for QuatD {
540    fn config(&self) -> &ModelConfig {
541        &self.base.config
542    }
543
544    fn model_id(&self) -> &Uuid {
545        &self.base.model_id
546    }
547
548    fn model_type(&self) -> &'static str {
549        "QuatD"
550    }
551
552    fn add_triple(&mut self, triple: Triple) -> Result<()> {
553        self.base.add_triple(triple)
554    }
555
556    async fn train(&mut self, epochs: Option<usize>) -> Result<TrainingStats> {
557        let start_time = Instant::now();
558        let max_epochs = epochs.unwrap_or(self.base.config.max_epochs);
559
560        // Initialize embeddings if needed
561        self.initialize_embeddings();
562
563        if !self.embeddings_initialized {
564            return Err(anyhow!("No training data available"));
565        }
566
567        let mut loss_history = Vec::new();
568        let learning_rate = self.base.config.learning_rate;
569
570        info!("Starting QuatD training for {} epochs", max_epochs);
571
572        for epoch in 0..max_epochs {
573            let epoch_loss = self.train_epoch(learning_rate).await?;
574            loss_history.push(epoch_loss);
575
576            if epoch % 100 == 0 {
577                debug!("Epoch {}: loss = {:.6}", epoch, epoch_loss);
578            }
579
580            // Simple convergence check
581            if epoch > 10 && epoch_loss < 1e-6 {
582                info!("Converged at epoch {} with loss {:.6}", epoch, epoch_loss);
583                break;
584            }
585        }
586
587        self.base.mark_trained();
588        let training_time = start_time.elapsed().as_secs_f64();
589
590        Ok(TrainingStats {
591            epochs_completed: loss_history.len(),
592            final_loss: loss_history.last().copied().unwrap_or(0.0),
593            training_time_seconds: training_time,
594            convergence_achieved: loss_history.last().copied().unwrap_or(f64::INFINITY) < 1e-6,
595            loss_history,
596        })
597    }
598
599    fn get_entity_embedding(&self, entity: &str) -> Result<Vector> {
600        if !self.embeddings_initialized {
601            return Err(anyhow!("Model not trained"));
602        }
603
604        let entity_id = self
605            .base
606            .get_entity_id(entity)
607            .ok_or_else(|| anyhow!("Entity not found: {}", entity))?;
608
609        let embedding = self.entity_embeddings.row(entity_id).to_owned();
610        Ok(Vector::new(
611            embedding.to_vec().into_iter().map(|x| x as f32).collect(),
612        ))
613    }
614
615    fn get_relation_embedding(&self, relation: &str) -> Result<Vector> {
616        if !self.embeddings_initialized {
617            return Err(anyhow!("Model not trained"));
618        }
619
620        let relation_id = self
621            .base
622            .get_relation_id(relation)
623            .ok_or_else(|| anyhow!("Relation not found: {}", relation))?;
624
625        let embedding = self.relation_embeddings.row(relation_id).to_owned();
626        Ok(Vector::new(
627            embedding.to_vec().into_iter().map(|x| x as f32).collect(),
628        ))
629    }
630
631    fn score_triple(&self, subject: &str, predicate: &str, object: &str) -> Result<f64> {
632        let subject_id = self
633            .base
634            .get_entity_id(subject)
635            .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
636        let predicate_id = self
637            .base
638            .get_relation_id(predicate)
639            .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
640        let object_id = self
641            .base
642            .get_entity_id(object)
643            .ok_or_else(|| anyhow!("Object not found: {}", object))?;
644
645        self.score_triple_ids(subject_id, predicate_id, object_id)
646    }
647
648    fn predict_objects(
649        &self,
650        subject: &str,
651        predicate: &str,
652        k: usize,
653    ) -> Result<Vec<(String, f64)>> {
654        if !self.embeddings_initialized {
655            return Err(anyhow!("Model not trained"));
656        }
657
658        let subject_id = self
659            .base
660            .get_entity_id(subject)
661            .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
662        let predicate_id = self
663            .base
664            .get_relation_id(predicate)
665            .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
666
667        let mut scores = Vec::new();
668
669        for object_id in 0..self.base.num_entities() {
670            let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
671            let object_name = self
672                .base
673                .get_entity(object_id)
674                .expect("entity should exist in index")
675                .clone();
676            scores.push((object_name, score));
677        }
678
679        scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("scores should be comparable"));
680        scores.truncate(k);
681
682        Ok(scores)
683    }
684
685    fn predict_subjects(
686        &self,
687        predicate: &str,
688        object: &str,
689        k: usize,
690    ) -> Result<Vec<(String, f64)>> {
691        if !self.embeddings_initialized {
692            return Err(anyhow!("Model not trained"));
693        }
694
695        let predicate_id = self
696            .base
697            .get_relation_id(predicate)
698            .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
699        let object_id = self
700            .base
701            .get_entity_id(object)
702            .ok_or_else(|| anyhow!("Object not found: {}", object))?;
703
704        let mut scores = Vec::new();
705
706        for subject_id in 0..self.base.num_entities() {
707            let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
708            let subject_name = self
709                .base
710                .get_entity(subject_id)
711                .expect("entity should exist in index")
712                .clone();
713            scores.push((subject_name, score));
714        }
715
716        scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("scores should be comparable"));
717        scores.truncate(k);
718
719        Ok(scores)
720    }
721
722    fn predict_relations(
723        &self,
724        subject: &str,
725        object: &str,
726        k: usize,
727    ) -> Result<Vec<(String, f64)>> {
728        if !self.embeddings_initialized {
729            return Err(anyhow!("Model not trained"));
730        }
731
732        let subject_id = self
733            .base
734            .get_entity_id(subject)
735            .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
736        let object_id = self
737            .base
738            .get_entity_id(object)
739            .ok_or_else(|| anyhow!("Object not found: {}", object))?;
740
741        let mut scores = Vec::new();
742
743        for predicate_id in 0..self.base.num_relations() {
744            let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
745            let predicate_name = self
746                .base
747                .get_relation(predicate_id)
748                .expect("relation should exist in index")
749                .clone();
750            scores.push((predicate_name, score));
751        }
752
753        scores.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("scores should be comparable"));
754        scores.truncate(k);
755
756        Ok(scores)
757    }
758
759    fn get_entities(&self) -> Vec<String> {
760        self.base.get_entities()
761    }
762
763    fn get_relations(&self) -> Vec<String> {
764        self.base.get_relations()
765    }
766
767    fn get_stats(&self) -> ModelStats {
768        self.base.get_stats("QuatD")
769    }
770
771    fn save(&self, path: &str) -> Result<()> {
772        info!("Saving QuatD model to {}", path);
773
774        let serializable = QuatDSerializable {
775            base: BaseModelSnapshot::capture(&self.base),
776            entity_embeddings: MatrixF64::from_array(&self.entity_embeddings),
777            relation_embeddings: MatrixF64::from_array(&self.relation_embeddings),
778            embeddings_initialized: self.embeddings_initialized,
779            scoring_function: self.scoring_function,
780            quaternion_regularization: self.quaternion_regularization,
781        };
782
783        let file = File::create(path)
784            .map_err(|e| anyhow!("Failed to create model file {}: {}", path, e))?;
785        let writer = BufWriter::new(file);
786        oxicode::serde::encode_into_std_write(&serializable, writer, oxicode::config::standard())
787            .map_err(|e| anyhow!("Failed to serialize QuatD model: {}", e))?;
788
789        info!("QuatD model saved successfully");
790        Ok(())
791    }
792
793    fn load(&mut self, path: &str) -> Result<()> {
794        info!("Loading QuatD model from {}", path);
795
796        if !Path::new(path).exists() {
797            return Err(anyhow!("Model file not found: {}", path));
798        }
799
800        let file =
801            File::open(path).map_err(|e| anyhow!("Failed to open model file {}: {}", path, e))?;
802        let reader = BufReader::new(file);
803        let (serializable, _): (QuatDSerializable, _) =
804            oxicode::serde::decode_from_std_read(reader, oxicode::config::standard())
805                .map_err(|e| anyhow!("Failed to deserialize QuatD model: {}", e))?;
806
807        self.entity_embeddings = serializable.entity_embeddings.to_array()?;
808        self.relation_embeddings = serializable.relation_embeddings.to_array()?;
809        self.embeddings_initialized = serializable.embeddings_initialized;
810        self.scoring_function = serializable.scoring_function;
811        self.quaternion_regularization = serializable.quaternion_regularization;
812        serializable.base.restore_into(&mut self.base);
813
814        info!("QuatD model loaded successfully");
815        Ok(())
816    }
817
818    fn clear(&mut self) {
819        self.base.clear();
820        self.entity_embeddings = Array2::zeros((0, 4));
821        self.relation_embeddings = Array2::zeros((0, 4));
822        self.embeddings_initialized = false;
823    }
824
825    fn is_trained(&self) -> bool {
826        self.base.is_trained
827    }
828
829    async fn encode(&self, _texts: &[String]) -> Result<Vec<Vec<f32>>> {
830        Err(anyhow!(
831            "Knowledge graph embedding model does not support text encoding"
832        ))
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839    use crate::NamedNode;
840
841    #[test]
842    fn test_quaternion_operations() {
843        let q1 = Quaternion::new(1.0, 2.0, 3.0, 4.0);
844        let q2 = Quaternion::new(2.0, 3.0, 4.0, 5.0);
845
846        // Test multiplication
847        let product = q1.multiply(&q2);
848        assert!(product.w.is_finite());
849
850        // Test conjugate
851        let conj = q1.conjugate();
852        assert_eq!(conj.w, q1.w);
853        assert_eq!(conj.x, -q1.x);
854
855        // Test normalization
856        let mut q3 = q1;
857        q3.normalize();
858        assert!((q3.norm() - 1.0).abs() < 1e-10);
859    }
860
861    #[tokio::test]
862    async fn test_quatd_basic() -> Result<()> {
863        let config = ModelConfig::default()
864            .with_dimensions(4) // Always 4 for quaternions
865            .with_max_epochs(10)
866            .with_seed(42);
867
868        let mut model = QuatD::new(config);
869
870        // Add test triples
871        let alice = NamedNode::new("http://example.org/alice")?;
872        let knows = NamedNode::new("http://example.org/knows")?;
873        let bob = NamedNode::new("http://example.org/bob")?;
874
875        model.add_triple(Triple::new(alice.clone(), knows.clone(), bob.clone()))?;
876        model.add_triple(Triple::new(bob.clone(), knows.clone(), alice.clone()))?;
877
878        // Train
879        let stats = model.train(Some(5)).await?;
880        assert!(stats.epochs_completed > 0);
881
882        // Test embeddings
883        let alice_emb = model.get_entity_embedding("http://example.org/alice")?;
884        assert_eq!(alice_emb.dimensions, 4); // Quaternion dimension
885
886        // Test scoring
887        let score = model.score_triple(
888            "http://example.org/alice",
889            "http://example.org/knows",
890            "http://example.org/bob",
891        )?;
892
893        // Score should be a finite number
894        assert!(score.is_finite());
895
896        Ok(())
897    }
898
899    #[test]
900    fn test_quatd_creation() {
901        let config = ModelConfig::default();
902        let quatd = QuatD::new(config);
903        assert!(!quatd.embeddings_initialized);
904        assert_eq!(quatd.model_type(), "QuatD");
905    }
906}