How to rotate pymunk joints at will?

1k views Asked by At

I'm trying to create a walking spider like this:
enter image description here

I considered using a SimpleMotor at the pink and red joints and control them using the rate function. But when I tried, I get an error that the function is not callable.

self.motorJoint1.rate(0.0) TypeError: 'float' object is not callable

I don't see any other functions in the pymunk API that allow controlling the joints at will. Is there really no function or am I missing something?

Basically in the run loop I want to specify rotations to the joints at certain points of time, to not just make the spider walk, but to eventually be able to use Neural Networks to allow it to experiment with various configurations of leg positions and figure out which ones can make it walk:

angle1 = 30
angle2 = 10
redJoint1.rotate(angle1)
pinkJoint2.rotate(angle2)
if angle1 < 50:
    angle1 = angle1 + 1

Is it possible at all to achieve such a level of control over joints using Pymunk? To be able to stop moving the legs (without needing to put the body to sleep), or to rotate the leg joints to whatever angle the spider 'wishes to' at any point in time? Sample code would be a great help.

2

There are 2 answers

0
Nav On BEST ANSWER

From the servo example I took a hint and implemented this basic leg:

enter image description here

import sys

import pygame
from pygame.locals import USEREVENT, QUIT, KEYDOWN, KEYUP, K_s, K_r, K_q, K_ESCAPE, K_UP, K_DOWN, K_RIGHT, K_LEFT
from pygame.color import THECOLORS

import pymunk
from pymunk import Vec2d
import pymunk.pygame_util

class Simulator(object):

    def __init__(self):
        self.display_flags = 0
        self.display_size = (600, 600)

        self.space = pymunk.Space()
        self.space.gravity = (0.0, -1900.0)
        #self.space.damping = 0.999 # to prevent it from blowing up.

        # Pymunk physics coordinates start from the lower right-hand corner of the screen.
        self.ground_y = 100
        ground = pymunk.Segment(self.space.static_body, (5, self.ground_y), (595, self.ground_y), 1.0)
        ground.friction = 1.0
        self.space.add(ground)

        self.screen = None

        self.draw_options = None

    def reset_bodies(self):
        for body in self.space.bodies:
            if not hasattr(body, 'start_position'):
                continue
            body.position = Vec2d(body.start_position)
            body.force = 0, 0
            body.torque = 0
            body.velocity = 0, 0
            body.angular_velocity = 0
            body.angle = body.start_angle

    def draw(self):        
        self.screen.fill(THECOLORS["white"])### Clear the screen        
        self.space.debug_draw(self.draw_options)### Draw space        
        pygame.display.flip()### All done, lets flip the display

    def main(self):
        pygame.init()
        self.screen = pygame.display.set_mode(self.display_size, self.display_flags)
        width, height = self.screen.get_size()
        self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)

        def to_pygame(p):            
            return int(p.x), int(-p.y+height) #Small hack to convert pymunk to pygame coordinates
        def from_pygame(p):
            return to_pygame(p)

        clock = pygame.time.Clock()
        running = True
        font = pygame.font.Font(None, 16)

        # Create the spider
        chassisXY = Vec2d(self.display_size[0]/2, self.ground_y+100)
        chWd = 70; chHt = 50
        chassisMass = 10
        
        legWd_a = 50; legHt_a = 5
        legWd_b = 100; legHt_b = 5
        legMass = 1
        relativeAnguVel = 0

        #---chassis
        chassis_b = pymunk.Body(chassisMass, pymunk.moment_for_box(chassisMass, (chWd, chHt)))
        chassis_b.position = chassisXY
        chassis_shape = pymunk.Poly.create_box(chassis_b, (chWd, chHt))
        chassis_shape.color = 200, 200, 200, 100
        print("chassis position");print(chassis_b.position)

        #---first left leg a
        leftLeg_1a_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_a, legHt_a)))
        leftLeg_1a_body.position = chassisXY - ((chWd/2)+(legWd_a/2), 0)
        leftLeg_1a_shape = pymunk.Poly.create_box(leftLeg_1a_body, (legWd_a, legHt_a))        
        leftLeg_1a_shape.color = 255, 0, 0, 100
        
        #---first left leg b
        leftLeg_1b_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_b, legHt_b)))
        leftLeg_1b_body.position = leftLeg_1a_body.position - ((legWd_a/2)+(legWd_b/2), 0)
        leftLeg_1b_shape = pymunk.Poly.create_box(leftLeg_1b_body, (legWd_b, legHt_b))        
        leftLeg_1b_shape.color = 0, 255, 0, 100        
        
        #---first right leg a
        rightLeg_1a_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_a, legHt_a)))
        rightLeg_1a_body.position = chassisXY + ((chWd/2)+(legWd_a/2), 0)
        rightLeg_1a_shape = pymunk.Poly.create_box(rightLeg_1a_body, (legWd_a, legHt_a))        
        rightLeg_1a_shape.color = 255, 0, 0, 100        
        
        #---first right leg b
        rightLeg_1b_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_b, legHt_b)))
        rightLeg_1b_body.position = rightLeg_1a_body.position + ((legWd_a/2)+(legWd_b/2), 0)
        rightLeg_1b_shape = pymunk.Poly.create_box(rightLeg_1b_body, (legWd_b, legHt_b))        
        rightLeg_1b_shape.color = 0, 255, 0, 100     
        
        #---link left leg b with left leg a       
        pj_ba1left = pymunk.PinJoint(leftLeg_1b_body, leftLeg_1a_body, (legWd_b/2,0), (-legWd_a/2,0))#anchor point coordinates are wrt the body; not the space
        motor_ba1Left = pymunk.SimpleMotor(leftLeg_1b_body, leftLeg_1a_body, relativeAnguVel)
        #---link left leg a with chassis
        pj_ac1left = pymunk.PinJoint(leftLeg_1a_body, chassis_b, (legWd_a/2,0), (-chWd/2, 0))
        motor_ac1Left = pymunk.SimpleMotor(leftLeg_1a_body, chassis_b, relativeAnguVel)
        #---link right leg b with right leg a       
        pj_ba1Right = pymunk.PinJoint(rightLeg_1b_body, rightLeg_1a_body, (-legWd_b/2,0), (legWd_a/2,0))#anchor point coordinates are wrt the body; not the space
        motor_ba1Right = pymunk.SimpleMotor(rightLeg_1b_body, rightLeg_1a_body, relativeAnguVel)
        #---link right leg a with chassis
        pj_ac1Right = pymunk.PinJoint(rightLeg_1a_body, chassis_b, (-legWd_a/2,0), (chWd/2, 0))
        motor_ac1Right = pymunk.SimpleMotor(rightLeg_1a_body, chassis_b, relativeAnguVel)              
        
        self.space.add(chassis_b, chassis_shape) 
        self.space.add(leftLeg_1a_body, leftLeg_1a_shape, rightLeg_1a_body, rightLeg_1a_shape) 
        self.space.add(leftLeg_1b_body, leftLeg_1b_shape, rightLeg_1b_body, rightLeg_1b_shape) 
        self.space.add(pj_ba1left, motor_ba1Left, pj_ac1left, motor_ac1Left)  
        self.space.add(pj_ba1Right, motor_ba1Right, pj_ac1Right, motor_ac1Right)      
        
        #---prevent collisions with ShapeFilter
        shape_filter = pymunk.ShapeFilter(group=1)
        chassis_shape.filter = shape_filter
        leftLeg_1a_shape.filter = shape_filter
        rightLeg_1a_shape.filter = shape_filter
        leftLeg_1b_shape.filter = shape_filter
        rightLeg_1b_shape.filter = shape_filter        
                    

        simulate = False
        rotationRate = 2
        while running:
            for event in pygame.event.get():
                if event.type == QUIT or (event.type == KEYDOWN and event.key in (K_q, K_ESCAPE)):
                    #running = False
                    sys.exit(0)
                elif event.type == KEYDOWN and event.key == K_s:
                    # Start/stop simulation.
                    simulate = not simulate
                elif event.type == KEYDOWN and event.key == K_r:
                    # Reset.
                    # simulate = False
                    self.reset_bodies()
                elif event.type == KEYDOWN and event.key == K_UP:
                    motor_ba1Left.rate = rotationRate
                elif event.type == KEYDOWN and event.key == K_DOWN:
                    motor_ba1Left.rate = -rotationRate
                elif event.type == KEYDOWN and event.key == K_LEFT:
                    motor_ac1Left.rate = rotationRate
                elif event.type == KEYDOWN and event.key == K_RIGHT:
                    motor_ac1Left.rate = -rotationRate                    
                elif event.type == KEYUP:
                    motor_ba1Left.rate = 0
                    motor_ac1Left.rate = 0

            self.draw()

            ### Update physics
            fps = 50
            iterations = 25
            dt = 1.0/float(fps)/float(iterations)
            if simulate:
                for x in range(iterations): # 10 iterations to get a more stable simulation
                    self.space.step(dt)

            pygame.display.flip()
            clock.tick(fps)

if __name__ == '__main__':
    sim = Simulator()
    sim.main()

It can be controlled with the up, left, right and down arrow keys after first pressing the s key to start the simulation. I've also made sure the variables are created properly linked with each other and named well.

The part about making the joints move to a desired angle is yet to be implemented, but perhaps that could be calculated by taking the x,y positions of the ends of the joints and using a formula to calculate the angle and then move the motor until it reaches a desired angle.

If there's a better way, do let me know by posting an answer or editing this one.

0
M lab On

Introduction

As the Nav's answer is really helpful to get start and also inspiring. Sp I would like to contribute my part on this by implementing Servo motor class and modify something from original code.

ServoMotor class

class ServoMotor(pymunk.SimpleMotor):
def __init__(self, body_a, body_b, max_force, max_rate, p_gain):
    super().__init__(body_a, body_b, 0)  # Start with rate = 0
    self.body_a = body_a
    self.body_b = body_b
    self.max_force = max_force
    self.max_rate = max_rate
    self.p_gain = p_gain
    self.target_angle = 0


def angle(self):
    """
    Get the current angle of the motor.
    """
    return self.body_a.angle - self.body_b.angle

def set_target_angle(self, target_angle):
    """
    Set the target angle for the motor to rotate towards.
    """
    self.target_angle = target_angle
def update(self):
    rate = (self.target_angle - self.angle()) * self.p_gain
    #to clamping the turning rate
    self.rate = max(min(rate, self.max_rate), -self.max_rate)

Explaination

As we want to control joint angle with joint velocity, which is integrating process, we can use simple proportional control with reasonable gain for stabillity. After set target angle once, we need to call update repeatly.

Full example code

import sys
import numpy as np
import pygame
from pygame.locals import USEREVENT, QUIT, KEYDOWN, KEYUP, K_s, K_r, K_q, K_ESCAPE, K_UP, K_DOWN, K_RIGHT, K_LEFT, K_x
from pygame.color import THECOLORS

import pymunk
from pymunk import Vec2d
import pymunk.pygame_util

class ServoMotor(pymunk.SimpleMotor):
    def __init__(self, body_a, body_b, max_force, max_rate, p_gain):
        super().__init__(body_a, body_b, 0)  # Start with rate = 0
        self.body_a = body_a
        self.body_b = body_b
        self.max_force = max_force
        self.max_rate = max_rate
        self.p_gain = p_gain
        self.target_angle = 0


    def angle(self):
        """
        Get the current angle of the motor.
        """
        return self.body_a.angle - self.body_b.angle

    def set_target_angle(self, target_angle):
        """
        Set the target angle for the motor to rotate towards.
        """
        self.target_angle = target_angle
    def update(self):
        rate = (self.target_angle - self.angle()) * self.p_gain
        #to clamping the turning rate
        self.rate = max(min(rate, self.max_rate), -self.max_rate)
class Simulator(object):

    def __init__(self):
        self.display_flags = 0

        self.display_size = (1000, 800)

        self.space = pymunk.Space()
        self.space.gravity = (0.0, -1900.0)
        # self.space.damping = 0.999 # to prevent it from blowing up.

        # Pymunk physics coordinates start from the lower right-hand corner of the screen.
        self.ground_y = 100
        ground = pymunk.Segment(self.space.static_body, (0, self.ground_y), (1000, self.ground_y), 5.0)
        ground.friction = 1.0
        self.space.add(ground)

        self.screen = None

        self.draw_options = None

    def reset_bodies(self,chassisXY):
        legWd_a = 75
        legWd_b = 75
        chWd = 70


        self.chassis_b.position = chassisXY
        self.leftLeg_1a_body.position = chassisXY - ((chWd / 2) + (legWd_a / 2), 0)
        self.leftLeg_1a_body.angle = 0
        self.leftLeg_1b_body.position = self.leftLeg_1a_body.position - ((legWd_a / 2) + (legWd_b / 2), 0)
        self.leftLeg_1b_body.angle = 0
        self.rightLeg_1a_body.position = chassisXY + ((chWd / 2) + (legWd_a / 2), 0)
        self.rightLeg_1a_body.angle = 0
        self.rightLeg_1b_body.position = self.rightLeg_1a_body.position + ((legWd_a / 2) + (legWd_b / 2), 0)
        self.rightLeg_1b_body.angle = 0

    def draw(self):
        self.screen.fill(THECOLORS["white"])  ### Clear the screen
        self.space.debug_draw(self.draw_options)  ### Draw space
        pygame.display.flip()  ### All done, lets flip the display


    def main(self):
        pygame.init()
        self.screen = pygame.display.set_mode(self.display_size, self.display_flags)
        width, height = self.screen.get_size()
        self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)
        pymunk.pygame_util.positive_y_is_up = True

        clock = pygame.time.Clock()
        running = True
        font = pygame.font.Font(None, 16)

        # Create the spider
        chassisXY = Vec2d(self.display_size[0] / 2, self.ground_y + 100)
        chWd = 70
        chHt = 50
        chassisMass = 10

        legWd_a = 75
        legHt_a = 5
        legWd_b = 75
        legHt_b = 5
        legMass = 1

        motor_max_force = np.inf
        motor_max_rate = 3
        motor_p_gain = 50
        # ---chassis
        self.chassis_b = pymunk.Body(chassisMass, pymunk.moment_for_box(chassisMass, (chWd, chHt)))
        self.chassis_b.position = chassisXY
        chassis_shape = pymunk.Poly.create_box(self.chassis_b, (chWd, chHt))
        chassis_shape.color = 200, 200, 200, 100
        chassis_shape.friction = 0.5
        print("chassis position")
        print(self.chassis_b.position)

        # ---first left leg a
        self.leftLeg_1a_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_a, legHt_a)))
        self.leftLeg_1a_body.position = chassisXY - ((chWd / 2) + (legWd_a / 2), 0)

        leftLeg_1a_shape = pymunk.Poly.create_box(self.leftLeg_1a_body, (legWd_a, legHt_a))
        leftLeg_1a_shape.color = 255, 0, 0, 100
        leftLeg_1a_shape.friction = 1

        # ---first left leg b
        self.leftLeg_1b_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_b, legHt_b)))
        self.leftLeg_1b_body.position = self.leftLeg_1a_body.position - ((legWd_a / 2) + (legWd_b / 2), 0)
        leftLeg_1b_shape = pymunk.Poly.create_box(self.leftLeg_1b_body, (legWd_b, legHt_b))
        leftLeg_1b_shape.color = 0, 255, 0, 100
        leftLeg_1b_shape.friction = 1
        # ---first right leg a
        self.rightLeg_1a_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_a, legHt_a)))
        self.rightLeg_1a_body.position = chassisXY + ((chWd / 2) + (legWd_a / 2), 0)
        rightLeg_1a_shape = pymunk.Poly.create_box(self.rightLeg_1a_body, (legWd_a, legHt_a))
        rightLeg_1a_shape.color = 255, 0, 0, 100
        rightLeg_1a_shape.friction = 1
        # ---first right leg b
        self.rightLeg_1b_body = pymunk.Body(legMass, pymunk.moment_for_box(legMass, (legWd_b, legHt_b)))
        self.rightLeg_1b_body.position = self.rightLeg_1a_body.position + ((legWd_a / 2) + (legWd_b / 2), 0)
        rightLeg_1b_shape = pymunk.Poly.create_box(self.rightLeg_1b_body, (legWd_b, legHt_b))
        rightLeg_1b_shape.color = 0, 255, 0, 100
        rightLeg_1b_shape.friction = 1
        # ---link left leg b with left leg a
        pj_ba1left = pymunk.PinJoint(self.leftLeg_1b_body, self.leftLeg_1a_body, (legWd_b / 2, 0),
                                     (-legWd_a / 2, 0))  # anchor point coordinates are wrt the body; not the space
        motor_ba1Left = ServoMotor(self.leftLeg_1b_body, self.leftLeg_1a_body, motor_max_force,motor_max_rate,motor_p_gain)
        # ---link left leg a with chassis
        pj_ac1left = pymunk.PinJoint(self.leftLeg_1a_body, self.chassis_b, (legWd_a / 2, 0), (-chWd / 2, 0))
        motor_ac1Left = ServoMotor(self.leftLeg_1a_body, self.chassis_b, motor_max_force,motor_max_rate,motor_p_gain)
        # ---link right leg b with right leg a
        pj_ba1Right = pymunk.PinJoint(self.rightLeg_1b_body, self.rightLeg_1a_body, (-legWd_b / 2, 0),
                                      (legWd_a / 2, 0))  # anchor point coordinates are wrt the body; not the space
        motor_ba1Right = ServoMotor(self.rightLeg_1b_body, self.rightLeg_1a_body, motor_max_force,motor_max_rate,motor_p_gain)
        # ---link right leg a with chassis
        pj_ac1Right = pymunk.PinJoint(self.rightLeg_1a_body, self.chassis_b, (-legWd_a / 2, 0), (chWd / 2, 0))

        motor_ac1Right = ServoMotor(self.rightLeg_1a_body, self.chassis_b, motor_max_force,motor_max_rate,motor_p_gain)
        self.space.add(self.chassis_b, chassis_shape)
        self.space.add(self.leftLeg_1a_body, leftLeg_1a_shape, self.rightLeg_1a_body, rightLeg_1a_shape)
        self.space.add(self.leftLeg_1b_body, leftLeg_1b_shape, self.rightLeg_1b_body, rightLeg_1b_shape)
        self.space.add(pj_ba1left, motor_ba1Left, pj_ac1left, motor_ac1Left)
        self.space.add(pj_ba1Right, motor_ba1Right, pj_ac1Right, motor_ac1Right)

        motor_list = [motor_ba1Left,motor_ac1Left,motor_ba1Right,motor_ac1Right]
        # ---prevent collisions with ShapeFilter
        shape_filter = pymunk.ShapeFilter(group=1)
        chassis_shape.filter = shape_filter
        leftLeg_1a_shape.filter = shape_filter
        rightLeg_1a_shape.filter = shape_filter
        leftLeg_1b_shape.filter = shape_filter
        rightLeg_1b_shape.filter = shape_filter

        simulate = True
        rotationRate = 2
        while running:
            for event in pygame.event.get():
                if event.type == QUIT or (event.type == KEYDOWN and event.key in (K_q, K_ESCAPE)):
                    # running = False
                    sys.exit(0)
                elif event.type == KEYDOWN and event.key == K_s:
                    # Start/stop simulation.
                    simulate = not simulate
                elif event.type == KEYDOWN and event.key == K_r:
                    # Reset.
                    # simulate = False
                    print('reset')
                    self.chassis_b.position = chassisXY
                    self.reset_bodies(chassisXY)


                elif event.type == KEYDOWN and event.key == K_RIGHT:
                    motor_ba1Left.set_target_angle(np.deg2rad(170))
                    motor_ba1Right.set_target_angle(np.deg2rad(0))
                elif event.type == KEYDOWN and event.key == K_LEFT:
                    motor_ba1Left.set_target_angle(np.deg2rad(0))
                    motor_ba1Right.set_target_angle(np.deg2rad(-170))
                elif event.type == KEYDOWN and event.key == K_DOWN:
                    motor_ac1Left.set_target_angle(np.deg2rad(10))
                    motor_ac1Right.set_target_angle(np.deg2rad(-10))
                elif event.type == KEYDOWN and event.key == K_UP:
                    motor_ac1Left.set_target_angle(np.deg2rad(-60))
                    motor_ac1Right.set_target_angle(np.deg2rad(60))
                elif event.type == KEYUP:
                    motor_ba1Left.rate = 0
                    motor_ac1Left.rate = 0

            for motor in motor_list:
                motor.update()

            #print(np.rad2deg(self.leftLeg_1a_body.angle-self.chassis_b.angle))
            self.draw()

            ### Update physics
            fps = 50
            iterations = 25
            dt = 1.0 / float(fps) / float(iterations)
            if simulate:
                for x in range(iterations):  # 10 iterations to get a more stable simulation
                    self.space.step(dt)

            pygame.display.flip()
            clock.tick(fps)


if __name__ == '__main__':
    sim = Simulator()
    sim.main()

Modification sumary

  1. Display seem inverted vertically so I add pymunk.pygame_util.positive_y_is_up = True
  2. The reset body not working as is can;t find attributes start_position so I made a new stright forward one.
  3. Use arrow keys to control joint position instead of joint rate.