MPC Drone

This is a custom implementation of MPC control. This might get implemented into jdrones in the future.

[1] M. Islam, M. Okasha, and M. M. Idres, ‘Dynamics and control of quadcopter using linear model predictive control approach’, IOP Conf. Ser.: Mater. Sci. Eng., vol. 270, p. 012007, Dec. 2017, doi: 10.1088/1757-899X/270/1/012007.

[2] M. Ireland and D. Anderson, ‘Development of Navigation Algorithms for Nap-of-the-Earth UAV Flight in a Constrained Urban Environment’, presented at the ICAS 2012 - 28th Congress of the International Council of the Aeronautical Sciences, Sep. 2012. Accessed: Feb. 07, 2023. [Online]. Available: http://www.icas.org/ICAS_ARCHIVE/ICAS2012/PAPERS/184.PDF

[3] M. L. Ireland and D. Anderson, ‘Optimisation of Trajectories for Wireless Power Transmission to a Quadrotor Aerial Robot’, J Intell Robot Syst, vol. 95, no. 2, pp. 567–584, Aug. 2019, doi: 10.1007/s10846-018-0824-6.

[4] Wang, P. et al. (2016) ‘Dynamics modelling and linear control of quadcopter’, in 2016 International Conference on Advanced Mechatronic Systems (ICAMechS). 2016 International Conference on Advanced Mechatronic Systems (ICAMechS), pp. 498–503. Available at: https://doi.org/10.1109/ICAMechS.2016.7813499.

[1]:
%matplotlib notebook
from notebook_quick_setup import *
Beginning notebook setup...
        Added /home/jhewers/Documents/projects/jdrones/src to path
        Imported gymnasium version 0.27.1
        Imported jdrones version unknown
pybullet build time: Feb  2 2023 13:13:41
        Imported scipy==1.7.3, numpy==1.22.4, pandas==1.3.5
        Imported functools, collections and itertools
        Imported tqdm (standard and trange)
        Imported seaborn==0.11.2, matplotlib==3.5.1
End of notebook setup
[2]:
T = 3
dt = 1/50
mpc_dt = 1/5
M = 1  # Control horizon
P = 5  # Prediction horizon
[3]:
initial_state = State()
initial_state.pos = (0,0,-0.1)
[4]:
def reshape(u):
    return np.reshape(u,(-1,4))
[5]:
def prediction(u, x, dt=mpc_dt):
    env = gymnasium.make("LinearDynamicModelDroneEnv-v0", dt=dt, initial_state=x)
    y = collections.deque()
    obs, _ = env.reset()
    assert np.allclose(obs.pos, x.pos)
    assert np.allclose(obs.vel, x.vel)
    assert np.allclose(obs.rpy, x.rpy)
    assert np.allclose(obs.ang_vel, x.ang_vel)
    for ui in u:
        obs, *_ = env.step(ui)
        y.append(np.copy(obs))
    return np.array(y)
[6]:
%timeit prediction(reshape(np.ones((4,M+P))),State())
4.1 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
[7]:
Q = State(np.ones(20))
Q.quat = Q.prop_omega = (0, 0, 0, 0)
Q.pos = (10, 10, 10)
Q.rpy = (1000, 1000, 1000)
print(Q)


def cost_fn(y):
    return np.sum((Q * y) ** 2)


def objective(u, x, _P=P, _mpc_dt=mpc_dt):
    """Calculate the sum of the square error for the control problem"""
    y = prediction(reshape(u), x, dt=_mpc_dt)
    return cost_fn(y)
[  10.   10.   10.    0.    0.    0.    0. 1000. 1000. 1000.    1.    1.
    1.    1.    1.    1.    0.    0.    0.    0.]
[8]:
env = gymnasium.wrappers.TimeLimit(
    gymnasium.make(
        "NonLinearDynamicModelDroneEnv-v0", dt=dt, initial_state=initial_state
    ),
    max_episode_steps=int(T / dt),
)
obs, _ = env.reset()

observations = collections.deque()
cost = collections.deque()
controls = collections.deque()

for i in trange(int(T / dt) - 1):
    # Control
    res = scipy.optimize.minimize(
        functools.partial(objective, x=obs), np.zeros((P + M, 4))
    )
    uopt = reshape(res.x)[0]
    control_action = uopt.squeeze()
    # Store
    cost.append((cost_fn(obs), res.fun))
    observations.append(np.copy(obs))
    controls.append(control_action)
    # Step
    u = np.sqrt(np.clip(control_action, 0, np.inf))
    obs, _, term, trunc, info = env.step(u)
    # Termination
    if trunc or term:
        break
/home/jhewers/.local/lib/python3.10/site-packages/gymnasium/utils/passive_env_checker.py:181: UserWarning: WARN: The obs returned by the `reset()` method is not within the observation space.
  logger.warn(f"{pre} is not within the observation space.")
/home/jhewers/.local/lib/python3.10/site-packages/gymnasium/utils/passive_env_checker.py:181: UserWarning: WARN: The obs returned by the `step()` method is not within the observation space.
  logger.warn(f"{pre} is not within the observation space.")
[16]:
N = 1000
df = States(observations).to_df(tag="States", N=N, dt=dt)
df_meta = Conversions.iter_to_df(
    np.array(cost), tag="Cost", dt=dt, N=N, cols=["state", "optim"]
)
df_ctrl = Conversions.iter_to_df(
    np.array(controls), tag="Control", dt=dt, N=N, cols=["roll", "pitch", "yaw", "T"]
)
[17]:
fig, ax = plt.subplots(1, 1)
sns.lineplot(data=df_ctrl, x="t", y="value", hue="variable", ax=ax)
plt.show()
[18]:
fig, ax = plt.subplots(2, 2, figsize=(10, 8))
ax = ax.flatten()

sns.lineplot(
    data=df.query("variable in ('x','y','z')"),
    x="t",
    y="value",
    hue="variable",
    ax=ax[0],
)
ax[0].legend()

sns.lineplot(
    data=df.query("variable in ('phi','theta','psi')"),
    x="t",
    y="value",
    hue="variable",
    ax=ax[1],
)
ax[1].legend()

sns.lineplot(
    data=df.query("variable in ('vx','vy','vz')"),
    x="t",
    y="value",
    hue="variable",
    ax=ax[2],
)
ax[2].legend()

sns.lineplot(
    data=df.query("variable in ('P0','P1','P2','P3')"),
    x="t",
    y="value",
    hue="variable",
    ax=ax[3],
)
ax[3].legend()

fig.tight_layout()
[19]:
fig, ax = plt.subplots(1, 1)
sns.lineplot(data=df_meta, x="t", y="value", hue="variable", ax=ax)
plt.show()