1use crate::models::serialization::{BaseModelSnapshot, MatrixF64};
10use crate::models::{common::*, BaseModel};
11use crate::{EmbeddingModel, ModelConfig, ModelStats, TrainingStats, Triple, Vector};
12use anyhow::{anyhow, Result};
13use async_trait::async_trait;
14use scirs2_core::ndarray_ext::Array2;
15#[allow(unused_imports)]
16use scirs2_core::random::{Random, RngExt};
17use serde::{Deserialize, Serialize};
18use std::fs::File;
19use std::io::{BufReader, BufWriter};
20use std::path::Path;
21use std::time::Instant;
22use tracing::{debug, info};
23use uuid::Uuid;
24
25#[derive(Debug, Serialize, Deserialize)]
27struct RotatESerializable {
28 base: BaseModelSnapshot,
29 entity_embeddings_real: MatrixF64,
30 entity_embeddings_imag: MatrixF64,
31 relation_phases: MatrixF64,
32 embeddings_initialized: bool,
33 adversarial_temperature: f64,
34 modulus_constraint: bool,
35}
36
37#[derive(Debug)]
39pub struct RotatE {
40 base: BaseModel,
42 entity_embeddings_real: Array2<f64>,
44 entity_embeddings_imag: Array2<f64>,
46 relation_phases: Array2<f64>,
48 embeddings_initialized: bool,
50 adversarial_temperature: f64,
52 modulus_constraint: bool,
54}
55
56impl RotatE {
57 pub fn new(config: ModelConfig) -> Self {
59 let base = BaseModel::new(config.clone());
60
61 let adversarial_temperature = config
63 .model_params
64 .get("adversarial_temperature")
65 .copied()
66 .unwrap_or(1.0);
67
68 let modulus_constraint = config
69 .model_params
70 .get("modulus_constraint")
71 .map(|&x| x > 0.0)
72 .unwrap_or(true);
73
74 Self {
75 base,
76 entity_embeddings_real: Array2::zeros((0, config.dimensions)),
77 entity_embeddings_imag: Array2::zeros((0, config.dimensions)),
78 relation_phases: Array2::zeros((0, config.dimensions)),
79 embeddings_initialized: false,
80 adversarial_temperature,
81 modulus_constraint,
82 }
83 }
84
85 fn initialize_embeddings(&mut self) {
87 if self.embeddings_initialized {
88 return;
89 }
90
91 let num_entities = self.base.num_entities();
92 let num_relations = self.base.num_relations();
93 let dimensions = self.base.config.dimensions;
94
95 if num_entities == 0 || num_relations == 0 {
96 return;
97 }
98
99 let mut rng = Random::default();
100
101 self.entity_embeddings_real = uniform_init((num_entities, dimensions), -1.0, 1.0, &mut rng);
103
104 self.entity_embeddings_imag = uniform_init((num_entities, dimensions), -1.0, 1.0, &mut rng);
105
106 self.relation_phases = uniform_init(
108 (num_relations, dimensions),
109 0.0,
110 2.0 * std::f64::consts::PI,
111 &mut rng,
112 );
113
114 if self.modulus_constraint {
116 self.apply_modulus_constraint();
117 }
118
119 self.embeddings_initialized = true;
120 debug!(
121 "Initialized RotatE embeddings: {} entities, {} relations, {} dimensions",
122 num_entities, num_relations, dimensions
123 );
124 }
125
126 fn apply_modulus_constraint(&mut self) {
128 for i in 0..self.entity_embeddings_real.nrows() {
129 let mut real_row = self.entity_embeddings_real.row_mut(i);
130 let mut imag_row = self.entity_embeddings_imag.row_mut(i);
131
132 for j in 0..real_row.len() {
133 let real = real_row[j];
134 let imag = imag_row[j];
135 let modulus = (real * real + imag * imag).sqrt();
136
137 if modulus > 1e-10 {
138 real_row[j] = real / modulus;
139 imag_row[j] = imag / modulus;
140 }
141 }
142 }
143 }
144
145 fn score_triple_ids(
148 &self,
149 subject_id: usize,
150 predicate_id: usize,
151 object_id: usize,
152 ) -> Result<f64> {
153 if !self.embeddings_initialized {
154 return Err(anyhow!("Model not trained"));
155 }
156
157 let h_real = self.entity_embeddings_real.row(subject_id);
158 let h_imag = self.entity_embeddings_imag.row(subject_id);
159 let r_phases = self.relation_phases.row(predicate_id);
160 let t_real = self.entity_embeddings_real.row(object_id);
161 let t_imag = self.entity_embeddings_imag.row(object_id);
162
163 let mut distance_squared = 0.0;
169
170 for ((((&h_r, &h_i), &phase), &t_r), &t_i) in h_real
171 .iter()
172 .zip(h_imag.iter())
173 .zip(r_phases.iter())
174 .zip(t_real.iter())
175 .zip(t_imag.iter())
176 {
177 let cos_phase = phase.cos();
178 let sin_phase = phase.sin();
179
180 let rotated_real = h_r * cos_phase - h_i * sin_phase;
182 let rotated_imag = h_r * sin_phase + h_i * cos_phase;
183
184 let diff_real = rotated_real - t_r;
186 let diff_imag = rotated_imag - t_i;
187
188 distance_squared += diff_real * diff_real + diff_imag * diff_imag;
189 }
190
191 Ok(-distance_squared.sqrt())
193 }
194
195 fn compute_gradients(
197 &self,
198 pos_triple: (usize, usize, usize),
199 neg_triple: (usize, usize, usize),
200 pos_score: f64,
201 neg_score: f64,
202 ) -> Result<(Array2<f64>, Array2<f64>, Array2<f64>)> {
203 let mut entity_grads_real = Array2::zeros(self.entity_embeddings_real.raw_dim());
204 let mut entity_grads_imag = Array2::zeros(self.entity_embeddings_imag.raw_dim());
205 let mut relation_grads = Array2::zeros(self.relation_phases.raw_dim());
206
207 let margin = self
209 .base
210 .config
211 .model_params
212 .get("margin")
213 .copied()
214 .unwrap_or(6.0);
215 let loss = margin + (-pos_score) - (-neg_score); if loss > 0.0 {
218 self.add_triple_gradients(
220 pos_triple,
221 1.0,
222 &mut entity_grads_real,
223 &mut entity_grads_imag,
224 &mut relation_grads,
225 );
226
227 self.add_triple_gradients(
229 neg_triple,
230 -1.0,
231 &mut entity_grads_real,
232 &mut entity_grads_imag,
233 &mut relation_grads,
234 );
235 }
236
237 Ok((entity_grads_real, entity_grads_imag, relation_grads))
238 }
239
240 fn add_triple_gradients(
242 &self,
243 triple: (usize, usize, usize),
244 grad_coeff: f64,
245 entity_grads_real: &mut Array2<f64>,
246 entity_grads_imag: &mut Array2<f64>,
247 relation_grads: &mut Array2<f64>,
248 ) {
249 let (s, p, o) = triple;
250
251 let h_real = self.entity_embeddings_real.row(s);
252 let h_imag = self.entity_embeddings_imag.row(s);
253 let r_phases = self.relation_phases.row(p);
254 let t_real = self.entity_embeddings_real.row(o);
255 let t_imag = self.entity_embeddings_imag.row(o);
256
257 for (i, ((((&h_r, &h_i), &phase), &t_r), &t_i)) in h_real
258 .iter()
259 .zip(h_imag.iter())
260 .zip(r_phases.iter())
261 .zip(t_real.iter())
262 .zip(t_imag.iter())
263 .enumerate()
264 {
265 let cos_phase = phase.cos();
266 let sin_phase = phase.sin();
267
268 let rotated_real = h_r * cos_phase - h_i * sin_phase;
270 let rotated_imag = h_r * sin_phase + h_i * cos_phase;
271
272 let diff_real = rotated_real - t_r;
274 let diff_imag = rotated_imag - t_i;
275
276 let distance = (diff_real * diff_real + diff_imag * diff_imag).sqrt();
277
278 if distance > 1e-10 {
279 let norm_factor = grad_coeff / distance;
280 let grad_real = diff_real * norm_factor;
281 let grad_imag = diff_imag * norm_factor;
282
283 entity_grads_real[[s, i]] += grad_real * cos_phase + grad_imag * sin_phase;
285 entity_grads_imag[[s, i]] += -grad_real * sin_phase + grad_imag * cos_phase;
286
287 entity_grads_real[[o, i]] -= grad_real;
289 entity_grads_imag[[o, i]] -= grad_imag;
290
291 let phase_grad = grad_real * (-h_r * sin_phase - h_i * cos_phase)
293 + grad_imag * (h_r * cos_phase - h_i * sin_phase);
294 relation_grads[[p, i]] += phase_grad;
295 }
296 }
297 }
298
299 fn generate_adversarial_negatives(
301 &self,
302 positive_triple: (usize, usize, usize),
303 num_samples: usize,
304 rng: &mut Random,
305 ) -> Vec<(usize, usize, usize)> {
306 let mut negatives = Vec::new();
307 let num_entities = self.base.num_entities();
308
309 for _ in 0..num_samples {
310 let corrupt_head = rng.random_f64() < 0.5;
312
313 if corrupt_head {
314 let mut candidate_scores = Vec::new();
316 for entity_id in 0..num_entities {
317 if entity_id != positive_triple.0 {
318 let neg_triple = (entity_id, positive_triple.1, positive_triple.2);
319 if let Ok(score) =
320 self.score_triple_ids(neg_triple.0, neg_triple.1, neg_triple.2)
321 {
322 candidate_scores.push((entity_id, score));
323 }
324 }
325 }
326
327 if !candidate_scores.is_empty() {
328 let weights: Vec<f64> = candidate_scores
330 .iter()
331 .map(|(_, score)| (-score / self.adversarial_temperature).exp())
332 .collect();
333
334 let total_weight: f64 = weights.iter().sum();
335 let mut cumulative = 0.0;
336 let threshold = rng.random_f64() * total_weight;
337
338 for (i, &weight) in weights.iter().enumerate() {
339 cumulative += weight;
340 if cumulative >= threshold {
341 let entity_id = candidate_scores[i].0;
342 negatives.push((entity_id, positive_triple.1, positive_triple.2));
343 break;
344 }
345 }
346 }
347 } else {
348 let mut candidate_scores = Vec::new();
350 for entity_id in 0..num_entities {
351 if entity_id != positive_triple.2 {
352 let neg_triple = (positive_triple.0, positive_triple.1, entity_id);
353 if let Ok(score) =
354 self.score_triple_ids(neg_triple.0, neg_triple.1, neg_triple.2)
355 {
356 candidate_scores.push((entity_id, score));
357 }
358 }
359 }
360
361 if !candidate_scores.is_empty() {
362 let weights: Vec<f64> = candidate_scores
363 .iter()
364 .map(|(_, score)| (-score / self.adversarial_temperature).exp())
365 .collect();
366
367 let total_weight: f64 = weights.iter().sum();
368 let mut cumulative = 0.0;
369 let threshold = rng.random_f64() * total_weight;
370
371 for (i, &weight) in weights.iter().enumerate() {
372 cumulative += weight;
373 if cumulative >= threshold {
374 let entity_id = candidate_scores[i].0;
375 negatives.push((positive_triple.0, positive_triple.1, entity_id));
376 break;
377 }
378 }
379 }
380 }
381 }
382
383 while negatives.len() < num_samples {
385 let corrupt_head = rng.random_f64() < 0.5;
386 let negative_triple = if corrupt_head {
387 let new_head = rng.random_range(0..num_entities);
388 (new_head, positive_triple.1, positive_triple.2)
389 } else {
390 let new_tail = rng.random_range(0..num_entities);
391 (positive_triple.0, positive_triple.1, new_tail)
392 };
393
394 if !self
395 .base
396 .has_triple(negative_triple.0, negative_triple.1, negative_triple.2)
397 {
398 negatives.push(negative_triple);
399 }
400 }
401
402 negatives
403 }
404
405 async fn train_epoch(&mut self, learning_rate: f64) -> Result<f64> {
407 let mut rng = Random::default();
408
409 let mut total_loss = 0.0;
410 let num_batches = (self.base.triples.len() + self.base.config.batch_size - 1)
411 / self.base.config.batch_size;
412
413 let mut shuffled_triples = self.base.triples.clone();
414 for i in (1..shuffled_triples.len()).rev() {
416 let j = rng.random_range(0..i + 1);
417 shuffled_triples.swap(i, j);
418 }
419
420 for batch_triples in shuffled_triples.chunks(self.base.config.batch_size) {
421 let mut batch_entity_grads_real = Array2::zeros(self.entity_embeddings_real.raw_dim());
422 let mut batch_entity_grads_imag = Array2::zeros(self.entity_embeddings_imag.raw_dim());
423 let mut batch_relation_grads = Array2::zeros(self.relation_phases.raw_dim());
424 let mut batch_loss = 0.0;
425
426 for &pos_triple in batch_triples {
427 let neg_samples = self.generate_adversarial_negatives(
429 pos_triple,
430 self.base.config.negative_samples,
431 &mut rng,
432 );
433
434 for neg_triple in neg_samples {
435 let pos_score =
436 self.score_triple_ids(pos_triple.0, pos_triple.1, pos_triple.2)?;
437 let neg_score =
438 self.score_triple_ids(neg_triple.0, neg_triple.1, neg_triple.2)?;
439
440 let pos_distance = -pos_score;
442 let neg_distance = -neg_score;
443
444 let margin = self
445 .base
446 .config
447 .model_params
448 .get("margin")
449 .copied()
450 .unwrap_or(6.0);
451 let loss = margin_loss(neg_distance, pos_distance, margin);
457 batch_loss += loss;
458
459 if loss > 0.0 {
460 let (entity_grads_real, entity_grads_imag, relation_grads) =
461 self.compute_gradients(pos_triple, neg_triple, pos_score, neg_score)?;
462
463 batch_entity_grads_real += &entity_grads_real;
464 batch_entity_grads_imag += &entity_grads_imag;
465 batch_relation_grads += &relation_grads;
466 }
467 }
468 }
469
470 gradient_update(
472 &mut self.entity_embeddings_real,
473 &batch_entity_grads_real,
474 learning_rate,
475 self.base.config.l2_reg,
476 );
477
478 gradient_update(
479 &mut self.entity_embeddings_imag,
480 &batch_entity_grads_imag,
481 learning_rate,
482 self.base.config.l2_reg,
483 );
484
485 gradient_update(
486 &mut self.relation_phases,
487 &batch_relation_grads,
488 learning_rate,
489 0.0, );
491
492 if self.modulus_constraint {
494 self.apply_modulus_constraint();
495 }
496
497 self.relation_phases.mapv_inplace(|x| {
499 let mut angle = x % (2.0 * std::f64::consts::PI);
500 if angle < 0.0 {
501 angle += 2.0 * std::f64::consts::PI;
502 }
503 angle
504 });
505
506 total_loss += batch_loss;
507 }
508
509 Ok(total_loss / num_batches as f64)
510 }
511
512 fn get_entity_embedding_vector(&self, entity_id: usize) -> Vector {
514 let real_part = self.entity_embeddings_real.row(entity_id);
515 let imag_part = self.entity_embeddings_imag.row(entity_id);
516
517 let mut values = Vec::with_capacity(real_part.len() * 2);
518 for &val in real_part.iter() {
519 values.push(val as f32);
520 }
521 for &val in imag_part.iter() {
522 values.push(val as f32);
523 }
524
525 Vector::new(values)
526 }
527
528 fn get_relation_embedding_vector(&self, relation_id: usize) -> Vector {
530 let phases = self.relation_phases.row(relation_id);
531 let values: Vec<f32> = phases.iter().copied().map(|x| x as f32).collect();
532 Vector::new(values)
533 }
534}
535
536#[async_trait]
537impl EmbeddingModel for RotatE {
538 fn config(&self) -> &ModelConfig {
539 &self.base.config
540 }
541
542 fn model_id(&self) -> &Uuid {
543 &self.base.model_id
544 }
545
546 fn model_type(&self) -> &'static str {
547 "RotatE"
548 }
549
550 fn add_triple(&mut self, triple: Triple) -> Result<()> {
551 self.base.add_triple(triple)
552 }
553
554 async fn train(&mut self, epochs: Option<usize>) -> Result<TrainingStats> {
555 let start_time = Instant::now();
556 let max_epochs = epochs.unwrap_or(self.base.config.max_epochs);
557
558 self.initialize_embeddings();
559
560 if !self.embeddings_initialized {
561 return Err(anyhow!("No training data available"));
562 }
563
564 let mut loss_history = Vec::new();
565 let learning_rate = self.base.config.learning_rate;
566
567 info!("Starting RotatE training for {} epochs", max_epochs);
568
569 for epoch in 0..max_epochs {
570 let epoch_loss = self.train_epoch(learning_rate).await?;
571 loss_history.push(epoch_loss);
572
573 if epoch % 100 == 0 {
574 debug!("Epoch {}: loss = {:.6}", epoch, epoch_loss);
575 }
576
577 if epoch > 10 && epoch_loss < 1e-6 {
578 info!("Converged at epoch {} with loss {:.6}", epoch, epoch_loss);
579 break;
580 }
581 }
582
583 self.base.mark_trained();
584 let training_time = start_time.elapsed().as_secs_f64();
585
586 Ok(TrainingStats {
587 epochs_completed: loss_history.len(),
588 final_loss: loss_history.last().copied().unwrap_or(0.0),
589 training_time_seconds: training_time,
590 convergence_achieved: loss_history.last().copied().unwrap_or(f64::INFINITY) < 1e-6,
591 loss_history,
592 })
593 }
594
595 fn get_entity_embedding(&self, entity: &str) -> Result<Vector> {
596 if !self.embeddings_initialized {
597 return Err(anyhow!("Model not trained"));
598 }
599
600 let entity_id = self
601 .base
602 .get_entity_id(entity)
603 .ok_or_else(|| anyhow!("Entity not found: {}", entity))?;
604
605 Ok(self.get_entity_embedding_vector(entity_id))
606 }
607
608 fn get_relation_embedding(&self, relation: &str) -> Result<Vector> {
609 if !self.embeddings_initialized {
610 return Err(anyhow!("Model not trained"));
611 }
612
613 let relation_id = self
614 .base
615 .get_relation_id(relation)
616 .ok_or_else(|| anyhow!("Relation not found: {}", relation))?;
617
618 Ok(self.get_relation_embedding_vector(relation_id))
619 }
620
621 fn score_triple(&self, subject: &str, predicate: &str, object: &str) -> Result<f64> {
622 let subject_id = self
623 .base
624 .get_entity_id(subject)
625 .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
626 let predicate_id = self
627 .base
628 .get_relation_id(predicate)
629 .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
630 let object_id = self
631 .base
632 .get_entity_id(object)
633 .ok_or_else(|| anyhow!("Object not found: {}", object))?;
634
635 self.score_triple_ids(subject_id, predicate_id, object_id)
636 }
637
638 fn predict_objects(
639 &self,
640 subject: &str,
641 predicate: &str,
642 k: usize,
643 ) -> Result<Vec<(String, f64)>> {
644 if !self.embeddings_initialized {
645 return Err(anyhow!("Model not trained"));
646 }
647
648 let subject_id = self
649 .base
650 .get_entity_id(subject)
651 .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
652 let predicate_id = self
653 .base
654 .get_relation_id(predicate)
655 .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
656
657 let mut scores = Vec::new();
658
659 for object_id in 0..self.base.num_entities() {
660 let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
661 let object_name = self
662 .base
663 .get_entity(object_id)
664 .expect("entity should exist for valid id")
665 .clone();
666 scores.push((object_name, score));
667 }
668
669 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
670 scores.truncate(k);
671
672 Ok(scores)
673 }
674
675 fn predict_subjects(
676 &self,
677 predicate: &str,
678 object: &str,
679 k: usize,
680 ) -> Result<Vec<(String, f64)>> {
681 if !self.embeddings_initialized {
682 return Err(anyhow!("Model not trained"));
683 }
684
685 let predicate_id = self
686 .base
687 .get_relation_id(predicate)
688 .ok_or_else(|| anyhow!("Predicate not found: {}", predicate))?;
689 let object_id = self
690 .base
691 .get_entity_id(object)
692 .ok_or_else(|| anyhow!("Object not found: {}", object))?;
693
694 let mut scores = Vec::new();
695
696 for subject_id in 0..self.base.num_entities() {
697 let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
698 let subject_name = self
699 .base
700 .get_entity(subject_id)
701 .expect("entity should exist for valid id")
702 .clone();
703 scores.push((subject_name, score));
704 }
705
706 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
707 scores.truncate(k);
708
709 Ok(scores)
710 }
711
712 fn predict_relations(
713 &self,
714 subject: &str,
715 object: &str,
716 k: usize,
717 ) -> Result<Vec<(String, f64)>> {
718 if !self.embeddings_initialized {
719 return Err(anyhow!("Model not trained"));
720 }
721
722 let subject_id = self
723 .base
724 .get_entity_id(subject)
725 .ok_or_else(|| anyhow!("Subject not found: {}", subject))?;
726 let object_id = self
727 .base
728 .get_entity_id(object)
729 .ok_or_else(|| anyhow!("Object not found: {}", object))?;
730
731 let mut scores = Vec::new();
732
733 for predicate_id in 0..self.base.num_relations() {
734 let score = self.score_triple_ids(subject_id, predicate_id, object_id)?;
735 let predicate_name = self
736 .base
737 .get_relation(predicate_id)
738 .expect("relation should exist for valid id")
739 .clone();
740 scores.push((predicate_name, score));
741 }
742
743 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
744 scores.truncate(k);
745
746 Ok(scores)
747 }
748
749 fn get_entities(&self) -> Vec<String> {
750 self.base.get_entities()
751 }
752
753 fn get_relations(&self) -> Vec<String> {
754 self.base.get_relations()
755 }
756
757 fn get_stats(&self) -> ModelStats {
758 self.base.get_stats("RotatE")
759 }
760
761 fn save(&self, path: &str) -> Result<()> {
762 info!("Saving RotatE model to {}", path);
763
764 let serializable = RotatESerializable {
765 base: BaseModelSnapshot::capture(&self.base),
766 entity_embeddings_real: MatrixF64::from_array(&self.entity_embeddings_real),
767 entity_embeddings_imag: MatrixF64::from_array(&self.entity_embeddings_imag),
768 relation_phases: MatrixF64::from_array(&self.relation_phases),
769 embeddings_initialized: self.embeddings_initialized,
770 adversarial_temperature: self.adversarial_temperature,
771 modulus_constraint: self.modulus_constraint,
772 };
773
774 let file = File::create(path)
775 .map_err(|e| anyhow!("Failed to create model file {}: {}", path, e))?;
776 let writer = BufWriter::new(file);
777 oxicode::serde::encode_into_std_write(&serializable, writer, oxicode::config::standard())
778 .map_err(|e| anyhow!("Failed to serialize RotatE model: {}", e))?;
779
780 info!("RotatE model saved successfully");
781 Ok(())
782 }
783
784 fn load(&mut self, path: &str) -> Result<()> {
785 info!("Loading RotatE model from {}", path);
786
787 if !Path::new(path).exists() {
788 return Err(anyhow!("Model file not found: {}", path));
789 }
790
791 let file =
792 File::open(path).map_err(|e| anyhow!("Failed to open model file {}: {}", path, e))?;
793 let reader = BufReader::new(file);
794 let (serializable, _): (RotatESerializable, _) =
795 oxicode::serde::decode_from_std_read(reader, oxicode::config::standard())
796 .map_err(|e| anyhow!("Failed to deserialize RotatE model: {}", e))?;
797
798 self.entity_embeddings_real = serializable.entity_embeddings_real.to_array()?;
799 self.entity_embeddings_imag = serializable.entity_embeddings_imag.to_array()?;
800 self.relation_phases = serializable.relation_phases.to_array()?;
801 self.embeddings_initialized = serializable.embeddings_initialized;
802 self.adversarial_temperature = serializable.adversarial_temperature;
803 self.modulus_constraint = serializable.modulus_constraint;
804 serializable.base.restore_into(&mut self.base);
805
806 info!("RotatE model loaded successfully");
807 Ok(())
808 }
809
810 fn clear(&mut self) {
811 self.base.clear();
812 self.entity_embeddings_real = Array2::zeros((0, self.base.config.dimensions));
813 self.entity_embeddings_imag = Array2::zeros((0, self.base.config.dimensions));
814 self.relation_phases = Array2::zeros((0, self.base.config.dimensions));
815 self.embeddings_initialized = false;
816 }
817
818 fn is_trained(&self) -> bool {
819 self.base.is_trained
820 }
821
822 async fn encode(&self, _texts: &[String]) -> Result<Vec<Vec<f32>>> {
823 Err(anyhow!(
824 "Knowledge graph embedding model does not support text encoding"
825 ))
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::*;
832
833 #[tokio::test]
834 async fn test_rotate_basic() -> Result<()> {
835 let config = ModelConfig::default()
836 .with_dimensions(10)
837 .with_max_epochs(5)
838 .with_seed(42);
839
840 let mut model = RotatE::new(config);
841
842 let alice = crate::NamedNode::new("http://example.org/alice")?;
843 let knows = crate::NamedNode::new("http://example.org/knows")?;
844 let bob = crate::NamedNode::new("http://example.org/bob")?;
845
846 model.add_triple(crate::Triple::new(
847 alice.clone(),
848 knows.clone(),
849 bob.clone(),
850 ))?;
851
852 let stats = model.train(Some(3)).await?;
853 assert!(stats.epochs_completed > 0);
854
855 let alice_emb = model.get_entity_embedding("http://example.org/alice")?;
856 assert_eq!(alice_emb.dimensions, 20); let score = model.score_triple(
859 "http://example.org/alice",
860 "http://example.org/knows",
861 "http://example.org/bob",
862 )?;
863
864 assert!(score.is_finite());
865
866 Ok(())
867 }
868
869 #[test]
873 fn regression_rotate_margin_loss_orientation() {
874 use crate::models::common::margin_loss;
875 let margin = 6.0;
876 let pos_distance = 9.0;
877 let neg_distance = 2.0;
878 let loss = margin_loss(neg_distance, pos_distance, margin);
879 assert!(
880 loss > 0.0,
881 "violated triple must yield positive hinge loss, got {loss}"
882 );
883 assert!((loss - (margin + pos_distance - neg_distance)).abs() < 1e-9);
884
885 let good = margin_loss(20.0, 0.0, margin);
886 assert_eq!(good, 0.0, "well-separated triple must yield zero loss");
887 }
888
889 #[tokio::test]
892 async fn regression_rotate_save_load_roundtrip() -> Result<()> {
893 let config = ModelConfig::default()
894 .with_dimensions(8)
895 .with_max_epochs(4)
896 .with_seed(11);
897 let mut model = RotatE::new(config);
898
899 let alice = crate::NamedNode::new("http://example.org/alice")?;
900 let knows = crate::NamedNode::new("http://example.org/knows")?;
901 let bob = crate::NamedNode::new("http://example.org/bob")?;
902 model.add_triple(Triple::new(alice.clone(), knows.clone(), bob.clone()))?;
903 model.add_triple(Triple::new(bob.clone(), knows.clone(), alice.clone()))?;
904 model.train(Some(4)).await?;
905
906 let before = model.get_entity_embedding("http://example.org/alice")?;
907
908 let path = std::env::temp_dir().join(format!("rotate-roundtrip-{}.bin", Uuid::new_v4()));
909 let path_str = path.to_string_lossy().to_string();
910 model.save(&path_str)?;
911
912 let mut restored = RotatE::new(ModelConfig::default());
913 restored.load(&path_str)?;
914 assert!(restored.is_trained());
915 let after = restored.get_entity_embedding("http://example.org/alice")?;
916 assert_eq!(before.dimensions, after.dimensions);
917 for (x, y) in before.values.iter().zip(after.values.iter()) {
918 assert!((x - y).abs() < 1e-9, "embedding mismatch after load");
919 }
920
921 let _ = std::fs::remove_file(&path);
922 Ok(())
923 }
924}