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
« 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
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
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")
24expected_project_args = set(Project.__init__.__code__.co_varnames)
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'
29class CustomHelpFormatter(RawTextHelpFormatter):
30 """Custom formatter for argparse help text with better organization and spacing"""
32 def __init__(self, prog, indent_increment=2, max_help_position=6, width=None):
33 super().__init__(prog, indent_increment, max_help_position, width)
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
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
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)
69 # For options with choices, format them nicely
70 opts = ', '.join(action.option_strings)
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}"
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]
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 ---------------------------------------------------------------------------------
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")
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 )
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 :)')
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 :)')
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 )
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)
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")
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")
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)
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)
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")
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)
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
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)
554# Set a list with the alias of all requestable dependencies
555choices = sorted(list(requestables.keys()) + list(DEPENDENCY_FLAGS.keys()))
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."))
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)
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")
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")
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)")
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.")
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.")
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)