-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxtb.py
More file actions
492 lines (392 loc) · 16.4 KB
/
xtb.py
File metadata and controls
492 lines (392 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
import subprocess
import json
from pathlib import Path
import shutil
import socket
import os
from ase import Atoms
from ase.io import read, write
class XtbInput:
"""
Class to generate XTB input files from a configuration dictionary.
Attributes:
VALID_TYPES (dict): Valid XTB calculation types.
VALID_METHODS (dict): Valid XTB methods.
config (dict): Configuration dictionary.
Methods:
__init__(config):
Initialize the XTB_input object with a configuration dictionary.
_validate_config():
Validate the configuration dictionary.
_generate_command():
Generate the main XTB command line options based on the configuration.
_generate_blocks():
Generate the input blocks for geometry constraints and relaxed scans.
generate_input(work_dir, molecule=None):
Generate the complete XTB input file and command line options.
write_input(filename, work_dir, molecule=None):
Write the XTB input and coordinates to a file, and extend the command line options.
"""
# Valid XTB calculation types
VALID_TYPES = {
"sp": "--scc", # Single point
"opt": "--opt", # Geometry optimization
"grad": "--grad", # Energy gradient
"freq": "--hess", # Frequency calculation
"optfreq": "--ohess", # Optimization + Frequencies
"md": "--md", # Molecular dynamics
"metadyn": "--metadyn", # Metadynamics
}
# Valid XTB methods
VALID_METHODS = {
"gfn1": "--gfn 1",
"gfn2": "--gfn 2",
"gfnff": "--gfnff",
}
def __init__(self, config):
"""
Initialize the XTB_input object with a configuration dictionary.
Args:
config (dict): A dictionary containing configuration parameters.
"""
self.config = config
self._validate_config()
def _validate_config(self):
"""
Validate the configuration dictionary.
Raises:
ValueError: If any required key is missing or contains an invalid value.
"""
required_keys = ["type"]
for key in required_keys:
if key not in self.config:
raise ValueError(f"Missing required configuration key: {key}")
# Validate calculation type
if self.config["type"].lower() not in self.VALID_TYPES:
raise ValueError(f"Invalid calculation type: {self.config['type']}. Valid types are: {', '.join(self.VALID_TYPES.keys())}")
# Validate method if specified
if "method" in self.config and self.config["method"].lower() not in self.VALID_METHODS:
raise ValueError(f"Invalid method: {self.config['method']}. Valid methods are: {', '.join(self.VALID_METHODS.keys())}")
def _generate_command(self):
"""
Generate the main XTB command line options.
Returns:
str: The generated XTB command line options as a single string.
"""
parts = [""]
# Add calculation type
calc_type = self.VALID_TYPES[self.config["type"].lower()]
if calc_type:
parts.append(calc_type)
# Add base name if specified
if "base" in self.config:
parts.append(f"--namespace {self.config['base']}")
# Add method if specified
if "method" in self.config:
parts.append(self.VALID_METHODS[self.config["method"].lower()])
# Add charge if specified
if "charge" in self.config:
parts.append(f"--chrg {self.config['charge']}")
# Add Spin (multiplicity - 1) if specified
if "multiplicity" in self.config:
spin = int(self.config["multiplicity"]) - 1
parts.append(f"--uhf {spin}")
# Add solvent if specified
if "solvent" in self.config:
parts.append(f"--alpb {self.config['solvent']}")
# Add number of processors if specified
if "nprocs" in self.config:
parts.append(f"--parallel {self.config['nprocs']}")
# Add other keywords if specified
if "keywords" in self.config:
parts.append(self.config["keywords"])
return " ".join(parts)
def _generate_blocks(self):
"""
Generate the input blocks for geometry constraints and relaxed scans.
Returns:
str: A string containing the concatenated blocks of settings.
"""
blocks = []
# Geometry constraints block
if "constraints" in self.config:
blocks.append(f"$constrain")
if "force constant" in self.config:
blocks.append(f" force constant={self.config['force constant']}")
for constraint in self.config["constraints"].split(';'):
blocks.append(f" {constraint}")
blocks.append("$end")
# Relaxed scan block
if "scan" in self.config:
blocks.append(f"$scan")
for scan in self.config["scan"].split(';'):
blocks.append(f" {scan}")
blocks.append("$end")
return "\n".join(filter(bool, blocks)) # filter out empty strings
def generate_input(self):
"""
Generate the complete XTB input file and command line options.
Returns:
tuple: A tuple containing the command line options and input content.
"""
# Generate the main command line options
command = self._generate_command()
# Generate the input blocks, if necessary
if "constraints" in self.config or "scan" in self.config:
input_content = self._generate_blocks()
else:
input_content = None
return command, input_content
def write_input(self, filename, work_dir, molecule=None):
"""
Write the XTB input and coordinates to a file, and extend the command line options.
Args:
filename (str): The name of the file to write the input to.
work_dir (Path): The working directory for the calculation.
molecule (ase.Atoms, optional): An ASE Atoms object representing the molecule.
Returns:
str: The complete command line options including the input and coordinates files.
"""
if not molecule:
molecule = Atoms("H", positions=[[0, 0, 0]])
print("Warning: No molecule provided. Using default hydrogen atom.")
# Generate input content and command
command, input_content = self.generate_input()
# Write the input file
if input_content:
with open(filename, 'w') as f:
f.write(input_content)
command += f" --input {filename}"
# Write the coordinates to a separate file
coords_file = os.path.join(work_dir, f"{self.config['base']}_input.xyz")
write(coords_file, molecule, format='xyz')
# Add coordinates file to the command
command += f" -- {coords_file}"
return command
class XtbOutput:
"""
Class to handle XTB output file parsing.
Methods:
parse_xtb_output(content: str) -> dict:
Parse XTB output file SUMMARY into a structured dictionary.
"""
def parse_xtb_output(self, content):
"""
Parse XTB output file SUMMARY sections into a structured dictionary.
Args:
content (str): The content of the XTB output file as a string.
Returns:
dict: A dictionary containing parsed data from the XTB output file.
"""
result = {"Properties": []}
lines = content.split('\n')
summary_found = False
dipole_found = False
# Process lines
for line in lines:
line = line.strip()
# Start of property section
if 'SUMMARY' in line:
summary = {}
summary_found = True
continue
# End of property section
if '..........' in line and summary_found:
result["Properties"].append(summary)
summary_found = False
# Parse properties
if summary_found:
# Remove leading/trailing ':' and split by whitespace
clean_line = line.replace(':', '').strip()
parts = clean_line.split()
if len(parts) >= 2:
# Join all parts except the last two (value and unit) as the key
key = ' '.join(parts[:-2]).strip()
value = float(parts[-2]) # Convert value to float
summary[key] = value
# Start of dipole section
if 'molecular dipole' in line:
dipole_found = True
continue
# End of dipole section
if 'molecular quadrupole' in line and dipole_found:
result["Properties"][-1]["Dipole"] = dipole
dipole_found = False
# Parse dipole
if dipole_found and 'full:' in line:
parts = line.strip().split()
dipole = [float(x) for x in parts[1:4]]
return result
class Xtb:
"""
Class to manage XTB calculations: input generation, execution, and output parsing.
Attributes:
config (dict): Dictionary containing calculation parameters.
work_dir (Path): Working directory for the calculation.
xtb_cmd (str): Path to XTB executable.
base_name (str): Base name for input, output, and property files.
input_file (Path): Path to the XTB input file.
output_file (Path): Path to the XTB output file.
results (dict): Parsed results from the XTB calculation.
Constants:
PATTERNS_TO_KEEP (list): List of file patterns to keep after cleaning.
Methods:
prepare_input(molecule=None):
Generate XTB input file, if necessary.
run():
Execute XTB calculation and wait for it to complete.
check_status():
Check if the calculation has completed and was successful.
parse_output():
Parse XTB output file.
clean_up(keep_main_files=True):
Clean up calculation files.
get_molecule():
Return the last molecule from XTB output as an ASE Atoms object.
"""
# Constant for file patterns to keep
PATTERNS_TO_KEEP = ["*.inp",
"*.out",
"*.xyz",
"*.coord",
"*.log"]
def __init__(self, config, xtb_cmd=None, work_dir=None):
"""
Initialize the XTB class with configuration, command path, and working directory.
Args:
config (dict): Configuration dictionary containing necessary parameters.
xtb_cmd (str, optional): Path to the XTB executable. Defaults to the system path.
work_dir (str or Path, optional): Path to the working directory. Defaults to the current working directory.
"""
self.config = config
self.work_dir = Path.cwd().resolve() if work_dir is None else Path(work_dir).resolve()
self.xtb_cmd = shutil.which("xtb") if xtb_cmd is None else xtb_cmd
# Create working directory if it doesn't exist
self.work_dir.mkdir(parents=True, exist_ok=True)
# Initialize file paths using base name from config
self.base_name = self.config['base'] if 'base' in self.config else "orca"
self.input_file = None
self.output_file = None
self.results = None
def prepare_input(self, molecule=None):
"""
Generate XTB input file, if necessary.
Args:
molecule (ase.Atoms, optional): The molecular structure to be used in the XTB calculation. If not provided, a default structure will be used.
"""
self.input_file = self.work_dir / f"{self.base_name}.inp"
self.output_file = self.work_dir / f"{self.base_name}.out"
# Generate input file if necessary
generator = XtbInput(self.config)
self.cmd_options = generator.write_input(self.input_file, self.work_dir, molecule=molecule)
def run(self):
"""
Execute XTB calculation and wait for it to complete.
Raises:
ValueError: If the input file is not prepared.
subprocess.CalledProcessError: If the XTB calculation fails.
Exception: If there is an error running XTB.
"""
if not self.input_file:
raise ValueError("Input file not prepared. Call prepare_input() first.")
if "verbose" in self.config and self.config["verbose"]:
print(f"xtb running in {self.work_dir} on {socket.gethostname()}")
# Clean up temporary files
self.clean_up()
# Prepare command
cmd = f"{self.xtb_cmd} {self.cmd_options} > {self.output_file}"
try:
# Run and wait for completion
self.cmd_result = subprocess.run(
cmd,
cwd=self.work_dir,
shell=True,
capture_output=True,
check=True,
text=True,
errors='ignore'
)
except subprocess.CalledProcessError as e:
print(f"XTB calculation failed with error:\n{e.stderr}")
raise
except Exception as e:
print(f"Error running XTB: {str(e)}")
raise
# Parse output
self.results = self.parse_output()
# Add configuration to results
self.results["config"] = self.config
# Clean up temporary files
self.clean_up()
def check_status(self):
"""
Check if the calculation has completed and was successful.
Returns:
bool: True if the output file exists, False otherwise.
"""
if not self.output_file.exists():
return False
else:
return True
def parse_output(self):
"""
Parse XTB output file.
Returns:
dict: A dictionary containing parsed results from the XTB calculation.
"""
if not self.check_status():
raise RuntimeError("Calculation not complete or failed.")
parser = XtbOutput()
# Parse output file if it exists
if self.output_file.exists():
with open(self.output_file, 'r') as f:
self.results = parser.parse_xtb_output(f.read())
else:
print("Warning: Output file not found.")
self.results = None
return self.results
def clean_up(self):
"""
Clean up calculation files.
"""
for file in self.work_dir.iterdir():
if not any(file.match(pattern) for pattern in self.PATTERNS_TO_KEEP):
file.unlink()
def get_molecule(self):
"""
Return the last molecule from XTB output as an ASE Atoms object.
Returns:
ase.Atoms: The last geometry of the calculation as an ASE Atoms object.
Raises:
ValueError: If no results are available.
FileNotFoundError: If the geometry file is not found.
"""
if not self.results:
raise ValueError("No results available. Run calculation first.")
# Construct path to last geometry
mol_path = os.path.join(self.work_dir, f"{self.base_name}.xtbopt.xyz")
if not os.path.exists(mol_path):
raise FileNotFoundError(f"Geometry file not found: {mol_path}")
# Read last geometry from file
mol = read(mol_path, format="xyz")
return mol
# Example usage
if __name__ == "__main__":
# Example configuration
config = {
"base": "xtb",
"type": "opt",
"method": "gfn2",
"nprocs": "2",
"constraints": "angle: 2, 1, 3, 120.0",
"force constant": 1.5,
}
# Read molecule from xyz file
mol = read("./test/water.xyz", format='xyz')
# Create XTB manager
xtb = Xtb(config, work_dir="./test/xtb")
# Prepare input and run calculation
xtb.prepare_input(molecule=mol)
xtb.run()
# Print results
print(json.dumps(xtb.results, indent=2))