1use std::collections::{HashMap, VecDeque};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use thiserror::Error;
10
11pub type LangId = [u8; 8];
15
16pub type MeaMultilingualEmbeddingAligner = MultilingualEmbeddingAligner;
18pub type MeaLanguageSpace = LanguageSpace;
20pub type MeaAlignmentMatrix = AlignmentMatrix;
22pub type MeaAlignerCfg = MeaAlignerConfig;
24pub type MeaStats = MeaAlignerStats;
26
27#[derive(Debug, Error)]
31pub enum MeaError {
32 #[error("language '{0}' not found")]
33 LanguageNotFound(String),
34
35 #[error("embedding id {0} not found in language '{1}'")]
36 EmbeddingNotFound(u64, String),
37
38 #[error("dimension mismatch: expected {expected}, got {actual}")]
39 DimensionMismatch { expected: usize, actual: usize },
40
41 #[error("not enough anchor pairs: need at least {min}, got {got}")]
42 NotEnoughAnchors { min: usize, got: usize },
43
44 #[error("alignment matrix not found for ({0}, {1})")]
45 AlignmentNotFound(String, String),
46
47 #[error("SVD power iteration did not converge")]
48 SvdNotConverged,
49
50 #[error("arithmetic error: {0}")]
51 Arithmetic(String),
52
53 #[error("empty embedding vector")]
54 EmptyEmbedding,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum MeaAlignmentMethod {
62 #[default]
64 Procrustes,
65 LinearRegression,
67 Cca,
69 IdentityPassthrough,
71}
72
73#[derive(Debug, Clone)]
77pub struct LanguageSpace {
78 pub id: LangId,
79 pub name: String,
80 pub dim: usize,
81 pub embeddings: Vec<(u64, Vec<f64>)>,
83 pub centroid: Option<Vec<f64>>,
84}
85
86impl LanguageSpace {
87 fn new(id: LangId, name: String, dim: usize) -> Self {
88 Self {
89 id,
90 name,
91 dim,
92 embeddings: Vec::new(),
93 centroid: None,
94 }
95 }
96
97 fn lang_name(&self) -> String {
98 String::from_utf8_lossy(&self.id)
99 .trim_end_matches('\0')
100 .to_string()
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct AlignmentMatrix {
107 pub src_lang: LangId,
108 pub tgt_lang: LangId,
109 pub matrix: Vec<f64>,
111 pub quality: f64,
113}
114
115impl AlignmentMatrix {
116 fn apply(&self, v: &[f64]) -> Result<Vec<f64>, MeaError> {
118 let dim = v.len();
119 let expected_len = dim * dim;
120 if self.matrix.len() != expected_len {
121 return Err(MeaError::DimensionMismatch {
122 expected: expected_len,
123 actual: self.matrix.len(),
124 });
125 }
126 let mut out = vec![0.0_f64; dim];
127 for (i, out_i) in out.iter_mut().enumerate() {
128 let mut acc = 0.0_f64;
129 let row_off = i * dim;
130 for (j, vj) in v.iter().enumerate() {
131 acc += self.matrix[row_off + j] * vj;
132 }
133 *out_i = acc;
134 }
135 Ok(out)
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct AlignmentRecord {
142 pub ts: u64,
143 pub src_lang: LangId,
144 pub tgt_lang: LangId,
145 pub n_anchors: usize,
146 pub quality: f64,
147}
148
149#[derive(Debug, Clone, Default)]
151pub struct MeaAlignerStats {
152 pub n_languages: usize,
153 pub n_alignments: usize,
154 pub n_history_records: usize,
155 pub total_embeddings: usize,
156 pub avg_alignment_quality: f64,
157}
158
159#[derive(Debug, Clone)]
161pub struct MeaAlignerConfig {
162 pub dim: usize,
163 pub normalize_embeddings: bool,
164 pub alignment_method: MeaAlignmentMethod,
165 pub min_anchor_pairs: usize,
166}
167
168impl Default for MeaAlignerConfig {
169 fn default() -> Self {
170 Self {
171 dim: 128,
172 normalize_embeddings: true,
173 alignment_method: MeaAlignmentMethod::Procrustes,
174 min_anchor_pairs: 5,
175 }
176 }
177}
178
179#[inline]
182fn xorshift64(state: &mut u64) -> u64 {
183 let mut x = *state;
184 x ^= x << 13;
185 x ^= x >> 7;
186 x ^= x << 17;
187 *state = x;
188 x
189}
190
191#[inline]
194fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
195 let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
196 let na: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
197 let nb: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
198 if na < 1e-12 || nb < 1e-12 {
199 0.0
200 } else {
201 (dot / (na * nb)).clamp(-1.0, 1.0)
202 }
203}
204
205#[inline]
207fn normalize_vec(v: &mut [f64]) -> bool {
208 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
209 if norm < 1e-12 {
210 return false;
211 }
212 for x in v.iter_mut() {
213 *x /= norm;
214 }
215 true
216}
217
218fn now_secs() -> u64 {
219 SystemTime::now()
220 .duration_since(UNIX_EPOCH)
221 .map(|d| d.as_secs())
222 .unwrap_or(0)
223}
224
225fn lang_name_from_id(id: &LangId) -> String {
226 String::from_utf8_lossy(id)
227 .trim_end_matches('\0')
228 .to_string()
229}
230
231fn matmul(a: &[f64], b: &[f64], m: usize, k: usize, n: usize) -> Vec<f64> {
235 let mut c = vec![0.0_f64; m * n];
236 for i in 0..m {
237 for l in 0..k {
238 let a_il = a[i * k + l];
239 for j in 0..n {
240 c[i * n + j] += a_il * b[l * n + j];
241 }
242 }
243 }
244 c
245}
246
247fn transpose(a: &[f64], m: usize, n: usize) -> Vec<f64> {
249 let mut t = vec![0.0_f64; n * m];
250 for i in 0..m {
251 for j in 0..n {
252 t[j * m + i] = a[i * n + j];
253 }
254 }
255 t
256}
257
258fn identity(d: usize) -> Vec<f64> {
260 let mut m = vec![0.0_f64; d * d];
261 for i in 0..d {
262 m[i * d + i] = 1.0;
263 }
264 m
265}
266
267type SvdResult = Result<(Vec<f64>, Vec<f64>, Vec<f64>), MeaError>;
269
270fn svd_power_iteration(
276 a: &[f64],
277 m: usize,
278 n: usize,
279 k: usize,
280 max_iter: usize,
281 tol: f64,
282 rng: &mut u64,
283) -> SvdResult {
284 let mut u_cols: Vec<Vec<f64>> = Vec::with_capacity(k);
286 let mut sigmas: Vec<f64> = Vec::with_capacity(k);
287 let mut v_cols: Vec<Vec<f64>> = Vec::with_capacity(k);
288
289 let mut a_work = a.to_vec();
291
292 for _ in 0..k {
293 let mut v = (0..n)
295 .map(|_| {
296 let r = xorshift64(rng);
297 (r as i64 as f64) / (i64::MAX as f64)
299 })
300 .collect::<Vec<f64>>();
301 let vn: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
303 if vn < 1e-12 {
304 return Err(MeaError::SvdNotConverged);
305 }
306 for x in v.iter_mut() {
307 *x /= vn;
308 }
309
310 let mut sigma = 0.0_f64;
311 let mut u = vec![0.0_f64; m];
312
313 for iter in 0..max_iter {
314 for i in 0..m {
316 let mut acc = 0.0;
317 for j in 0..n {
318 acc += a_work[i * n + j] * v[j];
319 }
320 u[i] = acc;
321 }
322 let new_sigma: f64 = u.iter().map(|x| x * x).sum::<f64>().sqrt();
323 if new_sigma < 1e-12 {
324 break;
325 }
326 for x in u.iter_mut() {
327 *x /= new_sigma;
328 }
329
330 let mut v_new = vec![0.0_f64; n];
332 for j in 0..n {
333 let mut acc = 0.0;
334 for i in 0..m {
335 acc += a_work[i * n + j] * u[i];
336 }
337 v_new[j] = acc;
338 }
339 let v_norm: f64 = v_new.iter().map(|x| x * x).sum::<f64>().sqrt();
340 if v_norm < 1e-12 {
341 break;
342 }
343 for x in v_new.iter_mut() {
344 *x /= v_norm;
345 }
346
347 let diff: f64 = v_new.iter().zip(v.iter()).map(|(a, b)| (a - b).abs()).sum();
349 v = v_new;
350 sigma = new_sigma;
351 if iter > 0 && diff < tol {
352 break;
353 }
354 }
355
356 if sigma < 1e-12 {
357 let mut ui = vec![0.0_f64; m];
359 let mut vi = vec![0.0_f64; n];
360 let idx = u_cols.len().min(m - 1);
361 if idx < m {
362 ui[idx] = 1.0;
363 }
364 let idx = u_cols.len().min(n - 1);
365 if idx < n {
366 vi[idx] = 1.0;
367 }
368 u_cols.push(ui);
369 sigmas.push(0.0);
370 v_cols.push(vi);
371 continue;
372 }
373
374 for i in 0..m {
376 for j in 0..n {
377 a_work[i * n + j] -= sigma * u[i] * v[j];
378 }
379 }
380
381 u_cols.push(u);
382 sigmas.push(sigma);
383 v_cols.push(v);
384 }
385
386 let mut u_mat = vec![0.0_f64; m * k];
388 let mut vt_mat = vec![0.0_f64; k * n];
389 for t in 0..k {
390 for i in 0..m {
391 u_mat[i * k + t] = u_cols[t][i];
392 }
393 for j in 0..n {
394 vt_mat[t * n + j] = v_cols[t][j];
395 }
396 }
397
398 Ok((u_mat, sigmas, vt_mat))
399}
400
401fn procrustes(
404 x: &[f64], y: &[f64], n: usize,
407 dim: usize,
408 rng: &mut u64,
409) -> Result<Vec<f64>, MeaError> {
410 let xt = transpose(x, n, dim); let m = matmul(&xt, y, dim, n, dim); let k = dim.min(n); let (u, _s, vt) = svd_power_iteration(&m, dim, dim, k, 200, 1e-7, rng)?;
417
418 let u_full = if k == dim {
420 u
421 } else {
422 let mut uf = identity(dim);
423 for i in 0..dim {
424 for t in 0..k {
425 uf[i * dim + t] = u[i * k + t];
426 }
427 }
428 uf
429 };
430 let vt_full = if k == dim {
431 vt
432 } else {
433 let mut vtf = identity(dim);
434 for t in 0..k {
435 for j in 0..dim {
436 vtf[t * dim + j] = vt[t * k + j];
437 }
438 }
439 vtf
440 };
441
442 let v_full = transpose(&vt_full, dim, dim); let ut_full = transpose(&u_full, dim, dim); let w = matmul(&v_full, &ut_full, dim, dim, dim); Ok(w)
447}
448
449fn linear_regression(x: &[f64], y: &[f64], n: usize, dim: usize) -> Result<Vec<f64>, MeaError> {
452 let xt = transpose(x, n, dim); let xtx = matmul(&xt, x, dim, n, dim); let xty = matmul(&xt, y, dim, n, dim); let inv_xtx = gauss_jordan_invert(&xtx, dim)?;
458 let w = matmul(&inv_xtx, &xty, dim, dim, dim);
459 Ok(w)
460}
461
462fn gauss_jordan_invert(a: &[f64], d: usize) -> Result<Vec<f64>, MeaError> {
464 let mut aug = vec![0.0_f64; d * 2 * d];
466 for i in 0..d {
467 for j in 0..d {
468 aug[i * 2 * d + j] = a[i * d + j];
469 }
470 aug[i * 2 * d + d + i] = 1.0;
471 }
472
473 for col in 0..d {
474 let mut max_row = col;
476 let mut max_val = aug[col * 2 * d + col].abs();
477 for row in (col + 1)..d {
478 let v = aug[row * 2 * d + col].abs();
479 if v > max_val {
480 max_val = v;
481 max_row = row;
482 }
483 }
484 if max_val < 1e-14 {
485 return Err(MeaError::Arithmetic(
486 "singular matrix in Gauss-Jordan".to_string(),
487 ));
488 }
489 if max_row != col {
491 for j in 0..2 * d {
492 aug.swap(col * 2 * d + j, max_row * 2 * d + j);
493 }
494 }
495 let pivot = aug[col * 2 * d + col];
496 for j in 0..2 * d {
497 aug[col * 2 * d + j] /= pivot;
498 }
499 for row in 0..d {
500 if row == col {
501 continue;
502 }
503 let factor = aug[row * 2 * d + col];
504 for j in 0..2 * d {
505 let v = aug[col * 2 * d + j];
506 aug[row * 2 * d + j] -= factor * v;
507 }
508 }
509 }
510
511 let mut inv = vec![0.0_f64; d * d];
512 for i in 0..d {
513 for j in 0..d {
514 inv[i * d + j] = aug[i * 2 * d + d + j];
515 }
516 }
517 Ok(inv)
518}
519
520fn cca_alignment(
522 x: &[f64],
523 y: &[f64],
524 n: usize,
525 dim: usize,
526 rng: &mut u64,
527) -> Result<Vec<f64>, MeaError> {
528 let wx = whiten(x, n, dim)?;
530 let wy = whiten(y, n, dim)?;
532 procrustes(&wx, &wy, n, dim, rng)
534}
535
536fn whiten(x: &[f64], n: usize, dim: usize) -> Result<Vec<f64>, MeaError> {
538 let xt = transpose(x, n, dim);
539 let cov = matmul(&xt, x, dim, n, dim);
540 let mut w_diag = vec![0.0_f64; dim];
543 for i in 0..dim {
544 let v = cov[i * dim + i];
545 w_diag[i] = if v > 1e-14 { 1.0 / v.sqrt() } else { 1.0 };
546 }
547 let mut xw = vec![0.0_f64; n * dim];
549 for i in 0..n {
550 for j in 0..dim {
551 xw[i * dim + j] = x[i * dim + j] * w_diag[j];
552 }
553 }
554 Ok(xw)
555}
556
557pub struct MultilingualEmbeddingAligner {
562 pub config: MeaAlignerConfig,
563 pub language_spaces: HashMap<LangId, LanguageSpace>,
564 pub alignment_matrices: HashMap<(LangId, LangId), AlignmentMatrix>,
565 pub alignment_history: VecDeque<AlignmentRecord>,
566 rng: u64,
567}
568
569impl MultilingualEmbeddingAligner {
570 const HISTORY_CAP: usize = 500;
571
572 pub fn new(config: MeaAlignerConfig) -> Self {
574 let seed = SystemTime::now()
575 .duration_since(UNIX_EPOCH)
576 .map(|d| d.subsec_nanos() as u64 + d.as_secs() * 1_000_000_000)
577 .unwrap_or(0x123456789abcdef0);
578 Self {
579 config,
580 language_spaces: HashMap::new(),
581 alignment_matrices: HashMap::new(),
582 alignment_history: VecDeque::with_capacity(Self::HISTORY_CAP),
583 rng: if seed == 0 { 0xdeadbeefcafe_u64 } else { seed },
584 }
585 }
586
587 pub fn with_defaults() -> Self {
589 Self::new(MeaAlignerConfig::default())
590 }
591
592 pub fn add_language(&mut self, id: LangId, name: String, dim: usize) {
596 self.language_spaces
597 .entry(id)
598 .or_insert_with(|| LanguageSpace::new(id, name, dim));
599 }
600
601 pub fn remove_language(&mut self, id: LangId) {
603 self.language_spaces.remove(&id);
604 self.alignment_matrices
605 .retain(|(src, tgt), _| src != &id && tgt != &id);
606 }
607
608 pub fn add_embedding(
612 &mut self,
613 lang_id: LangId,
614 emb_id: u64,
615 mut vector: Vec<f64>,
616 ) -> Result<(), MeaError> {
617 let space = self
618 .language_spaces
619 .get_mut(&lang_id)
620 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&lang_id)))?;
621 if vector.is_empty() {
622 return Err(MeaError::EmptyEmbedding);
623 }
624 if vector.len() != space.dim {
625 return Err(MeaError::DimensionMismatch {
626 expected: space.dim,
627 actual: vector.len(),
628 });
629 }
630 if self.config.normalize_embeddings {
631 normalize_vec(&mut vector);
632 }
633 if let Some(entry) = space.embeddings.iter_mut().find(|(id, _)| *id == emb_id) {
635 entry.1 = vector;
636 } else {
637 space.embeddings.push((emb_id, vector));
638 }
639 space.centroid = None;
641 Ok(())
642 }
643
644 pub fn remove_embedding(&mut self, lang_id: LangId, emb_id: u64) -> Result<(), MeaError> {
646 let space = self
647 .language_spaces
648 .get_mut(&lang_id)
649 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&lang_id)))?;
650 let before = space.embeddings.len();
651 space.embeddings.retain(|(id, _)| *id != emb_id);
652 if space.embeddings.len() == before {
653 return Err(MeaError::EmbeddingNotFound(emb_id, space.lang_name()));
654 }
655 space.centroid = None;
656 Ok(())
657 }
658
659 pub fn compute_alignment(
666 &mut self,
667 src: LangId,
668 tgt: LangId,
669 anchors: &[(u64, u64)],
670 ) -> Result<(), MeaError> {
671 let min = self.config.min_anchor_pairs;
672 if anchors.len() < min {
673 return Err(MeaError::NotEnoughAnchors {
674 min,
675 got: anchors.len(),
676 });
677 }
678
679 let dim = self.config.dim;
680
681 let src_vecs: Vec<Vec<f64>> = {
683 let space = self
684 .language_spaces
685 .get(&src)
686 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&src)))?;
687 anchors
688 .iter()
689 .map(|(sid, _)| {
690 space
691 .embeddings
692 .iter()
693 .find(|(id, _)| id == sid)
694 .map(|(_, v)| v.clone())
695 .ok_or_else(|| MeaError::EmbeddingNotFound(*sid, space.lang_name()))
696 })
697 .collect::<Result<Vec<_>, _>>()?
698 };
699
700 let tgt_vecs: Vec<Vec<f64>> = {
701 let space = self
702 .language_spaces
703 .get(&tgt)
704 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&tgt)))?;
705 anchors
706 .iter()
707 .map(|(_, tid)| {
708 space
709 .embeddings
710 .iter()
711 .find(|(id, _)| id == tid)
712 .map(|(_, v)| v.clone())
713 .ok_or_else(|| MeaError::EmbeddingNotFound(*tid, space.lang_name()))
714 })
715 .collect::<Result<Vec<_>, _>>()?
716 };
717
718 for v in src_vecs.iter().chain(tgt_vecs.iter()) {
720 if v.len() != dim {
721 return Err(MeaError::DimensionMismatch {
722 expected: dim,
723 actual: v.len(),
724 });
725 }
726 }
727
728 let n = anchors.len();
729
730 let x: Vec<f64> = src_vecs.iter().flat_map(|v| v.iter().copied()).collect();
732 let y: Vec<f64> = tgt_vecs.iter().flat_map(|v| v.iter().copied()).collect();
733
734 let matrix = match self.config.alignment_method {
735 MeaAlignmentMethod::IdentityPassthrough => identity(dim),
736 MeaAlignmentMethod::Procrustes => procrustes(&x, &y, n, dim, &mut self.rng)?,
737 MeaAlignmentMethod::LinearRegression => linear_regression(&x, &y, n, dim)?,
738 MeaAlignmentMethod::Cca => cca_alignment(&x, &y, n, dim, &mut self.rng)?,
739 };
740
741 let quality = self.measure_quality(&matrix, &src_vecs, &tgt_vecs);
743
744 let record = AlignmentRecord {
745 ts: now_secs(),
746 src_lang: src,
747 tgt_lang: tgt,
748 n_anchors: n,
749 quality,
750 };
751
752 self.alignment_matrices.insert(
753 (src, tgt),
754 AlignmentMatrix {
755 src_lang: src,
756 tgt_lang: tgt,
757 matrix,
758 quality,
759 },
760 );
761
762 if self.alignment_history.len() >= Self::HISTORY_CAP {
763 self.alignment_history.pop_front();
764 }
765 self.alignment_history.push_back(record);
766
767 Ok(())
768 }
769
770 fn measure_quality(&self, matrix: &[f64], src_vecs: &[Vec<f64>], tgt_vecs: &[Vec<f64>]) -> f64 {
771 let n = src_vecs.len();
772 if n == 0 {
773 return 0.0;
774 }
775 let am = AlignmentMatrix {
776 src_lang: [0; 8],
777 tgt_lang: [0; 8],
778 matrix: matrix.to_vec(),
779 quality: 0.0,
780 };
781 let total: f64 = src_vecs
782 .iter()
783 .zip(tgt_vecs.iter())
784 .map(|(sv, tv)| {
785 am.apply(sv)
786 .map(|aligned| cosine_similarity(&aligned, tv))
787 .unwrap_or(0.0)
788 })
789 .sum();
790 total / n as f64
791 }
792
793 pub fn align_embedding(
795 &self,
796 src: LangId,
797 tgt: LangId,
798 vector: &[f64],
799 ) -> Result<Vec<f64>, MeaError> {
800 if vector.is_empty() {
801 return Err(MeaError::EmptyEmbedding);
802 }
803 if vector.len() != self.config.dim {
804 return Err(MeaError::DimensionMismatch {
805 expected: self.config.dim,
806 actual: vector.len(),
807 });
808 }
809 let am = self.alignment_matrices.get(&(src, tgt)).ok_or_else(|| {
810 MeaError::AlignmentNotFound(lang_name_from_id(&src), lang_name_from_id(&tgt))
811 })?;
812 let mut out = am.apply(vector)?;
813 if self.config.normalize_embeddings {
814 normalize_vec(&mut out);
815 }
816 Ok(out)
817 }
818
819 pub fn cross_lingual_search(
825 &self,
826 query_lang: LangId,
827 query: &[f64],
828 target_lang: LangId,
829 top_k: usize,
830 ) -> Result<Vec<(u64, f64)>, MeaError> {
831 if query.is_empty() {
832 return Err(MeaError::EmptyEmbedding);
833 }
834
835 if !self.language_spaces.contains_key(&target_lang) {
837 return Err(MeaError::LanguageNotFound(lang_name_from_id(&target_lang)));
838 }
839
840 let aligned_query = if query_lang == target_lang {
841 query.to_vec()
842 } else {
843 self.align_embedding(query_lang, target_lang, query)?
844 };
845
846 let tgt_space = self
847 .language_spaces
848 .get(&target_lang)
849 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&target_lang)))?;
850
851 let mut scores: Vec<(u64, f64)> = tgt_space
852 .embeddings
853 .iter()
854 .map(|(id, v)| (*id, cosine_similarity(&aligned_query, v)))
855 .collect();
856
857 scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
858 scores.truncate(top_k);
859 Ok(scores)
860 }
861
862 pub fn alignment_quality(&self, src: LangId, tgt: LangId) -> Option<f64> {
866 self.alignment_matrices
867 .get(&(src, tgt))
868 .map(|am| am.quality)
869 }
870
871 pub fn compute_centroid(&mut self, lang_id: LangId) -> Result<Vec<f64>, MeaError> {
873 let space = self
874 .language_spaces
875 .get_mut(&lang_id)
876 .ok_or_else(|| MeaError::LanguageNotFound(lang_name_from_id(&lang_id)))?;
877
878 if space.embeddings.is_empty() {
879 return Err(MeaError::Arithmetic(
880 "no embeddings to compute centroid from".to_string(),
881 ));
882 }
883
884 let dim = space.dim;
885 let mut c = vec![0.0_f64; dim];
886 for (_, v) in &space.embeddings {
887 for (ci, vi) in c.iter_mut().zip(v.iter()) {
888 *ci += vi;
889 }
890 }
891 let n = space.embeddings.len() as f64;
892 for ci in c.iter_mut() {
893 *ci /= n;
894 }
895 space.centroid = Some(c.clone());
896 Ok(c)
897 }
898
899 pub fn aligner_stats(&self) -> MeaAlignerStats {
901 let n_alignments = self.alignment_matrices.len();
902 let avg_quality = if n_alignments == 0 {
903 0.0
904 } else {
905 let sum: f64 = self.alignment_matrices.values().map(|am| am.quality).sum();
906 sum / n_alignments as f64
907 };
908 MeaAlignerStats {
909 n_languages: self.language_spaces.len(),
910 n_alignments,
911 n_history_records: self.alignment_history.len(),
912 total_embeddings: self
913 .language_spaces
914 .values()
915 .map(|s| s.embeddings.len())
916 .sum(),
917 avg_alignment_quality: avg_quality,
918 }
919 }
920
921 pub fn embedding_count(&self, lang_id: LangId) -> Option<usize> {
925 self.language_spaces
926 .get(&lang_id)
927 .map(|s| s.embeddings.len())
928 }
929
930 pub fn get_alignment_matrix(&self, src: LangId, tgt: LangId) -> Option<&AlignmentMatrix> {
932 self.alignment_matrices.get(&(src, tgt))
933 }
934
935 pub fn language_ids(&self) -> Vec<LangId> {
937 self.language_spaces.keys().copied().collect()
938 }
939
940 pub fn has_alignment(&self, src: LangId, tgt: LangId) -> bool {
942 self.alignment_matrices.contains_key(&(src, tgt))
943 }
944
945 pub fn centroid(&self, lang_id: LangId) -> Option<&Vec<f64>> {
947 self.language_spaces.get(&lang_id)?.centroid.as_ref()
948 }
949
950 pub fn history(&self, limit: usize) -> Vec<&AlignmentRecord> {
952 self.alignment_history.iter().rev().take(limit).collect()
953 }
954}
955
956#[cfg(test)]
959mod tests {
960 use super::*;
961
962 fn make_lang_id(s: &str) -> LangId {
963 let mut id = [0u8; 8];
964 for (i, b) in s.as_bytes().iter().take(8).enumerate() {
965 id[i] = *b;
966 }
967 id
968 }
969
970 fn make_aligner(dim: usize, method: MeaAlignmentMethod) -> MultilingualEmbeddingAligner {
971 MultilingualEmbeddingAligner::new(MeaAlignerConfig {
972 dim,
973 normalize_embeddings: false,
974 alignment_method: method,
975 min_anchor_pairs: 3,
976 })
977 }
978
979 fn add_lang(aligner: &mut MultilingualEmbeddingAligner, name: &str, dim: usize) -> LangId {
980 let id = make_lang_id(name);
981 aligner.add_language(id, name.to_string(), dim);
982 id
983 }
984
985 fn add_emb(aligner: &mut MultilingualEmbeddingAligner, lang: LangId, emb_id: u64, v: Vec<f64>) {
986 aligner
987 .add_embedding(lang, emb_id, v)
988 .expect("add_embedding failed");
989 }
990
991 #[test]
994 fn test_new_aligner_default() {
995 let a = MultilingualEmbeddingAligner::with_defaults();
996 assert!(a.language_spaces.is_empty());
997 assert!(a.alignment_matrices.is_empty());
998 assert!(a.alignment_history.is_empty());
999 }
1000
1001 #[test]
1002 fn test_new_aligner_custom_config() {
1003 let cfg = MeaAlignerConfig {
1004 dim: 32,
1005 normalize_embeddings: true,
1006 alignment_method: MeaAlignmentMethod::LinearRegression,
1007 min_anchor_pairs: 10,
1008 };
1009 let a = MultilingualEmbeddingAligner::new(cfg.clone());
1010 assert_eq!(a.config.dim, 32);
1011 assert_eq!(a.config.min_anchor_pairs, 10);
1012 assert!(a.config.normalize_embeddings);
1013 }
1014
1015 #[test]
1018 fn test_add_language() {
1019 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1020 let en = make_lang_id("en");
1021 a.add_language(en, "English".to_string(), 4);
1022 assert_eq!(a.language_spaces.len(), 1);
1023 assert_eq!(a.language_spaces[&en].name, "English");
1024 }
1025
1026 #[test]
1027 fn test_add_language_idempotent() {
1028 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1029 let en = make_lang_id("en");
1030 a.add_language(en, "English".to_string(), 4);
1031 a.add_language(en, "English2".to_string(), 4);
1032 assert_eq!(a.language_spaces[&en].name, "English");
1034 }
1035
1036 #[test]
1037 fn test_remove_language() {
1038 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1039 let en = add_lang(&mut a, "en", 4);
1040 a.remove_language(en);
1041 assert!(a.language_spaces.is_empty());
1042 }
1043
1044 #[test]
1045 fn test_remove_language_cleans_alignments() {
1046 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1047 let en = add_lang(&mut a, "en", 4);
1048 let fr = add_lang(&mut a, "fr", 4);
1049 a.alignment_matrices.insert(
1051 (en, fr),
1052 AlignmentMatrix {
1053 src_lang: en,
1054 tgt_lang: fr,
1055 matrix: identity(4),
1056 quality: 1.0,
1057 },
1058 );
1059 a.remove_language(en);
1060 assert!(!a.alignment_matrices.contains_key(&(en, fr)));
1061 }
1062
1063 #[test]
1066 fn test_add_embedding_ok() {
1067 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1068 let en = add_lang(&mut a, "en", 4);
1069 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1070 assert_eq!(a.embedding_count(en), Some(1));
1071 }
1072
1073 #[test]
1074 fn test_add_embedding_dim_mismatch() {
1075 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1076 let en = add_lang(&mut a, "en", 4);
1077 let res = a.add_embedding(en, 1, vec![1.0, 0.0]);
1078 assert!(matches!(res, Err(MeaError::DimensionMismatch { .. })));
1079 }
1080
1081 #[test]
1082 fn test_add_embedding_unknown_lang() {
1083 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1084 let unknown = make_lang_id("xx");
1085 let res = a.add_embedding(unknown, 1, vec![1.0, 0.0, 0.0, 0.0]);
1086 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1087 }
1088
1089 #[test]
1090 fn test_add_embedding_empty_vector() {
1091 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1092 let en = add_lang(&mut a, "en", 4);
1093 let res = a.add_embedding(en, 1, vec![]);
1094 assert!(matches!(res, Err(MeaError::EmptyEmbedding)));
1095 }
1096
1097 #[test]
1098 fn test_add_embedding_upsert() {
1099 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1100 let en = add_lang(&mut a, "en", 4);
1101 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1102 add_emb(&mut a, en, 1, vec![0.0, 1.0, 0.0, 0.0]);
1103 assert_eq!(a.embedding_count(en), Some(1));
1104 let v = &a.language_spaces[&en].embeddings[0].1;
1105 assert!((v[1] - 1.0).abs() < 1e-9);
1106 }
1107
1108 #[test]
1109 fn test_remove_embedding_ok() {
1110 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1111 let en = add_lang(&mut a, "en", 4);
1112 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1113 a.remove_embedding(en, 1).expect("remove failed");
1114 assert_eq!(a.embedding_count(en), Some(0));
1115 }
1116
1117 #[test]
1118 fn test_remove_embedding_not_found() {
1119 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1120 let en = add_lang(&mut a, "en", 4);
1121 let res = a.remove_embedding(en, 99);
1122 assert!(matches!(res, Err(MeaError::EmbeddingNotFound(99, _))));
1123 }
1124
1125 #[test]
1126 fn test_remove_embedding_unknown_lang() {
1127 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1128 let unknown = make_lang_id("xx");
1129 let res = a.remove_embedding(unknown, 1);
1130 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1131 }
1132
1133 #[test]
1136 fn test_compute_alignment_identity() {
1137 let dim = 4;
1138 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1139 let en = add_lang(&mut a, "en", dim);
1140 let fr = add_lang(&mut a, "fr", dim);
1141 for i in 0..5u64 {
1142 let v: Vec<f64> = (0..dim)
1143 .map(|j| ((i * dim as u64 + j as u64) % 7) as f64)
1144 .collect();
1145 add_emb(&mut a, en, i, v.clone());
1146 add_emb(&mut a, fr, i, v);
1147 }
1148 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1149 a.compute_alignment(en, fr, &anchors)
1150 .expect("alignment failed");
1151 assert!(a.has_alignment(en, fr));
1152 }
1153
1154 #[test]
1155 fn test_compute_alignment_too_few_anchors() {
1156 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1157 let en = add_lang(&mut a, "en", 4);
1158 let fr = add_lang(&mut a, "fr", 4);
1159 let anchors: Vec<(u64, u64)> = vec![(0, 0), (1, 1)]; let res = a.compute_alignment(en, fr, &anchors);
1161 assert!(matches!(res, Err(MeaError::NotEnoughAnchors { .. })));
1162 }
1163
1164 #[test]
1165 fn test_compute_alignment_unknown_src() {
1166 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1167 let xx = make_lang_id("xx");
1168 let fr = add_lang(&mut a, "fr", 4);
1169 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1170 let res = a.compute_alignment(xx, fr, &anchors);
1171 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1172 }
1173
1174 #[test]
1175 fn test_compute_alignment_unknown_tgt() {
1176 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1177 let en = add_lang(&mut a, "en", 4);
1178 let xx = make_lang_id("xx");
1179 for i in 0..5u64 {
1180 add_emb(&mut a, en, i, vec![i as f64, 0.0, 0.0, 0.0]);
1181 }
1182 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1183 let res = a.compute_alignment(en, xx, &anchors);
1184 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1185 }
1186
1187 #[test]
1188 fn test_alignment_quality_stored() {
1189 let dim = 4;
1190 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1191 let en = add_lang(&mut a, "en", dim);
1192 let fr = add_lang(&mut a, "fr", dim);
1193 for i in 0..5u64 {
1194 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1195 add_emb(&mut a, en, i, v.clone());
1196 add_emb(&mut a, fr, i, v);
1197 }
1198 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1199 a.compute_alignment(en, fr, &anchors)
1200 .expect("test: compute_alignment failed");
1201 let q = a
1203 .alignment_quality(en, fr)
1204 .expect("test: alignment_quality not found");
1205 assert!(q > 0.99, "quality={q}");
1206 }
1207
1208 #[test]
1209 fn test_alignment_quality_none_for_missing() {
1210 let a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1211 let en = make_lang_id("en");
1212 let fr = make_lang_id("fr");
1213 assert!(a.alignment_quality(en, fr).is_none());
1214 }
1215
1216 #[test]
1219 fn test_align_embedding_identity() {
1220 let dim = 4;
1221 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1222 let en = add_lang(&mut a, "en", dim);
1223 let fr = add_lang(&mut a, "fr", dim);
1224 for i in 0..5u64 {
1225 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1226 add_emb(&mut a, en, i, v.clone());
1227 add_emb(&mut a, fr, i, v);
1228 }
1229 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1230 a.compute_alignment(en, fr, &anchors)
1231 .expect("test: compute_alignment identity passthrough failed");
1232 let q = vec![2.0, 0.0, 0.0, 0.0];
1233 let aligned = a
1234 .align_embedding(en, fr, &q)
1235 .expect("test: align_embedding identity passthrough failed");
1236 assert!((aligned[0] - 2.0).abs() < 1e-6, "aligned={aligned:?}");
1238 }
1239
1240 #[test]
1241 fn test_align_embedding_no_matrix() {
1242 let a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1243 let en = make_lang_id("en");
1244 let fr = make_lang_id("fr");
1245 let res = a.align_embedding(en, fr, &[1.0, 0.0, 0.0, 0.0]);
1246 assert!(matches!(res, Err(MeaError::AlignmentNotFound(_, _))));
1247 }
1248
1249 #[test]
1250 fn test_align_embedding_dim_mismatch() {
1251 let dim = 4;
1252 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1253 let en = add_lang(&mut a, "en", dim);
1254 let fr = add_lang(&mut a, "fr", dim);
1255 for i in 0..5u64 {
1256 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1257 add_emb(&mut a, en, i, v.clone());
1258 add_emb(&mut a, fr, i, v);
1259 }
1260 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1261 a.compute_alignment(en, fr, &anchors)
1262 .expect("test: compute_alignment for dim mismatch test failed");
1263 let res = a.align_embedding(en, fr, &[1.0, 0.0]); assert!(matches!(res, Err(MeaError::DimensionMismatch { .. })));
1265 }
1266
1267 #[test]
1268 fn test_align_embedding_empty() {
1269 let dim = 4;
1270 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1271 let en = add_lang(&mut a, "en", dim);
1272 let fr = add_lang(&mut a, "fr", dim);
1273 for i in 0..5u64 {
1274 add_emb(&mut a, en, i, vec![i as f64 + 1.0, 0.0, 0.0, 0.0]);
1275 add_emb(&mut a, fr, i, vec![i as f64 + 1.0, 0.0, 0.0, 0.0]);
1276 }
1277 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1278 a.compute_alignment(en, fr, &anchors)
1279 .expect("test: compute_alignment for empty align test failed");
1280 let res = a.align_embedding(en, fr, &[]);
1281 assert!(matches!(res, Err(MeaError::EmptyEmbedding)));
1282 }
1283
1284 #[test]
1287 fn test_cross_lingual_search_same_lang() {
1288 let dim = 4;
1289 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1290 let en = add_lang(&mut a, "en", dim);
1291 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1292 add_emb(&mut a, en, 2, vec![0.0, 1.0, 0.0, 0.0]);
1293 let res = a
1294 .cross_lingual_search(en, &[1.0, 0.0, 0.0, 0.0], en, 2)
1295 .expect("test: cross_lingual_search same lang failed");
1296 assert_eq!(res[0].0, 1);
1297 }
1298
1299 #[test]
1300 fn test_cross_lingual_search_top_k() {
1301 let dim = 4;
1302 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1303 let en = add_lang(&mut a, "en", dim);
1304 for i in 1..=10u64 {
1305 add_emb(&mut a, en, i, vec![i as f64, 0.0, 0.0, 0.0]);
1306 }
1307 let res = a
1308 .cross_lingual_search(en, &[10.0, 0.0, 0.0, 0.0], en, 3)
1309 .expect("test: cross_lingual_search top k failed");
1310 assert_eq!(res.len(), 3);
1311 }
1312
1313 #[test]
1314 fn test_cross_lingual_search_sorted_by_score() {
1315 let dim = 4;
1316 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1317 let en = add_lang(&mut a, "en", dim);
1318 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1319 add_emb(&mut a, en, 2, vec![0.9, 0.1, 0.0, 0.0]);
1320 add_emb(&mut a, en, 3, vec![0.0, 0.0, 1.0, 0.0]);
1321 let res = a
1322 .cross_lingual_search(en, &[1.0, 0.0, 0.0, 0.0], en, 3)
1323 .expect("test: cross_lingual_search sorted failed");
1324 for w in res.windows(2) {
1326 assert!(w[0].1 >= w[1].1);
1327 }
1328 }
1329
1330 #[test]
1331 fn test_cross_lingual_search_cross_lang() {
1332 let dim = 4;
1333 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1334 let en = add_lang(&mut a, "en", dim);
1335 let fr = add_lang(&mut a, "fr", dim);
1336 for i in 0..5u64 {
1337 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1338 add_emb(&mut a, en, i, v.clone());
1339 add_emb(&mut a, fr, i, v);
1340 }
1341 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1342 a.compute_alignment(en, fr, &anchors)
1343 .expect("test: compute_alignment cross lang search failed");
1344 let res = a
1345 .cross_lingual_search(en, &[5.0, 0.0, 0.0, 0.0], fr, 1)
1346 .expect("test: cross_lingual_search cross lang failed");
1347 assert_eq!(res.len(), 1);
1348 }
1349
1350 #[test]
1351 fn test_cross_lingual_search_empty_query() {
1352 let dim = 4;
1353 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1354 let en = add_lang(&mut a, "en", dim);
1355 let res = a.cross_lingual_search(en, &[], en, 1);
1356 assert!(matches!(res, Err(MeaError::EmptyEmbedding)));
1357 }
1358
1359 #[test]
1360 fn test_cross_lingual_search_unknown_target() {
1361 let dim = 4;
1362 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1363 let en = add_lang(&mut a, "en", dim);
1364 let xx = make_lang_id("xx");
1365 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1366 let res = a.cross_lingual_search(en, &[1.0, 0.0, 0.0, 0.0], xx, 1);
1368 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1369 }
1370
1371 #[test]
1374 fn test_compute_centroid_basic() {
1375 let dim = 4;
1376 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1377 let en = add_lang(&mut a, "en", dim);
1378 add_emb(&mut a, en, 1, vec![2.0, 0.0, 0.0, 0.0]);
1379 add_emb(&mut a, en, 2, vec![4.0, 0.0, 0.0, 0.0]);
1380 let c = a
1381 .compute_centroid(en)
1382 .expect("test: compute_centroid basic failed");
1383 assert!((c[0] - 3.0).abs() < 1e-9);
1384 }
1385
1386 #[test]
1387 fn test_compute_centroid_cached() {
1388 let dim = 4;
1389 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1390 let en = add_lang(&mut a, "en", dim);
1391 add_emb(&mut a, en, 1, vec![2.0, 0.0, 0.0, 0.0]);
1392 a.compute_centroid(en)
1393 .expect("test: compute_centroid cached failed");
1394 assert!(a.centroid(en).is_some());
1395 }
1396
1397 #[test]
1398 fn test_compute_centroid_empty() {
1399 let dim = 4;
1400 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1401 let en = add_lang(&mut a, "en", dim);
1402 let res = a.compute_centroid(en);
1403 assert!(matches!(res, Err(MeaError::Arithmetic(_))));
1404 }
1405
1406 #[test]
1407 fn test_compute_centroid_unknown_lang() {
1408 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1409 let xx = make_lang_id("xx");
1410 let res = a.compute_centroid(xx);
1411 assert!(matches!(res, Err(MeaError::LanguageNotFound(_))));
1412 }
1413
1414 #[test]
1415 fn test_centroid_invalidated_after_add() {
1416 let dim = 4;
1417 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1418 let en = add_lang(&mut a, "en", dim);
1419 add_emb(&mut a, en, 1, vec![2.0, 0.0, 0.0, 0.0]);
1420 a.compute_centroid(en)
1421 .expect("test: compute_centroid for invalidation test failed");
1422 add_emb(&mut a, en, 2, vec![4.0, 0.0, 0.0, 0.0]);
1424 assert!(a.centroid(en).is_none());
1425 }
1426
1427 #[test]
1430 fn test_aligner_stats_empty() {
1431 let a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1432 let stats = a.aligner_stats();
1433 assert_eq!(stats.n_languages, 0);
1434 assert_eq!(stats.n_alignments, 0);
1435 assert_eq!(stats.total_embeddings, 0);
1436 }
1437
1438 #[test]
1439 fn test_aligner_stats_after_ops() {
1440 let dim = 4;
1441 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1442 let en = add_lang(&mut a, "en", dim);
1443 let fr = add_lang(&mut a, "fr", dim);
1444 for i in 0..5u64 {
1445 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1446 add_emb(&mut a, en, i, v.clone());
1447 add_emb(&mut a, fr, i, v);
1448 }
1449 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1450 a.compute_alignment(en, fr, &anchors)
1451 .expect("test: compute_alignment for stats test failed");
1452 let stats = a.aligner_stats();
1453 assert_eq!(stats.n_languages, 2);
1454 assert_eq!(stats.n_alignments, 1);
1455 assert_eq!(stats.total_embeddings, 10);
1456 assert_eq!(stats.n_history_records, 1);
1457 }
1458
1459 #[test]
1460 fn test_history_bounded() {
1461 let dim = 2;
1462 let mut a = MultilingualEmbeddingAligner::new(MeaAlignerConfig {
1463 dim,
1464 normalize_embeddings: false,
1465 alignment_method: MeaAlignmentMethod::IdentityPassthrough,
1466 min_anchor_pairs: 1,
1467 });
1468 let en = add_lang(&mut a, "en", dim);
1469 let fr = add_lang(&mut a, "fr", dim);
1470 for i in 0..510u64 {
1471 let _ = a.add_embedding(en, i % 5, vec![1.0, 0.0]);
1473 let _ = a.add_embedding(fr, i % 5, vec![1.0, 0.0]);
1474 let anchors = vec![(i % 5, i % 5)];
1475 let _ = a.compute_alignment(en, fr, &anchors);
1476 }
1477 assert!(a.alignment_history.len() <= MultilingualEmbeddingAligner::HISTORY_CAP);
1478 }
1479
1480 #[test]
1483 fn test_procrustes_identity_mapping() {
1484 let dim = 4;
1485 let mut a = make_aligner(dim, MeaAlignmentMethod::Procrustes);
1486 let en = add_lang(&mut a, "en", dim);
1487 let fr = add_lang(&mut a, "fr", dim);
1488 let vecs: Vec<Vec<f64>> = vec![
1490 vec![1.0, 0.0, 0.0, 0.0],
1491 vec![0.0, 1.0, 0.0, 0.0],
1492 vec![0.0, 0.0, 1.0, 0.0],
1493 vec![0.0, 0.0, 0.0, 1.0],
1494 vec![1.0, 1.0, 0.0, 0.0],
1495 ];
1496 for (i, v) in vecs.iter().enumerate() {
1497 add_emb(&mut a, en, i as u64, v.clone());
1498 add_emb(&mut a, fr, i as u64, v.clone());
1499 }
1500 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1501 a.compute_alignment(en, fr, &anchors)
1502 .expect("test: compute_alignment procrustes failed");
1503 let q = a
1504 .alignment_quality(en, fr)
1505 .expect("test: alignment_quality procrustes not found");
1506 assert!(q > 0.9, "Procrustes quality on identity={q}");
1507 }
1508
1509 #[test]
1510 fn test_procrustes_rotation() {
1511 let dim = 2;
1512 let mut a = make_aligner(dim, MeaAlignmentMethod::Procrustes);
1513 let en = add_lang(&mut a, "en", dim);
1514 let fr = add_lang(&mut a, "fr", dim);
1515 let src_vecs: Vec<Vec<f64>> = vec![
1517 vec![1.0, 0.0],
1518 vec![0.0, 1.0],
1519 vec![1.0, 1.0],
1520 vec![2.0, 0.0],
1521 vec![0.0, 2.0],
1522 ];
1523 let tgt_vecs: Vec<Vec<f64>> = src_vecs.iter().map(|v| vec![-v[1], v[0]]).collect();
1524 for (i, (sv, tv)) in src_vecs.iter().zip(tgt_vecs.iter()).enumerate() {
1525 add_emb(&mut a, en, i as u64, sv.clone());
1526 add_emb(&mut a, fr, i as u64, tv.clone());
1527 }
1528 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1529 a.compute_alignment(en, fr, &anchors)
1530 .expect("test: compute_alignment procrustes rotation failed");
1531 let q = a
1532 .alignment_quality(en, fr)
1533 .expect("test: alignment_quality procrustes rotation not found");
1534 assert!(q > 0.9, "Procrustes rotation quality={q}");
1535 }
1536
1537 #[test]
1540 fn test_linear_regression_identity() {
1541 let dim = 4;
1542 let mut a = make_aligner(dim, MeaAlignmentMethod::LinearRegression);
1543 let en = add_lang(&mut a, "en", dim);
1544 let fr = add_lang(&mut a, "fr", dim);
1545 let vecs: Vec<Vec<f64>> = vec![
1546 vec![1.0, 0.0, 0.0, 0.0],
1547 vec![0.0, 1.0, 0.0, 0.0],
1548 vec![0.0, 0.0, 1.0, 0.0],
1549 vec![0.0, 0.0, 0.0, 1.0],
1550 vec![0.5, 0.5, 0.0, 0.0],
1551 ];
1552 for (i, v) in vecs.iter().enumerate() {
1553 add_emb(&mut a, en, i as u64, v.clone());
1554 add_emb(&mut a, fr, i as u64, v.clone());
1555 }
1556 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1557 a.compute_alignment(en, fr, &anchors)
1558 .expect("test: compute_alignment linreg identity failed");
1559 let q = a
1560 .alignment_quality(en, fr)
1561 .expect("test: alignment_quality linreg identity not found");
1562 assert!(q > 0.9, "LinReg quality={q}");
1563 }
1564
1565 #[test]
1566 fn test_linear_regression_scaling() {
1567 let dim = 2;
1568 let mut a = make_aligner(dim, MeaAlignmentMethod::LinearRegression);
1569 let en = add_lang(&mut a, "en", dim);
1570 let fr = add_lang(&mut a, "fr", dim);
1571 let src: Vec<Vec<f64>> = vec![
1572 vec![1.0, 0.0],
1573 vec![0.0, 1.0],
1574 vec![1.0, 1.0],
1575 vec![2.0, 0.0],
1576 vec![0.0, 2.0],
1577 ];
1578 let tgt: Vec<Vec<f64>> = src.iter().map(|v| vec![v[0] * 2.0, v[1] * 3.0]).collect();
1580 for (i, (sv, tv)) in src.iter().zip(tgt.iter()).enumerate() {
1581 add_emb(&mut a, en, i as u64, sv.clone());
1582 add_emb(&mut a, fr, i as u64, tv.clone());
1583 }
1584 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1585 a.compute_alignment(en, fr, &anchors)
1586 .expect("test: compute_alignment linreg scaling failed");
1587 let out = a
1589 .align_embedding(en, fr, &[1.0, 0.0])
1590 .expect("test: align_embedding linreg scaling failed");
1591 assert!((out[0] - 2.0).abs() < 0.1, "out={out:?}");
1592 }
1593
1594 #[test]
1597 fn test_cca_alignment() {
1598 let dim = 4;
1599 let mut a = make_aligner(dim, MeaAlignmentMethod::Cca);
1600 let en = add_lang(&mut a, "en", dim);
1601 let fr = add_lang(&mut a, "fr", dim);
1602 let vecs: Vec<Vec<f64>> = vec![
1603 vec![1.0, 0.0, 0.0, 0.0],
1604 vec![0.0, 1.0, 0.0, 0.0],
1605 vec![0.0, 0.0, 1.0, 0.0],
1606 vec![0.0, 0.0, 0.0, 1.0],
1607 vec![0.5, 0.5, 0.5, 0.5],
1608 ];
1609 for (i, v) in vecs.iter().enumerate() {
1610 add_emb(&mut a, en, i as u64, v.clone());
1611 add_emb(&mut a, fr, i as u64, v.clone());
1612 }
1613 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1614 a.compute_alignment(en, fr, &anchors)
1615 .expect("test: compute_alignment CCA failed");
1616 assert!(a.has_alignment(en, fr));
1617 }
1618
1619 #[test]
1622 fn test_language_ids() {
1623 let mut a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1624 let en = add_lang(&mut a, "en", 4);
1625 let fr = add_lang(&mut a, "fr", 4);
1626 let ids = a.language_ids();
1627 assert!(ids.contains(&en));
1628 assert!(ids.contains(&fr));
1629 }
1630
1631 #[test]
1632 fn test_has_alignment_false() {
1633 let a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1634 assert!(!a.has_alignment(make_lang_id("en"), make_lang_id("fr")));
1635 }
1636
1637 #[test]
1638 fn test_get_alignment_matrix() {
1639 let dim = 4;
1640 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1641 let en = add_lang(&mut a, "en", dim);
1642 let fr = add_lang(&mut a, "fr", dim);
1643 for i in 0..5u64 {
1644 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1645 add_emb(&mut a, en, i, v.clone());
1646 add_emb(&mut a, fr, i, v);
1647 }
1648 let anchors: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1649 a.compute_alignment(en, fr, &anchors)
1650 .expect("test: compute_alignment for get_alignment_matrix test failed");
1651 assert!(a.get_alignment_matrix(en, fr).is_some());
1652 assert!(a.get_alignment_matrix(fr, en).is_none());
1653 }
1654
1655 #[test]
1656 fn test_history_recent_first() {
1657 let dim = 2;
1658 let mut a = MultilingualEmbeddingAligner::new(MeaAlignerConfig {
1659 dim,
1660 normalize_embeddings: false,
1661 alignment_method: MeaAlignmentMethod::IdentityPassthrough,
1662 min_anchor_pairs: 1,
1663 });
1664 let en = add_lang(&mut a, "en", dim);
1665 let fr = add_lang(&mut a, "fr", dim);
1666 let de = add_lang(&mut a, "de", dim);
1667 for i in 0..3u64 {
1668 let _ = a.add_embedding(en, i, vec![1.0, 0.0]);
1669 let _ = a.add_embedding(fr, i, vec![1.0, 0.0]);
1670 let _ = a.add_embedding(de, i, vec![1.0, 0.0]);
1671 }
1672 a.compute_alignment(en, fr, &[(0, 0)])
1673 .expect("test: compute_alignment en->fr history failed");
1674 a.compute_alignment(en, de, &[(0, 0)])
1675 .expect("test: compute_alignment en->de history failed");
1676 let hist = a.history(2);
1677 assert_eq!(hist[0].tgt_lang, de);
1679 }
1680
1681 #[test]
1684 fn test_cosine_similarity_identical() {
1685 let v = vec![1.0, 0.0, 0.0];
1686 assert!((cosine_similarity(&v, &v) - 1.0).abs() < 1e-9);
1687 }
1688
1689 #[test]
1690 fn test_cosine_similarity_orthogonal() {
1691 let a = vec![1.0, 0.0];
1692 let b = vec![0.0, 1.0];
1693 assert!(cosine_similarity(&a, &b).abs() < 1e-9);
1694 }
1695
1696 #[test]
1697 fn test_cosine_similarity_zero_vector() {
1698 let a = vec![0.0, 0.0];
1699 let b = vec![1.0, 0.0];
1700 assert_eq!(cosine_similarity(&a, &b), 0.0);
1701 }
1702
1703 #[test]
1704 fn test_cosine_similarity_opposite() {
1705 let a = vec![1.0, 0.0];
1706 let b = vec![-1.0, 0.0];
1707 assert!((cosine_similarity(&a, &b) + 1.0).abs() < 1e-9);
1708 }
1709
1710 #[test]
1713 fn test_xorshift_not_zero() {
1714 let mut state = 0xdeadbeef_u64;
1715 let v = xorshift64(&mut state);
1716 assert_ne!(v, 0);
1717 }
1718
1719 #[test]
1720 fn test_xorshift_deterministic() {
1721 let mut s1 = 42u64;
1722 let mut s2 = 42u64;
1723 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1724 }
1725
1726 #[test]
1727 fn test_xorshift_state_changes() {
1728 let mut state = 1u64;
1729 let v1 = xorshift64(&mut state);
1730 let v2 = xorshift64(&mut state);
1731 assert_ne!(v1, v2);
1732 }
1733
1734 #[test]
1737 fn test_normalize_vec_unit() {
1738 let mut v = vec![3.0, 4.0];
1739 normalize_vec(&mut v);
1740 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
1741 assert!((norm - 1.0).abs() < 1e-9);
1742 }
1743
1744 #[test]
1745 fn test_normalize_vec_zero_returns_false() {
1746 let mut v = vec![0.0, 0.0];
1747 assert!(!normalize_vec(&mut v));
1748 }
1749
1750 #[test]
1753 fn test_normalize_flag_normalizes_on_insert() {
1754 let dim = 2;
1755 let mut a = MultilingualEmbeddingAligner::new(MeaAlignerConfig {
1756 dim,
1757 normalize_embeddings: true,
1758 alignment_method: MeaAlignmentMethod::IdentityPassthrough,
1759 min_anchor_pairs: 1,
1760 });
1761 let en = add_lang(&mut a, "en", dim);
1762 a.add_embedding(en, 1, vec![3.0, 4.0])
1763 .expect("test: add_embedding normalize flag failed");
1764 let v = &a.language_spaces[&en].embeddings[0].1;
1765 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
1766 assert!((norm - 1.0).abs() < 1e-9);
1767 }
1768
1769 #[test]
1772 fn test_matmul_identity() {
1773 let i = identity(3);
1774 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
1775 let out = matmul(&i, &v, 3, 3, 3);
1776 for (a, b) in out.iter().zip(v.iter()) {
1777 assert!((a - b).abs() < 1e-9);
1778 }
1779 }
1780
1781 #[test]
1782 fn test_transpose_correctness() {
1783 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let t = transpose(&a, 2, 3); assert_eq!(t, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1786 }
1787
1788 #[test]
1789 fn test_gauss_jordan_2x2() {
1790 let a = vec![2.0, 0.0, 0.0, 4.0];
1792 let inv = gauss_jordan_invert(&a, 2).expect("test: gauss_jordan_invert 2x2 failed");
1793 assert!((inv[0] - 0.5).abs() < 1e-9);
1794 assert!((inv[3] - 0.25).abs() < 1e-9);
1795 }
1796
1797 #[test]
1798 fn test_gauss_jordan_singular() {
1799 let a = vec![1.0, 2.0, 2.0, 4.0]; let res = gauss_jordan_invert(&a, 2);
1801 assert!(matches!(res, Err(MeaError::Arithmetic(_))));
1802 }
1803
1804 #[test]
1807 fn test_alignment_matrix_apply_identity() {
1808 let dim = 3;
1809 let am = AlignmentMatrix {
1810 src_lang: [0; 8],
1811 tgt_lang: [0; 8],
1812 matrix: identity(dim),
1813 quality: 1.0,
1814 };
1815 let v = vec![1.0, 2.0, 3.0];
1816 let out = am
1817 .apply(&v)
1818 .expect("test: AlignmentMatrix::apply on identity matrix failed");
1819 assert_eq!(out, v);
1820 }
1821
1822 #[test]
1823 fn test_alignment_matrix_apply_dim_mismatch() {
1824 let am = AlignmentMatrix {
1825 src_lang: [0; 8],
1826 tgt_lang: [0; 8],
1827 matrix: identity(3), quality: 1.0,
1829 };
1830 let res = am.apply(&[1.0, 2.0, 3.0, 4.0]);
1832 assert!(matches!(res, Err(MeaError::DimensionMismatch { .. })));
1833 }
1834
1835 #[test]
1838 fn test_multiple_language_pairs() {
1839 let dim = 4;
1840 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1841 let en = add_lang(&mut a, "en", dim);
1842 let fr = add_lang(&mut a, "fr", dim);
1843 let de = add_lang(&mut a, "de", dim);
1844 for i in 0..5u64 {
1845 let v = vec![i as f64 + 1.0, 0.0, 0.0, 0.0];
1846 add_emb(&mut a, en, i, v.clone());
1847 add_emb(&mut a, fr, i, v.clone());
1848 add_emb(&mut a, de, i, v);
1849 }
1850 let anch: Vec<(u64, u64)> = (0..5).map(|i| (i, i)).collect();
1851 a.compute_alignment(en, fr, &anch)
1852 .expect("test: compute_alignment en->fr for multiple pairs failed");
1853 a.compute_alignment(en, de, &anch)
1854 .expect("test: compute_alignment en->de for multiple pairs failed");
1855 assert_eq!(a.alignment_matrices.len(), 2);
1856 }
1857
1858 #[test]
1861 fn test_embedding_count_none_for_unknown_lang() {
1862 let a = make_aligner(4, MeaAlignmentMethod::IdentityPassthrough);
1863 assert!(a.embedding_count(make_lang_id("xx")).is_none());
1864 }
1865
1866 #[test]
1867 fn test_embedding_count_increments() {
1868 let dim = 4;
1869 let mut a = make_aligner(dim, MeaAlignmentMethod::IdentityPassthrough);
1870 let en = add_lang(&mut a, "en", dim);
1871 add_emb(&mut a, en, 1, vec![1.0, 0.0, 0.0, 0.0]);
1872 add_emb(&mut a, en, 2, vec![0.0, 1.0, 0.0, 0.0]);
1873 assert_eq!(a.embedding_count(en), Some(2));
1874 }
1875
1876 #[test]
1879 fn test_type_aliases() {
1880 let _: MeaMultilingualEmbeddingAligner = MultilingualEmbeddingAligner::with_defaults();
1881 let _id: LangId = make_lang_id("test");
1882 }
1883}