publish buildable source (threaded + tile-sorted kernel)
Browse files- build.toml +22 -0
- flake.nix +17 -0
- load_local.py +35 -0
- local_bind.cpp +11 -0
- physarum_csrc/physarum_cpu.cpp +225 -0
- tests/test_physarum.py +28 -0
- torch-ext/physarum/__init__.py +85 -0
- torch-ext/torch_binding.cpp +15 -0
- torch-ext/torch_binding.h +7 -0
build.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[general]
|
| 2 |
+
name = "physarum"
|
| 3 |
+
version = 1
|
| 4 |
+
edition = 5
|
| 5 |
+
license = "Apache-2.0"
|
| 6 |
+
backends = [
|
| 7 |
+
"cpu",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
[general.hub]
|
| 11 |
+
repo-id = "phanerozoic/physarum"
|
| 12 |
+
|
| 13 |
+
[torch]
|
| 14 |
+
src = [
|
| 15 |
+
"torch-ext/torch_binding.cpp",
|
| 16 |
+
"torch-ext/torch_binding.h",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[kernel.physarum]
|
| 20 |
+
backend = "cpu"
|
| 21 |
+
depends = ["torch"]
|
| 22 |
+
src = ["physarum_csrc/physarum_cpu.cpp"]
|
flake.nix
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
description = "Flake for physarum kernel";
|
| 3 |
+
|
| 4 |
+
inputs = {
|
| 5 |
+
builder.url = "github:huggingface/kernels";
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
outputs =
|
| 9 |
+
{
|
| 10 |
+
self,
|
| 11 |
+
builder,
|
| 12 |
+
}:
|
| 13 |
+
builder.lib.genKernelFlakeOutputs {
|
| 14 |
+
inherit self;
|
| 15 |
+
path = ./.;
|
| 16 |
+
};
|
| 17 |
+
}
|
load_local.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load physarum natively on hosts without a published variant (e.g. a Pi).
|
| 2 |
+
|
| 3 |
+
JIT-builds the local source with `torch.utils.cpp_extension` and exposes the
|
| 4 |
+
identical `physarum` module API.
|
| 5 |
+
|
| 6 |
+
import load_local
|
| 7 |
+
physarum = load_local.load()
|
| 8 |
+
sim = physarum.Physarum(width=1024, height=1024, agents=200000)
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import types
|
| 13 |
+
|
| 14 |
+
_cached = None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load(verbose=False):
|
| 18 |
+
global _cached
|
| 19 |
+
if _cached is not None:
|
| 20 |
+
return _cached
|
| 21 |
+
from torch.utils.cpp_extension import load as _jit
|
| 22 |
+
|
| 23 |
+
root = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
+
ext = _jit(name="physarum_ext",
|
| 25 |
+
sources=[os.path.join(root, "local_bind.cpp"),
|
| 26 |
+
os.path.join(root, "physarum_csrc", "physarum_cpu.cpp")],
|
| 27 |
+
extra_cflags=["-O3"],
|
| 28 |
+
verbose=verbose)
|
| 29 |
+
ops_mod = types.ModuleType("physarum._ops")
|
| 30 |
+
ops_mod.ops = type("_Ops", (), {"physarum_step": staticmethod(ext.physarum_step)})()
|
| 31 |
+
sys.modules["physarum._ops"] = ops_mod
|
| 32 |
+
sys.path.insert(0, os.path.join(root, "torch-ext"))
|
| 33 |
+
import physarum
|
| 34 |
+
_cached = physarum
|
| 35 |
+
return physarum
|
local_bind.cpp
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// JIT binding for load_local.py; the published build registers via torch_binding.cpp.
|
| 2 |
+
#include <torch/extension.h>
|
| 3 |
+
|
| 4 |
+
void physarum_step(torch::Tensor trail, torch::Tensor tmp,
|
| 5 |
+
torch::Tensor ax, torch::Tensor ay, torch::Tensor ah,
|
| 6 |
+
int64_t seed, double sense_dist, double sense_ang,
|
| 7 |
+
double turn, double speed, double deposit, double decay);
|
| 8 |
+
|
| 9 |
+
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
| 10 |
+
m.def("physarum_step", &physarum_step);
|
| 11 |
+
}
|
physarum_csrc/physarum_cpu.cpp
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// physarum-cpu: one step of a Physarum (slime-mould) transport-network sim.
|
| 2 |
+
// Synchronous update: all agents sense the start-of-step field along three
|
| 3 |
+
// sensors, steer up-gradient and move (parallel over agents, each agent's jitter
|
| 4 |
+
// from an index-seeded RNG); deposits are then applied in a serial index-order
|
| 5 |
+
// pass, and the field is diffused (separable 3-tap) and decayed. On a cadence
|
| 6 |
+
// the agents are counting-sorted into tile order so that consecutive (and
|
| 7 |
+
// same-thread) agents sense a small hot window instead of scattering three
|
| 8 |
+
// gathers across the whole field -- the gathers dominate and otherwise miss.
|
| 9 |
+
// Read-only sensing + ordered deposit + serial sort make the step deterministic
|
| 10 |
+
// for any thread count. Agent trig/arithmetic and the diffuse stencil vectorise
|
| 11 |
+
// on NEON (aarch64); a portable scalar path covers everything else.
|
| 12 |
+
#include <torch/torch.h>
|
| 13 |
+
#include <ATen/Parallel.h>
|
| 14 |
+
#include <cstdint>
|
| 15 |
+
#include <cstring>
|
| 16 |
+
#include <cmath>
|
| 17 |
+
#include <thread>
|
| 18 |
+
#include <vector>
|
| 19 |
+
|
| 20 |
+
static const int TILE = 32; // sort granularity (px); ~8-step drift stays inside
|
| 21 |
+
static const int RESORT = 8; // re-sort every RESORT steps (amortises the sort)
|
| 22 |
+
|
| 23 |
+
namespace {
|
| 24 |
+
|
| 25 |
+
// Fan a range [0,total) across at::get_num_threads() worker threads, each given
|
| 26 |
+
// a contiguous chunk; f(begin,end) must touch only its own slice. std::thread
|
| 27 |
+
// (not OpenMP) so it parallelises under the stock kernel-builder compile, which
|
| 28 |
+
// exposes no way to pass -fopenmp to a CPU kernel. Falls back to serial for a
|
| 29 |
+
// single thread or a small range.
|
| 30 |
+
template<class F>
|
| 31 |
+
inline void parallel_chunks(int total, F f){
|
| 32 |
+
int T = at::get_num_threads(); if (T < 1) T = 1;
|
| 33 |
+
if (T == 1 || total < 4096){ f(0, total); return; }
|
| 34 |
+
if (T > total) T = total;
|
| 35 |
+
int chunk = (total + T - 1) / T;
|
| 36 |
+
std::vector<std::thread> pool; pool.reserve(T - 1);
|
| 37 |
+
for (int t=1; t<T; t++){
|
| 38 |
+
int b=t*chunk, e=b+chunk>total?total:b+chunk;
|
| 39 |
+
if (b>=e) break;
|
| 40 |
+
pool.emplace_back([&,b,e]{ f(b,e); });
|
| 41 |
+
}
|
| 42 |
+
f(0, chunk>total?total:chunk); // calling thread runs chunk 0
|
| 43 |
+
for (auto& th : pool) th.join();
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
inline uint32_t xr(uint32_t &s){ s ^= s<<13; s ^= s>>17; s ^= s<<5; return s; }
|
| 47 |
+
inline float frand(uint32_t &s){ return (xr(s) >> 8) * (1.0f/16777216.0f); }
|
| 48 |
+
|
| 49 |
+
// per-agent, per-step independent seed: the step's `seed` argument already
|
| 50 |
+
// encodes base_seed+t, so hashing it with the agent index gives a stream that
|
| 51 |
+
// varies per agent and per step yet does not depend on iteration order.
|
| 52 |
+
inline uint32_t agent_seed(uint32_t base, int i){
|
| 53 |
+
uint32_t s = (uint32_t)i*2654435761u ^ base*2246822519u;
|
| 54 |
+
s ^= s>>15; s *= 2246822519u; s ^= s>>13; s *= 3266489917u; s ^= s>>16;
|
| 55 |
+
return s ? s : 1u;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
inline float fsin(float x){ // parabola approx, |err|<1e-3
|
| 59 |
+
float k = x * 0.15915494f;
|
| 60 |
+
k = x - 6.2831853f * (float)((int)(k + (k < 0 ? -0.5f : 0.5f)));
|
| 61 |
+
float y = 1.27323954f*k - 0.405284735f*k*(k<0?-k:k);
|
| 62 |
+
return 0.225f*(y*(y<0?-y:y) - y) + y;
|
| 63 |
+
}
|
| 64 |
+
inline float fcos(float x){ return fsin(x + 1.5707963f); }
|
| 65 |
+
|
| 66 |
+
inline float samp(const float* t, float x, float y, int W, int H){
|
| 67 |
+
int xi = ((int)x % W + W) % W, yi = ((int)y % H + H) % H;
|
| 68 |
+
return t[(size_t)yi*W + xi];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
|
| 72 |
+
float sd, float sa, float turn, float speed, uint32_t base);
|
| 73 |
+
void deposit(float* t, const float* ax, const float* ay, int W, int H, int N, float dep);
|
| 74 |
+
void diffuse(float* t, float* tmp, int W, int H, float decay);
|
| 75 |
+
|
| 76 |
+
#if defined(__ARM_NEON)
|
| 77 |
+
#include <arm_neon.h>
|
| 78 |
+
inline float32x4_t vsin(float32x4_t x){
|
| 79 |
+
float32x4_t k = vrndnq_f32(vmulq_n_f32(x, 0.15915494f));
|
| 80 |
+
x = vmlsq_n_f32(x, k, 6.2831853f);
|
| 81 |
+
float32x4_t ax = vabsq_f32(x);
|
| 82 |
+
float32x4_t y = vsubq_f32(vmulq_n_f32(x,1.27323954f),
|
| 83 |
+
vmulq_n_f32(vmulq_f32(x,ax),0.405284735f));
|
| 84 |
+
float32x4_t ay = vabsq_f32(y);
|
| 85 |
+
return vmlaq_n_f32(y, vsubq_f32(vmulq_f32(y,ay), y), 0.225f);
|
| 86 |
+
}
|
| 87 |
+
inline float32x4_t vcos(float32x4_t x){ return vsin(vaddq_f32(x, vdupq_n_f32(1.5707963f))); }
|
| 88 |
+
|
| 89 |
+
// sense + steer + move for the agent quads [q0,q1); reads the field only, writes
|
| 90 |
+
// ax/ay/ah. No field writes, so any set of quads runs on any thread.
|
| 91 |
+
inline void move_quads(const float* t, float* ax, float* ay, float* ah,
|
| 92 |
+
int q0, int q1, int W, int H, float sd, float sa,
|
| 93 |
+
float turn, float speed, uint32_t base){
|
| 94 |
+
float32x4_t vsd=vdupq_n_f32(sd), vsp=vdupq_n_f32(speed), vsa=vdupq_n_f32(sa);
|
| 95 |
+
float32x4_t vW=vdupq_n_f32((float)W), vH=vdupq_n_f32((float)H);
|
| 96 |
+
float32x4_t iW=vdupq_n_f32(1.0f/W), iH=vdupq_n_f32(1.0f/H);
|
| 97 |
+
for (int q=q0; q<q1; q++){
|
| 98 |
+
int i=q*4;
|
| 99 |
+
float32x4_t x=vld1q_f32(ax+i), y=vld1q_f32(ay+i), h=vld1q_f32(ah+i);
|
| 100 |
+
float32x4_t hl=vsubq_f32(h,vsa), hr=vaddq_f32(h,vsa);
|
| 101 |
+
float cx[4],cy[4],lx[4],ly[4],rx[4],ry[4];
|
| 102 |
+
vst1q_f32(cx, vmlaq_f32(x, vcos(h), vsd)); vst1q_f32(cy, vmlaq_f32(y, vsin(h), vsd));
|
| 103 |
+
vst1q_f32(lx, vmlaq_f32(x, vcos(hl),vsd)); vst1q_f32(ly, vmlaq_f32(y, vsin(hl),vsd));
|
| 104 |
+
vst1q_f32(rx, vmlaq_f32(x, vcos(hr),vsd)); vst1q_f32(ry, vmlaq_f32(y, vsin(hr),vsd));
|
| 105 |
+
float FC[4],FL[4],FR[4];
|
| 106 |
+
for (int l=0;l<4;l++){ FC[l]=samp(t,cx[l],cy[l],W,H);
|
| 107 |
+
FL[l]=samp(t,lx[l],ly[l],W,H); FR[l]=samp(t,rx[l],ry[l],W,H); }
|
| 108 |
+
float32x4_t fc=vld1q_f32(FC), fl=vld1q_f32(FL), fr=vld1q_f32(FR);
|
| 109 |
+
float jit[4]; for(int l=0;l<4;l++){ uint32_t s=agent_seed(base,i+l); jit[l]=0.5f+0.5f*frand(s); }
|
| 110 |
+
float32x4_t vj=vld1q_f32(jit), vturn=vmulq_n_f32(vj,turn);
|
| 111 |
+
uint32x4_t straight=vandq_u32(vcgtq_f32(fc,fl), vcgtq_f32(fc,fr));
|
| 112 |
+
float32x4_t dh=vbslq_f32(vcgtq_f32(fl,fr), vnegq_f32(vturn),
|
| 113 |
+
vbslq_f32(vcgtq_f32(fr,fl), vturn,
|
| 114 |
+
vmulq_n_f32(vsubq_f32(vj,vdupq_n_f32(0.5f)), 2.0f*turn)));
|
| 115 |
+
dh=vbslq_f32(straight, vdupq_n_f32(0.0f), dh);
|
| 116 |
+
h=vaddq_f32(h,dh);
|
| 117 |
+
x=vmlaq_f32(x, vcos(h), vsp); y=vmlaq_f32(y, vsin(h), vsp);
|
| 118 |
+
x=vsubq_f32(x, vmulq_f32(vW, vrndmq_f32(vmulq_f32(x,iW))));
|
| 119 |
+
y=vsubq_f32(y, vmulq_f32(vH, vrndmq_f32(vmulq_f32(y,iH))));
|
| 120 |
+
vst1q_f32(ax+i,x); vst1q_f32(ay+i,y); vst1q_f32(ah+i,h);
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
|
| 124 |
+
float sd, float sa, float turn, float speed, uint32_t base){
|
| 125 |
+
int nq = N/4;
|
| 126 |
+
parallel_chunks(nq, [&](int q0, int q1){
|
| 127 |
+
move_quads(t, ax, ay, ah, q0, q1, W, H, sd, sa, turn, speed, base);
|
| 128 |
+
});
|
| 129 |
+
for (int i=nq*4; i<N; i++){ // scalar tail, same sync rule
|
| 130 |
+
float x=ax[i], y=ay[i], h=ah[i];
|
| 131 |
+
float fc=samp(t,x+fcos(h)*sd,y+fsin(h)*sd,W,H);
|
| 132 |
+
float fl=samp(t,x+fcos(h-sa)*sd,y+fsin(h-sa)*sd,W,H);
|
| 133 |
+
float fr=samp(t,x+fcos(h+sa)*sd,y+fsin(h+sa)*sd,W,H);
|
| 134 |
+
uint32_t s=agent_seed(base,i); float j=0.5f+0.5f*frand(s);
|
| 135 |
+
if(fc>fl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn;
|
| 136 |
+
x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H);
|
| 137 |
+
ax[i]=x; ay[i]=y; ah[i]=h;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
void diffuse(float* t, float* tmp, int W, int H, float dec){
|
| 141 |
+
for (int y=0;y<H;y++){ const float* r=t+(size_t)y*W; float* o=tmp+(size_t)y*W;
|
| 142 |
+
o[0]=0.25f*r[W-1]+0.5f*r[0]+0.25f*r[1]; int x=1;
|
| 143 |
+
for (; x+4<=W-1; x+=4){ float32x4_t L=vld1q_f32(r+x-1),C=vld1q_f32(r+x),R=vld1q_f32(r+x+1);
|
| 144 |
+
vst1q_f32(o+x, vmlaq_n_f32(vmlaq_n_f32(vmulq_n_f32(C,0.5f),L,0.25f),R,0.25f)); }
|
| 145 |
+
for (; x<W-1; x++) o[x]=0.25f*r[x-1]+0.5f*r[x]+0.25f*r[x+1];
|
| 146 |
+
o[W-1]=0.25f*r[W-2]+0.5f*r[W-1]+0.25f*r[0]; }
|
| 147 |
+
float32x4_t vd=vdupq_n_f32(dec);
|
| 148 |
+
for (int y=0;y<H;y++){ int yu=y?y-1:H-1, yd=y+1<H?y+1:0;
|
| 149 |
+
const float* U=tmp+(size_t)yu*W,*C=tmp+(size_t)y*W,*D=tmp+(size_t)yd*W;
|
| 150 |
+
float* o=t+(size_t)y*W; int x=0;
|
| 151 |
+
for (; x+4<=W; x+=4){ float32x4_t s=vmlaq_n_f32(vmlaq_n_f32(vmulq_n_f32(vld1q_f32(C+x),0.5f),
|
| 152 |
+
vld1q_f32(U+x),0.25f), vld1q_f32(D+x),0.25f); vst1q_f32(o+x, vmulq_f32(s,vd)); }
|
| 153 |
+
for (; x<W; x++) o[x]=(0.25f*U[x]+0.5f*C[x]+0.25f*D[x])*dec; }
|
| 154 |
+
}
|
| 155 |
+
#else // ---------- portable scalar ----------
|
| 156 |
+
void agents(const float* t, float* ax, float* ay, float* ah, int W, int H, int N,
|
| 157 |
+
float sd, float sa, float turn, float speed, uint32_t base){
|
| 158 |
+
parallel_chunks(N, [&](int i0, int i1){
|
| 159 |
+
for (int i=i0;i<i1;i++){
|
| 160 |
+
float x=ax[i], y=ay[i], h=ah[i];
|
| 161 |
+
float fc=samp(t,x+fcos(h)*sd,y+fsin(h)*sd,W,H);
|
| 162 |
+
float fl=samp(t,x+fcos(h-sa)*sd,y+fsin(h-sa)*sd,W,H);
|
| 163 |
+
float fr=samp(t,x+fcos(h+sa)*sd,y+fsin(h+sa)*sd,W,H);
|
| 164 |
+
uint32_t s=agent_seed(base,i); float j=0.5f+0.5f*frand(s);
|
| 165 |
+
if(fc>fl&&fc>fr){} else if(fl>fr) h-=turn*j; else if(fr>fl) h+=turn*j; else h+=(j-0.5f)*2.0f*turn;
|
| 166 |
+
x=fmodf(x+fcos(h)*speed+W,W); y=fmodf(y+fsin(h)*speed+H,H);
|
| 167 |
+
ax[i]=x; ay[i]=y; ah[i]=h;
|
| 168 |
+
}
|
| 169 |
+
});
|
| 170 |
+
}
|
| 171 |
+
void diffuse(float* t, float* tmp, int W, int H, float dec){
|
| 172 |
+
for (int y=0;y<H;y++){ const float* r=t+(size_t)y*W; float* o=tmp+(size_t)y*W;
|
| 173 |
+
for (int x=0;x<W;x++){ int xl=x?x-1:W-1, xr=x+1<W?x+1:0;
|
| 174 |
+
o[x]=0.25f*r[xl]+0.5f*r[x]+0.25f*r[xr]; } }
|
| 175 |
+
for (int y=0;y<H;y++){ int yu=y?y-1:H-1, yd=y+1<H?y+1:0;
|
| 176 |
+
const float* U=tmp+(size_t)yu*W,*C=tmp+(size_t)y*W,*D=tmp+(size_t)yd*W;
|
| 177 |
+
float* o=t+(size_t)y*W;
|
| 178 |
+
for (int x=0;x<W;x++) o[x]=(0.25f*U[x]+0.5f*C[x]+0.25f*D[x])*dec; }
|
| 179 |
+
}
|
| 180 |
+
#endif
|
| 181 |
+
|
| 182 |
+
// deposit is a serial index-order scatter so the field sum is identical for any
|
| 183 |
+
// thread count; positions are already wrapped into range by the move.
|
| 184 |
+
void deposit(float* t, const float* ax, const float* ay, int W, int H, int N, float dep){
|
| 185 |
+
for (int i=0;i<N;i++){ int xi=(int)ax[i], yi=(int)ay[i];
|
| 186 |
+
if(xi>=0&&xi<W&&yi>=0&&yi<H) t[(size_t)yi*W+xi]+=dep; }
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// counting-sort agents into row-major tile order (serial, deterministic). Tile
|
| 190 |
+
// coords are clamped because the position wrap can round to exactly x==W.
|
| 191 |
+
void bin_sort(float* ax, float* ay, float* ah, int W, int H, int N, int tile){
|
| 192 |
+
int TW=(W+tile-1)/tile, TH=(H+tile-1)/tile, nt=TW*TH;
|
| 193 |
+
std::vector<int> off(nt+1, 0), cur(nt);
|
| 194 |
+
auto tid=[&](int i){ int xi=(int)ax[i]; xi=xi<0?0:(xi>=W?W-1:xi);
|
| 195 |
+
int yi=(int)ay[i]; yi=yi<0?0:(yi>=H?H-1:yi);
|
| 196 |
+
return (yi/tile)*TW + xi/tile; };
|
| 197 |
+
for (int i=0;i<N;i++) off[tid(i)+1]++;
|
| 198 |
+
for (int t=1;t<=nt;t++) off[t]+=off[t-1];
|
| 199 |
+
for (int t=0;t<nt;t++) cur[t]=off[t];
|
| 200 |
+
std::vector<float> sx(N), sy(N), sh(N);
|
| 201 |
+
for (int i=0;i<N;i++){ int p=cur[tid(i)]++; sx[p]=ax[i]; sy[p]=ay[i]; sh[p]=ah[i]; }
|
| 202 |
+
memcpy(ax, sx.data(), (size_t)N*4);
|
| 203 |
+
memcpy(ay, sy.data(), (size_t)N*4);
|
| 204 |
+
memcpy(ah, sh.data(), (size_t)N*4);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
} // namespace
|
| 208 |
+
|
| 209 |
+
void physarum_step(torch::Tensor trail, torch::Tensor tmp,
|
| 210 |
+
torch::Tensor ax, torch::Tensor ay, torch::Tensor ah,
|
| 211 |
+
int64_t seed, double sense_dist, double sense_ang,
|
| 212 |
+
double turn, double speed, double deposit_, double decay){
|
| 213 |
+
TORCH_CHECK(trail.dtype()==torch::kFloat32 && trail.is_contiguous(), "trail must be f32 contiguous");
|
| 214 |
+
TORCH_CHECK(trail.dim()==2 && tmp.sizes()==trail.sizes(), "trail/tmp must be [H,W]");
|
| 215 |
+
int H=(int)trail.size(0), W=(int)trail.size(1), N=(int)ax.size(0);
|
| 216 |
+
uint32_t base = (uint32_t)(seed ^ 0x9e3779b9u); if(!base) base=1;
|
| 217 |
+
float* t = trail.data_ptr<float>();
|
| 218 |
+
// seed increments per step (base_seed + t), so this re-sorts every RESORT steps
|
| 219 |
+
if ((uint64_t)seed % (uint64_t)RESORT == 0)
|
| 220 |
+
bin_sort(ax.data_ptr<float>(), ay.data_ptr<float>(), ah.data_ptr<float>(), W,H,N, TILE);
|
| 221 |
+
agents(t, ax.data_ptr<float>(), ay.data_ptr<float>(), ah.data_ptr<float>(),
|
| 222 |
+
W,H,N, (float)sense_dist,(float)sense_ang,(float)turn,(float)speed, base);
|
| 223 |
+
deposit(t, ax.data_ptr<float>(), ay.data_ptr<float>(), W,H,N, (float)deposit_);
|
| 224 |
+
diffuse(t, tmp.data_ptr<float>(), W,H,(float)decay);
|
| 225 |
+
}
|
tests/test_physarum.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import kernels
|
| 3 |
+
|
| 4 |
+
physarum = kernels.get_kernel("phanerozoic/physarum", version=1, trust_remote_code=True)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def test_network_develops():
|
| 8 |
+
sim = physarum.Physarum(width=256, height=256, agents=20000, seed=1)
|
| 9 |
+
sim.step(120)
|
| 10 |
+
t = sim.trail
|
| 11 |
+
assert torch.isfinite(t).all()
|
| 12 |
+
assert float(t.max()) > 0.0
|
| 13 |
+
# a network is high-contrast, not a uniform wash
|
| 14 |
+
assert float(t.std()) > 0.4 * float(t.mean())
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_image_shape():
|
| 18 |
+
sim = physarum.Physarum(width=128, height=128, agents=5000, seed=2)
|
| 19 |
+
sim.step(60)
|
| 20 |
+
img = sim.image()
|
| 21 |
+
assert tuple(img.shape) == (128, 128, 3)
|
| 22 |
+
assert img.dtype == torch.uint8
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_deterministic():
|
| 26 |
+
a = physarum.Physarum(width=128, height=128, agents=5000, seed=7); a.step(40)
|
| 27 |
+
b = physarum.Physarum(width=128, height=128, agents=5000, seed=7); b.step(40)
|
| 28 |
+
assert torch.equal(a.trail, b.trail)
|
torch-ext/physarum/__init__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""physarum: a Physarum (slime-mould) transport-network simulation on the CPU.
|
| 2 |
+
|
| 3 |
+
Agents diffuse a chemoattractant trail and steer up its gradient; the emergent
|
| 4 |
+
network is the same structure a real slime mould forms solving a maze. The
|
| 5 |
+
agent step (three sensor look-ups, steering, motion, deposition) and the trail
|
| 6 |
+
diffusion vectorise with NEON on aarch64; the agent step is threaded across
|
| 7 |
+
cores (set torch.set_num_threads) and works on tile-sorted agents for cache
|
| 8 |
+
locality, with a portable scalar fallback. The trail is an ordinary CPU tensor
|
| 9 |
+
you read and render each step.
|
| 10 |
+
|
| 11 |
+
from kernels import get_kernel
|
| 12 |
+
physarum = get_kernel("phanerozoic/physarum", version=1, trust_remote_code=True)
|
| 13 |
+
|
| 14 |
+
sim = physarum.Physarum(width=1024, height=1024, agents=200_000)
|
| 15 |
+
for _ in range(400):
|
| 16 |
+
sim.step()
|
| 17 |
+
img = sim.image() # [H, W, 3] uint8, inferno colormap
|
| 18 |
+
"""
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from ._ops import ops
|
| 22 |
+
|
| 23 |
+
# inferno-ish colormap stops (black -> violet -> magenta -> orange -> pale yellow)
|
| 24 |
+
_CMAP = torch.tensor(
|
| 25 |
+
[[0.0, 0.0, 0.0], [0.12, 0.02, 0.28], [0.52, 0.08, 0.42],
|
| 26 |
+
[0.88, 0.28, 0.16], [0.99, 0.68, 0.12], [1.0, 1.0, 0.86]],
|
| 27 |
+
dtype=torch.float32)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Physarum:
|
| 31 |
+
"""A Physarum transport-network simulation.
|
| 32 |
+
|
| 33 |
+
Attributes:
|
| 34 |
+
trail: [H, W] float32 chemoattractant field (read/modify/render freely).
|
| 35 |
+
ax, ay, ah: [N] float32 agent x, y, heading.
|
| 36 |
+
|
| 37 |
+
Parameters follow Jones (2010): sensor offset `sense_dist` (px), sensor
|
| 38 |
+
angle `sense_ang` (rad, 22.5 deg default), rotation `turn` (rad, 45 deg),
|
| 39 |
+
step `speed` (px), `deposit` per agent per step, and per-step `decay` of the
|
| 40 |
+
field (after a 3-tap diffusion).
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, width=1024, height=1024, agents=200_000, seed=0,
|
| 44 |
+
sense_dist=9.0, sense_ang=0.3927, turn=0.7854, speed=1.0,
|
| 45 |
+
deposit=5.0, decay=0.88):
|
| 46 |
+
g = torch.Generator().manual_seed(seed)
|
| 47 |
+
self.W, self.H, self.N = int(width), int(height), int(agents)
|
| 48 |
+
self.trail = torch.zeros(self.H, self.W, dtype=torch.float32)
|
| 49 |
+
self._tmp = torch.zeros(self.H, self.W, dtype=torch.float32)
|
| 50 |
+
self.ax = (torch.rand(self.N, generator=g) * self.W).contiguous()
|
| 51 |
+
self.ay = (torch.rand(self.N, generator=g) * self.H).contiguous()
|
| 52 |
+
self.ah = (torch.rand(self.N, generator=g) * 6.2831853).contiguous()
|
| 53 |
+
self.params = dict(sense_dist=float(sense_dist), sense_ang=float(sense_ang),
|
| 54 |
+
turn=float(turn), speed=float(speed),
|
| 55 |
+
deposit=float(deposit), decay=float(decay))
|
| 56 |
+
self.base_seed = int(seed)
|
| 57 |
+
self.t = 0
|
| 58 |
+
|
| 59 |
+
def step(self, n: int = 1) -> torch.Tensor:
|
| 60 |
+
"""Advance the simulation `n` steps in place; returns the trail field."""
|
| 61 |
+
p = self.params
|
| 62 |
+
for _ in range(n):
|
| 63 |
+
ops.physarum_step(self.trail, self._tmp, self.ax, self.ay, self.ah,
|
| 64 |
+
self.base_seed + self.t, p["sense_dist"], p["sense_ang"],
|
| 65 |
+
p["turn"], p["speed"], p["deposit"], p["decay"])
|
| 66 |
+
self.t += 1
|
| 67 |
+
return self.trail
|
| 68 |
+
|
| 69 |
+
def image(self, gamma: float = 0.62, percentile: float = 0.995) -> torch.Tensor:
|
| 70 |
+
"""Colormap the trail field to an [H, W, 3] uint8 image.
|
| 71 |
+
|
| 72 |
+
Normalised by a high percentile (not the max) so the network shows
|
| 73 |
+
rather than a few hot cells crushing it to black.
|
| 74 |
+
"""
|
| 75 |
+
t = self.trail.flatten()
|
| 76 |
+
stride = max(1, t.numel() // 1_000_000)
|
| 77 |
+
mx = torch.quantile(t[::stride], percentile).clamp(min=1e-6)
|
| 78 |
+
v = (self.trail / mx).clamp(0, 1).pow(gamma) * 5.0
|
| 79 |
+
s = v.floor().clamp(max=4).long()
|
| 80 |
+
f = (v - s).unsqueeze(-1)
|
| 81 |
+
rgb = (_CMAP[s] + (_CMAP[s + 1] - _CMAP[s]) * f).clamp(0, 1)
|
| 82 |
+
return (rgb * 255 + 0.5).to(torch.uint8)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
__all__ = ["Physarum"]
|
torch-ext/torch_binding.cpp
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include <torch/library.h>
|
| 2 |
+
|
| 3 |
+
#include "registration.h"
|
| 4 |
+
#include "torch_binding.h"
|
| 5 |
+
|
| 6 |
+
TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
| 7 |
+
ops.def("physarum_step(Tensor! trail, Tensor! tmp, Tensor! ax, Tensor! ay, "
|
| 8 |
+
"Tensor! ah, int seed, float sense_dist, float sense_ang, float turn, "
|
| 9 |
+
"float speed, float deposit, float decay) -> ()");
|
| 10 |
+
#if defined(CPU_KERNEL)
|
| 11 |
+
ops.impl("physarum_step", torch::kCPU, &physarum_step);
|
| 12 |
+
#endif
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
|
torch-ext/torch_binding.h
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include <torch/torch.h>
|
| 3 |
+
|
| 4 |
+
void physarum_step(torch::Tensor trail, torch::Tensor tmp,
|
| 5 |
+
torch::Tensor ax, torch::Tensor ay, torch::Tensor ah,
|
| 6 |
+
int64_t seed, double sense_dist, double sense_ang,
|
| 7 |
+
double turn, double speed, double deposit, double decay);
|