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.

'''
Provides functionality to generate COSMO files and sigma-profiles
for the further use in COSMO-RS/COSMO-SAC computations and/or ML
'''

#%% 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**3] 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**3] ''' # 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 * _BOHR**3
#%% COSMO-files def _check_feps(mf, ignore_low_feps): '''Checks f_epsilon value and raises ValueError for f_eps < 1 Arguments: mf: processed SCF with PCM solvation ignore_low_feps (bool): if True, does not raise ValueError if feps < 1 Raises: ValueError: if f_epsilon < 1 and ignore_low_feps flag is set to False ''' if ignore_low_feps: return f_eps = mf.with_solvent._intermediates['f_epsilon'] 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 correct epsilon or use the ignore_low_feps argument.' raise ValueError(message) return 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 ''' # 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 '???' # dispersion match = _re.search('D3|D4', str(type(mf))) disp = '-' + match.group(0) if match else '' # other basis = mf.mol.basis solv = '/' + mf.with_solvent.method if hasattr(mf, 'with_solvent') else '' # approx lot = f'{method}{disp}/{basis}{solv}'.lower() return lot
[docs] def write_cosmo_file(fout, mf, step=0.2, ignore_low_feps=False): '''Saves COSMO file Arguments: fout: writable file object mf: processed SCF with PCM solvation step (float): grid spacing parameter to compute volume, [Bohr] ignore_low_feps (bool): if True, does not raise ValueError if feps < 1 Raises: ValueError: if f_epsilon < 1 and ignore_low_feps flag is set to False ''' _check_feps(mf, ignore_low_feps) # qm params fout.write('$info\n') fout.write(f'PySCF v. {__version__}, {_lot(mf)}\n') # cosmo data f_epsilon = mf.with_solvent._intermediates['f_epsilon'] n_segments = len(mf.with_solvent._intermediates['q']) area = sum(mf.with_solvent.surface['area']) volume = get_sas_volume(mf.with_solvent.surface, step) / _BOHR**3 # print fout.write('$cosmo_data\n') fout.write(f' fepsi = {f_epsilon:>.8f}\n') fout.write(f' nps = {n_segments:>10d}\n') fout.write(f' area = {area:>10.2f} # [Bohr**2]\n') fout.write(f' volume = {volume:>10.2f} # [Bohr**3]\n') # atomic coordinates atoms = range(1, 1 + len(mf.mol.elements)) xs, ys, zs = zip(*mf.mol.atom_coords().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']] # print fout.write('$coord_rad\n') fout.write('#atom x [Bohr] y [Bohr] z [Bohr] element radius [Bohr]\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') # cosmo parameters screening_charge = sum(mf.with_solvent._intermediates['q']) E_tot = sum(mf.scf_summary.values()) E_diel = mf.scf_summary['e_solvent'] # print fout.write('$screening_charge\n') fout.write(f' cosmo = {screening_charge:>15.6f}\n') fout.write('$cosmo_energy\n') fout.write(f' Total energy [a.u.] = {E_tot:>19.10f}\n') fout.write(f' Dielectric energy [a.u.] = {E_diel:>19.10f}\n') # segments xs, ys, zs = zip(*mf.with_solvent.surface['grid_coords'].tolist()) ns = range(1, len(xs) + 1) atoms = [] for idx, (start, end) in enumerate(mf.with_solvent.surface['gslice_by_atom']): atoms += [idx + 1] * (end - start) qs = mf.with_solvent._intermediates['q'] areas = mf.with_solvent.surface['area'] * _BOHR**2 sigmas = qs / areas pots = mf.with_solvent._intermediates['v_grids'] # print legend fout.write('$segment_information\n') fout.write('# n - segment number\n') fout.write('# atom - atom associated with segment n\n') fout.write('# x, y, z - segment coordinates, [Bohr]\n') fout.write('# charge - segment charge\n') fout.write('# area - segment area [A**2]\n') fout.write('# charge/area - segment charge density [e/A**2]\n') fout.write('# potential - solute potential on segment\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, sigma, pot in zip(ns, xs, ys, zs, atoms, qs, areas, sigmas, pots): line = f'{n:>5d}{atom:>5d}{x:>15.9f}{y:>15.9f}{z:>15.9f}' line += f'{q:>15.9f}{area:>15.9f}{sigma:>15.9f}{pot:>15.9f}\n' fout.write(line) return
#%% Sigma-profile
[docs] def get_sigma_profile(mf, sigmas_grid, ignore_low_feps=False): '''Computes sigma-profile in [-0.025..0.025] e/A**2 range as in https://github.com/usnistgov/COSMOSAC/blob/master/profiles/to_sigma.py#L181 https://doi.org/10.1021/acs.jctc.9b01016 Arguments: mf: processed SCF with PCM solvation sigmas_grid (_np.ndarray): grid of screening charge values, e.g. np.linspace(-0.025, 0.025, 51) ignore_low_feps (bool): if True, does not raise ValueError if feps < 1 Returns: _np.ndarray: array of 51 elements corresponding to the p(sigma) values for the screening charge values in [-0.025..0.025] e/A**2 range Raises: ValueError: if f_epsilon < 1 and ignore_low_feps flag is set to False ''' _check_feps(mf, ignore_low_feps) # prepare params qs = mf.with_solvent._intermediates['q'] areas = mf.with_solvent.surface['area'] * _BOHR**2 sigmas = qs / areas step = sigmas_grid[1] - sigmas_grid[0] max_sigma = sigmas_grid[-1] + step n_bins = len(sigmas_grid) # compute profile psigma = _np.zeros(n_bins) for sigma, area in zip(sigmas, areas): # get index of left sigma grid value left = int(_np.floor((sigma - sigmas_grid[0]) / step)) if left < -1 or left > n_bins - 1: continue # get impact of the segment to the left bin val_right = sigmas_grid[left+1] if left < n_bins - 1 else max_sigma w_left = (val_right - sigma) / step # add areas to p(sigma) if left > -1: psigma[left] += area * w_left if left < n_bins - 1: psigma[left+1] += area * (1 - w_left) return psigma
[docs] def get_cosmors_parameters(mf, sigmas_grid=_np.linspace(-0.025, 0.025, 51), step=0.2, ignore_low_feps=False): '''Computes main COSMO-RS parameters Arguments: mf: processed SCF with PCM solvation step (float): grid spacing parameter to compute volume, [Bohr] ignore_low_feps (bool): if True, does not raise ValueError if feps < 1 Returns: dict: main COSMO-RS parameters, including sigma-profile, volume, surface area, SCF and dielectric energies Raises: ValueError: if f_epsilon < 1 and ignore_low_feps flag is set to False ''' _check_feps(mf, ignore_low_feps) # get params lot = _lot(mf) area = sum(mf.with_solvent.surface['area']) * _BOHR**2 volume = get_sas_volume(mf.with_solvent.surface, step) psigma = get_sigma_profile(mf, sigmas_grid, ignore_low_feps=True) E_tot = sum(mf.scf_summary.values()) E_diel = mf.scf_summary['e_solvent'] # output params = {'PySCF version': __version__, 'Level of theory': lot, 'Surface area, A**2': float(area), # since np.float is not json-seriazable 'Volume, A**3': float(volume), 'Total energy, a.u.': float(E_tot), 'Dielectric energy, a.u.': float(E_diel), 'Screening charge density, A**2': psigma.tolist(), 'Screening charge, e/A**2': sigmas_grid.tolist()} return params