Ir al contenido principal

Escritura técnica: usando matemáticas

En una publicación anterior mencioné algunos aspectos generales de la escritura técnica. En este me gustaría hablar sobre la inclusión de expresiones matemáticas en documentos técnicos.

Hay dos formas principales de incluir matemáticas en documentos:

  • utilizando texto; y

  • utilizando una interfaz gráfica.

Usar una interfaz gráfica, como el editor de ecuaciones en LibreOffice Writer o MS Word, o MathType es conveniente. No es necesario memorizar nada y se pueden mirar las expresiones mientras se crean. Sin embargo, puede ser algo lento en comparación con el uso de entrada de texto —una vez que se está cómodo con la sintaxis—.

Hay dos tipos de ecuaciones utilizadas en Internet:

  • MathML es un estándar W3C para ecuaciones y está incluido en HTML5, por lo que funcionaría en todos los navegadores modernos. El problema con este es que no está diseñado para ser escrito a mano. Por tanto, uno puede usarlo si tiene alguna forma automática de generar el código.

  • LaTeX es mi forma sugerida de escribir ecuaciones. La curva de aprendizaje podría ser un un poco empinada al principio pero vale la pena.

Una herramienta que ayuda con las ecuaciones es MathPix Snip que genera automáticamente código LaTeX o MathML a partir de una imagen, incluso una escrita a mano. Otra herramienta que es realmente útil es Detexify que permite dibujar un símbolo y proporciona la sintaxis de LaTeX de este.

En el resto de la publicación mostraré mis sugerencias para trabajar con ecuaciones en LibreOffice y MS Word. Si estás utilizando LaTeX o MarkDown/ReStructuredText para tus documentos ya estás utilizando LaTeX para tus ecuaciones.

LibreOffice

LibreOffice tiene su propio editor de ecuaciones con su propia sintaxis y funciona bien para expresiones pequeñas, pero se complica para ecuaciones grandes o largas manipulaciones algebraicas. Para LibreOffice sugeriría usar TexMaths, es fácil de usar y funciona para el procesador de textos (Writer) y presentaciones (Impress). Supongo que también funciona para hojas de cálculo (Calc), pero no recuerdo haber usado ecuaciones en una.

MS Office

MS Office también tiene su propio editor de ecuaciones, funciona bien y es fácil de usar. Sin embargo, el mismo problema aparece cuando quieres expresiones largas. Una opción es usar directamente LaTeX en Office pero prefiero usar IguanaTex. Es un complemento que permite ingresar ecuaciones de forma similar a TexMaths en LibreOffice.

También puede pegar directamente las ecuaciones MathML en MS Word (>2013 y Windows).

Usa SymPy

Independientemente de la herramienta que use para escribir sus documentos, sugiero usar un CAS (del inglés Computer Algebra System), como Mathematica o SymPy. Estos programas permiten la generación automática de LaTeX y MathML a partir de expresiones y esto facilita mucho el proceso.

Veamos un ejemplo. Supongamos que tenemos la función

\begin{equation*} f(x) = \exp(-x^2) \sin(3*x) \end{equation*}

y queremos calcular su segunda derivada

\begin{equation*} f''(x) = \left(- 12 x \cos{\left(3 x \right)} + 2 \left(2 x^{2} - 1\right) \sin{\left(3 x \right)} - 9 \sin{\left(3 x \right)}\right) e^{- x^{2}} \end{equation*}

El siguiente bloque de código nos da el código LaTex

from sympy import *
init_session()
f = exp(-x**2)*sin(3*x)
fxx = diff(f, x, 2)
print(latex(fxx))

que es

\left(- 12 x \cos{\left(3 x \right)} + 2 \left(2 x^{2} - 1\right) \sin{\left(3 x \right)} - 9 \sin{\left(3 x \right)}\right) e^{- x^{2}}

Este corresponde con el código que usé arriba para representar la ecuación.

Si quisiéramos el código MathML de esa expresión podríamos usar el siguiente fragmento de código

from sympy import *
init_session()
f = exp(-x**2)*sin(3*x)
fxx = diff(f, x, 2)
print(mathml(fxx, printer="presentation"))

observe el argumento opcional printer = "presentation". Si queremos agregar esto a MS Word, por ejemplo, podríamos agregar la salida (que no voy a mostrar porque es realmente larga) dentro del siguiente código

<math xmlns = "http://www.w3.org/1998/Math/MathML">
</math>

Cuando se usa Jupyter Notebook, esto se puede hacer gráficamente con un clic derecho sobre la expresión. Y se muestra el siguiente menú.

/images/jupyter_export_eqs.png

Referencias

  1. “How to Insert Equations in Microsoft Word.” WikiHow, https://www.wikihow.com/Insert-Equations-in-Microsoft-Word. Fecha de acceso: Agosto 3, 2020.

  2. “Copy MathML into Word to Use as Equation.” Stack Overflow, https://stackoverflow.com/questions/25430775/copy-mathml-into-word-to-use-as-equation. Fecha de acceso: Agosto 3, 2020.

  3. “Python - Output Sympy Equation to Word Using Mathml.” Stack Overflow, https://stackoverflow.com/questions/40921128/output-sympy-equation-to-word-using-mathml. Fecha de acceso: Agosto 3, 2020.

  4. OERPUB (2016), “Mathconverter”, https://github.com/oerpub/mathconverter, Fecha de acceso: Agosto 3, 2020.

Descargar videos de MS Stream

Esta semana un estudiante me preguntó acerca de descargar los videos de uno de los cursos de MS Stream. El problema es que si no eres propietario del video no puedes descargarlo. Entonces, voy a mostrar una opción para descargar videos sin ser propietario de los mismos.

Descargo de responsabilidad: puede ser una buena idea preguntar a tu organización sobre los derechos de autor de los videos.

Prerrequisitos

Vas a necesitar lo siguiente:

Instalación de destreamer

Después de obtener los requisitos previos, puedes descargar destreamer usando

$ git clone https://github.com/snobu/destreamer
$ cd destreamer
$ npm install
$ npm run build

en una terminal.

Si no se quiere jugar con variables de entorno, sugiero agregar ffmpeg a la misma carpeta en donde se encuentra destreamer.

Descarga

Después de eso, debe navegar a la carpeta donde descargaste destreamer y

$ ./destreamer.sh -i "https://web.microsoftstream.com/video/VIDEO_ID"

en Mac o Linux,

$ destreamer.cmd -i "https://web.microsoftstream.com/video/VIDEO_ID"

en el command prompt en Windows, y

$ destreamer.ps1 -i "https://web.microsoftstream.com/video/VIDEO_ID"

en PowerShell. VIDEO_ID se refiere al identificador del video en MS Stream.

Si deseas descargar varios archivos (como un curso completo), puedes crear un archivo con las URL y usar

$ destreamer.cmd -f filename.txt

Marcado Aleatorio de un Tetraedro

El día de ayer (junio 4 de 2020), Christian Howard publicó en Twitter la siguiente pregunta

Dado un tetraedro τ. Para cada cara triangular de τ, marcamos al azar uniformemente uno de sus aristas. ¿Cuál es el probabilidad de que exista una arista de τ que se marca dos veces?

Pensé un poco pero no pude encontrar la manera apropiada de hacer e conteo. Entonces, se me ocurrió un número de la nada: \(2/3\), pero no sé por qué. Así que decidí correr una simulación para verificar este número.

La respuesta correcta es \(51/81\), aproximadamente 63%. este cálculo está bien explicado en el blog de Christian con algunos gráficos interesantes (y memes).

El algoritmo

El algoritmo es bastante simple. Se enumeran las aristas de cada cara siguiendo una orientación antihooraria:

  • cara 0: arista 0, arista 1, arista 2

  • cara 1: arista 0, arista 3, arista 4

  • cara 2: arista 1, arista 5, arista 3

  • cara 3: arista 2, arista 4, arista 5

Luego, se toma cada cara y se elige un número al azar entre \((0, 1, 2)\) se marca la arista correspondiente, y se procede a la siguiente cara. Se repite este proceso muchas veces y se cuentan los casos favorables y se dividen por el número total de intentos para obtener un estimado de la probabilidad.

A continuación se muestra un código de Python con esta idea.

import numpy as np
import matplotlib.pyplot as plt

faces = np.array([
        [0, 1, 2],
        [0, 3, 4],
        [1, 5, 3],
        [2, 4, 5]])


def mark_edges():
    marked_edges = np.zeros((6), dtype=int)
    for face in faces:
        num = np.random.randint(0, 3)
        edge = face[num]
        marked_edges[edge] += 1
    return marked_edges


def comp_probs(N_min=1, N_max=5, ntrials=100):
    prob = []
    N_vals = np.logspace(N_min, N_max, ntrials, dtype=int)
    for N in N_vals:
        cont_marked = 0
        for cont in range(N):
            marked = mark_edges()
            if 2 in marked:
                cont_marked += 1
        prob.append(cont_marked/N)
    return N_vals, prob


#%% Cálculos
N_min = 1
N_max = 5
ntrials = 100
np.random.seed(seed=2)
N_vals, prob = comp_probs(N_min, N_max, ntrials)

#%% Gráficos
plt.figure(figsize=(4, 3))
plt.hlines(0.63, 10**N_min, 10**N_max, color="#3f3f3f")
plt.semilogx(N_vals, np.array(prob), "o", alpha=0.5)
plt.xlabel("Number of trials")
plt.ylabel("Estimated probability")
plt.savefig("prob_tet.svg", dpi=300, bbox_inches="tight")
plt.show()

Y podemos ver la evolución del estimado para diferente número de intentos.

Probabilidad estimada para diferente número de intentos.

Escritura técnica

Esta es la primera publicación sobre redacción técnica * de una serie que crearé durante el transcurso de este año. La escritura técnica es algo con lo que la mayoría de nosotros tenemos que lidiar en diferentes contextos. Por ejemplo, en cursos universitarios, publicaciones de investigación, documentación de software. La idea principal de la serie es mencionar algunos de los trucos que he aprendido a lo largo de los años y algunas herramientas que pueden ser útiles.

Las publicaciones futuras serán (probablemente) sobre:

  • Uso de ecuaciones;

  • Uso de figuras;

  • Uso de tablas; y

  • Gestión de referencias bibliográficas.

Esta publicación

Como se mencionó anteriormente, la escritura técnica es algo con lo que muchas personas tienen que lidiar. Esta es una habilidad que a veces se pasa por alto, pero no debería. De acuerdo con la U.S. Bureau of Labor Statistics

Los redactores técnicos preparan manuales, guías prácticas, artículos de revistas y otros documentos de respaldo para comunicar información compleja y técnica con mayor facilidad.

Y es una habilidad deseada en el lugar de trabajo. Se espera que su demanda crezca alrededor del 10% en la década actual.

Tipografía

Lo primero que debo mencionar es que escribir documentos es tipografía ya que estamos diseñando con texto (Butterick, 2019). Por lo tanto, deberíamos considerarnos tipógrafos, ya que constantemente diseñamos documentos.

Sugeriría echar un vistazo a "Butterick's Practical Typography" ya que es un libro muy bueno sobre el tema y es fácil de leer. Voy a mencionar algunos puntos importantes de acuerdo con la sección "Tipografía en diez minutos":

  1. La selección tipográfica más importante está en el cuerpo del texto. Esto se debe al hecho de que es la mayor parte del documento.

  2. Elija un tamaño de punto entre 10-12 puntos para documentos impresos y 15-25 píxeles para documentos digitales.

  3. El espacio entre líneas debe estar entre 120-145% del tamaño de la letra.

  4. La longitud de la línea debe tener entre 45 y 90 caracteres. Esto es aproximadamente 2 o 3 alfabetos en minúsculas:

    abcdefghijklmnñopqrstuvwxyzabcdefghijklmnñopqrstuvwxyzabcd

  5. Cuidado con la selección de su fuente. Intente evitar las fuentes predeterminadas como Arial, Calibri o Times New Roman.

Editores

Otro punto que quiero tocar en esta publicación es el de los editores. La primera pregunta que surge es "¿qué editor debo usar?". La respuesta corta es: usa lo que tus colegas estén usando. Ese es mi mejor consejo; de esa manera tu tienes personas con quienes hablar sobre tus dudas.

La respuesta larga ... es que cada editor tiene sus puntos débiles y fuertes. Yo ha escrito artículos científicos en LaTeX, LibreOffice Writer y MS Word; todos se ven profesionales. Entonces, al final, puedes escribir tu documentos de varias maneras y lograr un resultado similar. Prefiero usar LaTeX para documentos largos, ya que se centra en la estructura de la documento en lugar de la apariencia y esta es la forma en que uno debe administrar un documento largo como una disertación, en mi opinión.

Si solo quieres que elija un editor y te lo sugiera, diría que LibreOffice. Una buena referencia para es "Designing with LibreOffice". Una vez que aprendas cómo usar estilos, preguntarás cómo has estado escribiendo todos los documentos hasta ahora.

Hay dos grupos de editores que voy a discutir: WYSIWYG (siglas en inglés para "Lo que ves es lo que obtienes") y editores basados en marcado.

  • WYSIWYG. Esta categoría es la que la mayoría de la gente conoce. Dos ejemplos son:

    • LibreOffice writer; y

    • Microsoft Word.

  • Los editores basados en marcado dependen de las marcas en el "texto" para diferenciar secciones y estilos. En este caso, su texto parece código, como se ve en la siguiente imagen

    /images/rst_code.png

    Algunos ejemplos son:

Independientemente de cuál sea tu editor principal, sugiero utilizar Pandoc. Te permite convertir entre varios formatos, haciendo el proceso un poco más fácil. Incluso hay un editor basado completamente en él llamado Panwriter.

References

  1. Matthew Butterick (2019). Butterick's Practical Typography. Segunda edición, Matthew Butterick.

  2. Wikibooks contributors. (2020). LaTeX. Wikibooks, The Free Textbook Project.

  3. Bruce Byfield (2016). Designing with LibreOffice. Friends of OpenDocument, Inc.

  4. Deville, S. (2015). Writing academic papers in plain text with Markdown and Jupyter notebook. Sylvain Deville.

*

Esta publicación está (algo) relacionada con una publicación anterior donde discutí sobre algunas herramientas de investigación que la mayoría de nosotros necesitamos pero que comúnmente no se enseñan de manera formal.

Revisión ortográfica en Jupyter Notebook

El objetivo de esta publicación es mostrar cómo tener revisión automática de ortografía en Jupyter Notebook * para español, como se muestra a continuación.

Ejemplo de corrección ortográfica en Jupyter Notebook.

Existen varias formas de realizar esto. Sin embargo, la forma más fácil es a través del complemento (nbextension) Spellchecker.

Paso a paso

Los pasos a seguir son los siguientes:

  1. Instalar Jupyter notebook extensions (nbextensions). Este incluye Spellchecker.

  2. Ubicar los diccionarios en la carpeta donde está el complemento. Los diccionarios deben usar la codificación UTF-8.

  3. Configurar la ruta de los diccionarios. Esta puede ser una URL o una ruta relativa respecto a la carpeta en donde se encuentra el complemento.

A continuación describiremos en detalle cada paso.

Paso 1: Instalación de nbextensions

Existe una lista de complementos que agregan algunas funcionalidades comúnmente usadas a Jupyter notebook.

Escriba lo siguiente en una terminal, para instalarlo usando PIP.

pip install jupyter_contrib_nbextensions

Sin embargo, si se está usando Anaconda el método recomendado es usar conda, como se muestra a continuación.

conda install -c conda-forge jupyter_contrib_nbextensions

Esto debe instalar los complementos y también la interfaz de configuración. En el menú principal de Jupyter notebook aparecerá una nueva pestaña nombrada Nbextensions en donde se pueden elegir los complementos a usar. La apariencia es la siguiente.

Interfaz gráfica para los complementos de Jupyter.

Algunos complementos recomendados son:

  • Collapsible Headings: que permite ocultar secciones de los documentos.

  • RISE: que convierte los notebooks en presentaciones.

Paso 2: Diccionarios para español

La documentación de Spellchecker sugiere usar un script de Python para descargar diccionarios del proyecto Chromium. Sin embargo, estos tienen como codificación ISO-8859-1 (occidente) y falla para caracteres con tildes o virgulillas. Para que no haya problemas el diccionario debe tener codificación UTF-8. Pueden descargarse en este enlace.

Una vez que se tienen los diccionarios se deben ubicar en la ruta del complemento. En mi computador esta sería

~/.local/share/jupyter/nbextensions/spellchecker/

y dentro de esta los ubicaremos en

~/.local/share/jupyter/nbextensions/spellchecker/typo/dictionaries

Esta ubicación es arbitraria, lo importante es que necesitamos conocer la ruta relativa al complemento.

Paso 3: Configuración complementos

Ahora, en la pestaña Nbextensions seleccionamos el complemento y llenamos los campos con la información de nuestro diccionario:

  • language code to use with typo.js: es_ES

  • url for the dictionary .dic file to use: ./typo/dictionaries/es_ES.dic

  • url for the dictionary .aff file to use: ./typo/dictionaries/es_ES.aff

Esto se muestra a continuación.

Configuración con archivos locales.

Otra opción es usar la URL para los archivos. En https://github.com/wooorm/dictionaries están disponibles los diccionarios del proyecto hunspell en UTF-8. En este caso, la configuración sería:

  • language code to use with typo.js: es_ES

  • url for the dictionary .dic file to use: https://raw.githubusercontent.com/wooorm/dictionaries/master/dictionaries/es/index.dic

  • url for the dictionary .aff file to use: https://raw.githubusercontent.com/wooorm/dictionaries/master/dictionaries/es/index.aff

Y se muestra a continuación.

Configuración con archivos remotos.
*

Dado el público objetivo de esta publicación está hecha en español y no en inglés como se acostumbra en este blog.

#SWDchallenge: Mejorando un gráfico

En storytelling with data tienen un reto mensual en visualización de datos llamado #SWDchallenge. La idea principal de los retos es poner a prueba las habilidades de visualización de datos y storytelling — contar historias atractivas para facilitar su entendimiento —.

Cada mes el reto tiene un tópico diferente. Este mes el reto era mejorar un gráfico existente. Yo decidí tomar un gráfico hecho por mí que fue publicado en un artículo en 2015.

El "original"

El gráfico original con el que vamos a empezar es el siguiente.

Gráfico original.

Este no es el gráfico original en el artículo de 2015 pero sirve el propósito del reto. Los datos se pueden descargar aquí. Este grafico presenta la fracción de energía transmitida (\(\eta\)) para un compuesto helicoidadl con diferentes parámetros geométricos. Los parámetros que se variaron fueron los siguientes:

  • Ángulo de rotación \(\alpha\): el ángulo formado entre capas consecutivas;

  • Grosor del compuesto \(D\), normalizdo por la longitud de onda \(\lambda\); y

  • Número de capas \(N\) en cada celda del compuesto.

El siguiente esquema ilustra estos parámetros.

Parámetros del compuesto helicoidal.

Yo no diría que el gráfico es horrible, y, en comparación con lo que se encuentra en la literatura cientítica es incluso bueno. Pero … en tierra de ciegos, el tuerto es rey. Entonces, vamos a enumarara cada uno de los pecados del gráfico, y a corregirlos:

  • Tiene dos ejes horizontales.

  • La leyenda está encerrada en una recuadro que no es necesario.

  • Los ejes de la derecha y la parte superior no contribuyen al gráfico.

  • Las anotaciones están robando protagonismo a los datos.

  • Se ve desordenado con las líneas y los marcadores.

  • Es un gráfico espagueti.

El siguiente código genera este gráfico.

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import rcParams

rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
rcParams['legend.fontsize'] = 15
rcParams['mathtext.fontset'] = 'cm'

markers = ['o', '^', 'v', 's', '<', '>', 'd', '*', 'x', 'D', '+', 'H']
data = np.loadtxt("Energy_vs_D.csv", skiprows=1, delimiter=",")
labels = np.loadtxt("Energy_vs_D.csv", skiprows=0, delimiter=",",
                    usecols=range(1, 9))
labels = labels[0, :]

fig = plt.figure()
ax = plt.subplot(111)
for cont in range(8):
    plt.plot(data[:, 0], data[:, cont + 1], marker=markers[cont],
             label=r"$D/\lambda={:.3g}$".format(labels[cont]))

# First x-axis
xticks, xlabels = plt.xticks()
plt.xlabel(r"Number of layers - $N$", size=15)
plt.ylabel(r"Fraction of Energy - $\eta$", size=15)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))

# Second x-axis
ax2 = ax.twiny()
ax2.set_xticks(xticks[2:])
ax2.set_xticklabels(180./xticks[2:])
plt.xlabel(r"Angle - $\alpha\ (^\circ)$", size=15)

plt.tight_layout()
plt.savefig("energy_vs_D_orig.svg")
plt.savefig("energy_vs_D_orig.png", dpi=300)

Correcciones

Voy a reivindicar el gráfico un pecado a la vez, veamos cómo resulta.

Tiene dos ejes horizontales

Originalmente, agregue dos ejes horizontales para mostrar el número de capas y el ángulo de rotación al mismo tiempo. La recomendación general es evitar esto, así que vamos a deshacernos del eje superior.

Primera iteración.

Leyenda en un recuadro

Es claro a qué se refiere …

Segunda iteración.

Ejes a la derecha y arriba

Quítemoslos

Tercera iteración.

Las anotaciones están robando protagonismo

Vamos a desplazar las anotaciones hacia el fondo cambiando el color del texto a un gris claro.

Cuarta iteración.

Líneas y marcadores

Conservemos únicamente las líneas.

Quinta iteración.

E incrementemos su grosor.

Sexta iteración.

Es un gráfico espagueti

Creo que una buena opción para este gráfico sería resaltar una línea al tiempo. Haciendo esto, terminamos con el siguiente gráfico.

Séptima iteración.

El siguiente bloque de código genera esta versión.

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import rcParams

# Plots setup
gray = '#757575'
plt.rcParams["mathtext.fontset"] = "cm"
plt.rcParams["text.color"] = gray
plt.rcParams["xtick.color"] = gray
plt.rcParams["ytick.color"] = gray
plt.rcParams["axes.labelcolor"] = gray
plt.rcParams["axes.edgecolor"] = gray
plt.rcParams["axes.spines.right"] = False
plt.rcParams["axes.spines.top"] = False
rcParams['font.family'] = 'serif'
rcParams['mathtext.fontset'] = 'cm'



def line_plots(data, highlight_line, title):
    plt.title(title)
    for cont, datum in enumerate(data[:, 1:].T):
        if cont == highlight_line:
            plt.plot(data[:, 0], datum, zorder=3, color="#984ea3",
                     linewidth=2)
        else:
            plt.plot(data[:, 0], datum, zorder=2, color=gray,
                     linewidth=1, alpha=0.5)


data = np.loadtxt("Energy_vs_D.csv", skiprows=1, delimiter=",")
labels = np.loadtxt("Energy_vs_D.csv", skiprows=0, delimiter=",",
                    usecols=range(1, 9))
labels = labels[0, :]

plt.close("all")
plt.figure(figsize=(8, 4))
for cont in range(8):
    ax = plt.subplot(2, 4, cont + 1)
    title = r"$D/\lambda={:.3g}$".format(labels[cont])
    line_plots(data, cont, title)
    plt.ylim(0.4, 1.0)
    if cont < 4:
        plt.xlabel("")
        ax.xaxis.set_ticks([])
        ax.spines["bottom"].set_color("none")
    else:
        plt.xlabel(r"Number of layers - $N$")
    if cont % 4 > 0:
        ax.yaxis.set_ticks([])
        ax.spines["left"].set_color("none")
    else:
        plt.ylabel(r"Fraction of Energy - $\eta$")


plt.tight_layout()
plt.savefig("energy_vs_D_7.svg")
plt.savefig("energy_vs_D_7.png", dpi=300)

Ajustes finales

Añadimos algunos detalles en Inkscape para terminar con la siguiente versión.

Versión final.

Lecturas adicionales

Gráficos isométricos en Inkscape: Parte 2

La semana pasada publiqué una guía rápida sobre dibujos isométricos dibujar usando Inkscape. En ese post, mostré cómo obtener imágenes que se proyectan a las caras de un bloque isométrico.

Después de mi publicación, Biswajit Banerjee me preguntó en Twitter si podría repetir el proceso con un ejemplo más complejo, y él sugirió el siguiente diagramaa

Cálculo del momento de inercia para una viga.

que, creo, se creó en Inkscape usando la opción "Crear caja 3D".

En esta publicación, haré lo siguiente:

  1. Usar el mismo enfoque de la semana pasada para recrear este esquema.

  2. Sugerir mi enfoque preferido para dibujar este esquema.

Primer enfoque

Repetiré el resumen de la semana pasada. Hay que tener en cuenta que Inkscape trata la rotación en sentido horario como positiva.

Resumen para esquemas isométricos en Inkscape.

Luego, para crear una caja con las dimensiones deseadas, primero creamos cada rectángulo con las dimensiones correctas (en proyecciones paralelas). En el siguiente ejemplo, usamos caras con relaciones de aspecto 3:2, 2:1 y 4:3. La caja de la derecha es la cifra obtenida después de aplicar las transformaciones descritas en el esquema anterior.

Orthogonal views and final isometric figure

Ahora podemos proceder, para recrear la figura deseada. No conozco las dimensiones exactas de la caja; supongo que la sección transversal era un cuadrado y la relación de aspecto era 5:1. Después de aplicar las transformaciones para cada rectángulo obtenemos lo siguiente (agregando algunos ajustes aquí y allá).

Figura usando el enfoque descrito.

Segundo enfoque

Para este tipo de esquema, preferiría crear una cuadrícula axonométrica (Archivo > Propiedades de documento > Cuadrículas). Después de agregar la cuadrícula a nuestro lienzo es bastante sencillo dibujar las figuras en vista isométrica. El lienzo debe ser similar a la siguiente imagen.

Segundo enfoque: usando una rejilla axonométrica.

Luego podemos dibujar cada cara usando la cuadrícula. Si queremos ser más precisos podemos activar Ajustar a nodos cúspides. La siguiente animación muestra la construcción paso a paso.

Construcción del isométrico paso a paso.

Y obtenemos la siguiente imagen.

Figura usando el segundo enfoque.

Conclusión

Como mencioné, Inkscape se puede usar para dibujar figuras simples en proyección isométrica. Sin embargo, sugiero utilizar un CAD como FreeCAD para geometrías más complicadas.

Gráficos isométricos en Inkscape

A veces me encuentro en la necesidad de hacer un diagrama usando una proyección isométrica. Cuando el diagrama es complicado la mejor opción es usar algún software de CAD como FreeCAD, pero a veces sólo se necesita un diagrama simple. Otra situación en la que esto es común es en videojuegos, donde se el arte isométrico y arte pixelado son bastante comunes.

Lo que queremos se muestra en la siguiente figura.

Ejemplo de isométrico.

Es decir, queremos comenzar con cierto dibujo, o escrito en el caso del ejemplo, y queremos saber cómo se vería en una de las caras de una bloque isométrico.

A continuación, describiré brevemente las transformaciones involucradas en este proceso. Si sólo está interesado en la receta para hacer esto en Inkscape, pase al final de este post.

Transformaciones involucradas

Como estamos trabajando en una pantalla de computador, estamos hablando de 2 dimensiones. Por lo tanto, todas las transformaciones están representadas por matrices 2×2. En el ejemplo de interés en este post necesitamos las siguientes transformaciones:

  1. Escalado vertical

  2. Inclinación horizontal

  3. Rotación

Las siguientes son las matrices de transformación.

Escalado en la dirección vertical

La matriz está dada por

\begin{equation*} M_\text{scaling} = \begin{bmatrix} 1 &0\\ 0 &a\end{bmatrix}\, , \end{equation*}

donde \(a\) es el factor de escalamiento.

Inclinación horizontal

La matriz está dada por

\begin{equation*} M_\text{skew} = \begin{bmatrix} 1 &\tan a\\ 0 &1\end{bmatrix}\, , \end{equation*}

donde \(a\) es el ángulo de inclinación.

Rotación

La matriz está dada por

\begin{equation*} M_\text{rotation} = \begin{bmatrix} \cos a &-\sin a\\ \sin a &\cos a\end{bmatrix}\, , \end{equation*}

donde \(a\) es el ángulo de rotación.

Transformación completa

La transformación completa está dada por

\begin{equation*} M_\text{complete} = M_\text{rotation} M_\text{skew} M_\text{scaling}\, , \end{equation*}

particularmente,

\begin{align*} &M_\text{side} = \frac{1}{2}\begin{bmatrix} \sqrt{3} & 0\\ -1 &2\end{bmatrix}\approx \begin{bmatrix} 0.866 & 0.000\\ -0.500 &1.000\end{bmatrix}\, , \\ &M_\text{front} = \frac{1}{2}\begin{bmatrix} \sqrt{3} & 0\\ 1 &2\end{bmatrix}\approx \begin{bmatrix} 0.866 & 0.000\\ 0.500 &1.000\end{bmatrix}\, , \\ &M_\text{plant} = \frac{1}{2}\begin{bmatrix} \sqrt{3} & -\sqrt{3}\\ -1 &1\end{bmatrix}\approx \begin{bmatrix} 0.866 & -0.866\\ 0.500 &0.500\end{bmatrix}\, . \end{align*}

Estos valores parecen un poco arbitrarios, pero pueden obtenerse de la proyección isométrica misma. Pero esta explicación sería un poco larga para este post.

Tranformación en Inskcape

Ya tenemos las matrices de transformación y podemos usarlas en Inkscape. Pero primero, necesitamos entender cómo funciona. Inkscape usa SVG, el estándar web para gráficos vectoriales. Las transformaciones en SVG se realizan utilizando la siguiente matriz

\begin{equation*} \begin{bmatrix} a &c &e\\ b &d &f\\ 0 &0 &1\end{bmatrix}\, , \end{equation*}

que usa coordenadas homogéneas. Entonces, se puede presionar Shift + Ctrl + M y digitar las componentes de las matrices que se obtuvieron anteriormente para \(a\), \(b\), \(c\), y \(d\); dejando \(e\) y \(f\) con el valor 0.

Sin embargo, mi método preferido es aplicar cada transformación después de otro en el cuadro de diálogo Transformar (Shift + Ctrl + M). Y este es el método presentado en el resumen al final de esta publicación.

TL;DR

Si solo está interesado en las transformaciones necesarias en Inkscape Puedes consultar el resumen a continuación. Utiliza el tercer cuadrante como se presenta abajo.

Nombres para la representación en tercer cuadrante.

Resumen

Tenga en cuenta que Inkscape trata la rotación en sentido horario como positiva. Opuesto a la notación común en matemáticas.

Resumen para diagramas isométricos en Inkscape.

Diferencias finitas en dominios infinitos

Diferencias finitas en dominios infinitos

Gracias a mi amigo, Edward Villegas, terminé pensando acerca del uso de cambio de variables en la solución de problemas de valores propios con diferencias finitas.

El problema

Digamos que queremos resolver una ecuación diferencias sobre un dominio infinito. Un caso común es la solución de la ecuación de Schrödinger independiente del tiempo sujeta a un potencial \(V(x)\). Por ejemplo

\begin{equation*} -\frac{1}{2}\frac{\mathrm{d}^2}{\mathrm{d}x^2}\psi(x) + V(x) \psi(x) = E\psi(x),\quad \forall x\in (-\infty, \infty), \end{equation*}

en donde queremos encontrar las parejas de valores/funciones propias \((E_n, \psi_n(x))\).

Lo que normalmente hago cuando uso diferencias finitas es dividir el dominio regularmente. Donde tomo un dominio lo suficientemente grande, para que la solución haya decaído a cero. Lo que hago en esta publicación es usar un cambio de variable para convertir el intervalo a uno finito y luego dividir el dominio transformado regularmente en intervalos finitos.

Mi enfoque usual

Mi enfoque usual es aproximar la segunda derivada con una diferencia centrada para el punto \(x_i\), de la siguiente manera

\begin{equation*} \frac{\mathrm{d}^2 f(x)}{\mathrm{d}x^2} \approx \frac{f(x + \Delta x) - 2 f(x_i) + f(x - \Delta x)}{\Delta x^2}\, , \end{equation*}

con \(\Delta x\) la separación entre puntos consecutivos.

Podemos solucionar este problem en Python con el siguiente bloque de código:

import numpy as np
from scipy.sparse import diags
from scipy.sparse.linalg import eigs


def regular_FD(pot, npts=101, x_max=10, nvals=6):
    """
    Find eigenvalues/eigenfunctions for Schrodinger
    equation for the given potential `pot` using
    finite differences
    """
    x = np.linspace(-x_max, x_max, npts)
    dx = x[1] - x[0]
    D2 = diags([1, -2, 1], [-1, 0, 1], shape=(npts, npts))/dx**2
    V = diags(pot(x))
    H = -0.5*D2 + V
    vals, vecs = eigs(H, k=nvals, which="SM")
    return x, np.real(vals), vecs

Configuremos los gráficos con el siguiente bloque de código.

# Jupyter notebook plotting setup & imports
%matplotlib notebook
import matplotlib.pyplot as plt

gray = '#757575'
plt.rcParams["figure.figsize"] = 6, 4
plt.rcParams["mathtext.fontset"] = "cm"
plt.rcParams["text.color"] = gray
fontsize = plt.rcParams["font.size"] = 12
plt.rcParams["xtick.color"] = gray
plt.rcParams["ytick.color"] = gray
plt.rcParams["axes.labelcolor"] = gray
plt.rcParams["axes.edgecolor"] = gray
plt.rcParams["axes.spines.right"] = False
plt.rcParams["axes.spines.top"] = False

Consideremos el oscilador armónico cuántico, que tiene como valores propios

\begin{equation*} E_n = n + \frac{1}{2},\quad \forall n = 0, 1, 2 \cdots \end{equation*}

Usando el método de diferencias finitas obtenemos valores que están muy cerca de los analíticos.

x, vals, vecs = regular_FD(lambda x: 0.5*x**2, npts=201)
vals

Con respuesta

array([0.4996873 , 1.49843574, 2.49593063, 3.49216962, 4.48715031,
       5.4808703 ])

Los valores analíticos son los siguientes

[0.5, 1.5, 2.5, 3.5, 4.5, 5.5])

Si graficamos estos dos conjuntos, obtenemos lo siguiente.

plt.figure()
plt.plot(anal_vals)
plt.plot(vals, "o")
plt.xlabel(r"$n$", fontsize=16)
plt.ylabel(r"$E_n$", fontsize=16)
plt.legend(["Analytic", "Finite differences"])
plt.tight_layout();
Valores propios para la diferencia finita regular.

Veamos las funciones propias

plt.figure()
plt.plot(x, np.abs(vecs[:, :3])**2)
plt.xlim(-6, 6)
plt.xlabel(r"$x$", fontsize=16)
plt.ylabel(r"$|\psi_n(x)|^2$", fontsize=16)
plt.yticks([])
plt.tight_layout();
Funciones propias para la diferencia finita regular.

Un inconveniente con este método es el muestreo redundante hacia los extremos del intervalo mientras submuestreamos el centro.

Transformando el dominio

Ahora, consideremos el caso en el que transormamos el dominio infinito a uno finito usando un cambio de variable

\begin{equation*} \xi = \xi(x) \end{equation*}

con \(\xi \in (-1, 1)\). Dos opciones para esta transformación son:

  • \(\xi = \tanh x\); y

  • \(\xi = \frac{2}{\pi} \arctan x\).

Haciendo este cambio de variable la ecuación, debemos resolver la siguiente ecuación

\begin{equation*} -\frac{1}{2}\left(\frac{\mathrm{d}\xi}{\mathrm{d}x}\right)^2\frac{\mathrm{d}^2}{\mathrm{d}\xi^2}\psi(\xi) - \frac{1}{2}\frac{\mathrm{d}^2\xi}{\mathrm{d}x^2}\frac{\mathrm{d}}{\mathrm{d}\xi}\psi(\xi) + V(\xi) \psi(\xi) = E\psi(\xi) \end{equation*}

El siguiente bloque de código resuelve el problema de valores propios en el dominio transformado:

def mapped_FD(pot, fun, dxdxi, dxdxi2, npts=101, nvals=6, xi_tol=1e-6):
    """
    Find eigenvalues/eigenfunctions for Schrodinger
    equation for the given potential `pot` using
    finite differences over a mapped domain on (-1, 1)
    """
    xi = np.linspace(-1 + xi_tol, 1 - xi_tol, npts)
    x = fun(xi)
    dxi = xi[1] - xi[0]
    D2 = diags([1, -2, 1], [-1, 0, 1], shape=(npts, npts))/dxi**2
    D1 = 0.5*diags([-1, 1], [-1, 1], shape=(npts, npts))/dxi
    V = diags(pot(x))
    fac1 = diags(dxdxi(xi)**2)
    fac2 = diags(dxdxi2(xi))
    H = -0.5*fac1.dot(D2) - 0.5*fac2.dot(D1) + V
    vals, vecs = eigs(H, k=nvals, which="SM")
    return x, np.real(vals), vecs

Primera transformación: \(\xi = \tanh(x)\)

Consideremos la primera transformación

\begin{equation*} \xi = \tanh(x)\, . \end{equation*}

En este caso,

\begin{equation*} \frac{\mathrm{d}\xi}{\mathrm{d}x} = 1 - \tanh^2(x) = 1 - \xi^2\, , \end{equation*}

y

\begin{equation*} \frac{\mathrm{d}^2\xi}{\mathrm{d}x^2} = -2\tanh(x)[1 - \tanh^2(x)] = -2\xi[1 - \xi^2]\, . \end{equation*}

Necesitamos definir las funciones

pot = lambda x: 0.5*x**2
fun = lambda xi: np.arctanh(xi)
dxdxi = lambda xi: 1 - xi**2
dxdxi2 = lambda xi: -2*xi*(1 - xi**2)

y correr

x, vals, vecs = mapped_FD(pot, fun, dxdxi, dxdxi2, npts=201)
vals

Y obtenemos los siguientes valores propios

array([0.49989989, 1.4984226 , 2.49003572, 3.46934257, 4.46935021,
       5.59552989])

Si los comparamos con los valores analítivos obtenemos lo siguiente.

plt.figure()
plt.plot(anal_vals)
plt.plot(vals, "o")
plt.legend(["Analytic", "Finite differences"])
plt.xlabel(r"$n$", fontsize=16)
plt.ylabel(r"$E_n$", fontsize=16)
plt.tight_layout();
Valores propios para la primera transormación.

Y las siguientes funciones propias.

plt.figure()
plt.plot(x, np.abs(vecs[:, :3])**2)
plt.xlim(-6, 6)
plt.xlabel(r"$x$", fontsize=16)
plt.ylabel(r"$|\psi_n(x)|^2$", fontsize=16)
plt.yticks([])
plt.tight_layout();
Funciones propias para la primera transformación.

Segunda transformación: \(\xi = \frac{2}{\pi}\mathrm{atan}(x)\)

Consideremos ahora la segunda transformación

\begin{equation*} \xi = \frac{2}{\pi}\mathrm{atan}(x)\, . \end{equation*}

En este caso,

\begin{equation*} \frac{\mathrm{d}\xi}{\mathrm{d}x} = \frac{2}{\pi(1 + x^2)} = \frac{2 \cos^2\xi}{\pi} \, , \end{equation*}

y

\begin{equation*} \frac{\mathrm{d}^2\xi}{\mathrm{d}x^2} = -\frac{4x}{\pi(1 + x^2)^2} = -\frac{4 \cos^4\xi \tan \xi}{\pi}\, . \end{equation*}

Una vez más, definimos las funciones

pot = lambda x: 0.5*x**2
fun = lambda xi: np.tan(0.5*np.pi*xi)
dxdxi = lambda xi: 2/np.pi * np.cos(0.5*np.pi*xi)**2
dxdxi2 = lambda xi: -4/np.pi * np.cos(0.5*np.pi*xi)**4 * np.tan(0.5*np.pi*xi)

y ejecutamos

x, vals, vecs = mapped_FD(pot, fun, dxdxi, dxdxi2, npts=201)
vals

para obtener los siguientes valores propios

array([0.49997815, 1.49979632, 2.49930872, 3.49824697, 4.49627555,
       5.49295665])

con la siguiente gráfica

plt.figure()
plt.plot(anal_vals)
plt.plot(vals, "o")
plt.legend(["Analytic", "Finite differences"])
plt.xlabel(r"$n$", fontsize=16)
plt.ylabel(r"$E_n$", fontsize=16)
plt.tight_layout();
Valores propios para la segunda transformación.

y las siguientes funciones propias.

plt.figure()
plt.plot(x, np.abs(vecs[:, :3])**2)
plt.xlabel(r"$x$", fontsize=16)
plt.ylabel(r"$|\psi|^2$", fontsize=16)
plt.xlim(-6, 6)
plt.xlabel(r"$x$", fontsize=16)
plt.ylabel(r"$|\psi_n(x)|^2$", fontsize=16)
plt.yticks([])
plt.tight_layout();
Funciones propias para la segunda transformación.

Conclusión

El método funciona bien, aunque la ecuación diferencial es más complicada por el cambio de variable. Aunque existen métodos más elegantes para considerar dominiso infinitos, este es lo suficientemente simple para hacerse en 10 líneas de código.

Podemos ver que la transforamción \(\xi = \mathrm{atan}(x)\), cubre mejor el dominio que \(\xi = \tanh(x)\), donde la mayoría de los puntos están ubicados en el centro del intervalo.

¡Gracias por leer!

Esta publicación se escribió en el Jupyter notebook. Puedes descargar este notebook, o ver una versión estática en nbviewer.

Comparación de mapas de colores cíclicos

Empecé esta publicación buscando implementaciones de mapas de difusión en Python, que no pude encontrar. Me puse entonces a seguir un ejemplo sobre aprendizaje de variedades de Jake Vanderplas en un conjunto de datos diferente. Y funcionó bien

/images/manifold_learning_toroidal_helix.svg

pero el mapa de color es "Spectral", que es divergente. Esto me puso a pensar acerca de usar un mapa de colores cícliclo, y terminé en esta pregunta de StackOverflow. Y decidí comparar algunos mapas de colores cíclicos.

Elegí mapas de colores de diferentes fuentes

  • cmocean:

    • phase

  • Paraview:

    • hue_L60

    • erdc_iceFire

    • nic_Edge

  • Colorcet:

    • colorwheel

    • cyclic_mrybm_35_75_c68

    • cyclic_mygbm_30_95_c78

y, obviamente, hsv. Los mapas de colores en formato de texto plano se pueden descargar aquí.

Comparación

Para todos los ejemplos se importaron los siguientes módulos:

from __future__ import division, print_function
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
from colorspacious import cspace_convert

Imagen de prueba

Peter Kovesi propuso una manera de comparar mapas de colores cíclicos en un artículo en 2015. Consta de una rampa en espiral con ondulaciones. En coordenadas polares es

\begin{equation*} c = (A \rho^p \sin(n \theta) + \theta)\, \mathrm{mod}\, 2\pi \end{equation*}

con \(A\) la amplitud de la oscilación, \(\rho\) el radio normalizado en [0, 1], \(p\) un número positivo, y \(n\) el número de ciclos.

Y la siguiente función crea la rejilla en Python.

def circle_sine_ramp(r_max=1, r_min=0.3, amp=np.pi/5, cycles=50,
                     power=2, nr=50, ntheta=1025):
r, t = np.mgrid[r_min:r_max:nr*1j, 0:2*np.pi:ntheta*1j]
r_norm = (r - r_min)/(r_max - r_min)
vals = amp * r_norm**power * np.sin(cycles*t) + t
vals = np.mod(vals, 2*np.pi)
return t, r, vals

El resultado es el siguiente

/images/sine_helix_cyclic_cmap.png

Prueba para daltonismo

t, r, vals = circle_sine_ramp(cycles=0)
cmaps = ["hsv",
         "cmocean_phase",
         "hue_L60",
         "erdc_iceFire",
         "nic_Edge",
         "colorwheel",
         "cyclic_mrybm",
         "cyclic_mygbm"]
severity = [0, 50, 50, 50]
for colormap in cmaps:
    data = np.loadtxt(colormap + ".txt")
    fig = plt.figure()
    for cont in range(4):
        cvd_space = {"name": "sRGB1+CVD",
             "cvd_type": cvd_type[cont],
             "severity": severity[cont]}
        data2 = cspace_convert(data, cvd_space, "sRGB1")
        data2 = np.clip(data2, 0, 1)
        cmap = LinearSegmentedColormap.from_list('my_colormap', data2)
        ax = plt.subplot(2, 2, 1 + cont, projection="polar")
        ax.pcolormesh(t, r, vals, cmap=cmap)
        ax.set_xticks([])
        ax.set_yticks([])
    plt.suptitle(colormap)
    plt.tight_layout()
    plt.savefig(colormap + ".png", dpi=300)
/images/hsv_eval.png

Comparación de hsv para diferentes deficiencias visuales del color.

/images/cmocean_phase_eval.png

Comparación de Phase para diferentes deficiencias visuales del color.

/images/hue_L60_eval.png

Comparación de hue_L60 para diferentes deficiencias visuales del color.

/images/erdc_iceFire_eval.png

Comparación de erdc_iceFire para diferentes deficiencias visuales del color.

/images/nic_Edge_eval.png

Comparación de nic_Edge para diferentes deficiencias visuales del color.

/images/colorwheel_eval.png

Comparación de Colorwheel para diferentes deficiencias visuales del color.

/images/cyclic_mrybm_eval.png

Comparación de Cyclic_mrybm para diferentes deficiencias visuales del color.

/images/cyclic_mygbm_eval.png

Comparación de Cyclic_mygbm para diferentes deficiencias visuales del color.

Mapas de colores cíclicos generados aleatoriamente

¿Qué pasa si generamos mapas de colores cíclicos aleatoriamente? ¿Cómo lucirían?

A continuación están las piezas de código y mapas de colores resultantes.

from __future__ import division, print_function
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap


# Next line to silence pyflakes. This import is needed.
Axes3D

plt.close("all")
fig = plt.figure()
fig2 = plt.figure()
nx = 4
ny = 4
azimuths = np.arange(0, 361, 1)
zeniths = np.arange(30, 70, 1)
values = azimuths * np.ones((30, 361))
for cont in range(nx * ny):
    np.random.seed(seed=cont)
    rad = np.random.uniform(0.1, 0.5)
    center = np.random.uniform(rad, 1 - rad, size=(3, 1))
    mat = np.random.rand(3, 3)
    rot_mat, _ = np.linalg.qr(mat)
    t = np.linspace(0, 2*np.pi, 256)
    x = rad*np.cos(t)
    y = rad*np.sin(t)
    z = 0.0*np.cos(t)
    X = np.vstack((x, y, z))
    X = rot_mat.dot(X) + center

    ax = fig.add_subplot(ny, nx, 1 + cont, projection='polar')
    cmap = LinearSegmentedColormap.from_list('my_colormap', X.T)
    ax.pcolormesh(azimuths*np.pi/180.0, zeniths, values, cmap=cmap)
    ax.set_xticks([])
    ax.set_yticks([])

    ax2 = fig2.add_subplot(ny, nx, 1 + cont, projection='3d')
    ax2.plot(X[0, :], X[1, :], X[2, :])
    ax2.set_xlim(0, 1)
    ax2.set_ylim(0, 1)
    ax2.set_zlim(0, 1)
    ax2.view_init(30, -60)
    ax2.set_xticks([0, 0.5, 1.0])
    ax2.set_yticks([0, 0.5, 1.0])
    ax2.set_zticks([0, 0.5, 1.0])
    ax2.set_xticklabels([])
    ax2.set_yticklabels([])
    ax2.set_zticklabels([])


fig.savefig("random_cmaps.png", dpi=300, transparent=True)
fig2.savefig("random_cmaps_traj.svg", transparent=True)
/images/random_cmaps.png

16 mapas de colores generados aleatoriamente.

/images/random_cmaps_traj.svg

Trayectorias en espacio RGB para los mapas de colores generados aleatoriamente.

Una idea interesante sería tomar estos mapas de colores y optimizar algunos parámetros perceptuales como luminosidad para obtener mapas de colores útiles.

Conclusiones

Probablemente, yo usaría phase, colorwheel, o mrybm en el futuro.

/images/toroidal_helix_phase.svg

Imagen inicial usando phase.

/images/toroidal_helix_colorwheel.svg

Imagen inicial usando colorwheel.

/images/toroidal_helix_mrybm.svg

Imagen inicial usando mrybm.

Referencias

  1. Peter Kovesi. Good Colour Maps: How to Design Them. arXiv:1509.03700 [cs.GR] 2015