Capítulo 11
Visualizar los resultados
Una tabla de desplazamientos y fuerzas contiene toda la información, pero no la muestra. Un dibujo de la armadura deformada, o de sus barras coloreadas según trabajen en tracción o en compresión, se entiende de un vistazo y revela errores que en una tabla pasarían inadvertidos: un apoyo que se mueve, una barra que debería estar comprimida y aparece traccionada. En este capítulo profundizamos en Matplotlib y escribimos las funciones que dibujarán los resultados de cualquier armadura.
11.1 Dibujar para entender
Los desplazamientos de una armadura son muy pequeños comparados con sus dimensiones: en el ejemplo A, menos de un milímetro en una armadura de seis metros. Dibujados a escala real, la armadura deformada sería indistinguible de la original. Por eso la deformada se dibuja con los desplazamientos multiplicados por un factor de escala (scale factor) : cada nodo de coordenadas se dibuja en
Un buen factor hace que el mayor desplazamiento dibujado sea una fracción visible del tamaño de la armadura, por ejemplo el 10 %. Si es la mayor dimensión de la armadura y el mayor desplazamiento, .
11.2 Matplotlib con más detalle
En el capítulo 3 usamos ax.plot con un texto corto,
como "k-o", para indicar el color, el estilo y el marcador. Para gráficos
cuidados conviene indicarlos por separado, con
argumentos con nombre (keyword arguments): color para el color,
linewidth para el grosor, linestyle para el estilo ("-",
"--", ":"), marker para el símbolo de los puntos y label
para el texto de la leyenda. ax.text escribe un texto en una posición, y
sus argumentos ha y va lo alinean horizontal y verticalmente.
Primero definimos los colores del libro como constantes, escritas en
mayúsculas por convención:
# Colores del libro, distinguibles con daltonismo
TRACCION = "#0072B2"
COMPRESION = "#D55E00"
PRINCIPAL = "#1F4E79"
NEUTRO = "#8C8C8C"
fig, ax = plt.subplots()
ax.plot([0, 6], [0, 0], color=TRACCION, linewidth=3,
label="Tracción")
ax.plot([0, 3], [0, 4], color=COMPRESION, linewidth=3,
label="Compresión")
ax.plot([0, 3, 6], [0, 4, 0], color=NEUTRO,
linestyle="--", marker="o", label="Geometría")
ax.text(3, 0.2, "texto en (3, 0.2)", ha="center")
ax.set_aspect("equal")
ax.legend()
plt.show()
11.3 Una función para dibujar la armadura
Todos los gráficos de este capítulo dibujan las barras de una armadura, a
veces en su posición original y a veces en la deformada. Escribimos una sola
función, que recibe los ejes ax donde dibujar. Así puede usarse varias
veces en un mismo gráfico o en distintos paneles de una figura:
def dibujar_armadura(ax, nodos, barras, color=NEUTRO,
estilo="-", etiqueta=None):
"""Dibuja las barras de una armadura en ax."""
for barra in barras.values():
i = nodos[barra["nodo_inicial"]]
j = nodos[barra["nodo_final"]]
ax.plot([i["x"], j["x"]], [i["y"], j["y"]],
color=color, linestyle=estilo, marker="o",
markersize=4, label=etiqueta)
etiqueta = None # la leyenda, una sola vez
ax.set_aspect("equal")
El método values de un diccionario recorre solo sus valores, sin las
claves, que aquí no necesitamos. La etiqueta de la leyenda se asigna a
la primera barra y después se anula, para que la leyenda no repita el mismo
texto una vez por barra.
11.4 La deformada
Para dibujar la deformada no necesitamos una función nueva: basta con
calcular las posiciones desplazadas de los nodos y dibujar la armadura con
ellas. La función devuelve un diccionario con la misma forma que nodos,
de modo que dibujar_armadura lo acepta sin cambios:
def nodos_desplazados(nodos, U, escala):
"""Nodos en su posición deformada, con los
desplazamientos multiplicados por la escala."""
nuevos = {}
for nodo, punto in nodos.items():
ix, iy = indices(gdl_del_nodo(nodo))
nuevos[nodo] = {"x": punto["x"] + escala * U[ix],
"y": punto["y"] + escala * U[iy]}
return nuevos
El factor de escala se calcula a partir de las coordenadas extremas de la
armadura. Las funciones max y min de Python dan el mayor y el menor
elemento de una lista:
def escala_automatica(nodos, U, fraccion=0.1):
"""Escala para que el mayor desplazamiento dibujado
sea una fracción del tamaño de la armadura."""
xs = []
ys = []
for punto in nodos.values():
xs.append(punto["x"])
ys.append(punto["y"])
tamano = max(max(xs) - min(xs), max(ys) - min(ys))
return fraccion * tamano / np.abs(U).max()
Dibujamos la armadura original con línea de trazos y, encima, la deformada:
escala = escala_automatica(nodos, U)
fig, ax = plt.subplots()
dibujar_armadura(ax, nodos, barras, estilo="--",
etiqueta="Original")
deformados = nodos_desplazados(nodos, U, escala)
dibujar_armadura(ax, deformados, barras, color=PRINCIPAL,
etiqueta="Deformada")
ax.set_title(f"Ejemplo A, deformada ×{escala:.0f}")
ax.legend()
plt.show()
Verificación. El dibujo debe respetar los apoyos y coincidir con los desplazamientos del capítulo 9. El nodo 1, articulado, no se mueve. El nodo 2 se desplaza solo en horizontal, hacia la derecha, porque el rodillo impide el movimiento vertical. El nodo 3 se mueve hacia la derecha y hacia abajo, como indican sus desplazamientos ( = 0.563 mm, = −0.650 mm). La barra 1 se alarga, en tracción, y las barras inclinadas se acortan, en compresión.
11.5 El diagrama de fuerzas axiales
El diagrama de fuerzas axiales colorea cada barra según su estado y escribe
su fuerza. Reutilizamos clasificar_fuerza del capítulo 10
para elegir el color. Los valores se escriben al 40 % de cada
barra, desde su nodo inicial, y no en su punto medio: así, como vimos en el
ejercicio 4.4, no se tapan los valores de dos diagonales que se
cruzan.
def dibujar_fuerzas(ax, nodos, barras, N, decimales=1,
tamano=None):
"""Barras en colores según su fuerza axial, con su
valor en kN al 40 % de cada barra. tamano: tamaño
de letra de los valores."""
for numero, barra in barras.items():
estado = clasificar_fuerza(N[numero])
if estado == "tracción":
color = TRACCION
elif estado == "compresión":
color = COMPRESION
else:
color = NEUTRO
i = nodos[barra["nodo_inicial"]]
j = nodos[barra["nodo_final"]]
ax.plot([i["x"], j["x"]], [i["y"], j["y"]],
color=color, linewidth=3)
# Al 40 % y no en el punto medio: así no se tapan
# los valores de dos diagonales que se cruzan
xm = i["x"] + 0.4 * (j["x"] - i["x"])
ym = i["y"] + 0.4 * (j["y"] - i["y"])
valor = round(N[numero], decimales) + 0.0
fondo = {"facecolor": "white", "linewidth": 0}
ax.text(xm, ym, f"{valor:.{decimales}f}",
color=color, ha="center", va="center",
bbox=fondo, fontsize=tamano)
# Leyenda con líneas vacías: solo muestran los colores
ax.plot([], [], color=TRACCION, linewidth=3,
label="Tracción")
ax.plot([], [], color=COMPRESION, linewidth=3,
label="Compresión")
# Leyenda debajo del dibujo, para no tapar las barras
ax.legend(loc="upper center", bbox_to_anchor=(0.5, 0),
ncol=2, frameon=False)
ax.set_aspect("equal")
ax.axis("off")
Para la leyenda dibujamos dos líneas vacías, ax.plot([], [], ...), que
no aparecen en el gráfico pero sí en la leyenda con su color. La leyenda va
debajo del dibujo y se quitan los ejes, porque en un esquema de la armadura
las coordenadas no aportan información. El argumento bbox recibe un
diccionario con las opciones del recuadro blanco que hay detrás de cada
valor.
fig, ax = plt.subplots()
dibujar_fuerzas(ax, nodos, barras, N)
ax.set_title("Fuerzas axiales del ejemplo A (kN)")
plt.show()
Problema resuelto 11.1 · Un informe gráfico
Preparar una figura con dos paneles para el ejemplo A: la deformada a la izquierda y el diagrama de fuerzas axiales a la derecha.
Solución
plt.subplots(1, 2) crea una figura con una fila y dos columnas de ejes,
que recibimos en dos variables. El argumento figsize fija el tamaño de
la figura en pulgadas, y tight_layout ajusta los márgenes para que
nada se superponga. Cada función dibuja en los ejes que le indicamos:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(4.5, 2.2))
escala = escala_automatica(nodos, U)
dibujar_armadura(ax1, nodos, barras, estilo="--")
dibujar_armadura(ax1, nodos_desplazados(nodos, U, escala),
barras, color=PRINCIPAL)
ax1.set_title(f"Deformada (×{escala:.0f})")
dibujar_fuerzas(ax2, nodos, barras, N)
ax2.set_title("Fuerzas axiales (kN)")
for ax in (ax1, ax2):
ax.axis("off") # sin ejes: es un esquema
fig.tight_layout()
plt.show()
Verificación. Ambos paneles cuentan la misma historia: la barra que se alarga en la deformada, la 1, es la única en tracción, y las que se acortan están comprimidas.
Repaso y resumen del capítulo
- La deformada se dibuja con los desplazamientos multiplicados por un factor de escala, , que siempre debe indicarse.
- Los argumentos con nombre de
ax.plot, comocolor,linewidtholabel, controlan el aspecto de cada línea;ax.textescribe textos yax.legendmuestra la leyenda. - Las funciones de dibujo reciben los ejes
axcomo parámetro, para poder combinarse en una misma figura. - Cuatro funciones nuevas de la calculadora dibujan los resultados de cualquier armadura: la geometría, las posiciones deformadas, el factor de escala y el diagrama de fuerzas.
plt.subplots(1, 2)crea una figura con dos paneles.- Un dibujo es también una verificación: la deformada debe respetar los apoyos y los colores deben coincidir con el alargamiento de cada barra.
Ejercicios
Ejercicio 11.1
Dibujar la deformada del ejemplo A con factores de escala de 100 y de 2000, en dos paneles. ¿Cuál es más útil y por qué?
Ver la solución
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(4.5, 2.2))
for ax, escala in ((ax1, 100), (ax2, 2000)):
dibujar_armadura(ax, nodos, barras, estilo="--")
deformados = nodos_desplazados(nodos, U, escala)
dibujar_armadura(ax, deformados, barras,
color=PRINCIPAL)
ax.set_title(f"Escala ×{escala}")
ax.axis("off")
fig.tight_layout()
plt.show()
Con un factor de 100, la deformada casi se confunde con la armadura original y no permite ver el movimiento. Con 2000, el movimiento es evidente, aunque la armadura parece muy distorsionada. Un factor intermedio, como el automático, muestra el movimiento sin exagerarlo.
Ejercicio 11.2
Agregar al diagrama de fuerzas del ejemplo A un triángulo debajo de cada
nodo apoyado. El argumento marker="^" de ax.plot dibuja un
triángulo, y markersize, su tamaño.
Ver la solución
fig, ax = plt.subplots()
dibujar_fuerzas(ax, nodos, barras, N)
for nodo in apoyos:
punto = nodos[nodo]
ax.plot(punto["x"], punto["y"] - 0.25, marker="^",
markersize=14, color="black")
ax.set_ylim(-0.8, 4.5)
ax.set_title("Fuerzas axiales y apoyos (kN)")
plt.show()
Los triángulos se dibujan un poco por debajo de cada nodo, y
ax.set_ylim amplía el rango vertical para que entren en el gráfico.
Ejercicio 11.3
Preparar el informe gráfico del ejemplo B: su deformada y su diagrama de fuerzas en dos paneles.
Ver la solución
Calculamos primero los resultados del ejemplo B con las funciones de los capítulos anteriores:
geometria_b = calcular_geometria(nodos_b, barras_b)
rigideces_b = calcular_rigideces(barras_b, geometria_b)
K_b = ensamblar_rigidez(nodos_b, barras_b, rigideces_b)
F_b = ensamblar_cargas(nodos_b, cargas_b)
libres_b, restringidos_b = clasificar_gdl(nodos_b,
apoyos_b)
K_ll_b, K_lr_b, _, _ = particionar(K_b, libres_b,
restringidos_b)
U_b = np.zeros(2 * len(nodos_b))
U_b[indices(libres_b)] = resolver_desplazamientos(
K_ll_b, K_lr_b, F_b[indices(libres_b)],
np.zeros(len(restringidos_b)))
N_b = fuerzas_axiales(barras_b, geometria_b, U_b)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(4.5, 2.8))
escala = escala_automatica(nodos_b, U_b)
dibujar_armadura(ax1, nodos_b, barras_b, estilo="--")
deformados = nodos_desplazados(nodos_b, U_b, escala)
dibujar_armadura(ax1, deformados, barras_b,
color=PRINCIPAL)
ax1.set_title(f"Deformada (×{escala:.0f})")
dibujar_fuerzas(ax2, nodos_b, barras_b, N_b)
ax2.set_title("Fuerzas axiales (kN)")
for ax in (ax1, ax2):
ax.axis("off")
fig.tight_layout()
plt.show()
La deformada muestra el cuadro desplazándose hacia la derecha, empujado por la carga horizontal: la diagonal 6 se acorta y la 5 se alarga, como indican sus colores.
Ejercicio 11.4
Dibujar el ejemplo A con el grosor de cada barra proporcional al valor absoluto de su fuerza axial, de modo que la barra más cargada se vea más gruesa.
Ver la solución
fig, ax = plt.subplots()
mayor = 0.0
for fuerza in N.values():
mayor = max(mayor, abs(fuerza))
for numero, barra in barras.items():
i = nodos[barra["nodo_inicial"]]
j = nodos[barra["nodo_final"]]
if N[numero] > 0:
color = TRACCION
else:
color = COMPRESION
grosor = 1 + 7 * abs(N[numero]) / mayor
ax.plot([i["x"], j["x"]], [i["y"], j["y"]],
color=color, linewidth=grosor,
solid_capstyle="round")
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Grosor proporcional a |N|")
plt.show()
El grosor va de 1 a 8 puntos, proporcional a respecto de la mayor
fuerza. El argumento solid_capstyle="round" redondea los extremos de
las líneas gruesas.