276 lines
10 KiB
Python
276 lines
10 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Experiment: Add guardian-like perturbations to 256×256 LBM
|
|||
|
|
Simulate the precipitation system at small scale
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
import math
|
|||
|
|
|
|||
|
|
class GuardianLBM:
|
|||
|
|
"""LBM with guardian (precipitation) system."""
|
|||
|
|
|
|||
|
|
def __init__(self, nx=256, ny=256, num_guardians=12):
|
|||
|
|
self.nx = nx
|
|||
|
|
self.ny = ny
|
|||
|
|
self.q = 9
|
|||
|
|
|
|||
|
|
# Guardian parameters (scaled for 256×256)
|
|||
|
|
self.num_guardians = num_guardians # 12 for 256×256 (scaled from 194)
|
|||
|
|
self.rho_thresh = 1.01 # Density threshold for precipitation
|
|||
|
|
self.drain_radius = 4 # Scaled from 16 (256/1024 = 1/4)
|
|||
|
|
self.sink_radius = 6 # Scaled from 24
|
|||
|
|
|
|||
|
|
# Guardian positions and strengths
|
|||
|
|
self.guardians = []
|
|||
|
|
|
|||
|
|
# D2Q9 parameters
|
|||
|
|
self.w = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36], dtype=np.float32)
|
|||
|
|
self.ex = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1], dtype=np.int32)
|
|||
|
|
self.ey = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1], dtype=np.int32)
|
|||
|
|
|
|||
|
|
# Distribution functions
|
|||
|
|
self.f = np.zeros((self.q, self.ny, self.nx), dtype=np.float32)
|
|||
|
|
self.f_new = np.zeros((self.q, self.ny, self.nx), dtype=np.float32)
|
|||
|
|
|
|||
|
|
# Macroscopic variables
|
|||
|
|
self.rho = np.ones((self.ny, self.nx), dtype=np.float32)
|
|||
|
|
self.ux = np.zeros((self.ny, self.nx), dtype=np.float32)
|
|||
|
|
self.uy = np.zeros((self.ny, self.nx), dtype=np.float32)
|
|||
|
|
|
|||
|
|
# Relaxation parameter
|
|||
|
|
self.omega = 1.0
|
|||
|
|
|
|||
|
|
print(f"Guardian LBM: {nx}×{ny}, {num_guardians} guardians")
|
|||
|
|
print(f" RHO_THRESH: {self.rho_thresh}")
|
|||
|
|
print(f" Drain radius: {self.drain_radius}, Sink radius: {self.sink_radius}")
|
|||
|
|
|
|||
|
|
def equilibrium(self, rho, ux, uy):
|
|||
|
|
"""Compute equilibrium distribution."""
|
|||
|
|
f_eq = np.zeros((self.q, self.ny, self.nx), dtype=np.float32)
|
|||
|
|
|
|||
|
|
for i in range(self.q):
|
|||
|
|
eu = self.ex[i] * ux + self.ey[i] * uy
|
|||
|
|
u2 = ux**2 + uy**2
|
|||
|
|
f_eq[i] = rho * self.w[i] * (1 + 3*eu + 4.5*eu**2 - 1.5*u2)
|
|||
|
|
|
|||
|
|
return f_eq
|
|||
|
|
|
|||
|
|
def compute_macroscopic(self):
|
|||
|
|
"""Compute macroscopic variables."""
|
|||
|
|
self.rho = np.sum(self.f, axis=0)
|
|||
|
|
|
|||
|
|
rho_safe = np.where(self.rho > 1e-10, self.rho, 1.0)
|
|||
|
|
|
|||
|
|
self.ux = np.zeros_like(self.rho)
|
|||
|
|
self.uy = np.zeros_like(self.rho)
|
|||
|
|
|
|||
|
|
for i in range(self.q):
|
|||
|
|
self.ux += self.ex[i] * self.f[i]
|
|||
|
|
self.uy += self.ey[i] * self.f[i]
|
|||
|
|
|
|||
|
|
self.ux /= rho_safe
|
|||
|
|
self.uy /= rho_safe
|
|||
|
|
|
|||
|
|
def add_guardian(self, x, y, strength=0.1):
|
|||
|
|
"""Add a guardian at position (x,y)."""
|
|||
|
|
self.guardians.append({
|
|||
|
|
'x': x,
|
|||
|
|
'y': y,
|
|||
|
|
'strength': strength,
|
|||
|
|
'mass': 0.0,
|
|||
|
|
'alive': True
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Add density perturbation (high density spot)
|
|||
|
|
for dy in range(-self.drain_radius, self.drain_radius + 1):
|
|||
|
|
for dx in range(-self.drain_radius, self.drain_radius + 1):
|
|||
|
|
dist2 = dx*dx + dy*dy
|
|||
|
|
if dist2 <= self.drain_radius*self.drain_radius:
|
|||
|
|
xx = (x + dx) % self.nx
|
|||
|
|
yy = (y + dy) % self.ny
|
|||
|
|
|
|||
|
|
# Gaussian density increase
|
|||
|
|
weight = math.exp(-dist2 / (self.drain_radius*self.drain_radius/4))
|
|||
|
|
self.rho[yy, xx] += strength * weight
|
|||
|
|
|
|||
|
|
print(f"Added guardian at ({x}, {y}), strength={strength}")
|
|||
|
|
|
|||
|
|
def place_guardians_random(self):
|
|||
|
|
"""Place guardians randomly across grid."""
|
|||
|
|
for i in range(self.num_guardians):
|
|||
|
|
x = np.random.randint(0, self.nx)
|
|||
|
|
y = np.random.randint(0, self.ny)
|
|||
|
|
strength = 0.05 + 0.1 * np.random.random() # 0.05 to 0.15
|
|||
|
|
self.add_guardian(x, y, strength)
|
|||
|
|
|
|||
|
|
def place_guardians_grid(self):
|
|||
|
|
"""Place guardians in a grid pattern."""
|
|||
|
|
spacing = int(math.sqrt(self.nx * self.ny / self.num_guardians))
|
|||
|
|
|
|||
|
|
positions = []
|
|||
|
|
for y in range(spacing//2, self.ny, spacing):
|
|||
|
|
for x in range(spacing//2, self.nx, spacing):
|
|||
|
|
if len(positions) < self.num_guardians:
|
|||
|
|
positions.append((x, y))
|
|||
|
|
|
|||
|
|
for x, y in positions:
|
|||
|
|
self.add_guardian(x, y, strength=0.1)
|
|||
|
|
|
|||
|
|
def apply_guardian_drain(self):
|
|||
|
|
"""Apply guardian drain effect on fluid."""
|
|||
|
|
for guardian in self.guardians:
|
|||
|
|
if not guardian['alive']:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
x, y = guardian['x'], guardian['y']
|
|||
|
|
|
|||
|
|
# Drain mass from surrounding area
|
|||
|
|
for dy in range(-self.sink_radius, self.sink_radius + 1):
|
|||
|
|
for dx in range(-self.sink_radius, self.sink_radius + 1):
|
|||
|
|
dist2 = dx*dx + dy*dy
|
|||
|
|
if dist2 <= self.sink_radius*self.sink_radius:
|
|||
|
|
xx = (x + dx) % self.nx
|
|||
|
|
yy = (y + dy) % self.ny
|
|||
|
|
|
|||
|
|
# Drain strength decreases with distance
|
|||
|
|
weight = math.exp(-dist2 / (self.sink_radius*self.sink_radius/4))
|
|||
|
|
drain_amount = 0.001 * weight * guardian['strength']
|
|||
|
|
|
|||
|
|
# Reduce density
|
|||
|
|
self.rho[yy, xx] -= drain_amount
|
|||
|
|
guardian['mass'] += drain_amount
|
|||
|
|
|
|||
|
|
# Guardian dies if it collects too much mass
|
|||
|
|
if guardian['mass'] > 0.5:
|
|||
|
|
guardian['alive'] = False
|
|||
|
|
print(f"Guardian at ({x}, {y}) died, mass={guardian['mass']:.3f}")
|
|||
|
|
|
|||
|
|
def check_precipitation(self):
|
|||
|
|
"""Check for new guardian precipitation (where density > threshold)."""
|
|||
|
|
# Find locations where density exceeds threshold
|
|||
|
|
high_density = np.where(self.rho > self.rho_thresh)
|
|||
|
|
|
|||
|
|
if len(high_density[0]) > 0:
|
|||
|
|
# Pick a random high-density spot
|
|||
|
|
idx = np.random.randint(0, len(high_density[0]))
|
|||
|
|
y, x = high_density[0][idx], high_density[1][idx]
|
|||
|
|
|
|||
|
|
# Check if too close to existing guardians
|
|||
|
|
too_close = False
|
|||
|
|
for guardian in self.guardians:
|
|||
|
|
if guardian['alive']:
|
|||
|
|
dx = (x - guardian['x']) % self.nx
|
|||
|
|
dy = (y - guardian['y']) % self.ny
|
|||
|
|
dist = math.sqrt(dx*dx + dy*dy)
|
|||
|
|
if dist < self.drain_radius * 2:
|
|||
|
|
too_close = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not too_close and len(self.guardians) < self.num_guardians * 2:
|
|||
|
|
# Birth new guardian
|
|||
|
|
strength = 0.05 + 0.05 * (self.rho[y, x] - self.rho_thresh)
|
|||
|
|
self.add_guardian(x, y, strength)
|
|||
|
|
print(f"Precipitation: New guardian at ({x}, {y}), ρ={self.rho[y, x]:.3f}")
|
|||
|
|
|
|||
|
|
def collide_and_stream(self):
|
|||
|
|
"""One LBM step with guardian effects."""
|
|||
|
|
# Apply guardian drain
|
|||
|
|
self.apply_guardian_drain()
|
|||
|
|
|
|||
|
|
# Check for precipitation
|
|||
|
|
if np.random.random() < 0.1: # 10% chance per step
|
|||
|
|
self.check_precipitation()
|
|||
|
|
|
|||
|
|
# Compute equilibrium
|
|||
|
|
f_eq = self.equilibrium(self.rho, self.ux, self.uy)
|
|||
|
|
|
|||
|
|
# Collision
|
|||
|
|
for i in range(self.q):
|
|||
|
|
self.f_new[i] = self.f[i] - self.omega * (self.f[i] - f_eq[i])
|
|||
|
|
|
|||
|
|
# Stream (periodic boundaries)
|
|||
|
|
for i in range(self.q):
|
|||
|
|
self.f[i] = np.roll(self.f_new[i], (self.ey[i], self.ex[i]), axis=(0, 1))
|
|||
|
|
|
|||
|
|
# Update macroscopic variables
|
|||
|
|
self.compute_macroscopic()
|
|||
|
|
|
|||
|
|
def run(self, steps=200):
|
|||
|
|
"""Run simulation."""
|
|||
|
|
print(f"\nRunning {steps} steps with guardians...")
|
|||
|
|
|
|||
|
|
# Initialize distribution from macroscopic variables
|
|||
|
|
f_eq = self.equilibrium(self.rho, self.ux, self.uy)
|
|||
|
|
for i in range(self.q):
|
|||
|
|
self.f[i] = f_eq[i]
|
|||
|
|
|
|||
|
|
# Track statistics
|
|||
|
|
energies = []
|
|||
|
|
guardian_counts = []
|
|||
|
|
|
|||
|
|
for step in range(steps):
|
|||
|
|
self.collide_and_stream()
|
|||
|
|
|
|||
|
|
if step % 20 == 0 or step == steps - 1:
|
|||
|
|
# Compute kinetic energy
|
|||
|
|
speed2 = self.ux**2 + self.uy**2
|
|||
|
|
energy = np.mean(0.5 * self.rho * speed2)
|
|||
|
|
|
|||
|
|
# Count alive guardians
|
|||
|
|
alive = sum(1 for g in self.guardians if g['alive'])
|
|||
|
|
|
|||
|
|
energies.append(energy)
|
|||
|
|
guardian_counts.append(alive)
|
|||
|
|
|
|||
|
|
if step % 100 == 0:
|
|||
|
|
print(f" Step {step:4d}: energy={energy:.2e}, guardians={alive}")
|
|||
|
|
|
|||
|
|
print(f"\nFinal: {sum(1 for g in self.guardians if g['alive'])} guardians alive")
|
|||
|
|
print(f"Max density: {self.rho.max():.3f}, Min density: {self.rho.min():.3f}")
|
|||
|
|
|
|||
|
|
return energies, guardian_counts
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("=== Guardian Precipitation Experiment ===")
|
|||
|
|
print("Testing if guardians work at 256×256 scale")
|
|||
|
|
print("="*50)
|
|||
|
|
|
|||
|
|
# Test with 12 guardians (scaled from 194)
|
|||
|
|
print("\n[TEST] 256×256 with 12 guardians")
|
|||
|
|
lbm = GuardianLBM(256, 256, num_guardians=12)
|
|||
|
|
|
|||
|
|
# Place initial guardians in grid pattern
|
|||
|
|
lbm.place_guardians_grid()
|
|||
|
|
|
|||
|
|
# Run simulation
|
|||
|
|
energies, counts = lbm.run(200)
|
|||
|
|
|
|||
|
|
# Analysis
|
|||
|
|
print("\n=== Analysis ===")
|
|||
|
|
print(f"Initial guardians: {len(lbm.guardians)}")
|
|||
|
|
print(f"Final alive: {sum(1 for g in lbm.guardians if g['alive'])}")
|
|||
|
|
|
|||
|
|
density_variation = np.std(lbm.rho)
|
|||
|
|
print(f"Density variation (std): {density_variation:.6f}")
|
|||
|
|
|
|||
|
|
if density_variation > 0.01:
|
|||
|
|
print("[SUCCESS] Guardians created significant density variations")
|
|||
|
|
else:
|
|||
|
|
print("[NOTE] Density field remains relatively uniform")
|
|||
|
|
|
|||
|
|
# Compare with theoretical
|
|||
|
|
print("\n=== Theoretical Scaling ===")
|
|||
|
|
print("1024×1024: 194 guardians, drain_radius=16, sink_radius=24")
|
|||
|
|
print("256×256 (1/4 scale):")
|
|||
|
|
print(f" Guardians: 194 × (256/1024)² = {194 * (256/1024)**2:.1f} ≈ 12")
|
|||
|
|
print(f" Drain radius: 16 × (256/1024) = {16 * (256/1024)} = 4 ✓")
|
|||
|
|
print(f" Sink radius: 24 × (256/1024) = {24 * (256/1024)} = 6 ✓")
|
|||
|
|
print(f" RHO_THRESH: 1.01 (same, scales with viscosity not grid)")
|
|||
|
|
|
|||
|
|
print("\n" + "="*50)
|
|||
|
|
print("EXPERIMENT COMPLETE")
|
|||
|
|
print("\nNext: Test with actual brain state + guardians")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|