class ManualController: """ The robots are moved following a “distributed controller”, that is a simple proportional controller PID, with P fixed to -0.01, whose goal is to align the robots by minimizing the difference between the values recorded by the front and rear sensors. :param name: name of the controller used (in this case manual) :param goal: task to perform (in this case distribute) :param N: number of agents in the simulation :param net_input: input of the network (between: prox_values, prox_comm and all_sensors) :param kwargs: other arguments :var p_distributed_controller: a simple proportional controller that returns the speed to apply """ def __init__(self, name, goal, N, net_input, **kwargs): super().__init__(**kwargs) self.name = name self.goal = goal self.N = N self.p_distributed_controller = PID(5, 0, 0, max_out=16.6, min_out=-16.6) self.net_input = net_input def neighbors_distance(self, state): """ Check if there is a robot ahead using the infrared sensor 2 (front-front) [2 and 9 in case of all sensors]. Check if there is a robot ahead using the infrared sensor 5 (back-left) and 6 (back-right) [5, 6 and 12, 13 in case of all sensors]. The distance from the neighbors is computed using a mapping function obtained experimentally that converts the intensities values contained in the sensor readings into distances. .. note:: The response values are actually intensities: the front correspond to the frontal center sensor and the back to the mean of the response values of the rear sensors. :param state: object containing the agent information :return back, front: back distance and front distance of the thymio from the others """ sensing = utils.get_input_sensing(self.net_input, state, normalise=False) if sensing == [0] * len(sensing): return 0, 0 file = os.path.join('source/controllers', 'sensing_to_distances.pkl') distances, front_prox_values, back_prox_values, front_prox_comm, back_prox_comm = np.load( file, allow_pickle=True) f_pv_indices = np.nonzero(front_prox_values)[0] f_pv_indices = f_pv_indices[np.argsort( front_prox_values[f_pv_indices])] b_pv_indices = np.nonzero(back_prox_values)[0] b_pv_indices = b_pv_indices[np.argsort(back_prox_values[b_pv_indices])] f_pc_indices = np.nonzero(front_prox_comm)[0] f_pc_indices = f_pc_indices[np.argsort(front_prox_comm[f_pc_indices])] b_pc_indices = np.nonzero(back_prox_comm)[0] b_pc_indices = b_pc_indices[np.argsort(back_prox_comm[b_pc_indices])] front_distance_pv = np.interp(sensing[2], front_prox_values[f_pv_indices], distances[f_pv_indices]) back_distance_pv = np.interp( np.mean(np.array([sensing[5], sensing[6]])), back_prox_values[b_pv_indices], distances[b_pv_indices]) if sensing[7:] == [0] * 7 or self.net_input != 'all_sensors': return back_distance_pv, front_distance_pv elif sensing[0:7] == [0] * 7: front_distance_pc = np.interp(sensing[9], front_prox_comm[f_pc_indices], distances[f_pc_indices]) back_distance_pc = np.interp( np.mean(np.array([sensing[12], sensing[13]])), back_prox_comm[b_pc_indices], distances[b_pc_indices]) return back_distance_pc, front_distance_pc elif self.net_input == 'all_sensors': front_distance_pc = np.interp(sensing[9], front_prox_comm[f_pc_indices], distances[f_pc_indices]) back_distance_pc = np.interp( np.mean(np.array([sensing[12], sensing[13]])), back_prox_comm[b_pc_indices], distances[b_pc_indices]) if sensing[2] == 0.0: front = front_distance_pc else: front = front_distance_pv if np.mean([sensing[5], sensing[6]]) == 0.0: back = back_distance_pc elif np.mean([sensing[12], sensing[13]]) == 0.0: back = back_distance_pv else: back = np.mean([back_distance_pv, back_distance_pc]) return back, front else: raise ValueError('Impossible values for sensing.') def compute_difference(self, state): """ .. note:: Apply a small correction to the distance measured by the rear sensors since the front sensor used is at a different x coordinate from the point to which the rear sensor of the robot that follows points. This is because of the curved shape of the face of the Thymio. The final difference is computed as follow: .. math:: out = front - (back - correction) :param state: object containing the agent information :return out: the difference between the front and the rear distances """ back, front = self.neighbors_distance(state) if back == 0 and front == 0: return 0 # compute the correction delta_x = 7.41150769 x = 7.95 # Maximum possible response values # delta_x_m = 4505 * delta_x / 14 # x_m = 4505 * x / 14 # correction = x_m - delta_x_m correction = x - delta_x out = front - (back - correction) return out def perform_control(self, state, dt): """ Keeping still the robots at the ends of the line, moves the others using the distributed controller, setting the ``target {left, right} wheel speed`` each at the same value in order to moves the robot straight ahead. This distributed controller is a simple proportional controller ``PID(5, 0, 0, max_out=16.6, min_out=-16.6)`` that takes in input the difference between the front and back distances measured by the agent. :param state: object containing the agent information :param dt: control step duration :return speed, communication: the velocity and the message to communicate """ # Don't move the first and last robots in the line # if state.index == 0 or state.index == self.N - 1: if np.isclose(round(state.goal_position[0], 2), round(state.initial_position[0], 2), rtol=1e-2): return 0, 0 else: speed = self.p_distributed_controller.step( self.compute_difference(state), dt) return speed, 0
class Thymio(pyenki.Thymio2): """ Superclass: `pyenki.Thymio2` -> the world update step will automatically call the Thymio `controlStep`. :param name: name of the agent :param index: index of the agents in the row :param p: proportional term of the PID controller :param i: integral term of the PID controller :param d: derivative term of the PID controller :param kwargs: other arguments :var initial_position: the initial position of the agent is set to None :var goal_position: the goal position of the agent is set to None :var goal_angle: the goal angle of the agent is set to None :var dictionary: the dictionary containing all the agent attributes is set to None :var colour: the colour of the agent is set to None :var p_distributed_controller: a simple proportional controller that returns the speed to apply """ def __init__(self, name, index, p, i, d, **kwargs) -> None: super().__init__(**kwargs) self.name = name self.index = index self.initial_position = None self.goal_position = None self.goal_angle = 0 self.dictionary = None self.colour = None self.p_distributed_controller = PID(p, i, d, max_out=16.6, min_out=-16.6) def get_input_sensing(self): """ :return sensing: the sensing perceived by the robot based on the net input """ if len(self.prox_comm_events) == 0: prox_comm = {'sender': {'intensities': [0, 0, 0, 0, 0, 0, 0]}} else: prox_comm = utils.get_prox_comm(self) prox_values = getattr(self, 'prox_values').copy() sensing = utils.get_all_sensors(prox_values, prox_comm) return sensing def controlStep(self, dt: float) -> None: """ Perform one control step: Compute the error as the difference between the goal and the actual position and use it to compute the velocity of the robot through a proportional controller. :param dt: control step duration """ error = self.goal_position[0] - self.position[0] speed = self.p_distributed_controller.step(error, dt) self.motor_right_target = speed self.motor_left_target = speed