| |
| |
|
|
| from typing import Literal, Optional |
|
|
| import torch |
|
|
| from ...diffusion.corruption.sde_lib import SDE, maybe_expand |
| from ...diffusion.data.batched_data import BatchedData |
| from ...diffusion.training.field_loss import aggregate_per_sample |
|
|
|
|
| def get_pbc_offsets(pbc: torch.Tensor, max_offset_integer: int = 3) -> torch.Tensor: |
| """Build the Cartesian product of integer offsets of the periodic boundary. That is, if dim=3 and max_offset_integer=1 we build the (2*1 + 1)^3 = 27 |
| possible combinations of the Cartesian product of (i,j,k) for i,j,k in -max_offset_integer, ..., max_offset_integer. Then, we construct |
| the tensor of integer offsets of the pbc vectors, i.e., L_{ijk} = row_stack([i * l_1, j * l_2, k * l_3]). |
| |
| Args: |
| pbc (torch.Tensor, [batch_size, dim, dim]): The input pbc matrix. |
| max_offset_integer (int): The maximum integer offset per dimension to consider for the Cartesian product. Defaults to 3. |
| |
| Returns: |
| torch.Tensor, [batch_size, (2 * max_offset_integer + 1)^dim, dim]: The tensor containing the integer offsets of the pbc vectors. |
| """ |
| offset_range = torch.arange(-max_offset_integer, max_offset_integer + 1, device=pbc.device) |
| meshgrid = torch.stack( |
| torch.meshgrid(offset_range, offset_range, offset_range, indexing="xy"), dim=-1 |
| ) |
| offset = (pbc[:, None, None, None] * meshgrid[None, :, :, :, :, None]).sum(-2) |
| pbc_offset_per_molecule = offset.reshape(pbc.shape[0], -1, 3) |
| return pbc_offset_per_molecule |
|
|
|
|
| def wrapped_normal_score( |
| x: torch.Tensor, |
| mean: torch.Tensor, |
| wrapping_boundary: torch.Tensor, |
| variance_diag: torch.Tensor, |
| batch: torch.Tensor, |
| max_offset_integer: int = 3, |
| ) -> torch.Tensor: |
| """Approximate the the score of a 3D wrapped normal distribution with diagonal covariance matrix w.r.t. x via a truncated sum. |
| See docstring of `wrapped_normal_score` for details about the arguments |
| |
| Args: |
| x (torch.Tensor, [num_atoms, dim]) |
| mean (torch.Tensor, [num_atoms, dim]) |
| wrapping_boundary (torch.Tensor, [num_molecules, dim, dim]) |
| variance_diag (torch.Tensor, [num_atoms,]) |
| batch (torch.Tensor, [num_atoms, ]) |
| max_offset_integer (int), Defaults to 3. |
| |
| Returns: |
| torch.Tensor, [num_atoms, dim]: The approximated score of the wrapped normal distribution. |
| """ |
| offset_add = get_pbc_offsets( |
| wrapping_boundary, |
| max_offset_integer, |
| ) |
| diffs_k = (x - mean)[:, None] + offset_add[batch] |
| dists_sqr_k = diffs_k.pow(2).sum(-1) |
| score_softmax = torch.softmax(-dists_sqr_k / (2 * variance_diag[:, None]), dim=-1) |
| score = -(score_softmax[:, :, None] * diffs_k).sum((-2)) / (variance_diag[:, None]) |
| return score |
|
|
|
|
| def wrapped_normal_loss( |
| *, |
| corruption: SDE, |
| score_model_output: torch.Tensor, |
| t: torch.Tensor, |
| batch_idx: Optional[torch.LongTensor], |
| batch_size: int, |
| x: torch.Tensor, |
| noisy_x: torch.Tensor, |
| reduce: Literal["sum", "mean"], |
| batch: BatchedData, |
| **_ |
| ) -> torch.Tensor: |
| """Compute the loss for a wrapped normal distribution. |
| Compares the score of the wrapped normal distribution to the score of the score model. |
| """ |
| assert len(t) == batch_size |
| _, std = corruption.marginal_prob( |
| x=torch.zeros((x.shape[0], 1), device=t.device), |
| t=t, |
| batch_idx=batch_idx, |
| batch=batch, |
| ) |
|
|
| pred: torch.Tensor = score_model_output |
| if pred.ndim != 2: |
| raise NotImplementedError |
|
|
| assert hasattr( |
| corruption, "wrapping_boundary" |
| ), "SDE must be a WrappedSDE, i.e., must have a wrapping boundary." |
| wrapping_boundary = corruption.wrapping_boundary |
| |
| wrapping_boundary = wrapping_boundary * torch.eye(x.shape[-1], device=t.device)[None].expand( |
| batch_size, -1, -1 |
| ) |
|
|
| |
| target = ( |
| wrapped_normal_score( |
| x=noisy_x, |
| mean=x, |
| wrapping_boundary=wrapping_boundary, |
| variance_diag=std.squeeze() ** 2, |
| batch=batch_idx, |
| ) |
| * std |
| ) |
| delta = target - pred |
|
|
| losses = delta.square() |
|
|
| return aggregate_per_sample(losses, batch_idx, reduce=reduce, batch_size=batch_size) |
|
|