Análisis matricial de armaduras con Python

Capítulo 12

Mi calculadora de armaduras

A lo largo de los capítulos anteriores construimos, pieza por pieza, todas las etapas del método de rigidez. Cada una es una función que podemos leer, entender y verificar. Pero para analizar una armadura todavía tenemos que llamarlas una por una, en el orden correcto, como hicimos cada vez con el ejemplo B. En este capítulo damos el último paso: reunimos todas las funciones en una calculadora que, con los datos de una armadura, entrega sus desplazamientos, reacciones y fuerzas, verifica los resultados y los dibuja. Y la ponemos a prueba con una armadura de puente de 21 barras.

12.1 El camino recorrido

La figura 12.1 resume la calculadora. Los datos describen la armadura (capítulo 3); de ellos salen la geometría y la rigidez de cada barra (capítulos 4 y 5); la numeración de los grados de libertad (capítulo 6) permite ensamblar el sistema y particionarlo (capítulos 7 y 8); su solución da los desplazamientos y las reacciones (capítulo 9), y de los desplazamientos salen las fuerzas axiales (capítulo 10). Por último, los resultados se presentan, se verifican y se dibujan.

Figura 12.1:

Las etapas de la calculadora, cada una con el capítulo donde la construimos. La función analizar\_armadura encadena los pasos de cálculo.

12.2 Una función que lo hace todo

Una función puede llamar a otras funciones. La que escribimos ahora no calcula nada por sí misma: llama, en orden, a todas las que escribimos antes, pasando el resultado de cada etapa a la siguiente. Es la traducción directa de la figura 12.1:

def analizar_armadura(nodos, barras, apoyos, cargas):
    """Análisis completo de una armadura plana.

    Devuelve un diccionario con los resultados."""
    geometria = calcular_geometria(nodos, barras)
    rigideces = calcular_rigideces(barras, geometria)
    K = ensamblar_rigidez(nodos, barras, rigideces)
    F = ensamblar_cargas(nodos, cargas)
    libres, restringidos = clasificar_gdl(nodos, apoyos)
    K_ll, K_lr, K_rl, K_rr = particionar(K, libres,
                                         restringidos)
    U_r = np.zeros(len(restringidos))
    U_l = resolver_desplazamientos(
        K_ll, K_lr, F[indices(libres)], U_r)
    U = np.zeros(2 * len(nodos))
    U[indices(libres)] = U_l
    U[indices(restringidos)] = U_r
    R = calcular_reacciones(K_rl, K_rr, U_l, U_r,
                            F[indices(restringidos)])
    N = fuerzas_axiales(barras, geometria, U)
    return {"geometria": geometria, "K": K, "F": F,
            "U": U, "R": R, "N": N, "libres": libres,
            "restringidos": restringidos}

Con una sola llamada obtenemos todos los resultados del ejemplo A, y las fuerzas son, como debe ser, las mismas del capítulo 10:

resultados = analizar_armadura(nodos, barras, apoyos,
                               cargas)
print(list(resultados.keys()))
for numero, fuerza in resultados["N"].items():
    print(f"Barra {numero}: N = {fuerza:.2f} kN")
['geometria', 'K', 'F', 'U', 'R', 'N', 'libres', 'restringidos']
Barra 1: N = 28.75 kN
Barra 2: N = -47.92 kN
Barra 3: N = -14.58 kN

12.3 El informe de resultados

Con los resultados en un diccionario, escribimos una función que los presenta en tres tablas: los desplazamientos de cada nodo, las reacciones y las fuerzas axiales. Para las reacciones necesitamos el nodo y la dirección de cada grado de libertad restringido gg, que obtenemos invirtiendo la numeración del capítulo 6: el nodo es (g+1)/2(g + 1) / 2 redondeado hacia abajo, y la dirección es xx si gg es impar.

def mostrar_resultados(nodos, barras, resultados):
    """Tablas de desplazamientos, reacciones y fuerzas."""
    U = resultados["U"]
    R = resultados["R"]
    N = resultados["N"]
    print("Desplazamientos de los nodos (mm)")
    filas = []
    for nodo in nodos:
        ix, iy = indices(gdl_del_nodo(nodo))
        filas.append([str(nodo), U[ix] * 1000,
                      U[iy] * 1000])
    mostrar_tabla(["Nodo", "ux", "uy"], filas,
                  decimales=3)

    print("\nReacciones (kN)")
    filas = []
    restringidos = resultados["restringidos"]
    for posicion, g in enumerate(restringidos):
        nodo = (g + 1) // 2
        if g % 2 == 1:
            direccion = "x"
        else:
            direccion = "y"
        filas.append([str(nodo), direccion, R[posicion]])
    mostrar_tabla(["Nodo", "Dirección", "R"], filas)

    print("\nFuerzas axiales")
    filas = []
    for numero, fuerza in N.items():
        sigma = fuerza / barras[numero]["area"] / 1000
        filas.append([str(numero), fuerza, sigma,
                      clasificar_fuerza(fuerza)])
    encabezados = ["Barra", "N (kN)", "σ (MPa)", "Estado"]
    mostrar_tabla(encabezados, filas)
mostrar_resultados(nodos, barras, resultados)
Desplazamientos de los nodos (mm)
       Nodo         ux         uy
          1      0.000      0.000
          2      0.431      0.000
          3      0.563     -0.650

Reacciones (kN)
       Nodo  Dirección          R
          1          x     -20.00
          1          y      11.67
          2          y      38.33

Fuerzas axiales
      Barra     N (kN)    σ (MPa)     Estado
          1      28.75      14.38   tracción
          2     -47.92     -23.96 compresión
          3     -14.58      -7.29 compresión

La verificación del equilibrio, global y de cada nodo, también se convierte en una función, que reúne las de los capítulos 9 y 10 e informa si ambas se cumplen:

def verificar_resultados(nodos, barras, resultados,
                         tolerancia=1e-6):
    """Equilibrio global y de cada nodo. Devuelve True
    si ambos se cumplen dentro de la tolerancia."""
    F, R = resultados["F"], resultados["R"]
    restringidos = resultados["restringidos"]
    suma_x, suma_y, momento = comprobar_equilibrio(
        nodos, F, R, restringidos)
    global_ok = max(abs(suma_x), abs(suma_y),
                    abs(momento)) < tolerancia
    nodal_ok = equilibrio_nodal(
        nodos, barras, resultados["geometria"],
        resultados["N"], F, R, restringidos) < tolerancia
    for nombre, correcto in (("global", global_ok),
                             ("nodal", nodal_ok)):
        if correcto:
            print(f"Equilibrio {nombre}: correcto")
        else:
            print(f"Equilibrio {nombre}: ERROR")
    return global_ok and nodal_ok

Y lo mismo con los gráficos del capítulo 11. Para que las armaduras largas y bajas, como las de los puentes, se vean bien, la función coloca los dos paneles uno sobre otro cuando la armadura es más de dos veces más ancha que alta:

def graficar_resultados(nodos, barras, resultados,
                        titulo="", decimales=1,
                        tamano=None):
    """Deformada y diagrama de fuerzas, en dos paneles:
    uno sobre otro si la armadura es muy alargada."""
    U, N = resultados["U"], resultados["N"]
    xs = []
    ys = []
    for punto in nodos.values():
        xs.append(punto["x"])
        ys.append(punto["y"])
    ancho = max(xs) - min(xs)
    alto = max(ys) - min(ys)
    if ancho > 2 * alto:
        fig, (ax1, ax2) = plt.subplots(2, 1,
                                       figsize=(4.5, 3.6))
    else:
        fig, (ax1, ax2) = plt.subplots(1, 2,
                                       figsize=(4.5, 2.4))
    escala = escala_automatica(nodos, U)
    dibujar_armadura(ax1, nodos, barras, estilo="--")
    deformados = nodos_desplazados(nodos, U, escala)
    dibujar_armadura(ax1, deformados, barras,
                     color=PRINCIPAL)
    ax1.set_title(f"Deformada (×{escala:.0f})")
    ax1.axis("off")
    dibujar_fuerzas(ax2, nodos, barras, N, decimales,
                    tamano)
    ax2.set_title("Fuerzas axiales (kN)")
    fig.suptitle(titulo)
    fig.tight_layout()
    plt.show()

Con estas funciones, el ejemplo B, que en cada capítulo nos costó una docena de líneas, se resuelve, verifica y dibuja en tres:

resultados_b = analizar_armadura(nodos_b, barras_b,
                                 apoyos_b, cargas_b)
verificar_resultados(nodos_b, barras_b, resultados_b)
graficar_resultados(nodos_b, barras_b, resultados_b,
                    "Ejemplo B")
Equilibrio global: correcto
Equilibrio nodal: correcto

12.4 El ejemplo C: un puente peatonal

Pongamos la calculadora a prueba con una armadura de verdad: un puente peatonal de tipo Pratt, de 18 m de luz y 3 m de altura, con seis paneles de 3 m (figura 12.2). Los cordones y las diagonales extremas tienen 30 cm² de área; los montantes y las diagonales interiores, 15 cm². El tablero transmite una carga de 25 kN a cada nodo interior del cordón inferior.

Figura 12.2:

Ejemplo C: puente peatonal tipo Pratt, con la numeración de sus 12 nodos y 21 barras.

Con 12 nodos y 21 barras, escribir los datos a mano como en el capítulo 3 sería largo y propenso a errores. Las coordenadas siguen un patrón, así que las generamos con bucles. Para las barras escribimos una pequeña función que arma el diccionario de cada una, de modo que cada barra ocupa una sola expresión. Las áreas se definen una vez como constantes, en mayúsculas por convención:

# Ejemplo C: puente peatonal tipo Pratt, 6 paneles de
# 3 m (luz de 18 m) y 3 m de altura. Unidades: kN y m

# Nodos: cordón inferior (1 a 7) y superior (8 a 12)
nodos_c = {}
for k in range(7):
    nodos_c[k + 1] = {"x": 3.0 * k, "y": 0.0}
for k in range(5):
    nodos_c[k + 8] = {"x": 3.0 * (k + 1), "y": 3.0}


def barra(i, j, area, elasticidad=2.0e8):
    """Datos de una barra del nodo i al nodo j."""
    return {"nodo_inicial": i, "nodo_final": j,
            "elasticidad": elasticidad, "area": area}


A_CORDON = 0.0030  # m², cordones y diagonales extremas
A_ALMA = 0.0015    # m², montantes y diagonales

barras_c = {
    # Cordón inferior
    1: barra(1, 2, A_CORDON), 2: barra(2, 3, A_CORDON),
    3: barra(3, 4, A_CORDON), 4: barra(4, 5, A_CORDON),
    5: barra(5, 6, A_CORDON), 6: barra(6, 7, A_CORDON),
    # Cordón superior
    7: barra(8, 9, A_CORDON), 8: barra(9, 10, A_CORDON),
    9: barra(10, 11, A_CORDON),
    10: barra(11, 12, A_CORDON),
    # Diagonales extremas
    11: barra(1, 8, A_CORDON), 12: barra(12, 7, A_CORDON),
    # Montantes
    13: barra(2, 8, A_ALMA), 14: barra(3, 9, A_ALMA),
    15: barra(4, 10, A_ALMA), 16: barra(5, 11, A_ALMA),
    17: barra(6, 12, A_ALMA),
    # Diagonales, descendiendo hacia el centro
    18: barra(8, 3, A_ALMA), 19: barra(9, 4, A_ALMA),
    20: barra(11, 4, A_ALMA), 21: barra(12, 5, A_ALMA),
}

apoyos_c = {
    1: {"x": True, "y": True},   # articulado
    7: {"x": False, "y": True},  # rodillo
}

# 25 kN en cada nodo interior del cordón inferior
cargas_c = {}
for nodo in range(2, 7):
    cargas_c[nodo] = {"x": 0.0, "y": -25.0}

El análisis completo, con su verificación y sus gráficos, es igual que para cualquier otra armadura:

resultados_c = analizar_armadura(nodos_c, barras_c,
                                 apoyos_c, cargas_c)
verificar_resultados(nodos_c, barras_c, resultados_c)
graficar_resultados(nodos_c, barras_c, resultados_c,
                    "Ejemplo C: puente Pratt",
                    decimales=0, tamano=7)
Equilibrio global: correcto
Equilibrio nodal: correcto

Los resultados muestran el comportamiento típico de una armadura Pratt: el cordón superior trabaja en compresión y el inferior en tracción, como las fibras superior e inferior de una viga; las diagonales interiores, inclinadas hacia el centro, trabajan en tracción, mientras que las extremas, que llevan la carga a los apoyos, y los montantes interiores trabajan en compresión. El montante central no tiene fuerza, y los montantes extremos, de los que «cuelga» la carga de los nodos 2 y 6, trabajan en tracción. La flecha en el centro del vano es de 6.74 mm, del orden de L/2700L/2700, y cada apoyo recibe 62.5 kN, la mitad de la carga total. Las tablas completas son:

mostrar_resultados(nodos_c, barras_c, resultados_c)
Desplazamientos de los nodos (mm)
       Nodo         ux         uy
          1      0.000      0.000
          2      0.313     -3.321
          3      0.625     -5.695
          4      1.125     -6.736
          5      1.625     -5.695
          6      1.938     -3.321
          7      2.250      0.000
          8      2.188     -3.071
          9      1.688     -5.820
         10      1.125     -6.736
         11      0.563     -5.820
         12      0.063     -3.071

Reacciones (kN)
       Nodo  Dirección          R
          1          x       0.00
          1          y      62.50
          7          y      62.50

Fuerzas axiales
      Barra     N (kN)    σ (MPa)     Estado
          1      62.50      20.83   tracción
          2      62.50      20.83   tracción
          3     100.00      33.33   tracción
          4     100.00      33.33   tracción
          5      62.50      20.83   tracción
          6      62.50      20.83   tracción
          7    -100.00     -33.33 compresión
          8    -112.50     -37.50 compresión
          9    -112.50     -37.50 compresión
         10    -100.00     -33.33 compresión
         11     -88.39     -29.46 compresión
         12     -88.39     -29.46 compresión
         13      25.00      16.67   tracción
         14     -12.50      -8.33 compresión
         15       0.00       0.00 sin fuerza
         16     -12.50      -8.33 compresión
         17      25.00      16.67   tracción
         18      53.03      35.36   tracción
         19      17.68      11.79   tracción
         20      17.68      11.79   tracción
         21      53.03      35.36   tracción

Problema resuelto 12.1 · El método de las secciones

Verificar las fuerzas de las barras 3 (cordón inferior) y 8 (cordón superior) del ejemplo C con el método de las secciones.

Solución

Cortamos la armadura por el tercer panel, entre x=6x = 6 y x=9x = 9 m, y estudiamos el tramo izquierdo. Por simetría, cada reacción vale la mitad de la carga total, R=5⋅25/2=62.5 kNR = 5 \cdot 25 / 2 = 62.5\,\mathrm{kN}. Tomando momentos respecto del nodo 9, en (6,3)(6, 3), solo la barra 3 tiene momento entre las barras cortadas; respecto del nodo 4, en (9,0)(9, 0), solo la barra 8:

N3=62.5⋅6−25⋅33,N8=−62.5⋅9−25⋅6−25⋅33.N_3 = \frac{62.5 \cdot 6 - 25 \cdot 3}{3}, \qquad N_8 = -\frac{62.5 \cdot 9 - 25 \cdot 6 - 25 \cdot 3}{3}.
# Corte por el tercer panel (x entre 6 y 9 m)
R1 = 5 * 25.0 / 2  # reacción en cada apoyo, por simetría
h = 3.0
# Momentos respecto del nodo 9 (x = 6): cordón inferior
N_inferior = (R1 * 6 - 25.0 * 3) / h
# Momentos respecto del nodo 4 (x = 9): cordón superior
N_superior = -(R1 * 9 - 25.0 * 6 - 25.0 * 3) / h

N_c = resultados_c["N"]
print(f"Barra 3: secciones {N_inferior:8.2f} kN, "
      f"calculadora {N_c[3]:8.2f} kN")
print(f"Barra 8: secciones {N_superior:8.2f} kN, "
      f"calculadora {N_c[8]:8.2f} kN")
Barra 3: secciones   100.00 kN, calculadora   100.00 kN
Barra 8: secciones  -112.50 kN, calculadora  -112.50 kN

Verificación. Ambos métodos dan los mismos valores: la calculadora reproduce el análisis clásico de una armadura isostática.

12.5 Mi calculadora

Llegamos al producto final del libro: un notebook propio, organizado en tres secciones.

Separar el motor de los datos es la lección más valiosa de este capítulo: la misma herramienta, escrita y verificada una vez, sirve para cualquier armadura.

Los errores más frecuentes al usar la calculadora, y cómo reconocerlos, son los de la tabla 12.1.

Tabla 12.1: Errores frecuentes al usar la calculadora.

SíntomaCausa probableSolución
KeyErrorBarra con un nodo inexistenteRevisar barras
ZeroDivisionErrorDos nodos coincidenRevisar nodos
«Es inestable»Faltan barras o apoyosRevisar el dibujo
Desplazamientos enormesUnidades mezcladasPasar a kN y m
El dibujo no coincideDatos mal escritosCorregir los datos

Repaso y resumen del capítulo

Ejercicios

Ejercicio 12.1

Calcular la flecha (el desplazamiento vertical hacia abajo) del nodo 4 del ejemplo C, en el centro del vano, y la relación entre la luz y la flecha.

Ver la solución

El nodo 4 tiene los grados de libertad 7 y 8; la flecha es el desplazamiento vertical con el signo cambiado:

resultados_c = analizar_armadura(nodos_c, barras_c,
                                 apoyos_c, cargas_c)
iy = indices([8])[0]  # gdl 8: nodo 4, dirección y
flecha = -resultados_c["U"][iy] * 1000
print(f"Flecha en el centro: {flecha:.2f} mm")
print(f"Luz / flecha: {18000 / flecha:.0f}")
Flecha en el centro: 6.74 mm
Luz / flecha: 2672

Una relación luz/flecha del orden de 2700 es muy holgada: los límites de servicio habituales están entre 300 y 1000.

Ejercicio 12.2

Analizar con la calculadora la armadura de cubierta del ejercicio 3.2. ¿Por qué la barra 5 no tiene fuerza?

Ver la solución

Usamos la función barra del ejemplo C para escribir las barras:

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: barra(1, 2, 0.0015), 2: barra(2, 3, 0.0015),
    3: barra(1, 4, 0.0015), 4: barra(4, 3, 0.0015),
    5: barra(2, 4, 0.0010),
}
apoyos_cubierta = {
    1: {"x": True, "y": True},
    3: {"x": False, "y": True},
}
cargas_cubierta = {4: {"x": 0.0, "y": -30.0}}

resultados = analizar_armadura(
    nodos_cubierta, barras_cubierta, apoyos_cubierta,
    cargas_cubierta)
verificar_resultados(nodos_cubierta, barras_cubierta,
                     resultados)
mostrar_resultados(nodos_cubierta, barras_cubierta,
                   resultados)
Equilibrio global: correcto
Equilibrio nodal: correcto
Desplazamientos de los nodos (mm)
       Nodo         ux         uy
          1      0.000      0.000
          2      0.533     -3.155
          3      1.067      0.000
          4      0.533     -3.155

Reacciones (kN)
       Nodo  Dirección          R
          1          x       0.00
          1          y      15.00
          3          y      15.00

Fuerzas axiales
      Barra     N (kN)    σ (MPa)     Estado
          1      40.00      26.67   tracción
          2      40.00      26.67   tracción
          3     -42.72     -28.48 compresión
          4     -42.72     -28.48 compresión
          5       0.00       0.00 sin fuerza

En el nodo 2 concurren las barras 1 y 2, horizontales, y la barra 5, vertical. Como el nodo 2 no tiene carga, el equilibrio vertical exige que la barra 5 no tenga fuerza.

Ejercicio 12.3

Agregar al ejemplo C una carga de viento de 10 kN horizontales en cada nodo del cordón superior, además de las cargas verticales, y comparar el diagrama de fuerzas con el original.

Ver la solución
# Viento: 10 kN horizontales en cada nodo superior
cargas_viento = {}
for nodo in range(2, 7):
    cargas_viento[nodo] = {"x": 0.0, "y": -25.0}
for nodo in range(8, 13):
    cargas_viento[nodo] = {"x": 10.0, "y": 0.0}

resultados_v = analizar_armadura(nodos_c, barras_c,
                                 apoyos_c, cargas_viento)
verificar_resultados(nodos_c, barras_c, resultados_v)
graficar_resultados(nodos_c, barras_c, resultados_v,
                    "Ejemplo C con viento", decimales=0,
                    tamano=7)

El viento empuja el puente hacia la derecha: las fuerzas dejan de ser simétricas y la reacción horizontal del apoyo articulado equilibra los 50 kN de viento.

Ejercicio 12.4

Quitar del ejemplo C las diagonales 18 y 19 e intentar analizarlo. Explicar el mensaje de la calculadora con ayuda de la figura 12.2. La expresión numero not in (18, 19) es verdadera si el número no es 18 ni 19.

Ver la solución
# Sin las diagonales 18 y 19, el tramo izquierdo
# queda formado por rectángulos sin arriostrar
barras_sin_diagonales = {}
for numero, datos in barras_c.items():
    if numero not in (18, 19):
        barras_sin_diagonales[numero] = datos

try:
    analizar_armadura(nodos_c, barras_sin_diagonales,
                      apoyos_c, cargas_c)
except ValueError as error:
    print(f"No se puede analizar. {error}")
No se puede analizar. La armadura es inestable: K_ll es singular.

Sin las diagonales 18 y 19, los paneles segundo y tercero son rectángulos formados por cordones y montantes, sin ninguna barra que impida que se deformen como un paralelogramo: la armadura es un mecanismo. Además, g=19+3−24=−2g = 19 + 3 - 24 = -2.