chemfiles 0.8.0

Modern library for chemistry trajectories reading and writing
Documentation
// Chemfiles, a modern library for chemistry file reading and writing
// Copyright (C) 2015-2017 Guillaume Fraux
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
use std::ops::Drop;
use std::path::Path;
use std::ptr;

use chemfiles_sys::*;
use errors::{check, Error};
use strings;
use Result;

use {Frame, Topology, UnitCell};

/// The `Trajectory` type is the main entry point when using chemfiles. A
/// `Trajectory` behave a bit like a file, allowing to read and/or write
/// `Frame`.
pub struct Trajectory {
    handle: *const CHFL_TRAJECTORY,
}

impl Trajectory {
    /// Create a `Trajectory` from a C pointer.
    ///
    /// This function is unsafe because no validity check is made on the pointer,
    /// except for it being non-null.
    #[inline]
    #[doc(hidden)]
    pub unsafe fn from_ptr(ptr: *const CHFL_TRAJECTORY) -> Result<Trajectory> {
        if ptr.is_null() {
            Err(Error::null_ptr())
        } else {
            Ok(Trajectory { handle: ptr })
        }
    }

    /// Get the underlying C pointer as a const pointer.
    #[inline]
    #[doc(hidden)]
    pub fn as_ptr(&self) -> *const CHFL_TRAJECTORY {
        self.handle
    }

    /// Get the underlying C pointer as a mutable pointer.
    #[inline]
    #[doc(hidden)]
    pub fn as_mut_ptr(&mut self) -> *mut CHFL_TRAJECTORY {
        self.handle as *mut CHFL_TRAJECTORY
    }

    /// Open the file at the given `path` in the given `mode`.
    ///
    /// Valid modes are `'r'` for read, `'w'` for write and `'a'` for append.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::Trajectory;
    /// let trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// ```
    pub fn open<P>(path: P, mode: char) -> Result<Trajectory>
    where
        P: AsRef<Path>,
    {
        let path = try!(path.as_ref().to_str().ok_or(Error::utf8_path_error(path.as_ref())));

        let path = strings::to_c(path);
        unsafe {
            #[allow(cast_possible_wrap)]
            let handle = chfl_trajectory_open(path.as_ptr(), mode as i8);
            Trajectory::from_ptr(handle)
        }
    }

    /// Open the file at the given `path` using a specific file `format` and
    /// the given `mode`.
    ///
    /// Valid modes are `'r'` for read, `'w'` for write and `'a'` for append.
    ///
    /// Specifying a format is needed when the file format does not match the
    /// extension, or when there is not standard extension for this format. If
    /// `format` is an empty string, the format will be guessed from the
    /// extension.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::Trajectory;
    /// let trajectory = Trajectory::open_with_format("water.zeo", 'r', "XYZ").unwrap();
    /// ```
    pub fn open_with_format<'a, P, S>(filename: P, mode: char, format: S) -> Result<Trajectory>
    where
        P: AsRef<Path>,
        S: Into<&'a str>,
    {
        let filename =
            try!(filename.as_ref().to_str().ok_or(Error::utf8_path_error(filename.as_ref())));

        let filename = strings::to_c(filename);
        let format = strings::to_c(format.into());
        unsafe {
            #[allow(cast_possible_wrap)]
            let handle =
                chfl_trajectory_with_format(filename.as_ptr(), mode as i8, format.as_ptr());
            Trajectory::from_ptr(handle)
        }
    }

    /// Read the next step of this trajectory into a `frame`.
    ///
    /// If the number of atoms in frame does not correspond to the number of atom
    /// in the next step, the frame is resized.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::{Trajectory, Frame};
    /// let mut trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// let mut frame = Frame::new().unwrap();
    ///
    /// trajectory.read(&mut frame).unwrap();
    /// ```
    pub fn read(&mut self, frame: &mut Frame) -> Result<()> {
        unsafe { try!(check(chfl_trajectory_read(self.as_mut_ptr(), frame.as_mut_ptr()))) }
        Ok(())
    }

    /// Read a specific `step` of this trajectory into a `frame`.
    ///
    /// If the number of atoms in frame does not correspond to the number of
    /// atom at this step, the frame is resized.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::{Trajectory, Frame};
    /// let mut trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// let mut frame = Frame::new().unwrap();
    ///
    /// trajectory.read_step(10, &mut frame).unwrap();
    /// ```
    pub fn read_step(&mut self, step: u64, frame: &mut Frame) -> Result<()> {
        unsafe {
            try!(check(chfl_trajectory_read_step(self.as_mut_ptr(), step, frame.as_mut_ptr())))
        }
        Ok(())
    }

    /// Write a `frame` to this trajectory.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::{Trajectory, Frame};
    /// let mut trajectory = Trajectory::open("water.pdb", 'w').unwrap();
    /// let mut frame = Frame::new().unwrap();
    ///
    /// trajectory.write(&mut frame).unwrap();
    /// ```
    pub fn write(&mut self, frame: &Frame) -> Result<()> {
        unsafe { try!(check(chfl_trajectory_write(self.as_mut_ptr(), frame.as_ptr()))) }
        Ok(())
    }

    /// Set the `topology` associated with this trajectory. This topology will
    /// be used when reading and writing the files, replacing any topology in
    /// the frames or files.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::{Trajectory, Atom, Topology};
    /// let mut topology = Topology::new().unwrap();
    /// topology.add_atom(&Atom::new("H").unwrap()).unwrap();
    /// topology.add_atom(&Atom::new("O").unwrap()).unwrap();
    /// topology.add_atom(&Atom::new("H").unwrap()).unwrap();
    /// topology.add_bond(0, 1).unwrap();
    /// topology.add_bond(1, 2).unwrap();
    ///
    /// let mut trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// trajectory.set_topology(&topology).unwrap();
    /// ```
    pub fn set_topology(&mut self, topology: &Topology) -> Result<()> {
        unsafe { try!(check(chfl_trajectory_set_topology(self.as_mut_ptr(), topology.as_ptr()))) }
        Ok(())
    }

    /// Set the topology associated with this trajectory by reading the first
    /// frame of the file at the given `path` using the file format in
    /// `format`; and extracting the topology of this frame.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::Trajectory;
    /// let mut trajectory = Trajectory::open("water.nc", 'r').unwrap();
    /// trajectory.set_topology_file("topology.pdb").unwrap();
    /// ```
    pub fn set_topology_file<P>(&mut self, path: P) -> Result<()>
    where
        P: AsRef<Path>,
    {
        let path = try!(path.as_ref().to_str().ok_or(Error::utf8_path_error(path.as_ref())));

        let path = strings::to_c(path);
        unsafe {
            try!(check(
                chfl_trajectory_topology_file(self.as_mut_ptr(), path.as_ptr(), ptr::null())
            ))
        }
        Ok(())
    }

    /// Set the topology associated with this trajectory by reading the first
    /// frame of the file at the given `path` using the file format in
    /// `format`; and extracting the topology of this frame.
    ///
    /// If `format` is an empty string, the format will be guessed from the
    /// `path` extension.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::Trajectory;
    /// let mut trajectory = Trajectory::open("water.nc", 'r').unwrap();
    /// trajectory.set_topology_with_format("topology.mol", "PDB").unwrap();
    /// ```
    pub fn set_topology_with_format<'a, P, S>(&mut self, path: P, format: S) -> Result<()>
    where
        P: AsRef<Path>,
        S: Into<&'a str>,
    {
        let path = try!(path.as_ref().to_str().ok_or(Error::utf8_path_error(path.as_ref())));

        let format = strings::to_c(format.into());
        let path = strings::to_c(path);
        unsafe {
            try!(check(
                chfl_trajectory_topology_file(self.as_mut_ptr(), path.as_ptr(), format.as_ptr())
            ))
        }
        Ok(())
    }

    /// Set the unit `cell` associated with a trajectory. This cell will be
    /// used when reading and writing the files, replacing any unit cell in the
    /// frames or files.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::{Trajectory, UnitCell};
    /// let mut trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// trajectory.set_cell(&UnitCell::new([10.0, 11.0, 12.5]).unwrap()).unwrap();
    /// ```
    pub fn set_cell(&mut self, cell: &UnitCell) -> Result<()> {
        unsafe {
            try!(check(chfl_trajectory_set_cell(self.as_mut_ptr(), cell.as_ptr())));
        }
        Ok(())
    }

    /// Get the number of steps (the number of frames) in a trajectory.
    ///
    /// # Example
    /// ```no_run
    /// # use chemfiles::Trajectory;
    /// let mut trajectory = Trajectory::open("water.xyz", 'r').unwrap();
    /// let steps = trajectory.nsteps().unwrap();
    ///
    /// println!("This trajectory contains {} steps", steps);
    /// ```
    // FIXME should this take &self instead? The file can be modified by this
    // function, but the format should reset the state.
    pub fn nsteps(&mut self) -> Result<u64> {
        let mut res = 0;
        unsafe {
            try!(check(chfl_trajectory_nsteps(self.as_mut_ptr(), &mut res)));
        }
        Ok(res)
    }
}

impl Drop for Trajectory {
    fn drop(&mut self) {
        unsafe {
            let status = chfl_trajectory_close(self.as_mut_ptr());
            debug_assert_eq!(status, chfl_status::CHFL_SUCCESS);
        }
    }
}

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

    use std::fs;
    use std::path::Path;
    use std::io::Read;

    use {Atom, Frame, Topology, UnitCell};

    #[test]
    fn read() {
        let root = Path::new(file!()).parent().unwrap().join("..");
        let filename = root.join("data").join("water.xyz");
        let mut file = Trajectory::open(filename.to_str().unwrap(), 'r').unwrap();

        assert_eq!(file.nsteps(), Ok(100));

        let mut frame = Frame::new().unwrap();
        assert!(file.read(&mut frame).is_ok());

        assert_eq!(frame.size(), Ok(297));

        {
            let positions = frame.positions().unwrap();
            assert_eq!(positions[0], [0.417219, 8.303366, 11.737172]);
            assert_eq!(positions[124], [5.099554, -0.045104, 14.153846]);
        }

        assert_eq!(frame.atom(0).unwrap().name(), Ok(String::from("O")));

        assert!(file.set_cell(&UnitCell::new([30.0, 30.0, 30.0]).unwrap()).is_ok());

        assert!(file.read_step(41, &mut frame).is_ok());
        let cell = frame.cell().unwrap();
        assert_eq!(cell.lengths(), Ok([30.0, 30.0, 30.0]));

        {
            let positions = frame.positions().unwrap();
            assert_eq!(positions[0], [0.761277, 8.106125, 10.622949]);
            assert_eq!(positions[124], [5.13242, 0.079862, 14.194161]);
        }

        let topology = frame.topology().unwrap();
        assert_eq!(topology.size(), Ok(297));
        assert_eq!(topology.bonds_count(), Ok(0));

        assert!(frame.guess_topology().is_ok());
        let topology = frame.topology().unwrap();
        assert_eq!(topology.size(), Ok(297));
        assert_eq!(topology.bonds_count(), Ok(181));
        assert_eq!(topology.angles_count(), Ok(87));

        let mut topology = Topology::new().unwrap();
        let atom = Atom::new("Cs").unwrap();
        for _ in 0..297 {
            topology.add_atom(&atom).unwrap();
        }

        assert!(file.set_topology(&topology).is_ok());
        assert!(file.read_step(10, &mut frame).is_ok());
        assert_eq!(frame.atom(42).unwrap().name(), Ok(String::from("Cs")));

        let filename = root.join("data").join("topology.xyz");
        assert!(file.set_topology_file(filename.to_str().unwrap()).is_ok());
        assert!(file.read(&mut frame).is_ok());
        assert_eq!(frame.atom(100).unwrap().name(), Ok(String::from("Rd")));

        let filename = root.join("data").join("helium.xyz.but.not.really");
        let filename = filename.to_str().unwrap();
        let mut file = Trajectory::open_with_format(filename, 'r', "XYZ").unwrap();
        assert!(file.read(&mut frame).is_ok());
        assert_eq!(frame.size(), Ok(125));
    }

    fn write_file(path: &str) {
        let mut file = Trajectory::open(path, 'w').unwrap();
        let mut frame = Frame::new().unwrap();
        frame.resize(4).unwrap();

        {
            let positions = frame.positions_mut().unwrap();
            for i in 0..positions.len() {
                positions[i] = [1.0, 2.0, 3.0];
            }
        }

        let mut topology = Topology::new().unwrap();
        let atom = Atom::new("X").unwrap();
        for _ in 0..4 {
            topology.add_atom(&atom).unwrap();
        }
        frame.set_topology(&topology).unwrap();
        assert!(file.write(&frame).is_ok());
    }

    #[test]
    fn write() {
        let filename = "test-tmp.xyz";
        write_file(filename);

        let expected_content = "4
Written by the chemfiles library
X 1 2 3
X 1 2 3
X 1 2 3
X 1 2 3".lines().collect::<Vec<_>>();

        let mut file = fs::File::open(filename).unwrap();
        let mut content = String::new();
        let _ = file.read_to_string(&mut content).unwrap();

        assert_eq!(expected_content, content.lines().collect::<Vec<_>>());
        fs::remove_file(filename).unwrap();
    }
}