Análisis matricial de armaduras con Python

Capítulo 4

La geometría de todas las barras

En el capítulo 2 calculamos la geometría de una barra, y en el capítulo 3 describimos la armadura completa con diccionarios. Ahora unimos ambas cosas: calculamos la longitud y los cosenos directores de todas las barras, sin importar cuántas sean. La herramienta que lo hace posible es el bucle, la instrucción que repite un mismo cálculo para cada elemento de una colección. Con ella, nuestra calculadora deja de depender del tamaño de la armadura.

4.1 El mismo cálculo para cada barra

La geometría de una barra depende solo de las coordenadas de sus dos nodos:

L=Δx2+Δy2,c=ΔxL,s=ΔyL.L = \sqrt{\Delta x^2 + \Delta y^2}, \qquad c = \frac{\Delta x}{L}, \qquad s = \frac{\Delta y}{L}.

Para el ejemplo A, con tres barras, podríamos copiar tres veces el cálculo del capítulo 2. Pero una armadura de cubierta típica tiene entre 20 y 50 barras, y la de un puente, varios cientos. Copiar el cálculo sería tedioso y, sobre todo, una fuente segura de errores: basta con olvidar cambiar un número en una de las copias.

Lo que necesitamos es escribir el cálculo una sola vez y pedirle a Python que lo repita para cada barra del diccionario barras.

4.2 El bucle for

Un bucle (loop) for repite un bloque de instrucciones para cada elemento de una colección. En cada repetición, la variable que sigue a for toma el valor del elemento siguiente:

for numero in [1, 2, 3]:
    print(f"Barra {numero}")
Barra 1
Barra 2
Barra 3

Si recorremos un diccionario, la variable toma, una por una, sus claves. Con cada clave podemos leer el valor correspondiente:

for numero in barras:
    print(numero, barras[numero]["area"])
1 0.002
2 0.002
3 0.002

Casi siempre necesitamos la clave y el valor a la vez. El método .items() entrega ambos en cada repetición, y los recibimos en dos variables separadas por una coma:

for numero, barra in barras.items():
    i = barra["nodo_inicial"]
    j = barra["nodo_final"]
    print(f"Barra {numero}: del nodo {i} al nodo {j}")
Barra 1: del nodo 1 al nodo 2
Barra 2: del nodo 2 al nodo 3
Barra 3: del nodo 1 al nodo 3

Un uso muy frecuente de los bucles es acumular un resultado. Empezamos con un valor inicial y, en cada repetición, le sumamos algo. El operador += suma al valor que la variable ya tenía: a += b equivale a a = a + b. Así obtenemos, por ejemplo, la longitud total de las barras:

longitud_total = 0.0
for numero, barra in barras.items():
    inicial = nodos[barra["nodo_inicial"]]
    final = nodos[barra["nodo_final"]]
    dx = final["x"] - inicial["x"]
    dy = final["y"] - inicial["y"]
    longitud_total += math.sqrt(dx**2 + dy**2)
print(f"Longitud total: {longitud_total:.3f} m")
Longitud total: 16.000 m

4.3 La geometría de todas las barras

Ya tenemos todas las piezas. Recorremos las barras; para cada una, buscamos sus nodos en el diccionario nodos, calculamos las proyecciones, la longitud y los cosenos directores, y guardamos el resultado en un diccionario nuevo, geometria, con el número de barra como clave:

geometria = {}
for numero, barra in barras.items():
    inicial = nodos[barra["nodo_inicial"]]
    final = nodos[barra["nodo_final"]]
    dx = final["x"] - inicial["x"]
    dy = final["y"] - inicial["y"]
    L = math.sqrt(dx**2 + dy**2)
    geometria[numero] = {"longitud": L,
                         "c": dx / L, "s": dy / L}

Este código forma parte de nuestra calculadora: es la primera pieza que funciona para cualquier armadura descrita con los diccionarios del capítulo 3.

Para presentar los resultados recorremos geometria e imprimimos una fila por barra. En las f-strings, el número antes del punto indica el ancho de la columna: :7.3f reserva siete caracteres con tres decimales, y :>5 alinea el número de barra a la derecha en cinco caracteres. Así las columnas quedan alineadas:

print("Barra   L (m)       c       s")
for numero, g in geometria.items():
    L, c, s = g["longitud"], g["c"], g["s"]
    print(f"{numero:>5} {L:7.3f} {c:7.4f} {s:7.4f}")
Barra   L (m)       c       s
    1   6.000  1.0000  0.0000
    2   5.000 -0.6000  0.8000
    3   5.000  0.6000  0.8000

4.4 Verificación

Los valores coinciden con los que calculamos a mano en el capítulo 2: la barra 1 es horizontal (c=1c = 1, s=0s = 0), y las barras 2 y 3 son las hipotenusas de dos triángulos 3-4-5, con L=5L = 5, s=0.8s = 0.8 y c=±0.6c = \pm 0.6. El signo de cc en la barra 2 es negativo porque va del nodo 2 al nodo 3, hacia la izquierda.

Una verificación que no depende del ejemplo es que, para toda barra, debe cumplirse c2+s2=1c^2 + s^2 = 1:

for numero, g in geometria.items():
    suma = g["c"] ** 2 + g["s"] ** 2
    print(f"Barra {numero}: c² + s² = {suma:.12f}")
Barra 1: c² + s² = 1.000000000000
Barra 2: c² + s² = 1.000000000000
Barra 3: c² + s² = 1.000000000000

Finalmente, comprobamos que los datos de entrada no cambiaron: la barra 1 conserva sus cuatro datos originales y nada más.

barras[1]
{'nodo_inicial': 1, 'nodo_final': 2, 'elasticidad': 200000000.0, 'area': 0.002}

4.5 Dibujar cualquier armadura

En el capítulo 3 escribimos una línea ax.plot por cada barra. Con un bucle, el mismo dibujo funciona para cualquier armadura. Aprovechamos además para escribir el número de cada barra en su punto medio, con ax.text(x, y, texto):

fig, ax = plt.subplots()
for numero, barra in barras.items():
    inicial = nodos[barra["nodo_inicial"]]
    final = nodos[barra["nodo_final"]]
    ax.plot([inicial["x"], final["x"]],
            [inicial["y"], final["y"]], "k-o")
    # Número de la barra en su punto medio
    xm = (inicial["x"] + final["x"]) / 2
    ym = (inicial["y"] + final["y"]) / 2
    ax.text(xm, ym, f" {numero}", color="tab:blue")
ax.set_aspect("equal")
ax.set_title("Ejemplo A")
plt.show()

Problema resuelto 4.1 · La geometría del ejemplo B

Calcular la longitud y los cosenos directores de las seis barras del ejemplo B (figura 3.2) y verificar los resultados.

Solución

Aplicamos el mismo bucle a los diccionarios nodos_b y barras_b del capítulo 3, y guardamos los resultados en geometria_b:

geometria_b = {}
for numero, barra in barras_b.items():
    inicial = nodos_b[barra["nodo_inicial"]]
    final = nodos_b[barra["nodo_final"]]
    dx = final["x"] - inicial["x"]
    dy = final["y"] - inicial["y"]
    L = math.sqrt(dx**2 + dy**2)
    geometria_b[numero] = {"longitud": L,
                           "c": dx / L, "s": dy / L}

print("Barra   L (m)       c       s")
for numero, g in geometria_b.items():
    L, c, s = g["longitud"], g["c"], g["s"]
    print(f"{numero:>5} {L:7.3f} {c:7.4f} {s:7.4f}")
Barra   L (m)       c       s
    1   3.000  1.0000  0.0000
    2   4.000  0.0000  1.0000
    3   3.000  1.0000  0.0000
    4   4.000  0.0000  1.0000
    5   5.000  0.6000  0.8000
    6   5.000 -0.6000  0.8000

Verificación. Las barras 1 y 3 son horizontales de 3 m y las barras 2 y 4, verticales de 4 m. Las diagonales 5 y 6 miden 5 m; la diagonal 5 sube hacia la derecha (c>0c > 0) y la 6, hacia la izquierda (c<0c < 0), como se ve en la figura.

Para resolver el problema copiamos el bucle y cambiamos los nombres de los diccionarios. Funciona, pero es justamente la clase de repetición que queríamos evitar. En el capítulo 5 aprenderemos a darle un nombre a un cálculo, con una función, para usarlo con cualquier armadura sin copiarlo.

Repaso y resumen del capítulo

Ejercicios

Ejercicio 4.1

Modificar el bucle de la geometría para calcular también el ángulo θ\theta de cada barra del ejemplo A, en grados, y mostrarlo para las tres barras.

Ver la solución
geometria_con_angulo = {}
for numero, barra in barras.items():
    inicial = nodos[barra["nodo_inicial"]]
    final = nodos[barra["nodo_final"]]
    dx = final["x"] - inicial["x"]
    dy = final["y"] - inicial["y"]
    theta = math.degrees(math.atan2(dy, dx))
    geometria_con_angulo[numero] = {"theta": theta}

for numero, g in geometria_con_angulo.items():
    print(f"Barra {numero}: θ = {g['theta']:.2f}°")
Barra 1: θ = 0.00°
Barra 2: θ = 126.87°
Barra 3: θ = 53.13°

Ejercicio 4.2

El peso específico del acero es γ=78.5 kN/m3\gamma = 78.5\,\mathrm{kN/m^3}. Calcular el peso propio total de las barras del ejemplo A, sabiendo que el peso de una barra es γAL\gamma A L.

Ver la solución

Usamos las longitudes ya calculadas en el diccionario geometria:

gamma = 78.5  # peso específico del acero, en kN/m³
peso_total = 0.0
for numero, barra in barras.items():
    L = geometria[numero]["longitud"]
    peso_total += gamma * barra["area"] * L
print(f"Peso propio: {peso_total:.3f} kN")
Peso propio: 2.512 kN

Ejercicio 4.3

Calcular la longitud total de las barras del ejemplo B con un acumulador.

Ver la solución
longitud_total_b = 0.0
for numero, barra in barras_b.items():
    inicial = nodos_b[barra["nodo_inicial"]]
    final = nodos_b[barra["nodo_final"]]
    dx = final["x"] - inicial["x"]
    dy = final["y"] - inicial["y"]
    longitud_total_b += math.sqrt(dx**2 + dy**2)
print(f"Longitud total: {longitud_total_b:.3f} m")
Longitud total: 24.000 m

El resultado coincide con la suma de las longitudes de la tabla del problema resuelto 4.1: 3+4+3+4+5+5=24 m3 + 4 + 3 + 4 + 5 + 5 = 24\,\mathrm{m}.

Ejercicio 4.4

Dibujar la armadura del ejemplo B con un bucle, numerando sus barras, y comparar el dibujo con la figura 3.2.

Ver la solución
fig, ax = plt.subplots()
for numero, barra in barras_b.items():
    inicial = nodos_b[barra["nodo_inicial"]]
    final = nodos_b[barra["nodo_final"]]
    ax.plot([inicial["x"], final["x"]],
            [inicial["y"], final["y"]], "k-o")
    xm = (inicial["x"] + final["x"]) / 2
    ym = (inicial["y"] + final["y"]) / 2
    ax.text(xm, ym, f" {numero}", color="tab:blue")
ax.set_aspect("equal")
ax.set_title("Ejemplo B")
plt.show()

Los números de las diagonales 5 y 6 se superponen, porque ambas barras tienen el mismo punto medio. Una mejora sencilla es ubicar cada número en otro punto de su barra, por ejemplo al 40 % de su longitud desde el nodo inicial: xm = inicial["x"] + 0.4 * (final["x"] - inicial["x"]), y lo mismo para ym.