Guest User

Untitled

a guest
Jan 7th, 2026
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.64 KB | None | 0 0
  1. import pygame
  2. from pygame.locals import *
  3. from OpenGL.GL import *
  4. import pycuda.driver as cuda
  5. import pycuda.autoinit
  6. from pycuda.compiler import SourceModule
  7. import pycuda.gpuarray as gpuarray
  8. import numpy as np
  9.  
  10. # ================== PARAMETERS ==================
  11. NUM_TYPES = 12
  12. NUM_PARTICLES = 3000
  13. WORLD_SIZE = 600.0
  14. NUM_GLOBAL_REPULSIVE = 10 # User-defined: number of global repulsive force layers
  15. NUM_GLOBAL_ATTRACTIVE = 0 # User-defined: number of global attractive force layers
  16. NUM_TYPE_SPECIFIC = 8 # User-defined: number of type-specific force layers
  17. NUM_DENSITY_REPULSIVE = 1 # User-defined: number of density-dependent repulsive force layers
  18. NUM_FORCES = NUM_GLOBAL_REPULSIVE + NUM_GLOBAL_ATTRACTIVE + NUM_TYPE_SPECIFIC + NUM_DENSITY_REPULSIVE
  19.  
  20. # Parameters for global repulsive
  21. FIRST_REPULSIVE_RANGE = 5
  22. FIRST_REPULSIVE_STRENGTH = 0.02
  23. REPULSIVE_RANGE_MULTIPLIER = 1.25 # Multiplier for range scaling between layers
  24. REPULSIVE_STRENGTH_MULTIPLIER = 0.8 # Multiplier for strength scaling between layers
  25. REPULSIVE_FALLOFF = 'quadratic' # 'linear', 'quadratic', or 'none'
  26.  
  27. # Parameters for global attractive
  28. FIRST_ATTRACTIVE_RANGE = 8
  29. FIRST_ATTRACTIVE_STRENGTH = 0.04
  30. ATTRACTIVE_RANGE_MULTIPLIER = 2.0 # Multiplier for range scaling between layers
  31. ATTRACTIVE_STRENGTH_MULTIPLIER = 0.8# Multiplier for strength scaling between layers
  32. ATTRACTIVE_FALLOFF = 'linear' # 'linear', 'quadratic', or 'none'
  33.  
  34. # Parameters for type-specific
  35. FIRST_TYPE_RANGE =3.5
  36. FIRST_TYPE_STRENGTH = 0.5
  37. TYPE_RANGE_MULTIPLIER = 1.5 # Multiplier for range scaling between layers
  38. TYPE_STRENGTH_MULTIPLIER = 0.85 # Multiplier for strength scaling between layers
  39. TYPE_FALLOFF = 'quadratic' # 'linear', 'quadratic', or 'none'
  40.  
  41. # Parameters for density-dependent repulsive
  42. FIRST_DENSITY_RANGE =7
  43. FIRST_DENSITY_STRENGTH = 0.1
  44. DENSITY_RANGE_MULTIPLIER = 1.8 # Multiplier for range scaling between layers
  45. DENSITY_STRENGTH_MULTIPLIER = 0.25 # Multiplier for strength scaling between layers
  46. DENSITY_FALLOFF = 'linear' # 'linear', 'quadratic', or 'none'
  47. FIRST_DENSITY_SCALING = 1 # Scaling factor per nearby particle (additional scaling per particle)
  48. DENSITY_SCALING_MULTIPLIER = 1 # Multiplier for scaling factor between layers
  49.  
  50. # Density-dependent damping parameters - SMOOTH QUADRATIC VERSION
  51. DENSITY_DAMPING_RADIUS = 25.0
  52. MIN_NEIGHBORS = 0.0 # below this → almost full damping (very little slowing)
  53. MAX_NEIGHBORS = 350.0 # above this → maximum damping (strong slowing)
  54. MAX_DAMPING = 0.9 # low density: very little velocity loss
  55. MIN_DAMPING = 0.8 # high density: strong velocity loss
  56.  
  57. DT = 0.3 # reduced time step for stability
  58.  
  59. # ================== CUDA KERNEL ==================
  60.  
  61. kernel_code = """
  62. __global__ void update_particles(
  63. float *pos_x, float *pos_y,
  64. float *vel_x, float *vel_y,
  65. int *types,
  66. float *interaction_matrices, // shape: [num_forces * num_types * num_types]
  67. float *force_ranges,
  68. float *force_strengths,
  69. float *falloff_exponents,
  70. int *force_types,
  71. float *density_scalings,
  72. int n_particles,
  73. int n_types,
  74. int num_forces,
  75. float dt,
  76. float world_size,
  77. float density_damping_radius,
  78. float min_neighbors,
  79. float max_neighbors,
  80. float max_damping,
  81. float min_damping
  82. ) {
  83. int idx = blockIdx.x * blockDim.x + threadIdx.x;
  84. if (idx >= n_particles) return;
  85.  
  86. float x = pos_x[idx];
  87. float y = pos_y[idx];
  88. int ta = types[idx];
  89.  
  90. float fx = 0.0f;
  91. float fy = 0.0f;
  92.  
  93. float damping_count = 0.0f;
  94.  
  95. // First pass: compute neighbor counts for density-dependent forces and damping
  96. float counts[32];
  97. for (int i = 0; i < 32; i++) counts[i] = 0.0f;
  98.  
  99. for (int j = 0; j < n_particles; ++j) {
  100. if (j == idx) continue;
  101.  
  102. float dx = pos_x[j] - x;
  103. float dy = pos_y[j] - y;
  104.  
  105. // periodic boundary
  106. dx -= world_size * roundf(dx / world_size);
  107. dy -= world_size * roundf(dy / world_size);
  108.  
  109. float dist2 = dx*dx + dy*dy;
  110. if (dist2 < 1e-10f) continue;
  111. float dist = sqrtf(dist2);
  112.  
  113. if (dist < density_damping_radius) {
  114. damping_count += 1.0f;
  115. }
  116.  
  117. for (int f = 0; f < num_forces; ++f) {
  118. if (force_types[f] == 1 && dist < force_ranges[f]) {
  119. counts[f] += 1.0f;
  120. }
  121. }
  122. }
  123.  
  124. // SMOOTH QUADRATIC DAMPING
  125. float t = (damping_count - min_neighbors) / (max_neighbors - min_neighbors);
  126. t = fmaxf(0.0f, fminf(1.0f, t)); // clamp to [0,1]
  127. float t2 = t * t;
  128. float local_damping = max_damping - (max_damping - min_damping) * t2;
  129.  
  130. // Second pass: compute forces
  131. for (int j = 0; j < n_particles; ++j) {
  132. if (j == idx) continue;
  133.  
  134. float dx = pos_x[j] - x;
  135. float dy = pos_y[j] - y;
  136.  
  137. // periodic boundary
  138. dx -= world_size * roundf(dx / world_size);
  139. dy -= world_size * roundf(dy / world_size);
  140.  
  141. float dist2 = dx*dx + dy*dy;
  142. if (dist2 < 1e-10f) continue;
  143. float dist = sqrtf(dist2);
  144.  
  145. int tb = types[j];
  146.  
  147. // Sum over all force layers
  148. for (int f = 0; f < num_forces; ++f) {
  149. float r_max = force_ranges[f];
  150. if (dist >= r_max) continue;
  151.  
  152. // Get the interaction strength for this pair and this force
  153. float k = interaction_matrices[f * n_types * n_types + ta * n_types + tb];
  154.  
  155. float norm_dist = dist / r_max;
  156. float base_fall = 1.0f - norm_dist;
  157. base_fall = fmaxf(0.0f, base_fall);
  158.  
  159. float exp = falloff_exponents[f];
  160. float fall = (exp < 1e-6f) ? 1.0f : powf(base_fall, exp);
  161.  
  162. float scale = 1.0f;
  163. if (force_types[f] == 1) {
  164. scale = density_scalings[f] * counts[f];
  165. }
  166.  
  167. float f_mag = k * force_strengths[f] * fall * scale;
  168. float inv_dist = 1.0f / (dist + 1e-5f);
  169.  
  170. fx += f_mag * dx * inv_dist;
  171. fy += f_mag * dy * inv_dist;
  172. }
  173. }
  174.  
  175. // Update velocity and position
  176. vel_x[idx] += fx * dt;
  177. vel_y[idx] += fy * dt;
  178.  
  179. vel_x[idx] *= local_damping;
  180. vel_y[idx] *= local_damping;
  181.  
  182. pos_x[idx] += vel_x[idx] * dt;
  183. pos_y[idx] += vel_y[idx] * dt;
  184.  
  185. // Wrap around (periodic boundaries)
  186. float hw = world_size * 0.5f;
  187. if (pos_x[idx] < -hw) pos_x[idx] += world_size;
  188. if (pos_x[idx] > hw) pos_x[idx] -= world_size;
  189. if (pos_y[idx] < -hw) pos_y[idx] += world_size;
  190. if (pos_y[idx] > hw) pos_y[idx] -= world_size;
  191. }
  192.  
  193. __global__ void prepare_render(
  194. float *pos_x, float *pos_y,
  195. int *types,
  196. float *type_colors, // flattened [type * 3]
  197. float *rx, float *ry,
  198. float *colors,
  199. int n_particles,
  200. float world_size
  201. ) {
  202. int idx = blockIdx.x * blockDim.x + threadIdx.x;
  203. if (idx >= n_particles) return;
  204.  
  205. rx[idx] = (pos_x[idx] / world_size) * 2.0f;
  206. ry[idx] = (pos_y[idx] / world_size) * 2.0f;
  207.  
  208. int t = types[idx];
  209. colors[idx*3 + 0] = type_colors[t*3 + 0];
  210. colors[idx*3 + 1] = type_colors[t*3 + 1];
  211. colors[idx*3 + 2] = type_colors[t*3 + 2];
  212. }
  213. """
  214.  
  215. class MinimalParticleLife:
  216. def __init__(self):
  217. pygame.init()
  218. self.screen = pygame.display.set_mode((1280, 720), DOUBLEBUF | OPENGL)
  219. pygame.display.set_caption("Particle Life - Press R to reset with new matrices")
  220. glEnable(GL_POINT_SMOOTH)
  221. glPointSize(3.0)
  222. glEnable(GL_BLEND)
  223. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  224.  
  225. self.mod = SourceModule(kernel_code)
  226. self.update_kernel = self.mod.get_function("update_particles")
  227. self.render_kernel = self.mod.get_function("prepare_render")
  228.  
  229. # GPU arrays that persist
  230. self.d_rx = gpuarray.zeros(NUM_PARTICLES, np.float32)
  231. self.d_ry = gpuarray.zeros(NUM_PARTICLES, np.float32)
  232. self.d_colors = gpuarray.zeros(NUM_PARTICLES * 3, np.float32)
  233.  
  234. # Initialize first simulation state
  235. self.reset_simulation()
  236.  
  237. def reset_simulation(self):
  238. """Regenerate all random elements: positions, velocities, types, matrices, colors"""
  239. half = WORLD_SIZE / 2
  240.  
  241. # Random particles
  242. pos_x = np.random.uniform(-half, half, NUM_PARTICLES).astype(np.float32)
  243. pos_y = np.random.uniform(-half, half, NUM_PARTICLES).astype(np.float32)
  244. vel_x = np.random.uniform(-0.3, 0.3, NUM_PARTICLES).astype(np.float32)
  245. vel_y = np.random.uniform(-0.3, 0.3, NUM_PARTICLES).astype(np.float32)
  246. types = np.random.randint(0, NUM_TYPES, NUM_PARTICLES).astype(np.int32)
  247.  
  248. self.d_pos_x = gpuarray.to_gpu(pos_x)
  249. self.d_pos_y = gpuarray.to_gpu(pos_y)
  250. self.d_vel_x = gpuarray.to_gpu(vel_x)
  251. self.d_vel_y = gpuarray.to_gpu(vel_y)
  252. self.d_types = gpuarray.to_gpu(types)
  253.  
  254. # === Per-force interaction matrices ===
  255. matrices = np.zeros((NUM_FORCES, NUM_TYPES, NUM_TYPES), dtype=np.float32)
  256.  
  257. # Force parameters: ranges, strengths, exponents
  258. ranges = np.zeros(NUM_FORCES, dtype=np.float32)
  259. strengths = np.zeros(NUM_FORCES, dtype=np.float32)
  260. falloff_exponents = np.zeros(NUM_FORCES, dtype=np.float32)
  261. force_types = np.zeros(NUM_FORCES, dtype=np.int32) # 0: normal, 1: density-dependent
  262. density_scalings = np.zeros(NUM_FORCES, dtype=np.float32)
  263.  
  264. falloff_map = {'none': 0.0, 'linear': 1.0, 'quadratic': 2.0, '8-dratic': 8.0, '16-dratic': 16.0}
  265.  
  266. # Global repulsive forces
  267. force_index = 0
  268. r = FIRST_REPULSIVE_RANGE
  269. s = FIRST_REPULSIVE_STRENGTH
  270. falloff_exp = falloff_map[REPULSIVE_FALLOFF]
  271. for _ in range(NUM_GLOBAL_REPULSIVE):
  272. ranges[force_index] = r
  273. strengths[force_index] = s
  274. falloff_exponents[force_index] = falloff_exp
  275. matrices[force_index, :, :] = -1.0
  276. force_index += 1
  277. r *= REPULSIVE_RANGE_MULTIPLIER
  278. s *= REPULSIVE_STRENGTH_MULTIPLIER
  279.  
  280. # Global attractive forces
  281. r = FIRST_ATTRACTIVE_RANGE
  282. s = FIRST_ATTRACTIVE_STRENGTH
  283. falloff_exp = falloff_map[ATTRACTIVE_FALLOFF]
  284. for _ in range(NUM_GLOBAL_ATTRACTIVE):
  285. ranges[force_index] = r
  286. strengths[force_index] = s
  287. falloff_exponents[force_index] = falloff_exp
  288. matrices[force_index, :, :] = 1.0
  289. force_index += 1
  290. r *= ATTRACTIVE_RANGE_MULTIPLIER
  291. s *= ATTRACTIVE_STRENGTH_MULTIPLIER
  292.  
  293. # Type-specific forces
  294. r = FIRST_TYPE_RANGE
  295. s = FIRST_TYPE_STRENGTH
  296. falloff_exp = falloff_map[TYPE_FALLOFF]
  297. for _ in range(NUM_TYPE_SPECIFIC):
  298. ranges[force_index] = r
  299. strengths[force_index] = s
  300. falloff_exponents[force_index] = falloff_exp
  301. matrices[force_index, :, :] = np.random.uniform(-1.0, 1.0, (NUM_TYPES, NUM_TYPES))
  302. force_index += 1
  303. r *= TYPE_RANGE_MULTIPLIER
  304. s *= TYPE_STRENGTH_MULTIPLIER
  305.  
  306. # Density-dependent repulsive forces
  307. r = FIRST_DENSITY_RANGE
  308. s = FIRST_DENSITY_STRENGTH
  309. sc = FIRST_DENSITY_SCALING
  310. falloff_exp = falloff_map[DENSITY_FALLOFF]
  311. for _ in range(NUM_DENSITY_REPULSIVE):
  312. ranges[force_index] = r
  313. strengths[force_index] = s
  314. falloff_exponents[force_index] = falloff_exp
  315. density_scalings[force_index] = sc
  316. matrices[force_index, :, :] = -1.0
  317. force_types[force_index] = 1
  318. force_index += 1
  319. r *= DENSITY_RANGE_MULTIPLIER
  320. s *= DENSITY_STRENGTH_MULTIPLIER
  321. sc *= DENSITY_SCALING_MULTIPLIER
  322.  
  323. self.d_matrices = gpuarray.to_gpu(matrices.flatten())
  324. self.d_ranges = gpuarray.to_gpu(ranges)
  325. self.d_strengths = gpuarray.to_gpu(strengths)
  326. self.d_falloff_exponents = gpuarray.to_gpu(falloff_exponents)
  327. self.d_force_types = gpuarray.to_gpu(force_types)
  328. self.d_density_scalings = gpuarray.to_gpu(density_scalings)
  329.  
  330. # Random colors
  331. colors = np.random.rand(NUM_TYPES, 3).astype(np.float32)
  332. self.d_type_colors = gpuarray.to_gpu(colors.flatten())
  333.  
  334. print(f"Simulation reset with {NUM_GLOBAL_REPULSIVE} global repulsive, {NUM_GLOBAL_ATTRACTIVE} global attractive, {NUM_TYPE_SPECIFIC} type-specific, and {NUM_DENSITY_REPULSIVE} density-dependent repulsive force layers")
  335.  
  336. def update(self):
  337. block = (256, 1, 1)
  338. grid = ((NUM_PARTICLES + 255) // 256, 1)
  339.  
  340. self.update_kernel(
  341. self.d_pos_x, self.d_pos_y,
  342. self.d_vel_x, self.d_vel_y,
  343. self.d_types,
  344. self.d_matrices,
  345. self.d_ranges,
  346. self.d_strengths,
  347. self.d_falloff_exponents,
  348. self.d_force_types,
  349. self.d_density_scalings,
  350. np.int32(NUM_PARTICLES),
  351. np.int32(NUM_TYPES),
  352. np.int32(NUM_FORCES),
  353. np.float32(DT),
  354. np.float32(WORLD_SIZE),
  355. np.float32(DENSITY_DAMPING_RADIUS),
  356. np.float32(MIN_NEIGHBORS),
  357. np.float32(MAX_NEIGHBORS),
  358. np.float32(MAX_DAMPING),
  359. np.float32(MIN_DAMPING),
  360. block=block, grid=grid
  361. )
  362.  
  363. def render(self):
  364. glClear(GL_COLOR_BUFFER_BIT)
  365. glLoadIdentity()
  366.  
  367. self.render_kernel(
  368. self.d_pos_x, self.d_pos_y,
  369. self.d_types,
  370. self.d_type_colors,
  371. self.d_rx, self.d_ry,
  372. self.d_colors,
  373. np.int32(NUM_PARTICLES),
  374. np.float32(WORLD_SIZE),
  375. block=(256,1,1), grid=((NUM_PARTICLES+255)//256, 1)
  376. )
  377.  
  378. x = self.d_rx.get()
  379. y = self.d_ry.get()
  380. c = self.d_colors.get().reshape(-1, 3)
  381.  
  382. vertices = np.column_stack((x, y)).astype(np.float32)
  383.  
  384. glEnableClientState(GL_VERTEX_ARRAY)
  385. glEnableClientState(GL_COLOR_ARRAY)
  386. glVertexPointer(2, GL_FLOAT, 0, vertices)
  387. glColorPointer(3, GL_FLOAT, 0, c)
  388. glDrawArrays(GL_POINTS, 0, NUM_PARTICLES)
  389. glDisableClientState(GL_VERTEX_ARRAY)
  390. glDisableClientState(GL_COLOR_ARRAY)
  391.  
  392. pygame.display.flip()
  393.  
  394. def run(self):
  395. clock = pygame.time.Clock()
  396.  
  397. running = True
  398.  
  399. print("Particle Life with user-defined global repulsive, attractive, type-specific, and density-dependent repulsive force layers")
  400. print("Now with smooth quadratic density-dependent damping:")
  401. print(f" - Damping smoothly transitions from {MAX_DAMPING} (low density) to {MIN_DAMPING} (high density)")
  402. print(f" - Low density threshold: {MIN_NEIGHBORS} neighbors")
  403. print(f" - High density threshold: {MAX_NEIGHBORS} neighbors")
  404. print(f" - Density measured within radius {DENSITY_DAMPING_RADIUS}")
  405. print("Press R to reset with new random matrices")
  406. print("ESC or close window to quit")
  407.  
  408. while running:
  409. for event in pygame.event.get():
  410. if event.type == QUIT:
  411. running = False
  412. elif event.type == KEYDOWN:
  413. if event.key == K_ESCAPE:
  414. running = False
  415. elif event.key == K_r:
  416. self.reset_simulation()
  417.  
  418. self.update()
  419. self.render()
  420. clock.tick(60)
  421.  
  422. pygame.quit()
  423.  
  424. if __name__ == "__main__":
  425. sim = MinimalParticleLife()
  426. sim.run()
Advertisement
Add Comment
Please, Sign In to add comment