class Simulation: def __init__(self, animate=True, reactivity_ratio=15, iterations=5000, distributions=10): self.animate = animate # The processor contains the guessed Concern Zone and the logic # that determines whether to attack or not: self.processor = Processor(reactivity_ratio=reactivity_ratio) # The reaper contains the payoff matrix that determines how likely # the agent is to survive when it attacks or ignores a given input: self.reaper = Reaper() # Noise is added to "actual" input probabilities to compute # how likely the agent "thinks" the threat is: self.noise = Noise() # The distribution represents the threat level of the environment, # i.e. the distribution of threat likelihoods the agent encounters: self.distribution = Distribution() # How many frames are left in the animation: self.iterations_remaining = iterations # How many frames until the environment changes: self.iterations_per_distribution = round( float(iterations) / distributions) self.last_input = None if self.animate: # Credit for animation tutorial: # jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/ self.figure = plt.figure() ax = plt.axes(xlim=(0, 1), ylim=(0, Distribution.max_y)) ax.xaxis.set_major_formatter(ticker.PercentFormatter(1.0)) ax.set_yticks([]) plt.xlabel("Likelihood of being safe") plt.ylabel("") #("Frequency of encountering") # The actual Paranoia Line: plt.axvline(x=self._actual_paranoia_line(), linewidth=4, color='k', alpha=0.2, zorder=1) # The line showing the distribution of threats ("the environment"): self.dist_line, = ax.plot([], [], color='k', zorder=2) # A triangle showing the actual threat likelihood of the input: self.input_tri = ax.scatter([], [], color='k', marker='v', zorder=5) # A triangle showing the threat likelihood the robot perceives, # computed by taking the input and adding noise: self.after_noise_tri = ax.scatter([], [], color='w', marker='^', zorder=5) # A visual representation of the Concern Zone: self.attack_polygon = patches.Polygon( [[0, 0], [0, 0], [0, 0], [0, 0]], alpha=0.5, color='r', edgecolor=None, zorder=3) # A visual representation of the non-Concern Zone: self.chill_polygon = patches.Polygon( [[0, 0], [0, 0], [0, 0], [0, 0]], alpha=0.5, color='b', edgecolor=None, zorder=3) ax.add_patch(self.attack_polygon) ax.add_patch(self.chill_polygon) # If you want to track the imputed average Paranoia Line guessed # so far, change the alpha of this to > 0: self.avg_guess_line, = ax.plot([], [], '--', color='k', alpha=0.0, zorder=4) def _actual_paranoia_line(self): # The actual Paranoia Line is the threat likelihood where the odds # of surviving are the same whether attacking or not. This formula # is a more general version of the one in this footnote: # https://www.adamjuliangoldstein.com/blog/paranoia-parameter/#fn3 r = self.reaper n = r.true_positive_survival_odds - r.false_negative_survival_odds d = n + r.true_negative_survival_odds - r.false_positive_survival_odds return n / d def prep_animation(self): # Set up each of the visual elements self.dist_line.set_data([], []) self.input_tri.set_offsets([0, 0]) self.after_noise_tri.set_offsets([0, 0]) self.attack_polygon.set_xy([[0, 0], [0, 0], [0, 0], [0, 0]]) self.chill_polygon.set_xy([[0, 0], [0, 0], [0, 0], [0, 0]]) self.avg_guess_line.set_data( [self.processor.mean_c_guess(), self.processor.mean_c_guess()], [0, Distribution.max_y]) return (self.dist_line, self.input_tri, self.after_noise_tri, self.attack_polygon, self.chill_polygon, self.avg_guess_line) def start(self): if self.animate: anim = animation.FuncAnimation(self.figure, self.advance, init_func=self.prep_animation, frames=self.iterations_remaining, interval=1, repeat=False, blit=True) # Un-comment these lines to export an animated gif of the # animation instead of showing it on the screen: # writer = animation.PillowWriter(fps = 2) # anim.save('output.gif', writer = writer) # raise plt.show() else: while self.iterations_remaining > 1: self.advance(None) def advance(self, i): if self.iterations_remaining == 1: self.end() # If it's time for a new distribution ("the environment has changed"): if self.iterations_remaining % self.iterations_per_distribution == 0: # Get a new random distribution: self.distribution = Distribution() if self.animate: X = np.linspace(0, 1, num=2) Y = self.distribution.pdf()(X) self.dist_line.set_data(X, Y) # On odd frames of the animation, update the input and perceived input: if self.iterations_remaining % 2 == 1: self.last_input = self.distribution.generate_likelihood() self.after_noise = self.noise.adjust(self.last_input) if self.animate: self.input_tri.set_offsets([self.last_input, 0.1]) self.after_noise_tri.set_offsets([self.after_noise, 0.05]) # On even frames of the animation, determine how the agent reacts to # the perceived input, and update the Concern Zone if appropriate: elif self.last_input: # The chance it's a threat is given by the input # (actual threat level): is_a_threat = self.reaper.is_threat(self.last_input) # Whether to attack is given by whether the perceived risk is # greater or less than the currently-guessed Paranoia Line: does_attack = self.processor.does_attack(self.after_noise) # The odds of survival are determined by the reaper: does_survive = self.reaper.does_survive(is_a_threat, does_attack) # Adjust the Concern Zone if needed: if does_survive: self.processor.survives(does_attack) else: self.processor.dies(does_attack) if self.animate: # Draw the Concern Zone from x = 0 to x = the implied guessed # Paranoia Line. The height of the Concern Zone is given by the # probability distribution of threats in the environment: p_guess = self.distribution.c_to_p(self.processor.c_guess) pdf = self.distribution.pdf() self.attack_polygon.set_xy([[0, 0], [0, pdf(0)], [p_guess, pdf(p_guess)], [p_guess, 0]]) self.chill_polygon.set_xy([[p_guess, pdf(p_guess)], [p_guess, 0], [1, 0], [1, pdf(1)]]) mean_c_guess = self.processor.mean_c_guess() avg_p_guess = self.distribution.c_to_p(mean_c_guess) self.avg_guess_line.set_data([avg_p_guess, avg_p_guess], [0, pdf(avg_p_guess)]) self.iterations_remaining -= 1 if self.animate: return (self.dist_line, self.input_tri, self.after_noise_tri, self.attack_polygon, self.chill_polygon, self.avg_guess_line) def end(self): plt.close(self.figure) print("With a Reactivity Ratio of", self.processor.reactivity_ratio, "the agent had survival rate", self.processor.survival_rate(), "and atttack rate", self.processor.attack_rate()) # Things that would be nice: # TODO: Label X and Y axes # TODO: Fix even/odd alternation of animation being hard-coded into simulation instead of handled by animation # TODO: Add internal links and installation instructions to readme # TODO: Make it a directory/module structure with import dependencies # TODO: Change animation.py to the file that handles animation and make a different main file # TODO: Measure impact of noise on how reactive you have to be # TODO: Make setting for saving anim vs showing it # TODO: Display the reactivity ratio in the animation, not just in the terminal # TODO: Make more things configurable from the command line