def __init__(self, name, duration, datafolder, timestep=0.1): super().__init__(name, duration, datafolder, timestep=timestep) self.add_ref(ReferenceTrajectory("sos", trajectory_function=sos_gen())) p1_handle_obj = DynamicObject("p1_handle", 1.0, initial_state=[-1.0, 0.0, 0.0], appearance={ "shape": "circle", "radius": 0.1, "color": (0, 0, 255) }) self.add_obj(p1_handle_obj) p2_handle_obj = DynamicObject("p2_handle", 1.0, initial_state=[1.0, 0.0, 0.0], appearance={ "shape": "circle", "radius": 0.1, "color": (255, 0, 255) }) self.add_obj(p2_handle_obj) self.add_constraint( TensionSpring(p1_handle_obj, p2_handle_obj, 10.0, 0.1)) self.add_constraint( TensionSpring(p2_handle_obj, p1_handle_obj, 10.0, 0.1)) self.roles.append(Role(p1_handle_obj, perspective=Perspective())) self.roles.append(Role(p2_handle_obj, perspective=FlippedPerspective()))
def __init__(self, name, message, timestep, duration): super().__init__(name, timestep, duration=duration) self.message = message handle_obj = DynamicObject('handle', 0.0) self.add_obj(handle_obj) self.roles.append(Role(handle_obj))
def __init__(self, name, timestep, datafolder=None, duration=None): super().__init__(name, timestep, datafolder=datafolder, duration=duration) handle_obj = DynamicObject('handle', 0.0) self.add_obj(handle_obj) self.roles.append(Role(handle_obj))
def __init__( self, simulation_freq_Hz=960, action_freq_Hz=60, episode_length_s=20.0, n_agents=2, agent_force_min=-10.0, agent_force_max=10.0, force_net_limits=np.array([-np.inf, np.inf]), force_interaction_limits=np.array([-np.inf, np.inf]), slider_mass=1.0, slider_damping=0.5, slider_limits=np.array([-1.0, 1.0]), handle_mass=0.1, handle_damping=0.1, handle_spring_const=40.0, handle_rest_length=0.1, agent1_push_multiplier=1.0, agent1_pull_multiplier=1.0, agent1_handle_tracking_ratio=1.0, agent2_push_multiplier=1.0, agent2_pull_multiplier=1.0, agent2_handle_tracking_ratio=1.0, reference_generator=fixed_sos_gen, ): self.simulation_freq_Hz = simulation_freq_Hz self.simulation_timestep_s = 1.0 / simulation_freq_Hz self.action_timestep_s = 1.0 / action_freq_Hz self.simsteps_per_action = int(simulation_freq_Hz / action_freq_Hz) self.max_episode_steps = int(action_freq_Hz * episode_length_s) self.episode_length_s = episode_length_s self.n_agents = n_agents self.agent_force_min = agent_force_min self.agent_force_max = agent_force_max if np.isscalar(agent_force_min): self.action_space = spaces.Box(low=agent_force_min, high=agent_force_max, shape=(n_agents, ), dtype=np.float32) else: self.action_space = spaces.Box(low=agent_force_min, high=agent_force_max, dtype=np.float32) self.slider_mass = slider_mass self.slider_damping = slider_damping self.slider_limits = slider_limits self.slider_range = slider_limits[1] - slider_limits[0] self.handle_mass = handle_mass self.handle_damping = handle_damping self.handle_spring_const = handle_spring_const self.handle_rest_length = handle_rest_length self.agent1_push_multiplier = agent1_push_multiplier self.agent1_pull_multiplier = agent1_pull_multiplier self.agent1_handle_tracking_ratio = agent1_handle_tracking_ratio self.agent2_push_multiplier = agent2_push_multiplier self.agent2_pull_multiplier = agent2_pull_multiplier self.agent2_handle_tracking_ratio = agent2_handle_tracking_ratio self.dynamic_objects = [] self.constraints = [] self.slider = DynamicObject(slider_mass) self.dynamic_objects.append(self.slider) self.agent1_handle = DynamicObject(handle_mass) self.dynamic_objects.append(self.agent1_handle) self.agent1_handle_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent1_handle_copy) self.agent1_slider_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent1_slider_copy) self.agent2_handle = DynamicObject(handle_mass) self.dynamic_objects.append(self.agent2_handle) self.agent2_handle_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent2_handle_copy) self.agent2_slider_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent2_slider_copy) self.constraints.append( BindPosition(self.agent1_slider_copy, self.slider, proportion=self.agent1_handle_tracking_ratio)) self.constraints.append( BindPosition(self.agent2_slider_copy, self.slider, proportion=self.agent2_handle_tracking_ratio)) self.constraints.append( BindPosition(self.agent1_handle_copy, self.agent1_handle)) self.constraints.append( BindPosition(self.agent2_handle_copy, self.agent2_handle)) self.constraints.append( CompressionSpring( self.agent1_handle, self.agent1_slider_copy, self.agent1_push_multiplier * self.handle_spring_const, -self.handle_rest_length)) self.constraints.append( TensionSpring( self.agent1_handle, self.agent1_slider_copy, self.agent1_pull_multiplier * self.handle_spring_const, -self.handle_rest_length)) if (n_agents > 1): self.constraints.append( CompressionSpring( self.agent2_handle, self.agent2_slider_copy, self.agent2_push_multiplier * self.handle_spring_const, self.handle_rest_length)) self.constraints.append( TensionSpring( self.agent2_handle, self.agent2_slider_copy, self.agent2_pull_multiplier * self.handle_spring_const, self.handle_rest_length)) self.constraints.append( CompressionSpring(self.slider, self.agent1_handle_copy, self.handle_spring_const, -self.handle_rest_length)) self.constraints.append( TensionSpring(self.slider, self.agent1_handle_copy, self.handle_spring_const, -self.handle_rest_length)) if (n_agents > 1): self.constraints.append( CompressionSpring(self.slider, self.agent2_handle_copy, self.handle_spring_const, self.handle_rest_length)) self.constraints.append( TensionSpring(self.slider, self.agent2_handle_copy, self.handle_spring_const, -self.handle_rest_length)) self.constraints.append(Damping(self.slider_damping, self.slider)) self.constraints.append( PositionLimits(self.slider, self.slider_limits[1], self.slider_limits[0])) self.constraints.append( Damping(self.handle_damping, self.agent1_handle)) self.constraints.append( PositionLimits(self.agent1_handle, self.slider_limits[1], self.slider_limits[0])) self.constraints.append( Damping(self.handle_damping, self.agent2_handle)) self.constraints.append( PositionLimits(self.agent2_handle, self.slider_limits[1], self.slider_limits[0])) self.force_net_limits = force_net_limits self.force_interaction_limits = force_interaction_limits self.reference_generator = reference_generator self.viewer = None self.reset()
class DyadSliderAsymEffortEnv(gym.Env): metadata = { 'render.modes': ['human', 'rgb_array'], 'video.frames_per_second': 60, } def __init__( self, simulation_freq_Hz=960, action_freq_Hz=60, episode_length_s=20.0, n_agents=2, agent_force_min=-10.0, agent_force_max=10.0, force_net_limits=np.array([-np.inf, np.inf]), force_interaction_limits=np.array([-np.inf, np.inf]), slider_mass=1.0, slider_damping=0.5, slider_limits=np.array([-1.0, 1.0]), handle_mass=0.1, handle_damping=0.1, handle_spring_const=40.0, handle_rest_length=0.1, agent1_push_multiplier=1.0, agent1_pull_multiplier=1.0, agent1_handle_tracking_ratio=1.0, agent2_push_multiplier=1.0, agent2_pull_multiplier=1.0, agent2_handle_tracking_ratio=1.0, reference_generator=fixed_sos_gen, ): self.simulation_freq_Hz = simulation_freq_Hz self.simulation_timestep_s = 1.0 / simulation_freq_Hz self.action_timestep_s = 1.0 / action_freq_Hz self.simsteps_per_action = int(simulation_freq_Hz / action_freq_Hz) self.max_episode_steps = int(action_freq_Hz * episode_length_s) self.episode_length_s = episode_length_s self.n_agents = n_agents self.agent_force_min = agent_force_min self.agent_force_max = agent_force_max if np.isscalar(agent_force_min): self.action_space = spaces.Box(low=agent_force_min, high=agent_force_max, shape=(n_agents, ), dtype=np.float32) else: self.action_space = spaces.Box(low=agent_force_min, high=agent_force_max, dtype=np.float32) self.slider_mass = slider_mass self.slider_damping = slider_damping self.slider_limits = slider_limits self.slider_range = slider_limits[1] - slider_limits[0] self.handle_mass = handle_mass self.handle_damping = handle_damping self.handle_spring_const = handle_spring_const self.handle_rest_length = handle_rest_length self.agent1_push_multiplier = agent1_push_multiplier self.agent1_pull_multiplier = agent1_pull_multiplier self.agent1_handle_tracking_ratio = agent1_handle_tracking_ratio self.agent2_push_multiplier = agent2_push_multiplier self.agent2_pull_multiplier = agent2_pull_multiplier self.agent2_handle_tracking_ratio = agent2_handle_tracking_ratio self.dynamic_objects = [] self.constraints = [] self.slider = DynamicObject(slider_mass) self.dynamic_objects.append(self.slider) self.agent1_handle = DynamicObject(handle_mass) self.dynamic_objects.append(self.agent1_handle) self.agent1_handle_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent1_handle_copy) self.agent1_slider_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent1_slider_copy) self.agent2_handle = DynamicObject(handle_mass) self.dynamic_objects.append(self.agent2_handle) self.agent2_handle_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent2_handle_copy) self.agent2_slider_copy = DynamicObject(0.0) self.dynamic_objects.append(self.agent2_slider_copy) self.constraints.append( BindPosition(self.agent1_slider_copy, self.slider, proportion=self.agent1_handle_tracking_ratio)) self.constraints.append( BindPosition(self.agent2_slider_copy, self.slider, proportion=self.agent2_handle_tracking_ratio)) self.constraints.append( BindPosition(self.agent1_handle_copy, self.agent1_handle)) self.constraints.append( BindPosition(self.agent2_handle_copy, self.agent2_handle)) self.constraints.append( CompressionSpring( self.agent1_handle, self.agent1_slider_copy, self.agent1_push_multiplier * self.handle_spring_const, -self.handle_rest_length)) self.constraints.append( TensionSpring( self.agent1_handle, self.agent1_slider_copy, self.agent1_pull_multiplier * self.handle_spring_const, -self.handle_rest_length)) if (n_agents > 1): self.constraints.append( CompressionSpring( self.agent2_handle, self.agent2_slider_copy, self.agent2_push_multiplier * self.handle_spring_const, self.handle_rest_length)) self.constraints.append( TensionSpring( self.agent2_handle, self.agent2_slider_copy, self.agent2_pull_multiplier * self.handle_spring_const, self.handle_rest_length)) self.constraints.append( CompressionSpring(self.slider, self.agent1_handle_copy, self.handle_spring_const, -self.handle_rest_length)) self.constraints.append( TensionSpring(self.slider, self.agent1_handle_copy, self.handle_spring_const, -self.handle_rest_length)) if (n_agents > 1): self.constraints.append( CompressionSpring(self.slider, self.agent2_handle_copy, self.handle_spring_const, self.handle_rest_length)) self.constraints.append( TensionSpring(self.slider, self.agent2_handle_copy, self.handle_spring_const, -self.handle_rest_length)) self.constraints.append(Damping(self.slider_damping, self.slider)) self.constraints.append( PositionLimits(self.slider, self.slider_limits[1], self.slider_limits[0])) self.constraints.append( Damping(self.handle_damping, self.agent1_handle)) self.constraints.append( PositionLimits(self.agent1_handle, self.slider_limits[1], self.slider_limits[0])) self.constraints.append( Damping(self.handle_damping, self.agent2_handle)) self.constraints.append( PositionLimits(self.agent2_handle, self.slider_limits[1], self.slider_limits[0])) self.force_net_limits = force_net_limits self.force_interaction_limits = force_interaction_limits self.reference_generator = reference_generator self.viewer = None self.reset() def step(self, action): done = False r_1 = self.reference_trajectory_fn(self.t + self.action_timestep_s) self.r_dot = (r_1 - self.r) / self.action_timestep_s action = np.clip(action, self.agent_force_min, self.agent_force_max) p1_force, p2_force = action p2_force *= -1.0 for i in range(self.simsteps_per_action): for constraint in self.constraints: constraint.apply() if ((p1_force <= 0 and self.agent1_handle.force <= 0) or (p1_force >= 0 and self.agent1_handle.force >= 0)): agent1_interaction_force = min(p1_force, self.agent1_handle.force) else: agent1_interaction_force = 0.0 self.agent1_interaction_force_dot = ( agent1_interaction_force - self.agent1_interaction_force) / self.simulation_timestep_s self.agent1_interaction_force = agent1_interaction_force self.agent1_handle.add_force(p1_force) if (self.n_agents > 1): if ((p2_force <= 0 and self.agent2_handle.force <= 0) or (p2_force >= 0 and self.agent2_handle.force >= 0)): agent2_interaction_force = min(p2_force, self.agent2_handle.force) else: agent2_interaction_force = 0.0 self.agent2_handle.add_force(p2_force) self.agent2_interaction_force_dot = ( agent2_interaction_force - self.agent2_interaction_force) / self.simulation_timestep_s self.agent2_interaction_force = agent2_interaction_force for dyn_obj in self.dynamic_objects: dyn_obj.step(self.simulation_timestep_s) self.t += self.action_timestep_s self.r = r_1 reward = self.action_timestep_s * ( 1.0 - (abs(self.slider.state[0] - r_1) / self.slider_range)) if (self.t >= self.episode_length_s): done = True return self.observe(), reward, done def reset(self): self.t = 0.0 self.error = 0.0 self.reference_trajectory_fn = self.reference_generator() self.r = self.reference_trajectory_fn(self.t) self.r_dot = 0.0 if self.viewer: self.viewer.close() self.viewer = None self.slider.state = [0.0, 0.0, 0.0] self.agent1_slider_copy.state = [0.0, 0.0, 0.0] self.agent2_slider_copy.state = [0.0, 0.0, 0.0] self.agent1_handle.state = [-self.handle_rest_length, 0.0, 0.0] self.agent1_handle_copy.state = [-self.handle_rest_length, 0.0, 0.0] self.agent2_handle.state = [self.handle_rest_length, 0.0, 0.0] self.agent2_handle_copy.state = [self.handle_rest_length, 0.0, 0.0] self.agent1_interaction_force = 0.0 self.agent1_interaction_force_dot = 0.0 self.agent2_interaction_force = 0.0 self.agent2_interaction_force_dot = 0.0 return self.observe() def observe(self): x = self.slider.state[0] x_dot = self.slider.state[1] state = np.array([ x, x_dot, self.r, self.r_dot, self.agent1_interaction_force, self.agent1_interaction_force_dot, self.agent2_interaction_force, self.agent2_interaction_force_dot ]) return state def render(self, mode='human'): screen_width = 600 screen_height = 400 world_height = self.slider_range scale_y = screen_height / world_height scale_x = 1.0 * (screen_width / 2) egg_x = screen_width / 2 egg_width = 20.0 egg_height = 30.0 handle_width = 10 handle_height = 15 reference_width = 2.0 reference_x_resolution = int(20 * self.episode_length_s) reference_points = np.zeros((reference_x_resolution, 2)) reference_scale = np.linspace(0, self.episode_length_s, reference_x_resolution) reference_points[:, 0] = (scale_x * reference_scale) + (screen_width / 2) reference_points[:, 1] = scale_y * self.reference_trajectory_fn( reference_scale) if self.viewer is None: from gym.envs.classic_control import rendering self.viewer = rendering.Viewer(screen_width, screen_height) l, r, t, b = -egg_width / 2, egg_width / 2, egg_height / 2, -egg_height / 2 egg = rendering.FilledPolygon([(l, 0), (0, t), (r, 0), (0, b)]) self.egg_transform = rendering.Transform() egg.add_attr(self.egg_transform) self.viewer.add_geom(egg) l, r, t, b = -handle_width / 2, handle_width / 2, handle_height / 2, -handle_height / 2 a1_handle = rendering.FilledPolygon([(l, 0), (0, t), (r, 0), (0, b)]) self.a1_handle_transform = rendering.Transform() a1_handle.add_attr(self.a1_handle_transform) self.viewer.add_geom(a1_handle) l, r, t, b = -handle_width / 2, handle_width / 2, handle_height / 2, -handle_height / 2 a2_handle = rendering.FilledPolygon([(l, 0), (0, t), (r, 0), (0, b)]) self.a2_handle_transform = rendering.Transform() a2_handle.add_attr(self.a2_handle_transform) self.viewer.add_geom(a2_handle) reference = rendering.PolyLine(reference_points, False) self.reference_transform = rendering.Transform() reference.add_attr(self.reference_transform) self.viewer.add_geom(reference) egg_y = (self.slider.state[0] * scale_y) + (screen_height / 2) self.egg_transform.set_translation(egg_x, egg_y) a1_handle_y = (self.agent1_handle.state[0] * scale_y) + (screen_height / 2) self.a1_handle_transform.set_translation(egg_x, a1_handle_y) a2_handle_y = (self.agent2_handle.state[0] * scale_y) + (screen_height / 2) self.a2_handle_transform.set_translation(egg_x, a2_handle_y) self.reference_transform.set_translation(-self.t * scale_x, screen_height / 2) return self.viewer.render(return_rgb_array=mode == 'rgb_array') def close(self): if self.viewer: self.viewer.close() self.viewer = None
def __init__(self, name, timestep, datafolder, duration, parameters): super().__init__(name, timestep, datafolder=datafolder, duration=duration, parameters=parameters) k = parameters.get('k', 10.0) p1_push = parameters.get('p1_push', 1.0) p1_pull = parameters.get('p1_pull', 1.0) p2_push = parameters.get('p2_push', 1.0) p2_pull = parameters.get('p2_pull', 1.0) self.add_ref(ReferenceTrajectory('sos', trajectory_function=sos_gen())) obj_radius = 0.05 rest_length = obj_radius * 4 cursor_color = (220, 220, 220) self_color = (0, 0, 255) link_color = (0, 64, 128) other_color = (255, 0, 255) p1_handle_obj = DynamicObject('p1_handle', 0.0, initial_state=[-0.0, 0.0, 0.0]) p1_handle_draw = DynamicObject('p1_handle_draw', 0.0, initial_state=[-0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': self_color }) p2_handle_obj = DynamicObject('p2_handle', 0.0, initial_state=[0.0, 0.0, 0.0]) p2_handle_draw = DynamicObject('p2_handle_draw', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': self_color }) cursor = DynamicObject('cursor', 0.1, initial_state=[0.0, 0.0, 0.0], appearance={ 'shape': 'rectangle', 'width': obj_radius * 3.0, 'height': obj_radius * 1.0, 'color': cursor_color }) p1_self_contact_obj = DynamicObject('p1_self_contact', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius / 2, 'color': self_color }) p1_contact_link = DynamicObject('p1_contact_link', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'link', 'linktype': 'bulge', 'start_ref': 'p1_self_contact', 'end_ref': 'p1_handle_draw', 'color': link_color }) p1_other_contact_obj = DynamicObject('p1_other_contact', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius / 2, 'color': other_color }) p2_self_contact_obj = DynamicObject('p2_self_contact', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius / 2, 'color': self_color }) p2_contact_link = DynamicObject('p2_contact_link', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'link', 'linktype': 'bulge', 'start_ref': 'p1_self_contact', 'start_ref': 'p2_self_contact', 'end_ref': 'p2_handle_draw', 'color': link_color }) p2_other_contact_obj = DynamicObject('p2_other_contact', 0.0, initial_state=[0.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius / 2, 'color': other_color }) self.add_obj(p1_handle_obj) self.add_obj(p2_handle_obj) self.add_obj(cursor) self.add_obj(p1_contact_link) self.add_obj(p1_self_contact_obj) self.add_obj(p1_other_contact_obj) self.add_obj(p2_contact_link) self.add_obj(p2_self_contact_obj) self.add_obj(p2_other_contact_obj) self.add_obj(p1_handle_draw) self.add_obj(p2_handle_draw) self.add_pre_constraint( BindPosition(p1_self_contact_obj, cursor, offset=-obj_radius)) self.add_pre_constraint( BindPosition(p1_other_contact_obj, cursor, offset=obj_radius)) self.add_pre_constraint( BindPosition(p2_self_contact_obj, cursor, offset=obj_radius)) self.add_pre_constraint( BindPosition(p2_other_contact_obj, cursor, offset=-obj_radius)) self.add_constraint(BindPosition(p1_handle_draw, p1_handle_obj)) self.add_constraint( PositionLimits(p1_handle_draw, pos=-rest_length, reference=cursor)) self.add_constraint(BindPosition(p2_handle_draw, p2_handle_obj)) self.add_constraint( PositionLimits(p2_handle_draw, neg=rest_length, reference=cursor)) self.add_constraint( SpringLawSolid(p1_handle_obj, cursor, -k, -rest_length)) self.add_constraint( SpringLawSolid(cursor, p1_handle_obj, k * p1_push, rest_length)) self.add_constraint( SpringLawSolid(p1_handle_obj, cursor, k, rest_length)) self.add_constraint( SpringLawSolid(cursor, p1_handle_obj, -k * p1_pull, -rest_length)) self.add_constraint( SpringLawSolid(p2_handle_obj, cursor, k, rest_length)) self.add_constraint( SpringLawSolid(cursor, p2_handle_obj, -k * p2_push, -rest_length)) self.add_constraint( SpringLawSolid(p2_handle_obj, cursor, -k, -rest_length)) self.add_constraint( SpringLawSolid(cursor, p2_handle_obj, k * p2_pull, rest_length)) self.add_constraint(Damping(1, cursor)) self.roles.append( Role(p1_handle_obj, perspective=HiddenObjectsPerspective([ 'p2_handle_draw', 'p2_self_contact', 'p2_contact_link', 'p2_other_contact' ]))) self.roles.append( Role(p2_handle_obj, perspective=FlippedHiddenObjectsPerspective([ 'p1_handle_draw', 'p1_self_contact', 'p1_contact_link', 'p1_other_contact' ])))
def __init__(self, name, timestep, duration): super().__init__(name, timestep, duration=duration) self.message = 'Align your cursor (blue) with the line. When both subjects are aligned, the task will start.' self.add_ref( ReferenceTrajectory('flat', trajectory_function=flat_gen())) obj_radius = 0.05 self_color = (0, 0, 255) other_color = (255, 0, 127) p1_handle_obj = DynamicObject('p1_handle', 0.0, initial_state=[-1.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': self_color }) p1_handle_draw = DynamicObject('p1_handle_draw', 0.0, initial_state=[-1.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': other_color }) p2_handle_obj = DynamicObject('p2_handle', 0.0, initial_state=[1.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': self_color }) p2_handle_draw = DynamicObject('p2_handle_draw', 0.0, initial_state=[1.0, 0.0, 0.0], record_data=False, appearance={ 'shape': 'circle', 'radius': obj_radius, 'color': other_color }) self.add_obj(p1_handle_draw) self.add_obj(p2_handle_draw) self.add_obj(p1_handle_obj) self.add_obj(p2_handle_obj) self.add_constraint(BindPosition(p1_handle_draw, p1_handle_obj)) self.add_constraint(BindPosition(p2_handle_draw, p2_handle_obj)) self.roles.append( Role(p1_handle_obj, perspective=HiddenObjectsPerspective( ['p1_handle_draw', 'p2_handle']))) self.roles.append( Role(p2_handle_obj, perspective=FlippedHiddenObjectsPerspective( ['p2_handle_draw', 'p1_handle']))) self.add_endcond( ConditionAND([ InRangeForDuration(p1_handle_obj, 0.2, -0.2, duration_s=1.0), InRangeForDuration(p2_handle_obj, 0.2, -0.2, duration_s=1.0), ]))