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, , se leen del vector en los grados de libertad de su vector de correspondencia. Con la matriz de transformación los llevamos al eje de la barra, , y con la rigidez local obtenemos las fuerzas en sus extremos, : las mismas matrices del capítulo 5, ahora recorridas en sentido inverso.
La fuerza en el extremo final, , actúa en la dirección del eje : si es positiva, la barra tira de sus nodos y está en tracción (figura 10.2). Por eso tomamos . Desarrollando el producto obtenemos una expresión que muestra el sentido físico del cálculo:
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.
Los desplazamientos de los extremos (exagerados), proyectados sobre el eje de la barra, dan y . Su diferencia es el alargamiento.
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 resulta 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, , 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) , que comparamos con la resistencia del material. Clasificamos las fuerzas con una tolerancia, para no llamar «tracción» a un 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 ().
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 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.
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 , y la barra 3, en la dirección . Con la carga :
En el nodo 2, el equilibrio horizontal (el rodillo no tiene reacción
horizontal) da . Resolvemos el sistema de 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.
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
- La fuerza axial es , con : la rigidez axial por el alargamiento de la barra. Es positiva en tracción.
- El esfuerzo es ; las barras en compresión deben verificarse además por pandeo.
- La función
mostrar_tablapresenta resultados con columnas alineadas, eisinstancedistingue textos de números. - El equilibrio de cada nodo, con las fuerzas de las barras, verifica de forma independiente el ensamblaje y la solución.
- En una armadura isostática, el método de rigidez y el método de los nudos coinciden; en una hiperestática, solo el primero resuelve el problema.
Ejercicios
Ejercicio 10.1
Calcular el mayor esfuerzo, en valor absoluto, de las barras del ejemplo A
y expresarlo como porcentaje de . 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.