Guía de Trabajo Práctico 4#
Materia: Aprendizaje Profundo Basado en la Física (optativa del Instituto Balseiro)
Docente: José I. Robledo
Edición: abril-mayo 2026
Física Diferenciable con PyTorch#
En esta guía vas a trabajar con simuladores differentiables escritos directamente en PyTorch. La idea central es simple: si la simulación está construida con operaciones diferenciables, entonces podés derivar una función de costo respecto de parámetros físicos, condiciones iniciales o variables de control y usar gradiente descendente para resolver problemas inversos.
Ejercicio 1 — Tiro parabólico y diseño inverso con física diferenciable#
Vamos a trabajar con un proyectil 2D lanzado desde el origen. El estado es (x, y, v_x, v_y) y usaremos el modelo más simple sin rozamiento:
El disparo estará parametrizado por una rapidez inicial v0 y un ángulo theta, de modo que
Como queremos usar autograd para optimizar v0 y theta, toda la simulación debe implementarse con operaciones diferenciables de PyTorch.
Tareas#
Completar
projectile_rolloutpara devolver la trayectoriax(t), y(t)usando Euler explícito.Simular una trayectoria con
v0 = 15,theta = 0.9 rad,dt = 0.02,steps = 180.Graficar la trayectoria en el plano
y(x).Definir un blanco en
(x_target, y_target) = (18, 6).Usar el simulador diferenciable para ajustar
v0ythetade forma que el proyectil pase lo más cerca posible del blanco.Graficar la trayectoria inicial, la trayectoria optimizada y el blanco.
Sugerencias:
Para asegurar
v0 > 0, optimizá un parámetro libreraw_v0y definív0 = softplus(raw_v0).Para mantener
thetaen un rango razonable, podés usar una sigmoide y reescalarla, por ejemplo al intervalo(0.05, 1.45)rad.Como la trayectoria tiene longitud fija, conviene definir la loss usando la menor distancia al blanco a lo largo de todos los pasos temporales.
def projectile_rollout(v0, theta, dt=0.02, steps=180, g=9.81):
x = torch.tensor(0.0)
y = torch.tensor(0.0)
vx = v0 * torch.cos(theta)
vy = v0 * torch.sin(theta)
xs = [x]
ys = [y]
for _ in range(steps):
# TODO: actualizar velocidades con la gravedad
# TODO: actualizar posiciones
# TODO: guardar x, y
pass
# TODO: devolver tensores xs, ys con shape (steps + 1,)
raise NotImplementedError
x_target = torch.tensor(18.0)
y_target = torch.tensor(6.0)
def closest_distance_loss(xs, ys, x_target, y_target):
# TODO: calcular la distancia cuadratica a cada punto de la trayectoria
# TODO: devolver el minimo de esas distancias
raise NotImplementedError
v0_test = torch.tensor(15.0)
theta_test = torch.tensor(0.9)
# TODO: simular la trayectoria inicial y graficarla
raw_v0 = torch.tensor(2.0, requires_grad=True)
raw_theta = torch.tensor(0.0, requires_grad=True)
optimizer = torch.optim.Adam([raw_v0, raw_theta], lr=0.05)
history = []
for epoch in range(400):
# TODO: parametrizar v0 y theta
# TODO: simular la trayectoria
# TODO: definir la loss respecto del blanco
# TODO: hacer backward y optimizer.step()
pass
# TODO: reconstruir trayectoria final y compararla con la inicial
Ejercicio 2 — Inferencia del amortiguamiento a partir de datos#
Para centrarnos en la física diferenciable, volvamos ahora al oscilador amortiguado 1D con masa unitaria y ecuación
donde k es la constante del resorte y c el coeficiente de amortiguamiento.
Usaremos un esquema de Euler semiimplícito:
Supongamos ahora que mediste una trayectoria x_obs(t) y querés estimar el valor de c. Vamos a generar primero una trayectoria sintética con un valor verdadero c_true y luego vamos a recuperar ese parámetro minimizando el error cuadrático medio entre la trayectoria observada y la simulada.
Tareas#
Completar
oscillator_stepysimulate_oscillator.Construir
x_obsusandoc_true = 0.18.Introducir ruido gaussiano pequeño para volver el problema un poco más realista.
Definir un parámetro libre
raw_cy parametrizar el amortiguamiento comoc_hat = softplus(raw_c)para asegurar positividad.Optimizar
raw_ccon Adam.Graficar la historia de la loss y comparar
x_obs(t)con la trayectoria reconstruida.
def oscillator_step(x, v, k, c, dt):
# TODO: calcular la aceleracion a = -k * x - c * v
# TODO: actualizar velocidad con Euler
# TODO: actualizar posicion usando la nueva velocidad
raise NotImplementedError
def simulate_oscillator(x0, v0, k, c, dt, steps):
xs = [x0]
vs = [v0]
x, v = x0, v0
for _ in range(steps):
# TODO: avanzar un paso de tiempo con oscillator_step
# TODO: append de los nuevos estados
pass
# TODO: devolver tensores apilados con shape (steps + 1,)
raise NotImplementedError
x0 = torch.tensor(1.0)
v0 = torch.tensor(0.0)
k = torch.tensor(1.5)
dt = 0.05
steps = 200
c_true = torch.tensor(0.18)
noise_std = 0.01
with torch.no_grad():
# TODO: generar trayectoria observada libre de ruido
# TODO: sumar ruido gaussiano pequeno a la posicion observada
pass
raw_c = torch.tensor(-2.0, requires_grad=True)
optimizer = torch.optim.Adam([raw_c], lr=0.05)
loss_history = []
for epoch in range(400):
# TODO: convertir raw_c en c_hat usando softplus
# TODO: simular con c_hat
# TODO: definir loss MSE contra x_obs
# TODO: hacer backward y step
pass
# TODO: reconstruir la mejor trayectoria y graficar resultados
Ejercicio 3 — Sensibilidad respecto de condiciones iniciales y parámetros#
En física diferenciable no sólo interesa ajustar parámetros: también importa entender qué tan sensible es una cantidad de interés ante pequeños cambios en las entradas del simulador.
Tomemos como observable la posición final x(T) del oscilador luego de T = steps * dt.
Repetir la simulación con
x0como tensor conrequires_grad=True.Calcular
\partial x(T) / \partial x0usando autograd.Repetir el experimento con
ccomo tensor conrequires_grad=Truey calcular\partial x(T) / \partial c.Comparar ambas derivadas en valor absoluto e interpretar cuál variable parece más influyente para este horizonte temporal.
Bonus: cambiar el observable por la energía final aproximada
E(T) = 0.5 * (v(T)^2 + k x(T)^2)y repetir el análisis.
Este tipo de derivadas es útil para diseño de experimentos, calibración e identificación de parámetros.
x0_sens = torch.tensor(1.0, requires_grad=True)
c_sens = torch.tensor(0.15, requires_grad=True)
k_sens = torch.tensor(1.5)
# TODO: simular con x0_sens y obtener gradiente de x(T) respecto de x0_sens
# TODO: simular con c_sens y obtener gradiente de x(T) respecto de c_sens
# TODO: imprimir ambas sensibilidades y discutir el resultado
Ejercicio 4 — Circuito RC como sistema diferenciable#
Consideremos un circuito RC serie excitado por una fuente constante V_in. Si elegimos como variable de estado la tensión del capacitor V_c(t), el modelo viene dado por
Este sistema es útil para física diferenciable porque:
tiene una dinámica simple pero no trivial;
permite plantear inferencia de parámetros;
permite estudiar sensibilidad respecto de
R,Cy la condición inicial.
Completar
rc_stepysimulate_rc.Simular la carga del capacitor con
R = 2,C = 1.5,V_in = 5,V0 = 0,dt = 0.05,steps = 120.Graficar
V_c(t).Generar datos sintéticos
V_obs(t)con un valor verdaderoR_true, agregando ruido pequeño.Inferir
Rminimizando el MSE entreV_obs(t)y la trayectoria simulada.Calcular con autograd las sensibilidades
\partial V_c(T)/\partial Ry\partial V_c(T)/\partial C.
Preguntas para discutir:
¿Qué parámetro parece afectar más el voltaje final?
¿Cómo cambia la dificultad de inferir
Rsi el horizonte temporal es muy corto?¿Qué interpretación física tiene la combinación
RCen este problema?
def rc_step(vc, R, C, V_in, dt):
# TODO: calcular dVc/dt = (V_in - vc) / (R * C)
# TODO: actualizar vc con Euler
raise NotImplementedError
def simulate_rc(V0, R, C, V_in, dt, steps):
vcs = [V0]
vc = V0
for _ in range(steps):
# TODO: avanzar un paso con rc_step
# TODO: guardar el nuevo voltaje
pass
# TODO: devolver la trayectoria completa
raise NotImplementedError
V0 = torch.tensor(0.0)
R = torch.tensor(2.0)
C = torch.tensor(1.5)
V_in = torch.tensor(5.0)
dt = 0.05
steps = 120
t = torch.linspace(0.0, steps * dt, steps + 1)
# TODO: simular y graficar la carga del capacitor
R_true = torch.tensor(1.8)
noise_std = 0.03
with torch.no_grad():
# TODO: generar trayectoria observada con ruido
pass
raw_R = torch.tensor(0.0, requires_grad=True)
optimizer = torch.optim.Adam([raw_R], lr=0.05)
loss_history = []
for epoch in range(300):
# TODO: parametrizar R_hat con softplus
# TODO: simular el circuito
# TODO: definir la loss MSE y optimizar
pass
# TODO: calcular sensibilidades de Vc(T) respecto de R y C
# TODO: graficar historia de la loss y ajuste final
Ejercicio 5 - FD + NN para controlar un circuito RC#
En los ejercicios anteriores, la física diferenciable se usó para ajustar parámetros físicos como R o condiciones de disparo como v0 y theta. Ahora queremos mostrar una idea complementaria: usar una red neuronal como controlador, mientras la evolución temporal del sistema sigue estando dada por la física conocida.
El circuito sigue siendo el mismo:
Pero en vez de fijar V_in de antemano, vamos a hacer que una red neuronal la produzca en cada paso de tiempo:
Intención física del problema#
La idea no es “que la red aprenda la física del capacitor”. Esa parte ya la conocemos. La intención es otra:
el simulador físico se encarga de imponer la dinámica correcta del circuito;
la red neuronal actúa como un controlador que decide qué tensión aplicar;
la función de costo mide si el capacitor llega al valor deseado y cuánta acción de control fue necesaria.
Esto ilustra bien la lógica FD + NN: la red propone acciones; la física diferenciable calcula sus consecuencias a lo largo del tiempo; backward() ajusta los pesos de la red a partir del objetivo final.
Implementar una red pequeña que reciba
V_cyV_targety devuelvaV_in.Implementar
simulate_rc_controlled.Entrenar la red para llevar el capacitor desde
V0 = 0hastaV_target = 4.Penalizar también señales de control excesivamente grandes.
Graficar la evolución de
V_c(t)y deV_in(t).
Sugerencia para la loss:
class RCController(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 32),
nn.Tanh(),
nn.Linear(32, 32),
nn.Tanh(),
nn.Linear(32, 1),
)
def forward(self, vc, v_target):
# TODO: concatenar vc y v_target como entrada
# TODO: devolver un escalar de control V_in
raise NotImplementedError
def simulate_rc_controlled(V0, R, C, controller, v_target, dt, steps):
vc = V0
vcs = [vc]
vins = []
for _ in range(steps):
# TODO: pedirle a la red la tension de entrada
# TODO: avanzar la dinamica del capacitor
# TODO: guardar Vc y Vin
pass
# TODO: devolver trayectorias completas
raise NotImplementedError
V0 = torch.tensor(0.0)
R = torch.tensor(2.0)
C = torch.tensor(1.5)
v_target = torch.tensor(4.0)
dt = 0.05
steps = 120
lambda_u = 1e-3
controller = RCController()
optimizer = torch.optim.Adam(controller.parameters(), lr=1e-3)
history = []
for epoch in range(600):
# TODO: simular el sistema controlado
# TODO: definir loss de seguimiento + regularizacion del control
# TODO: hacer backward y optimizer.step()
pass
# TODO: graficar trayectoria de Vc y señal Vin