Skip to content

Commit f61fec1

Browse files
committed
Start implementing operational space dynamics for planar pcs
1 parent 3f87cd6 commit f61fec1

File tree

3 files changed

+172
-21
lines changed

3 files changed

+172
-21
lines changed

src/jsrm/symbolic_derivation/planar_pcs.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sympy as sp
44
from typing import Callable, Dict, Optional, Tuple, Union
55

6-
from .symbolic_utils import compute_coriolis_matrix
6+
from .symbolic_utils import compute_coriolis_matrix, compute_dAdt
77

88

99
def symbolically_derive_planar_pcs_model(
@@ -55,8 +55,8 @@ def symbolically_derive_planar_pcs_model(
5555

5656
# matrix with symbolic expressions to derive the poses along each segment
5757
chi_sms = []
58-
# Jacobians (positional + orientation) in each segment as a function of the point coordinate s
59-
J_sms = []
58+
# Jacobians (positional + orientation) in each segment as a function of the point coordinate s and its time derivative
59+
J_sms, J_d_sms = [], []
6060
# cross-sectional area of each segment
6161
A = sp.zeros(num_segments)
6262
# second area moment of inertia of each segment
@@ -118,6 +118,10 @@ def symbolically_derive_planar_pcs_model(
118118
J = Jp.col_join(Jo)
119119
J_sms.append(J)
120120

121+
# compute the time derivative of the Jacobian
122+
J_d = compute_dAdt(J, xi, xi_d) # time derivative of the end-effector Jacobian
123+
J_d_sms.append(J_d)
124+
121125
# derivative of mass matrix with respect to the point coordinate s
122126
dB_ds = rho[i] * A[i] * Jp.T @ Jp + rho[i] * I[i] * Jo.T @ Jo
123127
if simplify_expressions:
@@ -179,8 +183,10 @@ def symbolically_derive_planar_pcs_model(
179183
"chiee": chi_sms[-1].subs(
180184
s, l[-1]
181185
), # expression for end-effector pose of shape (3, )
182-
"J_sms": J_sms,
183-
"Jee": J_sms[-1].subs(s, l[-1]),
186+
"J_sms": J_sms, # list of Jacobians (for each segment) of shape (3, num_dof)
187+
"Jee": J_sms[-1].subs(s, l[-1]), # end-effector Jacobian of shape (3, num_dof)
188+
"J_d_sms": J_d_sms, # list of time derivatives of Jacobians (for each segment)
189+
"Jee_d": J_d_sms[-1].subs(s, l[-1]), # time derivative of end-effector Jacobian of shape (3, num_dof)
184190
"B": B, # mass matrix
185191
"C": C, # coriolis matrix
186192
"G": G, # gravity vector
1.3 KB
Binary file not shown.

src/jsrm/systems/planar_pcs.py

Lines changed: 161 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def factory(
4848
params_syms = sym_exps["params_syms"]
4949

5050
@jit
51-
def select_params_for_lambdify(params: Dict[str, Array]) -> List[Array]:
51+
def select_params_for_lambdify_fn(params: Dict[str, Array]) -> List[Array]:
5252
"""
5353
Select the parameters for lambdify
5454
Args:
@@ -101,6 +101,27 @@ def select_params_for_lambdify(params: Dict[str, Array]) -> List[Array]:
101101
"jax",
102102
)
103103
chi_lambda_sms.append(chi_lambda)
104+
J_lambda_sms = []
105+
for J_exp in sym_exps["exps"]["J_sms"]:
106+
J_lambda = sp.lambdify(
107+
params_syms_cat
108+
+ sym_exps["state_syms"]["xi"]
109+
+ [sym_exps["state_syms"]["s"]],
110+
J_exp,
111+
"jax",
112+
)
113+
J_lambda_sms.append(J_lambda)
114+
J_d_lambda_sms = []
115+
for J_d_exp in sym_exps["exps"]["J_d_sms"]:
116+
J_d_lambda = sp.lambdify(
117+
params_syms_cat
118+
+ sym_exps["state_syms"]["xi"]
119+
+ sym_exps["state_syms"]["xi_d"]
120+
+ [sym_exps["state_syms"]["s"]],
121+
J_d_exp,
122+
"jax",
123+
)
124+
J_d_lambda_sms.append(J_d_lambda)
104125

105126
B_lambda = sp.lambdify(
106127
params_syms_cat + sym_exps["state_syms"]["xi"], sym_exps["exps"]["B"], "jax"
@@ -153,6 +174,30 @@ def apply_eps_to_bend_strains(xi: Array, _eps: float) -> Array:
153174

154175
return xi_epsed
155176

177+
def classify_segment(params: Dict[str, Array], s: Array) -> Tuple[Array, Array] :
178+
"""
179+
Classify the point along the robot to the corresponding segment
180+
Args:
181+
params: Dictionary of robot parameters
182+
s: point coordinate along the robot in the interval [0, L].
183+
Returns:
184+
segment_idx: index of the segment
185+
s_segment: point coordinate along the segment in the interval [0, l_segment
186+
"""
187+
# cumsum of the segment lengths
188+
l_cum = jnp.cumsum(params["l"])
189+
# add zero to the beginning of the array
190+
l_cum_padded = jnp.concatenate([jnp.array([0.0]), l_cum], axis=0)
191+
# determine in which segment the point is located
192+
# use argmax to find the last index where the condition is true
193+
segment_idx = (
194+
l_cum.shape[0] - 1 - jnp.argmax((s >= l_cum_padded[:-1])[::-1]).astype(int)
195+
)
196+
# point coordinate along the segment in the interval [0, l_segment]
197+
s_segment = s - l_cum_padded[segment_idx]
198+
199+
return segment_idx, s_segment
200+
156201
@jit
157202
def forward_kinematics_fn(
158203
params: Dict[str, Array], q: Array, s: Array, eps: float = global_eps
@@ -172,31 +217,57 @@ def forward_kinematics_fn(
172217
"""
173218
# map the configuration to the strains
174219
xi = xi_eq + B_xi @ q
175-
176220
# add a small number to the bending strain to avoid singularities
177221
xi_epsed = apply_eps_to_bend_strains(xi, eps)
178222

179-
# cumsum of the segment lengths
180-
l_cum = jnp.cumsum(params["l"])
181-
# add zero to the beginning of the array
182-
l_cum_padded = jnp.concatenate([jnp.array([0.0]), l_cum], axis=0)
183-
# determine in which segment the point is located
184-
# use argmax to find the last index where the condition is true
185-
segment_idx = (
186-
l_cum.shape[0] - 1 - jnp.argmax((s >= l_cum_padded[:-1])[::-1]).astype(int)
187-
)
188-
# point coordinate along the segment in the interval [0, l_segment]
189-
s_segment = s - l_cum_padded[segment_idx]
223+
# classify the point along the robot to the corresponding segment
224+
segment_idx, s_segment = classify_segment(params, s)
190225

191226
# convert the dictionary of parameters to a list, which we can pass to the lambda function
192-
params_for_lambdify = select_params_for_lambdify(params)
227+
params_for_lambdify = select_params_for_lambdify_fn(params)
193228

194229
chi = lax.switch(
195230
segment_idx, chi_lambda_sms, *params_for_lambdify, *xi_epsed, s_segment
196231
).squeeze()
197232

198233
return chi
199234

235+
@jit
236+
def jacobian_fn(
237+
params: Dict[str, Array], q: Array, s: Array, eps: float = global_eps
238+
) -> Array:
239+
"""
240+
Evaluate the forward kinematics the tip of the links
241+
Args:
242+
params: Dictionary of robot parameters
243+
q: generalized coordinates of shape (n_q, )
244+
s: point coordinate along the rod in the interval [0, L].
245+
eps: small number to avoid singularities (e.g., division by zero)
246+
Returns:
247+
J: Jacobian matrix of shape (3, n_q) of the backbone point in Cartesian-space
248+
Relates the configuration-space velocity q_d to the Cartesian-space velocity chi_d,
249+
where chi_d = J @ q_d. Chi_d consists of [p_x_d, p_y_d, theta_d]
250+
"""
251+
# map the configuration to the strains
252+
xi = xi_eq + B_xi @ q
253+
# add a small number to the bending strain to avoid singularities
254+
xi_epsed = apply_eps_to_bend_strains(xi, eps)
255+
256+
# classify the point along the robot to the corresponding segment
257+
segment_idx, s_segment = classify_segment(params, s)
258+
259+
# convert the dictionary of parameters to a list, which we can pass to the lambda function
260+
params_for_lambdify = select_params_for_lambdify_fn(params)
261+
262+
J = lax.switch(
263+
segment_idx, J_lambda_sms, *params_for_lambdify, *xi_epsed, s_segment
264+
).squeeze()
265+
266+
# apply the strain basis to the Jacobian
267+
J = J @ B_xi
268+
269+
return J
270+
200271
@jit
201272
def dynamical_matrices_fn(
202273
params: Dict[str, Array], q: Array, q_d: Array, eps: float = 1e4 * global_eps
@@ -239,7 +310,7 @@ def dynamical_matrices_fn(
239310
# dissipative matrix from the parameters
240311
D = params.get("D", jnp.zeros((n_xi, n_xi)))
241312

242-
params_for_lambdify = select_params_for_lambdify(params)
313+
params_for_lambdify = select_params_for_lambdify_fn(params)
243314

244315
B = B_xi.T @ B_lambda(*params_for_lambdify, *xi_epsed) @ B_xi
245316
C_xi = C_lambda(*params_for_lambdify, *xi_epsed, *xi_d)
@@ -303,7 +374,7 @@ def potential_energy_fn(params: Dict[str, Array], q: Array, eps: float = 1e4 * g
303374
U_K = (xi - xi_eq).T @ K @ (xi - xi_eq) # evaluate K(xi) = K @ xi
304375

305376
# gravitational potential energy
306-
params_for_lambdify = select_params_for_lambdify(params)
377+
params_for_lambdify = select_params_for_lambdify_fn(params)
307378
U_G = U_lambda(*params_for_lambdify, *xi_epsed)
308379

309380
# total potential energy
@@ -327,11 +398,85 @@ def energy_fn(params: Dict[str, Array], q: Array, q_d: Array) -> Array:
327398

328399
return E
329400

401+
def operational_space_dynamical_matrices_fn(
402+
params: Dict[str, Array],
403+
q: Array,
404+
q_d: Array,
405+
s: Array,
406+
B: Array,
407+
C: Array,
408+
eps: float = 1e4 * global_eps,
409+
) -> Tuple[Array, Array, Array, Array, Array]:
410+
"""
411+
Compute the dynamics in operational space.
412+
The implementation is based on Chapter 7.8 of "Modelling, Planning and Control of Robotics" by
413+
Siciliano, Sciavicco, Villani, Oriolo.
414+
Args:
415+
params: dictionary of parameters
416+
q: generalized coordinates of shape (n_q,)
417+
q_d: generalized velocities of shape (n_q,)
418+
s: point coordinate along the robot in the interval [0, L].
419+
B: inertia matrix in the generalized coordinates of shape (n_q, n_q)
420+
C: coriolis matrix derived with Christoffer symbols in the generalized coordinates of shape (n_q, n_q)
421+
eps: small number to avoid singularities (e.g., division by zero)
422+
Returns:
423+
Lambda: inertia matrix in the operational space of shape (3, 3)
424+
mu: matrix with corioli and centrifugal terms in the operational space of shape (3, 3)
425+
J: Jacobian of the Cartesian pose with respect to the generalized coordinates of shape (3, n_q)
426+
J: time-derivative of the Jacobian of the end-effector pose with respect to the generalized coordinates
427+
of shape (3, n_q)
428+
JB_pinv: Dynamically-consistent pseudo-inverse of the Jacobian. Allows the mapping of torques
429+
from the generalized coordinates to the operational space: f = JB_pinv.T @ tau_q
430+
Shape (n_q, 3)
431+
"""
432+
## map the configuration to the strains
433+
xi = xi_eq + B_xi @ q
434+
xi_d = B_xi @ q_d
435+
# add a small number to the bending strain to avoid singularities
436+
xi_epsed = apply_eps_to_bend_strains(xi, eps)
437+
438+
# classify the point along the robot to the corresponding segment
439+
segment_idx, s_segment = classify_segment(params, s)
440+
441+
# convert the dictionary of parameters to a list, which we can pass to the lambda function
442+
params_for_lambdify = select_params_for_lambdify_fn(params)
443+
444+
# Jacobian and its time-derivative
445+
J = lax.switch(
446+
segment_idx, J_lambda_sms, *params_for_lambdify, *xi_epsed, s_segment
447+
).squeeze()
448+
J_d = lax.switch(
449+
segment_idx, J_d_lambda_sms, *params_for_lambdify, *xi_epsed, *xi_d, s_segment
450+
).squeeze()
451+
# apply the strain basis to the J and J_d
452+
J = J @ B_xi
453+
J_d = J_d @ B_xi
454+
455+
# inverse of the inertia matrix in the configuration space
456+
B_inv = jnp.linalg.inv(B)
457+
458+
Lambda = jnp.linalg.inv(
459+
J @ B_inv @ J.T
460+
) # inertia matrix in the operational space
461+
mu = Lambda @ (
462+
J @ B_inv @ C - J_d
463+
) # coriolis and centrifugal matrix in the operational space
464+
465+
JB_pinv = (
466+
B_inv @ J.T @ Lambda
467+
) # dynamically-consistent pseudo-inverse of the Jacobian
468+
# apply TODO: remove
469+
# JB_pinv = B_xi.T @ JB_pinv
470+
471+
return Lambda, mu, J, J_d, JB_pinv
472+
330473
auxiliary_fns = {
331474
"apply_eps_to_bend_strains": apply_eps_to_bend_strains,
475+
"jacobian_fn": jacobian_fn,
332476
"kinetic_energy_fn": kinetic_energy_fn,
333477
"potential_energy_fn": potential_energy_fn,
334478
"energy_fn": energy_fn,
479+
"operational_space_dynamical_matrices_fn": operational_space_dynamical_matrices_fn,
335480
}
336481

337482
return B_xi, forward_kinematics_fn, dynamical_matrices_fn, auxiliary_fns

0 commit comments

Comments
 (0)