def update_rangefinder_sensors(self):
        """
        The function to update the agent range finder sensors.
        """
        for i, angle in enumerate(self.agent.range_finder_angles):
            rad = geometry.deg_to_rad(angle)
            # project a point from agent location outwards
            projection_point = geometry.Point(
                x = self.agent.location.x + math.cos(rad) * self.agent.range_finder_range,
                y = self.agent.location.y + math.sin(rad) * self.agent.range_finder_range
            )
            # rotate the projection point by the agent's heading angle to
            # align it with heading direction
            projection_point.rotate(self.agent.heading, self.agent.location)
            # create the line segment from the agent location to the projected point
            projection_line = geometry.Line(
                a = self.agent.location,
                b = projection_point
            )
            # set range to maximum detection range
            min_range = self.agent.range_finder_range

            # now test against maze walls to see if projection line hits any wall
            # and find the closest hit
            for wall in self.walls:
                found, intersection = wall.intersection(projection_line)
                if found:
                    found_range = intersection.distance(self.agent.location)
                    # we are interested in the closest hit
                    if found_range < min_range:
                        min_range = found_range

            # Update sensor value
            self.agent.range_finders[i] = min_range
def maze_simulation_evaluate(env, net, time_steps, mcns=0.0, n_item=None, path_points=None):
    """
    The function to evaluate maze simulation for specific environment
    and controll ANN provided. The results will be saved into provided
    agent record holder.
    Arguments:
        env:            The maze configuration environment.
        net:            The maze solver agent's control ANN.
        time_steps:     The number of time steps for maze simulation.
        mcns:           The minimal criteria fitness value.
        n_item:         The NoveltyItem to store evaluation results.
        path_points:    The holder for path points collected during simulation. If
                        provided None then nothing will be collected.
    Returns:
        The goal-oriented fitness value, i.e., how close is agent to the exit at
        the end of simulation.
    """
    exit_found = False
    for i in range(time_steps):
        if maze_simulation_step(env, net):
            print("Maze solved in %d steps" % (i + 1))
            exit_found = True
            break

        if path_points is not None:
            # collect current position
            path_points.append(geometry.Point(env.agent.location.x, env.agent.location.y))

        # store agent path points at a given sample size rate
        if (time_steps - i) % env.location_sample_rate == 0 and n_item is not None:
            n_item.data.append(env.agent.location.x)
            n_item.data.append(env.agent.location.y)

    # store final agent coordinates as genome's novelty characteristics
    if n_item is not None:
        n_item.data.append(env.agent.location.x)
        n_item.data.append(env.agent.location.y) 

    # Calculate the fitness score based on distance from exit
    fitness = 0.0
    if exit_found:
        fitness = 1.0
    else:
        # Normalize distance to range (0,1]
        distance = env.agent_distance_to_exit()
        fitness = (env.initial_distance - distance) / env.initial_distance
        if fitness <= 0:
            fitness = 0.01

    # Use minimal criteria fitness value to signal if genome should be included into population
    if fitness < mcns:
        fitness = -1 # mark genome to be excluded

    if n_item is not None:
        n_item.fitness = fitness

    return fitness
    def update(self, control_signals):
        """
        The function to update solver agent position within maze. After agent position
        updated it will be checked to find out if maze exit was reached afetr that.
        Arguments:
            control_signals: The control signals received from the control ANN
        Returns:
            The True if maze exit was found after update or maze exit was already
            found in previous simulation cycles.
        """
        if self.exit_found:
            # Maze exit already found
            return True

        # Apply control signals
        self.apply_control_signals(control_signals)

        # get X and Y velocity components
        vx = math.cos(geometry.deg_to_rad(self.agent.heading)) * self.agent.speed
        vy = math.sin(geometry.deg_to_rad(self.agent.heading)) * self.agent.speed

        # Update current Agent's heading (we consider the simulation time step size equal to 1s
        # and the angular velocity as degrees per second)
        self.agent.heading += self.agent.angular_vel

        # Enforce angular velocity bounds by wrapping
        if self.agent.heading > 360:
            self.agent.heading -= 360
        elif self.agent.heading < 0:
            self.agent.heading += 360

        # find the next location of the agent
        new_loc = geometry.Point(
            x = self.agent.location.x + vx, 
            y = self.agent.location.y + vy
        )

        if not self.test_wall_collision(new_loc):
            self.agent.location = new_loc

        # update agent's sensors
        self.update_rangefinder_sensors()
        self.update_radars()

        # check if agent reached exit point
        distance = self.agent_distance_to_exit()
        self.exit_found = (distance < self.exit_range)
        return self.exit_found
    def update_radars(self):
        """
        The function to update the agent radar sensors.
        """
        target = geometry.Point(self.exit_point.x, self.exit_point.y)
        # rotate target with respect to the agent's heading to align it with heading direction
        target.rotate(self.agent.heading, self.agent.location)
        # translate with respect to the agent's location
        target.x -= self.agent.location.x
        target.y -= self.agent.location.y
        # the angle between maze exit point and the agent's heading direction
        angle = target.angle()
        # find the appropriate radar sensor to be fired
        for i, r_angles in enumerate(self.agent.radar_angles):
            self.agent.radar[i] = 0.0 # reset specific radar 

            if (angle >= r_angles[0] and angle < r_angles[1]) or (angle + 360 >= r_angles[0] and angle + 360 < r_angles[1]):
                self.agent.radar[i] = 1.0 # fire the radar
def draw_maze_records(maze_env,
                      records,
                      best_threshold=0.8,
                      filename=None,
                      view=False,
                      show_axes=False,
                      width=400,
                      height=400,
                      fig_height=7):
    """
    The function to draw maze with recorded agents positions.
    Arguments:
        maze_env:       The maze environment configuration.
        records:        The records of solver agents collected during NEAT execution.
        best_threshold: The minimal fitness of maze solving agent's species to be included into the best ones.
        filename:       The name of file to store plot.
        view:           The flag to indicate whether to view plot.
        width:          The width of drawing in pixels
        height:         The height of drawing in pixels
        fig_height:      The plot figure height in inches
    """
    # find the distance threshold for the best species
    dist_threshold = maze_env.agent_distance_to_exit() * (1.0 - best_threshold)
    # generate color palette and find the best species IDS
    max_sid = 0
    for r in records:
        if r.species_id > max_sid:
            max_sid = r.species_id
    colors = [None] * (max_sid + 1)
    sp_idx = [False] * (max_sid + 1)
    best_sp_idx = [0] * (max_sid + 1)
    for r in records:
        if not sp_idx[r.species_id]:
            sp_idx[r.species_id] = True
            rgb = (random.random(), random.random(), random.random())
            colors[r.species_id] = rgb
        if maze_env.exit_point.distance(geometry.Point(r.x,
                                                       r.y)) <= dist_threshold:
            best_sp_idx[r.species_id] += 1

    # initialize plotting
    fig = plt.figure()
    fig.set_dpi(100)
    fig_width = fig_height * (float(width) / float(2.0 * height)) - 0.2
    print("Plot figure width: %.1f, height: %.1f" % (fig_width, fig_height))
    fig.set_size_inches(fig_width, fig_height)
    ax1, ax2 = fig.subplots(2, 1, sharex=True)
    ax1.set_xlim(0, width)
    ax1.set_ylim(0, height)
    ax2.set_xlim(0, width)
    ax2.set_ylim(0, height)

    # draw species
    n_best_species = 0
    for i, v in enumerate(best_sp_idx):
        if v > 0:
            n_best_species += 1
            _draw_species_(records=records, sid=i, colors=colors, ax=ax1)
        else:
            _draw_species_(records=records, sid=i, colors=colors, ax=ax2)

    ax1.set_title('fitness >= %.1f, species: %d' %
                  (best_threshold, n_best_species))
    ax2.set_title('fitness < %.1f' % best_threshold)

    # draw maze
    _draw_maze_(maze_env, ax1)
    _draw_maze_(maze_env, ax2)

    # turn off axis rendering
    if not show_axes:
        ax1.axis('off')
        ax2.axis('off')
    # Invert Y axis to have coordinates origin at the top left
    ax1.invert_yaxis()
    ax2.invert_yaxis()

    # Save figure to file
    if filename is not None:
        plt.savefig(filename)

    if view:
        plt.show()

    plt.close()