Análisis matricial de armaduras con Python

Capítulo 3

Describir una armadura

Antes de calcular cualquier cosa, nuestra calculadora necesita conocer la armadura: dónde están sus nodos, qué barras los unen, de qué material y sección son, cómo está apoyada y qué cargas recibe. En el capítulo anterior guardamos cada coordenada en una variable suelta; con tres barras eso ya resulta incómodo, y con cincuenta sería imposible. En este capítulo aprendemos a organizar toda esa información en estructuras de datos de Python, y construimos el modelo completo del ejemplo A.

3.1 Los datos de una armadura

Recordemos las hipótesis con las que analizamos armaduras: las barras son rectas y se unen en nodos articulados, las cargas actúan solo en los nodos y el material es elástico lineal. Con ellas, cada barra trabaja únicamente a fuerza axial y la armadura queda definida por cuatro grupos de datos:

La figura 3.1 y la tabla 3.1 reúnen estos datos para el ejemplo A, que nos acompañará en los próximos capítulos.

Figura 3.1:

Ejemplo A: armadura triangular isostática.

Tabla 3.1: Datos del ejemplo A. Todas las barras son de acero (E=200 GPaE = 200\,\mathrm{GPa}) con un área de 20 cm².

NodoCoordenadas (m)ApoyoCarga (kN)
1(0,0)(0, 0)articulado—
2(6,0)(6, 0)rodillo (impide yy)—
3(3,4)(3, 4)—Fx=20F_x = 20, Fy=−50F_y = -50
BarraNodo inicialNodo final
112
223
313

3.2 Listas

Una lista (list) es una colección ordenada de valores, escrita entre corchetes y separada por comas. Por ejemplo, las coordenadas del nodo 3:

coordenadas_nodo_3 = [3.0, 4.0]
coordenadas_nodo_3
[3.0, 4.0]

Cada elemento ocupa una posición, llamada índice (index), y se obtiene escribiendo el índice entre corchetes:

coordenadas_nodo_3[0], coordenadas_nodo_3[1]
(3.0, 4.0)

Ese desfase se vuelve peligroso cuando usamos una lista para guardar propiedades de las barras. Si guardamos las áreas de las barras 1, 2 y 3 en una lista (aquí con un área distinta en la barra 3, para notar la diferencia), el área de la barra 3 está en la posición 2:

# Áreas de las barras 1, 2 y 3, en m²
areas = [0.0020, 0.0020, 0.0015]
areas[2]
0.0015

Las listas pueden crecer: el método append agrega un elemento al final, y la función len cuenta los elementos:

areas.append(0.0030)
len(areas), areas
(4, [0.002, 0.002, 0.0015, 0.003])

3.3 Diccionarios

Un diccionario (dictionary) guarda pares clave: valor, entre llaves. A diferencia de la lista, cada valor se obtiene por su clave, no por su posición:

nodo_3 = {"x": 3.0, "y": 4.0}
nodo_3["y"]
4.0

Las claves pueden ser textos, como "x" e "y", o números. Con números como claves resolvemos el problema de la sección anterior: el área de la barra 3 se obtiene con la clave 3, sin traducciones.

# Las claves son los números de barra, empezando en 1
areas = {1: 0.0020, 2: 0.0020, 3: 0.0015}
areas[3]
0.0015

3.4 El modelo del ejemplo A

Un valor de un diccionario puede ser, a su vez, otro diccionario. Con esta idea, los diccionarios anidados (nested dictionaries), escribimos el modelo completo. Primero los nodos, cada uno con sus coordenadas:

# Ejemplo A. Unidades: kN y m
nodos = {
    1: {"x": 0.0, "y": 0.0},
    2: {"x": 6.0, "y": 0.0},
    3: {"x": 3.0, "y": 4.0},
}

Después las barras, cada una con sus nodos y sus propiedades. Un diccionario puede ocupar varias líneas: Python sabe que continúa mientras la llave no se haya cerrado.

# E en kN/m² (acero) y área en m²
barras = {
    1: {"nodo_inicial": 1, "nodo_final": 2,
        "elasticidad": 2.0e8, "area": 0.0020},
    2: {"nodo_inicial": 2, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0020},
    3: {"nodo_inicial": 1, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0020},
}

Los apoyos indican, para cada nodo apoyado, si el desplazamiento en xx y en yy está impedido. Los valores True (verdadero) y False (falso) son los booleanos (booleans) de Python:

# True: el desplazamiento en esa dirección está impedido
apoyos = {
    1: {"x": True, "y": True},   # articulado
    2: {"x": False, "y": True},  # rodillo
}

Por último, las cargas, con su signo según los ejes globales:

# Cargas en kN, positivas en el sentido de los ejes
cargas = {
    3: {"x": 20.0, "y": -50.0},
}

Separar los apoyos y las cargas de los nodos tiene una ventaja: los nodos sin apoyo o sin carga simplemente no aparecen en esos diccionarios, sin entradas vacías que mantener.

3.5 Consultar el modelo

Para leer un valor anidado encadenamos las claves. Los nodos de la barra 2:

barras[2]["nodo_inicial"], barras[2]["nodo_final"]
(2, 3)

Y las coordenadas del nodo inicial de la barra 2, usando el número de nodo como clave del diccionario nodos:

inicial = barras[2]["nodo_inicial"]
nodos[inicial]["x"], nodos[inicial]["y"]
(6.0, 0.0)

Esta consulta encadenada, de la barra a sus nodos y de los nodos a sus coordenadas, es exactamente la que necesitaremos en el capítulo 4 para calcular la geometría de todas las barras.

El operador in pregunta si una clave existe en un diccionario. El nodo 3 no tiene apoyo, pero sí carga:

3 in apoyos, 3 in cargas
(False, True)

Pedir una clave que no existe, como apoyos[3], produce el error KeyError y detiene el programa. El método get devuelve en cambio el valor None («nada»), o el valor que indiquemos:

print(apoyos.get(3))
print(apoyos.get(3, "sin apoyo"))
None
sin apoyo

Finalmente, len también cuenta las entradas de un diccionario:

print(f"Nodos: {len(nodos)}, barras: {len(barras)}")
Nodos: 3, barras: 3

3.6 Ver la armadura

Un error en las coordenadas o en la conectividad es fácil de cometer y difícil de detectar leyendo números. La mejor verificación es ver la armadura. Para dibujar usamos Matplotlib, la biblioteca de gráficos más usada de Python, que importamos con el nombre corto plt:

import matplotlib.pyplot as plt

plt.subplots() crea una figura con unos ejes; ax.plot dibuja una línea entre los puntos cuyas abscisas y ordenadas recibe en dos listas, y el texto "k-o" pide línea negra (k) continua (-) con círculos en los extremos (o). Dibujamos así cada barra:

n1, n2, n3 = nodos[1], nodos[2], nodos[3]

fig, ax = plt.subplots()
# Barra 1: del nodo 1 al nodo 2
ax.plot([n1["x"], n2["x"]], [n1["y"], n2["y"]], "k-o")
# Barra 2: del nodo 2 al nodo 3
ax.plot([n2["x"], n3["x"]], [n2["y"], n3["y"]], "k-o")
# Barra 3: del nodo 1 al nodo 3
ax.plot([n1["x"], n3["x"]], [n1["y"], n3["y"]], "k-o")
ax.set_aspect("equal")
ax.set_xlabel("x (m)")
ax.set_ylabel("y (m)")
ax.set_title("Ejemplo A")
plt.show()

La instrucción ax.set_aspect("equal") usa la misma escala en ambos ejes, para que la armadura no se vea deformada.

Verificación. El dibujo coincide con la figura 3.1: tres nodos en (0,0)(0, 0), (6,0)(6, 0) y (3,4)(3, 4), unidos por tres barras.

Figura 3.2:

Ejemplo B: cuadro arriostrado hiperestático.

Problema resuelto 3.1 · El modelo del ejemplo B

Escribir el modelo de datos del ejemplo B (figura 3.2): un cuadro de 3 m por 4 m con sus dos diagonales, articulado en el nodo 1 y con un rodillo en el nodo 2. Todas las barras son de acero, con un área de 20 cm². Verificar la cantidad de nodos y de barras.

Solución

Seguimos la estrategia de resolución: la numeración ya está en la figura, las unidades son kN y m, y los diccionarios llevan el sufijo _b para no reemplazar los del ejemplo A:

# Ejemplo B. Unidades: kN y m
nodos_b = {
    1: {"x": 0.0, "y": 0.0},
    2: {"x": 3.0, "y": 0.0},
    3: {"x": 0.0, "y": 4.0},
    4: {"x": 3.0, "y": 4.0},
}

barras_b = {
    1: {"nodo_inicial": 1, "nodo_final": 2,
        "elasticidad": 2.0e8, "area": 0.0020},
    2: {"nodo_inicial": 2, "nodo_final": 4,
        "elasticidad": 2.0e8, "area": 0.0020},
    3: {"nodo_inicial": 3, "nodo_final": 4,
        "elasticidad": 2.0e8, "area": 0.0020},
    4: {"nodo_inicial": 1, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0020},
    5: {"nodo_inicial": 1, "nodo_final": 4,
        "elasticidad": 2.0e8, "area": 0.0020},
    6: {"nodo_inicial": 2, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0020},
}

apoyos_b = {
    1: {"x": True, "y": True},   # articulado
    2: {"x": False, "y": True},  # rodillo
}

cargas_b = {
    3: {"x": 30.0, "y": -40.0},
    4: {"x": 0.0, "y": -40.0},
}

Verificación. Contamos los nodos y las barras, y revisamos una de las diagonales:

inicial = barras_b[5]["nodo_inicial"]
final = barras_b[5]["nodo_final"]
print(f"Nodos: {len(nodos_b)}, barras: {len(barras_b)}")
print(f"Barra 5: del nodo {inicial} al nodo {final}")
Nodos: 4, barras: 6
Barra 5: del nodo 1 al nodo 4

La cantidad de nodos y de barras coincide con la figura, y la barra 5 es la diagonal que va del nodo 1 al nodo 4. Este modelo nos acompañará, junto con el del ejemplo A, en los próximos capítulos.

Repaso y resumen del capítulo

Ejercicios

Ejercicio 3.1

Usando los diccionarios del ejemplo A, obtener las coordenadas de los nodos de la barra 3 a partir de barras[3], sin escribir los números de nodo a mano, y calcular la longitud de la barra.

Ver la solución
inicial = nodos[barras[3]["nodo_inicial"]]
final = nodos[barras[3]["nodo_final"]]
dx = final["x"] - inicial["x"]
dy = final["y"] - inicial["y"]
L = math.sqrt(dx**2 + dy**2)
print(f"L = {L:.3f} m")
L = 5.000 m

La consulta encadenada nodos[barras[3]["nodo_inicial"]] obtiene el nodo desde la barra, sin escribir su número a mano.

Ejercicio 3.2

Una armadura de cubierta tiene los nodos 1 en (0,0)(0, 0), 2 en (4,0)(4, 0), 3 en (8,0)(8, 0) y 4 en (4,1.5)(4, 1.5), en metros. Sus barras son 1: 1–2, 2: 2–3, 3: 1–4, 4: 4–3 y 5: 2–4, todas de acero; las cuatro primeras tienen un área de 15 cm² y la barra 5, de 10 cm². Está articulada en el nodo 1, tiene un rodillo en el nodo 3 y una carga de 30 kN hacia abajo en el nodo 4. Escribir su modelo con los nombres nodos_cubierta, barras_cubierta, apoyos_cubierta y cargas_cubierta.

Ver la solución
# Armadura de cubierta. Unidades: kN y m
nodos_cubierta = {
    1: {"x": 0.0, "y": 0.0},
    2: {"x": 4.0, "y": 0.0},
    3: {"x": 8.0, "y": 0.0},
    4: {"x": 4.0, "y": 1.5},
}

barras_cubierta = {
    1: {"nodo_inicial": 1, "nodo_final": 2,
        "elasticidad": 2.0e8, "area": 0.0015},
    2: {"nodo_inicial": 2, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0015},
    3: {"nodo_inicial": 1, "nodo_final": 4,
        "elasticidad": 2.0e8, "area": 0.0015},
    4: {"nodo_inicial": 4, "nodo_final": 3,
        "elasticidad": 2.0e8, "area": 0.0015},
    5: {"nodo_inicial": 2, "nodo_final": 4,
        "elasticidad": 2.0e8, "area": 0.0010},
}

apoyos_cubierta = {
    1: {"x": True, "y": True},   # articulado
    3: {"x": False, "y": True},  # rodillo
}

cargas_cubierta = {
    4: {"x": 0.0, "y": -30.0},
}

print(f"Nodos: {len(nodos_cubierta)}")
print(f"Barras: {len(barras_cubierta)}")
Nodos: 4
Barras: 5

Ejercicio 3.3

Predecir, sin ejecutarlo, qué muestran apoyos.get(3) y 3 in cargas, 1 in cargas para el ejemplo A. Comprobar la predicción en el notebook.

Ver la solución
print(apoyos.get(3))
print(3 in cargas, 1 in cargas)
None
True False

El nodo 3 no tiene apoyo, de modo que get devuelve None. El nodo 3 tiene carga y el nodo 1 no.

Ejercicio 3.4

Dibujar la armadura del ejemplo B con Matplotlib y comparar el dibujo con la figura 3.2.

Ver la solución
n1, n2 = nodos_b[1], nodos_b[2]
n3, n4 = nodos_b[3], nodos_b[4]

fig, ax = plt.subplots()
ax.plot([n1["x"], n2["x"]], [n1["y"], n2["y"]], "k-o")
ax.plot([n2["x"], n4["x"]], [n2["y"], n4["y"]], "k-o")
ax.plot([n3["x"], n4["x"]], [n3["y"], n4["y"]], "k-o")
ax.plot([n1["x"], n3["x"]], [n1["y"], n3["y"]], "k-o")
ax.plot([n1["x"], n4["x"]], [n1["y"], n4["y"]], "k-o")
ax.plot([n2["x"], n3["x"]], [n2["y"], n3["y"]], "k-o")
ax.set_aspect("equal")
ax.set_title("Ejemplo B")
plt.show()