kbkit 1.0.0__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.
Files changed (45) hide show
  1. kbkit/__init__.py +5 -0
  2. kbkit/_version.py +1 -0
  3. kbkit/analysis/__init__.py +7 -0
  4. kbkit/analysis/kb_integrator.py +283 -0
  5. kbkit/analysis/kb_thermo.py +876 -0
  6. kbkit/analysis/system_state.py +381 -0
  7. kbkit/calculators/__init__.py +6 -0
  8. kbkit/calculators/kbi_calculator.py +268 -0
  9. kbkit/calculators/static_structure_calculator.py +415 -0
  10. kbkit/config/__init__.py +6 -0
  11. kbkit/config/mplstyle.py +18 -0
  12. kbkit/config/presentation.mplstyle +48 -0
  13. kbkit/config/unit_registry.py +82 -0
  14. kbkit/core/__init__.py +7 -0
  15. kbkit/core/kb_pipeline.py +196 -0
  16. kbkit/core/system_loader.py +352 -0
  17. kbkit/core/system_properties.py +222 -0
  18. kbkit/core/system_registry.py +136 -0
  19. kbkit/data/__init__.py +5 -0
  20. kbkit/data/gmx_units.json +12 -0
  21. kbkit/data/property_resolver.py +101 -0
  22. kbkit/parsers/__init__.py +8 -0
  23. kbkit/parsers/edr_file.py +265 -0
  24. kbkit/parsers/gro_file.py +88 -0
  25. kbkit/parsers/rdf_file.py +259 -0
  26. kbkit/parsers/top_file.py +126 -0
  27. kbkit/schema/kbi_metadata.py +49 -0
  28. kbkit/schema/plot_spec.py +39 -0
  29. kbkit/schema/system_config.py +51 -0
  30. kbkit/schema/system_metadata.py +49 -0
  31. kbkit/schema/thermo_property.py +77 -0
  32. kbkit/schema/thermo_state.py +121 -0
  33. kbkit/utils/__init__.py +5 -0
  34. kbkit/utils/chem.py +53 -0
  35. kbkit/utils/file_resolver.py +123 -0
  36. kbkit/utils/format.py +82 -0
  37. kbkit/utils/io.py +54 -0
  38. kbkit/utils/logging.py +35 -0
  39. kbkit/utils/validation.py +61 -0
  40. kbkit/viz/__init__.py +5 -0
  41. kbkit/viz/plotter.py +653 -0
  42. kbkit-1.0.0.dist-info/METADATA +217 -0
  43. kbkit-1.0.0.dist-info/RECORD +45 -0
  44. kbkit-1.0.0.dist-info/WHEEL +4 -0
  45. kbkit-1.0.0.dist-info/licenses/LICENSE +21 -0
kbkit/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """kbkit package."""
2
+
3
+ from kbkit._version import __version__
4
+
5
+ __all__ = ["__version__"]
kbkit/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,7 @@
1
+ """Scientific computation and transformation."""
2
+
3
+ from kbkit.analysis.kb_integrator import KBIntegrator
4
+ from kbkit.analysis.kb_thermo import KBThermo
5
+ from kbkit.analysis.system_state import SystemState
6
+
7
+ __all__ = ["KBIntegrator", "KBThermo", "SystemState"]
@@ -0,0 +1,283 @@
1
+ """
2
+ Computes Kirkwood-Buff integrals (KBIs) from RDF data and applies thermodynamic limit corrections.
3
+
4
+ Relies on RDF parsing and system composition data to produce corrected KBIs for use in thermodynamic models.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import matplotlib.pyplot as plt
12
+ import numpy as np
13
+ from numpy.typing import NDArray
14
+ from scipy.integrate import cumulative_trapezoid
15
+
16
+ from kbkit.core.system_properties import SystemProperties
17
+ from kbkit.parsers.rdf_file import RDFParser
18
+
19
+
20
+ class KBIntegrator:
21
+ """
22
+ Class to compute the Kirkwood-Buff Integrals (KBI) from RDF data.
23
+
24
+ Parameters
25
+ ----------
26
+ rdf_file : str
27
+ Path to the RDF file containing radial distances and corresponding g(r) values.
28
+ use_fixed_rmin : bool
29
+ Whether to use a fixed minimum distance (rmin) for analysis.
30
+ system_properties : SystemProperties
31
+ SystemProperties object containing information about the system, including topology and box dimensions.
32
+
33
+ Attributes
34
+ ----------
35
+ rdf: RDFParser
36
+ RDFParser object for parsing RDF file.
37
+ system_properties: SystemProperties
38
+ SystemProperties object.
39
+ """
40
+
41
+ def __init__(self, rdf_file: str | Path, use_fixed_rmin: bool, system_properties: SystemProperties) -> None:
42
+ self.rdf = RDFParser(rdf_file=str(rdf_file), use_fixed_rmin=use_fixed_rmin)
43
+ self.system_properties = system_properties
44
+
45
+ def box_vol(self) -> float:
46
+ """Return the volume of the system box in nm^3."""
47
+ vol = self.system_properties.get("volume", units="nm^3")
48
+ if isinstance(vol, tuple):
49
+ vol = vol[0]
50
+ return float(vol)
51
+
52
+ def rdf_molecules(self) -> list[str]:
53
+ """Get the molecules corresponding to the RDF file from the system topology.
54
+
55
+ Returns
56
+ -------
57
+ list
58
+ List of molecule IDs corresponding to the RDF file.
59
+ """
60
+ # extract molecules from file name and topology information
61
+ rdf_mols = RDFParser.extract_mols(Path(self.rdf.rdf_file).name, self.system_properties.topology.molecules)
62
+ # check length of molecules found --- must be two for rdfs
63
+ N_RDF_MOLS = 2
64
+ if len(rdf_mols) != N_RDF_MOLS:
65
+ raise ValueError("Number of molecules corresponding to ID in .top file is not 2!")
66
+ return rdf_mols
67
+
68
+ def kd(self) -> int:
69
+ """Return the Kronecker delta between molecules in RDF, i.e., determines if molecules :math:`i,j` are the same."""
70
+ return int(self.rdf_molecules()[0] == self.rdf_molecules()[1])
71
+
72
+ def n_j(self) -> int:
73
+ """Return the number of molecule :math:`j` in the system."""
74
+ return self.system_properties.topology.molecule_count[self.rdf_molecules()[1]]
75
+
76
+ def g_gv(self) -> NDArray[np.float64]:
77
+ r"""
78
+ Compute the corrected pair distribution function, accounting for finite-size effects in the simulation box, based on the approach by `Ganguly and Van der Vegt (2013) <https://doi.org/10.1021/ct301017q>`_.
79
+
80
+ Returns
81
+ -------
82
+ np.ndarray
83
+ Corrected g(r) values as a numpy array corresponding to distances `r` from the RDF.
84
+
85
+ Notes
86
+ -----
87
+ The correction is calculated as
88
+
89
+ .. math::
90
+ v_r = 1 - \frac{\frac{4}{3} \pi r^3}{V}
91
+
92
+ .. math::
93
+ \rho_j = \frac{N_j}{V}
94
+
95
+ .. math::
96
+ \Delta N_j = \int_0^r 4 \pi r^2 \rho_j \bigl(g(r) - 1 \bigr) \, dr
97
+
98
+ .. math::
99
+ g_{GV}(r) = g(r) \cdot \frac{N_j v_r}{N_j v_r - \Delta N_j - \delta_{ij}}
100
+
101
+
102
+ where:
103
+ - :math:`r` is the distance
104
+ - :math:`V` is the box volume
105
+ - :math:`N_j` is the number of particles of type \( j \)
106
+ - :math:`g(r)` is the raw radial distribution function
107
+ - :math:`\delta_{ij}` is a kronecker delta
108
+
109
+ .. note::
110
+ The cumulative integral :math:`\Delta N_j` is approximated numerically using the trapezoidal rule.
111
+ """
112
+ # calculate the reduced volume
113
+ vr = 1 - ((4 / 3) * np.pi * self.rdf.r**3 / self.box_vol())
114
+
115
+ # get the number density for molecule j
116
+ rho_j = self.n_j() / self.box_vol()
117
+
118
+ # function to integrate over
119
+ f = 4.0 * np.pi * self.rdf.r**2 * rho_j * (self.rdf.g - 1)
120
+ Delta_Nj = cumulative_trapezoid(f, x=self.rdf.r, dx=self.rdf.r[1] - self.rdf.r[0])
121
+ Delta_Nj = np.append(Delta_Nj, Delta_Nj[-1])
122
+
123
+ # correct g(r) with GV correction
124
+ g_gv = self.rdf.g * self.n_j() * vr / (self.n_j() * vr - Delta_Nj - self.kd())
125
+ return np.asarray(g_gv) # make sure that an array is returned
126
+
127
+ def window(self) -> NDArray[np.float64]:
128
+ r"""
129
+ Apply cubic correction (or window weight) to the radial distribution function, which is useful for ensuring that the integral converges properly at larger distances, based on the method described by `Krüger et al. (2013) <https://doi.org/10.1021/jz301992u>`_.
130
+
131
+ Returns
132
+ -------
133
+ np.ndarray
134
+ Windowed weight for the RDF
135
+
136
+ Notes
137
+ -----
138
+ The windowed weight is defined as:
139
+
140
+ .. math::
141
+ w(r) = 4 \pi r^2 \left(1 - \left(\frac{r}{r_{max}}\right)^3\right)
142
+
143
+ where:
144
+ - :math:`r` is the radial distance
145
+ - :math:`r_{max}` is the maximum radial distance in the RDF
146
+ """
147
+ w = 4 * np.pi * self.rdf.r**2 * (1 - (self.rdf.r / self.rdf.rmax) ** 3)
148
+ return np.asarray(w)
149
+
150
+ def h(self) -> NDArray[np.float64]:
151
+ r"""
152
+ Calculate correlation function h(r) from the corrected g(r) values.
153
+
154
+ Returns
155
+ -------
156
+ np.ndarray
157
+ Correlation function h(r) as a numpy array.
158
+
159
+ Notes
160
+ -----
161
+ The correlation function is defined as:
162
+
163
+ .. math::
164
+ h(r) = g_{GV}(r) - 1
165
+
166
+ """
167
+ return self.g_gv() - 1
168
+
169
+ def rkbi(self) -> NDArray[np.float64]:
170
+ r"""
171
+ Compute KBI as a function of radial distance between molecules :math:`i` and :math:`j`.
172
+
173
+ Returns
174
+ -------
175
+ np.ndarray
176
+ KBI values as a numpy array corresponding to distances :math:`r` from the RDF.
177
+
178
+ Notes
179
+ -----
180
+ The KBI is computed using the formula:
181
+
182
+ .. math::
183
+ G_{ij}(r) = \int_0^r h(r) w(r) dr
184
+
185
+ where:
186
+ - :math:`h(r)` is the correlation function
187
+ - :math:`w(r)` is the window function
188
+ - :math:`r` is the radial distance
189
+
190
+ .. note::
191
+ The integration is performed using the trapezoidal rule.
192
+ """
193
+ rkbi_arr = cumulative_trapezoid(self.window() * self.h(), self.rdf.r, initial=0)
194
+ return np.asarray(rkbi_arr)
195
+
196
+ def lambda_ratio(self) -> NDArray[np.float64]:
197
+ r"""
198
+ Calculate length ratio (:math:`\lambda`) of the system based on the radial distances and the box volume.
199
+
200
+ Returns
201
+ -------
202
+ np.ndarray
203
+ Length ratio as a numpy array corresponding to distances :math:`r` from the RDF.
204
+
205
+ Notes
206
+ -----
207
+ The length ratio is defined as:
208
+
209
+ .. math::
210
+ \lambda = \left(\frac{\frac{4}{3} \pi r^3}{V}\right)^{1/3}
211
+
212
+ where:
213
+ - :math:`r` is the radial distance
214
+ - :math:`V` is the box volume
215
+ """
216
+ Vr = (4 / 3) * np.pi * self.rdf.r**3 / self.box_vol()
217
+ return Vr ** (1 / 3)
218
+
219
+ def fit_kbi_inf(self) -> NDArray[np.float64]:
220
+ r"""
221
+ Fit a linear model to the product of the length ratio and the KBI values for extrapolation to thermodynamic limit.
222
+
223
+ Returns
224
+ -------
225
+ tuple
226
+ Tuple containing the slope and intercept of the linear fit, which represents the KBI at infinite distance.
227
+
228
+
229
+ .. note::
230
+ The KBI at infinite distance is estimated by fitting a linear model to the product of the length ratio and the KBI values, using only the radial distances that are within the specified range (rmin to rmax).
231
+ """
232
+ # get x and y values to fit thermodynamic correction
233
+ lam = self.lambda_ratio() # characteristic length
234
+ lam_kbi = lam * self.rkbi() # length x KBI (r)
235
+
236
+ # fit linear regression to masked values
237
+ fit_params = np.polyfit(lam[self.rdf.r_mask], lam_kbi[self.rdf.r_mask], 1)
238
+ return fit_params # return fit
239
+
240
+ def integrate(self) -> float:
241
+ """
242
+ Compute KBI in thermodynamic limit.
243
+
244
+ Returns
245
+ -------
246
+ float
247
+ KBI in the thermodynamic limit, which is the slope of the linear fit to the product
248
+ of the length ratio and the KBI values.
249
+ """
250
+ return float(self.fit_kbi_inf()[0])
251
+
252
+ def plot(self, save_dir: Optional[str] = None) -> None:
253
+ """Plot RDF and the running KBI fit to thermodynamic limit.
254
+
255
+ Parameters
256
+ ----------
257
+ save_dir : str, optional
258
+ Directory to save the plot. If not provided, the plot will be displayed but not saved
259
+ """
260
+ # get running kbi
261
+ rkbi = self.rkbi()
262
+ # parameters for thermo-limit extrapolation
263
+ lam = self.lambda_ratio()
264
+ lam_rkbi = lam * rkbi
265
+ # fits to thermo limit
266
+ fit_params = self.fit_kbi_inf()
267
+ lam_fit = lam[self.rdf.r_mask]
268
+ lam_rkbi_fit = np.polyval(fit_params, lam_fit)
269
+
270
+ fig, ax = plt.subplots(1, 2, figsize=(9, 4))
271
+ ax[0].plot(self.rdf.r, rkbi)
272
+ ax[0].set_xlabel("r / nm")
273
+ ax[0].set_ylabel("G$_{ij}$ / nm$^3$")
274
+ ax[1].plot(lam, lam_rkbi)
275
+ ax[1].plot(lam_fit, lam_rkbi_fit, ls="--", c="k", label=f"KBI: {fit_params[0]:.2g} nm$^3$")
276
+ ax[1].set_xlabel(r"$\lambda$")
277
+ ax[1].set_ylabel(r"$\lambda$ G$_{ij}$ / nm$^3$")
278
+ fig.suptitle(
279
+ f"KBI Analysis for system: {os.path.basename(self.system_properties.system_path)} {'-'.join(self.rdf_molecules())}"
280
+ )
281
+ if save_dir is not None:
282
+ plt.savefig(os.path.join(save_dir, self.rdf.rdf_file[:-4] + ".png"))
283
+ plt.show()