Cython y fortran – cómo comstackr juntos sin f2py

ACTUALIZACIÓN FINAL

Esta pregunta es sobre cómo escribir un setup.py que comstackrá un módulo cython que accede directamente al código FORTRAN, como lo haría C. Fue un viaje bastante largo y arduo hacia la solución, pero el lío completo se incluye a continuación para el contexto.

PREGUNTA ORIGINAL

Tengo una extensión que es un archivo de Cython, que configura algo de memoria del montón y lo pasa al código de Fortran, y un archivo de Fortran, que es un módulo antiguo venerable que me gustaría evitar volver a implementar si puedo.

El archivo .pyx comstack bien en C, pero el comstackdor de cython se ahoga en el archivo .f90 con el siguiente error:

 $ 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') 

Aquí está (la mitad superior de) mi archivo de configuración:

 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: Originalmente, la ubicación del archivo fortran se especificó de forma incorrecta (sin el prefijo de directorio), pero esto se interrumpe exactamente de la misma manera después de que lo solucioné.

Cosas que he probado:

Encontré esto , e intenté pasar el nombre del comstackdor fortran (es decir, gfortran) como este:

 $ 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 

Y también he intentado eliminar – en el --inplace , en caso de que ese fuera el problema (no lo era, igual que el mensaje de error superior).

Entonces, ¿cómo compilo este fortran? ¿Puedo hackearlo en un .o y .o con la vinculación? ¿O es esto un error en Cython , que me obligará a volver a implementar distutils o hackear con el preprocesador?

ACTUALIZAR

Por lo tanto, después de revisar los paquetes numpy.distutils , entiendo el problema un poco más. Parece que tienes que

  1. Use cython para convertir los archivos .pyx en archivos .py de cpython,
  2. Luego use una combinación de Extension / setup() que admita fortran, como numpy ‘s.

Habiendo probado esto, mi setup.py ahora se ve así:

 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", ... ) 

(tenga en cuenta que también he reestructurado un poco el módulo, ya que aparentemente un __init__.pyx no está permitido …)

Ahora es donde las cosas se vuelven buggy y dependen de la plataforma. Tengo dos sistemas de prueba disponibles: uno Mac OS X 10.6 (Snow Leopard), usando Macports Python 2.7, y uno Mac OS X 10.7 (Lion) utilizando el sistema python 2.7.

En Snow Leopard, se aplica lo siguiente:

Esto significa que el módulo se comstack (¡hurra!) (Aunque no hay --inplace para numpy, parece, así que tuve que instalar el módulo de prueba en todo el sistema: /) pero todavía tengo un fallo al import siguiente manera:

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

y en Lion, recibo un error de comstackción, siguiendo una línea de comstackción de aspecto bastante confuso:

 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 

Ahora retrocedamos un momento antes de examinar detenidamente los detalles aquí. En primer lugar, sé que hay muchos problemas con los conflictos de architecture en Mac OS X de 64 bits; Tuve que trabajar mucho para que Macports Python trabajara en la máquina Snow Leopard (solo para actualizar desde el sistema Python 2.6). También sé que cuando ves gfortran -arch i686 -arch x86_64 estás enviando mensajes mezclados a tu comstackdor. Hay todo tipo de problemas específicos de la plataforma enterrados allí, de los que no tenemos que preocuparnos en el contexto de esta pregunta.

Pero veamos esta línea : gfortran:f77: build/src.macosx-10.7-intel-2.7/delaunay/sphere-f2pywrappers.f

¿Qué está haciendo numpy? ¡No necesito ninguna característica f2py en esta comstackción! De hecho, escribí un módulo de cython para evitar lidiar con la locura de f2py (necesito tener 4 o 5 variables de salida, así como tampoco argumentos in-nor-out, ninguno de los cuales está bien soportado en f2py). Solo quiero para comstackr .c -> .o , y .f90 -> .o y vincularlos. Podría escribir esta línea del comstackdor si supiera cómo incluir todos los encabezados relevantes.

Por favor, dime que no necesito escribir mi propio makefile para esto … o que hay una manera de traducir fortran a (salida compatible) C, así puedo evitar que Python vea la extensión .f90 (que corrige todo el problema). problema.) Tenga en cuenta que f2c no es adecuado para esto, ya que solo funciona en F77 y este es un dialecto más moderno (de ahí la extensión de archivo .f90 ).

ACTUALIZACIÓN 2 La siguiente secuencia de comandos bash comstackrá y vinculará el código en su lugar:

 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 

¿Algún consejo sobre cómo hacer que este tipo de hack sea compatible con un setup.py? No tengo a nadie instalando este módulo para tener que ir a buscar Python.h manualmente …

ACTUALIZACIÓN: he creado un proyecto en github que envuelve esta generación de líneas de comstackción a mano. se llama complicada_build .

ACTUALIZACIÓN 2: de hecho, “generar a mano” es una muy mala idea ya que es específica de la plataforma: el proyecto ahora lee los valores del módulo distutils.sysconfig , que es la configuración utilizada para comstackr python (es decir, exactamente lo que queremos) la única configuración que se adivina es el comstackdor fortran y las extensiones de archivo (que son configurables por el usuario). ¡Sospecho que se está reimplementando un poco de distutils ahora!


La forma de hacerlo es escribir sus propias líneas de comstackdor y piratearlas en su setup.py . A continuación muestro un ejemplo que funciona para mi caso (muy simple), que tiene la siguiente estructura:

  • importaciones
  • cythonize() cualquier archivo .pyx , por lo que solo tiene archivos .pyx y C.
  • define una función build() que comstack tu código:
    • tal vez algunas constantes fáciles de cambiar, como los nombres de comstackdores y la architecture
    • Enumerar los archivos de Fortran y C
    • Genera los comandos de shell que construirán los módulos.
    • añadir la línea de enlace
    • Ejecutar los comandos de shell.
  • Si el comando se install y el destino aún no existe, créelo.
  • Ejecutar configuración (que construirá las secciones de python puro)
  • Si el comando fue build , ejecute el comstackción ahora.

mi implementación de esto se muestra a continuación. Está diseñado para un solo módulo de extensión y comstack todos los archivos cada vez, por lo que puede requerir una extensión adicional para un uso más general. También tenga en cuenta que he codificado varios Unix / s, así que si va a transferir esto a Windows, asegúrese de adaptarlo o reemplazarlo 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() 

Esto podría ser envuelto en una nueva clase de Extension , supongo, con su propio comando build_ext – un ejercicio para el estudiante avanzado;)

Simplemente construya e instale su biblioteca Fortran antigua fuera de Python, luego enlístela en distutils. Su pregunta indica que no tiene la intención de moderar la vida con esta biblioteca, por lo que es probable que se realice una instalación definitiva (utilizando las instrucciones de construcción e instalación de la biblioteca). Luego vincule la extensión de Python a la biblioteca externa instalada:

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

Este enfoque también es seguro en el caso de que se dé cuenta de que también necesita envoltorios para otros idiomas, como Matlab, Octave, IDL, …

Actualizar

En algún momento, si terminas con más de unas pocas bibliotecas externas que quieres envolver, es ventajoso agregar un sistema de comstackción de nivel superior que instale todas estas bibliotecas y también administre la comstackción de todos los envoltorios. Tengo este propósito para este propósito, que es excelente para manejar las comstackciones e instalaciones de todo el sistema. Sin embargo, no puede construir cosas de Python fuera de la caja, pero se le puede enseñar fácilmente a llamar a “python setup.py instalar” en cada subdirectorio de python , invocando así distutils. Así que el proceso de construcción general se ve así:

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

Es muy importante separar siempre el código de la biblioteca central de los envoltorios para lenguajes front-end específicos (¡también para sus propios proyectos!), Ya que tienden a cambiar bastante rápido. Lo que sucede de otra manera se puede ver en el ejemplo de numpy : en lugar de escribir una biblioteca de C de propósito general libndarray.so y crear envolturas delgadas para Python, hay fonts de la API de Python C en todas partes en las fonts. Esto es lo que ahora retiene a Pypy como una seria alternativa al CPython, ya que para obtener el numpy tienen que soportar hasta el último bit de la API de CPython, lo que no pueden hacer, ya que tienen un comstackdor justo a tiempo. y un recolector de basura diferente. Esto significa que nos estamos perdiendo muchas mejoras potenciales.

Línea de fondo:

  1. Cree bibliotecas Fortran / C de propósito general por separado e instálelas en todo el sistema.

  2. Tenga un paso de comstackción separado para los envoltorios, que debe mantenerse lo más liviano posible, de modo que sea fácil de adaptar para el próximo gran lenguaje X que se produce. Si hay una suposición segura, es que X admitirá la vinculación con bibliotecas de C.

Puede comstackr el archivo de objeto fuera de los distutils luego incluirlo en el paso de vinculación mediante el argumento extra_objects al constructor de Extension. En setup.py :

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

En el símbolo del sistema:

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

Con un solo objeto externo, esta será la forma más fácil para muchos.