Análisis matricial de armaduras con Python

Capítulo 8

Cargas y partición del sistema

La matriz global K\mK relaciona todas las fuerzas nodales con todos los desplazamientos, F=K U\vF = \mK\,\vU. Pero no podemos resolver ese sistema tal como está: K\mK es singular, y en cada grado de libertad conocemos o la fuerza o el desplazamiento, nunca ambos. Antes de resolver debemos ordenar lo que sabemos y lo que no. En este capítulo ensamblamos el vector de cargas y dividimos el sistema en bloques de grados de libertad libres y restringidos. Al hacerlo, veremos cómo los apoyos eliminan la singularidad.

8.1 Lo conocido y lo desconocido

Ordenemos los grados de libertad en dos grupos, los libres (ll) y los restringidos (rr), y escribamos el sistema por bloques:

[FlFr]=[KllKlrKrlKrr][UlUr]\begin{aligned} \begin{bmatrix} \vF_l \\ \vF_r \end{bmatrix} = \begin{bmatrix} \mK_{ll} & \mK_{lr} \\ \mK_{rl} & \mK_{rr} \end{bmatrix} \begin{bmatrix} \vU_l \\ \vU_r \end{bmatrix} \end{aligned}

En los grados de libertad libres conocemos las cargas Fl\vF_l y buscamos los desplazamientos Ul\vU_l. En los restringidos conocemos los desplazamientos Ur\vU_r, que son nulos salvo que haya asentamientos, y buscamos las fuerzas Fr\vF_r, que contienen las reacciones. El sistema por bloques equivale a dos ecuaciones:

Fl=Kll Ul+Klr Ur,(8.1)\vF_l = \mK_{ll}\,\vU_l + \mK_{lr}\,\vU_r, \tag{8.1} Fr=Krl Ul+Krr Ur.(8.2)\vF_r = \mK_{rl}\,\vU_l + \mK_{rr}\,\vU_r. \tag{8.2}

La primera tiene una sola incógnita, Ul\vU_l: la resolveremos en el capítulo 9. Con Ul\vU_l conocido, la segunda da directamente las reacciones.

La figura 8.1 muestra los cuatro bloques para el ejemplo A. El dibujo ordena los grados de libertad poniendo primero los libres para que los bloques se vean contiguos; en el programa no hace falta reordenar nada, porque cada bloque se selecciona con las listas libres y restringidos.

Figura 8.1:

Partición de la matriz global del ejemplo A en bloques de grados de libertad libres (3, 5, 6) y restringidos (1, 2, 4).

8.2 El vector de cargas

El vector de cargas F\vF tiene un término por cada grado de libertad. Se ensambla como la matriz: empieza en ceros y cada carga se suma en los grados de libertad de su nodo. Usamos += para que, si un mismo nodo recibiera varias cargas, se acumulen.

def ensamblar_cargas(nodos, cargas):
    """Vector de cargas nodales F de la armadura."""
    F = np.zeros(2 * len(nodos))
    for nodo, carga in cargas.items():
        fila_x, fila_y = indices(gdl_del_nodo(nodo))
        F[fila_x] += carga["x"]
        F[fila_y] += carga["y"]
    return F


F = ensamblar_cargas(nodos, cargas)

Como con las matrices, escribimos una función para mostrar un vector junto con sus grados de libertad:

def mostrar_vector(vector, gdl, decimales=2, ancho=10):
    """Imprime un vector con sus grados de libertad."""
    for i, g in enumerate(gdl):
        valor = round(vector[i], decimales) + 0.0
        print(f"{g:>4}{valor:>{ancho}.{decimales}f}")
print(" gdl  F (kN)")
mostrar_vector(F, list(range(1, len(F) + 1)))
 gdl  F (kN)
   1      0.00
   2      0.00
   3      0.00
   4      0.00
   5     20.00
   6    -50.00

8.3 Rebanadas

Además de seleccionar elementos con una lista de índices, NumPy permite tomar rebanadas (slices): tramos regulares de un arreglo. La notación es [inicio:fin:paso], donde el fin no se incluye y cualquiera de los tres valores puede omitirse. Como los grados de libertad en xx ocupan las posiciones pares (0, 2, 4, …) y los de yy las impares, las rebanadas [0::2] y [1::2] separan las componentes:

print(F[0:2])   # gdl 1 y 2: el nodo 1
print(F[0::2])  # gdl 1, 3, 5: las componentes en x
print(F[1::2])  # gdl 2, 4, 6: las componentes en y
[0. 0.]
[ 0.  0. 20.]
[  0.   0. -50.]

Con el método sum obtenemos la resultante de las cargas en cada dirección, una verificación rápida de que el vector se ensambló bien:

print(f"Suma de cargas en x: {F[0::2].sum():7.2f} kN")
print(f"Suma de cargas en y: {F[1::2].sum():7.2f} kN")
Suma de cargas en x:   20.00 kN
Suma de cargas en y:  -50.00 kN

8.4 La partición del sistema

Los cuatro bloques se obtienen con np.ix_, combinando los índices de los grados de libertad libres y restringidos. Una función los devuelve juntos, y quien la llama los recibe en cuatro variables, en el mismo orden:

def particionar(K, libres, restringidos):
    """Bloques Kll, Klr, Krl y Krr de la matriz K."""
    l = indices(libres)
    r = indices(restringidos)
    K_ll = K[np.ix_(l, l)]
    K_lr = K[np.ix_(l, r)]
    K_rl = K[np.ix_(r, l)]
    K_rr = K[np.ix_(r, r)]
    return K_ll, K_lr, K_rl, K_rr


K_ll, K_lr, K_rl, K_rr = particionar(K_global, libres,
                                     restringidos)
F_l = F[indices(libres)]

La selección de Fl\vF_l es la misma que hicimos en el capítulo 6 con un vector de prueba. Veamos los bloques del ejemplo A; para Klr\mK_{lr}, cuyas filas y columnas son distintas, indicamos ambas listas a mostrar_matriz:

print("K_ll en kN/mm:")
mostrar_matriz(K_ll / 1000, libres)
print("K_lr en kN/mm:")
mostrar_matriz(K_lr / 1000, libres, restringidos)
print(" gdl  F_l (kN)")
mostrar_vector(F_l, libres)
K_ll en kN/mm:
 gdl      3      5      6
   3   95.5  -28.8   38.4
   5  -28.8   57.6    0.0
   6   38.4    0.0  102.4
K_lr en kN/mm:
 gdl      1      2      4
   3  -66.7    0.0  -38.4
   5  -28.8  -38.4   38.4
   6  -38.4  -51.2  -51.2
 gdl  F_l (kN)
   3      0.00
   5     20.00
   6    -50.00

8.5 Verificación

La matriz global era singular porque la armadura podía moverse como cuerpo rígido. Al separar los grados de libertad restringidos, impedimos esos movimientos: si la armadura está bien apoyada, Kll\mK_{ll} tiene rango completo y puede resolverse. Además, como K\mK es simétrica, Klr=KrlT\mK_{lr} = \mK_{rl}\transp.

print(f"K_ll: {K_ll.shape}, rango "
      f"{np.linalg.matrix_rank(K_ll)}")
print(f"K_lr es la transpuesta de K_rl: "
      f"{np.allclose(K_lr, K_rl.T)}")
K_ll: (3, 3), rango 3
K_lr es la transpuesta de K_rl: True

Problema resuelto 8.1 · El ejemplo B

Ensamblar el vector de cargas del ejemplo B, particionar su sistema y comprobar que Kll\mK_{ll} tiene rango completo.

Solución

Calculamos primero la matriz global del ejemplo B con las funciones de los capítulos anteriores, y después aplicamos las de este capítulo:

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)
F_l_b = F_b[indices(libres_b)]

print("K_ll en kN/mm:")
mostrar_matriz(K_ll_b / 1000, libres_b, decimales=0,
               ancho=6)
print(f"Rango de K_ll: {np.linalg.matrix_rank(K_ll_b)}")
print(" gdl  F_l (kN)")
mostrar_vector(F_l_b, libres_b)
K_ll en kN/mm:
 gdl     3     5     6     7     8
   3   162   -29    38     0     0
   5   -29   162   -38  -133     0
   6    38   -38   151     0     0
   7     0  -133     0   162    38
   8     0     0     0    38   151
Rango de K_ll: 5
 gdl  F_l (kN)
   3      0.00
   5     30.00
   6    -40.00
   7      0.00
   8    -40.00

Verificación. Kll\mK_{ll} es de 5×55\times5 y su rango es 5: la armadura está bien apoyada. En Fl\vF_l aparecen las cargas de los nodos 3 y 4 en sus grados de libertad (5 y 6 para el nodo 3, 8 para el nodo 4), y ceros en los demás.

Repaso y resumen del capítulo

Ejercicios

Ejercicio 8.1

Calcular la resultante de las cargas del ejemplo B en cada dirección y compararla con la figura 3.2.

Ver la solución
F_b = ensamblar_cargas(nodos_b, cargas_b)
print(f"Ejemplo B, en x: {F_b[0::2].sum():7.2f} kN")
print(f"Ejemplo B, en y: {F_b[1::2].sum():7.2f} kN")
Ejemplo B, en x:   30.00 kN
Ejemplo B, en y:  -80.00 kN

La resultante es de 30 kN hacia la derecha y 80 kN hacia abajo, la suma de las tres cargas de la figura.

Ejercicio 8.2

Agregar al ejemplo A una carga horizontal de 15 kN en el nodo 2, sin modificar el diccionario cargas original. Mostrar los vectores F\vF y Fl\vF_l.

Ver la solución

Creamos un diccionario nuevo con las dos cargas, para no modificar los datos del ejemplo:

cargas_2 = {
    2: {"x": 15.0, "y": 0.0},
    3: {"x": 20.0, "y": -50.0},
}
F_2 = ensamblar_cargas(nodos, cargas_2)
print(" gdl  F (kN)")
mostrar_vector(F_2, list(range(1, 7)))
print(" gdl  F_l (kN)")
mostrar_vector(F_2[indices(libres)], libres)
 gdl  F (kN)
   1      0.00
   2      0.00
   3     15.00
   4      0.00
   5     20.00
   6    -50.00
 gdl  F_l (kN)
   3     15.00
   5     20.00
   6    -50.00

El grado de libertad 3 (nodo 2, dirección xx) es libre, de modo que la nueva carga aparece también en Fl\vF_l.

Ejercicio 8.3

Ensamblar la matriz global del ejemplo A sin la barra 1, particionarla con los mismos grados de libertad y calcular el rango de Kll\mK_{ll}. Relacionar el resultado con el ejercicio 6.3.

Ver la solución
barras_sin_1 = {2: barras[2], 3: barras[3]}
K_sin_1 = ensamblar_rigidez(nodos, barras_sin_1,
                            rigideces)
K_ll_sin_1 = particionar(K_sin_1, libres,
                         restringidos)[0]
print(f"Rango de K_ll: "
      f"{np.linalg.matrix_rank(K_ll_sin_1)} de 3")
Rango de K_ll: 2 de 3

Con rango 2 de 3, Kll\mK_{ll} es singular: existe un movimiento de los nodos libres que no deforma ninguna barra, el mecanismo que encontramos en el ejercicio 6.3.

Ejercicio 8.4

Construir la matriz global del ejemplo A ordenada como en la figura 8.1, con los grados de libertad libres primero, y mostrarla. Dos listas se unen en una sola con el operador +.

Ver la solución
orden = libres + restringidos
K_ordenada = K_global[np.ix_(indices(orden),
                             indices(orden))]
mostrar_matriz(K_ordenada / 1000, orden)
 gdl      3      5      6      1      2      4
   3   95.5  -28.8   38.4  -66.7    0.0  -38.4
   5  -28.8   57.6    0.0  -28.8  -38.4   38.4
   6   38.4    0.0  102.4  -38.4  -51.2  -51.2
   1  -66.7  -28.8  -38.4   95.5   38.4    0.0
   2    0.0  -38.4  -51.2   38.4   51.2    0.0
   4  -38.4   38.4  -51.2    0.0    0.0   51.2

El bloque superior izquierdo de 3×33\times3 es Kll\mK_{ll}, y el superior derecho, Klr\mK_{lr}.