Source code for pyscf.solvent.cosmors

# Copyright 2014-2018 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author: Ivan Chernyshov <ivan.chernyshov@gmail.com>
#

'''
Provides functionality to generate COSMO-files for COSMO-RS computations
'''

#%% Imports

import re as _re
from itertools import product as _product

import numpy as _np

from pyscf import __version__
from pyscf.data.nist import BOHR as _BOHR



#%% SAS volume

def _get_line_atom_intersection(x, y, atom, r):
    '''Returns z-coordinates of boundaries of intersection of
    the (x,y,0) + t(0,0,1) line and vdW sphere

    Arguments:
        x (float): x parameter of the line
        y (float): y parameter of the line
        atom (np.ndarray): xyz-coordinates of the atom
        r (float): van der Waals radii of the atom

    Returns:
        _tp.Optional[_tp.List[float, float]]: z-coordinates
            of the line/sphere intersection, and None
            if they do not intersect

    '''
    # check distance between line and atom
    d = _np.linalg.norm(_np.array([x,y]) - atom[:2])
    if d >= r:
        return None
    # find interval
    dz = (r**2 - d**2)**0.5
    interval = [atom[2] - dz, atom[2] + dz]

    return interval


def _unite_intervals(intervals):
    '''Unites float intervals

    Arguments:
        intervals (_tp.List[_tp.List[float, float]]): list of intervals

    Returns:
        _tp.List[_tp.List[float, float]]: list of united intervals

    '''
    united = []
    for begin, end in sorted(intervals):
        if united and united[-1][1] >= begin:
            united[-1][1] = end
        else:
            united.append([begin, end])

    return united


def _get_line_sas_intersection_length(x, y, coords, radii):
    '''Finds total length of all (x,y,0) + t(0,0,1) line's intervals
    enclosed by intersections with solvent-accessible surface

    Arguments:
        x (float): x parameter of the line
        y (float): y parameter of the line
        atoms (_np.ndarray): (n*3) array of atomic coordinates
        radii (_np.ndarray): array of vdW radii

    Returns:
        float: total length of all line's intervals enclosed by SAS

    '''
    intervals = [_get_line_atom_intersection(x, y, atom, r) for atom, r in zip(coords, radii)]
    intervals = _unite_intervals([_ for _ in intervals if _])
    length = sum([end - begin for begin, end in intervals])

    return length


[docs] def get_sas_volume(surface, step=0.2): '''Computes volume [a.u.] of space enclosed by solvent-accessible surface Arguments: surface (dict): dictionary containing SAS parameters (mc.with_solvent.surface) step (float): grid spacing parameter, [Bohr] Returns: float: SAS-defined volume of the molecule [a.u.] ''' # get parameters coords = surface['atom_coords'] radii = _np.array([surface['R_vdw'][i] for i, j in surface['gslice_by_atom']]) # compute minimaxes of SAS coordinates bounds = _np.array([(coords - radii[:, _np.newaxis]).min(axis = 0), (coords + radii[:, _np.newaxis]).max(axis = 0)]) # swap axes to minimize integration mesh axes = [(val, idx) for idx, val in enumerate(bounds[1] - bounds[0])] axes = [idx for val, idx in sorted(axes)] coords = coords[:,axes] bounds = bounds[:,axes] # mesh parameters xs = _np.linspace(bounds[0,0], bounds[1,0], int(_np.ceil((bounds[1,0] - bounds[0,0])/step)) + 1) ys = _np.linspace(bounds[0,1], bounds[1,1], int(_np.ceil((bounds[1,1] - bounds[0,1])/step)) + 1) dxdy = (xs[1] - xs[0])*(ys[1] - ys[0]) # integration V = sum([dxdy * _get_line_sas_intersection_length(x, y, coords, radii) for x, y in _product(xs, ys)]) return V
#%% COSMO-files def _lot(mf): '''Returns level of theory in method-disp/basis/solvation format (raw solution) Arguments: mf: processed SCF Returns: str: method-dispersion/basis/solvation ''' lot = { 'method': None, 'basis': None, 'dispersion': None, 'solvation_model': None } # method method = mf.xc if hasattr(mf, 'xc') else None if method is None: match = _re.search('HF|MP2|CCSD', str(type(mf))) method = match.group(0) if match else None lot['method'] = method # basis lot['basis'] = mf.mol.basis # dispersion match = _re.search('D3|D4', str(type(mf))) disp = match.group(0) if match else None lot['dispersion'] = disp # solvation solv = mf.with_solvent.method if hasattr(mf, 'with_solvent') else '' lot['solvation_model'] = solv return lot
[docs] def get_pcm_parameters(mf, step=0.2): '''Returns a dictionary containing the main PCM computation parameters. All physical parameters are expressed in atomic units (a.u.). Please also note, that outlying charge correction is not implemented yet. Arguments: mf: processed SCF with PCM solvation step (float): grid spacing parameter to compute volume, [Bohr] Returns: dict: main PCM parameters, including dielectric energy and parameters of surface segments ''' # get params lot = _lot(mf) f_epsilon = mf.with_solvent._intermediates['f_epsilon'] n_segments = len(mf.with_solvent._intermediates['q']) area = float(sum(mf.with_solvent.surface['area'])) # np.float is not json-seriazable volume = float(get_sas_volume(mf.with_solvent.surface, step)) E_tot = float(sum(mf.scf_summary.values())) E_diel = float(mf.scf_summary['e_solvent']) # atoms atom_idxs = list(range(1, 1 + mf.mol.natm)) a_xs, a_ys, a_zs = mf.mol.atom_coords().T.tolist() elems = [elem.lower() for elem in mf.mol.elements] radii = [mf.with_solvent.surface['R_vdw'][i] for i, j in mf.with_solvent.surface['gslice_by_atom']] # segments s_xs, s_ys, s_zs = zip(*mf.with_solvent.surface['grid_coords'].tolist()) segment_idxs = list(range(1, len(s_xs) + 1)) atom_segment_idxs = [] for idx, (start, end) in enumerate(mf.with_solvent.surface['gslice_by_atom']): atom_segment_idxs += [idx + 1] * (end - start) charges = mf.with_solvent._intermediates['q'] areas = mf.with_solvent.surface['area'] potentials = mf.with_solvent._intermediates['v_grids'] # output params = { 'pyscf_version': __version__, 'approximation': lot, 'pcm_data': { 'f_eps': f_epsilon, 'n_segments': n_segments, 'area': float(area), 'volume': float(volume), }, 'screening_charge': { 'cosmo': float(sum(charges)), 'correction': 0.0, 'total': float(sum(charges)) }, 'energies': { 'e_tot': float(E_tot), 'e_tot_corr': float(E_tot), # TODO: implement OCC 'e_diel': float(E_diel), 'e_diel_corr': float(E_diel) # TODO: implement OCC }, 'atoms': { 'atom_index': atom_idxs, 'x': a_xs, 'y': a_ys, 'z': a_zs, 'element': elems, 'radius': radii }, 'segments': { 'segment_index': segment_idxs, 'atom_index': atom_segment_idxs, 'x': s_xs, 'y': s_ys, 'z': s_zs, 'charge': charges.tolist(), 'charge_corr': charges.tolist(), # TODO: implement OCC 'area': areas.tolist(), 'sigma': (charges / areas).tolist(), 'sigma_corr': (charges / areas).tolist(), # TODO: implement OCC 'potential': potentials.tolist(), } } return params
[docs] def write_cosmo_file(fout, mf, step=0.2, volume=None): '''Saves COSMO file in Turbomole format. Please note, that outlying charge correction is not implemented yet Arguments: fout: writable file object mf: processed SCF with PCM solvation step (float): grid spacing parameter to compute volume, [Bohr] volume (float): uses given value (Bohr^3) as molecular volume instead of the computed one Raises: ValueError: if f_epsilon < 1 ''' # extract PCM parameters ps = get_pcm_parameters(mf, step) # check F_epsilon f_eps = ps['pcm_data']['f_eps'] if f_eps < 1.0: message = f'Low f_eps value: {f_eps}. ' message += 'COSMO-RS requires f_epsilon=1.0 or eps=float("inf"). ' message += 'Rerun computation with the correct epsilon value.' raise ValueError(message) # qm params fout.write('$info\n') info = [ f'program: PySCF, version {ps["pyscf_version"]}', f'solvation model: {ps["approximation"]["solvation_model"]}', f'dispersion: {ps["approximation"]["dispersion"]}', f'{ps["approximation"]["method"]}', f'{ps["approximation"]["basis"]}' ] fout.write(';'.join(info) + '\n') # cosmo data n_segments = ps['pcm_data']['n_segments'] area = ps['pcm_data']['area'] if volume is None: volume = ps['pcm_data']['volume'] # print fout.write('$cosmo_data\n') fout.write(f' fepsi = {f_eps:>.8f}\n') fout.write(f' nps = {n_segments:>10d}\n') fout.write(f' area = {area:>10.2f} # [a.u.]\n') fout.write(f' volume = {volume:>10.2f} # [a.u.]\n') # atomic coordinates atoms = ps['atoms']['atom_index'] xs, ys, zs = (ps['atoms'][ax] for ax in 'xyz') elems = ps['atoms']['element'] radii = [r * _BOHR for r in ps['atoms']['radius']] # print fout.write('$coord_rad\n') fout.write('#atom x [a.u.] y [a.u.] z [a.u.] element radius [A]\n') for atom, x, y, z, elem, r in zip(atoms, xs, ys, zs, elems, radii): fout.write(f'{atom:>4d}{x:>19.14f}{y:>19.14f}{z:>19.14f} {elem:<2}{r:>12.5f}\n') # screening charges charge_cosmo = ps['screening_charge']['cosmo'] charge_correction = ps['screening_charge']['correction'] charge_total = ps['screening_charge']['total'] # print fout.write('$screening_charge\n') fout.write(f' cosmo = {charge_cosmo:>10.6f}\n') fout.write(f' correction = {charge_correction:>10.6f}\n') fout.write(f' total = {charge_total:>10.6f}\n') # energies E_tot = ps['energies']['e_tot'] E_tot_corr = ps['energies']['e_tot_corr'] E_diel = ps['energies']['e_diel'] E_diel_corr = ps['energies']['e_diel_corr'] fout.write('$cosmo_energy\n') fout.write(f' Total energy [a.u.] = {E_tot:>19.10f}\n') fout.write(f' Total energy + OC corr. [a.u.] = {E_tot_corr:>19.10f}\n') fout.write(f' Total energy corrected [a.u.] = {E_tot_corr:>19.10f}\n') fout.write(f' Dielectric energy [a.u.] = {E_diel:>19.10f}\n') fout.write(f' Diel. energy + OC corr. [a.u.] = {E_diel_corr:>19.10f}\n') # segments ns = ps['segments']['segment_index'] atoms = ps['segments']['atom_index'] xs, ys, zs = (ps['segments'][ax] for ax in 'xyz') qs = ps['segments']['charge_corr'] areas = [a * _BOHR**2 for a in ps['segments']['area']] pots = [p / _BOHR for p in ps['segments']['potential']] # print legend fout.write('$segment_information\n') fout.write('# n - segment number\n') fout.write('# atom - atom associated with segment n\n') fout.write('# position - segment coordinates, [a.u.]\n') fout.write('# charge - segment charge (corrected)\n') fout.write('# area - segment area [A**2]\n') fout.write('# potential - solute potential on segment (A length scale)\n') fout.write('#\n') fout.write('# n atom position (X, Y, Z) ') fout.write('charge area charge/area potential\n') fout.write('#\n') fout.write('#\n') # print params for n, x, y, z, atom, q, area, pot in zip(ns, xs, ys, zs, atoms, qs, areas, pots): line = f'{n:>5d}{atom:>5d}{x:>15.9f}{y:>15.9f}{z:>15.9f}' line += f'{q:>15.9f}{area:>15.9f}{q/area:>15.9f}{pot:>15.9f}\n' fout.write(line) return