Skip to main content

oxirs_embed/models/
serialization.rs

1//! Shared serialization helpers for knowledge-graph embedding models.
2//!
3//! Embedding models keep their weights in `scirs2_core` ndarray matrices which
4//! do not serialize directly through `oxicode`/`serde`. These helpers convert
5//! those matrices to/from flat, shape-tagged vectors and provide a serializable
6//! snapshot of the shared [`BaseModel`] state so that each model's `save`/`load`
7//! implementation stays small and consistent.
8
9use crate::models::base::BaseModel;
10use crate::ModelConfig;
11use anyhow::{anyhow, Result};
12use chrono::{DateTime, Utc};
13use scirs2_core::ndarray_ext::{Array1, Array2, Array3};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use uuid::Uuid;
17
18/// Serializable, shape-tagged representation of a 2-D `f64` matrix.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MatrixF64 {
21    pub rows: usize,
22    pub cols: usize,
23    pub data: Vec<f64>,
24}
25
26impl MatrixF64 {
27    /// Capture a matrix into a serializable form (row-major).
28    pub fn from_array(a: &Array2<f64>) -> Self {
29        let (rows, cols) = a.dim();
30        Self {
31            rows,
32            cols,
33            data: a.iter().copied().collect(),
34        }
35    }
36
37    /// Reconstruct the matrix, validating the element count against the shape.
38    pub fn to_array(&self) -> Result<Array2<f64>> {
39        if self.rows * self.cols != self.data.len() {
40            return Err(anyhow!(
41                "corrupt matrix payload: {}x{} != {} elements",
42                self.rows,
43                self.cols,
44                self.data.len()
45            ));
46        }
47        Array2::from_shape_vec((self.rows, self.cols), self.data.clone())
48            .map_err(|e| anyhow!("failed to rebuild matrix: {}", e))
49    }
50}
51
52/// Serializable, shape-tagged representation of a 2-D `f32` matrix.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct MatrixF32 {
55    pub rows: usize,
56    pub cols: usize,
57    pub data: Vec<f32>,
58}
59
60impl MatrixF32 {
61    pub fn from_array(a: &Array2<f32>) -> Self {
62        let (rows, cols) = a.dim();
63        Self {
64            rows,
65            cols,
66            data: a.iter().copied().collect(),
67        }
68    }
69
70    pub fn to_array(&self) -> Result<Array2<f32>> {
71        if self.rows * self.cols != self.data.len() {
72            return Err(anyhow!(
73                "corrupt matrix payload: {}x{} != {} elements",
74                self.rows,
75                self.cols,
76                self.data.len()
77            ));
78        }
79        Array2::from_shape_vec((self.rows, self.cols), self.data.clone())
80            .map_err(|e| anyhow!("failed to rebuild matrix: {}", e))
81    }
82}
83
84/// Serializable, shape-tagged representation of a 1-D `f32` vector.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct VectorF32 {
87    pub data: Vec<f32>,
88}
89
90impl VectorF32 {
91    pub fn from_array(a: &Array1<f32>) -> Self {
92        Self {
93            data: a.iter().copied().collect(),
94        }
95    }
96
97    pub fn to_array(&self) -> Array1<f32> {
98        Array1::from_vec(self.data.clone())
99    }
100}
101
102/// Serializable, shape-tagged representation of a 3-D `f64` tensor.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Tensor3F64 {
105    pub d0: usize,
106    pub d1: usize,
107    pub d2: usize,
108    pub data: Vec<f64>,
109}
110
111impl Tensor3F64 {
112    pub fn from_array(a: &Array3<f64>) -> Self {
113        let (d0, d1, d2) = a.dim();
114        Self {
115            d0,
116            d1,
117            d2,
118            data: a.iter().copied().collect(),
119        }
120    }
121
122    pub fn to_array(&self) -> Result<Array3<f64>> {
123        if self.d0 * self.d1 * self.d2 != self.data.len() {
124            return Err(anyhow!(
125                "corrupt tensor payload: {}x{}x{} != {} elements",
126                self.d0,
127                self.d1,
128                self.d2,
129                self.data.len()
130            ));
131        }
132        Array3::from_shape_vec((self.d0, self.d1, self.d2), self.data.clone())
133            .map_err(|e| anyhow!("failed to rebuild tensor: {}", e))
134    }
135}
136
137/// Serializable snapshot of the shared [`BaseModel`] state.
138///
139/// `positive_triples` is intentionally omitted: it is a lookup set derived from
140/// `triples` and is rebuilt on restore, keeping the payload compact.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct BaseModelSnapshot {
143    pub config: ModelConfig,
144    pub model_id: Uuid,
145    pub entity_to_id: HashMap<String, usize>,
146    pub id_to_entity: HashMap<usize, String>,
147    pub relation_to_id: HashMap<String, usize>,
148    pub id_to_relation: HashMap<usize, String>,
149    pub triples: Vec<(usize, usize, usize)>,
150    pub is_trained: bool,
151    pub creation_time: DateTime<Utc>,
152    pub last_training_time: Option<DateTime<Utc>>,
153}
154
155impl BaseModelSnapshot {
156    /// Capture the persistable state of a base model.
157    pub fn capture(base: &BaseModel) -> Self {
158        Self {
159            config: base.config.clone(),
160            model_id: base.model_id,
161            entity_to_id: base.entity_to_id.clone(),
162            id_to_entity: base.id_to_entity.clone(),
163            relation_to_id: base.relation_to_id.clone(),
164            id_to_relation: base.id_to_relation.clone(),
165            triples: base.triples.clone(),
166            is_trained: base.is_trained,
167            creation_time: base.creation_time,
168            last_training_time: base.last_training_time,
169        }
170    }
171
172    /// Restore a base model in-place from this snapshot, rebuilding the
173    /// derived `positive_triples` lookup set.
174    pub fn restore_into(self, base: &mut BaseModel) {
175        base.config = self.config;
176        base.model_id = self.model_id;
177        base.entity_to_id = self.entity_to_id;
178        base.id_to_entity = self.id_to_entity;
179        base.relation_to_id = self.relation_to_id;
180        base.id_to_relation = self.id_to_relation;
181        base.positive_triples = self.triples.iter().copied().collect();
182        base.triples = self.triples;
183        base.is_trained = self.is_trained;
184        base.creation_time = self.creation_time;
185        base.last_training_time = self.last_training_time;
186    }
187}