Tuesday, March 25, 2014

Il modulo ctypes e la libreria libswe.so

Siccome la mia class SVG2Tk è ancora un po' indietro di cottura, apro una piccola parentesi sull'accesso diretto alle librerie condivise scritte in linguaggio C.

La libreria pyswisseph, che ho usato nei primi post, è un lavoro splendido e ci permette di effettuare chiamate alla libreria con istruzioni in Python nativo. Se pero' volessimo usare la libreria originaria, installata dal software center di Ubuntu, dovremmo confrontarci con un problema: non è scritta in Python ma in C e compilata verosimilmente con gcc, il compilatore GNU. La stessa pyswisseph è un insieme di funzioni wrapper (involucro) che fanno da ponte fra i due linguaggi. Anche noi, però, possiamo farlo direttamente dal nostro codice.

Tra le librerie integrate nell'installazione di Python c'è un modulo realizzato appositamente per chiamare una libreria in C, a patto che si conoscano le specifiche della libreria, i nomi delle funzioni e i parametri in input e in output.

Fortunatamente la libreria swisseph è molto ben documentata e con un po' di sforzo possiamo effettuare delle chiamate come se utilizzassimo una libreria pitonica. Vediamo come, prendendo a spunto un post del curatore delle pyswisseph Stanislas Marquis link.

Per prima cosa apriamo IDLE e scriviamo l'import di ctypes e di datetime. A questo punto creiamo una variabile shared_lib che costituirà il link alla libreria, attraverso il metodo CDLL di ctypes (o, in alternativa, il metodo cdll.LoadLibrary, sovrapponibile al precedente). L'installazione di swisseph da ubuntu software center mette la libreria libswe.so in una specifica posizione sul disco. Qualora volessimo spostarla nella stessa directory del sorgente Python sarà necessario cambiare il path della libreria stessa. Aggiungiamo inoltre due dizionari per i nomi dei pianeti e dei segni zodiacali.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding: utf-8 -*-
import datetime
import ctypes as ct 

shared_lib = ct.CDLL('/usr/lib/x86_64-linux-gnu/libswe.so')

pianeti = {0:'Sole',
           1:'Luna',
           2:'Mercurio',
           3:'Venere',
           4:'Marte',
           5:'Giove',
           6:'Saturno',
           7:'Urano',
           8:'Nettuno',
           9:'Plutone',
           10:'Nodo lunare medio',
           11:'Nodo lunare vero'}

segni = {0:'Ariete',
         1:'Toro',
         2:'Gemelli',
         3:'Cancro',
         4:'Leone',
         5:'Vergine',
         6:'Bilancia',
         7:'Scorpione',
         8:'Sagittario',
         9:'Capricorno',
         10:'Acquario',
         11:'Pesci'}

Della libreria swisseph, in questo programma, uso solo due funzioni, una per il calcolo della data giuliana, l'altra per il calcolo della posizione dei pianeti. Vediamo la prima.


1
2
3
4
5
6
7
_julday=shared_lib.swe_julday
_julday.argtypes=[ct.c_int, ct.c_int, ct.c_int, ct.c_double, ct.c_int]
_julday.restype=ct.c_double

def julday(year, month, day, hour, gregflag=1):
    return _julday(ct.c_int(year), ct.c_int(month), ct.c_int(day),
                   ct.c_double(hour), ct.c_int(gregflag))

La prima parte è molto semplice. La libreria ctypes consente di accedere alla funzione nascosta nella shared library swe_julday definendo prima gli argtypes, cioè le tipologie di variabili da usare come parametri di input, poi il restype, cioè il tipo di variabile di ritorno dalla funzione (ricordiamo che il linguaggio C non consente la tipizzazione dinamica, cioè l'assegnazione arbitraria in runtime di un tipo all'atto della creazione della variabile. Essendo un linguaggio compilato, richiede, ovviamente, la tipizzazione di tipo statico).

La chiamata da codice Python risulta molto naturale, in questo caso i parametri passati alla funzione julday vengono trasferiti ai tipi definiti per l'interfaccia al linguaggio C. La funzione restituisce una variabile double, in Python corrisponde a una float e fornisce il valore della data giuliana calcolata da data e ora.

Un po' più complessa risulta, invece, la seconda funzione, che calcola le posizioni planetarie. Di seguito un estratto della guida Programming Interface to the Swiss Ephemeris:

2.1. The call parameters
swe_calc_ut() was introduced with Swisseph version 1.60 and makes planetary calculations a bit simpler. For the steps required, see the chapter  The programming steps to get a planet’s position.
swe_calc_ut() and swe_calc() work exactly the same way except that swe_calc() requires Ephemeris Time( more accurate: Dynamical Time ) as a parameter whereas swe_calc_ut() expects Universal Time. For common astrological calculations, you will only need swe_calc_ut() and will not have to think anymore about the conversion between Universal Time and Ephemeris Time.
swe_calc_ut() and swe_calc() compute positions of planets, asteroids, lunar nodes and apogees. They are defined as follows:
 
int swe_calc_ut ( double tjd_ut, int ipl, int iflag, double* xx, char* serr),
where
tjd_ut     =Julian day, Universal Time
ipl       =body number
iflag    =a 32 bit integer containing bit flags that indicate what kind of computation is wanted
xx       =array of 6 doubles for longitude, latitude, distance, speed in long., speed in lat., and speed in dist.
serr[256] =character string to return error messages in case of error.
 
and
int swe_calc(double tjd_et, int ipl, int iflag, double *xx, char *serr),
same but
tjd_et     =     Julian day, Ephemeris time,  where tjd_et = tjd_ut + swe_deltat(tjd_ut)

Nella dichiarazione della funzione swe_calc compaiono due puntatori (in Python non esistono, o meglio le variabili Python sono dei puntatori, ma non dilunghiamoci), cioè gli indirizzi di memoria nei quali possiamo trovare le variabili xx (che contiene un array di 6 doubles, in Python diremmo una tupla di 6 float) e serr (per l'eventuale messaggio di errore).

I tipi di variabili ctypes possono essere inizializzati nel codice Python con modalità differenti.

Per i tipi più semplici, ctypes.c_int(42) ctypes.c_float(3.1415), ad esempio, il valore tra parentesi viene conservato in una locazione di memoria da cui possono essere recuperati con l'espressione .value. Per esempio:

>>> a = ctypes.c_int(42)
>>> print a
c_int(42)
>>> print a.value
42
>>> b = ctypes.c_float(3.1415)
>>> print b
c_float(3.1414999961853027)
>>> print b.value
3.14149999619

Vediamo ora come si puo' effettuare la traduzione in Python della funzione swe_calc di swisseph utilizzando ctypes.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
_calc = shared_lib.swe_calc
_calc.argtypes = [ct.c_double, ct.c_int, ct.c_int, ct.POINTER(ct.c_double*6), ct.c_char_p]
_calc.restype = ct.c_int

def calc(julday, planet, flag = 0):

    xx = (ct.c_double * 6)()
    err = ct.c_char_p('')
    if _calc(ct.c_double(julday), ct.c_int(planet), ct.c_int(flag),
             ct.byref(xx), err) == 0:
        return [x for x in xx]
    return err.value

Come potete vedere, il tipo POINTER viene usato per tradurre il puntatore ad una variabile (preceduta da *) di C. Per la variabile serr si usa il tipo predefinito ctypes.c_char_p che è esso stesso un puntatore. La funzione accetta come parametri un double che riprende il risultato del calcolo della data giuliana, una variabile c_int per il numero di posizione del pianeta nella serie e infine il tipo c_int(flag), inizializzato a 0 perchè "If no bits are set, i.e. if iflag == 0, swe_calc() computes what common astrological ephemerides (as available in book shops) supply". Se il valore di ritorno di _calc è zero, cioè non viene resituito un errore, l'array di 6 double va a popolare una list di 6 float contenente vari elementi relativi al pianeta considerato. Di questi ci interessa solo l'elemento con indice 0, riferito alla longitudine in gradi. La lista viene generata con una list comprehension [x for x in xx].

L'ultima parte del programma contiene istruzioni per l'uso delle funzioni C che abbiamo creato in precedenza.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def d2hm(x):
    lon30 = x % 30
    segno = int(x) / 30
    grado = int(lon30)
    minuto = int((lon30-grado)*60)
    return (grado, segno, minuto)


adesso = datetime.datetime.now()
in_questo_momento = julday(adesso.year,
       adesso.month,
       adesso.day,
       adesso.hour + adesso.minute/60.0 + adesso.second/3600.0)

for i in range(0,12):
    longitudine = calc(in_questo_momento,i)[0]
    pos = d2hm(longitudine)
    print "%18s %2d° %10s %2d'" % (pianeti[i], pos[0], segni[pos[1]], pos[2])

Lanciando il programma si ottiene il seguente risultato:

              Sole  4°     Ariete 49'
              Luna 24° Capricorno 33'
          Mercurio  9°      Pesci 42'
            Venere 18°   Acquario 18'
             Marte 23°   Bilancia 56'
             Giove 11°     Cancro  1'
           Saturno 22°  Scorpione 53'
             Urano 12°     Ariete  1'
           Nettuno  6°      Pesci  8'
           Plutone 13° Capricorno 28'
 Nodo lunare medio 29°   Bilancia 50'
  Nodo lunare vero 28°   Bilancia 36'

Riferito ad oggi 25 marzo alle 13.37 tempo di Greenwich

Spero che questo post sia stato utile per capire come interfacciarsi ad una shared library compilata in linguaggio C. Ulteriori informazioni sull'uso del modulo ctypes sono rintracciabili alla pagina http://docs.python.org/2/library/ctypes.html#

Se volete creare voi stessi la libreria libswe.so, scaricate il file a questo link. Decomprimetelo e lanciate l'istruzione make, che crea i file oggetto. Quindi create la libreria condivisa, nella stessa directory, con l'istruzione gcc -g -O9 -Wall -fPIC -lm -shared -o libswe.so swedate.o swehouse.o swejpl.o swemmoon.o swemplan.o swepcalc.o sweph.o swepdate.o swephlib.o swecl.o swehel.o. La libreria libswe.so comparirà nella directory. Copiatela dove avete collocato il sorgente .py di cui a questo post. Modificate il riferimento alla libreria nel vostro codice in shared_lib = ct.CDLL('libswe.so') e lanciate in esecuzione il codice python, funzionerà perfettamente.

Sunday, March 16, 2014

Quarto intermezzo - L'indice di concentrazione planetaria

Si deve ad André Barbault, insigne astrologo francese, l'introduzione di un indice sintetico che rappresenta il grado di maggiore o minore allontanamento reciproco dei pianeti, che, prescindendo dalle metodologie tradizionali dell'astrologia, basate sulla posizione dei pianeti nei segni e sugli aspetti, cioè distanze angolari discrete dotate di tolleranze contenute nell'ordine di qualche grado, consentirebbe la lettura degli eventi collettivi (astrologia mondiale) secondo cicli complessi che variano tra minimi e massimi nell'arco di giorni, mesi, anni o addirittura secoli. Per i necessari approfondimenti rimando all'opera dell'autore citato, in particolare a:

  1. A. Barbault. Il pronostico sperimentale in astrologia. Mursia ed. 1979
  2. A. Barbault. Astrologia mondiale (trad. Clara Negri). Armenia 1980

Per il calcolo dell'indice ho pensato di fornire un piccolo contributo personale ai lettori esperti di linguaggio Python, appoggiandomi ad alcune librerie esterne per Python:

La prima è pyephem di Brandon Rhodes, una libreria astronomica (non astrologica come swisseph di Astrodienst)

la seconda è pylab, dedicata alla grafica matematica, che fornisce una sintesi di numpy, scipy e matplotlib per fornire un ambiente simile all'ambiente MATLAB, realizzato per il linguaggio C.

Come sempre fornisco un listato (quick and dirty) per il calcolo dell'indice. Ho dato un orizzonte temporale di 50 anni, ma, ovviamente, chi ha già familiarità con Python puo' divertirsi a cambiare data di inizio, lunghezza del periodo, e altri parametri. Le librerie datetime e time sono incluse nell'installazione Python di base, quindi vanno solo importate. Per pylab nel repository di Ubuntu trovate il pacchetto python-matplotlib. Numpy può essere installata attraverso pip o dai repository di Ubuntu senza particolari difficoltà.

Ok, vi passo il codice per l'indice di concentrazione planetaria, considerando i pianeti lenti da Giove a Plutone. Nel grafico compare anche (quasi incomprensibile, lo so) la posizione all'inizio di ogni anno dei cinque pianeti considerati, nell'intervallo 0-360 gradi. Puo' aiutare a capire almeno quando si realizzano delle congiunzioni importanti.


import  pylab ,  numpy 
import  ephem 
import  time ,  datetime

planets = { 0 :ephem.Sun, 1 :ephem.Moon, 2 :ephem.Mercury, 3 :ephem.Venus,
            4 :ephem.Mars, 5 :ephem.Jupiter, 6 :ephem.Saturn, 7 :ephem.Uranus,
            8 :ephem.Neptune, 9 :ephem.Pluto}

def  longitude (x):
     return ephem.Ecliptic (x)

year =  1990 
month =  1 
day =  1 
hour =  0 
minute =  0 
time_span =  50

initial_date = datetime.datetime (year, month, day, hour, minute)

x = []
y = []
jup = []
sat = []
ura = []
nep = []
plu = []

for i in  range ( 0 , time_span):
     date = datetime.datetime (year + i, month, day, hour, minute)
     b = []
     icp =  0 
     for key in  range ( 5 , 10 ):
          a = planets[key](date, epoch = date)
          b.append(ephem.degrees(ephem.Ecliptic(a).lon) / ephem.pi * 180 )
     for j in  range ( 0 , len (b)):
          for k in  range (j, len (b)):
               _icp = (b [k] - b [j])
               if _icp <  0 :
                    _icp +=  360 
               if _icp >  180 :
                    _icp =  360 - _icp
               icp += _icp
     x.append (i + year)
     y.append (icp)
     jup.append (b[0])
     sat.append (b[1])
     ura.append (b[2])
     nep.append (b[3])
     plu.append (b[4])

pylab.figure(1,figsize =(15 , 10 ))
pylab.title ( 'Index planetary concentration' )
pylab.xlabel ( 'year' )
pylab.ylabel ( 'ICP' )
pylab.xticks (numpy.linspace (year, year + time_span, 50 , endpoint = True ), rotation = - 90 )
pylab.plot (x, y, 'r' , label = 'ICP' )
pylab.plot (x, jup, 'bo' , label = 'jupiter' )
pylab.plot (x, sat, 'g ^' , label = 'saturn' )
pylab.plot (x, ura, 'c.' , label = 'uranus' )
pylab.plot (x, nep, 'm,' , label = 'neptune' )
pylab.plot (x, plu, 'k.' , label = 'bar' )

pylab.grid (axis = 'Both' )
pylab.legend (loc = 'best' )
pylab.show ()

A fra qualche giorno con l'ultima puntata di SVG2Tk.

Friday, March 14, 2014

XML - ricerca ricorsiva rivisitata

Le cose non vanno sempre lisce quando si esplora un protocollo. Nel caso dei file SVG il procedimento seguito finora puo' non funzionare (anzi non funziona quasi mai) perchè il protocollo SVG è lontano dall'avere sempre gli stessi contenuti e la stessa disposizione degli stessi. L'unica cosa certa è che il file SVG è un file XML, quindi composto da tag nidificate, che i dati sono prevalentemente negli attributi piuttosto che in testo libero e che dobbiamo quindi trovare una modalità di esplorazione, sempre ricorsiva, che in primo luogo ci dia la certezza di estrarre tutti i tag e relativi attributi e quindi ci consenta la comparazione dei tag con modelli tipo e relativi pattern di estrazione con le regexp.

Ho quindi riscritto (e allego di seguito) il prototipo della funzione xmlrecur per adattarsi a qualsiasi file xml o SVG di cui vogliamo fare successive conversioni.


#|/usr/bin/env python
from xml.etree import ElementTree as ET

def xmlrecur(x):
    print '<elemento',x,'>'
    print '<tag>', x.tag
    print '<attributo>',x.attrib
    for key in x.attrib:
        print 'singolo attributo', key, '-', x.attrib[key]
    print '<testo>',x.text
    print '-'*80
        
    for i in x:
            xmlrecur(i)

tree = ET.parse('xxx.xml')
root = tree.getroot()
xmlrecur(root)

Vi prego di notare che la proprietà attrib è un dizionario, di cui si puo' fare la scansione. Su questa particolarità concentreremo l'analisi per l'identificazione delle word caratteristiche di nostro interesse.

Ancora qualche giorno per finire il tutto. A presto.

Friday, March 7, 2014

Ulteriore miglioramento del codice

Avevo annunciato l'intenzione di eliminare, dalla classe SVG2Tk, l'uso di file di appoggio esterni.

Nel file che allego in questo post mostro come si puo' fare. Un difetto fondamentale della classe, come è stata progettata finora, è la pesantezza dell'analisi del file SVG, la necessità di riversare le informazioni ottenuta in file esterni, che vengono scritti e riletti senza che ce ne sia un'effettiva necessità. Un ulteriore miglioramento puo' essere realizzato con l'uso dei generatori, argomento tutt'altro che facile da capire, ma che cercherò di rendere nel modo più semplice possibile.

In primo luogo, disponendo di una metodo ricorsivo per l'identificazione degli attributi fondamentali del file SVG, utili al nostro lavoro di traduzione, possiamo cercare di fare in un'unica passata la lettura e la trascrizione dei punti di tracciato, senza salvataggi intermedi e senza creare lunghe liste in memoria, che, nonostante le notevoli migliorie delle tecnologie attualmente disponibili, rischierebbe di saturare la memoria RAM e allungare i tempi.

I generatori sono funzioni speciali, in cui i risultati ottenuti dallo scorrimento di una sequenza vengono restituiti on demand, cioè man mano che il programma ne richiede la disponibilità.

Ciò significa che il file viene letto in modo da assicurare la immediata disponibilità di un'informazione, che puo' essere trattata ignorando le informazioni successive.

Non essendo più necessari i file intermedi di appoggio, forniamo solo definizione del file SVG, e la forniamo alla classe attraverso il suo inizializzatore o costruttore.

Per agevolare la comprensione, spezzo il codice nelle sezioni corrispondenti ai metodi, che pubblico una alla volta per commentarle immediatamente dopo. Il lavoro non è ancora concluso, usero' ancora una volta (l'ultima) un file per riversare i dati sul disco fisso, ma sarà evidente il guadagno di tempo e memoria e la maggiore snellezza del codice.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# -*- coding: utf-8 -*-
#|/usr/bin/env python
import re
import xml.etree.ElementTree as ET

class SVG2Tk:
    """La classe è finalizzata ad assicurare la traduzione, per quanto possibile
    fedele alle specifiche SVG, negli oggetti caratteristici del widget Canvas
    di Tkinter"""

    def __init__(self, file_in):
        
        self.file_in = open(file_in, 'r')

La prima parte non è di difficile comprensione, riprende più o meno i precedenti contenuti. Ho eliminato i parametri riferiti ai nomi dei file di appoggio e ho inserito delle righe di commento che vanno a comporre la documentazione della classe così come si puo' ottenere con i normali strumenti di documentazione di Python: lo vedremo più chiaramente alla fine.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    def s_match(self, pattern, d):
        """restituisce una stringa contenente la substringa che soddisfa
            il pattern fornito alla funzione, e la stringa risultante dopo
            l'estrazione della substringa"""
        
        matchObj = re.match(pattern, d)
        if matchObj:
            string = matchObj.group()
            d = d.replace(string, "")
            s_string = re.split(r'[;\s]*',string)
            string = ','.join(s_string[:-1])
            return (string, d)
        return None

Anche questa sezione, che utilizza le regular expressions per il match tra stringhe di SVG e modelli, non differisce dalla precedente.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    def xmlrecur(self,x):
        "restituisce l'insieme degli attributi che soddisfano le stringhe di ricerca "
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                yield (i.attrib['id'], i.attrib['style'], i.attrib['d'])
            elif set(['width','height']).issubset(i.attrib):
                yield ('width - height', i.attrib['width'], i.attrib['height'])
            else:
                for j in self.xmlrecur(i):
                    yield j

La routine xmlrecur è stata, invece, pesantemente rimaneggiata. Ogni volta che l'identificazione dell'attributo è positiva, uso la word yield che è simile al return, cioè alla restituzione dei risultati delle attività previste dal metodo, ma avviene ogni volta che ho necessità di fornire al codice chiamante dei valori senza interrompere la scansione dell'iterable, in questo caso del file di origine dei dati, che viene solo temporaneamente sospesa. Ad ogni successiva chiamata verrà ripresa la scansione e verranno forniti ulteriori risultati.

Ciò che rende di difficile comprensione il metodo è il fatto che apparentemente viene restituito al codice chiamante un dato, mentre in realtà viene restituito un metodo, che si chiama generatore.

Vediamo la sezione finale e ne discutiamo immediatamente.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    def re_load(self):

        tree = ET.parse(self.file_in)
        root = tree.getroot()
        generatore = self.xmlrecur(root)
        tk_out = open('tk_out','w')
        for i in generatore:
            if i[0] == 'width - height':
                self.width  = float(i[1])
                self.height = float(i[2])
            else:
                self.id    = i[0]
                self.style = i[1]
                self.d     = i[2]
                try:
                    pattern=[
                    r'([M]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]?[-\d]*[\.]?[\d]*[\s]*)',
                    r'([L]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*)',
                    r'([C]{1}[\s]*)(([-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*){3})',
                    r'([z]{1}[\s]*)'
                    ]
                    
                    while len(self.d) > 0:
                        for p in pattern:
                            ret = self.s_match(p, self.d)
                            if ret != None:
                                string = ret[0]
                                self.d = ret[1]
                                tk_out.write(string+'\n')                            
                except Exception:
                    print Exception
                    tk_out.close()
                    break

if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg')
    app.re_load()

La funzione re_load è simile alla precedente, ma ne differisce per il fatto che i dati ottenuti dalla scansione vengono caricati, non come dati, ma come metodo, sull'item generatore da cui vengono estratti uno alla volta e sottoposti al matching. Non è intuitivo, il generatore non contiene alcun dato, non è una lista, bensì una chiamata iterativa al metodo recurxml che estrae i dati on demand. Ci ho messo parecchio a capire come funziona questo metodo e rinuncio a tentare di spiegarlo ulteriormente. Se avete voglia di discuterlo mandatemi un commento e ne parliamo insieme.

Nella sezione finale inizializzo la class col solo nome del file SVG e poi faccio una chiamata al metodo re_load che fa girare il tutto. Nel prossimo post eliminerò anche l'ultimo puntello, cioè il file di appoggio, e farò in modo che l'estrazione avvenga in streaming con la creazione degli oggetti di Tkinter.Canvas. Alla prossima

Saturday, March 1, 2014

Miglioramento del codice

Lavorando in Python si apprezza la facilità con cui si colgono le imperfezioni del codice, quasi subito dopo averlo scritto. A volte sono puramente estetiche e basta seguire le indicazioni della PEP 8 (Python Enhancement Proposal) per ottenere un risultato migliore. Altre volte bisogna cogliere ridondanze e mancanza di snellezza del codice. Ora che ho verificato che il codice del post precedente funziona, posso iniziare a migliorarlo.

  1. trasferisco le importazioni delle librerie esterne fuori della classe
  2. limito le funzioni del costruttore alla inizializzazione dei file esterni
  3. riduco la ridondanza, spostando su una funzione la ricerca del pattern sulla stringa 'd' e creando una lista di pattern da leggere iterativamente. la funzione s_match restituisce sia la stringa risultante dal match pattern/stringa d, sia la stringa d modificata, per mezzo di una tupla
  4. affido l'esecuzione del programma al metodo re_load che gestisce anche le exception per le espressioni regolari

E questo è il risultato, un po' più pitonico:


#|/usr/bin/env python
import re
import xml.etree.ElementTree as ET

class SVG2Tk:
    
    def __init__(self, file_s, file_out):

        self.file_s = open(file_s, 'r')
        self.file_out= open(file_out,'w')
        self.tk_out = open('tk_out.txt','w')


    def s_match(self, pattern, d):
        matchObj = re.match(pattern, d)
        if matchObj:
            string = matchObj.group()
            d = d.replace(string, "")
            s_string = re.split(r'[;\s]*',string)
            string = ','.join(s_string[:-1])
            return (string, d)
        return None
    
    def xmlrecur(self,x):
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                    self.file_out.write(i.attrib['id'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['style'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['d'])
                    self.file_out.write('\n')
            elif set(['width','height']).issubset(i.attrib):
                self.file_out.write(i.attrib['width'])
                self.file_out.write('\n')
                self.file_out.write(i.attrib['height'])
                self.file_out.write('\n')
                
            else:
                self.xmlrecur(i)

    def re_load(self, file_out):

        tree = ET.parse(self.file_s)
        root = tree.getroot()
        self.xmlrecur(root)
        self.file_out.close()
        
        self.file_in = open(file_out, 'r')
        
        try:
            width=self.file_in.next()
            height=self.file_in.next()
            self.tk_out.write('width %sheight %s' % (width, height))
        except Exception:
            print Exception

        while True:
            
            try:
                id_ = self.file_in.next()
                style = self.file_in.next()
                d = self.file_in.next()

                self.tk_out.write('id,' + id_)
                pattern=[
                r'([M]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]?[-\d]*[\.]?[\d]*[\s]*)',
                r'([L]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*)',
                r'([C]{1}[\s]*)(([-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*){3})',
                r'([z]{1}[\s]*)'
                ]
                
                while len(d) > 0:
                    for p in pattern:
                        ret = self.s_match(p, d)
                        if ret != None:
                            string = ret[0]
                            d = ret[1]
                            self.tk_out.write(string+'\n')
                        
            except Exception:
                print Exception
                self.tk_out.close()
                break



if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg','out_xml.txt')
    app.re_load('out_xml.txt')

Modificata così la classe, procederemo all'ultimo step, cioè alla traduzione in Tkinter.

Friday, February 28, 2014

Creazione di oggetti poligonali complessi in Tkinter.

Continuando la riflessione di cui al post precedente, in cui abbiamo visto come recuperare le istruzioni fondamentali per la realizzazione di spostamenti sulla canvas (istruzione M in SVG), realizzazione di linee (istruzione L) e curve (istruzione C), siamo in grado di iniziare a tradurre le istruzioni SVG in Tkinter. Dobbiamo considerare pero' che, mentre in SVG possiamo tracciare il percorso <path> d=' nell'ambito della stessa istruzione, in Tkinter abbiamo delle primitive per la creazione di curve o figure che devono essere realizzate separatamente. Per avere un criterio unificatore, possiamo considerare l'attributo generico 'tag' dei widget, già visto in precedenza, che ci permetterà di usare più oggetti canvas simultaneamente, come fossero un solo oggetto.

E' necessario, prima di andare avanti, chiarire due aspetti. Uno riguarda la differenza tra le istruzioni SVG M, L, C e le corrispondenti minuscole. Le prime indicano riferimenti assoluti, le seconde riferimenti relativi. Nel primo caso vengono indicate le coordinate esatte, nel secondo gli spostamenti.

Altro chiarimento che vi devo riguarda il metodo usato nel post precedente per estrarre le informazioni dal path; la ricerca fatta con l'uso delle espressioni regolari avviene in tutta la stringa, prescindendo dall'ordine.

Pertanto la ricostruzione del tracciato è sicuramente affetta da errori, se non si tiene conto del punto corretto di inizio tracciato della nuova linea o curva.

Ciò detto, riprendiamo a lavorare sul file di estrazione, apportando le correzioni necessarie al codice.


#|/usr/bin/env python
import re

class SVG2Tk:

    import xml.etree.ElementTree as ET
    
    def __init__(self, file_s, file_out):

        self.file_s = open(file_s, 'r')
        self.file_out= open(file_out,'w')

        tree = SVG2Tk.ET.parse(file_s)
        root = tree.getroot()
        self.xmlrecur(root)
        self.file_out.close()
        
        file_in = open(file_out)
        self.tk_out = open('tk_out.txt','w')
        try:
            width=file_in.next()
            height=file_in.next()
            self.tk_out.write('width %sheight %s' % (width, height))
        except Exception:
            print Exception

        while True:
            
            try:
                id_ = file_in.next()
                style = file_in.next()
                d = file_in.next()
                self.tk_out.write('id,' + id_)
                while len(d)>0:
                    pattern1 = r'([M]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]?[-\d]*[\.]?[\d]*[\s]*)'
                    matchObj = re.match(pattern1, d)
                    if matchObj:
                        string = matchObj.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')
    
                    pattern2 = r'([L]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*)'
                    matchOb2 = re.match(pattern2, d)
                    if matchOb2:
                        string = matchOb2.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')

                    pattern3 = r'([C]{1}[\s]*)(([-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*){3})'
                    matchOb3 = re.match(pattern3, d)
                    if matchOb3:
                        string = matchOb3.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')

                    pattern4 = r'([z]{1}[\s]*)'
                    matchOb4 = re.match(pattern4, d)
                    if matchOb4:
                        string = matchOb4.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')
                        
            except Exception:
                print Exception
                self.tk_out.close()
                break
                

    def xmlrecur(self,x):
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                    self.file_out.write(i.attrib['id'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['style'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['d'])
                    self.file_out.write('\n')
            elif set(['width','height']).issubset(i.attrib):
                self.file_out.write(i.attrib['width'])
                self.file_out.write('\n')
                self.file_out.write(i.attrib['height'])
                self.file_out.write('\n')
                
            else:
                self.xmlrecur(i)

if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg','out_xml.txt')

Le correzioni maggiori apportate al codice del post precedente riguardano:

  1. L'uso del solo metodo 'match' delle regular expression, perchè in questo particolare contesto vogliamo che la stringa corrispondente all'attributo 'd' sia consumata man mano che si trova la corrispondenza tra il modello e la prima parte della stringa. Ogni volta che si verifica l'abbinamento, la parte iniziale corrispondente della stringa viene cancellata e la procedura viene iterata fino a consumare tutta la stringa.
  2. Il file SVG sorgente usa solo i codici path M C e L maiuscoli e z minuscolo, per cui la ricerca della lettera iniziale si limita a queste consonanti. Per inciso la lettera z rappresenta il codice di chiusura della poligonale sul punto iniziale.
  3. In questa fase, e sempre in modo temporaneo, creiamo un nuovo file di appoggio, denominato tk_out.txt, in cui scriviamo le stringhe risultanti dalla rilettura del file out_file.txt che viene generato all'inizio. Le stringhe contengono elementi separati da ';' per maggiore agio nella successiva suddivisione. Tutto questo, bene inteso, fino alla stesura finale del codice.

Ci stiamo progressivamente avvicinando al momento della traduzione. Portate pazienza ancora un po'

Tuesday, February 25, 2014

Dal file SVG all'istruzione create_polygon in Tkinter. Uso delle espressioni regolari

Non c'è strumento più importante, per la manipolazione di testi, delle espressioni regolari. Costituiscono uno strumento scomodo, per la loro scarsa leggibilità, ma nello stesso tempo potente perchè consentono di leggere stringhe di testo in cui la disposizione e il numero di caratteri puo' essere estremamente variabile, ma presenta delle regolarità di modello, o pattern.

Prendiamo, per esempio, una stringa in cui compaiono il cognome e il nome di un certo numero di persone, una per stringa. Supponiamo che il software che ha generato le stringhe abbia assegnato caratteri tutti maiuscoli al cognome e solo l'iniziale maiuscola al nome.

Il pattern, scritto in forma verbosa, sarà pertanto il seguente:

Leggi da inizio riga una o più stringhe formate da soli caratteri maiuscoli, eventualmente intervallati e seguiti da spazi.

Leggi quindi una o più stringhe formate da 1 carattere alfabetico maiuscolo seguito da un certo numero di caratteri minuscoli e spazi.

Il pattern così descritto potrà essere tradotto in un'espressione regolare, che potremo cimentare contro ognuna delle stringhe formanti il cognome e nome delle persone in elenco.

In Python si usano pattern identici a quelli usati in linguaggio Perl e simili a quelli che troviamo in quasi tutti i linguaggi di programmazione, da C++ a Java, perfino a VBA per Excel.

Il pattern sopra descritto puo' essere reso dalla seguente espressione regolare:

([A-Z]+[\s]+)+([A-Z]{1}[a-z]+[\s]?)+

che, ovviamente, è pressochè illeggibile.

Non c'è alternativa allo studio analitico dei simboli utilizzati nelle espressioni regolari. Un buon tester/editor per collaudare le vostre regexp è il seguente: pythex

Con il suo aiuto, e con molta pazienza, potete voi stessi comporre espressioni regolari anche molto complesse e sperimentarle su stringhe test.


Veniamo alla traduzione SVG->Tkinter. Qualche post fa abbiamo estratto dai file SVG un intero set di stringhe di coordinate associate al tag <path> come attributi. Ora si tratta di ripescarle dai file (più avanti elimineremo il passaggio intermedio, costruendo un parser) e convertirle in istruzioni datate di senso nell'equivalente Tkinter di che è create_polygon.

Come ricorderete, ho salvato le stringhe in gruppi di tre, 'id','style' e 'd': la prima stringa è l'attribuzione identificativa fatta dal software grafico per riconoscere, nel blocco path, a cosa si riferisce il blocco stesso. Poi c'è la stringa style, che rappresenta il complesso delle informazioni di formato che permettono di personalizzare la poligonale. Infine la stringa d contiene le coordinate di percorso. Non abbiamo incertezze sul fatto che i tre gruppi di informazioni esistano tutti, per cui possiamo procedere.

Una piccola modifica alle stringhe di ricerca mi permette di catturare un'informazione utile per il futuro dimensionamento dell'immagine. Cerco di intercettare gli attrib che identificano la larghezza (width) e l'altezza (height) dell'immagine. Trattandosi di informazioni generali, verranno intercettate molto presto dalla procedura ricorsiva e inserite nel file in uscita per prime.

Il programma seguente, molto preliminare e non necessariamente funzionante in tutti i contesti e con tutti i file SVG, ricerca ricorsivamente gli attributi corrispondenti alle variabili di interesse e le salva in un file in modo da poter essere rilette con molta tranquillità in una fase successiva. Il salvataggio dei dati in un file non è un passaggio obbligatorio, ma ci aiuta a costruire il programma intercettando gli errori.

Per le prove ho utilizzato il file SVG: EU-Italy.svg.

Di seguito invece il programma che sto elaborando per la manipolazione di questo file.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#|/usr/bin/env python
import re

class SVG2Tk:

    import xml.etree.ElementTree as ET
    
    def __init__(self, file_s, file_out):

        self.file_s = open(file_s, 'r')
        self.file_out= open(file_out,'w')

        tree = SVG2Tk.ET.parse(file_s)
        root = tree.getroot()
        self.xmlrecur(root)
        self.file_out.close()
        
        file_in = open(file_out)
        try:
            width=file_in.next()
            height=file_in.next()
            print 'width %sheight %s' % (width, height)
        except Exception:
            print Exception

        while True:
            
            try:
                id_ = file_in.next()
                style = file_in.next()
                d = file_in.next()

                pattern1 = r'([M|m]{1}[\s]*)([\d]*[\.]?[\d]*[\,]?[\d]*[\.]?[\d]*)'
                matchObj = re.search(pattern1, d)
                if matchObj:
                    print matchObj.groups()
                pattern2 = r'([L|l]{1}[\s]*)([\d]*[\.]?[\d]*[\,]{1}[\d]*[\.]?[\d]*)'
                for matchOb2 in re.finditer(pattern2, d):
                        print matchOb2.groups()
                pattern3 = r'([C|c]{1}[\s]*)(([\d]*[\.]?[\d]*[\,]{1}[\d]*[\.]?[\d]*[\s]+){3})'
                matchOb3 = re.search(pattern3, d)
                if matchOb3:
                    print matchOb3.group(1,2)
                    
            except Exception:
                print 'exception'
                break
                

    def xmlrecur(self,x):
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                    self.file_out.write(i.attrib['id'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['style'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['d'])
                    self.file_out.write('\n')
            elif set(['width','height']).issubset(i.attrib):
                self.file_out.write(i.attrib['width'])
                self.file_out.write('\n')
                self.file_out.write(i.attrib['height'])
                self.file_out.write('\n')
                
            else:
                self.xmlrecur(i)

if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg','out_xml.txt')

Il codice presenta alcune modalità di utilizzo delle espressioni regolari in Python:

  • La libreria di riferimento è re, che viene importata in testa al programma
  • con la opzione re.search vengono cimentati dei pattern con le stringhe che, con metodo iterativo, sono estratte dal file salvato.
  • La variabile matchObj e analoghe (nomi di fantasia, come sempre) sono oggetti delle libreria re, non sono utilizzabili in modo diretto. I metodi group() e groups() servono per estrarre le sottostringhe corrispondenti ai pattern. Al momento viene fatta esclusivamente una stampa su console dei risultati, come si vede dalla stampa seguente.
Python 2.7.5+ (default, Sep 19 2013, 13:48:49) 
[GCC 4.8.1] on linux2
Type "copyright", "credits" or "license()" for more information.
==== No Subprocess ====
>>> 
width 207036
height 173880

('M ', '168720,114651')
('C ', '170405.84,114660.61 171877.76,115218.21 173517.99,115465.97\n')
('M ', '790.46875,0.875')
('L ', '426.9375,0.90625')
('L ', '426.46875,1.5')
('L ', '423.84375,2.25')
('L ', '418.375,7')
('L ', '416.65625,8.28125')
('L ', '414.53125,8.625')
('L ', '413,6.90625')
('L ', '411.75,4.5')
('L ', '410.96875,2.15625')
('L ', '409.15625,1.5625')
('L ', '408.1875,1.96875')
('L ', '406.875,3.96875')
('L ', '404.25,4.96875')
('L ', '402.25,7.15625')
('L ', '401.375,9.875')
('L ', '400.09375,11.1875')
('L ', '398.46875,11.3125')
('L ', '397.09375,10.53125')
('L ', '395.6875,10.90625')
('L ', '395.34375,12.28125')
('L ', '396,13.25')
('L ', '396.125,14.875')
('L ', '395.125,15.75')
('L ', '390,15.4375')
('L ', '388.65625,14.65625')
('L ', '387.03125,14.3125')
('L ', '385.09375,15.5625')
('L ', '383.5,18.71875')
('L ', '380.96875,21.5625')
('L ', '380.75,25.03125')
('L ', '384.3125,31.5')
('L ', '386.3125,33.03125')
('L ', '389.3125,33.65625')
('L ', '391.4375,33.34375')
('L ', '396.53125,30.65625')
('L ', '400.21875,31.34375')

In un prossimo post faro' un'ulteriore elaborazione del codice, eliminando il file di appoggio e iniziando la traduzione in codice Tkinter. Come penso possiate apprezzare, la combinazione di strumenti raffinati come le librerie di Python consente, con pochissimo codice, di affrontare problemi di notevole complessità. Sono interessato a qualche vostro commento, anche per migliorare il codice e pulirlo un po' dalla grezzezza del mio stile. A presto.

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...