P3HT_ET

Markdown example P3HT_ET example

In this example, we will run through a simplified procedure to calculate the inter-molecular electronic coupling energies between penta-3-hexylthiophene

Procedure: * Generate models of thiophene and hexane based on based on quantum chemistry data from NWChem * Use streamm to create a 3-hexylthiophene pentamer * Replicate the pentamer into a periodic simulation cell * Anneal the system with LAMMPS * Calculate the inter-molecular electronic coupling using NWChem’s electron transfer module

import os
from pprint import pprint
from pathlib2 import Path
import os
import csv
import numpy as np
import decimal
import copy
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import time

Set wait time to check if calculation has finished

status_refresh = 1
import streamm

In this getting started example we will calculate the coupling between P3HT oligomers

import logging
logging.basicConfig(filename='p3ht_et.log',level=logging.DEBUG)

Load Resource objects from Resource example

need_files = ['local_res.json','remote_res.json']
for f in need_files:
    path = Path(f)
    if not path.is_file():
        print("Need to run resource_example.ipynb")
        os.system("jupyter nbconvert --to python  resource_example.ipynb")
        os.system("python resource_example.py")
res_local = streamm.Resource('local')

The calc resource can be changed to local or remote host resouce

res_calc = streamm.Resource('remote')
res_local.import_json()
res_calc.import_json()

Create needed directories

res_local.make_dir()
res_calc.make_dir()

Now let’s create project and resource to keep track of our work

p3ht_et = streamm.Project('P3HT_ET')

Set the directory structure for the project

p3ht_et.set_resource(res_local)

Explicitly create a thiophene molecule

bbTh = streamm.Buildingblock('thiophene')
symbols = ['C','C','C','C','S','H','H','H','H']

positions = [ ]
positions.append([-1.55498576,-1.91131218,-0.00081000])
positions.append([-0.17775976,-1.91131218,-0.00081000])
positions.append([0.34761524,-0.57904218,-0.00081000])
positions.append([-0.65884476,0.36101082,0.00000000])
positions.append([-2.16948076,-0.35614618,-0.00000800])
positions.append([-2.18966076,-2.79526518,-0.00132100])
positions.append([0.45389024,-2.80145418,-0.00106400])
positions.append([1.41682424,-0.35961818,-0.00138200])
positions.append([-0.51943676,1.44024682,0.00064700])

for i in range(len(symbols)):
    pt_i = streamm.Particle(symbol=symbols[i])
    pos_i = positions[i]
    bbTh.add_partpos(pt_i,pos_i)

Set the names of the terminal sites to be joined later

bbTh.particles[5].rsite = 'termcap'
bbTh.particles[6].rsite = 'funccap'
bbTh.particles[8].rsite = 'termcap'

Set some properties of the molecule to keep track of the parts

c_cnt =1
h_cnt =1

for pkey_i, particle_i  in bbTh.particles.items():

    if( particle_i.symbol == 'C' ):
        particle_i.label = "C%d"%(c_cnt)
        particle_i.resname = "SCP2"
        particle_i.residue = 1

        c_cnt +=1
    if( particle_i.symbol == 'S' ):
        particle_i.resname = "ThS"
        particle_i.residue = 2

    if( particle_i.symbol == 'H' ):
        particle_i.label = "H%d"%(h_cnt)
        particle_i.resname = "HA"
        particle_i.residue = 3

        h_cnt +=1

Set the forcefield type and guess some reasonable charges

for pkey_i, particle_i  in bbTh.particles.items():
    if( particle_i.symbol == 'C' ):
        particle_i.paramkey = 'CA'
        particle_i.charge = -0.025
    if( particle_i.symbol == 'S' ):
        particle_i.paramkey = 'S'
        particle_i.charge = -0.3
    if( particle_i.symbol == 'H' ):
        particle_i.paramkey = 'HA'
        particle_i.charge = 0.1

Check molecule is neutral

total_charge = 0.0
for pkey_i, particle_i  in bbTh.particles.items():
    total_charge += particle_i.charge

print(total_charge)

Optimize structure with NWChem

But let’s put it in a function this time

def nw_opt(project_i,bb_i,res_i):
    '''
    Optimize a streamm Buildingblock object with nwchem
    '''
    calc_n =  len(project_i.calculations)
    nwchem_i = streamm.NWChem('nw_opt_{}_calc_{}'.format(bb_i.tag,calc_n))
    print(nwchem_i.tag)

    # Add thiophene structure
    nwchem_i.strucC = copy.deepcopy(bb_i)

    # Set calculation to run on external resource
    nwchem_i.set_resource(res_i)

    # Make the local directories
    nwchem_i.make_dir()

    #Change to the `launch` directory
    os.chdir(nwchem_i.dir['launch'])

    # Copy over templates
    nwchem_i.cp_file('templates','run',"nwchem_remote.pbs",'templates','launch')
    nwchem_i.cp_file('templates','nw',"nwchem.nw",'templates','launch')

    # Read in templates files
    nwchem_i.load_str('templates','nw')
    nwchem_i.load_str('templates','run')

    # Set calculation properties
    nwchem_i.properties['basis'] = '6-31g'
    nwchem_i.properties['method'] = 'UHF'
    nwchem_i.properties['charge'] = 0
    nwchem_i.properties['spin_mult'] = 1
    nwchem_i.properties['task'] = 'SCF optimize'
    nwchem_i.properties['coord'] = nwchem_i.strucC.write_coord()
    #
    pprint(nwchem_i.properties)

    # Replace <key> with properties value
    nwchem_i.replacewrite_prop('nw','input','nw','%s.nw'%(nwchem_i.tag))
    nwchem_i.properties['input_nw'] = nwchem_i.files['input']['nw']
    nwchem_i.replacewrite_prop('run','scripts','run','%s.pbs'%(nwchem_i.tag))
    #
    nwchem_i.add_file('output','log',"%s.log"%(nwchem_i.tag))

    # Save details in .json files
    os.chdir(nwchem_i.dir['home'])
    p3ht_et.export_json()
    #
    os.chdir(nwchem_i.dir['launch'])
    #
    nwchem_i.push()
    #
    nwchem_i.run()
    # Add calculation to project
    project_i.add_calc(nwchem_i,deepcopy = True)
    #
    return project_i
p3ht_et = nw_opt(p3ht_et,bbTh,res_calc)
nwchem_i = p3ht_et.calculations['nw_opt_thiophene_calc_0']

Check status unit finished

nwchem_i.check()
print(nwchem_i.meta['status'])
while( nwchem_i.meta['status'] != 'finished'):
    nwchem_i.check()
    time.sleep(status_refresh)
print(nwchem_i.meta['status'])

Store the results

nwchem_i.store()

Download full log file for analysis

nwchem_i.pull()
os.chdir(nwchem_i.dir['launch'])
nwchem_i.analysis()

Print energies, just for fun

print(nwchem_i.properties['energy'],nwchem_i.unit_conf['energy'])

Check that the positions of the structure have been optimized

print(bbTh.positions)
bbTh.unit_conf['length']
print(nwchem_i.strucC.positions)
nwchem_i.strucC.unit_conf['length']

Update positions with optimized geometry

for pk,p in bbTh.particles.items():
    bbTh.positions[pk] = nwchem_i.strucC.positions[pk]
    print(pk,p.symbol,bbTh.positions[pk])

Store the results in a tar ball in the storage directory

nwchem_i.store()

Now let us calculate the ESP charges to use in our forcefield

Again let’s make it a function

def nw_esp(project_i,bb_i,res_i):
    '''
    Calculate ESP charges of a streamm Buildingblock object with nwchem
    '''
    calc_n =  len(project_i.calculations)
    nwchem_esp = streamm.NWChem('nw_esp_{}_calc_{}'.format(bb_i.tag,calc_n))
    print(nwchem_esp.tag)

    # Add thiophene structure with optimized coordinates from previous calculation
    nwchem_esp.strucC = copy.deepcopy(bb_i)

    # Set calculation to run on external resource
    nwchem_esp.set_resource(res_i)

    # Add calculation to project
    project_i.add_calc(nwchem_esp)

    # Make the local directories
    nwchem_esp.make_dir()

    # Change to the `launch` directory
    os.chdir(nwchem_esp.dir['launch'])
    #
    nwchem_esp.cp_file('templates','run',"nwchem_remote.pbs",'templates','launch')
    nwchem_esp.cp_file('templates','nw',"nwchem_esp.nw",'templates','launch')
    #
    nwchem_esp.load_str('templates','nw')
    nwchem_esp.load_str('templates','run')
    #
    nwchem_esp.properties['basis'] = '6-31g'
    nwchem_esp.properties['method'] = 'UHF'
    nwchem_esp.properties['charge'] = 0
    nwchem_esp.properties['spin_mult'] = 1
    nwchem_esp.properties['task'] = 'SCF'
    nwchem_esp.properties['coord'] = nwchem_esp.strucC.write_coord()

    pprint(nwchem_esp.properties)

    nwchem_esp.replacewrite_prop('nw','input','nw','%s.nw'%(nwchem_esp.tag))

    nwchem_esp.properties['input_nw'] = nwchem_esp.files['input']['nw']
    nwchem_esp.replacewrite_prop('run','scripts','run','%s.pbs'%(nwchem_esp.tag))

    nwchem_esp.add_file('output','log',"%s.log"%(nwchem_esp.tag))

    # Save details in .json files

    os.chdir(nwchem_esp.dir['home'])
    nwchem_esp.export_json()

    os.chdir(nwchem_esp.dir['launch'])
    nwchem_esp.push()
    nwchem_esp.run()

    # Add calculation to project
    project_i.add_calc(nwchem_esp,deepcopy = True)
    #
    return project_i
p3ht_et = nw_esp(p3ht_et,bbTh,res_calc)

Check status until finished

p3ht_et.check()
nwchem_i = p3ht_et.calculations['nw_esp_thiophene_calc_1']
os.chdir(nwchem_i.dir['launch'])
while( nwchem_i.meta['status'] != 'finished'):
    nwchem_i.check()
    time.sleep(status_refresh)

Store the results

nwchem_i.store()

Download full log file for analysis

nwchem_i.pull()

Run analysis to get the ESP charges

nwchem_i.analysis()

Check the new charges

for pk,p in nwchem_i.strucC.particles.items():
    print(p.symbol, p.charge)
nwchem_i.strucC.calc_charge()
print(nwchem_i.strucC.charge)

A little extra charge can cause problems with our MD simulation so, if our total is not zero let’s round and set to neutral

def charges_round_neutral(strucC,ndigits = 2 ):

    total_charge = 0.0
    for pk,p in strucC.particles.items():
        p.charge = round(p.charge,ndigits)
        total_charge += p.charge
    print(total_charge)

    for pk,p in strucC.particles.items():
        p.charge += -1.0*total_charge/strucC.n_particles
    strucC.calc_charge()

    print(strucC.charge)
if( abs(nwchem_i.strucC.charge) > 1.0e-16 ):
    charges_round_neutral(nwchem_i.strucC)

Update the charges of the Buildingblock

bbTh.tag += '_HFesp'
print(bbTh.tag)
for pk,p in bbTh.particles.items():
    p.charge = nwchem_i.strucC.particles[pk].charge
    print(pk,p.symbol,p.charge)

Create the neighbor list and use it to set the bonds, bond angles and dihedrals for the forcefield model

bbTh.bonded_nblist = bbTh.guess_nblist(0,radii_buffer=1.35)
bbTh.bonded_bonds()
bbTh.bonded_angles()
bbTh.bonded_dih()

Store an object of the Buildingblock

os.chdir(res_local.dir['materials'])
th_json = bbTh.export_json()

Let us optimize the structure with the oplsaa forcefield to check the parameters

os.chdir(res_local.dir['home'])
need_files = ['oplsaa_param.json']
for f in need_files:
    path = Path(f)
    if not path.is_file():
        print("Need to run forcefields_example.ipynb")
        os.system("jupyter nbconvert --to python  forcefields_example.ipynb")
        os.system("python forcefields_example.py")
oplsaa = streamm.Parameters('oplsaa')
oplsaa.import_json(read_file=True)
print(oplsaa)
print(oplsaa.unit_conf['energy'])

We need to add the conjugated carbons, hydrogen and sulfur atom types

import streamm.forcefields.particletype as particletype
import pymatgen_core.core.periodic_table as periodic_table

Set some parameters from J. Am. Chem. Soc., 1996, 118 (45), pp 11225–11236

CA = particletype.Particletype('CA')
HA = particletype.Particletype('HA')
CA.update_units(oplsaa.unit_conf)
HA.update_units(oplsaa.unit_conf)
CA.epsilon = 0.070 # kcal/mol
CA.sigma = 3.55 # Angstroms
HA.epsilon = 0.030 # kcal/mol
HA.sigma = 2.42 # Angstroms
CA.mass =  periodic_table.Element['C'].atomic_mass.real
HA.mass =  periodic_table.Element['H'].atomic_mass.real
print(CA,HA)
S = particletype.Particletype('S')
S.update_units(oplsaa.unit_conf)

Set some parameters from J. Am. Chem. Soc., 1996, 118 (45), pp 11225–11236

S.epsilon = 0.25 # kcal/mol
S.sigma = 3.55 # Angstroms
S.mass =  periodic_table.Element['S'].atomic_mass.real

Add to forcefield parameters container

oplsaa.add_particletype(CA)
oplsaa.add_particletype(HA)
oplsaa.add_particletype(S)

Set the bond stretching parameters

import streamm.forcefields.bondtype as bondtype
bt_i = bondtype.Bondtype('CA','HA',unit_conf=oplsaa.unit_conf)
bt_i.setharmonic(1.080,367.0)
oplsaa.add_bondtype(bt_i)
bt_i = bondtype.Bondtype('CA','CA',unit_conf=oplsaa.unit_conf)
bt_i.setharmonic(1.400,469.0)
oplsaa.add_bondtype(bt_i)
bt_i = bondtype.Bondtype('S','CA',unit_conf=oplsaa.unit_conf)
bt_i.setharmonic(1.71,250.0)
oplsaa.add_bondtype(bt_i)
for btk,bt in oplsaa.bondtypes.items():
    print(btk,bt)
import streamm.forcefields.angletype as angletype
bat_i = angletype.Angletype('CA','CA','CA',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(120.0,63.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('CA','CA','HA',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(120.0,35.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('CA','S','CA',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(92.2,70.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('S','CA','HA',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(120.0,35.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('S','CA','CA',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(111.0,70.0)
oplsaa.add_angletype(bat_i)
for atk,at in oplsaa.angletypes.items():
    print(atk,at)

Set some reasonable dihedral parameters

import streamm.forcefields.dihtype as dihtype
dih_i = dihtype.Dihtype('X','CA','CA','X',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,1.812532,0.0,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('X','S','CA','X',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,2.416710,0.0,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('S','CA','CA','HA',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,1.812532,0.0,0.0)
oplsaa.add_dihtype(dih_i)
for dk,d in oplsaa.dihtypes.items():
    print(dk,d)

Let us make an MD simulation of just the monomer to check that our parameters are okay

def lmp_run(project_i,bb_i,param_i,res_i,md_type = 'min'):

    # Create LAMMPS calculation object
    calc_n =  len(project_i.calculations)
    lmp_i = streamm.LAMMPS('lmp_{}_{}_calc_{}'.format(md_type,bb_i.tag,calc_n))

    # lmp_i = streamm.LAMMPS('lmp_{}_{}'.format(md_type,bb_i.tag))
    # Set parameter container
    lmp_i.paramC = param_i
    lmp_i.set_strucC(bb_i)

    # Set forcefield parameters
    lmp_i.set_ffparam()

    # Set resource to local
    lmp_i.set_resource(res_i)

    # Make local directories
    lmp_i.make_dir()

    # Set pbc's to on
    lmp_i.strucC.lat.pbcs = [True,True,True]

    # Change to launch directory
    os.chdir(lmp_i.dir['launch'])

    # Copy over the templates from the template directory
    lmp_i.cp_file('templates','in',"lammps_{}.in".format(md_type),'templates','launch')
    lmp_i.cp_file('templates','run',"lammps_remote.pbs",'templates','launch')

    # Change to scratch
    os.chdir(lmp_i.dir['launch'])

    # Read in template files and store them as strings in the `str` dictionary
    lmp_i.load_str('templates','in')
    lmp_i.load_str('templates','run')

    # Write LAMMPS .data file
    lmp_i.write_data()

    # Replace keys in template string with properties
    lmp_i.replacewrite_prop('in','input','in','%s.in'%(lmp_i.tag))

    # Add the input file to the properties to be written into the run file
    lmp_i.properties['input_in'] = lmp_i.files['input']['in']
    lmp_i.replacewrite_prop('run','scripts','run','%s.pbs'%(lmp_i.tag))

    # Save json file in root directory
    os.chdir(lmp_i.dir['home'])
    lmp_i.export_json()

    # Run bash script or submit to cluster
    lmp_i.add_file('output','log',"%s.log"%(lmp_i.tag))

    # Save details in .json files
    os.chdir(lmp_i.dir['home'])
    project_i.export_json()
    lmp_i.export_json()
    #
    os.chdir(lmp_i.dir['launch'])
    lmp_i.push()
    lmp_i.run()
    # Add calculation to project
    project_i.add_calc(lmp_i,deepcopy = True)
    #
    return project_i
p3ht_et.check()
p3ht_et = lmp_run(p3ht_et,bbTh,oplsaa,res_calc)
lmp_i = p3ht_et.calculations['lmp_min_thiophene_HFesp_calc_2']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)

Run analysis of .in and .log files

lmp_i.analysis()
run_i= lmp_i.run_list[0]
print(run_i.timeseries['toteng'])

Energy decreased and nothing exploded so that’s good

lmp_i.store()

Read in data file positions

lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_1']
print(datafn)
lmp_i.read_data_pos(datafn)
print(lmp_i.strucC.lat.matrix)
lmp_i.strucC.write_xyz()

We will use the oplsaa optimized structure as the initial structure since we will be running MD

bbTh.tag += '_oplsaa'
for pk,p in bbTh.particles.items():
    bbTh.positions[pk] = lmp_i.strucC.positions[pk]
    print(pk,p.symbol,bbTh.positions[pk])

Save the Buildingblock and forcefield

os.chdir(res_local.dir['materials'])
bbTh.write_xyz()
th_json = bbTh.export_json()
oplsaa_json = oplsaa.export_json()

Okay now that we have a handle on thiophene let’s follow the same procedure for hexane

Build hexane

bbHex = streamm.Buildingblock('hexane')
symbols = ['C','H','H','H','C','H','H','C','H','H','C','H','H','C','H','H','C','H','H','H']

positions = [ ]
positions.append([-6.410969,-0.381641,-0.000031])
positions.append([-7.310084,0.245311,-0.000038])
positions.append([-6.456117,-1.028799,0.884636])
positions.append([-6.456111,-1.028812,-0.884689])
positions.append([-5.135268,0.467175,-0.000033])
positions.append([-5.135484,1.128782,0.877977])
positions.append([-5.135479,1.128771,-0.87805])
positions.append([-3.850566,-0.371258,-0.000024])
positions.append([-3.85112,-1.033978,0.87841])
positions.append([-3.851114,-1.033987,-0.878451])
positions.append([-2.567451,0.469603,-0.000024])
positions.append([-2.567784,1.132155,0.8784])
positions.append([-2.567776,1.132146,-0.878455])
positions.append([-1.283527,-0.370234,-0.000013])
positions.append([-1.28337,-1.032804,0.87836])
positions.append([-1.28336,-1.032812,-0.87838])
positions.append([0.00482234,0.47342231,-0.00000898])
positions.append([0.02595107,1.09220686,0.87266464])
positions.append([0.85585781,-0.17514133,0.00194589])
positions.append([0.02780957,1.08937798,-0.87463473])

for i in range(len(symbols)):
    pt_i = streamm.Particle(symbol=symbols[i])
    pos_i = positions[i]
    bbHex.add_partpos(pt_i,pos_i)
bbHex.particles[0].rsite = 'rg'
bbHex.particles[1].rsite = 'rgcap'
c_cnt =1
h_cnt =1
for pkey_i, particle_i  in bbHex.particles.items():
            if( particle_i.symbol == 'C' ):
                particle_i.label = "C%d"%(c_cnt)
                particle_i.resname = "SCP3"
                particle_i.residue = c_cnt
                c_cnt +=1
            if( particle_i.symbol == 'H' ):
                particle_i.label = "H%d"%(h_cnt)
                particle_i.resname = "HC"
                particle_i.residue = c_cnt -1
                h_cnt +=1

Set the parameter keys and some reasonable atomic charges

for pkey_i, particle_i  in bbHex.particles.items():
            if( particle_i.symbol == 'C' ):
                particle_i.paramkey = 'CT'
                particle_i.charge = -0.12

            if( particle_i.symbol == 'H' ):
                particle_i.paramkey = 'HC'
                particle_i.charge = 0.06
            print pkey_i, particle_i.symbol,particle_i.charge
bbHex.particles[0].charge  = -0.18
bbHex.particles[16].charge  = -0.18

Check that the molecule is neutral

bbHex.calc_charge()
print(bbHex.charge)

Now let us optimize and calculate ESP charges for hexane

Optimize structure with NWChem

print(p3ht_et.calculations.keys())
p3ht_et = nw_opt(p3ht_et,bbHex,res_calc)
p3ht_et.check()
nwchem_i = p3ht_et.calculations['nw_opt_hexane_calc_3']
os.chdir(nwchem_i.dir['launch'])
while( nwchem_i.meta['status'] != 'finished'):
    nwchem_i.check()
    time.sleep(status_refresh)

Store the results

nwchem_i.store()

Download full log file for analysis

nwchem_i.pull()

Get the calculation from the project object

nwchem_i.analysis()

Print energies

print(nwchem_i.properties['alpha_energies'][10:20])
print(nwchem_i.properties['energy'])

Check that the positions of the structure have been optimized

for pk,p in bbHex.particles.items():
    print(pk,p.symbol,bbHex.positions[pk])
print(nwchem_i.strucC.positions)

Update positions in Buildingblock object

for pk,p in bbHex.particles.items():
    bbHex.positions[pk] = nwchem_i.strucC.positions[pk]
    print(pk,p.symbol,bbHex.positions[pk])

Store the results in a tar ball in the storage directory

nwchem_i.store()

Now let us calculate the ESP charges to use in our forcefield

p3ht_et = nw_esp(p3ht_et,bbHex,res_calc)

Check status unit finished

p3ht_et.check()
nwchem_i = p3ht_et.calculations['nw_esp_hexane_calc_4']
os.chdir(nwchem_i.dir['launch'])
while( nwchem_i.meta['status'] != 'finished'):
    nwchem_i.check()
    time.sleep(status_refresh)

Store the results

nwchem_i.store()

Download full log file for analysis

nwchem_i.pull()

Get the calculation from the project object

nwchem_i.analysis()

Check the new charges

for pk,p in nwchem_i.strucC.particles.items():
    print(p.symbol, p.charge)
nwchem_i.strucC.calc_charge()
print(nwchem_i.strucC.charge)

Hum a little extra charge can cause problems with our MD simulation so let’s round and set to neutral

if( abs(nwchem_i.strucC.charge) > 1.0e-16 ):
    charges_round_neutral(nwchem_i.strucC)
for pk,p in nwchem_i.strucC.particles.items():
    print(pk,p.symbol,p.charge)

Print energies

print(nwchem_i.properties['energy'],nwchem_i.unit_conf['energy'])

Update the charges of the Buildingblock

for pk,p in bbHex.particles.items():
    p.charge = nwchem_i.strucC.particles[pk].charge
bbHex.tag += '_HFesp'

First, we need to identify the bonding within the Buildingblock

bbHex.bonded_nblist = bbHex.guess_nblist(0,radii_buffer=1.35)
bbHex.bonded_bonds()
bbHex.bonded_angles()
bbHex.bonded_dih()

Add the need parameters the oplsaa parameter container

bat_i = angletype.Angletype('CT','CT','CT',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(109.50,40.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('CT','CT','CT',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(109.50,40.0)
oplsaa.add_angletype(bat_i)
bat_i = angletype.Angletype('CT','CT','HC',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(109.50,50.0)
oplsaa.add_angletype(bat_i)
dih_i = dihtype.Dihtype('CT','CT','CT','CT',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.433341,-0.016667,0.066668,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('HC','CT','CT','CT',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,-0.0,0.1,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('HC','CT','CT','HC',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,-0.0,0.1,0.0)
oplsaa.add_dihtype(dih_i)

Run an oplsaa minimization to get the minimized structure

p3ht_et = lmp_run(p3ht_et,bbHex,oplsaa,res_calc)
p3ht_et.check()
lmp_i = p3ht_et.calculations['lmp_min_hexane_HFesp_calc_5']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)
lmp_i.analysis()
run_i= lmp_i.run_list[0]
print(run_i.timeseries['toteng'])

Energy decreased and nothing exploded so that’s good

lmp_i.store()

Read in data file positions

lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_1']
print(datafn)
lmp_i.read_data_pos(datafn)
print(lmp_i.strucC.lat.matrix)
lmp_i.strucC.write_xyz()

We will use the oplsaa optimized structure as the initial structure since we will be running MD

bbHex.tag += '_oplsaa'
for pk,p in bbHex.particles.items():
    bbHex.positions[pk] = lmp_i.strucC.positions[pk]
    print(pk,p.symbol,bbHex.positions[pk])

Save the Buildingblock and forcefield

os.chdir(res_local.dir['materials'])
bbHex.write_xyz()
bbhex_json = bbHex.export_json()
oplsaa_json = oplsaa.export_json()
print(bbHex.tag,bbTh.tag)

So, let us make some P3HT oligomers

os.chdir(res_local.dir['materials'])
bbTh.find_rsites()
bbHex.find_rsites()
print(bbTh.show_rsites())
print(bbHex.show_rsites())
import streamm.structures.buildingblock as bb
ht = bb.attach(bbTh,bbHex,'funccap',0,'rgcap',0,tag='3-hexyl-thiophene')

Update bond angles and dihedrals after Buildingblock join

ht.bonded_bonds()
ht.bonded_angles()
ht.bonded_dih()

Check that the molecule looks good

ht.write_xyz()

Check the charges of the removed hydrogens got summed onto the functionalized carbons correctly

ht.calc_charge()
ht.charge
print(ht.show_rsites())

Add inter thiophene hexane parameters

bt_i = bondtype.Bondtype('CT','CA',unit_conf=oplsaa.unit_conf)
bt_i.setharmonic(1.51,317.0)
oplsaa.add_bondtype(bt_i)

Bond angle parameters

bat_i = angletype.Angletype('CA','CA','CT',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(120.0,70.0)
oplsaa.add_angletype(bat_i)

bat_i = angletype.Angletype('HA','CA','CT',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(120.0,35.0)
oplsaa.add_angletype(bat_i)

bat_i = angletype.Angletype('CA','CT','HC',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(109.5,50.0)
oplsaa.add_angletype(bat_i)

bat_i = angletype.Angletype('CA','CT','CT',unit_conf=oplsaa.unit_conf)
bat_i.setharmonic(114.0,63.0)
oplsaa.add_angletype(bat_i)
for atk,at in oplsaa.angletypes.items():
    print(atk,at)

Note: The inter-ring torsional is not consider as a separate set of parameters for the simplicity of this example

dih_i = dihtype.Dihtype('HC','CT','CT','CA',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,-0.0,0.1,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('CT','CT','CT','CA',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.433341,-0.016667,0.066668,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('HC','CT','CA','CA',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,-0.0,0.1,0.0)
oplsaa.add_dihtype(dih_i)
dih_i = dihtype.Dihtype('CT','CT','CA','CA',unit_conf=oplsaa.unit_conf)
dih_i.type ='opls'
dih_i.setopls(0.0,-0.0,0.0,0.0)
oplsaa.add_dihtype(dih_i)
for dk,d in oplsaa.dihtypes.items():
    print(dk,d)

Run an oplsaa minimization to get the minimized structure

p3ht_et = lmp_run(p3ht_et,ht,oplsaa,res_calc)
p3ht_et.check()
lmp_i = p3ht_et.calculations['lmp_min_3-hexyl-thiophene_calc_6']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)
lmp_i.analysis()
run_i= lmp_i.run_list[0]
print(run_i.timeseries['toteng'])

Energy decreased and nothing exploded so that’s good

lmp_i.store()

Read in data file positions

lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_1']
print(datafn)
lmp_i.read_data_pos(datafn)
print(lmp_i.strucC.lat.matrix)

We will use the oplsaa optimized structure as the initial structure since we will be running MD

ht.tag += '_oplsaa'
for pk,p in ht.particles.items():
    ht.positions[pk] = lmp_i.strucC.positions[pk]
    print(pk,p.symbol,ht.positions[pk])

Save the Buildingblock and forcefield

os.chdir(res_local.dir['materials'])
ht.write_xyz()
ht_json = ht.export_json()
ht_json = oplsaa.export_json()

Okay we have the monomer, so let’s make a pentamer

penta_ht = copy.deepcopy(ht)
# We could use prepattach to change the tacticity
# penta_ht = ht.prepattach('termcap',0,dir=-1,yangle=180.0)
# See buildingblock example
for n in range(4):
    penta_ht = bb.attach(penta_ht,ht,'termcap',1,'termcap',0,tag='penta_3-hexyl-thiophene')

Check the charges of the removed hydrogens got summed onto the functionalized carbons correctly

penta_ht.calc_charge()
penta_ht.charge
penta_ht.write_xyz()

Well it’s cis, but we can run some high temperature MD to randomize that

Update bond angles and dihedrals after Buildingblock join

penta_ht.bonded_bonds()
penta_ht.bonded_angles()
penta_ht.bonded_dih()
print(penta_ht.print_properties())

Run an oplsaa minimization to get the minimized structure

p3ht_et = lmp_run(p3ht_et,penta_ht,oplsaa,res_calc)
p3ht_et.check()
lmp_i = p3ht_et.calculations['lmp_min_penta_3-hexyl-thiophene_calc_7']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)
lmp_i.analysis()
run_i= lmp_i.run_list[0]
print(run_i.timeseries['toteng'])

Energy decreased and nothing exploded so that’s good

lmp_i.store()

Read in data file positions

lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_1']
print(datafn)
lmp_i.read_data_pos(datafn)
print(lmp_i.strucC.lat.matrix)
lmp_i.strucC.write_xyz()

We will use the oplsaa optimized structure as the initial structure since we will be running MD

penta_ht.tag += '_oplsaa'
for pk,p in penta_ht.particles.items():
    penta_ht.positions[pk] = lmp_i.strucC.positions[pk]
    print(pk,p.symbol,penta_ht.positions[pk])

Save the Buildingblock and forcefield

oplsaa.tag += '_p3ht'
os.chdir(res_local.dir['materials'])
penta_ht.write_xyz()
penta_ht_json = penta_ht.export_json()
oplsaa_json = oplsaa.export_json()

Cool let’s run some MD

p3ht_et = lmp_run(p3ht_et,penta_ht,oplsaa,res_calc,md_type='nvt')
p3ht_et.check()
lmp_i = p3ht_et.calculations['lmp_nvt_penta_3-hexyl-thiophene_oplsaa_calc_8']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)
lmp_i.analysis()
run_i= lmp_i.run_list[0]
print(run_i.timeseries['toteng'])
lmp_i.store()

Read in data file positions

lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_3']
print(datafn)
lmp_i.read_data_pos(datafn)
print(lmp_i.strucC.lat.matrix)
lmp_i.strucC.write_xyz()

Awesome! We have a randomized pentamer, so let’s save that as new Buildingblock

bbPHTh_1 = copy.deepcopy(lmp_i.strucC)
print bbPHTh_1
print bbPHTh_1.n_particles
os.chdir(p3ht_et.dir['home'])
p3ht_et.export_json()
os.chdir(res_local.dir['materials'])
bbPHTh_1.write_xyz()
bbPHTh_1_json = bbPHTh_1.export_json()

Now let’s replicate the oligomer 50 times to create a low density system

Increase the box size

pHTh_x = streamm.Buildingblock()
def replicate(pHTh_x,bbPHTh_1,res_local):
    '''Replciate structure '''

    pHTh_x.lat.matrix = [ 200.,0.,0., 0.,200.,0.,  0.,0.,200.]
    pHTh_x.lat.pbcs = [False,False,False]

    seed = 394572

    # Randomly place oligomers into the simulation cell

    pHTh_x = streamm.add_struc(pHTh_x,bbPHTh_1,50,seed)
    pHTh_x.tag = 'p3HTx50'
    pHTh_x.lat.pbcs = [True,True,True]

    os.chdir(res_local.dir['materials'])
    pHTh_x.write_xyz()
    pHTh_json = pHTh_x.export_json()

    return pHTh_x
need_files = ['p3HTx50_struc.json']
read_p3HTx50 = True
for f in need_files:
    path = Path(f)
    if not path.is_file():
        print("Need to run replicate")
        pHTh_x = replicate(pHTh_x,bbPHTh_1,res_local)
        read_p3HTx50 = False
if( read_p3HTx50 ):
    pHTh_x.tag = 'p3HTx50'
    pHTh_x.import_json()
print(pHTh_x.n_particles)
print(pHTh_x.lat.matrix)

Check grouping

groupset_i = streamm.Groups('mol',pHTh_x)
groupset_i.group_prop('mol','oligomers')
print(len(groupset_i.groups))
groupset_i.strucC.lat.pbcs

Run a heat cool cycle with NPT to create a solid phase representation of p3HT

p3ht_et = lmp_run(p3ht_et,pHTh_x,oplsaa,res_calc,md_type = 'equ0')
p3ht_et.check()
lmp_i = p3ht_et.calculations['lmp_equ0_p3HTx50_calc_9']
os.chdir(lmp_i.dir['launch'])
while( lmp_i.meta['status'] != 'finished'):
    lmp_i.check()
    time.sleep(status_refresh)
lmp_i.analysis()

Check how many runs there were in the output

print(lmp_i.properties['run_cnt'])

Plot the time series data from the MD runs

def plot_mdrun(lmp_i):

    fig, ax = plt.subplots(1,sharey=True)
    ax2 = ax.twinx()

    for run_i in lmp_i.run_list:
        ax.plot(run_i.timeseries['step'],run_i.timeseries['volume'],'b.-')
        ax2.plot(run_i.timeseries['step'],run_i.timeseries['temp'],'k.-')

    ax.set_ylabel('volume', color='b')
    ax2.set_ylabel('temp', color='k')
    ax.set_xlabel('time (fs)', color='k')


    fig.subplots_adjust(hspace=0.0)
    fig.set_size_inches(8.0, 12.0)

    fig.savefig('{}.pdf'.format(lmp_i.tag),format='pdf')
plot_mdrun(lmp_i)

Cool the volume is decreasing

Note:: If you want to collapse the system entirely you will have to run a slower cooling cycle

lmp_i.store()
lmp_i.pull()

Read in data file output and update positions

datafn = lmp_i.files['output']['data_3']
print(datafn)

Update positions

lmp_i.read_data_pos(datafn)

Check the size of the simulation cell

print(lmp_i.strucC.lat.matrix)

Update tag

lmp_i.strucC.tag += '_equ0'
lmp_i.strucC.write_xyz()

We can use streamm to calculate some properties of the system

lmp_i.strucC.calc_mass()
lmp_i.strucC.calc_volume()
lmp_i.strucC.calc_center_mass()
print(lmp_i.strucC.center_mass)
struc_i = lmp_i.strucC

Save annealed structure

os.chdir(res_local.dir['materials'])
struc_json = struc_i.export_json()

Let us create a new project to hold all the ET calculations we need to do for each pair of groups

mol_et_equ0 = streamm.Project('mol_et_equ0')
mol_et_equ0.set_resource(res_local)
os.chdir(mol_et_equ0.dir['materials'])

If we need to restart the project here all we have to do is load in the structure

try:
    print(struc_i.n_particles)
except:
    struc_i = streamm.Buildingblock('p3HTx50_equ0')
    struc_i.import_json()

Create groups out of the molecules

groupset_i = streamm.Groups('mol',struc_i)
groupset_i.group_prop('mol','oligomers')
print(len(groupset_i.groups))
groupset_i.strucC.lat.pbcs = [True,True,True]
print(groupset_i.strucC.lat.pbcs)
print(groupset_i.strucC.lat.matrix)

Apply periodic boundaries to all the groups, so the molecules are not split across pbc’s

groupset_i.group_pbcs()
lmp_i.strucC.calc_center_mass()
print(lmp_i.strucC.center_mass)

Write out the new structure and check that all the molecules are whole

groupset_i.strucC.write_xyz('groups.xyz')

Calculate some group properties to use to build a neighbor list

groupset_i.calc_cent_mass()
groupset_i.calc_radius()
# groupset_i.calc_dl()
print(groupset_i.strucC.lat)
print(len(groupset_i.cent_mass))
print(len(groupset_i.radius))

Save the structure we are creating our pairs from

gmol_json = groupset_i.strucC.export_json()

Create a neighbor list of groups

groupset_i.group_nblist.radii_nblist(groupset_i.strucC.lat,groupset_i.cent_mass,groupset_i.radius,radii_buffer=0.500)
print(groupset_i.group_nblist)
g_nbs = []
for gk_i,g_i in groupset_i.groups.items():
        n_nbs = groupset_i.group_nblist.calc_nnab(gk_i)
        g_nbs.append(n_nbs)

g_nbs = np.array(g_nbs)

Check the min, average and max numbers of neighbors

print(g_nbs.min(),g_nbs.mean(),g_nbs.max())

Loop over each group, shift the group to the center of the simulation cell and write an .xyz file that includes the neighbors of the group.

for gk_i,g_i in groupset_i.groups.items():
        list_i = copy.deepcopy(g_i.pkeys)
        for g_j in groupset_i.group_nblist.getnbs(gk_i):
            list_i += groupset_i.groups[g_j].pkeys

        print(gk_i,groupset_i.group_nblist.calc_nnab(gk_i),len(list_i))

        groupset_i.strucC.shift_pos(-1.0*g_i.cent_mass)  # Place center of mass at origin
        groupset_i.strucC.write_xyz_list(list_i,xyz_file='nn_{}.xyz'.format(gk_i))
        groupset_i.strucC.shift_pos(g_i.cent_mass)  # Return center of mass

        list_i = []

The nearest neighbor cluster look good so let us calculate the electron transfer

First create a list of unique pairs

et_pairs = {}
et_pairs['i'] = []
et_pairs['j'] = []
for gk_i,g_i in groupset_i.groups.items():
    for gk_j in groupset_i.group_nblist.getnbs(gk_i):
        if( gk_j > gk_i ):
            et_pairs['i'].append(gk_i)
            et_pairs['j'].append(gk_j)

Convert the dictionary to a pandas Dataframe

import pandas as pd
et_df = pd.DataFrame(et_pairs)
et_df.columns

Save that in a local file

et_fn = 'et_pairs.csv'
if( len(et_df) == 0 ):
    et_df = pd.read_csv(et_fn)
print(len(et_df))
et_df.to_csv('et_pairs.csv',sep=',')
def nw_et(project_i,res_i,groupset_i,gk_i,gk_j,run_calc = True):

    calc_n =  len(project_i.calculations)
    nwchem_et = streamm.NWChem('nw_et_{}_g{}_g{}'.format(project_i.tag,gk_i,gk_j))

    # Set calculation to run on external resource
    nwchem_et.set_resource(res_i)

    # Make the local directories
    nwchem_et.make_dir()
    # Change to the `launch` directory
    os.chdir(nwchem_et.dir['launch'])

    group_i = groupset_i.groups[gk_i]
    group_j = groupset_i.groups[gk_j]

    nwchem_et.properties['coord_i'] = group_i.write_coord()
    nwchem_et.properties['coord_j'] = group_j.write_coord()
    nwchem_et.properties['coord_ij'] = nwchem_et.properties['coord_i'] + nwchem_et.properties['coord_j']

    nwchem_et.cp_file('templates','run',"nwchem_remote.pbs",'templates','launch')
    nwchem_et.cp_file('templates','nw',"nwchem_et.nw",'templates','launch')
    #
    nwchem_et.load_str('templates','nw')
    nwchem_et.load_str('templates','run')
    #
    nwchem_et.replacewrite_prop('nw','input','nw','%s.nw'%(nwchem_et.tag))

    nwchem_et.properties['input_nw'] = nwchem_et.files['input']['nw']
    nwchem_et.replacewrite_prop('run','scripts','run','%s.pbs'%(nwchem_et.tag))

    nwchem_et.add_file('output','log',"%s.log"%(nwchem_et.tag))

    # Save details in .json files
    #
    os.chdir(nwchem_et.dir['home'])
    nwchem_et.export_json()
    #
    #
    if( run_calc ):
        os.chdir(nwchem_et.dir['launch'])
        nwchem_et.push()
        nwchem_et.run()

    return nwchem_et

Loop over all the pairs and create NWChem ET input files

et_df['calc_id'] = None
for k,pair_i in et_df.iterrows():
    gk_i = pair_i['i']
    gk_j = pair_i['j']
    nwchem_et = nw_et(mol_et_equ0,res_calc,groupset_i,gk_i,gk_j)
    # Add calculation to project
    mol_et_equ0.add_calc(nwchem_et,deepcopy = True)
    # Add the tag to the Dataframe
    #
    et_row = et_df.loc[ (et_df['i'] == gk_i ) & (et_df['j'] == gk_j)]
    et_index =  int(et_row.index[0])
    et_df.loc[et_index,'calc_id'] = nwchem_et.tag

Check the first couple of entries in the Dataframe

et_df.head()
os.chdir(mol_et_equ0.dir['home'])
et_json = mol_et_equ0.export_json()

Now we have to wait for all of these calculations to finish

import sys
calcsrunning = True
while( calcsrunning ):
    calcsrunning = False
    n_running = 0
    for k,calc_i in mol_et_equ0.calculations.items():
        os.chdir(calc_i.dir['launch'])
        calc_i.check()
        if( calc_i.meta['status'] == 'finished' ):
            n_running += 1
        else:
            calcsrunning = True
    sys.stdout.write("progress: {}/{} \r".format(n_running,len(mol_et_equ0.calculations)))
    sys.stdout.flush()
    time.sleep(status_refresh)

Run analysis on the results

S_ij is the Reactants/Products overlap between group i and group j and V_ij is the Electron Transfer Coupling Energy between groups i and j.

et_df['S_ij'] = None
et_df['S_ji'] = None
et_df['V_ij'] = None
et_df['V_ji'] = None
for k,calc_i in mol_et_equ0.calculations.items():
    if(  calc_i.meta['status'] == 'finished' ):

        print(calc_i.tag,calc_i.meta['status'])
        os.chdir(calc_i.dir['launch'])

        et_row = et_df.loc[ et_df['calc_id'] == calc_i.tag ]

        et_index =  int(et_row.index[0])
        calc_i.store()
        calc_i.pull()
        calc_i.analysis()

        if( len(calc_i.et_list) > 0  ):
            et_df.loc[et_index,'S_ij'] = calc_i.et_list[0].S
            et_df.loc[et_index,'V_ij'] = calc_i.et_list[0].V
            et_df.loc[et_index,'S_ji'] = calc_i.et_list[1].S
            et_df.loc[et_index,'V_ji'] = calc_i.et_list[1].V


        calc_i.store()

Check the values in the Dataframe

et_df

Remove inf and NaN values

et_c1 = et_df.replace([np.inf], np.nan)
et_c1.dropna()
print(et_c1['V_ij'].min(),et_c1['V_ij'].mean(),et_c1['V_ij'].max())

We can take a look at the histogram of magnitudes of V_ij

et_c1['log_V_ij'] = np.log10(et_c1['V_ij'])
et_c1['log_V_ij'].plot.hist(bins=30,alpha=1.0)

Just calculated the inter-molecular electronic coupling between P3ht

Boom!

There is a stripped down python version of this example (P3HT_ET.py) that will run the calculations on an external resource.