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 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
@@ -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,8 @@
1
+ """
2
+ Electrostatic Map Suite (emsuite) - A suite of calculators for quantified electrostatic interactions.
3
+ """
4
+
5
+ from .tuning import TuningCalculator
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = ["TuningCalculator"]
@@ -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,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ emsuite/__init__.py
4
+ emsuite/tuning.py
5
+ emsuite.egg-info/PKG-INFO
6
+ emsuite.egg-info/SOURCES.txt
7
+ emsuite.egg-info/dependency_links.txt
8
+ emsuite.egg-info/requires.txt
9
+ emsuite.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ geometric>=1.1
2
+ pyscf>=2.10.0
3
+ rdkit>=2025.3.5
4
+ vdw-surfgen>=0.5.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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+