1use serde::{Deserialize, Serialize};
2
3use crate::{
4 digest::json_digest,
5 directions::directions_for_method,
6 lloyd::{refine_codebook, LloydReportV1},
7 profile::{FibQuantProfileV1, LloydMode, RadiusMethod},
8 rotation::StoredRotation,
9 spherical_beta::{radius_quantile, radius_quantile_k2_closed_form},
10 FibQuantError, Result,
11};
12
13pub const CODEBOOK_SCHEMA: &str = "fib_codebook_v1";
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct FibCodebookV1 {
18 pub schema_version: String,
20 pub profile: FibQuantProfileV1,
22 pub profile_digest: String,
24 pub codebook_digest: String,
26 pub rotation_digest: String,
28 pub codewords: Vec<f32>,
30 pub init_mse: f64,
32 pub training_mse: f64,
34 pub refinement_report: LloydReportV1,
36}
37
38impl FibCodebookV1 {
39 pub fn build(profile: FibQuantProfileV1) -> Result<Self> {
41 profile.validate()?;
42 let profile_digest = profile.digest()?;
43 let rotation_digest =
44 StoredRotation::new(profile.ambient_dim as usize, profile.rotation_seed)?.digest()?;
45 let initial = build_initial_codebook(&profile)?;
46 let refined = match profile.lloyd_mode {
47 LloydMode::Refine => refine_codebook(&profile, &initial)?,
48 LloydMode::Skip => crate::lloyd::skip_refinement(&profile, &initial)?,
49 };
50 let mut codebook = Self {
51 schema_version: CODEBOOK_SCHEMA.into(),
52 profile,
53 profile_digest,
54 codebook_digest: String::new(),
55 rotation_digest,
56 codewords: refined.codewords,
57 init_mse: refined.init_mse,
58 training_mse: refined.training_mse,
59 refinement_report: refined.report,
60 };
61 codebook.codebook_digest = codebook.compute_digest()?;
62 Ok(codebook)
63 }
64
65 pub fn validate(&self) -> Result<()> {
67 if self.schema_version != CODEBOOK_SCHEMA {
68 return Err(FibQuantError::CorruptPayload(format!(
69 "codebook schema_version {}, expected {CODEBOOK_SCHEMA}",
70 self.schema_version
71 )));
72 }
73 self.profile.validate()?;
74 self.refinement_report
75 .validate_against_profile(&self.profile)?;
76 let expected_profile = self.profile.digest()?;
77 if self.profile_digest != expected_profile {
78 return Err(FibQuantError::ProfileDigestMismatch {
79 expected: expected_profile,
80 actual: self.profile_digest.clone(),
81 });
82 }
83 let expected_rotation = StoredRotation::new(
84 self.profile.ambient_dim as usize,
85 self.profile.rotation_seed,
86 )?
87 .digest()?;
88 if self.rotation_digest != expected_rotation {
89 return Err(FibQuantError::RotationDigestMismatch {
90 expected: expected_rotation,
91 actual: self.rotation_digest.clone(),
92 });
93 }
94 let expected_codebook = self.compute_digest()?;
95 if self.codebook_digest != expected_codebook {
96 return Err(FibQuantError::CodebookDigestMismatch {
97 expected: expected_codebook,
98 actual: self.codebook_digest.clone(),
99 });
100 }
101 let expected_len = (self.profile.codebook_size as usize)
102 .checked_mul(self.profile.block_dim as usize)
103 .ok_or_else(|| {
104 FibQuantError::ResourceLimitExceeded("codebook value count overflow".into())
105 })?;
106 if self.codewords.len() != expected_len {
107 return Err(FibQuantError::CorruptPayload(format!(
108 "codebook has {} values, expected {expected_len}",
109 self.codewords.len()
110 )));
111 }
112 if self.codewords.iter().any(|value| !value.is_finite()) {
113 return Err(FibQuantError::CorruptPayload(
114 "codebook contains non-finite value".into(),
115 ));
116 }
117 Ok(())
118 }
119
120 pub fn codeword(&self, index: usize) -> Result<Vec<f64>> {
122 let n = self.profile.codebook_size as usize;
123 let k = self.profile.block_dim as usize;
124 if index >= n {
125 return Err(FibQuantError::IndexOutOfRange {
126 index: index as u32,
127 codebook_size: n as u32,
128 });
129 }
130 Ok(self.codewords[index * k..(index + 1) * k]
131 .iter()
132 .map(|value| f64::from(*value))
133 .collect())
134 }
135
136 pub fn compute_digest(&self) -> Result<String> {
138 #[derive(Serialize)]
139 struct DigestView<'a> {
140 schema_version: &'a str,
141 profile_digest: &'a str,
142 rotation_digest: &'a str,
143 codewords: &'a [f32],
144 init_mse: f64,
145 training_mse: f64,
146 refinement_report: &'a LloydReportV1,
147 }
148 json_digest(
149 CODEBOOK_SCHEMA,
150 &DigestView {
151 schema_version: &self.schema_version,
152 profile_digest: &self.profile_digest,
153 rotation_digest: &self.rotation_digest,
154 codewords: &self.codewords,
155 init_mse: self.init_mse,
156 training_mse: self.training_mse,
157 refinement_report: &self.refinement_report,
158 },
159 )
160 }
161}
162
163pub fn build_initial_codebook(profile: &FibQuantProfileV1) -> Result<Vec<f64>> {
165 profile.validate()?;
166 let d = profile.ambient_dim as usize;
167 let k = profile.block_dim as usize;
168 let n = profile.codebook_size as usize;
169 let directions = directions_for_method(k, n, &profile.direction_method)?;
170 let value_count = n.checked_mul(k).ok_or_else(|| {
171 FibQuantError::ResourceLimitExceeded("codebook value count overflow".into())
172 })?;
173 let mut codewords = Vec::with_capacity(value_count);
174 for (idx, direction) in directions.iter().enumerate() {
175 let radius = radius_for_method(profile, d, k, idx + 1, n)?;
176 for value in direction {
177 let code = radius * value;
178 if !code.is_finite() {
179 return Err(FibQuantError::NumericalFailure(
180 "non-finite initialized codeword".into(),
181 ));
182 }
183 codewords.push(code);
184 }
185 }
186 Ok(codewords)
187}
188
189fn radius_for_method(
190 profile: &FibQuantProfileV1,
191 d: usize,
192 k: usize,
193 idx: usize,
194 n: usize,
195) -> Result<f64> {
196 match profile.radius_method {
197 RadiusMethod::K2ClosedForm if k == 2 => {
198 let q = (idx as f64 - 0.5) / n as f64;
199 radius_quantile_k2_closed_form(d, q)
200 }
201 RadiusMethod::BetaQuantile if k >= 3 => radius_quantile(d, k, idx, n),
202 _ => Err(FibQuantError::CorruptPayload(format!(
203 "radius method {:?} is not supported for k={k}",
204 profile.radius_method
205 ))),
206 }
207}