-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprobcomp-localization-tutorial.jl
2986 lines (2460 loc) · 159 KB
/
probcomp-localization-tutorial.jl
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
# -*- coding: utf-8 -*-
# ---
# jupyter:
# jupytext:
# cell_metadata_filter: -all
# custom_cell_magics: kql
# text_representation:
# extension: .jl
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.11.2
# kernelspec:
# display_name: Julia 1.9.1
# language: julia
# name: julia-1.9
# ---
# %% [markdown]
# # ProbComp Localization Tutorial
#
# This notebook aims to give an introduction to probabilistic computation (ProbComp). This term refers to a way of expressing probabilistic constructs in a computational paradigm, made precise by a probablistic programming language (PPL). The programmer can thus encode their probabilistic intuition for solving a problem into an algorithm. Back-end language work automates the routine but error-prone derivations.
# %%
# Global setup code
# Install dependencies listed in Project.toml
using Pkg
Pkg.activate(".")
Pkg.instantiate()
# The dependencies consist of the following Julia packages.
using Dates: now, value
using JSON: parsefile
using Plots
using Gen
# Ensure a location for image generation.
mkpath("imgs");
# %% [markdown]
# ## Sensing a robot's location on a map
#
# ### The map
#
# The tutorial will revolve around modeling the activity of a robot within some space. A large simplifying assumption, which could be lifted with more effort, is that we have been given a *map* of the space, to which the robot will have access.
#
# The code below loads such a map, along with other data for later use. Generally speaking, we keep general code and specific examples in separate cells, as signposted here.
# %%
# General code here
struct Segment
p1 :: Vector{Float64}
p2 :: Vector{Float64}
# The quantity `p2-p1` is called upon in hot loops, so we cache it.
dp :: Vector{Float64}
Segment(p1 :: Vector{Float64}, p2 :: Vector{Float64}) = new(p1, p2, p2-p1)
end
Base.show(io :: IO, s :: Segment) = Base.show(io, "Segment($(s.p1), $(s.p2))")
function create_segments(verts; loop_around=false)
segs = [Segment(p1, p2) for (p1, p2) in zip(verts[1:end-1], verts[2:end])]
if loop_around; push!(segs, Segment(verts[end], verts[1])) end
return segs
end
function make_world(walls_vec, clutters_vec; args...)
walls = create_segments(walls_vec; args...)
clutters = [create_segments(clutter; args...) for clutter in clutters_vec]
all_points = [walls_vec ; clutters_vec...]
x_min, x_max = extrema(first, all_points)
y_min, y_max = extrema(last, all_points)
bounding_box = (x_min, x_max, y_min, y_max)
box_size = max(x_max - x_min, y_max - y_min)
center_point = [(x_min + x_max) / 2.0, (y_min + y_max) / 2.0]
return (; walls, clutters, bounding_box, box_size, center_point)
end
function load_world(file_name; args...)
data = parsefile(file_name)
walls_vec = Vector{Vector{Float64}}(data["wall_verts"])
clutters_vec = Vector{Vector{Vector{Float64}}}(data["clutter_vert_groups"])
return make_world(walls_vec, clutters_vec; args...)
end;
# %%
# Specific example code here
world = load_world("world.json");
# %% [markdown]
# ### Plotting
#
# It is crucial to picture what we are doing at all times, so we develop plotting code early and often.
# %%
unit_circle_xs = [cos(t) for t in LinRange(0., 2pi, 500)]
unit_circle_ys = [sin(t) for t in LinRange(0., 2pi, 500)]
make_circle(p, r) = (p[1] .+ r * unit_circle_xs, p[2] .+ r * unit_circle_ys)
function plot_list!(list; label=nothing, args...)
if !isempty(list)
plt = plot!(list[1]; label=label, args...)
for item in list[2:end]; plot!(item; label=nothing, args...) end
return plt
end
end
Plots.plot!(seg :: Segment; args...) = plot!([seg.p1[1], seg.p2[1]], [seg.p1[2], seg.p2[2]]; args...)
Plots.plot!(segs :: Vector{Segment}; args...) = plot_list!(segs; args...)
Plots.plot!(seg_groups :: Vector{Vector{Segment}}; args...) = plot_list!(seg_groups; args...)
function plot_world(world, title; show=())
border = world.box_size * (3.)/19.
the_plot = plot(
size = (500, 500),
aspect_ratio = :equal,
grid = false,
xlim = (world.bounding_box[1]-border, world.bounding_box[2]+border),
ylim = (world.bounding_box[3]-border, world.bounding_box[4]+border),
title = title,
legend = :bottomleft)
(walls_label, clutter_label) = :label in show ? ("walls", "clutters") : (nothing, nothing)
plot!(world.walls; c=:black, label=walls_label)
if :clutters in show; plot!(world.clutters; c=:magenta, label=clutter_label) end
return the_plot
end;
# %% [markdown]
# Following this initial display of the given data, we *suppress the clutters* until much later in the notebook.
# %%
plot_world(world, "Given data", show=(:label, :clutters))
# %% [markdown]
# POSSIBLE VIZ GOAL: user-editable map, clutters, etc.
# %% [markdown]
# ### Robot poses
#
# We will model the robot's physical state as a *pose* (or mathematically speaking a ray), defined to be a *position* (2D point relative to the map) plus a *heading* (angle from -$\pi$ to $\pi$).
#
# These will be visualized using arrows whose tip is at the position, and whose direction indicates the heading.
# %%
struct Pose
p :: Vector{Float64}
hd :: Float64
# The quantity `[cos(hd), sin(hd)]` is called upon in hot loops, so we cache it.
dp :: Vector{Float64}
Pose(p :: Vector{Float64}, hd :: Float64) = new(p, rem2pi(hd, RoundNearest), [cos(hd), sin(hd)])
end
Base.show(io :: IO, p :: Pose) = Base.show(io, "Pose($(p.p), $(p.hd))")
Plots.plot!(p :: Pose; r=0.5, args...) = plot!(Segment(p.p, p.p + r * p.dp); arrow=true, args...)
Plots.plot!(ps :: Vector{Pose}; args...) = plot_list!(ps; args...);
# %%
some_poses = [Pose([uniform(world.bounding_box[1], world.bounding_box[2]),
uniform(world.bounding_box[3], world.bounding_box[4])],
uniform(-pi,pi))
for _ in 1:20]
plot_world(world, "Given data")
plot!(Pose([1., 2.], 0.); color=:green3, label="a pose")
plot!(Pose([2., 3.], pi/2.); color=:green4, label="another pose")
plot!(some_poses; color=:brown, label="some poses")
# %% [markdown]
# POSSIBLE VIZ GOAL: user can manipulate a pose. (Unconstrained vs. map for now.)
# %% [markdown]
# ### Ideal sensors
#
# The robot will need to reason about its location on the map, on the basis of LIDAR-like sensor data.
#
# An "ideal" sensor reports the exact distance cast to a wall. (It is capped off at a max value in case of error.)
# %%
# A general algorithm to find the interection of a ray and a line segment.
det2(u, v) = u[1] * v[2] - u[2] * v[1]
function distance(p :: Pose, seg :: Segment; PARALLEL_TOL=1.0e-10)
# Check if pose is parallel to segment.
det = det2(p.dp, seg.dp)
if abs(det) < PARALLEL_TOL; return Inf end
# Return unique s, t such that p.p + s * p.dp == seg.p1 + t * seg.dp.
pq = (p.p[1] - seg.p1[1], p.p[2] - seg.p1[2])
s = det2(seg.dp, pq) / det
t = det2(p.dp, pq) / det
# Pose is oriented towards from segment iff s >= 0.
# Point of intersection lies on segment (as opposed to the infinite line) iff 0 <= t <= 1.
return (s >= 0. && 0. <= t <= 1.) ? s : Inf
end;
# %%
function sensor_distance(pose, walls, box_size)
d = minimum(distance(pose, seg) for seg in walls)
# Capping to a finite value avoids issues below.
return isinf(d) ? 2. * box_size : d
end;
sensor_angle(sensor_settings, j) =
sensor_settings.fov * (j - (sensor_settings.num_angles - 1) / 2.) / (sensor_settings.num_angles - 1)
function ideal_sensor(pose, walls, sensor_settings)
readings = Vector{Float64}(undef, sensor_settings.num_angles)
for j in 1:sensor_settings.num_angles
sensor_pose = Pose(pose.p, pose.hd + sensor_angle(sensor_settings, j))
readings[j] = sensor_distance(sensor_pose, walls, sensor_settings.box_size)
end
return readings
end;
# %%
project_sensor(pose, angle, s) = let rotated = Pose(pose.p, pose.hd + angle); rotated.p + s * rotated.dp end
function plot_sensors!(pose, color, readings, label, sensor_settings)
plot!([pose.p[1]], [pose.p[2]]; color=color, label=nothing, seriestype=:scatter, markersize=3, markerstrokewidth=0)
projections = [project_sensor(pose, sensor_angle(sensor_settings, j), s) for (j, s) in enumerate(readings)]
plot!(first.(projections), last.(projections);
color=:blue, label=label, seriestype=:scatter, markersize=3, markerstrokewidth=1, alpha=0.25)
plot!([Segment(pose.p, pr) for pr in projections]; color=:blue, label=nothing, alpha=0.25)
end
function frame_from_sensors(world, title, poses, poses_color, poses_label, pose, readings, readings_label, sensor_settings; show=())
the_plot = plot_world(world, title; show=show)
plot!(poses; color=poses_color, label=poses_label)
plot_sensors!(pose, poses_color, readings, readings_label, sensor_settings)
return the_plot
end;
# %%
sensor_settings = (fov = 2π*(2/3), num_angles = 41, box_size = world.box_size)
ani = Animation()
for pose in some_poses
frame_plot = frame_from_sensors(
world, "Ideal sensor distances",
pose, :green2, "robot pose",
pose, ideal_sensor(pose, world.walls, sensor_settings), "ideal sensors",
sensor_settings)
frame(ani, frame_plot)
end
gif(ani, "imgs/ideal_distances.gif", fps=1)
# %% [markdown]
# POSSIBLE VIZ GOAL: as user manipulates pose, sensors get updated.
# %% [markdown]
# ## First steps in modeling uncertainty using Gen
#
# The robot will need to reason about its possible location on the map using incomplete information—in a pun, it must nagivate the uncertainty. The `Gen` system facilitates programming the required probabilistic logic. We will introduce the features of Gen, starting with some simple features now, and bringing in more complex ones later.
#
# Each piece of the model is declared as a *generative function* (GF). The `Gen` library provides two DSLs for constructing GFs: the dynamic DSL using the decorator `@gen` on a function declaration, and the static DSL similarly decorated with `@gen (static)`. The dynamic DSL allows a rather wide class of program structures, whereas the static DSL only allows those for which a certain static analysis may be performed.
#
# The library offers primitive *distributions* such as "Bernoulli" and "normal", and these two DLSs offer the *sampling operator* `~`. GFs may sample from distributions and, recursively, other GFs using `~`. A generative function embodies the *joint distribution* over the latent choices indicated by the sampling operations.
# %% [markdown]
# ### Creating noisy measurements using `Gen.propose`
#
# We have on hand two kinds of things to model: the robot's pose (and possibly its motion), and its sensor data. We tackle the sensor model first because it is simpler.
#
# Here is its declarative model in `Gen`:
# %%
@gen function sensor_model(pose, walls, sensor_settings)
for j in 1:sensor_settings.num_angles
sensor_pose = Pose(pose.p, pose.hd + sensor_angle(sensor_settings, j))
{j => :distance} ~ normal(sensor_distance(sensor_pose, walls, sensor_settings.box_size), sensor_settings.s_noise)
end
end;
# %% [markdown]
# This model differs from `ideal_sensor` in the following ways. The ideal sensor measurements themselves are no longer stored into an array, but are instead used as the means of Gaussian distributions (representing our uncertainty about them). *Sampling* from these distributions, using the `~` operator, occurs at the addresses `j => :distance`.
#
# Moreover, the function returns no explicit value. But there is no loss of information here: the model can be run with `Gen.propose` semantics, which performs the required draws from the sampling operations and organizes them according to their address, returning the corresponding *choice map* data structure. The method is called with the GF plus a tuple of arguments.
# %%
sensor_settings = (sensor_settings..., s_noise = 0.10)
cm, w = propose(sensor_model, (Pose([1., 1.], pi/2.), world.walls, sensor_settings))
cm
# %%
# For brevity, show just a subset of the choice map's addresses.
get_selected(cm, select((1:5)...))
# %% [markdown]
# With a little wrapping, one gets a function of the same type as `ideal_sensor`.
# %%
function noisy_sensor(pose, walls, sensor_settings)
cm, _ = propose(sensor_model, (pose, walls, sensor_settings))
return [cm[j => :distance] for j in 1:sensor_settings.num_angles]
end;
# %% [markdown]
# Let's get a picture of the distances returned by the model:
# %%
ani = Animation()
for pose in some_poses
frame_plot = frame_from_sensors(
world, "Sensor model (samples)",
pose, :green2, "robot pose",
pose, noisy_sensor(pose, world.walls, sensor_settings), "noisy sensors",
sensor_settings)
frame(ani, frame_plot)
end
gif(ani, "imgs/noisy_distances.gif", fps=1)
# %% [markdown]
# POSSIBLE VIZ GOAL: same sensor interactive as before, now with noisy sensors.
# %% [markdown]
# ### Weighing data with `Gen.assess`
#
# The mathematical picture is as follows. Given the parameters of a pose $z$, walls $w$, and settings $\nu$, one gets a distribution $\text{sensor}(z, w, \nu)$ over certain choice maps. The supporting choice maps are identified with vectors $o = o^{(1:J)} = (o^{(1)}, o^{(2)}, \ldots, o^{(J)})$, where $J := \nu_\text{num\_angles}$, each $o^{(j)}$ independently following a certain normal distribution (depending, notably, on a distance to a wall). Thus the density of $o$ factors into a product of the form
# $$
# P_\text{sensor}(o) = \prod\nolimits_{j=1}^J P_\text{normal}(o^{(j)})
# $$
# where we begin a habit of omitting the parameters to distributions that are implied by the code.
#
# As `propose` draws a sample, it simultaneously computes this density or *score* and returns its logarithm:
# %%
exp(w)
# %% [markdown]
# There are many scenarios where one has on hand a full set of data, perhaps via observation, and seeks their score according to the model. One could write a program by hand to do this—but one would simply recapitulate the code for `noisy_sensor`. The difference is that the sampling operations would be replaced with density computations, and instead of storing them in a choice map it would compute their log product.
#
# The construction of a log density function is automated by the `Gen.assess` semantics for generative functions. This method is passed the GF, a tuple of arguments, and a choice map, and returns the log weight plus the return value.
# %%
exp(assess(sensor_model, (Pose([1., 1.], pi/2.), world.walls, sensor_settings), cm)[1])
# %% [markdown]
# ## First steps in probabilistic reasoning
#
# Let pick some measured noisy distance data
#
# > ***LOAD THEM FROM FILE***
#
# and try to reason about where the robot could have taken them from.
# %% [markdown]
# POSSIBLE VIZ GOAL: User can start from the loaded data, or move around to grab some noisy sensors. Then, user can move around a separate candiate-match pose, and `assess` the data against it with updated result somehow.
# %% [markdown]
# What we are exploring here is in Bayesian parlance the *likelihood* of the varying pose. One gets a sense that certain poses were somehow more likely than others, and the modeling of this intuitive sense is called *inference*.
#
# The above exploration points at a strategy of finding the pose (parameter) that optimizes the likelihood (some statistic), a ubiquitous process called *variational inference*.
#
# A subtle but crucial matter must be dealt with. This act of variational inference silently adopts assumptions having highly nontrivial consequences for our inferences, having to do with the issue of *prior* over the parameter.
#
# First we must acknowledge at all, that our reasoning always approaches the question "Where is the robot?" already having some idea of where it is possible for the robot to be. We interpret new information in terms of these assumptions, and they definitely influence the inferences we make. For example, if we were utterly sure the robot were near the center of the map, we would only really consider how the sensor data around such poses, even if there were better fits elsewhere that we did not expect the robot to be.
#
# These assumptions are modeled by a distribution over poses called the *prior*. Then, according to Bayes's Law, the key quantity to examine is not the likelihood density $P_\text{sensor}(o;z)$ but rather the *posterior* density
# $$
# P_\text{posterior}(z|o) = P_\text{sensor}(o;z) \cdot P_\text{prior}(z) / Z
# $$
# where $Z > 0$ is a normalizing constant. Likelihood optimization amounts to assuming a prior having $P_\text{prior}(z) \equiv 1$, a so-called "uniform" prior over the parameter space.
#
# The uniform prior may appear to be a natural expression of "complete ignorance", not preferencing any parameter over another. The other thing to acknowledge is that this is not the case: the parameterization of the latents itself embodies preferences among parameter values. Different parametramizations of the latents lead to different "uniform" distributions over them. For example, parameterizing the spread of a univariate normal distribution by its standard deviation and its variance lead to different "uniform" priors over the parameter space, the square map being nonlinear. Thus likelihood optimization's second tacit assumption is a particular parametric representation of the latents space, according to which uniformity occurs.
#
# Summarizing, likelihood optimization does not lead to *intrinsic* inference conclusions, because it relies on a prior that in turn is not intrinsic, but rather depends on how the parameters are presented. Intrinsic conclusions are instead drawn by specifying the prior as a distribution, which has a consistent meaning across parameterizations.
#
# So let us be upfront that we choose the uniform prior relative to the conventional meaning of the pose parameters. Here is a view onto the posterior distribution over poses, given a set of sensor measurements.
# %% [markdown]
# POSSIBLE VIZ GOAL: Gather the preceding viz into one view: alpha blend all candidate-match poses by likelihood, so only plausible things appear, with the mode highlighted.
# %% [markdown]
# PUT HERE: expanded discussion of single-pose inference problem.
#
# From optimization/VI to sampling techniques. Reasons:
# * Note *how much information we are throwing away* when passing from the distribution to a single statistic. Something must be afoot.
# * Later inferences depend on the *whole distribution* of parameters.
# * Reducing to (Dirac measures on) the modes breaks compositional validity!
# * The modes might not even be *representative* of the posterior:
# * The mode might not even be where any mass actually accumulates, as in a high-dimensional Gaussian!
# * Mass might be distributed among multiple near-tied modes, unnaturally preferencing one of them.
# * The posterior requires clearly specifying a prior, which (as mentioned above) prevents ambiguities of parameterization.
#
# Replace `argmax` with a resampling operation (SIR). Grid vs. free choice.
#
# Compare to a NN approach.
# %% [markdown]
# ## Modeling robot motion
#
# As said initially, we are uncertain about the true initial position and subsequent motion of the robot. In order to reason about these, we now specify a model using `Gen`.
#
# Each piece of the model is declared as a *generative function* (GF). The `Gen` library provides two DSLs for constructing GFs: the dynamic DSL using the decorator `@gen` on a function declaration, and the static DSL similarly decorated with `@gen (static)`. The dynamic DSL allows a rather wide class of program structures, whereas the static DSL only allows those for which a certain static analysis may be performed.
#
# The library offers two basic constructs for use within these DSLs: primitive *distributions* such as "Bernoulli" and "normal", and the sampling operator `~`. Recursively, GFs may sample from other GFs using `~`.
# %% [markdown]
# ### Robot programs
#
# We also assume given a description of a robot's movement via
# * an estimated initial pose (= position + heading), and
# * a program of controls (= advance distance, followed by rotate heading).
# %%
# A value `c :: Control` corresponds to the robot *first* advancing in its present direction by `c.ds`, *then* rotating by `c.dhd`.
struct Control
ds :: Float64
dhd :: Float64
end
function load_program(file_name)
data = parsefile(file_name)
start = Pose(Vector{Float64}(data["start_pose"]["p"]), Float64(data["start_pose"]["hd"]))
controls = Vector{Control}([Control(control["ds"], control["dhd"]) for control in data["program_controls"]])
return (; start, controls), length(controls)
end;
# %%
robot_inputs, T = load_program("robot_program.json");
# %% [markdown]
# Before we can visualize such a program, we will need to model robot motion.
# %% [markdown]
# POSSIBLE VIZ GOAL: user can manipulate a pose, and independently a control (vecor-like relative to it), with new pose in shadow.
# %% [markdown]
# ### Integrate a path from a starting pose and controls
#
# If the motion of the robot is determined in an ideal manner by the controls, then we may simply integrate to determine the resulting path. Naïvely, this results in the following.
# %%
function integrate_controls_unphysical(robot_inputs)
path = Vector{Pose}(undef, length(robot_inputs.controls) + 1)
path[1] = robot_inputs.start
for t in 1:length(robot_inputs.controls)
p = path[t].p + robot_inputs.controls[t].ds * path[t].dp
hd = path[t].hd + robot_inputs.controls[t].dhd
path[t+1] = Pose(p, hd)
end
return path
end;
# %% [markdown]
# POSSIBLE VIZ GOAL: user can manipulate a whole path, still ignoring walls.
# %% [markdown]
# This code has the problem that it is **unphysical**: the walls in no way constrain the robot motion.
#
# We employ the following simple physics: when the robot's forward step through a control comes into contact with a wall, that step is interrupted and the robot instead "bounces" a fixed distance from the point of contact in the normal direction to the wall.
# %%
norm(v) = sqrt(sum(v.^2))
function physical_step(p1, p2, hd, world_inputs)
p21 = (p2[1] - p1[1], p2[2] - p1[2])
step_pose = Pose(p1, atan(p21[2], p21[1]))
s, i = findmin(w -> distance(step_pose, w), world_inputs.walls)
if s > norm(p21)
# Step succeeds without contact with walls.
return Pose(p2, hd)
else
contact_point = p1 + s * step_pose.dp
unit_tangent = world_inputs.walls[i].dp / norm(world_inputs.walls[i].dp)
unit_normal = [-unit_tangent[2], unit_tangent[1]]
# Sign of 2D cross product determines orientation of bounce.
if det2(step_pose.dp, world_inputs.walls[i].dp) < 0.
unit_normal = -unit_normal
end
return Pose(contact_point + world_inputs.bounce * unit_normal, hd)
end
end
function integrate_controls(robot_inputs, world_inputs)
path = Vector{Pose}(undef, length(robot_inputs.controls) + 1)
path[1] = robot_inputs.start
for t in 1:length(robot_inputs.controls)
p = path[t].p + robot_inputs.controls[t].ds * path[t].dp
hd = path[t].hd + robot_inputs.controls[t].dhd
path[t+1] = physical_step(path[t].p, p, hd, world_inputs)
end
return path
end;
# %%
# How bouncy the walls are in this world.
world_inputs = (walls = world.walls, bounce = 0.1)
path_integrated = integrate_controls(robot_inputs, world_inputs);
# %%
plot_world(world, "Given data", show=(:label,))
plot!(robot_inputs.start; color=:green3, label="given start pose")
plot!([pose.p[1] for pose in path_integrated], [pose.p[2] for pose in path_integrated];
color=:green2, label="path from integrating controls", seriestype=:scatter, markersize=3, markerstrokewidth=0)
# %% [markdown]
# We can also visualize the behavior of the model of physical motion:
#
# 
# %% [markdown]
# POSSIBLE VIZ GOAL: user can manipulate a whole path, now obeying walls.
# %% [markdown]
# ### Components of the motion model
#
# We start with the two building blocks: the starting pose and individual steps of motion.
# %%
@gen (static) function start_pose_prior(start, motion_settings)
p ~ mvnormal(start.p, motion_settings.p_noise^2 * [1 0 ; 0 1])
hd ~ normal(start.hd, motion_settings.hd_noise)
return Pose(p, hd)
end
@gen (static) function step_model(start, c, world_inputs, motion_settings)
p ~ mvnormal(start.p + c.ds * start.dp, motion_settings.p_noise^2 * [1 0 ; 0 1])
hd ~ normal(start.hd + c.dhd, motion_settings.hd_noise)
return physical_step(start.p, p, hd, world_inputs)
end;
# %% [markdown]
# Returning to the code, we can call a GF like a normal function and it will just run stochastically:
# %%
motion_settings = (p_noise = 0.5, hd_noise = 2π / 360)
N_samples = 50
pose_samples = [start_pose_prior(robot_inputs.start, motion_settings) for _ in 1:N_samples]
std_devs_radius = 2.5 * motion_settings.p_noise
plot_world(world, "Start pose prior (samples)")
plot!(make_circle(robot_inputs.start.p, std_devs_radius);
color=:red, linecolor=:red, label="95% region", seriestype=:shape, alpha=0.25)
plot!(pose_samples; color=:red, label="start pose samples")
# %%
N_samples = 50
noiseless_step = robot_inputs.start.p + robot_inputs.controls[1].ds * robot_inputs.start.dp
step_samples = [step_model(robot_inputs.start, robot_inputs.controls[1], world_inputs, motion_settings) for _ in 1:N_samples]
plot_world(world, "Motion step model model (samples)")
plot!(robot_inputs.start; color=:black, label="step from here")
plot!(make_circle(noiseless_step, std_devs_radius);
color=:red, linecolor=:red, label="95% region", seriestype=:shape, alpha=0.25)
plot!(step_samples; color=:red, label="step samples")
# %% [markdown]
# ### Traces: choice maps
#
# We can also perform *traced execution* of a generative function using the construct `Gen.simulate`. This means that certain information is recorded during execution and packaged into a *trace*, and this trace is returned instead of the bare return value sample.
#
# The foremost information stored in the trace is the *choice map*, which is an associative array from labels to the labeled stochastic choices, i.e. occurrences of the `~` operator, that were encountered. It is accessed by `Gen.get_choices`:
# %%
# `simulate` takes the GF plus a tuple of args to pass to it.
trace = simulate(start_pose_prior, (robot_inputs.start, motion_settings))
get_choices(trace)
# %% [markdown]
# The choice map being the point of focus of the trace in most discussions, we often abusively just speak of a *trace* when we really mean its *choice map*.
# %% [markdown]
# ### Gen.jl API for traces
#
# One can access the primitive choices in a trace using the bracket syntax `trace[address]`. One can access from a trace the GF that produced it using `Gen.get_gen_fn`, along with with arguments that were supplied using `Gen.get_args`, and the return value sample of the GF using `Gen.get_retval`. See below the fold for examples of all these.
# %%
trace[:p]
# %%
trace[:hd]
# %%
get_gen_fn(trace)
# %%
get_args(trace)
# %%
get_retval(trace)
# %% [markdown]
# ### Traces: scores/weights/densities
#
# Traced execution of a generative function also produces a particular kind of score/weight/density. It is very important to be clear about which score/weight/density value is to be expected, and why. Consider the following generative function
# ```
# p = 0.25
# @gen function g(x,y)
# flip ~ bernoulli(p)
# return flip ? x : y
# end
# ```
# that, given two inputs `x` and `y`, flips a coin with weight `p`, and accordingly returns `x` or `y`. When `x` and `y` are unequal, a sensible reporting of the score/weight/density in the sampling process would produce `p` or `1.0-p` accordingly. If the user supplied equal values `x == y`, then which score/weight/density should be returned?
#
# One tempting view identifies a GF with a *distribution over its return values*. In this view, the correct score/weight/density of `g` above would be $1$.
#
# The mathematical picture would be as follows. Given a stochastic function $g$ from $X$ to $X'$, the results of calling $g$ on the input $x$ are described by a probability distribution $k_{g;x}$ on $X'$. A family of probability distributions of this form is called a *probability kernel* and is indicated by the dashed arrow $k_g \colon X \dashrightarrow X'$. And for some $x,x'$ we would be seeking the density $k_{g;x}(x')$ with which the sample $x' \sim k_{g;x}$ occurs. Pursuing this approach requires knowlege of all execution histories that $g$ that might have followed from $x$ to $x'$, and then performing a sum or integral over them. For some small finite situations this may be fine, but this general problem of computing marginalizations is computationally impossible.
#
# The marginalization question is especially forced upon us when trying to compose stochastic functions. Given a second stochastic function $g'$ from $X'$ to $X''$, corresponding to a probability kernel $k_{g'} \colon X' \dashrightarrow X''$, the composite $g' \circ g$ from $X$ to $X''$ should correspond to the following probability kernel $k_{g' \circ g} \colon X \dashrightarrow X''$. To sample $x'' \sim k_{g' \circ g;x}$ means "sample $x' \sim k_{g;x}$, then sample $x'' \sim k_{g';x'}$, then return $x''$". However, computing the density $k_{g' \circ g;x}(x'')$, even if one can compute $k_{g;x}(x')$ and $k_{g';x'}(x'')$ for any given $x,x',x''$, would require summing or integrating over all possible intermediate values $x'$ (which manifests an "execution history" of $g' \circ g$) that could have intervened in producing $x''$ given $x$.
#
# Therefore, distribution-over-return-values is ***not the viewpoint of Gen***, and the score/weight/density being introduced here is a ***different number***.
#
# The only thing a program can reasonably be expected to know is the score/weight/density of its arriving at its return value *via the particular stochastic computation path* that got it there, and the approach of Gen is to report this number. The corresponding mathematical picture imagines GFs as factored into *distributions over choice maps*, whose score/weight/density is computable, together with *deterministic functions on these data* that produce the return value from them. In mathematical language, a GF $g$ from $X$ to $X'$ corresponds to the data of an auxiliary space $U_g$ containing all of the choice map information, a probability kernel $k_g \colon X \dashrightarrow U_g$ (with computable density) embodying the stochastic execution history, and a deterministic function that we will (somewhat abusively) denote $g \colon X \times U_g \to X'$ embodying extraction of the return value from the particular stochastic execution choices.
#
# In the toy example `g` above, choice map consists of `flip` so the space $U_g$ is binary; the deterministic computation $g$ amounts to the `return` statement; and the score/weight/density is `p` or `1.0-p`, regardless of whether the inputs are equal.
#
# Tractable compositionality holds in this formulation; let's spell it out. If another GF $g'$ from $X'$ to $X''$ has data $U_{g'}$, $k_{g'}$, and $g' \colon X' \times U_{g'} \to X''$, then the composite GF $g' \circ g$ from $X$ to $X''$ has the following data.
# * The auxiliary space is $U_{g' \circ g} := U_g \times U_{g'}$.
# * The kernel $k_{g' \circ g}$ is defined by "sample $u \sim k_{g;x}$, then compute $x' = \text{ret}_g(x,u)$, then sample $u' \sim k_{g';x'}$, then return $(u,u')$", and
# * its density is computed via $k_{g' \circ g; x}(u,u') := k_{g;x}(u) \cdot k_{g';g(x,u)}(u')$.
# * The return value function is $(g' \circ g)(x,(u,u')) := g'(g(x,u),u')$.
#
# As one composes more GFs, the auxiliary space accumulates more factors $U$, reflecting how the "execution history" consists of longer and longer records.
#
# In this picture, one may still be concerned with the distribution on return values as in the straw man viewpoint. This information is still embodied in the aggregate of the stochastic executions that lead to any return value, together with their weights. (Consider that this is true even in the toy example! More math?) In a sense, when we kick the can of marginalization down the road, we can proceed without difficulty.
#
# A final caveat: The common practice of confusing traces with their choice maps continues here, and we speak of a GF inducing a "distribution over traces".
# %% [markdown]
# Let's have a look at the score/weight/densities in our running example.
#
# A pose consists of a pair $z = (z_\text p, z_\text{hd})$ where $z_\text p$ is a position vector and $z_\text{hd}$ is an angle. A control consists of a pair $(s, \eta)$ where $s$ is a distance of displacement and $\eta$ is a change in angle. Write $u(\theta) = (\cos\theta, \sin\theta)$ for the unit vector in the direction $\theta$. We are given a "world" $w$ and "motion settings" parameters $\nu = (\nu_\text p, \nu_\text{hd})$.
#
# The models `start_pose_prior` and `step_model` correspond to distributions over their traces, respectively written $\text{start}$ and $\text{step}$. In both cases these traces consist of the choices at addresses `:p` and `:hd`, so they may be identified with poses $z$ as above. The distributions are defined as follows, when $y$ is a pose:
# * $z \sim \text{start}(y, \nu)$ means that $z_\text p \sim \text{mvnormal}(y_\text p, \nu_\text p^2 I)$ and $z_\text{hd} \sim \text{normal}(y_\text{hd}, \nu_\text{hd})$ independently.
# * $z \sim \text{step}(y, (s, \eta), w, \nu)$ means that $z_\text p \sim \text{mvnormal}(y_\text p + s\,u(y_\text{hd}), \nu_\text p^2 I)$ and $z_\text{hd} \sim \text{normal}(y_\text{hd} + \eta, \nu_\text {hd})$ independently.
#
# The return values $\text{retval}(z)$ of these models are obtained from traces $z$ by reducing $z_\text{hd}$ modulo $2\pi$, and in the second case applying collision physics (relative to $w$) to the path from $y_\text p$ to $z_\text p$. (We invite the reader to imagine if PropComp required us to compute the marginal density of the return value here!) We have the following closed form for the density functions:
# $$\begin{align*}
# P_\text{start}(z; y, \nu)
# &= P_\text{mvnormal}(z_\text p; y_\text p, \nu_\text p^2 I)
# \cdot P_\text{normal}(z_\text{hd}; y_\text{hd}, \nu_\text{hd}), \\
# P_\text{step}(z; y, (s, \eta), w, \nu)
# &= P_\text{mvnormal}(z_\text p; y_\text p + s\,u(y_\text{hd}), \nu_\text p^2 I)
# \cdot P_\text{normal}(z_\text{hd}; y_\text{hd} + \eta, \nu_\text{hd}).
# \end{align*}$$
#
# In general, the density of any trace factors as the product of the densities of the individual primitive choices that appear in it. Since the primitive distributions of the language are equipped with efficient probability density functions, this overall computation is tractable. It is represented by `Gen.get_score`:
# %%
get_score(trace)
# %% [markdown]
# #### Subscores/subweights/subdensities
#
# Instead of (the log of) the product of all the primitive choices made in a trace, one can take the product over just a subset using `Gen.project`. See below the fold for examples.
# %%
project(trace, select())
# %%
project(trace, select(:p))
# %%
project(trace, select(:hd))
# %%
project(trace, select(:p, :hd)) == get_score(trace)
# %% [markdown]
# ### Modeling a full path
#
# The model contains all information in its trace, rendering its return value redundant. The noisy path integration will just be a wrapper around its functionality, extracting what it needs from the trace.
#
# (It is worth acknowledging two strange things in the code below: the extra text "`_loop`" in the function name, and the seemingly redundant new parameter `T`. Both will be addressed shortly, along with the aforementioned wrapper.)
# %%
@gen function path_model_loop(T, robot_inputs, world_inputs, motion_settings)
pose = {:initial => :pose} ~ start_pose_prior(robot_inputs.start, motion_settings)
for t in 1:T
pose = {:steps => t => :pose} ~ step_model(pose, robot_inputs.controls[t], world_inputs, motion_settings)
end
end
prefix_address(t, rest) = (t == 1) ? (:initial => rest) : (:steps => (t-1) => rest)
get_path(trace) = [trace[prefix_address(t, :pose)] for t in 1:(get_args(trace)[1]+1)];
# %% [markdown]
# A trace of `path_model_loop` is more interesting than the ones for `start_pose_prior` and `step_model`. Let's take a look. (To reduce notebook clutter, we just show the start pose plus the initial 5 timesteps.)
# %%
trace = simulate(path_model_loop, (T, robot_inputs, world_inputs, motion_settings))
get_selected(get_choices(trace), select((prefix_address(t, :pose) for t in 1:6)...))
# %% [markdown]
# We find that a choicemap is a tree structure rather than a flat associative array. Addresses are actually root-to-node paths, which are constructed using the `=>` operator.
#
# Moreover, when the source code of a GF applies the `~` operator not to a primitive distribution but to some other generative function, the latter's choice map is included as a subtree rooted at the corresponding node. That is, the choice map captures the recursive structure of the stochastic locations of the control flow.
# %% [markdown]
# The corresponding mathematical picture is as follows. We write $x_{a:b} = (x_a, x_{a+1}, \ldots, x_b)$ to gather items $x_t$ into a vector.
#
# In addition to the previous data, we are given an estimated start pose $r_0$ and controls $r_t = (s_t, \eta_t)$ for $t=1,\ldots,T$. Then `path_model` corresponds to a distribution over traces denoted $\text{path}$; these traces are identified with vectors, namely, $z_{0:T} \sim \text{path}(r_{0:T}, w, \nu)$ is the same as $z_0 \sim \text{start}(r_0, \nu)$ and $z_t \sim \text{step}(z_{t-1}, r_t, w, \nu)$ for $t=1,\ldots,T$. Here and henceforth we use the shorthand $\text{step}(z, \ldots) := \text{step}(\text{retval}(z), \ldots)$. The density function is
# $$
# P_\text{path}(z_{0:T}; r_{0:T}, w, \nu)
# = P_\text{start}(z_0; r_0, \nu) \cdot \prod\nolimits_{t=1}^T P_\text{step}(z_t; z_{t-1}, r_t, w, \nu)
# $$
# where each term, in turn, factors into a product of two (multivariate) normal densities as described above.
# %% [markdown]
# As our truncation of the example trace above might suggest, visualization is an essential practice in ProbComp. We could very well pass the output of the above `integrate_controls_noisy` to the `plot!` function to have a look at it. However, we want to get started early in this notebook on a good habit: writing interpretive code for GFs in terms of their traces rather than their return values. This enables the programmer include the parameters of the model in the display for clarity.
# %%
function frames_from_motion_trace(world, title, trace; show=())
T = get_args(trace)[1]
robot_inputs = get_args(trace)[2]
poses = get_path(trace)
noiseless_steps = [robot_inputs.start.p, [pose.p + c.ds * pose.dp for (pose, c) in zip(poses, robot_inputs.controls)]...]
motion_settings = get_args(trace)[4]
std_devs_radius = 2.5 * motion_settings.p_noise
plots = Vector{Plots.Plot}(undef, T+1)
for t in 1:(T+1)
frame_plot = plot_world(world, title; show=show)
plot!(poses[1:t-1]; color=:black, label="past poses")
plot!(make_circle(noiseless_steps[t], std_devs_radius);
color=:red, linecolor=:red, label="95% region", seriestype=:shape, alpha=0.25)
plot!(Pose(trace[prefix_address(t, :pose => :p)], poses[t].hd); color=:red, label="sampled next step")
plots[t] = frame_plot
end
return plots
end;
# %% [markdown]
# Here is what a step through the code looks like:
#
# 
# %%
N_samples = 5
ani = Animation()
for n in 1:N_samples
trace = simulate(path_model_loop, (T, robot_inputs, world_inputs, motion_settings))
frames = frames_from_motion_trace(world, "Motion model (samples)", trace)
for frame_plot in frames; frame(ani, frame_plot) end
end
gif(ani, "imgs/motion.gif", fps=2)
# %% [markdown]
# ### Modfying traces
#
# The metaprogramming approach of Gen affords the opportunity to explore alternate stochastic execution histories. Namely, `Gen.update` takes as inputs a trace, together with modifications to its arguments and primitive choice values, and returns an accordingly modified trace. It also returns (the log of) the ratio of the updated trace's density to the original trace's density, together with a precise record of the resulting modifications that played out.
# %% [markdown]
# One could, for instance, consider just the placement of the first pose, and replace its stochastic choice of heading with a specific value.
# %%
trace = simulate(start_pose_prior, (robot_inputs.start, motion_settings))
rotated_trace, rotated_trace_weight_diff, _, _ =
update(trace, (robot_inputs.start, motion_settings), (NoChange(), NoChange()), choicemap((:hd, π/2.)))
plot_world(world, "Modifying a heading")
plot!(get_retval(trace); color=:green, label="some pose")
plot!(get_retval(rotated_trace); color=:red, label="with heading modified")
# %% [markdown]
# The original trace was typical under the pose prior model, whereas the modified one is rather less likely. This is the log of how much unlikelier:
# %%
rotated_trace_weight_diff
# %% [markdown]
# It is worth carefully thinking through a tricker instance of this. Suppose instead, within the full path, we replaced the $t = 1$ step's stochastic choice of heading with some specific value.
# %%
trace = simulate(path_model_loop, (T, robot_inputs, world_inputs, motion_settings))
rotated_first_step, rotated_first_step_weight_diff, _, _ =
update(trace,
(T, robot_inputs, world_inputs, motion_settings), (NoChange(), NoChange(), NoChange(), NoChange()),
choicemap((:steps => 1 => :pose => :hd, π/2.)))
plot_world(world, "Modifying another heading")
plot!(get_path(trace); color=:green, label="some path")
plot!(get_path(rotated_first_step); color=:red, label="with heading at first step modified")
# %% [markdown]
# In the above picture, the green path is apparently missing, having been near-completely overdrawn by the red path. This is because in the execution of the model, the only change in the stochastic choices took place where we specified. In particular, the stochastic choice of pose at the second step was left unchanged. This choice was typical relative to the first step's heading in the old trace, and while it is not impossible relative to the first step's heading in the new trace, it is *far unlikelier* under the mulitvariate normal distribution supporting it:
# %%
rotated_first_step_weight_diff
# %% [markdown]
# Another capability of `Gen.update` is to modify the *arguments* to the generative function used to produce the trace. In our example, we might have on hand a very long list of controls, and we wish to explore the space of paths incrementally in the timestep:
# %%
change_only_T = (UnknownChange(), NoChange(), NoChange(), NoChange())
trace = simulate(path_model_loop, (0, robot_inputs, world_inputs, motion_settings))
for t in 1:T
trace, _, _, _ = update(trace, (t, robot_inputs, world_inputs, motion_settings), change_only_T, choicemap())
# ...
# Do something with the trace of the partial path up to time t.
# ...
@assert has_value(get_choices(trace), :steps => t => :pose => :p)
@assert !has_value(get_choices(trace), :steps => (t+1) => :pose => :p)
end
println("Success");
# %% [markdown]
# Because performing such updates to traces occur frequently, and they seemingly require re-running the entire model, computational complexity considerations become important. We detour next through an important speedup.
# %% [markdown]
# ### Improving performance using the static DSL and combinators
#
# Because the dynamic DSL does not understand the loop inside `path_model_loop`, calling `Gen.update` with the new value of `T` requires re-execution of the whole loop. This means that the update requires $O(T)$ time, and the above code requires $O(T^2)$ time.
#
# But we humans understand that incrementing the argument `T` simply requires running the loop body once more. This operation runs in $O(1)$ time, so the outer loop should require only $O(T)$ time. Gen can intelligently work this way if we encode the structure of Markov chain in this model using a *combinator* for the static DSL, as follows.
# %%
@gen (static) function motion_path_kernel(t, state, robot_inputs, world_inputs, motion_settings)
return {:pose} ~ step_model(state, robot_inputs.controls[t], world_inputs, motion_settings)
end
motion_path_chain = Unfold(motion_path_kernel)
@gen (static) function path_model(T, robot_inputs, world_inputs, motion_settings)
initial = {:initial => :pose} ~ start_pose_prior(robot_inputs.start, motion_settings)
{:steps} ~ motion_path_chain(T, initial, robot_inputs, world_inputs, motion_settings)
end;
# %% [markdown]
# The models `path_model_loop` and `path_model` have been arranged to produce identically structured traces with the same frequencies and return values, and to correspond to identical distributions over traces in the mathematical picture, thereby yielding the same weights. They give rise to identical computations under `Gen.simulate`, whereas the new model is sometimes more efficient under `Gen.update`. Here we illustrate the efficiency gain.
#
# (The noise in the graph is an artifact of Julia's garbage collection.)
#
# 
# %% [markdown]
# Owing to the efficiency comparison, we eschew `path_model_loop` in favor of `path_model` in what follows. Thus we finally write our noisy path integration wrapper.
# %%
function integrate_controls_noisy(robot_inputs, world_inputs, motion_settings)
return get_path(simulate(path_model, (length(robot_inputs.controls), robot_inputs, world_inputs, motion_settings)))
end;
# %% [markdown]
# ### Full model
#
# We fold the sensor model into the motion model to form a "full model", whose traces describe simulations of the entire robot situation as we have described it.
# %%
@gen (static) function full_model_initial(robot_inputs, walls, full_settings)
pose ~ start_pose_prior(robot_inputs.start, full_settings.motion_settings)
{:sensor} ~ sensor_model(pose, walls, full_settings.sensor_settings)
return pose
end
@gen (static) function full_model_kernel(t, state, robot_inputs, world_inputs, full_settings)
pose ~ step_model(state, robot_inputs.controls[t], world_inputs, full_settings.motion_settings)
{:sensor} ~ sensor_model(pose, world_inputs.walls, full_settings.sensor_settings)
return pose
end
full_model_chain = Unfold(full_model_kernel)
@gen (static) function full_model(T, robot_inputs, world_inputs, full_settings)
initial ~ full_model_initial(robot_inputs, world_inputs.walls, full_settings)
steps ~ full_model_chain(T, initial, robot_inputs, world_inputs, full_settings)
end
get_sensors(trace) =
[[trace[prefix_address(t, :sensor => j => :distance)]
for j in 1:get_args(trace)[4].sensor_settings.num_angles]
for t in 1:(get_args(trace)[1]+1)];
# %% [markdown]
# Again, the trace of the full model contains many choices, so we just show a subset of them: the initial pose plus 2 timesteps, and 5 sensor readings from each.
# %%
full_settings = (motion_settings=motion_settings, sensor_settings=sensor_settings)
full_model_args = (robot_inputs, world_inputs, full_settings)
trace = simulate(full_model, (T, full_model_args...))
selection = select((prefix_address(t, :pose) for t in 1:3)..., (prefix_address(t, :sensor => j) for t in 1:3, j in 1:5)...)
get_selected(get_choices(trace), selection)
# %% [markdown]
# In the math picture, `full_model` corresponds to a distribution $\text{full}$ over its traces. Such a trace is identified with of a pair $(z_{0:T}, o_{0:T})$ where $z_{0:T} \sim \text{path}(\ldots)$ and $o_t \sim \text{sensor}(z_t, \ldots)$ for $t=0,\ldots,T$. The density of this trace is then
# $$\begin{align*}
# P_\text{full}(z_{0:T}, o_{0:T})
# &= P_\text{path}(z_{0:T}) \cdot \prod\nolimits_{t=0}^T P_\text{sensor}(o_t) \\
# &= \big(P_\text{start}(z_0)\ P_\text{sensor}(o_0)\big)
# \cdot \prod\nolimits_{t=1}^T \big(P_\text{step}(z_t)\ P_\text{sensor}(o_t)\big).
# \end{align*}$$
#
# By this point, visualization is essential.
# %%
function frame_from_sensors_trace(world, title, poses, poses_color, poses_label, pose, trace; show=())
readings = [trace[j => :distance] for j in 1:sensor_settings.num_angles]
return frame_from_sensors(world, title, poses, poses_color, poses_label, pose,
readings, "trace sensors", get_args(trace)[3]; show=show)
end
function frames_from_full_trace(world, title, trace; show=())
T = get_args(trace)[1]
robot_inputs = get_args(trace)[2]
poses = get_path(trace)
noiseless_steps = [robot_inputs.start.p, [pose.p + c.ds * pose.dp for (pose, c) in zip(poses, robot_inputs.controls)]...]
settings = get_args(trace)[4]
std_devs_radius = 2.5 * settings.motion_settings.p_noise
sensor_readings = get_sensors(trace)
plots = Vector{Plots.Plot}(undef, 2*(T+1))
for t in 1:(T+1)
frame_plot = plot_world(world, title; show=show)
plot!(poses[1:t-1]; color=:black, label="past poses")
plot!(make_circle(noiseless_steps[t], std_devs_radius);
color=:red, linecolor=:red, label="95% region", seriestype=:shape, alpha=0.25)
plot!(Pose(trace[prefix_address(t, :pose => :p)], poses[t].hd); color=:red, label="sampled next step")
plots[2*t-1] = frame_plot
plots[2*t] = frame_from_sensors(
world, title,
poses[1:t], :black, nothing,
poses[t], sensor_readings[t], "sampled sensors",
settings.sensor_settings; show=show)
end
return plots
end;
# %% [markdown]
# Here is a stepping through the code:
#
# 
# %%
N_samples = 5
ani = Animation()
for n in 1:N_samples
trace = simulate(full_model, (T, full_model_args...))
frames = frames_from_full_trace(world, "Full model (samples)", trace)
for frame_plot in frames; frame(ani, frame_plot) end
end
gif(ani, "imgs/full_1.gif", fps=2)
# %% [markdown]
# ## The data
#
# Let us generate some fixed synthetic motion data that, for pedagogical purposes, we will work with as if it were the actual path of the robot. We will generate two versions, one each with low or high motion deviation.
# %%
motion_settings_low_deviation = (p_noise = 0.05, hd_noise = (1/10.) * 2π / 360)
trace_low_deviation = simulate(full_model, (T, robot_inputs, world_inputs, (full_settings..., motion_settings=motion_settings_low_deviation)))
motion_settings_high_deviation = (p_noise = 0.25, hd_noise = 2π / 360)
trace_high_deviation = simulate(full_model, (T, robot_inputs, world_inputs, (full_settings..., motion_settings=motion_settings_high_deviation)))
frames_low = frames_from_full_trace(world, "Low motion deviation", trace_low_deviation)
frames_high = frames_from_full_trace(world, "High motion deviation", trace_high_deviation)
ani = Animation()
for (low, high) in zip(frames_low, frames_high)
frame_plot = plot(low, high; size=(1000,500), plot_title="Two synthetic data sets")
frame(ani, frame_plot)
end
gif(ani, "imgs/the_data.gif", fps=2)
# %% [markdown]
# Since we imagine these data as having been recorded from the real world, keep only their extracted data, *discarding* the traces that produced them.
# %%
# These are are what we hope to recover...
path_low_deviation = get_path(trace_low_deviation)
path_high_deviation = get_path(trace_high_deviation)
# ...using these data.
observations_low_deviation = get_sensors(trace_low_deviation)
observations_high_deviation = get_sensors(trace_high_deviation)
# Encode sensor readings into choice map.
constraint_from_sensors(t, readings) =
choicemap(( (prefix_address(t, :sensor => j => :distance), reading) for (j, reading) in enumerate(readings) )...)
constraints_low_deviation = [constraint_from_sensors(o...) for o in enumerate(observations_low_deviation)]
constraints_high_deviation = [constraint_from_sensors(o...) for o in enumerate(observations_high_deviation)]
merged_constraints_low_deviation = merge(constraints_low_deviation...)
merged_constraints_high_deviation = merge(constraints_high_deviation...);
# %% [markdown]
# We summarize the information available to the robot to determine its location. On the one hand, one has to produce a guess of the start pose plus some controls, which one might integrate to produce an idealized guess of path. On the other hand, one has the sensor data.
# %%
function plot_bare_sensors(world, title, readings, label, sensor_settings)
border = world.box_size * (3.)/19.
the_plot = plot(
size = (500, 500),
aspect_ratio = :equal,
grid = false,
xlim = (world.bounding_box[1]-border, world.bounding_box[2]+border),
ylim = (world.bounding_box[3]-border, world.bounding_box[4]+border),
title = title,
legend = :bottomleft)
plot_sensors!(Pose(world.center_point, 0.), :black, readings, label, sensor_settings)
return the_plot
end;