Source code for waxmorph.torch.warp_autograd

"""Torch autograd adapters for Warp mechanics and diffusion.

Each forward call owns a fresh :class:`warp.Tape` and a frozen neighbor list. Backward seeds
the output adjoint and replays that tape. The gradient contract covers the documented Torch
state.
Torch uses the input's Warp-compatible device. JAX uses custom VJPs on CUDA. Each bridge
provides its documented gradient contract.
"""

from __future__ import annotations

import torch
import warp as wp

from waxmorph.emulator import (
    diffusion_step_differentiable,
    mech_step_sticky_differentiable,
)


[docs] class WarpMechStep(torch.autograd.Function): r"""Sticky-sphere Euler step with position gradients. Forward gives Warp a zero-copy view of contiguous detached Torch storage, discovers pairs outside a fresh tape, and records :math:`X_{out}=X+\Delta t\,F(X,R)` on that frozen list. Backward returns :math:`dL/dX`; radii and scratch objects remain constants in Torch. """
[docs] @staticmethod def forward( ctx, X_torch: torch.Tensor, R_wp: wp.array, particle_count: int, dt: float, f_net_wp: wp.array, grid: wp.HashGrid | None, ) -> torch.Tensor: # Warp shares storage with the contiguous detached tensor. X_wp = wp.from_torch(X_torch.detach().contiguous(), dtype=wp.vec3f) X_wp.requires_grad = True tape = wp.Tape() X_out = mech_step_sticky_differentiable( tape, X_wp, R_wp, particle_count, dt, f_net_wp, grid ) ctx.tape = tape ctx.X_wp = X_wp ctx.X_out = X_out return wp.to_torch(X_out).view(-1, 3)
[docs] @staticmethod def backward(ctx, grad_output: torch.Tensor): ctx.X_out.grad = wp.from_torch(grad_output.detach().contiguous(), dtype=wp.vec3f) ctx.tape.backward() # Clone before tape.zero() clears the shared gradient buffer in place. grad_input = wp.to_torch(ctx.X_wp.grad).view(-1, 3).clone() ctx.tape.zero() return grad_input, None, None, None, None, None
[docs] class WarpDiffusionStep(torch.autograd.Function): r"""Graph diffusion step with concentration gradients. On frozen pairs, forward records :math:`c_{out}=\max(c-D_{emu}L_Gc\,\Delta t,0)` independently per molecule. Positions and radii determine fixed topology; concentrations receive the gradient. Warp views contiguous flattened Torch storage reshaped to ``[N, C]``. """
[docs] @staticmethod def forward( ctx, c_torch: torch.Tensor, X_wp: wp.array, R_wp: wp.array, particle_count: int, D_emu: float, dt: float, grid: wp.HashGrid | None, ) -> torch.Tensor: n, num_molecules = c_torch.shape c_wp = wp.from_torch(c_torch.detach().contiguous().view(-1), dtype=wp.float32) c_wp = c_wp.reshape((n, num_molecules)) c_wp.requires_grad = True tape = wp.Tape() c_out = diffusion_step_differentiable( tape, X_wp, R_wp, c_wp, particle_count, D_emu, dt, grid ) ctx.tape = tape ctx.c_wp = c_wp ctx.c_out = c_out ctx.shape = (n, num_molecules) return wp.to_torch(c_out).view(n, num_molecules)
[docs] @staticmethod def backward(ctx, grad_output: torch.Tensor): n, num_molecules = ctx.shape ctx.c_out.grad = wp.from_torch( grad_output.detach().contiguous().view(-1), dtype=wp.float32 ).reshape((n, num_molecules)) ctx.tape.backward() # Clone before tape.zero() clears the shared gradient buffer in place. grad_input = wp.to_torch(ctx.c_wp.grad).view(n, num_molecules).clone() ctx.tape.zero() return grad_input, None, None, None, None, None, None