1use serde::{Deserialize, Serialize};
16
17use crate::error::{invalid_input, require_finite};
18use crate::{ScoringError, ScoringResult, VectorProbabilityTransform};
19
20pub const VECTOR_CALIBRATION_MODEL_SCHEMA_VERSION: u32 = 1;
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct VectorCalibrationTarget {
28 pub corpus_id: String,
29 pub corpus_version: String,
30 pub index_id: String,
31 pub index_version: String,
32 pub index_kind: String,
33 pub embedding_model_id: String,
34 pub embedding_model_version: String,
35 pub candidate_k: usize,
36 pub dimensions: u32,
37}
38
39impl VectorCalibrationTarget {
40 pub fn validate(&self) -> ScoringResult<()> {
41 for (name, value) in [
42 ("corpus_id", self.corpus_id.as_str()),
43 ("corpus_version", self.corpus_version.as_str()),
44 ("index_id", self.index_id.as_str()),
45 ("index_version", self.index_version.as_str()),
46 ("index_kind", self.index_kind.as_str()),
47 ("embedding_model_id", self.embedding_model_id.as_str()),
48 (
49 "embedding_model_version",
50 self.embedding_model_version.as_str(),
51 ),
52 ] {
53 if value.trim().is_empty() {
54 return Err(invalid_input(format!("{name} must not be empty")));
55 }
56 }
57 if self.candidate_k == 0 {
58 return Err(invalid_input("candidate_k must be greater than zero"));
59 }
60 if self.dimensions == 0 {
61 return Err(invalid_input("dimensions must be greater than zero"));
62 }
63 Ok(())
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct VectorCalibrationProvenance {
70 pub model_version: String,
72 pub target: VectorCalibrationTarget,
74 pub fit_sample_count: usize,
76}
77
78impl VectorCalibrationProvenance {
79 pub fn validate(&self) -> ScoringResult<()> {
80 if self.model_version.trim().is_empty() {
81 return Err(invalid_input("model_version must not be empty"));
82 }
83 if self.fit_sample_count < 2 {
84 return Err(invalid_input(format!(
85 "fit_sample_count must be at least 2, got {}",
86 self.fit_sample_count
87 )));
88 }
89 self.target.validate()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct VectorCalibrationModel {
96 schema_version: u32,
97 transform: VectorProbabilityTransform,
98 provenance: VectorCalibrationProvenance,
99}
100
101impl VectorCalibrationModel {
102 pub fn new(
103 transform: VectorProbabilityTransform,
104 provenance: VectorCalibrationProvenance,
105 ) -> ScoringResult<Self> {
106 let model = Self {
107 schema_version: VECTOR_CALIBRATION_MODEL_SCHEMA_VERSION,
108 transform,
109 provenance,
110 };
111 model.validate()?;
112 Ok(model)
113 }
114
115 pub fn transform(&self) -> VectorProbabilityTransform {
116 self.transform
117 }
118
119 pub fn provenance(&self) -> &VectorCalibrationProvenance {
120 &self.provenance
121 }
122
123 pub fn validate_for(&self, target: &VectorCalibrationTarget) -> ScoringResult<()> {
124 self.validate()?;
125 target.validate()?;
126 if self.provenance.target != *target {
127 return Err(invalid_input(format!(
128 "vector calibration target mismatch: model={:?}, runtime={target:?}",
129 self.provenance.target
130 )));
131 }
132 Ok(())
133 }
134
135 pub fn calibrate_one(
136 &self,
137 distance: f64,
138 target: &VectorCalibrationTarget,
139 ) -> ScoringResult<f64> {
140 self.validate_for(target)?;
141 self.transform.calibrate_one(distance)
142 }
143
144 pub fn calibrate(
145 &self,
146 distances: &[f64],
147 target: &VectorCalibrationTarget,
148 ) -> ScoringResult<Vec<f64>> {
149 self.validate_for(target)?;
150 self.transform.calibrate(distances, None)
151 }
152
153 pub fn to_json(&self) -> ScoringResult<String> {
154 self.validate()?;
155 serde_json::to_string(self)
156 .map_err(|error| invalid_input(format!("serialize vector calibration model: {error}")))
157 }
158
159 pub fn from_json(json: &str) -> ScoringResult<Self> {
160 let model: Self = serde_json::from_str(json).map_err(|error| {
161 invalid_input(format!("deserialize vector calibration model: {error}"))
162 })?;
163 model.validate()?;
164 Ok(model)
165 }
166
167 fn validate(&self) -> ScoringResult<()> {
168 if self.schema_version != VECTOR_CALIBRATION_MODEL_SCHEMA_VERSION {
169 return Err(invalid_input(format!(
170 "unsupported vector calibration schema version {}, expected {}",
171 self.schema_version, VECTOR_CALIBRATION_MODEL_SCHEMA_VERSION
172 )));
173 }
174 VectorProbabilityTransform::new(
175 self.transform.mu_match,
176 self.transform.mu_random,
177 self.transform.sigma,
178 self.transform.base_rate,
179 )?;
180 self.provenance.validate()
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq)]
188pub struct VectorCalibrationStabilityReport {
189 pub reference_k: usize,
190 pub candidate_k: usize,
191 pub probe_count: usize,
192 pub mean_absolute_drift: f64,
193 pub max_absolute_drift: f64,
194}
195
196impl VectorCalibrationStabilityReport {
197 pub fn compare(
198 reference: &VectorCalibrationModel,
199 candidate: &VectorCalibrationModel,
200 probe_distances: &[f64],
201 ) -> ScoringResult<Self> {
202 if probe_distances.is_empty() {
203 return Err(invalid_input(
204 "calibration stability probes must not be empty",
205 ));
206 }
207 let reference_target = &reference.provenance.target;
208 let candidate_target = &candidate.provenance.target;
209 let reference_probabilities = reference.calibrate(probe_distances, reference_target)?;
210 let candidate_probabilities = candidate.calibrate(probe_distances, candidate_target)?;
211 let mut sum = 0.0;
212 let mut max = 0.0_f64;
213 for (left, right) in reference_probabilities
214 .iter()
215 .zip(candidate_probabilities.iter())
216 {
217 let drift = (left - right).abs();
218 require_finite(drift, "vector calibration probability drift")?;
219 sum += drift;
220 max = max.max(drift);
221 if !sum.is_finite() {
222 return Err(ScoringError::ArithmeticOverflow(
223 "vector calibration drift accumulation is not finite".into(),
224 ));
225 }
226 }
227 Ok(Self {
228 reference_k: reference_target.candidate_k,
229 candidate_k: candidate_target.candidate_k,
230 probe_count: probe_distances.len(),
231 mean_absolute_drift: sum / probe_distances.len() as f64,
232 max_absolute_drift: max,
233 })
234 }
235}