1use 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#[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#[derive(Debug, Clone, Copy)]
36pub struct Quaternion {
37 pub w: f64,
39 pub x: f64,
41 pub y: f64,
43 pub z: f64,
45}
46
47impl Quaternion {
48 pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
50 Self { w, x, y, z }
51 }
52
53 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 pub fn to_array(&self) -> [f64; 4] {
61 [self.w, self.x, self.y, self.z]
62 }
63
64 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 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 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 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 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 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 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 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#[derive(Debug)]
138pub struct QuatD {
139 base: BaseModel,
141 entity_embeddings: Array2<f64>,
143 relation_embeddings: Array2<f64>,
145 embeddings_initialized: bool,
147 scoring_function: QuatDScoringFunction,
149 quaternion_regularization: f64,
151}
152
153#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
155pub enum QuatDScoringFunction {
156 Standard,
158 L2Distance,
160 CosineSimilarity,
162}
163
164impl QuatD {
165 pub fn new(config: ModelConfig) -> Self {
167 let base = BaseModel::new(config.clone());
168
169 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)), relation_embeddings: Array2::zeros((0, 4)), embeddings_initialized: false,
188 scoring_function,
189 quaternion_regularization,
190 }
191 }
192
193 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 self.entity_embeddings =
216 Array2::from_shape_fn((num_entities, 4), |_| rng.random_range(-0.1..0.1));
217
218 self.relation_embeddings =
220 Array2::from_shape_fn((num_relations, 4), |_| rng.random_range(-0.1..0.1));
221
222 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 fn normalize_all_quaternions(&mut self) {
234 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 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 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 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 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 let hr = h.multiply(&r);
288 Ok(hr.dot(&t))
289 }
290 QuatDScoringFunction::L2Distance => {
291 let hr = h.multiply(&r);
293 let diff = hr.subtract(&t);
294 Ok(-diff.norm())
295 }
296 QuatDScoringFunction::CosineSimilarity => {
297 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 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 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 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 self.compute_triple_gradients(pos_triple, pos_grad, &mut entity_grads, &mut relation_grads);
335
336 self.compute_triple_gradients(neg_triple, neg_grad, &mut entity_grads, &mut relation_grads);
338
339 Ok((entity_grads, relation_grads))
340 }
341
342 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 let hr = h.multiply(&r);
360
361 let r_conj = r.conjugate();
363 let grad_h = r_conj.multiply(&t).scale(loss_grad);
364
365 let h_conj = h.conjugate();
367 let grad_r = h_conj.multiply(&t).scale(loss_grad);
368
369 let grad_t = hr.scale(loss_grad);
371
372 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 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 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 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 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 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 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 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 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 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 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 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 if batch_loss > 0.0 {
507 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 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 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 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 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 let product = q1.multiply(&q2);
848 assert!(product.w.is_finite());
849
850 let conj = q1.conjugate();
852 assert_eq!(conj.w, q1.w);
853 assert_eq!(conj.x, -q1.x);
854
855 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) .with_max_epochs(10)
866 .with_seed(42);
867
868 let mut model = QuatD::new(config);
869
870 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 let stats = model.train(Some(5)).await?;
880 assert!(stats.epochs_completed > 0);
881
882 let alice_emb = model.get_entity_embedding("http://example.org/alice")?;
884 assert_eq!(alice_emb.dimensions, 4); let score = model.score_triple(
888 "http://example.org/alice",
889 "http://example.org/knows",
890 "http://example.org/bob",
891 )?;
892
893 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}