quantui 0.5.1__py3-none-any.whl
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.
- quantui/__init__.py +311 -0
- quantui/analytics.py +609 -0
- quantui/app.py +5650 -0
- quantui/app_analysis.py +662 -0
- quantui/app_builders.py +2465 -0
- quantui/app_exports.py +194 -0
- quantui/app_formatters.py +493 -0
- quantui/app_history.py +624 -0
- quantui/app_runflow.py +1544 -0
- quantui/app_visualization.py +2620 -0
- quantui/ase_bridge.py +236 -0
- quantui/benchmarks.py +1543 -0
- quantui/c_stderr.py +124 -0
- quantui/cactus.py +88 -0
- quantui/calc_log.py +1116 -0
- quantui/calculator.py +204 -0
- quantui/cancellation.py +88 -0
- quantui/cli.py +288 -0
- quantui/comparison.py +306 -0
- quantui/config.py +725 -0
- quantui/data/js/3Dmol-min.js +2 -0
- quantui/data/js/3Dmol-min.js.LICENSE.txt +5 -0
- quantui/data/library/library.sqlite +0 -0
- quantui/data/manifests/bulk_qm9.json +1 -0
- quantui/data/manifests/curated.json +15482 -0
- quantui/data/manifests/presets.json +816 -0
- quantui/descriptor_cards.py +186 -0
- quantui/freq_calc.py +712 -0
- quantui/freq_ir_workers.py +229 -0
- quantui/gpu_offload.py +278 -0
- quantui/help_content.py +474 -0
- quantui/ir_plot.py +130 -0
- quantui/issue_tracker.py +170 -0
- quantui/live_log.py +387 -0
- quantui/log_utils.py +492 -0
- quantui/molecule.py +577 -0
- quantui/molecule_library.py +433 -0
- quantui/nmr_calc.py +437 -0
- quantui/optimizer.py +670 -0
- quantui/orbital_visualization.py +1102 -0
- quantui/pes_scan.py +420 -0
- quantui/preopt.py +355 -0
- quantui/progress.py +111 -0
- quantui/pubchem.py +1157 -0
- quantui/reorganization_energy.py +435 -0
- quantui/results_storage.py +902 -0
- quantui/security.py +14 -0
- quantui/session_calc.py +622 -0
- quantui/structure_providers.py +277 -0
- quantui/tddft_calc.py +307 -0
- quantui/user_settings.py +238 -0
- quantui/utils.py +287 -0
- quantui/vib_cache.py +247 -0
- quantui/visualization_py3dmol.py +593 -0
- quantui/viz_assets.py +101 -0
- quantui/viz_backend_router.py +243 -0
- quantui-0.5.1.dist-info/METADATA +533 -0
- quantui-0.5.1.dist-info/RECORD +62 -0
- quantui-0.5.1.dist-info/WHEEL +5 -0
- quantui-0.5.1.dist-info/entry_points.txt +2 -0
- quantui-0.5.1.dist-info/licenses/LICENSE +21 -0
- quantui-0.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuantUI Reorganization Energy Module
|
|
3
|
+
|
|
4
|
+
Computes the internal (inner-sphere) reorganization energy ``λ`` from Marcus
|
|
5
|
+
theory using the standard **4-point scheme**.
|
|
6
|
+
|
|
7
|
+
For a charge-transfer event between a neutral molecule and its ion, the
|
|
8
|
+
reorganization energy is the energy penalty for relaxing each charge state
|
|
9
|
+
from the *other* state's equilibrium geometry to its own:
|
|
10
|
+
|
|
11
|
+
λ = λ₁ + λ₂
|
|
12
|
+
λ₁ = E_ion(R_neutral) − E_ion(R_ion) # ion relaxation
|
|
13
|
+
λ₂ = E_neutral(R_ion) − E_neutral(R_neutral) # neutral relaxation
|
|
14
|
+
|
|
15
|
+
where ``R_x`` is the fully relaxed (optimized) geometry of charge state ``x``.
|
|
16
|
+
The "4 points" are the four single-point energies that appear above:
|
|
17
|
+
|
|
18
|
+
E_neutral(R_neutral), E_ion(R_ion) — the two optimized minima
|
|
19
|
+
E_ion(R_neutral), E_neutral(R_ion) — the two cross evaluations
|
|
20
|
+
|
|
21
|
+
Two channels are supported:
|
|
22
|
+
|
|
23
|
+
* **hole** (charge +1) — relevant for hole/p-type charge transport,
|
|
24
|
+
* **electron** (charge −1) — relevant for electron/n-type transport.
|
|
25
|
+
|
|
26
|
+
Running ``mode="both"`` computes both channels while sharing the single
|
|
27
|
+
neutral geometry optimization, so it costs three optimizations rather than
|
|
28
|
+
four.
|
|
29
|
+
|
|
30
|
+
The heavy lifting (SCF + gradients) is delegated to the same code paths as
|
|
31
|
+
the rest of QuantUI: :func:`quantui.optimizer.optimize_geometry` for the
|
|
32
|
+
relaxations and :func:`quantui.session_calc.run_in_session` for the
|
|
33
|
+
single-point cross evaluations.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import sys
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import IO, List, Optional
|
|
41
|
+
|
|
42
|
+
from .molecule import Molecule
|
|
43
|
+
from .optimizer import DEFAULT_FMAX, DEFAULT_OPT_STEPS, optimize_geometry
|
|
44
|
+
from .session_calc import HARTREE_TO_EV, run_in_session
|
|
45
|
+
|
|
46
|
+
# 1 Hartree in kcal/mol (CODATA-consistent with HARTREE_TO_EV).
|
|
47
|
+
HARTREE_TO_KCAL: float = 627.509474
|
|
48
|
+
|
|
49
|
+
VALID_MODES = ("hole", "electron", "both")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# Result dataclasses
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ReorgChannelResult:
|
|
59
|
+
"""Reorganization energy for a single charge-transfer channel.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
kind: ``"hole"`` (cation, +1) or ``"electron"`` (anion, −1).
|
|
63
|
+
ion_charge: Total charge of the ion for this channel.
|
|
64
|
+
ion_multiplicity: Spin multiplicity used for the ion.
|
|
65
|
+
e_neutral_at_neutral: E_neutral(R_neutral) in Hartree (shared point).
|
|
66
|
+
e_ion_at_ion: E_ion(R_ion) in Hartree.
|
|
67
|
+
e_ion_at_neutral: E_ion(R_neutral) in Hartree.
|
|
68
|
+
e_neutral_at_ion: E_neutral(R_ion) in Hartree.
|
|
69
|
+
lambda1_hartree: Ion relaxation energy λ₁ (Ha).
|
|
70
|
+
lambda2_hartree: Neutral relaxation energy λ₂ (Ha).
|
|
71
|
+
lambda_hartree: Total reorganization energy λ = λ₁ + λ₂ (Ha).
|
|
72
|
+
converged: True if every SCF/opt feeding this channel converged.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
kind: str
|
|
76
|
+
ion_charge: int
|
|
77
|
+
ion_multiplicity: int
|
|
78
|
+
e_neutral_at_neutral: float
|
|
79
|
+
e_ion_at_ion: float
|
|
80
|
+
e_ion_at_neutral: float
|
|
81
|
+
e_neutral_at_ion: float
|
|
82
|
+
lambda1_hartree: float
|
|
83
|
+
lambda2_hartree: float
|
|
84
|
+
lambda_hartree: float
|
|
85
|
+
converged: bool
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def lambda_ev(self) -> float:
|
|
89
|
+
"""Total reorganization energy in electronvolts."""
|
|
90
|
+
return self.lambda_hartree * HARTREE_TO_EV
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def lambda_mev(self) -> float:
|
|
94
|
+
"""Total reorganization energy in millielectronvolts."""
|
|
95
|
+
return self.lambda_ev * 1000.0
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def lambda_kcal(self) -> float:
|
|
99
|
+
"""Total reorganization energy in kcal/mol."""
|
|
100
|
+
return self.lambda_hartree * HARTREE_TO_KCAL
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def label(self) -> str:
|
|
104
|
+
"""Human-readable channel label."""
|
|
105
|
+
return "Hole (cation)" if self.kind == "hole" else "Electron (anion)"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class ReorganizationEnergyResult:
|
|
110
|
+
"""Structured output from a 4-point reorganization energy calculation.
|
|
111
|
+
|
|
112
|
+
Exposes ``formula``/``method``/``basis``/``energy_hartree``/``converged``
|
|
113
|
+
so it can be persisted by :func:`quantui.results_storage.save_result`
|
|
114
|
+
like every other result type. ``energy_hartree`` reports the optimized
|
|
115
|
+
neutral SCF energy (the physical reference for the run).
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
formula: str
|
|
119
|
+
method: str
|
|
120
|
+
basis: str
|
|
121
|
+
mode: str
|
|
122
|
+
molecule: Molecule # optimized neutral geometry (used for 3D display)
|
|
123
|
+
neutral_charge: int
|
|
124
|
+
neutral_multiplicity: int
|
|
125
|
+
neutral_energy_hartree: float
|
|
126
|
+
channels: List[ReorgChannelResult] = field(default_factory=list)
|
|
127
|
+
converged: bool = True
|
|
128
|
+
n_total_opt_steps: int = 0
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def energy_hartree(self) -> float:
|
|
132
|
+
"""Optimized neutral SCF energy (Ha) — the run's reference energy."""
|
|
133
|
+
return self.neutral_energy_hartree
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def energy_ev(self) -> float:
|
|
137
|
+
"""Optimized neutral SCF energy in electronvolts."""
|
|
138
|
+
return self.neutral_energy_hartree * HARTREE_TO_EV
|
|
139
|
+
|
|
140
|
+
def channel(self, kind: str) -> Optional[ReorgChannelResult]:
|
|
141
|
+
"""Return the channel result for ``"hole"``/``"electron"`` or None."""
|
|
142
|
+
for ch in self.channels:
|
|
143
|
+
if ch.kind == kind:
|
|
144
|
+
return ch
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def to_spectra(self) -> dict:
|
|
148
|
+
"""Serialisable payload stored under result.json ``spectra`` key."""
|
|
149
|
+
return {
|
|
150
|
+
"reorganization_energy": {
|
|
151
|
+
"mode": self.mode,
|
|
152
|
+
"neutral_charge": self.neutral_charge,
|
|
153
|
+
"neutral_multiplicity": self.neutral_multiplicity,
|
|
154
|
+
"neutral_energy_hartree": self.neutral_energy_hartree,
|
|
155
|
+
"n_total_opt_steps": self.n_total_opt_steps,
|
|
156
|
+
"channels": [
|
|
157
|
+
{
|
|
158
|
+
"kind": ch.kind,
|
|
159
|
+
"ion_charge": ch.ion_charge,
|
|
160
|
+
"ion_multiplicity": ch.ion_multiplicity,
|
|
161
|
+
"e_neutral_at_neutral": ch.e_neutral_at_neutral,
|
|
162
|
+
"e_ion_at_ion": ch.e_ion_at_ion,
|
|
163
|
+
"e_ion_at_neutral": ch.e_ion_at_neutral,
|
|
164
|
+
"e_neutral_at_ion": ch.e_neutral_at_ion,
|
|
165
|
+
"lambda1_hartree": ch.lambda1_hartree,
|
|
166
|
+
"lambda2_hartree": ch.lambda2_hartree,
|
|
167
|
+
"lambda_hartree": ch.lambda_hartree,
|
|
168
|
+
"lambda_ev": ch.lambda_ev,
|
|
169
|
+
"lambda_kcal": ch.lambda_kcal,
|
|
170
|
+
"converged": ch.converged,
|
|
171
|
+
}
|
|
172
|
+
for ch in self.channels
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def summary(self) -> str:
|
|
178
|
+
"""Return a multi-line human-readable result summary."""
|
|
179
|
+
lines = [
|
|
180
|
+
"=" * 60,
|
|
181
|
+
"Reorganization Energy (Marcus 4-point)",
|
|
182
|
+
"=" * 60,
|
|
183
|
+
f" Molecule : {self.formula}",
|
|
184
|
+
f" Method/Basis : {self.method}/{self.basis}",
|
|
185
|
+
f" Neutral state : charge {self.neutral_charge:+d}, "
|
|
186
|
+
f"mult {self.neutral_multiplicity}",
|
|
187
|
+
f" All converged : {'Yes' if self.converged else 'NO'}",
|
|
188
|
+
f" Total opt steps: {self.n_total_opt_steps}",
|
|
189
|
+
"-" * 60,
|
|
190
|
+
]
|
|
191
|
+
for ch in self.channels:
|
|
192
|
+
lines.append(
|
|
193
|
+
f" {ch.label:<18}: λ = {ch.lambda_ev:.4f} eV "
|
|
194
|
+
f"({ch.lambda_kcal:.2f} kcal/mol)"
|
|
195
|
+
)
|
|
196
|
+
lines.append(
|
|
197
|
+
f" λ₁ (ion relax) = {ch.lambda1_hartree * HARTREE_TO_EV:.4f} eV,"
|
|
198
|
+
f" λ₂ (neutral relax) = {ch.lambda2_hartree * HARTREE_TO_EV:.4f} eV"
|
|
199
|
+
)
|
|
200
|
+
lines.append("=" * 60)
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ============================================================================
|
|
205
|
+
# Helpers
|
|
206
|
+
# ============================================================================
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _promote_method(method: str, multiplicity: int) -> str:
|
|
210
|
+
"""Return a method suitable for the given spin state.
|
|
211
|
+
|
|
212
|
+
PySCF's restricted RHF cannot treat an open-shell ion, and the QuantUI
|
|
213
|
+
optimizer only special-cases ``RHF``/``UHF`` for Hartree-Fock (DFT is
|
|
214
|
+
auto-restricted/unrestricted from the spin). So promote a closed-shell
|
|
215
|
+
HF request to UHF whenever the species is open-shell.
|
|
216
|
+
"""
|
|
217
|
+
if multiplicity > 1 and method.upper() in ("RHF", "HF"):
|
|
218
|
+
return "UHF"
|
|
219
|
+
return method
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _ion_multiplicity(molecule: Molecule, ion_charge: int) -> int:
|
|
223
|
+
"""Low-spin multiplicity for an ion at ``ion_charge``.
|
|
224
|
+
|
|
225
|
+
Removing/adding one electron flips the electron-count parity, so the
|
|
226
|
+
ground-state multiplicity is 1 (even electrons) or 2 (odd electrons).
|
|
227
|
+
This picks the minimal valid multiplicity; users wanting a high-spin ion
|
|
228
|
+
can build the calculation manually.
|
|
229
|
+
"""
|
|
230
|
+
n_electrons = molecule.get_electron_count() - (ion_charge - molecule.charge)
|
|
231
|
+
return 1 if n_electrons % 2 == 0 else 2
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _emit(stream: IO[str], message: str) -> None:
|
|
235
|
+
try:
|
|
236
|
+
stream.write(message)
|
|
237
|
+
except Exception: # noqa: BLE001 — logging must never kill the run
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ============================================================================
|
|
242
|
+
# Main entry point
|
|
243
|
+
# ============================================================================
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_reorganization_energy(
|
|
247
|
+
molecule: Molecule,
|
|
248
|
+
mode: str = "both",
|
|
249
|
+
method: str = "B3LYP",
|
|
250
|
+
basis: str = "6-31G*",
|
|
251
|
+
fmax: float = DEFAULT_FMAX,
|
|
252
|
+
steps: int = DEFAULT_OPT_STEPS,
|
|
253
|
+
progress_stream: Optional[IO[str]] = None,
|
|
254
|
+
solvent: Optional[str] = None,
|
|
255
|
+
) -> ReorganizationEnergyResult:
|
|
256
|
+
"""Compute the 4-point Marcus reorganization energy for a molecule.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
molecule: The **neutral** (reference) molecule. Its charge and
|
|
260
|
+
multiplicity define the reference state; ions are derived from it.
|
|
261
|
+
mode: ``"hole"``, ``"electron"``, or ``"both"``.
|
|
262
|
+
method: SCF method / DFT functional (e.g. ``"B3LYP"``). Automatically
|
|
263
|
+
promoted to UHF for open-shell HF cases.
|
|
264
|
+
basis: Basis set name recognised by PySCF.
|
|
265
|
+
fmax: Force convergence threshold (eV/Å) for the optimizations.
|
|
266
|
+
steps: Maximum optimizer steps per optimization.
|
|
267
|
+
progress_stream: Writable stream for live log output (Jupyter widget
|
|
268
|
+
stream in the app, ``sys.stdout`` otherwise).
|
|
269
|
+
solvent: Optional PCM solvent name for the single-point evaluations.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
:class:`ReorganizationEnergyResult`.
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: If ``mode`` is not one of :data:`VALID_MODES`.
|
|
276
|
+
RuntimeError: If a required single-point evaluation fails to converge.
|
|
277
|
+
"""
|
|
278
|
+
mode = (mode or "both").lower()
|
|
279
|
+
if mode not in VALID_MODES:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
f"Invalid mode '{mode}'. Choose one of {', '.join(VALID_MODES)}."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
stream: IO[str] = progress_stream if progress_stream is not None else sys.stdout
|
|
285
|
+
base_charge = molecule.charge
|
|
286
|
+
base_mult = molecule.multiplicity
|
|
287
|
+
neutral_method = _promote_method(method, base_mult)
|
|
288
|
+
|
|
289
|
+
def _single_point(mol: Molecule, mth: str, tag: str) -> float:
|
|
290
|
+
"""Run a single point and return its energy, asserting convergence."""
|
|
291
|
+
_emit(stream, f"\n── Single point: {tag} ──────────────────────────\n")
|
|
292
|
+
res = run_in_session(
|
|
293
|
+
molecule=mol,
|
|
294
|
+
method=mth,
|
|
295
|
+
basis=basis,
|
|
296
|
+
progress_stream=stream, # type: ignore[arg-type]
|
|
297
|
+
solvent=solvent,
|
|
298
|
+
)
|
|
299
|
+
if not bool(getattr(res, "converged", False)):
|
|
300
|
+
raise RuntimeError(f"Single point did not converge: {tag}")
|
|
301
|
+
return float(res.energy_hartree)
|
|
302
|
+
|
|
303
|
+
_emit(
|
|
304
|
+
stream,
|
|
305
|
+
"\n"
|
|
306
|
+
+ "═" * 62
|
|
307
|
+
+ f"\n Reorganization energy (4-point) — mode: {mode}\n"
|
|
308
|
+
+ f" {molecule.get_formula()} @ {method}/{basis}\n"
|
|
309
|
+
+ "═" * 62
|
|
310
|
+
+ "\n",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# ── Step 1: optimize the neutral reference geometry ──────────────────────
|
|
314
|
+
_emit(stream, "\n── Optimizing neutral geometry (R_neutral) ──────────\n")
|
|
315
|
+
neutral_opt = optimize_geometry(
|
|
316
|
+
molecule=molecule,
|
|
317
|
+
method=neutral_method,
|
|
318
|
+
basis=basis,
|
|
319
|
+
fmax=fmax,
|
|
320
|
+
steps=steps,
|
|
321
|
+
progress_stream=stream, # type: ignore[arg-type]
|
|
322
|
+
status_label="Reorg: optimizing neutral geometry",
|
|
323
|
+
report_fraction=False, # Don't let sub-opt 0→1 resets oscillate ETA
|
|
324
|
+
)
|
|
325
|
+
neutral_mol = neutral_opt.molecule
|
|
326
|
+
n_total_steps = neutral_opt.n_steps
|
|
327
|
+
all_converged = bool(neutral_opt.converged)
|
|
328
|
+
|
|
329
|
+
# E_neutral(R_neutral) — shared "point 1" across both channels.
|
|
330
|
+
e_neutral_at_neutral = _single_point(
|
|
331
|
+
neutral_mol, neutral_method, "E_neutral(R_neutral)"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# ── Step 2: per-channel ion optimization + cross single points ───────────
|
|
335
|
+
targets: List[tuple[str, int]] = []
|
|
336
|
+
if mode in ("hole", "both"):
|
|
337
|
+
targets.append(("hole", base_charge + 1))
|
|
338
|
+
if mode in ("electron", "both"):
|
|
339
|
+
targets.append(("electron", base_charge - 1))
|
|
340
|
+
|
|
341
|
+
channels: List[ReorgChannelResult] = []
|
|
342
|
+
for kind, ion_charge in targets:
|
|
343
|
+
ion_mult = _ion_multiplicity(molecule, ion_charge)
|
|
344
|
+
ion_method = _promote_method(method, ion_mult)
|
|
345
|
+
_emit(
|
|
346
|
+
stream,
|
|
347
|
+
f"\n── {kind.capitalize()} channel: optimizing ion geometry "
|
|
348
|
+
f"(charge {ion_charge:+d}, mult {ion_mult}) ──\n",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Seed the ion optimization from the relaxed neutral geometry — it is
|
|
352
|
+
# closer to the ion minimum than the raw input geometry.
|
|
353
|
+
ion_seed = Molecule(
|
|
354
|
+
atoms=list(molecule.atoms),
|
|
355
|
+
coordinates=[list(c) for c in neutral_mol.coordinates],
|
|
356
|
+
charge=ion_charge,
|
|
357
|
+
multiplicity=ion_mult,
|
|
358
|
+
)
|
|
359
|
+
ion_opt = optimize_geometry(
|
|
360
|
+
molecule=ion_seed,
|
|
361
|
+
method=ion_method,
|
|
362
|
+
basis=basis,
|
|
363
|
+
fmax=fmax,
|
|
364
|
+
steps=steps,
|
|
365
|
+
progress_stream=stream, # type: ignore[arg-type]
|
|
366
|
+
status_label=f"Reorg: optimizing {kind} ion geometry",
|
|
367
|
+
report_fraction=False, # See neutral-opt note above
|
|
368
|
+
)
|
|
369
|
+
ion_mol = ion_opt.molecule
|
|
370
|
+
n_total_steps += ion_opt.n_steps
|
|
371
|
+
all_converged = all_converged and bool(ion_opt.converged)
|
|
372
|
+
|
|
373
|
+
# The four energies (two already share R_neutral / R_ion optimizations).
|
|
374
|
+
e_ion_at_ion = _single_point(ion_mol, ion_method, f"E_{kind}(R_{kind})")
|
|
375
|
+
e_ion_at_neutral = _single_point(
|
|
376
|
+
Molecule(
|
|
377
|
+
atoms=list(molecule.atoms),
|
|
378
|
+
coordinates=[list(c) for c in neutral_mol.coordinates],
|
|
379
|
+
charge=ion_charge,
|
|
380
|
+
multiplicity=ion_mult,
|
|
381
|
+
),
|
|
382
|
+
ion_method,
|
|
383
|
+
f"E_{kind}(R_neutral)",
|
|
384
|
+
)
|
|
385
|
+
e_neutral_at_ion = _single_point(
|
|
386
|
+
Molecule(
|
|
387
|
+
atoms=list(molecule.atoms),
|
|
388
|
+
coordinates=[list(c) for c in ion_mol.coordinates],
|
|
389
|
+
charge=base_charge,
|
|
390
|
+
multiplicity=base_mult,
|
|
391
|
+
),
|
|
392
|
+
neutral_method,
|
|
393
|
+
f"E_neutral(R_{kind})",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
lambda1 = e_ion_at_neutral - e_ion_at_ion # ion relaxation
|
|
397
|
+
lambda2 = e_neutral_at_ion - e_neutral_at_neutral # neutral relaxation
|
|
398
|
+
lambda_total = lambda1 + lambda2
|
|
399
|
+
|
|
400
|
+
channels.append(
|
|
401
|
+
ReorgChannelResult(
|
|
402
|
+
kind=kind,
|
|
403
|
+
ion_charge=ion_charge,
|
|
404
|
+
ion_multiplicity=ion_mult,
|
|
405
|
+
e_neutral_at_neutral=e_neutral_at_neutral,
|
|
406
|
+
e_ion_at_ion=e_ion_at_ion,
|
|
407
|
+
e_ion_at_neutral=e_ion_at_neutral,
|
|
408
|
+
e_neutral_at_ion=e_neutral_at_ion,
|
|
409
|
+
lambda1_hartree=lambda1,
|
|
410
|
+
lambda2_hartree=lambda2,
|
|
411
|
+
lambda_hartree=lambda_total,
|
|
412
|
+
converged=bool(ion_opt.converged),
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
_emit(
|
|
416
|
+
stream,
|
|
417
|
+
f"\n → λ_{kind} = {lambda_total * HARTREE_TO_EV:.4f} eV "
|
|
418
|
+
f"({lambda_total * HARTREE_TO_KCAL:.2f} kcal/mol)\n",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
result = ReorganizationEnergyResult(
|
|
422
|
+
formula=molecule.get_formula(),
|
|
423
|
+
method=method,
|
|
424
|
+
basis=basis,
|
|
425
|
+
mode=mode,
|
|
426
|
+
molecule=neutral_mol,
|
|
427
|
+
neutral_charge=base_charge,
|
|
428
|
+
neutral_multiplicity=base_mult,
|
|
429
|
+
neutral_energy_hartree=e_neutral_at_neutral,
|
|
430
|
+
channels=channels,
|
|
431
|
+
converged=all_converged,
|
|
432
|
+
n_total_opt_steps=n_total_steps,
|
|
433
|
+
)
|
|
434
|
+
_emit(stream, "\n" + result.summary() + "\n")
|
|
435
|
+
return result
|