1use crate::Embedding;
6
7mod sha256;
8#[cfg(test)]
9mod tests;
10
11pub const REVISION: &str = "be9e066984345c92424c86bfcce3b17f3e3e8484";
13pub const ATTRIBUTION: &str = include_str!("../../assets/photo-anchor/NOTICE.md");
15const MANIFEST: &[u8] = include_bytes!("../../assets/photo-anchor/manifest.json");
16const MANIFEST_SHA: &str = "4dbde655fdd44a1feb4b010685fe065ffbe9c1f996331b4f4a40c9a5b2580c1e";
17
18#[derive(Debug, thiserror::Error, PartialEq, Eq)]
20pub enum Error {
21 #[error("Photo Anchor tables are not bundled for dimension {0}")]
22 UnsupportedDimension(usize),
23 #[error("Photo Anchor asset integrity failure: {0}")]
24 AssetIntegrity(&'static str),
25 #[error("expected {expected} values/bytes, got {actual}")]
26 Length { expected: usize, actual: usize },
27 #[error("Photo Anchor requires a finite unit embedding")]
28 NonUnitEmbedding,
29}
30
31pub struct PhotoAnchorQ4 {
33 anchor: Vec<f32>,
34 offset: Vec<f32>,
35 scale: Vec<f32>,
36}
37
38impl PhotoAnchorQ4 {
39 pub fn new(dim: usize) -> Result<Self, Error> {
41 let (anchor, table, anchor_sha, table_sha) = match dim {
42 256 => (
43 include_bytes!("../../assets/photo-anchor/photo_anchor_256.f32.bin").as_slice(),
44 include_bytes!("../../assets/photo-anchor/quant4_256.f32.bin").as_slice(),
45 "4e30ed052129ee745bb810fc1673e360857557e4a97daffdb2734e2b8dc5dc28",
46 "fe029c4cf510f6a628a24bcdc3db097ce8c387e948080d5065ef81ff904a2353",
47 ),
48 768 => (
49 include_bytes!("../../assets/photo-anchor/photo_anchor_768.f32.bin").as_slice(),
50 include_bytes!("../../assets/photo-anchor/quant4_768.f32.bin").as_slice(),
51 "a3978221be716a22f0d738efdc93df0de90fb8c490339011422ab15c2e8a3bb6",
52 "5088d57dbb8ae53da9f45f94ce20aa9177160fcbc499d41a8ecb755270ba5560",
53 ),
54 _ => return Err(Error::UnsupportedDimension(dim)),
55 };
56 Self::load(dim, MANIFEST, anchor, table, anchor_sha, table_sha)
57 }
58
59 fn load(
60 dim: usize,
61 manifest: &[u8],
62 anchor: &[u8],
63 table: &[u8],
64 anchor_sha: &str,
65 table_sha: &str,
66 ) -> Result<Self, Error> {
67 check_hash(manifest, MANIFEST_SHA, "manifest")?;
70 let manifest =
71 std::str::from_utf8(manifest).map_err(|_| Error::AssetIntegrity("manifest"))?;
72 for expected in [
73 "google/embeddinggemma-2",
74 "914f7f89142e33e77833254d9c9b90c3cef7303b",
75 anchor_sha,
76 table_sha,
77 ] {
78 if !manifest.contains(expected) {
79 return Err(Error::AssetIntegrity("manifest metadata"));
80 }
81 }
82 check_hash(anchor, anchor_sha, "anchor")?;
83 check_hash(table, table_sha, "calibration")?;
84 check_len(anchor.len(), dim * 4)?;
85 check_len(table.len(), dim * 8)?;
86 let floats = |bytes: &[u8]| {
87 bytes
88 .as_chunks::<4>()
89 .0
90 .iter()
91 .map(|b| f32::from_le_bytes(*b))
92 .collect::<Vec<_>>()
93 };
94 let anchor = floats(anchor);
95 let offset = floats(&table[..dim * 4]);
96 let scale = floats(&table[dim * 4..]);
97 if anchor
98 .iter()
99 .chain(&offset)
100 .chain(&scale)
101 .any(|x| !x.is_finite())
102 || scale.iter().any(|&x| x <= 0.0)
103 {
104 return Err(Error::AssetIntegrity(
105 "non-finite values or nonpositive scale",
106 ));
107 }
108 Ok(Self {
109 anchor,
110 offset,
111 scale,
112 })
113 }
114
115 pub fn dim(&self) -> usize {
116 self.anchor.len()
117 }
118
119 pub fn encode(&self, embedding: &Embedding) -> Result<Vec<u8>, Error> {
122 check_embedding(embedding, self.dim())?;
123 Ok(embedding
124 .as_slice()
125 .as_chunks::<2>()
126 .0
127 .iter()
128 .enumerate()
129 .map(|(j, pair)| {
130 let code = |i: usize, x: f32| {
131 let centered = (x - self.anchor[i])
132 .clamp(self.offset[i], self.offset[i] + 15.0 * self.scale[i]);
133 ((centered - self.offset[i]) / self.scale[i])
134 .round_ties_even()
135 .clamp(0.0, 15.0) as u8
136 };
137 (code(2 * j, pair[0]) << 4) | code(2 * j + 1, pair[1])
138 })
139 .collect())
140 }
141}
142
143pub struct QueryLut {
146 bias: f32,
147 pairs: Vec<[f32; 256]>,
148}
149
150impl QueryLut {
151 pub fn new(query: &Embedding) -> Result<Self, Error> {
154 let codec = PhotoAnchorQ4::new(query.dim())?;
155 check_embedding(query, codec.dim())?;
156 let q = query.as_slice();
157 let bias = (0..codec.dim())
158 .map(|i| (codec.anchor[i] + codec.offset[i]) * q[i])
159 .sum();
160 let pairs = (0..codec.dim())
161 .step_by(2)
162 .map(|i| {
163 let (hi, lo) = (codec.scale[i] * q[i], codec.scale[i + 1] * q[i + 1]);
164 std::array::from_fn(|byte| (byte >> 4) as f32 * hi + (byte & 15) as f32 * lo)
165 })
166 .collect();
167 Ok(Self { bias, pairs })
168 }
169
170 pub fn score(&self, codes: &[u8]) -> Result<f32, Error> {
172 check_len(codes.len(), self.pairs.len())?;
173 Ok(self.bias
174 + self
175 .pairs
176 .iter()
177 .zip(codes)
178 .map(|(table, &code)| table[code as usize])
179 .sum::<f32>())
180 }
181}
182
183fn check_len(actual: usize, expected: usize) -> Result<(), Error> {
184 if actual == expected {
185 Ok(())
186 } else {
187 Err(Error::Length { expected, actual })
188 }
189}
190
191fn check_embedding(embedding: &Embedding, dim: usize) -> Result<(), Error> {
192 check_len(embedding.dim(), dim)?;
193 if embedding.normalized() {
194 Ok(())
195 } else {
196 Err(Error::NonUnitEmbedding)
197 }
198}
199
200fn check_hash(bytes: &[u8], expected: &str, name: &'static str) -> Result<(), Error> {
201 if sha256::hex(bytes) == expected {
202 Ok(())
203 } else {
204 Err(Error::AssetIntegrity(name))
205 }
206}