Coverage for model_workflow/console.py: 86%

290 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-23 10:54 +0000

1from pathlib import Path 

2from os.path import exists 

3from shutil import copyfile 

4from subprocess import call 

5from typing import List 

6from argparse import ArgumentParser, RawTextHelpFormatter, Action, _SubParsersAction 

7from textwrap import wrap 

8 

9from model_workflow.mwf import workflow, Project, requestables, DEPENDENCY_FLAGS 

10from model_workflow.utils.structures import Structure 

11from model_workflow.utils.file import File 

12from model_workflow.utils.filters import filter_atoms 

13from model_workflow.utils.subsets import get_trajectory_subset 

14from model_workflow.utils.constants import * 

15from model_workflow.utils.auxiliar import InputError 

16from model_workflow.utils.nassa_file import generate_nassa_config 

17from model_workflow.tools.conversions import convert 

18from model_workflow.analyses.nassa import workflow_nassa 

19 

20# Set the path to the input setter jupyter notebook 

21inputs_template = str(Path(__file__).parent / "resources" / "inputs_file_template.yml") 

22nassa_template = str(Path(__file__).parent / "resources" / "nassa_template.yml") 

23 

24expected_project_args = set(Project.__init__.__code__.co_varnames) 

25 

26test_docs_url = 'https://mddb-workflow.readthedocs.io/en/latest/usage.html#tests-and-other-checking-processes' 

27task_docs_url = 'https://mddb-workflow.readthedocs.io/en/latest/tasks.html' 

28 

29class CustomHelpFormatter(RawTextHelpFormatter): 

30 """Custom formatter for argparse help text with better organization and spacing""" 

31 

32 def __init__(self, prog, indent_increment=2, max_help_position=6, width=None): 

33 super().__init__(prog, indent_increment, max_help_position, width) 

34 

35 def _split_lines(self, text, width): 

36 lines = [] 

37 for line in text.splitlines(): 

38 if line.strip() != '': 

39 if line.startswith('https'): 

40 lines.append(line) 

41 else: 

42 lines.extend(wrap(line, width, break_long_words=False, replace_whitespace=False)) 

43 return lines 

44 

45 def _format_usage(self, usage, actions, groups, prefix): 

46 essential_usage = super()._format_usage(usage, actions, groups, prefix) 

47 # Only for mwf run 

48 if 'run' in self._prog: 

49 # Combine the aguments for -i, -e, -ow 

50 lines = essential_usage.split('\n') 

51 filtered_lines = [] 

52 for line in lines: 

53 if line.strip().startswith("[-i "): 

54 line = line.replace("[-i", "[-i/-e/-ow") 

55 filtered_lines.append(line) 

56 elif line.strip().startswith("[-e") or line.strip().startswith("[-ow"): 

57 continue 

58 else: 

59 filtered_lines.append(line) 

60 essential_usage = '\n'.join(filtered_lines) 

61 return essential_usage 

62 

63 def _format_action_invocation(self, action): 

64 """Format the display of options with choices more cleanly""" 

65 if not action.option_strings: 

66 # This is a positional argument 

67 return super()._format_action_invocation(action) 

68 

69 # For options with choices, format them nicely 

70 opts = ', '.join(action.option_strings) 

71 

72 # Special case for include, exclude, and overwrite 

73 if action.dest in ['include', 'exclude', 'overwrite']: 

74 opts = ', '.join(action.option_strings) 

75 metavar = 'TASKS' 

76 return f"{opts} {metavar}" 

77 if action.nargs == 0: 

78 # Boolean flag 

79 return opts 

80 else: 

81 # Format with metavar or choices 

82 metavar = self._format_args(action, action.dest.upper()) 

83 if action.choices: 

84 choice_str = '{' + ','.join(str(c) for c in action.choices) + '}' 

85 # if action.nargs is not None and action.nargs != 1: 

86 # choice_str += ' ...' 

87 return f"{opts} [{choice_str}]" 

88 else: 

89 return f"{opts} {metavar}" 

90 

91class CustomArgumentParser(ArgumentParser): 

92 """This parser extends the ArgumentParser to handle subcommands and errors more gracefully.""" 

93 def error(self, message): 

94 # Check for subcommand in sys.argv 

95 import sys 

96 # Extract subcommand from command line if it exists 

97 if hasattr(self, '_subparsers') and self._subparsers is not None: 

98 subcommands = [choice for action in self._subparsers._actions 

99 if isinstance(action, _SubParsersAction) 

100 for choice in action.choices] 

101 if len(sys.argv) > 1 and sys.argv[1] in subcommands: 

102 self.subcommand = sys.argv[1] 

103 

104 # Now continue with your existing logic 

105 if hasattr(self, 'subcommand') and self.subcommand: 

106 self._print_message(f"{self.prog} {self.subcommand}: error: {message}\n", sys.stderr) 

107 # Show help for the specific subparser 

108 for action in self._subparsers._actions: 

109 if isinstance(action, _SubParsersAction): 

110 for choice, subparser in action.choices.items(): 

111 if choice == self.subcommand: 

112 subparser.print_usage() 

113 break 

114 else: 

115 # Default error behavior for main parser 

116 self.print_usage(sys.stderr) 

117 self._print_message(f"{self.prog}: error: {message}\n", sys.stderr) 

118 sys.exit(2) 

119# Main ---------------------------------------------------------------------------------  

120 

121# Function called through argparse 

122def main (): 

123 # Parse input arguments from the console 

124 # The vars function converts the args object to a dictionary 

125 args = parser.parse_args() 

126 if hasattr(args, 'subcommand') and args.subcommand: 

127 parser.subcommand = args.subcommand 

128 # Apply common arguments as necessary 

129 if hasattr(args, 'no_symlinks') and args.no_symlinks: 

130 GLOBALS['no_symlinks'] = True 

131 # Find which subcommand was called 

132 subcommand = args.subcommand 

133 # If there is not subcommand then print help 

134 if not subcommand: 

135 parser.print_help() 

136 # If user wants to run the workflow 

137 elif subcommand == "run": 

138 # Ger all parsed arguments 

139 dict_args = vars(args) 

140 # Remove arguments not related to this subcommand 

141 del dict_args['subcommand'] 

142 # Remove common arguments from the dict as well 

143 common_args = [ action.dest for action in common_parser._actions ] 

144 for arg in common_args: 

145 del dict_args[arg] 

146 # Find out which arguments are for the Project class and which ones are for the workflow 

147 project_args = {} 

148 workflow_args = {} 

149 for k, v in dict_args.items(): 

150 if k in expected_project_args: 

151 project_args[k] = v 

152 else: 

153 workflow_args[k] = v 

154 # Call the actual main function 

155 workflow(project_parameters = project_args, **workflow_args) 

156 # If user wants to setup the inputs 

157 elif subcommand == "inputs": 

158 # Make a copy of the template in the local directory if there is not an inputs file yet 

159 if exists(DEFAULT_INPUTS_FILENAME): 

160 print(f"File {DEFAULT_INPUTS_FILENAME} already exists") 

161 else: 

162 copyfile(inputs_template, DEFAULT_INPUTS_FILENAME) 

163 print(f"File {DEFAULT_INPUTS_FILENAME} has been generated") 

164 # Set the editor to be used to modify the inputs file 

165 editor_command = args.editor 

166 if editor_command: 

167 if editor_command == 'none': return 

168 return call([editor_command, DEFAULT_INPUTS_FILENAME]) 

169 # If no editor argument is passed then ask the user for one 

170 print("Choose your preferred editor:") 

171 available_editors = list(AVAILABLE_TEXT_EDITORS.keys()) 

172 for i, editor_name in enumerate(available_editors, 1): 

173 print(f"{i}. {editor_name}") 

174 print("*. exit") 

175 try: 

176 choice = int(input("Number: ").strip()) 

177 if not (1 <= choice <= len(available_editors)): raise ValueError 

178 editor_name = available_editors[choice - 1] 

179 editor_command = AVAILABLE_TEXT_EDITORS[editor_name] 

180 # Open a text editor for the user 

181 print(f"{editor_name} was selected") 

182 call([editor_command, DEFAULT_INPUTS_FILENAME]) 

183 except ValueError: 

184 print(f"No editor was selected") 

185 

186 

187 # In case the convert tool was called 

188 elif subcommand == 'convert': 

189 # If no input arguments are passed print help 

190 if args.input_structure == None and args.input_trajectories == None: 

191 convert_parser.print_help() 

192 return 

193 if args.input_trajectories == None: 

194 args.input_trajectories = [] 

195 # Run the convert command 

196 convert( 

197 input_structure_filepath=args.input_structure, 

198 output_structure_filepath=args.output_structure, 

199 input_trajectory_filepaths=args.input_trajectories, 

200 output_trajectory_filepath=args.output_trajectory, 

201 ) 

202 

203 # In case the filter tool was called 

204 elif subcommand == 'filter': 

205 # Run the convert command 

206 filter_atoms( 

207 input_structure_file = File(args.input_structure), 

208 output_structure_file = File(args.output_structure), 

209 input_trajectory_file = File(args.input_trajectory), 

210 output_trajectory_file = File(args.output_trajectory), 

211 selection_string = args.selection_string, 

212 selection_syntax = args.selection_syntax 

213 ) 

214 print('There you have it :)') 

215 

216 # In case the subset tool was called 

217 elif subcommand == 'subset': 

218 output_trajectory = args.output_trajectory if args.output_trajectory else args.input_trajectory 

219 get_trajectory_subset( 

220 input_structure_file=File(args.input_structure), 

221 input_trajectory_file=File(args.input_trajectory), 

222 output_trajectory_file=File(output_trajectory), 

223 start=args.start, 

224 end=args.end, 

225 step=args.step, 

226 skip=args.skip, 

227 frames=args.frames 

228 ) 

229 print('All done :)') 

230 

231 # In case the chainer tool was called 

232 elif subcommand == 'chainer': 

233 # Parse the structure 

234 structure = Structure.from_pdb_file(args.input_structure) 

235 # Select atom accoridng to inputs 

236 selection = structure.select(args.selection_string, args.selection_syntax) if args.selection_string else structure.select_all() 

237 if not selection: raise InputError(f'Empty selection {selection}') 

238 # Run the chainer logic 

239 structure.chainer(selection, args.letter, args.whole_fragments) 

240 # Generate the output file from the modified structure 

241 structure.generate_pdb_file(args.output_structure) 

242 print(f'Changes written to {args.output_structure}') 

243 # If user wants to run the NASSA analysis 

244 elif subcommand == "nassa": 

245 # If no input arguments are passed print help 

246 if args.config == None and args.make_config == False: 

247 nassa_parser.print_help() 

248 print('Please provide a configuration file or make one with the -m flag') 

249 return 

250 # If the user wants to make a configuration file 

251 if args.make_config: 

252 #print('args.make_config: ', args.make_config) 

253 if args.make_config == True or args.make_config == []: 

254 # Make a copy of the template in the local directory if there is not an inputs file yet 

255 if not exists(DEFAULT_NASSA_CONFIG_FILENAME): 

256 copyfile(nassa_template, DEFAULT_NASSA_CONFIG_FILENAME) 

257 # Open a text editor for the user 

258 call(["vim", DEFAULT_NASSA_CONFIG_FILENAME]) 

259 print('Configuration file created as nassa.json\nNow you can run the analysis with the -c flag.') 

260 return 

261 # If the user provides a path to the files 

262 else: 

263 generate_nassa_config( 

264 args.make_config, 

265 args.seq_path, 

266 args.output, 

267 args.unit_len, 

268 args.n_sequences 

269 ) 

270 print('Configuration file created as nassa.json\nNow you can run the analysis with the -c flag.') 

271 return 

272 # If the user wants to run the analysis. With the config file an analysis name must be provided, or the all flag must be set 

273 if args.config and args.analysis_names == None and args.all == False: 

274 nassa_parser.print_help() 

275 print('Please provide an analysis name to run:', ', '.join(NASSA_ANALYSES_LIST)) 

276 return 

277 # If the user wants to run the helical parameters analysis we must check if the necessary files are provided (structure, topology and trajectory) 

278 if args.helical_parameters: 

279 # Also, it is necesary to provide the project directories. Each of the project directories must contain an independent MD 

280 if args.proj_directories == None: 

281 nassa_parser.print_help() 

282 print('Please provide a project directory to run the helical parameters analysis with the -pdirs flag') 

283 return 

284 if args.input_structure_filepath == None: 

285 raise InputError('Please provide a structure file to run the helical parameters analysis with the -stru flag') 

286 elif args.input_trajectory_filepath == None: 

287 raise InputError('Please provide a trajectory file to run the helical parameters analysis with the -traj flag') 

288 elif args.input_topology_filepath == None: 

289 raise InputError('Please provide a topology file to run the helical parameters analysis with the -top flag') 

290 # If the all flag is set, the user must provide the path to the sequences because it is necessary to create the nassa.yml and run the NASSA analysis 

291 if args.all: 

292 if not args.seq_path: 

293 raise InputError('Please, if all option is selected provide the path to the sequences (--seq_path)') 

294 # If all the flags are correctly set, we can run the analysis 

295 workflow_nassa( 

296 config_file_path=None, # The configuration file is not needed in this case because we are going to run the helical parameters analysis so it will be created then 

297 analysis_names=args.analysis_names, 

298 overwrite=args.overwrite, 

299 overwrite_nassa=args.overwrite_nassa, 

300 helical_par=args.helical_parameters, 

301 proj_dirs=args.proj_directories, 

302 input_structure_file=args.input_structure_filepath, 

303 input_trajectory_file=args.input_trajectory_filepath, 

304 input_top_file=args.input_topology_filepath, 

305 all=args.all, 

306 unit_len=args.unit_len, 

307 n_sequences=args.n_sequences, 

308 seq_path=args.seq_path, 

309 md_directories=args.md_directories, 

310 trust=args.trust, 

311 mercy=args.mercy 

312 ) 

313 # If the user wants to run the NASSA analysis with the config file already created and the analysis name provided 

314 else: 

315 dict_args = vars(args) 

316 del dict_args['subcommand'] # preguntar Dani ¿? 

317 # Call the actual main function 

318 workflow_nassa( 

319 config_file_path = args.config, 

320 analysis_names = args.analysis_names, 

321 make_config = args.make_config, 

322 output = args.output, 

323 working_directory = args.working_directory, 

324 overwrite = args.overwrite, 

325 overwrite_nassa = args.overwrite_nassa, 

326 n_sequences = args.n_sequences, 

327 unit_len = args.unit_len, 

328 all= args.all, 

329 md_directories=args.md_directories, 

330 trust=args.trust, 

331 mercy=args.mercy 

332 ) 

333 

334# Define a common parser running in top of all others 

335# This arguments declared here are available among all subparsers 

336common_parser = ArgumentParser(add_help=False) 

337 

338# If this argument is passed then no symlinks will be used anywhere 

339# Files will be copied instead thus taking more time and disk 

340# However symlinks are not always allowed in all file systems so this is sometimes necessary 

341common_parser.add_argument("-ns", "--no_symlinks", default=False, action='store_true', help="Do not use symlinks internally") 

342 

343# Define console arguments to call the workflow 

344parser = CustomArgumentParser(description="MDDB Workflow") 

345subparsers = parser.add_subparsers(help='Name of the subcommand to be used', dest="subcommand") 

346 

347# Set the run subcommand 

348run_parser = subparsers.add_parser("run", 

349 help="Run the workflow", 

350 formatter_class=CustomHelpFormatter, 

351 parents=[common_parser] 

352) 

353 

354# Set optional arguments 

355run_parser_input_group = run_parser.add_argument_group('INPUT OPTIONS') 

356run_parser_input_group.add_argument( 

357 "-top", "--input_topology_filepath", 

358 default=None, # There is no default since many formats may be possible 

359 help="Path to input topology file. It is relative to the project directory.") 

360run_parser_input_group.add_argument( 

361 "-stru", "--input_structure_filepath", 

362 default=None, 

363 help=("Path to input structure file. It may be relative to the project or to each MD directory.\n" 

364 "If this value is not passed then the standard structure file is used as input by default")) 

365run_parser_input_group.add_argument( 

366 "-traj", "--input_trajectory_filepaths", 

367 #type=argparse.FileType('r'), 

368 nargs='*', 

369 default=None, 

370 help=("Path to input trajectory file. It is relative to each MD directory.\n" 

371 "If this value is not passed then the standard trajectory file path is used as input by default")) 

372run_parser_input_group.add_argument( 

373 "-dir", "--working_directory", 

374 default='.', 

375 help="Directory where the whole workflow is run. Current directory by default.") 

376run_parser_input_group.add_argument( 

377 "-mdir", "--md_directories", 

378 nargs='*', 

379 default=None, 

380 help=("Path to the different MD directories. Each directory is to contain an independent trajectory and structure.\n" 

381 "Several output files will be generated in every MD directory") 

382) 

383run_parser_input_group.add_argument( 

384 "-md", "--md_config", 

385 action='append', 

386 nargs='*', 

387 default=None, 

388 help=("Configuration of a specific MD. You may declare as many as you want.\n" 

389 "Every MD requires a directory name and at least one trajectory path.\n" 

390 "The structure is -md <directory> <trajectory_1> <trajectory_2> ...\n" 

391 "Note that all trajectories from the same MD will be merged.\n" 

392 "For legacy reasons, you may also provide a specific structure for an MD.\n" 

393 "e.g. -md <directory> <structure> <trajectory_1> <trajectory_2> ...") 

394) 

395run_parser_input_group.add_argument( 

396 "-proj", "--accession", 

397 default=None, 

398 help="Project accession to download missing input files from the database.") 

399run_parser_input_group.add_argument( 

400 "-url", "--database_url", 

401 default=DEFAULT_API_URL, 

402 help=f"API URL to download missing data. Default value is {DEFAULT_API_URL}") 

403run_parser_input_group.add_argument( 

404 "-inp", "--inputs_filepath", 

405 default=None, 

406 help="Path to inputs file") 

407run_parser_input_group.add_argument( 

408 "-pop", "--populations_filepath", 

409 default=DEFAULT_POPULATIONS_FILENAME, 

410 help="Path to equilibrium populations file (Markov State Model only)") 

411run_parser_input_group.add_argument( 

412 "-tpro", "--transitions_filepath", 

413 default=DEFAULT_TRANSITIONS_FILENAME, 

414 help="Path to transition probabilities file (Markov State Model only)") 

415# Set a group for the workflow control options 

416run_parser_workflow_group = run_parser.add_argument_group('WORKFLOW CONTROL OPTIONS') 

417run_parser_workflow_group.add_argument( 

418 "-img", "--image", 

419 action='store_true', 

420 help="Set if the trajectory is to be imaged so atoms stay in the PBC box. See -pbc for more information.") 

421run_parser_workflow_group.add_argument( 

422 "-fit", "--fit", 

423 action='store_true', 

424 help="Set if the trajectory is to be fitted (both rotation and translation) to minimize the RMSD to PROTEIN_AND_NUCLEIC_BACKBONE selection.") 

425run_parser_workflow_group.add_argument( 

426 "-trans", "--translation", 

427 nargs='*', 

428 default=[0,0,0], 

429 help=("Set the x y z translation for the imaging process\n" 

430 "e.g. -trans 0.5 -1 0")) 

431run_parser_workflow_group.add_argument( 

432 "-d", "--download", 

433 action='store_true', 

434 help="If passed, only download required files. Then exits.") 

435run_parser_workflow_group.add_argument( 

436 "-s", "--setup", 

437 action='store_true', 

438 help="If passed, only download required files and run mandatory dependencies. Then exits.") 

439run_parser_workflow_group.add_argument( 

440 "-smp", "--sample_trajectory", 

441 type=int, 

442 nargs='?', 

443 default=None, 

444 const=10, 

445 metavar='N_FRAMES', 

446 help="If passed, download the first 10 (by default) frames from the trajectory. You can specify a different number by providing an integer value.") 

447run_parser_workflow_group.add_argument( 

448 "-rcut", "--rmsd_cutoff", 

449 type=float, 

450 default=DEFAULT_RMSD_CUTOFF, 

451 help=(f"Set the cutoff for the RMSD sudden jumps analysis to fail (default {DEFAULT_RMSD_CUTOFF}).\n" 

452 "This cutoff stands for the number of standard deviations away from the mean an RMSD value is to be.\n")) 

453run_parser_workflow_group.add_argument( 

454 "-icut", "--interaction_cutoff", 

455 type=float, 

456 default=DEFAULT_INTERACTION_CUTOFF, 

457 help=(f"Set the cutoff for the interactions analysis to fail (default {(DEFAULT_INTERACTION_CUTOFF)}).\n" 

458 "This cutoff stands for percent of the trajectory where the interaction happens (from 0 to 1).\n")) 

459run_parser_workflow_group.add_argument( 

460 "-iauto", "--interactions_auto", 

461 type=str, 

462 nargs='?', 

463 const=True, 

464 help=("""Guess input interactions automatically. Available options: 

465 - greedy (default): All chains against all chains 

466 - humble: If there are only two chains then select the interaction between them 

467 - <chain letter>: All chains against this specific chain 

468 - ligands: All chains against every ligand""") 

469) 

470 

471# Set a group for the selection options 

472run_parser_selection_group = run_parser.add_argument_group('SELECTION OPTIONS') 

473run_parser_selection_group.add_argument( 

474 "-filt", "--filter_selection", 

475 nargs='?', 

476 default=False, 

477 const=True, 

478 help=("Atoms selection to be filtered in VMD format. " 

479 "If the argument is passed alone (i.e. with no selection) then water and counter ions are filtered")) 

480run_parser_selection_group.add_argument( 

481 "-pbc", "--pbc_selection", 

482 default=None, 

483 help=("Selection of atoms which stay in Periodic Boundary Conditions even after imaging the trajectory. " 

484 "e.g. remaining solvent, ions, membrane lipids, etc.")) 

485run_parser_selection_group.add_argument( 

486 "-cg", "--cg_selection", 

487 default=None, 

488 help="Selection of atoms which are not actual atoms but Coarse Grained beads.") 

489run_parser_selection_group.add_argument( 

490 "-pcafit", "--pca_fit_selection", 

491 default=PROTEIN_AND_NUCLEIC_BACKBONE, 

492 help="Atom selection for the pca fitting in vmd syntax") 

493run_parser_selection_group.add_argument( 

494 "-pcana", "--pca_analysis_selection", 

495 default=PROTEIN_AND_NUCLEIC_BACKBONE, 

496 help="Atom selection for pca analysis in vmd syntax") 

497 

498 

499# Set a custom argparse action to handle the following 2 arguments 

500# This is done becuase it is not possible to combine nargs='*' with const 

501# https://stackoverflow.com/questions/72803090/argparse-how-to-create-the-equivalent-of-const-with-nargs 

502class custom (Action): 

503 # If argument is not passed -> default 

504 # If argument is passed empty -> const 

505 # If argument is passed with values -> values 

506 def __call__(self, parser, namespace, values, option_string=None): 

507 if values: 

508 setattr(namespace, self.dest, values) 

509 else: 

510 setattr(namespace, self.dest, self.const) 

511 

512# Set a function to pretty print a list of available checkings / failures 

513def pretty_list (availables : List[str]) -> str: 

514 final_line = 'Available protocols:' 

515 for available in availables: 

516 nice_name = NICE_NAMES.get(available, None) 

517 if not nice_name: 

518 raise Exception('Flag "' + available + '" has not a defined nice name') 

519 final_line += '\n - ' + available + ' -> ' + nice_name 

520 final_line += f'\nTo know more about each test please visit:\n{test_docs_url}' 

521 return final_line 

522 

523run_parser_checks_group = run_parser.add_argument_group('INPUT CHECKS OPTIONS', description=f"For more information about each check please visit:\n{test_docs_url}") 

524run_parser_checks_group.add_argument( 

525 "-t", "--trust", 

526 type=str, 

527 nargs='*', 

528 default=[], 

529 action=custom, 

530 const=AVAILABLE_CHECKINGS, 

531 choices=AVAILABLE_CHECKINGS, 

532 help="If passed, do not run the specified checking. Note that all checkings are skipped if passed alone. " + pretty_list(AVAILABLE_CHECKINGS) 

533) 

534run_parser_checks_group.add_argument( 

535 "-m", "--mercy", 

536 type=str, 

537 nargs='*', 

538 default=[], 

539 action=custom, 

540 const=AVAILABLE_FAILURES, 

541 choices=AVAILABLE_FAILURES, 

542 help=("If passed, do not kill the process when any of the specfied checkings fail and proceed with the workflow. " 

543 "Note that all checkings are allowed to fail if the argument is passed alone. " + pretty_list(AVAILABLE_FAILURES)) 

544) 

545run_parser_checks_group.add_argument( 

546 "-f", "--faith", 

547 action='store_true', 

548 default=False, 

549 help=("Use this flag to force-skip all data processing thus asuming inputs are already processed.\n" 

550 "WARNING: Do not use this flag if you don't know what you are doing.\n" 

551 "This may lead to several silent errors.") 

552) 

553 

554# Set a list with the alias of all requestable dependencies 

555choices = sorted(list(requestables.keys()) + list(DEPENDENCY_FLAGS.keys())) 

556 

557run_parser_analysis_group = run_parser.add_argument_group('TASKS OPTIONS', description=f"Available tasks: {choices}\nFor more information about each task please visit:\n{task_docs_url}") 

558run_parser_analysis_group.add_argument( 

559 "-i", "--include", 

560 nargs='*', 

561 choices=choices, 

562 help="""Set the unique analyses or tools to be run. All other steps will be skipped. There are also some additional flags to define a preconfigured group of dependencies: 

563 - download: Check/download input files (already ran with analyses) 

564 - setup: Process and test input files (already ran with analyses) 

565 - network: Run dependencies which require internet connection 

566 - minimal: Run dependencies required by the web client to work 

567 - interdeps: Run interactions and all its dependent analyses""") 

568run_parser_analysis_group.add_argument( 

569 "-e", "--exclude", 

570 nargs='*', 

571 choices=choices, 

572 help=("Set the analyses or tools to be skipped. All other steps will be run. Note that if we exclude a dependency of something else then it will be run anyway.")) 

573run_parser_analysis_group.add_argument( 

574 "-ow", "--overwrite", 

575 type=str, 

576 nargs='*', 

577 default=[], 

578 action=custom, 

579 const=True, 

580 choices=choices, 

581 help=("Set the output files to be overwritten thus re-runing its corresponding analysis or tool. Use this flag alone to overwrite everything.")) 

582 

583 

584# Add a new to command to aid in the inputs file setup 

585inputs_parser = subparsers.add_parser("inputs", 

586 help="Set the inputs file", 

587 formatter_class=CustomHelpFormatter, 

588 parents=[common_parser] 

589) 

590 

591# Chose the editor in advance 

592inputs_parser.add_argument( 

593 "-ed", "--editor", 

594 choices=[*AVAILABLE_TEXT_EDITORS.values(), 'none'], 

595 help="Set the text editor to modify the inputs file") 

596 

597# The convert command 

598convert_parser = subparsers.add_parser("convert", 

599 help="Convert a structure and/or several trajectories to other formats\n" + 

600 "If several input trajectories are passed they will be merged previously", 

601 formatter_class=CustomHelpFormatter, 

602 parents=[common_parser]) 

603convert_parser.add_argument( 

604 "-is", "--input_structure", 

605 help="Path to input structure file") 

606convert_parser.add_argument( 

607 "-os", "--output_structure", 

608 help="Path to output structure file") 

609convert_parser.add_argument( 

610 "-it", "--input_trajectories", nargs='*', 

611 help="Path to input trajectory file or same format files.") 

612convert_parser.add_argument( 

613 "-ot", "--output_trajectory", 

614 help="Path to output trajectory file") 

615 

616# The filter command 

617filter_parser = subparsers.add_parser("filter", 

618 help="Filter atoms in a structure and/or a trajectory\n", 

619 formatter_class=CustomHelpFormatter, 

620 parents=[common_parser]) 

621filter_parser.add_argument( 

622 "-is", "--input_structure", required=True, 

623 help="Path to input structure file") 

624filter_parser.add_argument( 

625 "-os", "--output_structure", 

626 help="Path to output structure file") 

627filter_parser.add_argument( 

628 "-it", "--input_trajectory", 

629 help="Path to input trajectory file") 

630filter_parser.add_argument( 

631 "-ot", "--output_trajectory", 

632 help="Path to output trajectory file") 

633filter_parser.add_argument( 

634 "-sel", "--selection_string", 

635 help="Atom selection") 

636filter_parser.add_argument( 

637 "-syn", "--selection_syntax", default='vmd', 

638 help="Atom selection syntax (vmd by default)") 

639 

640# The subset command 

641subset_parser = subparsers.add_parser("subset", 

642 help="Get a subset of frames from the current trajectory", 

643 formatter_class=CustomHelpFormatter, 

644 parents=[common_parser]) 

645subset_parser.add_argument( 

646 "-is", "--input_structure", required=True, 

647 help="Path to input structure file") 

648subset_parser.add_argument( 

649 "-it", "--input_trajectory", 

650 help="Path to input trajectory file") 

651subset_parser.add_argument( 

652 "-ot", "--output_trajectory", 

653 help="Path to output trajectory file") 

654subset_parser.add_argument( 

655 "-start", "--start", type=int, default=0, 

656 help="Start frame") 

657subset_parser.add_argument( 

658 "-end", "--end", type=int, default=None, 

659 help="End frame") 

660subset_parser.add_argument( 

661 "-step", "--step", type=int, default=1, 

662 help="Frame step") 

663subset_parser.add_argument( 

664 "-skip", "--skip", nargs='*', type=int, default=[], 

665 help="Frames to be skipped") 

666subset_parser.add_argument( 

667 "-fr", "--frames", nargs='*', type=int, default=[], 

668 help="Frames to be returned. Input frame order is ignored as original frame order is conserved.") 

669 

670# The chainer command 

671chainer_parser = subparsers.add_parser("chainer", 

672 help="Edit structure (pdb) chains", 

673 formatter_class=CustomHelpFormatter, 

674 parents=[common_parser]) 

675chainer_parser.add_argument( 

676 "-is", "--input_structure", required=True, 

677 help="Path to input structure file") 

678chainer_parser.add_argument( 

679 "-os", "--output_structure", default='chained.pdb', 

680 help="Path to output structure file") 

681chainer_parser.add_argument( 

682 "-sel", "--selection_string", 

683 help="Atom selection (the whole structure by default)") 

684chainer_parser.add_argument( 

685 "-syn", "--selection_syntax", default='vmd', 

686 choices=Structure.SUPPORTED_SELECTION_SYNTAXES, 

687 help="Atom selection syntax (VMD syntax by default)") 

688chainer_parser.add_argument( 

689 "-let", "--letter", 

690 help="New chain letter (one letter per fragment by default)") 

691chainer_parser.add_argument( 

692 "-whfr", "--whole_fragments", type=bool, default=True, 

693 help="Consider fragments beyond the atom selection. Otherwise a fragment could end up having multiple chains.") 

694 

695# The NASSA commands  

696nassa_parser = subparsers.add_parser("nassa", formatter_class=CustomHelpFormatter, 

697 help="Run and set the configuration of the NASSA analysis", 

698 parents=[common_parser]) 

699nassa_parser.add_argument( 

700 "-c", "--config", 

701 help="Configuration file for the NASSA analysis") 

702nassa_parser.add_argument( 

703 "-n", "--analysis_names", 

704 nargs='*', 

705 default=None, 

706 help="Name of the analysis to be run. It can be: " + ', '.join(NASSA_ANALYSES_LIST)) 

707nassa_parser.add_argument( 

708 "-w", "--make_config", 

709 #type=str, 

710 nargs='*', 

711 default=None, 

712 # const=True, 

713 # action=custom, 

714 help="Make a configuration file for the NASSA analysis: makecfg.\nThe base path could be given as an argument. If not, an example of configuration file is created.") 

715nassa_parser.add_argument( 

716 "-seq", "--seq_path", 

717 type=str, 

718 const=False, 

719 action=custom, 

720 help="Set the base path of the sequences. If not given, the sequences are searched in the current directory.") 

721nassa_parser.add_argument( 

722 "-o", "--output", 

723 help="Output path for the NASSA analysis") 

724nassa_parser.add_argument( 

725 "-dir", "--working_directory", 

726 default='.', 

727 help="Directory where the whole workflow is run. Current directory by default.") 

728nassa_parser.add_argument( 

729 "-ow", "--overwrite", 

730 type=str, 

731 nargs='*', 

732 default=[], 

733 action=custom, 

734 const=True, 

735 help="Set the output files to be overwritten thus re-runing its corresponding analysis or tool") 

736nassa_parser.add_argument( 

737 "-own", "--overwrite_nassa", 

738 type=str, 

739 nargs='*', 

740 default=[], 

741 action=custom, 

742 const=True, 

743 help="Set the output files to be overwritten thus re-runing its corresponding analysis or tool for the NASSA analysis") 

744nassa_parser.add_argument( 

745 "-nseq", "--n_sequences", 

746 type=int, 

747 help="Number of sequences to be analyzed") 

748nassa_parser.add_argument( 

749 "-i", "--unit_len", 

750 type=int, 

751 default=6, 

752 help="Number of base pairs to be analyzed") 

753nassa_parser.add_argument( 

754 "-hp", "--helical_parameters", 

755 action='store_true', 

756 default=False, 

757 help="Run the helical parameters analysis") 

758nassa_parser.add_argument( 

759 "-pdirs", "--proj_directories", 

760 nargs='*', 

761 default=None, 

762 help=("Path to the different project directories. Each directory is to contain an independent project.\n" 

763 "Several output files will be generated in the same folder directory")) 

764nassa_parser.add_argument( 

765 "-all", "--all", 

766 action='store_true', 

767 default=False, 

768 help="Run all the helical parameters and NASSA analyses") 

769nassa_parser.add_argument( 

770 "-stru", "--input_structure_filepath", 

771 default=None, 

772 help=("Path to input structure file. It may be relative to the project or to each MD directory.\n" 

773 "If this value is not passed then the standard structure file is used as input by default")) 

774nassa_parser.add_argument( 

775 "-traj", "--input_trajectory_filepath", 

776 nargs='*', 

777 default=None, 

778 help=("Path to input trajectory file. It is relative to each MD directory.\n" 

779 "If this value is not passed then the standard trajectory file path is used as input by default")) 

780nassa_parser.add_argument( 

781 "-top", "--input_topology_filepath", 

782 default=None, # There is no default since many formats may be possible 

783 help="Path to input topology file. It is relative to the project directory.") 

784nassa_parser.add_argument( 

785 "-mdir", "--md_directories", 

786 nargs='*', 

787 default=None, 

788 help=("Path to the different MD directories. Each directory is to contain an independent trajectory and structure.\n" 

789 "Several output files will be generated in every MD directory")) 

790nassa_parser.add_argument( 

791 "-t", "--trust", 

792 type=str, 

793 nargs='*', 

794 default=[], 

795 action=custom, 

796 const=AVAILABLE_CHECKINGS, 

797 choices=AVAILABLE_CHECKINGS, 

798 help="If passed, do not run the specified checking. Note that all checkings are skipped if passed alone.\n" + pretty_list(AVAILABLE_CHECKINGS) 

799) 

800nassa_parser.add_argument( 

801 "-m", "--mercy", 

802 type=str, 

803 nargs='*', 

804 default=[], 

805 action=custom, 

806 const=AVAILABLE_FAILURES, 

807 choices=AVAILABLE_FAILURES, 

808 help=("If passed, do not kill the process when any of the specfied checkings fail and proceed with the workflow.\n" 

809 "Note that all checkings are allowed to fail if the argument is passed alone.\n" + pretty_list(AVAILABLE_FAILURES)) 

810) 

811nassa_parser.add_argument( 

812 "-dup", "--duplicates", 

813 default=False, 

814 action='store_true', 

815 help="If passed, merge duplicate subunits if there is more than one, in the sequences. if not only the last will be selected" 

816)