Variante de piste avec point d'arrivée exact et vitesse terminale nulle

9

introduction

Le défi est une variante très intéressante du circuit de jeu et de ces deux défis:

La source de ce défi est ici (en allemand): c't-Racetrack

Ce défi est particulièrement intéressant (et différent des deux défis susmentionnés) car il combine un immense espace de recherche avec certaines conditions exactes qui doivent être remplies. En raison de l'immense espace de recherche, les techniques de recherche exhaustives sont difficiles à utiliser, en raison des conditions exactes, les méthodes approximatives ne sont pas non plus facilement utilisables. En raison de cette combinaison unique (plus l'intuition sous-jacente de la physique), le problème est fascinant (et tout ce qui concerne les voitures de course est fascinant de toute façon ;-)

Défi

Jetez un oeil à l'hippodrome suivant ( source ):

entrez la description de l'image ici

Vous devez commencer (120,180)et terminer exactement à (320,220)("Ziel" en allemand) sans toucher l'un des murs.

La voiture est contrôlée par des vecteurs d'accélération de la forme (a_x,a_y)- à titre d'exemple:

(8,-6)
(10,0)
(1,9)

Le premier nombre est l'accélération pour le vecteur x, le second pour le vecteur y. Ils doivent être des entiers car vous n'êtes autorisé à utiliser que les points entiers sur la grille. De plus, la condition suivante doit être remplie:

a_x^2 + a_y^2 <= 100,

ce qui signifie que l'accélération dans n'importe quelle direction doit être inférieure ou égale à 10.

Pour voir comment cela fonctionne, regardez l'image suivante ( source ):

entrez la description de l'image ici

Par exemple: à partir de (120,180)vous, accélérez 8dans la direction x et -6dans la direction y. Pour l'étape suivante, c'est votre vitesse à laquelle vous ajoutez votre accélération (10,0)pour obtenir (physiquement correct) votre prochain mouvement résultant (à pointer (146,168). Le mouvement résultant est ce qui compte quand il s'agit d'examiner si vous avez touché l'un des murs. À l'étape suivante vous ajoutez à nouveau votre prochain vecteur d'accélération à votre vitesse actuelle pour obtenir le prochain mouvement et ainsi de suite. Ainsi, à chaque étape, votre voiture a une position et une vitesse. (Dans l'illustration ci-dessus, les flèches bleues sont pour la vitesse, les flèches orange pour l'accélération et les flèches rouges foncées pour le mouvement résultant.)

Comme condition supplémentaire, vous devez avoir (0,0)une vitesse terminale lorsque vous êtes sur le point d'arrivée (320,220).

La sortie doit être une liste de vecteurs d'accélération sous la forme susmentionnée.

Le gagnant est celui qui fournit un programme qui trouve une solution avec le moins de vecteurs d'accélération.

Tiebreaker
De plus, ce serait bien si vous pouvez montrer qu'il s'agit d'une solution optimale et s'il s'agit de la seule solution optimale ou s'il existe plusieurs solutions optimales (et quelles sont-elles).

Il serait également bon que vous puissiez donner un aperçu général du fonctionnement de votre algorithme et commenter le code afin que nous puissions le comprendre.

J'ai un programme qui vérifie si une solution donnée est valide et je donnerai des commentaires.

Addendum
Vous pouvez utiliser n'importe quel langage de programmation mais je serais particulièrement ravi que quelqu'un utilise R parce que je l'utilise beaucoup dans mon travail de jour et que je m'y suis habitué :-)

Addendum II
Pour la première fois, j'ai commencé une prime - j'espère que cela fait rouler la balle (ou mieux: que la voiture roule :-)

vonjd
la source
@Mego: Pourtant ... après y avoir réfléchi: je ne sais pas si je devrais ajouter le programme pour au moins deux raisons: premièrement, dans le défi d'origine, il n'était pas inclus non plus, deuxièmement, il contient par exemple des routines qui font partie de le défi (par exemple la détection des collisions), donc cela gâcherait une partie du plaisir ... Je vais devoir y dormir ...
vonjd
1
Le programme doit-il réellement calculer le chemin, ou pourrais-je simplement calculer le chemin optimal à l'avance, puis publier quelque chose comme print "(10,42)\n(62,64)..."?
Loovjo
@Loovjo: Non, le programme doit calculer le chemin lui-même, donc l'intelligence doit être incluse dans le programme, pas seulement une routine de sortie.
vonjd

Réponses:

4

Python, 24 étapes (travaux en cours)

L'idée était de résoudre le problème continu en premier, en réduisant considérablement l'espace de recherche, puis en quantifiant le résultat sur la grille (en arrondissant simplement au point de grille le plus proche et en recherchant les 8 carrés environnants)

Je paramétre le chemin comme une somme de fonctions trigonométriques (contrairement aux polynômes, ils ne divergent pas et sont plus faciles à contrôler). Je contrôle également la vitesse directement au lieu de l'accélération, car il est plus facile d'appliquer la condition aux limites en multipliant simplement une fonction de pondération qui tend vers 0 à la fin.
Ma fonction objective consiste en
un score exponentiel pour l'accélération> 10
un score polynomial pour la distance euclidienne entre le dernier point et le
score cible constant élevé pour chaque intersection avec un mur, diminuant vers les bords du mur

Pour minimiser le score, je lance tout cela dans l' optimisation Nelder-Mead et j'attends quelques secondes. L'algorithme réussit toujours à arriver au bout, à s'arrêter là et à ne pas dépasser l'accélération maximale, mais il a des problèmes avec les murs. Le chemin se téléporte dans les coins et s'y bloque, ou s'arrête à côté d'un mur avec le but juste en face (image de gauche)
entrez la description de l'image ici

Pendant les tests, j'ai eu de la chance et j'ai trouvé un chemin qui a été gondolé de manière prometteuse (image de droite) et après avoir peaufiné les paramètres, je pourrais l'utiliser comme hypothèse de départ pour une optimisation réussie.

Quantification
Après avoir trouvé un chemin paramétrique, il était temps de supprimer les points décimaux. Regarder le voisinage 3x3 réduit l'espace de recherche d'environ 300 ^ N à 9 ^ N, mais c'est toujours trop grand et ennuyeux à mettre en œuvre. Avant d'emprunter cette voie, j'ai essayé d'ajouter un terme "Snap to Grid" à la fonction objectif (les parties commentées). Une centaine d'étapes d'optimisation supplémentaires avec l'objectif actualisé et un simple arrondi ont suffi pour obtenir la solution.

[(9, -1), (4, 0), (1, 1), (2, 2), (-1, 2), (-3, 4), (-3, 3), (-2 , 3), (-2, 2), (-1, 1), (0, 0), (1, -2), (2, -3), (2, -2), (3, -5 ), (2, -4), (1, -5), (-2, -3), (-2, -4), (-3, -9), (-4, -4), (- 5, 8), (-4, 8), (5, 8)]

Le nombre d'étapes était fixe et ne faisait pas partie de l'optimisation, mais comme nous avons une description analytique du chemin, (et puisque l'accélération maximale est bien inférieure à 10), nous pouvons la réutiliser comme point de départ pour une optimisation plus poussée avec un nombre plus petit de pas de temps

from numpy import *
from scipy.optimize import fmin
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection as LC

walls = array([[[0,0],[500,0]],   # [[x0,y0],[x1,y1]]
        [[500,0],[500,400]],
        [[500,400],[0,400]],
        [[0,400],[0,0]],

        [[200,200],[100,200]],
        [[100,200],[100,100]],
        [[100,100],[200,100]],

        [[250,300],[250,200]],

        [[300,300],[300,100]],
        [[300,200],[400,200]],
        [[300,100],[400,100]],

        [[100,180],[120, 200]], #debug walls
        [[100,120],[120, 100]],
        [[300,220],[320, 200]],
        #[[320,100],[300, 120]],
])

start = array([120,180])
goal = array([320,220])

###################################
# Boring stuff below, scroll down #
###################################
def weightedintersection2D(L1, L2):
    # http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
    p = L1[0]
    q = L2[0]
    r = L1[1]-L1[0]
    s = L2[1]-L2[0]
    d = cross(r,s)
    if d==0: # parallel
        if cross(q-p,r)==0: return 1 # overlap
    else:
        t = cross(q-p,s)*1.0/d
        u = cross(q-p,r)*1.0/d
        if 0<=t<=1 and 0<=u<=1: return 1-0*abs(t-.5)-1*abs(u-.5) # intersect at p+tr=q+us
    return 0

def sinsum(coeff, tt):
    '''input: list of length 2(2k+1), 
    first half for X-movement, second for Y-movement.
    Of each, the first k elements are sin-coefficients
    the next k+1 elements are cos-coefficients'''
    N = len(coeff)/2
    XS = [0]+list(coeff[:N][:N/2])
    XC =     coeff[:N][N/2:]
    YS = [0]+list(coeff[N:][:N/2])
    YC =     coeff[N:][N/2:]
    VX = sum([XS[i]*sin(tt*ww[i]) + XC[i]*cos(tt*ww[i]) for i in range(N/2+1)], 0)
    VY = sum([YS[i]*sin(tt*ww[i]) + YC[i]*cos(tt*ww[i]) for i in range(N/2+1)], 0)
    return VX*weightfunc, VY*weightfunc

def makepath(vx, vy):
    # turn coordinates into line segments, to check for intersections
    xx = cumsum(vx)+start[0]
    yy = cumsum(vy)+start[1]
    path = []
    for i in range(1,len(xx)):
        path.append([[xx[i-1], yy[i-1]],[xx[i], yy[i]]])
    return path

def checkpath(path):
    intersections = 0
    for line1 in path[:-1]: # last two elements are equal, and thus wrongly intersect each wall
        for line2 in walls:
            intersections += weightedintersection2D(array(line1), array(line2))
    return intersections

def eval_score(coeff):
    # tweak everything for better convergence
    vx, vy = sinsum(coeff, tt)
    path = makepath(vx, vy)
    score_int = checkpath(path)
    dist = hypot(*(path[-1][1]-goal))
    score_pos = abs(dist)**3
    acc = hypot(diff(vx), diff(vy))
    score_acc = sum(exp(clip(3*(acc-10), -10,20)))
    #score_snap = sum(abs(diff(vx)-diff(vx).round())) + sum(abs(diff(vy)-diff(vy).round()))
    print score_int, score_pos, score_acc#, score_snap
    return score_int*100 + score_pos*.5 + score_acc #+ score_snap

######################################
# Boring stuff above, scroll to here #
######################################
Nw = 4 # <3: paths not squiggly enough, >6: too many dimensions, slow
ww = [1*pi*k for k in range(Nw)]
Nt = 30 # find a solution with tis many steps
tt = linspace(0,1,Nt)
weightfunc = tanh(tt*30)*tanh(30*(1-tt)) # makes sure end velocity is 0

guess = random.random(4*Nw-2)*10-5
guess = array([ 5.72255365, -0.02720178,  8.09631272,  1.88852287, -2.28175362,
        2.915817  ,  8.29529905,  8.46535503,  5.32069444, -1.7422171 ,
       -3.87486437,  1.35836498, -1.28681144,  2.20784655])  # this is a good start...
array([ 10.50877078,  -0.1177561 ,   4.63897574,  -0.79066986,
         3.08680958,  -0.66848585,   4.34140494,   6.80129358,
         5.13853914,  -7.02747384,  -1.80208349,   1.91870184,
        -4.21784737,   0.17727804]) # ...and it returns this solution      

optimsettings = dict(
    xtol = 1e-6,
    ftol = 1e-6,
    disp = 1,
    maxiter = 1000, # better restart if not even close after 300
    full_output = 1,
    retall = 1)

plt.ion()
plt.axes().add_collection(LC(walls))
plt.xlim(-10,510)
plt.ylim(-10,410)
path = makepath(*sinsum(guess, tt))
plt.axes().add_collection(LC(path, color='red'))
plt.plot(*start, marker='o')
plt.plot(*goal, marker='o')
plt.show()

optres = fmin(eval_score, guess, **optimsettings)
optcoeff = optres[0]    

#for c in optres[-1][::optimsettings['maxiter']/10]:
for c in array(optres[-1])[logspace(1,log10(optimsettings['maxiter']-1), 10).astype(int)]:
    vx, vy = sinsum(c, tt)
    path = makepath(vx,vy)
    plt.axes().add_collection(LC(path, color='green'))
    plt.show()

À faire: GUI qui vous permet de dessiner un chemin initial pour obtenir un sens approximatif de la direction. Tout est meilleur que l'échantillonnage aléatoire à partir d'un espace à 14 dimensions

DenDenDo
la source
Bien joué! Il semble que 17 étapes soit le minimum - comment changeriez-vous votre programme pour trouver une solution avec ces informations supplémentaires?
vonjd
Oh mon cher: Mon programme montre que vous ne vous terminez pas à (320,220) mais à (320,240) - pourriez-vous vérifier cela
vonjd
1
whoops, a mis à jour la solution, l'a également rééchantillonnée en 24 étapes. Un réglage fin à la main est très facile en regardant l'image, en l'automatisant pour qu'elle fonctionne avec un cas général - pas tellement
DenDenDo