Source code for physicsnemo.diffusion.noise_schedulers.ve

# SPDX-FileCopyrightText: Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Variance Exploding (VE) noise scheduler."""

import math

import torch
from jaxtyping import Float
from torch import Tensor

from .linear_gaussian import LinearGaussianNoiseScheduler


[docs] class VENoiseScheduler(LinearGaussianNoiseScheduler): r""" Variance Exploding (VE) noise scheduler. Implements the VE formulation with :math:`\sigma(t) = \sqrt{t}` and :math:`\alpha(t) = 1` (no signal attenuation). **Sampling time-steps** use geometric spacing in :math:`\sigma^2` space: .. math:: \sigma_i^2 = \sigma_{\max}^2 \cdot \left(\frac{\sigma_{\min}^2}{\sigma_{\max}^2}\right)^{i/(N-1)} **Training times** are sampled log-uniformly between ``sigma_min`` and ``sigma_max``, then mapped to time via :math:`t = \sigma^2`. Parameters ---------- sigma_min : float, optional Minimum noise level, by default 0.02. sigma_max : float, optional Maximum noise level, by default 100. Note ---- Reference: `Score-Based Generative Modeling through Stochastic Differential Equations <https://arxiv.org/abs/2011.13456>`_ Examples -------- Basic training and sampling workflow using the VE noise scheduler: >>> import torch >>> from physicsnemo.diffusion.noise_schedulers import VENoiseScheduler >>> >>> scheduler = VENoiseScheduler(sigma_min=0.02, sigma_max=100.0) >>> >>> # Training: sample times and add noise >>> x0 = torch.randn(4, 3, 8, 8) # Clean data >>> t = scheduler.sample_time(4) # Sample diffusion times >>> x_t = scheduler.add_noise(x0, t) # Create noisy samples >>> x_t.shape torch.Size([4, 3, 8, 8]) >>> >>> # Sampling: generate timesteps and initial latents >>> t_steps = scheduler.timesteps(10) >>> tN = t_steps[0].expand(4) # Initial time for batch of 4 >>> xN = scheduler.init_latents((3, 8, 8), tN) # Initial noise >>> xN.shape torch.Size([4, 3, 8, 8]) >>> >>> # Convert x0-predictor to denoiser for sampling >>> x0_predictor = lambda x, t: x / (1 + t.view(-1, 1, 1, 1)**2) # Toy x0-predictor >>> denoiser = scheduler.get_denoiser(x0_predictor=x0_predictor) >>> denoiser(xN, tN).shape # ODE RHS for sampling torch.Size([4, 3, 8, 8]) """ def __init__( self, sigma_min: float = 0.02, sigma_max: float = 100.0, ) -> None: self.sigma_min = sigma_min self.sigma_max = sigma_max
[docs] def sigma( self, t: Float[Tensor, " *shape"], ) -> Float[Tensor, " *shape"]: r"""VE noise coefficient: :math:`\sigma(t) = \sqrt{t}`.""" return t.sqrt()
[docs] def sigma_inv( self, sigma: Float[Tensor, " *shape"], ) -> Float[Tensor, " *shape"]: r"""Inverse VE mapping: :math:`t = \sigma^2`.""" return sigma**2
[docs] def sigma_dot( self, t: Float[Tensor, " *shape"], ) -> Float[Tensor, " *shape"]: r"""Time derivative: :math:`\dot{\sigma}(t) = 1/(2\sqrt{t})`.""" return 0.5 / t.sqrt()
[docs] def alpha( self, t: Float[Tensor, " *shape"], ) -> Float[Tensor, " *shape"]: r"""Constant signal coefficient: :math:`\alpha(t) = 1`.""" return torch.ones_like(t)
[docs] def alpha_dot( self, t: Float[Tensor, " *shape"], ) -> Float[Tensor, " *shape"]: r"""Zero derivative: :math:`\dot{\alpha}(t) = 0`.""" return torch.zeros_like(t)
[docs] def timesteps( self, num_steps: int, *, device: torch.device | None = None, dtype: torch.dtype | None = None, ) -> Float[Tensor, " N+1"]: r""" Generate VE time-steps with geometric spacing in :math:`\sigma^2`. Parameters ---------- num_steps : int Number of sampling steps. device : torch.device, optional Device to place the tensor on. dtype : torch.dtype, optional Data type of the tensor. Returns ------- torch.Tensor Time-steps tensor of shape :math:`(N + 1,)`. """ step_indices = torch.arange(num_steps, dtype=dtype, device=device) ratio = self.sigma_min**2 / self.sigma_max**2 exponent = step_indices / (num_steps - 1) t_steps = (self.sigma_max**2) * (ratio**exponent) zero = torch.zeros(1, dtype=dtype, device=device) return torch.cat([t_steps, zero])
[docs] def sample_time( self, N: int, *, device: torch.device | None = None, dtype: torch.dtype | None = None, ) -> Float[Tensor, " N"]: r""" Sample N diffusion times log-uniformly in sigma space, mapped to time. Parameters ---------- N : int Number of time values to sample. device : torch.device, optional Device to place the tensor on. dtype : torch.dtype, optional Data type of the tensor. Returns ------- Tensor Sampled diffusion times of shape :math:`(N,)`. """ u = torch.rand(N, device=device, dtype=dtype) log_ratio = math.log(self.sigma_max / self.sigma_min) sigma = self.sigma_min * torch.exp(u * log_ratio) return self.sigma_inv(sigma)
[docs] def loss_weight( self, t: Float[Tensor, " N"], ) -> Float[Tensor, " N"]: r""" Compute VE loss weight: :math:`w(t) = 1 / \sigma(t)^2`. .. important:: This loss weight is designed for training an x0-predictor (clean data predictor) wrapped with :class:`~physicsnemo.diffusion.preconditioners.VEPreconditioner`. It is not suitable for training a score-predictor, or a model without a pre-conditioner. Parameters ---------- t : Tensor Diffusion time values of shape :math:`(N,)`. Returns ------- Tensor Loss weight of shape :math:`(N,)`. """ return 1 / self.sigma(t) ** 2