-
Notifications
You must be signed in to change notification settings - Fork 1
/
simulation.py
766 lines (598 loc) · 26.5 KB
/
simulation.py
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
# a simple agent using dynamical generative model of FEP
# that can control it's own temperature
# directly
import numpy as np
from matplotlib import pyplot as plt
from utils import running_mean
def get_noise(scale=0.1, positive=False):
""" Simulate noise with the mean at 0 and the standard deviation of 0.1 """
noise = np.random.normal() * scale
if positive:
noise = np.abs(noise)
return noise
class InteroceptiveAgent:
"""Interoceptive agent resembling homeostatic regulation """
def __init__(self, i_s_z_0=0.1, i_s_z_1=0.1, i_s_w_0=0.1, i_s_w_1=0.1,
action_bound=6, temp_const_change_initial=0,
learn_r_a=None,
temp_viable_range=10, dt=0.1, learn_r=0.1,
simulate_current=False):
# sigma (variances) of sensory noise (z) and model noise (w)
self.i_s_z_0 = i_s_z_0
self.i_s_z_1 = i_s_z_1
self.i_s_w_0 = i_s_w_0
self.i_s_w_1 = i_s_w_1
# learning rate
self.learn_r = learn_r
self.learn_r_a = learn_r_a if learn_r_a else learn_r
self.dt = dt
# goal temperature
self.T0 = 30
self.temp_viable_mean = 30
self.temp_viable_range = temp_viable_range
# action bound -- agent can't set action more or less than this
self.temp_change_action_bound = action_bound
self.temp_change_environment_initial = temp_const_change_initial
# if the current should be simulated
self.simulate_current = simulate_current
self.reset()
def reset(self):
# errors
self.i_e_z_0 = []
self.i_e_z_1 = []
self.i_e_w_0 = []
self.i_e_w_1 = []
# senses
self.sense_i = []
self.sense_i_d1 = []
# world
# >> temperature params
# start with temperature at T0
self.temp = [self.T0]
# overall change of temperature
self.temp_change = [0]
# change of agent's temperature, generated by the environment
self.temp_change_environment = [self.temp_change_environment_initial]
self.temp_change_instant_update = [0]
# change of temperature, generated by the action
self.temp_change_action = [0]
# >> light params
# overall light
self.light_change = [0]
# light change generated by the environment
self.light_change_instant = [0]
self.light_change_environment = [0]
# light change generated by the movement
self.light_change_movement = []
# sense of light
self.ex_sense = []
# >> movement params
self.velocity_action = [0]
self.velocity = [0]
# action (interoceptive)
# temperature change set by action
self.temp_relative_change = [0]
self.temp_desire = [self.temp_viable_mean]
# brain state mu
self.mu_i = [0]
self.mu_i_d1 = [0]
self.mu_i_d2 = [0]
# variational free energy
# on the interoceptive layer
self.vfe_i = []
# total
self.vfe = []
def generate_senses(self):
self.generate_sense()
self.generate_sense_d1()
def generate_sense(self):
sense = self.temp[-1] + get_noise()
self.sense_i.append(sense)
def generate_sense_d1(self):
""" change in sensation is felt change in temperature """
sense_i_d1 = self.temp_change[-1] + get_noise()
self.sense_i_d1.append(sense_i_d1)
def update_world(self):
""" update world parameters """
# an agent lives in the water world where
# a temperature changes at each timestep
# at some time this rate of change can be adjusted
if self.time == 50:
# jump to 2
new_temp_chage_desired = 3
elif self.time == 100:
# jump to 5
new_temp_chage_desired = 5
elif self.time == 150:
# jump to -1
new_temp_chage_desired = -1
elif self.time == 200:
# jump to -6
new_temp_chage_desired = -6.2
elif self.time == 250:
# jump to 0
new_temp_chage_desired = 0
else:
new_temp_chage_desired = self.temp_change_environment[-1]
temp_change_instant_update = new_temp_chage_desired - \
self.temp_change_environment[-1]
self.temp_change_instant_update.append(temp_change_instant_update)
def upd_velocity(self):
velocity = self.velocity_action[-1]
# simulate external force -- current after time step 300
if self.simulate_current and self.time > 300:
velocity += 0.1
self.velocity.append(velocity)
def upd_light_change(self):
""" Light change is composed of the light change
due to the environment and light change due to the movement of the
agent in the environment (set by its velocity) """
# light change due to the environment is updated by the
light_change_environment = self.light_change_environment[-1]
# instant change of light (Euler integrated)
light_change_environment += self.light_change_instant[-1] * self.dt
self.light_change_environment.append(light_change_environment)
# the light change due to velocity (Euler integrated)
# assuming 1:1 relationship
self.light_change_movement.append(self.velocity[-1])
# combined light change is light change due to environment
light_change = self.light_change_environment[-1]
# and light change due to the movement
light_change += self.light_change_movement[-1]
self.light_change.append(light_change)
def upd_temp_change(self):
""" Change in temperature or the organism is composed of
the change in temperature due to the environment and the change
of temperature, generated by the agent.
"""
# as agent moves though the world, the change of temperature
# is affected by instant changes in change in temperature
# and by movement of the agent
# by adding velocity effect (Euler integrated)
temp_change_environment = self.temp_change_environment[-1]
# first update by instant change in temperature change (Euler integrating)
temp_change_environment += self.temp_change_instant_update[-1]
# update movement effect by adding Euler integrated velocity
# assuming 1:1 relationship
temp_change_environment += self.velocity[-1] * self.dt
self.temp_change_environment.append(temp_change_environment)
# total temperature change is change due to the environment
# and change produced by the organism
# add noise
action = self.temp_change_action[-1] + get_noise() * self.dt
temp_change = self.temp_change_environment[-1] + action
self.temp_change.append(temp_change)
def upd_temp(self):
# update temperature with the current temperature update
upd = self.temp_change[-1]
# Euler integrating it
upd *= self.dt
self.temp.append(self.temp[-1] + upd)
def upd_err_z_0(self):
# error between sensation and generated sensations
self.i_e_z_0.append(self.sense_i[-1] - self.mu_i[-1])
def upd_err_z_1(self):
# error between first derivatives of sensation and generated sensations
self.i_e_z_1.append(self.sense_i_d1[-1] - self.mu_i_d1[-1])
def upd_err_w_0(self):
# error between model and generation of model
# here: model of dynamics at 1st derivative
# and it's generation for the 1st derivative
self.i_e_w_0.append(self.mu_i_d1[-1] + self.mu_i[-1] - self.temp_desire[-1])
def upd_err_w_1(self):
# error between model and generation of model
# here: model of dynamics at 2nd derivative)
# and it's generation for the 2nd derivative
self.i_e_w_1.append(self.mu_i_d2[-1] + self.mu_i_d1[-1])
def upd_mu_i_d2(self):
upd = -self.learn_r * (self.i_e_w_1[-1] / self.i_s_w_1)
upd *= self.dt
self.mu_i_d2.append(self.mu_i_d2[-1] + upd)
def upd_mu_i_d1(self):
upd = -self.learn_r * (-self.i_e_z_1[-1] / self.i_s_z_1 +
self.i_e_w_0[-1] / self.i_s_w_0 + self.i_e_w_1[-1] / self.i_s_w_1)
upd += self.mu_i_d2[-2]
upd *= self.dt
self.mu_i_d1.append(self.mu_i_d1[-1] + upd)
def upd_mu_i(self):
upd = -self.learn_r * \
(-self.i_e_z_0[-1] / self.i_s_z_0 + self.i_e_w_0[-1] / self.i_s_w_0)
upd += self.mu_i_d1[-2]
upd *= self.dt
self.mu_i.append(self.mu_i[-1] + upd)
def upd_vfe_i(self):
def sqrd_err(err, sigma):
return np.power(err, 2) / sigma
vfe_i = 0.5 * (sqrd_err(self.i_e_z_0[-1], self.i_s_z_0) +
sqrd_err(self.i_e_z_1[-1], self.i_s_z_1) +
sqrd_err(self.i_e_w_0[-1], self.i_s_w_0) +
sqrd_err(self.i_e_w_1[-1], self.i_s_w_1))
self.vfe_i.append(vfe_i)
def upd_action(self):
# TODO action is noisy! Add noise here but not forget about integration
# sensation change over action is always 1
upd = -self.learn_r_a * 1 * (self.i_e_z_1[-1] / self.i_s_z_1)
upd *= self.dt
action = self.temp_change_action[-1] + upd
# action must be bound by some plausible constraints
# e.g. temperature can't change more than action bound at each timestep
if abs(action) > self.temp_change_action_bound:
action = np.sign(action) * self.temp_change_action_bound
# update action
self.temp_change_action.append(action)
def upd_no_action(self):
# if agent is not acting update it's variables anyway
self.temp_change_action.append(self.temp_change_action[-1])
def interoception(self):
# update interoception
# --> update errors
self.upd_err_z_0()
self.upd_err_z_1()
self.upd_err_w_0()
self.upd_err_w_1()
# --> update recognition dynamics
self.upd_mu_i_d2()
self.upd_mu_i_d1()
self.upd_mu_i()
# update free energy
self.upd_vfe_i()
def active_inference(self):
self.temp_desire.append(self.temp_viable_mean)
self.interoception()
def upd_vfe(self):
self.vfe.append(self.vfe_i[-1])
def plot_results(self):
fig, ax = plt.subplots(3, 2, constrained_layout=True)
timeline = [s * self.dt for s in range(self.steps)]
# Temperature
min_temp = self.temp_viable_mean - self.temp_viable_range
max_temp = self.temp_viable_mean + self.temp_viable_range
ax[0][0].plot(timeline, self.temp[1:])
ax[0][0].plot(timeline, self.temp_desire[1:], ls='--', lw=0.75, c='green')
ax[0][0].set_title('Temperature')
ax[0][0].set_xlabel('time step')
ax[0][0].set_ylabel('temperature')
ax[0][0].plot(timeline, np.ones_like(timeline) *
min_temp, lw=0.75, ls='--', c='red')
ax[0][0].plot(timeline, np.ones_like(timeline) *
max_temp, lw=0.75, ls='--', c='red')
ax[0][0].legend(['agent, $T$', 'goal, $T_{goal}$', 'viability'],
loc='lower right')
ax[0][0].set_ylim(-10, 50)
ax[0][0].set_xlim(-10, self.time + 30)
# Temperature change
temp_change_organism = np.array(self.temp_change_environment[1:]) + \
np.array(self.temp_change_action[1:])
ax[1][0].plot(timeline, self.temp_change_environment[1:])
ax[1][0].plot(timeline, self.temp_change_action[1:])
ax[1][0].plot(timeline, temp_change_organism, lw=1.25, ls='--', c='g')
ax[1][0].plot(timeline, np.ones_like(timeline) * 0, lw=0.65, c='gray')
ax[1][0].legend(['env-t, $\\dot{T}_e$', 'action, $\\dot{T}_i$',
'agent, $\\dot{T}$'],
loc='lower right',
# loc='center right',
labelspacing=0.3)
ax[1][0].set_title('Temperature change')
ax[1][0].set_xlabel('time step')
ax[1][0].set_ylabel('temperature change')
ax[1][0].set_ylim(-8, 8)
ax[1][0].set_xlim(-10, self.time + 30)
# Light change
ax[2][0].plot(timeline, running_mean(self.light_change[1:]), lw=2)
ax[2][0].set_title('Light change')
ax[2][0].set_xlabel('time step')
ax[2][0].set_ylabel('light change')
ax[2][0].legend(['total, $\\dot{L}$'], loc='lower right')
ax[2][0].set_xlim(-10, self.time + 30)
# mu
ax[0][1].plot(timeline, self.mu_i[1:])
ax[0][1].plot(timeline, self.mu_i_d1[1:])
ax[0][1].plot(timeline, self.mu_i_d2[1:])
ax[0][1].set_title('Environmental variable, $\\mu$')
ax[0][1].set_xlabel('time step')
ax[0][1].set_ylabel('$\\mu$')
ax[0][1].legend(['$\\mu_i$', "$\\mu_i'$", "$\\mu_i''$"], loc='upper right')
ax[0][1].set_ylim(-10, 50)
# VFE
ax[1][1].plot(timeline, running_mean(self.vfe), lw=3, c="#3f92d2")
ax[1][1].set_title('Variational free energy (VFE), $F$')
ax[1][1].set_xlabel('time step')
ax[1][1].set_ylabel('$F$')
ax[1][1].set_ylim(-5, 500)
# Error terms
ax[2][1].plot(timeline, running_mean(self.i_e_z_0), lw=0.75)
ax[2][1].plot(timeline, running_mean(self.i_e_z_1), lw=0.75)
ax[2][1].plot(timeline, running_mean(self.i_e_w_0), lw=0.75)
ax[2][1].plot(timeline, running_mean(self.i_e_w_1), lw=0.75)
ax[2][1].set_ylim(-10, 10)
ax[2][1].set_title('Error terms, $\\epsilon$')
ax[2][1].set_xlabel('time step')
ax[2][1].set_ylabel('$\\epsilon$')
ax[2][1].legend(['$\\epsilon_{z0}$', '$\\epsilon_{z1}$', '$\\epsilon_{w0}$',
'$\\epsilon_{w1}$'], loc='upper right')
return ax
def simulate(self, sim_time=300, act_time=50):
self.reset()
self.act_time = act_time
plt.ion()
self.steps = int(sim_time / self.dt)
print(f'Simulating {self.steps} steps')
for step in range(self.steps):
self.time = step * self.dt
# update world
self.update_world()
self.upd_velocity()
self.upd_temp_change()
self.upd_temp()
self.upd_light_change()
# generate sensations
self.generate_senses()
# perform active inference
self.active_inference()
# update variational free energy
self.upd_vfe()
# act
if step * self.dt > act_time:
self.upd_action()
else:
self.upd_no_action()
self.plot_results()
plt.show()
class ExteroceptiveAgent(InteroceptiveAgent):
"""Exteroceptive agent that infers the desired temperature
and passes it down to the interoceptive layer"""
def __init__(self, ex_s_z_0=0.01, simulate_current=True, **kwargs):
# simulate current by default
super().__init__(simulate_current=simulate_current, **kwargs)
self.learn_r_ex = self.learn_r
# sigma (variances)
self.ex_s_z_0 = ex_s_z_0
def reset(self):
super().reset()
# exteroceptive errors
self.ex_e_z_0 = []
self.ex_e_w_0 = []
# brain state mu of the exteroception layer
self.ex_mu = [0]
# variational free energy of the exteroception layer
self.vfe_ex = []
def generate_senses(self):
super().generate_senses()
self.generate_ex_sense()
def generate_ex_sense(self):
ex_sense = self.light_change[-1] + get_noise()
self.ex_sense.append(ex_sense)
def upd_ex_err_z_0(self):
# error between sensation and generated sensations
self.ex_e_z_0.append(self.ex_sense[-1] - 0.1 * (-self.ex_mu[-1] + 30))
def upd_ex_mu(self):
upd = -self.learn_r_ex * \
(0.1 * self.ex_e_z_0[-1] / self.ex_s_z_0)
upd *= self.dt
self.ex_mu.append(self.ex_mu[-1] + upd)
def upd_vfe_ex(self):
def sqrd_err(err, sigma):
return np.power(err, 2) / sigma
vfe_ex = 0.5 * (sqrd_err(self.ex_e_z_0[-1], self.ex_s_z_0))
self.vfe_ex.append(vfe_ex)
def upd_vfe(self):
vfe = self.vfe_i[-1] + self.vfe_ex[-1]
self.vfe.append(vfe)
def exteroception(self):
# the agent performs exteroception
# that updates the desired temperature
# setting new set point for the underlying
# interoceptive inference
# --> update errors
self.upd_ex_err_z_0()
# --> update recognition dynamics
self.upd_ex_mu()
# update free energy
self.upd_vfe_ex()
def update_world(self):
""" simulate light drop before temperature drop
for a short period of time to show the proof of concept """
# update world as in Interoceptive Agent
super().update_world()
# change in light starts before the temperature drop
if int(self.time) == 175:
light_change_instant = -0.7
elif int(self.time) == 200:
light_change_instant = 0.7
else:
light_change_instant = 0
self.light_change_instant.append(light_change_instant)
def active_inference(self):
# exteroception first
self.exteroception()
# pass the prior about desired temperature to the interoception
# generative model
self.temp_desire.append(self.ex_mu[-1])
# then perform interoception
self.interoception()
def simulate(self, sim_time=400, act_time=50):
# simulate for 400 time steps by default
super().simulate(sim_time=sim_time)
def plot_results(self):
ax = super().plot_results()
timeline = [s * self.dt for s in range(self.steps)]
ax[0][1].plot(timeline, self.ex_mu[1:], ls='--')
ax[0][1].legend(['$\\mu_i$', "$\\mu_i'$", "$\\mu_i''$",
"$\\mu_e$"], loc='upper right')
ax[1][1].plot(timeline, running_mean(self.vfe_i, 5), lw=1, c='tab:red')
ax[1][1].plot(timeline, running_mean(self.vfe_ex), lw=1, c='tab:pink')
ax[1][1].legend(['$F$', '$F_i$', '$F_e$'], loc='upper right')
ax[1][1].set_ylim(-10, 200)
ax[2][1].plot(timeline, running_mean(self.ex_e_z_0), lw=0.75)
ax[2][1].legend(['$\\epsilon^{z0}_i$', '$\\epsilon^{z1}_i$', '$\\epsilon^{w0}_i$',
'$\\epsilon^{w1}_i$', '$\\epsilon^{z0}_e$'], loc='upper right')
ax[2][1].set_ylim(-5, 5)
return ax
class ActiveExteroceptiveAgent(ExteroceptiveAgent):
"""An agent now lives in the world where it can act
In this world an agent can move up and down.
Where moving up increases the amount of light
and, therefore, the temperature,
while moving down has the contrary effect.
The generative model of interoception is re-used for
exteroceptive active inference.
As a result, an agent can both regulate it's temperature
interoceptive and exteroceptively. At the same time,
the generative dynamic, specifying a settling point resides on a level
above. On a next level of the hierarchy the desired temperature is inferred."""
def __init__(self, aex_s_z_0=0.1, aex_s_z_1=0.1, aex_s_w_0=0.1, aex_s_w_1=0.1,
aex_action_bound=0.2, supress_action=False, learn_r_aex=None,
supress_desired_temp_inference=False,
simulate_current=True,
**kwargs):
super().__init__(simulate_current=simulate_current, **kwargs)
self.learn_r_aex = self.learn_r if learn_r_aex is None else learn_r_aex
# sigma (variances)
self.aex_s_z_0 = aex_s_z_0
self.aex_s_w_0 = aex_s_w_0
# action bound of the action on the world
self.aex_action_bound = aex_action_bound
self.supress_action = supress_action
self.supress_desired_temp_inference = supress_desired_temp_inference
def reset(self):
super().reset()
# active exteroceptive errors
self.aex_e_z_0 = [0]
self.aex_e_w_0 = [0]
# mu of the exteroceptive layer -- inferred change in light
# produced by the agent
self.aex_mu = [0]
self.aex_mu_d1 = [0]
# variational free energy of the active exteroception layer
self.vfe_aex = []
# action
self.aex_action = [0]
self.aex_action_pre_bound = [0]
def update_world(self):
super().update_world()
if self.time < self.act_time:
self.velocity_action.append(0)
return
if not self.supress_action:
# extra_noise = get_noise() if np.random.rand() < 0.2 else 0
noise = get_noise() if self.time < 300 else 0
self.velocity_action.append(self.aex_action[-1] + noise)
def upd_aex_mu_d1(self):
upd = -self.learn_r_ex * (self.aex_e_w_0[-1] / self.aex_s_w_0)
upd *= self.dt
self.aex_mu_d1.append(self.aex_mu_d1[-1] + upd)
def upd_aex_mu(self):
upd = -self.learn_r_ex * \
(+self.aex_e_z_0[-1] / self.aex_s_z_0 +
+self.aex_e_w_0[-1] / self.aex_s_w_0
)
upd += self.aex_mu_d1[-1]
upd *= self.dt
self.aex_mu.append(self.aex_mu[-1] + upd)
def upd_ex_err_z_0(self):
# update of the error calculation at the exteroception layer
# now we subtract the (prediction) of how much light change
# is generated by the agent acting on the world
prediction = self.ex_sense[-1] - self.aex_action[-1]
self.ex_e_z_0.append(prediction - 0.1 * (-self.ex_mu[-1] + 30))
def upd_aex_err_w_0(self):
""" agent's expected dynamics is that environment temperature change
needs to be explained
set point is when the inferred change in light
corresponds to the change in temperature """
t_change_goal = self.sense_i_d1[-1] - self.temp_change_action[-1]
self.aex_e_w_0.append(self.aex_mu_d1[-1] - t_change_goal + self.aex_mu[-1])
def upd_aex_err_z_0(self):
""" sensory error is the difference between the best guess
about the change in light and the agent's prediction about it
(through the copy of action)
"""
error = self.aex_action[-1]
self.aex_e_z_0.append(error - (-self.aex_mu[-1]))
def upd_avfe_ex(self):
""" active exteroception VFE """
def sqrd_err(err, sigma):
return np.power(err, 2) / sigma
vfe_aex = 0.5 * (sqrd_err(self.aex_e_z_0[-1], self.aex_s_z_0)
+ sqrd_err(self.aex_e_w_0[-1], self.aex_s_w_0))
self.vfe_aex.append(vfe_aex)
def upd_vfe(self):
""" total VFE """
vfe = self.vfe_i[-1] + self.vfe_ex[-1] + self.vfe_aex[-1]
self.vfe.append(vfe)
def upd_action(self):
super().upd_action()
# sensation change over action is always 1
upd = -self.learn_r_aex * 1 * (self.aex_e_z_0[-1] / self.aex_s_z_0)
upd *= self.dt
aex_action = self.aex_action[-1] + upd
aex_action += get_noise() * self.dt
self.aex_action_pre_bound.append(aex_action)
# action must be bound by some plausible constraints
# e.g. can't move faster than some limit
if abs(aex_action) > self.aex_action_bound:
aex_action = np.sign(aex_action) * self.aex_action_bound
# update action
self.aex_action.append(aex_action)
def upd_no_action(self):
super().upd_no_action()
self.aex_action.append(self.aex_action[-1])
self.aex_action_pre_bound.append(self.aex_action_pre_bound[-1])
def active_exteroception(self):
""" an agents performs action
based on it's exteroceptive inference
about how temperature change causes light change """
self.upd_aex_mu_d1()
self.upd_aex_mu()
# update errors
self.upd_aex_err_z_0()
self.upd_aex_err_w_0()
# update free energy
self.upd_avfe_ex()
def active_inference(self):
# exteroception first
self.exteroception()
# pass the prior about desired temperature to the generative model
if not self.supress_desired_temp_inference:
self.temp_desire.append(self.ex_mu[-1])
else:
self.temp_desire.append(self.temp_viable_mean)
# then perform interoception
self.interoception()
# and active exteroception
self.active_exteroception()
def plot_results(self):
ax = super().plot_results()
timeline = [s * self.dt for s in range(self.steps)]
# change in light
ax[0][1].plot(timeline, self.aex_mu[1:], ls='--')
ax[0][1].legend(['$\\mu_i$', "$\\mu_i'$", "$\\mu_i''$",
"$\\mu_e$", '$\\mu_a$'], loc='upper right')
ax[2][1].plot(timeline, running_mean(self.aex_e_z_0[1:]), lw=0.75)
ax[2][1].plot(timeline, running_mean(self.aex_e_w_0[1:]), lw=0.75)
ax[2][1].legend(['$\\epsilon^{z0}_i$', '$\\epsilon^{z1}_i$', '$\\epsilon^{w0}_i$',
'$\\epsilon^{w1}_i$', '$\\epsilon^{z0}_e$',
'$\\epsilon^{z0}_a$', '$\\epsilon^{w0}_a$'],
loc='upper right',
labelspacing=0)
ax[2][1].set_ylim(-3.5, 3.5)
# change in light produced by the external force
# is 0.1 after the time step 300
light_change_external = np.zeros(self.steps + 1)
light_change_external[3000:] = 0.1
# environmental change in light (e.g. modelled sunset/sunrise)
# is all change in light minus change in light generated by the agent's action
# and generated by the external force dragging agent closer to surface
light_change_env = np.array(self.light_change) - np.array(self.velocity_action) \
- light_change_external
# action produced by the organism (including environmental noise)
ax[2][0].plot(timeline, running_mean(self.velocity_action[1:]), lw=0.75)
ax[2][0].plot(timeline, light_change_env[1:], lw=1, c='tab:red', ls='--')
ax[2][0].plot(timeline[3000:], light_change_external[3001:], lw=1, c='tab:green')
ax[2][0].legend(['total, $\\dot{L}$', 'action, $\\dot{L}_a$',
'env-t, $\\dot{L}_e$', 'external'],
loc='lower right')
ax[1][1].plot(timeline, running_mean(self.vfe_aex), lw=1, c="tab:orange")
ax[1][1].legend(['$F$', '$F_i$', '$F_e$', '$F_a$'])
ax[1][1].set_ylim(-5, 175)
plt.show()