Sunday, April 26, 2015

VSOP 2013 - Eliminare i task ripetitivi

L'economia nell'uso delle risorse è importante in qualsiasi ambiente di sviluppo, ma diventa fondamentale quando si usa Python. La sua lentezza operativa puo', nella maggior parte dei casi, essere attributa alla scorretta scelta di tools, alla scrittura di codice non preottimizzato, magari perchè si è di vecchia scuola come me, abituato ad usare il Visual Basic o il C++ o Java, linguaggi compilati e naturalmente più performanti. Python ha pero' il vantaggio di essere un linguaggio di altissimo livello, consentendo di scrivere programmi non solo più puliti e in modo più veloce, ma anche di costruire in pochissime righe strutture che, in linguaggi più classici, richiederebbero molto più codice.

Se torniamo alle prove del post precedente, vediamo che ho associato la ricerca di migliori performance alla scelta di librerie e tools differenti. Ho sostituito mpmath con gmpy2, passando da una libreria in pure Python ad una in C compilato. Ora pero' devo iniziare a intervenire sul codice, per gestire meglio l'accesso ai file e le routine di calcolo.

Se guardate i file di parametri che sono presenti nel sito ftp dell'IMCCE potete verificare che si tratta di nove file, denominati VSOP2013pX.dat, dove la X sta per il numero corrispondente al pianeta, 1 per Mercurio, 2 per Venere, 3 per il baricentro Terra-Luna eccetera. Ogni file è strutturato in questo modo:

  • un header di una riga che contiene alcuni numeri interi che rappresentano il pianeta, la variabile (di 6) che corrisponde ad un parametro orbitale, la potenza di a cui va elevato il tempo in millenni dalla data JD2000 di riferimento, il numero di righe successive da leggere e una serie di informazioni testuali che corrispondono agli integer iniziali.
  • una serie di righe testuali che contengono i termini da utilizzare per il calcolo in relazione al pianeta scelto, alla variabile e alla potenza da applicare al tempo, contenuti, come si è detto, nell'header.

Ho detto in un post precedente che per evitare di passare le ore ad aspettare la fine del calcolo, si puo' predefinire il livello di precisione, riducendo il numero di termini da utilizzare. Ogni riga consente di stimare tale livello utilizzando gli ultimi quattro elementi della riga, che sono i parametri S e C e relativi esponenti in base 10 per la notazione scientifica. Da notare che questa verifica, indicata dagli autori dell'IMCCE, non è dipendente dal tempo, per cui è possibile effettuarla una volta per tutte, visto che fare in tempo reale calcolo e verifica di precisione è sicuramente dispendioso. In questo post cerco di scrivere un piccolo programma che fa il calcolo una volta per tutte e salva in un file esterno i risultati.

Iniziamo con l'intestazione e due parole di spiegazione.

import gmpy2 as gmp
gmp.get_context().precision=200
from itertools import islice
import cPickle

In queste prime righe importo alcuni moduli fondamentali:

  1. gmpy2, libreria di alta precisione molto veloce,l'abbiamo già discussa nel post precedente. La precisione è fissata arbitrariamente in 200 bits, contro i 53 standard di Python
  2. itertools è un modulo della libreria standard. Contiene funzioni per gestire gli iterators, cioè oggetti che rappresentano uno stream di dati. Sono utili per scorrere sequenzialmente un dato che puo' essere una lista, una tupla, un dictionary o anche, come vediamo in seguito, un file su disco. Delle funzioni disponibili mi interessa, in questo contesto, solo la funzione islice, che permette di generare un sottoinsieme da un iterator.
  3. Infine cPickle è un modulo che consente di serializzare (cioè trasformare in stream di byte) un oggetto di Python e anche deserializzarlo, cioè fare l'operazione inversa. Il valore di questo modulo è inestimabile, perchè se l'oggetto ha una struttura complessa e non risolvibile immediatamente in una sequenza, Python lo fa per noi, consentendo di salvare su file qualcosa che possiamo rileggere in altro contesto e ritrovare identico a come è stato costruito.

La prima cosa che voglio fare è trovare i puntatori agli header nei file di parametri. Leggiamo insieme il codice seguente, parte iniziale di una funzione (def):

def find_limits():
    # create an empty dictionary
    limits={}
    for planet in xrange(1,10):
        # read date files, one per planet
        file_in = open('VSOP2013p'+str(planet)+'.dat')
        # create a list with the header position and values for variables planet, var, power
        elenco = [(s[0], s[1].split()) for s in enumerate(file_in) if "VSOP" in s[1]]
        file_in.close()

Uso un dizionario strutturato (limits) per conservare i riferimenti ai blocchi di ogni file che sono riferiti al pianeta, al parametro orbitale (variabile), alla potenza del tempo e alla soglia di precisione desiderata.

Inizio con un dizionario vuoto {}, che estendero' progressivamente di livello. Preliminarmente, pero', devo individuare gli headers nei file originari dell'IMCCE.

Le righe successive mostrano come aprire i file di parametri e come ricavarne gli header. La variabile elenco è pensata per elencare le sole righe del file che contengono la sequenza di testo 'VSOP2013'. Per fare questo uso una particolare struttura di Python chiamata list comprehension che rientra nella programmazione di tipo funzionale. In pratica la variabile file_in, riferita al file aperto all'inizio, viene scandita con la funzione enumerate, che restituisce insieme la posizione in lista e l'elemento della lista (in questo caso le righe del file) e, usando come filtro il termine 'VSOP2013' si accumulano nella lista elenco tutte le righe che soddisfano la condizione. Rispetto al ciclo for usuale, strutturato in più righe, la lista comprehension risulta più compatta come spazio e molto più performante in termini di velocità esecutiva.

        # extend the dictionary with a subdictionary for each planet 
        limits[planet]={}
        for var in xrange(1,7):
            # extend the dictionary with a subdictionary for each variable
            limits[planet][var] = {}
            for power in xrange(0,21):
                # extend the dictionary with a subdictionary for each power
                limits[planet][var][power] = {}
                for threshold in xrange(0,21):
                    # extend the dictionary with a two values tuple (min and max)
                    # for each sensibility threshold
                    # and for each item in list assign (0,0) to the tuple
                    limits[planet][var][power][threshold]=(0,0)

Nelle righe successive costruiamo il nostro dizionario, estendonolo progressivamente di livello. Usando un ciclo for nidificato creo tanti sottodizionari quante sono le modalità per ogni pianeta, ogni variabile, ogni potenza e ogni soglia di precisione. Al termine inizializzo a (0,0) il dettaglio più fine del dizionario.

A questo punto dispongo di due strumenti fondamentali, il dizionario contenitore e l'elenco degli header (per ogni file di parametri il dizionario non viene rigenerato, bensì ampliato. Andiamo oltre con il codice.

        # create a tuple with range limits for each planet, variable, power
        for i in xrange(len(elenco)):
            start = int(elenco[i][0])+1
            planet = int(elenco[i][1][1])
            var = int(elenco[i][1][2])
            power = int(elenco[i][1][3])
            end = int(elenco[i][1][4])+start
            # load a file chunk for each voice in list and find extreme value for each threshold
            file_in = open('VSOP2013p'+str(planet)+'.dat')
            segmento = islice(file_in, start, end)
            for j, k in enumerate(segmento):
                cursor = j
                line = k.split()
                Sb = float(line[-4])
                Se = int(line[-3])
                S = Sb * 10 ** Se
                Cb =float(line[-2])
                Ce = int(line[-1])
                C = Cb * 10 ** Ce
                ro = gmp.sqrt(S*S+C*C)
                threshold = -int(gmp.floor(gmp.log10(ro)))
                limits[planet][var][power][threshold]=(start, start+cursor)
            file_in.close()

Inizio una scansione sequenziale della lista in <elenco>, da cui, riga per riga, ottengo il punto di inizio e fine nonchè le variabili e i parametri di riferimento nel file. Una volta ottenuti i riferimenti di inizio e fine, li uso con la funzione islice di itertools per ottenere un iteratore che è un sottoinsieme del file. Da ogni riga ricavo i parametri S e C e ottengo il livello di precisione corrispondente (dato dalla radice quadrata della somma dei quadrati di S e C). Trasformo il valore ottenuto nel logaritmo in base 10, cambio il segno e trasformo il risultato in integer. Questo è il livello di precisione che mi serve per indicizzare il mio dizionario. In corrispondenza della chiave pianeta, variabile, potenza e soglia (indicata dall'integer ottenuto dalla procedura precedente) indico i nuovi estremi di posizione nel file. A questo punto il lavoro è quasi concluso.

Ultime righe di codice:

    for planet in xrange(1,10):
        for var in xrange(1,7):
            for power in xrange(0,21):
                for threshold in xrange(1,21):
                    prec_ = limits[planet][var][power][threshold-1]
                    subs_ = limits[planet][var][power][threshold]
                    if subs_ == (0,0) and prec_ !=0:
                        limits[planet][var][power][threshold] = limits[planet][var][power][threshold-1]
    return limits

Un'ultima procedura prima di restituire il dizionario al termine della funzione consiste nel riempire i buchi: se non è stato assegnato un valore alla tupla nello specifico dettaglio, la tupla rimane (0,0), il che mi permetterà, nell'uso successivo, di considerare inutile il dettaglio per la precisione richiesta. Per le soglie di precisione superiori non toccate dalla procedura di riempimento, ho scelto di assumere il valore di precisione inferiore se diverso da (0,0).

Vediamo ora la parte di salvataggio e caricamento del dizionario.

if __name__ == '__main__':
    limits = find_limits()
    cPickle.dump(limits, open('VSOP2013_ranges.pickle','wb'))
    # reload limits from disk
    load_limits = cPickle.load(open('VSOP2013_ranges.pickle','rb'))

La funzione dump di cPickle permette di serializzare il nostro dizionario e salvarlo su disco. La funzione load effettua il caricamento e la deserializzazione. In questo modo, dopo circa un minuto di lavoro (nella mia macchina, almeno), avremo un file illeggibile in modo diretto che potremo rileggere in programmi successivi per ricreare lo stesso dizionario, con tempi di elaborazione molto inferiori.

Salvo in github il programma sotto il nome di VSOP2013_ranges.py, nonchè il file serializzato che contiene il dizionario.Nel prossimo post vedremo come utilizzare in pratica il dizionario e misureremo le performance del nuovo codice. Al prossimo post.

Friday, April 17, 2015

VSOP2013 - mpmath, gmpy2 e aumento della velocità di esecuzione

Per questo post e i successivi ho pensato di trattare un po' il tema della velocità di esecuzione del codice. Finora non ne ho sentito ll bisogno, in quanto, con routine statiche e limitata dipendenza da file di parametri su disco, anche nel caso delle VSOP87, i tempi di risposta rimanevano adeguati. Con le VSOP2013 i tempi sono significativamente più lunghi, avendo file di parametri con centinaia di migliaia di righe e la necessità di una lettura sequenziale degli stessi con verifica del punto di arresto ad una precisione prefissata. Il precedente post delineava un approccio puramente imitativo del file FORTRAN allegato dal gruppo dell'Observatoire de Paris ai file di parametri presenti sul sito ftp dell'IMCCE. Come scrivevo nello stesso post non pretendo di imitare con il linguaggio Python (interpretato, a tipizzazione dinamica) la stessa velocità esecutiva di FORTRAN (compilato e a tipizzazione statica), ma vorrei tentare comunque di ridurre il tempo di esecuzione, per consentire una fluidità operativa che attualmente non è disponibile.

Per prima cosa vorrei quindi presentarvi i tempi di esecuzione dell'eseguibile ottenuto dalla compilazione in FORTRAN e quindi quelli che raggiungo attualmente con Python. Ho fatto il timing su una macchina equipaggiata con processore AMD A8-Series APU A8-6600K quadcore con 8 giga di RAM :

VSOP2013.f compilato: time ./VSOP2013 real 0m18.422s user 0m18.260s sys 0m0.072s

Il file produce un output su disco (VSOP.out)

python e mpmath

timing del file mp_struct.py, traduzione in linguaggio Python con l'uso della libreria di alta precisione mpmath, eseguito con l'interprete Python2.7.9. La lettura sequenziale dei file prevede una precisione d'arresto calcolata dai parametri S e C mediante la formula

mp_struct.py interpretato con Python 2.7.9 (precisione 1e-12): real 118m56.059s user 118m40.096s sys 0m6.192s

Si tratta chiaramente di una tempistica inaccettabile per calcolare le posizioni di 9 pianeti in 11 date. Accettando una precisione inferiore (1e-7) le cose migliorano un po':

mp_struct.py interpretato con Python 2.7.9 (precisione 1e-7): real 2m2.971s user 2m2.004s sys 0m0.796s

Siamo ancora lontani da un risultato soddisfacente. Ho provato a utilizzare pypy, che è un compilatore jit (just in time) ad alta efficienza. Non permette di includere la libreria mpmath dai moduli aggiunti di Python, ma si puo' fare un'installazione dedicata partendo dai sorgenti, decomprimendoli e lanciando all'interno della directory così ottenuta il comando:

sudo pypy setup.py install

La libreria viene ricreata nell'ambiente di pypy, dove, almeno relativamente al mio codice, funziona perfettamente.

mp_struct.py interpretato con PyPy 2.5.0 (precisione 1e-7): real 0m43.148s user 0m41.128s sys 0m1.024s

Il miglioramento è evidente. L'ottimizzazione creata dal compilatore pypy è decisamente migliorativa portando il tempo esecutivo al 35% del timing iniziale. Rimane ancora un'opzione, prima di passare alla reingegnerizzazione del codice: cambiare libreria di alta precisione. Senza ristrutturare pesantemente il codice, cambiando solo i riferimenti alle classi, ho importato la libreria gmpy2, che è un wrapper python di librerie scritte in C, pertanto molto veloci nell'esecuzione:
gmpy2 is a C-coded Python extension module that supports multiple-precision arithmetic. gmpy2 is the successor to the original gmpy module. The gmpy module only supported the GMP multiple-precision library. gmpy2 adds support for the MPFR (correctly rounded real floating-point arithmetic) and MPC (correctly rounded complex floating-point arithmetic) libraries. gmpy2 also updates the API and naming conventions to be more consistent and support the additional functionality.

Per l'installazione in python si puo' utilizzare pip, previa installazione di alcune dipendenze. Rimando alla pagina dedicata nella documentazione di gmpy2: installazione di gmpy2 su linux

Allego il nuovo codice in forma integrale. Trovate questo, come il precedente scritto con mpmath, nel mio repository git sotto astrometry/vsop2013

import gmpy2 as gmp

import struct

gmp.get_context().precision=200

def cal2jul(year, month, day, hour=0, minute =0, second=0):
    month2 = month
    year2 = year
    if month2 <= 2:
        year2 -= 1
        month2 += 12
    if (year*10000 + month*100 + day) > 15821015:
        a = int(year2/100)
        b = 2 - a + int(a/4)
    else:
        a = 0
        b = 0
    if year < 0:
        c = int((365.25 * year2)-0.75)
    else:
        c = int(365.25 * year2)
    d = int(30.6001 *(month2 + 1))
    return b + c + d + day + hour / 24.0 + minute / 1440.0 + second / 86400.0 + 1720994.5        




class VSOP2013():
    
    def __init__(self, t, planet, precision=1e-7):
        # calculate millennia from J2000
        self.JD = t
        self.t = gmp.div((t - cal2jul(2000,1,1,12)), 365250.0)
        # predefine powers of self.t
        self.power = []; self.power.append(gmp.mpfr(1.0)); self.power.append(self.t)
        for i in xrange(2,21):
            t = self.power[-1]
            self.power.append(gmp.mul(self.t,t))
        # choose planet file in a dict
        self.planet = planet
        self.planets = {'Mercury':'VSOP2013p1.dat',
                        'Venus'  :'VSOP2013p2.dat',
                        'EMB'    :'VSOP2013p3.dat',
                        'Mars'   :'VSOP2013p4.dat',
                        'Jupiter':'VSOP2013p5.dat',
                        'Saturn' :'VSOP2013p6.dat',
                        'Uranus' :'VSOP2013p7.dat',
                        'Neptune':'VSOP2013p8.dat',
                        'Pluto'  :'VSOP2013p9.dat'}
        # VSOP2013 routines precision
        self.precision = precision
        
        # lambda coefficients
        # l(1,13) : linear part of the mean longitudes of the planets (radian).
        # l(14): argument derived from TOP2013 and used for Pluto (radian).
        # l(15,17) : linear part of Delaunay lunar arguments D, F, l (radian).

        self.l = (
            (gmp.mpfr(4.402608631669), gmp.mpfr(26087.90314068555)),
            (gmp.mpfr(3.176134461576), gmp.mpfr(10213.28554743445)),
            (gmp.mpfr(1.753470369433), gmp.mpfr( 6283.075850353215)),
            (gmp.mpfr(6.203500014141), gmp.mpfr( 3340.612434145457)),
            (gmp.mpfr(4.091360003050), gmp.mpfr( 1731.170452721855)),
            (gmp.mpfr(1.713740719173), gmp.mpfr( 1704.450855027201)), 
            (gmp.mpfr(5.598641292287), gmp.mpfr( 1428.948917844273)),
            (gmp.mpfr(2.805136360408), gmp.mpfr( 1364.756513629990)),
            (gmp.mpfr(2.326989734620), gmp.mpfr( 1361.923207632842)),
            (gmp.mpfr(0.599546107035), gmp.mpfr(  529.6909615623250)),
            (gmp.mpfr(0.874018510107), gmp.mpfr(  213.2990861084880)),
            (gmp.mpfr(5.481225395663), gmp.mpfr(   74.78165903077800)),
            (gmp.mpfr(5.311897933164), gmp.mpfr(   38.13297222612500)),
            (gmp.mpfr(0.000000000000), gmp.mpfr(    0.3595362285049309)),
            (gmp.mpfr(5.198466400630), gmp.mpfr(77713.7714481804)),
            (gmp.mpfr(1.627905136020), gmp.mpfr(84334.6615717837)),
            (gmp.mpfr(2.355555638750), gmp.mpfr(83286.9142477147))
            )

        # planetary frequencies in longitude
        self.freqpla = {'Mercury'   :   gmp.mpfr(0.2608790314068555e5),  
                        'Venus'     :   gmp.mpfr(0.1021328554743445e5),
                        'EMB'       :   gmp.mpfr(0.6283075850353215e4),
                        'Mars'      :   gmp.mpfr(0.3340612434145457e4), 
                        'Jupiter'   :   gmp.mpfr(0.5296909615623250e3),  
                        'Saturn'    :   gmp.mpfr(0.2132990861084880e3),
                        'Uranus'    :   gmp.mpfr(0.7478165903077800e2),  
                        'Neptune'   :   gmp.mpfr(0.3813297222612500e2), 
                        'Pluto'     :   gmp.mpfr(0.2533566020437000e2)}  

        
            
        # target variables
        self.ax = gmp.mpfr(0.0)            # major semiaxis
        self.ml = gmp.mpfr(0.0)            # mean longitude
        self.kp = gmp.mpfr(0.0)            # e*cos(perielium longitude)
        self.hp = gmp.mpfr(0.0)            # e*sin(perielium longitude)
        self.qa = gmp.mpfr(0.0)            # sin(inclination/2)*cos(ascending node longitude)
        self.pa = gmp.mpfr(0.0)            # sin(inclination/2)*cos(ascending node longitude)
        
        self.tg_var = {'A':self.ax, 'L':self.ml, 'K':self.kp,
                       'H':self.hp, 'Q':self.qa, 'P':self.pa } 

        # eps  = (23.d0+26.d0/60.d0+21.41136d0/3600.d0)*dgrad
        self.eps = gmp.mpfr((23.0+26.0/60.0+21.411360/3600.0)*gmp.const_pi()/180.0)
        self.phi  = gmp.mpfr(-0.051880 * gmp.const_pi() / 180.0 / 3600.0)
        self.ceps = gmp.cos(self.eps)
        self.seps = gmp.sin(self.eps)
        self.cphi = gmp.cos(self.phi)
        self.sphi = gmp.sin(self.phi)
        
        # rotation of ecliptic -> equatorial rect coords
        self.rot = [[self.cphi, -self.sphi*self.ceps,  self.sphi*self.seps],
                    [self.sphi,  self.cphi*self.ceps, -self.cphi*self.seps],
                    [0.0,        self.seps,            self.ceps          ]]
        
        self.fmt = struct.Struct("""6s 3s 3s 3s 3s x 3s 3s 3s 3s 3s x 4s 4s 4s 4s x
                       6s x 3s 3s 3s 20s x 3s 20s x 3s x""")

        self.gmp_ = {
       'Mercury' : gmp.mpfr(4.9125474514508118699e-11),
       'Venus'   : gmp.mpfr(7.2434524861627027000e-10),
       'EMB'     : gmp.mpfr(8.9970116036316091182e-10),
       'Mars'    : gmp.mpfr(9.5495351057792580598e-11),
       'Jupiter' : gmp.mpfr(2.8253458420837780000e-07),
       'Saturn'  : gmp.mpfr(8.4597151856806587398e-08),
       'Uranus'  : gmp.mpfr(1.2920249167819693900e-08),
       'Neptune' : gmp.mpfr(1.5243589007842762800e-08),
       'Pluto'   : gmp.mpfr(2.1886997654259696800e-12)
       }

        self.gmsol = gmp.mpfr(2.9591220836841438269e-04)
        self.rgm = gmp.sqrt(self.gmp_[self.planet]+self.gmsol)
        # run calculus routine
        self.calc()
        

    def __str__(self):
        
        vsop_out = "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.tg_var['A'],
                                          self.tg_var['L'],
                                          self.tg_var['K'],
                                          self.tg_var['H'],
                                          self.tg_var['Q'],
                                          self.tg_var['P'])
        vsop_out += "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.ecl[0],
                                          self.ecl[1],
                                          self.ecl[2],
                                          self.ecl[3],
                                          self.ecl[4],
                                          self.ecl[5])
        vsop_out += "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.equat[0],
                                          self.equat[1],
                                          self.equat[2],
                                          self.equat[3],
                                          self.equat[4],
                                          self.equat[5])
                        

        return vsop_out



    def calc(self):
        with open(self.planets[self.planet]) as file_in:
            terms = []
            b = '*'
            while b != '':
                b = file_in.readline()
                if b != '':
                    if b[:5] == ' VSOP':
                        header = b.split()
                        #print header[3], header[7], header[8], self.t**int(header[3])
                        no_terms = int(header[4])
                        for i in xrange(no_terms):
                            #6x,4i3,1x,5i3,1x,4i4,1x,i6,1x,3i3,2a24
                            terms = file_in.readline()
                            a = self.fmt.unpack(terms)
                            S = gmp.mul(gmp.mpfr(a[18]),gmp.exp10(int(a[19])))
                            C = gmp.mul(gmp.mpfr(a[20]),gmp.exp10(int(a[21])))
                            if gmp.sqrt(S*S+C*C) < self.precision:
                                break
                            aa = 0.0; bb = 0.0;
                            for j in xrange(1,18):
                                aa += gmp.mul(gmp.mpfr(a[j]), self.l[j-1][0])
                                bb += gmp.mul(gmp.mpfr(a[j]), self.l[j-1][1])
                            arg = aa + bb * self.t
                            power = int(header[3])
                            comp = self.power[power] * (S * gmp.sin(arg) + C * gmp.cos(arg))
                            if header[7] == 'L' and power == 1 and int(a[0]) == 1:
                                pass
                            else:
                                self.tg_var[header[7]] += comp
        self.tg_var['L'] = self.tg_var['L'] + self.t * self.freqpla[self.planet]
        self.tg_var['L'] = self.tg_var['L'] % (2 * gmp.const_pi())
        if self.tg_var['L'] < 0:
            self.tg_var['L'] += 2*gmp.const_pi()
        print "Julian date {}".format(self.JD)
        file_in.close()
        ##print self.tg_var
    
        ####    def ELLXYZ(self):

        xa = self.tg_var['A']
        xl = self.tg_var['L']
        xk = self.tg_var['K']
        xh = self.tg_var['H']
        xq = self.tg_var['Q']
        xp = self.tg_var['P']
        
        # Computation
  
        xfi = gmp.sqrt(1.0 -xk * xk - xh * xh)
        xki = gmp.sqrt(1.0 -xq * xq - xp * xp)
        u = 1.0/(1.0 + xfi)
        z = complex(xk, xh)
        ex = abs(z)
        ex2 = ex  * ex
        ex3 = ex2 * ex
        z1 = z.conjugate()
        #
        gl = xl % (2*gmp.const_pi())
        gm = gl - gmp.atan2(xh, xk)
        e  = gl + (ex - 0.1250 * ex3) * gmp.sin(gm)
        e += 0.50 * ex2 * gmp.sin(2.0 * gm)
        e += 0.3750 * ex3 * gmp.sin(3.0 * gm)
        #
        while True:
            z2 = complex(0.0, e)
            zteta = gmp.exp(z2)
            z3 = z1 * zteta
            dl = gl - e + z3.imag
            rsa = 1.0 - z3.real
            e = e + dl / rsa
            if abs(dl) < 1e-15:
                break
        #
        z1  =  u * z * z3.imag
        z2  =  gmp.mpc(z1.imag, -z1.real)
        zto = (-z+zteta+z2)/rsa
        xcw = zto.real
        xsw = zto.imag
        xm  = xp * xcw - xq * xsw
        xr  = xa * rsa
        #
        self.ecl = []; self.equ = {}
        self.ecl.append(xr * (xcw -2.0 *xp * xm))
        self.ecl.append(xr * (xsw +2.0 *xq * xm))
        self.ecl.append(-2.0 * xr * xki * xm)
        #
        xms = xa *(xh + xsw) / xfi
        xmc = xa *(xk + xcw) / xfi
        xn  = self.rgm / xa ** (1.50)
        #
        self.ecl.append( xn *((2.0 * xp * xp - 1.0) * xms + 2.0 * xp * xq * xmc))
        self.ecl.append( xn *((1.0 -2.0 * xq * xq) * xmc -2.0 * xp * xq * xms))
        self.ecl.append( 2.0 * xn * xki * (xp * xms + xq * xmc))
        
        # Equatorial rectangular coordinates and velocity

        #         
        #
        # --- Computation ------------------------------------------------------
        #
        self.equat = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]    
        for i in xrange(3):
           for j in xrange(3):
               self.equat[i]   = self.equat[i]   + self.rot[i][j] * self.ecl[j]
               self.equat[i+3] = self.equat[i+3] + self.rot[i][j] * self.ecl[j+3]



if __name__ == '__main__':
    for planet in ('Mercury', 'Venus', 'EMB', 'Mars', 'Jupiter',
          'Saturn', 'Uranus', 'Neptune', 'Pluto'):
        print "PLANETARY EPHEMERIS VSOP2013  "+ planet + "(TDB)\n"+""" 
  1/ Elliptic   Elements:    a (au), lambda (radian), k, h, q, p        - Dynamical Frame J2000
  2/ Ecliptic   Heliocentric Coordinates:  X,Y,Z (au)  X',Y',Z' (au/d)  - Dynamical Frame J2000
  3/ Equatorial Heliocentric Coordinates:  X,Y,Z (au)  X',Y',Z' (au/d)  - ICRS Frame J2000
"""
        init_date = cal2jul(1890,6,26,12)
        set_date = init_date

        while set_date < init_date + 41000:
            v = VSOP2013(set_date, planet)
            print v
            set_date += 4000
          
        

gmp_struct.py interpretato con Python 2.7.9 (precisione 1e-7): real 0m27.228s user 0m26.184s sys 0m1.000s

Il risultato è incoraggiante: 67% rispetto alla performance di pypy sul file con mpmath. Rinuncio a tentare di compilare gmpy2 sotto pypy, non mi aspetto in questo momento grandi miglioramenti, visto che la gmpy2 è scritta in C. Penso che sia venuto il momento di provare a fare un po' di profiling ed eventualmente riscrivere un po' il codice. Alla prossima.

Thursday, April 9, 2015

VSOP2013 e nuove routine di precisione

Ho scoperto che il gruppo francese dell'Observatoire de Paris ha continuato a elaborare le teorie semianalitiche per il calcolo della posizione dei pianeti, fino ad una edizione del 2013, di elevatissima precisione, di cui gli autori parlano in questo articolo.

Le VSOP87, di cui ho presentato una piccola libreria personale in linguaggio Python, sono ancora molto diffuse. Il lavoro degli studiosi francesi ha pero' prodotto delle novità nel corso degli anni, producendo una versione 2010 e quella di cui parlo oggi, aggiornata al 2013.

Tra le novità più salienti, l'inclusione nella teoria del pianeta Plutone, il calcolo del baricentro Terra_Luna, e una teoria apposita per i pianeti esterni.

Inoltre la precisione di calcolo si estende nel tempo ad un intervallo che va dal 4000 a.C. all'8000 d.C. con mantenimento di elevata accuratezza.

Tutto il materiale necessario per il calcolo è fornito nel sito ftp: ftp://ftp.imcce.fre.

Per gli esempi contenuti in questo post vi consiglio di scaricare l'intera directory ftp://ftp.imcce.fr/pub/ephem/planets/vsop2013/solution/ che contiene anche le tabelle dati per il calcolo, molto voluminose per l'inclusione di decine di migliaia di termini correttivi che tengono conto di tutte le perturbazioni al moto prodotte dai corpi celesti del sistema solare.

Il programma che presento di seguito è, di fatto, una traduzione in python del programma in linguaggio FORTRAN allegato dagli autori, che serve a produrre una tabella di risultati per 11 date a partire dal 26 giugno 1890 ore 12 e intervallate fra loro di 4000 giorni Per garantire la precisione del risultato ho incluso la libreria mpmath che consente l'esecuzione di calcoli in elevata precisione. Se confrontate la tabella prodotta a video dal mio programma con il file VSOP2013.ctl, constaterete un'elevata corrispondenza, che arriva fino alla decima cifra decimale se si imposta una precisione pari ad almeno 1e-12. Con il crescere della precisione crescono esponenzialmente i tempi di calcolo, per l'inclusione di un sempre maggiore numero di termini. Operativamente, per le nostre necessità, una precisione di 1e-7 puo' essere ritenuta sufficiente ed è impostata di default nella classe che vi presento di seguito.

import mpmath as mpm
from mpmath import mp

import struct

mp.dps = 50
mp.pretty = True

def cal2jul(year, month, day, hour=0, minute =0, second=0):
    month2 = month
    year2 = year
    if month2 <= 2:
        year2 -= 1
        month2 += 12
    if (year*10000 + month*100 + day) > 15821015:
        a = int(year2/100)
        b = 2 - a + int(a/4)
    else:
        a = 0
        b = 0
    if year < 0:
        c = int((365.25 * year2)-0.75)
    else:
        c = int(365.25 * year2)
    d = int(30.6001 *(month2 + 1))
    return b + c + d + day + hour / 24.0 + minute / 1440.0 + second / 86400.0 + 1720994.5        




class VSOP2013():
    
    def __init__(self, t, planet, precision=1e-7):
        # calculate millennia from J2000
        self.JD = t
        self.t = mpm.fdiv((t - cal2jul(2000,1,1,12)), 365250.0)
        # predefine powers of self.t
        self.power = []; self.power.append(mpm.mpf('1.0')); self.power.append(self.t)
        for i in xrange(2,21):
            t = self.power[-1]
            self.power.append(mpm.fmul(self.t,t))
        # choose planet file in a dict
        self.planet = planet
        self.planets = {'Mercury':'VSOP2013p1.dat',
                        'Venus'  :'VSOP2013p2.dat',
                        'EMB'    :'VSOP2013p3.dat',
                        'Mars'   :'VSOP2013p4.dat',
                        'Jupiter':'VSOP2013p5.dat',
                        'Saturn' :'VSOP2013p6.dat',
                        'Uranus' :'VSOP2013p7.dat',
                        'Neptune':'VSOP2013p8.dat',
                        'Pluto'  :'VSOP2013p9.dat'}
        # VSOP2013 routines precision
        self.precision = precision
        
        # lambda coefficients
        # l(1,13) : linear part of the mean longitudes of the planets (radian).
        # l(14): argument derived from TOP2013 and used for Pluto (radian).
        # l(15,17) : linear part of Delaunay lunar arguments D, F, l (radian).

        self.l = (
            (mpm.mpf('4.402608631669'), mpm.mpf('26087.90314068555')),
            (mpm.mpf('3.176134461576'), mpm.mpf('10213.28554743445')),
            (mpm.mpf('1.753470369433'), mpm.mpf(' 6283.075850353215')),
            (mpm.mpf('6.203500014141'), mpm.mpf(' 3340.612434145457')),
            (mpm.mpf('4.091360003050'), mpm.mpf(' 1731.170452721855')),
            (mpm.mpf('1.713740719173'), mpm.mpf(' 1704.450855027201')), 
            (mpm.mpf('5.598641292287'), mpm.mpf(' 1428.948917844273')),
            (mpm.mpf('2.805136360408'), mpm.mpf(' 1364.756513629990')),
            (mpm.mpf('2.326989734620'), mpm.mpf(' 1361.923207632842')),
            (mpm.mpf('0.599546107035'), mpm.mpf('  529.6909615623250')),
            (mpm.mpf('0.874018510107'), mpm.mpf('  213.2990861084880')),
            (mpm.mpf('5.481225395663'), mpm.mpf('   74.78165903077800')),
            (mpm.mpf('5.311897933164'), mpm.mpf('   38.13297222612500')),
            (mpm.mpf('0.000000000000'), mpm.mpf('    0.3595362285049309')),
            (mpm.mpf('5.198466400630'), mpm.mpf('77713.7714481804')),
            (mpm.mpf('1.627905136020'), mpm.mpf('84334.6615717837')),
            (mpm.mpf('2.355555638750'), mpm.mpf('83286.9142477147'))
            )

        # planetary frequencies in longitude
        self.freqpla = {'Mercury'   :   mpm.mpf('0.2608790314068555e5'),  
                        'Venus'     :   mpm.mpf('0.1021328554743445e5'),
                        'EMB'       :   mpm.mpf('0.6283075850353215e4'),
                        'Mars'      :   mpm.mpf('0.3340612434145457e4'), 
                        'Jupiter'   :   mpm.mpf('0.5296909615623250e3'),  
                        'Saturn'    :   mpm.mpf('0.2132990861084880e3'),
                        'Uranus'    :   mpm.mpf('0.7478165903077800e2'),  
                        'Neptune'   :   mpm.mpf('0.3813297222612500e2'), 
                        'Pluto'     :   mpm.mpf('0.2533566020437000e2')}  

        
            
        # target variables
        self.ax = mpm.mpf('0.0')            # major semiaxis
        self.ml = mpm.mpf('0.0')            # mean longitude
        self.kp = mpm.mpf('0.0')            # e*cos(perielium longitude)
        self.hp = mpm.mpf('0.0')            # e*sin(perielium longitude)
        self.qa = mpm.mpf('0.0')            # sin(inclination/2)*cos(ascending node longitude)
        self.pa = mpm.mpf('0.0')            # sin(inclination/2)*cos(ascending node longitude)
        
        self.tg_var = {'A':self.ax, 'L':self.ml, 'K':self.kp,
                       'H':self.hp, 'Q':self.qa, 'P':self.pa } 

        # eps  = (23.d0+26.d0/60.d0+21.41136d0/3600.d0)*dgrad
        self.eps = mpm.mpf((23.0+26.0/60.0+21.411360/3600.0)*mpm.pi/180.0)
        self.phi  = mpm.mpf(-0.051880 * mpm.pi / 180.0 / 3600.0)
        self.ceps = mpm.cos(self.eps)
        self.seps = mpm.sin(self.eps)
        self.cphi = mpm.cos(self.phi)
        self.sphi = mpm.sin(self.phi)
        
        # rotation of ecliptic -> equatorial rect coords
        self.rot = [[self.cphi, -self.sphi*self.ceps,  self.sphi*self.seps],
                    [self.sphi,  self.cphi*self.ceps, -self.cphi*self.seps],
                    [0.0,        self.seps,            self.ceps          ]]
        
        self.fmt = struct.Struct("""6s 3s 3s 3s 3s x 3s 3s 3s 3s 3s x 4s 4s 4s 4s x
                       6s x 3s 3s 3s 20s x 3s 20s x 3s x""")

        self.gmp = {
       'Mercury' : mpm.mpf('4.9125474514508118699e-11'),
       'Venus'   : mpm.mpf('7.2434524861627027000e-10'),
       'EMB'     : mpm.mpf('8.9970116036316091182e-10'),
       'Mars'    : mpm.mpf('9.5495351057792580598e-11'),
       'Jupiter' : mpm.mpf('2.8253458420837780000e-07'),
       'Saturn'  : mpm.mpf('8.4597151856806587398e-08'),
       'Uranus'  : mpm.mpf('1.2920249167819693900e-08'),
       'Neptune' : mpm.mpf('1.5243589007842762800e-08'),
       'Pluto'   : mpm.mpf('2.1886997654259696800e-12')
       }

        self.gmsol = mpm.mpf('2.9591220836841438269e-04')
        self.rgm = mpm.sqrt(self.gmp[self.planet]+self.gmsol)
        # run calculus routine
        self.calc()
        

    def __str__(self):
        
        vsop_out = "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.tg_var['A'],
                                          self.tg_var['L'],
                                          self.tg_var['K'],
                                          self.tg_var['H'],
                                          self.tg_var['Q'],
                                          self.tg_var['P'])
        vsop_out += "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.ecl[0],
                                          self.ecl[1],
                                          self.ecl[2],
                                          self.ecl[3],
                                          self.ecl[4],
                                          self.ecl[5])
        vsop_out += "{:3.13} {:3.13} {:3.13} {:3.13} {:3.13} {:3.13}\n".format(
                                          self.equat[0],
                                          self.equat[1],
                                          self.equat[2],
                                          self.equat[3],
                                          self.equat[4],
                                          self.equat[5])
                        

        return vsop_out



    def calc(self):
        with open(self.planets[self.planet]) as file_in:
            terms = []
            b = '*'
            while b != '':
                b = file_in.readline()
                if b != '':
                    if b[:5] == ' VSOP':
                        header = b.split()
                        #print header[3], header[7], header[8], self.t**int(header[3])
                        no_terms = int(header[4])
                        for i in xrange(no_terms):
                            #6x,4i3,1x,5i3,1x,4i4,1x,i6,1x,3i3,2a24
                            terms = file_in.readline()
                            a = self.fmt.unpack(terms)
                            S = mpm.fmul(a[18],mpm.power(10,int(a[19])))
                            C = mpm.fmul(a[20],mpm.power(10,int(a[21])))
                            if mpm.sqrt(S*S+C*C) < self.precision:
                                break
                            aa = 0.0; bb = 0.0;
                            for j in xrange(1,18):
                                aa += mpm.fmul(mpm.mpf(a[j]), self.l[j-1][0])
                                bb += mpm.fmul(mpm.mpf(a[j]), self.l[j-1][1])
                            arg = aa + bb * self.t
                            power = int(header[3])
                            comp = self.power[power] * (S * mpm.sin(arg) + C * mpm.cos(arg))
                            if header[7] == 'L' and power == 1 and int(a[0]) == 1:
                                pass
                            else:
                                self.tg_var[header[7]] += comp
        self.tg_var['L'] = self.tg_var['L'] + self.t * self.freqpla[self.planet]
        self.tg_var['L'] = self.tg_var['L'] % (2 * mpm.pi)
        if self.tg_var['L'] < 0:
            self.tg_var['L'] += 2*mpm.pi
        print "Julian date {}".format(self.JD)
        file_in.close()
        ##print self.tg_var
    
        ####    def ELLXYZ(self):

        xa = self.tg_var['A']
        xl = self.tg_var['L']
        xk = self.tg_var['K']
        xh = self.tg_var['H']
        xq = self.tg_var['Q']
        xp = self.tg_var['P']
        
        # Computation
  
        xfi = mpm.sqrt(1.0 -xk * xk - xh * xh)
        xki = mpm.sqrt(1.0 -xq * xq - xp * xp)
        u = 1.0/(1.0 + xfi)
        z = complex(xk, xh)
        ex = abs(z)
        ex2 = ex  * ex
        ex3 = ex2 * ex
        z1 = z.conjugate()
        #
        gl = xl % (2*mpm.pi)
        gm = gl - mpm.atan2(xh, xk)
        e  = gl + (ex - 0.1250 * ex3) * mpm.sin(gm)
        e += 0.50 * ex2 * mpm.sin(2.0 * gm)
        e += 0.3750 * ex3 * mpm.sin(3.0 * gm)
        #
        while True:
            z2 = complex(0.0, e)
            zteta = mpm.exp(z2)
            z3 = z1 * zteta
            dl = gl - e + z3.imag
            rsa = 1.0 - z3.real
            e = e + dl / rsa
            if abs(dl) < 1e-15:
                break
        #
        z1  =  u * z * z3.imag
        z2  =  complex(z1.imag, -z1.real)
        zto = (-z+zteta+z2)/rsa
        xcw = zto.real
        xsw = zto.imag
        xm  = xp * xcw - xq * xsw
        xr  = xa * rsa
        #
        self.ecl = []; self.equ = {}
        self.ecl.append(xr * (xcw -2.0 *xp * xm))
        self.ecl.append(xr * (xsw +2.0 *xq * xm))
        self.ecl.append(-2.0 * xr * xki * xm)
        #
        xms = xa *(xh + xsw) / xfi
        xmc = xa *(xk + xcw) / xfi
        xn  = self.rgm / xa ** (1.50)
        #
        self.ecl.append( xn *((2.0 * xp * xp - 1.0) * xms + 2.0 * xp * xq * xmc))
        self.ecl.append( xn *((1.0 -2.0 * xq * xq) * xmc -2.0 * xp * xq * xms))
        self.ecl.append( 2.0 * xn * xki * (xp * xms + xq * xmc))
        
        # Equatorial rectangular coordinates and velocity

        #         
        #
        # --- Computation ------------------------------------------------------
        #
        self.equat = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]    
        for i in xrange(3):
           for j in xrange(3):
               self.equat[i]   = self.equat[i]   + self.rot[i][j] * self.ecl[j]
               self.equat[i+3] = self.equat[i+3] + self.rot[i][j] * self.ecl[j+3]



if __name__ == '__main__':
    for planet in ('Mercury', 'Venus', 'EMB', 'Mars', 'Jupiter',
          'Saturn', 'Uranus', 'Neptune', 'Pluto'):
        print "PLANETARY EPHEMERIS VSOP2013  "+ planet + "(TDB)\n"+""" 
  1/ Elliptic   Elements:    a (au), lambda (radian), k, h, q, p        - Dynamical Frame J2000
  2/ Ecliptic   Heliocentric Coordinates:  X,Y,Z (au)  X',Y',Z' (au/d)  - Dynamical Frame J2000
  3/ Equatorial Heliocentric Coordinates:  X,Y,Z (au)  X',Y',Z' (au/d)  - ICRS Frame J2000
"""
        init_date = cal2jul(1890,6,26,12)
        set_date = init_date

        while set_date < init_date + 41000:
            v = VSOP2013(set_date, planet, 1e-12)
            print v
            set_date += 4000
          
        

Il programma accetta in input tre parametri:

  1. la data giuliana comprensiva dell'ora in frazione decimale di giorno
  2. il nome del pianeta richiesto (in inglese con l'iniziale maiuscola, vedere la lista
  3. la precisione richiesta, se non è inserita scatta quella di default (1e-7)

Il programma è in ogni caso lento nell'esecuzione. Nel prossimo post cerchero' di sfruttare qualche tecnica per migliorare la velocità esecutiva, ma dubito di poter lavorare in Python alla velocità di Fortran. Al prossimo post.

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...