diff --git a/Cargo.lock b/Cargo.lock index 8e33374..27e5366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -276,7 +285,7 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" [[package]] name = "gcenter" -version = "1.0.4" +version = "1.1.0" dependencies = [ "assert_cmd", "backitup", @@ -297,13 +306,14 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "groan_rs" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0763c3647f2fa8af2801805b49e047058a93f8a9e65b4a8e0d1fadcaf0f8dbef" +checksum = "6a1fa86d0c89e3f90a982e2a8fc905cc5e56c2457f09bc2e5819160f9b241834" dependencies = [ "cc", "colored", "indexmap", + "regex", "thiserror", ] @@ -496,11 +506,34 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rustix" diff --git a/Cargo.toml b/Cargo.toml index 6d8b052..f62814c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,13 @@ name = "gcenter" authors = ["Ladislav Bartos "] description = "Center Any Group in a Gromacs Trajectory" -version = "1.0.4" +version = "1.1.0" license = "MIT" edition = "2021" repository = "https://github.com/Ladme/gcenter" keywords = ["gromacs", "molecular-dynamics"] categories = ["command-line-utilities", "science"] +exclude = ["/tests"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -15,7 +16,7 @@ categories = ["command-line-utilities", "science"] backitup = "0.1.1" clap = { version = "4.4.3", features = ["derive"] } colored = "2.0.4" -groan_rs = "0.3.3" +groan_rs = "0.4.1" thiserror = "1.0.48" [dev-dependencies] diff --git a/src/argparse.rs b/src/argparse.rs new file mode 100644 index 0000000..657e489 --- /dev/null +++ b/src/argparse.rs @@ -0,0 +1,222 @@ +// Released under MIT License. +// Copyright (c) 2023 Ladislav Bartos + +//! Implementation of a command line argument parser. + +use std::path::Path; + +use clap::Parser; +use groan_rs::files::FileType; + +use crate::errors::RunError; + +// Center Gromacs trajectory or structure file. +#[derive(Parser, Debug)] +#[command( + author, + version, + about, + long_about = "Center your chosen group within a Gromacs trajectory or structure file effortlessly using the Bai & Breen algorithm.\n +With `gcenter`, you can accurately center atom groups, even when they span multiple molecules that may extend beyond the box boundaries. +`gcenter` does not employ connectivity information, so it doesn't require a tpr file as input. +Be aware that `gcenter` exclusively supports orthogonal simulation boxes." +)] +pub struct Args { + #[arg( + short = 'c', + long = "structure", + help = "Input structure file", + long_help = "Path to a gro or pdb file containing the system structure. If a trajectory is also provided, the coordinates from the structure file will be ignored.", + value_parser = validate_structure_type, + )] + pub structure: String, + + #[arg( + short = 'f', + long = "trajectory", + help = "Input trajectory file(s)", + long_help = "Path to xtc or trr file(s) containing the trajectory or trajectories to be manipulated. +If not provided, the centering operation will use the structure file itself. +Multiple files separated by whitespace can be provided. These will be concatenated into one output file. +When joining trajectories, the last frame of each trajectory and the first frame of the following trajectory are checked for matching simulation steps. +If the simulation steps coincide, only the first of these frames is centered and written to output.", + num_args = 0.., + value_parser = validate_trajectory_type, + )] + pub trajectories: Vec, + + #[arg( + short = 'n', + long = "index", + help = "Input index file [default: index.ndx]", + long_help = "Path to an ndx file containing groups associated with the system.\n\n[default: index.ndx]" + )] + pub index: Option, + + #[arg( + short = 'o', + long = "output", + help = "Output file name", + long_help = "Name of the output file, which can be in gro, pdb (if no trajectory is provided), xtc, or trr format." + )] + pub output: String, + + #[arg( + short = 'r', + long = "reference", + help = "Group to center", + default_value = "Protein", + long_help = "Specify the group to be centered. Use VMD-like 'groan selection language' to define the group. This language also supports ndx group names." + )] + pub reference: String, + + #[arg( + short = 'b', + long = "begin", + help = "Time of the first frame to read (in ps) [default: 0.0]", + requires = "trajectories", + long_help = "Time of the first frame to read from the trajectory (in ps). All previous frames will be skipped. This option is only applicable when trajectory file(s) is/are provided.\n\n[default: 0.0]" + )] + pub start_time: Option, + + #[arg( + short = 'e', + long = "end", + help = "Time of the last frame to read (in ps) [default: NaN]", + requires = "trajectories", + long_help = "Time of the last frame to read from the trajectory (in ps). All following frames will be skipped. This option is only applicable when trajectory file(s) is/are provided.\n\n[default: NaN]" + )] + pub end_time: Option, + + #[arg( + short = 's', + long = "step", + help = "Write every th frame", + default_value_t = 1, + requires = "trajectories", + long_help = "Center and write only every th frame of the trajectory to the output file. This option is only applicable when a SINGLE trajectory file is provided." + )] + pub step: usize, + + #[arg( + short = 'x', + action, + help = "Center in the x dimension", + default_value_t = false, + long_help = "Perform centering operation in the x-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." + )] + pub xdimension: bool, + + #[arg( + short = 'y', + action, + help = "Center in the y dimension", + default_value_t = false, + long_help = "Perform centering operation in the y-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." + )] + pub ydimension: bool, + + #[arg( + short = 'z', + action, + help = "Center in the z dimension", + default_value_t = false, + long_help = "Perform centering operation in the z-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." + )] + pub zdimension: bool, + + #[arg( + long = "silent", + action, + help = "Suppress standard output", + default_value_t = false, + long_help = "Suppress all standard output generated by the 'gcenter' tool, except for error messages written to stderr." + )] + pub silent: bool, + + #[arg( + long = "overwrite", + action, + help = "Overwrite existing files with the same name", + default_value_t = false, + long_help = "Enable this option to overwrite existing files with the same name as the output file. No backup copies will be created." + )] + pub overwrite: bool, +} + +/// Validate that the structure is gro or pdb file. +fn validate_structure_type(s: &str) -> Result { + match FileType::from_name(s) { + FileType::GRO | FileType::PDB => Ok(s.to_owned()), + _ => Err(String::from("unsupported file extension")), + } +} + +/// Validate that the trajectories are xtc or trr files. +/// Validate that no trajectory is provided multiple times. +fn validate_trajectory_type(s: &str) -> Result { + match FileType::from_name(s) { + FileType::XTC | FileType::TRR => Ok(s.to_owned()), + _ => Err(String::from("unsupported file extension")), + } +} + +/// Perform various sanity checks: +/// a) Check that the input and output files are not identical. +/// This protects the user from accidentaly overwriting their data. +/// b) Check that the output file has the correct file extension. +fn sanity_check_inputs(args: &Args) -> Result<(), RunError> { + // check that the input structure exists + if !Path::new(&args.structure).exists() { + return Err(RunError::InputStructureNotFound(args.structure.to_string())); + } + + // check for input-output matches + if args.trajectories.is_empty() { + if args.structure == args.output { + return Err(RunError::IOMatch(args.structure.to_string())); + } + } else { + for (t, traj) in args.trajectories.iter().enumerate() { + // check that the trajectory exists + if !Path::new(traj).exists() { + return Err(RunError::InputTrajectoryNotFound(traj.to_string())); + } + + // check that the trajectory does not match the output + if traj.as_str() == args.output { + return Err(RunError::IOMatch(traj.to_string())); + } + + // check that no other trajectory file matches this one + for traj2 in args.trajectories.iter().skip(t + 1) { + if traj == traj2 { + return Err(RunError::IdenticalInputFiles( + traj.to_owned(), + traj2.to_owned(), + )); + } + } + } + } + + if args.step != 1 && args.trajectories.len() > 1 { + return Err(RunError::StepJoinUnsupported(args.step)); + } + + // check the extension of the output file + let output_type = FileType::from_name(&args.output); + match (args.trajectories.is_empty(), output_type) { + (true, FileType::GRO | FileType::PDB) => Ok(()), + (true, _) => Err(RunError::OutputUnsupported(args.output.clone())), + (false, FileType::XTC | FileType::TRR) => Ok(()), + (false, _) => Err(RunError::OutputUnsupported(args.output.clone())), + } +} + +pub fn parse() -> Result> { + let args = Args::parse(); + sanity_check_inputs(&args)?; + + Ok(args) +} diff --git a/src/center.rs b/src/center.rs new file mode 100644 index 0000000..6c65e1d --- /dev/null +++ b/src/center.rs @@ -0,0 +1,309 @@ +// Released under MIT License. +// Copyright (c) 2023 Ladislav Bartos + +//! Implementation of the centering procedure. + +use colored::{ColoredString, Colorize}; +use groan_rs::errors::ReadTrajError; +use groan_rs::files::FileType; +use groan_rs::prelude::*; +use std::io::{self, Write}; +use std::path::Path; + +use crate::argparse::Args; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ProgressStatus { + Running, + Completed, + Failed, +} + +/// Structure containing formatting for the printing of the centering progress. +#[derive(Debug, PartialEq)] +struct ProgressPrinter { + status: ProgressStatus, + print_freq: u64, + silent: bool, + step_fmt: ColoredString, + time_fmt: ColoredString, + running_fmt: ColoredString, + completed_fmt: ColoredString, + failed_fmt: ColoredString, +} + +impl ProgressPrinter { + /// Create default instance of the `ProgressPrinter`. + fn new(silent: bool) -> Self { + ProgressPrinter { + silent, + print_freq: 500000, + status: ProgressStatus::Running, + step_fmt: "Step:".cyan(), + time_fmt: "Time:".bright_purple(), + running_fmt: "CENTERING".yellow(), + completed_fmt: "COMPLETED".green(), + failed_fmt: " FAILED! ".red(), + } + } + + /// Print progress info with formatting from `ProgressPrinter`. + fn print(&self, sim_step: u64, sim_time: u64) { + if !self.silent + && (self.status != ProgressStatus::Running || sim_step % self.print_freq == 0) + { + match self.status { + ProgressStatus::Running => print!("[{: ^9}] ", self.running_fmt), + ProgressStatus::Completed => print!("[{: ^9}] ", self.completed_fmt), + ProgressStatus::Failed => print!("[{: ^9}] ", self.failed_fmt), + } + + print!( + "{} {:12} | {} {:12} ps\r", + self.step_fmt, sim_step, self.time_fmt, sim_time + ); + + io::stdout().flush().unwrap(); + } + } + + fn set_status(&mut self, status: ProgressStatus) { + self.status = status; + } +} + +/// Center the reference group and write an output gro or pdb file. +fn center_structure_file( + system: &mut System, + output: &str, + output_type: FileType, + dimension: Dimension, +) -> Result<(), Box> { + system.atoms_center("Reference", dimension)?; + + match output_type { + FileType::GRO => system.write_gro(output, system.has_velocities())?, + FileType::PDB => system.write_pdb(output)?, + _ => panic!("Gcenter error. Output file has unsupported file extension but this should have been handled before."), + } + + Ok(()) +} + +/// Center a single simulation frame. +fn center_frame( + frame: &mut System, + dimension: Dimension, + writer: &mut impl TrajWrite, + printer: &ProgressPrinter, +) -> Result<(), Box> { + printer.print( + frame.get_simulation_step(), + frame.get_simulation_time() as u64, + ); + frame.atoms_center("Reference", dimension)?; + writer.write_frame()?; + + Ok(()) +} + +/// Check for duplicate frames at trajectory boundaries and center the frame. +fn handle_frame( + frame: Result<&mut System, ReadTrajError>, + dimension: Dimension, + writer: &mut impl TrajWrite, + printer: &ProgressPrinter, + last_step: Option, + is_first_frame: &mut bool, +) -> Result<(), Box> { + let frame = frame?; + + if *is_first_frame { + if let Some(step) = last_step { + // skip this step if it matches the previous one + if frame.get_simulation_step() == step { + *is_first_frame = false; + return Ok(()); + } + } + *is_first_frame = false; + } + + center_frame(frame, dimension, writer, printer) +} + +/// Center any trajectory file. +fn center_traj_file<'a, Reader>( + system: &'a mut System, + trajectory: impl AsRef, + writer: &mut impl TrajWrite, + start_time: Option, + end_time: Option, + step: usize, + dimension: Dimension, + printer: &ProgressPrinter, + last_step: Option, + is_last_file: bool, +) -> Result<(), Box> +where + Reader: TrajRead<'a> + TrajRangeRead<'a> + TrajStepRead<'a> + 'a, + Reader::FrameData: FrameDataTime, +{ + let mut reader = system.traj_iter::(&trajectory)?; + + let mut is_first_frame = true; + + let process_frame = |frame| { + handle_frame( + frame, + dimension, + writer, + printer, + last_step, + &mut is_first_frame, + ) + }; + + match (start_time, end_time, step) { + (Some(s), _, 1) => match reader.with_range(s, end_time.unwrap_or(f32::MAX)) { + Ok(mut r) => r.try_for_each(process_frame)?, + Err(ReadTrajError::StartNotFound(_)) if !is_last_file => (), + Err(e) => return Err(Box::new(e)), + }, + + (_, Some(e), 1) => match reader.with_range(start_time.unwrap_or(0.0), e) { + Ok(mut r) => r.try_for_each(process_frame)?, + Err(ReadTrajError::StartNotFound(_)) if !is_last_file => (), + Err(e) => return Err(Box::new(e)), + }, + + (None, None, 1) => reader.try_for_each(process_frame)?, + + (Some(s), _, step) => match reader.with_range(s, end_time.unwrap_or(f32::MAX)) { + Ok(r) => r.with_step(step)?.try_for_each(process_frame)?, + Err(ReadTrajError::StartNotFound(_)) if !is_last_file => (), + Err(e) => return Err(Box::new(e)), + }, + + (_, Some(e), step) => match reader.with_range(start_time.unwrap_or(0.0), e) { + Ok(r) => r.with_step(step)?.try_for_each(process_frame)?, + Err(ReadTrajError::StartNotFound(_)) if !is_last_file => (), + Err(e) => return Err(Box::new(e)), + }, + + (None, None, step) => reader.with_step(step)?.try_for_each(process_frame)?, + }; + + Ok(()) +} + +/// Center all the provided trajectories. +fn center_trajectories( + system: &mut System, + args: &Args, + writer: &mut impl TrajWrite, + dimension: Dimension, + printer: &ProgressPrinter, +) -> Result<(), Box> { + for (t, traj) in args.trajectories.iter().enumerate() { + let input_type = FileType::from_name(traj); + + let last_step = match t { + 0 => None, + _ => Some(system.get_simulation_step()), + }; + + // check whether this is a last file + let is_last_file = t == args.trajectories.len() - 1; + + match input_type { + FileType::XTC => center_traj_file::( + system, + traj, + writer, + args.start_time, + args.end_time, + args.step, + dimension, + printer, + last_step, + is_last_file, + )?, + + FileType::TRR => center_traj_file::( + system, + traj, + writer, + args.start_time, + args.end_time, + args.step, + dimension, + printer, + last_step, + is_last_file, + )?, + + _ => panic!("Gcenter error. Input file has unsupported file extension but this should have been handled before."), + } + } + + Ok(()) +} + +/// Center the structure or trajectory file. +pub fn center( + system: &mut System, + args: &Args, + dimension: Dimension, +) -> Result<(), Box> { + // determine type of the output file + let output_type = FileType::from_name(&args.output); + + if args.trajectories.is_empty() { + // trajectory file not provided, center the structure file + center_structure_file(system, &args.output, output_type, dimension)?; + } else { + let mut printer = ProgressPrinter::new(args.silent); + + let return_type = match output_type { + FileType::XTC => { + let mut writer = XtcWriter::new(system, &args.output)?; + center_trajectories(system, args, &mut writer, dimension, &printer) + } + FileType::TRR => { + let mut writer = TrrWriter::new(system, &args.output)?; + center_trajectories(system, args, &mut writer, dimension, &printer) + } + _ => panic!("Gcenter error. Output file has unsupported file extension but this should have been handled before."), + }; + + match return_type { + Ok(_) => { + printer.set_status(ProgressStatus::Completed); + printer.print( + system.get_simulation_step(), + system.get_simulation_time() as u64, + ); + + if !args.silent { + println!("\n") + } + } + Err(e) => { + printer.set_status(ProgressStatus::Failed); + printer.print( + system.get_simulation_step(), + system.get_simulation_time() as u64, + ); + + if !args.silent { + println!("\n") + } + + return Err(e); + } + } + } + + Ok(()) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..77e7522 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,53 @@ +// Released under MIT License. +// Copyright (c) 2023 Ladislav Bartos + +//! Implementation of errors originating from the `gcenter` program. + +use colored::Colorize; +use thiserror::Error; + +/// Errors originating directly from `gcenter`. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum RunError { + #[error("{} invalid value '{}' for '{}': output path matches input path\n\nFor more information, try '{}'.", "error:".red().bold(), .0.yellow(), "--output ".bold(), "--help".bold())] + IOMatch(String), + #[error("{} invalid value '{}' for '{}': unsupported file extension\n\nFor more information, try '{}'.", "error:".red().bold(), .0.yellow(), "--output ".bold(), "--help".bold())] + OutputUnsupported(String), + #[error("{} invalid values '{}' and '{}' for '{}': paths correspond to the same file\n\nFor more information, try '{}'.", "error:".red().bold(), .0.yellow(), .1.yellow(), "--trajectory [...]".bold(), "--help".bold())] + IdenticalInputFiles(String, String), + #[error("{} invalid value '{}' for '{}': when multiple input trajectories are provided, must be 1\n\nFor more information, try '{}'.", "error:".red().bold(), .0.to_string().yellow(), "--step ".bold(), "--help".bold())] + StepJoinUnsupported(usize), + #[error("{} invalid value '{}' for '{}': input structure file does not exist\n\nFor more information, try '{}'.", "error:".red().bold(), .0.to_string().yellow(), "--structure ".bold(), "--help".bold())] + InputStructureNotFound(String), + #[error("{} invalid value '{}' for '{}': input trajectory file does not exist\n\nFor more information, try '{}'.", "error:".red().bold(), .0.to_string().yellow(), "--trajectory [...]".bold(), "--help".bold())] + InputTrajectoryNotFound(String), + #[error("{} reference group '{}' is empty\n", "error:".red().bold(), .0.yellow())] + EmptyReference(String), + #[error("{} no protein atoms autodetected\n", "error:".red().bold())] + AutodetectionFailed, + #[error("{} simulation box is not orthogonal; this is not supported, sorry\n", "error:".red().bold())] + BoxNotOrthogonal, + #[error("{} simulation box is not a valid simulation box; some required dimensions are not positive\n", "error:".red().bold())] + BoxNotValid, +} + +/* // [DEV] print all RunErrors +pub fn print_all_errors() { + let errors = vec![ + RunError::IOMatch(String::from("N/A")), + RunError::OutputUnsupported(String::from("N/A")), + RunError::IdenticalInputFiles(String::from("N/A"), String::from("N/A")), + RunError::StepJoinUnsupported(3), + RunError::InputStructureNotFound(String::from("N/A")), + RunError::InputTrajectoryNotFound(String::from("N/A")), + RunError::EmptyReference(String::from("N/A")), + RunError::AutodetectionFailed, + RunError::BoxNotOrthogonal, + RunError::BoxNotValid, + ]; + + for e in errors { + println!("{}", e); + } +} +*/ diff --git a/src/lib.rs b/src/lib.rs index 49fcfe4..87baa31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,313 +1,69 @@ // Released under MIT License. // Copyright (c) 2023 Ladislav Bartos -use clap::Parser; -use colored::{ColoredString, Colorize}; -use std::io::{self, Write}; -use std::path::Path; -use thiserror::Error; +mod argparse; +mod center; +mod errors; +mod reference; -use groan_rs::errors::GroupError; -use groan_rs::files::FileType; +use colored::Colorize; use groan_rs::prelude::*; +use std::path::Path; -/// Frequency of printing during analysis of an xtc file. -const PRINT_FREQ: u64 = 500000; - -// Center Gromacs trajectory or structure file. -#[derive(Parser, Debug)] -#[command( - author, - version, - about, - long_about = "Center your chosen group within a Gromacs trajectory or structure file effortlessly using the Bai & Breen algorithm.\n -With `gcenter`, you can accurately center atom groups, even when they span multiple molecules that may extend beyond the box boundaries. -`gcenter` does not employ connectivity information, so it doesn't require a tpr file as input. -Be aware that `gcenter` exclusively supports orthogonal simulation boxes." -)] -pub struct Args { - #[arg( - short = 'c', - long = "structure", - help = "Input structure file", - long_help = "Path to a gro or pdb file containing the system structure. If a trajectory is also provided, the coordinates from the structure file will be ignored.", - value_parser = validate_structure_type, - )] - structure: String, - - #[arg( - short = 'f', - long = "trajectory", - help = "Input trajectory file", - long_help = "Path to an xtc or trr file containing the trajectory to be manipulated. If not provided, the centering operation will use the structure file itself.", - value_parser = validate_trajectory_type, - )] - trajectory: Option, - - #[arg( - short = 'n', - long = "index", - help = "Input index file [default: index.ndx]", - long_help = "Path to an ndx file containing groups associated with the system.\n\n[default: index.ndx]" - )] - index: Option, - - #[arg( - short = 'o', - long = "output", - help = "Output file name", - long_help = "Name of the output file, which can be in gro, pdb (if no trajectory is provided), xtc, or trr format." - )] - output: String, - - #[arg( - short = 'r', - long = "reference", - help = "Group to center", - default_value = "Protein", - long_help = "Specify the group to be centered. Use VMD-like 'groan selection language' to define the group. This language also supports ndx group names." - )] - reference: String, - - #[arg( - short = 's', - long = "step", - help = "Write every th frame", - default_value_t = 1, - requires = "trajectory", - long_help = "Center and write only every th frame of the trajectory to the output file. This option is only applicable when a trajectory file is provided." - )] - step: usize, - - #[arg( - short = 'x', - action, - help = "Center in the x dimension", - default_value_t = false, - long_help = "Perform centering operation in the x-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." - )] - xdimension: bool, - - #[arg( - short = 'y', - action, - help = "Center in the y dimension", - default_value_t = false, - long_help = "Perform centering operation in the y-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." - )] - ydimension: bool, - - #[arg( - short = 'z', - action, - help = "Center in the z dimension", - default_value_t = false, - long_help = "Perform centering operation in the z-dimension. This can be combined with other dimensions. If no dimensions are selected, it defaults to '-xyz'." - )] - zdimension: bool, - - #[arg( - long = "silent", - action, - help = "Suppress standard output", - default_value_t = false, - long_help = "Suppress all standard output generated by the 'gcenter' tool, except for error messages written to stderr." - )] - silent: bool, - - #[arg( - long = "overwrite", - action, - help = "Overwrite existing files with the same name", - default_value_t = false, - long_help = "Enable this option to overwrite existing files with the same name as the output file. No backup copies will be created." - )] - overwrite: bool, -} - -/// Errors originating directly from `gcenter`. -#[derive(Error, Debug, PartialEq, Eq)] -pub enum RunError { - #[error("{} invalid value '{}' for '{}': output path matches input path\n\nFor more information, try '{}'.", "error:".red().bold(), .0.yellow(), "--output ".bold(), "--help".bold())] - IOMatch(String), - #[error("{} invalid value '{}' for '{}': unsupported file extension\n\nFor more information, try '{}'.", "error:".red().bold(), .0.yellow(), "--output ".bold(), "--help".bold())] - OutputUnsupported(String), - #[error("{} reference group '{}' is empty", "error:".red().bold(), .0.yellow())] - EmptyReference(String), - #[error("{} no protein atoms autodetected", "error:".red().bold())] - AutodetectionFailed, - #[error("{} simulation box is not orthogonal; this is not supported, sorry", "error:".red().bold())] - BoxNotOrthogonal, - #[error("{} simulation box is not a valid simulation box; some required dimensions are not positive", "error:".red().bold())] - BoxNotValid, - #[error("{} output file '{}' has unsupported file extension", "error:".red().bold(), .0.yellow())] - UnsupportedFileExtension(String), -} - -/// Validate that the structure is gro or pdb file. -fn validate_structure_type(s: &str) -> Result { - match FileType::from_name(s) { - FileType::GRO | FileType::PDB => Ok(s.to_owned()), - _ => Err(String::from("unsupported file extension")), - } -} - -/// Validate that the trajectory is xtc or trr file. -fn validate_trajectory_type(s: &str) -> Result { - match FileType::from_name(s) { - FileType::XTC | FileType::TRR => Ok(s.to_owned()), - _ => Err(String::from("unsupported file extension")), - } -} - -/// Check that the input and output files are not identical. -/// This protects the user from accidentaly overwriting their data. -/// Check that the output file has the correct file extension. -fn sanity_check_files(args: &Args) -> Result<(), RunError> { - if args.trajectory.is_none() { - if args.structure == args.output { - return Err(RunError::IOMatch(args.structure.to_string())); - } - } else if *args.trajectory.as_ref().unwrap() == args.output { - return Err(RunError::IOMatch( - args.trajectory.as_ref().unwrap().to_string(), - )); - } - - let output_type = FileType::from_name(&args.output); - - match (&args.trajectory, output_type) { - (None, FileType::GRO | FileType::PDB) => Ok(()), - (None, _) => Err(RunError::OutputUnsupported(args.output.clone())), - (Some(_), FileType::XTC | FileType::TRR) => Ok(()), - (Some(_), _) => Err(RunError::OutputUnsupported(args.output.clone())), - } -} - -/// Center the reference group and write an output gro or pdb file. -fn center_structure_file( - system: &mut System, - reference: &str, - output: &str, - output_type: FileType, - dimension: Dimension, -) -> Result<(), Box> { - system.atoms_center(reference, dimension)?; - - match output_type { - FileType::GRO => system.write_gro(output, system.has_velocities())?, - FileType::PDB => system.write_pdb(output)?, - _ => { - return Err(Box::new(RunError::UnsupportedFileExtension( - output.to_string(), - ))) - } - } - - Ok(()) -} - -/// Print progress of the analysis -fn print_progress( - sim_step: u64, - sim_time: u64, - step_fmt: &ColoredString, - time_fmt: &ColoredString, - status: &ColoredString, -) { - print!( - "[{: ^9}] {} {:12} | {} {:9} ps\r", - status, step_fmt, sim_step, time_fmt, sim_time - ); - io::stdout().flush().unwrap(); -} - -/// Center the reference group for every frame of the xdr file and write output xdr file. -fn center_xdr_file<'a, Reader, Writer>( - system: &'a mut System, - reference: &str, - trajectory: impl AsRef, - output: impl AsRef, - step: usize, - dimension: Dimension, - silent: bool, -) -> Result<(), Box> -where - Writer: XdrWriter, - Reader: XdrReader<'a>, -{ - let mut writer = Writer::new(system, output)?; - let iterator = Reader::new(system, trajectory)?; - - let colored_step = "Step:".cyan(); - let colored_time = "Time:".bright_purple(); - let colored_running = "CENTERING".yellow(); - - // iterate through the input trajectory file - for (curr_step, raw_frame) in iterator.enumerate() { - let frame = raw_frame?; - - if !silent && frame.get_simulation_step() % PRINT_FREQ == 0 { - print_progress( - frame.get_simulation_step(), - frame.get_simulation_time() as u64, - &colored_step, - &colored_time, - &colored_running, - ); - } - - if curr_step % step == 0 { - frame.atoms_center(reference, dimension)?; - writer.write_frame()?; - } - } - - if !silent { - println!("[{: ^9}]\n", &"COMPLETED".green()); - } - - Ok(()) -} +use argparse::Args; +use errors::RunError; /// Print options specified for the centering. Non-default values are colored in blue. fn print_options(args: &Args, system: &System, dim: &Dimension) { - println!("[STRUCTURE] {}", &args.structure.bright_blue()); + println!("[STRUCTURE] {}", &args.structure.bright_blue()); - if args.trajectory.is_some() { - println!( - "[TRAJECTORY] {}", - &args.trajectory.clone().unwrap().bright_blue() - ); + match args.trajectories.len() { + 0 => (), + 1 => println!("[TRAJECTORY] {}", args.trajectories[0].bright_blue()), + _ => { + print!("[TRAJECTORIES] "); + println!("{}", args.trajectories[0].bright_blue()); + for traj in args.trajectories.iter().skip(1) { + println!(" {}", traj.bright_blue()); + } + } } - println!("[OUTPUT] {}", &args.output.bright_blue()); + println!("[OUTPUT] {}", &args.output.bright_blue()); if args.index.is_some() { println!( - "[INDEX] {}", + "[INDEX] {}", &args.index.clone().unwrap().bright_blue() ); } else if system.get_n_groups() > 2 { - println!("[INDEX] index.ndx"); + println!("[INDEX] index.ndx"); } if args.reference == "Protein" { - println!("[REFERENCE] {}", &args.reference); + println!("[REFERENCE] {}", &args.reference); } else { - println!("[REFERENCE] {}", &args.reference.bright_blue()); + println!("[REFERENCE] {}", &args.reference.bright_blue()); } if !args.xdimension && !args.ydimension && !args.zdimension { - println!("[DIMENSIONS] {}", dim); + println!("[DIMENSIONS] {}", dim); } else { - println!("[DIMENSIONS] {}", dim.to_string().bright_blue()); + println!("[DIMENSIONS] {}", dim.to_string().bright_blue()); + } + + if let Some(s) = args.start_time { + let time = format!("{} ns", s / 1000.0); + println!("[START TIME] {}", time.bright_blue()); + } + + if let Some(e) = args.end_time { + let time = format!("{} ns", e / 1000.0); + println!("[END TIME] {}", time.bright_blue()); } if args.step != 1 { - println!("[STEP] {}", &args.step.to_string().bright_blue()); - } else { - println!("[STEP] {}", &args.step); + println!("[STEP] {}", &args.step.to_string().bright_blue()); } println!(); @@ -315,14 +71,16 @@ fn print_options(args: &Args, system: &System, dim: &Dimension) { /// Perform the centering. pub fn run() -> Result<(), Box> { - let args = Args::parse(); - sanity_check_files(&args)?; + let args = argparse::parse()?; if !args.silent { let version = format!("\n >> gcenter {} <<\n", env!("CARGO_PKG_VERSION")); println!("{}", version.bold()); } + // [DEV] print all errors + // errors::print_all_errors(); + // construct a dimension; if no dimension has been chosen, use all of them let dim: Dimension = match [args.xdimension, args.ydimension, args.zdimension].into() { Dimension::None => Dimension::XYZ, @@ -373,94 +131,10 @@ pub fn run() -> Result<(), Box> { } // select reference atoms - let autodetect = match system.group_create("Reference", &args.reference) { - // ignore group overwrite - Ok(_) | Err(GroupError::AlreadyExistsWarning(_)) => false, - // if the reference group is 'Protein' and such group does not exist, try autodetecting the protein atoms - Err(GroupError::InvalidQuery(_)) if &args.reference == "Protein" => { - if !args.silent { - println!( - "{} group '{}' not found. Autodetecting protein atoms...\n", - "warning:".yellow().bold(), - "Protein".yellow() - ); - } - - match system.group_create("Reference", "@protein") { - Ok(_) | Err(GroupError::AlreadyExistsWarning(_)) => true, - Err(_) => panic!("gcenter: Fatal Error. Autodetection failed."), - } - } - // propagate all the other errors - Err(e) => return Err(Box::from(e)), - }; - - // check that the reference group is not empty - if system.group_get_n_atoms("Reference").unwrap() == 0 { - if !autodetect { - return Err(Box::new(RunError::EmptyReference(args.reference))); - } else { - return Err(Box::new(RunError::AutodetectionFailed)); - } - } + reference::create_reference(&mut system, &args)?; - // determine type of the output file - let output_type = FileType::from_name(&args.output); - - match args.trajectory { - // trajectory file not provided, use gro or pdb file - None => center_structure_file(&mut system, "Reference", &args.output, output_type, dim)?, - - // use trajectory file - Some(ref file) => { - // determine the type of the input trajectory file - let input_type = FileType::from_name(file); - - match (input_type, output_type) { - (FileType::XTC, FileType::XTC) => center_xdr_file::( - &mut system, - "Reference", - file, - &args.output, - args.step, - dim, - args.silent, - )?, - - (FileType::XTC, FileType::TRR) => center_xdr_file::( - &mut system, - "Reference", - file, - &args.output, - args.step, - dim, - args.silent, - )?, - - (FileType::TRR, FileType::XTC) => center_xdr_file::( - &mut system, - "Reference", - file, - &args.output, - args.step, - dim, - args.silent, - )?, - - (FileType::TRR, FileType::TRR) => center_xdr_file::( - &mut system, - "Reference", - file, - &args.output, - args.step, - dim, - args.silent, - )?, - - _ => return Err(Box::new(RunError::UnsupportedFileExtension(args.output))), - } - } - } + // perform centering + center::center(&mut system, &args, dim)?; if !args.silent { let result = format!("Successfully written output file '{}'.", &args.output); diff --git a/src/reference.rs b/src/reference.rs new file mode 100644 index 0000000..5f41520 --- /dev/null +++ b/src/reference.rs @@ -0,0 +1,52 @@ +// Released under MIT License. +// Copyright (c) 2023 Ladislav Bartos + +//! Implementation of reference atoms selection. + +use colored::Colorize; +use groan_rs::errors::GroupError; +use groan_rs::prelude::*; + +use crate::argparse::Args; +use crate::errors::RunError; + +/// Select reference atoms for centering. +pub fn create_reference( + system: &mut System, + args: &Args, +) -> Result<(), Box> { + let autodetect = match system.group_create("Reference", &args.reference) { + // ignore group overwrite + Ok(_) | Err(GroupError::AlreadyExistsWarning(_)) => false, + // if the reference group is 'Protein' and such group does not exist, try autodetecting the protein atoms + Err(GroupError::InvalidQuery(_)) if &args.reference == "Protein" => { + if !args.silent { + println!( + "{} group '{}' not found. Autodetecting protein atoms...\n", + "warning:".yellow().bold(), + "Protein".yellow() + ); + } + + match system.group_create("Reference", "@protein") { + Ok(_) | Err(GroupError::AlreadyExistsWarning(_)) => true, + Err(_) => panic!("gcenter: Fatal Error. Autodetection failed."), + } + } + // propagate all the other errors + Err(e) => return Err(Box::from(e)), + }; + + // check that the reference group is not empty + if system.group_get_n_atoms("Reference").unwrap() == 0 { + if !autodetect { + return Err(Box::new(RunError::EmptyReference( + args.reference.to_owned(), + ))); + } else { + return Err(Box::new(RunError::AutodetectionFailed)); + } + } + + Ok(()) +} diff --git a/tests/main.rs b/tests/main.rs index 181bcdb..4b3c6e5 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -540,6 +540,73 @@ mod pass_tests { )); } + #[test] + fn regex() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + "-ntests/test_files/index.ndx", + &output_arg, + "-ftests/test_files/input.xtc", + "-rr'^T.*_all$'", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_begin() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-b400", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_end() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-e700", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_end.xtc", + output.path().to_str().unwrap() + )); + } + #[test] fn xyz_xtc_step() { let output = Builder::new().suffix(".xtc").tempfile().unwrap(); @@ -562,6 +629,359 @@ mod pass_tests { )); } + #[test] + fn xyz_xtc_begin_end() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-b400", + "-e800", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_begin_step() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-b400", + "-s3", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_step.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_end_step() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-e800", + "-s3", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_end_step.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_begin_end_step() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.xtc", + "-b400", + "-e800", + "-s3", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end_step.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_multiple_inputs() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_multiple_inputs_begin() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + "-b400", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_multiple_inputs_end() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + "-e700", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_end.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_multiple_inputs_begin_end() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + "-b400", + "-e800", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_trr_begin_end() { + let output = Builder::new().suffix(".trr").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.trr", + "-b400", + "-e800", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end.trr", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_trr_begin_end_step() { + let output = Builder::new().suffix(".trr").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input.trr", + "-b400", + "-e800", + "-s3", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end_step.trr", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_to_trr_multiple_inputs() { + let output = Builder::new().suffix(".trr").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_from_xtc.trr", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_trr_to_trr_multiple_inputs() { + let output = Builder::new().suffix(".trr").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.trr", + "-ftests/test_files/input_part2.trr", + "-ftests/test_files/input_part3.trr", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_from_trr.trr", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_trr_to_xtc_multiple_inputs() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.trr", + "-ftests/test_files/input_part2.trr", + "-ftests/test_files/input_part3.trr", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_trr_multiple_inputs_begin_end() { + let output = Builder::new().suffix(".trr").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.trr", + "-ftests/test_files/input_part2.trr", + "-ftests/test_files/input_part3.trr", + "-b400", + "-e800", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz_begin_end.trr", + output.path().to_str().unwrap() + )); + } + + #[test] + fn xyz_xtc_and_trr_multiple_inputs() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.trr", + "-ftests/test_files/input_part3.xtc", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } + #[test] fn backup() { let mut file = File::create("tests/test_files/temporary.gro").unwrap(); @@ -739,6 +1159,28 @@ mod fail_tests { std::fs::remove_file("tests/test_files/tmp_input.xtc").unwrap(); } + #[test] + fn file_protection_xtc_multiple() { + std::fs::copy( + "tests/test_files/input.xtc", + "tests/test_files/tmp_input2.xtc", + ) + .unwrap(); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + "-ftests/test_files/input.xtc", + "-ftests/test_files/tmp_input2.xtc", + "-otests/test_files/tmp_input2.xtc", + ]) + .assert() + .failure(); + + std::fs::remove_file("tests/test_files/tmp_input2.xtc").unwrap(); + } + #[test] fn nonexistent_group() { let output = Builder::new().suffix(".xtc").tempfile().unwrap(); @@ -908,4 +1350,76 @@ mod fail_tests { .assert() .failure(); } + + #[test] + fn begin_requires_traj() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args(["-ctests/test_files/input.gro", &output_arg, "-b400"]) + .assert() + .failure(); + } + + #[test] + fn end_requires_traj() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args(["-ctests/test_files/input.gro", &output_arg, "-e700"]) + .assert() + .failure(); + } + + #[test] + fn step_requires_traj() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args(["-ctests/test_files/input.gro", &output_arg, "-s3"]) + .assert() + .failure(); + } + + #[test] + fn multiple_input_step() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part2.xtc", + "-ftests/test_files/input_part3.xtc", + "-s3", + ]) + .assert() + .failure(); + } + + #[test] + fn multiple_inputs_identical() { + let output = Builder::new().suffix(".xtc").tempfile().unwrap(); + let output_arg = format!("-o{}", output.path().display()); + + Command::cargo_bin("gcenter") + .unwrap() + .args([ + "-ctests/test_files/input.gro", + &output_arg, + "-ftests/test_files/input_part1.xtc", + "-ftests/test_files/input_part1.xtc", + ]) + .assert() + .failure(); + } } diff --git a/tests/test_files/input_part1.trr b/tests/test_files/input_part1.trr new file mode 100644 index 0000000..a89e552 Binary files /dev/null and b/tests/test_files/input_part1.trr differ diff --git a/tests/test_files/input_part1.xtc b/tests/test_files/input_part1.xtc new file mode 100644 index 0000000..560d8a0 Binary files /dev/null and b/tests/test_files/input_part1.xtc differ diff --git a/tests/test_files/input_part2.trr b/tests/test_files/input_part2.trr new file mode 100644 index 0000000..ddce13c Binary files /dev/null and b/tests/test_files/input_part2.trr differ diff --git a/tests/test_files/input_part2.xtc b/tests/test_files/input_part2.xtc new file mode 100644 index 0000000..700ac86 Binary files /dev/null and b/tests/test_files/input_part2.xtc differ diff --git a/tests/test_files/input_part3.trr b/tests/test_files/input_part3.trr new file mode 100644 index 0000000..b888cd5 Binary files /dev/null and b/tests/test_files/input_part3.trr differ diff --git a/tests/test_files/input_part3.xtc b/tests/test_files/input_part3.xtc new file mode 100644 index 0000000..9496ae8 Binary files /dev/null and b/tests/test_files/input_part3.xtc differ diff --git a/tests/test_files/output_xyz_begin.xtc b/tests/test_files/output_xyz_begin.xtc new file mode 100644 index 0000000..9c5fe1b Binary files /dev/null and b/tests/test_files/output_xyz_begin.xtc differ diff --git a/tests/test_files/output_xyz_begin_end.trr b/tests/test_files/output_xyz_begin_end.trr new file mode 100644 index 0000000..597abcd Binary files /dev/null and b/tests/test_files/output_xyz_begin_end.trr differ diff --git a/tests/test_files/output_xyz_begin_end.xtc b/tests/test_files/output_xyz_begin_end.xtc new file mode 100644 index 0000000..a808f18 Binary files /dev/null and b/tests/test_files/output_xyz_begin_end.xtc differ diff --git a/tests/test_files/output_xyz_begin_end_step.trr b/tests/test_files/output_xyz_begin_end_step.trr new file mode 100644 index 0000000..ba09844 Binary files /dev/null and b/tests/test_files/output_xyz_begin_end_step.trr differ diff --git a/tests/test_files/output_xyz_begin_end_step.xtc b/tests/test_files/output_xyz_begin_end_step.xtc new file mode 100644 index 0000000..ab5b474 Binary files /dev/null and b/tests/test_files/output_xyz_begin_end_step.xtc differ diff --git a/tests/test_files/output_xyz_begin_step.xtc b/tests/test_files/output_xyz_begin_step.xtc new file mode 100644 index 0000000..1a036a1 Binary files /dev/null and b/tests/test_files/output_xyz_begin_step.xtc differ diff --git a/tests/test_files/output_xyz_end.xtc b/tests/test_files/output_xyz_end.xtc new file mode 100644 index 0000000..a9814a2 Binary files /dev/null and b/tests/test_files/output_xyz_end.xtc differ diff --git a/tests/test_files/output_xyz_end_step.xtc b/tests/test_files/output_xyz_end_step.xtc new file mode 100644 index 0000000..a62b581 Binary files /dev/null and b/tests/test_files/output_xyz_end_step.xtc differ