
ПИД-регулятор — один из самых распространённых механизмов обратной связи. Он есть буквально везде: нажимаете ручку газа на электросамокате - ПИД, когда выставляете температуру на обогревателе - ПИД, и даже когда сливаете воду из бачка…
Начнём с как раз с этого примера

Здесь уровень воды должен быть «полным». Если есть ненулевая ошибка
то клапан приоткрывается и подаёт управляющий сигнал , пропорциональный этой ошибке:
Разумеется, в реальной жизни не может быть отрицательным и всегда ограничен максимальным расходом воды, который вообще доступен в системе.
Уже здесь видно главное: мы измеряем отклонение от цели и воздействуем на систему так, чтобы это отклонение уменьшить.
История с обогревателем
Теперь возьмём пример поинтереснее - обогреватель.

Проблема тут в том, что тепло не доходит до термометра мгновенно. Воздух имеет конечную теплопроводность, нагревательный элемент тоже не магический, и вся система реагирует с запаздыванием.
Такие процессы описываются уравнением теплопроводности - параболическим уравнением в частных производных:
Попробуем решить его для двумерного материала, который нагревается в одной точке :
где — функция нагрева.
sol = NDSolveValue[
{
D[w[x, y, t], t] == Laplacian[w[x, y, t], {x, y}] +
If[(x + 2)^2 + y^2 < 0.1 && t > 0.0, 100.0, 0],
w[x, y, 0] == 0
},
w,
{x, y} ∈ Rectangle[{-2, -1}, {2, 1}],
{t, 0, 10}
];
Визуализируем распределение температуры во времени:
Table[
DensityPlot[
sol[x, y, t],
{x, -2, 2}, {y, -1, 1},
Epilog -> {
Text["🔥", {-2, 0}],
Text[Style["A", White, FontSize -> 14], {1.6, 0.}, {0, 0}]
},
PlotLabel -> ("t = " <> ToString[t]),
ColorFunctionScaling -> False,
ImageSize -> 200
],
{t, {0.2, 1, 10}}
] // Row

Теперь посмотрим, как ведёт себя температура в фиксированной точке A:
Plot[
sol[1.6, 0., t],
{t, 0, 10},
AxesLabel -> {"time", "Temperature"},
PlotLabel -> "A",
PlotRange -> Full
]

Вот это и есть реакция нашей системы. Она запаздывает… или нет
В отличие от электромагнитных или упругих волн, уравнение теплопроводности описывает диффузионные волны. Их скорость зависит от времени и постепенно падает. Можно даже сказать, что это предельно “задавленная” потерями версия обычной волны.
И вот здесь начинается интересное: контроллер нельзя просто так «вклеить» в это уравнение. Получается петля обратной связи, с которой NDSolve и другие встроенные решатели работают уже не так охотно. Поэтому приходится переходить к дискретизации.
Метод FDTD
Один из естественных путей - дискретизировать уравнение и по времени, и по пространству.
Обычно проще работать с уравнениями первого порядка, но для начала давайте просто напишем самый базовый численный шаг для двумерного уравнения теплопроводности. Оператор Лапласа в дискретном виде довольно известен:
solveHeat[w_, f_, dt_: 0.0025, dx_: 0.1] := Table[
If[i > 1 && i < 50 && j > 1 && j < 50,
w[[i, j]] + dt (
w[[i - 1, j]] + w[[i, j - 1]] - 4 w[[i, j]] +
w[[i, j + 1]] + w[[i + 1, j]]
)/dx^2 + dt f[i, j],
w[[i, j]]
],
{i, 50}, {j, 50}
];
Поставим источник тепла в тот же угол:
Module[{w = Table[0., {50}, {50}]},
FixedPoint[
solveHeat[
#,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, 1.0, 0.0]]
] &,
w,
20
] // ListDensityPlot
]

А теперь посмотрим на эволюцию по времени, если мы сначала включим, а потом через 100 отсчётов выключим нагреватель:
Module[{w = Table[0., {50}, {50}]},
Table[
With[{heater = If[steps > 100 && steps < 300, 100.0, 0.0]},
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{steps, w[[25, 25]]}, {steps, Clip[heater, {0, 0.002}]}}
],
{steps, 1, 1000}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"}
]

Для устойчивости здесь действует условие CFL (Критерий Куранта-Фридрихса-Леви):
Пропорциональный регулятор P
А теперь подключим контроллер.
Module[{w = Table[0., {50}, {50}]},
Table[
With[{error = (0.0022 - w[[25, 25]])},
{heater = 100000.0 Clip[error, {0, Infinity}]},
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{steps, w[[25, 25]]}, {steps, heater/30000.0}}
],
{steps, 1, 3000}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"},
Epilog -> {Red, Dashed, Line[{{0, 0.0022}, {5000, 0.0022}}]},
PlotRange -> {Automatic, {0, 0.0022}},
PlotLabel -> "1x heater power"
]

И тут видно неприятную вещь: возникает систематическая ошибка. Температура не доходит до целевого значения.
Попробуем раскачать систему сильнее:
Module[{w = Table[0., {50}, {50}]},
Table[
With[{error = (0.0022 - w[[25, 25]])},
{heater = 100000.0 Clip[error, {0, Infinity}]},
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{steps, w[[25, 25]]}, {steps, heater/30000.0}}
],
{steps, 1, 3000}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"},
Epilog -> {Red, Dashed, Line[{{0, 0.0022}, {5000, 0.0022}}]},
PlotRange -> {Automatic, {0, 3 0.0022}},
PlotLabel -> "10x heater power"
]

Смотрится уже бодрее, но теперь система начинает жить собственной жизнью: появляются колебания, а систематическая ошибка всё ещё никуда не делась.
Интегральная часть (I)
До сих пор мы игрались только с пропорциональной частью PID. Но вторая по важности штука - это интеграл, который накапливает ошибку со временем.
Ровно так же, как мы до этого делали интегрирование уравнения теплопроводности, можно копить и ошибку.
Module[{w = Table[0., {50}, {50}], accError = 0.0},
Table[
With[{error = (0.0022 - w[[25, 25]])},
{
heater = 100000.0 Clip[error + 0.001 accError, {0, Infinity}]
},
accError += error;
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{steps, w[[25, 25]]}, {steps, heater/30000.0}}
],
{steps, 1, 3000}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"},
Epilog -> {Red, Dashed, Line[{{0, 0.0022}, {5000, 0.0022}}]},
PlotRange -> {Automatic, {0, 3 0.0022}},
PlotLabel -> "10x heater power + I"
]

Ура! Базовая линия медленно подползает к цели.
Уменьшим общую мощность нагревателя и посмотрим ещё раз:
Module[{w = Table[0., {50}, {50}], accError = 0.0},
Table[
With[{error = (0.0022 - w[[25, 25]])},
{
heater = 20000.0 Clip[error + 0.001 accError, {0, Infinity}]
},
accError += error;
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{steps, w[[25, 25]]}, {steps, heater/30000.0}}
],
{steps, 1, 3000}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"},
Epilog -> {Red, Dashed, Line[{{0, 0.0022}, {5000, 0.0022}}]},
PlotRange -> {Automatic, {0, 3 0.0022}},
PlotLabel -> "2x heater power + I"
]

Уже хорошо.
Но можно ли сделать ещё лучше?
Дифференциальная часть (D)
Можно реагировать ещё и на то, на сколько быстро меняется ошибка. Это позволяет заранее подтормаживать систему и гасить нарастающие колебания.
Module[{g = 0, w = Table[0., {50}, {50}], accError = 0.0, prevError = 0.0022},
Table[
With[{error = (0.0022 - w[[25, 25]])},
{
heater = 100000.0 Clip[
0.9 error + 0.001 accError + 100 (error - prevError),
{0, Infinity}
]
},
accError += error;
prevError = error;
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
w = solveHeat[
w,
Function[{i, j}, If[Max[Abs[{i, j} - {25, 2}]] < 1, heater, 0.0]]
];
{{2 steps, w[[25, 25]]}, {2 steps, heater/30000.0}}
],
{steps, 1, 3000/2}
]
];
ListLinePlot[
% // Transpose,
Filling -> 0,
PlotLegends -> {"Temperature (A)", "Heater"},
Epilog -> {Red, Dashed, Line[{{0, 0.0022}, {5000, 0.0022}}]},
PlotRange -> {Automatic, {0, 3 0.0022}},
PlotLabel -> "10x heater power + I + D"
]

Мы заметно уменьшили время нарастания, но за это пришлось заплатить небольшими колебаниями из-за слишком большого коэффициента при D.
Подбор PID-коэффициентов - это вообще отдельное ремесло и наука, и здесь мы этого вряд ли раскроем…
Для сравнения — вариант без D:

В итоге получился отличный виртуальный полигон: FDTD-модель обогревателя очень наглядно показывает, зачем в PID нужны P, I и D.
Вот такой небольшой аттракцион, и мы только только добрались до определения ПИД.
Формальное определение ПИД
Ну что ж, теперь уже можно честно записать ПИД в общем виде:
Где:
-
— управляющий сигнал;
-
— ошибка, то есть разница между желаемым и измеренным значением;
-
,
,
— коэффициенты настройки.
Регулятор положения грузика
Теперь давайте перейдём к примеру по-проще и по-чище: стабилизации положения грузика в заданной точке.
Чтобы решить задачу аналитически (и позже численно), удобно продифференцировать выражение для ПИД и избавиться от интеграла:
Дифференцируем:
Но при этом надо не забыть начальное условие:
Жизнь становится проще, когда в уравнении остаются только производные.
Физическая система
Представим себе грузик , который движется по направляющей, а толкает его маленький ракетный двигатель, управляемый ПИД-регулятором.

Система уравнений тогда будет такой:
system = {
x''[t] == u[t]/m,
x'[0] == 0,
x[0] == 0
};
Проверка
Прежде чем прикручивать PID, проверим, что система вообще ведёт себя как должна.
Подадим постоянную силу:
DSolve[system /. {u[t_] :> 1}, x, t]
Получим:
Ровно та самая парабола из школьной механики.
Function[{t}, t^2/(2 m)] /. {m -> 1.0};
Plot[
%[t],
{t, 0, 10},
AxesLabel -> {"t", "x"}
]

Подключаем П-контроллер
Теперь свяжем управляющий сигнал с положением
через ошибку:
Схема выглядит так

А система уравнений следующая
system = {
u[t] == Kp e[t],
x''[t] == u[t]/m + 1,
x'[0] == 0,
x[0] == 0,
e[t] == (a - x[t])
};
Решим её аналитически:
sol = DSolve[system, {u, x, e}, t] // Flatten;
sol = Simplify[sol, Assumptions -> {Element[a, Reals], m > 0, Kp > 0}];
Построим графики для регулируемого и почти нерегулируемого случая:
x /. sol /. {m -> 1.0, a -> 1.0} /. {{Kp -> 1}, {Kp -> 0.01}};
Plot[
#[t] & /@ % // Evaluate,
{t, 0, 10},
Epilog -> {Dashed, Red, Line[{{0, 1.0}, {10, 1.0}}]},
PlotLegends -> Placed[{"Regulated", "Unregulated"}, {0.15, 0.5}]
]

И сразу видно главное: одного P здесь маловато. Система перелетает цель и начинает осциллировать.
Именно в этот момент в игру и входят I и D.
Расширяем до ПИД
Аналитически это уже решать не так удобно, поэтому перейдём к численным методам NDSolve
system = {
(u')[t] == e[t] Ki + Kp (e')[t] + Kd (e'')[t],
u[0] == Kp (a - x[0]),
x''[t] == u[t]/m + 1.0,
x'[0] == 0.,
x[0] == 0.,
e[t] == (a - x[t])
};
ClearAll[sol];
sol[p_, i_, d_] := Flatten @ NDSolve[
system /. {
m -> 5.0,
a -> 2.5,
Kp -> p,
Ki -> i p,
Kd -> d p
},
{u, x, e},
{t, 0, 200}
];
Сделаем интерактивно
Manipulate[
With[
{
eqs = {Clip[x[t], {-0.5, 4}] + 0.05, Clip[0.01 u[t], {-0.5, 4}]} /. sol[10.0, i, d]
},
Plot[
eqs,
{t, 0, 50},
Frame -> True,
PlotLegends -> {"x[t]", "u[t]"},
FrameLabel -> {"t (time)", "value"},
Epilog -> {Dashed, Red, Line[{{0, 2.5}, {50, 2.5}}]},
PlotRange -> {{-1, 50}, {-1, 4.5}}
]
],
{{i, 0, "I"}, 0, 0.1, 0.025},
{{d, 0, "D"}, 0, 1.0, 0.25},
ContinuousAction -> True,
Appearance -> None
]

Картина получается ровно такая, какую и хочется увидеть:
-
D гасит колебания,
-
I убирает систематическую ошибку.
Ещё более интерактивная версия
Чтобы обновлять систему совсем вживую, например, мышкой, снова переходим к итерационной схеме, так как NDSolve тут не поможет
Исходный PID:
Поднимаем интеграл в производную:
Введём вспомогательную переменную:
Тогда:
Дальше дискретизируем правилами замены (ничего не делаем вручную):
{
h[t] == e'[t],
u'[t] == Kp h[t] + Ki e[t] + Kd h'[t]
} /. {
(e_)'[t] :> (e[n] - e[n - 1])/[Delta]t,
e_[t] :> e[n]
};
dsol = Solve[%, {h[n], u[n]}] // Flatten;
Определим конструктор дискретного PID-контроллера:
createPID := Module[{uState, hState, eState},
With[
{
rules1 = dsol,
rules2 = {
e[n - 1] -> Indexed[eState, 2],
e[n] -> Indexed[eState, 1],
h[n - 1] -> Indexed[hState, 2],
h[n] -> Indexed[hState, 1],
u[n - 1] -> Indexed[uState, 2],
u[n] -> Indexed[uState, 1],
[Delta]t -> #5,
Kd -> #4,
Kp -> #2,
Ki -> #3
}
},
Hold@Module[{},
uState = {0, 0};
hState = {0, 0};
eState = {0, 0};
(
eState[[2]] = eState[[1]];
eState[[1]] = #1;
hState[[2]] = hState[[1]];
hState[[1]] = h[n];
uState[[2]] = uState[[1]];
uState[[1]] = u[n]
) &
] /. rules1 /. rules2
]
] // ReleaseHold;
Здесь мы использовали Indexed вместо [[]] для того, чтобы WL не ругался на то, что наши символы пока ещё не являются массивами.
Создадим экземпляр:
pid = createPID;
Проверим:
pid[0.1, 0.1, 0.1, 0.1, 0.1]
В принципе, каждый шаг интегрирования здесь можно было бы свернуть до простого умножения на матрицу - коэффициенты ведь постоянны.
Теперь напишем тестовую систему с грузиком и ракетным двигателем:
{
x''[t] == u[t]/m,
x'[0] == 0,
x[0] == 0,
e[t] == (a - x[t])
}
Переведём её в дискретную форму:
{
v'[t] == u[t]/m,
x'[t] == v[t],
e[t] == (a - x[t])
} /. {
v'[t] -> (v[n] - v[n - 1])/[Delta]t,
v[t] -> v[n],
x'[t] -> (x[n] - x[n - 1])/[Delta]t,
x[t] -> x[n],
e[t] -> e[n]
} // Simplify;
Solve[%, {v[n], x[n], e[n]}] // Flatten // TableForm
А теперь вручную соберём совсем простую модель:
testSystem[initialPos_, mass_] := Module[
{stateV = {0., 0.}, stateX = {1., 1.} initialPos},
Function[{u, target, dt},
stateV[[2]] = stateV[[1]];
stateV[[1]] = If[
Abs[stateX[[1]]] > 1,
-stateV[[1]],
stateV[[2]] + (dt/mass) u
];
stateX[[2]] = stateX[[1]];
stateX[[1]] = Clip[stateX[[2]] + dt stateV[[1]], {-0.9, 0.9}, {-0.88, 0.88}];
{stateX[[1]], target - stateX[[1]]}
]
];
Не забудем о стенках: пусть блок отскакивает и не улетает в космос - для этого Clip.
Собираем всё вместе
Теперь цель уже не фиксированная, а управляется движением мыши в реальном времени.
Схема примерно такая:

Код (сработает только в WLJS Notebook, но не в Mathematica):
pid = createPID;
sys = testSystem[0., 1.0];
Module[
{
value = 0.0,
error = 0.0,
target = 0.5,
control = 0.0,
gravity = 0.25,
history = Table[{i, 0.}, {i, 300}],
p = <|"P" -> 1.0, "I" -> 0.0, "D" -> 0.0|>,
handler
},
handler = Function[Null,
{value, error} = sys[
Clip[pid[error, p["P"], p["I"], p["D"], 0.1], {-10, 10}] - gravity,
target,
0.1
];
history[[1, 2]] = error;
history = Transpose[{history[[All, 1]], RotateLeft[history[[All, 2]]]}];
];
Row[{
EventHandler[
Graphics[
{
Red, Line[{{-1, target}, {1, target}}],
Blue, Line[{{-1, value}, {1, value}}],
EventHandler[AnimationFrameListener[value], handler]
},
PlotRange -> {{-1, 1}, {-1, 1}},
ImageSize -> {100, 300}
],
{
"mousemove" -> Function[xy, target = Clip[xy[[2]], {-0.8, 0.8}]]
}
],
Graphics[
Line[history],
PlotRange -> {{1, 300}, {-1, 1}},
ImageSize -> {200, 300},
Axes -> True,
AxesOrigin -> {0, 0.00001},
Ticks -> {False, True},
Frame -> True,
PlotLabel -> "Error"
],
EventHandler[
InputGroup[<|
"P" -> InputRange[0, 2.0, 0.01, p["P"], "Label" -> "P"],
"I" -> InputRange[0, 2.0, 0.01, p["I"], "Label" -> "I"],
"D" -> InputRange[0, 2.0, 0.01, p["D"], "Label" -> "D"]
|>],
Function[data, p = data]
]
}]
]

Если поиграться руками, очень быстро становится видно: D нужен, чтобы успокаивать колебания, а I — чтобы компенсировать постоянные смещения, вроде той же «гравитации» в нашей игрушечной модели.
Самобалансирующийся робот
Ну а теперь - к сложному!

Прежде чем собирать целого робота, давайте дойдём до него через несколько более простых систем.
Второй закон Ньютона для вращения
Начнём с маятника

Для вращательного движения имеем:
Если оставить только одну координату, получим:
А если перейти в ускоряющуюся систему отсчёта, появится фиктивный момент силы:

Если добавить трение, получится:
Соберём численную модель маятника:
createPendulum[iθ_, g_, l_, d_: 0.1] := Module[
{ω = {0., 0.}, θ = {1., 1.} iθ},
Function[{ax, dt},
ω[[2]] = ω[[1]];
ω[[1]] =
(1.0 - d) ω[[2]] +
(dt g/l) Sin[θ[[1]]] +
(dt ax/l) Cos[θ[[1]]];
θ[[2]] = θ[[1]];
θ[[1]] = θ[[2]] + dt ω[[1]];
θ[[1]]
]
];
Посмотрим, насколько это вообще нестабильная штука:
pen = createPendulum[0.01, 9.9, 1.0, 0.003];
Module[{pos = {0, 1.0}, t = 0.0},
Animate[
t = pen[0.0, 0.1];
pos = {Sin[t], Cos[t]};
Graphics[
{Line[{{0, 0}, pos}], Disk[pos, 0.05]},
PlotRange -> {{-1, 1}, {-1, 1}},
AxesOrigin -> {0.00001, 0},
Axes -> True,
Ticks -> None
],
{dummy, 0, 0.5, 0.01},
Appearance -> None
]
]

Колёса для робота
Что делать с платформой? PID, скорее всего, будет управлять либо напряжением, либо током на моторе.
Для электрических моторов, особенно DC, напряжение обычно определяет скорость, а ток - вращательный момент.
Пойдём по пути напряжения. Вращение колёс будем переводить напрямую в координату (сферический конь):
createPlatform[initialPos_, mass_] := Module[
{stateV = {0., 0.}, stateX = {1., 1.} initialPos},
Function[{u, dt},
stateV[[2]] = stateV[[1]];
stateV[[1]] = If[Abs[Abs[stateX[[1]]] - 2.0] < 0.01, 0, u];
stateX[[2]] = stateX[[1]];
stateX[[1]] = Clip[stateX[[2]] + dt stateV[[1]], {-2, 2}];
{stateX[[1]], (stateV[[1]] - stateV[[2]])/dt}
]
];
Мы сделали также простое ограничение: если платформа упёрлась слишком далеко, моторы больше не тянут.
Проверим, как это всё выглядит без регулятора:
Module[
{
platform = createPlatform[0.0, 3.0],
pen = createPendulum[0.00, 9.9, 1.0, 0.003],
x = .0,
ac = .0,
voltage = .0,
pos = {0., 0.},
angle = 0.0,
handler
},
handler = Function[Null,
{x, ac} = platform[voltage, 0.01];
angle = pen[-ac, 0.01];
pos = {Sin[angle], Cos[angle]};
];
EventHandler[
Graphics[
{
Translate[
{
{Brown, Disk[{0, -0.8 + 1.0}, 0.1]},
Rectangle[{-0.4, -1.0 + 0.2 + 1.0}, {0.4, -1.0 + 0.4 + 1.0}],
Red, Line[{{0, 0.4}, pos}], Disk[pos, 0.05]
},
{x, -0.1 - 1.0}
],
Line[{{-2.0, -1.0}, {2, -1.0}}],
EventHandler[AnimationFrameListener[x], handler]
},
PlotRange -> {{-2, 2}, {-1, 1}},
ImageSize -> {400, 200}
],
{
"mousemove" -> Function[xy, voltage = 2.0 xy[[1]]]
}
]
]

Мышкой это стабилизировать всё еще можно, если постараться.
Добавляем PID
Теперь добавим PID по углу маятника. Он будет управлять напряжением моторов так, чтобы угол стремился к нулю

Но чтобы ещё и двигать робота к нужной позиции, добавим смещение по положению:

Соберём всё вместе:
Module[{p = <|"P" -> 11.71, "I" -> 36.56, "D" -> 0|>},
Module[
{
platform = createPlatform[0.0, 1.0],
pid = createPID,
pid2 = createPID,
pen = createPendulum[0.01, 9., 1.0, 0.03],
x = .0,
ac = .0,
target = 0.0,
voltage = .0,
pos = {0., 0.},
angle = 0.0,
history = Table[{i, 0.}, {i, 300}],
handler
},
handler = Function[Null,
voltage = Clip[pid[angle, p["P"], p["I"], p["D"], 2 0.02], {-15, 15}];
voltage = Clip[pid[angle, p["P"], p["I"], p["D"], 2 0.02], {-15, 15}];
{x, ac} = platform[voltage + 2.0 (x - target), 2 0.02];
angle = pen[-ac, 2 0.02];
{x, ac} = platform[voltage + 2.0 (x - target), 2 0.02];
angle = pen[-ac, 2 0.02];
pos = {Sin[angle], Cos[angle]};
history[[1, 2]] = voltage;
history = Transpose[{history[[All, 1]], RotateLeft[history[[All, 2]]]}];
];
Row[{
EventHandler[
Graphics[
{
Translate[
{
{Brown, Disk[{0, -0.8 + 1.0}, 0.1]},
Rectangle[{-0.4, -1.0 + 0.2 + 1.0}, {0.4, -1.0 + 0.4 + 1.0}],
Red, Line[{{0, 0.4}, pos}], Disk[pos, 0.05]
},
{x, -0.1 - 1.0}
],
Line[{{-2.0, -1.0}, {2, -1.0}}],
EventHandler[AnimationFrameListener[pos], handler],
Green, Arrow[{{target, -1.2}, {target, -1.0}}]
},
PlotRange -> {{-2, 2}, {-1.2, 0.8}},
ImageSize -> {400, 200}
],
{
"mousemove" -> Function[xy, target = 0.6 target + 0.4 xy[[1]]]
}
],
Graphics[
Line[history],
PlotRange -> {{1, 300}, {-2, 2}},
ImageSize -> {200, 300},
Axes -> True,
AxesOrigin -> {0, 0.00001},
Ticks -> {False, True},
Frame -> True,
PlotLabel -> "Voltage"
],
Column[{
Button[
"Reset",
platform = createPlatform[0.0, 1.0];
pid = createPID;
pen = createPendulum[0.01, 9., 1.0, 0.03];
ac = .0;
voltage = .0;
angle = 0.0;
],
EventHandler[
InputGroup[<|
"P" -> InputRange[0, 40.0, 0.01, p["P"], "Label" -> "P"],
"I" -> InputRange[0, 40.0, 0.01, p["I"], "Label" -> "I"],
"D" -> InputRange[0, 10.0, 0.01, p["D"], "Label" -> "D"]
|>],
Function[data, p = data]
]
}]
}]
]
]

И вот тут уже видно особенно хорошо: в этой системе критичны прежде всего P и I.
Система сильно нелинейная: вращение маятника связано с линейным движением платформы через моторы, поэтому влияние I, D здесь может показаться неочевидным. Это уже совсем не тот уютный обогреватель и не тот блок на рельсе, который мы прогали в начале.
Вместо вывода
За самыми обычными вещами — обогревателем, мотором, ручками самоката - часто стоят удивительно красивые идеи.
PID - это одна из них.
Ссылки
-
Среда песочница, где были построены все примеры
Автор: JerryI
