Un tutorial de codificación paso a paso sobre NVIDIA PhysicsNeMo: Darcy Flow, FNO, PINN, modelos sustitutos y evaluación comparativa de inferencia
print(“\n” + “=”*80) print(“SECCIÓN 4: VISUALIZACIÓN DE DATOS”) print(“=”*80) def visualize_darcy_samples( permeabilidad: np.ndarray, presión: np.ndarray, n_samples: int = 3 ): “””Visualizar muestras de flujo de Darcy.””” fig, axes = plt.subplots(n_samples, 2, tamaño de figura = (10, 4 * n_muestras)) para i en el rango (n_muestras): im1 = ejes[i, 0].imshow(permeabilidad[i]cmap=’viridis’, origin=’inferior’) ejes[i, 0].set_title(f’Campo de permeabilidad (Muestra {i+1})’) ejes[i, 0].set_xlabel(‘x’) ejes[i, 0].set_ylabel(‘y’) plt.colorbar(im1, hacha=ejes[i, 0]etiqueta=”k(x,y)”) im2 = ejes[i, 1].imshow(presión[i]cmap=’hot’, origin=’inferior’) ejes[i, 1].set_title(f’Campo de presión (Muestra {i+1})’) ejes[i, 1].set_xlabel(‘x’) ejes[i, 1].set_ylabel(‘y’) plt.colorbar(im2, hacha=ejes[i, 1]label=”u(x,y)”) plt.tight_layout() plt.savefig(‘darcy_samples.png’, dpi=150, bbox_inches=”tight”) plt.show() print(” ✓ Visualización guardada en ‘darcy_samples.png'”) visualize_darcy_samples(perm_train[:3]prensa_tren[:3]) print(“\n” + “=”*80) print(“SECCIÓN 5: OPERADOR NEURAL DE FOURIER (FNO)”) print(“=”*80) “”” El Operador Neural de Fourier (FNO) aprende asignaciones entre espacios funcionales parametrizando el núcleo integral en el espacio de Fourier. Información clave: Convolución en el espacio físico = multiplicación en el espacio de Fourier La capa FNO consta de: 1. FFT para transformar al dominio de frecuencia 2. Multiplicación con pesos que se pueden aprender (manteniendo solo los modos de baja frecuencia) 3. FFT inversa para volver a transformar 4. Conexión residual con una transformación lineal local “”” clase SpectralConv2d(nn.Module): “”” Capa de convolución espectral 2D para FNO Realiza convolución en el espacio de Fourier mediante: 1. Calcular la FFT de la entrada 2. Multiplicar con pesos complejos que se pueden aprender 3. Calcular la FFT inversa “”” def. __init__( self, in_channels: int, out_channels: int, modos1: int, modos2: int ): super().__init__() self.in_channels = in_channels self.out_channels = out_channels self.modes1 = modos1 self.modes2 = modos2 self.scale = 1 / (in_channels * out_channels) self.weights1 = nn.Parameter( self.scale * torch.rand(in_channels, out_channels, modos1, modos2, dtype=torch.cfloat) ) self.weights2 = nn.Parameter( self.scale * torch.rand(in_channels, out_channels, modos1, modos2, dtype=torch.cfloat) ) def compl_mul2d(self, entrada: torch.Tensor, pesos: torch.Tensor) -> torch.Tensor: “””Multiplicación compleja para lotes de tensores 2D.””” return torch.einsum(“bixy,ioxy->boxy”, input,weights) def forward(self, x: torch.Tensor) -> torch.Tensor: batch_size = x.shape[0]

x_ft = antorcha.fft.rfft2(x) out_ft = torch.zeros( tamaño_lote, self.out_channels, x.size(-2), x.size(-1) // 2 + 1, dtype=torch.cfloat, dispositivo=x.dispositivo ) out_ft[:, :, :self.modes1, :self.modes2] = \ self.compl_mul2d(x_ft[:, :, :self.modes1, :self.modes2]pesos propios1) out_ft[:, :, -self.modes1:, :self.modes2] = \ self.compl_mul2d(x_ft[:, :, -self.modes1:, :self.modes2]self.weights2) x = torch.fft.irfft2(out_ft, s=(x.size(-2), x.size(-1))) return x class FNOBlock(nn.Module): “”” Bloque FNO que combina convolución espectral con transformación lineal local. salida = σ(SpectralConv(x) + LocalLinear(x)) “”” def __init__( self, canales: int, modos1: int, modos2: int, activación: str=”gelu” ): super().__init__() self.spectral_conv = SpectralConv2d(canales, canales, modos1, modos2) self.local_linear = nn.Conv2d(canales, canales, 1) self.activation = nn.GELU() if activación == ‘gelu’ else nn.ReLU() def forward(self, x: torch.Tensor) -> torch.Tensor: return self.activation(self.spectral_conv(x) + self.local_linear(x)) class FourierNeuralOperator2D(nn.Module): “”” Operador neuronal de Fourier 2D completo para aprender operadores. Arquitectura: 1. Elevar la entrada a un espacio de canal de dimensiones superiores 2. Aplicar múltiples bloques FNO (convoluciones espectrales + residuales) 3. Proyectar de nuevo al espacio de salida Esto aprende el mapeo: k(x,y) -> u(x,y) para el flujo de Darcy “”” def __init__( self, in_channels: int = 1, out_channels: int = 1, modos1: int = 12, modos2: int = 12, ancho: int = 32, n_layers: int = 4, relleno: int = 9): super().__init__() self.modes1 = modos1 self.modes2 = modos2 self.width = ancho self.padding = relleno self.fc0 = nn.Linear(in_channels + 2, ancho) self.fno_blocks = nn.ModuleList([
FNOBlock(width, modes1, modes2) for _ in range(n_layers)
]) self.fc1 = nn.Linear(ancho, 128) self.fc2 = nn.Linear(128, out_channels) def get_grid(self, forma: Tupla, dispositivo: torch.device) -> torch.Tensor: “””Crear coordenadas de cuadrícula normalizadas.””” tamaño_lote, tamaño_x, tamaño_y = forma[0]forma[2]forma[3]

gridx = torch.linspace(0, 1, size_x, dispositivo=dispositivo) gridy = torch.linspace(0, 1, size_y, dispositivo=dispositivo) gridx, gridy = torch.meshgrid(gridx, gridy, indexing=’ij’) grid = torch.stack([gridx, gridy]dim=-1) grid = grid.unsqueeze(0).repeat(batch_size, 1, 1, 1) return grid def forward(self, x: torch.Tensor) -> torch.Tensor: batch_size = x.shape[0]

grid = self.get_grid(x.shape, x.device) x = x.permute(0, 2, 3, 1) x = torch.cat([x, grid]dim=-1) x = self.fc0(x) x = x.permute(0, 3, 1, 2) si self.padding > 0: x = F.pad(x, [0, self.padding, 0, self.padding]) para bloque en self.fno_blocks: x = block(x) si self.padding > 0: x = x[…, :-self.padding, :-self.padding]

x = x.permute(0, 2, 3, 1) x = F.gelu(self.fc1(x)) x = self.fc2(x) x = x.permute(0, 3, 1, 2) return x print(“\nCreando el modelo de operador neuronal de Fourier…”) fno_model = FourierNeuralOperator2D( in_channels=1, out_channels=1, modos1=8, modos2=8, ancho=32, n_capas=4, relleno=5 ).to(dispositivo) n_params = sum(p.numel() for p in fno_model.parameters() if p.requires_grad) print(f” ✓ Modelo FNO creado con {n_params:,} parámetros entrenables”)