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
//! Alchemical free energy calculations for LogP estimation.
//!
//! # Overview
//!
//! LogP (the octanol-solvent partition coefficient) can be estimated from alchemical
//! free energy simulations using thermodynamic integration (TI):
//!
//! 1. Run **N separate MD simulations** at different λ values (e.g., 0.0, 0.05, …, 1.0),
//! **in both solvent and octanol**. λ = 0 means the solute interacts normally with the
//! solvent; λ = 1 means it is fully decoupled (non-interacting ghost).
//!
//! 2. At each simulation frame, record **∂H/∂λ** — the derivative of the Hamiltonian
//! with respect to λ. For linear coupling this equals minus the solute–solvent
//! interaction energy, which is already tracked in `MdState::potential_energy_between_mols`.
//! This is stored per frame in [`Snapshot::dh_dl`].
//!
//! 3. Average ⟨∂H/∂λ⟩ over each λ window's trajectory into a [`LambdaWindow`].
//!
//! 4. Compute **ΔG = ∫₀¹ ⟨∂H/∂λ⟩_λ dλ** via [`free_energy_ti`].
//!
//! 5. Repeat for octanol and compute **LogP** via [`log_p`].
//!
//! # Force scaling requirement
//!
//! For physically correct intermediate-λ sampling, the non-bonded forces on the
//! alchemical molecule must be scaled by `(1 − λ)` in `apply_nonbonded_forces()`.
//! Without this, each window samples the fully-coupled (λ = 0) ensemble and TI
//! reduces to a single endpoint calculation. The infrastructure here is ready;
//! add the force scaling in `non_bonded.rs` using `MdState::alch_mol_idx` and
//! `MdState::lambda` to complete the implementation.
//!
//! # Soft-core potentials
//!
//! Near λ = 0 or 1, the simple linear LJ coupling diverges when two atoms overlap.
//! Production simulations should replace linear LJ scaling with a soft-core potential
//! such as Beutler et al. (1994):
//!
//! ```text
//! U_sc(r, λ) = 4·ε·λ · [ 1/(α(1−λ)² + (r/σ)⁶)² − 1/(α(1−λ)² + (r/σ)⁶) ]
//! ```
//!
//! The electrostatic coupling can remain linear; switch it off before LJ to avoid
//! charge–charge singularities.
use crateSnapshot;
const GAS_CONST_R_KCAL: f64 = 0.001_987_204_1; // kcal / (mol · K)
/// Data collected from one MD run at a single fixed λ value.
///
/// Build this from a trajectory slice via [`collect_window`].
/// Build a [`LambdaWindow`] from a slice of snapshots taken at one fixed λ value.
///
/// All snapshots must come from the same λ window. Uses the `dh_dl` field that
/// is recorded every step by `MdState::take_snapshot`.
///
/// # Panics
/// Panics if `snapshots` is empty.
/// Compute ΔG via **Thermodynamic Integration** (TI) using the trapezoidal rule.
///
/// ΔG = ∫₀¹ ⟨∂H/∂λ⟩_λ dλ ≈ Σᵢ ½(⟨∂H/∂λ⟩ᵢ + ⟨∂H/∂λ⟩ᵢ₊₁) · Δλᵢ
///
/// `windows` must be sorted by `lambda` in ascending order and span [0, 1]
/// (or whatever range was simulated — partial ranges give partial ΔG).
///
/// Returns ΔG in **kcal/mol**. A positive value means decoupling costs energy
/// (solute prefers the solvent); a negative value means it is favourable to remove.
/// Compute **LogP** from free energies in solvent and octanol.
///
/// Both `dg_water` and `dg_octanol` should be the decoupling free energies
/// (ΔG for turning off solute–solvent interactions), in kcal/mol, obtained from
/// [`free_energy_ti`] run in each solvent.
///
/// ```text
/// LogP = (ΔG_water − ΔG_octanol) / (2.303 · R · T)
/// ```
///
/// `temperature_k` is the simulation temperature in Kelvin (typically 298.15 K).
///
/// A positive LogP means the solute prefers octanol (lipophilic).