from __future__ import annotations import torch from torch.nn import functional as F LAPLACIAN = torch.tensor( [ [0.05, 0.20, 0.05], [0.20, -1.00, 0.20], [0.05, 0.20, 0.05], ], dtype=torch.float32, ).reshape(1, 1, 3, 3) def initial_state( batch: int, size: int, seed: int, ) -> torch.Tensor: generator = torch.Generator().manual_seed(seed) u = torch.ones(batch, 1, size, size) v = torch.zeros(batch, 1, size, size) radius = max(2, size // 10) center = size // 2 u[ :, :, center - radius : center + radius, center - radius : center + radius, ] = 0.50 v[ :, :, center - radius : center + radius, center - radius : center + radius, ] = 0.25 noise = torch.rand((batch, 1, size, size), generator=generator) * 0.04 - 0.02 u = torch.clamp(u + noise, 0, 1) v = torch.clamp(v - noise, 0, 1) return torch.cat([u, v], dim=1) def gray_scott_step( state: torch.Tensor, feed: torch.Tensor, kill: torch.Tensor, diffusion_u: float = 0.16, diffusion_v: float = 0.08, ) -> torch.Tensor: kernel = LAPLACIAN.to(state) u = state[:, 0:1] v = state[:, 1:2] padded_u = F.pad(u, (1, 1, 1, 1), mode="circular") padded_v = F.pad(v, (1, 1, 1, 1), mode="circular") laplacian_u = F.conv2d(padded_u, kernel) laplacian_v = F.conv2d(padded_v, kernel) reaction = u * v.square() feed_field = feed.reshape(-1, 1, 1, 1) kill_field = kill.reshape(-1, 1, 1, 1) delta_u = diffusion_u * laplacian_u - reaction + feed_field * (1 - u) delta_v = diffusion_v * laplacian_v + reaction - (feed_field + kill_field) * v return torch.clamp( torch.cat([u + delta_u, v + delta_v], dim=1), 0, 1, )