emsuite 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emsuite-0.1.0/PKG-INFO +80 -0
- emsuite-0.1.0/README.md +68 -0
- emsuite-0.1.0/emsuite/__init__.py +8 -0
- emsuite-0.1.0/emsuite/tuning.py +954 -0
- emsuite-0.1.0/emsuite.egg-info/PKG-INFO +80 -0
- emsuite-0.1.0/emsuite.egg-info/SOURCES.txt +9 -0
- emsuite-0.1.0/emsuite.egg-info/dependency_links.txt +1 -0
- emsuite-0.1.0/emsuite.egg-info/requires.txt +4 -0
- emsuite-0.1.0/emsuite.egg-info/top_level.txt +1 -0
- emsuite-0.1.0/pyproject.toml +19 -0
- emsuite-0.1.0/setup.cfg +4 -0
emsuite-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emsuite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Electrostatic Map Suite - A suite of calculators for quantified electrostatic interactions.
|
|
5
|
+
Author-email: "Stephen O. Ajagbe" <ajagbetobistephen@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: geometric>=1.1
|
|
9
|
+
Requires-Dist: pyscf>=2.10.0
|
|
10
|
+
Requires-Dist: rdkit>=2025.3.5
|
|
11
|
+
Requires-Dist: vdw-surfgen>=0.5.1
|
|
12
|
+
|
|
13
|
+
# EMSuite
|
|
14
|
+
|
|
15
|
+
A suite of calculators for quantified electrostatic interactions.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install emsuite
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from emsuite import TuningCalculator
|
|
27
|
+
|
|
28
|
+
# Initialize calculator
|
|
29
|
+
calc = TuningCalculator(
|
|
30
|
+
molecule="water.xyz",
|
|
31
|
+
basis_set="6-31G*",
|
|
32
|
+
functional="m06-2x"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Run calculations
|
|
36
|
+
calc.run_calculation(['gse', 'homo', 'lumo', 'gap'])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Available Properties
|
|
40
|
+
|
|
41
|
+
**Basic Electronic Properties:**
|
|
42
|
+
- `gse` - Ground state energy
|
|
43
|
+
- `homo` - HOMO energy
|
|
44
|
+
- `lumo` - LUMO energy
|
|
45
|
+
- `gap` - HOMO-LUMO gap
|
|
46
|
+
- `dm` - Dipole moment
|
|
47
|
+
|
|
48
|
+
**Ionization & Electron Affinity:**
|
|
49
|
+
- `ie` - Ionization energy
|
|
50
|
+
- `ea` - Electron affinity
|
|
51
|
+
|
|
52
|
+
**Chemical Descriptors:**
|
|
53
|
+
- `cp` - Chemical potential
|
|
54
|
+
- `eng` - Electronegativity
|
|
55
|
+
- `hard` - Chemical hardness
|
|
56
|
+
- `efl` - Electrophilicity index
|
|
57
|
+
- `nfl` - Nucleophilicity index
|
|
58
|
+
|
|
59
|
+
**Excited States (TD-DFT):**
|
|
60
|
+
- `exe` - Excitation energies
|
|
61
|
+
- `osc` - Oscillator strengths
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- Electronic structure calculations (DFT, HF)
|
|
67
|
+
- Molecular property analysis (HOMO/LUMO, ionization energy, electron affinity)
|
|
68
|
+
- Solvent effects and geometry optimization
|
|
69
|
+
- TD-DFT excited state calculations
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Python 3.8+
|
|
74
|
+
- PySCF
|
|
75
|
+
- RDKit
|
|
76
|
+
- NumPy
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
emsuite-0.1.0/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# EMSuite
|
|
2
|
+
|
|
3
|
+
A suite of calculators for quantified electrostatic interactions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install emsuite
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from emsuite import TuningCalculator
|
|
15
|
+
|
|
16
|
+
# Initialize calculator
|
|
17
|
+
calc = TuningCalculator(
|
|
18
|
+
molecule="water.xyz",
|
|
19
|
+
basis_set="6-31G*",
|
|
20
|
+
functional="m06-2x"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Run calculations
|
|
24
|
+
calc.run_calculation(['gse', 'homo', 'lumo', 'gap'])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Available Properties
|
|
28
|
+
|
|
29
|
+
**Basic Electronic Properties:**
|
|
30
|
+
- `gse` - Ground state energy
|
|
31
|
+
- `homo` - HOMO energy
|
|
32
|
+
- `lumo` - LUMO energy
|
|
33
|
+
- `gap` - HOMO-LUMO gap
|
|
34
|
+
- `dm` - Dipole moment
|
|
35
|
+
|
|
36
|
+
**Ionization & Electron Affinity:**
|
|
37
|
+
- `ie` - Ionization energy
|
|
38
|
+
- `ea` - Electron affinity
|
|
39
|
+
|
|
40
|
+
**Chemical Descriptors:**
|
|
41
|
+
- `cp` - Chemical potential
|
|
42
|
+
- `eng` - Electronegativity
|
|
43
|
+
- `hard` - Chemical hardness
|
|
44
|
+
- `efl` - Electrophilicity index
|
|
45
|
+
- `nfl` - Nucleophilicity index
|
|
46
|
+
|
|
47
|
+
**Excited States (TD-DFT):**
|
|
48
|
+
- `exe` - Excitation energies
|
|
49
|
+
- `osc` - Oscillator strengths
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- Electronic structure calculations (DFT, HF)
|
|
55
|
+
- Molecular property analysis (HOMO/LUMO, ionization energy, electron affinity)
|
|
56
|
+
- Solvent effects and geometry optimization
|
|
57
|
+
- TD-DFT excited state calculations
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- Python 3.8+
|
|
62
|
+
- PySCF
|
|
63
|
+
- RDKit
|
|
64
|
+
- NumPy
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pyscf import gto, scf, dft, qmmm, tdscf
|
|
6
|
+
from pyscf.solvent import smd
|
|
7
|
+
from pyscf.hessian import thermo
|
|
8
|
+
from pyscf.geomopt.geometric_solver import optimize
|
|
9
|
+
from rdkit import Chem
|
|
10
|
+
from rdkit.Chem import AllChem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TuningCalculator:
|
|
14
|
+
"""
|
|
15
|
+
A comprehensive class for calculating molecular properties and their electronic responses
|
|
16
|
+
to external electrostatics.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
HARTREE_TO_KCAL = 627.509
|
|
21
|
+
HARTREE_TO_EV = 27.2114
|
|
22
|
+
SHE_REFERENCE_POTENTIAL = 4.43
|
|
23
|
+
|
|
24
|
+
# Property dependency and unit mapping
|
|
25
|
+
PROPERTY_CONFIG = {
|
|
26
|
+
'gse': {'deps': [], 'calc': [], 'unit': 1},
|
|
27
|
+
'homo': {'deps': [], 'calc': [], 'unit': 1},
|
|
28
|
+
'lumo': {'deps': [], 'calc': [], 'unit': 1},
|
|
29
|
+
'gap': {'deps': ['homo', 'lumo'], 'calc': [], 'unit': 1},
|
|
30
|
+
'dm': {'deps': [], 'calc': [], 'unit': 1},
|
|
31
|
+
'ie': {'deps': [], 'calc': ['cation'], 'unit': HARTREE_TO_KCAL},
|
|
32
|
+
'ea': {'deps': [], 'calc': ['anion'], 'unit': HARTREE_TO_KCAL},
|
|
33
|
+
'cp': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_KCAL},
|
|
34
|
+
'eng': {'deps': ['cp'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
35
|
+
'hard': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
36
|
+
'efl': {'deps': ['cp', 'hard'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
37
|
+
'nfl': {'deps': ['efl'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
38
|
+
'exe': {'deps': [], 'calc': ['td'], 'unit': 1},
|
|
39
|
+
'osc': {'deps': [], 'calc': ['td'], 'unit': 1}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __init__(self, molecule='water', method='dft', functional='b3lyp',
|
|
43
|
+
basis_set='augccpvdz', charge=0, spin=1, gfec_functional='b3lyp',
|
|
44
|
+
gfec_basis_set='6-31+G*', state_of_interest=2, triplet_excitation=False,
|
|
45
|
+
solvent=None, rdx_solvent='acetonitrile', input_type='xyz',
|
|
46
|
+
smiles_input='O', optimize_geometry=True):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the TuningCalculator with molecular parameters.
|
|
49
|
+
"""
|
|
50
|
+
self.molecule = molecule
|
|
51
|
+
self.method = method
|
|
52
|
+
self.functional = functional
|
|
53
|
+
self.basis_set = basis_set
|
|
54
|
+
self.charge = charge
|
|
55
|
+
self.spin = spin
|
|
56
|
+
self.gfec_functional = gfec_functional
|
|
57
|
+
self.gfec_basis_set = gfec_basis_set
|
|
58
|
+
self.state_of_interest = state_of_interest
|
|
59
|
+
self.triplet_excitation = triplet_excitation
|
|
60
|
+
self.solvent = solvent
|
|
61
|
+
self.rdx_solvent = rdx_solvent
|
|
62
|
+
self.input_type = input_type
|
|
63
|
+
self.smiles_input = smiles_input
|
|
64
|
+
self.optimize_geometry = optimize_geometry
|
|
65
|
+
|
|
66
|
+
# Will be populated during calculations
|
|
67
|
+
self.molecule_object = None
|
|
68
|
+
self.anion_mf = None
|
|
69
|
+
self.cation_mf = None
|
|
70
|
+
self.td_object = None
|
|
71
|
+
self.xyz_file = None
|
|
72
|
+
self.molecule_name = None
|
|
73
|
+
self.surface_coords = []
|
|
74
|
+
self.properties_alone = {}
|
|
75
|
+
self.tuning = {}
|
|
76
|
+
|
|
77
|
+
def setup_calculation(self, requested_props):
|
|
78
|
+
"""Setup properties and calculations needed"""
|
|
79
|
+
if 'all' in requested_props:
|
|
80
|
+
requested_props = list(self.PROPERTY_CONFIG.keys())
|
|
81
|
+
|
|
82
|
+
# Resolve dependencies
|
|
83
|
+
props_needed = set()
|
|
84
|
+
def add_deps(prop):
|
|
85
|
+
if prop in props_needed: return
|
|
86
|
+
props_needed.add(prop)
|
|
87
|
+
for dep in self.PROPERTY_CONFIG[prop]['deps']:
|
|
88
|
+
add_deps(dep)
|
|
89
|
+
|
|
90
|
+
for prop in requested_props:
|
|
91
|
+
add_deps(prop)
|
|
92
|
+
|
|
93
|
+
# Determine required calculations
|
|
94
|
+
calcs_needed = {'neutral': True}
|
|
95
|
+
for prop in props_needed:
|
|
96
|
+
for calc in self.PROPERTY_CONFIG[prop]['calc']:
|
|
97
|
+
calcs_needed[calc] = True
|
|
98
|
+
|
|
99
|
+
return list(props_needed), calcs_needed
|
|
100
|
+
|
|
101
|
+
def create_molecule_object(self, atom_input, basis_set, method='dft',
|
|
102
|
+
functional='m06-2x', charge=0, spin=1):
|
|
103
|
+
"""
|
|
104
|
+
Create a PySCF molecule and KS/DFT object from either:
|
|
105
|
+
- atom_input: list of (atom_symbol, (x,y,z)) tuples
|
|
106
|
+
- atom_input: string path to XYZ file
|
|
107
|
+
"""
|
|
108
|
+
spin -= 1
|
|
109
|
+
mol = gto.M(basis=basis_set, charge=charge, spin=spin, symmetry=False)
|
|
110
|
+
if isinstance(atom_input, str) and os.path.isfile(atom_input):
|
|
111
|
+
mol.atom = atom_input
|
|
112
|
+
else:
|
|
113
|
+
mol.atom = atom_input
|
|
114
|
+
mol.build()
|
|
115
|
+
|
|
116
|
+
# Choose method and handle spin
|
|
117
|
+
if method.lower() == 'dft':
|
|
118
|
+
mol.xc = functional
|
|
119
|
+
mf = dft.UKS(mol) if spin > 0 else dft.RKS(mol)
|
|
120
|
+
elif method.lower() == 'hf':
|
|
121
|
+
mf = scf.UHF(mol) if spin > 0 else scf.RHF(mol)
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError("method must be 'dft' or 'hf'")
|
|
124
|
+
return mf
|
|
125
|
+
|
|
126
|
+
def create_charged_molecule_object(self, atom_input, basis_set, method='dft',
|
|
127
|
+
functional='m06-2x', original_charge=0,
|
|
128
|
+
original_spin=1, charge_change=-1):
|
|
129
|
+
"""
|
|
130
|
+
Create charged form of molecule (anion or cation) and return the lowest energy one
|
|
131
|
+
"""
|
|
132
|
+
new_charge = original_charge + charge_change
|
|
133
|
+
|
|
134
|
+
# Determine possible spin states based on charge change
|
|
135
|
+
if charge_change == -1:
|
|
136
|
+
if original_spin == 2:
|
|
137
|
+
possible_spins = [1, 3]
|
|
138
|
+
elif original_spin == 1:
|
|
139
|
+
possible_spins = [2]
|
|
140
|
+
elif original_spin == 3:
|
|
141
|
+
possible_spins = [2, 4]
|
|
142
|
+
|
|
143
|
+
elif charge_change == +1:
|
|
144
|
+
if original_spin == 1:
|
|
145
|
+
possible_spins = [2]
|
|
146
|
+
elif original_spin == 2:
|
|
147
|
+
possible_spins = [1, 3]
|
|
148
|
+
elif original_spin == 3:
|
|
149
|
+
possible_spins = [2, 4]
|
|
150
|
+
|
|
151
|
+
energies = []
|
|
152
|
+
objects = []
|
|
153
|
+
|
|
154
|
+
for spin in possible_spins:
|
|
155
|
+
pyscf_spin = spin - 1
|
|
156
|
+
mol = gto.Mole(basis=basis_set, charge=new_charge, spin=pyscf_spin)
|
|
157
|
+
mol.atom = atom_input
|
|
158
|
+
mol.build()
|
|
159
|
+
if method.lower() == 'dft':
|
|
160
|
+
mf = dft.UKS(mol) if pyscf_spin > 0 else dft.RKS(mol)
|
|
161
|
+
mf.xc = functional
|
|
162
|
+
elif method.lower() == 'hf':
|
|
163
|
+
mf = scf.UHF(mol) if pyscf_spin > 0 else scf.RHF(mol)
|
|
164
|
+
energy = mf.kernel()
|
|
165
|
+
energies.append(energy)
|
|
166
|
+
objects.append(mf)
|
|
167
|
+
|
|
168
|
+
# Find the lowest energy
|
|
169
|
+
min_idx = energies.index(min(energies))
|
|
170
|
+
lowest_energy = energies[min_idx]
|
|
171
|
+
optimal_spin = possible_spins[min_idx]
|
|
172
|
+
best_object = objects[min_idx]
|
|
173
|
+
|
|
174
|
+
return best_object, optimal_spin, lowest_energy
|
|
175
|
+
|
|
176
|
+
def create_td_molecule_object(self, mf, nstates=5, triplet=False):
|
|
177
|
+
"""Create time-dependent DFT/HF object"""
|
|
178
|
+
# Handle solvated molecules
|
|
179
|
+
if hasattr(mf, 'with_solvent'):
|
|
180
|
+
# For solvated molecules, use tdscf directly
|
|
181
|
+
if hasattr(mf, 'xc'): # DFT case
|
|
182
|
+
td = tdscf.TDDFT(mf)
|
|
183
|
+
else: # HF case
|
|
184
|
+
td = tdscf.TDHF(mf)
|
|
185
|
+
else:
|
|
186
|
+
# For non-solvated molecules, use the original method
|
|
187
|
+
if hasattr(mf, 'TDDFT') and mf.TDDFT is not None:
|
|
188
|
+
td = mf.TDDFT()
|
|
189
|
+
elif hasattr(mf, 'TDHF') and mf.TDHF is not None:
|
|
190
|
+
td = mf.TDHF()
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError("Unsupported ground state object type")
|
|
193
|
+
|
|
194
|
+
td.singlet = not triplet
|
|
195
|
+
td.nstates = nstates
|
|
196
|
+
return td
|
|
197
|
+
|
|
198
|
+
def solvate_molecule(self, mf, solvent='water'):
|
|
199
|
+
"""Apply solvation to molecule"""
|
|
200
|
+
solvent = solvent.lower()
|
|
201
|
+
mf = mf.PCM()
|
|
202
|
+
mf.with_solvent.eps = smd.solvent_db[solvent][5]
|
|
203
|
+
mf.with_solvent.method = 'C-PCM'
|
|
204
|
+
mf.with_solvent.lebedev_order = 29
|
|
205
|
+
return mf
|
|
206
|
+
|
|
207
|
+
def optimize_and_get_equilibrium(self, mf):
|
|
208
|
+
"""Optimize the geometry of the molecule and return the equilibrium geometry"""
|
|
209
|
+
mol_eq = optimize(mf, conv_tol_grad=1e-7, conv_tol=1e-10)
|
|
210
|
+
coords = mol_eq.atom_coords(unit='Ang')
|
|
211
|
+
atoms = [mol_eq.atom_symbol(i) for i in range(mol_eq.natm)]
|
|
212
|
+
atom_list = [(atom, coord) for atom, coord in zip(atoms, coords)]
|
|
213
|
+
return atom_list
|
|
214
|
+
|
|
215
|
+
def smiles_to_xyz(self, smiles, filename=None):
|
|
216
|
+
"""Convert SMILES to XYZ file"""
|
|
217
|
+
mol = Chem.AddHs(Chem.MolFromSmiles(smiles))
|
|
218
|
+
AllChem.EmbedMolecule(mol)
|
|
219
|
+
AllChem.MMFFOptimizeMolecule(mol)
|
|
220
|
+
|
|
221
|
+
if not filename:
|
|
222
|
+
filename = f"mol_{abs(hash(smiles)) % 10000}.xyz"
|
|
223
|
+
|
|
224
|
+
conf = mol.GetConformer()
|
|
225
|
+
with open(filename, 'w') as f:
|
|
226
|
+
f.write(f"{mol.GetNumAtoms()}\n{smiles}\n")
|
|
227
|
+
for i, atom in enumerate(mol.GetAtoms()):
|
|
228
|
+
pos = conf.GetAtomPosition(i)
|
|
229
|
+
f.write(f"{atom.GetSymbol()} {pos.x:.6f} {pos.y:.6f} {pos.z:.6f}\n")
|
|
230
|
+
return filename
|
|
231
|
+
|
|
232
|
+
def create_optimized_molecule(self, atom_input, basis_set, method='dft',
|
|
233
|
+
functional='m06-2x', charge=0, spin=1, optimize=True):
|
|
234
|
+
"""Create molecule with optional geometry optimization"""
|
|
235
|
+
if optimize:
|
|
236
|
+
# Create initial molecule for optimization
|
|
237
|
+
mf_initial = self.create_molecule_object(atom_input, basis_set, method, functional, charge, spin)
|
|
238
|
+
mf_initial.kernel()
|
|
239
|
+
|
|
240
|
+
# Optimize geometry using existing function
|
|
241
|
+
opt_coords = self.optimize_and_get_equilibrium(mf_initial)
|
|
242
|
+
|
|
243
|
+
# Create new molecule with optimized coordinates
|
|
244
|
+
return self.create_molecule_object(opt_coords, basis_set, method, functional, charge, spin)
|
|
245
|
+
else:
|
|
246
|
+
return self.create_molecule_object(atom_input, basis_set, method, functional, charge, spin)
|
|
247
|
+
|
|
248
|
+
def find_homo_lumo_and_gap(self, mf):
|
|
249
|
+
"""Find HOMO, LUMO, and gap energies"""
|
|
250
|
+
homo = -float("inf")
|
|
251
|
+
lumo = float("inf")
|
|
252
|
+
for energy, occ in zip(mf.mo_energy, mf.mo_occ):
|
|
253
|
+
if occ > 0 and energy > homo:
|
|
254
|
+
homo = energy
|
|
255
|
+
if occ == 0 and energy < lumo:
|
|
256
|
+
lumo = energy
|
|
257
|
+
return homo, lumo, lumo - homo
|
|
258
|
+
|
|
259
|
+
def get_molecule_gfec(self, mf):
|
|
260
|
+
"""Calculate Gibbs free energy correction"""
|
|
261
|
+
mf.kernel()
|
|
262
|
+
hess = mf.Hessian().kernel()
|
|
263
|
+
|
|
264
|
+
vib_data = thermo.harmonic_analysis(
|
|
265
|
+
mf.mol,
|
|
266
|
+
hess,
|
|
267
|
+
imaginary_freq=False
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
thermo_data = thermo.thermo(
|
|
271
|
+
mf,
|
|
272
|
+
vib_data['freq_au'],
|
|
273
|
+
temperature=298.15,
|
|
274
|
+
pressure=101325
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
G_corr = thermo_data['G_tot'][0] - thermo_data['E0'][0]
|
|
278
|
+
return vib_data, thermo_data, G_corr
|
|
279
|
+
|
|
280
|
+
def calculate_all_properties(self, mf, anion_mf=None, cation_mf=None,
|
|
281
|
+
td_obj=None, props_to_calc=None):
|
|
282
|
+
"""Calculate all requested molecular properties"""
|
|
283
|
+
results = {}
|
|
284
|
+
|
|
285
|
+
# Basic properties
|
|
286
|
+
if 'gse' in props_to_calc:
|
|
287
|
+
results['gse'] = mf.e_tot * self.HARTREE_TO_KCAL
|
|
288
|
+
|
|
289
|
+
if any(p in props_to_calc for p in ['homo', 'lumo', 'gap']):
|
|
290
|
+
homo, lumo, gap = [x * self.HARTREE_TO_EV for x in self.find_homo_lumo_and_gap(mf)]
|
|
291
|
+
results.update({p: v for p, v in zip(['homo', 'lumo', 'gap'], [homo, lumo, gap]) if p in props_to_calc})
|
|
292
|
+
|
|
293
|
+
if 'dm' in props_to_calc:
|
|
294
|
+
results['dm'] = np.linalg.norm(mf.dip_moment())
|
|
295
|
+
|
|
296
|
+
# Charged state properties
|
|
297
|
+
if 'ie' in props_to_calc and cation_mf:
|
|
298
|
+
results['ie'] = cation_mf.e_tot - mf.e_tot
|
|
299
|
+
if 'ea' in props_to_calc and anion_mf:
|
|
300
|
+
results['ea'] = mf.e_tot - anion_mf.e_tot
|
|
301
|
+
|
|
302
|
+
# Derived properties
|
|
303
|
+
if 'cp' in props_to_calc and all(k in results for k in ['ie', 'ea']):
|
|
304
|
+
results['cp'] = -(results['ie'] + results['ea']) / 2
|
|
305
|
+
if 'eng' in props_to_calc and 'cp' in results:
|
|
306
|
+
results['eng'] = -results['cp']
|
|
307
|
+
if 'hard' in props_to_calc and all(k in results for k in ['ie', 'ea']):
|
|
308
|
+
results['hard'] = (results['ie'] - results['ea']) / 2
|
|
309
|
+
if 'efl' in props_to_calc and all(k in results for k in ['cp', 'hard']):
|
|
310
|
+
results['efl'] = results['cp']**2 / (2 * results['hard']) if results['hard'] != 0 else 0
|
|
311
|
+
if 'nfl' in props_to_calc and 'efl' in results:
|
|
312
|
+
results['nfl'] = 1/results['efl'] if results['efl'] != 0 else 0
|
|
313
|
+
|
|
314
|
+
# Excited state properties
|
|
315
|
+
if td_obj and any(p in props_to_calc for p in ['exe', 'osc']):
|
|
316
|
+
state_idx = self.state_of_interest - 1
|
|
317
|
+
if 'exe' in props_to_calc:
|
|
318
|
+
results['exe'] = td_obj.e[state_idx] * self.HARTREE_TO_EV
|
|
319
|
+
if 'osc' in props_to_calc:
|
|
320
|
+
results['osc'] = td_obj.oscillator_strength()[state_idx]
|
|
321
|
+
|
|
322
|
+
return results
|
|
323
|
+
|
|
324
|
+
def create_mol2_files(self, tuning, molecule_name, tuning_names):
|
|
325
|
+
"""Create MOL2 files for tuning data"""
|
|
326
|
+
for property_name in tuning_names:
|
|
327
|
+
filename = f"{molecule_name}_{property_name}_tm.mol2"
|
|
328
|
+
property_data = tuning[property_name]
|
|
329
|
+
num_points = len(property_data)
|
|
330
|
+
|
|
331
|
+
with open(filename, 'w') as f:
|
|
332
|
+
# Write header
|
|
333
|
+
f.write("@<TRIPOS>MOLECULE\n")
|
|
334
|
+
f.write(f"{filename}\n")
|
|
335
|
+
f.write(f" {num_points} 0 0 0\n")
|
|
336
|
+
f.write("SMALL\n")
|
|
337
|
+
f.write("GASTEIGER\n")
|
|
338
|
+
f.write("@<TRIPOS>ATOM\n")
|
|
339
|
+
|
|
340
|
+
# Write atom records
|
|
341
|
+
for i, (coord, delta_value) in enumerate(property_data, 1):
|
|
342
|
+
x, y, z = coord[0] # Extract coordinates from (1,3) array
|
|
343
|
+
f.write(f"{i:>4} H {x:>8.4f} {y:>8.4f} {z:>8.4f} H1 1 {property_name.upper()} {delta_value:>10.6f}\n")
|
|
344
|
+
|
|
345
|
+
def prepare_molecule(self):
|
|
346
|
+
"""Prepare the initial molecule structure"""
|
|
347
|
+
# Handle input and create molecule
|
|
348
|
+
if self.input_type == 'smiles':
|
|
349
|
+
self.xyz_file = self.smiles_to_xyz(self.smiles_input)
|
|
350
|
+
self.molecule_name = self.xyz_file.replace('.xyz', '')
|
|
351
|
+
else:
|
|
352
|
+
self.xyz_file = f'{self.molecule}.xyz'
|
|
353
|
+
self.molecule_name = self.molecule
|
|
354
|
+
|
|
355
|
+
self.molecule_object = self.create_molecule_object(
|
|
356
|
+
self.xyz_file, self.basis_set, method=self.method,
|
|
357
|
+
functional=self.functional, charge=self.charge, spin=self.spin
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if self.solvent:
|
|
361
|
+
self.molecule_object = self.solvate_molecule(self.molecule_object, solvent=self.solvent)
|
|
362
|
+
molecule_opt = self.optimize_and_get_equilibrium(self.molecule_object)
|
|
363
|
+
self.molecule_object = self.create_molecule_object(
|
|
364
|
+
molecule_opt, self.basis_set, method=self.method,
|
|
365
|
+
functional=self.functional, charge=self.charge, spin=self.spin
|
|
366
|
+
)
|
|
367
|
+
self.molecule_object = self.solvate_molecule(self.molecule_object, solvent=self.solvent)
|
|
368
|
+
|
|
369
|
+
if self.optimize_geometry and self.solvent is None:
|
|
370
|
+
self.molecule_object = self.create_optimized_molecule(
|
|
371
|
+
self.molecule_object.mol.atom, self.basis_set, method=self.method,
|
|
372
|
+
functional=self.functional, charge=self.charge, spin=self.spin,
|
|
373
|
+
optimize=self.optimize_geometry
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
print(self.molecule_object.mol.atom)
|
|
377
|
+
|
|
378
|
+
# Create XYZ file from molecule_object.mol.atom
|
|
379
|
+
atom_data = self.molecule_object.mol.atom
|
|
380
|
+
self.xyz_file = f"{self.molecule_name}_opt.xyz"
|
|
381
|
+
with open(self.xyz_file, 'w') as f:
|
|
382
|
+
f.write(f"{len(atom_data)}\n")
|
|
383
|
+
f.write(f"\n")
|
|
384
|
+
for element, coords in atom_data:
|
|
385
|
+
f.write(f"{element} {coords[0]:.6f} {coords[1]:.6f} {coords[2]:.6f}\n")
|
|
386
|
+
print(f"Created XYZ file: {self.xyz_file}")
|
|
387
|
+
|
|
388
|
+
def prepare_calculations(self, required_calculations):
|
|
389
|
+
"""Prepare all required calculation objects"""
|
|
390
|
+
self.molecule_object.kernel()
|
|
391
|
+
|
|
392
|
+
# Handle charged species with optional solvation
|
|
393
|
+
if required_calculations.get('anion'):
|
|
394
|
+
self.anion_mf = self.create_charged_molecule_object(
|
|
395
|
+
self.xyz_file, self.basis_set, method=self.method,
|
|
396
|
+
functional=self.functional, original_charge=self.charge,
|
|
397
|
+
original_spin=self.spin, charge_change=-1
|
|
398
|
+
)[0]
|
|
399
|
+
if self.solvent:
|
|
400
|
+
self.anion_mf = self.solvate_molecule(self.anion_mf, solvent=self.solvent)
|
|
401
|
+
self.anion_mf.kernel()
|
|
402
|
+
|
|
403
|
+
if required_calculations.get('cation'):
|
|
404
|
+
self.cation_mf = self.create_charged_molecule_object(
|
|
405
|
+
self.xyz_file, self.basis_set, method=self.method,
|
|
406
|
+
functional=self.functional, original_charge=self.charge,
|
|
407
|
+
original_spin=self.spin, charge_change=+1
|
|
408
|
+
)[0]
|
|
409
|
+
if self.solvent:
|
|
410
|
+
self.cation_mf = self.solvate_molecule(self.cation_mf, solvent=self.solvent)
|
|
411
|
+
self.cation_mf.kernel()
|
|
412
|
+
|
|
413
|
+
# Handle excited state calculations with optional solvation
|
|
414
|
+
if required_calculations.get('td'):
|
|
415
|
+
self.td_object = self.create_td_molecule_object(
|
|
416
|
+
self.molecule_object, triplet=self.triplet_excitation,
|
|
417
|
+
nstates=self.state_of_interest
|
|
418
|
+
)
|
|
419
|
+
if self.solvent:
|
|
420
|
+
self.td_object.with_solvent.equilibrium_solvation = True
|
|
421
|
+
self.td_object.kernel()
|
|
422
|
+
|
|
423
|
+
def calculate_surface_coordinates(self):
|
|
424
|
+
"""Calculate van der Waals surface coordinates"""
|
|
425
|
+
vdw_surface_operation = subprocess.run(
|
|
426
|
+
['vsg', self.xyz_file, '--txt'], capture_output=True, text=True
|
|
427
|
+
)
|
|
428
|
+
self.surface_coords = []
|
|
429
|
+
surface_file = self.xyz_file.replace('.xyz', '_vdw_surface.txt')
|
|
430
|
+
with open(surface_file, 'r') as f:
|
|
431
|
+
for line in f:
|
|
432
|
+
arr = np.fromstring(line.strip(), sep=' ').reshape(1, 3)
|
|
433
|
+
self.surface_coords.append(arr)
|
|
434
|
+
|
|
435
|
+
def calculate_tuning_properties(self, properties_to_calculate):
|
|
436
|
+
"""Calculate tuning properties for all surface coordinates"""
|
|
437
|
+
self.tuning = {name: [] for name in properties_to_calculate}
|
|
438
|
+
|
|
439
|
+
for coord in self.surface_coords:
|
|
440
|
+
# Create perturbed molecules
|
|
441
|
+
molecule_wsc = qmmm.mm_charge(self.molecule_object, coord, np.array([1.0]))
|
|
442
|
+
molecule_wsc.kernel()
|
|
443
|
+
|
|
444
|
+
anion_wsc = qmmm.mm_charge(self.anion_mf, coord, np.array([1.0])) if self.anion_mf else None
|
|
445
|
+
if anion_wsc: anion_wsc.kernel()
|
|
446
|
+
|
|
447
|
+
cation_wsc = qmmm.mm_charge(self.cation_mf, coord, np.array([1.0])) if self.cation_mf else None
|
|
448
|
+
if cation_wsc: cation_wsc.kernel()
|
|
449
|
+
|
|
450
|
+
td_wsc = None
|
|
451
|
+
if self.td_object:
|
|
452
|
+
td_wsc = self.create_td_molecule_object(
|
|
453
|
+
molecule_wsc, triplet=self.triplet_excitation,
|
|
454
|
+
nstates=self.state_of_interest
|
|
455
|
+
)
|
|
456
|
+
if self.solvent:
|
|
457
|
+
td_wsc.with_solvent.equilibrium_solvation = True
|
|
458
|
+
td_wsc.kernel()
|
|
459
|
+
|
|
460
|
+
# Calculate perturbed properties and store deltas
|
|
461
|
+
properties_wsc = self.calculate_all_properties(
|
|
462
|
+
molecule_wsc, anion_wsc, cation_wsc, td_wsc, properties_to_calculate
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
for prop in properties_to_calculate:
|
|
466
|
+
if prop in properties_wsc and prop in self.properties_alone:
|
|
467
|
+
delta = (properties_wsc[prop] - self.properties_alone[prop]) * self.PROPERTY_CONFIG[prop]['unit']
|
|
468
|
+
self.tuning[prop].append([coord, delta])
|
|
469
|
+
|
|
470
|
+
def run_calculation(self, requested_properties=['gse', 'homo', 'lumo', 'gap']):
|
|
471
|
+
"""Main method to run the entire tuning calculation"""
|
|
472
|
+
properties_to_calculate, required_calculations = self.setup_calculation(requested_properties)
|
|
473
|
+
print(f"Properties: {properties_to_calculate}")
|
|
474
|
+
print(f"Calculations: {required_calculations}")
|
|
475
|
+
|
|
476
|
+
# Prepare molecule and calculations
|
|
477
|
+
self.prepare_molecule()
|
|
478
|
+
self.prepare_calculations(required_calculations)
|
|
479
|
+
|
|
480
|
+
# Calculate reference properties
|
|
481
|
+
self.properties_alone = self.calculate_all_properties(
|
|
482
|
+
self.molecule_object, self.anion_mf, self.cation_mf,
|
|
483
|
+
self.td_object, properties_to_calculate
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Calculate surface coordinates and tuning properties
|
|
487
|
+
self.calculate_surface_coordinates()
|
|
488
|
+
self.calculate_tuning_properties(properties_to_calculate)
|
|
489
|
+
|
|
490
|
+
# Create output files
|
|
491
|
+
self.create_mol2_files(self.tuning, self.molecule_name, properties_to_calculate)
|
|
492
|
+
print(f"Created {len(properties_to_calculate)} MOL2 files for {self.molecule_name}")
|
|
493
|
+
|
|
494
|
+
return self.tuning, self.properties_alone
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# Keep constants at module level for backward compatibility
|
|
498
|
+
HARTREE_TO_KCAL = 627.509
|
|
499
|
+
HARTREE_TO_EV = 27.2114
|
|
500
|
+
SHE_REFERENCE_POTENTIAL = 4.43
|
|
501
|
+
|
|
502
|
+
PROPERTY_CONFIG = {
|
|
503
|
+
'gse': {'deps': [], 'calc': [], 'unit': 1},
|
|
504
|
+
'homo': {'deps': [], 'calc': [], 'unit': 1},
|
|
505
|
+
'lumo': {'deps': [], 'calc': [], 'unit': 1},
|
|
506
|
+
'gap': {'deps': ['homo', 'lumo'], 'calc': [], 'unit': 1},
|
|
507
|
+
'dm': {'deps': [], 'calc': [], 'unit': 1},
|
|
508
|
+
'ie': {'deps': [], 'calc': ['cation'], 'unit': HARTREE_TO_KCAL},
|
|
509
|
+
'ea': {'deps': [], 'calc': ['anion'], 'unit': HARTREE_TO_KCAL},
|
|
510
|
+
'cp': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_KCAL},
|
|
511
|
+
'eng': {'deps': ['cp'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
512
|
+
'hard': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
513
|
+
'efl': {'deps': ['cp', 'hard'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
514
|
+
'nfl': {'deps': ['efl'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
515
|
+
'exe': {'deps': [], 'calc': ['td'], 'unit': 1},
|
|
516
|
+
'osc': {'deps': [], 'calc': ['td'], 'unit': 1}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# Keep the original functions for backward compatibility
|
|
521
|
+
def setup_calculation(requested_props):
|
|
522
|
+
"""Setup properties and calculations needed"""
|
|
523
|
+
PROPERTY_CONFIG = {
|
|
524
|
+
'gse': {'deps': [], 'calc': [], 'unit': 1},
|
|
525
|
+
'homo': {'deps': [], 'calc': [], 'unit': 1},
|
|
526
|
+
'lumo': {'deps': [], 'calc': [], 'unit': 1},
|
|
527
|
+
'gap': {'deps': ['homo', 'lumo'], 'calc': [], 'unit': 1},
|
|
528
|
+
'dm': {'deps': [], 'calc': [], 'unit': 1},
|
|
529
|
+
'ie': {'deps': [], 'calc': ['cation'], 'unit': HARTREE_TO_KCAL},
|
|
530
|
+
'ea': {'deps': [], 'calc': ['anion'], 'unit': HARTREE_TO_KCAL},
|
|
531
|
+
'cp': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_KCAL},
|
|
532
|
+
'eng': {'deps': ['cp'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
533
|
+
'hard': {'deps': ['ie', 'ea'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
534
|
+
'efl': {'deps': ['cp', 'hard'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
535
|
+
'nfl': {'deps': ['efl'], 'calc': [], 'unit': HARTREE_TO_EV},
|
|
536
|
+
'exe': {'deps': [], 'calc': ['td'], 'unit': 1},
|
|
537
|
+
'osc': {'deps': [], 'calc': ['td'], 'unit': 1}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if 'all' in requested_props:
|
|
541
|
+
requested_props = list(PROPERTY_CONFIG.keys())
|
|
542
|
+
|
|
543
|
+
# Resolve dependencies
|
|
544
|
+
props_needed = set()
|
|
545
|
+
def add_deps(prop):
|
|
546
|
+
if prop in props_needed: return
|
|
547
|
+
props_needed.add(prop)
|
|
548
|
+
for dep in PROPERTY_CONFIG[prop]['deps']:
|
|
549
|
+
add_deps(dep)
|
|
550
|
+
|
|
551
|
+
for prop in requested_props:
|
|
552
|
+
add_deps(prop)
|
|
553
|
+
|
|
554
|
+
# Determine required calculations
|
|
555
|
+
calcs_needed = {'neutral': True}
|
|
556
|
+
for prop in props_needed:
|
|
557
|
+
for calc in PROPERTY_CONFIG[prop]['calc']:
|
|
558
|
+
calcs_needed[calc] = True
|
|
559
|
+
|
|
560
|
+
return list(props_needed), calcs_needed
|
|
561
|
+
|
|
562
|
+
# Keep other original functions for backward compatibility
|
|
563
|
+
def calculate_all_properties(mf, anion_mf=None, cation_mf=None, td_obj=None, props_to_calc=None):
|
|
564
|
+
results = {}
|
|
565
|
+
|
|
566
|
+
# Basic properties
|
|
567
|
+
if 'gse' in props_to_calc:
|
|
568
|
+
results['gse'] = mf.e_tot * HARTREE_TO_KCAL
|
|
569
|
+
|
|
570
|
+
if any(p in props_to_calc for p in ['homo', 'lumo', 'gap']):
|
|
571
|
+
homo, lumo, gap = [x * HARTREE_TO_EV for x in find_homo_lumo_and_gap(mf)]
|
|
572
|
+
results.update({p: v for p, v in zip(['homo', 'lumo', 'gap'], [homo, lumo, gap]) if p in props_to_calc})
|
|
573
|
+
|
|
574
|
+
if 'dm' in props_to_calc:
|
|
575
|
+
results['dm'] = np.linalg.norm(mf.dip_moment())
|
|
576
|
+
|
|
577
|
+
# Charged state properties
|
|
578
|
+
if 'ie' in props_to_calc and cation_mf:
|
|
579
|
+
results['ie'] = cation_mf.e_tot - mf.e_tot
|
|
580
|
+
if 'ea' in props_to_calc and anion_mf:
|
|
581
|
+
results['ea'] = mf.e_tot - anion_mf.e_tot
|
|
582
|
+
|
|
583
|
+
# Derived properties
|
|
584
|
+
if 'cp' in props_to_calc and all(k in results for k in ['ie', 'ea']):
|
|
585
|
+
results['cp'] = -(results['ie'] + results['ea']) / 2
|
|
586
|
+
if 'eng' in props_to_calc and 'cp' in results:
|
|
587
|
+
results['eng'] = -results['cp']
|
|
588
|
+
if 'hard' in props_to_calc and all(k in results for k in ['ie', 'ea']):
|
|
589
|
+
results['hard'] = (results['ie'] - results['ea']) / 2
|
|
590
|
+
if 'efl' in props_to_calc and all(k in results for k in ['cp', 'hard']):
|
|
591
|
+
results['efl'] = results['cp']**2 / (2 * results['hard']) if results['hard'] != 0 else 0
|
|
592
|
+
if 'nfl' in props_to_calc and 'efl' in results:
|
|
593
|
+
results['nfl'] = 1/results['efl'] if results['efl'] != 0 else 0
|
|
594
|
+
|
|
595
|
+
# Excited state properties
|
|
596
|
+
if td_obj and any(p in props_to_calc for p in ['exe', 'osc']):
|
|
597
|
+
# Use a default state index for backward compatibility
|
|
598
|
+
state_idx = 1 # Default to first excited state
|
|
599
|
+
if 'exe' in props_to_calc:
|
|
600
|
+
results['exe'] = td_obj.e[state_idx] * HARTREE_TO_EV
|
|
601
|
+
if 'osc' in props_to_calc:
|
|
602
|
+
results['osc'] = td_obj.oscillator_strength()[state_idx]
|
|
603
|
+
|
|
604
|
+
return results
|
|
605
|
+
|
|
606
|
+
def create_molecule_object(atom_input, basis_set, method='dft', functional='m06-2x', charge=0, spin=1):
|
|
607
|
+
"""
|
|
608
|
+
Create a PySCF molecule and KS/DFT object from either:
|
|
609
|
+
- atom_input: list of (atom_symbol, (x,y,z)) tuples
|
|
610
|
+
- atom_input: string path to XYZ file
|
|
611
|
+
"""
|
|
612
|
+
spin -= 1
|
|
613
|
+
mol = gto.M(basis=basis_set, charge=charge, spin=spin, symmetry=False)
|
|
614
|
+
if isinstance(atom_input, str) and os.path.isfile(atom_input):
|
|
615
|
+
mol.atom = atom_input
|
|
616
|
+
else:
|
|
617
|
+
mol.atom = atom_input
|
|
618
|
+
mol.build()
|
|
619
|
+
|
|
620
|
+
# Choose method and handle spin
|
|
621
|
+
if method.lower() == 'dft':
|
|
622
|
+
mol.xc = functional
|
|
623
|
+
mf = dft.UKS(mol) if spin > 0 else dft.RKS(mol)
|
|
624
|
+
elif method.lower() == 'hf':
|
|
625
|
+
mf = scf.UHF(mol) if spin > 0 else scf.RHF(mol)
|
|
626
|
+
else:
|
|
627
|
+
raise ValueError("method must be 'dft' or 'hf'")
|
|
628
|
+
return mf
|
|
629
|
+
|
|
630
|
+
def create_charged_molecule_object(atom_input, basis_set, method='dft', functional='m06-2x', original_charge=0, original_spin=1, charge_change=-1):
|
|
631
|
+
"""
|
|
632
|
+
Create charged form of molecule (anion or cation) and return the lowest energy one with its spin and energy
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
charge_change: -1 for anion (add electron), +1 for cation (remove electron)
|
|
636
|
+
"""
|
|
637
|
+
new_charge = original_charge + charge_change
|
|
638
|
+
|
|
639
|
+
# Determine possible spin states based on charge change
|
|
640
|
+
if charge_change == -1:
|
|
641
|
+
if original_spin == 2:
|
|
642
|
+
possible_spins = [1, 3]
|
|
643
|
+
elif original_spin == 1:
|
|
644
|
+
possible_spins = [2]
|
|
645
|
+
elif original_spin == 3:
|
|
646
|
+
possible_spins = [2, 4]
|
|
647
|
+
|
|
648
|
+
elif charge_change == +1:
|
|
649
|
+
if original_spin == 1:
|
|
650
|
+
possible_spins = [2]
|
|
651
|
+
elif original_spin == 2:
|
|
652
|
+
possible_spins = [1, 3]
|
|
653
|
+
elif original_spin == 3:
|
|
654
|
+
possible_spins = [2, 4]
|
|
655
|
+
|
|
656
|
+
energies = []
|
|
657
|
+
objects = []
|
|
658
|
+
|
|
659
|
+
for spin in possible_spins:
|
|
660
|
+
pyscf_spin = spin - 1
|
|
661
|
+
mol = gto.Mole(basis=basis_set, charge=new_charge, spin=pyscf_spin)
|
|
662
|
+
mol.atom = atom_input
|
|
663
|
+
mol.build()
|
|
664
|
+
if method.lower() == 'dft':
|
|
665
|
+
mf = dft.UKS(mol) if pyscf_spin > 0 else dft.RKS(mol)
|
|
666
|
+
mf.xc = functional
|
|
667
|
+
elif method.lower() == 'hf':
|
|
668
|
+
mf = scf.UHF(mol) if pyscf_spin > 0 else scf.RHF(mol)
|
|
669
|
+
energy = mf.kernel()
|
|
670
|
+
energies.append(energy)
|
|
671
|
+
objects.append(mf)
|
|
672
|
+
|
|
673
|
+
# Find the lowest energy
|
|
674
|
+
min_idx = energies.index(min(energies))
|
|
675
|
+
lowest_energy = energies[min_idx]
|
|
676
|
+
optimal_spin = possible_spins[min_idx]
|
|
677
|
+
best_object = objects[min_idx]
|
|
678
|
+
|
|
679
|
+
return best_object, optimal_spin, lowest_energy
|
|
680
|
+
|
|
681
|
+
def create_td_molecule_object(mf, nstates=5, triplet=False):
|
|
682
|
+
# Handle solvated molecules
|
|
683
|
+
if hasattr(mf, 'with_solvent'):
|
|
684
|
+
# For solvated molecules, use tdscf directly
|
|
685
|
+
if hasattr(mf, 'xc'): # DFT case
|
|
686
|
+
td = tdscf.TDDFT(mf)
|
|
687
|
+
else: # HF case
|
|
688
|
+
td = tdscf.TDHF(mf)
|
|
689
|
+
else:
|
|
690
|
+
# For non-solvated molecules, use the original method
|
|
691
|
+
if hasattr(mf, 'TDDFT') and mf.TDDFT is not None:
|
|
692
|
+
td = mf.TDDFT()
|
|
693
|
+
elif hasattr(mf, 'TDHF') and mf.TDHF is not None:
|
|
694
|
+
td = mf.TDHF()
|
|
695
|
+
else:
|
|
696
|
+
raise ValueError("Unsupported ground state object type")
|
|
697
|
+
|
|
698
|
+
td.singlet = not triplet
|
|
699
|
+
td.nstates = nstates
|
|
700
|
+
return td
|
|
701
|
+
|
|
702
|
+
#Manipulation and Extraction
|
|
703
|
+
def solvate_molecule(mf, solvent='water'):
|
|
704
|
+
solvent = solvent.lower()
|
|
705
|
+
mf = mf.PCM()
|
|
706
|
+
mf.with_solvent.eps = smd.solvent_db[solvent][5]
|
|
707
|
+
mf.with_solvent.method = 'C-PCM'
|
|
708
|
+
mf.with_solvent.lebedev_order = 29
|
|
709
|
+
return mf
|
|
710
|
+
|
|
711
|
+
def optimize_and_get_equilibrium(mf):
|
|
712
|
+
"""
|
|
713
|
+
Optimize the geometry of the molecule and return the equilibrium geometry.
|
|
714
|
+
"""
|
|
715
|
+
mol_eq = optimize(mf,conv_tol_grad=1e-7,conv_tol=1e-10)
|
|
716
|
+
coords = mol_eq.atom_coords(unit='Ang')
|
|
717
|
+
atoms = [mol_eq.atom_symbol(i) for i in range(mol_eq.natm)]
|
|
718
|
+
atom_list = [(atom, coord) for atom, coord in zip(atoms, coords)]
|
|
719
|
+
return atom_list
|
|
720
|
+
|
|
721
|
+
def smiles_to_xyz(smiles, filename=None):
|
|
722
|
+
"""Convert SMILES to XYZ file"""
|
|
723
|
+
mol = Chem.AddHs(Chem.MolFromSmiles(smiles))
|
|
724
|
+
AllChem.EmbedMolecule(mol)
|
|
725
|
+
AllChem.MMFFOptimizeMolecule(mol)
|
|
726
|
+
|
|
727
|
+
if not filename:
|
|
728
|
+
filename = f"mol_{abs(hash(smiles)) % 10000}.xyz"
|
|
729
|
+
|
|
730
|
+
conf = mol.GetConformer()
|
|
731
|
+
with open(filename, 'w') as f:
|
|
732
|
+
f.write(f"{mol.GetNumAtoms()}\n{smiles}\n")
|
|
733
|
+
for i, atom in enumerate(mol.GetAtoms()):
|
|
734
|
+
pos = conf.GetAtomPosition(i)
|
|
735
|
+
f.write(f"{atom.GetSymbol()} {pos.x:.6f} {pos.y:.6f} {pos.z:.6f}\n")
|
|
736
|
+
return filename
|
|
737
|
+
|
|
738
|
+
def create_optimized_molecule(atom_input, basis_set, method='dft', functional='m06-2x', charge=0, spin=1, optimize=True):
|
|
739
|
+
"""Create molecule with optional geometry optimization"""
|
|
740
|
+
if optimize:
|
|
741
|
+
# Create initial molecule for optimization
|
|
742
|
+
mf_initial = create_molecule_object(atom_input, basis_set, method, functional, charge, spin)
|
|
743
|
+
mf_initial.kernel()
|
|
744
|
+
|
|
745
|
+
# Optimize geometry using existing function
|
|
746
|
+
opt_coords = optimize_and_get_equilibrium(mf_initial)
|
|
747
|
+
|
|
748
|
+
# Create new molecule with optimized coordinates
|
|
749
|
+
return create_molecule_object(opt_coords, basis_set, method, functional, charge, spin)
|
|
750
|
+
else:
|
|
751
|
+
return create_molecule_object(atom_input, basis_set, method, functional, charge, spin)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def find_homo_lumo_and_gap(mf):
|
|
755
|
+
homo = -float("inf")
|
|
756
|
+
lumo = float("inf")
|
|
757
|
+
for energy, occ in zip(mf.mo_energy, mf.mo_occ):
|
|
758
|
+
if occ > 0 and energy > homo:
|
|
759
|
+
homo = energy
|
|
760
|
+
if occ == 0 and energy < lumo:
|
|
761
|
+
lumo = energy
|
|
762
|
+
return homo, lumo, lumo - homo
|
|
763
|
+
|
|
764
|
+
def get_molecule_gfec(mf):
|
|
765
|
+
mf.kernel()
|
|
766
|
+
hess = mf.Hessian().kernel()
|
|
767
|
+
|
|
768
|
+
vib_data = thermo.harmonic_analysis(
|
|
769
|
+
mf.mol,
|
|
770
|
+
hess,
|
|
771
|
+
imaginary_freq=False
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
thermo_data = thermo.thermo(
|
|
775
|
+
mf,
|
|
776
|
+
vib_data['freq_au'],
|
|
777
|
+
temperature=298.15,
|
|
778
|
+
pressure=101325
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
G_corr = thermo_data['G_tot'][0] - thermo_data['E0'][0]
|
|
782
|
+
return vib_data, thermo_data, G_corr
|
|
783
|
+
|
|
784
|
+
#File Manipulation
|
|
785
|
+
def create_mol2_files(tuning, molecule_name, tuning_names):
|
|
786
|
+
for property_name in tuning_names:
|
|
787
|
+
filename = f"{molecule_name}_{property_name}_tm.mol2"
|
|
788
|
+
property_data = tuning[property_name]
|
|
789
|
+
num_points = len(property_data)
|
|
790
|
+
|
|
791
|
+
with open(filename, 'w') as f:
|
|
792
|
+
# Write header
|
|
793
|
+
f.write("@<TRIPOS>MOLECULE\n")
|
|
794
|
+
f.write(f"{filename}\n")
|
|
795
|
+
f.write(f" {num_points} 0 0 0\n")
|
|
796
|
+
f.write("SMALL\n")
|
|
797
|
+
f.write("GASTEIGER\n")
|
|
798
|
+
f.write("@<TRIPOS>ATOM\n")
|
|
799
|
+
|
|
800
|
+
# Write atom records
|
|
801
|
+
for i, (coord, delta_value) in enumerate(property_data, 1):
|
|
802
|
+
x, y, z = coord[0] # Extract coordinates from (1,3) array
|
|
803
|
+
f.write(f"{i:>4} H {x:>8.4f} {y:>8.4f} {z:>8.4f} H1 1 {property_name.upper()} {delta_value:>10.6f}\n")
|
|
804
|
+
|
|
805
|
+
# Example usage
|
|
806
|
+
if __name__ == "__main__":
|
|
807
|
+
# Initialize calculator
|
|
808
|
+
calculator = TuningCalculator(
|
|
809
|
+
molecule='water',
|
|
810
|
+
method='dft',
|
|
811
|
+
functional='b3lyp',
|
|
812
|
+
basis_set='augccpvdz',
|
|
813
|
+
charge=0,
|
|
814
|
+
spin=1,
|
|
815
|
+
optimize_geometry=True
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Run calculation
|
|
819
|
+
tuning_results, reference_properties = calculator.run_calculation(
|
|
820
|
+
requested_properties=['gse', 'homo', 'lumo', 'gap']
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
print("Reference properties:", reference_properties)
|
|
824
|
+
print(f"Tuning calculation completed with {len(tuning_results)} properties")
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
# Original script execution (for backward compatibility)
|
|
828
|
+
# Uncomment below to use the original function-based approach
|
|
829
|
+
"""
|
|
830
|
+
#Parameters
|
|
831
|
+
molecule = 'water'
|
|
832
|
+
method = 'dft'
|
|
833
|
+
functional = 'b3lyp'
|
|
834
|
+
basis_set = 'augccpvdz'
|
|
835
|
+
charge = 0
|
|
836
|
+
spin = 1
|
|
837
|
+
gfec_functional = 'b3lyp'
|
|
838
|
+
gfec_basis_set = '6-31+G*'
|
|
839
|
+
state_of_interest = 2
|
|
840
|
+
triplet_excitation = False
|
|
841
|
+
solvent = None
|
|
842
|
+
rdx_solvent = 'acetonitrile'
|
|
843
|
+
input_type = 'xyz' # 'xyz' or 'smiles'
|
|
844
|
+
smiles_input = 'O'
|
|
845
|
+
optimize_geometry = True
|
|
846
|
+
|
|
847
|
+
# User input and setup
|
|
848
|
+
requested_properties = ['gse', 'homo', 'lumo', 'gap'] # User specifies
|
|
849
|
+
# requested_properties = ['all'] # Or everything
|
|
850
|
+
|
|
851
|
+
properties_to_calculate, required_calculations = setup_calculation(requested_properties)
|
|
852
|
+
print(f"Properties: {properties_to_calculate}")
|
|
853
|
+
print(f"Calculations: {required_calculations}")
|
|
854
|
+
|
|
855
|
+
# Handle input and create molecule
|
|
856
|
+
if input_type == 'smiles':
|
|
857
|
+
xyz_file = smiles_to_xyz(smiles_input)
|
|
858
|
+
molecule_name = xyz_file.replace('.xyz', '')
|
|
859
|
+
else:
|
|
860
|
+
xyz_file = f'{molecule}.xyz'
|
|
861
|
+
molecule_name = molecule
|
|
862
|
+
|
|
863
|
+
molecule_object = create_molecule_object(xyz_file, basis_set, method=method, functional=functional, charge=charge, spin=spin)
|
|
864
|
+
|
|
865
|
+
if solvent:
|
|
866
|
+
molecule_object = solvate_molecule(molecule_object, solvent=solvent)
|
|
867
|
+
molecule_opt = optimize_and_get_equilibrium(molecule_object)
|
|
868
|
+
molecule_object = create_molecule_object(molecule_opt, basis_set, method=method, functional=functional, charge=charge, spin=spin)
|
|
869
|
+
molecule_object = solvate_molecule(molecule_object, solvent=solvent)
|
|
870
|
+
|
|
871
|
+
if optimize_geometry and solvent is None:
|
|
872
|
+
molecule_object = create_optimized_molecule(molecule_object.mol.atom, basis_set, method=method, functional=functional, charge=charge, spin=spin, optimize=optimize_geometry)
|
|
873
|
+
|
|
874
|
+
print(molecule_object.mol.atom)
|
|
875
|
+
|
|
876
|
+
# Create XYZ file from molecule_object.mol.atom
|
|
877
|
+
atom_data = molecule_object.mol.atom
|
|
878
|
+
xyz_file = f"{molecule_name}_opt.xyz"
|
|
879
|
+
with open(xyz_file, 'w') as f:
|
|
880
|
+
f.write(f"{len(atom_data)}\n")
|
|
881
|
+
f.write(f"\n")
|
|
882
|
+
for element, coords in atom_data:
|
|
883
|
+
f.write(f"{element} {coords[0]:.6f} {coords[1]:.6f} {coords[2]:.6f}\n")
|
|
884
|
+
print(f"Created XYZ file: {xyz_file}")
|
|
885
|
+
|
|
886
|
+
molecule_object.kernel()
|
|
887
|
+
|
|
888
|
+
# Handle charged species with optional solvation
|
|
889
|
+
anion_mf = None
|
|
890
|
+
if required_calculations.get('anion'):
|
|
891
|
+
anion_mf = create_charged_molecule_object(xyz_file, basis_set, method=method, functional=functional, original_charge=charge, original_spin=spin, charge_change=-1)[0]
|
|
892
|
+
if solvent:
|
|
893
|
+
anion_mf = solvate_molecule(anion_mf, solvent=solvent)
|
|
894
|
+
anion_mf.kernel()
|
|
895
|
+
|
|
896
|
+
cation_mf = None
|
|
897
|
+
if required_calculations.get('cation'):
|
|
898
|
+
cation_mf = create_charged_molecule_object(xyz_file, basis_set, method=method, functional=functional, original_charge=charge, original_spin=spin, charge_change=+1)[0]
|
|
899
|
+
if solvent:
|
|
900
|
+
cation_mf = solvate_molecule(cation_mf, solvent=solvent)
|
|
901
|
+
cation_mf.kernel()
|
|
902
|
+
|
|
903
|
+
# Handle excited state calculations with optional solvation
|
|
904
|
+
td_object = None
|
|
905
|
+
if required_calculations.get('td'):
|
|
906
|
+
td_object = create_td_molecule_object(molecule_object, triplet=triplet_excitation, nstates=state_of_interest)
|
|
907
|
+
if solvent:
|
|
908
|
+
td_object.with_solvent.equilibrium_solvation = True
|
|
909
|
+
td_object.kernel()
|
|
910
|
+
|
|
911
|
+
# Calculate reference properties
|
|
912
|
+
properties_alone = calculate_all_properties(molecule_object, anion_mf, cation_mf, td_object, properties_to_calculate)
|
|
913
|
+
|
|
914
|
+
# Surface calculations
|
|
915
|
+
vdw_surface_operation = subprocess.run(['vsg', xyz_file, '--txt'], capture_output=True, text=True)
|
|
916
|
+
surface_coords = []
|
|
917
|
+
surface_file = xyz_file.replace('.xyz', '_vdw_surface.txt')
|
|
918
|
+
with open(surface_file, 'r') as f:
|
|
919
|
+
for line in f:
|
|
920
|
+
arr = np.fromstring(line.strip(), sep=' ').reshape(1, 3)
|
|
921
|
+
surface_coords.append(arr)
|
|
922
|
+
|
|
923
|
+
tuning = {name: [] for name in properties_to_calculate}
|
|
924
|
+
|
|
925
|
+
for coord in surface_coords:
|
|
926
|
+
# Create perturbed molecules
|
|
927
|
+
molecule_wsc = qmmm.mm_charge(molecule_object, coord, np.array([1.0]))
|
|
928
|
+
molecule_wsc.kernel()
|
|
929
|
+
|
|
930
|
+
anion_wsc = qmmm.mm_charge(anion_mf, coord, np.array([1.0])) if anion_mf else None
|
|
931
|
+
if anion_wsc: anion_wsc.kernel()
|
|
932
|
+
|
|
933
|
+
cation_wsc = qmmm.mm_charge(cation_mf, coord, np.array([1.0])) if cation_mf else None
|
|
934
|
+
if cation_wsc: cation_wsc.kernel()
|
|
935
|
+
|
|
936
|
+
td_wsc = None
|
|
937
|
+
if td_object:
|
|
938
|
+
td_wsc = create_td_molecule_object(molecule_wsc, triplet=triplet_excitation, nstates=state_of_interest)
|
|
939
|
+
if solvent:
|
|
940
|
+
td_wsc.with_solvent.equilibrium_solvation = True
|
|
941
|
+
td_wsc.kernel()
|
|
942
|
+
|
|
943
|
+
# Calculate perturbed properties and store deltas
|
|
944
|
+
properties_wsc = calculate_all_properties(molecule_wsc, anion_wsc, cation_wsc, td_wsc, properties_to_calculate)
|
|
945
|
+
|
|
946
|
+
for prop in properties_to_calculate:
|
|
947
|
+
if prop in properties_wsc and prop in properties_alone:
|
|
948
|
+
delta = (properties_wsc[prop] - properties_alone[prop]) * PROPERTY_CONFIG[prop]['unit']
|
|
949
|
+
tuning[prop].append([coord, delta])
|
|
950
|
+
|
|
951
|
+
create_mol2_files(tuning, molecule_name, properties_to_calculate)
|
|
952
|
+
print(f"Created {len(properties_to_calculate)} MOL2 files for {molecule_name}")
|
|
953
|
+
"""
|
|
954
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emsuite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Electrostatic Map Suite - A suite of calculators for quantified electrostatic interactions.
|
|
5
|
+
Author-email: "Stephen O. Ajagbe" <ajagbetobistephen@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: geometric>=1.1
|
|
9
|
+
Requires-Dist: pyscf>=2.10.0
|
|
10
|
+
Requires-Dist: rdkit>=2025.3.5
|
|
11
|
+
Requires-Dist: vdw-surfgen>=0.5.1
|
|
12
|
+
|
|
13
|
+
# EMSuite
|
|
14
|
+
|
|
15
|
+
A suite of calculators for quantified electrostatic interactions.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install emsuite
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from emsuite import TuningCalculator
|
|
27
|
+
|
|
28
|
+
# Initialize calculator
|
|
29
|
+
calc = TuningCalculator(
|
|
30
|
+
molecule="water.xyz",
|
|
31
|
+
basis_set="6-31G*",
|
|
32
|
+
functional="m06-2x"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Run calculations
|
|
36
|
+
calc.run_calculation(['gse', 'homo', 'lumo', 'gap'])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Available Properties
|
|
40
|
+
|
|
41
|
+
**Basic Electronic Properties:**
|
|
42
|
+
- `gse` - Ground state energy
|
|
43
|
+
- `homo` - HOMO energy
|
|
44
|
+
- `lumo` - LUMO energy
|
|
45
|
+
- `gap` - HOMO-LUMO gap
|
|
46
|
+
- `dm` - Dipole moment
|
|
47
|
+
|
|
48
|
+
**Ionization & Electron Affinity:**
|
|
49
|
+
- `ie` - Ionization energy
|
|
50
|
+
- `ea` - Electron affinity
|
|
51
|
+
|
|
52
|
+
**Chemical Descriptors:**
|
|
53
|
+
- `cp` - Chemical potential
|
|
54
|
+
- `eng` - Electronegativity
|
|
55
|
+
- `hard` - Chemical hardness
|
|
56
|
+
- `efl` - Electrophilicity index
|
|
57
|
+
- `nfl` - Nucleophilicity index
|
|
58
|
+
|
|
59
|
+
**Excited States (TD-DFT):**
|
|
60
|
+
- `exe` - Excitation energies
|
|
61
|
+
- `osc` - Oscillator strengths
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- Electronic structure calculations (DFT, HF)
|
|
67
|
+
- Molecular property analysis (HOMO/LUMO, ionization energy, electron affinity)
|
|
68
|
+
- Solvent effects and geometry optimization
|
|
69
|
+
- TD-DFT excited state calculations
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Python 3.8+
|
|
74
|
+
- PySCF
|
|
75
|
+
- RDKit
|
|
76
|
+
- NumPy
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
emsuite
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
|
|
3
|
+
name = "emsuite"
|
|
4
|
+
version = "0.1.0"
|
|
5
|
+
description = "Electrostatic Map Suite - A suite of calculators for quantified electrostatic interactions."
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Stephen O. Ajagbe", email = "ajagbetobistephen@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"geometric>=1.1",
|
|
13
|
+
"pyscf>=2.10.0",
|
|
14
|
+
"rdkit>=2025.3.5",
|
|
15
|
+
"vdw-surfgen>=0.5.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
packages = ["emsuite"]
|
emsuite-0.1.0/setup.cfg
ADDED