phanerozoic commited on
Commit
bfdbd33
·
verified ·
1 Parent(s): 6489247

publish buildable source (threaded + tile-sorted kernel)

Browse files
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);