Skip to content

Commit bea766d

Browse files
authored
Merge pull request #9 from JuliaComputing/fbc/angle-controlled-3d
Add closed-loop 3D model with angle controller and planar-equivalence test
2 parents fce9ade + 979705e commit bea766d

14 files changed

Lines changed: 655 additions & 3 deletions

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Plant models:
1313
position/velocity/tilt/tilt-rate out.
1414
- `DyadBot3D`: three-dimensional model with two individually spinning wheels
1515
with slip-based ground contact, allowing the body to tilt.
16+
- `RollingDyadBot3D`: three-dimensional model with an ideal no-slip rolling
17+
wheel axle (`MultibodyComponents.RollingWheelSet`). Same interface and
18+
parameters as `PlanarDyadBot`; for motion in the vertical plane it is
19+
dynamically equivalent to the planar model.
1620

1721
Closed-loop models around the planar plant:
1822

@@ -27,6 +31,13 @@ Closed-loop models around the planar plant:
2731
integral action (state-space system loaded from `data/lqg_*.csv`).
2832
- `LQGTuningDyadBot`: analysis model used by the LQG design script.
2933

34+
Closed-loop models around the 3D plant:
35+
36+
- `AngleControlledDyadBot3D`: `RollingDyadBot3D` stabilized by the same
37+
`AngleController` as `AngleControlledDyadBot`; with in-plane motion the
38+
closed-loop response is identical to the planar model
39+
(`test/test_stabilization.jl` verifies this).
40+
3041
## Controller tuning scripts (`scripts/`)
3142

3243
Each script activates the `scripts/` environment and can be run directly:

dyad/closed_loop.dyad

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,79 @@ metadata {
230230
}
231231
end
232232

233+
"""
234+
Three-dimensional version of `AngleControlledDyadBot`: the ideal-rolling 3D
235+
robot `RollingDyadBot3D` stabilized by the same `AngleController` with the
236+
same gains. The robot starts from a tilted initial configuration in the
237+
vertical plane and the controller recovers the upright pose; since the motion
238+
stays in the plane, the closed-loop response is identical to that of the
239+
planar model (verified by `test/test_stabilization.jl`).
240+
"""
241+
component AngleControlledDyadBot3D
242+
world = MultibodyComponents.World(g = 9.82, nominal_length = 0.1) {^world}
243+
plant = RollingDyadBot3D(phi0 = phi0) {^plant}
244+
controller = AngleController(k_angle = k_angle, Ti_angle = Ti_angle, Td_angle = Td_angle) {^controller}
245+
"Proportional gain of the angle controller"
246+
parameter k_angle::Real = 0.487401
247+
"Integrator time constant of the angle controller"
248+
parameter Ti_angle::Time = 0.0587352
249+
"Derivative time constant of the angle controller"
250+
parameter Td_angle::Time = 0.0420526
251+
"Initial tilt angle of the body"
252+
parameter phi0::Angle = 0.1
253+
relations
254+
connect(controller.torque, plant.torque) {^id4}
255+
connect(plant.theta, controller.measurement) {^id5}
256+
metadata {
257+
"Dyad": {
258+
"tests": {"case1": {"stop": 5}},
259+
"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}
260+
},
261+
"_links": {
262+
"world": {
263+
"Dyad": {
264+
"placement": {
265+
"diagram": {"iconName": "default", "x1": 200, "y1": 250, "x2": 100, "y2": 150, "rot": 180}
266+
},
267+
"tags": []
268+
}
269+
},
270+
"plant": {
271+
"Dyad": {
272+
"placement": {
273+
"diagram": {"iconName": "default", "x1": 560, "y1": 400, "x2": 760, "y2": 600, "rot": 0}
274+
},
275+
"tags": []
276+
}
277+
},
278+
"controller": {
279+
"Dyad": {"placement": {"diagram": {"x1": 270, "y1": 420, "x2": 420, "y2": 570}}}
280+
},
281+
"id4": {
282+
"Dyad": {
283+
"edges": [{"S": 1, "M": [{"x": 503.75, "y": 495}, {"x": 503.75, "y": 500}], "E": 2}]
284+
}
285+
},
286+
"id5": {
287+
"Dyad": {
288+
"edges": [
289+
{
290+
"S": 1,
291+
"M": [
292+
{"x": 770, "y": 492},
293+
{"x": 770, "y": 660},
294+
{"x": 220, "y": 660},
295+
{"x": 220, "y": 495}
296+
],
297+
"E": 2
298+
}
299+
]
300+
}
301+
}
302+
}
303+
}
304+
end
305+
233306
"""
234307
Discrete-time version of `AngleControlledDyadBot`. Identical to the continuous
235308
model except that the control system is a sampled-data `DiscreteAngleController`

dyad/segway_3d.dyad

Lines changed: 206 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ component DyadBot3D
2525
rod_right = MultibodyComponents.FixedTranslation(r = [0, 0, -track / 2]) {^rod_right}
2626
"Small mass at the center of the axle"
2727
axis_body = MultibodyComponents.Body(m = 0.01, r_cm = [0, 0, 0]) {^axis_body}
28-
"Main body extending upward from the axle"
29-
body = MultibodyComponents.BodyShape(m = body_mass, I_11 = 0.01 * 0.05 ^ 2, I_22 = 0.01 * 0.03 ^ 2, I_33 = 0.01 * 0.05 ^ 2, r = [0, body_height, 0]) {^body}
28+
"Main body extending upward from the axle, with its center of mass in the middle"
29+
body = MultibodyComponents.BodyShape(m = body_mass, r = [0, 2 * body_height, 0], r_cm = [0, body_height, 0], I_11 = 0.01 * 0.05 ^ 2, I_22 = 0.01 * 0.03 ^ 2, I_33 = 0.01 * 0.05 ^ 2) {^body}
3030
damper_left = RotationalComponents.Components.Damper(d = d_wheel) {^damper_left}
3131
damper_right = RotationalComponents.Components.Damper(d = d_wheel) {^damper_right}
3232
"Use a compliant (elastic) vertical wheel-ground contact instead of a rigid contact constraint"
@@ -188,4 +188,208 @@ metadata {
188188
}
189189
}
190190
}
191+
end
192+
193+
"""
194+
Three-dimensional model of a two-wheeled balancing robot with ideal rolling
195+
contact.
196+
197+
The two wheels form an ideal no-slip rolling axle
198+
(`MultibodyComponents.RollingWheelSet`, which resolves the kinematic loop
199+
through the two wheels internally), and the inverted-pendulum body is mounted
200+
at the center of the axle through a revolute joint about the axle axis. The
201+
motor torque acts between the body and the two wheels: half the commanded
202+
torque drives each wheel and the reaction acts on the body. Viscous motor
203+
friction acts between the body and each wheel in the same split.
204+
205+
For motion in the vertical plane the model is dynamically equivalent to
206+
`PlanarDyadBot`: the two wheels share the planar model's wheel mass `m`, spin
207+
inertia `Iw` and motor friction `d`, and the body parameters are identical.
208+
209+
Inputs and outputs (same interface as `PlanarDyadBot`):
210+
- `torque`: torque applied by the motor between body and wheels
211+
- `x`, `x_dot`: position and velocity of the wheel axis along the ground
212+
- `theta`, `theta_dot`: tilt angle and tilt rate of the body (zero when upright)
213+
214+
The model must be accompanied by a `MultibodyComponents.World` component in
215+
the top level of the enclosing model.
216+
"""
217+
component RollingDyadBot3D
218+
wheels = MultibodyComponents.RollingWheelSet(radius = R, m_wheel = m / 2, I_axis = Iw / 2, I_long = Iw / 4, track = track) {^wheels}
219+
"Body tilt joint about the axle axis"
220+
pitch = MultibodyComponents.Revolute(n = [0, 0, 1]) {^pitch}
221+
body = MultibodyComponents.BodyShape(color = [0.2, 0.2, 0.2, 0.9], m = M, r = [0, 2 * L, 0], r_cm = [0, L, 0], I_11 = Ib, I_22 = Ib / 10, I_33 = Ib, radius = 0.03) {^body}
222+
"Splits the motor torque equally between the two wheels"
223+
gain_half = BlockComponents.Math.Gain(k = 0.5) {^gain_half}
224+
torque_left = RotationalComponents.Sources.TorqueSource() {^torque_left}
225+
torque_right = RotationalComponents.Sources.TorqueSource() {^torque_right}
226+
damper_left = RotationalComponents.Components.Damper(d = d / 2) {^damper_left}
227+
damper_right = RotationalComponents.Components.Damper(d = d / 2) {^damper_right}
228+
"Motor torque command"
229+
torque = Dyad.RealInput() {^torque}
230+
"Position of the wheel axis"
231+
x = Dyad.RealOutput() {^x}
232+
"Body tilt angle"
233+
theta = Dyad.RealOutput() {^theta}
234+
"Velocity of the wheel axis"
235+
x_dot = Dyad.RealOutput() {^x_dot}
236+
"Body tilt rate"
237+
theta_dot = Dyad.RealOutput() {^theta_dot}
238+
"Body mass"
239+
parameter M::Mass = 0.1
240+
"Total wheel mass (split between the two wheels)"
241+
parameter m::Mass = 0.05
242+
"Wheel radius"
243+
parameter R::Length = 0.04
244+
"Distance from wheel axis to body center of mass"
245+
parameter L::Length = 0.08
246+
"Body moment of inertia about the axle axis"
247+
parameter Ib::Inertia = 2.5e-4
248+
"Total wheel + motor moment of inertia about the spin axis (split between the two wheels)"
249+
parameter Iw::Inertia = 1e-4
250+
"Motor viscous friction (split between the two wheels)"
251+
parameter d::RotationalDampingConstant = 0.0001
252+
"Initial tilt angle of the body"
253+
parameter phi0::Angle = 0.0
254+
"Distance between the wheels"
255+
parameter track::Length = 0.13
256+
relations
257+
initial pitch.phi = phi0
258+
initial pitch.w = 0
259+
connect(wheels.frame_middle, pitch.frame_a) {^id1}
260+
connect(pitch.frame_b, body.frame_a) {^id2}
261+
connect(torque, gain_half.u) {^id3}
262+
connect(gain_half.y, torque_left.tau, torque_right.tau) {^id4}
263+
connect(torque_left.spline, damper_left.spline_a, wheels.axis1) {^id5}
264+
connect(torque_right.spline, damper_right.spline_a, wheels.axis2) {^id6}
265+
connect(torque_left.support, torque_right.support, damper_left.spline_b, damper_right.spline_b, pitch.axis) {^id7}
266+
x = wheels.x
267+
theta = pitch.phi
268+
x_dot = der(wheels.x)
269+
theta_dot = pitch.w
270+
metadata {
271+
"Dyad": {"icons": {"default": "dyad://DyadBotComponents/DyadBot3D.svg"}},
272+
"_links": {
273+
"wheels": {
274+
"Dyad": {
275+
"placement": {
276+
"diagram": {"iconName": "default", "x1": 400, "y1": 620, "x2": 600, "y2": 820, "rot": 0}
277+
},
278+
"tags": []
279+
}
280+
},
281+
"pitch": {
282+
"Dyad": {"placement": {"diagram": {"x1": 450, "y1": 480, "x2": 550, "y2": 580}}}
283+
},
284+
"body": {
285+
"Dyad": {
286+
"placement": {"diagram": {"x1": 350, "y1": 130, "x2": 650, "y2": 430, "rot": 270}}
287+
}
288+
},
289+
"gain_half": {
290+
"Dyad": {"placement": {"diagram": {"x1": 180, "y1": 460, "x2": 260, "y2": 540}}}
291+
},
292+
"torque_left": {
293+
"Dyad": {"placement": {"diagram": {"x1": 150, "y1": 620, "x2": 250, "y2": 720}}}
294+
},
295+
"torque_right": {
296+
"Dyad": {"placement": {"diagram": {"x1": 850, "y1": 620, "x2": 750, "y2": 720}}}
297+
},
298+
"damper_left": {
299+
"Dyad": {"placement": {"diagram": {"x1": 150, "y1": 760, "x2": 250, "y2": 860}}}
300+
},
301+
"damper_right": {
302+
"Dyad": {"placement": {"diagram": {"x1": 850, "y1": 760, "x2": 750, "y2": 860}}}
303+
},
304+
"torque": {
305+
"Dyad": {
306+
"placement": {
307+
"diagram": {"iconName": "default", "x1": 20, "y1": 460, "x2": 100, "y2": 540, "rot": 0}
308+
},
309+
"tags": []
310+
}
311+
},
312+
"x": {
313+
"Dyad": {
314+
"placement": {
315+
"diagram": {"iconName": "default", "x1": 880, "y1": 300, "x2": 960, "y2": 380, "rot": 0}
316+
},
317+
"tags": []
318+
}
319+
},
320+
"theta": {
321+
"Dyad": {
322+
"placement": {
323+
"diagram": {"iconName": "default", "x1": 880, "y1": 420, "x2": 960, "y2": 500, "rot": 0}
324+
},
325+
"tags": []
326+
}
327+
},
328+
"x_dot": {
329+
"Dyad": {
330+
"placement": {
331+
"diagram": {"iconName": "default", "x1": 880, "y1": 540, "x2": 960, "y2": 620, "rot": 0}
332+
},
333+
"tags": []
334+
}
335+
},
336+
"theta_dot": {
337+
"Dyad": {
338+
"placement": {
339+
"diagram": {"iconName": "default", "x1": 880, "y1": 660, "x2": 960, "y2": 740, "rot": 0}
340+
},
341+
"tags": []
342+
}
343+
},
344+
"id1": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
345+
"id2": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
346+
"id3": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}], "renderStyle": "standard"}},
347+
"id4": {
348+
"Dyad": {
349+
"edges": [
350+
{"S": 1, "M": [], "E": -1},
351+
{"S": -1, "M": [{"x": 200, "y": 670}], "E": 2},
352+
{"S": 3, "M": [{"x": 800, "y": 670}], "E": -1}
353+
],
354+
"junctions": [{"x": 300, "y": 560}],
355+
"renderStyle": "standard"
356+
}
357+
},
358+
"id5": {
359+
"Dyad": {
360+
"edges": [
361+
{"S": 1, "M": [], "E": -1},
362+
{"S": -1, "M": [], "E": 2},
363+
{"S": 3, "M": [], "E": -1}
364+
],
365+
"junctions": [{"x": 350, "y": 700}],
366+
"renderStyle": "standard"
367+
}
368+
},
369+
"id6": {
370+
"Dyad": {
371+
"edges": [
372+
{"S": 1, "M": [], "E": -1},
373+
{"S": -1, "M": [], "E": 2},
374+
{"S": 3, "M": [], "E": -1}
375+
],
376+
"junctions": [{"x": 650, "y": 700}],
377+
"renderStyle": "standard"
378+
}
379+
},
380+
"id7": {
381+
"Dyad": {
382+
"edges": [
383+
{"S": 1, "M": [], "E": -1},
384+
{"S": -1, "M": [], "E": 2},
385+
{"S": 3, "M": [], "E": -1},
386+
{"S": 4, "M": [], "E": -1},
387+
{"S": 5, "M": [], "E": -1}
388+
],
389+
"junctions": [{"x": 500, "y": 900}],
390+
"renderStyle": "standard"
391+
}
392+
}
393+
}
394+
}
191395
end

0 commit comments

Comments
 (0)