258 KiB
Session 6 - Exercise solution¶
# Import libraries
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Set style for matplotlib plots
plt.style.use('seaborn-whitegrid')
# Dictionary for mapping node numbers to user chosen shear key names
shear_keys = {
# Shear key in Base Slab 101
'BS101': range(10101, 10199),
# Shear key in Base Slab 201
'BS201': range(20101, 20199),
# Shear key in Base Slab 301
'BS301': range(30101, 30214),
}
# Set file name of dataset
file_name = 'shear_keys_base_slab_v20.txt'
# Read dataset from text file into dataframe, save it as 'df'
df = pd.read_csv(file_name)
# Extract version number from file name as 'vXX'
# (assume the last 6 characters will always be '...vXX.txt')
version_number = file_name[-7:-4]
# Print the head of the dataframe to check it
df.head()
%%capture
# %%capture prevent plots from showing as cell output
# ------------------------------------------------------
# Contruct a dictionary that maps load case numbers to titles (auto removes duplicates)
lc_no_to_title_map = dict(zip(df['LC'], df['LC-title']))
# Loop over all shear key names and their corresponding node numbers
for shear_key, nodes in shear_keys.items():
# Loop over all load cases, create plots and save them to a png-file
for lc in df['LC'].unique():
# Get title of current load case from mapping dictionary
lc_title = lc_no_to_title_map[lc]
# Filter dataframe based on load case and elements in shear key
df_filtered = df[(df['LC'] == lc) & (df['NR'].isin(nodes))]
# Create figure
plt.figure(figsize=(12, 5))
# Create x-values for plot as numbers running from 1 to length of y-values
x = np.array(range(1, len(df_filtered['vx[kN/m]'])+1))
# Create y-values for plot as shear forces vx
y = df_filtered['vx[kN/m]'].values
# Extract indices where y-values are negative and positive, respectively
idx_neg = np.where(y<0)
idx_pos = np.where(y>=0)
# Extract x-values where y-values are negative and positive, respectively
x_neg, x_pos = np.take(x, idx_neg)[0], np.take(x, idx_pos)[0]
# Extract y-values where y-values are negative and positive, respectively
y_neg, y_pos = np.take(y, idx_neg)[0], np.take(y, idx_pos)[0]
# Plot lines for negative and positve values as two separate lines
plt.plot(x_neg, y_neg, '.', color='salmon')
plt.plot(x_pos, y_pos, '.', color='cornflowerblue')
# Fill between y=0 and the lines where y-values are negative and positive, respectively
plt.fill_between(x, y, where=y<0, color='salmon', alpha=0.25, interpolate=True)
plt.fill_between(x, y, where=y>=0, color='cornflowerblue', alpha=0.25, interpolate=True)
# Set titles and x- and y-labels
plt.title(f'Shear force $vx$ [kN/m] for base slab shear key ${shear_key}$' + '\n' +
f'{lc_title} $(LC: {lc}) ({version_number})$', fontsize=18)
plt.xlabel('Points along shear key', fontsize=14)
plt.ylabel('Slab shear force $vx$ [kN/m]', fontsize=14)
# Save figure to png-file with meaningful name that varies in every loop
plt.savefig(f'Plots/{version_number}/{shear_key}_{lc}.png')
Explanations to some of the code lines are given below¶
- Line with
df_filtered = ...
: The dataframedf_filtered
is uniqie in every loop, since it is filtered based on the current load case and the nodes in the current shear key. The filtering is done based on the original large dataframe. Every operation from here on out inside the loop must usedf_filtered
and not the original dataframe. For filtering based on the nodes in the shear key.isin
is used. This is a good way to filter based on values in a list. And the nodes for each shear key is is stored in the loop variableelements
as a list.
- Line with
x = ...
: This generates the x-values, which will just be increasing numbers from 1 and up. Note thatrange(start, stop)
goes fromstart
tostop-1
.
- Line with
y = ...
: Collects the y-values for the plot in the current loop.df['vx[kN/m']]
extracts a Series, which is a single column, but with index numbers to its left. To get only the column values as an array, we dodf['vx[kN/m']].values
. Btw, this will also work for dataframes, but will instead return a 'matrix' array instead of a 'vector' array as for Series.
- Lines with
plt.plot()
: The negative and positive points are plotting as two separate data series so they can have different colors. Connecting the points by lines makes the plot look strange after it has been separated, so only points are plotted.
- Lines with
plt.fillbetween()
: Parameteralpha
set the opacity. Parameterinterpolate=True
will make sure the fill is not "cut off" near a crossing of the y-axis.
- Lines with for
plt.title()
: When creating plots by loops the title for each plot should probably have a unique title that varies with the loop variable(s). A convenient way to put variables inside text is by using f-strings.
- Line with
plt.savefig()
Subfolder 'Plots' and subsubfolder '' has to be created before running this. It the folders are not presentFileNoteFoundError
will be raised. The png-files could also be saved directly in the same folder as the script. In that case only'<file_name>.png'
would be necessary. By saving in a subfolder whose name depends on the version number given in the name original txt file, it is easier to keep track of versions and avoid overwriting files from previous versions.
Improvements mentioned in exercise text¶
y-limits of plot¶
Set the y-limits of the plot by plt.ylim([ymin, ymax])
.
ymin
and ymax
could be determined as the largest occuring magnitude values among all plots.
# Put this line before the loop
all_loads = df['vx[kN/m]']
extr_magnitude = max(abs(min(all_loads)), abs(max(all_loads)))
# Put this line in each loop before saving the figure
plt.ylim([-1.1*extr_magnitude, 1.1*extr_magnitude])
Annotations of local extrema¶
For annotating the local extremum points, define the function find_local_extrema(y_curve)
from the exercise text somewhere in the script before the loop. Afterwards, include the line below within the for loop somewhere between the line where the figure is create and the line where the figure is saved.
# Find local extremum points of graph
extrema_indices = find_local_extrema(y)
for extr_idx in extreme_indices:
ax.annotate(f'{y[extr_idx]:.0f}', xy=(x[extr_idx], y[extr_idx]), xytext=(x[extr_idx], y[extr_idx]))
This annotates the local peaks which helps for readability of the graph. The annotations could be even better than this by ensuring that text does not overlap. and by always annotating the two end points of the graph.
Another improvement¶
Instead of having to manually create the subdirectories Plots
and /{version_no}
, we could check if they exist and create them if they don't. For this, we could use the built-in os
module to manipulate the file system.