structured_zstd/dictionary/
fastcover.rs1use alloc::collections::BTreeSet;
2use alloc::vec;
3use alloc::vec::Vec;
4
5#[derive(Debug, Clone, Copy)]
6pub struct FastCoverParams {
7 pub k: usize,
8 pub d: usize,
9 pub f: u32,
10 pub accel: usize,
11}
12
13#[derive(Debug, Clone, Copy)]
14pub struct FastCoverTuned {
15 pub k: usize,
16 pub d: usize,
17 pub f: u32,
18 pub accel: usize,
19 pub score: usize,
20}
21
22pub const DEFAULT_K_CANDIDATES: &[usize] = &[64, 128, 256, 512, 1024, 2048];
23pub const DEFAULT_D_CANDIDATES: &[usize] = &[6, 8, 12, 16];
24pub const DEFAULT_F_CANDIDATES: &[u32] = &[16, 18, 20];
25
26const PRIME_4_BYTES: u32 = 2_654_435_761;
30const PRIME_5_BYTES: u64 = 889_523_592_379;
31const PRIME_6_BYTES: u64 = 227_718_039_650_203;
32const PRIME_7_BYTES: u64 = 58_295_818_150_454_627;
33const PRIME_8_BYTES: u64 = 0xCF1B_BCDC_B7A5_6463;
34
35#[inline]
39fn dmer_read_len(d: usize) -> usize {
40 d.max(8)
41}
42
43#[inline]
47fn hash_dmer_index(sample: &[u8], pos: usize, f: u32, d: usize) -> usize {
48 if d.min(8) == 4 {
49 let v = u32::from_le_bytes(sample[pos..pos + 4].try_into().unwrap());
50 return (v.wrapping_mul(PRIME_4_BYTES) >> (32 - f)) as usize;
51 }
52 let v = u64::from_le_bytes(sample[pos..pos + 8].try_into().unwrap());
53 let h = match d.min(8) {
54 5 => (v << 24).wrapping_mul(PRIME_5_BYTES),
55 6 => (v << 16).wrapping_mul(PRIME_6_BYTES),
56 7 => (v << 8).wrapping_mul(PRIME_7_BYTES),
57 _ => v.wrapping_mul(PRIME_8_BYTES),
58 };
59 (h >> (64 - f)) as usize
60}
61
62fn clamp_table_bits(f: u32) -> u32 {
67 f.clamp(1, 31)
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub(crate) struct TableTooLarge {
74 pub(crate) entries: usize,
75}
76
77fn zeroed_counts<C: WindowCount>(len: usize) -> Result<Vec<C>, TableTooLarge> {
82 let too_large = TableTooLarge { entries: len };
83 let layout = core::alloc::Layout::array::<C>(len).map_err(|_| too_large)?;
84 if layout.size() == 0 {
85 return Ok(Vec::new());
86 }
87 let pointer = unsafe { alloc::alloc::alloc_zeroed(layout) };
89 if pointer.is_null() {
90 return Err(too_large);
91 }
92 Ok(unsafe { Vec::from_raw_parts(pointer.cast::<C>(), len, len) })
97}
98
99pub(crate) fn normalize_fastcover_params(mut params: FastCoverParams) -> FastCoverParams {
100 params.d = params.d.clamp(4, 32);
101 params.k = params.k.max(params.d).max(16);
102 params.f = clamp_table_bits(params.f);
103 params.accel = params.accel.clamp(1, 10);
104 params
105}
106
107fn build_frequency_table(
108 sample: &[u8],
109 d: usize,
110 f: u32,
111 accel: usize,
112) -> Result<Vec<u32>, TableTooLarge> {
113 let bits = clamp_table_bits(f);
114 let size = 1usize << bits;
115 let step = accel.max(1);
118 let mut table = zeroed_counts::<u32>(size)?;
119
120 let read_len = dmer_read_len(d);
121 if sample.len() < read_len {
122 return Ok(table);
123 }
124
125 let mut i = 0usize;
126 while i + read_len <= sample.len() {
127 table[hash_dmer_index(sample, i, bits, d)] += 1;
130 i += step;
131 }
132 Ok(table)
133}
134
135fn build_raw_dict(
136 sample: &[u8],
137 dict_size: usize,
138 params: FastCoverParams,
139) -> Result<Vec<u8>, TableTooLarge> {
140 if sample.is_empty() || dict_size == 0 {
141 return Ok(Vec::new());
142 }
143
144 let params = normalize_fastcover_params(params);
145 let k = params.k;
146 let d = params.d;
147 let f = clamp_table_bits(params.f);
148 let read_len = dmer_read_len(d);
149 if sample.len() < read_len {
150 return Ok(Vec::new());
153 }
154
155 let nb_dmers = sample.len() - read_len + 1;
167 let mut freqs = build_frequency_table(sample, d, f, params.accel)?;
168 let dmers_in_k = k - d + 1; let min_epoch_size = k
176 .checked_mul(10)
177 .map_or(nb_dmers, |floor| floor.min(nb_dmers));
178 let mut epoch_count = (dict_size / k).max(1);
179 let mut epoch_size = nb_dmers / epoch_count;
180 if epoch_size < min_epoch_size {
181 epoch_size = min_epoch_size;
182 epoch_count = (nb_dmers / epoch_size).max(1);
183 }
184
185 let layout = EpochLayout {
186 dmers_in_k,
187 epoch_size,
188 epoch_count,
189 };
190 if dmers_in_k < usize::from(u16::MAX) {
194 select_segments::<u16>(sample, dict_size, f, d, &mut freqs, layout)
195 } else {
196 select_segments::<u32>(sample, dict_size, f, d, &mut freqs, layout)
197 }
198}
199
200#[derive(Clone, Copy)]
203struct EpochLayout {
204 dmers_in_k: usize,
205 epoch_size: usize,
206 epoch_count: usize,
207}
208
209trait WindowCount: Copy + PartialEq + core::ops::AddAssign + core::ops::SubAssign + From<u8> {}
211impl WindowCount for u16 {}
212impl WindowCount for u32 {}
213
214fn select_segments<C: WindowCount>(
217 sample: &[u8],
218 dict_size: usize,
219 f: u32,
220 d: usize,
221 freqs: &mut [u32],
222 layout: EpochLayout,
223) -> Result<Vec<u8>, TableTooLarge> {
224 let EpochLayout {
225 dmers_in_k,
226 epoch_size,
227 epoch_count,
228 } = layout;
229 let zero = C::from(0);
230 let one = C::from(1);
231 let mut segment_freqs = zeroed_counts::<C>(1usize << f)?;
233 let mut out = vec![0u8; dict_size];
236 let mut tail = dict_size;
237 const MAX_ZERO_SCORE_RUN: usize = 10;
238 let mut zero_score_run = 0usize;
239 let mut epoch = 0usize;
240
241 while tail > 0 {
242 let epoch_begin = epoch * epoch_size;
243 let epoch_end = epoch_begin + epoch_size;
244 epoch = (epoch + 1) % epoch_count;
245
246 let mut best_begin = 0usize;
249 let mut best_end = 0usize;
250 let mut best_score = 0u64;
251 let mut active_begin = epoch_begin;
252 let mut active_end = epoch_begin;
253 let mut active_score = 0u64;
254 while active_end < epoch_end {
255 let idx = hash_dmer_index(sample, active_end, f, d);
256 if segment_freqs[idx] == zero {
257 active_score += u64::from(freqs[idx]);
258 }
259 active_end += 1;
260 segment_freqs[idx] += one;
261 if active_end - active_begin == dmers_in_k + 1 {
262 let del = hash_dmer_index(sample, active_begin, f, d);
263 segment_freqs[del] -= one;
264 if segment_freqs[del] == zero {
265 active_score -= u64::from(freqs[del]);
266 }
267 active_begin += 1;
268 }
269 if active_score > best_score {
270 best_begin = active_begin;
271 best_end = active_end;
272 best_score = active_score;
273 }
274 }
275 while active_begin < epoch_end {
277 let del = hash_dmer_index(sample, active_begin, f, d);
278 segment_freqs[del] -= one;
279 active_begin += 1;
280 }
281 for pos in best_begin..best_end {
283 freqs[hash_dmer_index(sample, pos, f, d)] = 0;
284 }
285
286 if best_score == 0 {
287 zero_score_run += 1;
290 if zero_score_run >= MAX_ZERO_SCORE_RUN {
291 break;
292 }
293 continue;
294 }
295 zero_score_run = 0;
296
297 let segment_size = (best_end - best_begin + d - 1).min(tail);
298 if segment_size < d {
299 break;
300 }
301 tail -= segment_size;
302 out[tail..tail + segment_size]
303 .copy_from_slice(&sample[best_begin..best_begin + segment_size]);
304 }
305
306 out.drain(..tail);
307 Ok(out)
308}
309
310fn coverage_score(dict: &[u8], eval: &[u8], d: usize, accel: usize) -> usize {
311 let read_len = dmer_read_len(d);
312 if dict.len() < read_len || eval.len() < read_len || d == 0 {
313 return 0;
314 }
315 const COVERAGE_F: u32 = 20;
316 let mut seen = BTreeSet::new();
317 for i in 0..=(dict.len() - read_len) {
318 seen.insert(hash_dmer_index(dict, i, COVERAGE_F, d));
319 }
320
321 let mut hits = 0usize;
322 let step = accel.max(1);
323 let mut i = 0usize;
324 while i + read_len <= eval.len() {
325 if seen.contains(&hash_dmer_index(eval, i, COVERAGE_F, d)) {
326 hits += 1;
327 }
328 i += step;
329 }
330 hits
331}
332
333pub fn train_fastcover_raw(
334 sample: &[u8],
335 dict_size: usize,
336 params: FastCoverParams,
337) -> Result<Vec<u8>, TableTooLarge> {
338 build_raw_dict(sample, dict_size, params)
339}
340
341pub fn optimize_fastcover_raw(
342 sample: &[u8],
343 dict_size: usize,
344 split_point: f64,
345 accel: usize,
346 d_candidates: &[usize],
347 f_candidates: &[u32],
348 k_values: &[usize],
349) -> Result<(Vec<u8>, FastCoverTuned), TableTooLarge> {
350 let d_values = if d_candidates.is_empty() {
351 DEFAULT_D_CANDIDATES
352 } else {
353 d_candidates
354 };
355 let f_values = if f_candidates.is_empty() {
356 DEFAULT_F_CANDIDATES
357 } else {
358 f_candidates
359 };
360 let k_candidates = if k_values.is_empty() {
361 DEFAULT_K_CANDIDATES
362 } else {
363 k_values
364 };
365
366 if sample.len() < 2 {
367 let params = normalize_fastcover_params(FastCoverParams {
368 k: k_candidates[0],
369 d: d_values[0],
370 f: f_values[0],
371 accel,
372 });
373 let mut dict = build_raw_dict(sample, dict_size, params)?;
374 if dict.is_empty() && dict_size > 0 {
375 let take = sample.len().min(dict_size);
376 dict.extend_from_slice(&sample[..take]);
377 }
378 return Ok((
379 dict,
380 FastCoverTuned {
381 k: params.k,
382 d: params.d,
383 f: params.f,
384 accel: params.accel,
385 score: 0,
386 },
387 ));
388 }
389
390 let (train, eval) = if split_point >= 1.0 {
395 (sample, sample)
396 } else {
397 let split = if split_point > 0.0 { split_point } else { 0.75 };
398 let split_idx = ((sample.len() as f64) * split) as usize;
399 sample.split_at(split_idx.clamp(1, sample.len() - 1))
400 };
401
402 let mut best_dict = Vec::new();
403 let mut best = FastCoverTuned {
404 k: 0,
405 d: 0,
406 f: 0,
407 accel: accel.clamp(1, 10),
408 score: 0,
409 };
410
411 for &f in f_values {
412 for &d in d_values {
413 for &k in k_candidates {
414 let params = normalize_fastcover_params(FastCoverParams { k, d, f, accel });
415 let dict = build_raw_dict(train, dict_size, params)?;
416 let score = coverage_score(dict.as_slice(), eval, params.d, params.accel);
417 if best_dict.is_empty() || score > best.score {
418 best.score = score;
419 best.k = params.k;
420 best.d = params.d;
421 best.f = params.f;
422 best.accel = params.accel;
423 best_dict = dict;
424 }
425 }
426 }
427 }
428
429 Ok((best_dict, best))
430}
431
432#[cfg(test)]
433mod tests;