add dmpytorch

This commit is contained in:
Brian Delhaisse
2019-04-21 22:11:14 +02:00
parent 17e7b454e1
commit 062969be8d
10 changed files with 2073 additions and 0 deletions
@@ -0,0 +1,94 @@
# DMPyTorch
This repository contains the code for the *DMPyTorch* library; a PyTorch library for Dynamic Movement Primitives. If you want to use with robots you can have a look at the [`pyrobolearn` framework](https://github.com/robotlearn/pyrobolearn)
**Warning**: The development of this framework is ongoing, and thus some substantial changes might occur. Sorry for the inconvenience.
## Requirements
The framework has been tested with Python 2.7 and 3.5 on Ubuntu 16.04 and 18.04.
## Installation
1. First download the `pip` Python package manager and create a virtual environment for Python as described in the following link: https://packaging.python.org/guides/installing-using-pip-and-virtualenv/
On Ubuntu, you can install `pip` and `virtualenv` by typing in the terminal:
- In Python 2.7:
```bash
sudo apt install python-pip
sudo pip install virtualenv
```
- In Python 3.5:
```bash
sudo apt install python3-pip
sudo pip install virtualenv
```
You can then create the virtual environment by typing:
```bash
virtualenv -p /usr/bin/python<version> <virtualenv_name>
# activate the virtual environment
source <virtualenv_name>/bin/activate
```
where `<version>` is the python version you want to use (select between `2.7` or `3.5`), and `<virtualenv_name>` is a name of your choice for the virtual environment. For instance, it can be `py2.7` or `py3.5`.
To deactivate the virtual environment, just type:
```bash
deactivate
```
2. clone this repository and install the requirements by executing the setup.py
In Python 2.7 or 3.5:
```bash
git clone https://github.com/robotlearn/dmpytorch
cd dmpytorch
pip install numpy
pip install -e . # this will install dmpytorch as well as the required packages (so no need for: pip install -r requirements.txt)
```
Depending on your computer configuration and the python version you use, you might need to install also the following packages through `apt-get`:
```bash
sudo apt install python-tk # if python 2.7
sudo apt install python3-tk # if python 3.5
```
## How to use it?
Check the `README.md` file in the `examples` folder.
## Citation
If you use `dmpytorch`, please cite:
```
@misc{delhaisse2019dmpytorch,
author = {Delhaisse, Brian and Rozo, Leonel, and Caldwell, Darwin},
title = {DMPyTorch: a PyTorch Library for Dynamic Movement Primitives},
howpublished = {\url{https://github.com/robotlearn/dmpytorch}},
year=2019,
}
```
## Acknowledgements
Parts of the code were inspired by the work from [Travis DeWolf](https://github.com/studywolf) and his library [`pydmps`](https://github.com/studywolf/pydmps). His blog which explains DMPs pretty well can be found [here](https://studywolf.wordpress.com/category/robotics/dynamic-movement-primitive/).
## References
1. "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
2. PyDMPs (from DeWolf, 2013): https://github.com/studywolf/pydmps
3. "Biologically-inspired Dynamical Systems for Movement Generation: Automatic Real-time Goal Adaptation and Obstacle Avoidance", Hoffmann et al., 2009
4. "Policy Search for Motor Primitives in Robotics", Kober et al., 2010
## TODO
- [ ] implement "Orientation in Cartesian Space Dynamic Movement Primitives", Ude et al., 2014
- [ ] implement "Action Sequencing using Dynamic Movement Primitives", Nemec et al., 2011
- [ ] implement "A Generalized Path Integral Control Approach to Reinforcement Learning", Theodorou et al., 2010
- [ ] test DMP with NN
@@ -0,0 +1,15 @@
# import canonical systems
from .canonical_systems import *
# import basis functions
from .basis_functions import *
# import forcing terms
from .forcing_terms import *
# import dynamic movement primitives
from .dmp import *
from .discrete_dmp import *
from .rhythmic_dmp import *
from .biodiscrete_dmp import *
@@ -0,0 +1,94 @@
#!/usr/bin/env python
"""Define basis functions used in the forcing terms in dynamic movement primitives
This file implements basis functions used for discrete and rhythmic dynamic movement primitives.
"""
from abc import ABCMeta, abstractmethod
import torch
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class BF(torch.nn.Module):
r"""Basis function used in the forcing terms
"""
__metaclass__ = ABCMeta
def __init__(self):
super(BF, self).__init__()
class EBF(BF):
r"""Exponential basis function
This basis function is given by the formula:
.. math:: \psi(s) = \exp \left( - \frac{1}{2 \sigma^2} (s - c)^2 \right)
where :math:`c` is the center, and :math:`\sigma` is the width of a normal distribution.
This is often used for discrete DMPs.
"""
def __init__(self, center=0, sigma=1., h=None):
"""Initialize basis function
Args:
center (float, torch.Tensor): center of the distribution
sigma (float, torch.Tensor): width of the distribution
h (float, torch.Tensor): concentration/precision of the basis fct (h = 1/(2*\sigma^2)).
if h is not provided, it will check sigma.
"""
super(EBF, self).__init__()
if isinstance(center, torch.Tensor):
pass
self.c = center
if h is None:
self.h = 1. / (2*sigma**2) # measure the concentration
else:
self.h = h
def forward(self, s):
if isinstance(s, torch.Tensor):
s = s[:, None]
return torch.exp(-self.h * (s - self.c)**2)
class CBF(BF):
r"""Circular basis function (aka von Mises basis function)
This basis function is given by the formula:
.. math:: \psi(s) = \exp \left( h (\cos(s - c) - 1) \right)
where :math:`c` is the center, and :math:`h` is a measure of concentration.
This is often used for rhythmic DMPs.
"""
def __init__(self, center=0, h=1.):
"""Initialize basis function
Args:
center (float, torch.Tensor): center of the basis fct
h (float, torch.Tensor): concentration/precision of the basis fct
"""
super(CBF, self).__init__()
self.c = center
self.h = h
def forward(self, s):
if isinstance(s, torch.Tensor):
s = s[:, None]
# return torch.exp(self.h * torch.cos(s - self.c) - 1) # this is bad as it is not bounded as we increase
# the number of basis functions.
return torch.exp(self.h * torch.cos(s - self.c) - self.h)
@@ -0,0 +1,268 @@
#!/usr/bin/env python
"""Define the biologically-inspired discrete dynamic movement primitive (as described in [1,2])
References:
[1] "Biologically-inspired Dynamical Systems for Movement Generation: Automatic Real-time Goal Adaptation
and Obstacle Avoidance", Hoffmann et al., 2009
[2] "Learning and Generalization of Motor Skills by Learning from Demonstration", Pastor et al., 2009
"""
import numpy as np
import torch
from pyrobolearn.models.dmp.dmpytorch.discrete_dmp import DiscreteDMP
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class BioDiscreteDMP(DiscreteDMP):
r"""Biologically-inspired Discrete DMPs
One of the main problems with the initial DMP formulation is when some goal coordinates coincide with their
corresponding initial position coordinates, it results in an inappropriate rescaling when displacing a little bit
the goal.
To deal with this problem, a new formulation of the transformation system was proposed in [2] and is given by:
.. math:: \tau^2 \ddot{y} = K (g - y) - D \tau \dot{y} - K(g - y_0)s + K f(s)
where :math:`\tau` is a scaling factor that allows to slow down or speed up the reproduced movement, :math:`K`
is the stiffness coefficient, :math:`D` is the damping coefficient, :math:`y, \dot{y}, \ddot{y}` are the position,
velocity, and acceleration of a DoF, and :math:`f(s)` is the non-linear forcing term.
The forcing term is expressed as:
.. math:: f(s) = \frac{\sum_i \psi_i(s) w_i}{ \sum_j \psi_j(s)} s
Properties (from [2]):
* Invariant under affine transformation
* Movement generalization to new targets
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
[2] "Biologically-inspired Dynamical Systems for Movement Generation: Automatic Real-time Goal Adaptation
and Obstacle Avoidance", Hoffmann et al., 2009
[3] "Learning and Generalization of Motor Skills by Learning from Demonstration", Pastor et al., 2009
"""
def __init__(self, num_dmps, num_basis, dt=0.01, y0=0, goal=1,
forces=None, stiffness=None, damping=None):
"""Initialize the discrete DMP
Args:
num_dmps (int): number of DMPs
num_basis (int): number of basis functions
dt (float): step integration for Euler's method
y0 (float, np.array): initial position(s)
goal (float, np.array): goal(s)
forces (list, ForcingTerm): the forcing terms (which can have different basis functions)
stiffness (float): stiffness coefficient
damping (float): damping coefficient
"""
# if stiffness is None and damping is None:
# # from paper [2]
# stiffness = 150 * np.ones(num_dmps)
# damping = 2 * np.sqrt(stiffness)
self.cst = 0.75 # this depends on the K and D value
super(BioDiscreteDMP, self).__init__(num_dmps, num_basis, dt=dt, y0=y0, goal=goal,
forces=forces, stiffness=stiffness, damping=damping)
def step(self, s=None, tau=1.0, error=0.0, forcing_term=None, new_goal=None, external_force=None,
rescale_force=True):
"""Run the DMP transformation system for a single time step.
Args:
s (None, float): the phase value. If None, it will use the canonical system.
tau (float): Increase tau to make the system slower, and decrease it to make it faster
error (float): optional system feedback
forcing_term (np.ndarray): if given, it will replace the forcing term (shape [dmp,])
new_goal (np.ndarray): new goal (of shape [num_dmps,])
"""
# system feedback
error_coupling = 1.0 / (1.0 + error)
# get phase from canonical system
if s is None:
s = self.cs.step(tau=tau, error_coupling=error_coupling)
elif not isinstance(s, (float, int)):
raise TypeError("Expecting the phase 's' to be a float or integer. Instead, I got {}".format(type(s)))
# check if same phase as before
if s == self.prev_s:
return self.y, self.dy, self.ddy
if new_goal is None:
new_goal = self.goal
else:
new_goal = new_goal + self.cst * (new_goal - self.goal)
# save previous position and velocity
prev_y, prev_dy = self.y.clone(), self.dy.clone()
# for each DMP, solve transformation system equation using Euler's method
for d in range(self.num_dmps):
# compute forcing term
if forcing_term is None:
f = self.forces[d](s) + self.K[d] * s * (self.goal[d] - new_goal[d])
else:
f = forcing_term[d]
# DMP acceleration
self.ddy[d] = self.K[d]/(tau**2) * (new_goal[d] - self.y[d]) - self.D[d]/tau * self.dy[d] + f/(tau**2)
if external_force is not None:
self.ddy[d] += external_force[d]
self.dy[d] += self.ddy[d] / tau * self.dt * error_coupling
self.y[d] += self.dy[d] * self.dt * error_coupling
# return self.y, self.dy, self.ddy
return prev_y, prev_dy, self.ddy
def _check_offset(self):
"""No need to check for an offset with this class"""
pass
def generate_goal(self, y0=None, dy0=None, ddy0=None, f0=None):
"""
Generate the goal from the initial positions, velocities, accelerations, and forces.
Args:
y0 (float[M], None): initial positions. If None, it will take the default initial positions.
dy0 (float[M], None): initial velocities. If None, it will take the default initial velocities.
ddy0 (float[M], None): initial accelerations. If None, it will take the default initial accerelations.
f0 (float[M], None): initial forcing terms. If None, it will compute it based on the learned weights.
You can also give `dmp.f_target[:,0]` to get the correct goal.
Returns:
float[M]: goal position for each DMP.
"""
if y0 is None:
y0 = self.y0
if dy0 is None:
dy0 = self.dy0
if ddy0 is None:
ddy0 = self.ddy0
if f0 is None:
s0 = self.cs.init_phase
f0 = self.get_forcing_term(s0)
return 1/self.K * (ddy0 + self.D * dy0 + self.K * y0 - self.K * f0)
# Tests
if __name__ == '__main__':
import matplotlib.pyplot as plt
# tests basis functions
num_basis = 100
# Test Biologically-inspired DMP
t = torch.linspace(0., 1., 100)
y_d = torch.sin(np.pi * t)
new_goal = torch.tensor([[0.8, -0.25],
[0.8, 0.25],
[1.2, -0.25]])
discrete_dmp = DiscreteDMP(num_dmps=2, num_basis=num_basis)
discrete_dmp.imitate(torch.stack([t, y_d]))
y, dy, ddy = discrete_dmp.rollout()
init_points = torch.stack([discrete_dmp.y0, discrete_dmp.goal])
# print(discrete_dmp.generate_goal())
# print(discrete_dmp.generate_goal(f0=discrete_dmp.f_target[:,0]))
y = y.detach().numpy() # convert to numpy
# check with standard discrete DMP when rescaling the goal
plt.subplot(1, 3, 1)
plt.title('Initial discrete DMP')
plt.scatter(init_points[:, 0], init_points[:, 1], color='b')
plt.scatter(new_goal[:, 0], new_goal[:, 1], color='r')
plt.plot(y[0], y[1], 'b', label='original')
plt.subplot(1, 3, 2)
plt.title('Rescaled discrete DMP')
plt.scatter(init_points[:, 0], init_points[:, 1], color='b')
plt.scatter(new_goal[:, 0], new_goal[:, 1], color='r')
plt.plot(y[0], y[1], 'b', label='original')
for g in new_goal:
y, dy, ddy = discrete_dmp.rollout(new_goal=g)
plt.plot(y[0].detach().numpy(), y[1].detach().numpy(), 'g', label='scaled')
plt.legend(['original', 'scaled'])
# change goal with biologically-inspired DMP
new_goal = torch.tensor([[0.8, -0.25],
[0.8, 0.25],
[0.4, 0.1],
[5., 0.15],
[1.2, -0.25],
[-0.8, 0.1],
[-0.8, -0.25],
[5., -0.25]])
bio_dmp = BioDiscreteDMP(num_dmps=2, num_basis=num_basis)
bio_dmp.imitate(torch.stack([t, y_d]))
y, dy, ddy = bio_dmp.rollout()
init_points = torch.stack([bio_dmp.y0, bio_dmp.goal])
y = y.detach().numpy() # convert to numpy
plt.subplot(1, 3, 3)
plt.title('Biologically-inspired DMP')
plt.scatter(init_points[:, 0], init_points[:, 1], color='b')
plt.scatter(new_goal[:, 0], new_goal[:, 1], color='r')
plt.plot(y[0], y[1], 'b', label='original')
for g in new_goal:
y, dy, ddy = bio_dmp.rollout(new_goal=g)
y = y.detach().numpy() # convert to numpy
plt.plot(y[0], y[1], 'g', label='scaled')
plt.legend(['original', 'scaled'])
plt.show()
# changing goal at the middle
y_list = []
for g in new_goal:
bio_dmp.reset()
y_traj = torch.zeros(2, 100)
for t in range(100):
if t < 30:
y, dy, ddy = bio_dmp.step()
else:
y, dy, ddy = bio_dmp.step(new_goal=g)
y_traj[:, t] = y
y_list.append(y_traj)
for y in y_list:
y = y.detach().numpy() # convert to numpy
plt.plot(y[0], y[1])
plt.scatter(bio_dmp.y0[0], bio_dmp.y0[1], color='b')
plt.scatter(new_goal[:, 0], new_goal[:, 1], color='r')
plt.title('change goal at the middle')
plt.show()
# changing goal at the middle but with a moving goal
g = torch.cat((torch.arange(1.0, 2.0, 0.1).reshape(10, -1),
torch.arange(0.0, 1.0, 0.1).reshape(10, -1)), dim=1)
bio_dmp.reset()
y_traj = np.zeros((2, 100))
y_list = []
for t in range(100):
y, dy, ddy = bio_dmp.step(new_goal=g[int(t/10)])
y = y.detach().numpy() # convert to numpy
y_traj[:, t] = y
if (t % 10) == 0:
y_list.append(y)
y_list = np.array(y_list)
plt.plot(y_traj[0], y_traj[1])
plt.scatter(bio_dmp.y0[0], bio_dmp.y0[1], color='b')
plt.scatter(g[:, 0], g[:, 1], color='r')
plt.scatter(y_list[:, 0], y_list[:, 1], color='g')
plt.title('moving goal')
plt.show()
@@ -0,0 +1,228 @@
#!/usr/bin/env python
"""Define canonical systems for dynamic movement primitives
This file implements canonical systems for discrete and rhythmic dynamic movement primitives.
"""
from abc import ABCMeta, abstractmethod
import numpy as np
import torch
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class CS(object):
r"""Canonical System.
A canonical system (CS) drives a dynamic movement primitive (DMP) by providing a phase variable [1].
The phase variable was introduced to avoid an explicit dependency with time in the DMP equations. Canonical
systems can be categorized in two main categories:
* discrete CS: used for discrete movements (such as reaching, pushing/pulling, hitting, etc)
* rhythmic CS: used for rhythmic movements (such as walking, running, dribbling, sewing, flipping a pancake, etc)
Each of these systems are described by differential equations which are solved using Euler's method.
See their corresponding classes `DiscreteCS` and `RhythmicCS` for more information.
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
"""
__metaclass__ = ABCMeta
def __init__(self, dt=0.01, T=1.):
"""Initialize the canonical system.
Args:
dt (float): the time step used in Euler's method when solving the differential equation
A very small step will lead to a better accuracy but will take more time.
"""
# set variables
self.dt = dt
self.T = T
self.timesteps = int(T / self.dt)
# rescale integration step (same as torch.linspace(0.,T.,timesteps) instead of torch.arange(0,T,dt))
self.dt = self.T / (self.timesteps - 1.)
self.init_phase = 1.0
self.s = 1.0
# reset the phase variable
self.reset()
@abstractmethod
def step(self, tau=1.0, error_coupling=1.0):
"""Perform a step using Euler's method. This needs to be implemented in the child classes."""
raise NotImplementedError
def reset(self):
"""Reset the phase variable"""
self.s = self.init_phase
return self.s
def rollout(self, tau=1.0, error_coupling=1.0):
"""Generate phase variable in an open loop fashion.
Args:
tau (float): Increase tau to make the system slower, and decrease it to make it faster
error_coupling (float): slow down if the error is > 1
"""
timesteps = int(self.timesteps * tau)
self.s_track = torch.zeros(timesteps)
# reset
self.reset()
# roll
for t in range(timesteps):
self.s_track[t] = self.s
self.step(tau, error_coupling)
return self.s_track
class DiscreteCS(CS):
r"""Discrete Canonical System.
The discrete canonical system drives the various DMPs by providing the phase variable at each time step, and is
given by:
.. math:: \tau \dot{s} = - \alpha_s s
where :math:`\tau` is a scaling factor that allows to slow down or speed up the movement, :math:`s` is the phase
variable that drives the DMP, and :math:`\alpha_s` is a predefined constant.
This differential equation is solved using Euler's method.
This version is used for discrete movements, where :math:`s` starts from 1 and converge to 0 as time progresses.
The phase variable was introduced to avoid an explicit dependency of time in the DMP equations.
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
"""
def __init__(self, alpha_s=1, dt=0.01):
super(DiscreteCS, self).__init__(dt=dt, T=1.0)
self.alpha_s = alpha_s
def reset(self):
"""Reset the phase variable"""
self.s = self.init_phase
return self.s
def step(self, tau=1.0, error_coupling=1.0):
"""Generate phase value for discrete movements.
The phase variable :math:`s` is generated by solving :math:`\tau \dot{s} = - \alpha_s s` using Euler's method.
This phase decays from 1 to 0.
Args:
tau (float): Increase tau to make the system slower, and decrease it to make it faster
error_coupling (float): slow down if the error is > 1
Returns:
float: phase value
"""
s = self.s
self.s += (-self.alpha_s/tau * self.s * error_coupling) * self.dt
# return self.s
return s
class RhythmicCS(CS):
r"""Rhythmic Canonical System.
The rhythmic canonical system drives the various DMPs by providing a phase variable that is periodic [1]. It is
used for rhythmic movements (such as walking, dribbling, sewing, etc.) and is given by:
.. math:: \tau \dot{s} = 1
where :math:`\tau` is a scaling factor that allows to slow down or speed up the movement, :math:`s` is the phase
variable that drives the DMP. This differential equation is solved using Euler's method.
Rhythmic canonical systems can also be coupled with each other as done in [2] to synchronize various DMPs.
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
[2] "A Framework for Learning Biped Locomotion with Dynamical Movement Primitives", Nakanishi et al., 2004
"""
def __init__(self, dt=0.01):
super(RhythmicCS, self).__init__(dt=dt, T=2*np.pi)
self.init_phase = 0.0
def reset(self):
"""Reset the phase variable"""
self.s = self.init_phase
return self.s
def step(self, tau=1.0, error_coupling=1.0):
r"""Generate phase value for rhythmic movements.
The phase variable :math:`s` is generated by solving :math:`\tau \dot{s} = 1` using Euler's method.
Args:
tau (float): Increase tau to make the system slower, and decrease it to make it faster
error_coupling (float): slow down if the error is > 1
Returns:
float: phase value
"""
s = self.s
self.s += (1./tau * error_coupling) * self.dt
# return self.s
return s
class RhythmicNetworkCS(CS):
r"""Rhythmic Network CS.
In this version, instead of having one canonical system that drives all the various DMPs, we have several
canonical systems coupled with each other, and where each one of them is associated to a particular DMP.
The evolution of the phase variable :math:`\phi` of the system :math:`i` is given by:
.. math:: \dot{\phi}_i = \omega_i + \sum_j a_j w_{ij} \sin(\phi_j - \phi_i - \varphi_{ij})
where :math:`\omega` is the desired angular velocity (desired frequency), :math:`w_{ij}` are the coupling weights,
:math:`\varphi_{ij}` are the phase biases, and :math:`a_j` are the amplitudes of the other systems :math:`j`.
This formulation is similar to Central Pattern Generators (CPGs), see [3].
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
[2] "A Framework for Learning Biped Locomotion with Dynamical Movement Primitives", Nakanishi et al., 2004
[3] "Central pattern generators for locomotion control in animals and robots: a review", Ijspeert, 2008
"""
def __init__(self, dt=0.01):
super(RhythmicNetworkCS, self).__init__(dt=dt)
# Tests
if __name__ == '__main__':
import matplotlib.pyplot as plt
# tests canonical systems
discrete_cs = DiscreteCS()
rhythmic_cs = RhythmicCS()
# check tau
plt.subplot(1, 2, 1)
plt.title('Discrete CS')
for tau in [1., 0.5, 2.]:
rollout = discrete_cs.rollout(tau=tau).numpy()
plt.plot(np.linspace(0, 1., len(rollout)), rollout, label='tau='+str(tau))
plt.legend()
plt.subplot(1, 2, 2)
plt.title('Rhythmic CS')
for tau in [1., 0.5, 2.]:
rollout = rhythmic_cs.rollout(tau=tau).numpy()
plt.plot(np.linspace(0, 1., len(rollout)), rollout, label='tau='+str(tau))
plt.legend()
plt.show()
@@ -0,0 +1,149 @@
#!/usr/bin/env python
"""Define the discrete dynamic movement primitive.
"""
import numpy as np
import torch
from pyrobolearn.models.dmp.dmpytorch.canonical_systems import DiscreteCS
from pyrobolearn.models.dmp.dmpytorch.forcing_terms import DiscreteForcingTerm
from pyrobolearn.models.dmp.dmpytorch.dmp import DMP
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class DiscreteDMP(DMP):
r"""Discrete Dynamic Movement Primitive
Discrete DMPs have the same mathematical formulation as general DMPs, which is given by:
.. math:: \tau^2 \ddot{y} = K (g - y) - D \tau \dot{y} + f(s) (g - y0)
where :math:`\tau` is a scaling factor that allows to slow down or speed up the reproduced movement, :math:`K`
is the stiffness coefficient, :math:`D` is the damping coefficient, :math:`y, \dot{y}, \ddot{y}` are the position,
velocity, and acceleration of a DoF, and :math:`f(s)` is the non-linear forcing term.
However, the forcing term in the case of discrete DMPs is given by:
.. math:: f(s) = \frac{\sum_i \psi_i(s) w_i}{\sum_i \psi_i(s)} s
where :math:`w` are the learnable weight parameters, and :math:`\psi` are the basis functions evaluated at the
given input phase variable :math:`s`, :math:`g` is the goal, and :math:`y_0` is the initial position. Note that
as the phase converges to 0, the forcing term also converges to that value.
The basis functions (in the discrete case) are given by:
.. math:: \psi_i(s) = \exp \left( - \frac{1}{2 \sigma_i^2} (x - c_i)^2 \right)
where :math:`c_i` is the center of the basis function :math:`i`, and :math:`\sigma_i` is its width.
Also, the canonical system associated with this transformation system is given by:
.. math:: \tau \dot{s} = - \alpha_s s
where :math:`\tau` is a scaling factor that allows to slow down or speed up the movement, :math:`s` is the phase
variable that drives the DMP, and :math:`\alpha_s` is a predefined constant.
All these differential equations are solved using Euler's method.
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
"""
def __init__(self, num_dmps, num_basis, dt=0.01, y0=0, goal=1,
forces=None, stiffness=None, damping=None):
"""Initialize the discrete DMP
Args:
num_dmps (int): number of DMPs
num_basis (int, int[M]): number of basis functions, or list of number of basis functions.
dt (float): step integration for Euler's method
y0 (float, float[M]): initial position(s)
goal (float, float[M]): goal(s)
forces (list, ForcingTerm): the forcing terms (which can have different basis functions)
stiffness (float): stiffness coefficient
damping (float): damping coefficient
"""
# create discrete canonical system
cs = DiscreteCS(dt=dt)
# create forcing terms (each one contains the basis functions and trainable weights)
if forces is None:
if isinstance(num_basis, int):
forces = [DiscreteForcingTerm(cs, num_basis) for _ in range(num_dmps)]
else:
if not isinstance(num_basis, (list, tuple)):
raise TypeError("Expecting 'num_basis' to be an int, list, or tuple of ints.")
if len(num_basis) != num_dmps:
raise ValueError("The length of th list of number of basis doesn't match the number of DMPs")
forces = [DiscreteForcingTerm(cs, n_basis) for n_basis in num_basis]
# call super class constructor
super(DiscreteDMP, self).__init__(canonical_system=cs, forces=forces, y0=y0, goal=goal,
stiffness=stiffness, damping=damping)
def get_scaling_term(self, new_goal=None):
"""
Return the scaling term for the forcing term.
Args:
new_goal (float, float[M], None): the new goal position. If None, it will be the current goal.
Returns:
float, float[M]: scaling term
"""
if new_goal is None:
new_goal = self.goal
return (new_goal - self.y0) / (self.goal - self.y0)
def _generate_goal(self, y_des):
"""Generate the goal for path imitation.
Args:
y_des (np.array): the desired trajectory to follow with shape [num_dmps, timesteps]
Returns:
float[M]: goal position
"""
return torch.clone(y_des[:, -1])
# Tests
if __name__ == '__main__':
import matplotlib.pyplot as plt
# tests canonical systems
discrete_cs = DiscreteCS()
# tests basis functions
num_basis = 20
discrete_f = DiscreteForcingTerm(discrete_cs, num_basis)
# tests forcing terms
force = torch.sin(torch.linspace(0, 2*np.pi, 100))
discrete_f.train(force, plot=True)
# Test discrete DMP
discrete_dmp = DiscreteDMP(num_dmps=1, num_basis=num_basis)
t = torch.linspace(-6, 6, 100)
y_target = 1. / (1 + torch.exp(-t))
discrete_dmp.imitate(y_target)
y, dy, ddy = discrete_dmp.rollout()
plt.plot(y_target.numpy(), label='y_target')
plt.plot(y[0].detach().numpy(), label='y_pred')
# plt.plot(dy[0])
# plt.plot(ddy[0])
y, dy, ddy = discrete_dmp.rollout(new_goal=torch.tensor([2.]))
plt.plot(y[0].detach().numpy(), label='y_scaled')
plt.title('Discrete DMP')
plt.legend()
plt.show()
+671
View File
@@ -0,0 +1,671 @@
#!/usr/bin/env python
"""Define the general dynamic movement primitive abstract class.
This file implements the DMP abstract class from which all dynamic movement primitive classes inherit from.
"""
import numpy as np
import torch
import copy
import scipy.interpolate
from pyrobolearn.models.dmp.dmpytorch.canonical_systems import CS
from pyrobolearn.models.dmp.dmpytorch.forcing_terms import ForcingTerm
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse", "Travis DeWolf"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class DMP(object):
r"""Dynamic Movement Primitive
Dynamic movement primitives (DMPs) are a set of differential equations (for each degree of freedoms (DoFs), i.e.
general coordinates) that encodes a movement [1]. It is thought that movement primitives are the building blocks
of a movement, and several evidences show that such modules exist in animals [2].
DMPs are often formulated as a 2nd-order differential equation:
.. math:: \tau^2 \ddot{y} = \alpha ( \beta (g - y) - \dot{y}) + f(s)
or sometimes, as a first-order differential system:
.. math::
\tau \dot{z} &= \alpha ( \beta (g - y) - z) + f(s) \\
\tau \dot{y} &= z
They can also be rewritten as:
.. math:: \tau^2 \ddot{y} = K (g - y) - D \tau \dot{y} + f(s)
where :math:`\tau` is a scaling factor that allows to slow down or speed up the reproduced movement, :math:`K`
is the stiffness coefficient, :math:`D` is the damping coefficient, :math:`y, \dot{y}, \ddot{y}` are the position,
velocity, and acceleration of a DoF, and :math:`f(s)` is the non-linear forcing term. These equations are also
known as the transformation systems and represent, with the canonical system, DMPs.
All of the above formulations are equivalent to each other. However, in my humble opinion, the last equation
depicts better what the transformation system constitutes; it is a unit-mass spring-damper system or PD controller
with a forcing term. This last term is non-linear and can be learned from the demonstrations.
If the forcing is zero, then the differential equation is stable, and the position :math:`y` converges to the goal.
The stiffness and damping coefficients (:math:`K` and :math:`D`) are often selected such that the whole system
(without the forcing term) is critically damped (:math:`D = 2 \sqrt{K}`). Other behaviors can be obtained by
selecting the stiffness and damping coefficient such that we obtain:
* an undamped system: :math:`D = 0` or :math:`K \rightarrow \infty`
* an underdamped system: :math:`D < 2 \sqrt{K}`
* a critically damped system: :math:`D = 2 \sqrt{K}`
* an overdamped system: :math:`D > 2 \sqrt{K}`
Because the last formulation is more intuitive (at least for me), it will be used in this class.
Imitation is performed by learning the forcing term.
DMPs can be categorized in two main categories:
* discrete DMP: used to represent discrete movements such as such as reaching, pushing/pulling, etc.
* rhythmic DMP: used to represent rhythmic movements such as walking, running dribbling, sewing, etc.
DMP have the following nice properties:
* translation invariant
* linear parameters but still allows to represent non-linear movements
Here are few limitations/shortcomings:
* hard to couple sensory information with it
* have to come up with the number of basis functions
For a more biologically-inspired DMP [5] which allows to adapt the goal in real-time and a better rescaling, see
the `BioDMP` class.
Note that this code was inspired by the `pydmps` code [3, 4], but differ in several ways, notably:
- we undertake a more object-oriented programming (OOP) approach
- we use pytorch instead of numpy which allows to use automatic differentiation, and allows to use deep learning
tools with DMPs. TODO: need to check if the gradient vanishes...
- the equations are a little bit differents (e.g. :math:`tau`) in which we use the ones presented in the refs
- we decouple the Euler's method time step with the time step for the number of data points
- timesteps: we go from 0 to T included, while DeWolf goes from 0 to T-1
- we use array operation instead of iterating over each element to update them
- we enforce consistency between the various methods and data structures
- we implement `BioDMP` which allows to adapt and rescale the goal in real-time based on [4]
- we implemented DMP sequencing based on [5]
- we implemented DMP that can be used with orientations based on [7]
- phase nodes which allows to couple phases, such as done in [8] for locomotion
- it can be used with RL algorithms, notably PoWER [9] and PI^2 [10]
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
[2] "Motor primitives in vertebrates and invertebrates", Flash et al., 2005
[3] Tutorials on DMP: https://studywolf.wordpress.com/category/robotics/dynamic-movement-primitive/
[4] PyDMPs (from DeWolf, 2013): https://github.com/studywolf/pydmps
[5] "Biologically-inspired Dynamical Systems for Movement Generation: Automatic Real-time Goal Adaptation
and Obstacle Avoidance", Hoffmann et al., 2009
[6] "Action Sequencing using Dynamic Movement Primitives", Nemec et al., 2011
[7] "Orientation in Cartesian Space Dynamic Movement Primitives", Ude et al., 2014
[8] "A Framework for Learning Biped Locomotion with Dynamical Movement Primitives", Nakanishi et al., 2004
[9] "Policy Search for Motor Primitives in Robotics", Kober et al., 2010
[10] "A Generalized Path Integral Control Approach to Reinforcement Learning", Theodorou et al., 2010
"""
def __init__(self, canonical_system, forces, y0=0, goal=1, stiffness=None, damping=None):
"""Initialize the DMP.
Args:
canonical_system (CS): canonical system which drives the DMP transformation system
forces (list): list of forcing terms (one forcing term for each DMP). Each forcing term can have
different number of basis functions.
y0 (float, float[M]): initial state of DMPs
goal (float, float[M]): goal state of DMPs
stiffness (float): stiffness term in the transformation system for DMPs
damping (float): damping term in the transformation system for DMPs
"""
self.cs = canonical_system
if isinstance(forces, ForcingTerm):
forces = [forces]
elif isinstance(forces, (list, tuple)):
for force in forces:
if not isinstance(force, ForcingTerm):
raise TypeError("An item in the iterable is not an instance of `ForcingTerm`.")
else:
raise TypeError("Expecting forcing term to be an instance of ForcingTerm or a list/tuple of ForcingTerm")
self.forces = forces
self.num_dmps = len(forces)
self.dt = self.cs.dt
self.timesteps = self.cs.timesteps
# check initial and goal positions # TODO use property to set them
self.y0 = y0
self.dy0 = torch.zeros(self.num_dmps)
self.ddy0 = torch.zeros(self.num_dmps)
self.goal = goal
self._check_offset()
self.y, self.dy, self.ddy = self.y0, self.dy0, self.ddy0
# set stiffness and damping coefficient (if not specified, make them critically damped, i.e. D=2\sqrt{K})
self.D = torch.ones(self.num_dmps) * 25. if damping is None else damping
self.K = self.D**2 / 4. if stiffness is None else stiffness
# set up the DMP system
self.prev_s = self.cs.init_phase
self.reset()
# target forcing term (keep a copy)
self.f_target = None
def __repr__(self):
return self.__class__.__name__
def __call__(self, *args, **kwargs):
return self.step(*args, **kwargs)
##############
# Properties #
##############
@property
def goal(self):
"""Return the goal position."""
return self._goal
@goal.setter
def goal(self, goal):
"""Set the goal position."""
if isinstance(goal, torch.Tensor):
goal = goal.clone().float()
elif isinstance(goal, (int, float)):
goal = torch.ones(self.num_dmps) * goal
elif isinstance(goal, (list, tuple)):
goal = torch.tensor(goal).float()
elif isinstance(goal, np.ndarray):
goal = torch.from_numpy(goal).float()
else:
raise TypeError("Expecting the goal to be a list/tuple/np.array/torch.Tensor of float/int, instead got: "
"{}".format(type(goal)))
self._goal = goal
@property
def y0(self):
"""Return the initial position."""
return self._y0
@y0.setter
def y0(self, y0):
"""Set the initial position."""
if isinstance(y0, torch.Tensor):
y0 = y0.clone().float()
elif isinstance(y0, (int, float)):
y0 = torch.ones(self.num_dmps) * y0
elif isinstance(y0, (list, tuple)):
y0 = torch.tensor(y0).float()
elif isinstance(y0, np.ndarray):
y0 = torch.from_numpy(y0).float()
else:
raise TypeError("Expecting the initial position to be a list/tuple/np.array/torch.Tensor of float/int, "
"instead got: {}".format(type(y0)))
self._y0 = y0
@property
def input_size(self):
"""Return the input size of the model."""
return 1 # 1 canonical system
@property
def output_size(self):
"""Return the output size of the model."""
return len(self.forces)
@property
def input_shape(self):
"""Return the input shape of the model."""
return tuple([self.input_size])
@property
def output_shape(self):
"""Return the output shape of the model."""
return tuple([self.output_size])
@property
def input_dim(self):
"""Return the input dimension of the model; i.e. len(input_shape)."""
return len(self.input_shape)
@property
def output_dim(self):
"""Return the output dimension of the model; i.e. len(output_shape)."""
return len(self.output_shape)
@property
def num_parameters(self):
"""Return the total number of parameters"""
return sum([force.num_parameters for force in self.forces])
##################
# Static Methods #
##################
@staticmethod
def copy(other, deep=False):
if not isinstance(other, DMP):
raise TypeError("Trying to copy an object which is not a DMP")
if deep:
return copy.deepcopy(other)
return copy.copy(other)
@staticmethod
def is_parametric():
"""Return True as a DMP has weights that need to be optimized."""
return True
@staticmethod
def is_linear():
"""Return True as a DMP is linear in terms of its weights (i.e. learnable parameters)"""
return True
@staticmethod
def is_recurrent():
"""Return False as a DMP is not a recurrent model."""
return False
@staticmethod
def is_sequential():
"""Return True as a DMP is a (temporal) sequential model."""
return True
@staticmethod
def is_probabilistic():
"""The DMP is a deterministic model."""
return False
@staticmethod
def is_discriminative():
"""The DMP is a discriminative model which predicts the output :math:`y` given the input :math:`x`"""
return True
@staticmethod
def is_generative():
"""The DMP is not a generative model."""
return False
###########
# Methods #
###########
def parameters(self):
"""Returns an iterator over the model parameters."""
for force in self.forces:
yield force.weights
def named_parameters(self):
"""Returns an iterator over the model parameters, yielding both the name and the parameter itself"""
for force in self.forces:
yield str(force), force.weights
def list_parameters(self):
"""Return a list of parameters"""
return list(self.parameters())
def hyperparameters(self):
"""Return an iterator over the hyper-parameters."""
yield self.K
yield self.D
# yield basis_functions
def named_hyperparameters(self):
"""Return an iterator over the hyper-parameters, yielding both the name and the hyper-parameter itself."""
yield "stiffness", self.K
yield "damping", self.D
def list_hyperparameters(self):
"""Return a list of hyper-parameters."""
return list(self.hyperparameters())
def get_vectorized_parameters(self, to_numpy=True):
"""Return a vectorized form (1 dimensional array) of the parameters."""
parameters = self.parameters()
vector = torch.cat([parameter.view(-1) for parameter in parameters])
# if to_numpy:
# return vector.detach().numpy()
return vector
def set_vectorized_parameters(self, vector):
"""Set the vector parameters."""
# convert the vector to torch array
# if isinstance(vector, np.ndarray):
# vector = torch.from_numpy(vector).float()
# set the parameters from the vectorized one
# idx = 0
# for parameter in self.parameters():
# size = parameter.nelement()
# parameter.data = vector[idx:idx+size].view(parameter.shape)
# idx += size
# set the parameters from the vectorized one
idx = 0
for force in self.forces:
size = force.weights.size
force.weights = vector[idx:idx+size].view(force.weights.shape)
idx += size
def get_damping_ratio(self):
r"""
Return the damping ratio :math:`\zeta = D / D_c` where :math:`D_c = 2 \sqrt{K}`.
* if :math:`\zeta` = 0, the system is undamped (i.e. no damping)
* if :math:`\zeta` < 1, the system is underdamped (i.e. there will be some oscillations)
* if :math:`\zeta` = 1, the system is critically damped (i.e. return to equilibrium as fast as possible
without oscillating).
* if :math:`\zeta` > 1, the system is overdamped (i.e. the system returns to equilibrium without oscillating
but might be slow depending on the damping value).
"""
return self.D / (2. * torch.sqrt(self.K))
def _check_offset(self):
"""Check to see if the initial position and goal are the same. If that is the case, offset slightly so that
the forcing term is not 0. Otherwise, look at the `BioDMP` class.
"""
self.goal[self.y0 == self.goal] += 1e-4
def get_scaling_term(self, new_goal=None):
# this is overridden by the child classes
return torch.ones(self.num_dmps)
def _generate_goal(self, y_des):
raise NotImplementedError()
def reset(self):
"""Reset the transformation and canonical systems"""
self.y = self.y0.clone()
self.dy = self.dy0.clone() # torch.zeros(self.num_dmps)
self.ddy = self.ddy0.clone()
self.prev_s = self.cs.reset()
def step(self, s=None, tau=1.0, error=0.0, forces=None, new_goal=None, external_force=None,
rescale_force=True):
"""Run the DMP transformation system for a single time step.
Args:
s (None, float): the phase value. If None, it will use the canonical system.
tau (float): Increase tau to make the system slower, and decrease it to make it faster
error (float): optional system feedback
forces (float[M]): if given, it will replace the forcing term (where `M` = number of DMPs)
new_goal (float[M]): new goal (where `M` = number of DMPs)
rescale_force (bool): if the given forcing term should be rescaled.
"""
# system feedback
error_coupling = 1.0 / (1.0 + error)
# get phase from canonical system
if s is None:
s = self.cs.step(tau=tau, error_coupling=error_coupling)
elif not isinstance(s, (float, int)):
raise TypeError("Expecting the phase 's' to be a float or integer. Instead, I got {}".format(type(s)))
# check if same phase as before
if s == self.prev_s:
return self.y, self.dy, self.ddy
if new_goal is None:
new_goal = self.goal
# save previous position and velocity
prev_y, prev_dy = self.y.clone(), self.dy.clone()
# compute scaling factor for the forcing term
scaling = self.get_scaling_term(new_goal)
# for each DMP, solve transformation system equation using Euler's method
for d in range(self.num_dmps):
# compute forcing term
if forces is None:
f = self.forces[d](s) * scaling[d]
# f = self.f_gen(s) * scaling[d]
else:
if rescale_force:
f = forces[d] * scaling[d]
else:
f = forces[d]
# DMP acceleration
self.ddy[d] = self.K[d]/(tau**2) * (new_goal[d] - self.y[d]) - self.D[d]/tau * self.dy[d] + f/(tau**2)
if external_force is not None:
self.ddy[d] += external_force[d]
self.dy[d] += self.ddy[d] / tau * self.dt * error_coupling
self.y[d] += self.dy[d] * self.dt * error_coupling
# return self.y, self.dy, self.ddy
return prev_y, prev_dy, self.ddy
def rollout(self, timesteps=None, tau=1.0, error=0.0, forces=None, new_goal=None, rescale_force=True,
**kwargs):
"""Generate position, velocity, and acceleration trajectories, no feedback is incorporated.
Args:
tau (float): Increase tau to make the system slower, and decrease it to make it faster
timesteps (None, int): the number of steps to perform
error (float): optional system feedback
forces (torch.Tensor): if given, it will replace the forcing term (shape [num_dmps, timesteps])
new_goal (torch.Tensor): new goal (of shape [num_dmps,])
Returns:
float[M,T]: y (position) trajectories
float[M,T]: dy (velocity) trajectories
float[M,T]: ddy (acceleration) trajectories
"""
# reset the canonical and transformation systems
self.reset()
if timesteps is None:
timesteps = int(self.timesteps * tau)
# set up tracking vectors
y_track = torch.zeros(self.num_dmps, timesteps)
dy_track = torch.zeros(self.num_dmps, timesteps)
ddy_track = torch.zeros(self.num_dmps, timesteps)
# for the other timesteps, solve DMP equation using Euler's method
for t in range(timesteps):
if forces is None:
y, dy, ddy = self.step(tau=tau, error=error, new_goal=new_goal, external_force=None)
else:
y, dy, ddy = self.step(tau=tau, error=error, forces=forces[:, t], new_goal=new_goal,
rescale_force=rescale_force)
# record timestep
y_track[:, t] = y
dy_track[:, t] = dy
ddy_track[:, t] = ddy
return y_track, dy_track, ddy_track
def train(self, f_target):
"""Train the forcing terms."""
# train each forcing term
if f_target.shape[0] != len(self.forces):
raise ValueError("Mismatch between the number of forcing terms")
# train each forcing term
for forces, target in zip(self.forces, f_target):
forces.train(target)
def imitate(self, y_des, dy_des=None, ddy_des=None, interpolation='cubic', plot=False):
"""Imitate a desired trajectory, and learn the parameters that best realizes it.
Args:
y_des (torch.Tensor): the desired position trajectories of each DMP with shape [num_dmps, timesteps]
dy_des (torch.Tensor): the desired velocities with shape [num_dmps, timesteps]
ddy_des (torch.Tensor): the desired accelerations with shape [num_dmps, timesteps]
interpolation (str): how to interpolate the data. Select between 'linear', 'cubic', and 'hermite'.
"""
if isinstance(y_des, np.ndarray):
y_des = torch.from_numpy(y_des).float()
if isinstance(dy_des, np.ndarray):
dy_des = torch.from_numpy(dy_des).float()
if isinstance(ddy_des, np.ndarray):
ddy_des = torch.from_numpy(ddy_des).float()
# set initial state and goal
if y_des.dim() == 1:
y_des = y_des.view(1, len(y_des))
self._y0 = torch.clone(y_des[:, 0])
self._goal = self._generate_goal(y_des)
self._check_offset()
timesteps = y_des.shape[1]
def interpolate(x, dt, period, timesteps, new_timesteps, interpolation=interpolation, return_gen=False):
# generate function to interpolate the desired trajectory
x = x.numpy()
t = np.linspace(0, period, timesteps)
if interpolation == 'linear': # use linear interpolation
path_gen = scipy.interpolate.interp1d(t, x, axis=-1)
elif interpolation == 'cubic': # use cubic spline interpolation
path_gen = scipy.interpolate.CubicSpline(t, x, axis=-1)
else: # TODO: implement hermite (see utils.interpolator.hermite)
raise ValueError("The requested interpolation has not been implemented. Select between 'linear' or "
"'cubic'")
if return_gen:
return path_gen
return torch.from_numpy(path_gen([t * self.dt for t in range(new_timesteps)])).float()
y_des = interpolate(y_des, dt=self.dt, period=self.cs.T, timesteps=timesteps,
new_timesteps=self.timesteps, interpolation=interpolation)
def diff(tensor):
if tensor.dim() == 1:
return y_des[1:] - y_des[:-1]
else:
return y_des[..., 1:] - y_des[..., :-1]
# compute desired velocity if necessary
if dy_des is None:
# calculate velocity of y_des
dy_des = diff(y_des) / self.dt
# add zero to the beginning of every row
dy_des = torch.cat((torch.zeros(self.num_dmps, 1), dy_des), dim=1)
else:
if dy_des.dim() == 1:
dy_des = dy_des.view(1, len(dy_des))
dy_des = interpolate(dy_des, self.dt, self.cs.T, dy_des.shape[1], self.timesteps,
interpolation=interpolation)
self.dy0 = torch.clone(dy_des[:, 0])
# compute desired acceleration if necessary
if ddy_des is None:
# calculate acceleration of y_des
ddy_des = diff(dy_des) / self.dt
# add zero to the beginning of every row
ddy_des = torch.cat((torch.zeros(self.num_dmps, 1), ddy_des), dim=1)
else:
if ddy_des.dim() == 1:
ddy_des = ddy_des.view(1, len(ddy_des))
ddy_des = interpolate(ddy_des, self.dt, self.cs.T, ddy_des.shape[1], self.timesteps,
interpolation=interpolation)
self.ddy0 = torch.clone(ddy_des[:, 0])
# find the force required to move along this trajectory (with shape [num_dmps, timesteps])
f_target = ddy_des - self.K.view(-1, 1) * (self.goal.view(-1, 1) - y_des) + self.D.view(-1, 1) * dy_des
# plot
if plot:
import matplotlib.pyplot as plt
plt.figure()
plt.plot(y_des[0].numpy(), 'b', label='pos')
plt.plot(dy_des[0].numpy(), 'g', label='vel')
plt.plot(ddy_des[0].numpy(), 'r', label='acc')
plt.plot(f_target[0].numpy(), 'k', label='force')
plt.legend()
plt.show()
# self.f_gen = interpolate(f_target, dt=self.dt, period=self.cs.T, timesteps=timesteps,
# new_timesteps=timesteps, interpolation=interpolation, return_gen=True)
# efficiently generate weights to realize f_target
self.f_target = f_target
self.train(f_target)
# reset the canonical and transformation systems
self.reset()
return y_des
def get_forces(self, s):
"""
Get the forcing terms based on the given phase value.
Args:
s (float, float[T]): phase value(s)
Returns:
float[M], float[M,T]: forcing terms
"""
return torch.tensor([self.forces[d](s) for d in range(self.num_dmps)])
def generate_goal(self, y0=None, dy0=None, ddy0=None, f0=None):
"""
Generate the goal from the initial positions, velocities, accelerations, and forces.
Args:
y0 (float[M], None): initial positions. If None, it will take the default initial positions.
dy0 (float[M], None): initial velocities. If None, it will take the default initial velocities.
ddy0 (float[M], None): initial accelerations. If None, it will take the default initial accerelations.
f0 (float[M], None): initial forcing terms. If None, it will compute it based on the learned weights.
You can also give `dmp.f_target[:,0]` to get the correct goal.
Returns:
float[M]: goal position for each DMP.
"""
if y0 is None:
y0 = self.y0
if dy0 is None:
dy0 = self.dy0
if ddy0 is None:
ddy0 = self.ddy0
if f0 is None:
s0 = self.cs.init_phase
f0 = self.get_forces(s0)
return 1/self.K * (ddy0 + self.D * dy0 + self.K * y0 - f0)
def sequence(self, model, mode=0):
"""
Define how to sequence with another DMP model.
Args:
model (DMP): DMP model
mode (int): specifies how to sequence the two DMP models.
Returns:
DMP: the sequenced model
References:
[1] "Action Sequencing using Dynamic Movement Primitives", Nemec et al., 2011
"""
if not isinstance(model, DMP):
raise TypeError("The given model is not an instance of DMP.")
pass
# def __rshift__(self, other):
# """
# Sequence DMP model with another learning model.
#
# Ref: "Action Sequencing using Dynamic Movement Primitives", Nemec et al., 2011
#
# :param other: another DMP model
# :return:
# """
# # If we sequence two DMP models
# if isinstance(other, DMP):
#
# else:
# # if it is another model, call the parent's method which knows how to sequence different models
# super(DMP, self).__rshift__(other)
@@ -0,0 +1,356 @@
#!/usr/bin/env python
"""Define the forcing terms used in dynamic movement primitives
This file implements the forcing terms used for discrete and rhythmic dynamic movement primitives.
"""
import matplotlib.pyplot as plt
from pyrobolearn.models.dmp.dmpytorch.canonical_systems import *
from pyrobolearn.models.dmp.dmpytorch.basis_functions import *
from pyrobolearn.models.dmp.dmpytorch.weight import WeightModule
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class ForcingTerm(torch.nn.Module):
r"""Forcing term used in DMPs
This basically computes the unscaled forcing term, i.e. a weighted sum of basis functions, which is given by:
.. math:: f(s) = \frac{ sum_{i} \psi_i(s) w_i }{ \sum_i \psi_i(s) }
where :math:`w` are the learnable weight parameters, and :math:`\psi` are the basis functions evaluated at the
given input phase variable :math:`s`.
"""
def __init__(self, weights, basis_functions):
"""
Initialize the forcing terms.
Args:
weights (torch.nn.Module): weight module.
basis_functions (BF): basis functions.
"""
# check that the arguments have the same length
super(ForcingTerm, self).__init__()
if not isinstance(weights, torch.nn.Module):
raise TypeError("Expecting the weights to be an instance of `torch.nn.Module`, instead got: "
"{}".format(type(weights)))
self._w = weights
if not isinstance(basis_functions, torch.nn.Module):
raise TypeError("Expecting the basis_functions to be an instance of `torch.nn.Module`, instead got: "
"{}".format(type(basis_functions)))
self.psi = basis_functions
@property
def weights(self):
"""Return the inner weights which might be a torch.Tensor or torch.nn.Module."""
if isinstance(self._w, WeightModule):
return self._w.weight
return self._w
@property
def num_parameters(self):
"""Return the number of parameters."""
weights = self.weights
if isinstance(weights, torch.Tensor):
return weights.numel()
return sum(p.numel() for p in weights.parameters())
@staticmethod
def is_linear():
return True
@staticmethod
def is_parametric():
return True
@staticmethod
def is_recurrent():
return False
def forward(self, s):
"""Compute the forcing term
Compute the value of the forcing term :math:`f(s)` at the given phase value :math:`s`.
Args:
s (float): phase value
Returns:
float: value of the forcing term at the given phase value
"""
psi_track = self.psi(s)
if psi_track.dim() == 1:
return torch.dot(psi_track, self._w(s)) / torch.sum(psi_track)
return (torch.mm(psi_track, self._w(s).unsqueeze(1))).squeeze(1) / torch.sum(psi_track, dim=1)
def weighted_basis(self, s):
"""Generate weighted basis
Returns:
np.array[T, M]: weighted basis
"""
return self.psi(s) * self._w(s)
def normalized_weighted_basis(self, s):
"""Generate normalized weighted basis
Args:
s (float): phase value
Returns:
np.array[T,M]: normalized weighted basis
"""
psi_track = self.psi(s)
return ((psi_track * self._w(s)).t() / torch.sum(psi_track, dim=1)).t()
def __str__(self):
return self.__class__.__name__
# To override in child classes
def train(self, f_target):
raise NotImplementedError
# alias
generate_weights = train
class DiscreteForcingTerm(ForcingTerm):
r"""Discrete Forcing Term
.. math:: f(s) = \frac{ sum_{i} \psi_i(s) w_i }{ \sum_i \psi_i(s) } s
where :math:`w` are the learnable weight parameters, and :math:`\psi` are the basis functions evaluated at the
given input phase variable :math:`s`.
This forcing term has the property that as the phase converges to 0, it also converges to 0, allowing the
linear part of the DMP equation to converge to the goal.
"""
def __init__(self, cs, num_basis):
"""Initialize the discrete forcing term.
Args:
cs (CS): discrete canonical system
num_basis (int): number of basis functions
"""
# set canonical system
if not isinstance(cs, DiscreteCS):
raise TypeError("Expecting 'cs' to be an instance of DiscreteCS")
self.cs = cs
# set num_basis
self.num_basis = num_basis
# create weights
weights = WeightModule(weight=torch.zeros(num_basis)) # default f=0
# desired activations throughout time
c = torch.linspace(0, cs.T, num_basis)
c = torch.exp(-cs.alpha_s * c)
# set variance of basis functions (this was found by trial and error by DeWolf)
h = torch.ones(num_basis) * num_basis**1.5 / c / cs.alpha_s
basis = EBF(center=c, h=h)
super(DiscreteForcingTerm, self).__init__(weights, basis)
def forward(self, s):
# call parent compute
force = super(DiscreteForcingTerm, self).forward(s)
# scale with phase s
return force * s
def train(self, f_target, plot=False):
"""Train the weights to match the given target forcing term
Generate a set of weights over the basis functions such that the target forcing term trajectory is matched.
Args:
f_target (np.array): the desired forcing term trajectory
"""
# calculate phase and basis functions
s_track = self.cs.rollout()
psi_track = self.psi(s_track) # shape=TxM
# efficiently calculate BF weights using LWR (Locally Weighted (Linear) Regression)
# spatial scaling term
if isinstance(self.weights, torch.Tensor):
for b in range(self.num_basis):
numerator = torch.sum(s_track * psi_track[:, b] * f_target)
denominator = torch.sum(s_track**2 * psi_track[:, b])
self.weights[b] = numerator / denominator
else:
raise NotImplementedError
# set nan to 0
if isinstance(self.weights, torch.Tensor):
self.weights[self.weights != self.weights] = 0.
# self.weights = np.nan_to_num(self.weights)
if plot:
# plot the basis function activations
plt.figure()
plt.subplot(211)
plt.plot(psi_track.numpy())
plt.title('basis functions')
# plot the desired forcing function vs approx for the first dmp
plt.subplot(212)
plt.title('discrete force')
plt.plot(f_target.numpy(), label='f_target', linewidth=2.5)
plt.plot(self.forward(s_track).detach().numpy(), label='f_pred', linewidth=2.5)
# weighted sum of basis functions
wps = self.weighted_basis(s_track)
plt.plot(wps.detach().numpy(), linewidth=0.5)
plt.legend()
plt.tight_layout()
plt.show()
class RhythmicForcingTerm(ForcingTerm):
r"""Rhythmic Forcing Term
.. math:: f(s) = \frac{ sum_{i} \psi_i(s) w_i }{ \sum_i \psi_i(s) } a
where :math:`w` are the learnable weight parameters, :math:`\psi` are the basis functions evaluated at the
given input phase variable :math:`s`, and :math:`a` is the amplitude.
When used with DMPs, it produces a limit cycle behavior.
"""
def __init__(self, cs, num_basis, amplitude=1.):
"""
Initialize the rhythmic forcing term.
Args:
cs (CS): rhythmic canonical system
num_basis (int): number of basis functions
amplitude (float): amplitude
"""
# set canonical system
if not isinstance(cs, RhythmicCS):
raise TypeError("Expecting 'cs' to be an instance of RhythmicCS")
self.cs = cs
# set num_basis and amplitude
self.num_basis = num_basis
self.a = amplitude
# create weights
weights = WeightModule(weight=torch.zeros(num_basis)) # default f=0
# set the centre of the Gaussian basis functions to be spaced evenly
c = torch.linspace(0, cs.T, num_basis + 1) # the '+1' is because it is rhythmic, c(0) = c(2pi)
c = c[:-1]
# set concentration of basis function (this was found by trial and error by DeWolf)
h = torch.ones(num_basis) * num_basis
# create basis functions
basis = CBF(center=c, h=h)
super(RhythmicForcingTerm, self).__init__(weights, basis)
def forward(self, s):
# call parent compute
force = super(RhythmicForcingTerm, self).forward(s)
# scale with amplitude and return it
return force * self.a
def train(self, f_target, plot=False):
"""Train the weights to match the given target forcing term
Generate a set of weights over the basis functions such that the target forcing term trajectory is matched.
Args:
f_target (np.array): the desired forcing term trajectory
plot (bool): If True, it will plot.
"""
# calculate phase and basis functions
s_track = self.cs.rollout()
psi_track = self.psi(s_track) # shape=TxM
# efficiently calculate BF weights using LWR (Locally Weighted (Linear) Regression)
if isinstance(self.weights, torch.Tensor):
for b in range(self.num_basis):
self.weights[b] = (torch.dot(psi_track[:, b], f_target) / (torch.sum(psi_track[:, b]))) # + 1e-10))
else:
raise NotImplementedError
if plot:
# plot the basis function activations
plt.figure()
plt.subplot(211)
plt.plot(psi_track.numpy())
plt.title('basis functions')
# plot the desired forcing function vs approx for the first dmp
plt.subplot(212)
plt.title('rhythmic force')
plt.plot(f_target.numpy(), label='f_target', linewidth=2.5)
plt.plot(self.forward(s_track).detach().numpy(), label='f_pred', linewidth=2.5)
wps = self.weighted_basis(s_track)
plt.plot(wps.detach().numpy(), linewidth=0.5)
plt.legend()
plt.tight_layout()
plt.show()
# Tests
if __name__ == '__main__':
# tests canonical systems
discrete_cs = DiscreteCS()
rhythmic_cs = RhythmicCS()
# plot canonical systems
plt.subplot(1, 2, 1)
plt.title('Discrete CS')
for tau in [1., 0.5, 2.]:
rollout = discrete_cs.rollout(tau=tau).numpy()
plt.plot(np.linspace(0, 1., len(rollout)), rollout, label='tau='+str(tau))
plt.legend()
plt.subplot(1, 2, 2)
plt.title('Rhythmic CS')
for tau in [1., 0.5, 2.]:
rollout = rhythmic_cs.rollout(tau=tau).numpy()
plt.plot(np.linspace(0, 1., len(rollout)), rollout, label='tau='+str(tau))
plt.legend()
plt.show()
# tests basis functions
num_basis = 20
discrete_f = DiscreteForcingTerm(discrete_cs, num_basis)
rhythmic_f = RhythmicForcingTerm(rhythmic_cs, num_basis)
plt.subplot(1, 2, 1)
rollout = discrete_cs.rollout()
plt.title('discrete basis fcts')
plt.plot(rollout.numpy(), discrete_f.psi(rollout).numpy())
plt.subplot(1, 2, 2)
rollout = rhythmic_cs.rollout()
plt.title('rhythmic basis fcts')
plt.plot(rollout.numpy(), rhythmic_f.psi(rollout).numpy())
plt.show()
# tests forcing terms
force = torch.sin(torch.linspace(0, 2*np.pi, 100))
discrete_f.train(force, plot=True)
force = torch.sin(torch.linspace(0, 2*np.pi, int(2*np.pi*100)))
rhythmic_f.train(force, plot=True)
@@ -0,0 +1,114 @@
#!/usr/bin/env python
"""Define the rhythmic dynamic movement primitive.
"""
import numpy as np
import torch
from pyrobolearn.models.dmp.dmpytorch.canonical_systems import RhythmicCS
from pyrobolearn.models.dmp.dmpytorch.forcing_terms import RhythmicForcingTerm
from pyrobolearn.models.dmp.dmpytorch.dmp import DMP
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class RhythmicDMP(DMP):
r"""Rhythmic Dynamic Movement Primitive
Rhythmic DMPs have the same mathematical formulation as general DMPs, which is given by:
.. math:: \tau^2 \ddot{y} = K (g - y) - D \tau \dot{y} + f(s)
where :math:`\tau` is a scaling factor that allows to slow down or speed up the reproduced movement, :math:`K`
is the stiffness coefficient, :math:`D` is the damping coefficient, :math:`y, \dot{y}, \ddot{y}` are the position,
velocity, and acceleration of a DoF, and :math:`f(s)` is the non-linear forcing term.
However, the forcing term in the case of rhythmic DMPs is given by:
.. math:: f(s) = \frac{\sum_i \psi_i(s) w_i}{\sum_i \psi_i(s)} a
where :math:`w` are the learnable weight parameters, and :math:`\psi` are the basis functions evaluated at the
given input phase variable :math:`s`, and :math:`a` is the amplitude.
The basis functions (in the rhythmic case) are given by:
.. math:: \psi_i(s) = \exp \left( - h_i (\cos(s - c_i) - 1) \right)
where :math:`c_i` is the center of the basis, and :math:`h_i` is a measure of concentration.
Also, the canonical system associated with this transformation system is given by:
.. math:: \tau \dot{s} = 1
where :math:`\tau` is a scaling factor that allows to slow down or speed up the movement, and :math:`s` is the
phase variable that drives the DMP.
All these differential equations are solved using Euler's method.
References:
[1] "Dynamical movement primitives: Learning attractor models for motor behaviors", Ijspeert et al., 2013
"""
def __init__(self, num_dmps, num_basis, dt=0.01, y0=0, goal=1,
forces=None, stiffness=None, damping=None):
"""Initialize the rhythmic DMP
Args:
num_dmps (int): number of DMPs
num_basis (int): number of basis functions
dt (float): step integration for Euler's method
y0 (float, np.array): initial position(s)
goal (float, np.array): goal(s)
forces (list, ForcingTerm): the forcing terms (which can have different basis functions)
stiffness (float): stiffness coefficient
damping (float): damping coefficient
"""
# create rhythmic canonical system
cs = RhythmicCS(dt=dt)
# create forcing terms (each one contains the basis functions and learnable weights)
if forces is None:
if isinstance(num_basis, int):
forces = [RhythmicForcingTerm(cs, num_basis) for _ in range(num_dmps)]
else:
if not isinstance(num_basis, (np.ndarray, list, tuple, set)):
raise TypeError("Expecting 'num_basis' to be an int, list, tuple, np.array or set.")
if len(num_basis) != num_dmps:
raise ValueError("The length of th list of number of basis doesn't match the number of DMPs")
forces = [RhythmicForcingTerm(cs, n_basis) for n_basis in num_basis]
# call super class constructor
super(RhythmicDMP, self).__init__(canonical_system=cs, forces=forces, y0=y0, goal=goal,
stiffness=stiffness, damping=damping)
def get_scaling_term(self, new_goal=None):
"""
Return the scaling term for the forcing term. For rhythmic DMPs it's non-diminishing, so this function just
returns 1.
"""
return torch.ones(self.num_dmps)
def _generate_goal(self, y_des):
"""Generate the goal for path imitation.
For rhythmic DMPs, the goal is the average of the desired trajectory.
Args:
y_des (float[M,T]): the desired trajectory to follow (with shape [num_dmps, timesteps])
Returns:
float[M]: goal positions (one for each DMP)
"""
goal = np.zeros(self.num_dmps)
for n in range(self.num_dmps):
num_idx = ~torch.isnan(y_des[n]) # ignore nan's when calculating goal
goal[n] = .5 * (y_des[n, num_idx].min() + y_des[n, num_idx].max())
return goal
@@ -0,0 +1,84 @@
#!/usr/bin/env python
"""Define weights used in the forcing terms in dynamic movement primitives
"""
import torch
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class WeightTensor(torch.nn.Module):
r"""Fixed weight vector
Weights used with the basis functions in the forcing terms.
"""
def __init__(self, weight=None, num_basis=None):
"""
Initialize the fixed weight vector.
Args:
weight (torch.Tensor, None): weight vector. If None, it will create a weight vector using the provided
:attr:`num_basis`.
num_basis (int): number of basis functions. This is used if no weight vector was given.
"""
super(WeightTensor, self).__init__()
if weight is None:
weight = torch.zeros(num_basis)
elif not isinstance(weight, torch.Tensor):
raise TypeError("Expecting the weight vector to be None or an instance of `torch.Tensor`, instead got: "
"{}".format(weight))
self._weight = weight
self._weight.requires_grad = True
@property
def weight(self):
return self._weight
def forward(self, *x):
"""Return the weight vector."""
return self.weight
class WeightModule(torch.nn.Module):
r"""Weight vector
Weights used with the basis functions in the forcing terms.
"""
def __init__(self, weight=None, num_basis=None):
"""
Initialize the weight vector.
Args:
weight (torch.nn.Module, torch.Tensor, None): weight module or vector. If None, it will create a weight
tensor/module using the provided :attr:`num_basis`.
num_basis (int): number of basis functions. This is used if no weight vector was given.
"""
super(WeightModule, self).__init__()
if weight is None:
weight = WeightTensor(num_basis=num_basis)
elif isinstance(weight, torch.Tensor):
weight = WeightTensor(weight=weight)
elif not isinstance(weight, torch.nn.Module):
raise TypeError("Expecting the weight vector to be None, an instance of `torch.Tensor`, or "
"`torch.nn.Module`, instead got: {}".format(type(weight)))
self._weight = weight
@property
def weight(self):
"""Return a torch.Tensor or torch.nn.Module."""
if isinstance(self._weight, WeightTensor):
return self._weight.weight
return self._weight
def forward(self, x):
"""Forward the input to the weight module."""
return self._weight(x)