Skip to content

Commit 48c35cc

Browse files
baggepinnenclaude
andcommitted
Add discrete-component PID controller models
Add sampled-data siblings of the continuous PID demos using DiscreteComponents: DiscretePIDStandard plus Sampler, ZeroOrderHold and PeriodicClock. Each discrete model lives alongside its continuous counterpart. - DiscreteAngleControlledDyadBot / DiscreteCascadeControlledDyadBot in dyad/closed_loop.dyad - DiscreteCascadeFFDyadBot in dyad/cascade_ff.dyad The tilt angle and position are sampled with Sampler blocks, the discrete controllers run on a common clock defined by the top-level structural parameter Ts (default 0.005 s) via PeriodicClock, and a ZeroOrderHold reconstructs the continuous motor torque. Since DiscretePIDStandard has no feedforward input, the feedforward signals in DiscreteCascadeFFDyadBot are injected with explicit Add blocks; its closed-loop response matches the continuous CascadeFFDyadBot. Adds DiscreteComponents as a dependency and regenerates the Dyad output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxiqD9h4podoj5e4tpLeGk
1 parent 69db0da commit 48c35cc

20 files changed

Lines changed: 1139 additions & 10 deletions

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ version = "0.2.0"
66
[deps]
77
BlockComponents = "1ef5d832-be8e-447f-9f2c-8b5d17d15fd3"
88
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
9+
DiscreteComponents = "b5590941-51af-4a5a-9bca-5ae7cd448b75"
910
DyadInterface = "99806f68-afab-45ca-9d8c-ceff6bc61f54"
1011
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1112
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
@@ -21,6 +22,7 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
2122
[compat]
2223
BlockComponents = "4"
2324
DelimitedFiles = "1"
25+
DiscreteComponents = "0.2"
2426
DyadInterface = "6, 7"
2527
Markdown = "1"
2628
ModelingToolkit = "11"

dyad/cascade_ff.dyad

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,213 @@ metadata {
206206
"id27": {"Dyad": {"edges": [{"S": 1, "M": [{"x": -110, "y": 380}], "E": 2}]}}
207207
}
208208
}
209+
end
210+
211+
"""
212+
Discrete-time version of `CascadeFFDyadBot`. The cascade-controlled balancing
213+
robot uses sampled-data `DiscretePIDStandard` controllers together with the
214+
same continuous feedforward generator.
215+
216+
Because `DiscretePIDStandard` has no feedforward input, the feedforward
217+
signals are injected with explicit `Add` blocks instead of the `u_ff` ports
218+
used by the continuous `LimPID`. The feedforward generator and selectors run
219+
in continuous time; their outputs are sampled before being combined with the
220+
discrete controller signals. The torque feedforward is added to the inner
221+
controller output, and the tilt-angle feedforward is added to the inner-loop
222+
reference produced by the outer controller. The sample interval is set by the
223+
top-level structural parameter `Ts`.
224+
225+
The feedforward generator matrices are computed with the script
226+
`scripts/compute_feedforward.jl`.
227+
"""
228+
component DiscreteCascadeFFDyadBot
229+
world = MultibodyComponents.PlanarMechanics.World(g = 9.82) {^world}
230+
plant = PlanarDyadBot(phi0 = phi0) {^plant}
231+
square = BlockComponents.Sources.Square(amplitude = 0.05, frequency = 1 / 10) {^square}
232+
angle_controller = DiscreteComponents.DiscretePIDStandard(K = 0.487401, Ti = 0.0587352, Td = 0.0420526, Nd = 119.368, y_max = 0.1) {^angle_controller}
233+
pos_controller = DiscreteComponents.DiscretePIDStandard(K = 0.0666576, Ti = 5.25024, Td = 4.81393, Nd = 4.76616, wd = 1, wp = 1, y_max = deg2rad(25.0)) {^pos_controller}
234+
gain = BlockComponents.Math.Gain(k = -1) {^gain}
235+
gain1 = BlockComponents.Math.Gain(k = -1) {^gain1}
236+
"""
237+
refgen has 1 input (position reference) and 3 outputs:
238+
y[1]: angle feedforward
239+
y[2]: filtered position reference
240+
y[3]: torque feedforward
241+
"""
242+
refgen = BlockComponents.Continuous.StateSpace(nx = ff_nx(), nu = 1, ny = 3, A = ff_A(), B = ff_B(), C = ff_C(), D = ff_D()) {^refgen}
243+
angle_ff = MultibodyComponents.Selector(nu = 3) {^angle_ff}
244+
pos_ref = MultibodyComponents.Selector(nu = 3, index = 2) {^pos_ref}
245+
torque_ff = MultibodyComponents.Selector(nu = 3, index = 3) {^torque_ff}
246+
mux1 = Mux1() {^mux1}
247+
sampler_theta = DiscreteComponents.Sampler() {^sampler_theta}
248+
sampler_x = DiscreteComponents.Sampler() {^sampler_x}
249+
sampler_posref = DiscreteComponents.Sampler() {^sampler_posref}
250+
sampler_angleff = DiscreteComponents.Sampler() {^sampler_angleff}
251+
sampler_torqueff = DiscreteComponents.Sampler() {^sampler_torqueff}
252+
"Adds the sampled tilt-angle feedforward to the inner-loop reference produced by the outer controller."
253+
add_angleref = BlockComponents.Math.Add() {^add_angleref}
254+
"Adds the sampled torque feedforward to the inner controller output before the actuator gain."
255+
add_torque = BlockComponents.Math.Add() {^add_torque}
256+
zoh = DiscreteComponents.ZeroOrderHold() {^zoh}
257+
clock = DiscreteComponents.PeriodicClock(dt = Ts) {^clock}
258+
"Initial tilt angle of the body"
259+
parameter phi0::Angle = 0.1
260+
"Controller sample interval"
261+
structural parameter Ts::Real = 0.005
262+
relations
263+
connect(square.y, mux1.u) {^id10}
264+
connect(mux1.y, refgen.u) {^id11}
265+
connect(refgen.y, pos_ref.u) {^id12}
266+
connect(refgen.y, angle_ff.u) {^id13}
267+
connect(refgen.y, torque_ff.u) {^id14}
268+
connect(pos_ref.y, sampler_posref.u) {^id15}
269+
connect(angle_ff.y, sampler_angleff.u) {^id16}
270+
connect(torque_ff.y, sampler_torqueff.u) {^id17}
271+
connect(plant.x, sampler_x.u) {^id18}
272+
connect(sampler_x.y, pos_controller.measurement) {^id19}
273+
connect(sampler_posref.y, pos_controller.reference) {^id20}
274+
connect(pos_controller.ctr_output, gain1.u) {^id21}
275+
connect(gain1.y, add_angleref.u1) {^id22}
276+
connect(sampler_angleff.y, add_angleref.u2) {^id23}
277+
connect(add_angleref.y, angle_controller.reference) {^id24}
278+
connect(plant.theta, sampler_theta.u) {^id25}
279+
connect(sampler_theta.y, angle_controller.measurement, clock.y) {^id26}
280+
connect(angle_controller.ctr_output, add_torque.u1) {^id27}
281+
connect(sampler_torqueff.y, add_torque.u2) {^id28}
282+
connect(add_torque.y, gain.u) {^id29}
283+
connect(gain.y, zoh.u) {^id30}
284+
connect(zoh.y, plant.torque) {^id31}
285+
metadata {
286+
"Dyad": {
287+
"tests": {"case1": {"stop": 20}},
288+
"icons": {"default": "dyad://DyadBotComponents/PlanarDyadBot.svg"}
289+
},
290+
"_links": {
291+
"world": {
292+
"Dyad": {"placement": {"diagram": {"x1": 150, "y1": 160, "x2": 50, "y2": 60, "rot": 180}}}
293+
},
294+
"plant": {
295+
"Dyad": {"placement": {"diagram": {"x1": 420, "y1": 400, "x2": 620, "y2": 600}}}
296+
},
297+
"square": {
298+
"Dyad": {"placement": {"diagram": {"x1": -830, "y1": 430, "x2": -730, "y2": 530}}}
299+
},
300+
"angle_controller": {
301+
"Dyad": {
302+
"placement": {
303+
"diagram": {"iconName": "default", "x1": 160, "y1": 450, "x2": 260, "y2": 550, "rot": 0}
304+
},
305+
"tags": []
306+
}
307+
},
308+
"pos_controller": {
309+
"Dyad": {
310+
"placement": {
311+
"diagram": {"iconName": "default", "x1": -420, "y1": 450, "x2": -320, "y2": 550, "rot": 0}
312+
},
313+
"tags": []
314+
}
315+
},
316+
"gain": {
317+
"Dyad": {"placement": {"diagram": {"x1": 300, "y1": 460, "x2": 380, "y2": 540}}}
318+
},
319+
"gain1": {
320+
"Dyad": {"placement": {"diagram": {"x1": -260, "y1": 460, "x2": -180, "y2": 540}}}
321+
},
322+
"refgen": {
323+
"Dyad": {"placement": {"diagram": {"x1": -680, "y1": 200, "x2": -560, "y2": 320}}}
324+
},
325+
"angle_ff": {
326+
"Dyad": {"placement": {"diagram": {"x1": -520, "y1": 180, "x2": -440, "y2": 260}}}
327+
},
328+
"pos_ref": {
329+
"Dyad": {"placement": {"diagram": {"x1": -520, "y1": 300, "x2": -440, "y2": 380}}}
330+
},
331+
"torque_ff": {
332+
"Dyad": {"placement": {"diagram": {"x1": -520, "y1": 60, "x2": -440, "y2": 140}}}
333+
},
334+
"mux1": {
335+
"Dyad": {"placement": {"diagram": {"x1": -750, "y1": 430, "x2": -690, "y2": 530}}}
336+
},
337+
"sampler_posref": {
338+
"Dyad": {"placement": {"diagram": {"x1": -440, "y1": 330, "x2": -360, "y2": 410}}}
339+
},
340+
"sampler_angleff": {
341+
"Dyad": {"placement": {"diagram": {"x1": -360, "y1": 180, "x2": -280, "y2": 260}}}
342+
},
343+
"sampler_torqueff": {
344+
"Dyad": {"placement": {"diagram": {"x1": 100, "y1": 60, "x2": 180, "y2": 140}}}
345+
},
346+
"sampler_x": {
347+
"Dyad": {"placement": {"diagram": {"x1": -380, "y1": 630, "x2": -480, "y2": 730}}}
348+
},
349+
"sampler_theta": {
350+
"Dyad": {"placement": {"diagram": {"x1": 320, "y1": 630, "x2": 220, "y2": 730}}}
351+
},
352+
"add_angleref": {
353+
"Dyad": {"placement": {"diagram": {"x1": -160, "y1": 450, "x2": -80, "y2": 530}}}
354+
},
355+
"add_torque": {
356+
"Dyad": {"placement": {"diagram": {"x1": 180, "y1": 450, "x2": 260, "y2": 530}}}
357+
},
358+
"zoh": {
359+
"Dyad": {"placement": {"diagram": {"x1": 400, "y1": 450, "x2": 480, "y2": 550}}}
360+
},
361+
"clock": {
362+
"Dyad": {"placement": {"diagram": {"x1": 200, "y1": 760, "x2": 300, "y2": 860}}}
363+
},
364+
"id10": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
365+
"id11": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
366+
"id12": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
367+
"id13": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
368+
"id14": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
369+
"id15": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
370+
"id16": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
371+
"id17": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
372+
"id18": {
373+
"Dyad": {
374+
"edges": [
375+
{
376+
"S": 1,
377+
"M": [
378+
{"x": 660, "y": 468},
379+
{"x": 660, "y": 780},
380+
{"x": -430, "y": 780},
381+
{"x": -430, "y": 680}
382+
],
383+
"E": 2
384+
}
385+
]
386+
}
387+
},
388+
"id19": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
389+
"id20": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
390+
"id21": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
391+
"id22": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
392+
"id23": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
393+
"id24": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
394+
"id25": {
395+
"Dyad": {
396+
"edges": [
397+
{
398+
"S": 1,
399+
"M": [
400+
{"x": 640, "y": 492},
401+
{"x": 640, "y": 720},
402+
{"x": 270, "y": 720},
403+
{"x": 270, "y": 680}
404+
],
405+
"E": 2
406+
}
407+
]
408+
}
409+
},
410+
"id26": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
411+
"id27": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
412+
"id28": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
413+
"id29": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
414+
"id30": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}},
415+
"id31": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
416+
}
417+
}
209418
end

0 commit comments

Comments
 (0)