2012-10-02 16 views
9

aggiornamento finaleCython e Fortran - come compilare insieme senza f2py

Questa domanda è su come scrivere un setup.py che compilerà un modulo Cython che accede codice FORTRAN direttamente, come il C avrebbe fatto. È stato un viaggio piuttosto lungo e arduo verso la soluzione, ma il disordine completo è incluso di seguito per il contesto.

domanda iniziale

Ho un prolungamento che è un file Cython, che imposta un po 'di memoria heap e lo passa al codice Fortran, e un file FORTRAN, che è un vecchio modulo veneranda che ho' Mi piacerebbe evitare di reimplementare se posso.

Il file .pyx compila bene a C, ma il compilatore Cython soffoca sul file .f90 con il seguente errore:

$ python setup.py build_ext --inplace 
running build_ext 
cythoning delaunay/__init__.pyx to delaunay/__init__.c 
building 'delaunay' extension 
error: unknown file type '.f90' (from 'delaunay/stripack.f90') 

Ecco (la metà superiore della) mia file di installazione:

from distutils.core import setup, Extension 
from Cython.Distutils import build_ext 

ext_modules = [ 
    Extension("delaunay", 
    sources=["delaunay/__init__.pyx", 
      "delaunay/stripack.f90"]) 
] 

setup(
    cmdclass = {'build_ext': build_ext}, 
    ext_modules = ext_modules, 
    ... 
) 

NOTA: originariamente la posizione del file di Fortran è stata specificata in modo errato (senza il prefisso della directory) ma ciò si interrompe esattamente nello stesso modo dopo averlo corretto.

Le cose che ho provato:

ho trovato this, e ha cercato passando il nome del compilatore fortran (cioè gfortran) come questo:

$ python setup.py config --fcompiler=gfortran build_ext --inplace 
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] 
    or: setup.py --help [cmd1 cmd2 ...] 
    or: setup.py --help-commands 
    or: setup.py cmd --help 

error: option --fcompiler not recognized 

E ho anche provato rimuovere --inplace, nel caso in cui quello era il problema (non lo era, come il messaggio di errore in alto).

Quindi, come si compila questo fortran? Posso hackerarlo in un .o e farla franca collegandola? O is this a bug in Cython, che mi obbligherà a reimplementare le distutils o hackerare con il preprocessore?

UPDATE

Così, dopo aver verificato i numpy.distutils pacchetti, capisco il problema un po 'più. Sembra che si deve

  1. Usa Cython per convertire i file .pyx per CPython file .c,
  2. quindi utilizzare un combinazione Extension/setup() che supporta FORTRAN, come numpy 's.

Dopo aver provato questo, il mio setup.py ora assomiglia a questo:

from numpy.distutils.core import setup 
from Cython.Build import cythonize 
from numpy.distutils.extension import Extension 

cy_modules = cythonize('delaunay/sphere.pyx') 
e = cy_modules[0] 

ext_modules = [ 
    Extension("delaunay.sphere", 
     sources=e.sources + ['delaunay/stripack.f90']) 
] 

setup(
    ext_modules = ext_modules, 
    name="delaunay", 
    ... 
) 

(notare che ho anche ristrutturato il modulo un po ', dal momento che apparentemente un __init__.pyx è annullato ...)

Ora è dove le cose diventano bacate e dipendenti dalla piattaforma. Ho due sistemi di test disponibili: un Mac OS X 10.6 (Snow Leopard), usando Macports Python 2.7 e un Mac OS X 10.7 (Lion) usando il sistema python 2.7.

su Snow Leopard, vale quanto segue:

Ciò significa che la compilazione del modulo (evviva!) (Anche se non c'è --inplace per NumPy, sembra, così ho dovuto livello di sistema installare il modulo di prova: /) ma ancora ottengo un incidente sul import come segue:

>>> import delaunay 
    Traceback (most recent call last): 
    File "<input>", line 1, in <module> 
    File "<snip>site-packages/delaunay/__init__.py", line 1, in <module> 
     from sphere import delaunay_mesh 
    ImportError: dlopen(<snip>site-packages/delaunay/sphere.so, 2): no suitable image found. Did find: 
    <snip>site-packages/delaunay/sphere.so: mach-o, but wrong architecture 

e su Lion, ottengo un errore di compilazione, seguendo una linea di compilazione cercando piuttosto confusa:

gfortran:f77: build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.f 
/usr/local/bin/gfortran -Wall -arch i686 -arch x86_64 -Wall -undefined dynamic_lookup -bundle build/temp.macosx-10.7-intel-2.7/delaunay/sphere.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/fortranobject.o build/temp.macosx-10.7-intel-2.7/delaunay/stripack.o build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.o -lgfortran -o build/lib.macosx-10.7-intel-2.7/delaunay/sphere.so 
ld: duplicate symbol _initsphere in build/temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o ldand :build /temp.macosx-10.7-intelduplicate- 2.7symbol/ delaunay/sphere.o _initsphere in forbuild architecture /i386 
temp.macosx-10.7-intel-2.7/build/src.macosx-10.7-intel-2.7/delaunay/spheremodule.o and build/temp.macosx-10.7-intel-2.7/delaunay/sphere.o for architecture x86_64 

Ora facciamo un passo indietro prima di esaminare i dettagli qui. In primo luogo, so che ci sono un sacco di grattacapi sulle architetture in Mac OS X a 64 bit; Ho dovuto lavorare molto duramente per far funzionare Macports Python sulla macchina Snow Leopard (solo per l'aggiornamento da system python 2.6). So anche che quando vedi gfortran -arch i686 -arch x86_64 stai inviando messaggi misti al tuo compilatore. Ci sono tutti i tipi di problemi specifici della piattaforma sepolti lì, che non dobbiamo preoccuparci nel contesto di questa domanda.

Ma facciamo solo un'occhiata a questa linea: gfortran:f77: build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.f

Qual è numpy facendo ?! Non ho bisogno di funzioni f2py in questa build! In realtà ho scritto un modulo cython per evitare che affronta la follia di f2py (ho bisogno di avere 4 o 5 variabili di output, oltre ad argomenti senza-in-e-out - nessuno dei quali è ben supportato in f2py.) I voglio solo compilare .c ->.o e .f90 ->.o e collegarli. Potrei scrivere questa riga del compilatore da solo se sapessi come includere tutte le intestazioni pertinenti.

Per favore dimmi che non ho bisogno di scrivere il mio makefile per questo ... o che c'è un modo per tradurre fortran in (output compatibile) C così posso solo evitare che python veda l'estensione .f90 (che risolve l'intero problema). Nota che f2c non è adatto per questo, poiché funziona solo su F77 e questo è un dialetto più moderno (da cui l'estensione del file .f90).

UPDATE 2 Il seguente script bash sarà lieto di compilare e collegare il codice a posto:

PYTHON_H_LOCATION="/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/" 

cython sphere.pyx 

gcc -arch x86_64 -c sphere.c -I$PYTHON_H_LOCATION 
gfortran -arch x86_64 -c stripack.f90 
gfortran -arch x86_64 -bundle -undefined dynamic_lookup -L/opt/local/lib *.o -o sphere.so 

Qualche consiglio su come fare questo tipo di hack compatibile con una setup.py? Non tutti l'installazione di questo modulo per andare trovare Python.h manualmente ...

+1

Il supporto Fortran sembra venire da 'numpy'. Forse puoi importare ['numpy.distutils.extension.Extension'] (http://www.scipy.org/doc/numpy_api_docs/numpy.distutils.extension.html) invece di' distutils.core.Extension'. – MvG

+0

Leggere anche la [Guida per gli utenti di NumPy Distutils] (https://github.com/numpy/numpy/blob/master/doc/DISTUTILS.rst.txt) e il [riferimento alla confezione] (http: //docs.scipy. org/doc/NumPy-1.6.0/riferimento/distutils.html). – MvG

+0

Avete installato gfortran? Hai veramente bisogno di un compilatore Fortran, è davvero necessario. –

risposta

3

UPDATE: Ho creato un progetto su GitHub che avvolge questa generazione di linee di compilazione a mano. si chiama complicated_build.

UPDATE 2: infatti, "Generazione a mano" è una pessima idea, come è specifica piattaforma — il progetto ora legge i valori dal modulo distutils.sysconfig, che è le impostazioni utilizzate per compilare python (vale a dire esattamente ciò che vogliamo,) l'unica impostazione che è indovinata è il compilatore fortran e le estensioni dei file (che sono configurabili dall'utente). Sospetto che ora stia reimplementando un bel po 'di distutils!


Il modo per farlo è quello di scrivere le proprie linee compilatore, e incidere nel vostro setup.py. Mi mostrano un esempio di sotto del quale lavora per il mio caso (molto semplice), che ha il seguente strucutre:

  • importazioni
  • cythonize() qualsiasi .pyx file, in modo da avere solo i file FORTRAN e C.
  • definire una funzione build() che compila il tuo codice:
    • forse alcune costanti facili da cambiare, come i nomi del compilatore e l'architettura
    • elenco dei file FORTRAN e C
    • generare i comandi di shell che costruiranno i moduli
    • aggiungere la riga del linker
    • eseguire i comandi della shell.
  • se il comando era install e la destinazione non esiste ancora, compila.
  • eseguire il programma di installazione (che creerà le sezioni Python pure)
  • se il comando era build, eseguire la generazione ora.

la mia implementazione di questo è mostrato di seguito. È progettato per un solo modulo di estensione e ricompila ogni volta tutti i file, quindi potrebbe richiedere un'ulteriore estensione per un uso più generale. Si noti inoltre che ho codificato in modo vario vari Unix / s, quindi se si sta effettuando il porting su Windows assicuratevi di adattare o sostituire con os.path.sep.

from distutils.core import setup 
from distutils.sysconfig import get_python_inc 
from Cython.Build import cythonize 
import sys, os, shutil 

cythonize('delaunay/sphere.pyx') 

target = 'build/lib/delaunay/sphere.so' 

def build(): 
    fortran_compiler = 'gfortran' 
    c_compiler = 'gcc' 
    architecture = 'x86_64' 
    python_h_location = get_python_inc() 
    build_temp = 'build/custom_temp' 
    global target 

    try: 
    shutil.rmtree(build_temp) 
    except OSError: 
    pass 

    os.makedirs(build_temp) # if you get an error here, please ensure the build/ ... 
    # folder is writable by this user. 

    c_files = ['delaunay/sphere.c'] 
    fortran_files = ['delaunay/stripack.f90'] 

    c_compile_commands = [] 

    for cf in c_files: 
    # use the path (sans /s), without the extension, as the object file name: 
    components = os.path.split(cf) 
    name = components[0].replace('/', '') + '.'.join(components[1].split('.')[:-1]) 
    c_compile_commands.append(
     c_compiler + ' -arch ' + architecture + ' -I' + python_h_location + ' -o ' + 
     build_temp + '/' + name + '.o -c ' + cf 
    ) 

    fortran_compile_commands = [] 

    for ff in fortran_files: 
    # prefix with f in case of name collisions with c files: 
    components = os.path.split(ff) 
    name = components[0].replace('/', '') + 'f' + '.'.join(components[1].split('.')[:-1]) 
    fortran_compile_commands.append(
     fortran_compiler + ' -arch ' + architecture + ' -o ' + build_temp + 
     '/' + name + '.o -c ' + ff 
    ) 

    commands = c_compile_commands + fortran_compile_commands + [ 
    fortran_compiler + ' -arch ' + architecture + 
    ' -bundle -undefined dynamic_lookup ' + build_temp + '/*.o -o ' + target 
    ] 

    for c in commands: 
    os.system(c) 


if 'install' in sys.argv and not os.path.exists(target): 
    try: 
    os.makedirs('build/lib/delaunay') 
    except OSError: 
    # we don't care if the containing folder already exists. 
    pass 
    build() 

setup(
    name="delaunay", 
    version="0.1", 
    ... 
    packages=["delaunay"] 
) 

if 'build' in sys.argv: 
    build() 

Questo potrebbe essere avvolto in un nuovo Extension classe di Credo, con il proprio build_ext comando - un esercizio per lo studente avanzato;)

2

sufficiente compilare ed installare la libreria Fortran d'epoca fuori di Python, quindi collegarlo a distutils. La tua domanda indica che non intendi moderare con questa libreria, quindi probabilmente eseguirai una installazione una volta per tutte (usando le istruzioni di installazione e installazione della libreria). Quindi collegare l'estensione Python alla libreria esterno installato:

ext_modules = [ 
    Extension("delaunay", 
       sources = ["delaunay/__init__.pyx"], 
       libraries = ["delaunay"]) 
] 

Questo approccio è anche sicuro per il caso che ci si rende conto che è necessario wrapper per altre lingue, oltre, come Matlab, Octave, IDL, ...

Aggiornamento

ad un certo punto, se si finisce con più di un paio di tali librerie esterne che si desidera per avvolgere, è vantaggioso aggiungere un sistema di compilazione di livello superiore che consente di installare tutte queste librerie, e gestisce anche la costruzione di tutti i wrapper. Io ho cmake per questo scopo, che è ottimo per gestire build e installazioni a livello di sistema. Tuttavia, non è possibile creare roba di Python, ma è possibile impostare per chiamare "python setup.py install" in ogni sottodirectory python, richiamando così le distutils.Così il processo di costruzione complessivo è simile al seguente:

mkdir build 
cd build 
cmake .. 
make 
make install 
make python 
(make octave) 
(make matlab) 

E 'molto importante per sempre separato nucleo codice della libreria da wrapper per specifiche lingue front-end (anche per i vostri progetti!), In quanto tendono a cambiare piuttosto rapidamente . Nell'esempio di numpy si può vedere cosa succede altrimenti: invece di scrivere una libreria C generica libndarray.so e creare thin wrappers per Python, ci sono le chiamate API Python C ovunque nelle sorgenti. Questo è ciò che sta trattenendo Pypy come una seria alternativa a CPython, dal momento che per ottenere numpy devono supportare ogni ultimo bit dell'API CPython, cosa che non possono fare, dato che hanno un compilatore just-in-time e un diverso garbage collector. Ciò significa che stiamo perdendo molti potenziali miglioramenti.

Bottom line:

  1. costruire general purpose librerie Fortran/C separatamente e installarli a livello di sistema.

  2. Avere un passo di compilazione separato per i wrapper, che dovrebbe essere il più leggero possibile, in modo che sia facile adattarsi per il prossimo grande linguaggio X che si presenta. Se esiste una sola ipotesi sicura, è che X supporterà il collegamento con le librerie C.

+0

questa è una soluzione molto elegante - ben fatto! Vedrò se riesco a farlo funzionare e cambiare la risposta "corretta" se posso. – tehwalrus

0

È possibile costruire il file oggetto al di fuori di distutils poi includerlo nella fase di collegamento utilizzando l'argomento extra_objects al costruttore di estensione. In setup.py:

... 
e = Extension(..., extra_objects = ['holycode.o']) 
... 

sul richiamo di ordine:

# gfortran -c -fPIC holycode.f 
# ./setup.py build_ext ... 

Con un solo oggetto esterno, questo sarà il modo più semplice per molti.

Problemi correlati