brodo 1.0.0

Sparse volumes in pure Rust: a VDB-style sparse tree, fields that share a topology, and zero-copy grid files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
//! Checking output (ARCHITECTURE.md §6.1, ADR-0018): a field written for ParaView as one
//! ASCII UnstructuredGrid (`.vtu`), from VTK's published file-format documentation.
//!
//! Each leaf voxel is a VTK_VOXEL cell of edge 1 and each tile one VTK_VOXEL cell of its
//! region's size, in index space. Points are shared between cells, in the order the cells
//! first use them; a cell's eight points run x fastest, then y, then z. Values are cell
//! data: a scalar field named by `CellData`'s `Scalars`, a field of three components, one
//! value's components to a line, by its `Vectors`. Cells are leaves by leaf number, each
//! leaf's voxels x fastest, then tiles by tile number, so the same input always gives the
//! same bytes.
//!
//! Points are `Float64`, which holds every `i32` coordinate exactly; connectivity and
//! offsets are `Int64`. VTK 9.6.2 reads `NaN` back as a NaN but `-inf` as `+inf`
//! (measured 2026-10-17, ADR-0018), so a NaN, or a NaN component, is written as `NaN` and a
//! value with an infinite component is refused.

use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;

use crate::error::VtkError;
use crate::field::Field;
use crate::topology::Topology;
use crate::tree::Coord;

/// VTK_VOXEL's cell type, from VTK's public Python API (ADR-0018).
const VOXEL: u8 = 11;

/// A point, in index space; `i64` so that a tile's far corner never overflows.
type Point = [i64; 3];

/// A value type with a VTK data-array type and a number of components (ADR-0018). It names
/// the types and promises nothing more: what writing one needs is brodo's own.
pub trait VtkValue: sealed::VtkWrite {}

/// Keeps what writing a value needs out of brodo's API.
mod sealed {
    use std::io::Write;

    use crate::field::Stored;

    /// What writing a value as VTK's needs.
    pub trait VtkWrite: Stored {
        /// The `type` attribute of its `DataArray`.
        const TYPE_NAME: &'static str;

        /// Its components: 1 for a scalar, 3 for a vector.
        const COMPONENTS: u32;

        /// Whether VTK reads the value back as written: false for an infinity.
        fn readable(self) -> bool;

        /// Writes the value as ASCII, components apart by a space; a NaN as `NaN`.
        fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()>;
    }
}

impl VtkValue for f32 {}
impl VtkValue for f64 {}
impl VtkValue for u8 {}
impl VtkValue for u32 {}
impl VtkValue for [f32; 3] {}
impl VtkValue for [f64; 3] {}

impl sealed::VtkWrite for f32 {
    const TYPE_NAME: &'static str = "Float32";
    const COMPONENTS: u32 = 1;

    fn readable(self) -> bool {
        !self.is_infinite()
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        // Display prints the shortest text that reads back to the same value.
        if self.is_nan() { w.write_all(b"NaN") } else { write!(w, "{self}") }
    }
}

impl sealed::VtkWrite for f64 {
    const TYPE_NAME: &'static str = "Float64";
    const COMPONENTS: u32 = 1;

    fn readable(self) -> bool {
        !self.is_infinite()
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        if self.is_nan() { w.write_all(b"NaN") } else { write!(w, "{self}") }
    }
}

impl sealed::VtkWrite for u8 {
    const TYPE_NAME: &'static str = "UInt8";
    const COMPONENTS: u32 = 1;

    fn readable(self) -> bool {
        true
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        write!(w, "{self}")
    }
}

impl sealed::VtkWrite for u32 {
    const TYPE_NAME: &'static str = "UInt32";
    const COMPONENTS: u32 = 1;

    fn readable(self) -> bool {
        true
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        write!(w, "{self}")
    }
}

/// Writes a vector's components through their scalar type, apart by a space.
fn write_components<S: VtkValue>(c: [S; 3], w: &mut dyn Write) -> std::io::Result<()> {
    c[0].write_ascii(w)?;
    for x in &c[1..] {
        w.write_all(b" ")?;
        x.write_ascii(w)?;
    }
    Ok(())
}

impl sealed::VtkWrite for [f32; 3] {
    const TYPE_NAME: &'static str = "Float32";
    const COMPONENTS: u32 = 3;

    fn readable(self) -> bool {
        self.iter().all(|x| x.readable())
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        write_components(self, w)
    }
}

impl sealed::VtkWrite for [f64; 3] {
    const TYPE_NAME: &'static str = "Float64";
    const COMPONENTS: u32 = 3;

    fn readable(self) -> bool {
        self.iter().all(|x| x.readable())
    }

    fn write_ascii(self, w: &mut dyn Write) -> std::io::Result<()> {
        write_components(self, w)
    }
}

/// One cell: its lowest corner, its edge, and its value.
struct Cell<T> {
    origin: Coord,
    edge: i64,
    value: T,
}

impl<T> Cell<T> {
    /// Its eight corners, x fastest, then y, then z.
    fn corners(&self) -> impl Iterator<Item = Point> + use<'_, T> {
        let o = self.origin.map(i64::from);
        (0..8_i64).map(move |k| {
            [
                o[0] + (k & 1) * self.edge,
                o[1] + (k >> 1 & 1) * self.edge,
                o[2] + (k >> 2) * self.edge,
            ]
        })
    }
}

/// The bytes of `f` on `t` as one ASCII `.vtu`, its values named `name`.
///
/// `name` is one or more printable ASCII characters or spaces, none of `<>&"'`; a name,
/// field or value refused, an infinite one among them, is refused before any byte is made.
pub fn to_bytes<T: VtkValue>(t: &Topology, f: &Field<T>, name: &str) -> Result<Vec<u8>, VtkError> {
    let cells = checked(t, f, name)?;
    let mut out = Vec::new();
    emit(&cells, name, &mut out)?;
    Ok(out)
}

/// Writes `f` on `t` as one ASCII `.vtu` at `path`, its values named `name`, as `to_bytes`
/// makes it: to a hidden file beside `path` renamed over it, as `cache::write` writes
/// (ARCHITECTURE.md §4.7). What `to_bytes` refuses is refused before the file is made.
pub fn write<T: VtkValue>(
    path: &Path,
    t: &Topology,
    f: &Field<T>,
    name: &str,
) -> Result<(), VtkError> {
    let cells = checked(t, f, name)?;
    crate::cache::replace(path, |out| emit(&cells, name, out))
}

/// The cells of `f` on `t`, with `name` checked.
fn checked<T: VtkValue>(t: &Topology, f: &Field<T>, name: &str) -> Result<Vec<Cell<T>>, VtkError> {
    check_name(name)?;
    cells(t, f)
}

/// Writes the cells as one `.vtu` to `w`.
fn emit<T: VtkValue>(cells: &[Cell<T>], name: &str, w: &mut impl Write) -> Result<(), VtkError> {
    let (ids, points) = share_points(cells);
    writeln!(w, r#"<?xml version="1.0"?>"#)?;
    writeln!(w, r#"<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">"#)?;
    writeln!(w, "<UnstructuredGrid>")?;
    writeln!(w, r#"<Piece NumberOfPoints="{}" NumberOfCells="{}">"#, points.len(), cells.len())?;
    let (role, components) = match T::COMPONENTS {
        1 => ("Scalars", String::new()),
        n => ("Vectors", format!(r#" NumberOfComponents="{n}""#)),
    };
    writeln!(w, r#"<CellData {role}="{name}">"#)?;
    writeln!(w, r#"<DataArray type="{}" Name="{name}"{components} format="ascii">"#, T::TYPE_NAME)?;
    for c in cells {
        c.value.write_ascii(w)?;
        writeln!(w)?;
    }
    writeln!(w, "</DataArray>\n</CellData>\n<Points>")?;
    writeln!(w, r#"<DataArray type="Float64" NumberOfComponents="3" format="ascii">"#)?;
    for p in &points {
        writeln!(w, "{} {} {}", p[0], p[1], p[2])?;
    }
    writeln!(w, "</DataArray>\n</Points>\n<Cells>")?;
    write_cells(w, cells, &ids)?;
    writeln!(w, "</Cells>\n</Piece>\n</UnstructuredGrid>\n</VTKFile>")?;
    Ok(())
}

/// Refuses a name that would need escaping in XML, or none.
fn check_name(name: &str) -> Result<(), VtkError> {
    let fits = |c: char| (c.is_ascii_graphic() || c == ' ') && !"<>&\"'".contains(c);
    if name.is_empty() || !name.chars().all(fits) {
        return Err(VtkError::Name(name.to_owned()));
    }
    Ok(())
}

/// The cells, leaves then tiles; an infinite value is refused before anything is written.
fn cells<T: VtkValue>(t: &Topology, f: &Field<T>) -> Result<Vec<Cell<T>>, VtkError> {
    // Checked first, so that an empty topology does not let another's field through.
    f.belongs_to(t)?;
    let mut out = Vec::new();
    for n in t.leaf_numbers() {
        for (origin, value) in t.leaf_voxels(n)?.zip(f.leaf(t, n)?) {
            out.push(Cell { origin, edge: 1, value: *value });
        }
    }
    for n in t.tile_numbers() {
        let (level, origin) = t.tile(n)?;
        out.push(Cell { origin, edge: i64::from(level.edge()), value: f.tile(t, n)? });
    }
    match out.iter().find(|c| !c.value.readable()) {
        Some(c) => Err(VtkError::Infinite { origin: c.origin }),
        None => Ok(out),
    }
}

/// Each cell corner's point number, and the points in the order first used.
fn share_points<T>(cells: &[Cell<T>]) -> (BTreeMap<Point, u64>, Vec<Point>) {
    let mut ids = BTreeMap::new();
    let mut points = Vec::new();
    for p in cells.iter().flat_map(Cell::corners) {
        ids.entry(p).or_insert_with(|| {
            points.push(p);
            // Lossless: a usize fits a u64 on every target.
            points.len() as u64 - 1
        });
    }
    (ids, points)
}

/// The `Cells` element's three arrays: connectivity, offsets and types.
fn write_cells<T>(
    w: &mut impl Write,
    cells: &[Cell<T>],
    ids: &BTreeMap<Point, u64>,
) -> Result<(), VtkError> {
    writeln!(w, r#"<DataArray type="Int64" Name="connectivity" format="ascii">"#)?;
    for c in cells {
        let corners: Vec<String> = c
            .corners()
            .map(|p| ids.get(&p).map(u64::to_string).ok_or(VtkError::Inconsistent))
            .collect::<Result<_, _>>()?;
        writeln!(w, "{}", corners.join(" "))?;
    }
    writeln!(w, "</DataArray>")?;
    writeln!(w, r#"<DataArray type="Int64" Name="offsets" format="ascii">"#)?;
    for k in 1..=cells.len() {
        writeln!(w, "{}", k * 8)?;
    }
    writeln!(w, "</DataArray>")?;
    writeln!(w, r#"<DataArray type="UInt8" Name="types" format="ascii">"#)?;
    for _ in cells {
        writeln!(w, "{VOXEL}")?;
    }
    writeln!(w, "</DataArray>")?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_field_of_another_topology_is_refused_on_an_empty_one() {
        let empty = Topology::new();
        let mut e = empty.edit();
        e.activate([0, 0, 0]).unwrap();
        let other = e.commit().unwrap().0;
        let f = Field::new(&other, 0.0_f32);
        let got = to_bytes(&empty, &f, "f");
        assert!(
            matches!(got, Err(VtkError::Field(crate::error::FieldError::StaleField))),
            "{got:?}"
        );
    }
    use crate::tree::TileLevel;

    /// One tile of edge 8 at the given origin, holding `value`, on a background of zero.
    fn one_tile<T: VtkValue + Default>(origin: Coord, value: T) -> (Topology, Field<T>) {
        let empty = Topology::new();
        let mut e = empty.edit();
        e.add_tile(TileLevel::Lower, origin).unwrap();
        let (t, _) = e.commit().unwrap();
        let mut f = Field::new(&t, T::default());
        let n = t.tile_numbers().next().unwrap();
        f.set_tile(&t, n, value).unwrap();
        (t, f)
    }

    fn text<T: VtkValue>(t: &Topology, f: &Field<T>) -> String {
        String::from_utf8(to_bytes(t, f, "d").unwrap()).unwrap()
    }

    #[test]
    fn a_tile_is_one_voxel_cell_of_its_size() {
        let (t, f) = one_tile([8, -16, 0], 2.5_f32);
        let want = r#"<?xml version="1.0"?>
<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">
<UnstructuredGrid>
<Piece NumberOfPoints="8" NumberOfCells="1">
<CellData Scalars="d">
<DataArray type="Float32" Name="d" format="ascii">
2.5
</DataArray>
</CellData>
<Points>
<DataArray type="Float64" NumberOfComponents="3" format="ascii">
8 -16 0
16 -16 0
8 -8 0
16 -8 0
8 -16 8
16 -16 8
8 -8 8
16 -8 8
</DataArray>
</Points>
<Cells>
<DataArray type="Int64" Name="connectivity" format="ascii">
0 1 2 3 4 5 6 7
</DataArray>
<DataArray type="Int64" Name="offsets" format="ascii">
8
</DataArray>
<DataArray type="UInt8" Name="types" format="ascii">
11
</DataArray>
</Cells>
</Piece>
</UnstructuredGrid>
</VTKFile>
"#;
        assert_eq!(text(&t, &f), want);
    }

    #[test]
    fn a_vector_is_three_components_named_as_vectors() {
        let (t, f) = one_tile([8, -16, 0], [0.5_f64, -0.0025, f64::NAN]);
        let s = text(&t, &f);
        let want = r#"<CellData Vectors="d">
<DataArray type="Float64" Name="d" NumberOfComponents="3" format="ascii">
0.5 -0.0025 NaN
</DataArray>
</CellData>"#;
        assert!(s.contains(want), "{s}");
        let (t, f) = one_tile([0, 0, 0], [1.25_f32, -2.0, 3.5]);
        assert!(text(&t, &f).contains("format=\"ascii\">\n1.25 -2 3.5\n</DataArray>"));
        for v in [[0.0, f32::NEG_INFINITY, 0.0], [0.0, 0.0, f32::INFINITY]] {
            let (t, f) = one_tile([0, 8, 0], v);
            let got = to_bytes(&t, &f, "d");
            assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
        }
        let (t, f) = one_tile([0, 8, 0], [1.0_f64, f64::INFINITY, 0.0]);
        let got = to_bytes(&t, &f, "d");
        assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
    }

    #[test]
    fn integers_are_written_as_they_are() {
        let (t, f) = one_tile([0, 0, 0], 200_u8);
        assert!(text(&t, &f).contains("format=\"ascii\">\n200\n</DataArray>"));
        let (t, f) = one_tile([0, 0, 0], 4_000_000_000_u32);
        assert!(text(&t, &f).contains("format=\"ascii\">\n4000000000\n</DataArray>"));
    }

    #[test]
    fn two_tiles_side_by_side_share_four_points() {
        let empty = Topology::new();
        let mut e = empty.edit();
        e.add_tile(TileLevel::Lower, [0, 0, 0]).unwrap();
        e.add_tile(TileLevel::Lower, [8, 0, 0]).unwrap();
        let (t, _) = e.commit().unwrap();
        let s = text(&t, &Field::new(&t, 1_u8));
        assert!(s.contains(r#"<Piece NumberOfPoints="12" NumberOfCells="2">"#), "{s}");
        assert!(s.contains(r#"<DataArray type="UInt8" Name="d" format="ascii">"#), "{s}");
    }

    #[test]
    fn an_empty_grid_has_no_cell() {
        let t = Topology::new();
        let s = text(&t, &Field::new(&t, 0.0_f64));
        assert!(s.contains(r#"<Piece NumberOfPoints="0" NumberOfCells="0">"#), "{s}");
    }

    #[test]
    fn a_nan_is_written_as_nan() {
        let (t, f) = one_tile([0, 0, 0], -f64::NAN);
        assert!(text(&t, &f).contains("format=\"ascii\">\nNaN\n</DataArray>"));
    }

    #[test]
    fn an_infinity_is_refused_before_anything_is_written() {
        for value in [f32::INFINITY, f32::NEG_INFINITY] {
            let (t, f) = one_tile([0, 8, 0], value);
            let got = to_bytes(&t, &f, "d");
            assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 8, 0] })), "{got:?}");
        }
        let empty = Topology::new();
        let mut e = empty.edit();
        e.activate([0, 0, 0]).unwrap();
        let (t, _) = e.commit().unwrap();
        let mut f = Field::new(&t, 0.0_f64);
        let n = t.leaf_numbers().next().unwrap();
        f.leaf_mut(&t, n).unwrap()[8 + 64] = f64::INFINITY;
        let got = to_bytes(&t, &f, "d");
        assert!(matches!(got, Err(VtkError::Infinite { origin: [0, 1, 1] })), "{got:?}");
    }

    #[test]
    fn a_name_needing_escapes_or_empty_is_refused() {
        let (t, f) = one_tile([0, 0, 0], 1_u32);
        for name in ["", "a<b", "a&b", "a\"b", "a'b", "tab\t", "é"] {
            let got = to_bytes(&t, &f, name);
            assert!(matches!(got, Err(VtkError::Name(ref n)) if n == name), "{name:?}: {got:?}");
        }
        assert!(to_bytes(&t, &f, "signed distance_2").is_ok());
    }
}