Variante di pista con esatto punto di finitura e velocità terminale zero


9

introduzione

La sfida è una variante molto interessante della pista di gioco e di queste due sfide:

La fonte di questa sfida è qui (in tedesco): c't-Racetrack

Questa sfida è particolarmente interessante (e diversa dalle due sfide sopra menzionate) perché combina un enorme spazio di ricerca con alcune condizioni esatte che devono essere soddisfatte. A causa dell'enorme spazio di ricerca, le tecniche di ricerca esaustiva sono difficili da usare, a causa delle condizioni esatte non sono facilmente utilizzabili metodi approssimativi. A causa di questa combinazione unica (più l'intuizione alla base della fisica) il problema è affascinante (e comunque tutto ciò che riguarda le auto da corsa è affascinante ;-)

Sfida

Dai un'occhiata al seguente circuito ( fonte ):

inserisci qui la descrizione dell'immagine

Devi iniziare (120,180)e terminare esattamente a (320,220)("Ziel" in tedesco) senza toccare una delle pareti.

L'auto è controllata da vettori di accelerazione del modulo (a_x,a_y), ad esempio:

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

Il primo numero è l'accelerazione per il vettore x, il secondo per il vettore y. Devono essere numeri interi perché è consentito utilizzare solo i punti interi sulla griglia. Inoltre, deve essere soddisfatta la seguente condizione:

a_x^2 + a_y^2 <= 100,

ciò significa che l'accelerazione in qualsiasi direzione deve essere inferiore o uguale a 10.

Per vedere come funziona dai un'occhiata alla seguente immagine ( fonte ):

inserisci qui la descrizione dell'immagine

Ad esempio: partendo da (120,180)te accelera 8in direzione xe in direzione -6y. Per il prossimo passo questa è la tua velocità in cui aggiungi la tua accelerazione (10,0)per ottenere (fisicamente corretto) il tuo prossimo movimento risultante (al punto (146,168). Il movimento risultante è ciò che conta quando si tratta di esaminare se hai toccato una delle pareti. Nel passaggio successivo aggiungi di nuovo il tuo prossimo vettore di accelerazione alla tua velocità attuale per ottenere il prossimo movimento e così via. Quindi ad ogni passo la tua auto ha una posizione e una velocità. (Nell'immagine illustrativa sopra le frecce blu sono per la velocità, le frecce arancioni per l'accelerazione e le frecce rosso scuro per il movimento risultante.)

Come condizione aggiuntiva devi avere una (0,0)velocità terminale quando sei sul punto di arrivo (320,220).

L'output deve essere un elenco di vettori di accelerazione nella forma sopra menzionata.

Il vincitore è colui che fornisce un programma che trova una soluzione con il minor numero di vettori di accelerazione.

Tiebreaker
Inoltre sarebbe fantastico se si può dimostrare che questa è una soluzione ottimale e se questa è l'unica soluzione ottimale o se ci sono diverse soluzioni ottimali (e quali sono).

Sarebbe anche utile se potessi fornire una descrizione generale di come funziona il tuo algoritmo e commentare il codice in modo che possiamo comprenderlo.

Ho un programma che verifica se una determinata soluzione è valida e fornirò un feedback.

Addendum
Puoi usare qualsiasi linguaggio di programmazione ma sarei particolarmente felice se qualcuno usasse R perché lo uso molto nel mio lavoro quotidiano e in qualche modo mi sono abituato :-)

Addendum II
Per la prima volta che ho iniziato una taglia - spero che questo faccia rotolare la palla (o meglio: fai guidare la macchina :-)


@Mego: Eppure ... dopo averci pensato: non sono sicuro che dovrei aggiungere il programma per almeno due motivi: in primo luogo, nella sfida originale non è stato incluso, in secondo luogo, ad esempio contiene routine che fanno parte di la sfida (ad esempio il rilevamento delle collisioni) in modo da rovinare parte del divertimento ... Dovrò dormirci sopra ...
vonjd,

1
Il programma deve effettivamente calcolare il percorso o potrei semplicemente calcolare in anticipo il percorso ottimale e quindi pubblicare qualcosa di simile print "(10,42)\n(62,64)..."?
Loovjo,

@Loovjo: No, il programma deve calcolare il percorso stesso, quindi l'intelligenza deve essere inclusa nel programma, non solo una routine di output.
vonjd,

Risposte:


4

Python, 24 passaggi (lavori in corso)

L'idea era di risolvere prima il problema continuo, riducendo notevolmente lo spazio di ricerca, quindi quantificando il risultato sulla griglia (semplicemente arrotondando al punto più vicino e cercando gli 8 quadrati circostanti)

Parametrizzo il percorso come una somma di funzioni trigonometriche (a differenza dei polinomi, non divergono e sono più facili da tenere sotto controllo). Controllo anche la velocità direttamente anziché l'accelerazione, perché è più semplice applicare la condizione al contorno semplicemente moltiplicando una funzione di ponderazione che tende a 0 alla fine.
La mia funzione oggettiva consiste in un
punteggio
esponenziale per l'accelerazione> 10 in un punteggio polinomiale per la distanza euclidea tra l'ultimo punto e il bersaglio
- punteggio costante elevato per ogni intersezione con un muro, diminuendo verso i bordi del muro

Per ridurre al minimo il punteggio, lancio tutto nell'ottimizzazione Nelder-Mead e aspetto qualche secondo. L'algoritmo riesce sempre a raggiungere la fine, fermandosi lì e non superando la massima accelerazione, ma ha problemi con i muri. Il percorso si teletrasporta attraverso gli angoli e rimane bloccato lì, o si ferma accanto a un muro con l'obiettivo appena attraversato (immagine a sinistra)
inserisci qui la descrizione dell'immagine

Durante i test, sono stato fortunato e ho trovato un percorso che è stato schiacciato in modo promettente (immagine a destra) e dopo aver modificato ulteriormente i parametri ho potuto usarlo come ipotesi iniziale per un'ottimizzazione riuscita.

Quantizzazione
Dopo aver trovato un percorso parametrico, era tempo di rimuovere i punti decimali. Osservare il quartiere 3x3 riduce lo spazio di ricerca da circa 300 ^ N a 9 ^ N, ma è ancora troppo grande e noioso da implementare. Prima di percorrere questa strada, ho provato ad aggiungere un termine "Aggancia alla griglia" alla funzione obiettivo (le parti commentate). Cento ulteriori passaggi di ottimizzazione con l'obiettivo aggiornato e il semplice arrotondamento sono stati sufficienti per ottenere la soluzione.

[(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)]

Il numero di passaggi era fisso e non faceva parte dell'ottimizzazione, ma poiché abbiamo una descrizione analitica del percorso (e poiché l'accelerazione massima è ben al di sotto di 10) possiamo riutilizzarlo come punto di partenza per un'ulteriore ottimizzazione con un numero inferiore di Timesteps

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()

Da fare: GUI che ti consente di tracciare un percorso iniziale per ottenere un senso approssimativo della direzione. Qualsiasi cosa è meglio del campionamento casuale dallo spazio 14-dimensionale


Molto bene! Sembra che 17 passi siano il minimo: come cambieresti il ​​tuo programma per trovare una soluzione con queste informazioni aggiuntive?
vonjd,

Oh mio Dio: il mio programma mostra che non finisci a (320.220) ma a (320.240) - per favore, controlla che
vonjd

1
whoops, ha aggiornato la soluzione, l'ha ricampionata fino a 24 passaggi. La messa a punto manuale è banalmente facile guardando l'immagine, automatizzandola per funzionare con un caso generale - non tanto
DenDenDo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.