fluid dynamics - code
import torch
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from matplotlib.animation import FuncAnimation
class FluidSimulator3D:
def __init__(self, width, height, depth, viscosity=0.1, dt=0.1):
self.width = width
self.height = height
self.depth = depth
self.viscosity = viscosity
self.dt = dt
# Initialize velocity fields (u: x, v: y, w: z)
self.u = torch.zeros((depth, height, width))
self.v = torch.zeros((depth, height, width))
self.w = torch.zeros((depth, height, width))
# Initialize pressure field
self.p = torch.zeros((depth, height, width))
# Initialize density/dye field for visualization
self.dye = torch.zeros((depth, height, width))
def set_initial_conditions(self, center_x, center_y, center_z, radius, strength):
"""Set initial toroidal vortex conditions (smoke ring)"""
z, y, x = torch.meshgrid(
torch.arange(self.depth),
torch.arange(self.height),
torch.arange(self.width)
)
# Calculate distances from center
dx = x - center_x
dy = y - center_y
dz = z - center_z
# Radial distance from vertical axis
r = torch.sqrt(dx**2 + dy**2)
# Distance from ring centerline
ring_radius = radius * 1.5
d = torch.sqrt((r - ring_radius)**2 + dz**2)
# Create toroidal vortex
mask = d < radius
# Set initial velocities for smoke ring
# Circular motion around the ring's core
theta = torch.atan2(dy, dx)
self.u[mask] = -strength * torch.sin(theta[mask]) * dz[mask] / radius
self.v[mask] = strength * torch.cos(theta[mask]) * dz[mask] / radius
self.w[mask] = strength * (r[mask] - ring_radius) / radius
# Add dye for visualization
self.dye[mask] = 1.0
def diffuse(self, field):
"""Apply viscous diffusion in 3D"""
laplacian = (
field[:-2, 1:-1, 1:-1] + field[2:, 1:-1, 1:-1] +
field[1:-1, :-2, 1:-1] + field[1:-1, 2:, 1:-1] +
field[1:-1, 1:-1, :-2] + field[1:-1, 1:-1, 2:] -
6 * field[1:-1, 1:-1, 1:-1]
)
field[1:-1, 1:-1, 1:-1] += self.viscosity * self.dt * laplacian
def advect(self):
"""Advect velocity and dye fields in 3D"""
z, y, x = torch.meshgrid(
torch.arange(self.depth),
torch.arange(self.height),
torch.arange(self.width)
)
# Calculate previous positions
x_prev = x - self.dt * self.u
y_prev = y - self.dt * self.v
z_prev = z - self.dt * self.w
# Clip to bounds
x_prev = torch.clamp(x_prev, 0, self.width-1)
y_prev = torch.clamp(y_prev, 0, self.height-1)
z_prev = torch.clamp(z_prev, 0, self.depth-1)
# Convert to indices
x0 = torch.floor(x_prev).long()
x1 = x0 + 1
y0 = torch.floor(y_prev).long()
y1 = y0 + 1
z0 = torch.floor(z_prev).long()
z1 = z0 + 1
# Clip again
x0 = torch.clamp(x0, 0, self.width-1)
x1 = torch.clamp(x1, 0, self.width-1)
y0 = torch.clamp(y0, 0, self.height-1)
y1 = torch.clamp(y1, 0, self.height-1)
z0 = torch.clamp(z0, 0, self.depth-1)
z1 = torch.clamp(z1, 0, self.depth-1)
# Get weights for trilinear interpolation
sx = x_prev - x0.float()
sy = y_prev - y0.float()
sz = z_prev - z0.float()
# Interpolate
def interpolate(field):
c000 = field[z0, y0, x0]
c001 = field[z0, y0, x1]
c010 = field[z0, y1, x0]
c011 = field[z0, y1, x1]
c100 = field[z1, y0, x0]
c101 = field[z1, y0, x1]
c110 = field[z1, y1, x0]
c111 = field[z1, y1, x1]
return (
(1-sz) * (1-sy) * (1-sx) * c000 +
(1-sz) * (1-sy) * sx * c001 +
(1-sz) * sy * (1-sx) * c010 +
(1-sz) * sy * sx * c011 +
sz * (1-sy) * (1-sx) * c100 +
sz * (1-sy) * sx * c101 +
sz * sy * (1-sx) * c110 +
sz * sy * sx * c111
)
# Update fields
self.u = interpolate(self.u)
self.v = interpolate(self.v)
self.w = interpolate(self.w)
self.dye = interpolate(self.dye)
def project(self):
"""Pressure projection to enforce incompressibility in 3D"""
# Calculate divergence
div = torch.zeros((self.depth, self.height, self.width))
div[1:-1, 1:-1, 1:-1] = (
(self.u[1:-1, 1:-1, 2:] - self.u[1:-1, 1:-1, :-2]) / 2 +
(self.v[1:-1, 2:, 1:-1] - self.v[1:-1, :-2, 1:-1]) / 2 +
(self.w[2:, 1:-1, 1:-1] - self.w[:-2, 1:-1, 1:-1]) / 2
)
# Solve pressure Poisson equation
for _ in range(20):
self.p[1:-1, 1:-1, 1:-1] = (
(self.p[:-2, 1:-1, 1:-1] + self.p[2:, 1:-1, 1:-1] +
self.p[1:-1, :-2, 1:-1] + self.p[1:-1, 2:, 1:-1] +
self.p[1:-1, 1:-1, :-2] + self.p[1:-1, 1:-1, 2:] -
div[1:-1, 1:-1, 1:-1]) / 6
)
# Enforce Neumann boundary conditions
self.p[0, :, :] = self.p[1, :, :]
self.p[-1, :, :] = self.p[-2, :, :]
self.p[:, 0, :] = self.p[:, 1, :]
self.p[:, -1, :] = self.p[:, -2, :]
self.p[:, :, 0] = self.p[:, :, 1]
self.p[:, :, -1] = self.p[:, :, -2]
# Subtract pressure gradient
self.u[1:-1, 1:-1, 1:-1] -= (self.p[1:-1, 1:-1, 2:] - self.p[1:-1, 1:-1, :-2]) / 2
self.v[1:-1, 1:-1, 1:-1] -= (self.p[1:-1, 2:, 1:-1] - self.p[1:-1, :-2, 1:-1]) / 2
self.w[1:-1, 1:-1, 1:-1] -= (self.p[2:, 1:-1, 1:-1] - self.p[:-2, 1:-1, 1:-1]) / 2
def step(self):
"""Perform one simulation step"""
self.diffuse(self.u)
self.diffuse(self.v)
self.diffuse(self.w)
self.project()
self.advect()
self.project()
def run_simulation(self, num_steps=100):
"""Run simulation and return animation frames"""
frames = []
for _ in range(num_steps):
self.step()
# Store a slice of the 3D volume for visualization
frames.append(self.dye[:, :, self.width//2].clone())
return frames
def create_and_run_simulation():
# Create simulator
size = 50 # smaller size for 3D
sim = FluidSimulator3D(size, size, size, viscosity=0.1, dt=0.1)
# Set initial conditions (smoke ring)
sim.set_initial_conditions(
center_x=size//2,
center_y=size//2,
center_z=size//4, # start lower
radius=8,
strength=2.0
)
# Run simulation
frames = sim.run_simulation(num_steps=200)
# Create animation
fig, ax = plt.subplots()
def animate(frame):
ax.clear()
ax.imshow(frame, cmap='viridis')
ax.set_title('Y-Z slice at middle of X axis')
ax.set_xticks([])
ax.set_yticks([])
anim = FuncAnimation(
fig, animate, frames=frames,
interval=50, blit=False
)
plt.show()
return anim
if __name__ == "__main__":
anim = create_and_run_simulation()
Public Last updated: 2024-10-27 01:13:40 AM
