Repository navigation
Expand file tree
/
Copy pathvalidate.flow
More file actions
2606 lines (2387 loc) · 103 KB
/
Copy pathvalidate.flow
File metadata and controls
2606 lines (2387 loc) · 103 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# FlowRat functional validation.
#
# Run: ./validate.sh
#
# Every check prints what it measured, not just whether it passed, because a
# number that is drifting toward its tolerance is worth seeing before it
# crosses it. Failures return a distinct exit code so a script can tell which
# one broke without parsing the output.
#
# These are properties of the model, checked against something independent:
# an integrator against its own closed-form solution, the structured
# connectivity against a dense matrix, a decoded position against the
# position it was decoded from. A check that only compares the code to itself
# would pass forever.
import "src/core/mem.flow"
import "src/core/rng.flow"
import "src/core/clock.flow"
import "src/core/profile.flow"
import "src/env/geometry.flow"
import "src/env/environment.flow"
import "src/env/queries.flow"
import "src/env/presets.flow"
import "src/sim/agents.flow"
import "src/sim/integrate.flow"
import "src/sim/behaviour.flow"
import "src/sim/memory.flow"
import "src/sim/spatial.flow"
import "src/sim/novelty.flow"
import "src/sim/motion.flow"
import "src/neural/population.flow"
import "src/neural/place.flow"
import "src/neural/head_direction.flow"
import "src/neural/velocity.flow"
import "src/neural/grid.flow"
import "src/neural/boundary.flow"
import "src/neural/attractor.flow"
import "src/neural/circuit.flow"
import "src/sim/simulation.flow"
import "src/analysis/ratemap.flow"
import "src/analysis/scores.flow"
import "src/analysis/session.flow"
import "src/ui/view_analysis.flow"
import "src/ui/draw.flow"
import "src/ui/widgets.flow"
import "src/ui/layout.flow"
import "src/ui/view_env.flow"
import "src/ui/view_pop.flow"
import "src/ui/view_sys.flow"
import "src/ui/view_net.flow"
import "src/ui/view_etho.flow"
import "src/io/record.flow"
import "src/io/config.flow"
extern {
function fopen(path: string, mode: string) -> ptr<void>
function fclose(f: ptr<void>) -> i32
function fgetc(f: ptr<void>) -> i32
function fgets(buf: ptr<void>, n: i32, f: ptr<void>) -> ptr<void>
function fputs(t: string, f: ptr<void>) -> i32
function fscanf(f: ptr<void>, fmt: string, out: ptr<f64>) -> i32
function fprintf(f: ptr<void>, fmt: string, a: f64) -> i32
function fabs(x: f64) -> f64
function sqrt(x: f64) -> f64
function sin(x: f64) -> f64
function cos(x: f64) -> f64
function malloc(size: i64) -> ptr<void>
function free(p: ptr<void>) -> void
}
let mut failures: i32 = 0
let mut first_failure: i32 = 0
function check(ok: bool, code: i32, name: string) -> void {
if ok {
printf(" ok %s\n", name)
} else {
printf(" FAIL %s\n", name)
failures = failures + 1
if first_failure == 0 { first_failure = code }
}
}
function section(name: string) -> void {
printf("\n%s\n", name)
}
# --- geometry --------------------------------------------------------------
function test_geometry() -> void {
section("geometry")
# A ray straight up from the origin meets a horizontal segment at y = 3.
let d: f64 = ray_seg(0.0, 0.0, 0.0, 1.0, -1.0, 3.0, 1.0, 3.0)
printf(" ray to horizontal wall at y=3: %.6f\n", d)
check(fabs(d - 3.0) < 0.000001, 10, "ray hits a wall at the right range")
# Pointing away from it, the same ray misses.
let d2: f64 = ray_seg(0.0, 0.0, 0.0, -1.0, -1.0, 3.0, 1.0, 3.0)
check(d2 >= GEO_FAR, 11, "ray away from a wall reports no hit")
# A ray parallel to a segment never meets it.
let d3: f64 = ray_seg(0.0, 0.0, 1.0, 0.0, -1.0, 3.0, 1.0, 3.0)
check(d3 >= GEO_FAR, 12, "ray parallel to a wall reports no hit")
# Perpendicular distance to a segment, and to its endpoint when the foot
# of the perpendicular falls outside it.
let mut t: f64 = 0.0
let pd: f64 = point_seg_dist(0.0, 2.0, -1.0, 0.0, 1.0, 0.0, &t)
check(fabs(pd - 2.0) < 0.000001, 13, "point to segment, foot inside")
let pd2: f64 = point_seg_dist(5.0, 0.0, -1.0, 0.0, 1.0, 0.0, &t)
check(fabs(pd2 - 4.0) < 0.000001, 14, "point to segment, foot outside")
# Angle wrapping is exact at the seam and idempotent.
check(fabs(wrap_pi(PI * 3.0) - (-PI)) < 0.000001 or
fabs(wrap_pi(PI * 3.0) - PI) < 0.000001, 15, "wrap_pi at the seam")
check(fabs(angle_delta(3.0, -3.0) - 0.2831853) < 0.0001, 16,
"angle_delta takes the short way round")
}
# --- environment -----------------------------------------------------------
function test_environment() -> void {
section("environment")
let mut e: Env = env_create()
# Every preset closes: a ray from the centre in any direction must hit
# something. An arena with a gap in its shell leaks rats.
let mut all_closed: bool = true
let mut worst_preset: i32 = -1
for p in 0 to PRESET_COUNT {
env_preset_apply(&e, p)
let mut cx: f64 = 0.0
let mut cy: f64 = 0.0
let ok: bool = env_sample_free(&e, 42, 1, 1, 200, &cx, &cy)
for k in 0 to 64 {
let ang: f64 = (k as f64) * TAU / 64.0
if env_ray_distance(&e, cx, cy, ang) >= GEO_FAR {
all_closed = false
worst_preset = p
}
}
}
if worst_preset >= 0 {
printf(" leaking preset: %s\n", env_preset_name(worst_preset))
}
check(all_closed, 20, "every preset is a closed arena")
# Free-space sampling never lands inside a solid or outside the shell.
env_preset_apply(&e, PRESET_OBSTACLES)
let mut bad: i32 = 0
for i in 0 to 4000 {
let mut x: f64 = 0.0
let mut y: f64 = 0.0
let ok: bool = env_sample_free(&e, 7, 3, (i as u32) + 1, 64, &x, &y)
if !env_is_free(&e, x, y) { bad = bad + 1 }
}
printf(" 4000 samples, outside free space: %d\n", bad)
check(bad == 0, 21, "free-space sampling stays in free space")
# A point inside a pillar is pushed out of it.
let px0: f64 = e.solid_x[0] + e.solid_w[0] * 0.5
let py0: f64 = e.solid_y[0] + e.solid_h[0] * 0.5
let mut px: f64 = px0
let mut py: f64 = py0
let moved: bool = env_push_inside(&e, 0.01, &px, &py)
printf(" pushed (%.3f,%.3f) out to (%.3f,%.3f)\n",
px0, py0, px, py)
check(moved and env_is_free(&e, px, py), 22,
"a point inside an obstacle is pushed to free space")
# Wall distance in a plain box matches the analytic answer.
env_set_rect(&e, 0.0, 0.0, 2.0, 2.0)
let wd: f64 = env_wall_distance(&e, 0.3, 1.0)
printf(" wall distance at x=0.3 in a 2 m box: %.6f\n", wd)
check(fabs(wd - 0.3) < 0.000001, 23, "wall distance matches geometry")
# A barrier blocks sight; removing it restores it.
env_set_rect(&e, 0.0, 0.0, 2.0, 2.0)
check(env_line_of_sight(&e, 0.5, 1.0, 1.5, 1.0), 24,
"clear line of sight across an empty box")
let widx: i32 = env_add_wall(&e, 1.0, 0.6, 1.0, 1.4)
check(!env_line_of_sight(&e, 0.5, 1.0, 1.5, 1.0), 25,
"a barrier blocks line of sight")
let removed: bool = env_remove_wall(&e, widx)
check(env_line_of_sight(&e, 0.5, 1.0, 1.5, 1.0), 26,
"removing the barrier restores it")
# The boundary is protected from deletion.
check(!env_remove_wall(&e, 0), 27, "boundary walls cannot be deleted")
env_destroy(&e)
}
# --- integrators -----------------------------------------------------------
function test_integrators() -> void {
section("integrators")
# Against the closed-form arc, over one step of a hard turn.
let v: f64 = 0.3
let w: f64 = 2.0
let dt: f64 = 0.02
let e_euler: f64 = kin_step_error(INT_EULER, v, 0.4, w, dt)
let e_semi: f64 = kin_step_error(INT_SEMI, v, 0.4, w, dt)
let e_rk4: f64 = kin_step_error(INT_RK4, v, 0.4, w, dt)
printf(" one step at dt=%.3f, v=%.2f, omega=%.2f\n", dt, v, w)
printf(" euler %.3e semi %.3e rk4 %.3e\n",
e_euler, e_semi, e_rk4)
check(e_semi < e_euler, 30, "semi-implicit beats euler")
check(e_rk4 < e_semi, 31, "rk4 beats semi-implicit")
check(e_rk4 < 0.000000001, 32, "rk4 is near machine accuracy here")
# Halving the step should quarter Euler's error and cut RK4's by
# sixteen. Order is the property worth checking, not a single number.
let e_euler_h: f64 = kin_step_error(INT_EULER, v, 0.4, w, dt * 0.5)
let ratio: f64 = e_euler / e_euler_h
printf(" euler error ratio on halving dt: %.3f (expect ~4)\n",
ratio)
check(ratio > 3.5 and ratio < 4.5, 33, "euler converges at second order")
# The exact integrator is exact by construction.
check(kin_step_error(INT_EXACT, v, 0.4, w, dt) < 0.0000000000001, 34,
"the closed form matches itself")
}
# --- motion ----------------------------------------------------------------
function test_motion() -> void {
section("motion")
let mut worst: i32 = -1
let mut total_escapes: i32 = 0
for p in 0 to PRESET_COUNT {
let mut s: Sim = sim_create(64, p, 20250825)
s.can_enabled = false
for k in 0 to 900 {
sim_step(&s)
}
let mut escaped: i32 = 0
for i in 0 to s.agents.n {
if !env_is_free(&s.env, s.agents.x[i], s.agents.y[i]) {
escaped = escaped + 1
}
}
let mut moved: f64 = 0.0
for i in 0 to s.agents.n {
moved = moved + s.agents.distance[i]
}
moved = moved / (s.agents.n as f64)
# Wall contact reduces path length, so the number differs between a
# wide arena and a narrow one. Printed to four places because two
# hides exactly that difference.
let mut touching: i32 = 0
for i in 0 to s.agents.n {
if s.agents.wall_dist[i] < 0.05 { touching = touching + 1 }
}
printf(" %-14s escaped %2d / %d, mean path %.4f m, near wall %2d\n",
env_preset_name(p), escaped, s.agents.n, moved, touching)
total_escapes = total_escapes + escaped
if escaped > 0 and worst < 0 { worst = p }
# A rat that never moves is as wrong as one that escapes.
if moved < 0.2 { total_escapes = total_escapes + 1000 }
sim_destroy(&s)
}
check(total_escapes == 0, 40,
"64 rats stay in free space for 900 steps in every arena")
}
# --- determinism -----------------------------------------------------------
# How far rat zero's path in a crowd departs from its path alone, with the
# social term on or off.
function solo_divergence(social: bool) -> f64 {
let mut push: f64 = 0.0
let mut crowd: f64 = 0.0
if social {
push = 1.2
crowd = 0.5
}
let mut many: Sim = sim_create(128, PRESET_OBSTACLES, 99991)
many.motion.social_push = push
many.motion.crowd_share = crowd
let mut one: Sim = sim_create(1, PRESET_OBSTACLES, 99991)
one.motion.social_push = push
one.motion.crowd_share = crowd
for k in 0 to 400 {
sim_step(&many)
sim_step(&one)
}
let d: f64 = dist(many.agents.x[0], many.agents.y[0],
one.agents.x[0], one.agents.y[0])
sim_destroy(&many)
sim_destroy(&one)
return d
}
function test_determinism() -> void {
section("determinism")
# The whole point of the counter-based generator: the same seed gives the
# same trajectory, whatever order `parallel for` visited the agents in.
let mut a: Sim = sim_create(128, PRESET_OBSTACLES, 99991)
let mut b: Sim = sim_create(128, PRESET_OBSTACLES, 99991)
for k in 0 to 400 {
sim_step(&a)
sim_step(&b)
}
let mut worst: f64 = 0.0
for i in 0 to a.agents.n {
let d: f64 = dist(a.agents.x[i], a.agents.y[i],
b.agents.x[i], b.agents.y[i])
if d > worst { worst = d }
}
printf(" 128 rats, 400 steps, largest divergence: %.3e m\n", worst)
check(worst == 0.0, 50, "two runs of one seed are bit identical")
# Rat zero's noise stream must not depend on how many rats were
# simulated alongside it. If it did, the stream would be indexed by
# something other than the rat, and a run would stop being reproducible
# at a different batch size.
#
# This has to be measured with the rats ignoring each other, because once
# they steer around their neighbours a rat in a crowd genuinely does take
# a different path from a rat on its own. That is the whole point of the
# social term, so the check is split: with it off the paths must match to
# the bit, and with it on they must not, which is the evidence that the
# term does anything at all.
let lonely_batch: f64 = solo_divergence(false)
let social_batch: f64 = solo_divergence(true)
printf(" rat 0 in a batch of 128 vs alone: %.3e m ignoring others\n",
lonely_batch)
printf(" %*s%.3e m noticing them\n", 42, "", social_batch)
check(lonely_batch == 0.0, 51,
"a lone rat's path does not depend on the batch size")
check(social_batch > 0.0, 52,
"and a sociable one's does, because it steers around the others")
let mut d2: Sim = sim_create(128, PRESET_OBSTACLES, 12345)
for k in 0 to 400 {
sim_step(&d2)
}
let diff: f64 = dist(a.agents.x[0], a.agents.y[0],
d2.agents.x[0], d2.agents.y[0])
printf(" different seed moves rat 0 by: %.3f m\n", diff)
check(diff > 0.01, 52, "a different seed gives a different trajectory")
sim_destroy(&a)
sim_destroy(&b)
sim_destroy(&d2)
}
# --- populations -----------------------------------------------------------
function test_populations() -> void {
section("populations")
let mut s: Sim = sim_create(1, PRESET_OPEN_FIELD, 4242)
s.can_enabled = false
# Place fields sit in free space.
let mut outside: i32 = 0
for c in 0 to s.place.n {
if !env_is_free(&s.env, s.place.p0[c], s.place.p1[c]) {
outside = outside + 1
}
}
check(outside == 0, 60, "no place field centre lands in a wall")
# A place cell peaks at its own centre and decays with distance.
let at_centre: f64 = place_rate_at(&s.place, 0, s.place.p0[0],
s.place.p1[0])
let one_sigma: f64 = place_rate_at(&s.place, 0,
s.place.p0[0] + s.place.p2[0],
s.place.p1[0])
printf(" place cell 0: peak %.3f, one sigma out %.3f, ratio %.4f\n",
at_centre, one_sigma, one_sigma / at_centre)
check(fabs(at_centre - s.place.p3[0]) < 0.0001, 61,
"a place cell reaches its peak rate at its centre")
# A Gaussian at one sigma is exp(-1/2) of its peak.
check(fabs(one_sigma / at_centre - 0.6065307) < 0.0001, 62,
"the field falls off as a gaussian")
# Head-direction tuning peaks at the preferred angle.
let hd_at: f64 = hd_rate_at(&s.hd, 3, s.hd.p0[3])
let hd_off: f64 = hd_rate_at(&s.hd, 3, s.hd.p0[3] + PI)
printf(" hd cell 3: at preferred %.3f, opposite %.5f\n",
hd_at, hd_off)
check(fabs(hd_at - s.hd.p2[3]) < 0.0001, 63,
"a head-direction cell peaks at its preferred angle")
check(hd_off < hd_at * 0.01, 64,
"and is nearly silent facing the other way")
# Run, then check the population decodes recover the true state.
let mut worst_place: f64 = 0.0
let mut worst_hd: f64 = 0.0
for k in 0 to 600 {
sim_step(&s)
let pe: f64 = sim_place_error(&s)
let he: f64 = sim_hd_error(&s)
if pe > worst_place { worst_place = pe }
if he > worst_hd { worst_hd = he }
}
printf(" over 600 steps, worst place decode error %.4f m\n",
worst_place)
printf(" over 600 steps, worst head decode error %.4f rad\n",
worst_hd)
check(worst_place < 0.15, 65, "the place code tracks position")
check(worst_hd < 0.05, 66, "the head-direction code tracks heading")
# A grid cell repeats on its lattice.
#
# The lattice translation vectors are not along the three wave vectors.
# They sit thirty degrees off them: with wave vectors at theta, theta+60
# and theta+120, a shift of one spacing along theta+30 gives phases of
# 2pi, 2pi and 0, so every term returns to where it started. Shifting
# along theta itself is not a lattice vector and does not repeat, which
# is what the first version of this check got wrong.
let spacing: f64 = s.grid.p0[0]
let orient: f64 = s.grid.p1[0]
let lat: f64 = orient + PI / 6.0
let gx: f64 = 1.0
let gy: f64 = 1.0
let r0: f64 = grid_rate_at(&s.grid, 0, &s.grid_p, gx, gy)
let r1: f64 = grid_rate_at(&s.grid, 0, &s.grid_p,
gx + spacing * cos(lat),
gy + spacing * sin(lat))
let r2: f64 = grid_rate_at(&s.grid, 0, &s.grid_p,
gx + spacing * cos(lat + PI / 3.0),
gy + spacing * sin(lat + PI / 3.0))
let r3: f64 = grid_rate_at(&s.grid, 0, &s.grid_p,
gx + spacing * 0.5 * cos(lat),
gy + spacing * 0.5 * sin(lat))
printf(" grid cell 0 at r: %.4f\n", r0)
printf(" one lattice vector along: %.4f, next vector: %.4f\n",
r1, r2)
printf(" half a lattice vector along: %.4f\n", r3)
check(fabs(r0 - r1) < 0.0001 and fabs(r0 - r2) < 0.0001, 67,
"a grid field repeats on both lattice vectors")
check(fabs(r0 - r3) > 0.1, 69,
"and does not repeat at half a lattice vector")
# Boundary cells respond to walls, so the population must be quieter in
# the open than against an edge.
let mut mid: f64 = 0.0
let mut edge: f64 = 0.0
agents_place(&s.agents, 0, 1.0, 1.0, 0.0, 0.0)
agents_sense(&s.agents, &s.env, s.require_sight)
boundary_update(&s.bvc, &s.agents, &s.bvc_p)
pop_summarise(&s.bvc, 0)
mid = s.bvc.last_max
agents_place(&s.agents, 0, 0.06, 1.0, 0.0, 0.0)
agents_sense(&s.agents, &s.env, s.require_sight)
boundary_update(&s.bvc, &s.agents, &s.bvc_p)
pop_summarise(&s.bvc, 0)
edge = s.bvc.last_max
printf(" boundary peak: arena centre %.3f, against a wall %.3f\n",
mid, edge)
check(edge > mid * 1.5, 68,
"boundary cells fire harder near a wall than in the open")
sim_destroy(&s)
}
# --- recurrent networks ----------------------------------------------------
function test_attractor() -> void {
section("recurrent attractors")
let dt: f64 = 1.0 / 60.0
# A bump forms out of noise and is a single localised blob.
let mut sh: Can = can_params_sheet(32, 32)
can_build_kernel(&sh)
printf(" sheet: lambda_bump %.3f, lambda_2nd %.3f, w_global %.3f\n",
sh.lambda_bump, sh.lambda_second, sh.w_global)
check(sh.lambda_second < 1.0, 70,
"only the leading spatial mode is unstable")
can_reset(&sh, 5)
can_settle(&sh, 600, dt)
printf(" after settling: peak %.3f, mean %.3f, strength %.3f\n",
sh.max_rate, sh.mean_rate, sh.bump_strength)
check(sh.bump_strength > 0.4, 71, "a single bump forms from noise")
check(sh.max_rate > sh.mean_rate * 2.0, 72,
"activity is localised rather than uniform")
# Structured connectivity against the dense matrix. Same start, same
# input, same number of steps, compared element by element.
let mut ref: Can = can_params_sheet(16, 16)
can_build_kernel(&ref)
can_reset(&ref, 11)
let built: bool = can_build_dense(&ref)
check(built, 73, "the dense reference builds within its size limit")
let n: i32 = ref.n
let saved: ptr<f64> = malloc((n as i64) * 8) as ptr<f64>
can_copy_state(ref.r, saved, n)
can_set_input_sheet(&ref, 1.7, -0.9, 0.0, 0.0, false)
ref.use_dense = false
for k in 0 to 40 {
can_step(&ref, dt)
}
let structured: ptr<f64> = malloc((n as i64) * 8) as ptr<f64>
can_copy_state(ref.r, structured, n)
can_copy_state(saved, ref.r, n)
can_set_input_sheet(&ref, 1.7, -0.9, 0.0, 0.0, false)
ref.use_dense = true
for k in 0 to 40 {
can_step(&ref, dt)
}
let diff: f64 = can_max_abs_diff(structured, ref.r, n)
printf(" structured vs dense over 40 steps: max diff %.3e\n", diff)
check(diff < 0.000000001, 74,
"structured connectivity matches the dense matrix")
free(saved as ptr<void>)
free(structured as ptr<void>)
can_destroy(&ref)
# Path integration: drive at a known rate and check the bump keeps up.
can_calibrate_sheet(&sh, dt)
can_settle(&sh, 400, dt)
can_decode(&sh)
let mut prev: f64 = sh.bump_x
let mut travelled: f64 = 0.0
let drive: f64 = 3.0
let steps: i32 = 240
for k in 0 to steps {
can_set_input_sheet(&sh, drive, 0.0, 0.0, 0.0, false)
can_step(&sh, dt)
can_decode(&sh)
travelled = travelled + torus_delta(sh.bump_x, prev, sh.nx as f64)
prev = sh.bump_x
}
let want: f64 = drive * (steps as f64) * dt
printf(" driven at %.1f cells/s for %.1f s: moved %.2f, want %.2f\n",
drive, (steps as f64) * dt, travelled, want)
check(fabs(travelled / want - 1.0) < 0.08, 75,
"the bump tracks velocity within eight per cent")
check(sh.bump_strength > 0.4, 76, "and the bump survives the motion")
can_destroy(&sh)
# The ring holds a direction and turns with angular velocity.
let mut rg: Can = can_params_ring(64)
can_build_kernel(&rg)
can_reset(&rg, 3)
can_calibrate_ring(&rg, dt)
can_settle(&rg, 400, dt)
can_decode(&rg)
printf(" ring: lambda_2nd %.3f, strength %.3f\n",
rg.lambda_second, rg.bump_strength)
check(rg.bump_strength > 0.4, 77, "the ring holds one bump")
let mut rprev: f64 = rg.bump_x
let mut rtrav: f64 = 0.0
let omega: f64 = 1.0
for k in 0 to steps {
can_set_input_ring(&rg, omega, 0.0, false)
can_step(&rg, dt)
can_decode(&rg)
rtrav = rtrav + torus_delta(rg.bump_x, rprev, rg.nx as f64)
rprev = rg.bump_x
}
let rrad: f64 = rtrav / (rg.n as f64) * TAU
let rwant: f64 = omega * (steps as f64) * dt
printf(" ring driven at %.1f rad/s: turned %.3f, want %.3f\n",
omega, rrad, rwant)
check(fabs(rrad / rwant - 1.0) < 0.10, 78,
"the ring integrates angular velocity within ten per cent")
can_destroy(&rg)
}
# --- the circuit -----------------------------------------------------------
# Peak response of place cell `c` over a sweep of the arena, and where it is.
# Measured by moving the rat, not by asking the model what it intended.
function sweep_peak(s: ptr<Sim>, c: i32, n: i32,
px: ptr<f64>, py: ptr<f64>) -> f64 {
let mut best: f64 = -1.0
for j in 0 to n {
let y: f64 = s.env.y0 + ((j as f64) + 0.5) / (n as f64)
* (s.env.y1 - s.env.y0)
for i in 0 to n {
let x: f64 = s.env.x0 + ((i as f64) + 0.5) / (n as f64)
* (s.env.x1 - s.env.x0)
if env_is_free(&s.env, x, y) {
agents_place(&s.agents, 0, x, y, 0.0, 0.1)
agents_sense(&s.agents, &s.env, s.require_sight)
grid_update(&s.grid, &s.agents, &s.grid_p)
boundary_update(&s.bvc, &s.agents, &s.bvc_p)
circuit_update(&s.circuit, &s.place, &s.bvc, &s.grid, 1, 0)
let r: f64 = s.place.rates[c]
if r > best {
best = r
px[0] = x
py[0] = y
}
}
}
}
return best
}
function test_circuit() -> void {
section("place cells driven by their inputs")
let mut s: Sim = sim_create(1, PRESET_OPEN_FIELD, 4242)
s.can_enabled = false
sim_set_circuit(&s, true)
check(s.circuit.enabled, 120, "the circuit switches on and wires itself")
let mut live_total: i32 = 0
for c in 0 to 16 {
live_total = live_total + circuit_live_inputs(&s.circuit, c)
}
printf(" mean inputs per cell over 16 cells: %.1f of %d\n",
(live_total as f64) / 16.0, s.circuit.fan_in)
check(live_total > 16 * 4, 121,
"every cell finds a usable set of inputs")
# A place cell here has no access to position. Its field is wherever its
# boundary and grid inputs happen to agree, so the check is that this
# lands where the cell was wired around.
let mut worst: f64 = 0.0
let mut worst_peak_err: f64 = 0.0
for c in 0 to 6 {
let mut mx: f64 = 0.0
let mut my: f64 = 0.0
let peak: f64 = sweep_peak(&s, c, 40, &mx, &my)
let off: f64 = dist(mx, my, s.place.p0[c], s.place.p1[c])
if off > worst { worst = off }
let perr: f64 = fabs(peak - s.place.p3[c]) / s.place.p3[c]
if perr > worst_peak_err { worst_peak_err = perr }
}
printf(" worst field offset from its wiring centre: %.3f m\n",
worst)
printf(" worst peak rate error against the configured peak: %.1f%%\n",
worst_peak_err * 100.0)
check(worst < 0.15, 122, "a field forms where the cell was wired")
check(worst_peak_err < 0.35, 123,
"and reaches roughly its configured peak rate")
# The point of the model: the field is anchored to walls, not to a
# coordinate. Put a barrier beside a cell and its response changes.
let mut bx: f64 = 0.0
let mut by: f64 = 0.0
let peak_before: f64 = sweep_peak(&s, 0, 36, &bx, &by)
let wall: i32 = env_add_wall(&s.env, bx + 0.10, by - 0.35,
bx + 0.10, by + 0.35)
agents_sense(&s.agents, &s.env, s.require_sight)
let mut ax2: f64 = 0.0
let mut ay2: f64 = 0.0
let peak_after: f64 = sweep_peak(&s, 0, 36, &ax2, &ay2)
let shift: f64 = dist(bx, by, ax2, ay2)
printf(" a wall beside cell 0: field moved %.3f m, peak %.1f -> %.1f\n",
shift, peak_before, peak_after)
check(shift > 0.0 or fabs(peak_after - peak_before) > 0.5, 124,
"adding a wall changes a field built from boundary input")
let removed: bool = env_remove_wall(&s.env, wall)
sim_env_changed(&s)
# Turning the circuit off returns the analytic fields, unchanged.
sim_set_circuit(&s, false)
sim_reset(&s, 1)
for k in 0 to 200 {
sim_step(&s)
}
let analytic_err: f64 = sim_place_error(&s)
check(analytic_err >= 0.0 and analytic_err < 0.2, 125,
"switching the circuit off restores the analytic fields")
sim_destroy(&s)
}
# --- end to end ------------------------------------------------------------
function test_end_to_end() -> void {
section("end to end")
let mut s: Sim = sim_create(8, PRESET_OPEN_FIELD, 777)
# Path integration against ground truth, with the anchor off, so what is
# measured is the network's own drift.
for k in 0 to 600 {
sim_step(&s)
}
let i: i32 = sim_selected(&s)
let mut ix: f64 = 0.0
let mut iy: f64 = 0.0
sim_sheet_position(&s, &ix, &iy)
let drift: f64 = dist(ix, iy, s.agents.x[i], s.agents.y[i])
printf(" after %.1f s unanchored, sheet is %.4f m from the truth\n",
s.time, drift)
check(drift < 0.25, 80, "path integration stays close over ten seconds")
check(s.sheet.bump_strength > 0.3, 81,
"the sheet keeps a readable bump while the rat moves")
# Editing the environment has to change the outcome.
#
# The barrier is dropped right beside the start zone, and the comparison
# is over the whole batch. An earlier version put the wall across the far
# side of the arena and compared one rat, which passed or failed on
# whether that rat happened to wander over there in ten seconds. A test
# whose result depends on that is not testing the edit.
let mut t: Sim = sim_create(8, PRESET_OPEN_FIELD, 777)
let before_wall: f64 = t.agents.wall_dist[0]
env_add_box(&t.env, 0.5, 0.0, 0.15, 1.0)
sim_env_changed(&t)
let after_wall: f64 = t.agents.wall_dist[0]
printf(" rat 0 wall distance before the edit %.4f, after %.4f\n",
before_wall, after_wall)
check(fabs(after_wall - before_wall) > 0.001, 86,
"an edit changes what the rats sense immediately")
for k in 0 to 600 {
sim_step(&t)
}
let mut worst_move: f64 = 0.0
for k in 0 to t.agents.n {
let d: f64 = dist(s.agents.x[k], s.agents.y[k],
t.agents.x[k], t.agents.y[k])
if d > worst_move { worst_move = d }
}
printf(" adding a barrier moved endpoints by up to %.3f m\n",
worst_move)
check(worst_move > 0.05, 82, "an environment edit changes the outcome")
let mut in_solid: i32 = 0
for k in 0 to t.agents.n {
if env_in_solid(&t.env, t.agents.x[k], t.agents.y[k]) {
in_solid = in_solid + 1
}
}
check(in_solid == 0, 83, "and no rat ends up inside the new barrier")
# Disabling a population must silence it and leave the others alone.
let hd_before: f64 = t.hd.last_max
sim_pop_toggle(&t, 0)
sim_step(&t)
printf(" place peak with the population off: %.4f\n",
t.place.last_max)
check(t.place.last_max == 0.0, 84, "a disabled population goes silent")
check(t.hd.last_max > 0.0, 85, "and its neighbours keep running")
# Poisson spiking. The control panel toggles it, so it has to actually
# produce spikes: an earlier version gated the draw on a per-population
# flag that nothing ever set, and the toggle did nothing at all.
sim_pop_toggle(&t, 0)
t.spiking = true
let mut total_spikes: i32 = 0
for k in 0 to 200 {
sim_step(&t)
let p: ptr<Pop> = sim_pop(&t, 0)
let base: i32 = pop_base(p, sim_selected(&t))
for c in 0 to p.n {
total_spikes = total_spikes + p.spikes[base + c]
}
}
printf(" place spikes over 200 steps with spiking on: %d\n",
total_spikes)
check(total_spikes > 0, 87, "the spiking toggle produces spikes")
t.spiking = false
for k in 0 to 5 {
sim_step(&t)
}
let mut off_spikes: i32 = 0
let p2: ptr<Pop> = sim_pop(&t, 0)
let base2: i32 = pop_base(p2, sim_selected(&t))
for c in 0 to p2.n {
off_spikes = off_spikes + p2.spikes[base2 + c]
}
check(off_spikes >= 0, 88, "and turning it off is harmless")
sim_destroy(&s)
sim_destroy(&t)
}
# Count lines in a file, or -1 when it cannot be opened. Used to check that
# a recording actually wrote something rather than trusting that fopen
# returning non-null means the rows arrived.
function count_lines(path: string) -> i32 {
let f: ptr<void> = fopen(path, "r")
if f == null { return -1 }
let mut n: i32 = 0
let mut ch: i32 = fgetc(f)
while ch != -1 {
if ch == 10 { n = n + 1 }
ch = fgetc(f)
}
let c: i32 = fclose(f)
return n
}
function test_io() -> void {
section("recording and configuration")
let mut s: Sim = sim_create(4, PRESET_TWO_ROOMS, 606)
let mut ed: Editor = editor_new()
let mut rec: Recorder = rec_new()
rec.stride = 2
let started: bool = rec_start(&rec, &s)
check(started, 90, "the recorder opens its three output files")
for k in 0 to 60 {
sim_step(&s)
rec_step(&rec, &s)
}
rec_stop(&rec)
let traj: i32 = count_lines("data/out/trajectory.csv")
let neural: i32 = count_lines("data/out/neural.csv")
let recur: i32 = count_lines("data/out/recurrent.csv")
printf(" rows: trajectory %d, neural %d, recurrent %d\n",
traj, neural, recur)
# 60 steps at stride 2 is 30 recorded steps, plus a header line, times
# four rats for the trajectory stream.
check(traj == 1 + 30 * 4, 91, "the trajectory stream has every rat row")
check(recur == 1 + 30, 92, "the recurrent stream has one row per step")
check(neural > 30, 93, "the neural stream has cell rows")
let map_ok: bool = export_ratemap(&s, 0, 3, "data/out/ratemap.csv")
let map_rows: i32 = count_lines("data/out/ratemap.csv")
printf(" rate map rows: %d\n", map_rows)
check(map_ok and map_rows == 1 + 96 * 96, 94,
"a place-cell rate map exports a full grid")
let occ_ok: bool = export_occupancy(&s, "data/out/occupancy.csv", 32)
check(occ_ok and count_lines("data/out/occupancy.csv") == 1 + 32 * 32,
95, "an occupancy map exports a full grid")
# Round trip: draw something into the arena, save, change everything,
# load, and check the arena and the parameters came back.
let cue_idx: i32 = env_add_cue(&s.env, 0.42, 0.77, 0.6)
env_add_box(&s.env, 1.3, 0.2, 0.2, 0.3)
s.motion.speed_mean = 0.271
s.place_p.sigma = 0.137
ed.trail_len = 123
let walls_before: i32 = s.env.n_walls
let cues_before: i32 = s.env.n_cues
let solids_before: i32 = s.env.n_solids
let saved: bool = config_save(&s, &ed, "data/out/roundtrip.flowrat")
check(saved, 96, "a configuration saves")
sim_set_preset(&s, PRESET_CIRCLE)
s.motion.speed_mean = 0.5
s.place_p.sigma = 0.3
ed.trail_len = 40
let loaded: bool = config_load(&s, &ed, "data/out/roundtrip.flowrat")
printf(" after reload: walls %d (was %d), cues %d (was %d), solids %d (was %d)\n",
s.env.n_walls, walls_before, s.env.n_cues, cues_before,
s.env.n_solids, solids_before)
printf(" speed %.4f, place sigma %.4f, trail %d\n",
s.motion.speed_mean, s.place_p.sigma, ed.trail_len)
check(loaded, 97, "a configuration loads")
check(s.env.n_walls == walls_before and s.env.n_cues == cues_before
and s.env.n_solids == solids_before, 98,
"the edited arena survives a round trip")
check(fabs(s.motion.speed_mean - 0.271) < 0.000001
and fabs(s.place_p.sigma - 0.137) < 0.000001
and ed.trail_len == 123, 99,
"parameters survive a round trip")
let bad: bool = config_load(&s, &ed, "data/out/does_not_exist")
check(!bad, 100, "a missing configuration is refused, not half applied")
sim_destroy(&s)
}
# The whole interface draws into an offscreen canvas, so it can be exercised
# without a window. This catches a panel that reads past the end of an array
# or divides by a count that happens to be zero.
function test_ui() -> void {
section("interface")
let mut cv: Canvas = canvas_create(WIN_W, WIN_H)
let mut lay: Layout = layout_build(WIN_W, WIN_H)
let mut u: Ui = ui_create()
let mut ed: Editor = editor_new()
let mut rec: Recorder = rec_new()
let mut defs: Defaults = defaults_new(32)
# The analysis panel needs a session to draw, and drawing it before
# anything is recorded is exactly the state a user first sees, so the
# interface check covers it unrecorded as well as recorded.
let mut ui_sess: Session = session_create(32, 32)
let mut drawn: i32 = 0
for p in 0 to PRESET_COUNT {
let mut s: Sim = sim_create(3, p, 8080)
let mut v: View = view_new(lay.env_x, lay.env_y, lay.env_w, lay.env_h)
view_fit(&v, s.env.x0, s.env.y0, s.env.x1, s.env.y1)
for k in 0 to 20 {
sim_step(&s)
}
# Every population selected in turn, and the mouse parked over the
# control panel, so the widget paths run too.
for sel in 0 to SIM_POP_COUNT {
ed.sel_pop = sel
ui_begin(&u, lay.ctl_x + 20, lay.ctl_y + 60, false, 0)
dr_clear(&cv, C_BG)
view_header_draw(&cv, &s, &ed, 16.7, &lay)
let a: bool = view_control_draw(&cv, &u, &s, &ed, &v, &defs, &lay)
let b: bool = view_editor_draw(&cv, &u, &s, &ed, &rec, &v, &defs, &lay)
view_pop_draw(&cv, &u, &s, &ed, &lay)
view_tabs_draw(&cv, &u, &ed, &lay)
# Every tab, every arena, every selected population.
for tb in 0 to TAB_COUNT {
ed.tab = tb
if tb == TAB_RAT { view_rat_draw(&cv, &u, &s, &ed, &lay) }
if tb == TAB_ETHO {
view_etho_draw(&cv, &u, &s, &defs, &ed, &lay)
}
if tb == TAB_CAN { view_can_draw(&cv, &u, &s, &defs, &lay) }
if tb == TAB_NET {
view_net_draw(&cv, &u, &s, &defs, &ed, &lay)
}
if tb == TAB_ANALYSIS {
view_analysis_draw(&cv, &u, &s, &ui_sess, &ed, &lay)
}
if tb == TAB_PERF { view_perf_draw(&cv, &u, &s, 16.7, &lay) }
if tb == TAB_EDIT {
let e2: bool = view_editor_draw(&cv, &u, &s, &ed, &rec,
&v, &defs, &lay)
}
}
ed.tab = TAB_RAT
view_env_draw(&cv, &u, &v, &s, &ed, &lay)
drawn = drawn + 1
}
sim_destroy(&s)
}
printf(" drew %d full frames across %d arenas\n",
drawn, PRESET_COUNT)
check(drawn == PRESET_COUNT * SIM_POP_COUNT, 101,
"every panel draws in every arena")
# The editor, driven the way a mouse would drive it. Synthesising the
# Ui state here reaches the drag-and-commit path that a headless run
# otherwise never touches.
let mut es: Sim = sim_create(2, PRESET_OPEN_FIELD, 21)
let mut ev: View = view_new(lay.env_x, lay.env_y, lay.env_w, lay.env_h)
view_fit(&ev, es.env.x0, es.env.y0, es.env.x1, es.env.y1)
ed.tool = TOOL_BOX
let sx0: i32 = view_sx(&ev, 1.2)
let sy0: i32 = view_sy(&ev, 1.2)
let sx1: i32 = view_sx(&ev, 1.5)
let sy1: i32 = view_sy(&ev, 1.5)
let solids0: i32 = es.env.n_solids
let walls0: i32 = es.env.n_walls
# Press, drag, release. The commit happens on release.
ui_begin(&u, sx0, sy0, true, 0)
let d1: bool = view_env_input(&u, &ev, &es, &ed, &lay)
ui_begin(&u, sx1, sy1, true, 0)
let d2: bool = view_env_input(&u, &ev, &es, &ed, &lay)
ui_begin(&u, sx1, sy1, false, 0)
let d3: bool = view_env_input(&u, &ev, &es, &ed, &lay)
printf(" drag a box: solids %d -> %d, walls %d -> %d\n",
solids0, es.env.n_solids, walls0, es.env.n_walls)
check(d3 and es.env.n_solids == solids0 + 1
and es.env.n_walls == walls0 + 4, 103,
"dragging with the box tool adds a block on release")
sim_env_changed(&es)
check(env_in_solid(&es.env, 1.35, 1.35), 104,
"the new block occupies the space it was drawn over")
# Erase it again.
ed.tool = TOOL_ERASE
let ex: i32 = view_sx(&ev, 1.35)
let ey: i32 = view_sy(&ev, 1.35)
ui_begin(&u, ex, ey, true, 0)
let d4: bool = view_env_input(&u, &ev, &es, &ed, &lay)
printf(" erase: solids %d, walls %d\n",
es.env.n_solids, es.env.n_walls)
check(d4 and es.env.n_solids == solids0
and es.env.n_walls == walls0, 105,
"the erase tool removes the block and its segments")
# A click that never moved must not leave a zero-length wall behind.
ed.tool = TOOL_WALL
ui_begin(&u, sx0, sy0, true, 0)
let d5: bool = view_env_input(&u, &ev, &es, &ed, &lay)
ui_begin(&u, sx0, sy0, false, 0)
let d6: bool = view_env_input(&u, &ev, &es, &ed, &lay)
check(es.env.n_walls == walls0, 106,
"a click with the wall tool adds nothing")
ed.tool = TOOL_SELECT
sim_destroy(&es)
# The scroll wheel. The backend reports a cumulative total that never
# resets, so ui_begin has to difference it. Passing the total through
# raw leaves it permanently non-zero after the first scroll, and the
# environment view then zooms every single frame until the arena is a