Source code for bayesflow.networks.inference.diffusion.schedules.cosine_noise_schedule

import math
from typing import Literal

from keras import ops

from bayesflow.types import Tensor
from bayesflow.utils.serialization import serializable

from .noise_schedule import NoiseSchedule


[docs] @serializable("bayesflow.networks") class CosineNoiseSchedule(NoiseSchedule): """Cosine noise schedule for diffusion models. This schedule is based on the cosine schedule from [1]. A cosine schedule is a popular technique for controlling how the variance (noise level) or learning rate evolves during the training of diffusion models. It was proposed as an improvement over the original linear beta schedule in [2]. The default `weighting=None` corresponds to the settings used in the benchmark of [3]. [1] Dhariwal, P., & Nichol, A. (2021). Diffusion models beat gans on image synthesis. Advances in Neural Information Processing Systems, 34, 8780-8794. [2] Ho, J., Jain, A., & Abbeel, P. (2020). Denoising diffusion probabilistic models. Advances in Neural Information Processing Systems, 33, 6840-6851. [3] Arruda, J., Bracher, N., Köthe, U., Hasenauer, J., & Radev, S. T. (2026). Diffusion Models in Simulation-Based Inference: A Tutorial Review. https://openreview.net/forum?id=86Q261z005 Parameters ---------- min_log_snr : float, optional The minimum log signal-to-noise ratio (lambda). Default is -12. max_log_snr : float, optional The maximum log signal-to-noise ratio (lambda). Default is 12. shift : float, optional Shift the log signal-to-noise ratio (lambda) by this amount. Default is 0.0. For images, use shift = log(base_resolution / d), where d is the used resolution of the image. weighting : Literal["likelihood_weighting"] | None, optional The type of weighting function to use for the noise schedule. Default is `None`. """ def __init__( self, min_log_snr: float = -12, max_log_snr: float = 12, shift: float = 0.0, weighting: Literal["likelihood_weighting"] | None = None, ): super().__init__(name="cosine_noise_schedule", variance_type="preserving", weighting=weighting) self._shift = shift self.log_snr_min = min_log_snr self.log_snr_max = max_log_snr self._t_min = self._truncated_t_from_log_snr(self.log_snr_max) self._t_max = self._truncated_t_from_log_snr(self.log_snr_min) def _truncated_t(self, t: Tensor) -> Tensor: return self._t_min + (self._t_max - self._t_min) * t def _untruncated_t(self, t: Tensor) -> Tensor: return (t - self._t_min) / (self._t_max - self._t_min) def _truncated_t_from_log_snr(self, log_snr_t: Tensor | float) -> Tensor: """Map log-SNR to the cosine coordinate before endpoint normalization.""" return 2 / math.pi * ops.arctan(ops.exp((2 * self._shift - log_snr_t) * 0.5))
[docs] def log_snr(self, t: Tensor | float, training: bool) -> Tensor: """Get the log signal-to-noise ratio (lambda) for a given diffusion time.""" t_trunc = self._truncated_t(t) return -2 * ops.log(ops.tan(math.pi * t_trunc * 0.5)) + 2 * self._shift
[docs] def time_from_log_snr(self, log_snr_t: Tensor | float, training: bool) -> Tensor: """Get the diffusion time (t) from the log signal-to-noise ratio (lambda).""" # SNR = -2 * log(tan(pi*t/2)) => t = 2/pi * arctan(exp(-snr/2)) truncated_t = self._truncated_t_from_log_snr(log_snr_t) return self._untruncated_t(truncated_t)
[docs] def derivative_log_snr(self, log_snr_t: Tensor, training: bool) -> Tensor: """Compute d/dt log(1 + e^(-snr(t))), which is used for the reverse SDE.""" t = self.time_from_log_snr(log_snr_t=log_snr_t, training=training) # Compute the truncated time t_trunc t_trunc = self._truncated_t(t) dsnr_dx = -(2 * math.pi) / ops.sin(math.pi * t_trunc) # Using the chain rule on f(t) = log(1 + e^(-snr(t))): # f'(t) = - (e^{-snr(t)} / (1 + e^{-snr(t)})) * dsnr_dt dsnr_dt = dsnr_dx * (self._t_max - self._t_min) factor = ops.exp(-log_snr_t) / (1 + ops.exp(-log_snr_t)) return -factor * dsnr_dt
[docs] def get_config(self): config = { "min_log_snr": self.log_snr_min, "max_log_snr": self.log_snr_max, "shift": self._shift, "weighting": self._weighting, } return config