Análisis matricial de armaduras con Python

Capítulo 10

Fuerzas internas

Los desplazamientos y las reacciones son resultados importantes, pero lo que el ingeniero necesita para diseñar es la fuerza en cada barra: cuánto se estira o se comprime, y con qué esfuerzo trabaja su sección. En este capítulo obtenemos la fuerza axial de cada barra a partir de los desplazamientos de sus nodos, la presentamos en una tabla clara y la verificamos de dos formas independientes. Con este paso, nuestra calculadora resuelve el análisis completo de una armadura.

10.1 De los desplazamientos a la fuerza axial

Para cada barra, los desplazamientos de sus extremos en ejes globales, u\vu, se leen del vector U\vU en los grados de libertad de su vector de correspondencia. Con la matriz de transformación los llevamos al eje de la barra, u′=T u\vul = \mT\,\vu, y con la rigidez local obtenemos las fuerzas en sus extremos, f′=k′ u′\vf' = \mkl\,\vul: las mismas matrices del capítulo 5, ahora recorridas en sentido inverso.

La fuerza en el extremo final, f2′f'_2, actúa en la dirección del eje x′x': si es positiva, la barra tira de sus nodos y está en tracción (figura 10.2). Por eso tomamos N=f2′N = f'_2. Desarrollando el producto obtenemos una expresión que muestra el sentido físico del cálculo:

N=EAL (u2′−u1′)=EAL (−c u1−s u2+c u3+s u4)\begin{aligned} N = \EAL\,(u'_2 - u'_1) = \EAL\,\bigl(-c\,u_1 - s\,u_2 + c\,u_3 + s\,u_4\bigr) \end{aligned}

La fuerza axial es la rigidez axial por el alargamiento de la barra, la diferencia entre los desplazamientos de sus extremos en la dirección de la barra: la ley de Hooke. La figura 10.1 lo muestra gráficamente. Cada nodo se desplaza en cualquier dirección, pero solo la componente de su desplazamiento a lo largo de la barra la estira o la acorta; la componente perpendicular solo la hace girar.

Figura 10.1:

Los desplazamientos de los extremos (exagerados), proyectados sobre el eje de la barra, dan u1′u'_1 y u2′u'_2. Su diferencia es el alargamiento.

Figura 10.2:

Convención de signos: la fuerza axial es positiva en tracción. En ambos casos se muestran las fuerzas que la barra recibe en sus extremos.

10.2 La fuerza axial en Python

Calculemos paso a paso la fuerza de la barra 2 del ejemplo A. Los desplazamientos de sus extremos se seleccionan del vector U con la lista de índices de la barra, como en el ensamblaje:

# Barra 2 del ejemplo A
g = geometria[2]
u = U[indices(gdl_de_barra(barras[2]))]
u_local = matriz_transformacion(g["c"], g["s"]) @ u
k_local = rigidez_local(2.0e8, 0.0020, g["longitud"])
f_local = k_local @ u_local
u1, u2 = u_local * 1000  # en mm
print(f"u'1 = {u1:.3f} mm, u'2 = {u2:.3f} mm")
print(f"Alargamiento: {u2 - u1:.3f} mm")
f1, f2 = f_local
print(f"f'1 = {f1:.2f} kN, f'2 = {f2:.2f} kN")
u'1 = -0.259 mm, u'2 = -0.858 mm
Alargamiento: -0.599 mm
f'1 = 47.92 kN, f'2 = -47.92 kN

La barra se acorta 0.599 mm, y con EA/L=80 kN/mmEA/L = 80\,\mathrm{kN/mm} resulta N=−47.92N = -47.92 kN: compresión. Las fuerzas en los extremos son iguales y opuestas, como corresponde a una barra en equilibrio.

La función que lo hace para todas las barras repite estos pasos en un bucle y guarda cada fuerza en un diccionario, con el número de barra como clave:

def fuerzas_axiales(barras, geometria, U):
    """Fuerza axial de cada barra (+ en tracción)."""
    N = {}
    for numero, barra in barras.items():
        g = geometria[numero]
        u = U[indices(gdl_de_barra(barra))]
        T = matriz_transformacion(g["c"], g["s"])
        u_local = T @ u
        k_local = rigidez_local(barra["elasticidad"],
                                barra["area"],
                                g["longitud"])
        N[numero] = (k_local @ u_local)[1]
    return N


N = fuerzas_axiales(barras, geometria, U)

Los corchetes al final de (k_local @ u_local)[1] seleccionan el segundo elemento, f2′f'_2, del resultado del producto.

10.3 Tablas de resultados

Para informar los resultados, a la fuerza de cada barra le agregamos su estado y su esfuerzo (stress) σ=N/A\sigma = N/A, que comparamos con la resistencia del material. Clasificamos las fuerzas con una tolerancia, para no llamar «tracción» a un 10−1510^{-15} producido por el redondeo:

def clasificar_fuerza(N, tolerancia=1e-6):
    """Tracción, compresión o sin fuerza."""
    if N > tolerancia:
        return "tracción"
    elif N < -tolerancia:
        return "compresión"
    else:
        return "sin fuerza"

Una tabla bien presentada evita errores de lectura. Escribimos una función general, que recibe los encabezados y una lista de filas, y alinea todas las columnas a la derecha; los textos se muestran tal cual y los números, con los decimales indicados:

def mostrar_tabla(encabezados, filas, decimales=2,
                  ancho=11):
    """Imprime una tabla con columnas alineadas. Los
    números se muestran con los decimales indicados."""
    linea = ""
    for encabezado in encabezados:
        linea += f"{encabezado:>{ancho}}"
    print(linea)
    for fila in filas:
        linea = ""
        for valor in fila:
            if isinstance(valor, str):
                linea += f"{valor:>{ancho}}"
            else:
                valor = round(valor, decimales) + 0.0
                linea += f"{valor:>{ancho}.{decimales}f}"
        print(linea)
filas = []
for numero, fuerza in N.items():
    sigma = fuerza / barras[numero]["area"] / 1000  # MPa
    filas.append([str(numero), fuerza, sigma,
                  clasificar_fuerza(fuerza)])
mostrar_tabla(["Barra", "N (kN)", "σ (MPa)", "Estado"],
              filas)
      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 barra 1, el tirante inferior, trabaja en tracción, y las dos barras inclinadas, en compresión: el comportamiento que esperamos de una armadura triangular cargada desde arriba (figura 10.3). El mayor esfuerzo, 24 MPa, es menos del 10 % de la fluencia de un acero estructural (fy=250 MPaf_y = 250\,\mathrm{MPa}).

Figura 10.3:

Resultados del ejemplo A: fuerzas axiales y reacciones. Las barras en tracción se dibujan en azul y las comprimidas, en naranja.

10.4 Verificación: el equilibrio de cada nodo

En el capítulo 9 verificamos el equilibrio de la armadura en conjunto. Con las fuerzas de las barras podemos hacer algo más exigente: verificar el equilibrio de cada nodo. En cada uno, las fuerzas de las barras que llegan a él, las cargas y las reacciones deben sumar cero. Una barra en tracción tira de su nodo inicial en la dirección (c,s)(c, s) y de su nodo final en la dirección opuesta:

def equilibrio_nodal(nodos, barras, geometria, N, F, R,
                     restringidos):
    """Mayor desequilibrio en los nodos, con las fuerzas
    de las barras, las cargas y las reacciones."""
    residuo = F.copy()
    residuo[indices(restringidos)] += R
    for numero, barra in barras.items():
        g = geometria[numero]
        c, s = g["c"], g["s"]
        ix, iy, jx, jy = indices(gdl_de_barra(barra))
        # En tracción, la barra tira de cada nodo hacia
        # el otro extremo
        residuo[[ix, iy]] += N[numero] * np.array([c, s])
        residuo[[jx, jy]] -= N[numero] * np.array([c, s])
    return np.abs(residuo).max()
desequilibrio = equilibrio_nodal(nodos, barras, geometria,
                                 N, F, R, restringidos)
print(f"Desequilibrio nodal: {desequilibrio:.1e} kN")
Desequilibrio nodal: 1.2e-14 kN

Esta verificación no es circular: usa solo las fuerzas de las barras y la geometría, no la matriz global. Si hubiéramos cometido un error en el ensamblaje o en la solución, las fuerzas no equilibrarían los nodos.

Figura 10.4:

Diagrama de cuerpo libre del nodo 3 del ejemplo A. Las fuerzas de las barras se suponen en tracción: cada barra tira del nodo hacia su otro extremo.

Problema resuelto 10.1 · El método de los nudos

Calcular las fuerzas de las barras del ejemplo A con el método de los nudos y compararlas con las del método de rigidez.

Solución

En el nodo 3 concurren dos barras, y sus dos ecuaciones de equilibrio dan las dos fuerzas (figura 10.4). Suponiendo tracción, cada barra tira del nodo 3 hacia su otro extremo: la barra 2, en la dirección (0.6,−0.8)(0.6, -0.8), y la barra 3, en la dirección (−0.6,−0.8)(-0.6, -0.8). Con la carga (20,−50)(20, -50):

0.6 N2−0.6 N3+20=0,−0.8 N2−0.8 N3−50=0.0.6\,N_2 - 0.6\,N_3 + 20 = 0, \qquad -0.8\,N_2 - 0.8\,N_3 - 50 = 0.

En el nodo 2, el equilibrio horizontal (el rodillo no tiene reacción horizontal) da −N1−0.6 N2=0-N_1 - 0.6\,N_2 = 0. Resolvemos el sistema de 2×22\times2 con la misma np.linalg.solve:

# Nodo 3: incógnitas N2 y N3, en ese orden.
# Barra 2: dirección del nodo 3 al nodo 2 (0.6, -0.8)
# Barra 3: dirección del nodo 3 al nodo 1 (-0.6, -0.8)
A_nodo_3 = np.array([[0.6, -0.6],
                     [-0.8, -0.8]])
b_nodo_3 = np.array([-20.0, 50.0])
N2, N3 = np.linalg.solve(A_nodo_3, b_nodo_3)

# Nodo 2, dirección x: -N1 - 0.6 N2 = 0
N1 = -0.6 * N2
print(f"N1 = {N1:.2f} kN, N2 = {N2:.2f} kN, "
      f"N3 = {N3:.2f} kN")
N1 = 28.75 kN, N2 = -47.92 kN, N3 = -14.58 kN

Verificación. Las tres fuerzas coinciden con las del método de rigidez. Era de esperar: en una armadura isostática, la estática basta, y ambos métodos deben dar el mismo resultado.

10.5 Las fuerzas del ejemplo B

El ejemplo B es hiperestático, y aquí el método de rigidez muestra todo su valor: el método de los nudos no puede repartir la carga entre sus dos diagonales, porque el reparto depende de la rigidez de cada barra. Aplicamos la cadena completa de funciones:

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, K_rl_b, K_rr_b = particionar(
    K_b, libres_b, restringidos_b)
U_r_b = np.zeros(len(restringidos_b))
U_l_b = resolver_desplazamientos(
    K_ll_b, K_lr_b, F_b[indices(libres_b)], U_r_b)
R_b = calcular_reacciones(
    K_rl_b, K_rr_b, U_l_b, U_r_b,
    F_b[indices(restringidos_b)])
U_b = np.zeros(2 * len(nodos_b))
U_b[indices(libres_b)] = U_l_b

N_b = fuerzas_axiales(barras_b, geometria_b, U_b)
filas = []
for numero, fuerza in N_b.items():
    filas.append([str(numero), fuerza,
                  clasificar_fuerza(fuerza)])
mostrar_tabla(["Barra", "N (kN)", "Estado"], filas)

desequilibrio = equilibrio_nodal(
    nodos_b, barras_b, geometria_b, N_b, F_b, R_b,
    restringidos_b)
print(f"Desequilibrio nodal: {desequilibrio:.1e} kN")
      Barra     N (kN)     Estado
          1      23.89   tracción
          2     -48.15 compresión
          3      -6.11 compresión
          4      -8.15 compresión
          5      10.19   tracción
          6     -39.81 compresión
Desequilibrio nodal: 1.8e-14 kN

Los nodos están en equilibrio. Las dos diagonales trabajan de forma muy distinta (figura 10.5): la diagonal 6, que va del apoyo con rodillo al nodo cargado 3, toma una compresión de 39.8 kN, y la diagonal 5, una tracción de 10.2 kN. La carga horizontal empuja el cuadro hacia la derecha, y la diagonal que se acorta con ese movimiento, la 6, es la que más trabaja.

Figura 10.5:

Fuerzas axiales del ejemplo B. Las dos diagonales se reparten la carga según su rigidez: algo que la estática sola no puede determinar.

Como el ejemplo B es hiperestático, el método de los nudos no puede confirmar estas fuerzas. En el anexo D las comparamos con las de OpenSees, un programa de análisis estructural de código abierto, y coinciden.

Repaso y resumen del capítulo

Ejercicios

Ejercicio 10.1

Calcular el mayor esfuerzo, en valor absoluto, de las barras del ejemplo A y expresarlo como porcentaje de fy=250 MPaf_y = 250\,\mathrm{MPa}. La función max(a, b) devuelve el mayor de dos números.

Ver la solución
sigma_maxima = 0.0
for numero, fuerza in N.items():
    sigma = abs(fuerza) / barras[numero]["area"] / 1000
    sigma_maxima = max(sigma_maxima, sigma)
print(f"Mayor esfuerzo: {sigma_maxima:.2f} MPa")
fraccion = sigma_maxima / 250  # fy = 250 MPa
print(f"Fracción de la fluencia: {fraccion:.1%}")
Mayor esfuerzo: 23.96 MPa
Fracción de la fluencia: 9.6%

Un formato terminado en el signo % muestra el número como porcentaje; con .1 antes del signo, con un decimal.

Ejercicio 10.2

Calcular las fuerzas del ejemplo A con dos apoyos articulados y compararlas con las originales. Explicar por qué cambia la fuerza de la barra 1.

Ver la solución
apoyos_2 = {
    1: {"x": True, "y": True},
    2: {"x": True, "y": True},
}
libres_2, restringidos_2 = clasificar_gdl(nodos, apoyos_2)
K_ll_2, K_lr_2, _, _ = particionar(K_global, libres_2,
                                   restringidos_2)
U_2 = np.zeros(6)
U_2[indices(libres_2)] = resolver_desplazamientos(
    K_ll_2, K_lr_2, F[indices(libres_2)],
    np.zeros(len(restringidos_2)))
N_2 = fuerzas_axiales(barras, geometria, U_2)
filas = []
for numero in barras:
    filas.append([str(numero), N[numero], N_2[numero]])
mostrar_tabla(["Barra", "Rodillo", "Articulado"], filas)
      Barra    Rodillo Articulado
          1      28.75       0.00
          2     -47.92     -47.92
          3     -14.58     -14.58

Con dos apoyos articulados, la carga horizontal se reparte entre los apoyos (ejercicio 9.2), y el apoyo del nodo 2 impide que la barra 1 se alargue: la barra queda sin fuerza. Las barras 2 y 3 no cambian, porque el equilibrio del nodo 3 sigue determinando sus fuerzas.

Ejercicio 10.3

Duplicar el área de todas las barras del ejemplo A y comparar los desplazamientos y las fuerzas con los originales. Para no modificar el diccionario barras, crear una copia de cada barra con dict(barra).

Ver la solución
barras_dobles = {}
for numero, barra in barras.items():
    copia = dict(barra)
    copia["area"] = 2 * barra["area"]
    barras_dobles[numero] = copia

geometria_d = calcular_geometria(nodos, barras_dobles)
rigideces_d = calcular_rigideces(barras_dobles,
                                 geometria_d)
K_d = ensamblar_rigidez(nodos, barras_dobles,
                        rigideces_d)
K_ll_d, K_lr_d, _, _ = particionar(K_d, libres,
                                   restringidos)
U_d = np.zeros(6)
U_d[indices(libres)] = resolver_desplazamientos(
    K_ll_d, K_lr_d, F_l, U_r)
N_d = fuerzas_axiales(barras_dobles, geometria_d, U_d)
print(f"Desplazamientos a la mitad: "
      f"{np.allclose(U_d, U / 2)}")
filas = []
for numero in barras:
    filas.append([str(numero), N[numero], N_d[numero]])
mostrar_tabla(["Barra", "N (kN)", "N, 2A (kN)"], filas)
Desplazamientos a la mitad: True
      Barra     N (kN) N, 2A (kN)
          1      28.75      28.75
          2     -47.92     -47.92
          3     -14.58     -14.58

Los desplazamientos se reducen a la mitad, pero las fuerzas no cambian. En una armadura isostática, las fuerzas dependen solo del equilibrio y no de la rigidez de las barras; en una hiperestática, en cambio, cambiar la rigidez de una barra cambia el reparto de las fuerzas.