cuRDF 0.5.0__tar.gz → 0.5.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuRDF
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: GPU-accelerated radial distribution functions with NVIDIA ALCHEMI Toolkit-Ops neighbor lists and PyTorch.
5
5
  Author: Joseph Hart
6
6
  License-Expression: MIT
@@ -45,7 +45,9 @@ Requires-Dist: sphinx-rtd-theme; extra == "docs"
45
45
  Requires-Dist: myst-parser; extra == "docs"
46
46
  Dynamic: license-file
47
47
 
48
- # cuRDF
48
+ ![cuRDF logo](logo.svg)
49
+
50
+ ---
49
51
 
50
52
  [![DOI](https://zenodo.org/badge/1085332119.svg)](https://doi.org/10.5281/zenodo.1085332119) [![PyPI](https://img.shields.io/pypi/v/cuRDF.svg)](https://pypi.org/project/cuRDF/)
51
53
 
@@ -56,7 +58,8 @@ CUDA-accelerated radial distribution functions using [NVIDIA ALCHEMI Toolkit-Ops
56
58
 
57
59
  ![cuRDF benchmark](benchmarks/results/results.png)
58
60
 
59
- cuRDF is benchmarked against RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
61
+ cuRDF is benchmarked against other RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
62
+ Benchmarks use random positions in cubic cells sized to maintain a fixed number density (orthorhombic boxes, periodic in all directions).
60
63
 
61
64
  ## Install
62
65
  Latest release:
@@ -114,9 +117,27 @@ bins, gr = rdf(
114
117
 
115
118
  If the topology lacks atom names (only numeric types), supply a mapping:
116
119
  ```python
117
- bins, gr = rdf(u, species_a="C", species_b="O", atom_types_map={1: "C", 2: "H"})
120
+ bins, gr = rdf(
121
+ u,
122
+ species_a="C",
123
+ species_b="O",
124
+ atom_types_map={1: "C", 2: "H"}
125
+ )
118
126
  ```
119
127
 
128
+ ## Validation
129
+
130
+ RDFs for liquid water (64 atoms, 1 ns) computed using a trajectory from [Lim et al. (2026)](https://arxiv.org/abs/2601.20134) match reference curves for all pairs:
131
+
132
+ <table>
133
+ <tr>
134
+ <td><img src="tests/cuRDF_results/rdf_OO_comparison.svg" alt="O–O RDF" width="250"></td>
135
+ <td><img src="tests/cuRDF_results/rdf_HH_comparison.svg" alt="H–H RDF" width="250"></td>
136
+ <td><img src="tests/cuRDF_results/rdf_OH_comparison.svg" alt="O–H RDF" width="250"></td>
137
+ </tr>
138
+ </table>
139
+
140
+
120
141
  ## Citation
121
142
  If you use cuRDF in your work, please cite:
122
143
  ```
@@ -126,7 +147,7 @@ If you use cuRDF in your work, please cite:
126
147
  month = dec,
127
148
  year = 2025,
128
149
  publisher = {Zenodo},
129
- version = {0.1.0},
150
+ version = {0.5.0},
130
151
  doi = {10.5281/zenodo.1085332119},
131
152
  url = {https://doi.org/10.5281/zenodo.1085332119}
132
153
  }
@@ -1,4 +1,6 @@
1
- # cuRDF
1
+ ![cuRDF logo](logo.svg)
2
+
3
+ ---
2
4
 
3
5
  [![DOI](https://zenodo.org/badge/1085332119.svg)](https://doi.org/10.5281/zenodo.1085332119) [![PyPI](https://img.shields.io/pypi/v/cuRDF.svg)](https://pypi.org/project/cuRDF/)
4
6
 
@@ -9,7 +11,8 @@ CUDA-accelerated radial distribution functions using [NVIDIA ALCHEMI Toolkit-Ops
9
11
 
10
12
  ![cuRDF benchmark](benchmarks/results/results.png)
11
13
 
12
- cuRDF is benchmarked against RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
14
+ cuRDF is benchmarked against other RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
15
+ Benchmarks use random positions in cubic cells sized to maintain a fixed number density (orthorhombic boxes, periodic in all directions).
13
16
 
14
17
  ## Install
15
18
  Latest release:
@@ -67,9 +70,27 @@ bins, gr = rdf(
67
70
 
68
71
  If the topology lacks atom names (only numeric types), supply a mapping:
69
72
  ```python
70
- bins, gr = rdf(u, species_a="C", species_b="O", atom_types_map={1: "C", 2: "H"})
73
+ bins, gr = rdf(
74
+ u,
75
+ species_a="C",
76
+ species_b="O",
77
+ atom_types_map={1: "C", 2: "H"}
78
+ )
71
79
  ```
72
80
 
81
+ ## Validation
82
+
83
+ RDFs for liquid water (64 atoms, 1 ns) computed using a trajectory from [Lim et al. (2026)](https://arxiv.org/abs/2601.20134) match reference curves for all pairs:
84
+
85
+ <table>
86
+ <tr>
87
+ <td><img src="tests/cuRDF_results/rdf_OO_comparison.svg" alt="O–O RDF" width="250"></td>
88
+ <td><img src="tests/cuRDF_results/rdf_HH_comparison.svg" alt="H–H RDF" width="250"></td>
89
+ <td><img src="tests/cuRDF_results/rdf_OH_comparison.svg" alt="O–H RDF" width="250"></td>
90
+ </tr>
91
+ </table>
92
+
93
+
73
94
  ## Citation
74
95
  If you use cuRDF in your work, please cite:
75
96
  ```
@@ -79,7 +100,7 @@ If you use cuRDF in your work, please cite:
79
100
  month = dec,
80
101
  year = 2025,
81
102
  publisher = {Zenodo},
82
- version = {0.1.0},
103
+ version = {0.5.0},
83
104
  doi = {10.5281/zenodo.1085332119},
84
105
  url = {https://doi.org/10.5281/zenodo.1085332119}
85
106
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuRDF
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: GPU-accelerated radial distribution functions with NVIDIA ALCHEMI Toolkit-Ops neighbor lists and PyTorch.
5
5
  Author: Joseph Hart
6
6
  License-Expression: MIT
@@ -45,7 +45,9 @@ Requires-Dist: sphinx-rtd-theme; extra == "docs"
45
45
  Requires-Dist: myst-parser; extra == "docs"
46
46
  Dynamic: license-file
47
47
 
48
- # cuRDF
48
+ ![cuRDF logo](logo.svg)
49
+
50
+ ---
49
51
 
50
52
  [![DOI](https://zenodo.org/badge/1085332119.svg)](https://doi.org/10.5281/zenodo.1085332119) [![PyPI](https://img.shields.io/pypi/v/cuRDF.svg)](https://pypi.org/project/cuRDF/)
51
53
 
@@ -56,7 +58,8 @@ CUDA-accelerated radial distribution functions using [NVIDIA ALCHEMI Toolkit-Ops
56
58
 
57
59
  ![cuRDF benchmark](benchmarks/results/results.png)
58
60
 
59
- cuRDF is benchmarked against RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
61
+ cuRDF is benchmarked against other RDF (MDAnalysis) and neighbour list implementations on CPU (AMD Ryzen 9 9950X, 32 threads) and GPU (NVIDIA RTX 5090) for systems of varying sizes at a density of 0.05 atoms/ų over 1000 frames.
62
+ Benchmarks use random positions in cubic cells sized to maintain a fixed number density (orthorhombic boxes, periodic in all directions).
60
63
 
61
64
  ## Install
62
65
  Latest release:
@@ -114,9 +117,27 @@ bins, gr = rdf(
114
117
 
115
118
  If the topology lacks atom names (only numeric types), supply a mapping:
116
119
  ```python
117
- bins, gr = rdf(u, species_a="C", species_b="O", atom_types_map={1: "C", 2: "H"})
120
+ bins, gr = rdf(
121
+ u,
122
+ species_a="C",
123
+ species_b="O",
124
+ atom_types_map={1: "C", 2: "H"}
125
+ )
118
126
  ```
119
127
 
128
+ ## Validation
129
+
130
+ RDFs for liquid water (64 atoms, 1 ns) computed using a trajectory from [Lim et al. (2026)](https://arxiv.org/abs/2601.20134) match reference curves for all pairs:
131
+
132
+ <table>
133
+ <tr>
134
+ <td><img src="tests/cuRDF_results/rdf_OO_comparison.svg" alt="O–O RDF" width="250"></td>
135
+ <td><img src="tests/cuRDF_results/rdf_HH_comparison.svg" alt="H–H RDF" width="250"></td>
136
+ <td><img src="tests/cuRDF_results/rdf_OH_comparison.svg" alt="O–H RDF" width="250"></td>
137
+ </tr>
138
+ </table>
139
+
140
+
120
141
  ## Citation
121
142
  If you use cuRDF in your work, please cite:
122
143
  ```
@@ -126,7 +147,7 @@ If you use cuRDF in your work, please cite:
126
147
  month = dec,
127
148
  year = 2025,
128
149
  publisher = {Zenodo},
129
- version = {0.1.0},
150
+ version = {0.5.0},
130
151
  doi = {10.5281/zenodo.1085332119},
131
152
  url = {https://doi.org/10.5281/zenodo.1085332119}
132
153
  }
@@ -14,4 +14,5 @@ curdf/plotting.py
14
14
  curdf/rdf.py
15
15
  tests/test_rdf_core.py
16
16
  tests/test_rdf_cuda.py
17
- tests/test_rdf_sources.py
17
+ tests/test_rdf_sources.py
18
+ tests/test_water_rdfs.py
@@ -50,9 +50,14 @@ def rdf_from_mdanalysis(
50
50
  selection_b: str | None = None,
51
51
  atom_types_map: dict | None = None,
52
52
  index=None,
53
+ start: int | None = None,
54
+ stop: int | None = None,
55
+ step: int | None = None,
53
56
  r_min: float = 1.0,
54
57
  r_max: float = 6.0,
55
58
  nbins: int = 100,
59
+ r_min_floor: float = 1e-6,
60
+ pbc: tuple[bool, bool, bool] = (True, True, True),
56
61
  device="cuda",
57
62
  torch_dtype=None,
58
63
  half_fill: bool = True,
@@ -77,12 +82,22 @@ def rdf_from_mdanalysis(
77
82
  Optional mapping for numeric atom types to element names (e.g., ``{1: "C", 2: "H"}``).
78
83
  index
79
84
  Optional trajectory index/selector.
85
+ start
86
+ Optional starting frame (inclusive) when iterating the trajectory.
87
+ stop
88
+ Optional stopping frame (exclusive) when iterating the trajectory.
89
+ step
90
+ Optional frame stride when iterating the trajectory.
80
91
  r_min
81
92
  Minimum distance included in the histogram.
82
93
  r_max
83
94
  Maximum distance included in the histogram.
84
95
  nbins
85
96
  Number of histogram bins.
97
+ r_min_floor
98
+ Small lower bound to exclude pathological self/near-zero distances when ``r_min`` is zero.
99
+ pbc
100
+ Tuple of periodic boundary flags for (a, b, c) directions.
86
101
  device
87
102
  Torch device string or object used for computation.
88
103
  torch_dtype
@@ -149,21 +164,26 @@ def rdf_from_mdanalysis(
149
164
  raise ValueError(f"No atoms found for species_a='{species_a}' (check atom names or --atom-types mapping)")
150
165
  if len(ag_b) == 0:
151
166
  raise ValueError(f"No atoms found for species_b='{species_b or species_a}' (check atom names or --atom-types mapping)")
152
- if wrap_positions and mda_wrap is not None:
167
+ if wrap_positions and mda_wrap is not None and all(pbc):
153
168
  ag_wrap = ag_a if selection_b is None else (ag_a | ag_b)
154
169
  universe.trajectory.add_transformations(mda_wrap(ag_wrap, compound="atoms"))
155
170
 
156
171
  same_species = len(ag_a) == len(ag_b) and ag_a is ag_b
157
172
 
158
173
  def frames():
159
- traj = universe.trajectory[index] if index is not None else universe.trajectory
174
+ if index is not None:
175
+ traj = universe.trajectory[index]
176
+ elif start is not None or stop is not None or step is not None:
177
+ traj = universe.trajectory[start:stop:step]
178
+ else:
179
+ traj = universe.trajectory
160
180
  for ts in tqdm(traj, desc="Frames (MDAnalysis)", unit="frame"):
161
181
  cell = _mdanalysis_cell_matrix(ts.dimensions)
162
182
  if same_species:
163
183
  yield {
164
184
  "positions": ag_a.positions.astype(np.float32, copy=False),
165
185
  "cell": cell,
166
- "pbc": (True, True, True),
186
+ "pbc": tuple(pbc),
167
187
  }
168
188
  else:
169
189
  pos_a = ag_a.positions.astype(np.float32, copy=False)
@@ -176,7 +196,7 @@ def rdf_from_mdanalysis(
176
196
  yield {
177
197
  "positions": pos,
178
198
  "cell": cell,
179
- "pbc": (True, True, True),
199
+ "pbc": tuple(pbc),
180
200
  "group_a_mask": group_a_mask,
181
201
  "group_b_mask": group_b_mask,
182
202
  }
@@ -189,6 +209,7 @@ def rdf_from_mdanalysis(
189
209
  r_min=r_min,
190
210
  r_max=r_max,
191
211
  nbins=nbins,
212
+ r_min_floor=r_min_floor,
192
213
  device=device,
193
214
  torch_dtype=torch_dtype,
194
215
  half_fill=half_fill,
@@ -234,6 +255,8 @@ def rdf_from_ase(
234
255
  r_min: float = 1.0,
235
256
  r_max: float = 6.0,
236
257
  nbins: int = 100,
258
+ r_min_floor: float = 1e-6,
259
+ pbc: tuple[bool, bool, bool] = (True, True, True),
237
260
  device="cuda",
238
261
  torch_dtype=None,
239
262
  half_fill: bool = True,
@@ -266,6 +289,10 @@ def rdf_from_ase(
266
289
  Maximum distance included in the histogram.
267
290
  nbins
268
291
  Number of histogram bins.
292
+ r_min_floor
293
+ Small lower bound to exclude pathological self/near-zero distances when ``r_min`` is zero.
294
+ pbc
295
+ Tuple of periodic boundary flags for (a, b, c) directions.
269
296
  device
270
297
  Torch device string or object used for computation.
271
298
  torch_dtype
@@ -308,7 +335,7 @@ def rdf_from_ase(
308
335
 
309
336
  if atom_types_map:
310
337
  # map numeric types to element names if provided
311
- types = frame.get_array("numbers", int, None)
338
+ types = frame.get_array("numbers", False)
312
339
  name_list = []
313
340
  for t in types:
314
341
  name = atom_types_map.get(t) or atom_types_map.get(str(t))
@@ -340,24 +367,38 @@ def rdf_from_ase(
340
367
  "Check element symbols or index selection."
341
368
  )
342
369
 
343
- pos_all = frame.get_positions(wrap=wrap_positions)
344
- pos_a = pos_all[idx_a]
345
- pos_b = pos_all[idx_b]
346
- pos = np.concatenate([pos_a, pos_b], axis=0)
370
+ if isinstance(pbc, Iterable):
371
+ pbc_tuple = tuple(bool(x) for x in pbc)
372
+ else:
373
+ pbc_tuple = (bool(pbc), bool(pbc), bool(pbc))
374
+ wrap_flag = wrap_positions and all(pbc_tuple)
375
+ pos_all = frame.get_positions(wrap=wrap_flag)
347
376
  cell = np.array(frame.get_cell().array, dtype=np.float32)
348
- pbc = tuple(bool(x) for x in frame.get_pbc())
349
- group_a_mask = np.zeros(len(pos), dtype=bool)
350
- group_b_mask = np.zeros(len(pos), dtype=bool)
351
- group_a_mask[: len(pos_a)] = True
352
- group_b_mask[len(pos_a) :] = True
353
-
354
- yield {
355
- "positions": pos.astype(np.float32, copy=False),
356
- "cell": cell,
357
- "pbc": pbc,
358
- "group_a_mask": group_a_mask,
359
- "group_b_mask": group_b_mask,
360
- }
377
+
378
+ if same_species:
379
+ # Same-species: keep one group so half_fill + pair_factor=2 normalisation stays correct
380
+ yield {
381
+ "positions": pos_all[idx_a].astype(np.float32, copy=False),
382
+ "cell": cell,
383
+ "pbc": pbc_tuple,
384
+ }
385
+ else:
386
+ # Cross-species: concatenate and mask groups for ordered pairs
387
+ pos_a = pos_all[idx_a]
388
+ pos_b = pos_all[idx_b]
389
+ pos = np.concatenate([pos_a, pos_b], axis=0)
390
+ group_a_mask = np.zeros(len(pos), dtype=bool)
391
+ group_b_mask = np.zeros(len(pos), dtype=bool)
392
+ group_a_mask[: len(pos_a)] = True
393
+ group_b_mask[len(pos_a) :] = True
394
+
395
+ yield {
396
+ "positions": pos.astype(np.float32, copy=False),
397
+ "cell": cell,
398
+ "pbc": pbc_tuple,
399
+ "group_a_mask": group_a_mask,
400
+ "group_b_mask": group_b_mask,
401
+ }
361
402
 
362
403
  same_species = (
363
404
  (species_b is None or species_b == species_a)
@@ -371,6 +412,7 @@ def rdf_from_ase(
371
412
  r_min=r_min,
372
413
  r_max=r_max,
373
414
  nbins=nbins,
415
+ r_min_floor=r_min_floor,
374
416
  device=device,
375
417
  torch_dtype=torch_dtype,
376
418
  half_fill=half_fill,
@@ -10,6 +10,8 @@ from torch import Tensor
10
10
  from .cell import cell_tensor, cell_volume, pbc_tensor
11
11
  from .neighbor import build_neighbor_list
12
12
 
13
+ DEFAULT_R_MIN_FLOOR = 1e-6
14
+
13
15
 
14
16
  def _update_counts(
15
17
  counts: Tensor,
@@ -22,6 +24,7 @@ def _update_counts(
22
24
  half_fill: bool,
23
25
  max_neighbors: int | None,
24
26
  method: str,
27
+ r_min_floor: float = DEFAULT_R_MIN_FLOOR,
25
28
  group_a_mask: Tensor | None = None,
26
29
  group_b_mask: Tensor | None = None,
27
30
  ) -> float:
@@ -44,6 +47,8 @@ def _update_counts(
44
47
  Minimum distance included in the histogram.
45
48
  r_max
46
49
  Maximum distance included in the histogram.
50
+ r_min_floor
51
+ Small lower bound to exclude pathological self/near-zero distances when ``r_min`` is zero.
47
52
  half_fill
48
53
  Whether to build unique pairs (same-species mode).
49
54
  max_neighbors
@@ -79,7 +84,10 @@ def _update_counts(
79
84
  dr_vec = (positions[tgt] + shift_cart) - positions[src]
80
85
  dist = torch.linalg.norm(dr_vec, dim=1)
81
86
 
82
- valid = (dist >= r_min) & (dist < r_max)
87
+ r_min_eff = max(r_min, r_min_floor)
88
+ valid = (dist >= r_min_eff) & (dist < r_max)
89
+ # Drop any self-pairs that may appear in the neighbor list (important when r_min == 0).
90
+ valid = valid & (src != tgt)
83
91
  if group_a_mask is not None and group_b_mask is not None:
84
92
  src_mask = group_a_mask[src]
85
93
  tgt_mask = group_b_mask[tgt]
@@ -91,13 +99,19 @@ def _update_counts(
91
99
  counts.scatter_add_(0, bin_idx, torch.ones_like(bin_idx, dtype=torch.int64))
92
100
 
93
101
  volume = cell_volume(cell)
102
+
94
103
  if group_a_mask is not None and group_b_mask is not None:
95
104
  n_a = group_a_mask.sum().item()
96
105
  n_b = group_b_mask.sum().item()
97
- norm_factor = n_a * (n_b / volume)
106
+ # Check if same species
107
+ if torch.equal(group_a_mask, group_b_mask):
108
+ norm_factor = n_a * ((n_b - 1) / volume) # Exclude self-pairs
109
+ else:
110
+ norm_factor = n_a * (n_b / volume) # Different species
98
111
  else:
99
112
  n_atoms = positions.shape[0]
100
- norm_factor = n_atoms * (n_atoms / volume) # n_atoms * rho
113
+ norm_factor = n_atoms * ((n_atoms - 1) / volume) # Exclude self-pairs
114
+
101
115
  return norm_factor
102
116
 
103
117
 
@@ -149,6 +163,7 @@ def compute_rdf(
149
163
  r_min: float = 1.0,
150
164
  r_max: float = 6.0,
151
165
  nbins: int = 100,
166
+ r_min_floor: float = DEFAULT_R_MIN_FLOOR,
152
167
  device: str | torch.device = "cuda",
153
168
  torch_dtype: torch.dtype = torch.float32,
154
169
  half_fill: bool = True,
@@ -174,6 +189,8 @@ def compute_rdf(
174
189
  Maximum distance included in the histogram.
175
190
  nbins
176
191
  Number of histogram bins.
192
+ r_min_floor
193
+ Small lower bound to exclude pathological self/near-zero distances when ``r_min`` is zero.
177
194
  device
178
195
  Torch device string or object used for computation.
179
196
  torch_dtype
@@ -202,7 +219,8 @@ def compute_rdf(
202
219
  cell_t = cell_tensor(cell, device=device, dtype=torch_dtype)
203
220
  pbc_t = pbc_tensor(pbc, device=device)
204
221
 
205
- edges = torch.linspace(r_min, r_max, nbins + 1, device=device, dtype=torch_dtype)
222
+ r_min_eff = max(r_min, r_min_floor)
223
+ edges = torch.linspace(r_min_eff, r_max, nbins + 1, device=device, dtype=torch_dtype)
206
224
  counts = torch.zeros(nbins, device=device, dtype=torch.int64)
207
225
 
208
226
  group_a_mask = group_b_mask = None
@@ -215,26 +233,29 @@ def compute_rdf(
215
233
  elif group_a_mask is not None:
216
234
  group_b_mask = group_a_mask
217
235
 
236
+ cross_mode = group_a_mask is not None and group_b_mask is not None and not torch.equal(
237
+ group_a_mask, group_b_mask
238
+ )
239
+ half_fill_eff = False if cross_mode else half_fill
240
+
218
241
  total_norm = _update_counts(
219
242
  counts,
220
243
  pos_t,
221
244
  cell=cell_t,
222
245
  pbc=pbc_t,
223
246
  edges=edges,
224
- r_min=r_min,
247
+ r_min=r_min_eff,
225
248
  r_max=r_max,
226
- half_fill=half_fill,
249
+ half_fill=half_fill_eff,
227
250
  max_neighbors=max_neighbors,
228
251
  method=method,
252
+ r_min_floor=r_min_floor,
229
253
  group_a_mask=group_a_mask,
230
254
  group_b_mask=group_b_mask,
231
255
  )
232
256
 
233
- cross_mode = group_a_mask is not None and group_b_mask is not None and not torch.equal(
234
- group_a_mask, group_b_mask
235
- )
236
257
  centers, g_r = _finalize_gr(
237
- counts, edges, total_norm, half_fill=half_fill, cross_mode=cross_mode
258
+ counts, edges, total_norm, half_fill=half_fill_eff, cross_mode=cross_mode
238
259
  )
239
260
  return centers.cpu().numpy(), g_r.cpu().numpy()
240
261
 
@@ -245,6 +266,7 @@ def accumulate_rdf(
245
266
  r_min: float,
246
267
  r_max: float,
247
268
  nbins: int,
269
+ r_min_floor: float,
248
270
  device: str | torch.device,
249
271
  torch_dtype: torch.dtype,
250
272
  half_fill: bool,
@@ -264,6 +286,8 @@ def accumulate_rdf(
264
286
  Maximum distance included in the histogram.
265
287
  nbins
266
288
  Number of histogram bins.
289
+ r_min_floor
290
+ Small lower bound to exclude pathological self/near-zero distances when ``r_min`` is zero.
267
291
  device
268
292
  Torch device string or object used for computation.
269
293
  torch_dtype
@@ -281,7 +305,8 @@ def accumulate_rdf(
281
305
  Bin centers and g(r) arrays on CPU.
282
306
  """
283
307
  device = torch.device(device)
284
- edges = torch.linspace(r_min, r_max, nbins + 1, device=device, dtype=torch_dtype)
308
+ r_min_eff = max(r_min, r_min_floor)
309
+ edges = torch.linspace(r_min_eff, r_max, nbins + 1, device=device, dtype=torch_dtype)
285
310
  counts = torch.zeros(nbins, device=device, dtype=torch.int64)
286
311
  total_norm = 0.0
287
312
 
@@ -303,6 +328,9 @@ def accumulate_rdf(
303
328
  group_a_mask, group_b_mask
304
329
  ):
305
330
  cross_flag = True
331
+ half_fill_frame = False
332
+ else:
333
+ half_fill_frame = half_fill
306
334
 
307
335
  norm = _update_counts(
308
336
  counts,
@@ -310,21 +338,23 @@ def accumulate_rdf(
310
338
  cell=cell_t,
311
339
  pbc=pbc_t,
312
340
  edges=edges,
313
- r_min=r_min,
341
+ r_min=r_min_eff,
314
342
  r_max=r_max,
315
- half_fill=half_fill,
343
+ half_fill=half_fill_frame,
316
344
  max_neighbors=max_neighbors,
317
345
  method=method,
346
+ r_min_floor=r_min_floor,
318
347
  group_a_mask=group_a_mask,
319
348
  group_b_mask=group_b_mask,
320
349
  )
321
350
  total_norm += norm
322
351
 
352
+ half_fill_eff = False if cross_flag else half_fill
323
353
  centers, g_r = _finalize_gr(
324
354
  counts,
325
355
  edges,
326
356
  total_norm,
327
- half_fill=half_fill,
357
+ half_fill=half_fill_eff,
328
358
  cross_mode=cross_flag,
329
359
  )
330
360
  return centers.cpu().numpy(), g_r.cpu().numpy()
@@ -336,6 +366,7 @@ def rdf(
336
366
  species_b: str | None = None,
337
367
  index=None,
338
368
  atom_types_map: dict | None = None,
369
+ pbc: tuple[bool, bool, bool] | None = None,
339
370
  method: str = "cell_list",
340
371
  outdir=None,
341
372
  output: str | None = None,
@@ -356,6 +387,8 @@ def rdf(
356
387
  Optional index/selector forwarded to source-specific readers.
357
388
  atom_types_map
358
389
  Optional mapping for numeric atom types to element names.
390
+ pbc
391
+ Optional periodic boundary flags; defaults to (True, True, True) when omitted.
359
392
  method
360
393
  Neighbor-list method name (e.g., ``"cell_list"`` or ``"naive"``).
361
394
  outdir
@@ -380,6 +413,7 @@ def rdf(
380
413
  species_b=species_b,
381
414
  index=index,
382
415
  atom_types_map=atom_types_map,
416
+ pbc=pbc if pbc is not None else (True, True, True),
383
417
  method=method,
384
418
  **kwargs,
385
419
  )
@@ -392,6 +426,7 @@ def rdf(
392
426
  species_b=species_b,
393
427
  index=index,
394
428
  atom_types_map=atom_types_map,
429
+ pbc=pbc if pbc is not None else (True, True, True),
395
430
  method=method,
396
431
  **kwargs,
397
432
  )
@@ -405,6 +440,7 @@ def rdf(
405
440
  species_b=species_b,
406
441
  index=index,
407
442
  atom_types_map=atom_types_map,
443
+ pbc=pbc if pbc is not None else (True, True, True),
408
444
  method=method,
409
445
  **kwargs,
410
446
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cuRDF"
7
- version = "0.5.0"
7
+ version = "0.5.2"
8
8
  description = "GPU-accelerated radial distribution functions with NVIDIA ALCHEMI Toolkit-Ops neighbor lists and PyTorch."
9
9
  authors = [{ name = "Joseph Hart" }]
10
10
  readme = { file = "README.md", "content-type" = "text/markdown" }
@@ -0,0 +1,55 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from curdf.rdf import rdf
5
+ from ase.io import read
6
+ import matplotlib.pyplot as plt
7
+ from pathlib import Path
8
+
9
+ DATAPATH = Path("darren_data/rdf-mace-nvt")
10
+ RMIN = 0.0
11
+ RMAX = 6.0
12
+ N_BINS = 200
13
+
14
+ def test_compute_rdf_cuda_runs():
15
+ torch_mod = pytest.importorskip("torch")
16
+ if not torch_mod.cuda.is_available():
17
+ pytest.skip("CUDA not available")
18
+
19
+ water_traj_list = read(DATAPATH / f'nvt.xyz', index=':')
20
+
21
+ for species_a, species_b in [("O", "O"), ("H", "H"), ("O", "H")]:
22
+ data = np.loadtxt(DATAPATH / f"rdf_{species_a}{species_b}_mace.dat")
23
+ r_ref, gr_ref = data[:,0], data[:,1]
24
+
25
+ try:
26
+ bins, gr = rdf(
27
+ water_traj_list,
28
+ species_a=species_a,
29
+ species_b=species_b,
30
+ r_min=RMIN,
31
+ r_max=RMAX,
32
+ nbins=N_BINS,
33
+ output=f"cuRDF_results/rdf_{species_a}{species_b}.csv"
34
+ )
35
+ plt.plot(bins, gr, label=f"cuRDF - {species_a}-{species_b}")
36
+ plt.plot(r_ref, gr_ref, label=f"Reference - {species_a}-{species_b}")
37
+ plt.xlabel("r (Å)")
38
+ plt.ylabel("g(r)")
39
+ plt.title(f"RDF {species_a}-{species_b}")
40
+ plt.ylim(0,)
41
+ plt.xlim(0, RMAX)
42
+ plt.legend()
43
+ plt.tight_layout()
44
+ plt.savefig(f"cuRDF_results/rdf_{species_a}{species_b}_comparison.png", dpi=300)
45
+ plt.savefig(f"cuRDF_results/rdf_{species_a}{species_b}_comparison.pdf")
46
+ plt.savefig(f"cuRDF_results/rdf_{species_a}{species_b}_comparison.svg")
47
+ plt.close()
48
+ # Numerical check: bins should align and g(r) should match reference within 1% relative.
49
+ assert bins.shape == r_ref.shape
50
+ assert gr.shape == gr_ref.shape
51
+ np.testing.assert_allclose(bins, r_ref, rtol=1e-6, atol=1e-6)
52
+ np.testing.assert_allclose(gr, gr_ref, rtol=1e-2, atol=1e-3)
53
+ except RuntimeError as err:
54
+ pytest.skip(f"CUDA neighbor list unavailable: {err}")
55
+
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes