Мощные инструменты, позволяющие это делать, либо сложно стыкуются со сторонними программами (Modelica), либо проприетарны (Wolfram Mathematica, различные САПР), и я решил делать велосипед на Julia. Потом все наработки можно будет состыковать с сервисами ROS.
Будем считать, что наши роботы двигаются достаточно медленно и их механика находится в состоянии с наименьшей энергией, при ограничениях, заданных конструкцией и сервоприводами. Таким образом нам достаточно решить задачу оптимизации в ограничениях, для чего понадобятся пакеты "JuMP" (для нелинейной оптимизации ему понадобится пакет "Ipopt", который не указан в зависимостях (вместо него можно использовать проприетарные библиотеки, но я хочу ограничится свободными) и должен быть установлен отдельно). Решать дифференциальные уравнения, как в Modelica, мы не будем, хотя для этого есть достаточно развитые пакеты, например "DASSL".
Управлять системой мы будем используя реактивное программирование (библиотеку "Reactive"). Рисовать в «блокноте» (Jupyter), для чего потребуются "IJulia", "Interact" и "Compose". Для удобства еще понадобится "MacroTools".
Для инсталляции пакетов надо выполнить в Julia REPL команду
foreach(Pkg.add, ["IJulia", "Ipopt", "Interact", "Reactive", "JuMP", "Compose", "MacroTools"])
Рассмотрим простую двумерную систему, схематически изображенную на рисунке.
Красными линиями там обозначены пружины, черными — нерастяжимые веревки, маленький кружечек — невесомый блок, большой — груз. У веревок есть длина, у пружины — длина и жесткость. Переменные координаты назовем:
(x,y) — координаты груза (большого круга)
(xctl, yctl) — координаты «управляющего конца» веревки, длиной 1.7 привязанной к грузу.
(xp, yp) — координаты невесомого точечного блока, способного скользить по веревке.
Одна пружина длины 0.1 и жесткости 1 закреплена в точке (0,3) и прицеплена к грузу. Вторая пружина длиной 0.15 и жесткостью 5 соединяет груз и блок, который скользит по веревке длиной 6 закрепленной в точках (5,1) и (4.5,5).
(параметры подбирались эволюционно, что бы мне было наглядно и понятно как она себя ведет при добавлении новых фич)
@wire(x,y, xctl,yctl, 1.7)
@wire(xp, yp, 5.0,1.0, 4.5,5.0, 6.0)
@energy([w(x,y, 0,3, 1, 0.1), w(x,y, xp,yp, 5, 0.15), 0.4*y])
полный код функции, решающей уравнение
function myModel(xctl, yctl)
m = Model()
@variable(m, y >= 0)
@variable(m, x)
@variable(m, yp)
@variable(m, xp)
@wire(x,y, xctl,yctl, 1.7)
@wire(xp, yp, 5.0,1.0, 4.5,5.0, 6.0)
@energy([w(x,y, 0,3, 1, 0.1), w(x,y, xp,yp, 5, 0.15), 0.4*y])
status = solve(m)
xval = getvalue(x)
yval = getvalue(y)
xpval = getvalue(xp)
ypval = getvalue(yp)
# print("calculate $status $xval $yval for $xctl, $yctl\n")
(status, xval, yval, xpval, ypval)
end
Макрос wire задает ограничение «связывание нерастяжимой веревкой» заданной длины два объекта или два объекта и блок.
Макрос energy описывает минимизируемую функцию энергии — специальный терм w описывает пружину (с заданной жесткостью и длиной), а 0.4*y — энергия груза в гравитационном поле, слишком простая, что бы для ее придумывать специальный синтаксис.
Эти макросы выражаются через библиотечными NLconstraint и NLobjective (линейные constraint и objective эффективнее, но с нашими функциями не справляются):
@NLconstraint(m, (x-xctl)^2+(y-yctl)^2 <= 1.7)
@NLconstraint(m, sqrt((5.0-xp)^2+(1.0-yp)^2) + sqrt((4.5-xp)^2+(5.0-yp)^2) <= 6.0)
@NLobjective(m, Min, 1*(sqrt(((x-0)^2 + (y - 3)^2)) - 0.1)^2
+ 5*(sqrt((x - xp)^2 + (y - yp)^2) - 0.15)^2
+ 0.4*y)
Использование в ограничениях «меньше или равно» вместо «равно» означает что веревка может провисать, но не р��стягиваться. Для двух точек можно использовать строгое равенство, если надо соединить их «жестким стержнем», заменив соответствующий макрос.
код макросов
macro wire(x,y,x0,y0, l)
v = l^2
:(@NLconstraint(m, ($x0-$x)^2+($y0-$y)^2 <= $v))
end
macro wire(x,y, x0,y0, x1,y1, l)
:(@NLconstraint(m, sqrt(($x0-$x)^2+($y0-$y)^2) + sqrt(($x1-$x)^2+($y1-$y)^2) <= $l))
end
calcenergy(d) = MacroTools.@match d begin
[t__] => :(+$(map(energy1,t)...))
v_ => energy1(v)
end
energy1(v) = MacroTools.@match v begin
w(x1_,y1_, x2_,y2_, k_, l_) => :($k*(sqrt(($x1 - $x2)^2 + ($y1 - $y2)^2) - $l)^2)
x_ => x
end
macro energy(v)
e = calcenergy(v)
:(@NLobjective(m, Min, $e))
end
Теперь опишем элементы управления:
xctl = slider(-2:0.01:5, label="X Control")
xctlsig = signal(xctl)
yctl = slider(-1:0.01:0.5, label="Y Control")
yctlsig = signal(yctl)
присоединим их к нашей системе:
ops = map(myModel, xctlsig, yctlsig)
И отобразим это в notebook:
пара вспомогательных функций
xscale = 3
yscale = 3
shift = 1.5
pscale(x,y) = ((x*xscale+shift)cm, (y*yscale)cm)
function l(x1,y1, x2,y2; w=0.3mm, color="black")
compose(context(),
linewidth(w), stroke(color),
line([pscale(x1, y1), pscale(x2, y2)]))
end
@manipulate for xc in xctl, yc in yctl, op in ops
compose(context(),
# text(150px, 220px, "m = $(op[2]) $(op[3])"),
# text(150px, 200px, "p = $(op[4]) $(op[5])"),
circle(pscale(op[2], op[3])..., 0.03),
circle(pscale(op[4], op[5])..., 0.01),
l(op[2], op[3], op[4], op[5], color="red"),
l(op[2], op[3], 0, 3, color="red"),
l(op[2], op[3], xc, yc),
l(op[4], op[5], 5, 1),
l(op[4], op[5], 4.5,5)
)
end
полный вариант кода
using Interact, Reactive, JuMP, Compose, MacroTools
macro wire(x,y,x0,y0, l)
v = l^2
:(@NLconstraint(m, ($x0-$x)^2+($y0-$y)^2 <= $v))
end
macro wire(x,y, x0,y0, x1,y1, l)
:(@NLconstraint(m, sqrt(($x0-$x)^2+($y0-$y)^2) + sqrt(($x1-$x)^2+($y1-$y)^2) <= $l))
end
calcenergy(d) = MacroTools.@match d begin
[t__] => :(+$(map(energy1,t)...))
v_ => energy1(v)
end
energy1(v) = MacroTools.@match v begin
w(x1_,y1_, x2_,y2_, k_, l_) => :($k*(sqrt(($x1 - $x2)^2 + ($y1 - $y2)^2) - $l)^2)
x_ => x
end
macro energy(v)
e = calcenergy(v)
:(@NLobjective(m, Min, $e))
end
function myModel(xctl, yctl)
m = Model()
@variable(m, y >= 0)
@variable(m, x)
@variable(m, yp)
@variable(m, xp)
@wire(x,y, xctl,yctl, 1.7)
@wire(xp, yp, 5.0,1.0, 4.5,5.0, 6.0)
@energy([w(x,y, 0,3, 1, 0.1), w(x,y, xp,yp, 5, 0.15), 0.4*y])
status = solve(m)
xval = getvalue(x)
yval = getvalue(y)
xpval = getvalue(xp)
ypval = getvalue(yp)
# print("calculate $status $xval $yval for $xctl, $yctl\n")
(status, xval, yval, xpval, ypval)
end
xctl = slider(-2:0.01:5, label="X Control")
xctlsig = signal(xctl)
yctl = slider(-1:0.01:0.5, label="Y Control")
yctlsig = signal(yctl)
ops = map(myModel, xctlsig, yctlsig)
xscale = 3
yscale = 3
shift = 1.5
pscale(x,y) = ((x*xscale+shift)cm, (y*yscale)cm)
function l(x1,y1, x2,y2; w=0.3mm, color="black")
compose(context(),
linewidth(w), stroke(color),
line([pscale(x1, y1), pscale(x2, y2)]))
end
@manipulate for xc in xctl, yc in yctl, op in ops
compose(context(),
# text(150px, 220px, "m = $(op[2]) $(op[3])"),
# text(150px, 200px, "p = $(op[4]) $(op[5])"),
circle(pscale(op[2], op[3])..., 0.03),
circle(pscale(op[4], op[5])..., 0.01),
l(op[2], op[3], op[4], op[5], color="red"),
l(op[2], op[3], 0, 3, color="red"),
l(op[2], op[3], xc, yc),
l(op[4], op[5], 5, 1),
l(op[4], op[5], 4.5,5)
)
end
Простой способ запустить notebook: в Julia REPL набрать
using IJulia notebook()
. Это должно открыть в браузере "http://localhost:8888/tree". Там надо выбрать «new»/«Julia», там в поле ввода скопировать код и нажать Shift-Enter.