Inauguramos sección nueva en el 2014 con las preguntas que nos llegan de nuestros lectores por las redes sociales o el correo electrónico 🙂 Nos llegan unas cuantas, ¡así que tenemos material para al menos una pregunta semanal! Estas serán entradas breves que publicaremos cada martes y que tratarán de responder vuestras dudas sin rodeos. ¡Si queréis mandar las vuestras no dudéis en contactar con nosotros!
Empezamos con Alberto, que me comenta:
entre la línea de estabilidad funcional (línea roja) pero solo rellena con color, no sobreescribe que es lo que pretendo. ¿Se te ocurre alguna forma?
¿Cómo puedo «borrar» lo que tengo por encima de una línea en matplotlib? He intentado con
fill_between
entre la línea de estabilidad funcional (línea roja) pero solo rellena con color, no sobreescribe que es lo que pretendo. ¿Se te ocurre alguna forma?

Alberto está escribiendo un programa para dibujar mapas de actuaciones de turbomáquinas en Python, similares a los que producen programas privativos como GSP (ejemplos) o GasTurb (ejemplos). En esos mapas aparece la línea de estabilidad funcional (surge line o stall line) por encima de la cual la turbomáquina no puede funcionar. Es preciso, por tanto, borrar todo lo que quede por encima de ella para suprimir información innecesaria del gráfico. El código es un poco complicado, así que voy a comentar solo los conceptos fundamentales.
Lo primero que hice (después de admitir que no tenía ni idea) fue intentar trabajar sobre lo que ya había intentado. Efectivamente, si usas la función fill_between
el relleno se queda «por debajo» de las líneas que ya había, en lugar de taparlas. Consultando la documentación de fill_between
vi que admitía un parámetro zorder, que controla la visibilidad de los elementos de la gráfica: por defecto vale 0, y cuanto mayor es más arriba aparece el elemento. Usando un valor lo suficientemente alto se llega a este resultado:

Que es más o menos lo que se pretendía… pero en mi ordenador se vio el detalle fatal: el color de fondo y la rejilla quedan tapados. Esta solución no es suficiente.
A continuación me puse a pensar en si habría alguna manera de calcular la intersección de esas líneas con matplotlib. Ya Kiko escribió un artículo que utilizaba Shapely para calcular intersecciones entre formas geométricas, así que tenía un punto de partida, pero introducir Shapely para resolver algo tan aparentemente simple no me gustaba.
Otra opción era crear funciones interpolantes usando SciPy y calcular intersecciones entre funciones usando cualquiera de los métodos de optimización disponibles. El problema es que las curvas negras de la figura, que se obtienen con la función contour, no se pueden trasformar en una función tan fácilmente y eso me causaría problemas.
Estaba ya desempolvando mi ejemplar de «Computational Geometry: Algorithms and Applications» cuando se me ocurrió que tal vez matplotlib tuviese el concepto de máscaras, es decir, poder utilizar una forma geométrica para enmascarar otra, de la misma forma que usamos máscaras en arrays de NumPy. Y efectivamente, después de un rato buscando en Google encontré justo lo que buscaba: el método set_clip_path.
La palabra clave aquí era clip, que en inglés quiere decir algo así como «recortar»: hay que pasarle al método la forma que queremos usar para recortar la línea. En este caso, podemos extraer el área que resulta de fill_between
(hacia abajo esta vez).
Si queremos además incluir etiquetas para las curvas de nivel, podemos definir manualmente su posición para que no queden fuera de la máscara. Esto se hace con el parámetro manual
de la función clabel
.
El código quedaría así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from matplotlib.patches import PathPatch # Relleno desde la línea hacia abajo fillb = plt.fill_between(surge_line_x, surge_line_y, color='none') # Extraemos el Path path, = fillb.get_paths() # Lo convertimos en un Patch mask = PathPatch(path, fc='none') # Y lo añadimos a la figura plt.gca().add_patch(mask) # Lo aplicamos a las curvas de nivel cs = plt.contour(cx, cy, cz, colors="black") for contour in cs.collections: cs.set_clip_path(mask) # Posicionamos las etiquetas labels_xy = [(10.8, 2.0), (12.4, 2.7), (12.4, 3.3)] plt.clabel(cs, fmt='%1.2f', manual=labels_xy) # Y aplicamos la máscara a las líneas normales ll, = plt.plot(lx, ly, color="blue") ll.set_clip_path(mask) |
Y este sería el resultado:

¡Ahora sí! 🙂
¡Y hasta aquí la pregunta de la semana! ¿Qué te ha parecido el método para llegar a la solución? ¿Se te ocurre una manera mejor? ¿Crees que te será útil para algo que estás haciendo ahora mismo? ¡Cuéntanos en los comentarios! Y si quieres mandarnos tu pregunta, ya sabes dónde estamos 😉
Vaya, vaya… y parecía inocente el problemita… me lo apunto, porque seguro que en algún momento me hace falta algo así.
Por cierto, no conocía Shapely y la verdad que me hubiese venido muy bien en una cosa que hice (tuve que acabar construyendo unos algoritmos caseros y malos…
¡Al final es una tontería, pero matplotlib todavía me intimida bastante!
¿Qué era lo que tenías que hacer, por curiosidad?
Te acuerdas de las partículas de hielo que chocaban contra el perfil? pues al final, me tuve que hacer un algoritmo para hallar los puntos de corte y parar la integración. Funcionaba bastante bien, pero si hubiese tenido esto…
Ya recuerdo. Tal vez habría ayudado algo… eso para la versión 2.0 🙂
Simplemente, te has pasado Juanlu :D. ¡Menudo curro! He implementado la solución ahora mismo en mi código, el resultado no podía haber quedado más profesional, nada que envidiar a las gráficas de Gasturb :). ¡Mil gracias!
¡Me alegro de que hayas quedado satisfecho! 😛 Ha sido una pregunta muy interesante, me ha gustado darle vueltas al coco (¡aunque en el fondo era una tontería!). Un saludo y quedamos pendientes de futuras actualizaciones 😉
Bueno, tanto como tontería… Matplotlib tiene una estructura de objetos bastante interesante, y se nota que está muy bien construida; con el conocimiento suficiente ¡puede hacerse prácticamente cualquier cosa! El problema es que es inmensa, y claro, convertirse en un auténtico gurú de la biblioteca puede llevar mucho, mucho tiempo jaja
¡Ah! Dos despistes, que se me ha olvidado comentarte. Las líneas:
8 mask = PathPatch(path, fc=’none’)
15 contour.set_clip_path(mask)
Visto 😉 ¡Gracias!