Running Many PLAMS Jobs in Parallel
***********************************

Technical Parallelization Workflow automation

Configure a parallel job runner, read a set of molecules from disk,
and dispatch many AMS jobs from one Python workflow.

**Requires:** AMS2026 or later

-[ Related examples ]-

* Summarizing PLAMS Jobs with JobAnalysis

* Logging and Job Log Files in PLAMS

-[ Related tutorials ]-

* Generating a batch of jobs

* Python Getting Started tutorial

-[ Related documentation ]-

* PLAMS runners

* PLAMS JobManager


Initial Imports
===============

   import psutil
   from scm.plams import JobRunner, config, Settings, from_smiles, AMSJob


Configure Job Runner
====================

Set the default job runner to run in parallel. Run as many jobs
simultaneously as there are cpu on the system. In addition, set the
number of cores for each job to 1.

   maxjobs = psutil.cpu_count(logical=False)
   print("Running up to {} jobs in parallel simultaneously".format(maxjobs))

   Running up to 12 jobs in parallel simultaneously

   config.default_jobrunner = JobRunner(parallel=True, maxjobs=maxjobs)

   config.job.runscript.nproc = 1


Load Molecules
==============

Generate a set of molecules from SMILES strings.

   molecule_smiles = {
       "Acetic_acid": "CC(=O)O",
       "Benzene": "c1ccccc1",
       "Butane": "CCCC",
       "Ethane": "CC",
       "Ethanol": "CCO",
       "Formic_acid": "C(=O)O",
       "Methanol": "CO",
       "Water": "O",
   }

   molecules = {
       name: from_smiles(smiles, forcefield="uff") for name, smiles in molecule_smiles.items()
   }


Set Up and Run Jobs
===================

Configure the calculation settings in the "Settings" object. Run a
geometry optimization job for each molecule in parallel.

   settings = Settings()
   settings.input.ams.Task = "GeometryOptimization"
   settings.input.dftb.Model = "GFN1-xTB"

   results = []
   for name, molecule in sorted(molecules.items()):
       job = AMSJob(molecule=molecule, settings=settings, name=name)
       results.append(job.run())

   [10.04|11:35:08] JOB Acetic_acid STARTED
   [10.04|11:35:08] JOB Benzene STARTED
   [10.04|11:35:08] JOB Butane STARTED
   [10.04|11:35:08] JOB Ethane STARTED
   [10.04|11:35:08] JOB Ethanol STARTED
   [10.04|11:35:08] JOB Formic_acid STARTED
   [10.04|11:35:08] JOB Methanol STARTED
   [10.04|11:35:08] JOB Water STARTED
   [10.04|11:35:08] JOB Acetic_acid RUNNING
   [10.04|11:35:08] JOB Benzene RUNNING


Results
=======

Print a table of results only for the successful calculations.

   # Only print the results of the succesful caluclations:
   for result in [r for r in results if r.ok()]:
       print(
           "Energy for {:<12}: {:>10.3f} kcal/mol".format(
               result.name, result.get_energy(unit="kcal/mol")
           )
       )

   [10.04|11:35:08] Waiting for job Acetic_acid to finish
   [10.04|11:35:08] JOB Butane RUNNING
   [10.04|11:35:08] JOB Ethanol RUNNING
   [10.04|11:35:08] JOB Ethane RUNNING
   [10.04|11:35:08] JOB Formic_acid RUNNING
   [10.04|11:35:08] JOB Water RUNNING
   [10.04|11:35:08] JOB Methanol RUNNING
   [10.04|11:35:08] JOB Benzene FINISHED
   [10.04|11:35:08] JOB Water FINISHED
   [10.04|11:35:08] JOB Ethane FINISHED
   ... output trimmed ....
   [10.04|11:35:08] Waiting for job Butane to finish
   [10.04|11:35:08] JOB Butane SUCCESSFUL
   Energy for Acetic_acid :  -9906.399 kcal/mol
   Energy for Benzene     :  -9973.315 kcal/mol
   Energy for Butane      :  -8699.182 kcal/mol
   Energy for Ethane      :  -4686.354 kcal/mol
   Energy for Ethanol     :  -7630.501 kcal/mol
   Energy for Formic_acid :  -7898.338 kcal/mol
   Energy for Methanol    :  -5621.724 kcal/mol
   Energy for Water       :  -3618.401 kcal/mol


See also
========


Related examples
----------------

* Summarizing PLAMS Jobs with JobAnalysis

* Logging and Job Log Files in PLAMS


Related tutorials
-----------------

* Generating a batch of jobs

* Python Getting Started tutorial


Related documentation
---------------------

* PLAMS runners

* PLAMS JobManager


Python Script
=============

   #!/usr/bin/env python
   # coding: utf-8

   # ## Initial Imports

   import psutil
   from scm.plams import JobRunner, config, Settings, from_smiles, AMSJob


   # ## Configure Job Runner
   #
   # Set the default job runner to run in parallel. Run as many jobs simultaneously as there are cpu on the system. In addition, set the number of cores for each job to 1.

   maxjobs = psutil.cpu_count(logical=False)
   print("Running up to {} jobs in parallel simultaneously".format(maxjobs))


   config.default_jobrunner = JobRunner(parallel=True, maxjobs=maxjobs)


   config.job.runscript.nproc = 1


   # ## Load Molecules
   #
   # Generate a set of molecules from SMILES strings.

   molecule_smiles = {
       "Acetic_acid": "CC(=O)O",
       "Benzene": "c1ccccc1",
       "Butane": "CCCC",
       "Ethane": "CC",
       "Ethanol": "CCO",
       "Formic_acid": "C(=O)O",
       "Methanol": "CO",
       "Water": "O",
   }

   molecules = {name: from_smiles(smiles, forcefield="uff") for name, smiles in molecule_smiles.items()}


   # ## Set Up and Run Jobs
   #
   # Configure the calculation settings in the `Settings` object. Run a geometry optimization job for each molecule in parallel.

   settings = Settings()
   settings.input.ams.Task = "GeometryOptimization"
   settings.input.dftb.Model = "GFN1-xTB"


   results = []
   for name, molecule in sorted(molecules.items()):
       job = AMSJob(molecule=molecule, settings=settings, name=name)
       results.append(job.run())


   # ## Results
   #
   # Print a table of results only for the successful calculations.

   # Only print the results of the succesful caluclations:
   for result in [r for r in results if r.ok()]:
       print("Energy for {:<12}: {:>10.3f} kcal/mol".format(result.name, result.get_energy(unit="kcal/mol")))
