diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index d6eb177..ed28a85 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -7,11 +7,6 @@ on: pull_request: branches: - "main" - schedule: - # Run on master by default Sunday morning at 3:30: - # Scheduled workflows run on the latest commit on the default or base branch. - # (from https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule) - - cron: "30 3 * * 0" jobs: ci: diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index ac211fe..0b96ec6 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -13,3 +13,12 @@ jobs: with: src : packmol_step secrets: inherit + + docker: + name: Docker + needs: release + uses: molssi-seamm/devops/.github/workflows/Docker.yaml@main + with: + image : molssi-seamm/seamm-packmol + description: A Packmol executable packaged for use with SEAMM or standalone + secrets: inherit diff --git a/HISTORY.rst b/HISTORY.rst index 6fb2a14..7b14603 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ ======= History ======= +2024.3.19 -- Updated installer for new scheme + * packmol-step-installer now uses the new scheme, which supports both Conda and + Docker installation. + * Added seamm-packmol Docker image + 2024.1.16 -- Adding support for containers * Added the ability to work in Docker containers. diff --git a/devtools/docker/Dockerfile b/devtools/docker/Dockerfile new file mode 100644 index 0000000..ae85009 --- /dev/null +++ b/devtools/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM molssi/mamba141 + +COPY ./environment.yml /root/environment.yml + +RUN mamba env update -f /root/environment.yml + +WORKDIR /home +ENTRYPOINT ["packmol"] diff --git a/devtools/docker/environment.yml b/devtools/docker/environment.yml new file mode 100644 index 0000000..68863b8 --- /dev/null +++ b/devtools/docker/environment.yml @@ -0,0 +1,8 @@ +name: base +channels: + - conda-forge +dependencies: + - python + # Executables, etc. + - packmol==20.2.2 + diff --git a/packmol_step/data/configuration.txt b/packmol_step/data/configuration.txt index c6e368e..2ae130d 100644 --- a/packmol_step/data/configuration.txt +++ b/packmol_step/data/configuration.txt @@ -1,19 +1,6 @@ [packmol-step] -# Information about where/how the executables are installed -# installation may be 'user', 'conda' or 'module'. If a module is -# specified it will be loaded and those executables used. In this -# case, any path specified using -path will be ignored. -installation = -conda-environment = -modules = +# The level of logging for the Packmol Step -# The path to the executable. Can be empty or not present, in which -# case the default PATH is used. If a path is given, packmol -# from this location will be used. -# -# Ignored if a module is used. The default is to use the PATH -# environment variable. - -packmol-path = +# log-level WARNING diff --git a/packmol_step/data/packmol.ini b/packmol_step/data/packmol.ini new file mode 100644 index 0000000..b73efca --- /dev/null +++ b/packmol_step/data/packmol.ini @@ -0,0 +1,71 @@ +# Configuration options for how to run Packmol + +[docker] +# The code to use. This may maybe more than just the name of the code, and variables in +# braces {} will be expanded. For example: +# code = mpiexec -np {NTASKS} lmp_mpi +# would expand {NTASKS} to the number of tasks and run the command + +code = packmol + +# The name and location of the Docker container to use, optionally with the version + +container = ghcr.io/molssi-seamm/seamm-packmol:{version} + +# In addition, you can specify the platform to use. This is useful on e.g. Macs with +# app silicon (M1, M3...) where the default platform is linux/arm64 but some containers +# are only available for linux/amd64. + +# platform = linux/amd64 + +[local] +# The type of local installation to use. Options are: +# conda: Use a conda environment +# modules: Use the modules system +# local: Use a local installation +# docker: Use a Docker container +# By default SEAMM installs Packmol using conda. + +installation = conda + +# The command line to use, which should start with the executable followed by any options. +# Variables in braces {} will be expanded. For example: +# +# code = mpiexec -np {NTASKS} lmp_mpi +# +# would expand {NTASKS} to the number of tasks and run the command. +# For a 'local' installation, the command line should include the full path to the +# executable or it should be in the path. + +code = packmol + +######################### conda section ############################ +# The full path to the conda executable: + +# conda = + +# The Conda environment to use. This is either the name or full path. + +# conda-environment = seamm-packmol + +######################### modules section ############################ +# The modules to load to run Packmol, as a list of strings. +# For example, to load the modules packmol and openmpi, you would use: +# modules = packmol openmpi + +# modules = + +######################### local section ############################ +# The full path to the Packmol executable should be in the 'code' option. + +######################### docker section ############################ +# The name and location of the Docker container to use, optionally with the version. +# {version} will be expanded to the version of the plug-in. + +# container = ghcr.io/molssi-seamm/seamm-packmol:{version} + +# In addition, you can specify the platform to use. This is useful on e.g. Macs with +# app silicon (M1, M3...) where the default platform is linux/arm64 but some containers +# are only available for linux/amd64. + +# platform = linux/amd64 diff --git a/packmol_step/installer.py b/packmol_step/installer.py index 595daef..1c8e103 100644 --- a/packmol_step/installer.py +++ b/packmol_step/installer.py @@ -54,40 +54,183 @@ def __init__(self, logger=logger): logger.debug("Initializing the PACKMOL installer object.") self.section = "packmol-step" - self.path_name = "packmol-path" self.executables = ["packmol"] self.resource_path = Path(pkg_resources.resource_filename(__name__, "data/")) + + # The environment.yaml file for Conda installations. + logger.debug(f"data directory: {self.resource_path}") + self.environment_file = self.resource_path / "seamm-packmol.yml" + + def check(self): + """Check the status of the Packmol installation.""" + print("Checking the Packmol installation.") + # What Conda environment is the default? - data = self.configuration.get_values(self.section) + path = self.configuration.path.parent / "packmol.ini" + if not path.exists(): + text = (self.resource_path / "packmol.ini").read_text() + path.write_text(text) + print(f" The packmol.ini file did not exist. Created {path}") + + self.exe_config.path = path + + # Get the current values + data = self.exe_config.get_values("local") + if "conda-environment" in data and data["conda-environment"] != "": self.environment = data["conda-environment"] else: self.environment = "seamm-packmol" - # The environment.yaml file for Conda installations. - path = Path(pkg_resources.resource_filename(__name__, "data/")) - logger.debug(f"data directory: {path}") - self.environment_file = path / "seamm-packmol.yml" + super().check() + + def install(self): + """Install Packmol in a conda environment.""" + print("Installing Packmol.") + + # What Conda environment is the default? + path = self.configuration.path.parent / "packmol.ini" + if not path.exists(): + text = (self.resource_path / "packmol.ini").read_text() + path.write_text(text) + print(f" The packmol.ini file did not exist. Created {path}") + + self.exe_config.path = path + + # Get the current values + data = self.exe_config.get_values("local") + + if "conda-environment" in data and data["conda-environment"] != "": + self.environment = data["conda-environment"] + else: + self.environment = "seamm-packmol" + + super().install() + + def show(self): + """Show the status of the Packmol installation.""" + print("Showing the Packmol installation.") + + # What Conda environment is the default? + path = self.configuration.path.parent / "packmol.ini" + if not path.exists(): + text = (self.resource_path / "packmol.ini").read_text() + path.write_text(text) + print(f" The packmol.ini file does not exist at {path}") + print(" The 'check' command will create it if Packmol is installed.") + print(" Otherwise 'install' will install Packmol.") + return + + self.exe_config.path = path + + if not self.exe_config.section_exists("local"): + print( + " Packmol is not configured: there is no 'local' section in " + f" {path}." + ) + return + + # Get the current values + data = self.exe_config.get_values("local") + + if "conda-environment" in data and data["conda-environment"] != "": + self.environment = data["conda-environment"] + else: + self.environment = "seamm-packmol" - def exe_version(self, path): + super().show() + + def uninstall(self): + """Uninstall the Packmol installation.""" + print("Uninstall the Packmol installation.") + + # What Conda environment is the default? + path = self.configuration.path.parent / "packmol.ini" + if not path.exists(): + text = (self.resource_path / "packmol.ini").read_text() + path.write_text(text) + print( + f"""" The packmol.ini file does not exist at {path} + Perhaps Packmol is not installed, but if it is the 'check' command may locate it + and create the ini file, after which 'uninstall' will remove it.""" + ) + return + + self.exe_config.path = path + + if not self.exe_config.section_exists("local"): + print( + f"""" The packmol.ini file at {path} does not have local section. + Perhaps Packmol is not installed, but if it is the 'check' command may locate it + and update the ini file, after which 'uninstall' will remove it.""" + ) + return + + # Get the current values + data = self.exe_config.get_values("local") + + if "conda-environment" in data and data["conda-environment"] != "": + self.environment = data["conda-environment"] + else: + self.environment = "seamm-packmol" + + super().uninstall() + + def update(self): + """Updates the Packmol installation.""" + print("Updating the Packmol installation.") + + # What Conda environment is the default? + path = self.configuration.path.parent / "packmol.ini" + if not path.exists(): + text = (self.resource_path / "packmol.ini").read_text() + path.write_text(text) + print(f" The packmol.ini file did not exist. Created {path}") + + self.exe_config.path = path + + # Get the current values + data = self.exe_config.get_values("local") + + if "conda-environment" in data and data["conda-environment"] != "": + self.environment = data["conda-environment"] + else: + self.environment = "seamm-packmol" + + super().update() + + def exe_version(self, config): """Get the version of the PACKMOL executable. Parameters ---------- - path : pathlib.Path - Path to the executable. + config : dict + Dictionary of options for running Packmol Returns ------- - str + "Packmol", str The version reported by the executable, or 'unknown'. """ + environment = config["conda-environment"] + conda = config["conda"] + if environment[0] == "~": + environment = str(Path(environment).expanduser()) + command = f"'{conda}' run --live-stream -p '{environment}'" + elif Path(environment).is_absolute(): + command = f"'{conda}' run --live-stream -p '{environment}'" + else: + command = f"'{conda}' run --live-stream -n '{environment}'" + command += " packmol -log none" + + logger.debug(f" Running {command}") try: result = subprocess.run( - [str(path), "-log", "none"], + command, stdin=subprocess.DEVNULL, capture_output=True, text=True, + shell=True, ) except Exception: version = "unknown" @@ -103,4 +246,4 @@ def exe_version(self, path): version = value break - return version + return "Packmol", version diff --git a/packmol_step/packmol.py b/packmol_step/packmol.py index 72b2b5f..4efba35 100644 --- a/packmol_step/packmol.py +++ b/packmol_step/packmol.py @@ -3,10 +3,12 @@ """A step for building fluids with Packmol in a SEAMM flowchart""" import configparser +import importlib import logging import math from pathlib import Path import pprint +import shutil import textwrap from tabulate import tabulate @@ -212,18 +214,48 @@ def run(self): executor = self.flowchart.executor - # Read configuration file for MOPAC - ini_dir = Path(seamm_options["root"]).expanduser() - full_config = configparser.ConfigParser() - full_config.read(ini_dir / "packmol.ini") + # Read configuration file for Packmol if it exists executor_type = executor.name + full_config = configparser.ConfigParser() + ini_dir = Path(seamm_options["root"]).expanduser() + path = ini_dir / "packmol.ini" + + if path.exists(): + full_config.read(ini_dir / "packmol.ini") + + # If the section we need doesn't exists, get the default + if not path.exists() or executor_type not in full_config: + resources = importlib.resources.files("packmol_step") / "data" + ini_text = (resources / "packmol.ini").read_text() + full_config.read_string(ini_text) + + # Getting desperate! Look for an executable in the path if executor_type not in full_config: - raise RuntimeError( - f"No section for '{executor_type}' in PACKMOL ini file " - f"({ini_dir / 'packmol.ini'})" - ) + path = shutil.which("packmol") + if path is None: + raise RuntimeError( + f"No section for '{executor_type}' in Packmol ini file " + f"({ini_dir / 'packmol.ini'}), nor in the defaults, nor " + "in the path!" + ) + else: + full_config[executor_type] = { + "installation": "local", + "code": str(path), + } + + # If the ini file does not exist, write it out! + if not path.exists(): + with path.open("w") as fd: + full_config.write(fd) + printer.normal(f"Wrote the Packmol configuration file to {path}") + printer.normal("") + config = dict(full_config.items(executor_type)) + # Use the matching version of the seamm-packmol image by default. + config["version"] = self.version + result = executor.run( cmd=["{code}", "<", "input.inp", ">", "packmol.out"], config=config, @@ -235,7 +267,7 @@ def run(self): ) if not result: - self.logger.error("There was an error running PACKMOL") + self.logger.error("There was an error running Packmol") return None self.logger.debug(pprint.pformat(result)) @@ -267,7 +299,7 @@ def run(self): configuration.clear() configuration.charge = total_q - # Create the configuration from the PDB output of PACKMOL + # Create the configuration from the PDB output of Packmol configuration.coordinate_system = "Cartesian" configuration.from_pdb_text(result["packmol.pdb"]["data"]) @@ -303,14 +335,14 @@ def run(self): alias="packmol", module="packmol_step", level=1, - note="The principle PACKMOL citation.", + note="The principle Packmol citation.", ) return next_node @staticmethod def get_input(P, system_db, tmp_db, context, ff=None): - """Create the input for PACKMOL.""" + """Create the input for Packmol.""" # Return the translation from points a to b def recenter(a, b): diff --git a/versioneer.py b/versioneer.py index 64fea1c..3aa5da3 100644 --- a/versioneer.py +++ b/versioneer.py @@ -339,9 +339,9 @@ def get_config_from_root(root): # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() + parser = configparser.ConfigParser() with open(setup_cfg, "r") as f: - parser.readfp(f) + parser.read_file(f) VCS = parser.get("versioneer", "VCS") # mandatory def get(parser, name):