// physarum-cpu: one step of a Physarum (slime-mould) transport-network sim. // Synchronous update: all agents sense the start-of-step field along three // sensors, steer up-gradient and move (parallel over agents, each agent's jitter // from an index-seeded RNG); deposits are then applied in a serial index-order // pass, and the field is diffused (separable 3-tap) and decayed. On a cadence // the agents are counting-sorted into tile order so that consecutive (and // same-thread) agents sense a small hot window instead of scattering three // gathers across the whole field -- the gathers dominate and otherwise miss. // Read-only sensing + ordered deposit + serial sort make the step deterministic // for any thread count. Agent trig/arithmetic and the diffuse stencil vectorise // on NEON (aarch64); a portable scalar path covers everything else. #include #include #include #include #include #include #include static const int TILE = 32; // sort granularity (px); ~8-step drift stays inside static const int RESORT = 8; // re-sort every RESORT steps (amortises the sort) namespace { // Fan a range [0,total) across at::get_num_threads() worker threads, each given // a contiguous chunk; f(begin,end) must touch only its own slice. std::thread // (not OpenMP) so it parallelises under the stock kernel-builder compile, which // exposes no way to pass -fopenmp to a CPU kernel. Falls back to serial for a // single thread or a small range. template inline void parallel_chunks(int total, F f){ int T = at::get_num_threads(); if (T < 1) T = 1; if (T == 1 || total < 4096){ f(0, total); return; } if (T > total) T = total; int chunk = (total + T - 1) / T; std::vector pool; pool.reserve(T - 1); for (int t=1; ttotal?total:b+chunk; if (b>=e) break; pool.emplace_back([&,b,e]{ f(b,e); }); } f(0, chunk>total?total:chunk); // calling thread runs chunk 0 for (auto& th : pool) th.join(); } inline uint32_t xr(uint32_t &s){ s ^= s<<13; s ^= s>>17; s ^= s<<5; return s; } inline float frand(uint32_t &s){ return (xr(s) >> 8) * (1.0f/16777216.0f); } // per-agent, per-step independent seed: the step's `seed` argument already // encodes base_seed+t, so hashing it with the agent index gives a stream that // varies per agent and per step yet does not depend on iteration order. inline uint32_t agent_seed(uint32_t base, int i){ uint32_t s = (uint32_t)i*2654435761u ^ base*2246822519u; s ^= s>>15; s *= 2246822519u; s ^= s>>13; s *= 3266489917u; s ^= s>>16; return s ? s : 1u; } inline float fsin(float x){ // parabola approx, |err|<1e-3 float k = x * 0.15915494f; k = x - 6.2831853f * (float)((int)(k + (k < 0 ? -0.5f : 0.5f))); float y = 1.27323954f*k - 0.405284735f*k*(k<0?-k:k); return 0.225f*(y*(y<0?-y:y) - y) + y; } inline float fcos(float x){ return fsin(x + 1.5707963f); } inline float samp(const float* t, float x, float y, int W, int H){ int xi = ((int)x % W + W) % W, yi = ((int)y % H + H) % H; return t[(size_t)yi*W + xi]; } void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N, float sd, float sa, float turn, float speed, uint32_t base); void deposit(float* t, const float* ax, const float* ay, int W, int H, int N, float dep); void diffuse(float* t, float* tmp, int W, int H, float decay); #if defined(__ARM_NEON) #include inline float32x4_t vsin(float32x4_t x){ float32x4_t k = vrndnq_f32(vmulq_n_f32(x, 0.15915494f)); x = vmlsq_n_f32(x, k, 6.2831853f); float32x4_t ax = vabsq_f32(x); float32x4_t y = vsubq_f32(vmulq_n_f32(x,1.27323954f), vmulq_n_f32(vmulq_f32(x,ax),0.405284735f)); float32x4_t ay = vabsq_f32(y); return vmlaq_n_f32(y, vsubq_f32(vmulq_f32(y,ay), y), 0.225f); } inline float32x4_t vcos(float32x4_t x){ return vsin(vaddq_f32(x, vdupq_n_f32(1.5707963f))); } // sense + steer + move for the agent quads [q0,q1); reads the field only, writes // ax/ay/ah. No field writes, so any set of quads runs on any thread. inline void move_quads(const float* t, float* ax, float* ay, float* ah, int q0, int q1, int W, int H, float sd, float sa, float turn, float speed, uint32_t base){ float32x4_t vsd=vdupq_n_f32(sd), vsp=vdupq_n_f32(speed), vsa=vdupq_n_f32(sa); float32x4_t vW=vdupq_n_f32((float)W), vH=vdupq_n_f32((float)H); float32x4_t iW=vdupq_n_f32(1.0f/W), iH=vdupq_n_f32(1.0f/H); for (int q=q0; qfl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn; x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H); ax[i]=x; ay[i]=y; ah[i]=h; } } void diffuse(float* t, float* tmp, int W, int H, float dec){ for (int y=0;yfl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn; x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H); ax[i]=x; ay[i]=y; ah[i]=h; } }); } void diffuse(float* t, float* tmp, int W, int H, float dec){ for (int y=0;y=0&&xi=0&&yi off(nt+1, 0), cur(nt); auto tid=[&](int i){ int xi=(int)ax[i]; xi=xi<0?0:(xi>=W?W-1:xi); int yi=(int)ay[i]; yi=yi<0?0:(yi>=H?H-1:yi); return (yi/tile)*TW + xi/tile; }; for (int i=0;i sx(N), sy(N), sh(N); for (int i=0;i(); // seed increments per step (base_seed + t), so this re-sorts every RESORT steps if ((uint64_t)seed % (uint64_t)RESORT == 0) bin_sort(ax.data_ptr(), ay.data_ptr(), ah.data_ptr(), W,H,N, TILE); agents(t, ax.data_ptr(), ay.data_ptr(), ah.data_ptr(), W,H,N, (float)sense_dist,(float)sense_ang,(float)turn,(float)speed, base); deposit(t, ax.data_ptr(), ay.data_ptr(), W,H,N, (float)deposit_); diffuse(t, tmp.data_ptr(), W,H,(float)decay); }