nonbond 0.1.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.
nonbond/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Nonbond - ASE Calculator for Lennard-Jones + Point Charge interactions
3
+ """
4
+
5
+ from .calculator import Nonbond
6
+ from .coul import CoulombCalculator
7
+ from .lj import LJCalculator
8
+
9
+ __all__ = [
10
+ 'Nonbond',
11
+ 'CoulombCalculator',
12
+ 'LJCalculator'
13
+ ]
nonbond/calculator.py ADDED
@@ -0,0 +1,190 @@
1
+ import numpy as np
2
+ from ase.calculators.calculator import Calculator, all_changes
3
+ from ase.units import mol, kcal, eV
4
+
5
+ import torch
6
+ from vesin.torch import NeighborList
7
+
8
+ from .lj import LJCalculator
9
+ from .coul import CoulombCalculator
10
+
11
+ # Coulomb constant in eV*Å/e²
12
+ COULOMB_CONSTANT = 14.399644853
13
+
14
+
15
+ class Nonbond(Calculator):
16
+ """
17
+ ASE Calculator for Lennard-Jones + Coulomb interactions
18
+
19
+ The total potential energy is:
20
+ E_total = E_LJ + E_coulomb
21
+
22
+ Where:
23
+ E_LJ = sum_i<j 4*epsilon_ij * [(sigma_ij/r_ij)^12 - (sigma_ij/r_ij)^6]
24
+ E_coulomb = sum_i<j k * q_i * q_j / r_ij
25
+
26
+ Mixing rules (Lorentz-Berthelot):
27
+ epsilon_ij = sqrt(epsilon_i * epsilon_j)
28
+ sigma_ij = (sigma_i + sigma_j) / 2
29
+
30
+ Parameters:
31
+ -----------
32
+ sigma : dict
33
+ LJ sigma parameters by atom type (Å)
34
+ Format: {'H': 2.51, 'O': 3.12}
35
+
36
+ epsilon : dict
37
+ LJ epsilon parameters by atom type (kcal/mol)
38
+ Format: {'H': 0.044, 'O': 0.126}
39
+
40
+ rc : float, optional
41
+ Cutoff distance for both LJ and Coulomb interactions (Å)
42
+ Default: 3 * max(sigma) or 10.0 Å
43
+
44
+ charge_method : str, optional
45
+ Method for Coulomb interaction calculation ('direct', 'ewald', 'pme', 'p3m')
46
+ Default: None
47
+
48
+ accuracy : float, optional
49
+ Accuracy for Long-Range Coulomb Interaction
50
+ Default: 1e-5
51
+
52
+ device : str, optional
53
+ Device for calculations ('cpu' or 'cuda')
54
+ Default: 'cpu'
55
+ """
56
+
57
+ implemented_properties = [
58
+ "energy",
59
+ "energies",
60
+ ]
61
+
62
+ def __init__(
63
+ self,
64
+ epsilon: dict,
65
+ sigma: dict,
66
+ rc: float = 10.0,
67
+ charge_method: str = None,
68
+ accuracy: float = 1e-5,
69
+ device: str = 'cpu',
70
+ ):
71
+
72
+ kwargs = {
73
+ 'epsilon': epsilon,
74
+ 'sigma': sigma,
75
+ 'rc': rc,
76
+ 'charge_method': charge_method,
77
+ 'accuracy': accuracy,
78
+ 'device': device,
79
+ 'dtype': torch.float64,
80
+ }
81
+ Calculator.__init__(self, **kwargs)
82
+
83
+ # Initialize neighbor lists
84
+ self.neighbor_indices, self.neighbor_distances = None, None
85
+
86
+ # Cache for parameters
87
+ self.atom_types = None
88
+ self.sigmas = None
89
+ self.epsilons = None
90
+ self.charges = None
91
+ self.atoms = None
92
+ self.positions = None
93
+ self.cell = None
94
+
95
+ def calculate(self,
96
+ atoms=None,
97
+ properties=None,
98
+ system_changes=all_changes):
99
+ """Calculate energy"""
100
+
101
+ if properties is None:
102
+ properties = self.implemented_properties
103
+
104
+ Calculator.calculate(self, atoms, properties, system_changes)
105
+
106
+ # ro = self.parameters.ro
107
+ # smooth = self.parameters.smooth
108
+
109
+ # Get atom types, 用以分配LJ参数
110
+
111
+ self.atoms = atoms
112
+ self._update_parameters()
113
+
114
+ self._update_neighbor_list()
115
+
116
+ energy, energies = self._calculate_energy()
117
+
118
+ self.results['energy'] = energy
119
+ self.results['energies'] = energies
120
+
121
+ # 构建邻接列表
122
+ def _update_neighbor_list(self):
123
+ nl = NeighborList(cutoff=self.parameters.rc, full_list=False)
124
+ self.neighbor_indices, self.neighbor_distances = nl.compute(
125
+ points=self.positions,
126
+ box=self.cell,
127
+ periodic=True,
128
+ quantities="Pd")
129
+
130
+ def _calculate_energy(self):
131
+
132
+ energy = 0.0
133
+ energy_lj = 0.0
134
+ energy_coulomb = 0.0
135
+
136
+ # Calculate LJ interactions (always calculated)
137
+ lj_calc = LJCalculator(cutoff=self.parameters.rc, )
138
+ energy_lj, energies_lj = lj_calc.get_lj_energy(self.neighbor_indices,
139
+ self.neighbor_distances,
140
+ self.epsilons, self.sigmas)
141
+
142
+ # Calculate Coulomb interactions (if charges are provided)
143
+ if self.parameters.charge_method is not None:
144
+ coulomb_calc = CoulombCalculator(
145
+ method=self.parameters.charge_method,
146
+ cutoff=self.parameters.rc,
147
+ accuracy=self.parameters.accuracy,
148
+ device=self.parameters.device,
149
+ dtype=self.parameters.dtype)
150
+ energy_coulomb, energies_coulomb = coulomb_calc.get_coul_energy(
151
+ self.neighbor_indices, self.neighbor_distances, self.positions, self.cell, self.charges)
152
+ else:
153
+ energy_coulomb = 0.0
154
+ energies_coulomb = torch.zeros_like(energies_lj)
155
+
156
+ energy = energy_lj + energy_coulomb
157
+ energies = energies_lj + energies_coulomb
158
+
159
+ return energy, energies
160
+
161
+ def _update_parameters(self):
162
+ """Update cached parameters for current system"""
163
+
164
+ # atom types
165
+ try:
166
+ self.atom_types = self.atoms.get_array('type')
167
+ except KeyError:
168
+ self.atom_types = self.atoms.get_chemical_symbols()
169
+
170
+ # Convert energy units from kcal/mol to eV
171
+ convert_factor = kcal / mol / eV
172
+ atom_types = self.atom_types
173
+ self.sigmas = np.array(
174
+ [self.parameters.sigma[atom_type] for atom_type in atom_types])
175
+ self.epsilons = np.array([
176
+ self.parameters.epsilon[atom_type] * convert_factor
177
+ for atom_type in atom_types
178
+ ])
179
+
180
+ dtype = self.parameters.dtype
181
+ device = self.parameters.device
182
+ self.positions = torch.tensor(self.atoms.positions,
183
+ dtype=dtype,
184
+ device=device,
185
+ requires_grad=True)
186
+ self.cell = torch.tensor(self.atoms.cell.array,
187
+ dtype=dtype,
188
+ device=device)
189
+ self.charges = torch.tensor(
190
+ self.atoms.get_initial_charges(), dtype=dtype, device=device).unsqueeze(1)
nonbond/coul.py ADDED
@@ -0,0 +1,156 @@
1
+ from torchpme.tuning import tune_ewald, tune_pme, tune_p3m
2
+ import torchpme
3
+ import torch
4
+ from torchpme.prefactors import eV_A
5
+
6
+ class CoulombCalculator:
7
+ """
8
+ Coulomb Calculator
9
+ """
10
+
11
+ def __init__(self,
12
+ method: str = 'direct',
13
+ cutoff: float = 10.0,
14
+ accuracy: float = 1e-5,
15
+ device: str = 'cpu',
16
+ dtype: torch.dtype = torch.float64,
17
+ ):
18
+
19
+ self.method = method.lower()
20
+ self.cutoff = cutoff
21
+ self.accuracy = accuracy
22
+ self.device = device
23
+ self.dtype = dtype
24
+
25
+ # 验证方法
26
+ valid_methods = ['direct', 'ewald', 'pme', 'p3m']
27
+ if self.method not in valid_methods:
28
+ raise ValueError(f"Unsupported method: {method}. Valid methods: {valid_methods}")
29
+
30
+ # 通用参数
31
+ self.neighbor_indices, self.neighbor_distances = None, None
32
+ self.positions = None
33
+ self.cell = None
34
+ self.charges = None
35
+
36
+ #结果
37
+ self.energy = None
38
+ self.energies = None
39
+ # self.forces = None
40
+
41
+ def get_coul_energy(self,
42
+ neighbor_indices: torch.Tensor,
43
+ neighbor_distances: torch.Tensor,
44
+ positions: torch.Tensor,
45
+ cell: torch.Tensor,
46
+ charges: torch.Tensor,
47
+ ) -> float:
48
+
49
+ self.positions = positions
50
+ self.cell = cell
51
+ self.charges = charges
52
+
53
+ self.neighbor_indices, self.neighbor_distances = neighbor_indices, neighbor_distances
54
+
55
+ if self.method == 'direct':
56
+ self._direct_coulomb()
57
+ else:
58
+ self._long_range_coulomb()
59
+
60
+ return self.energy, self.energies
61
+
62
+
63
+
64
+ def _optimize_params(self):
65
+ neighbor_indices, neighbor_distances = self.neighbor_indices, self.neighbor_distances
66
+ if self.method == 'ewald':
67
+ return tune_ewald(
68
+ charges=self.charges,
69
+ cell=self.cell,
70
+ positions=self.positions,
71
+ cutoff=self.cutoff,
72
+ accuracy=self.accuracy,
73
+ neighbor_indices=neighbor_indices,
74
+ neighbor_distances=neighbor_distances,
75
+ )
76
+ elif self.method == 'pme':
77
+ return tune_pme(
78
+ charges=self.charges,
79
+ cell=self.cell,
80
+ positions=self.positions,
81
+ cutoff=self.cutoff,
82
+ accuracy=self.accuracy,
83
+ neighbor_indices=neighbor_indices,
84
+ neighbor_distances=neighbor_distances,
85
+ )
86
+ elif self.method == 'p3m':
87
+ return tune_p3m(
88
+ charges=self.charges,
89
+ cell=self.cell,
90
+ positions=self.positions,
91
+ cutoff=self.cutoff,
92
+ accuracy=self.accuracy,
93
+ neighbor_indices=neighbor_indices,
94
+ neighbor_distances=neighbor_distances,
95
+ )
96
+ else:
97
+ raise RuntimeError("_optimize_params called for unsupported method")
98
+
99
+
100
+
101
+ def _long_range_coulomb(self) -> float:
102
+ """长程库伦相互作用计算 (带缓存)"""
103
+ smearing, params, _ = self._optimize_params()
104
+ prefactor = eV_A
105
+ if self.method == 'ewald':
106
+ base_calc = torchpme.EwaldCalculator(
107
+ potential=torchpme.CoulombPotential(smearing),
108
+ **params,
109
+ prefactor=prefactor
110
+ )
111
+ elif self.method == 'pme':
112
+ base_calc = torchpme.PMECalculator(
113
+ potential=torchpme.CoulombPotential(smearing),
114
+ **params,
115
+ prefactor=prefactor
116
+ )
117
+ elif self.method == 'p3m':
118
+ base_calc = torchpme.P3MCalculator(
119
+ potential=torchpme.CoulombPotential(smearing),
120
+ **params,
121
+ prefactor=prefactor
122
+ )
123
+ else:
124
+ raise RuntimeError("Unsupported method for long range")
125
+
126
+ calculator = base_calc
127
+ neighbor_indices, neighbor_distances = self.neighbor_indices, self.neighbor_distances
128
+ calculator.to(device=self.device, dtype=self.dtype)
129
+ potentials = calculator.forward(
130
+ self.charges, self.cell, self.positions, neighbor_indices, neighbor_distances
131
+ )
132
+ energies = self.charges * potentials
133
+ energy = torch.sum(energies)
134
+ # energy.backward()
135
+ # forces = -self.positions.grad
136
+
137
+ self.energy = energy.item()
138
+ self.energies = energies
139
+ # self.forces = forces
140
+
141
+ def _direct_coulomb(self):
142
+ COULOMB_CONSTANT = 14.399644853 # eV·Å/e²
143
+ neighbor_indices, neighbor_distances = self.neighbor_indices, self.neighbor_distances
144
+ qi = self.charges[neighbor_indices[:, 0]].flatten()
145
+ qj = self.charges[neighbor_indices[:, 1]].flatten()
146
+ rij = neighbor_distances.flatten()
147
+ # 避免除零
148
+ mask = rij > 1e-12
149
+ if not torch.all(mask):
150
+ qi = qi[mask]; qj = qj[mask]; rij = rij[mask]
151
+ energies = (COULOMB_CONSTANT * qi * qj / rij)
152
+ energy = energies.sum().item()
153
+
154
+ self.energy = energy
155
+ self.energies = energies
156
+
nonbond/lj.py ADDED
@@ -0,0 +1,38 @@
1
+
2
+ import torch
3
+
4
+ device = "cpu"
5
+ dtype = torch.float64
6
+
7
+ class LJCalculator:
8
+ def __init__(self,
9
+ cutoff: float = 10.0,
10
+ ):
11
+ self.cutoff = cutoff
12
+
13
+ def get_lj_energy(self, neighbor_indices, neighbor_distances, epsilon, sigma):
14
+
15
+ if neighbor_indices.numel() == 0:
16
+ return 0.0
17
+
18
+ sigma_t = torch.as_tensor(sigma, device=device, dtype=dtype)
19
+ eps_t = torch.as_tensor(epsilon, device=device, dtype=dtype)
20
+
21
+ i_idx = neighbor_indices[:, 0].to(device=device)
22
+ j_idx = neighbor_indices[:, 1].to(device=device)
23
+ r = neighbor_distances.to(device=device, dtype=dtype)
24
+
25
+ sigma_ij = 0.5 * (sigma_t[i_idx] + sigma_t[j_idx])
26
+ epsilon_ij = torch.sqrt(eps_t[i_idx] * eps_t[j_idx])
27
+
28
+ # (sigma/r)^n
29
+ inv_r = sigma_ij / r # (sigma_ij / r)
30
+ inv_r2 = inv_r * inv_r
31
+ inv_r6 = inv_r2 * inv_r2 * inv_r2
32
+ inv_r12 = inv_r6 * inv_r6
33
+
34
+ pair_energy = 4.0 * epsilon_ij * (inv_r12 - inv_r6) # (n_pairs,)
35
+ energies = pair_energy
36
+ energy = pair_energy.sum().item()
37
+
38
+ return energy, energies
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonbond
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: ase>=3.26.0
8
+ Requires-Dist: torch-pme>=0.3.2
9
+ Requires-Dist: vesin-torch>=0.4.2
10
+
11
+ # Nonbond
12
+
13
+ **Nonbond** is an [ASE (Atomic Simulation Environment)](https://ase-lib.org/) calculator designed for computing **non-bonded interactions**.
14
+ Currently, it is primarily intended for use in specific contexts such as **GCMC simulations**, as it outputs only the **total energy** and **per-atom energies** (forces are not yet implemented).
15
+
16
+ ---
17
+
18
+ ## 🧩 Features
19
+
20
+ - Lennard-Jones (LJ) potential energy calculations
21
+ - Efficient long-range Coulomb interaction algorithms
22
+ - Fast neighbor list construction using **PyTorch** and **vesin**
23
+ - **GPU acceleration** via PyTorch
24
+
25
+ ---
26
+
27
+ ## 🧠 Requirements
28
+
29
+ - Python ≥ 3.12
30
+ - ASE ≥ 3.26.0
31
+ - torch-pme ≥ 0.3.2
32
+ - vesin-torch ≥ 0.4.2
33
+ - PyTorch (latest stable version recommended)
34
+
35
+ ---
36
+
37
+ ## ⚙️ Installation
38
+
39
+ Install via **pip**:
40
+
41
+ ```bash
42
+ pip install nonbond
43
+ ```
44
+
45
+ # 🚀 Example
46
+
47
+ ```python
48
+ from ase import Atoms
49
+ from nonbond import Nonbond
50
+ import numpy as np
51
+
52
+ # Define Lennard-Jones parameters for SPC/E water
53
+ epsilon = {
54
+ 'Ow': 0.15535,
55
+ 'Hw': 0.0,
56
+ }
57
+
58
+ sigma = {
59
+ 'Ow': 3.16600,
60
+ 'Hw': 1.0,
61
+ }
62
+
63
+ # Create calculator
64
+ calc = Nonbond(
65
+ epsilon=epsilon,
66
+ sigma=sigma,
67
+ rc=14.0,
68
+ charge_method='ewald', # Options: 'direct', 'ewald', 'pme', 'p3m'
69
+ accuracy=1e-5
70
+ )
71
+
72
+ # Build an SPC/E water molecule
73
+ atoms = Atoms(
74
+ 'OH2',
75
+ positions=[
76
+ [0.00000, -0.06461, 0.00000],
77
+ [0.81649, 0.51275, 0.00000],
78
+ [-0.81649, 0.51275, 0.00000]
79
+ ],
80
+ cell=[40.0, 40.0, 40.0],
81
+ pbc=True
82
+ )
83
+
84
+ # Assign atom types and charges
85
+ atoms.set_array('type', np.array(['Ow', 'Hw', 'Hw']))
86
+ atoms.set_initial_charges([-0.8476, 0.4238, 0.4238])
87
+
88
+ # Attach calculator and compute energy
89
+ atoms.calc = calc
90
+ energy = atoms.get_potential_energy()
91
+ print(f"Potential energy: {energy:.6f} eV")
92
+ ```
93
+ ---
94
+
95
+ # 📝 Notes
96
+
97
+ - Only energy calculations are currently supported.
98
+ - Force and stress tensor support will be added in future releases.
99
+
@@ -0,0 +1,8 @@
1
+ nonbond/__init__.py,sha256=MZqV2Brs_6ohjkO8oBjASzz8Grx_NUfErJwI33tavL0,262
2
+ nonbond/calculator.py,sha256=KX1iurQp11b0I5TE2JcX9Uvs9d3k_syuDH1vqBQ4J8I,6107
3
+ nonbond/coul.py,sha256=c3liFeIw8jiRIxbtzhpd1YKEot4FI4AUykr_PmAn4g4,5503
4
+ nonbond/lj.py,sha256=crth6CXB6EzMaKGNCCEITabqqt2fj3bk4SjcrGwNikw,1197
5
+ nonbond-0.1.0.dist-info/METADATA,sha256=gqVAO4LLVuQ7QNJ4ZDTby16VHf0YtZZ6NuDYixV0BPE,2261
6
+ nonbond-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ nonbond-0.1.0.dist-info/top_level.txt,sha256=ZlwuJT3wNDOQLNA5qKQt5IroPCV_OOdeycss1Q0llAs,8
8
+ nonbond-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ nonbond