ADF and COSMO-RS workflow¶
(contributed by Bas van Beek)
-
run_crs_ams
(settings_ams, settings_crs, solvents, solutes=None, return_amsresults=False, **kwargs)[source]¶ A workflow for running COSMO-RS calculations with AMS (i.e. DFT) COSMO surface charges.
The workflow consists of four distinct steps:
- Perform gas-phase
AMSJob
calculations on the solvents and solutes (see settings_ams). - Perform COSMO
AMSJob
calculations using the .rkf file from step 1 - Extract the relevant COSMO information from the results and create .coskf files.
- Perform a COSMO-RS calculations with the .coskf files produced in steps 2 and 3 (see settings_crs). This calculation is conducted for all possible solvent/solute pairs, assuming solutes have been specified by the user.
The ams solvation block (ams_settings.input.solvation) is soft updated with suitable settings for constructing COSMO-RS compatible surface charges (see
add_solvation_block()
). No user-specified values are overwritten during this process.Examples
An example value for settings_ams:
>>> from scm.plams import Settings >>> settings_ams = Settings() >>> settings_ams.input.basis.type = 'TZ2P' >>> settings_ams.input.xc.gga = 'BP86' >>> settings_ams.input.scf.converge = '1.0e-06'
An example value for settings_crs (activity coefficient calculation):
>>> settings_crs = Settings() >>> settings_crs.input.temperature = 298.15 >>> settings_crs.input.property._h = 'activitycoef'
And finally the actual calculation with methanol, ethanol and propanol as solvents and acetic acid as solute:
>>> solvents = [Molecule('methanol.xyz'), Molecule('ethanol.xyz'), Molecule('propanol.xyz')] >>> solutes = Molecule('acetic_acid.xyz') >>> crs_dict = run_crs_ams(settings_ams, settings_crs, solvents, solutes) >>> for key in crs_dict: >>> print("\nSystem:", key) >>> res = crs_dict[key].get_results(>>>settings_crs.input.property._h.upper()) >>> print("activity coefficients:") >>> print(res["gamma"]) System: CRSJob.methanol.acetic_acid activity coefficients: [[1. ] [0.48067852]] System: CRSJob.ethanol.acetic_acid activity coefficients: [[1. ] [0.43502696]] System: CRSJob.propanol.acetic_acid activity coefficients: [[1. ] [0.58399355]]
Parameters: - settings_ams (
Settings
) – A Settings instance with settings forAMSJob
(see Examples). - settings_crs (
Settings
) – A Settings instance with settings forCRSJob
(see Examples). - solvents (
Molecule
orlist
[Molecule
]) – A Molecule or list of one or more Molecules representing solvents. - solutes (
Molecule
orlist
[Molecule
], optional) – An optional Molecule or list of one or more Molecules representing solutes. - return_amsresults (
bool
) – IfTrue
, return both the solvent and solute AMS results in addition to the final COSMO-RS. - optional (**kwargs,) – Optional keyword arguments that will be passed to all calls of
Job.run()
. For example, one could consider passing a custom Job runners or Job manager.
Returns: A dictionary with the resulting COSMO-RS output. The name of each
CRSResults
instance is used as key. Ifreturn_amsresults=True
, return both the COSMO-RS and AMS solvent and solute results.Return type: - Perform gas-phase
The source code:
import os
from scm.plams import Settings, JobError, AMSJob, CRSJob, Molecule, AMSResults, CRSResults, KFFile
__all__ = ['run_crs_ams']
def run_crs_ams(settings_ams, settings_crs,
solvents, solutes=None,
return_amsresults=False, **kwargs):
"""A workflow for running COSMO-RS calculations with AMS (*i.e.* DFT) COSMO surface charges.
The workflow consists of four distinct steps:
1. Perform gas-phase |AMSJob| calculations on the solvents and solutes (see *settings_ams*).
2. Perform COSMO |AMSJob| calculations using the .rkf file from step 1
3. Extract the relevant COSMO information from the results and create .coskf files.
4. Perform a COSMO-RS calculations with the .coskf files produced in steps 2 and 3
(see *settings_crs*).
This calculation is conducted for all possible solvent/solute pairs,
assuming solutes have been specified by the user.
The ams solvation block (*ams_settings.input.solvation*) is soft updated with suitable
settings for constructing COSMO-RS compatible surface charges
(see :func:`.add_solvation_block`).
No user-specified values are overwritten during this process.
.. admonition:: Examples
An example value for *settings_ams*:
.. code:: python
>>> from scm.plams import Settings
>>> settings_ams = Settings()
>>> settings_ams.input.basis.type = 'TZ2P'
>>> settings_ams.input.xc.gga = 'BP86'
>>> settings_ams.input.scf.converge = '1.0e-06'
An example value for *settings_crs* (`activity coefficient`_ calculation):
.. code:: python
>>> settings_crs = Settings()
>>> settings_crs.input.temperature = 298.15
>>> settings_crs.input.property._h = 'activitycoef'
And finally the actual calculation with methanol, ethanol and propanol as solvents and
acetic acid as solute:
.. code:: python
>>> solvents = [Molecule('methanol.xyz'), Molecule('ethanol.xyz'), Molecule('propanol.xyz')]
>>> solutes = Molecule('acetic_acid.xyz')
>>> crs_dict = run_crs_ams(settings_ams, settings_crs, solvents, solutes)
>>> for key in crs_dict:
>>> print("\\nSystem:", key)
>>> res = crs_dict[key].get_results(>>>settings_crs.input.property._h.upper())
>>> print("activity coefficients:")
>>> print(res["gamma"])
System: CRSJob.methanol.acetic_acid
activity coefficients:
[[1. ]
[0.48067852]]
System: CRSJob.ethanol.acetic_acid
activity coefficients:
[[1. ]
[0.43502696]]
System: CRSJob.propanol.acetic_acid
activity coefficients:
[[1. ]
[0.58399355]]
:type settings_ams: :class:`.Settings`
:parameter settings_ams:
A Settings instance with settings for :class:`.AMSJob` (see Examples).
:type settings_crs: :class:`.Settings`
:parameter settings_crs:
A Settings instance with settings for :class:`.CRSJob` (see Examples).
:type solvents: |Molecule| or :class:`list` [|Molecule|]
:parameter solvents: A Molecule or list of one or more Molecules representing solvents.
:type solutes: |Molecule| or :class:`list` [|Molecule|], optional
:parameter solutes: An optional Molecule or list of one or more Molecules representing solutes.
:type return_amsresults: :class:`bool`
:parameter return_amsresults: If ``True``, return both the solvent and solute AMS results in addition to the final COSMO-RS.
:parameter \**kwargs, optional:
Optional keyword arguments that will be passed to all calls of :meth:`.Job.run`.
For example, one could consider passing a custom :ref:`job_runners` or :ref:`job_manager`.
:returns: A dictionary with the resulting COSMO-RS output.
The `name` of each :class:`.CRSResults` instance is used as key.
If ``return_amsresults=True``, return both the COSMO-RS and AMS solvent and solute results.
:rtype: :class:`dict` or :class:`tuple` [:class:`dict`, :class:`list`, :class:`list`]
.. _`activity coefficient`: ../../COSMO-RS/Properties.html#activity-coefficients-solvent-and-solute
"""
solvents = [solvents] if isinstance(solvents, Molecule) else solvents
solutes = [solutes] if isinstance(solutes, Molecule) else solutes
# soft update to defaults
_settings_ams = update_adf_defaults(settings_ams)
_settings_ams = add_solvation_block(_settings_ams)
# Validate arguments
_validate_settings_ams(_settings_ams)
# Decapitalize the "solvation" and "allpoints" keys
if 'solvation' in _settings_ams.settings.input.adf:
_settings_ams.input.adf.solvation = _settings_ams.settings.input.adf.pop('solvation')
# Create the COSMO surfaces for the solute
solvent_list = [run_amsjob(mol, _settings_ams, **kwargs) for mol in solvents]
solute_list = [run_amsjob(mol, _settings_ams, **kwargs) for mol in solutes]
if not solutes:
solute_list = [None]
# Start the and return the COSMO-RS job
crs = {}
for solvent in solvent_list:
for solute in solute_list:
results = run_crsjob(solvent, settings_crs, solute=solute, **kwargs)
crs[results.job.name] = results
if not return_amsresults:
return crs
else:
return crs, solvent_list, solute_list
def run_amsjob(mol: Molecule, s: Settings, **kwargs) -> AMSResults:
"""Run an :class:`.AMSJob` on *mol* using the settings provided in *s*."""
name = 'AMSJob.' + mol.properties.name if 'name' in mol.properties else 'AMSJob.mol'
# Do a geometry optimization in the gas phase
_s = s.copy()
_s.input.ams.soft_update({"Task":"GeometryOptimization"})
job1 = AMSJob(molecule=mol, settings=_s, name=name+'.gas')
del job1.settings.input.adf.solvation
results1 = job1.run(**kwargs)
ams_out = results1['ams.rkf']
adf_out = results1['adf.rkf']
# Construct the AMS COSMO surface
_s.input.ams.Task = "SinglePoint"
_s.input.ams.EngineRestart = adf_out
_s.input.ams.LoadSystem.File = ams_out
job2 = AMSJob(molecule=None, settings=_s, depend=[job1], name=name)
results2 = job2.run(**kwargs)
coskf_name = job2.name.split('.')[1]
convert_to_coskf(results2, coskf_name)
return results2
def convert_to_coskf(res: AMSResults, name: str):
f = KFFile(res['adf.rkf'])
cosmo = f.read_section("COSMO")
# print (os.path.dirname(res.rkfpath(file='adf')), name)
coskf = KFFile(os.path.join(os.path.dirname(res.rkfpath(file='adf')),name+".coskf"))
for k,v in cosmo.items():
coskf.write("COSMO",k,v)
res.collect()
def run_crsjob(solvent: AMSResults, s: Settings, solute: AMSResults = None, **kwargs) -> CRSResults:
"""Run an :class:`.CRSJob` on with *solvent* and, optionally, *solute* using the settings provided in *s*."""
name = 'CRSJob.' + solvent.job.name.split('.')[1]
solv_coskf = solvent.job.name.split('.')[1]+".coskf"
if solute is not None:
name += '.' + solute.job.name.split('.')[1]
solute_coskf = solute.job.name.split('.')[1]+".coskf"
job = CRSJob(settings=s, depend=[solvent.job, solute.job], name=name)
set_header(job.settings, solvent[solv_coskf], solute[solute_coskf])
else:
job = CRSJob(settings=s, depend=[solvent.job], name=name)
set_header(job.settings, solvent[solv_coskf])
return job.run(**kwargs)
def set_header(s: Settings, *values: str) -> None:
"""Assign *value* to the ``["_h"]`` key in *s.input.compound*."""
s.input.compound = []
for item in values:
s.input.compound.append(Settings({'_h': item}))
s.input.compound[0].frac1 = 1.0 # The first item in *values should be the solvent
def add_solvation_block(ams_settings: Settings) -> None:
"""Add the solvation block to *ams_settings*, returning a copy of the new settings.
The solvation block (*ams_settings.input.solvation*) is soft updated
with the following Settings:
.. code::
c-mat: Exact
charged: method=Conj
radii:
Br: 2.16
C: 2.0
Cl: 2.05
F: 1.72
H: 1.3
I: 2.32
N: 1.83
O: 1.72
P: 2.13
S: 2.16
Si: 2.48
scf: Var All
solv: name=CRS cav0=0.0 cav1=0.0
surf: Delley
"""
# Find the solvation key
solvation = ams_settings.input.adf.find_case('solvation')
solvation_block = ams_settings.input.adf[solvation]
# Find all keys for within the solvation block
keys = ('surf', 'solv', 'charged', 'c-mat', 'scf', 'radii')
surf, solv, charged, cmat, scf, radii = [solvation_block.find_case(item) for item in keys]
# Construct the default solvation block
solvation_block_new = {
surf: 'Delley',
solv: 'name=CRS cav0=0.0 cav1=0.0',
charged: 'method=Conj',
cmat: 'Exact',
scf: 'Var All',
radii: {
'H': 1.30,
'C': 2.00,
'N': 1.83,
'O': 1.72,
'F': 1.72,
'Si': 2.48,
'P': 2.13,
'S': 2.16,
'Cl': 2.05,
'Br': 2.16,
'I': 2.32
}
}
# Copy ams_settings and perform a soft update
ret = ams_settings.copy()
ret.input.adf[solvation].soft_update(solvation_block_new)
return ret
def update_adf_defaults(ams_settings: Settings) -> None:
"""Add the COSMO-RS compound defaults to *ams_settings*, returning a copy of the new settings.
The engine block (*ams_settings.input.adf*) is soft updated
with the following Settings:
.. code::
Engine ADF
Basis
Type TZP
Core Small
End
XC
GGA BP86
End
Relativity
Level Scalar
End
BeckeGrid
Quality Good
End
EndEngine
"""
# Find the solvation key
# solvation = ams_settings.input.adf.find_case('solvation')
# solvation_block = ams_settings.input.adf[solvation]
adf = ams_settings.input.find_case('adf')
adf_block = ams_settings.input[adf]
# Find all keys for within the adf block
keys = ('basis', 'xc', 'relativity', 'BeckeGrid')
basis, xc, relativity, BeckeGrid = [adf_block.find_case(item) for item in keys]
# Construct the default solvation block
adf_defaults_block = Settings({
basis: {
'Type':'TZP',
'Core':'Small'
},
xc: {'GGA':'BP86'},
relativity: {'Level':'Scalar'},
BeckeGrid: {'Quality':'Good'}
})
# Copy ams_settings and perform a soft update
ret = ams_settings.copy()
ret.input.adf.soft_update(adf_defaults_block)
return ret
def _validate_settings_ams(s: Settings) -> None:
"""Validate the *settings_ams* argument in :func:`run_crs_ams`."""
solvation = s.input.adf.find_case('solvation')
solv = s.input.adf[solvation].find_case('solv')
if solvation not in s.input.adf:
raise JobError("run_crs_ams: The 'solvation' key is absent"
" from settings_ams.input'")
if solv not in s.input.adf[solvation]:
raise JobError("run_crs_ams: The 'solv' key is absent"
" from settings_ams.input.solvation")
if 'name=crs' not in s.input.adf[solvation][solv].lower():
raise JobError("run_crs_ams: The 'name=CRS' value is absent"
" from settings_ams.input.solvation.solv")