Module: 7CCEMIAA Intelligence and Autonomy (MSc Robotics) — coursework 1, 2026.
A chameleon launches its tongue at insects with remarkable accuracy. This project models the tongue as a simple 2-DOF planar arm and uses machine learning and system identification to reproduce the targeting and dynamics behaviour.
The tongue is described by two joint variables: an angle
q₁ (relative to horizontal) and a length
q₂ (extension from the mouth). The tip is modelled
as a point mass m. The forward kinematics are
r₁ = q₂ · cos(q₁)
r₂ = q₂ · sin(q₁)
which makes the inverse mapping
(r₁, r₂) → (q₁, q₂) the interesting problem: given where an
insect is, what tongue configuration should the chameleon fire?
| Sub-task | Method |
|---|---|
| 1. Inverse kinematics | Multi-layer perceptron (MLPRegressor) trained on
observed (r, q) pairs. |
| 2. Targeting accuracy | RMSE between predicted tip locations and ground-truth insect positions. |
| 3. Catch probability | Fraction of predicted tips landing inside a 0.01 m insect radius. |
| 4. Mass identification | Linear-in-parameters regression on the manipulator dynamics equation. |
Constraints from the brief: only numpy,
scikit-learn, and the provided chameleon
helper class are permitted — no extra libraries.
The closed-form inverse is straightforward here
(q₁ = atan2(r₂, r₁), q₂ = √(r₁² + r₂²)), but
the brief requires a learnt model, which mirrors how a
real chameleon would acquire the skill from experience.
Approach: an MLP mapping R → Q with two
hidden layers, ReLU activations, Adam optimiser, and early stopping via
n_iter_no_change. The non-default architecture and longer
training horizon were needed because the default MLPRegressor converges
far too early on this data.
model.set_params(
hidden_layer_sizes=(1000, 100),
max_iter=4500,
activation='relu',
solver='adam',
learning_rate_init=0.001,
random_state=1,
tol=1e-6,
n_iter_no_change=50,
)
model.fit(R.T, Q.T) # rows are samples, columns are dimensionsNote on data layout: the CSVs are stored as
(dim, n_samples), so they are transposed before fitting — a
small but easy-to-miss detail that the brief does not state
explicitly.
Accuracy is measured by sending the predicted q back
through forward kinematics and comparing the resulting
tip position to the requested insect location. RMSE is taken over the
full target set in Rd.csv.
predictedQ = model.predict(Rd.T)
predicted_tip = np.array([cham.forward_kinematics(q) for q in predictedQ])
rmse = np.sqrt(np.mean((predicted_tip - Rd.T) ** 2))This composition — predict → forward-kinematics → compare in task
space — is more meaningful than measuring error in q
directly, because joint-space errors can be magnified or shrunk by the
geometry.
An insect has a radius of 0.01 m. The chameleon catches it whenever the predicted tip lands within that radius of the centre. Probability is simply the fraction of hits:
distances = np.linalg.norm(predicted_tip - Rd.T, axis=1)
probability = np.sum(distances < 0.01) / len(distances)Why this matters separately from RMSE: RMSE squares and averages errors, so a model can have a very small RMSE but still miss the prey radius on a few outlier targets. Conversely, a noisier model that keeps every error under 1 cm catches every insect. The two metrics measure different things.
The provided dynamics are
M(q) q̈ + C(q, q̇) + g(q) = τ
with M, C, g given in terms of
the unknown mass m. Inspecting the equations, every
term on the left is linear in m, which means the
system can be rewritten in linear-in-parameters form:
Φ(q, q̇, q̈) · θ = τ with θ = m
This is the standard form used in robot parameter identification — once in this form, the unknown can be recovered with ordinary least squares over all samples.
Phi_list, tau_list = [], []
for i in range(Q.shape[1]):
q, qd, qdd, tau = Q[:, i], Qdot[:, i], Qddot[:, i], Tau[:, i]
M, C, G = cham.get_MCG(q, qd)
rhs = tau - C @ qd - G
Phi_list += [q[1]**2 * qdd[0], qdd[1]]
tau_list += [rhs[0], rhs[1]]
Phi = np.array(Phi_list).reshape(-1, 1)
mass = float(np.linalg.lstsq(Phi, np.array(tau_list), rcond=None)[0])For a noise-free model the recovered mass matches the
true 0.1 kg to several decimals.
q would have given a worse catch rate than supervising in
r after the forward-kinematics composition.cw.py — template provided by the module (do not modify
outside answer blocks).chameleon.py — provided helper class: forward
kinematics + dynamics terms M, C,
G.K22031784.py — completed submission (kept private; this
report describes the methodology).R.csv, Q.csv, Qdot.csv,
Qddot.csv, Tau.csv, Rd.csv.Methodology write-up. Full solution code is not published to protect the integrity of the coursework while the module is still running.