Skip to content

Commit 6c116fe

Browse files
authored
Implement operational-space dynamics for planar pcs and fix gravitational potential energy expression (#4)
* Start implementing operational space dynamics for planar pcs * Fix sign of gravitational potential energy * Add more plotting to the planar pcs simulation example * Improve labels for simulation data analysis plots * Do not simplify symbolic expressions if we are deriving dynamics for more than 2 segments * Add more symbolic expressions for planar pcs * Formatting * Add symbolic expression for 5 segments * Implement `operational_space_selector` into `operational_space_dynamical_matrices_fn` for planar pcs * Make `operational_space_dynamical_matrices_fn` jittable again by specifying `operational_space_selector` as a tuple * Add `stiffness` function to planar pcs system * Run formatting * Bump version and start testing on Python 3.13
1 parent 3f87cd6 commit 6c116fe

18 files changed

+425
-166
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v2
16+
uses: actions/checkout@v4
1717
- name: Set up Python
18-
uses: actions/setup-python@v1
18+
uses: actions/setup-python@v5
1919
with:
2020
python-version: '3.x'
2121
- name: Install build dependencies

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ jobs:
99
test:
1010
strategy:
1111
matrix:
12-
python: ['3.10', '3.11', '3.12']
12+
python: ['3.10', '3.11', '3.12', '3.13']
1313
platform: [ubuntu-latest, macos-latest, windows-latest]
1414
runs-on: ${{ matrix.platform }}
1515
steps:
1616
- name: Checkout
17-
uses: actions/checkout@v3
17+
uses: actions/checkout@v4
1818
- name: Set up Python ${{ matrix.python }}
19-
uses: actions/setup-python@v3
19+
uses: actions/setup-python@v5
2020
with:
2121
python-version: ${{ matrix.python }}
2222
- name: Install test dependencies

examples/analyze_segment_fusion.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import sympy as sp
22

3-
l1, l2 = sp.symbols('l1 l2', nonnegative=True, nonzero=True)
4-
kappa_be1, sigma_sh1, sigma_ax1 = sp.symbols('kappa_be1 sigma_sh1 sigma_ax1', real=True, nonnegative=True, nonzero=True)
5-
kappa_be2, sigma_sh2, sigma_ax2 = sp.symbols('kappa_be2 sigma_sh2 sigma_ax2', real=True, nonnegative=True, nonzero=True)
3+
l1, l2 = sp.symbols("l1 l2", nonnegative=True, nonzero=True)
4+
kappa_be1, sigma_sh1, sigma_ax1 = sp.symbols(
5+
"kappa_be1 sigma_sh1 sigma_ax1", real=True, nonnegative=True, nonzero=True
6+
)
7+
kappa_be2, sigma_sh2, sigma_ax2 = sp.symbols(
8+
"kappa_be2 sigma_sh2 sigma_ax2", real=True, nonnegative=True, nonzero=True
9+
)
610

711
# deactivate shear and axial strains
812
# sigma_sh1, sigma_sh2 = 0.0, 0.0
@@ -19,30 +23,42 @@
1923
# apply the forward kinematics on the individual segments
2024
s_be1, c_be1 = sp.sin(kappa_be1 * l1), sp.cos(kappa_be1 * l1)
2125
s_be2, c_be2 = sp.sin(kappa_be2 * l2), sp.cos(kappa_be2 * l2)
22-
chi1 = sp.Matrix([
23-
[sigma_sh1 * s_be1 / kappa_be1 + sigma_ax1 * (c_be1 - 1) / kappa_be1],
24-
[sigma_sh1 * (1 - c_be1) / kappa_be1 + sigma_ax1 * s_be1 / kappa_be1],
25-
[kappa_be1 * l1],
26-
])
26+
chi1 = sp.Matrix(
27+
[
28+
[sigma_sh1 * s_be1 / kappa_be1 + sigma_ax1 * (c_be1 - 1) / kappa_be1],
29+
[sigma_sh1 * (1 - c_be1) / kappa_be1 + sigma_ax1 * s_be1 / kappa_be1],
30+
[kappa_be1 * l1],
31+
]
32+
)
2733
# rotation matrix
28-
R1 = sp.Matrix([
29-
[c_be1, -s_be1, 0],
30-
[s_be1, c_be1, 0],
31-
[0, 0, 1],
32-
])
33-
chi2_rel = sp.Matrix([
34-
[sigma_sh2 * s_be2 / kappa_be2 + sigma_ax2 * (c_be2 - 1) / kappa_be2],
35-
[sigma_sh2 * (1 - c_be2) / kappa_be2 + sigma_ax2 * s_be2 / kappa_be2],
36-
[kappa_be2 * l2],
37-
])
34+
R1 = sp.Matrix(
35+
[
36+
[c_be1, -s_be1, 0],
37+
[s_be1, c_be1, 0],
38+
[0, 0, 1],
39+
]
40+
)
41+
chi2_rel = sp.Matrix(
42+
[
43+
[sigma_sh2 * s_be2 / kappa_be2 + sigma_ax2 * (c_be2 - 1) / kappa_be2],
44+
[sigma_sh2 * (1 - c_be2) / kappa_be2 + sigma_ax2 * s_be2 / kappa_be2],
45+
[kappa_be2 * l2],
46+
]
47+
)
3848
chi2 = sp.simplify(chi1 + R1 @ chi2_rel)
3949
# print("chi2 =\n", chi2)
4050
# apply the one-segment inverse kinematic on the distal end of the two segments
4151
px, py, th = chi2[0, 0], chi2[1, 0], chi2[2, 0]
4252
s_th, c_th = sp.sin(th), sp.cos(th)
43-
qfkik = sp.simplify(th / (2 * (l1 + l2)) * sp.Matrix([
44-
[2],
45-
[py - px * s_th / (c_th - 1)],
46-
[-px - py * s_th / (c_th - 1)],
47-
]))
53+
qfkik = sp.simplify(
54+
th
55+
/ (2 * (l1 + l2))
56+
* sp.Matrix(
57+
[
58+
[2],
59+
[py - px * s_th / (c_th - 1)],
60+
[-px - py * s_th / (c_th - 1)],
61+
]
62+
)
63+
)
4864
print("qfkik =\n", qfkik)

examples/demo_planar_hsa_motor2ee_jacobian.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
)
2727

2828

29-
def factory_fn(params: Dict[str, Array], verbose: bool = False) -> Tuple[Callable, Callable]:
29+
def factory_fn(
30+
params: Dict[str, Array], verbose: bool = False
31+
) -> Tuple[Callable, Callable]:
3032
"""
3133
Factory function for the planar HSA.
3234
Args:
@@ -45,7 +47,9 @@ def factory_fn(params: Dict[str, Array], verbose: bool = False) -> Tuple[Callabl
4547
sys_helpers,
4648
) = planar_hsa.factory(sym_exp_filepath, strain_selector)
4749
dynamical_matrices_fn = partial(dynamical_matrices_fn, params)
48-
forward_kinematics_end_effector_fn = jit(partial(forward_kinematics_end_effector_fn, params))
50+
forward_kinematics_end_effector_fn = jit(
51+
partial(forward_kinematics_end_effector_fn, params)
52+
)
4953
jacobian_end_effector_fn = jit(partial(jacobian_end_effector_fn, params))
5054

5155
def residual_fn(q: Array, phi: Array) -> Array:
@@ -64,7 +68,9 @@ def residual_fn(q: Array, phi: Array) -> Array:
6468
print("Compiling jac_residual_fn...")
6569
print(jac_residual_fn(jnp.zeros((3,)), jnp.zeros((2,))))
6670

67-
def phi2q_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[Array, Dict[str, Array]]:
71+
def phi2q_static_model_fn(
72+
phi: Array, q0: Array = jnp.zeros((3,))
73+
) -> Tuple[Array, Dict[str, Array]]:
6874
"""
6975
A static model mapping the motor angles to the planar HSA configuration using scipy.optimize.minimize.
7076
Arguments:
@@ -82,7 +88,12 @@ def phi2q_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[Arr
8288
options={"disp": True} if verbose else None,
8389
)
8490
if verbose:
85-
print("Optimization converged after", sol.nit, "iterations with residual", sol.fun)
91+
print(
92+
"Optimization converged after",
93+
sol.nit,
94+
"iterations with residual",
95+
sol.fun,
96+
)
8697

8798
# configuration that minimizes the residual
8899
q = jnp.array(sol.x)
@@ -95,7 +106,9 @@ def phi2q_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[Arr
95106

96107
return q, aux
97108

98-
def phi2chi_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[Array, Dict[str, Array]]:
109+
def phi2chi_static_model_fn(
110+
phi: Array, q0: Array = jnp.zeros((3,))
111+
) -> Tuple[Array, Dict[str, Array]]:
99112
"""
100113
A static model mapping the motor angles to the planar end-effector pose.
101114
Arguments:
@@ -110,7 +123,9 @@ def phi2chi_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[A
110123
aux["chi"] = chi
111124
return chi, aux
112125

113-
def jac_phi2chi_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tuple[Array, Dict[str, Array]]:
126+
def jac_phi2chi_static_model_fn(
127+
phi: Array, q0: Array = jnp.zeros((3,))
128+
) -> Tuple[Array, Dict[str, Array]]:
114129
"""
115130
Compute the Jacobian between the actuation space and the task-space.
116131
Arguments:
@@ -157,17 +172,12 @@ def jac_phi2chi_static_model_fn(phi: Array, q0: Array = jnp.zeros((3, ))) -> Tup
157172
phi = jnp.array([1.0, 1.0])
158173
case _:
159174
rng, subkey = random.split(rng)
160-
phi = random.uniform(
161-
subkey,
162-
phi_max.shape,
163-
minval=0.0,
164-
maxval=phi_max
165-
)
175+
phi = random.uniform(subkey, phi_max.shape, minval=0.0, maxval=phi_max)
166176

167177
print("i", i)
168178

169179
chi, aux = phi2chi_static_model_fn(phi, q0=q0)
170180
print("phi", phi, "q", aux["q"], "chi", chi)
171181

172182
J_phi2chi, aux = jac_phi2chi_static_model_fn(phi, q0=q0)
173-
print("J_phi2chi:\n", J_phi2chi)
183+
print("J_phi2chi:\n", J_phi2chi)

examples/derive_planar_pcs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@
1212
/ f"planar_pcs_ns-{NUM_SEGMENTS}.dill"
1313
)
1414
symbolically_derive_planar_pcs_model(
15-
num_segments=NUM_SEGMENTS, filepath=sym_exp_filepath, simplify_expressions=True
15+
num_segments=NUM_SEGMENTS,
16+
filepath=sym_exp_filepath,
17+
simplify_expressions=True if NUM_SEGMENTS < 3 else False,
1618
)

examples/simulate_planar_pcs.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
strain_selector = jnp.array([True, False, False])
4444

4545
# define initial configuration
46-
q0 = jnp.array([10 * jnp.pi])
46+
q0 = jnp.array([5 * jnp.pi])
4747
# number of generalized coordinates
4848
n_q = q0.shape[0]
4949

@@ -95,8 +95,8 @@ def draw_robot(
9595

9696

9797
if __name__ == "__main__":
98-
strain_basis, forward_kinematics_fn, dynamical_matrices_fn, auxiliary_fns = planar_pcs.factory(
99-
sym_exp_filepath, strain_selector
98+
strain_basis, forward_kinematics_fn, dynamical_matrices_fn, auxiliary_fns = (
99+
planar_pcs.factory(sym_exp_filepath, strain_selector)
100100
)
101101
batched_forward_kinematics = vmap(
102102
forward_kinematics_fn, in_axes=(None, None, 0), out_axes=-1
@@ -142,9 +142,38 @@ def draw_robot(
142142
# the evolution of the generalized velocities
143143
q_d_ts = sol.ys[:, n_q:]
144144

145+
# evaluate the forward kinematics along the trajectory
146+
chi_ee_ts = vmap(forward_kinematics_fn, in_axes=(None, 0, None))(
147+
params, q_ts, jnp.array([jnp.sum(params["l"])])
148+
)
149+
# plot the end-effector position along the trajectory
150+
plt.figure()
151+
plt.plot(chi_ee_ts[0, :], chi_ee_ts[1, :])
152+
plt.axis("equal")
153+
plt.grid(True)
154+
plt.xlabel("End-effector x [m]")
155+
plt.ylabel("End-effector y [m]")
156+
plt.tight_layout()
157+
plt.show()
158+
# plot end-effector position vs time
159+
plt.figure()
160+
plt.plot(video_ts, chi_ee_ts[:, 0], label="x")
161+
plt.plot(video_ts, chi_ee_ts[:, 1], label="y")
162+
plt.xlabel("Time [s]")
163+
plt.ylabel("End-effector Position [m]")
164+
plt.legend()
165+
plt.grid(True)
166+
plt.box(True)
167+
plt.tight_layout()
168+
plt.show()
169+
145170
# plot the energy along the trajectory
146-
kinetic_energy_fn_vmapped = vmap(partial(auxiliary_fns["kinetic_energy_fn"], params))
147-
potential_energy_fn_vmapped = vmap(partial(auxiliary_fns["potential_energy_fn"], params))
171+
kinetic_energy_fn_vmapped = vmap(
172+
partial(auxiliary_fns["kinetic_energy_fn"], params)
173+
)
174+
potential_energy_fn_vmapped = vmap(
175+
partial(auxiliary_fns["potential_energy_fn"], params)
176+
)
148177
U_ts = potential_energy_fn_vmapped(q_ts)
149178
T_ts = kinetic_energy_fn_vmapped(q_ts, q_d_ts)
150179
plt.figure()
@@ -155,6 +184,7 @@ def draw_robot(
155184
plt.legend()
156185
plt.grid(True)
157186
plt.box(True)
187+
plt.tight_layout()
158188
plt.show()
159189

160190
# create video

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ name = "jsrm" # Required
1717
#
1818
# For a discussion on single-sourcing the version, see
1919
# https://packaging.python.org/guides/single-sourcing-package-version/
20-
version = "0.0.10" # Required
20+
version = "0.0.11" # Required
2121

2222
# This is a one-line description or tagline of what your project does. This
2323
# corresponds to the "Summary" metadata field:
@@ -88,10 +88,10 @@ classifiers = [ # Optional
8888
# that you indicate you support Python 3. These classifiers are *not*
8989
# checked by "pip install". See instead "python_requires" below.
9090
"Programming Language :: Python :: 3",
91-
"Programming Language :: Python :: 3.9",
9291
"Programming Language :: Python :: 3.10",
9392
"Programming Language :: Python :: 3.11",
9493
"Programming Language :: Python :: 3.12",
94+
"Programming Language :: Python :: 3.13",
9595
"Programming Language :: Python :: 3 :: Only",
9696
]
9797

src/jsrm/symbolic_derivation/planar_hsa.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def symbolically_derive_planar_hsa_model(
219219
# inertia matrix
220220
B = sp.zeros(num_dof, num_dof)
221221
# potential energy
222-
U = sp.Matrix([[0]])
222+
U_g = sp.Matrix([[0]])
223223
# stiffness matrix
224224
Shat = sp.zeros(3 * num_segments, 3 * num_segments)
225225
# elastic vector
@@ -299,11 +299,11 @@ def symbolically_derive_planar_hsa_model(
299299
B = B + Br_ij
300300

301301
# derivative of the potential energy with respect to the point coordinate s
302-
dUr_ds = sp.simplify(rhor[i, j] * Ar[i, j] * g.T @ pr)
302+
dU_g_r_ds = sp.simplify(-rhor[i, j] * Ar[i, j] * g.T @ pr)
303303
# potential energy of the current segment
304-
U_ij = sp.simplify(sp.integrate(dUr_ds, (s, 0, l[i])))
304+
U_g_ij = sp.simplify(sp.integrate(dU_g_r_ds, (s, 0, l[i])))
305305
# add potential energy of segment to previous segments
306-
U = U + U_ij
306+
U_g = U_g + U_g_ij
307307

308308
# strains in physical HSA rod
309309
pxir = _sym_beta_fn(vxi, roff[i, j])
@@ -466,8 +466,8 @@ def symbolically_derive_planar_hsa_model(
466466
Bp = sp.simplify(Bp)
467467
B = B + Bp
468468
# potential energy of the platform
469-
Up = sp.simplify(mp * g.T @ pCoGp)
470-
U = U + Up
469+
U_g_p = sp.simplify(-mp * g.T @ pCoGp)
470+
U_g = U_g + U_g_p
471471

472472
# update the orientation for the next segment
473473
th_prev = th.subs(s, l[i])
@@ -507,8 +507,8 @@ def symbolically_derive_planar_hsa_model(
507507
Bpl = sp.simplify(Bpl)
508508
B = B + Bpl
509509
# add the gravitational potential energy of the payload
510-
Upl = sp.simplify(mpl * g.T @ pCoGpl)
511-
U = U + Upl
510+
Upl = sp.simplify(-mpl * g.T @ pCoGpl)
511+
U_g = U_g + Upl
512512

513513
# simplify mass matrix
514514
if simplify:
@@ -519,7 +519,7 @@ def symbolically_derive_planar_hsa_model(
519519
print("C =\n", C)
520520

521521
# compute the gravity force vector
522-
G = -U.jacobian(xi).transpose()
522+
G = U_g.jacobian(xi).transpose()
523523
if simplify:
524524
G = sp.simplify(G)
525525
print("G =\n", G)

0 commit comments

Comments
 (0)