We came together with a vague understanding of the movement we wanted to compute. Upon further diving into the spiral example code, we were intrigued with the possibilities presented by the marker. This brought up an interesting challenge where we could incorporate another component to this “duet”.

Our goal then became to integrate the marker with the motion of the two arms.

early iteration

After trying to output the locations of the last dot, we could find a way to get the two arms to connect via the dots. In our search, we noted that the markers were beginning to emulate “ribbons.” From there, we moved on and began trying to create a synchronized ribbon dancing pair of arms.

After successfully syncing our ribbon dancers, we thought it looked too robotic and not quite plausible. In response, we staggered the two arms to make it a bit more clear that the right arm was following the left arm. This gives the arms a bit more personality and character that comes across in the dance.

Final ribbon

Code: We updated the pendulumcontroller to have one mimic the state of the other using the PD feedback. It also places a new marker for every frame for each arm, to simulate a ribbon.

#!/usr/bin/env python3
"""Prototype Python 3 script for the 16-375 double-pendulum control exercise."""

################################################################
# Written in 2018-2019 by Garth Zeglin <garthz@cmu.edu&gt;

# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.

# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/&gt;.

################################################################

import rcp.doublependulum
from rcp.ex.ndblpend import main
import numpy as np
from PyQt5.QtGui import QPainter, QColor, QPen


################################################################
class PendulumController(rcp.doublependulum.DoublePendulumController):
    def __init__(self):
        super().__init__()

        # override the default initial state
        self.initial_state = np.array([3.0, 0.0, 0.0, 0.0])
        return

    #================================================================
    # Specialize this particular controller when a member of an ensemble.
    def set_identity(self, serial_number):
        super().set_identity(serial_number)
        # perturb the initial state
        self.initial_state[0] += self.identity * 0.05
        return
    
    #================================================================
    def setup(self):
        if self.identity == 0:
            self.write("""Passive double-pendulum simulation.
A zero torque vector is applied to the joints so they swing freely.  The underlying simulation has frictionless joints so the behavior is chaotic. It is ultimately unstable due to accumulated numerical error.\n""")

        return
    #================================================================
    def compute_control(self, t, dt, state, tau):
        """Method called from simulator to calculate the next step of applied torques.

        :param t: time in seconds since simulation began
        :param state: four element numpy array with joint positions ``q`` and joint velocities ``qd`` as ``[q0, q1, qd0, qd1]``, expressed in radians and radians/sec
        :param tau: two element numpy array to fill in with the joint torques to apply ``[tau0, tau1]``
        """
        tau[0:2] = (0,0)

        elbow, end = self.model.forwardKinematics(state[0:2])

        if self.identity == 1:
            #target = np.concatenate((self.world.get_some_angles(), np.zeros(2)))
            target = self.world.get_some_angles()
            target[3] /= 1.02
            target[2] /= 1.01

            # calculate position and velocity error as difference from reference state
            qerr = target - state

            # apply PD control to reach the pose (no integral term)
            # P constant decreases as the robot gets "tired"
            tau[0] = ((self.kp[0]) * qerr[0]) + (self.kd[0] * qerr[2])
            tau[1] = ((self.kp[1]) * qerr[1]) + (self.kd[1] * qerr[3])
        else:
            tau[0] = 0
            tau[1] = 0

        if self.identity == 0:
            self.world.set_some_angles(state)

        if self.identity == 0:
            self.world.set_marker(int(((t*200)//1 % 100) * 2), (end[0] - 0, end[1]))
        else:
            self.world.set_marker(int(((t*200)//1 % 100) * 2 + 1), (end[0] - 0, end[1]))
        return

################################################################$
if __name__ == "__main__":
    main(PendulumController)

To allow the right arm to see the positions of the angles of the left arm, we had to add two functions to the MainApp class:

    def set_some_angles(self, angles):
        self.angles = angles

    def get_some_angles(self):
        return self.angles

To make the ribbons, we had to modify the marker to increment the hue each frame, and to allow up to 200 markers instead of just 1. We updated the setupUi function to increment the hue for each new marker:

        self.markers = []
        lastColor = QColor(255,0,0)
        for i in range(200):
            newcolor = copy.copy(lastColor)
            print(lastColor, newcolor)
            hsl = lastColor.getHslF()
            hsl = ((hsl[0] + 0.005) % 1, hsl[1], hsl[2])
            newcolor.setHslF(*hsl)
            lastColor = newcolor
            print(lastColor, newcolor)
            marker = rcp.QtDoublePendulum.QtMarkerItem(color=newcolor)
            marker.setPos(2000, 0)
            self.markers.append(marker)
            self.scene.addItem(marker)

We modified the markers so you can set the color instead of hardcoded green:

class QtMarkerItem(QtWidgets.QGraphicsItem):

    def __init__(self, parent = None, color=QtCore.Qt.green):
        super().__init__(parent)
        self.bounds = QtCore.QRectF(-25, -25, 50, 50)
        self.color = color
        return

    def boundingRect(self):
        return self.bounds

    def paint(self, painter, options, widget):
        qp = painter

        # set up green fill with black outlines
        #pen = QtGui.QPen(QtCore.Qt.black)
        #pen.setWidthF(3.0)
        qp.setPen(QtCore.Qt.NoPen)
        brush = QtGui.QBrush(self.color)
        qp.setBrush(brush)

        # draw the marker as a circle
        qp.drawEllipse(QtCore.QPointF(0, 0), 20, 20)
        return

    def dim(self):
        self.color.setAlphaF(self.color.alphaF() - 0.01 if self.color.alphaF() &gt;= 0.01 else 0)

    def undim(self):
        self.color.setAlphaF(1)