hoomd_gsd/
hoomd.rs

1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Write HOOMD schema GSD files.
5//!
6//! Use [`HoomdGsdFile`] to write GSD files with the HOOMD schema.
7
8use std::{
9    array,
10    num::TryFromIntError,
11    path::Path,
12    time::{Duration, Instant},
13};
14use thiserror::Error;
15
16use hoomd_vector::{Cartesian, Versor};
17
18use crate::file_layer::{EncodeError, GsdFile, Mode, OpenError, SyncError, Type, WriteError};
19
20/// Longest type name size (including the null terminator).
21const MAX_NAME_LENGTH: usize = 64;
22
23/// Default delay between automatic calls to `sync_all`.
24const DEFAULT_AUTO_SYNC_DELAY: Duration = Duration::new(10, 0);
25
26/// Create and write to GSD files with the HOOMD schema.
27///
28/// Files written by [`HoomdGsdFile`] can be read by the [Ovito], [HOOMD-blue],
29/// the [GSD Python package], and other applications.
30///
31/// [GSD Python package]: https://gsd.readthedocs.io
32/// [HOOMD-blue]: https://hoomd-blue.readthedocs.io
33/// [Ovito]: https://www.ovito.org
34///
35/// # Buffering
36///
37/// GSD aggressively buffers data in memory before synchronizing it to the
38/// file. Your appended frames *may* not be present in the file until it is
39/// closed or you call [`sync_all`]. [`append_frame`] will automatically call
40/// [`sync_all`] when the last sync was more than 10 seconds prior (by default).
41/// Set `auto_sync_delay` to change the timeout.
42///
43/// # Example
44///
45/// ```
46/// use hoomd_gsd::hoomd::HoomdGsdFile;
47/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
48/// # use tempfile::tempdir;
49/// # let tmp_dir = tempdir().expect("temp dir should be created");
50/// # let path = tmp_dir.path().join("test.gsd");
51/// // let path = "file.gsd";
52/// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
53/// hoomd_gsd_file
54///     .append_frame(2_000)?
55///     .configuration_box([105.0, 48.0, 72.0, 0.0, 0.0, 0.0])?
56///     .particles_position([
57///         [2.0, 3.0, -1.0].into(),
58///         [18.0, 4.0, -6.0].into(),
59///     ])?
60///     .end()?;
61/// # Ok(())
62/// # }
63/// ```
64///
65/// [`append_frame`]: HoomdGsdFile::append_frame
66/// [`sync_all`]: HoomdGsdFile::sync_all
67pub struct HoomdGsdFile {
68    /// The wrapped GSD file.
69    gsd_file: GsdFile,
70
71    /// Time to auto synchronize.
72    auto_sync_delay: Duration,
73
74    /// Time of the last auto synchronization.
75    last_auto_sync: Instant,
76}
77
78/// The number of dimensions in the GSD file.
79#[non_exhaustive]
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub enum Dimensions {
82    /// All particle z coordinates and the box z should be zero.
83    Two,
84    /// Particle positions and the box shape can have non-zero z values.
85    Three,
86}
87
88/// In-progress frame in a HOOMD GSD file.
89///
90/// Call [`HoomdGsdFile::append_frame`] to create a new frame in the file. The
91/// [`Frame`] it returns has chainable methods you can call to add data
92/// chunks to the frame in the file. The frame is complete when the
93/// [`Frame`] is dropped.
94///
95/// `append_frame` always writes `configuration/step` with the given step.
96/// Each of the following methods writes the data chunk of the same name
97/// (where '/' has been replaced with '_'):
98///
99/// # Configuration
100///
101/// * [`configuration_dimensions`](Self::configuration_dimensions)
102/// * [`configuration_box`](Self::configuration_box)
103///
104/// # Particles
105///
106/// * [`particles_diameter`](Self::particles_diameter)
107/// * [`particles_position`](Self::particles_position)
108/// * [`particles_orientation`](Self::particles_orientation)
109/// * [`particles_type_id`](Self::particles_type_id)
110/// * [`particles_types`](Self::particles_types)
111///
112/// The first call to any `particles_*` method (except `particles_types`) implicitly
113/// writes the chunk `particles/N`. Any subsequent calls to other `particles_*`
114/// methods will return an error if `N` does not match.
115///
116/// # Log
117///
118/// All `log_*` methods write the chunk "log/{name}".
119///
120/// * [`log_scalar`](Self::log_scalar)
121/// * [`log_scalars`](Self::log_scalars)
122/// * [`log_arrays`](Self::log_arrays)
123pub struct Frame<'a> {
124    /// The GSD file this frame is part of.
125    hoomd_gsd_file: &'a mut HoomdGsdFile,
126
127    /// Stores the number of particles for error checking `particles_*` calls.
128    particles_n: Option<u32>,
129
130    /// Flag when the frame has ended.
131    ended: bool,
132}
133
134/// Errors that can occur while appending a frame to a HOOMD GSD file.
135#[non_exhaustive]
136#[derive(Error, Debug)]
137pub enum AppendError {
138    /// This data chunk does not match the dimensions of those previously written.
139    #[error("The length of data chunk {0} does not match previously written {1} chunks")]
140    InconsistentLength(String, String),
141
142    /// Cannot write to the file.
143    #[error("cannot write to the file")]
144    Write(#[from] WriteError),
145
146    /// Cannot encode data to write.
147    #[error("cannot encode data to write")]
148    Encode(#[from] EncodeError),
149
150    /// Cannot synchronize data to the file.
151    #[error("cannot synchronize data to the file")]
152    Sync(#[from] SyncError),
153
154    /// Too many entries to write.
155    #[error("cannot write {0} entries to data chunk {1}")]
156    ChunkTooLarge(usize, String, #[source] TryFromIntError),
157}
158
159impl HoomdGsdFile {
160    /// Overwrite an existing HOOMD GSD file (or create a new file).
161    ///
162    /// Creates a GSD file at the given path, overwriting any file that may already
163    /// exist. When successful, return a [`HoomdGsdFile`] opened in write mode.
164    ///
165    /// # Example
166    ///
167    /// ```
168    /// use hoomd_gsd::hoomd::HoomdGsdFile;
169    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// # use tempfile::tempdir;
171    /// # let tmp_dir = tempdir().expect("temp dir should be created");
172    /// # let path = tmp_dir.path().join("test.gsd");
173    /// // let path = "file.gsd";
174    /// let hoomd_gsd_file = HoomdGsdFile::create(path)?;
175    /// # Ok(())
176    /// # }
177    /// ```
178    ///
179    /// # Errors
180    ///
181    /// Returns a [`OpenError`] when any of the following occur:
182    /// * The file cannot be created.
183    /// * The file is corrupt, unreadable, or there is an I/O error (see
184    ///   [`DecodeError`]).
185    ///
186    /// [`DecodeError`]: crate::file_layer::DecodeError
187    #[inline]
188    pub fn create<P: AsRef<Path>>(path: P) -> Result<Self, OpenError> {
189        let version = env!("CARGO_PKG_VERSION");
190        let application = format!("hoomd-rs {version}");
191        let gsd_file = GsdFile::create(path, &application, "hoomd", (1, 4))?;
192
193        let auto_sync_delay = DEFAULT_AUTO_SYNC_DELAY;
194        let last_auto_sync = Instant::now();
195        Ok(Self {
196            gsd_file,
197            auto_sync_delay,
198            last_auto_sync,
199        })
200    }
201
202    /// Create a new HOOMD GSD file.
203    ///
204    /// Creates a new GSD file at the given path, returning an error when the
205    /// path already exists. When successful, return a [`HoomdGsdFile`] opened in
206    /// write mode.
207    ///
208    /// # Example
209    ///
210    /// ```
211    /// use hoomd_gsd::hoomd::HoomdGsdFile;
212    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
213    /// # use tempfile::tempdir;
214    /// # let tmp_dir = tempdir().expect("temp dir should be created");
215    /// # let path = tmp_dir.path().join("test.gsd");
216    /// // let path = "file.gsd";
217    /// let hoomd_gsd_file = HoomdGsdFile::create_new(path)?;
218    /// # Ok(())
219    /// # }
220    /// ```
221    ///
222    /// # Errors
223    ///
224    /// Returns a [`OpenError`] when any of the following occur:
225    /// * The file cannot be created.
226    /// * The file already exists.
227    /// * The file is corrupt, unreadable, or there is an I/O error (see
228    ///   [`DecodeError`]).
229    ///
230    /// [`DecodeError`]: crate::file_layer::DecodeError
231    #[inline]
232    pub fn create_new<P: AsRef<Path>>(path: P) -> Result<Self, OpenError> {
233        let version = env!("CARGO_PKG_VERSION");
234        let application = format!("hoomd-rs {version}");
235        let gsd_file = GsdFile::create_new(path, &application, "hoomd", (1, 4))?;
236
237        let auto_sync_delay = DEFAULT_AUTO_SYNC_DELAY;
238        let last_auto_sync = Instant::now();
239        Ok(Self {
240            gsd_file,
241            auto_sync_delay,
242            last_auto_sync,
243        })
244    }
245
246    /// Open an existing HOOMD GSD file in write mode.
247    ///
248    /// # Example
249    ///
250    /// ```
251    /// use hoomd_gsd::hoomd::HoomdGsdFile;
252    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
253    /// # use tempfile::tempdir;
254    /// # let tmp_dir = tempdir().expect("temp dir should be created");
255    /// # let path = tmp_dir.path().join("test.gsd");
256    /// // let path = "file.gsd";
257    /// # HoomdGsdFile::create_new(&path)?;
258    /// let hoomd_gsd_file = HoomdGsdFile::open(path)?;
259    /// # Ok(())
260    /// # }
261    /// ```
262    ///
263    /// # Errors
264    ///
265    /// Returns a [`OpenError`] when any of the following occur:
266    /// * The file does not exist.
267    /// * The file is corrupt, unreadable, or there is an I/O error (see
268    ///   [`DecodeError`]).
269    ///
270    /// [`DecodeError`]: crate::file_layer::DecodeError
271    #[inline]
272    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, OpenError> {
273        let gsd_file = GsdFile::open(path, Mode::Write)?;
274
275        let auto_sync_delay = DEFAULT_AUTO_SYNC_DELAY;
276        let last_auto_sync = Instant::now();
277        Ok(Self {
278            gsd_file,
279            auto_sync_delay,
280            last_auto_sync,
281        })
282    }
283
284    /// Write buffered data to the filesystem.
285    ///
286    /// `sync_all` ensures that the data and indices for all complete frames is
287    /// written to the filesystem.
288    ///
289    /// In most cases, callers should not invoke `sync_all` manually. It will
290    /// be called automatically when a [`HoomdGsdFile`] is dropped and after any
291    /// call to `append_frame` at least 10 seconds (by default) after a previous
292    /// call to `sync_all`.
293    ///
294    /// Call `sync_all` only want to ensure that all data up to a specific frame
295    /// are present in the file.
296    ///
297    /// # Errors
298    ///
299    /// Returns a [`WriteError`] when any of the following occur:
300    /// * The file is not opened in a write mode.
301    /// * An I/O error writing to the file.
302    pub fn sync_all(&mut self) -> Result<(), SyncError> {
303        self.gsd_file.sync_all()?;
304        self.last_auto_sync = Instant::now();
305        Ok(())
306    }
307
308    /// Append a new frame to the file.
309    ///
310    /// Use chained method calls to concisely write all the data chunks needed
311    /// for a single frame (see the example):
312    ///
313    /// # Ownership
314    ///
315    /// The returned [`Frame`] holds a mutable reference to this
316    /// [`HoomdGsdFile`], so the compiler forces you to complete writing the
317    /// frame and drop the [`Frame`] (either implicitly or explicitly) before
318    /// you can take any other actions that would mutate the [`HoomdGsdFile`].
319    ///
320    /// # Example
321    ///
322    /// ```
323    /// use hoomd_gsd::hoomd::HoomdGsdFile;
324    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
325    /// # use tempfile::tempdir;
326    /// # let tmp_dir = tempdir().expect("temp dir should be created");
327    /// # let path = tmp_dir.path().join("test.gsd");
328    /// // let path = "file.gsd";
329    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
330    /// hoomd_gsd_file
331    ///     .append_frame(1_000)?
332    ///     .configuration_box([100.0, 50.0, 80.0, 0.0, 0.0, 0.0])?
333    ///     .particles_position([[0.0, 1.0, 2.0].into(), [3.0, 6.0, 12.0].into()])?
334    ///     .end()?;
335    /// # Ok(())
336    /// # }
337    /// ```
338    ///
339    /// # Errors
340    ///
341    /// Returns a [`WriteError`] when any of the following occur:
342    /// * The file is not opened in a write mode.
343    /// * An I/O error writing to the file.
344    #[inline]
345    pub fn append_frame(&mut self, step: u64) -> Result<Frame<'_>, WriteError> {
346        self.gsd_file.write_scalars("configuration/step", [step])?;
347        Ok(Frame {
348            hoomd_gsd_file: self,
349            particles_n: None,
350            ended: false,
351        })
352    }
353
354    /// Get the auto sync delay.
355    ///
356    /// # Example
357    ///
358    /// ```
359    /// use hoomd_gsd::hoomd::HoomdGsdFile;
360    /// use std::time::Duration;
361    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
362    /// # use tempfile::tempdir;
363    /// # let tmp_dir = tempdir().expect("temp dir should be created");
364    /// # let path = tmp_dir.path().join("test.gsd");
365    /// // let path = "file.gsd";
366    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
367    /// let auto_sync_delay = hoomd_gsd_file.auto_sync_delay();
368    ///
369    /// assert_eq!(*auto_sync_delay, Duration::new(10, 0));
370    /// # Ok(())
371    /// # }
372    /// ```
373    #[inline]
374    #[must_use]
375    pub fn auto_sync_delay(&self) -> &Duration {
376        &self.auto_sync_delay
377    }
378
379    /// Get a mutable reference to the auto sync delay.
380    ///
381    /// # Example
382    ///
383    /// ```
384    /// use hoomd_gsd::hoomd::HoomdGsdFile;
385    /// use std::time::Duration;
386    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
387    /// # use tempfile::tempdir;
388    /// # let tmp_dir = tempdir().expect("temp dir should be created");
389    /// # let path = tmp_dir.path().join("test.gsd");
390    /// // let path = "file.gsd";
391    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
392    /// *hoomd_gsd_file.auto_sync_delay_mut() = Duration::new(60, 0);
393    /// # Ok(())
394    /// # }
395    /// ```
396    #[inline]
397    #[must_use]
398    pub fn auto_sync_delay_mut(&mut self) -> &mut Duration {
399        &mut self.auto_sync_delay
400    }
401}
402
403impl Frame<'_> {
404    /// Write [`configuration/dimensions`] to the current frame in the GSD file.
405    ///
406    /// [`configuration/dimensions`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-configuration-dimensions
407    ///
408    /// # Example
409    ///
410    /// ```
411    /// use hoomd_gsd::hoomd::{Dimensions, HoomdGsdFile};
412    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
413    /// # use tempfile::tempdir;
414    /// # let tmp_dir = tempdir().expect("temp dir should be created");
415    /// # let path = tmp_dir.path().join("test.gsd");
416    /// // let path = "file.gsd";
417    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
418    /// hoomd_gsd_file
419    ///     .append_frame(1_000)?
420    ///     .configuration_dimensions(Dimensions::Two)?
421    ///     .end()?;
422    /// # Ok(())
423    /// # }
424    /// ```
425    ///
426    /// # Errors
427    ///
428    /// Returns an [`AppendError`] when any of the following occur:
429    /// * The file is not opened in a write mode.
430    /// * An I/O error writing to the file.
431    pub fn configuration_dimensions(self, dimensions: Dimensions) -> Result<Self, AppendError> {
432        let chunk_name = "configuration/dimensions";
433
434        let dimensions: u8 = match dimensions {
435            Dimensions::Two => 2,
436            Dimensions::Three => 3,
437        };
438
439        self.hoomd_gsd_file
440            .gsd_file
441            .write_scalars(chunk_name, [dimensions])?;
442
443        Ok(self)
444    }
445
446    /// Write [`configuration/box`] to the current frame in the GSD file.
447    ///
448    /// The input `f64` values are converted to `f32`.
449    ///
450    /// [`configuration/box`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-configuration-box
451    ///
452    /// # Example
453    ///
454    /// ```
455    /// use hoomd_gsd::hoomd::HoomdGsdFile;
456    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
457    /// # use tempfile::tempdir;
458    /// # let tmp_dir = tempdir().expect("temp dir should be created");
459    /// # let path = tmp_dir.path().join("test.gsd");
460    /// // let path = "file.gsd";
461    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
462    /// hoomd_gsd_file
463    ///     .append_frame(1_000)?
464    ///     .configuration_box([10.0, 20.0, 30.0, 0.0, 0.0, 0.0])?
465    ///     .end()?;
466    /// # Ok(())
467    /// # }
468    /// ```
469    ///
470    /// # Errors
471    ///
472    /// Returns an [`AppendError`] when any of the following occur:
473    /// * The file is not opened in a write mode.
474    /// * An I/O error writing to the file.
475    #[expect(
476        clippy::cast_possible_truncation,
477        reason = "truncating to match the GSD specification"
478    )]
479    pub fn configuration_box(self, values: [f64; 6]) -> Result<Self, AppendError> {
480        let chunk_name = "configuration/box";
481
482        let values: [f32; 6] = array::from_fn(|i| values[i] as f32);
483        self.hoomd_gsd_file
484            .gsd_file
485            .write_scalars(chunk_name, values)?;
486
487        Ok(self)
488    }
489
490    /// Write the length of the data to `particles/N` or return an error when
491    /// `particles/N` has already been written and this `N` does not match.
492    fn validate_particles_chunk<T>(
493        &mut self,
494        data: &[T],
495        chunk_name: &str,
496    ) -> Result<(), AppendError> {
497        if let Some(n) = self.particles_n {
498            if data.len() != n as usize {
499                return Err(AppendError::InconsistentLength(
500                    chunk_name.to_string(),
501                    "particles".to_string(),
502                ));
503            }
504        } else {
505            let n = data
506                .len()
507                .try_into()
508                .map_err(|e| AppendError::ChunkTooLarge(data.len(), chunk_name.to_string(), e))?;
509            self.hoomd_gsd_file
510                .gsd_file
511                .write_scalars("particles/N", [n])?;
512            self.particles_n = Some(n);
513        }
514
515        Ok(())
516    }
517
518    /// Write [`particles/position`] to the current frame in the GSD file.
519    ///
520    /// The input `Cartesian<3>` argument is converted to `[f32; 3]`.
521    ///
522    /// [`particles/position`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-particles-position
523    ///
524    /// # Example
525    ///
526    /// ```
527    /// use hoomd_gsd::hoomd::HoomdGsdFile;
528    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
529    /// # use tempfile::tempdir;
530    /// # let tmp_dir = tempdir().expect("temp dir should be created");
531    /// # let path = tmp_dir.path().join("test.gsd");
532    /// // let path = "file.gsd";
533    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
534    /// hoomd_gsd_file
535    ///     .append_frame(1_000)?
536    ///     .particles_position([
537    ///         [2.0, 3.0, -1.0].into(),
538    ///         [18.0, 4.0, -6.0].into(),
539    ///     ])?
540    ///     .end()?;
541    /// # Ok(())
542    /// # }
543    /// ```
544    ///
545    /// # Errors
546    ///
547    /// Returns an [`AppendError`] when any of the following occur:
548    /// * The file is not opened in a write mode.
549    /// * An I/O error writing to the file.
550    /// * *N* does not match a previous `particles_*` data chunk in this frame.
551    #[expect(
552        clippy::cast_possible_truncation,
553        reason = "truncating to match the GSD specification"
554    )]
555    pub fn particles_position<I>(mut self, position: I) -> Result<Self, AppendError>
556    where
557        I: IntoIterator<Item = Cartesian<3>>,
558    {
559        let chunk_name = "particles/position";
560        let data: Vec<_> = position
561            .into_iter()
562            .map(|v| -> [f32; 3] { array::from_fn(|i| v[i] as f32) })
563            .collect();
564
565        self.validate_particles_chunk(&data, chunk_name)?;
566
567        self.hoomd_gsd_file
568            .gsd_file
569            .write_arrays(chunk_name, data)?;
570
571        Ok(self)
572    }
573
574    /// Write [`particles/orientation`] to the current frame in the GSD file.
575    ///
576    /// [`particles/orientation`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-particles-orientation
577    ///
578    /// The input `Versor` argument is converted to `[f32; 4]`.
579    ///
580    /// # Example
581    ///
582    /// ```
583    /// use hoomd_gsd::hoomd::HoomdGsdFile;
584    /// use hoomd_vector::Versor;
585    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
586    /// # use tempfile::tempdir;
587    /// # let tmp_dir = tempdir().expect("temp dir should be created");
588    /// # let path = tmp_dir.path().join("test.gsd");
589    /// // let path = "file.gsd";
590    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
591    /// hoomd_gsd_file
592    ///     .append_frame(1_000)?
593    ///     .particles_orientation([
594    ///         Versor::from_axis_angle([0.0, 0.0, 1.0].try_into()?, 1.2),
595    ///         Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, -0.3),
596    ///     ])?
597    ///     .end()?;
598    /// # Ok(())
599    /// # }
600    /// ```
601    ///
602    /// # Errors
603    ///
604    /// Returns an [`AppendError`] when any of the following occur:
605    /// * The file is not opened in a write mode.
606    /// * An I/O error writing to the file.
607    /// * *N* does not match a previous `particles_*` data chunk in this frame.
608    #[expect(
609        clippy::cast_possible_truncation,
610        reason = "truncating to match the GSD specification"
611    )]
612    pub fn particles_orientation<I>(mut self, orientation: I) -> Result<Self, AppendError>
613    where
614        I: IntoIterator<Item = Versor>,
615    {
616        let chunk_name = "particles/orientation";
617        let data: Vec<_> = orientation
618            .into_iter()
619            .map(|v| {
620                [
621                    v.get().scalar as f32,
622                    v.get().vector[0] as f32,
623                    v.get().vector[1] as f32,
624                    v.get().vector[2] as f32,
625                ]
626            })
627            .collect();
628
629        self.validate_particles_chunk(&data, chunk_name)?;
630
631        self.hoomd_gsd_file
632            .gsd_file
633            .write_arrays(chunk_name, data)?;
634
635        Ok(self)
636    }
637
638    /// Write [`particles/typeid`] to the current frame in the GSD file.
639    ///
640    /// [`particles/typeid`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-particles-typeid
641    ///
642    /// # Example
643    ///
644    /// ```
645    /// use hoomd_gsd::hoomd::HoomdGsdFile;
646    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
647    /// # use tempfile::tempdir;
648    /// # let tmp_dir = tempdir().expect("temp dir should be created");
649    /// # let path = tmp_dir.path().join("test.gsd");
650    /// // let path = "file.gsd";
651    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
652    /// hoomd_gsd_file
653    ///     .append_frame(1_000)?
654    ///     .particles_type_id([0, 0, 0, 1, 1, 1])?
655    ///     .end()?;
656    /// # Ok(())
657    /// # }
658    /// ```
659    ///
660    /// # Errors
661    ///
662    /// Returns an [`AppendError`] when any of the following occur:
663    /// * The file is not opened in a write mode.
664    /// * An I/O error writing to the file.
665    /// * *N* does not match a previous `particles_*` data chunk in this frame.
666    pub fn particles_type_id<I>(mut self, type_id: I) -> Result<Self, AppendError>
667    where
668        I: IntoIterator<Item = u32>,
669    {
670        let chunk_name = "particles/typeid";
671        let data: Vec<_> = type_id.into_iter().collect();
672
673        self.validate_particles_chunk(&data, chunk_name)?;
674
675        self.hoomd_gsd_file
676            .gsd_file
677            .write_scalars(chunk_name, data)?;
678
679        Ok(self)
680    }
681
682    /// Write [`particles/types`] to the current frame in the GSD file.
683    ///
684    /// [`particles/types`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-particles-types
685    ///
686    /// # Example
687    ///
688    /// ```
689    /// use hoomd_gsd::hoomd::HoomdGsdFile;
690    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
691    /// # use tempfile::tempdir;
692    /// # let tmp_dir = tempdir().expect("temp dir should be created");
693    /// # let path = tmp_dir.path().join("test.gsd");
694    /// // let path = "file.gsd";
695    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
696    /// hoomd_gsd_file
697    ///     .append_frame(1_000)?
698    ///     .particles_types(["A", "B", "linker"])?
699    ///     .end()?;
700    /// # Ok(())
701    /// # }
702    /// ```
703    ///
704    /// # Errors
705    ///
706    /// Returns an [`AppendError`] when any of the following occur:
707    /// * The file is not opened in a write mode.
708    /// * An I/O error writing to the file.
709    ///
710    /// # Panics
711    ///
712    /// Panics when any type name string is longer than 63 characters. This is a limitation
713    /// in the *hoomd-rs* implementation, not the GSD file format. The limitation may be
714    /// removed in a future release.
715    pub fn particles_types<'a, I>(self, types: I) -> Result<Self, AppendError>
716    where
717        I: IntoIterator<Item = &'a str>,
718    {
719        let chunk_name = "particles/types";
720        let types: Vec<_> = types.into_iter().map(str::to_string).collect();
721        let max_len = types.iter().map(String::len).fold(0, Ord::max);
722
723        assert!(max_len < MAX_NAME_LENGTH, "type name length too long");
724
725        self.hoomd_gsd_file.gsd_file.write_arrays(
726            chunk_name,
727            types.into_iter().map(|s| -> [u8; MAX_NAME_LENGTH] {
728                array::from_fn(|i| if i < s.len() { s.as_bytes()[i] } else { 0 })
729            }),
730        )?;
731
732        Ok(self)
733    }
734
735    /// Write [`particles/diameter`] to the current frame in the GSD file.
736    ///
737    /// [`particles/diameter`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#chunk-particles-diameter
738    ///
739    /// # Example
740    ///
741    /// ```
742    /// use hoomd_gsd::hoomd::HoomdGsdFile;
743    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
744    /// # use tempfile::tempdir;
745    /// # let tmp_dir = tempdir().expect("temp dir should be created");
746    /// # let path = tmp_dir.path().join("test.gsd");
747    /// // let path = "file.gsd";
748    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
749    /// hoomd_gsd_file
750    ///     .append_frame(1_000)?
751    ///     .particles_diameter([1.0, 0.5, 0.25, 2.0])?
752    ///     .end()?;
753    /// # Ok(())
754    /// # }
755    /// ```
756    ///
757    /// # Errors
758    ///
759    /// Returns an [`AppendError`] when any of the following occur:
760    /// * The file is not opened in a write mode.
761    /// * An I/O error writing to the file.
762    /// * *N* does not match a previous `particles_*` data chunk in this frame.
763    #[expect(
764        clippy::cast_possible_truncation,
765        reason = "truncating to match the GSD specification"
766    )]
767    pub fn particles_diameter<I>(mut self, diameter: I) -> Result<Self, AppendError>
768    where
769        I: IntoIterator<Item = f64>,
770    {
771        let chunk_name = "particles/diameter";
772        let data: Vec<_> = diameter.into_iter().map(|x| x as f32).collect();
773
774        self.validate_particles_chunk(&data, chunk_name)?;
775
776        self.hoomd_gsd_file
777            .gsd_file
778            .write_scalars(chunk_name, data)?;
779
780        Ok(self)
781    }
782
783    /// Write [`log/{name}`] to the current frame in the GSD file as a 1x1 array of type
784    /// `T`.
785    ///
786    /// [`log/{name}`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#logged-data
787    ///
788    /// # Example
789    ///
790    /// ```
791    /// use hoomd_gsd::hoomd::HoomdGsdFile;
792    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
793    /// # use tempfile::tempdir;
794    /// # let tmp_dir = tempdir().expect("temp dir should be created");
795    /// # let path = tmp_dir.path().join("test.gsd");
796    /// // let path = "file.gsd";
797    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
798    /// hoomd_gsd_file
799    ///     .append_frame(1_000)?
800    ///     .log_scalar("height", 10.0_f64)?
801    ///     .end()?;
802    /// # Ok(())
803    /// # }
804    /// ```
805    ///
806    /// # Errors
807    ///
808    /// Returns an [`AppendError`] when any of the following occur:
809    /// * The file is not opened in a write mode.
810    /// * An I/O error writing to the file.
811    pub fn log_scalar<T>(self, name: &str, scalar: T) -> Result<Self, AppendError>
812    where
813        T: Type,
814    {
815        let chunk_name = ["log", name].join("/");
816
817        self.hoomd_gsd_file
818            .gsd_file
819            .write_scalars(&chunk_name, [scalar])?;
820
821        Ok(self)
822    }
823
824    /// Write [`log/{name}`] to the current frame in the GSD file as a *N*x1 array of type
825    /// `T` where *N* is the number of items produced by the `scalars` iterator.
826    ///
827    /// [`log/{name}`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#logged-data
828    ///
829    /// # Example
830    ///
831    /// ```
832    /// use hoomd_gsd::hoomd::HoomdGsdFile;
833    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
834    /// # use tempfile::tempdir;
835    /// # let tmp_dir = tempdir().expect("temp dir should be created");
836    /// # let path = tmp_dir.path().join("test.gsd");
837    /// // let path = "file.gsd";
838    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
839    /// hoomd_gsd_file
840    ///     .append_frame(1_000)?
841    ///     .log_scalars("energy", [1.0_f64, 2.0, 3.0])?
842    ///     .end()?;
843    /// # Ok(())
844    /// # }
845    /// ```
846    ///
847    /// # Errors
848    ///
849    /// Returns an [`AppendError`] when any of the following occur:
850    /// * The file is not opened in a write mode.
851    /// * An I/O error writing to the file.
852    pub fn log_scalars<T, I>(self, name: &str, scalars: I) -> Result<Self, AppendError>
853    where
854        T: Type,
855        I: IntoIterator<Item = T>,
856    {
857        let chunk_name = ["log", name].join("/");
858
859        self.hoomd_gsd_file
860            .gsd_file
861            .write_scalars(&chunk_name, scalars)?;
862
863        Ok(self)
864    }
865
866    /// Write [`log/{name}`] to the current frame in the GSD file as a *N*x*M* array of type
867    /// `T` where *N* is the number of items produced by the `arrays` iterator.
868    ///
869    /// [`log/{name}`]: https://gsd.readthedocs.io/en/v4.2.0/schema-hoomd.html#logged-data
870    ///
871    /// # Example
872    ///
873    /// ```
874    /// use hoomd_gsd::hoomd::HoomdGsdFile;
875    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
876    /// # use tempfile::tempdir;
877    /// # let tmp_dir = tempdir().expect("temp dir should be created");
878    /// # let path = tmp_dir.path().join("test.gsd");
879    /// // let path = "file.gsd";
880    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
881    /// hoomd_gsd_file
882    ///     .append_frame(1_000)?
883    ///     .log_arrays("points", [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])?
884    ///     .end()?;
885    /// # Ok(())
886    /// # }
887    /// ```
888    ///
889    /// # Errors
890    ///
891    /// Returns an [`AppendError`] when any of the following occur:
892    /// * The file is not opened in a write mode.
893    /// * An I/O error writing to the file.
894    pub fn log_arrays<T, I, const M: usize>(
895        self,
896        name: &str,
897        arrays: I,
898    ) -> Result<Self, AppendError>
899    where
900        T: Type,
901        I: IntoIterator<Item = [T; M]>,
902    {
903        let chunk_name = ["log", name].join("/");
904
905        self.hoomd_gsd_file
906            .gsd_file
907            .write_arrays(&chunk_name, arrays)?;
908
909        Ok(self)
910    }
911
912    /// End the frame.
913    ///
914    /// Once the frame is complete, no more data chunks may be added to it. The next call
915    /// to [`HoomdGsdFile::append_frame`] will add a new frame to the file.
916    ///
917    /// Calling `end` is *optional*. The frame will automatically end when [`Frame`] is
918    /// dropped. `Drop` ignores any errors. Call `end` explicitly to check for
919    /// errors.
920    ///
921    /// # Errors
922    ///
923    /// Returns an I/O error when there is a problem writing to the file.
924    ///
925    /// # Example
926    ///
927    /// ```
928    /// use hoomd_gsd::hoomd::HoomdGsdFile;
929    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
930    /// # use tempfile::tempdir;
931    /// # let tmp_dir = tempdir().expect("temp dir should be created");
932    /// # let path = tmp_dir.path().join("test.gsd");
933    /// // let path = "file.gsd";
934    /// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
935    /// hoomd_gsd_file.append_frame(1_000)?.end()?;
936    /// # Ok(())
937    /// # }
938    /// ```
939    pub fn end(mut self) -> Result<(), AppendError> {
940        self.hoomd_gsd_file.gsd_file.end_frame()?;
941        if self.hoomd_gsd_file.last_auto_sync.elapsed() >= self.hoomd_gsd_file.auto_sync_delay {
942            self.hoomd_gsd_file.sync_all()?;
943        }
944
945        self.ended = true;
946
947        Ok(())
948    }
949}
950
951/// End the frame.
952///
953/// Once the frame is complete, no more data chunks may be added to it. The next call
954/// to [`HoomdGsdFile::append_frame`] will add a new frame to the file.
955///
956/// `drop` checks the amount of time since the last call to [`HoomdGsdFile::sync_all`].
957/// If it has been more than the auto sync delay, `drop` calls `sync_all`.
958/// `drop` ignores all errors. Call [`Frame::end`] to end the frame and
959/// check on potential I/O errors.
960///
961/// # Example
962///
963/// ```
964/// use hoomd_gsd::hoomd::HoomdGsdFile;
965/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
966/// # use tempfile::tempdir;
967/// # let tmp_dir = tempdir().expect("temp dir should be created");
968/// # let path = tmp_dir.path().join("test.gsd");
969/// // let path = "file.gsd";
970/// let mut hoomd_gsd_file = HoomdGsdFile::create(path)?;
971/// hoomd_gsd_file.append_frame(1_000)?;
972/// // ... some I/O errors might be ignored during the implicit drop.
973/// # Ok(())
974/// # }
975/// ```
976impl Drop for Frame<'_> {
977    fn drop(&mut self) {
978        if !self.ended {
979            let _ = self.hoomd_gsd_file.gsd_file.end_frame();
980            if self.hoomd_gsd_file.last_auto_sync.elapsed() >= self.hoomd_gsd_file.auto_sync_delay {
981                let _ = self.hoomd_gsd_file.sync_all();
982            }
983        }
984    }
985}
986
987#[cfg(test)]
988mod test {
989    use assert2::{assert, check};
990    use std::thread;
991    use tempfile::tempdir;
992
993    use super::*;
994    use hoomd_vector::Versor;
995
996    #[test]
997    fn create() -> anyhow::Result<()> {
998        let tmp_dir = tempdir()?;
999        let path = tmp_dir.path().join("test.gsd");
1000
1001        let hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1002        check!(*hoomd_gsd_file.gsd_file.mode() == Mode::Write);
1003        drop(hoomd_gsd_file);
1004
1005        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1006        check!(gsd_file.application().starts_with("hoomd-rs"));
1007        check!(gsd_file.schema() == "hoomd");
1008        check!(gsd_file.schema_version() == (1, 4));
1009        check!(gsd_file.n_frames() == 0);
1010        check!(gsd_file.name_id().is_empty());
1011
1012        Ok(())
1013    }
1014
1015    #[test]
1016    fn create_new() -> anyhow::Result<()> {
1017        let tmp_dir = tempdir()?;
1018        let path = tmp_dir.path().join("test.gsd");
1019        let hoomd_gsd_file = HoomdGsdFile::create_new(path.clone())?;
1020        check!(*hoomd_gsd_file.gsd_file.mode() == Mode::Write);
1021        drop(hoomd_gsd_file);
1022
1023        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1024        check!(gsd_file.application().starts_with("hoomd-rs"));
1025        check!(gsd_file.schema() == "hoomd");
1026        check!(gsd_file.schema_version() == (1, 4));
1027        check!(gsd_file.n_frames() == 0);
1028        check!(gsd_file.name_id().is_empty());
1029
1030        check!(matches!(
1031            HoomdGsdFile::create_new(path.clone()),
1032            Err(OpenError::IO(_, _))
1033        ));
1034
1035        Ok(())
1036    }
1037
1038    #[test]
1039    fn open() -> anyhow::Result<()> {
1040        let tmp_dir = tempdir()?;
1041        let path = tmp_dir.path().join("test.gsd");
1042
1043        let hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1044        check!(*hoomd_gsd_file.gsd_file.mode() == Mode::Write);
1045        drop(hoomd_gsd_file);
1046
1047        let hoomd_gsd_file = HoomdGsdFile::open(path.clone())?;
1048        check!(*hoomd_gsd_file.gsd_file.mode() == Mode::Write);
1049        // TODO: test that data chunks can be appended to an open file
1050        drop(hoomd_gsd_file);
1051
1052        Ok(())
1053    }
1054
1055    #[test]
1056    fn auto_sync_delay() -> anyhow::Result<()> {
1057        let tmp_dir = tempdir()?;
1058        let path = tmp_dir.path().join("test.gsd");
1059
1060        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1061        check!(*hoomd_gsd_file.auto_sync_delay() == DEFAULT_AUTO_SYNC_DELAY);
1062
1063        *hoomd_gsd_file.auto_sync_delay_mut() = Duration::new(20, 0);
1064        check!(*hoomd_gsd_file.auto_sync_delay() == Duration::new(20, 0));
1065
1066        let previous_auto_sync = hoomd_gsd_file.last_auto_sync;
1067
1068        thread::sleep(Duration::from_millis(40));
1069        hoomd_gsd_file.sync_all()?;
1070
1071        check!(previous_auto_sync != hoomd_gsd_file.last_auto_sync);
1072
1073        Ok(())
1074    }
1075
1076    #[test]
1077    fn configuration_dimensions() -> anyhow::Result<()> {
1078        let tmp_dir = tempdir()?;
1079        let path = tmp_dir.path().join("test.gsd");
1080        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1081        hoomd_gsd_file.append_frame(0)?;
1082        hoomd_gsd_file
1083            .append_frame(1)?
1084            .configuration_dimensions(Dimensions::Two)?;
1085        hoomd_gsd_file
1086            .append_frame(2)?
1087            .configuration_dimensions(Dimensions::Two)?;
1088        drop(hoomd_gsd_file);
1089
1090        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1091        check!(gsd_file.find_chunk(0, "configuration/dimensions") == None);
1092
1093        let data = gsd_file.iter_scalars::<u8>(1, "configuration/dimensions")?;
1094        itertools::assert_equal(data, [2]);
1095
1096        let data = gsd_file.iter_scalars::<u8>(2, "configuration/dimensions")?;
1097        itertools::assert_equal(data, [2]);
1098
1099        Ok(())
1100    }
1101
1102    #[test]
1103    fn configuration_box() -> anyhow::Result<()> {
1104        let tmp_dir = tempdir()?;
1105        let path = tmp_dir.path().join("test.gsd");
1106        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1107        hoomd_gsd_file.append_frame(0)?;
1108        hoomd_gsd_file
1109            .append_frame(1)?
1110            .configuration_box([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])?;
1111        drop(hoomd_gsd_file);
1112
1113        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1114        check!(gsd_file.find_chunk(0, "configuration/box") == None);
1115
1116        let data = gsd_file.iter_scalars::<f32>(1, "configuration/box")?;
1117        itertools::assert_equal(data, [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
1118
1119        Ok(())
1120    }
1121
1122    #[test]
1123    fn particles_position() -> anyhow::Result<()> {
1124        let tmp_dir = tempdir()?;
1125        let path = tmp_dir.path().join("test.gsd");
1126        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1127        hoomd_gsd_file.append_frame(0)?;
1128        hoomd_gsd_file.append_frame(1)?.particles_position([
1129            [1.0, 2.0, 4.0].into(),
1130            [3.0, 4.0, 8.0].into(),
1131            [5.0, 6.0, 16.0].into(),
1132        ])?;
1133        drop(hoomd_gsd_file);
1134
1135        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1136        check!(gsd_file.find_chunk(0, "particles/position") == None);
1137        check!(gsd_file.find_chunk(0, "particles/N") == None);
1138
1139        let data = gsd_file.iter_arrays::<f32, 3>(1, "particles/position")?;
1140        itertools::assert_equal(data, [[1.0, 2.0, 4.0], [3.0, 4.0, 8.0], [5.0, 6.0, 16.0]]);
1141        let data = gsd_file.iter_scalars::<u32>(1, "particles/N")?;
1142        itertools::assert_equal(data, [3_u32]);
1143
1144        Ok(())
1145    }
1146
1147    #[test]
1148    fn particles_orientation() -> anyhow::Result<()> {
1149        let tmp_dir = tempdir()?;
1150        let path = tmp_dir.path().join("test.gsd");
1151        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1152        hoomd_gsd_file.append_frame(0)?;
1153        hoomd_gsd_file
1154            .append_frame(1)?
1155            .particles_orientation([Versor::default(); 3])?;
1156        drop(hoomd_gsd_file);
1157
1158        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1159        check!(gsd_file.find_chunk(0, "particles/orientation") == None);
1160        check!(gsd_file.find_chunk(0, "particles/N") == None);
1161
1162        let data = gsd_file.iter_arrays::<f32, 4>(1, "particles/orientation")?;
1163        itertools::assert_equal(
1164            data,
1165            [
1166                [1.0, 0.0, 0.0, 0.0],
1167                [1.0, 0.0, 0.0, 0.0],
1168                [1.0, 0.0, 0.0, 0.0],
1169            ],
1170        );
1171        let data = gsd_file.iter_scalars::<u32>(1, "particles/N")?;
1172        itertools::assert_equal(data, [3_u32]);
1173
1174        Ok(())
1175    }
1176
1177    #[test]
1178    fn particles_type_id() -> anyhow::Result<()> {
1179        let tmp_dir = tempdir()?;
1180        let path = tmp_dir.path().join("test.gsd");
1181        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1182        hoomd_gsd_file.append_frame(0)?;
1183        hoomd_gsd_file
1184            .append_frame(1)?
1185            .particles_type_id([0, 1, 1, 2])?;
1186        drop(hoomd_gsd_file);
1187
1188        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1189        check!(gsd_file.find_chunk(0, "particles/typeid") == None);
1190        check!(gsd_file.find_chunk(0, "particles/N") == None);
1191
1192        let data = gsd_file.iter_scalars::<u32>(1, "particles/typeid")?;
1193        itertools::assert_equal(data, [0_u32, 1, 1, 2]);
1194        let data = gsd_file.iter_scalars::<u32>(1, "particles/N")?;
1195        itertools::assert_equal(data, [4_u32]);
1196
1197        Ok(())
1198    }
1199
1200    #[test]
1201    fn particles_len_check() -> anyhow::Result<()> {
1202        let tmp_dir = tempdir()?;
1203        let path = tmp_dir.path().join("test.gsd");
1204        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1205        check!(let Err(AppendError::InconsistentLength(_, _)) = hoomd_gsd_file
1206            .append_frame(1)?
1207            .particles_type_id([0, 1, 1, 2])?
1208            .particles_orientation([Versor::default(); 3]));
1209
1210        Ok(())
1211    }
1212
1213    #[test]
1214    fn particles_types() -> anyhow::Result<()> {
1215        let tmp_dir = tempdir()?;
1216        let path = tmp_dir.path().join("test.gsd");
1217        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1218        hoomd_gsd_file.append_frame(0)?;
1219        hoomd_gsd_file
1220            .append_frame(1)?
1221            .particles_types(["A", "B", "linker"])?;
1222        drop(hoomd_gsd_file);
1223
1224        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1225        check!(gsd_file.find_chunk(0, "particles/types") == None);
1226
1227        let data: Vec<_> = gsd_file
1228            .iter_arrays::<u8, 64>(1, "particles/types")?
1229            .collect();
1230        assert!(data.len() == 3);
1231        check!(data[0][0] == "A".as_bytes()[0]);
1232        check!(data[0][1] == 0);
1233
1234        check!(data[1][0] == "B".as_bytes()[0]);
1235        check!(data[1][1] == 0);
1236
1237        check!(data[2][0..6] == *"linker".as_bytes());
1238        check!(data[0][6] == 0);
1239
1240        Ok(())
1241    }
1242
1243    #[test]
1244    fn log_scalar() -> anyhow::Result<()> {
1245        let tmp_dir = tempdir()?;
1246        let path = tmp_dir.path().join("test.gsd");
1247        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1248        hoomd_gsd_file.append_frame(0)?;
1249        hoomd_gsd_file
1250            .append_frame(1)?
1251            .log_scalar("test", 4.2_f64)?;
1252        drop(hoomd_gsd_file);
1253
1254        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1255        check!(gsd_file.find_chunk(0, "log/test") == None);
1256
1257        let data = gsd_file.iter_scalars::<f64>(1, "log/test")?;
1258        itertools::assert_equal(data, [4.2_f64]);
1259
1260        Ok(())
1261    }
1262
1263    #[test]
1264    fn log_scalars() -> anyhow::Result<()> {
1265        let tmp_dir = tempdir()?;
1266        let path = tmp_dir.path().join("test.gsd");
1267        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1268        hoomd_gsd_file.append_frame(0)?;
1269        hoomd_gsd_file
1270            .append_frame(1)?
1271            .log_scalars("test", [4.2_f64, 8.4])?;
1272        drop(hoomd_gsd_file);
1273
1274        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1275        check!(gsd_file.find_chunk(0, "log/test") == None);
1276
1277        let data = gsd_file.iter_scalars::<f64>(1, "log/test")?;
1278        itertools::assert_equal(data, [4.2_f64, 8.4]);
1279
1280        Ok(())
1281    }
1282
1283    #[test]
1284    fn log_arrays() -> anyhow::Result<()> {
1285        let tmp_dir = tempdir()?;
1286        let path = tmp_dir.path().join("test.gsd");
1287        let mut hoomd_gsd_file = HoomdGsdFile::create(path.clone())?;
1288        hoomd_gsd_file.append_frame(0)?;
1289        hoomd_gsd_file
1290            .append_frame(1)?
1291            .log_arrays("test", [[4.2_f64, 8.4], [1.0, 2.0]])?;
1292        drop(hoomd_gsd_file);
1293
1294        let gsd_file = GsdFile::open(path.clone(), Mode::Read)?;
1295        check!(gsd_file.find_chunk(0, "log/test") == None);
1296
1297        let data = gsd_file.iter_arrays::<f64, 2>(1, "log/test")?;
1298        itertools::assert_equal(data, [[4.2_f64, 8.4], [1.0, 2.0]]);
1299
1300        Ok(())
1301    }
1302}