tbnpy 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.
tbnpy/__init__.py ADDED
File without changes
tbnpy/adaptiveMH.py ADDED
@@ -0,0 +1,627 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List, Sequence, Tuple, Optional, Set, Any
3
+
4
+ from tbnpy import variable, cpt
5
+ import time
6
+
7
+ import torch
8
+
9
+ # -----------------------------
10
+ # Utility: detect variable type
11
+ # -----------------------------
12
+ def is_discrete(var) -> bool:
13
+ return isinstance(var.values, list)
14
+
15
+ def is_continuous(var) -> bool:
16
+ return isinstance(var.values, tuple)
17
+
18
+ def num_categories(var) -> int:
19
+ assert is_discrete(var)
20
+ return len(var.values)
21
+
22
+ # ---------------------------------------------------------
23
+ # Adaptive, tensorised MH sampler for hybrid BNs
24
+ # ---------------------------------------------------------
25
+ # Factor adapter: build Cs for a probability object (factor)
26
+ def build_Cs_3d(prob, state: Dict[str, torch.Tensor], evidence_1d: Dict[str, torch.Tensor]) -> torch.Tensor:
27
+ """
28
+ Build Cs with shape (n_evi, n_chain, n_childs + n_parents) in the column order:
29
+ [child_0, ..., child_{n_childs-1}, parent_0, ..., parent_{n_parents-1}]
30
+ Evidence variables are broadcast to (n_evi, n_chain).
31
+ Latent variables come from `state[var_name]` which must be (n_evi, n_chain).
32
+
33
+ Input:
34
+ prob : probability object
35
+ Must have `prob.childs` and `prob.parents` lists of variable objects.
36
+ state: dict
37
+ state["X"] has shape (n_evi, n_chain)
38
+ evidence_1d: dict
39
+ evidence_1d["E"] has shape (n_evi,)
40
+ """
41
+ # Infer (n_evi, n_chain) from any state tensor, else from evidence.
42
+ n_evi = None
43
+ n_chain = None
44
+ if len(state) > 0:
45
+ any_t = next(iter(state.values()))
46
+ n_evi, n_chain = any_t.shape
47
+ else:
48
+ # fall back to evidence
49
+ any_e = next(iter(evidence_1d.values()))
50
+ n_evi = any_e.shape[0]
51
+ raise ValueError("State is empty; cannot infer n_chain. Provide initial state for latent variables.")
52
+
53
+ cols: List[torch.Tensor] = []
54
+
55
+ # Child columns
56
+ for v in prob.childs:
57
+ name = v.name
58
+ if name in evidence_1d:
59
+ col = evidence_1d[name].unsqueeze(1).expand(n_evi, n_chain)
60
+ else:
61
+ col = state[name]
62
+ cols.append(col)
63
+
64
+ # Parent columns
65
+ for v in prob.parents:
66
+ name = v.name
67
+ if name in evidence_1d:
68
+ col = evidence_1d[name].unsqueeze(1).expand(n_evi, n_chain)
69
+ else:
70
+ col = state[name]
71
+ cols.append(col)
72
+
73
+ Cs = torch.stack(cols, dim=2) # (n_evi, n_chain, n_childs+n_parents)
74
+ return Cs
75
+
76
+
77
+ def factor_logp_2d(prob, Cs_3d: torch.Tensor) -> torch.Tensor:
78
+ """
79
+ Evaluate prob.log_prob on Cs_3d (n_evi, n_chain, dim) by flattening.
80
+ Returns (n_evi, n_chain).
81
+ """
82
+ n_evi, n_chain, dim = Cs_3d.shape
83
+ Cs_flat = Cs_3d.reshape(n_evi * n_chain, dim)
84
+ logp_flat = prob.log_prob(Cs_flat)
85
+ return logp_flat.reshape(n_evi, n_chain)
86
+
87
+
88
+ # -----------------------------
89
+ # Build factor adjacency
90
+ # -----------------------------
91
+ def build_factors_by_var(probs: Dict[str, Any]) -> Dict[str, List[Any]]:
92
+ """
93
+ Map variable name -> list of probability objects (factors) that depend on it
94
+ (as either child or parent).
95
+ """
96
+ factors_by_var: Dict[str, List[Any]] = {}
97
+ for _, prob in probs.items():
98
+ for v in list(prob.childs) + list(prob.parents):
99
+ factors_by_var.setdefault(v.name, []).append(prob)
100
+ return factors_by_var
101
+
102
+
103
+ # -----------------------------
104
+ # Proposals (symmetric)
105
+ # -----------------------------
106
+ def propose_discrete_adaptive(
107
+ x: torch.Tensor,
108
+ logits: torch.Tensor,
109
+ alpha: float,
110
+ ) -> torch.Tensor:
111
+ """
112
+ Adaptive symmetric proposal for a discrete variable.
113
+
114
+ Parameters
115
+ ----------
116
+ x : torch.Tensor
117
+ Current state, shape (n_evi, n_chain), integer in [0, K-1]
118
+ logits : torch.Tensor
119
+ Global adaptive logits, shape (K,)
120
+ alpha : float
121
+ Mixing weight for local vs global proposal (0 < alpha < 1)
122
+
123
+ Returns
124
+ -------
125
+ x_new : torch.Tensor
126
+ Proposed state, same shape as x
127
+ """
128
+ device = x.device
129
+ n_evi, n_chain = x.shape
130
+ K = logits.numel()
131
+
132
+ # ---- Local symmetric proposal: uniform over other states ----
133
+ r = torch.randint(0, K - 1, size=(n_evi, n_chain), device=device)
134
+ x_local = r + (r >= x).to(r.dtype)
135
+
136
+ # ---- Global adaptive proposal (independent of x) ----
137
+ probs = torch.softmax(logits, dim=0)
138
+ x_global = torch.multinomial(
139
+ probs,
140
+ num_samples=n_evi * n_chain,
141
+ replacement=True,
142
+ ).reshape(n_evi, n_chain)
143
+
144
+ # ---- Mixture (still symmetric) ----
145
+ use_local = torch.rand((n_evi, n_chain), device=device) < alpha
146
+ x_new = torch.where(use_local, x_local, x_global)
147
+
148
+ # ---- Ensure x_new != x (important for MH efficiency) ----
149
+ same = x_new == x
150
+ if same.any():
151
+ r = torch.randint(0, K - 1, size=(same.sum(),), device=device)
152
+ x_new[same] = r + (r >= x[same]).to(r.dtype)
153
+
154
+ return x_new
155
+
156
+
157
+ def propose_continuous_rw_gaussian(x: torch.Tensor, sigma: float) -> torch.Tensor:
158
+ """
159
+ Symmetric Gaussian random-walk proposal.
160
+ x: (n_evi, n_chain) float tensor
161
+ """
162
+ return x + torch.randn_like(x) * sigma
163
+
164
+
165
+ # -----------------------------
166
+ # Adaptive, Block MH Sampler
167
+ # -----------------------------
168
+ @dataclass
169
+ class AdaptConfig:
170
+ burnin: int = 200
171
+ gamma: float = 0.2 # Robbins–Monro exponent in (0.5, 1]
172
+ target_accept: float = 0.234 # target acceptable rate for block updates
173
+ min_log_sigma: float = -12.0 # clamp for stability
174
+ max_log_sigma: float = 6.0
175
+ alpha: float = 0.5 # for discrete vars, pi(new) = alpha * pi_local(new | old) + (1-alpha) * pi_global(new)
176
+ # pi_local(new | old): uniform leap between states (for symmetry)
177
+ # pi_global(new): distribution of previous sampled states
178
+
179
+
180
+ class HybridAdaptiveMH:
181
+ """
182
+ Tensorised Metropolis–Hastings for hybrid (discrete + continuous) BN,
183
+ with many evidence rows and many chains.
184
+
185
+ - Latent variable values are stored in `state[var_name]` with shape (n_evi, n_chain).
186
+ - Evidence is stored in `evidence_1d[var_name]` with shape (n_evi,).
187
+ - Each MH step updates ONE variable at a time (easy and robust).
188
+ (You can extend to blocks later.)
189
+ """
190
+
191
+ def __init__(
192
+ self,
193
+ probs: Dict[str, Any],
194
+ variables: Dict[str, Any] | Sequence[Any], # list of Variable objects (all variables)
195
+ evidence_df, # pandas DataFrame: shape (n_evi, ...)
196
+ n_chain: int,
197
+ device: str | torch.device = "cpu",
198
+ adapt: AdaptConfig = AdaptConfig()
199
+ ):
200
+ self.probs = probs
201
+ if isinstance(variables, dict):
202
+ variables = list(variables.values())
203
+ else:
204
+ self.variables = list(variables)
205
+ self.device = torch.device(device)
206
+ self.adapt = adapt
207
+
208
+ # Evidence as 1D tensors (n_evi,)
209
+ self.n_evi = len(evidence_df)
210
+ self.n_chain = n_chain
211
+
212
+ self.evidence_1d: Dict[str, torch.Tensor] = {}
213
+ for col in evidence_df.columns:
214
+ # keep dtype flexible: discrete evidence should still be numeric indices in df if used
215
+ t = torch.tensor(evidence_df[col].values, device=self.device)
216
+ if t.dtype not in (torch.int64, torch.int32, torch.float32, torch.float64):
217
+ t = t.to(torch.float32)
218
+ self.evidence_1d[col] = t
219
+
220
+ # Determine which vars are evidence vs latent
221
+ self.evidence_vars: Set[str] = set(evidence_df.columns)
222
+ self.latent_vars: List[Any] = [v for v in self.variables if v.name not in self.evidence_vars]
223
+
224
+ # Adjacency: var_name -> factors
225
+ self.factors_by_var = build_factors_by_var(self.probs)
226
+
227
+ # Proposal params for continuous latent vars
228
+ self.log_sigma: Dict[str, torch.Tensor] = {}
229
+ for v in self.latent_vars:
230
+ if is_continuous(v):
231
+ self.log_sigma[v.name] = torch.tensor(0.0, device=self.device) # sigma=1 initially
232
+
233
+ # State placeholders
234
+ self.state: Dict[str, torch.Tensor] = {} # var_name -> (n_evi, n_chain) tensor
235
+
236
+ # Cache: per-factor logp (n_evi, n_chain) for current state (optional but useful)
237
+ self.factor_logp_cache: Dict[int, torch.Tensor] = {} # key by id(prob)
238
+
239
+ # Cache: total logp per evidence×chain and aggregated per chain
240
+ self.logp_evi_chain: Optional[torch.Tensor] = None
241
+ self.logp_chain: Optional[torch.Tensor] = None
242
+
243
+ # Proposal params for discrete latent vars (global adaptive logits)
244
+ self.discrete_logits: Dict[str, torch.Tensor] = {}
245
+ for v in self.latent_vars:
246
+ if is_discrete(v):
247
+ K = num_categories(v)
248
+ self.discrete_logits[v.name] = torch.zeros(
249
+ K, device=self.device, dtype=torch.float32
250
+ )
251
+
252
+ # Helper: get affected factors for a block of variables
253
+ def _affected_factors_for_block(self, var_names):
254
+ affected = []
255
+ seen = set()
256
+
257
+ for name in var_names:
258
+ for prob in self.factors_by_var.get(name, []):
259
+ pid = id(prob)
260
+ if pid not in seen:
261
+ affected.append(prob)
262
+ seen.add(pid)
263
+ return affected
264
+
265
+ # Initialisation
266
+ def init_state_from_forward_samples(self, probs_copy: Dict[str, Any]):
267
+ """
268
+ Initialise latent state from a forward-sampling result compatible with your sample_evidence(),
269
+ i.e. each prob in probs_copy has prob.Cs of shape (n_evi, n_sample, ...)
270
+ and prob.ps of shape (n_evi, n_sample).
271
+ We treat n_sample as n_chain here.
272
+ """
273
+ # Build samples[var_name] from prob.Cs columns (childs first).
274
+ samples: Dict[str, torch.Tensor] = {}
275
+ for prob in probs_copy.values():
276
+ Cs = prob.Cs # (n_evi, n_chain, dim)
277
+ for j, child_var in enumerate(prob.childs):
278
+ samples[child_var.name] = Cs[:, :, j]
279
+
280
+ # Keep only latent vars (evidence vars are fixed)
281
+ for v in self.latent_vars:
282
+ if v.name not in samples:
283
+ raise KeyError(f"Latent variable '{v.name}' not found in forward samples. "
284
+ f"Make sure ancestors include it and it is sampled.")
285
+ x = samples[v.name].to(self.device)
286
+
287
+ # dtype rules
288
+ if is_discrete(v):
289
+ x = x.to(torch.long)
290
+ else:
291
+ x = x.to(torch.float32)
292
+ self.state[v.name] = x
293
+
294
+ # Compute caches
295
+ self._recompute_all_logps()
296
+
297
+ def init_state_random(self, seed: Optional[int] = None):
298
+ """
299
+ Fallback initializer if you don't want to rely on forward sampling.
300
+ Discrete: uniform over categories
301
+ Continuous: standard normal
302
+ """
303
+ if seed is not None:
304
+ torch.manual_seed(seed)
305
+
306
+ for v in self.latent_vars:
307
+ if is_discrete(v):
308
+ K = num_categories(v)
309
+ x = torch.randint(0, K, (self.n_evi, self.n_chain), device=self.device, dtype=torch.long)
310
+ else:
311
+ x = torch.randn((self.n_evi, self.n_chain), device=self.device, dtype=torch.float32)
312
+ self.state[v.name] = x
313
+
314
+ self._recompute_all_logps()
315
+
316
+ # Log-probability bookkeeping
317
+ def _recompute_all_logps(self):
318
+ """
319
+ Compute and cache:
320
+ - per-factor logp (n_evi, n_chain)
321
+ - total logp_evi_chain (n_evi, n_chain)
322
+ - total logp_chain (n_chain,) = sum over evidence rows
323
+ """
324
+ logp_evi_chain = torch.zeros((self.n_evi, self.n_chain), device=self.device, dtype=torch.float32)
325
+ self.factor_logp_cache.clear()
326
+
327
+ for prob in self.probs.values():
328
+ Cs = build_Cs_3d(prob, self.state, self.evidence_1d)
329
+ logp2d = factor_logp_2d(prob, Cs).to(torch.float32)
330
+ self.factor_logp_cache[id(prob)] = logp2d
331
+ logp_evi_chain += logp2d
332
+
333
+ self.logp_evi_chain = logp_evi_chain
334
+ self.logp_chain = logp_evi_chain.sum(dim=0) # (n_chain,)
335
+
336
+ # One MH update for one variable
337
+ @torch.no_grad()
338
+ def mh_update_block(self, vars: List[variable.Variable], iteration: int) -> torch.Tensor:
339
+ """
340
+ Metropolis–Hastings update for a block of variables.
341
+ vars: list[Variable]
342
+ Returns accept mask: (n_chain,)
343
+ """
344
+ var_names = [v.name for v in vars]
345
+
346
+ # 1. Propose block
347
+ proposed = self._propose_block(vars) # proposed: Dict[str, torch.Tensor]; proposed[v.name].shape == (n_evi, n_chain)
348
+
349
+ # 2. Find affected factors
350
+ affected = self._affected_factors_for_block(var_names) # List[ProbabilityObject]
351
+
352
+ delta_evi_chain = torch.zeros(
353
+ (self.n_evi, self.n_chain),
354
+ device=self.device,
355
+ dtype=torch.float32,
356
+ )
357
+
358
+ # 3. Temporarily swap in proposed values
359
+ old_values = {}
360
+ for name in var_names:
361
+ old_values[name] = self.state[name]
362
+ self.state[name] = proposed[name]
363
+
364
+ # 4. Compute Δ log-probability
365
+ for prob in affected:
366
+ logp_old_2d = self.factor_logp_cache[id(prob)] # self.factor_logp_cache: Dict[int, torch.Tensor];
367
+ # self.factor_logp_cache[id(prob)].shape == (n_evi, n_chain)
368
+ Cs_new = build_Cs_3d(prob, self.state, self.evidence_1d)
369
+ logp_new_2d = factor_logp_2d(prob, Cs_new)
370
+ delta_evi_chain += (logp_new_2d - logp_old_2d)
371
+
372
+ # 5. Restore old values before accept/reject
373
+ for name, x_old in old_values.items():
374
+ self.state[name] = x_old
375
+
376
+ # 6. Aggregate over evidence
377
+ log_alpha = delta_evi_chain.sum(dim=0) # (n_chain,)
378
+
379
+ # 7. Accept / reject
380
+ u = torch.log(torch.rand((self.n_chain,), device=self.device))
381
+ accept = u < log_alpha
382
+
383
+ # 8. Apply accepted updates and refresh caches
384
+ if accept.any():
385
+ for name in var_names:
386
+ self.state[name][:, accept] = proposed[name][:, accept]
387
+
388
+ for prob in affected:
389
+ logp_old_2d = self.factor_logp_cache[id(prob)]
390
+ Cs_upd = build_Cs_3d(prob, self.state, self.evidence_1d)
391
+ logp_upd_2d = factor_logp_2d(prob, Cs_upd)
392
+ self.factor_logp_cache[id(prob)] = logp_upd_2d
393
+ self.logp_evi_chain += (logp_upd_2d - logp_old_2d)
394
+
395
+ self.logp_chain = self.logp_evi_chain.sum(dim=0)
396
+
397
+ # 9. Adaptation (continuous + discrete vars in block)
398
+ if iteration < self.adapt.burnin:
399
+ acc_rate = accept.float().mean().item()
400
+ eta = (iteration + 1.0) ** (-self.adapt.gamma)
401
+
402
+ for v in vars:
403
+ name = v.name
404
+
405
+ # ---- Continuous adaptation (unchanged) ----
406
+ if is_continuous(v):
407
+ target = self.adapt.target_accept
408
+ new_log_sigma = self.log_sigma[name] + eta * (acc_rate - target)
409
+ self.log_sigma[name] = new_log_sigma.clamp(
410
+ self.adapt.min_log_sigma,
411
+ self.adapt.max_log_sigma,
412
+ )
413
+
414
+ # ---- Discrete adaptation (NEW) ----
415
+ elif is_discrete(v):
416
+ K = num_categories(v)
417
+
418
+ # accepted samples for this variable
419
+ x_acc = self.state[name][:, accept] # (n_evi, n_accept)
420
+
421
+ if x_acc.numel() > 0:
422
+ # empirical frequencies
423
+ hist = torch.bincount(
424
+ x_acc.flatten(),
425
+ minlength=K
426
+ ).float()
427
+
428
+ hist = hist / hist.sum().clamp_min(1.0)
429
+
430
+ # Robbins–Monro update of global logits
431
+ pi = torch.softmax(self.discrete_logits[name], dim=0)
432
+ self.discrete_logits[name] += eta * (hist - pi)
433
+
434
+ return accept # accept : torch.BoolTensor of shape (n_chain,)
435
+
436
+ def _build_update_schedule(self, update_blocks):
437
+ if update_blocks is None:
438
+ return [[v] for v in self.latent_vars]
439
+
440
+ name_to_var = {v.name: v for v in self.latent_vars}
441
+
442
+ blocks = []
443
+ used = set()
444
+
445
+ for block_names in update_blocks:
446
+ block_vars = [name_to_var[n] for n in block_names]
447
+ blocks.append(block_vars)
448
+ used.update(block_names)
449
+
450
+ # leftover variables → singletons
451
+ for v in self.latent_vars:
452
+ if v.name not in used:
453
+ blocks.append([v])
454
+
455
+ return blocks
456
+
457
+ # Helper: block proposal
458
+ def _propose_block(self, vars):
459
+ """
460
+ Propose new values for a block of variables.
461
+ Returns dict {var_name: x_new_tensor}
462
+ """
463
+ proposed = {}
464
+ for v in vars:
465
+ name = v.name
466
+ x_old = self.state[name]
467
+
468
+ if is_discrete(v):
469
+ K = num_categories(v)
470
+ x_new = propose_discrete_adaptive(x_old, self.discrete_logits[name], self.adapt.alpha)
471
+ else:
472
+ sigma = float(torch.exp(self.log_sigma[name]).clamp_min(1e-12).item())
473
+ x_new = propose_continuous_rw_gaussian(x_old, sigma)
474
+
475
+ proposed[name] = x_new
476
+
477
+ return proposed
478
+
479
+ # Run
480
+ @torch.no_grad()
481
+ def run(
482
+ self,
483
+ n_iter: int,
484
+ update_blocks: Optional[List[List[str]]] = None,
485
+ store_every: int = 0,
486
+ progress_every: int = 100,
487
+ ) -> Dict[str, Any]:
488
+ """
489
+ Run MCMC with optional block updates.
490
+
491
+ Parameters
492
+ ----------
493
+ n_iter : int
494
+ Number of MCMC iterations.
495
+ update_blocks : list[list[str]] or None
496
+ Variable blocks to update together.
497
+ - None → update all latent variables one by one
498
+ - Each inner list is a block of variable names
499
+ - Variables not listed are updated individually
500
+ store_every : int
501
+ If >0, stores thinned samples every `store_every` iterations.
502
+ Warning: storing full (n_evi, n_chain) states can be very large.
503
+
504
+ Returns
505
+ -------
506
+ dict with:
507
+ - 'accept_rate': dict[block_name -> float]
508
+ - 'logp_chain': final (n_chain,)
509
+ - 'logp_evi_chain': final (n_evi, n_chain)
510
+ - 'states_thinned' (optional)
511
+ - 'log_sigma' (continuous proposal scales)
512
+ """
513
+ # --------------------------------------------------
514
+ # Safety check
515
+ # --------------------------------------------------
516
+ if self.logp_chain is None or self.logp_evi_chain is None:
517
+ raise RuntimeError(
518
+ "State not initialised. Call init_state_from_forward_samples() "
519
+ "or init_state_random()."
520
+ )
521
+
522
+ # --------------------------------------------------
523
+ # Build update schedule (blocks)
524
+ # --------------------------------------------------
525
+ name_to_var = {v.name: v for v in self.latent_vars}
526
+
527
+ if update_blocks is None:
528
+ # default: singleton blocks
529
+ blocks = [[v] for v in self.latent_vars]
530
+ else:
531
+ blocks = [] # List[variable.Variable]
532
+ used = set()
533
+
534
+ for block_names in update_blocks:
535
+ block_vars = []
536
+ for nm in block_names:
537
+ if nm not in name_to_var:
538
+ print(
539
+ f"[HybridAdaptiveMH] Skipping '{nm}' in update_blocks: "
540
+ "not a latent variable."
541
+ )
542
+ continue
543
+ block_vars.append(name_to_var[nm])
544
+ blocks.append(block_vars)
545
+ used.update(block_names)
546
+
547
+ # add leftover latent variables as singleton blocks
548
+ for v in self.latent_vars:
549
+ if v.name not in used:
550
+ blocks.append([v])
551
+
552
+ # --------------------------------------------------
553
+ # Bookkeeping
554
+ # --------------------------------------------------
555
+ def block_name(block):
556
+ if len(block) == 1:
557
+ return block[0].name
558
+ return "(" + ",".join(v.name for v in block) + ")"
559
+
560
+ block_names = [block_name(b) for b in blocks] # List[str]
561
+
562
+ accept_counts = {bn: 0.0 for bn in block_names}
563
+ total_updates = {bn: 0.0 for bn in block_names}
564
+
565
+ states_thinned = [] if (store_every and store_every > 0) else None
566
+
567
+ # --------------------------------------------------
568
+ # Main MCMC loop
569
+ # --------------------------------------------------
570
+ total_time = 0.0
571
+ st = time.time()
572
+ for it in range(n_iter):
573
+ for block, bn in zip(blocks, block_names):
574
+ accept = self.mh_update_block(block, iteration=it) # accept.shape == (n_chain,)
575
+
576
+ accept_rate_step = float(accept.float().mean().item())
577
+ accept_counts[bn] += accept_rate_step
578
+ total_updates[bn] += 1.0
579
+
580
+ if states_thinned is not None and ((it + 1) % store_every == 0):
581
+ # WARNING: this can be huge
582
+ snap = {k: v.detach().clone() for k, v in self.state.items()}
583
+ states_thinned.append(snap)
584
+
585
+ # ---- progress ----
586
+ if progress_every > 0 and (it + 1) % progress_every == 0:
587
+ et = time.time()
588
+ time1 = et - st
589
+ total_time += time1
590
+ st = et
591
+
592
+ # running acceptance rates
593
+ acc_str = ", ".join(
594
+ f"{bn}={accept_counts[bn] / max(total_updates[bn], 1):.2f}"
595
+ for bn in block_names
596
+ )
597
+
598
+ print(
599
+ f"[MCMC] {it+1}/{n_iter} iterations completed "
600
+ f"({n_iter - it - 1} remaining) "
601
+ f"{time1:.2f} sec taken, total {total_time:.2f} sec\n"
602
+ f" accept: {acc_str}"
603
+ )
604
+ # --------------------------------------------------
605
+ # Outputs
606
+ # --------------------------------------------------
607
+ accept_rate = {
608
+ bn: accept_counts[bn] / max(total_updates[bn], 1.0)
609
+ for bn in block_names
610
+ }
611
+
612
+ log_sigma_out = {
613
+ k: float(v.detach().cpu().item())
614
+ for k, v in self.log_sigma.items()
615
+ }
616
+
617
+ out = {
618
+ "accept_rate": accept_rate,
619
+ "logp_chain": self.logp_chain.detach().clone(),
620
+ "logp_evi_chain": self.logp_evi_chain.detach().clone(),
621
+ "log_sigma": log_sigma_out,
622
+ }
623
+
624
+ if states_thinned is not None:
625
+ out["states_thinned"] = states_thinned
626
+
627
+ return out