From cf304275cd4bd04a9c571c02988d9742da4f78ff Mon Sep 17 00:00:00 2001 From: James Graham <j.graham@soton.ac.uk> Date: Thu, 26 Nov 2020 22:03:02 +0000 Subject: [PATCH] refactor: prettify console output --- poetry.lock | 66 +++++++++++++++++++++++++++++++- pycgtool/__main__.py | 89 ++++++++++++++++++++++++++++++------------- pycgtool/frame.py | 5 ++- pycgtool/mapping.py | 6 +-- pyproject.toml | 1 + test/test_pycgtool.py | 4 +- 6 files changed, 136 insertions(+), 35 deletions(-) diff --git a/poetry.lock b/poetry.lock index ce586f3..0a63a9a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,6 +85,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "coverage" version = "5.3" @@ -104,6 +115,14 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "dataclasses" +version = "0.7" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = ">=3.6, <3.7" + [[package]] name = "docutils" version = "0.16" @@ -376,7 +395,7 @@ name = "pygments" version = "2.7.1" description = "Pygments is a syntax highlighting package written in Python." category = "main" -optional = true +optional = false python-versions = ">=3.5" [[package]] @@ -537,6 +556,24 @@ python-versions = "*" [package.dependencies] astroid = ">=1.4" +[[package]] +name = "rich" +version = "9.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +colorama = ">=0.4.0,<0.5.0" +commonmark = ">=0.9.0,<0.10.0" +dataclasses = {version = ">=0.7,<0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} +pygments = ">=2.6.0,<3.0.0" +typing-extensions = ">=3.7.4,<4.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "rstcheck" version = "3.3.1" @@ -737,6 +774,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "unidecode" version = "1.1.1" @@ -804,7 +849,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "4ec5a3e4bfd4075233d7356350beeb69f09f659a18bb7d1450d209e40f15447a" +content-hash = "684ae060e918da15c709fa66630983cb36502abed30a288f1e015ed337f0c1a0" [metadata.files] alabaster = [ @@ -843,6 +888,10 @@ colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] coverage = [ {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, @@ -914,6 +963,10 @@ cython = [ {file = "Cython-0.29.21-py2.py3-none-any.whl", hash = "sha256:5c4276fdcbccdf1e3c1756c7aeb8395e9a36874fa4d30860e7694f43d325ae13"}, {file = "Cython-0.29.21.tar.gz", hash = "sha256:e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad"}, ] +dataclasses = [ + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, +] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, @@ -1140,6 +1193,10 @@ requests = [ requirements-detector = [ {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"}, ] +rich = [ + {file = "rich-9.2.0-py3-none-any.whl", hash = "sha256:564fee761e0756c3a4fbe97517166703754e11b4e861983cf184c78c9de8b570"}, + {file = "rich-9.2.0.tar.gz", hash = "sha256:7003a1cce3b79bf4d34a26099b00a4b67e208d4a6896a1c25368603d5f49f295"}, +] rstcheck = [ {file = "rstcheck-3.3.1.tar.gz", hash = "sha256:92c4f79256a54270e0402ba16a2f92d0b3c15c8f4410cb9c57127067c215741f"}, ] @@ -1235,6 +1292,11 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] unidecode = [ {file = "Unidecode-1.1.1-py2.py3-none-any.whl", hash = "sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a"}, {file = "Unidecode-1.1.1.tar.gz", hash = "sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8"}, diff --git a/pycgtool/__main__.py b/pycgtool/__main__.py index 7c31421..a49263f 100755 --- a/pycgtool/__main__.py +++ b/pycgtool/__main__.py @@ -5,8 +5,11 @@ import cProfile import logging import pathlib import sys +import textwrap import typing +from rich.logging import RichHandler + from .frame import Frame from .mapping import Mapping from .bondset import BondSet @@ -37,31 +40,31 @@ def measure_bonds(frame: Frame, mapping: typing.Optional[Mapping], :param mapping: :param config: Program arguments from argparse """ - bonds = BondSet(config.bnd, config) - - logger.info("Bond measurements will be made") + bonds = BondSet(config.bondset, config) bonds.apply(frame) - if config.map and config.trajectory: + if config.mapping and config.trajectory: # Only perform Boltzmann Inversion if we have a mapping and a trajectory. # Otherwise we get infinite force constants. - logger.info("Beginning Boltzmann Inversion") + logger.info('Starting Boltzmann Inversion') bonds.boltzmann_invert() + logger.info('Finished Boltzmann Inversion') if config.output_forcefield: - logger.info("Creating GROMACS forcefield directory") + logger.info("Writing GROMACS forcefield directory") out_dir = pathlib.Path(config.out_dir) forcefield = ForceField(config.output_name, dir_path=out_dir) forcefield.write(config.output_name, mapping, bonds) - logger.info("GROMACS forcefield directory created") + logger.info("Finished writing GROMACS forcefield directory") else: bonds.write_itp(get_output_filepath('itp', config), mapping=mapping) if config.dump_measurements: - logger.info("Dumping bond measurements to file") + logger.info('Writing bond measurements to file') bonds.dump_values(config.dump_n_values, config.out_dir) + logger.info('Finished writing bond measurements to file') def mapping_loop(frame: Frame, config) -> typing.Tuple[Frame, Mapping]: @@ -70,11 +73,12 @@ def mapping_loop(frame: Frame, config) -> typing.Tuple[Frame, Mapping]: :param frame: :param config: Program arguments from argparse """ - logger.info("Mapping will be performed") - mapping = Mapping(config.map, config, itp_filename=config.itp) + logger.info('Starting AA->CG mapping') + mapping = Mapping(config.mapping, config, itp_filename=config.itp) cg_frame = mapping.apply(frame) cg_frame.save(get_output_filepath('gro', config), frame_number=0) + logging.info('Finished AA->CG mapping') return cg_frame, mapping @@ -92,24 +96,24 @@ def full_run(config): frame_start=config.begin, frame_end=config.end) - if config.map: + if config.mapping: cg_frame, mapping = mapping_loop(frame, config) else: - logger.info("Mapping will not be performed") + logger.info('Skipping AA->CG mapping') mapping = None cg_frame = frame if config.output_xtc: cg_frame.save(get_output_filepath('xtc', config)) - if config.bnd: + if config.bondset: measure_bonds(cg_frame, mapping, config) class BooleanAction(argparse.Action): """Set up a boolean argparse argument with matching `--no` argument. - + Based on https://thisdataguy.com/2017/07/03/no-options-with-argparse-and-python/ """ def __init__(self, option_strings, dest, nargs=None, **kwargs): @@ -134,9 +138,9 @@ def parse_arguments(arg_list): help="AA simulation topology - e.g. PDB, GRO, etc.") input_files.add_argument('trajectory', type=str, nargs='?', help="AA simulation trajectory - e.g. XTC, DCD, etc.") - input_files.add_argument('-m', '--map', type=str, + input_files.add_argument('-m', '--mapping', type=str, help="Mapping file") - input_files.add_argument('-b', '--bnd', type=str, + input_files.add_argument('-b', '--bondset', type=str, help="Bonds file") input_files.add_argument('-i', '--itp', type=str, help="GROMACS ITP file") @@ -200,6 +204,9 @@ def parse_arguments(arg_list): run_options.add_argument('--profile', '--no-profile', default=False, action=BooleanAction, help="Profile performance?") + run_options.add_argument('--log-level', default='INFO', + choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), + help="Which log messages should be shown?") # yapf: enable args = parser.parse_args(arg_list) @@ -219,12 +226,12 @@ def validate_arguments(args): :param args: Parsed arguments from ArgumentParser """ if not args.dump_measurements: - args.dump_measurements = bool(args.bnd) and not bool(args.map) + args.dump_measurements = bool(args.bondset) and not bool(args.mapping) if not args.map_only: - args.map_only = not bool(args.bnd) + args.map_only = not bool(args.bondset) - if not args.map and not args.bnd: + if not args.mapping and not args.bondset: raise ArgumentValidationError("One or both of -m and -b is required.") return args @@ -233,17 +240,47 @@ def validate_arguments(args): def main(): args = parse_arguments(sys.argv[1:]) - print("Using GRO: {0}".format(args.topology)) - print("Using XTC: {0}".format(args.trajectory)) + logging.basicConfig(level=args.log_level, + format='%(message)s', + datefmt='[%X]', + handlers=[RichHandler()]) + + banner = """\ + _____ _____ _____ _______ ____ ____ _ + | __ \ / ____/ ____|__ __/ __ \ / __ \| | + | |__) | _| | | | __ | | | | | | | | | | + | ___/ | | | | | | |_ | | | | | | | | | | | + | | | |_| | |___| |__| | | | | |__| | |__| | |____ + |_| \__, |\_____\_____| |_| \____/ \____/|______| + __/ | + |___/ + """ # noqa + + logger.info('[bold blue]%s[/]', + textwrap.dedent(banner), + extra={'markup': True}) + + logger.info(30 * '-') + logger.info('Topology:\t%s', args.topology) + logger.info('Trajectory:\t%s', args.trajectory) + logger.info('Mapping:\t%s', args.mapping) + logger.info('Bondset:\t%s', args.bondset) + logger.info(30 * '-') + + try: + if args.profile: + with cProfile.Profile() as profiler: + full_run(args) + + profiler.dump_stats('gprof.out') - if args.profile: - with cProfile.Profile() as profiler: + else: full_run(args) - profiler.dump_stats('gprof.out') + logger.info('Finished processing - goodbye!') - else: - full_run(args) + except Exception as exc: + logger.error(exc) if __name__ == "__main__": diff --git a/pycgtool/frame.py b/pycgtool/frame.py index d3bfe79..eb212ef 100644 --- a/pycgtool/frame.py +++ b/pycgtool/frame.py @@ -50,15 +50,18 @@ class Frame: """ if topology_file is not None: try: + logging.info('Loading topology file') self._trajectory = mdtraj.load(str(topology_file)) self._topology = self._trajectory.topology + logging.info('Finished loading topology file') if trajectory_file is not None: try: + logging.info('Loading trajectory file - this may take a while') self._trajectory = mdtraj.load(str(trajectory_file), top=self._topology) - self._slice_trajectory(frame_start, frame_end) + logging.info('Finished loading trajectory file') except ValueError as exc: raise NonMatchingSystemError from exc diff --git a/pycgtool/mapping.py b/pycgtool/mapping.py index 7046654..d47508d 100644 --- a/pycgtool/mapping.py +++ b/pycgtool/mapping.py @@ -249,7 +249,7 @@ class Mapping: name, typ, first, *atoms = mol_section[i][1:] except KeyError as exc: - raise ValueError(f'"{name}" line prefix invalid') from exc + raise ValueError(f'Invalid line prefix "{name}" in mapping file') from exc try: # Allow optional charge in mapping file @@ -262,8 +262,7 @@ class Mapping: atoms.insert(0, first) if not atoms: - # TODO should this stop execution? - logger.warning('Bead %s specification contains no atoms', name) + raise ValueError(f'Bead {name} specification contains no atoms') mol_map.append( bead_class(name, i, type=typ, atoms=atoms, charge=charge)) @@ -421,7 +420,6 @@ class Mapping: return cg_frame -# TODO: Use MDTraj instead def calc_coords_weight(ref_coords, coords, weights, box=None): """Calculate the coordinates of a single CG bead from weighted component atom coordinates. diff --git a/pyproject.toml b/pyproject.toml index 1bcfe2c..9fa18b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ wheel = "^0.35.1" numpy = "^1.19.1" cython = "^0.29.21" mdtraj = "^1.9.4" +rich = "^9.2.0" # Optional extras to enable additional functionality sphinx-autoapi = { version = "^1.5.0", optional = true } diff --git a/test/test_pycgtool.py b/test/test_pycgtool.py index 1d1cc7a..d0e7db3 100644 --- a/test/test_pycgtool.py +++ b/test/test_pycgtool.py @@ -57,7 +57,7 @@ class PycgtoolTest(unittest.TestCase): ]) self.assertEqual('TOPOLOGY', args.topology) - self.assertEqual('MAP', args.map) + self.assertEqual('MAP', args.mapping) self.assertEqual(1000, args.begin) def test_map_only(self): @@ -117,7 +117,7 @@ class PycgtoolTest(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: tmp_path = pathlib.Path(tmpdir) args = get_args('sugar', tmp_path, extra={ - 'map': None, + 'mapping': None, 'trajectory': None, }) -- GitLab