Source code for bayesflow.datasets.ensemble_indexed_dataset

from collections.abc import Sequence
from typing import Any

import math
import numpy as np
import keras

from bayesflow.utils import logging

from .helpers import ring_starts, ring_window_indices


[docs] class EnsembleIndexedDataset(keras.utils.PyDataset): """Ensemble batches drawn from member-specific windows into an indexable dataset. See :class:`~bayesflow.datasets.EnsembleDataset`, which is the recommended entry point. Parameters ---------- dataset : keras.utils.PyDataset An indexable BayesFlow dataset (OfflineDataset, DiskDataset). member_names : Sequence[str] Names of ensemble members, used as dictionary keys. data_reuse : float, default=1.0 Degree of independence between ensemble members in ``[0, 1]``. drop_last : bool, optional Whether to drop the last step of each epoch if the member windows have fewer than ``batch_size`` samples left. If ``None`` (the default), the setting is inherited from the wrapped dataset. """ def __init__( self, dataset: keras.utils.PyDataset, member_names: Sequence[str], data_reuse: float = 1.0, drop_last: bool | None = None, **kwargs, ): super().__init__(**kwargs) if len(member_names) < 2: raise ValueError("EnsembleIndexedDataset: len(member_names) must be >= 2.") if not (0.0 <= data_reuse <= 1.0): raise ValueError("EnsembleIndexedDataset: data_reuse must be in [0, 1].") for attr in ("batch_size", "num_samples", "get_batch_by_sample_indices"): if not hasattr(dataset, attr): raise TypeError(f"EnsembleIndexedDataset: wrapped dataset must expose `{attr}`.") self.dataset = dataset self.member_names = list(member_names) self.ensemble_size = len(member_names) self.data_reuse = float(data_reuse) self.batch_size = int(dataset.batch_size) self.num_samples = int(dataset.num_samples) self.drop_last = bool(getattr(dataset, "drop_last", False) if drop_last is None else drop_last) self.reduction_factor = 1 / (data_reuse + (1 - data_reuse) * self.ensemble_size) self.window_size = int(math.ceil(self.num_samples * self.reduction_factor)) if self.drop_last: self.steps_per_epoch = self.window_size // self.batch_size else: self.steps_per_epoch = int(math.ceil(self.window_size / self.batch_size)) pool = np.arange(self.num_samples, dtype="int64") starts = ring_starts(self.num_samples, self.ensemble_size) idx2d = ring_window_indices(self.num_samples, self.window_size, starts) # (E, W) self.member_indices = {name: pool[idx2d[k]].copy() for k, name in enumerate(self.member_names)} self.on_epoch_end() logging.info( f"EnsembleIndexedDataset: ensemble_size={self.ensemble_size}, " f"batch_size={self.batch_size}, num_samples={self.num_samples}, " f"data_reuse={self.data_reuse} -> " f"reduction_factor={self.reduction_factor:.2f}, window_size={self.window_size}, " f"steps_per_epoch={self.steps_per_epoch}, drop_last={self.drop_last}. " "Overlap is enforced at the subdataset level (member-specific windows into the global index pool)." ) def __len__(self) -> int: return self.steps_per_epoch
[docs] def on_epoch_end(self): if self.data_reuse == 1.0: np.random.shuffle(self.member_indices[self.member_names[0]]) for name in self.member_names[1:]: self.member_indices[name] = self.member_indices[self.member_names[0]] return # otherwise independent shuffle per member for name in self.member_names: np.random.shuffle(self.member_indices[name])
def __getitem__(self, step: int) -> dict[str, dict[str, Any]]: # copy so we can give error messages with the original input original_step = step if step < 0: step += self.steps_per_epoch if not 0 <= step < self.steps_per_epoch: raise IndexError(f"Index {original_step} is out of bounds for dataset with {self.steps_per_epoch} steps.") start = step * self.batch_size stop = min((step + 1) * self.batch_size, self.window_size) # allow shorter last batch if drop_last is False out: dict[str, dict[str, Any]] = {} for name in self.member_names: idx = self.member_indices[name][start:stop] out[name] = self.dataset.get_batch_by_sample_indices(idx) return self._flip_nested_dict(out) def _flip_nested_dict(self, d: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: flipped = {} for key, val in d.items(): for subkey, subval in val.items(): flipped.setdefault(subkey, {}) flipped[subkey][key] = subval return flipped