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}