bfflearn 0.1.0.dev1__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.
- bff/__init__.py +30 -0
- bff/bayes/__init__.py +11 -0
- bff/bayes/gaussian_process.py +285 -0
- bff/bayes/kernels.py +71 -0
- bff/bayes/learning.py +484 -0
- bff/bayes/likelihoods.py +142 -0
- bff/bayes/posterior.py +62 -0
- bff/bayes/priors.py +179 -0
- bff/bayes/results.py +327 -0
- bff/bayes/utils.py +333 -0
- bff/cli.py +254 -0
- bff/data.py +93 -0
- bff/domain/__init__.py +12 -0
- bff/domain/bias.py +149 -0
- bff/domain/specs.py +347 -0
- bff/domain/trainset.py +201 -0
- bff/io/__init__.py +12 -0
- bff/io/colvars.py +186 -0
- bff/io/commands.py +20 -0
- bff/io/cp2k.py +330 -0
- bff/io/logs.py +213 -0
- bff/io/mdp.py +98 -0
- bff/io/plumed.py +184 -0
- bff/io/progress.py +74 -0
- bff/io/schedulers.py +118 -0
- bff/io/utils.py +208 -0
- bff/mcmc/__init__.py +3 -0
- bff/mcmc/convergence.py +168 -0
- bff/mcmc/proposal.py +254 -0
- bff/mcmc/sampler.py +481 -0
- bff/plotting.py +362 -0
- bff/qoi/__init__.py +9 -0
- bff/qoi/analysis.py +209 -0
- bff/qoi/data.py +169 -0
- bff/qoi/hbonds.py +255 -0
- bff/qoi/rdf.py +108 -0
- bff/qoi/routines.py +272 -0
- bff/tools.py +168 -0
- bff/topology.py +581 -0
- bff/workflows/__init__.py +0 -0
- bff/workflows/configs.py +1147 -0
- bff/workflows/learn.py +77 -0
- bff/workflows/md.py +326 -0
- bff/workflows/prepare.py +788 -0
- bff/workflows/qoi.py +260 -0
- bff/workflows/runsims.py +620 -0
- bff/workflows/simulate.py +28 -0
- bff/workflows/train.py +69 -0
- bff/workflows/validate.py +29 -0
- bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
- bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
- bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
- bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
- bfflearn-0.1.0.dev1.dist-info/top_level.txt +1 -0
bff/mcmc/sampler.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from .convergence import (
|
|
9
|
+
ConvergenceInfo,
|
|
10
|
+
integrated_autocorr_time,
|
|
11
|
+
rank_normalize,
|
|
12
|
+
rank_normalized_split_rhat,
|
|
13
|
+
)
|
|
14
|
+
from .proposal import Proposal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Checkpoint:
|
|
19
|
+
step: int
|
|
20
|
+
total_steps: int
|
|
21
|
+
phase: str
|
|
22
|
+
warmup: int
|
|
23
|
+
thin: int
|
|
24
|
+
progress_stride: int
|
|
25
|
+
p: Optional[torch.Tensor] = None
|
|
26
|
+
logp: Optional[torch.Tensor] = None
|
|
27
|
+
accepted: Optional[torch.Tensor] = None
|
|
28
|
+
posterior: Optional[torch.Tensor] = None
|
|
29
|
+
acceptance_rate: Optional[float] = None
|
|
30
|
+
scale: Optional[float] = None
|
|
31
|
+
convergence: Optional[ConvergenceInfo] = None
|
|
32
|
+
it_per_sec: Optional[float] = None
|
|
33
|
+
rng_state: Optional[torch.Tensor] = None
|
|
34
|
+
proposal_state: Optional[dict[str, Any]] = None
|
|
35
|
+
converged: bool = False
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
"p": self.p,
|
|
40
|
+
"logp": self.logp,
|
|
41
|
+
"accepted": self.accepted,
|
|
42
|
+
"posterior": self.posterior,
|
|
43
|
+
"step": self.step,
|
|
44
|
+
"total_steps": self.total_steps,
|
|
45
|
+
"warmup": self.warmup,
|
|
46
|
+
"thin": self.thin,
|
|
47
|
+
"progress_stride": self.progress_stride,
|
|
48
|
+
"rng_state": self.rng_state,
|
|
49
|
+
"proposal_state": self.proposal_state,
|
|
50
|
+
"converged": self.converged,
|
|
51
|
+
"phase": self.phase,
|
|
52
|
+
"acceptance_rate": self.acceptance_rate,
|
|
53
|
+
"scale": self.scale,
|
|
54
|
+
"it_per_sec": self.it_per_sec,
|
|
55
|
+
"convergence": self._serialize_convergence(self.convergence),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: dict[str, Any]) -> "Checkpoint":
|
|
60
|
+
posterior = data.get("posterior", data.get("chain"))
|
|
61
|
+
if posterior is None:
|
|
62
|
+
raise KeyError("Checkpoint is missing 'posterior'.")
|
|
63
|
+
convergence = cls._deserialize_convergence(data.get("convergence"))
|
|
64
|
+
return cls(
|
|
65
|
+
p=data["p"],
|
|
66
|
+
logp=data["logp"],
|
|
67
|
+
accepted=data["accepted"],
|
|
68
|
+
posterior=posterior,
|
|
69
|
+
step=int(data["step"]),
|
|
70
|
+
total_steps=int(data["total_steps"]),
|
|
71
|
+
phase=data.get("phase", "sampling"),
|
|
72
|
+
acceptance_rate=data.get("acceptance_rate"),
|
|
73
|
+
scale=data.get("scale"),
|
|
74
|
+
convergence=convergence,
|
|
75
|
+
it_per_sec=data.get("it_per_sec"),
|
|
76
|
+
warmup=int(data["warmup"]),
|
|
77
|
+
thin=int(data["thin"]),
|
|
78
|
+
progress_stride=int(data["progress_stride"]),
|
|
79
|
+
rng_state=data["rng_state"],
|
|
80
|
+
proposal_state=data["proposal_state"],
|
|
81
|
+
converged=bool(data.get("converged", False)),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def load(cls, fn: Path | str) -> "Checkpoint":
|
|
86
|
+
return cls.from_dict(torch.load(fn, weights_only=False))
|
|
87
|
+
|
|
88
|
+
def write(self, fn: Path | str) -> None:
|
|
89
|
+
torch.save(self.to_dict(), fn)
|
|
90
|
+
|
|
91
|
+
def restore(
|
|
92
|
+
self,
|
|
93
|
+
sampler: "Sampler",
|
|
94
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]:
|
|
95
|
+
if self.p is None or self.logp is None or self.accepted is None:
|
|
96
|
+
raise ValueError("Checkpoint is missing sampler state.")
|
|
97
|
+
if self.posterior is None:
|
|
98
|
+
raise ValueError("Checkpoint is missing posterior samples.")
|
|
99
|
+
if self.rng_state is None or self.proposal_state is None:
|
|
100
|
+
raise ValueError("Checkpoint is missing restart metadata.")
|
|
101
|
+
|
|
102
|
+
p = sampler._to_tensor(self.p)
|
|
103
|
+
logp = sampler._to_tensor(self.logp)
|
|
104
|
+
accepted = sampler._to_tensor(self.accepted).to(torch.int64)
|
|
105
|
+
|
|
106
|
+
posterior = sampler._to_tensor(self.posterior)
|
|
107
|
+
if posterior.numel():
|
|
108
|
+
sampler._chain = [
|
|
109
|
+
posterior[i].detach().clone() for i in range(posterior.shape[0])
|
|
110
|
+
]
|
|
111
|
+
else:
|
|
112
|
+
sampler._chain = []
|
|
113
|
+
|
|
114
|
+
sampler.rng.set_state(self.rng_state)
|
|
115
|
+
sampler.proposal.load_state_dict(self.proposal_state)
|
|
116
|
+
sampler.converged = self.converged
|
|
117
|
+
|
|
118
|
+
return p, logp, accepted, self.step
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def tau(self) -> torch.Tensor | None:
|
|
122
|
+
if self.convergence is None or self.convergence.tau is None:
|
|
123
|
+
return None
|
|
124
|
+
tau = self.convergence.tau
|
|
125
|
+
return tau.mean(dim=0) if tau.ndim > 1 else tau
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _serialize_convergence(
|
|
129
|
+
convergence: Optional[ConvergenceInfo],
|
|
130
|
+
) -> dict[str, torch.Tensor | None] | None:
|
|
131
|
+
if convergence is None:
|
|
132
|
+
return None
|
|
133
|
+
return {
|
|
134
|
+
"rhat": convergence.rhat.cpu() if convergence.rhat is not None else None,
|
|
135
|
+
"tau": convergence.tau.cpu() if convergence.tau is not None else None,
|
|
136
|
+
"ess": convergence.ess.cpu() if convergence.ess is not None else None,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _deserialize_convergence(
|
|
141
|
+
data: dict[str, torch.Tensor | None] | None,
|
|
142
|
+
) -> Optional[ConvergenceInfo]:
|
|
143
|
+
if data is None:
|
|
144
|
+
return None
|
|
145
|
+
return ConvergenceInfo(
|
|
146
|
+
rhat=data.get("rhat"),
|
|
147
|
+
tau=data.get("tau"),
|
|
148
|
+
ess=data.get("ess"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Sampler:
|
|
153
|
+
"""
|
|
154
|
+
Parallel Metropolis-Hastings sampler with walker-vectorized target evaluation.
|
|
155
|
+
|
|
156
|
+
The sampler assumes that the target log-probability function accepts a batch
|
|
157
|
+
of walker positions of shape ``(n_walkers, n_dim)`` and returns one log
|
|
158
|
+
probability per walker.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
log_prob : callable
|
|
163
|
+
Target log-probability function with signature
|
|
164
|
+
``log_prob(x, *args) -> np.ndarray``, where ``x`` has shape
|
|
165
|
+
``(n_walkers, n_dim)`` and the return value has shape ``(n_walkers,)``.
|
|
166
|
+
proposal : Proposal
|
|
167
|
+
Proposal mechanism used to generate trial moves.
|
|
168
|
+
args : tuple, optional
|
|
169
|
+
Additional arguments passed to ``log_prob``.
|
|
170
|
+
rng : np.random.Generator, optional
|
|
171
|
+
Random number generator. If ``None``, a new default generator is created.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
log_prob: Callable,
|
|
177
|
+
proposal: Proposal,
|
|
178
|
+
args: Tuple = (),
|
|
179
|
+
rng: Optional[torch.Generator] = None,
|
|
180
|
+
device: Optional[torch.device] = 'cuda',
|
|
181
|
+
dtype: Optional[torch.dtype] = torch.float32,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.log_prob = log_prob
|
|
184
|
+
self.proposal = proposal
|
|
185
|
+
self.args = args
|
|
186
|
+
self.device = device
|
|
187
|
+
self.dtype = dtype
|
|
188
|
+
|
|
189
|
+
if rng is None:
|
|
190
|
+
rng = torch.Generator(device=self.device)
|
|
191
|
+
rng.manual_seed(torch.seed())
|
|
192
|
+
self.rng = rng
|
|
193
|
+
|
|
194
|
+
self._chain: list[torch.Tensor] = []
|
|
195
|
+
self.converged = False
|
|
196
|
+
self._warmup = 0
|
|
197
|
+
self._thin = 1
|
|
198
|
+
self._progress_stride = 1
|
|
199
|
+
self._total_steps = 0
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def chain(self) -> torch.Tensor:
|
|
203
|
+
"""Saved chain of walker positions.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
torch.Tensor
|
|
208
|
+
Tensor of shape ``(n_saved, n_walkers, n_dim)``.
|
|
209
|
+
Returns an empty tensor if no samples have been saved yet.
|
|
210
|
+
"""
|
|
211
|
+
if not self._chain:
|
|
212
|
+
return torch.empty((0, 0, 0), dtype=self.dtype, device=self.device)
|
|
213
|
+
return torch.stack(self._chain, dim=0)
|
|
214
|
+
|
|
215
|
+
def _to_tensor(self, x) -> torch.Tensor:
|
|
216
|
+
if isinstance(x, torch.Tensor):
|
|
217
|
+
return x.to(device=self.device, dtype=self.dtype)
|
|
218
|
+
return torch.as_tensor(x, device=self.device, dtype=self.dtype)
|
|
219
|
+
|
|
220
|
+
def _rand(self, shape) -> torch.Tensor:
|
|
221
|
+
return torch.rand(
|
|
222
|
+
shape, generator=self.rng, device=self.device, dtype=self.dtype)
|
|
223
|
+
|
|
224
|
+
def _validate_inputs(
|
|
225
|
+
self,
|
|
226
|
+
p0: torch.Tensor,
|
|
227
|
+
total_steps: int,
|
|
228
|
+
warmup: int,
|
|
229
|
+
thin: int,
|
|
230
|
+
progress_stride: int,
|
|
231
|
+
) -> torch.Tensor:
|
|
232
|
+
"""Validate run inputs."""
|
|
233
|
+
p = self._to_tensor(p0)
|
|
234
|
+
|
|
235
|
+
if p.ndim != 2:
|
|
236
|
+
raise ValueError("p0 must have shape (n_walkers, n_dim)")
|
|
237
|
+
if total_steps < 1:
|
|
238
|
+
raise ValueError("total_steps must be positive")
|
|
239
|
+
if warmup < 0:
|
|
240
|
+
raise ValueError("warmup must be non-negative")
|
|
241
|
+
if warmup >= total_steps:
|
|
242
|
+
raise ValueError("warmup must be smaller than total_steps")
|
|
243
|
+
if thin < 1:
|
|
244
|
+
raise ValueError("thin must be >= 1")
|
|
245
|
+
if progress_stride < 1:
|
|
246
|
+
raise ValueError("progress_stride must be >= 1")
|
|
247
|
+
return p
|
|
248
|
+
|
|
249
|
+
def _log_prob(self, x: torch.Tensor) -> torch.Tensor:
|
|
250
|
+
"""
|
|
251
|
+
Evaluate the target log-probability and sanitize non-finite values.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
x : torch.Tensor
|
|
256
|
+
Walker positions with shape ``(n_walkers, n_dim)``.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
torch.Tensor
|
|
261
|
+
Log probabilities with shape ``(n_walkers,)``. Non-finite values are
|
|
262
|
+
replaced by ``-inf``.
|
|
263
|
+
"""
|
|
264
|
+
y = self._to_tensor(self.log_prob(x, *self.args))
|
|
265
|
+
if y.ndim != 1:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"log_prob must return shape (n_walkers,), got {tuple(y.shape)}")
|
|
268
|
+
if y.shape[0] != x.shape[0]:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"log_prob returned wrong length: "
|
|
271
|
+
f"expected {x.shape[0]}, got {y.shape[0]}")
|
|
272
|
+
return torch.where(torch.isfinite(y), y, torch.full_like(y, -torch.inf))
|
|
273
|
+
|
|
274
|
+
def _diagnose_convergence(self, chain: torch.Tensor) -> ConvergenceInfo:
|
|
275
|
+
"""
|
|
276
|
+
Compute convergence diagnostics from a chain tensor.
|
|
277
|
+
"""
|
|
278
|
+
pooled = chain.reshape(-1, chain.shape[-1])
|
|
279
|
+
ranked_chain = rank_normalize(pooled).reshape(chain.shape)
|
|
280
|
+
rh = rank_normalized_split_rhat(chain)
|
|
281
|
+
tau = integrated_autocorr_time(ranked_chain)
|
|
282
|
+
n_total = chain.shape[0] * chain.shape[1]
|
|
283
|
+
ess = n_total / tau.mean(dim=0)
|
|
284
|
+
|
|
285
|
+
return ConvergenceInfo(
|
|
286
|
+
rhat=rh,
|
|
287
|
+
tau=tau,
|
|
288
|
+
ess=ess,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _validate_checkpoint(self, checkpoint: Checkpoint) -> None:
|
|
292
|
+
if checkpoint.warmup != self._warmup:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Checkpoint warmup={checkpoint.warmup} "
|
|
295
|
+
f"does not match requested warmup={self._warmup}."
|
|
296
|
+
)
|
|
297
|
+
if checkpoint.thin != self._thin:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"Checkpoint thin={checkpoint.thin} "
|
|
300
|
+
f"does not match requested thin={self._thin}."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _should_stop(
|
|
304
|
+
self,
|
|
305
|
+
conv: Optional[ConvergenceInfo],
|
|
306
|
+
*,
|
|
307
|
+
rhat_tol: float,
|
|
308
|
+
ess_min: int,
|
|
309
|
+
) -> bool:
|
|
310
|
+
if conv is None:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
rhat_ok = conv.max_rhat is not None and conv.max_rhat < rhat_tol
|
|
314
|
+
ess_ok = conv.min_ess is not None and conv.min_ess > ess_min
|
|
315
|
+
return rhat_ok and ess_ok
|
|
316
|
+
|
|
317
|
+
def write_posterior(self, fn: Path | str) -> None:
|
|
318
|
+
torch.save({"posterior": self.chain.detach().cpu()}, fn)
|
|
319
|
+
|
|
320
|
+
def run(
|
|
321
|
+
self,
|
|
322
|
+
p0: torch.Tensor,
|
|
323
|
+
total_steps: int = 1500,
|
|
324
|
+
warmup: int = 500,
|
|
325
|
+
thin: int = 1,
|
|
326
|
+
progress_stride: int = 100,
|
|
327
|
+
fn_checkpoint: Optional[str | Path] = None,
|
|
328
|
+
restart: bool = False,
|
|
329
|
+
rhat_tol: float = 1.01,
|
|
330
|
+
ess_min: int = 100,
|
|
331
|
+
):
|
|
332
|
+
"""
|
|
333
|
+
Run the sampler.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
p0 : torch.Tensor
|
|
338
|
+
Initial walker positions with shape ``(n_walkers, n_dim)``.
|
|
339
|
+
total_steps : int, default=1500
|
|
340
|
+
Total number of MCMC steps including warmup.
|
|
341
|
+
warmup : int, default=500
|
|
342
|
+
Number of warmup steps during which the proposal may adapt.
|
|
343
|
+
thin : int, default=1
|
|
344
|
+
Save every ``thin``-th production step.
|
|
345
|
+
progress_stride : int, default=100
|
|
346
|
+
Frequency of yielding progress information.
|
|
347
|
+
fn_chain: str or Path, optional
|
|
348
|
+
If provided, the chain will be saved to this file in
|
|
349
|
+
PyTorch format after each progress report.
|
|
350
|
+
restart: bool, default=False
|
|
351
|
+
If True and fn_chain exists, the sampler will attempt
|
|
352
|
+
to load the chain from the file
|
|
353
|
+
and resume sampling from the last saved position.
|
|
354
|
+
Otherwise, sampling starts from p0.
|
|
355
|
+
rhat_tol : float, default=1.01
|
|
356
|
+
Threshold for maximum R-hat to consider the chain converged.
|
|
357
|
+
ess_min : int, default=100
|
|
358
|
+
Minimum effective sample size to consider the chain converged.
|
|
359
|
+
Yields
|
|
360
|
+
------
|
|
361
|
+
Checkpoint
|
|
362
|
+
Progress snapshot for monitoring and restart.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
fn_checkpoint = Path(fn_checkpoint) if fn_checkpoint is not None else None
|
|
366
|
+
self.converged = False
|
|
367
|
+
self._warmup = warmup
|
|
368
|
+
self._thin = thin
|
|
369
|
+
self._progress_stride = progress_stride
|
|
370
|
+
self._total_steps = total_steps
|
|
371
|
+
|
|
372
|
+
if restart:
|
|
373
|
+
if fn_checkpoint is None or not fn_checkpoint.exists():
|
|
374
|
+
raise ValueError("restart=True requires an existing checkpoint file.")
|
|
375
|
+
p_probe = self._validate_inputs(
|
|
376
|
+
p0, total_steps, warmup, thin, progress_stride
|
|
377
|
+
)
|
|
378
|
+
_, n_dim = p_probe.shape
|
|
379
|
+
self.proposal.initialize(n_dim)
|
|
380
|
+
checkpoint = Checkpoint.load(fn_checkpoint)
|
|
381
|
+
self._validate_checkpoint(checkpoint)
|
|
382
|
+
p, logp, accepted, start_step = checkpoint.restore(self)
|
|
383
|
+
else:
|
|
384
|
+
p = self._validate_inputs(
|
|
385
|
+
p0, total_steps, warmup, thin, progress_stride
|
|
386
|
+
)
|
|
387
|
+
n_walkers, n_dim = p.shape
|
|
388
|
+
self.proposal.initialize(n_dim)
|
|
389
|
+
logp = self._log_prob(p)
|
|
390
|
+
accepted = torch.zeros(n_walkers, dtype=torch.int64, device=self.device)
|
|
391
|
+
self._chain = []
|
|
392
|
+
start_step = 0
|
|
393
|
+
|
|
394
|
+
n_walkers, n_dim = p.shape
|
|
395
|
+
last_report_time = time.time()
|
|
396
|
+
|
|
397
|
+
for t in range(start_step, self._total_steps):
|
|
398
|
+
|
|
399
|
+
# propose and evaluate
|
|
400
|
+
proposals = self._to_tensor(self.proposal.propose(p, self.rng))
|
|
401
|
+
logp_proposed = self._log_prob(proposals)
|
|
402
|
+
|
|
403
|
+
logu = torch.log(self._rand(n_walkers))
|
|
404
|
+
accept_mask = logu < (logp_proposed - logp)
|
|
405
|
+
|
|
406
|
+
p[accept_mask] = proposals[accept_mask]
|
|
407
|
+
logp[accept_mask] = logp_proposed[accept_mask]
|
|
408
|
+
|
|
409
|
+
# warmup / sampling
|
|
410
|
+
if t < warmup:
|
|
411
|
+
self.proposal.adapt(t + 1, p, accept_mask)
|
|
412
|
+
else:
|
|
413
|
+
accepted += accept_mask
|
|
414
|
+
if (t - warmup) % thin == 0:
|
|
415
|
+
self._chain.append(p.detach().clone())
|
|
416
|
+
|
|
417
|
+
report_progress = (
|
|
418
|
+
((t + 1) % self._progress_stride == 0)
|
|
419
|
+
or (t == self._total_steps - 1)
|
|
420
|
+
)
|
|
421
|
+
if not report_progress:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# number of iterations per second
|
|
425
|
+
now = time.time()
|
|
426
|
+
elapsed = max(now - last_report_time, 1e-12)
|
|
427
|
+
steps_since_start = t + 1 - start_step if t + 1 > start_step else 1
|
|
428
|
+
step_count = min(self._progress_stride, steps_since_start)
|
|
429
|
+
it_per_sec = step_count / elapsed
|
|
430
|
+
last_report_time = now
|
|
431
|
+
|
|
432
|
+
phase = "warmup" if t < warmup else "sampling"
|
|
433
|
+
chain = self.chain
|
|
434
|
+
convergence = None
|
|
435
|
+
if t >= warmup and chain.shape[0] >= 20:
|
|
436
|
+
convergence = self._diagnose_convergence(chain)
|
|
437
|
+
|
|
438
|
+
# acceptance rate during sampling phase
|
|
439
|
+
acceptance_rate = None
|
|
440
|
+
if t >= warmup:
|
|
441
|
+
n_sampling_steps_done = t + 1 - warmup
|
|
442
|
+
total_accepted = accepted.sum().item()
|
|
443
|
+
acceptance_rate = total_accepted / (n_sampling_steps_done * n_walkers)
|
|
444
|
+
|
|
445
|
+
should_stop = self._should_stop(
|
|
446
|
+
convergence,
|
|
447
|
+
rhat_tol=rhat_tol,
|
|
448
|
+
ess_min=ess_min,
|
|
449
|
+
)
|
|
450
|
+
if should_stop:
|
|
451
|
+
self.converged = True
|
|
452
|
+
|
|
453
|
+
# report progress
|
|
454
|
+
checkpoint = Checkpoint(
|
|
455
|
+
step=t + 1,
|
|
456
|
+
total_steps=self._total_steps,
|
|
457
|
+
phase=phase,
|
|
458
|
+
warmup=self._warmup,
|
|
459
|
+
thin=self._thin,
|
|
460
|
+
progress_stride=self._progress_stride,
|
|
461
|
+
p=p.detach().cpu(),
|
|
462
|
+
logp=logp.detach().cpu(),
|
|
463
|
+
accepted=accepted.detach().cpu(),
|
|
464
|
+
posterior=self.chain.detach().cpu(),
|
|
465
|
+
acceptance_rate=acceptance_rate,
|
|
466
|
+
scale=self.proposal.info().get("scale"),
|
|
467
|
+
convergence=convergence,
|
|
468
|
+
it_per_sec=it_per_sec,
|
|
469
|
+
rng_state=self.rng.get_state(),
|
|
470
|
+
proposal_state=self.proposal.state_dict(),
|
|
471
|
+
converged=self.converged,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if fn_checkpoint is not None:
|
|
475
|
+
checkpoint.write(fn_checkpoint)
|
|
476
|
+
|
|
477
|
+
yield checkpoint
|
|
478
|
+
|
|
479
|
+
# Early stopping based on convergence diagnostics
|
|
480
|
+
if should_stop:
|
|
481
|
+
break
|