Skip to main content

presk_data/
lib.rs

1//! The `.psk.d` data container and the training data pipeline.
2//!
3//! A `.psk.d` file is a sequence of *samples* of one Presk type:
4//!
5//! ```text
6//! magic     "PSKD"                       4 bytes
7//! version   u16 = 1                      little-endian
8//! type_len  u32, then the type, UTF-8    the sample type in Presk syntax
9//! count     u64                          number of samples
10//! data      the samples, back to back: the leaves of the type in order,
11//!           idx[N] as the smallest of u8/u16/u32/u64 holding N−1,
12//!           ℝ as f64 little-endian
13//! ```
14//!
15//! Version 2 (plan 0.17.0 step 7, D180) adds, after the count, one byte per
16//! leaf in storage order, the leaf's *encoding*:
17//!
18//! ```text
19//! leaves    u32                          number of leaves of the type
20//! encodings u8 per leaf                  0 f64 (and every idx), 1 f32, 2 bf16
21//! ```
22//!
23//! and stores each real leaf in its encoding, little-endian (`bf16` as the
24//! upper sixteen bits of an `f32`). Every number of `f32` and of `bf16` is
25//! a number of `f64`, so a leaf is read exactly whatever its encoding. A
26//! writer writes version 1 unless a leaf is narrower, so that a file of
27//! `f64` leaves is the same bytes as before.
28//!
29//! Real values are `f64` and are injected exactly into ℝ when read (the
30//! hyperparameter rule of the language). The type is parsed with the Presk
31//! parser; a *closed* type (every dimension a literal) gives the leaf layout
32//! directly. A `params M` type needs the program to know its layout: the
33//! reader then returns the header only, and the caller supplies the
34//! [`Layout`] with [`Reader::with_layout`].
35//!
36//! Errors carry a code: `E9001` (type not closed or not a data type),
37//! `E9002` (not a `.psk.d` file), `E9003` (truncated), `E9004` (a value out
38//! of its bound or unparsable), `E9005` (type mismatch).
39
40use std::fs::File;
41use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
42use std::path::Path;
43
44use presk_syntax::ast::{self, AxisSpec, Expr, Lit};
45use presk_syntax::{SourceMap, parse_type};
46use rand_chacha::ChaCha20Rng;
47use rand_core::{RngCore, SeedableRng};
48
49pub const MAGIC: &[u8; 4] = b"PSKD";
50/// The version a file of `f64` leaves is written in.
51pub const VERSION: u16 = 1;
52/// The version of a file with a leaf narrower than `f64`.
53pub const VERSION_ENCODED: u16 = 2;
54
55/// How a real leaf's elements are stored (version 2). Each is exactly a
56/// number of `f64`.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum Enc {
59    F64,
60    F32,
61    Bf16,
62}
63
64impl Enc {
65    pub fn code(self) -> u8 {
66        match self {
67            Enc::F64 => 0,
68            Enc::F32 => 1,
69            Enc::Bf16 => 2,
70        }
71    }
72
73    pub fn of_code(c: u8) -> Option<Enc> {
74        Some(match c {
75            0 => Enc::F64,
76            1 => Enc::F32,
77            2 => Enc::Bf16,
78            _ => return None,
79        })
80    }
81
82    pub fn name(self) -> &'static str {
83        match self {
84            Enc::F64 => "f64",
85            Enc::F32 => "f32",
86            Enc::Bf16 => "bf16",
87        }
88    }
89
90    /// Bytes of one real element.
91    pub fn width(self) -> usize {
92        match self {
93            Enc::F64 => 8,
94            Enc::F32 => 4,
95            Enc::Bf16 => 2,
96        }
97    }
98
99    /// The bytes of `x`, which must be a number of the encoding exactly.
100    fn encode(self, x: f64, out: &mut Vec<u8>) -> Result<()> {
101        match self {
102            Enc::F64 => out.extend_from_slice(&x.to_le_bytes()),
103            Enc::F32 => {
104                let y = x as f32;
105                if f64::from(y).to_bits() != x.to_bits() && !(x.is_nan() && y.is_nan()) {
106                    return err("E9004", format!("{x} is not a number of f32"));
107                }
108                out.extend_from_slice(&y.to_le_bytes());
109            }
110            Enc::Bf16 => {
111                let y = x as f32;
112                let bits = y.to_bits();
113                if f64::from(y).to_bits() != x.to_bits() && !(x.is_nan() && y.is_nan()) || bits & 0xffff != 0 {
114                    return err("E9004", format!("{x} is not a number of bf16"));
115                }
116                out.extend_from_slice(&((bits >> 16) as u16).to_le_bytes());
117            }
118        }
119        Ok(())
120    }
121
122    /// The `f64` that the bytes at `b` hold.
123    fn decode(self, b: &[u8]) -> f64 {
124        match self {
125            Enc::F64 => f64::from_le_bytes(b[..8].try_into().unwrap()),
126            Enc::F32 => f64::from(f32::from_le_bytes(b[..4].try_into().unwrap())),
127            Enc::Bf16 => f64::from(f32::from_bits(u32::from(u16::from_le_bytes(b[..2].try_into().unwrap())) << 16)),
128        }
129    }
130}
131
132#[derive(Clone, Debug)]
133pub struct DataError {
134    pub code: &'static str,
135    pub message: String,
136}
137
138impl std::fmt::Display for DataError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        write!(f, "[{}] {}", self.code, self.message)
141    }
142}
143
144impl std::error::Error for DataError {}
145
146pub type Result<T> = std::result::Result<T, DataError>;
147
148fn err<T>(code: &'static str, message: impl Into<String>) -> Result<T> {
149    Err(DataError { code, message: message.into() })
150}
151
152fn io(e: std::io::Error) -> DataError {
153    DataError { code: "E9003", message: e.to_string() }
154}
155
156// ----- layout ----------------------------------------------------------
157
158/// One tensor leaf of a sample type.
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub enum Leaf {
161    /// `idx[bound][shape]`
162    Idx { bound: u64, shape: Vec<usize> },
163    /// `ℝ[shape]`
164    Real { shape: Vec<usize> },
165}
166
167impl Leaf {
168    pub fn numel(&self) -> usize {
169        match self {
170            Leaf::Idx { shape, .. } | Leaf::Real { shape } => shape.iter().product(),
171        }
172    }
173
174    /// Bytes of one element on disk.
175    pub fn width(&self) -> usize {
176        match self {
177            Leaf::Idx { bound, .. } => idx_width(*bound),
178            Leaf::Real { .. } => 8,
179        }
180    }
181
182    pub fn bytes(&self) -> usize {
183        self.numel() * self.width()
184    }
185}
186
187/// The width of an `idx[N]` element: the smallest unsigned integer holding `N−1`.
188pub fn idx_width(bound: u64) -> usize {
189    match bound.saturating_sub(1) {
190        n if n <= u8::MAX as u64 => 1,
191        n if n <= u16::MAX as u64 => 2,
192        n if n <= u32::MAX as u64 => 4,
193        _ => 8,
194    }
195}
196
197/// The leaf structure of a sample type, in the order the leaves are stored.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub enum Layout {
200    Leaf(Leaf),
201    Tuple(Vec<Layout>),
202    /// A parameter tree (`params M`): named fields in declaration order.
203    Record(Vec<(String, Layout)>),
204}
205
206impl Layout {
207    /// The layout of a closed data type written in Presk syntax.
208    pub fn from_type_text(text: &str) -> Result<Layout> {
209        let mut sm = SourceMap::new();
210        let id = sm.add("<type>", text);
211        let (ty, diags) = parse_type(&sm, id);
212        if let Some(d) = diags.iter().find(|d| d.is_error()) {
213            return err("E9001", format!("`{text}` is not a type: {}", d.message));
214        }
215        let Some(ty) = ty else { return err("E9001", format!("`{text}` is not a type")) };
216        Self::from_ast(&ty, text)
217    }
218
219    fn from_ast(ty: &ast::Ty, text: &str) -> Result<Layout> {
220        match ty {
221            ast::Ty::Real { axes, .. } => Ok(Layout::Leaf(Leaf::Real { shape: Self::shape(axes, text)? })),
222            ast::Ty::Idx { bound, axes, .. } => {
223                let bound = Self::nat(bound, text)?;
224                if bound == 0 {
225                    return err("E9001", format!("`{text}`: an `idx[0]` has no values"));
226                }
227                Ok(Layout::Leaf(Leaf::Idx { bound, shape: Self::shape(axes, text)? }))
228            }
229            // A mask is stored as indices below 2, false 0 and true 1 (plan
230            // 0.10.0 step 7b, D99).
231            ast::Ty::Bool { axes, .. } => Ok(Layout::Leaf(Leaf::Idx { bound: 2, shape: Self::shape(axes, text)? })),
232            ast::Ty::Param { inner, .. } => Self::from_ast(inner, text),
233            ast::Ty::Tuple { elems, .. } => Ok(Layout::Tuple(elems.iter().map(|t| Self::from_ast(t, text)).collect::<Result<_>>()?)),
234            ast::Ty::Params { module, .. } => {
235                err("E9001", format!("`{text}`: the layout of `params {}` needs the program (open the file from `presk train`)", module.dotted()))
236            }
237            ast::Ty::Named { path, .. } => err("E9001", format!("`{text}`: `{}` is not a data type (only `ℝ[…]`, `idx[N][…]`, `bool[…]` and tuples of them, with literal dimensions)", path.dotted())),
238        }
239    }
240
241    fn shape(axes: &[AxisSpec], text: &str) -> Result<Vec<usize>> {
242        axes.iter()
243            .map(|a| match a {
244                AxisSpec::Axis { dim, .. } => Self::nat(dim, text).map(|n| n as usize),
245                AxisSpec::Ellipsis { .. } => err("E9001", format!("`{text}`: a sample type cannot have an open prefix `...`")),
246            })
247            .collect()
248    }
249
250    fn nat(e: &Expr, text: &str) -> Result<u64> {
251        match e {
252            Expr::Lit { lit: Lit::Nat(n), .. } => n.parse().map_err(|_| DataError { code: "E9001", message: format!("`{text}`: `{n}` is too large") }),
253            _ => err("E9001", format!("`{text}`: every dimension of a sample type must be a literal (a `.psk.d` holds concrete tensors)")),
254        }
255    }
256
257    /// The leaves, in storage order.
258    pub fn leaves(&self) -> Vec<&Leaf> {
259        let mut out = Vec::new();
260        self.collect(&mut out);
261        out
262    }
263
264    fn collect<'a>(&'a self, out: &mut Vec<&'a Leaf>) {
265        match self {
266            Layout::Leaf(l) => out.push(l),
267            Layout::Tuple(ts) => ts.iter().for_each(|t| t.collect(out)),
268            Layout::Record(fs) => fs.iter().for_each(|(_, t)| t.collect(out)),
269        }
270    }
271
272    /// Bytes of one sample on disk, every real leaf in `f64`.
273    pub fn sample_bytes(&self) -> usize {
274        self.leaves().iter().map(|l| l.bytes()).sum()
275    }
276
277    /// Bytes of one sample on disk, each leaf in its encoding (`encs` in
278    /// storage order; empty: every real leaf in `f64`, as version 1 has it).
279    pub fn sample_bytes_enc(&self, encs: &[Enc]) -> usize {
280        self.leaves().iter().enumerate().map(|(i, l)| leaf_bytes(l, encs.get(i).copied().unwrap_or(Enc::F64))).sum()
281    }
282}
283
284fn leaf_bytes(l: &Leaf, e: Enc) -> usize {
285    match l {
286        Leaf::Real { .. } => l.numel() * e.width(),
287        Leaf::Idx { .. } => l.bytes(),
288    }
289}
290
291// ----- values ------------------------------------------------------------
292
293/// One sample: the same tree as its [`Layout`], with flat row-major data per leaf.
294#[derive(Clone, Debug, PartialEq)]
295pub enum Value {
296    Idx(Vec<u64>),
297    Real(Vec<f64>),
298    Tuple(Vec<Value>),
299    Record(Vec<(String, Value)>),
300}
301
302impl Value {
303    fn leaves(&self) -> Vec<&Value> {
304        let mut out = Vec::new();
305        self.collect(&mut out);
306        out
307    }
308
309    fn collect<'a>(&'a self, out: &mut Vec<&'a Value>) {
310        match self {
311            Value::Idx(_) | Value::Real(_) => out.push(self),
312            Value::Tuple(ts) => ts.iter().for_each(|t| t.collect(out)),
313            Value::Record(fs) => fs.iter().for_each(|(_, t)| t.collect(out)),
314        }
315    }
316
317    /// Checks that the value has the layout's structure, counts and bounds.
318    pub fn check(&self, layout: &Layout) -> Result<()> {
319        let ls = layout.leaves();
320        let vs = self.leaves();
321        if ls.len() != vs.len() {
322            return err("E9005", format!("sample has {} leaves, the type has {}", vs.len(), ls.len()));
323        }
324        for (i, (l, v)) in ls.iter().zip(&vs).enumerate() {
325            match (l, v) {
326                (Leaf::Idx { bound, .. }, Value::Idx(d)) => {
327                    if d.len() != l.numel() {
328                        return err("E9005", format!("leaf {i}: {} values, {} expected", d.len(), l.numel()));
329                    }
330                    if let Some(x) = d.iter().find(|x| **x >= *bound) {
331                        return err("E9004", format!("leaf {i}: index {x} is not below the bound {bound}"));
332                    }
333                }
334                (Leaf::Real { .. }, Value::Real(d)) => {
335                    if d.len() != l.numel() {
336                        return err("E9005", format!("leaf {i}: {} values, {} expected", d.len(), l.numel()));
337                    }
338                }
339                (Leaf::Idx { .. }, _) => return err("E9005", format!("leaf {i}: indices expected, reals found")),
340                (Leaf::Real { .. }, _) => return err("E9005", format!("leaf {i}: reals expected, indices found")),
341            }
342        }
343        Ok(())
344    }
345
346    /// Parses one sample from whitespace-separated values, leaves in order.
347    pub fn parse_text(layout: &Layout, line: &str) -> Result<Value> {
348        let mut toks = line.split_whitespace();
349        let v = Self::parse_tokens(layout, &mut toks)?;
350        if let Some(extra) = toks.next() {
351            return err("E9004", format!("extra value `{extra}` after a complete sample"));
352        }
353        Ok(v)
354    }
355
356    fn parse_tokens<'a>(layout: &Layout, toks: &mut impl Iterator<Item = &'a str>) -> Result<Value> {
357        match layout {
358            Layout::Leaf(Leaf::Idx { bound, shape }) => {
359                let n: usize = shape.iter().product();
360                let mut d = Vec::with_capacity(n);
361                for _ in 0..n {
362                    let t = toks.next().ok_or_else(|| DataError { code: "E9004", message: "sample ends before all values are given".into() })?;
363                    let x: u64 = t.parse().map_err(|_| DataError { code: "E9004", message: format!("`{t}` is not an index") })?;
364                    if x >= *bound {
365                        return err("E9004", format!("index {x} is not below the bound {bound}"));
366                    }
367                    d.push(x);
368                }
369                Ok(Value::Idx(d))
370            }
371            Layout::Leaf(Leaf::Real { shape }) => {
372                let n: usize = shape.iter().product();
373                let mut d = Vec::with_capacity(n);
374                for _ in 0..n {
375                    let t = toks.next().ok_or_else(|| DataError { code: "E9004", message: "sample ends before all values are given".into() })?;
376                    let x: f64 = t.parse().map_err(|_| DataError { code: "E9004", message: format!("`{t}` is not a real") })?;
377                    d.push(x);
378                }
379                Ok(Value::Real(d))
380            }
381            Layout::Tuple(ts) => Ok(Value::Tuple(ts.iter().map(|t| Self::parse_tokens(t, toks)).collect::<Result<_>>()?)),
382            Layout::Record(fs) => Ok(Value::Record(fs.iter().map(|(n, t)| Self::parse_tokens(t, toks).map(|v| (n.clone(), v))).collect::<Result<_>>()?)),
383        }
384    }
385
386    /// Nested JSON: tensors as nested lists, tuples as lists, records as objects.
387    pub fn to_json(&self, layout: &Layout) -> serde_json::Value {
388        match (self, layout) {
389            (Value::Idx(d), Layout::Leaf(Leaf::Idx { shape, .. })) => nest(shape, &d.iter().map(|x| serde_json::json!(x)).collect::<Vec<_>>()),
390            (Value::Real(d), Layout::Leaf(Leaf::Real { shape })) => nest(shape, &d.iter().map(|x| serde_json::json!(x)).collect::<Vec<_>>()),
391            (Value::Tuple(vs), Layout::Tuple(ts)) => serde_json::Value::Array(vs.iter().zip(ts).map(|(v, t)| v.to_json(t)).collect()),
392            (Value::Record(fs), Layout::Record(ls)) => serde_json::Value::Object(fs.iter().zip(ls).map(|((n, v), (_, t))| (n.clone(), v.to_json(t))).collect()),
393            _ => serde_json::Value::Null,
394        }
395    }
396
397    /// One-line rendering for `presk data info`.
398    pub fn render(&self, layout: &Layout) -> String {
399        match (self, layout) {
400            (Value::Idx(d), Layout::Leaf(Leaf::Idx { shape, .. })) => nest_text(shape, &d.iter().map(|x| x.to_string()).collect::<Vec<_>>()),
401            (Value::Real(d), Layout::Leaf(Leaf::Real { shape })) => nest_text(shape, &d.iter().map(|x| format!("{x}")).collect::<Vec<_>>()),
402            (Value::Tuple(vs), Layout::Tuple(ts)) => format!("({})", vs.iter().zip(ts).map(|(v, t)| v.render(t)).collect::<Vec<_>>().join(", ")),
403            (Value::Record(fs), Layout::Record(ls)) => format!("{{{}}}", fs.iter().zip(ls).map(|((n, v), (_, t))| format!("{n} = {}", v.render(t))).collect::<Vec<_>>().join(", ")),
404            _ => "?".into(),
405        }
406    }
407}
408
409fn nest(shape: &[usize], flat: &[serde_json::Value]) -> serde_json::Value {
410    match shape {
411        [] => flat.first().cloned().unwrap_or(serde_json::Value::Null),
412        [_] => serde_json::Value::Array(flat.to_vec()),
413        [n, rest @ ..] => {
414            let chunk = flat.len() / n.max(&1);
415            serde_json::Value::Array((0..*n).map(|i| nest(rest, &flat[i * chunk..(i + 1) * chunk])).collect())
416        }
417    }
418}
419
420fn nest_text(shape: &[usize], flat: &[String]) -> String {
421    match shape {
422        [] => flat.first().cloned().unwrap_or_default(),
423        [_] => format!("[{}]", flat.join(", ")),
424        [n, rest @ ..] => {
425            let chunk = flat.len() / n.max(&1);
426            format!("[{}]", (0..*n).map(|i| nest_text(rest, &flat[i * chunk..(i + 1) * chunk])).collect::<Vec<_>>().join(", "))
427        }
428    }
429}
430
431// ----- files -------------------------------------------------------------
432
433#[derive(Clone, Debug, PartialEq, Eq)]
434pub struct Header {
435    pub version: u16,
436    /// The sample type, in Presk syntax.
437    pub sample_ty: String,
438    pub count: u64,
439    /// Version 2: each leaf's encoding, in storage order. Empty in a
440    /// version 1 file, whose real leaves are all `f64`.
441    pub encodings: Vec<Enc>,
442}
443
444impl Header {
445    /// Bytes of the header on disk.
446    #[allow(clippy::len_without_is_empty)]
447    pub fn len(&self) -> u64 {
448        let v2 = if self.version == VERSION_ENCODED { 4 + self.encodings.len() } else { 0 };
449        (4 + 2 + 4 + self.sample_ty.len() + 8 + v2) as u64
450    }
451
452    /// The encoding of leaf `i`.
453    pub fn encoding(&self, i: usize) -> Enc {
454        self.encodings.get(i).copied().unwrap_or(Enc::F64)
455    }
456}
457
458/// Writes a `.psk.d` file: `sample_ty` is the header text, `layout` its
459/// leaf structure (needed for `params M`, whose layout the text does not give).
460pub fn write<'a>(path: &Path, sample_ty: &str, layout: &Layout, samples: impl IntoIterator<Item = &'a Value>) -> Result<u64> {
461    write_encoded(path, sample_ty, layout, &[], samples)
462}
463
464/// [`write`], each real leaf in its encoding (`encs` in storage order,
465/// empty for every leaf in `f64`). The file is version 2 when a real leaf
466/// is narrower than `f64`, and version 1 otherwise, byte for byte what
467/// [`write`] writes. A value that is not a number of its leaf's encoding is
468/// refused (`E9004`): the file holds the numbers it is given, exactly.
469pub fn write_encoded<'a>(path: &Path, sample_ty: &str, layout: &Layout, encs: &[Enc], samples: impl IntoIterator<Item = &'a Value>) -> Result<u64> {
470    let leaves = layout.leaves();
471    if !encs.is_empty() && encs.len() != leaves.len() {
472        return err("E9005", format!("{} encodings for {} leaves", encs.len(), leaves.len()));
473    }
474    let enc = |i: usize| encs.get(i).copied().unwrap_or(Enc::F64);
475    let narrow = leaves.iter().enumerate().any(|(i, l)| matches!(l, Leaf::Real { .. }) && enc(i) != Enc::F64);
476    if let Some((i, _)) = leaves.iter().enumerate().find(|(i, l)| matches!(l, Leaf::Idx { .. }) && enc(*i) != Enc::F64) {
477        return err("E9005", format!("leaf {i} holds indices, which have no real encoding"));
478    }
479    let f = File::create(path).map_err(io)?;
480    let mut w = BufWriter::new(f);
481    w.write_all(MAGIC).map_err(io)?;
482    w.write_all(&(if narrow { VERSION_ENCODED } else { VERSION }).to_le_bytes()).map_err(io)?;
483    w.write_all(&(sample_ty.len() as u32).to_le_bytes()).map_err(io)?;
484    w.write_all(sample_ty.as_bytes()).map_err(io)?;
485    let count_pos = 4 + 2 + 4 + sample_ty.len() as u64;
486    w.write_all(&0u64.to_le_bytes()).map_err(io)?;
487    if narrow {
488        w.write_all(&(leaves.len() as u32).to_le_bytes()).map_err(io)?;
489        let codes: Vec<u8> = (0..leaves.len()).map(|i| enc(i).code()).collect();
490        w.write_all(&codes).map_err(io)?;
491    }
492    let mut count = 0u64;
493    for s in samples {
494        s.check(layout)?;
495        for (li, (leaf, v)) in leaves.iter().zip(s.leaves()).enumerate() {
496            match (leaf, v) {
497                (Leaf::Idx { .. }, Value::Idx(d)) => {
498                    for x in d {
499                        let b = x.to_le_bytes();
500                        w.write_all(&b[..leaf.width()]).map_err(io)?;
501                    }
502                }
503                (Leaf::Real { .. }, Value::Real(d)) => {
504                    let e = enc(li);
505                    let mut bytes = Vec::with_capacity(d.len() * e.width());
506                    for x in d {
507                        e.encode(*x, &mut bytes).map_err(|m| DataError { code: m.code, message: format!("leaf {li}: {}", m.message) })?;
508                    }
509                    w.write_all(&bytes).map_err(io)?;
510                }
511                _ => unreachable!("checked"),
512            }
513        }
514        count += 1;
515    }
516    w.flush().map_err(io)?;
517    let mut f = w.into_inner().map_err(|e| io(e.into_error()))?;
518    f.seek(SeekFrom::Start(count_pos)).map_err(io)?;
519    f.write_all(&count.to_le_bytes()).map_err(io)?;
520    Ok(count)
521}
522
523/// A reader with random access to samples (fixed sample size).
524#[derive(Debug)]
525pub struct Reader {
526    file: BufReader<File>,
527    header: Header,
528    layout: Option<Layout>,
529    data_start: u64,
530    file_len: u64,
531}
532
533impl Reader {
534    /// Opens a file and reads its header; the layout is derived from the
535    /// type when it is closed (`E9001` is deferred to [`Reader::layout`]).
536    pub fn open(path: &Path) -> Result<Reader> {
537        let f = File::open(path).map_err(|e| DataError { code: "E9002", message: format!("{}: {e}", path.display()) })?;
538        let file_len = f.metadata().map_err(io)?.len();
539        let mut r = BufReader::new(f);
540        let mut magic = [0u8; 4];
541        r.read_exact(&mut magic).map_err(|_| DataError { code: "E9002", message: format!("{}: not a .psk.d file (too short)", path.display()) })?;
542        if &magic != MAGIC {
543            return err("E9002", format!("{}: not a .psk.d file (bad magic)", path.display()));
544        }
545        let mut b2 = [0u8; 2];
546        r.read_exact(&mut b2).map_err(io)?;
547        let version = u16::from_le_bytes(b2);
548        if version != VERSION && version != VERSION_ENCODED {
549            return err("E9002", format!("{}: .psk.d version {version}, this tool reads versions {VERSION} and {VERSION_ENCODED}", path.display()));
550        }
551        let mut b4 = [0u8; 4];
552        r.read_exact(&mut b4).map_err(io)?;
553        let tlen = u32::from_le_bytes(b4) as usize;
554        let mut tbuf = vec![0u8; tlen];
555        r.read_exact(&mut tbuf).map_err(|_| DataError { code: "E9003", message: format!("{}: truncated header", path.display()) })?;
556        let sample_ty = String::from_utf8(tbuf).map_err(|_| DataError { code: "E9002", message: format!("{}: the sample type is not UTF-8", path.display()) })?;
557        let mut b8 = [0u8; 8];
558        r.read_exact(&mut b8).map_err(|_| DataError { code: "E9003", message: format!("{}: truncated header", path.display()) })?;
559        let count = u64::from_le_bytes(b8);
560        let mut encodings = Vec::new();
561        if version == VERSION_ENCODED {
562            r.read_exact(&mut b4).map_err(|_| DataError { code: "E9003", message: format!("{}: truncated header", path.display()) })?;
563            let n = u32::from_le_bytes(b4) as usize;
564            let mut codes = vec![0u8; n];
565            r.read_exact(&mut codes).map_err(|_| DataError { code: "E9003", message: format!("{}: truncated header", path.display()) })?;
566            for (i, c) in codes.iter().enumerate() {
567                encodings.push(Enc::of_code(*c).ok_or_else(|| DataError { code: "E9002", message: format!("{}: leaf {i}: unknown encoding {c}", path.display()) })?);
568            }
569        }
570        let header = Header { version, sample_ty, count, encodings };
571        let data_start = header.len();
572        let layout = Layout::from_type_text(&header.sample_ty).ok();
573        if let Some(l) = &layout {
574            check_encodings(l, &header, path)?;
575        }
576        let mut reader = Reader { file: r, header, layout, data_start, file_len };
577        reader.check_len(path)?;
578        Ok(reader)
579    }
580
581    fn check_len(&mut self, path: &Path) -> Result<()> {
582        if let Some(l) = &self.layout {
583            let size = l.sample_bytes_enc(&self.header.encodings);
584            let need = self.data_start + self.header.count * size as u64;
585            if self.file_len < need {
586                return err("E9003", format!("{}: truncated: {} samples of {} bytes announced, {} bytes of data present", path.display(), self.header.count, size, self.file_len.saturating_sub(self.data_start)));
587            }
588        }
589        Ok(())
590    }
591
592    /// Supplies the layout of a `params M` file (from the program).
593    ///
594    /// A `params` file records its model's name and not its leaves' shapes,
595    /// so the layout comes from the program at the dimensions it is opened
596    /// with, and the file must then be *exactly* the size that layout
597    /// implies. A longer file is not a truncation check passing: it is a
598    /// model of other dimensions, and decoding it by this layout would read
599    /// every leaf after the first mismatch from the wrong offsets (0.6.0 step
600    /// 5d, D48 — `pos : ℝ[T, D]` trained at `T = 256` and opened at `T = 32`).
601    pub fn with_layout(mut self, layout: Layout) -> Result<Reader> {
602        check_encodings(&layout, &self.header, Path::new("the file"))?;
603        let size = layout.sample_bytes_enc(&self.header.encodings);
604        let need = self.data_start + self.header.count * size as u64;
605        if self.file_len < need {
606            return err("E9003", format!("truncated: {} samples of {} bytes announced, {} bytes of data present", self.header.count, size, self.file_len.saturating_sub(self.data_start)));
607        }
608        if self.file_len > need {
609            return err("E9005", format!("{} bytes of data where {} samples of {} bytes were expected: the file holds a model of other dimensions (a parameter shaped by a dimension, such as `pos : ℝ[T, D]`, at another `T`?)", self.file_len.saturating_sub(self.data_start), self.header.count, size));
610        }
611        self.layout = Some(layout);
612        Ok(self)
613    }
614
615    /// Every value of a `params M` file, in file order, without its layout.
616    /// A parameter tree holds only reals, stored as `f64`, so two
617    /// checkpoints of one model can be compared value for value without
618    /// the program that gives the tree its shape (`presk data diff`).
619    pub fn params_reals(&mut self) -> Result<Vec<f64>> {
620        if !self.header.sample_ty.starts_with("params ") {
621            return err("E9005", format!("`{}` is not a parameter tree", self.header.sample_ty));
622        }
623        // Without the program the leaves' sizes are unknown: one encoding
624        // for every leaf reads them all.
625        let e = self.header.encodings.first().copied().unwrap_or(Enc::F64);
626        if self.header.encodings.iter().any(|x| *x != e) {
627            return err("E9005", "the leaves of the file have several encodings: their sizes need the program");
628        }
629        let n = self.file_len.saturating_sub(self.data_start);
630        if n % e.width() as u64 != 0 {
631            return err("E9003", format!("{n} bytes of parameters, not a whole number of {}", e.name()));
632        }
633        self.file.seek(SeekFrom::Start(self.data_start)).map_err(io)?;
634        let mut buf = vec![0u8; n as usize];
635        self.file.read_exact(&mut buf).map_err(io)?;
636        Ok(buf.chunks_exact(e.width()).map(|c| e.decode(c)).collect())
637    }
638
639    pub fn header(&self) -> &Header {
640        &self.header
641    }
642
643    /// The layout, or `E9001` when the type needs the program.
644    pub fn layout(&self) -> Result<&Layout> {
645        match &self.layout {
646            Some(l) => Ok(l),
647            None => Layout::from_type_text(&self.header.sample_ty).map(|_| unreachable!()),
648        }
649    }
650
651    pub fn len(&self) -> usize {
652        self.header.count as usize
653    }
654
655    pub fn is_empty(&self) -> bool {
656        self.header.count == 0
657    }
658
659    /// The `i`-th sample.
660    pub fn get(&mut self, i: usize) -> Result<Value> {
661        let layout = self.layout()?.clone();
662        if i >= self.len() {
663            return err("E9003", format!("sample {i} requested, {} in the file", self.len()));
664        }
665        let size = layout.sample_bytes_enc(&self.header.encodings);
666        self.file.seek(SeekFrom::Start(self.data_start + (i * size) as u64)).map_err(io)?;
667        let mut buf = vec![0u8; size];
668        self.file.read_exact(&mut buf).map_err(|_| DataError { code: "E9003", message: format!("truncated at sample {i}") })?;
669        let (mut pos, mut leaf) = (0, 0);
670        Ok(decode(&layout, &self.header.encodings, &buf, &mut pos, &mut leaf))
671    }
672
673    /// All samples, in file order.
674    pub fn read_all(&mut self) -> Result<Vec<Value>> {
675        (0..self.len()).map(|i| self.get(i)).collect()
676    }
677}
678
679/// A version 2 header has one encoding per leaf of the type, and none of an
680/// index leaf.
681fn check_encodings(layout: &Layout, header: &Header, path: &Path) -> Result<()> {
682    if header.version != VERSION_ENCODED {
683        return Ok(());
684    }
685    let leaves = layout.leaves();
686    if header.encodings.len() != leaves.len() {
687        return err("E9005", format!("{}: {} encodings for the {} leaves of `{}`", path.display(), header.encodings.len(), leaves.len(), header.sample_ty));
688    }
689    if let Some((i, _)) = leaves.iter().enumerate().find(|(i, l)| matches!(l, Leaf::Idx { .. }) && header.encodings[*i] != Enc::F64) {
690        return err("E9005", format!("{}: leaf {i} holds indices and has a real encoding", path.display()));
691    }
692    Ok(())
693}
694
695fn decode(layout: &Layout, encs: &[Enc], buf: &[u8], pos: &mut usize, leaf_no: &mut usize) -> Value {
696    match layout {
697        Layout::Leaf(leaf @ Leaf::Idx { .. }) => {
698            *leaf_no += 1;
699            let w = leaf.width();
700            let d = (0..leaf.numel())
701                .map(|_| {
702                    let mut b = [0u8; 8];
703                    b[..w].copy_from_slice(&buf[*pos..*pos + w]);
704                    *pos += w;
705                    u64::from_le_bytes(b)
706                })
707                .collect();
708            Value::Idx(d)
709        }
710        Layout::Leaf(leaf @ Leaf::Real { .. }) => {
711            let e = encs.get(*leaf_no).copied().unwrap_or(Enc::F64);
712            *leaf_no += 1;
713            let w = e.width();
714            let d = (0..leaf.numel())
715                .map(|_| {
716                    let x = e.decode(&buf[*pos..*pos + w]);
717                    *pos += w;
718                    x
719                })
720                .collect();
721            Value::Real(d)
722        }
723        Layout::Tuple(ts) => Value::Tuple(ts.iter().map(|t| decode(t, encs, buf, pos, leaf_no)).collect()),
724        Layout::Record(fs) => Value::Record(fs.iter().map(|(n, t)| (n.clone(), decode(t, encs, buf, pos, leaf_no))).collect()),
725    }
726}
727
728// ----- pipeline ----------------------------------------------------------
729
730/// `stream … |> shuffle(seed) |> batch(B) |> take(n)` over sample indices.
731#[derive(Clone, Debug, PartialEq, Eq)]
732pub struct Pipeline {
733    /// `shuffle(seed = s)`: a permutation per epoch derived from `s`.
734    pub shuffle: Option<u64>,
735    /// `batch(B = n)`: groups of `n` consecutive samples; an incomplete last group is dropped.
736    pub batch: usize,
737    /// `take(n)`: only the first `n` batches of each epoch.
738    pub take: Option<usize>,
739}
740
741impl Pipeline {
742    /// The batches of one epoch (1-based), as groups of sample indices.
743    pub fn batches(&self, count: usize, epoch: u64) -> Vec<Vec<usize>> {
744        let order: Vec<usize> = match self.shuffle {
745            Some(seed) => shuffle_indices(count, seed, epoch),
746            None => (0..count).collect(),
747        };
748        let b = self.batch.max(1);
749        let mut out: Vec<Vec<usize>> = order.chunks_exact(b).map(|c| c.to_vec()).collect();
750        if let Some(n) = self.take {
751            out.truncate(n);
752        }
753        out
754    }
755
756    /// Batches per epoch (what the schedules count).
757    pub fn batches_per_epoch(&self, count: usize) -> usize {
758        let n = count / self.batch.max(1);
759        self.take.map_or(n, |t| n.min(t))
760    }
761}
762
763/// A permutation of `0..n` from `seed` and the epoch (1-based): the same on
764/// every machine, different for every epoch.
765pub fn shuffle_indices(n: usize, seed: u64, epoch: u64) -> Vec<usize> {
766    let mut master = ChaCha20Rng::seed_from_u64(seed);
767    let mut key = [0u8; 32];
768    for _ in 0..epoch.max(1) {
769        master.fill_bytes(&mut key);
770    }
771    let mut rng = ChaCha20Rng::from_seed(key);
772    let mut v: Vec<usize> = (0..n).collect();
773    for i in (1..n).rev() {
774        let j = (rng.next_u64() % (i as u64 + 1)) as usize;
775        v.swap(i, j);
776    }
777    v
778}
779
780// ----- text -------------------------------------------------------------
781
782/// The character vocabulary of a text: every distinct character, sorted, one
783/// token each. Tokens are `String`s so that the same format can later hold
784/// sub-word tokens; index `i` denotes `vocab[i]`.
785pub fn char_vocab(text: &str) -> Vec<String> {
786    let mut chars: Vec<char> = text.chars().collect();
787    chars.sort_unstable();
788    chars.dedup();
789    chars.into_iter().map(String::from).collect()
790}
791
792/// Encode a text against a vocabulary. Only single-character vocabularies are
793/// supported: sub-word tokens need longest-match encoding, which comes with
794/// `presk data bpe`.
795pub fn encode(text: &str, vocab: &[String]) -> Result<Vec<u64>> {
796    let mut map = std::collections::HashMap::with_capacity(vocab.len());
797    for (i, t) in vocab.iter().enumerate() {
798        let mut cs = t.chars();
799        let (Some(c), None) = (cs.next(), cs.next()) else {
800            return err("E9004", format!("the vocabulary holds the multi-character token `{t}`; only character vocabularies can be encoded in this version"));
801        };
802        map.insert(c, i as u64);
803    }
804    text.chars()
805        .map(|c| map.get(&c).copied().ok_or_else(|| DataError { code: "E9004", message: format!("the character `{}` (U+{:04X}) is not in the vocabulary", c.escape_debug(), c as u32) }))
806        .collect()
807}
808
809/// Decode a token stream back to text (the inverse of [`encode`]).
810pub fn decode_text(tokens: &[u64], vocab: &[String]) -> Result<String> {
811    tokens
812        .iter()
813        .map(|t| vocab.get(*t as usize).map(String::as_str).ok_or_else(|| DataError { code: "E9004", message: format!("token {t} is beyond the vocabulary of {}", vocab.len()) }))
814        .collect()
815}
816
817// ----- data preparation --------------------------------------------------
818
819/// Language-modelling windows of a token stream: sample `k` is
820/// `(tokens[k·stride .. +seq], tokens[k·stride + 1 .. +seq])`, of type
821/// `(idx[vocab][seq: T], idx[vocab][seq: T])`.
822pub fn lm_windows(tokens: &[u64], vocab: u64, seq: usize, stride: usize) -> Result<(String, Layout, Vec<Value>)> {
823    if seq == 0 || stride == 0 {
824        return err("E9004", "`--seq` and `--stride` must be at least 1");
825    }
826    if let Some(x) = tokens.iter().find(|x| **x >= vocab) {
827        return err("E9004", format!("token {x} is not below the vocabulary size {vocab}"));
828    }
829    let ty = format!("(idx[{vocab}][seq: {seq}], idx[{vocab}][seq: {seq}])");
830    let layout = Layout::from_type_text(&ty)?;
831    let mut out = Vec::new();
832    let mut k = 0;
833    while k + seq < tokens.len() {
834        out.push(Value::Tuple(vec![Value::Idx(tokens[k..k + seq].to_vec()), Value::Idx(tokens[k + 1..k + 1 + seq].to_vec())]));
835        k += stride;
836    }
837    Ok((ty, layout, out))
838}
839
840/// `presk data info`: the header and the first `n` samples.
841pub fn info(reader: &mut Reader, n: usize) -> Result<String> {
842    let h = reader.header().clone();
843    let mut s = format!("type    {}\ncount   {}\nversion {}\n", h.sample_ty, h.count, h.version);
844    if !h.encodings.is_empty() {
845        // Version 2: the leaves' encodings, runs of one encoding together.
846        let mut runs: Vec<(Enc, usize)> = Vec::new();
847        for e in &h.encodings {
848            match runs.last_mut() {
849                Some((f, n)) if f == e => *n += 1,
850                _ => runs.push((*e, 1)),
851            }
852        }
853        s.push_str(&format!("leaves  {}\n", runs.iter().map(|(e, n)| format!("{n} × {}", e.name())).collect::<Vec<_>>().join(", ")));
854    }
855    match reader.layout() {
856        Ok(l) => {
857            let l = l.clone();
858            s.push_str(&format!("sample  {} bytes, {} leaves\n", l.sample_bytes_enc(&h.encodings), l.leaves().len()));
859            for i in 0..n.min(reader.len()) {
860                let v = reader.get(i)?;
861                s.push_str(&format!("[{i}] {}\n", v.render(&l)));
862            }
863        }
864        Err(e) => s.push_str(&format!("sample  layout not derivable from the type ({}): open it from the program\n", e.message)),
865    }
866    Ok(s)
867}