diff --git a/pack_mm/cli/packmm.py b/pack_mm/cli/packmm.py index d6eba9d..07565ed 100644 --- a/pack_mm/cli/packmm.py +++ b/pack_mm/cli/packmm.py @@ -12,7 +12,7 @@ from typer import Exit, Option, Typer from typer_config import use_config -from pack_mm.core.core import pack_molecules +from pack_mm.core.core import pack_molecules, setup_file_logger class InsertionMethod(str, Enum): @@ -133,41 +133,45 @@ def packmm( ), geometry: bool = Option(True, help="Perform geometry optimization at the end."), out_path: str = Option(".", help="path to save various outputs."), + log: str = Option("pack-mm.log", help="file to save logs."), ): """Pack molecules into a system based on the specified parameters.""" - print("Script called with following input") - print(f"{system=}") - print(f"{nmols=}") - print(f"{molecule=}") - print(f"{ntries=}") - print(f"{seed=}") - print(f"where={where.value}") - print(f"{centre=}") - print(f"{radius=}") - print(f"{height=}") - print(f"{a=}") - print(f"{b=}") - print(f"{c=}") - print(f"{cell_a=}") - print(f"{cell_b=}") - print(f"{cell_c=}") - print(f"{arch=}") - print(f"{model=}") - print(f"{dispersion=}") - print(f"{device=}") - print(f"{temperature=}") - print(f"{fmax=}") - print(f"{threshold=}") - print(f"{geometry=}") - print(f"{out_path=}") - print(f"{every=}") - print(f"insert_strategy={insert_strategy.value}") - print(f"relax_strategy={relax_strategy.value}") - print(f"{md_steps=}") - print(f"{md_timestep=}") - print(f"{md_temperature=}") + logger = setup_file_logger(log_file=log) + logger.info("Script called with following input") + logger.info(f"{system=}") + logger.info(f"{nmols=}") + logger.info(f"{molecule=}") + logger.info(f"{ntries=}") + logger.info(f"{seed=}") + logger.info(f"where={where.value}") + logger.info(f"{centre=}") + logger.info(f"{radius=}") + logger.info(f"{height=}") + logger.info(f"{a=}") + logger.info(f"{b=}") + logger.info(f"{c=}") + logger.info(f"{cell_a=}") + logger.info(f"{cell_b=}") + logger.info(f"{cell_c=}") + logger.info(f"{arch=}") + logger.info(f"{model=}") + logger.info(f"{dispersion=}") + logger.info(f"{device=}") + logger.info(f"{temperature=}") + logger.info(f"{fmax=}") + logger.info(f"{threshold=}") + logger.info(f"{geometry=}") + logger.info(f"{out_path=}") + logger.info(f"{every=}") + logger.info(f"insert_strategy={insert_strategy.value}") + logger.info(f"relax_strategy={relax_strategy.value}") + logger.info(f"{md_steps=}") + logger.info(f"{md_timestep=}") + logger.info(f"{md_temperature=}") + logger.info(f"log_file={log}") + print(f"Output is logger to {log}") if nmols == -1: - print("nothing to do, no molecule to insert") + logger.info("nothing to do, no molecule to insert") raise Exit(0) center = centre @@ -176,7 +180,7 @@ def packmm( lc = [x < 0.0 for x in center] if len(center) != 3 or any(lc): err = "Invalid centre 3 coordinates expected!" - print(f"{err}") + logger.info(f"{err}") raise Exception("Invalid centre 3 coordinates expected!") pack_molecules( @@ -209,4 +213,5 @@ def packmm( md_steps=md_steps, md_timestep=md_timestep, md_temperature=md_temperature, + logger=logger, ) diff --git a/pack_mm/core/core.py b/pack_mm/core/core.py index 650ca61..62dfed7 100644 --- a/pack_mm/core/core.py +++ b/pack_mm/core/core.py @@ -6,6 +6,8 @@ from __future__ import annotations +from copy import copy +import logging from pathlib import Path from ase import Atoms @@ -18,6 +20,29 @@ from numpy import cos, exp, pi, random, sin, sqrt +def setup_file_logger(log_file="pack-mm.log", log_level=logging.INFO): + """ + Set up a basic file logger. + + Args: + log_file (str): The name of the file to write logs to. + log_level (int): The minimum level of messages to log + (e.g., logging.DEBUG, logging.INFO). + """ + logger = logging.getLogger(__name__) + logger.setLevel(log_level) + + file_handler = logging.FileHandler(log_file) + + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + + def random_point_in_sphere(c: (float, float, float), r: float) -> (float, float, float): """ Generate a random point inside a sphere of radius r, centered at c. @@ -131,11 +156,14 @@ def random_point_in_cylinder( return (x, y, z) -def validate_value(label: str, x: float | int) -> None: +def validate_value(label: str, x: float | int, logger: logging.logger = None) -> None: """Validate input value, and raise an exception.""" if x is not None and x < 0.0: err = f"Invalid {label}, needs to be positive" - print(err) + if logger: + logger.info(err) + else: + print(err) raise Exception(err) @@ -216,6 +244,7 @@ def pack_molecules( md_steps: int = 10, md_timestep: float = 1.0, md_temperature: float = 100.0, + logger: logging.logger = None, ) -> tuple(float, Atoms): """ Pack molecules into a system based on the specified parameters. @@ -259,22 +288,22 @@ def pack_molecules( """ kbt = temperature * kB - validate_value("temperature", temperature) - validate_value("radius", radius) - validate_value("height", height) - validate_value("fmax", fmax) - validate_value("seed", seed) - validate_value("box a", a) - validate_value("box b", b) - validate_value("box c", c) - validate_value("ntries", ntries) - validate_value("cell box cell a", cell_a) - validate_value("cell box cell b", cell_b) - validate_value("cell box cell c", cell_c) - validate_value("nmols", nmols) - validate_value("MD steps", md_steps) - validate_value("MD timestep", md_timestep) - validate_value("MD temperature", md_temperature) + validate_value("temperature", temperature, logger) + validate_value("radius", radius, logger) + validate_value("height", height, logger) + validate_value("fmax", fmax, logger) + validate_value("seed", seed, logger) + validate_value("box a", a, logger) + validate_value("box b", b, logger) + validate_value("box c", c, logger) + validate_value("ntries", ntries, logger) + validate_value("cell box cell a", cell_a, logger) + validate_value("cell box cell b", cell_b, logger) + validate_value("cell box cell c", cell_c, logger) + validate_value("nmols", nmols, logger) + validate_value("MD steps", md_steps, logger) + validate_value("MD timestep", md_timestep, logger) + validate_value("MD temperature", md_temperature, logger) set_random_seed(seed) @@ -289,9 +318,13 @@ def pack_molecules( sysname = Path(system).stem + "+" # Print summary - print(f"Inserting {nmols} {molecule} molecules in {sysname}.") - print(f"Using {arch} model {model} on {device}.") - print(f"Insert in {where}.") + summary = f"""Inserting {nmols} {molecule} molecules in {sysname}. + Using {arch} model {model} on {device}. + Insert in {where}.""" + if logger: + logger.info(summary) + else: + print(summary) cell = sys.cell.lengths() @@ -305,12 +338,12 @@ def pack_molecules( device=device, dispersion=dispersion, ) - sys.calc = calc + sys.calc = copy(calc) e = sys.get_potential_energy() if len(sys) > 0 else 0.0 mol = load_molecule(molecule) - mol.calc = calc + mol.calc = copy(calc) emol = mol.get_potential_energy() csys = sys.copy() @@ -351,13 +384,17 @@ def pack_molecules( relax_strategy=relax_strategy, ) - tsys.calc = calc + tsys.calc = copy(calc) en = tsys.get_potential_energy() de = en - e acc = exp(-de / kbt) u = random.random() - print(f"Old energy={e}, new energy={en}, {de=}, {acc=}, random={u}") + message_ene = f"Old energy={e}, new energy={en}, {de=}, {acc=}, random={u}" + if logger: + logger.info(message_ene) + else: + print(message_ene) if abs(de / emol) > threshold and u <= acc: accept = True @@ -366,12 +403,22 @@ def pack_molecules( csys = tsys.copy() e = en i += 1 - print(f"Inserted particle {i}") + message_insert = f"Inserted particle {i}" + if logger: + logger.info(message_insert) + else: + print(message_insert) write(Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif", csys) else: # Things are bad, maybe geomatry optimisation saves us # once you hit here is bad, this can keep looping - print(f"Failed to insert particle {i + 1} after {ntries} tries") + message_fail = f"""Failed to insert particle {i + 1} after {ntries} tries. + Trying alternative methods, {relax_strategy}, to save the day. + """ + if logger: + logger.info(message_fail) + else: + print(message_fail) csys = save_the_day( csys, device, diff --git a/pyproject.toml b/pyproject.toml index 510b80d..54d812c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pack-mm" -version = "0.1.12" +version = "0.1.14" description = "packing materials and molecules in boxes using for machine learnt interatomic potentials" authors = [ { name = "Alin M. Elena" }, diff --git a/tests/test_advanced.py b/tests/test_advanced.py index 1a9dd7a..c9c1dea 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -15,7 +15,7 @@ runner = CliRunner() -err = 1.0e-8 +err = 1.0e-5 def test_packmm_hmc(tmp_path): @@ -84,4 +84,4 @@ def test_packmm_every(tmp_path): assert (tmp_path / "system+1H2.cif").exists() assert (tmp_path / "system+2H2.cif").exists() f = read(tmp_path / "system+2H2.cif") - assert f[0].position == pytest.approx([1.24701529, 0.024216, 0.11632905], abs=err) + assert f[0].position == pytest.approx([1.247015, 0.024216, 0.116329], abs=err) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8b77713..6dde3b2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,14 +14,14 @@ runner = CliRunner() -def test_packmm_default_values(): +def test_packmm_default_values(caplog): """Check values.""" result = runner.invoke(app) assert result.exit_code == 0 - assert "nothing to do" in strip_ansi_codes(result.output) + assert "nothing to do" in caplog.text -def test_packmm_config(tmp_path): +def test_packmm_config(tmp_path, caplog): """Check config file.""" with open(tmp_path / "sphere.yml", "w", encoding="utf-8") as f: conf = """nmols: 1 @@ -34,178 +34,179 @@ def test_packmm_config(tmp_path): """ print(conf, file=f) result = runner.invoke(app, ["--config", tmp_path / "sphere.yml"]) - assert "nmols=1" in strip_ansi_codes(result.output) + assert result.exit_code == 0 + assert "nmols=1" in caplog.text -def test_packmm_custom_molecule(): +def test_packmm_custom_molecule(caplog): """Check molecule.""" result = runner.invoke(app, ["--molecule", "CO2"]) assert result.exit_code == 0 - assert "molecule='CO2'" in strip_ansi_codes(result.output) + assert "molecule='CO2'" in caplog.text -def test_packmm_custom_nmols(): +def test_packmm_custom_nmols(caplog): """Check nmols.""" result = runner.invoke(app, ["--nmols", "-2"]) assert result.exit_code == 1 - assert "nmols=-2" in strip_ansi_codes(result.output) + assert "nmols=-2" in caplog.text -def test_packmm_custom_ntries(): +def test_packmm_custom_ntries(caplog): """Check ntries.""" result = runner.invoke(app, ["--ntries", "1"]) assert result.exit_code == 0 - assert "ntries=1" in strip_ansi_codes(result.output) + assert "ntries=1" in caplog.text -def test_packmm_custom_seed(): +def test_packmm_custom_seed(caplog): """Check seed.""" result = runner.invoke(app, ["--seed", "1234"]) assert result.exit_code == 0 - assert "seed=1234" in strip_ansi_codes(result.output) + assert "seed=1234" in caplog.text -def test_packmm_custom_every(): +def test_packmm_custom_every(caplog): """Check seed.""" result = runner.invoke(app, ["--every", "10"]) assert result.exit_code == 0 - assert "every=10" in strip_ansi_codes(result.output) + assert "every=10" in caplog.text -def test_packmm_custom_insertion_method(): +def test_packmm_custom_insertion_method(caplog): """Check insertion.""" result = runner.invoke(app, ["--where", "sphere"]) assert result.exit_code == 0 - assert "where=sphere" in strip_ansi_codes(result.output) + assert "where=sphere" in caplog.text -def test_packmm_custom_insert_strategy(): +def test_packmm_custom_insert_strategy(caplog): """Check insertion.""" result = runner.invoke(app, ["--insert-strategy", "hmc"]) assert result.exit_code == 0 - assert "insert_strategy=hmc" in strip_ansi_codes(result.output) + assert "insert_strategy=hmc" in caplog.text -def test_packmm_custom_relax_strategy(): +def test_packmm_custom_relax_strategy(caplog): """Check relax.""" result = runner.invoke(app, ["--relax-strategy", "md"]) assert result.exit_code == 0 - assert "relax_strategy=md" in strip_ansi_codes(result.output) + assert "relax_strategy=md" in caplog.text -def test_packmm_custom_center(): +def test_packmm_custom_center(caplog): """Check centre.""" result = runner.invoke(app, ["--centre", "0.5,0.5,0.5"]) assert result.exit_code == 0 - assert "centre='0.5,0.5,0.5'" in strip_ansi_codes(result.output) + assert "centre='0.5,0.5,0.5'" in caplog.text -def test_packmm_custom_radius(): +def test_packmm_custom_radius(caplog): """Check radius.""" result = runner.invoke(app, ["--radius", "10.0"]) assert result.exit_code == 0 - assert "radius=10.0" in strip_ansi_codes(result.output) + assert "radius=10.0" in caplog.text -def test_packmm_custom_height(): +def test_packmm_custom_height(caplog): """Check height.""" result = runner.invoke(app, ["--height", "5.0"]) assert result.exit_code == 0 - assert "height=5.0" in strip_ansi_codes(result.output) + assert "height=5.0" in caplog.text -def test_packmm_mlip(): +def test_packmm_mlip(caplog): """Check mlip.""" result = runner.invoke( app, ["--arch", "mace", "--model", "some", "--device", "cuda"] ) assert result.exit_code == 0 - assert "arch='mace'" in strip_ansi_codes(result.output) - assert "model='some'" in strip_ansi_codes(result.output) - assert "device='cuda'" in strip_ansi_codes(result.output) + assert "arch='mace'" in caplog.text + assert "model='some'" in caplog.text + assert "device='cuda'" in caplog.text -def test_packmm_out_path(): +def test_packmm_out_path(caplog): """Check out_path.""" result = runner.invoke(app, ["--out-path", "out"]) assert result.exit_code == 0 - assert "out_path='out'" in strip_ansi_codes(result.output) + assert "out_path='out'" in caplog.text -def test_packmm_custom_box_dimensions(): +def test_packmm_custom_box_dimensions(caplog): """Check box.""" result = runner.invoke(app, ["--a", "30.0", "--b", "30.0", "--c", "30.0"]) assert result.exit_code == 0 - assert "a=30.0" in strip_ansi_codes(result.output) - assert "b=30.0" in strip_ansi_codes(result.output) - assert "c=30.0" in strip_ansi_codes(result.output) + assert "a=30.0" in caplog.text + assert "b=30.0" in caplog.text + assert "c=30.0" in caplog.text -def test_packmm_empty_box_dimensions(): +def test_packmm_empty_box_dimensions(caplog): """Check box empty.""" result = runner.invoke( app, ["--cell-a", "30.0", "--cell-b", "30.0", "--cell-c", "30.0"] ) assert result.exit_code == 0 - assert "cell_a=30.0" in strip_ansi_codes(result.output) - assert "cell_b=30.0" in strip_ansi_codes(result.output) - assert "cell_c=30.0" in strip_ansi_codes(result.output) + assert "cell_a=30.0" in caplog.text + assert "cell_b=30.0" in caplog.text + assert "cell_c=30.0" in caplog.text -def test_packmm_custom_temperature(): +def test_packmm_custom_temperature(caplog): """Check temperature.""" result = runner.invoke(app, ["--temperature", "400.0"]) assert result.exit_code == 0 - assert "temperature=400.0" in strip_ansi_codes(result.output) + assert "temperature=400.0" in caplog.text -def test_packmm_md_temperature(): +def test_packmm_md_temperature(caplog): """Check md temperature.""" result = runner.invoke(app, ["--md-temperature", "300.0"]) assert result.exit_code == 0 - assert "md_temperature=300.0" in strip_ansi_codes(result.output) + assert "md_temperature=300.0" in caplog.text -def test_packmm_md_timestep(): +def test_packmm_md_timestep(caplog): """Check md temperature.""" result = runner.invoke(app, ["--md-timestep", "1.0"]) assert result.exit_code == 0 - assert "md_timestep=1.0" in strip_ansi_codes(result.output) + assert "md_timestep=1.0" in caplog.text -def test_packmm_md_steps(): +def test_packmm_md_steps(caplog): """Check md steps.""" result = runner.invoke(app, ["--md-steps", "10"]) assert result.exit_code == 0 - assert "md_steps=10" in strip_ansi_codes(result.output) + assert "md_steps=10" in caplog.text -def test_packmm_custom_fmax(): +def test_packmm_custom_fmax(caplog): """Check fmax.""" result = runner.invoke(app, ["--fmax", "0.05"]) assert result.exit_code == 0 - assert "fmax=0.05" in strip_ansi_codes(result.output) + assert "fmax=0.05" in caplog.text -def test_packmm_custom_threshold(): +def test_packmm_custom_threshold(caplog): """Check threshold.""" result = runner.invoke(app, ["--threshold", "0.9"]) assert result.exit_code == 0 - assert "threshold=0.9" in strip_ansi_codes(result.output) + assert "threshold=0.9" in caplog.text -def test_packmm_with_dispersion(): +def test_packmm_with_dispersion(caplog): """Check dispersion.""" result = runner.invoke(app, ["--dispersion"]) assert result.exit_code == 0 - assert "dispersion=True" in strip_ansi_codes(result.output) + assert "dispersion=True" in caplog.text -def test_packmm_no_geometry_optimization(): +def test_packmm_no_geometry_optimization(caplog): """Check optimisation.""" result = runner.invoke(app, ["--no-geometry"]) assert result.exit_code == 0 - assert "geometry=False" in strip_ansi_codes(result.output) + assert "geometry=False" in caplog.text def test_packmm_invalid_insertion_method(): @@ -229,82 +230,85 @@ def test_packmm_invalid_relax_strategy(): assert "Invalid value for '--relax-strategy'" in strip_ansi_codes(result.output) -def test_packmm_invalid_md_steps(): +def test_packmm_invalid_md_steps(caplog): """Check md steps.""" result = runner.invoke(app, ["--nmols", "1", "--md-steps", "-10"]) - assert "Invalid MD steps" in strip_ansi_codes(result.output) + assert result.exit_code != 0 + assert "Invalid MD steps" in caplog.text -def test_packmm_invalid_md_temperature(): +def test_packmm_invalid_md_temperature(caplog): """Check md steps.""" result = runner.invoke(app, ["--nmols", "1", "--md-temperature", "-10.0"]) - assert "Invalid MD temperature" in strip_ansi_codes(result.output) + assert result.exit_code != 0 + assert "Invalid MD temperature" in caplog.text -def test_packmm_invalid_md_timestep(): +def test_packmm_invalid_md_timestep(caplog): """Check md timestep.""" result = runner.invoke(app, ["--nmols", "1", "--md-timestep", "-10.0"]) - assert "Invalid MD timestep" in strip_ansi_codes(result.output) + assert result.exit_code != 0 + assert "Invalid MD timestep" in caplog.text -def test_packmm_invalid_centre_format(): +def test_packmm_invalid_centre_format(caplog): """Check centre.""" result = runner.invoke(app, ["--nmols", "1", "--centre", "0.5,0.5"]) assert result.exit_code != 0 - assert "Invalid centre" in strip_ansi_codes(result.output) + assert "Invalid centre" in caplog.text -def test_packmm_invalid_radius(): +def test_packmm_invalid_radius(caplog): """Check box radius.""" result = runner.invoke(app, ["--nmols", "1", "--radius", "-10.0"]) assert result.exit_code != 0 - assert "Invalid radius" in strip_ansi_codes(result.output) + assert "Invalid radius" in caplog.text -def test_packmm_invalid_height(): +def test_packmm_invalid_height(caplog): """Check box height.""" result = runner.invoke(app, ["--nmols", "1", "--height", "-5.0"]) assert result.exit_code != 0 - assert "Invalid height" in strip_ansi_codes(result.output) + assert "Invalid height" in caplog.text -def test_packmm_invalid_box_dimensions_a(): +def test_packmm_invalid_box_dimensions_a(caplog): """Check box dimension.""" result = runner.invoke(app, ["--nmols", "1", "--a", "-30.0"]) assert result.exit_code != 0 - assert "Invalid box a" in strip_ansi_codes(result.output) + assert "Invalid box a" in caplog.text -def test_packmm_invalid_box_dimensions_b(): +def test_packmm_invalid_box_dimensions_b(caplog): """Check box dimension.""" result = runner.invoke(app, ["--nmols", "1", "--b", "-30.0"]) assert result.exit_code != 0 - assert "Invalid box b" in strip_ansi_codes(result.output) + assert "Invalid box b" in caplog.text -def test_packmm_invalid_box_dimensions_c(): +def test_packmm_invalid_box_dimensions_c(caplog): """Check box dimension.""" result = runner.invoke(app, ["--nmols", "1", "--c", "-30.0"]) assert result.exit_code != 0 - assert "Invalid box c" in strip_ansi_codes(result.output) + assert "Invalid box c" in caplog.text -def test_packmm_invalid_temperature(): +def test_packmm_invalid_temperature(caplog): """Check temperature.""" result = runner.invoke(app, ["--nmols", "1", "--temperature", "-400.0"]) assert result.exit_code != 0 - assert "Invalid temperature" in strip_ansi_codes(result.output) + assert "Invalid temperature" in caplog.text -def test_packmm_invalid_fmax(): +def test_packmm_invalid_fmax(caplog): """Check fmax.""" result = runner.invoke(app, ["--nmols", "1", "--fmax", "-0.05"]) assert result.exit_code != 0 - assert "Invalid fmax" in strip_ansi_codes(result.output) + assert "Invalid fmax" in caplog.text -def test_packmm_invalid_ntries(): +def test_packmm_invalid_ntries(caplog): """Check ntries.""" result = runner.invoke(app, ["--nmols", "1", "--ntries", "-1"]) assert result.exit_code != 0 - assert "Invalid ntries" in strip_ansi_codes(result.output) + assert "Invalid ntries" in caplog.text diff --git a/tests/test_core.py b/tests/test_core.py index 45e959d..be54b3d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,7 +29,7 @@ validate_value, ) -err = 1.0e-8 +err = 1.0e-6 # Set a fixed seed for reproducibility in tests