diff --git a/Cargo.lock b/Cargo.lock index b642475..8597b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" @@ -142,9 +142,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.3" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -276,7 +276,7 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" [[package]] name = "gcenter" -version = "1.0.2" +version = "1.0.3" dependencies = [ "assert_cmd", "backitup", @@ -297,9 +297,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "groan_rs" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c887bed093d82c2515012e5a9445eb926e89517abc8de67c198e99265ac04278" +checksum = "63a6e02fb640818f700b234f4b0a2be996c10ce1ce33eeeae9536fd07e157577" dependencies = [ "cc", "colored", @@ -321,9 +321,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "iana-time-zone" @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -443,9 +443,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "predicates" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", @@ -471,9 +471,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -504,9 +504,9 @@ checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -543,9 +543,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.32" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6db422e..9abe6db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "gcenter" authors = ["Ladislav Bartos "] description = "Center Any Group in a Gromacs Trajectory" -version = "1.0.2" +version = "1.0.3" license = "MIT" edition = "2021" repository = "https://github.com/Ladme/gcenter" @@ -15,7 +15,7 @@ categories = ["command-line-utilities", "science"] backitup = "0.1.1" clap = { version = "4.4.3", features = ["derive"] } colored = "2.0.4" -groan_rs = "0.2.0" +groan_rs = "0.3.1" thiserror = "1.0.48" [dev-dependencies] diff --git a/README.md b/README.md index ed54338..a19ada4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Center your chosen group within a Gromacs trajectory or structure file effortles `gcenter` 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. `gcenter` exclusively supports orthogonal simulation boxes. -`gcenter` supports gro and pdb structure files and xtc and trr trajectories and it can autodetect protein residues. Use VMD-like [groan selection language](https://docs.rs/groan_rs/0.1.0/groan_rs/#groan-selection-language) to select groups of atoms to center. +`gcenter` supports gro and pdb structure files and xtc and trr trajectories and it can autodetect protein residues. Use VMD-like [groan selection language](https://docs.rs/groan_rs/latest/groan_rs/#groan-selection-language) to select groups of atoms to center. ## Installation diff --git a/src/lib.rs b/src/lib.rs index 793e833..49fcfe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use std::io::{self, Write}; use std::path::Path; use thiserror::Error; +use groan_rs::errors::GroupError; use groan_rs::files::FileType; use groan_rs::prelude::*; @@ -29,7 +30,8 @@ pub struct Args { 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." + 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, @@ -37,14 +39,15 @@ pub struct Args { 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." + 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)", + 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, @@ -60,7 +63,7 @@ pub struct Args { #[arg( short = 'r', long = "reference", - help = "Group to center (default: Protein)", + 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." )] @@ -69,7 +72,7 @@ pub struct Args { #[arg( short = 's', long = "step", - help = "Write every th frame (default: 1)", + 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." @@ -125,10 +128,10 @@ pub struct Args { /// Errors originating directly from `gcenter`. #[derive(Error, Debug, PartialEq, Eq)] pub enum RunError { - #[error("{} structure file '{}' is the same file as the output file", "error:".red().bold(), .0.yellow())] - IOMatchStructure(String), - #[error("{} trajectory file '{}' is the same file as the output file", "error:".red().bold(), .0.yellow())] - IOMatchTrajectory(String), + #[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())] @@ -141,20 +144,44 @@ pub enum RunError { 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::IOMatchStructure(args.structure.to_string())); + return Err(RunError::IOMatch(args.structure.to_string())); } } else if *args.trajectory.as_ref().unwrap() == args.output { - return Err(RunError::IOMatchTrajectory( + return Err(RunError::IOMatch( args.trajectory.as_ref().unwrap().to_string(), )); } - Ok(()) + 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. @@ -289,15 +316,13 @@ 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)?; if !args.silent { let version = format!("\n >> gcenter {} <<\n", env!("CARGO_PKG_VERSION")); println!("{}", version.bold()); } - // check that the input file is not the same as the output file - sanity_check_files(&args)?; - // 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, @@ -349,25 +374,25 @@ pub fn run() -> Result<(), Box> { // select reference atoms let autodetect = match system.group_create("Reference", &args.reference) { - Ok(_) => false, - Err(e) => { - if &args.reference == "Protein" { - if !args.silent { - println!( - "{} group '{}' not found. Autodetecting protein atoms...\n", - "warning:".yellow().bold(), - "Protein".yellow() - ); - } - - system - .group_create("Reference", "@protein") - .expect("gcenter: Fatal Error. Autodetection failed."); - true - } else { - return Err(e); + // 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 diff --git a/tests/main.rs b/tests/main.rs index 93aa840..181bcdb 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -622,6 +622,75 @@ mod pass_tests { fs::remove_file("tests/test_files/temporary_overwrite.gro").unwrap(); } + + #[test] + fn silent() { + 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", + "--silent", + ]) + .assert() + .success() + .stdout(""); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn group_overwrite_default() { + 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_with_reference.ndx", + &output_arg, + "-ftests/test_files/input.xtc", + "-r(resname ASN and serial 35 to 45 and name BB)", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_group.xtc", + output.path().to_str().unwrap() + )); + } + + #[test] + fn group_overwrite_during_autodetection() { + 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_with_reference.ndx", + &output_arg, + "-ftests/test_files/input.xtc", + "-rProtein", + ]) + .assert() + .success(); + + assert!(file_diff::diff( + "tests/test_files/output_xyz.xtc", + output.path().to_str().unwrap() + )); + } } #[cfg(test)] @@ -670,6 +739,24 @@ mod fail_tests { std::fs::remove_file("tests/test_files/tmp_input.xtc").unwrap(); } + #[test] + fn nonexistent_group() { + 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", + "-rNonexistent", + "-ntests/test_files/index.ndx", + ]) + .assert() + .failure(); + } + #[test] fn empty_group() { let output = Builder::new().suffix(".xtc").tempfile().unwrap(); diff --git a/tests/test_files/index_with_reference.ndx b/tests/test_files/index_with_reference.ndx new file mode 100644 index 0000000..b1b5f38 --- /dev/null +++ b/tests/test_files/index_with_reference.ndx @@ -0,0 +1,12 @@ +[ Reference ] + 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 + 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 + 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 + 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 + 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 + 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 + 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 + 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 + 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 + 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165