Theo Jansen 行走机构的进化算法
有一位荷兰艺术家/工程师创造了一个非常复杂的行走机构。工作原理可以看这里:
http://www.strandbeest.com/beests_leg.php
奇怪的是,他使用了一种自制的进化算法来计算理想的链接长度,这在页面底部有描述。
我创建了一个Python脚本来直观地分析自行车的地面接触部分,这必须满足两个要求:
- 尽可能直,以免上下晃动;
- 速度尽可能恒定,以免一只脚拖拽另一只脚;
这两个标准将导致“轮状”效应,机器线性前进而不浪费动能。
问题是:
“您是否有任何建议,可以使用简单的进化迭代公式来优化腿长(通过在下面的代码中插入正确的突变),从而根据上述两个标准来改善步行路径?”
编辑:关于基因组候选者的“拟合规则”的一些建议:
- 取循环的“下部”(地面接触),因为它对应于曲柄转数的三分之一(注意下部可能有非水平斜率)并且仍然是线性的);
- 对该“地面接触”部分的点位置应用线性回归;
- 从线性回归计算垂直变化(最小二乘?)
- 通过平行于回归线的一个点与前一个点之间的差异计算速度变化;
- (可选)绘制这些“误差函数”的图表,可能允许直观地选择突变体(嘘声......;o)。
这是我的代码,使用 Python + GTK,它提供了对问题的一些直观了解: (编辑:现在参数化的幻数会受到 mut
值的影响)
# coding: utf-8
import pygtk
pygtk.require('2.0')
import gtk, cairo
from math import *
class Mechanism():
def __init__(s):
pass
def assemble(s, angle):
# magic numbers (unmutated)
mu = [38, 7.8, 15, 50, 41.5, 39.3, 61.9, 55.8, 40.1, 39.4, 36.7, 65.7, 49]
# mutations
mut = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
# mutated
mn = [mu[n]+mut[n] for n in range(13)]
s.A = Point(0,0)
s.B = Point(-mn[0], -mn[1])
s.C = fromPoint(s.A, mn[2], angle)
s.ac = Line(s.A, s.C)
s.D = linkage(s.C, mn[3], s.B, mn[4])
s.cd = Line(s.C, s.D)
s.bd = Line(s.B, s.D)
s.E = linkage(s.B, mn[5], s.C, mn[6])
s.be = Line(s.B, s.E)
s.ce = Line(s.C, s.E)
s.F = linkage(s.D, mn[7], s.B, mn[8])
s.df = Line(s.D, s.F)
s.bf = Line(s.B, s.F)
s.G = linkage(s.F, mn[9], s.E, mn[10])
s.fg = Line(s.F, s.G)
s.eg = Line(s.E, s.G)
s.H = linkage(s.G, mn[11], s.E, mn[12])
s.gh = Line(s.G, s.H)
s.EH = Line(s.E, s.H)
return s.H
class Point:
def __init__(self, x, y):
self.x, self.y = float(x), float(y)
def __str__(self):
return "(%.2f, %.2f)" % (self.x, self.y)
class Line:
def __init__(self, p1, p2):
self.p1, self.p2 = p1, p2
def length(self):
return sqrt((p1.x-p2.x)**2 + (p1.y-p2.y)**2)
def fromPoint(point, distance, angle):
angle = radians(angle)
return Point(point.x + distance * cos(angle),
point.y + distance * sin(angle))
def distance(p1, p2):
return sqrt( (p1.x - p2.x)**2 + (p1.y - p2.y)**2 )
def ccw(p1, p2, px):
""" Test if px is at the right side of the line p1p2 """
ax, ay, bx, by = p1.x, p1.y, p2.x, p2.y
cx, cy = px.x, px.y
return (bx-ax)*(cy-ay)-(by-ay)*(cx-ax) < 0
def linkage(p1, l1, p2, l2):
l1 = float(l1)
l2 = float(l2)
dx,dy = p2.x-p1.x, p2.y-p1.y
d = sqrt(dx**2 + dy**2) # distance between the centers
a = (l1**2 - l2**2 + d**2) / (2*d) # distance from first center to the radical line
M = Point(p1.x + (dx * a/d), p1.y + (dy * a/d)) # intersection of centerline with radical line
h = sqrt(l1**2 - a**2) # distance from the midline to any of the points
rx,ry = -dy*(h/d), dx*(h/d)
# There are two results, but only one (the correct side of the line) must be chosen
R1 = Point(M.x + rx, M.y + ry)
R2 = Point(M.x - rx, M.y - ry)
test1 = ccw(p1, p2, R1)
test2 = ccw(p1, p2, R2)
if test1:
return R1
else:
return R2
###############################33
mec = Mechanism()
stepcurve = [mec.assemble(p) for p in xrange(360)]
window=gtk.Window()
panel = gtk.VBox()
window.add(panel)
toppanel = gtk.HBox()
panel.pack_start(toppanel)
class Canvas(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
self.connect("expose_event", self.expose)
def expose(self, widget, event):
cr = widget.window.cairo_create()
rect = self.get_allocation()
w = rect.width
h = rect.height
cr.translate(w*0.85, h*0.3)
scale = 1
cr.scale(scale, -scale)
cr.set_line_width(1)
def paintpoint(p):
cr.arc(p.x, p.y, 1.2, 0, 2*pi)
cr.set_source_rgb(1,1,1)
cr.fill_preserve()
cr.set_source_rgb(0,0,0)
cr.stroke()
def paintline(l):
cr.move_to(l.p1.x, l.p1.y)
cr.line_to(l.p2.x, l.p2.y)
cr.stroke()
for i in mec.__dict__:
if mec.__dict__[i].__class__.__name__ == 'Line':
paintline(mec.__dict__[i])
for i in mec.__dict__:
if mec.__dict__[i].__class__.__name__ == 'Point':
paintpoint(mec.__dict__[i])
cr.move_to(stepcurve[0].x, stepcurve[0].y)
for p in stepcurve[1:]:
cr.line_to(p.x, p.y)
cr.close_path()
cr.set_source_rgb(1,0,0)
cr.set_line_join(cairo.LINE_JOIN_ROUND)
cr.stroke()
class FootPath(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
self.connect("expose_event", self.expose)
def expose(self, widget, event):
cr = widget.window.cairo_create()
rect = self.get_allocation()
w = rect.width
h = rect.height
cr.save()
cr.translate(w/2, h/2)
scale = 20
cr.scale(scale, -scale)
cr.translate(40,92)
twocurves = stepcurve.extend(stepcurve)
cstart = 305
cr.set_source_rgb(0,0.5,0)
for p in stepcurve[cstart:cstart+121]:
cr.arc(p.x, p.y, 0.1, 0, 2*pi)
cr.fill()
cr.move_to(stepcurve[cstart].x, stepcurve[cstart].y)
for p in stepcurve[cstart+1:cstart+121]:
cr.line_to(p.x, p.y)
cr.set_line_join(cairo.LINE_JOIN_ROUND)
cr.restore()
cr.set_source_rgb(1,0,0)
cr.set_line_width(1)
cr.stroke()
cr.save()
cr.translate(w/2, h/2)
scale = 20
cr.scale(scale, -scale)
cr.translate(40,92)
cr.move_to(stepcurve[cstart+120].x, stepcurve[cstart+120].y)
for p in stepcurve[cstart+120+1:cstart+360+1]:
cr.line_to(p.x, p.y)
cr.restore()
cr.set_source_rgb(0,0,1)
cr.set_line_width(1)
cr.stroke()
canvas = Canvas()
canvas.set_size_request(140,150)
toppanel.pack_start(canvas, False, False)
toppanel.pack_start(gtk.VSeparator(), False, False)
footpath = FootPath()
footpath.set_size_request(1000,-1)
toppanel.pack_start(footpath, True, True)
def changeangle(par):
mec.assemble(par.get_value()-60)
canvas.queue_draw()
angleadjust = gtk.Adjustment(value=0, lower=0, upper=360, step_incr=1)
angleScale = gtk.HScale(adjustment=angleadjust)
angleScale.set_value_pos(gtk.POS_LEFT)
angleScale.connect("value-changed", changeangle)
panel.pack_start(angleScale, False, False)
window.set_position(gtk.WIN_POS_CENTER)
window.show_all()
gtk.main()
There is a Dutch artist/engineer who created a very elaborate walking mechanism. The working principle can be seen here:
http://www.strandbeest.com/beests_leg.php
The curious part is that he used a self-made Evolutionary Algorithm to calculate the ideal link lengths, which are described at the bottom of the page.
I created a Python script to visually analyze the ground-contact part of the cycle, which must have two requisites fulfilled:
- Be as straight as possible, so as not to wobble up and down;
- Have a speed as constant as possible, so as not to drag one foot against the other;
These two criteria would result in a "wheel like" effect, with the machine going linearly ahead without wasting kinetic energy.
The question is:
"Do you have any suggestion of a simple evolutionary iterative formula to optimize leg lengths (by inserting the correct mutations in the code below) so as to improve the walking path given the two criteria above?"
EDIT: some suggestions about the "fitting rule" for genome candidates:
- Take the "lower part" (ground contact) of the cycle, given that it corresponds to one third of crank revolution (mind the lower part might have a non-horizontal slope and still be linear);
- Apply linear regression on the point positions of this "ground contact" part;
- Calculate vertical variation from the linear regression (least squares?)
- Calculate speed variation by the difference between one point and the previous one, parallel to the regression line;
- (optional) plot graphs of these "error functions", possibly allowing to select mutants visually (boooring... ;o).
Here is my code, in Python + GTK, which gives some visual insight into the problem:
(EDIT: now with parametrized magic numbers subject to mutation by mut
's values)
# coding: utf-8
import pygtk
pygtk.require('2.0')
import gtk, cairo
from math import *
class Mechanism():
def __init__(s):
pass
def assemble(s, angle):
# magic numbers (unmutated)
mu = [38, 7.8, 15, 50, 41.5, 39.3, 61.9, 55.8, 40.1, 39.4, 36.7, 65.7, 49]
# mutations
mut = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
# mutated
mn = [mu[n]+mut[n] for n in range(13)]
s.A = Point(0,0)
s.B = Point(-mn[0], -mn[1])
s.C = fromPoint(s.A, mn[2], angle)
s.ac = Line(s.A, s.C)
s.D = linkage(s.C, mn[3], s.B, mn[4])
s.cd = Line(s.C, s.D)
s.bd = Line(s.B, s.D)
s.E = linkage(s.B, mn[5], s.C, mn[6])
s.be = Line(s.B, s.E)
s.ce = Line(s.C, s.E)
s.F = linkage(s.D, mn[7], s.B, mn[8])
s.df = Line(s.D, s.F)
s.bf = Line(s.B, s.F)
s.G = linkage(s.F, mn[9], s.E, mn[10])
s.fg = Line(s.F, s.G)
s.eg = Line(s.E, s.G)
s.H = linkage(s.G, mn[11], s.E, mn[12])
s.gh = Line(s.G, s.H)
s.EH = Line(s.E, s.H)
return s.H
class Point:
def __init__(self, x, y):
self.x, self.y = float(x), float(y)
def __str__(self):
return "(%.2f, %.2f)" % (self.x, self.y)
class Line:
def __init__(self, p1, p2):
self.p1, self.p2 = p1, p2
def length(self):
return sqrt((p1.x-p2.x)**2 + (p1.y-p2.y)**2)
def fromPoint(point, distance, angle):
angle = radians(angle)
return Point(point.x + distance * cos(angle),
point.y + distance * sin(angle))
def distance(p1, p2):
return sqrt( (p1.x - p2.x)**2 + (p1.y - p2.y)**2 )
def ccw(p1, p2, px):
""" Test if px is at the right side of the line p1p2 """
ax, ay, bx, by = p1.x, p1.y, p2.x, p2.y
cx, cy = px.x, px.y
return (bx-ax)*(cy-ay)-(by-ay)*(cx-ax) < 0
def linkage(p1, l1, p2, l2):
l1 = float(l1)
l2 = float(l2)
dx,dy = p2.x-p1.x, p2.y-p1.y
d = sqrt(dx**2 + dy**2) # distance between the centers
a = (l1**2 - l2**2 + d**2) / (2*d) # distance from first center to the radical line
M = Point(p1.x + (dx * a/d), p1.y + (dy * a/d)) # intersection of centerline with radical line
h = sqrt(l1**2 - a**2) # distance from the midline to any of the points
rx,ry = -dy*(h/d), dx*(h/d)
# There are two results, but only one (the correct side of the line) must be chosen
R1 = Point(M.x + rx, M.y + ry)
R2 = Point(M.x - rx, M.y - ry)
test1 = ccw(p1, p2, R1)
test2 = ccw(p1, p2, R2)
if test1:
return R1
else:
return R2
###############################33
mec = Mechanism()
stepcurve = [mec.assemble(p) for p in xrange(360)]
window=gtk.Window()
panel = gtk.VBox()
window.add(panel)
toppanel = gtk.HBox()
panel.pack_start(toppanel)
class Canvas(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
self.connect("expose_event", self.expose)
def expose(self, widget, event):
cr = widget.window.cairo_create()
rect = self.get_allocation()
w = rect.width
h = rect.height
cr.translate(w*0.85, h*0.3)
scale = 1
cr.scale(scale, -scale)
cr.set_line_width(1)
def paintpoint(p):
cr.arc(p.x, p.y, 1.2, 0, 2*pi)
cr.set_source_rgb(1,1,1)
cr.fill_preserve()
cr.set_source_rgb(0,0,0)
cr.stroke()
def paintline(l):
cr.move_to(l.p1.x, l.p1.y)
cr.line_to(l.p2.x, l.p2.y)
cr.stroke()
for i in mec.__dict__:
if mec.__dict__[i].__class__.__name__ == 'Line':
paintline(mec.__dict__[i])
for i in mec.__dict__:
if mec.__dict__[i].__class__.__name__ == 'Point':
paintpoint(mec.__dict__[i])
cr.move_to(stepcurve[0].x, stepcurve[0].y)
for p in stepcurve[1:]:
cr.line_to(p.x, p.y)
cr.close_path()
cr.set_source_rgb(1,0,0)
cr.set_line_join(cairo.LINE_JOIN_ROUND)
cr.stroke()
class FootPath(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
self.connect("expose_event", self.expose)
def expose(self, widget, event):
cr = widget.window.cairo_create()
rect = self.get_allocation()
w = rect.width
h = rect.height
cr.save()
cr.translate(w/2, h/2)
scale = 20
cr.scale(scale, -scale)
cr.translate(40,92)
twocurves = stepcurve.extend(stepcurve)
cstart = 305
cr.set_source_rgb(0,0.5,0)
for p in stepcurve[cstart:cstart+121]:
cr.arc(p.x, p.y, 0.1, 0, 2*pi)
cr.fill()
cr.move_to(stepcurve[cstart].x, stepcurve[cstart].y)
for p in stepcurve[cstart+1:cstart+121]:
cr.line_to(p.x, p.y)
cr.set_line_join(cairo.LINE_JOIN_ROUND)
cr.restore()
cr.set_source_rgb(1,0,0)
cr.set_line_width(1)
cr.stroke()
cr.save()
cr.translate(w/2, h/2)
scale = 20
cr.scale(scale, -scale)
cr.translate(40,92)
cr.move_to(stepcurve[cstart+120].x, stepcurve[cstart+120].y)
for p in stepcurve[cstart+120+1:cstart+360+1]:
cr.line_to(p.x, p.y)
cr.restore()
cr.set_source_rgb(0,0,1)
cr.set_line_width(1)
cr.stroke()
canvas = Canvas()
canvas.set_size_request(140,150)
toppanel.pack_start(canvas, False, False)
toppanel.pack_start(gtk.VSeparator(), False, False)
footpath = FootPath()
footpath.set_size_request(1000,-1)
toppanel.pack_start(footpath, True, True)
def changeangle(par):
mec.assemble(par.get_value()-60)
canvas.queue_draw()
angleadjust = gtk.Adjustment(value=0, lower=0, upper=360, step_incr=1)
angleScale = gtk.HScale(adjustment=angleadjust)
angleScale.set_value_pos(gtk.POS_LEFT)
angleScale.connect("value-changed", changeangle)
panel.pack_start(angleScale, False, False)
window.set_position(gtk.WIN_POS_CENTER)
window.show_all()
gtk.main()
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
尝试演示!
这是一个令人着迷的问题,尽管我认为有点超出了 Stack Overflow 的范围:这不是几分钟内就能解决的问题,所以我会在这里写一个大纲,如果取得任何进展,我会更新它。任何方法都将分为三个部分:
对足迹进行评分:链接是否会中断?足迹的形状是否正确?有多平坦?运动有多流畅?它在平坦部分花费了足够的时间吗?
寻找神奇数字的良好值。目前尚不清楚这是否必须是一种进化算法(尽管我可以理解为什么这种算法的想法会吸引 Theo Jansen,因为它符合他艺术中的动物隐喻);也许其他方法,如局部搜索(爬山)或模拟退火会很有成效。
寻找良好的手臂配置。这就是进化方法似乎最有价值的地方。
您可以在我的 Javascript/canvas 演示中尝试不同的幻数,看看可以获得什么样的运动(例如,CD = 55.4 就非常有趣)。有一个完整的数学联系理论,由方式,将连杆的配置空间连接到拓扑流形。
我在演示中添加了一些简单的评分。 地面得分是脚在地面上花费的周期的分数,我将其视为 y 坐标在最低点的某个公差范围内的所有点。 阻力分数是脚踩在地面上时任意两个水平速度之间的最大差异。 (它总是负数,因此较高的值 = 速度差异较小 = 更好。)
但这就是困难所在。为了对任何类型的搜索进行编程,我需要能够组合这些分数。但我该如何平衡它们呢? Jansen 魔法数字给我的 groundScore:0.520;拖动分数:-0.285。如果我设置 AC=10,GH=65,EH=50,我得到 groundScore:0.688;拖动分数:-0.661。近70%的时间脚是踩在地面上的。但起飞过程很艰难。比詹森的好还是差?
我认为获得实际的工程反馈以确定一个好的分数将是这里的大问题,而不是实际的搜索。
Try the demo!
This is a fascinating question, though I think somewhat beyond the scope of Stack Overflow: it's not something that going to be solved in a few minutes, so I'll stick an outline here and update it if I make any progress. There are going to be three parts to any approach:
Scoring the footprint: does the linkage break? does the footprint have the right kind of shape? how flat is it? how smooth is the motion? does it spend enough time in the flat portion?
Searching for good values of the magic numbers. It's not clear that this has to be an evolutionary algorithm (though I can see why the idea of such an algorithm would appeal to Theo Jansen as it fits in with the animal metaphor in his art); perhaps other approaches like local search (hill climbing) or simulated annealing would be productive.
Searching for good configurations of the arms. This is where an evolutionary approach seems like it might be most worthwhile.
You can try out different magic numbers in my Javascript/canvas demo to see what kinds of motion you can get (CD = 55.4 is quite entertaining, for example). There's a whole mathematical theory of linkages, by the way, that connects the configuration spaces of linkages to topological manifolds.
I added some simple scoring to the demo. The ground score is the fraction of the cycle that the foot spends on the ground, which I take to be all points whose y-coordinate is within some tolerance of the lowest point. The drag score is the biggest difference between any two horizontal velocities while the foot is on the ground. (It's always negative, so that higher values = small differences in velocities = better.)
But here's where the difficulty comes in. In order to program any kind of search, I need to be able to combine these scores. But how do I balance them against each other? The Jansen magic numbers give me groundScore: 0.520; dragScore: -0.285. If I set AC=10, GH=65, EH=50, i get groundScore: 0.688; dragScore: -0.661. Nearly 70% of the time the foot is on the ground. But the take-off is draggy. Is it better or worse than Jansen's?
I think that getting actual engineering feedback in order to determine a good score is going to be the big problem here, not the actual search.