Python For Civil and Structural Engineers
Python For Civil and Structural Engineers
Part 1
Introduction
Python basics
Numpy
Sympy
Computer Algebra Systems
Importing the library
Symbols
The subs() command and numerical
evaluation
Calculus
Solvers
Pandas
Dataframes
Working with external data
EXAMPLE: load cases for a two span
beam
Matplotlib
Materials
Cross-section
Column buckling
Member verification
Conclusions
Exporting in Latex
Installing nbextensions
Formatting the output of cells in LaTeX
Converting dataframes to LaTeX tables
Calculating the deflection of a steel
beam
Exporting the notebook
Conclusions
Wrapping up
Introduction
Solid mechanics;
Theory of structures;
Structural analysis and design;
installing Anaconda
The icons below the menu let you quickly select some of the
tools present in the drop-down menus described above.
Let's focus our attention on the little drop-down window that
says "code": from here we can change the cell's type from
code to \markdown. Markdown is a typesetting language
used to format your notebook. Usually, a well formatted
notebook is made out of both code type cells and markdown
type cells. The first contain all the various algorithms and
calculations, the second contain text that explains what the
code does, or that displays the result of the \emph{code}
cells. Let's leave the cell's type to \emph{code} for now,
and click inside the frame of the cell. The frame turns green:
we can now write some code inside it. Let's write
1 print("Hello World!")
Hello World!
NOTE
From now on, when you wish to run one of the
examples presented in this book, simply write the
code in an empty cell and run it. I suggest writing
the examples yourself, since simply copy-pasting
them won't be much effective if you want to
understand how they work.
Python syntax
Now we will go over the main elements that constitute the
Python language.
Comments
3
'''This is a
4 multiline comment'''
Integer numbers
1
a=1
2
b=2
3
c=a+b
4 print(c)
NOTE
Additions, subtractions and multiplications of
integers will always result in an int-type result.
Divisions between integers however will always
result in a floating point value.
2
b=12.345 #this another floating point number
3
c=2.4e5 #this is floating point number
4
#specified using scientific notation
5
print(type(a))
6 print(c/b)
<class 'float'>
19441.069258809235
Boolean values
2
b=False #this another bool value
3 print(type(a))
<class 'bool'>
Strings (str)
string_1="Hello"
string_2='World'
dog"
print(string_1)
print(string_2)
6
print(string_3)
Hello
World
The quick 'brown' fox jumps "over" the lazy dog As you can
see in line 3, it's possible to use a ' character inside a
string that has been specified with the " character.
However if the string has been specified using " and we
want to insert that same character inside it, we have to
precede " with a backslash.
Now let's talk about string concatenation and formatting.
1
a="Hello"+" World"
2
b=2+2
3
print(a)
Hello World
2
b=3
2
b=5
Lists (list)
2
myList2=["hello", 2, 4.15, myList1]
3
mat=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
5
print(myList1)
6
print(myList2)
7 print(mat)
[1, 2, 3, 4]
2
print(a[0]) #prints the first element of a
3
a[1]="hello" # assign 'hello' to the second position of a
4
print(a)
5
b=[[3,6],[9,12]]
3
a.append("seal")
4
print("append: {0}".format(a))
6
a.insert(2, "ant")
7
print("insert: {0}".format(a))
9
a.remove("dog")
10
print("remove: {0}".format(a))
11
12
a.pop(3)
13
print("pop: {0}".format(a))
14
15
a.extend(["apple", "pear"])
16 print("extend: {0}".format(a))
Dictionaries (dict)
2
"material" : "steel",
3
"type" : "IPE120",
4
"h" : 120,
5
"b" : 64}
7 print(mydict["material"])
steel
Tuples
2
print(mytuple)
4
print(mytuple.count("apple"))
5
print(mytuple.index(5))
7 print(mydict["material"])
(1, 2, 3, 'apple', 5)
As you can see, tuples are specified like lists, except they
use round brackets instead of curly brackets. The two
methods that can be used on a tuple are count and index.
The first counts the number of elements within the tuple
that correspond to the argument passed, the second returns
the position of the first occurrence.
You might ask yourself what is the point of tuples if python
already has lists. Well, tuples are used mostly when
returning values from functions, and can be unpacked into
separate variables. All of this will be explained later when
we start talking about functions.
2
y=5
4
if x<y:
5
print("x is smaller than y")
6
else:
x is smaller than y
Python indentation
Python conditions
Equals: a == b
Not equals: a != b
Greater than: a > b
Greater than or equal to: a >= b
Less than: a < b
Less than or equal to: a <= b
NOTE
A common mistake that inexperienced programmers
make is that they forget to use two equal signs to
specify an equals condition. The single equal sign is
used to assign values to variables, NOT as a
conditional statement!
3
if beam["mat"] == "concrete":
4
print("This is a concrete beam")
5
elif beam["mat"] == "steel":
6
print("This is a steel beam")
7
elif beam["mat"] == "wood":
8
print("This is a wooden beam")
9
else:
3
if a < b and b < c:
4 print("a<b<c")
a<b<c
Before discussing the IF statement, let's examine the first
line: this is a shorthand notation used to assign a set of
values to a set of variables. It simply stores 1 in a, 2 in b
and 3 in c. Moving on, in line 3 we see the logical operator
and. This operator returns True only when both the
conditions associated with it are True. In this case, since the
conditions a<b and b<c are indeed both true, the code
inside the IF statement is executed. In python there are
three logical operators:
Shorthand notation
if x>y: print("x>y")
x>y
x>y
x>y
First, we assign two values to x and y, using the same
shorthand notation that we already saw in code previously.
In line 3 we see the shorthand notation for a single IF
statement: it's the same as a normal IF statement, except
we write the code in one single line. In line 3 we see the
shorthand notation for the IF-ELSE statement. This one is a
little bit different, because the order in which the condition
and the code to execute are written is reversed. The first
print command is executed if x>y, otherwise the code that
follows the keyword else is executed. Having understood
this, it should be easy to read the fourth line: this is the
shorthand notation for a statement that normally would use
the elif keyword. Instead of stopping the chain after the else
keyword, we pass another shorthand IF statement that uses
exactly the same notation as the one in line 3.
Python operators
Arithmetic operators;
Assignment operators;
Logical operators
Comparison operators;
Identity operators;
Membership operators;
Bitwise operators
Arithmetic operators
Arithmetic operators are used to perform the basic
mathematical operations that we all know. The next table
gives a synopsis of their usage, for x=13 and y=4
Operator Name Expression Result
+ Addition x+y 17
- Subtraction x-y 9
* Multiplication x*y 52
% Modulus x%y 1
** Exponentiation x ** y 28561
// Floor x // y 3
NOTE
Notice that exponentiation is performed using two
multiplication symbols, NOT using the ^ symbol like
in other programming languages.
Assignment operators
2
print(x)
3
x=x+2 #the result of x+2 is stored in x
4 print(x)
2
print(x)
3
x+=2 #the result of x+2 is stored in x
4 print(x)
+= x += 2 x=x+2 5
-= x -= 2 x=x-2 1
*= x *= 2 x=x*2 6
/= x /= 2 x=x/2 1.5
%= x %= 2 x=x%2 1
**= x **= 2 x = x ** 2 9
//= x //= 2 x = x // 2 1
Comparison and logical operators
Membership operators
2
b=12
3
myList=[1,2,3,4,5]
5
if a in myList:
6
print("a is in myList")
7
else:
8
print("a is not in myList")
10
if b in myList:
11
print("b is in myList")
12
else:
a is in myList
b is not in myList
For loops
1
myList=[2,4,8,16]
3
for i in myList:
4 print(i)
16
2 print(i))
NOTE
The range(n) function creates a sequence that goes
from 0 to n-1, NOT from 0 to n or from 1 to n.
myList=[2,4,8,16]
2.
list_length=len(myList)
3.
for i in range(list_length):
4.
print(myList[i])
16
2
if i==3:
3
break
4 print(i)
2
if i==3:
3 continue
4
This time when i is equal to 3 the print order for that
iteration is ignored (because it comes after the continue
statement) and the program jumps at the next iteration,
where i=4.
Functions
2
return (B+b)*h/2
4 myArea = area(3,1,2)
4.0
A function is defined using the keyword def, followed by its
name, followed by the function's inputs. In this case, we
create a function called area that takes three inputs:
NOTE
don't be confused by the fact that in code
\ref{code:functions1} the variables B, b and h don't
have a value assigned to them. The variables used
within a function's definition only get assigned a
value when the function is called later in the code,
in this case at line 4.
Variable scope
myVariable
2
= 1
return a**2
res=myFunc(4)
print(myVariable)
6
print(res)
def myFunc(a):
myVariable
3
= 1
return a**2
res=myFunc(4)
print(myVariable)
7
print(res)
16
Function parameters
2
if perimeter==False:
3
return 3.14*radius**2
4
else:
5
return (3.14*radius**2, 2*3.14*radius)
7
print(myFunc(4))
8 print(myFunc(4, perimeter=True))
50.24
(50.24, 25.12)
Unpacking
2
print(area)
3 print(perimeter)
50.24
25.12
2
a=np.zeros((4))
3 print(a)
[0. 0. 0. 0.]
NOTE
From this point on, in the code examples written in
this chapter the line import numpy as np will be
omitted. They will all work, as long as you have
already imported NumPy in another cell.
Creating arrays
You can create NumPy arrays from lists and tuples with the
function array(). Let's see how it works:
1
a=np.array([1,2,3,4])
2
b=np.array([[1,2],[3.5, 4.2]])
3
print("a={0}".format(a))
4 print("b={0}".format(b))
a=[1 2 3 4{]}
b=[[1. 2. ]
[3.5 4.2]]
2
b=np.ones((2,3), dtype=int)
3
print("a={0}".format(a))
4 print("b={0}".format(b))
a=[0. 0. 0.]
b=[[1 1 1]
[1 1 1]]
2 print("a={0}".format(a))
2 print("a={0}".format(a))
a=[1 2 3 4 5 6 7 8 9]
2
print(a.ndim)
3
print(a.shape)
4
print(a.size)
5 print(a.dtype)
(2, 2)
int32
As you can see, a is composed by int32-type objects. This is
one of the various int-type objects provided by NumPy, and
it represents a 32-bit long integer. You generally don't need
to worry about which type of integer or float NumPy is
using, as long as you recognize that it is indeed an int or
float.
2
a[0]=16
3
b=np.array([[0,1],[2,3]])
4
print(a[0])
5 print(b[1,1])
16.0
[i,j]
[i][j]
2
b=np.array([2,4,8,16])
3
print(a+b)
4
print(a-b)
5
print(a*b)
6
print(a/b)
7
print(a**2)
8 print(np.sqrt(b))
[ 3 6 11 20]
[ -1 -2 -5 -12]
[ 2 8 24 64]
[ 1 4 9 16]
[1.41421356 2. 2.82842712 4. ]
If you have used Excel before, you may have noticed that
the operation in line 3 is smilar to summing two columns of
a spreadsheet. These kinds of operations between arrays
are very common, and indeed very useful. but what if you
wanted to perform the matrix product instead of the
element-wise multiplication? Then you would use @ instead
of * as the multiplication symbol:
1
a=np.array([[1,2],[3,4]])
2
b=np.array([[2,4],[8,16]])
3 print(a@b)
[[18 36]
[38 76]]
Array slicing
2
print(a)
3
print(a[2:5])
4 print(a[1:8:2])
[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[2. 3. 4.]
[13,14,15,16]])
print(mat[:,2])
print(mat[3,:])
4
print(mat[:,2:4])
[ 3 7 11 15]
[13 14 15 16]
[[ 3 4]
[ 7 8]
[11 12]
[15 16]]
In line 2 we select all the lines using the semicolon, and only
the third column by passing 2 as the positional index for the
second axis. Line 3 is similar, except it selects the fourth
line of the matrix. The print order in line 3 displays the third
and fourth columns instead.
NOTE
You may have noticed that when slicing using the
semicolon, NumPy cuts the array from the starting
position to the ending position excluded. This is why
line 4 of the previous code cell does not give an
error, even tough the index 4 is out of bounds for an
array with 4 columns. (remember that arrays are
indexed from 0!)
2
b=np.array([2,2])
3
c=np.array([3,3])
4
print(np.hstack((a,b,c)))
5 print(np.vstack((a,b,c)))
[1 1 2 2 3 3]
[[1 1]
[2 2]
[3 3]]
2
print(a)
3
a=np.append(a,4)
4
print(a)
5
a=np.insert(a,2,7)
6 print(a)
[1 2 3]
[1 2 3 4]
[1 2 7 3 4]
Problem definition
q
A B
l
Where:
q=20 kN/m
l=5 m
How to input the problem's quantities in
Python
2 q=20 #kN/m
HA x
y
The equilibrium equations are:
3
M=q/2*(l*x-x**2)
4 V=q*(l/2-x)
2
plt.figure(figsize=(10,4))
3
plt.plot([0]*len(x), color='k')
4
plt.plot(-M)
5 plt.plot(V)
20
20
40
60
1
import sympy as sp
2
sp.init_printing(use_latex="mathjax")
3
display(2+sp.sqrt(3))
4 display(sp.sqrt(32))
NOTE
LaTeX is a typesetting language, and is in fact the
lingua franca for creating beautiful expressions and
scientific documents. Don't worry if you have never
used it, for now it's just a way to display prettier
outputs in the cells. The second part of this book will
give more informations about LaTeX in Jupyter
notebooks.
Symbols
SymPy lets the user define the symbols that will be used
during symbolic calculations. You can think of them as
undefined variables, created using the command symbols().
1
a, b=sp.symbols("a b")
2
expr1=a+b
3
expr2=expr1**2
4
display(expr1)
5 display(expr2)
1
x, y=sp.symbols("x y")
2
f=x**2+y**2+sp.Rational(1,3)*x*y
3
display(f)
4
res1=f.subs(x,1)
5
display(res1)
6
res2=f.subs([(x,1), (y,5)])
7
display(res2)
8 display(res2.evalf(5))
1
x=sp.symbols("x")
2
expr=x**2+sp.Rational(1,2)*x-5
3
display(expr)
4
derivative=sp.diff(expr,x)
5
display(derivative)
6
integral1=sp.integrate(expr,x)
7
display(integral1)
8
integral2=sp.integrate(expr,(x, 0, 3))
9 display(integral2)
Equations
1
x=sp.symbols("x")
2
my_eq=sp.Eq(x**2-4,0)
3
display(my_eq)
4 sp.solveset(my_eq,x)
NOTE
It is often superfluous to use Eq() to define an
equation. Most of the times it's faster to write the
equation as f(x)=0 and pass f(x) straight to the
solver. By default, if the solver receives an
expression as input, it will automatically add =0 at
the end of it, and solve the equation accordingly.
The solveset command
1
x=sp.symbols("x")
2
res1=sp.solveset(sp.cos(x)*2, x)
3
display(res1)
5
res2=sp.solveset(sp.Rational(2,5)*x+3, x)
6
display(res2)
7 display(res2.args[0])
1
x, y=sp.symbols("x y")
2
res1=sp.linsolve([x+y-4, x-y-9], (x,y))
3
display(res1.args[0])
5
res2=sp.nonlinsolve([2*x**2+3*x-y-1, 3*x+2*y-5],(x,y))
6 display(res2)
For the scope of this book, the tools presented in the section
above are more than enough. Next up, we will learn how to
use pandas to manage data structures.
Pandas
Up until now we have encountered four types of data structures:
lists, dictionaries, tuples and numpy arrays. These are useful when
the
data they handle is relatively simple, especially numpy arrays are
designed to work fast with regular matrices. But what if you
need to import data from an Excel spreadsheet? or if you want to
structure, filter and categorize the data contained in a table? Well,
that is exactly what pandas is designed to do: handle big, complex
data structures with ease.
The main tool that pandas provides to the user is a new type of
structure called a DataFrame. Think of it like a 2D data structure
with headers for both columns and rows. It allows the user to
perform all kinds of operations on the values contained within, from
simple slicing to complex interpolation schemes. In the next sections
we will go through the main features of pandas, all of wich will be
used extensively in the following chapters of this book.
Dataframes
The best way to understand how pandas works is to create a simple
dataframe and use it to test the functionalities of the library.
Creating a dataframe
We create a simple 3x3 dataframe called df that will be used for the
next examples:
index=[1,2,3])
2
display(df)
3
A B C
1 4 9 5
2 2 1 7
3 6 8 4
1 import numpy as np
mat=np.random.randint(0, 10, (4,4))
2
numpy_df=pd.DataFrame(mat, columns=['A', 'B', 'C', 'D'])
numpy_df
A B C D
0 2 5 0 7
1 4 6 8 9
2 4 0 7 5
3 3 0 7 0
In the first line we import numpy, since so far we had only imported
pandas. Then we create an array of random integer numbers from 0
to 10
with shape (4,4) called mat. In line 3 we create the dataframe
numpy_df using the values stored in mat, specifying the
column names. This is not mandatory: without the colums argument
pandas would have created a dataframe with column names ranging
from 0 to 3.
sort_values
A B C
2 2 1 7
3 6 8 4
1 4 9 5
As you can see, the rows have been sorted in ascending order with
respect to the values contained in the column B. Notice that the
index of the dataframe has been affected as well, and the indices
remain "connected" to their corrispective row.
NOTE
It's important to specify that with functions like sort_values
the new configuration of the dataframe is not saved. In
order to
maintain the new configuration you must call the function
and
assign the result to the dataframe, like so:
df=df.sort_values("B")
rename
with rename you can assign new names to the rows and columns of
the dataframe:
drop
you can use drop() to remove columns and rows from a dataframe:
df.drop(columns={"B"}, index={1,3})
1
A C
1 4 5
3 6 4
df=df.drop(1)
2
display(df)
3
df=df.reset_index(drop=True)
4
df
5
A B C
0 2 3 3
2 5 6 8
the position of the row you wish to remove. However, after doing
this
the index is no longer a series of subsequent integers. To fix this we
call reset_index, specifying that we don't wish to keep the
previous index as a column by setting the argument drop to
True.
concat
df1=pd.DataFrame({"A":[4,2], "B":[9,1]},
1
df2=pd.DataFrame({"A":[5,3], "B":[7,9]},
3
res=pd.concat([df1, df2])
5
res
6
A B
row 1 4 9
row 2 2 1
row 3 5 7
row 4 3 9
df=pd.DataFrame(np.random.randint(0,10, (3,5)),
1
df
3
a b c d e
0 4 2 2 0 4
1 3 5 7 9 5
2 9 4 9 5 9
display(df.filter(regex="b"))
2
a c
0 4 2
1 3 7
2 9 9
b
0 2
1 5
2 4
display(df.loc[:, "b":"d"])
1
display(df.iloc[2, 3]
2
b c d
0 2 2 0
1 5 7 9
2 4 9 5
5
NOTE
Slicing with loc returns a pandas dataframe, but slicing with
iloc returns a number.
data=np.array([[9,4,8,5],[4,4,0,1],[5,7,4,5],[6,6,2,2]])
5
df
7
A B
x y u v
bar 9 4 8 5
M
foo 4 4 0 1
baz 5 7 4 5
N
qux 6 6 2 2
As you can see, before we create the dataframe we must first create
the header and index. We use the function
pd.MultiIndex.from_tuples to build the header, passing a list
of tuples that defines the hierarchy of the columns. The first element
of the tuples creates the first level of the dataframe, and so on.
Then
in line 3 we create the index for the rows using a list of NumPy
arrays. The first array defines the first level of the index, and so
on. In line 5 we create an array for the data, making sure that the
dimensions match the header and index that were created before.
Finally
we build the dataframe in line 6, passing header and ind
to the column and index arguments.
To slice a multi-index dataframe you can use loc or
iloc. With loc, you can specify the sub-columns and the sub-rows
using tuples:
2 h=np.array([1,2,3])
3 df=pd.DataFrame({"b":b, "h":h})
4
5 df["I"]=(df.b*(df.h)**3)/12
df
b h I
0 1 1 0.083333
1 1 2 0.666667
2 1 3 2.250000
loading a spreadsheet
df=df.round(4)
2
df.head()
3
Column
x M neg M pos V neg V pos N neg N pos
0 0.0000 0.0 0.0000 0.0 0.0000 0.0 0.0000
1 0.0000 0.0 2.9919 0.0 0.5679 0.0 806.8224
2 0.1675 0.0 2.9024 0.0 0.5679 0.0 806.8224
3 0.3350 0.0 2.8133 0.0 0.5678 0.0 806.8224
4 0.5025 0.0 2.7247 0.0 0.5677 0.0 806.8224
The file we are importing contains the bending moment, shear and
axial force of a column part of a concrete frame. If you take a look at
the .xlsx file you will notice that the data has a header that
says column in the first line and x, M neg, M
pos,T neg, T pos, N neg, N pos in the second. The first column
represents the position (height) considered.
To import the data we use the function read_excel,
specifying the name of the file we want to load. We also pass the
header argument, specifying which lines will be used as headers
for the dataframe. We could have specified an index as well by
passing
the index_col argument, telling pandas which column must be used
as index. In line two we round the values so that they have
maximum
four decimal digits, and finally using head() we display the first few
lines of the dataframe.
NOTE
When importing data in a dataframe, it's important to have
at least a general idea of how the data is structured. So
take some time to examine the file you are loading and if
possible organize the data so that it's easy to manipulate.
This will save a lot of time down the line cleaning an ill-
presented dataframe.
df.tail()
1
Column
x M neg M pos V neg V pos N neg N pos
21 3.350 0.0 1.3601 0.0 0.5140 0.0 806.8227
22 3.350 0.0 3.0870 0.0 0.6027 0.0 220.7086
23 5.025 0.0 2.1503 0.0 0.6027 0.0 220.7086
24 6.700 0.0 1.3178 0.0 0.6027 0.0 220.7086
25 6.700 NaN NaN NaN NaN NaN NaN
df.columns=df.columns.droplevel(0)
1
df=df.dropna()
2
df=df.drop(0)
3
df.head()
4
In the first line we delete the first level of the header using
droplevel, then in line 2 we use the function dropna to
delete every row that contains a NaN value, and finally we
delete the first line of the dataframe (the one that contains only zero
values) with the drop function. At last our dataframe is ready and
can be used for further calculations.
df=pd.read_csv('beam_example.csv', header=[1])
1
df=df.round(3)
2
df=df.dropna()
3
df=df.reset_index(drop=True)
4
df=df.drop(0)
5
df=df.reset_index(drop=True)
6
df.head()
7
Notice how we specify that we want to use the second line of the
dataframe as header, then we round all the number so that they
have a
maximum of three decimal places. In line 3 we call dropna() to
clean the data, and then we reset the index using reset_index. In
line 5 we delete the first line of data as well, and then we reset the
index once again.
NOTE
The function read_csv offers a lot of parameters to import
the
data in the best possible way. For example if you have a csv
file that
use a period instead of a comma as the separator, you can
pass the sep argument and specify a different string.
Please refer to the documentation for more in-depth
information about this command.
Exporting a dataframe
df.to_excel("output.xlsx")
1
df.to_csv("output.csv")
2
If you open up the folder where the jupyter notebook you are
working
on is stored, you will see that python has created two new files
called
output.xlsx and output.csv.
EXAMPLE: load cases for a two span
beam
The goal of this exercise is to apply to a practical situation what
we have learned up until now about NumPy, SymPy and
Pandas. In the end we will have an useful piece of code that can be
adapted to a lot different situations.
Problem definition
COMBO 1
q1=4.5 kN/m q =3.2 kN/m
2
A B C
COMBO 2
q1=3.2 kN/m q2=4.5 kN/m
A B C
l1=4 m l2=5 m
The best approach to solve this problem is to write a
function to wich we pass the beam's dimensions and loads that
returns the bending moment and shear. Since the beam is
hyperstatic, to
find the reactions we can't simply solve the equilibrium equations.
So
we split the beam in two and introduce an unknown bending
moment
Mx to mantain congruence, as shown in the next figure:
q1 q2
Then we can calculate the bending moment and shear. For the first
span:
import sympy as sp
import pandas as pd
#calculate Mx
Mx=sp.solveset(Mx*l1/3+q1*l1**3/24+Mx*l2/3+q2*l2**3/24,Mx).args[0]
7
8
(Va, Vb1)).args[0]
11
(Vc, Vb2)).args[0]
13
Vb=Vb1+Vb2
14
15
16
x2=np.arange(0, l2+0.1, 0.1) #create axis x2
17
span
20
store it
22
23
beam2["V"]=Vb2-q2*beam2.x # calculate V and store it
24
25
27
28
29
30
31
32
33
34
35
36
The code looks complicated, but in reality it's quite simple. Let's
explain what each line does.
3 combos=pd.DataFrame(columns=header)
5
8 combos=combos.set_index("x")
9
10 combos=combos.astype("float")
11 combos.head()
combo 1 combo 2
M V M V
x
0.0 0.000000 3.735764 0.000000 6.611111
0.1 0.357576 3.415764 0.638611 6.161111
0.2 0.683153 3.095764 1.232222 5.711111
0.3 0.976729 2.775764 1.780833 5.261111
0.4 1.238306 2.455764 2.284444 4.811111
The library matplotlib (used for plotting) will be explained in the next
chapter. For now just copy the following code in an empty cell and
run it:
fig = plt.figure(figsize=(8,8))
ax = plt.subplot(211)
ax.invert_yaxis()
combos.loc[:,pd.IndexSlice[:,"M"]].plot(ax=ax)
7
ax = plt.subplot(212)
ax.invert_yaxis()
10
combos.loc[:,pd.IndexSlice[:,"V"]].plot(ax=ax)
11
10 None,None
(combo 1, M)
5 (combo 2, M)
10
0 1 2 3 4 5 6 7 8 9
x
10
5 None,None
(combo 1, V)
10
(combo 2, V)
0 1 2 3 4 5 6 7 8 9
x
Matplotlib
In this chapter you will learn how to visualize various kinds of
data using the functionalities offered by Matplotlib. Those
who
have used MatLab at some point in their lives will find this
library somewhat familiar. Indeed Matplotlib was written to
mimic the
behaviour of Matlab, at least to some extent. In the vast
landscape of python libraries matplotlib as become virtually the
only library that people use to plot data. Like all the
libraries we have seen so far matplotlib is in a sense the
"industry standard" to do this kind of task.
In this chapter you will find many code examples that
showcase some of the functionalities of matplotlib. The main
goal is to give you the tools necessary so that you know what
to do in order to obtain a certain type of plot. In the end,
however, everything comes down to personal taste. In fact I
believe that tinkering and experimenting is the best way to
learn how matplotlib works.
Loading the library and importing
the data
Before we start exploring matplotlib we must first load the
library and import some data to plot. Let's start with the usual
preamble:
import numpy as np
2 import pandas as pd
import matplotlib.pyplot as plt
data=pd.read_csv("beam.csv")
1
x=np.array(data.x)
2
M1=np.array(data.M1)
3
M2=np.array(data.M2)
4
V1=np.array(data.V1)
5
V2=np.array(data.V2)
6
plt.plot(x,M1)
1
plt.show()
2
fig, ax=plt.subplots()
1
ax.plot(x, M1)
2
plt.show()
3
plt.xlabel("x")
2
plt.ylabel("bending moment")
3
plt.title("Beam")
4
plt.legend()
5
plt.gca().invert_yaxis()
6
plt.gcf().set_size_inches(6,3)
7
plt.show()
8
fig, ax=plt.subplots()
1
ax.plot(x,M1, label="M1")
2
ax.set_xlabel("x")
3
ax.set_ylabel("bending moment")
4
ax.set_title("Beam")
5
ax.legend()
6
ax.invert_yaxis()
7
fig.set_size_inches(6,3)
8
plt.show()
9
fig, ax=plt.subplots(figsize=(8,3))
1
ax.invert_yaxis()
2
ax.plot(x,M1, label="M1", color="deeppink", linestyle="--",
3 linewidth=3)
4 plt.show()
fig, ax=plt.subplots()
1
ax.invert_yaxis()
2
plt.show()
5
Plotting styles
plt.style.use("seaborn")
1
fig, ax=plt.subplots(figsize=(8,3))
2
ax.invert_yaxis()
3
plt.show()
6
At the end of this chapter you will find a chart containing all the
available styles.
Filling areas
With fill_between you can fill the area between two curves:
plt.style.use("seaborn-whitegrid")
1
fig, ax=plt.subplots(figsize=(8,3))
2
ax.invert_yaxis()
3
plt.show()
8
plt.style.use("seaborn-whitegrid")
1
fig, ax=plt.subplots(figsize=(8,3))
2
ax.invert_yaxis()
3
plt.show()
8
Finally, with the parameter where you can tell matplotlib to fill
only the portions of the plot that fulfill a certain condition:
fig, ax=plt.subplots(figsize=(8,3))
1
ax.invert_yaxis()
2
Adding markers
A marker is a symbol that matplotlib displays in
correspondence of
each data point. Markers are also what constitute scatter plots,
as we
will see later. When passing the marker argument to
plot(), each datapoint is visualized with a symbol. We can use
this to our advantage to display the supports of the beam we
are printing:
fig, ax=plt.subplots(figsize=(8,3))
1
ax.invert_yaxis()
2
ax.plot(x,M1, label="M1")
3
ax.plot(x,M2, label="M2")
4
marker=6, markersize=16)
8
plt.show()
9
figsize=(8, 6))
ax1.plot(x,M1, label="M1")
ax1.set_ylabel("bending moment")
ax1.set_title("Combo 1")
ax1.invert_yaxis()
ax2.plot(x,M2, label="M2")
ax2.set_ylabel("bending moment")
ax2.set_title("Combo 2")
ax2.invert_yaxis()
10
plt.show()
11
2
fig.set_size_inches(8, 4)
3
ax1.plot(x,M1, label="M1")
4
ax1.set_ylabel("bending moment")
5
ax1.set_title("Combo 1")
6
ax1.invert_yaxis()
7
ax2.plot(x,V1, label="V1")
8
ax2.set_title("Combo 1")
9
ax3.plot(x,M2, label="M2")
10
ax3.set_ylabel("bending moment")
11
ax3.set_title("Combo 2")
12
ax3.invert_yaxis()
13
ax4.plot(x,V2, label="V2")
14
ax4.set_title("Combo 2")
15
plt.show()
16
17
18
fig, ax=plt.subplots(figsize=(8,3))
1
ax.invert_yaxis()
2
ax.plot(x,M1, label="M1")
3
ax.set_xticks(np.arange(0,10,1))
4
plt.show()
5
raindata=pd.read_csv("raindata.csv")
1
province=np.array(raindata["province"])
2
T=np.array(raindata["T"])
3
daily=np.array(raindata["daily prec."])
4
fig, ax=plt.subplots()
2
ax.set_xlabel("daily prec.")
4
plt.show()
6
fig, ax=plt.subplots()
1
ax.set_xlabel("daily prec.")
3
cbar=fig.colorbar(im, ax=ax)
5
cbar.set_label("avg. temperature")
6
plt.show()
7
plt.xscale("log")
2
cbar=plt.colorbar()
3
plt.show()
4
fig, ax = plt.subplots()
1
ax.bar(province[:15], daily[:15])
2
plt.draw()
3
ax.set_xticklabels(ax.get_xticklabels(), rotation=45)
4
ax.set_ylabel("Avg. temperature")
5
plt.show()
6
To plot more than one array of data and group the columns by
province we have to tell matplotlib the position and width of
each column:
1
barWidth=0.3
2 r1=np.arange(len(province[:15]))
3 r2=r1+barWidth
4 fig, ax = plt.subplots()
7 ax.set_xticks(r1)
8 ax.set_xticklabels(province[:15], rotation=45)
9 ax.set_ylabel("Avg. temperature")
10 ax.legend()
11 plt.show()
12
The variable barWidth stores the width of the bars. in line 2
and 3 we create two arrays that store the position of each bar
that we
wish to plot. Notice how the values of r2 have an offset from
the values in r1 equal to barWidth. In lines 5 and 6 we
create the bar plots, specifying a custom position for the bars
by
passing r1 and r2 as first arguments. The width
parameter is used to control the width of the bars, so we pass
barWidth to it. The next thing to do is to set up the x axis so
that it gets displayed properly. Using set_xticks() we
define the position of the ticks and with
set_xticklabels() we tell matplotib to use the strings
stored in province as labels and to rotate them by 45°.
APPENDIX: parameter
references
Named colors
Linetypes
Markers
Plot Styles
Color maps
Calculating section
properties
We finally have all the tools necessary to start solving real-
world problems . The libraries introduced previously will play
a vital role in every example of this second part of the book,
so please refer to the first if you forget some of the
commands that you might encounter.
Section properties of a steel beam
In this example we will consider a welded I beam with
asymmetric flanges. The properties that will be calculated
are:
Area A
Section centroid yG
first moment of area about the x axis Sx
first moment of area about the y axis Sy
second moment of area about the x axis Ix
second moment of area about the y axis Iy
elastic section modulus about the x axis Wx
elastic section modulus about the y axis Wy
elastic moment of resistance about the x axis
MRd,x
b 250 mm
B 300 mm
h 400 mm
tf1 18 mm
tf2 15 mm
tw 12 mm
1
import numpy as np
2
import sympy as sp
3
import pandas as pd
5 sp.init_printing(use_latex="mathjax")
1
b=250 #mm
2
B=300 #mm
3
h=400 #mm
4
tf1=18 #mm
5
tf2=15 #mm
6
tw=12 #mm
8 hw=h-tf1-tf2
2
Af2=B*tf2
3
Aw=hw*tw
4
A=Af1+Af2+Aw
5 A
NOTE
The first moment of area for a rectangle is given by
the area of the rectangle multiplied by the distance
of its centroid from the axis taken in consideration.
In this case we must calculate the first moment of
area relative to yG, which at the moment is still an
unknown quantity.
1
yG=sp.symbols('yG')
2
Stot=b*tf1*(yG-tf1/2)-B*tf2*(h-yG-tf2/2)\
3
+tw*(yG-tf1)**2/2-tw*(h-yG-tf2)**2/2
4
yG=sp.solveset(Stot, yG).args[0]
5 yG
1
Sx=b*tf1*(yG-tf1/2)+tw*(yG-tf1)*(yG-tf1)/2
2 Sx
1
Sy=tf1*(b/2)*(b/4)+tf2*(B/2)*(B/4)+hw*(tw/2)*(tw/4)
2 Sy
Where bi, hi and Ai are the width, height and area of the
rectangle, and di is the distance between the centroid of the
rectangle and the axis we are considering. To implement this
formula in the code we calculate I separately for the two
flanges and the web, and then we sum the results.
1
Ix_f1=(b*tf1**3)/12+Af1*(yG-tf1/2)**2
2
Ix_f2=(B*tf2**3)/12+Af2*(h-yG-tf2/2)**2
3
Ix_w=(tw*hw**3)/12+Aw*(tf1+hw/2-yG)**2
4
Ix=Ix_f1+Ix_f2+Ix_w
5 Ix
1
Iy_f1=(tf1*b**3)/12
2
Iy_f2=(tf2*B**3)/12
3
Iy_w=(hw*tw**3)/12
4
Iy=Iy_f1+Iy_f2+Iy_w
5 Iy
2 Wx
1
fy=235 #N/mm^2
2
Mx=Wx*fy*10E-3 #kNm
3 Mx
A 13404 mm2
yG 200.99 mm
Sx 1064910.02 mm3
Sy 315981 mm3
Ix 380550963.82 mm4
Iy 57240348 mm4
Wx 1893322.10 mm3
Mx 4449306.95 kNm
Conclusions
If you have followed the steps above you now have a
functioning jupyter notebook capable of calculating the
cross-section properties of a welded steel beam. All we had
to do was to implement some formulas, so the code was not
that complex. Nevertheless, it still gives a comprehensive
insight on how to use python to translate theory into
practice. In the next chapter you will tackle a much more
difficult problem, designed to make you understand how to
use functions in order to organize the different parts of your
code.
M-N interaction in a
concrete section
Concrete
Rebar steel
Strain domains
Because the concrete can't bear tensile stresses, we are
facing a
non linear problem. The portion of concrete that
contributes to the resistance of the section depends on the
depth of the neutral axis. This means that the resistance
varies as the strain profile of the section changes. To plot
the resistance domain we must therefore consider all the
possible strain profiles that bring the section to failure, in
accordance to the ULS design filosofy. For each profile there
is an associated M-N couple that we can use to plot the
resistance domain of the section.
We can define five different domains for the strain profile,
as seen in the next figure:
1
import numpy as np
2
a=400 #mm, section height
b=300 #mm, section width
d=a-c
As1=1017 #mm^2, 4phi18, bottom rebar area
#concrete
5 fck=25 #MPa
gamma_c=1.5
fcd=0.85*fck/gamma_c #MPa
6 eps_cc=-0.0020
eps_cu=-0.0035
#steel
8 fyk=450 #MPa
gamma_s=1.15
fyd=fyk/gamma_s #MPa
9
Es=200000 #MPa
eps_su=0.01
10 eps_y=fyd/Es
11
12
13
14
2 n=len(eps)
3 sig=np.zeros(n)
4 for i in range(n):
5 if eps[i]>=0:
6 sig[i]=0
elif 0>eps[i] and eps[i]>=eps_cc:
7
sig[i]=-fcd*(2-eps[i]/eps_cc)*eps[i]/eps_cc
8
elif eps_cc>eps[i] and eps[i]>=eps_cu:
sig[i]=-fcd
10
else:
11
print('invalid eps value')
12
return(sig)
13
n=len(eps)
2
sig=np.zeros(n)
3
for i in range(n):
4
sig[i]=-fyd
6
sig[i]=eps[i]*Es
8
sig[i]=fyd
10
else:
11
return(sig)
13
Domain Ⅰ
We must decide a coordinate system for the position of the
neutral
axis. The origin will correspond to the top of the section, and
the
axis will be directed downward. For the first domain, then,
the neutral
axis will go from ∞ to 0. A general strain distribution is
shown
in the next figure:
n_step=20
2
3 yn_sup=-9999
yn_inf=0
psi=eps_su/(d-yn)
5
eps_s1=np.full(n_step, eps_su)
6
eps_s2=-psi*(yn-c)
7
sig_s1=rel_s(eps_s1)
8
sig_s2=rel_s(eps_s2)
9
10
Nrd_1=As1*sig_s1+As2*sig_s2
11
Mrd_1=As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
12
13
Domain Ⅱ
The expressions for the strain in the bars remain the same
as those for the first domain.
Now let's code all of this in a cell:
1 yn_sup=0
yn_inf=-d*eps_cu/(eps_su-eps_cu)
psi=eps_su/(d-yn)
eps_s1=np.full(n_step, eps_su)
eps_s2=-psi*(yn-c)
sig_s1=rel_s(eps_s1)
sig_s2=rel_s(eps_s2)
Nc=np.zeros(n_step)
10
Mc=np.zeros(n_step)
11
for i in range(n_step):
12
13
eps_c=-(yn[i]-y)*psi[i]
14
eps_c=np.round(eps_c, 7)
15
sig_c=rel_c(eps_c)
16
Nc[i]=b*np.trapz(sig_c, y)
17
ygc=np.nan_to_num(np.trapz(sig_c*y,
y)/np.trapz(sig_c, y))
18
19 Mc[i]=Nc[i]*(a/2-ygc)
20
21 Nrd_2=Nc+As1*sig_s1+As2*sig_s2
Mrd_2=-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
22
The first eight lines are exactly the same as those written
for the
first domain. The only thing that changes are the starting
and ending
positions of the neutral axis. In lines 10 and 11 we initialize
Nc and Mc as arrays of zeros. We then start a
for loop that cycles through all the neutral axis
positions stored in yn, and calculates Nc, ygc and Mc for
each
one of them. Let's take a closer look to what is written inside
this
for loop. The best way to calculate the integral is to
discretyze the distance from 0 to yn with a finite number of
points, and then perform an approximate integration. In line
13 we
create an array of 10 points that go from 0 to the current
position of the neutral axis yn[i]. For each of these points
we calculate the corresponding strain, according to this
expression:
Domain Ⅲ
2 yn_inf=-d*eps_cu/(eps_y-eps_cu)
4 psi=-eps_cu/yn
5 eps_s1=-psi*(yn-d)
6 eps_s2=-psi*(yn-c)
7 eps_s1=np.round(eps_s1, 5)
8 eps_s2=np.round(eps_s2, 5)
9 sig_s1=rel_s(eps_s1)
10 sig_s2=rel_s(eps_s2)
11
12 Nc=np.zeros(n_step)
13 Mc=np.zeros(n_step)
14 for i in range(n_step):
16 eps_c=-(yn[i]-y)*psi[i]
17 eps_c=np.round(eps_c, 7)
18 sig_c=rel_c(eps_c)
19 Nc[i]=b*np.trapz(sig_c, y)
20 ygc=np.trapz(sig_c*y, y)/np.trapz(sig_c, y)
21 Mc[i]=Nc[i]*(a/2-ygc)
22
23 Nrd_3=Nc+As1*sig_s1+As2*sig_s2
24 Mrd_3=-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
Domain Ⅳ
2 yn_inf=a
4 psi=-eps_cu/yn
5 eps_s1=-psi*(yn-d)
6 eps_s2=-psi*(yn-c)
7 sig_s1=rel_s(eps_s1)
8 sig_s2=rel_s(eps_s2)
9
10 Nc=np.zeros(n_step)
11 Mc=np.zeros(n_step)
12 for i in range(n_step):
14 eps_c=-(yn[i]-y)*psi[i]
15 eps_c=np.round(eps_c, 7)
16 sig_c=rel_c(eps_c)
17 Nc[i]=b*np.trapz(sig_c, y)
18 ygc=np.trapz(sig_c*y, y)/np.trapz(sig_c, y)
19 Mc[i]=Nc[i]*(a/2-ygc)
20
21 Nrd_4=Nc+As1*sig_s1+As2*sig_s2
22 Mrd_4=-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
Domain Ⅴ
2 yn_inf=a+9999
3
t=3/7*a
4
psi=-eps_cc/(yn-t)
eps_s1=-psi*(yn-d)
6
eps_s2=-psi*(yn-c)
7
sig_s1=rel_s(eps_s1)
sig_s2=rel_s(eps_s2)
9
10
Nc=np.zeros(n_step)
11
Mc=np.zeros(n_step)
12
for i in range(n_step):
13
y=np.linspace(0, a, 10)
14
eps_c=-(yn[i]-y)*psi[i]
15
eps_c=np.round(eps_c, 7)
16
sig_c=rel_c(eps_c)
17
Nc[i]=b*np.trapz(sig_c, y)
18
ygc=np.trapz(sig_c*y, y)/np.trapz(sig_c, y)
19
Mc[i]=Nc[i]*(a/2-ygc)
20
21
Nrd_5=Nc+As1*sig_s1+As2*sig_s2
22
Mrd_5=-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
23
plt.style.use("seaborn-whitegrid")
1
fig, ax=plt.subplots(figsize=(6,4))
2
ax.plot(Nrd_1*1E-3, Mrd_1*1E-6)
3
ax.plot(Nrd_2*1E-3, Mrd_2*1E-6)
4
ax.plot(Nrd_3*1E-3, Mrd_3*1E-6)
5
ax.plot(Nrd_4*1E-3, Mrd_4*1E-6)
6
ax.plot(Nrd_5*1E-3, Mrd_5*1E-6)
7
ax.set_xlabel("N [KN]")
8
ax.set_ylabel("M [KNm]")
9
Conclusions
If you followed through this chapter carefully, you should
now
understand how to use functions to organize your code,
and how
to use for loops to perform operations on array elements. In
the next chapter we will use the diagram that we have
obtained to perform member verifications on a concrete
beam. Obviously, the code only works for a rectangular
section with top and bottom reinforcement bars, but
expanding it shouldn't be difficult.
APPENDIX: saving the code in an
external file
The notebook that we have written in this chapter is able to
calculate the resistance diagram of a rectangular concrete
section.
However, it is rather impractical to utilize. Suppose that you
had to
plot the resistance diagram of ten different sections: you
would need
to change the input data of the notebook each time, surely
there must
be a better way to approach this problem. The solution is to
wrap
everything that we have written inside a single function.
This function will take as inputs the dimensions of whatever
cross-section we are considering, and return the resistance
diagram. Then we will be able to call this function whenever
we want. In order to do this, we need to create a python
script that contains our function. A python script is just a
text file that ends in .py that has some python code written
inside.
Navigate to the folder where you have saved the notebook
created in
this chapter, and create a new file named MN.py. You can
do this
by right-clicking and selecting new>text document. This will
create a .txt file that can then be renamed to MN.py. To edit
its contents you can open it using notepad.
MN.py will contain a function called
getDomain(a,b,c,As1,As2,fck,fyk) that takes the
cross-section dimensions and the material properties as
inputs. The
block of code inside this function will be exactly the same as
the code found in the cells of the notebook, so it is just a
matter of copy-pasting the contents of each cell. Because
The function will use numpy arrays, we need to import the
library with the usual line of code import numpy as np, which
will be placed in the first line of the file.
Here you have the whole function written in a single code
block. You
can copy-paste everything inside your file, or download a
copy of MN.py
from https://python4civil.weebly.com/m-n-diagram.html.
1 import numpy as np
def getDomain(a,b,c,As1,As2,fck,fyk):
'''
PARAMETERS:
5
b = section width (mm)
6
As1 = bottom rebar area (mm^2)
7 RETURNS:
'''
8 d=a-c
9
10 #concrete
gamma_c=1.5
fcd=0.85*fck/gamma_c #MPa
11 eps_cc=-0.0020
eps_cu=-0.0035
12
#steel
13 gamma_s=1.15
fyd=fyk/gamma_s #MPa
14 Es=200000 #MPa
eps_su=0.01
eps_y=fyd/Es
15
16
def rel_c(eps):
17
18 n=len(eps)
sig=np.zeros(n)
19
for i in range(n):
20
21 if eps[i]>=0:
sig[i]=0
22
elif 0>eps[i] and
eps[i]>=eps_cc:
23
sig[i] =-fcd*(2-
24 eps[i]/eps_cc)*eps[i]/eps_cc
26
sig[i]=-fcd
27
else:
28
print('invalid eps
value')
29
return(sig)
30
31
32 def rel_s(eps):
n=len(eps)
34
sig=np.zeros(n)
35
for i in range(n):
36
if -eps_su<=eps[i] and eps[i]
<=-eps_y:
37
sig[i]=-fyd
38
40 sig[i]=eps[i]*Es
<=eps_su:
42
sig[i]=fyd
43
else:
44
print('invalid eps
value')
45
return(sig)
46
47
48 n_step=50
# DOMAIN I
yn_sup=-9999
49 yn_inf=0
psi=eps_su/(d-yn)
51
eps_s1=np.full(n_step, eps_su)
52
eps_s2=-psi*(yn-c)
53
sig_s1=rel_s(eps_s1)
54
sig_s2=rel_s(eps_s2)
55
Nrd_1=As1*sig_s1+As2*sig_s2
56
Mrd_1=As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-c)
57
58
#DOMAIN II
yn_sup=0.1
59
yn_inf=-d*eps_cu/(eps_su-eps_cu)
61
psi=eps_su/(d-yn)
62
eps_s1=np.full(n_step, eps_su)
63
eps_s2=-psi*(yn-c)
64
sig_s1=rel_s(eps_s1)
65
sig_s2=rel_s(eps_s2)
66
Nc=np.zeros(n_step)
67
Mc=np.zeros(n_step)
68
for i in range(n_step):
69
eps_c=-(yn[i]-y)*psi[i]
71
eps_c=np.round(eps_c, 7)
72
sig_c=rel_c(eps_c)
73
Nc[i]=b*np.trapz(sig_c, y)
74
ygc =np.nan_to_num(np.trapz(sig_c*y,
75 y)/np.trapz(sig_c, y))
76 Mc[i]=Nc[i]*(a/2-ygc)
Nrd_2=Nc+As1*sig_s1+As2*sig_s2
77
78 Mrd_2 =-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-
c)
79
80
#DOMAIN III
yn_sup=-d*eps_cu/(eps_su-eps_cu)
81
yn_inf=-d*eps_cu/(eps_y-eps_cu)
82
psi=-eps_cu/yn
84
eps_s1=-psi*(yn-d)
85
eps_s2=-psi*(yn-c)
86
eps_s1=np.round(eps_s1, 5)
87
eps_s2=np.round(eps_s2, 5)
88
sig_s1=rel_s(eps_s1)
89
sig_s2=rel_s(eps_s2)
90
Nc=np.zeros(n_step)
91
Mc=np.zeros(n_step)
92
for i in range(n_step):
93
95 eps_c=-(yn[i]-y)*psi[i]
96 eps_c=np.round(eps_c, 7)
97 sig_c=rel_c(eps_c)
98 Nc[i]=b*np.trapz(sig_c, y)
99 ygc =np.trapz(sig_c*y,
y)/np.trapz(sig_c, y)
100
Mc[i]=Nc[i]*(a/2-ygc)
101
Nrd_3=Nc+As1*sig_s1+As2*sig_s2
102
Mrd_3 =-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-
c)
103
104
#DOMAIN IV
105 yn_sup=-d*eps_cu/(eps_y-eps_cu)
106 yn_inf=a
108 psi=-eps_cu/yn
109 eps_s1=-psi*(yn-d)
110 eps_s2=-psi*(yn-c)
111 sig_s1=rel_s(eps_s1)
112 sig_s2=rel_s(eps_s2)
113 Nc=np.zeros(n_step)
114 Mc=np.zeros(n_step)
117 eps_c=-(yn[i]-y)*psi[i]
118 eps_c=np.round(eps_c, 7)
119 sig_c=rel_c(eps_c)
120 Nc[i]=b*np.trapz(sig_c, y)
y)/np.trapz(sig_c, y)
122
Mc[i]=Nc[i]*(a/2-ygc)
123
Nrd_4=Nc+As1*sig_s1+As2*sig_s2
124
Mrd_4 =-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-
c)
125
126
127 #DOMAIN IV
yn_sup=a
128
yn_inf=a+9999
t=3/7*a
130
psi=-eps_cc/(yn-t)
131
eps_s1=-psi*(yn-d)
132
eps_s2=-psi*(yn-c)
133
sig_s1=rel_s(eps_s1)
134
sig_s2=rel_s(eps_s2)
135
Nc=np.zeros(n_step)
136
Mc=np.zeros(n_step)
137
for i in range(n_step):
138
y=np.linspace(0, a, 50)
139
eps_c=-(yn[i]-y)*psi[i]
140
eps_c=np.round(eps_c, 7)
141
sig_c=rel_c(eps_c)
142
Nc[i]=b*np.trapz(sig_c, y)
143
ygc =np.trapz(sig_c*y,
144 y)/np.trapz(sig_c, y)
145 Mc[i]=Nc[i]*(a/2-ygc)
Nrd_5=Nc+As1*sig_s1+As2*sig_s2
146
Mrd_5 =-Mc+As1*sig_s1*(a/2-c)-As2*sig_s2*(a/2-
147
c)
148
149
Mrd =np.hstack((Mrd_1*1E-6,Mrd_2*1E-6,
Mrd_3*1E-6,
150
Mrd_4*1E-6, Mrd_5*1E-6))
151
Nrd =np.hstack((Nrd_1*1E-6,Nrd_2*1E-6,
152 Nrd_3*1E-6,
154
155
return((Nrd, Mrd))
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
Designing a concrete
beam
In this chapter we will design a two-span concrete beam in
bending and shear, using the domain calculated in the
previous chapter. The beam is exactly the same as the one
in in the chapter about pandas, so we will import the
bending moment and shear distributions as .csv files. This is
a really simple example, so only two different load
combinations will be considered. Still, two will be more than
enough to explain how to find and plot the envelope and the
maximum-minimum values. Then, we will find the critical
sections to be considered, and calculate MRd for each one of
them. Finally, we will move on to the shear verification of
the beam.
Throughout this chapter, verifications will be
performed according to UNI EN 1992-1-1. Even if you are not
familiar with european standards, you won't have any
problem understanding this example. The goal is learning
how to use python, not learning how to use the standards.
By the end of this chapter, you won't have any problems
applying what you have learned to your particular design
environment.
Dimensions and loads
The load combinations are shown in the next figure:
Importing the necessary libraries
In this example we will use numpy, pandas and
matplotlib. In addition, we will also use the MN.py file
created at the end of the previous chapter. Create a new
folder containing MN.py and an empty jupyter notebook. In
the first cell write:
1
import numpy as np
2
import pandas as pd
3
import matplotlib.pyplot as plt
4 import MN
the last line of code loads all the contents of MN.py in the
notebook. Now we have access to the function getDomain(),
and we use it by writing MN.getDomain().
NOTE
If you modify the contents of MN.py after it has
been loaded in the notebook, the changes won't
register untill you restart the notebook's kernel.
Calculating the envelope of the
distributions
Next, we need to import the bending moment and shear
distributions. All the data we need is contained in a file
called beam.csv that can be downloaded from
https://python4civil.weebly.com/concrete-beam.html. The
first column of the file contains the x coordinates of the
beam, and the rest contain the values of the bending
moment and shear at each point. In the next code block we
use pandas to load the data and store it inside five numpy
arrays:
1
data=pd.read_csv("beam.csv")
2
x=np.array(data.x)
3
M1=np.array(data.M1) #kNm
4
M2=np.array(data.M2) #kNm
5
V1=np.array(data.V1) #kN
6 V2=np.array(data.V2) #kN
1
M_neg=np.minimum(M1,M2)
2
M_pos=np.maximum(M1,M2)
4
M_neg[M_neg>0]=0
5
M_pos[M_pos<0]=0
7
V_neg=np.minimum(V1,V2)
8
V_pos=np.maximum(V1,V2)
10
V_neg[V_neg>0]=0
11 V_pos[V_pos<0]=0
2
maxima=[]
3
index=[]
4
n=len(a)
5
for i in range(n):
6
if i==0:
7
if a[0]>a[1]:
8
maxima.append(a[0])
9
index.append(i)
10
elif i==n-1:
11
if a[-1]>a[-2]:
12
maxima.append(a[-1])
13
index.append(i)
14
else:
15
if a[i]>=a[i-1] and a[i]>a[i+1]:
16
maxima.append(a[i])
17 index.append(i)
1
M_pos_max, index_pos_M=find_maxima(M_pos)
2
M_neg_max, index_neg_M=find_maxima(abs(M_neg))
3
M_pos_max=np.array(M_pos_max)
4
M_neg_max=-np.array(M_neg_max)
6
V_pos_max, index_pos_V=find_maxima(V_pos)
7
V_neg_max, index_neg_V=find_maxima(abs(V_neg))
8
V_pos_max=np.array(V_pos_max)
9 V_neg_max=-np.array(V_neg_max)
2
b=300 #mm
3
c=50 #mm
4
d=a-c #mm
5
z=0.9*d #mm
6
Abar=201.062 #mm^2
8
fck=25 #N/mm^2
9
fyk=450 #N/mm^2
10
gamma_s=1.15
11
gamma_c=1.5
12 fcd=0.85*fck/gamma_c #N/mm^2
where:
2
Mrd1=np.interp(0, Nrd_tot[::-1], Mrd_tot[::-1])
3
Nrd_tot, Mrd_tot=MN.getDomain(a, b, c, 3*Abar, 2*Abar,
4
fck, fyk)
Mrd2=-Mrd3
NOTE
The function getDomain calculates the resistance
domain for positive bending moments. To calculate
the resistance to negative bending moments we
simply need to switch the positions of the
reinforcement bars, and treat the negative bending
moment as if it was positive.
Section verifications
Now that we know the bending moment resistances for each
of the three section we can compare them with the design
bending moments calculated before. A simple if-else
statement will suffice for this example:
1
if M_pos_max[0]<Mrd1:
2
print("sec. 1: VERIFIED")
3
else:
4
print("sec. 1: NOT VERIFIED")
5
if M_neg_max[0]>Mrd2:
6
print("sec. 2: VERIFIED")
7
else:
8
print("sec. 2: NOT VERIFIED")
9
if M_pos_max[1]<Mrd3:
10
print("sec. 3: VERIFIED")
11
else:
sec. 1: VERIFIED
sec. 2: VERIFIED
sec. 3: VERIFIED
with a minimum of
where:
fck is in MPa
1+(200/d)0.5 with d in mm
ρl=Asl/(bwd)≤0.02
Ssl is the area of the rebarsunder tension
bw is the width of the cross-section
σcp=NEd/Ac<0.2fcd MPa
NRd is the axial force in the cross-section
VRd,c is in newton.
Vmax = 75 kN
1
bw=b
2
k=1+(200/d)**0.5
3
rho_l=max(3*Abar/(bw*d), 0.02)
4
sig_cp=0
5
Ac=b*a
6
CRd_c=0.18/gamma_c
7
k1=0.15
8
nu_min=0.035*k**1.5*fck**0.5
10
Vrd_c=max((CRd_c*k*(100*rho_l*fck)**(1/3)+k1*sig_cp)*bw*d,
11
(nu_min+k1*sig_cp)*bw*d)
Vrd_c = 81 kN
1
Asw=100 #mm^2, 2phi8
2
s=250 #mm, step
3
rho_w=Asw/(s*bw)
4
rho_w_min=(0.08*(fck)**0.5)/fyk
5
print("rho_w= \%.6f"\%(rho_w))
6 print("rho_w_min= \%.6f"\%(rho_w_min))
rho_w= 0.001333
rho_w_min= 0.000889
Conclusions
Hopefully this chapter has given you a solid starting point to
begin developing your own member verifications in python.
Later in this book you will learn how to use pandas to
create more scalable applications, and also how to typeset
the results in various useful formats.
Designing a steel column
The goal of this chapter is to design a steel column subject
to vertical and horizontal loads. We will use pandas
manage different load combinations, and conveniently store
the results in a table. The verifications will be carried out in
accodance to the eurocodes, but like in the previous chapter
you don't need to be familiar with the european standards
to follow along.
As always, let's create a new jupyter notebook and import
all the necessary libraries:
1
import numpy as np
2 import pandas as pd
L
In this example we will consider L to be 7.5 m, and the
loads to be grouped in three different load combinations:
Now let's input all these values in the notebook. We will use
a variable named L to store the length, and a dataframe
named loads to store the loads combinations:
1
L=7.5*10**3 #mm
2
combo1=pd.Series({"Ned":300, "Hed":30}, name="Combo1")
3
combo2=pd.Series({"Ned":500, "Hed":10}, name="Combo2")
4
combo3=pd.Series({"Ned":250, "Hed":40}, name="Combo3")
5
6
loads=pd.DataFrame([combo1, combo2, combo3])
7
loads["Med"]=loads.Hed*L*10**(-3)
8 loads
1
fyk=355 #N/mm^2
2
E=200000 #Mpa
3 G = 81000 #N/mm^2
Cross-section
Now let's pick a standard hot-rolled section and store its
properties into correspondent variables. We will consider a
HE300B, with the following characteristics:
1
# HE300B
2
h=300 #mm (section height)
3
b=300 #mm (section width)
4
tw=11 #mm (web width)
5
tf=19 #mm (flange width)
6
A=161.3*10**2 #mm^2 (total area)
7
Iy=25170*10**4 #mm^4 (second moment of area, y axis)
8
iy=13*10 #mm (radius of gyration, y axis)
9
Wy=1678*10**3 #mm^3 (elastic section modulus, y axis)
10
11
Iz=8563*10**4 #mm^4 (second moment of area, z axis)
12
iz=7.58*10 #mm (radius of gyration, z axis)
13
Wz=570.9*10**3 #mm^3 (elastic section modulus, z axis)
14
15
It=185*10**4 #mm^4 (torsional constant)
16
Iw=(Iz*(h-tf)**2)/4 #mm^6 (warping constant)
17
18
fyk=355 #N/mm^2
19
E=200000 #Mpa
20 G = 81000 #N/mm^2
where
In our case:
2
Lcr_y = 2*L
3
lambda_y = (Lcr_y/iy)*(1/lambda_1)
4
phi_y = 0.5*(1+alpha_y*(lambda_y-0.2)+lambda_y**2)
5
chi_y = 1/(phi_y+np.sqrt(phi_y**2-lambda_y**2))
6
print("alpha_y= %.2f"%(alpha_y))
7
print("Lcr_y= %.2f"%(Lcr_y))
8
print("lambda_y= %.2f"%(lambda_y))
9
print("phi_y= %.2f"%(phi_y))
10 print("chi_y= %.7f"%(chi_y))
alpha_y= 0.21
Lcr_y= 15000.00
lambda_y= 1.55
phi_y= 1.84
chi_y= 0.3531319
k=0.7
kw=0.7
C1 = 2.092
alpha_lt = 0.34
lambda_lt_0 = 0.4
beta = 0.75
Mcr = C1*np.pi**2*E*Iz/((k*L)**2)*np.sqrt((k/kw)*Iw/(Iz)\
+(k*L)**2*G*It/(np.pi**2*E*Iz))*1e-6
10
lambda_lt = np.sqrt(Wy*fyk/(Mcr*1e6))
11
phi_lt = 0.5*(1+alpha_lt*(lambda_lt-
12
lambda_lt_0)+beta*lambda_lt**2)
13
chi_lt = 1/(phi_lt+np.sqrt(phi_lt**2-beta*lambda_lt**2))
14
15
print("Mcr= %.2f"%(Mcr))
16
print("lambda_lt= %.2f"%(lambda_lt))
17
print("phi_lt= %.2f"%(phi_lt))
18
print("chi_lt= %.2f"%(chi_lt))
Mcr= 2696.44
lambda_lt= 0.47
phi_lt= 0.59
chi_lt= 0.97
k=0.7
kw=0.7
C1 = 2.092
alpha_lt = 0.34
lambda_lt_0 = 0.4
beta = 0.75
Mcr = C1*np.pi**2*E*Iz/((k*L)**2)*np.sqrt((k/kw)*Iw/(Iz)\
+(k*L)**2*G*It/(np.pi**2*E*Iz))*1e-6
10
lambda_lt = np.sqrt(Wy*fyk/(Mcr*1e6))
11
phi_lt = 0.5*(1+alpha_lt*(lambda_lt-
12
lambda_lt_0)+beta*lambda_lt**2)
13
chi_lt = 1/(phi_lt+np.sqrt(phi_lt**2-beta*lambda_lt**2))
14
15
print("Mcr= %.2f"%(Mcr))
16
print("lambda_lt= %.2f"%(lambda_lt))
17
print("phi_lt= %.2f"%(phi_lt))
18
print("chi_lt= %.2f"%(chi_lt))
kyy= 1.08
kzz= 1.44
kyz= 0.86
kzy= 1.00\\
The last elements that we need are NRk and $MRk,y, given by
the following expressions:
1
Nrk = A*fyk*0.001 # kN
2
Mrk_y = Wy*fyk*1e-6 # kNm
4
print("Nrk= %.2f"%(Nrk))
5 print("Mrk_y= %.2f"%(Mrk_y))
Nrk= 5726.15
Mrk_y= 595.69
Member verification
We finally have everything that we need to implement the
verification formulas:
1
gamma_m1 = 1.05
2
results=pd.DataFrame()
3
results["check1"] = loads.Ned/(chi_y*Nrk/gamma_m1)\
4
+kyy*(loads.Med)/(chi_lt*Mrk_y/gamma_m1)
5
results["check2"] = loads.Ned/(chi_y*Nrk/gamma_m1)\
6
+kzy*(loads.Med)/(chi_lt*Mrk_y/gamma_m1)
7 results
check1 check2
Combo1 0.596262 0.563633
Combo2 0.406460 0.395584
Combo3 0.717126 0.673621
NOTE
Even if you are not familiar with LaTeX, you can still follow
along the code written in this chapter. The code works
anyway, whether you have installed nbextensions or not.
Installing nbextensions
If you followed the Anaconda installation at the beginning of the
book,
your computer should also have installed anaconda prompt
by
default. Anaconda prompt is a command line interface that allows
the
user to install additional packages using the pip or
conda
package managers. We will use conda to install
nbextensions.
Open anaconda navigator, and type: conda install -c conda-forge
jupyter\_contrib\_nbextensions
NOTE
If you have trouble installing nbextensions, you can find
plenty of tutorials online. I suggest you take a look to the
official nbextensions installation guide, which can be found
here:
https://jupyter-contrib-
nbextensions.readthedocs.io/en/latest/install.html
Now when you start jupyter notebook you can access the
configuration page by
writing "nbextensions" in place of "tree" in the
address bar. The
configuration page looks like this:
From here you can activate the extensions that you need. In order to
be
able to write latex syntax inside markdown cell we will need the
extension called (some) LaTeX environments for Jupyter, and in
order to hide code cells we will also need the hide input
extension.
Now create a new notebook. You should see some new icons in the
toolbar:
The button with the ^ symbol is used to hide the input of code cells.
Now that LaTeX_envs is enabled we can also use some basic LaTeX
environments inside markdown cells, like section and
itemize for
example, or math formulas using dollar signs.
Formatting the output of cells in LaTeX
If the final goal is to output the notebook as a LaTeX document (.tex),
then we need a way to display the outputs of code cells accordingly.
A
way to do this is using the Latex module contained in
IPython.display:
1
from IPython.display import Latex
2
a=15
3
b=7
4
res=a+b
5 display(Latex("$a+b=%d+%d=%d$"%(a,b,res)))
a
+
b
=
15
+
7
=
22
As you can see the output of the cell is now beautifully rendered in
LaTeX.
Converting dataframes to LaTeX tables
Pandas gives the user the possibility to convert any dataframe into a
LaTeX table, using pandas.DataFrame.to\_latex(). Here is an
example:
1
import pandas as pd
2
df=pd.DataFrame({"A":[3,2,1,6], "B":[4,5,9,7]}, index=[0,1,2,3])
3 display(Latex(df.to_latex(bold_rows=True, column_format="lll")))
\begin{tabular}{lll}
\toprule
{} & A & B \\
\midrule
\bottomrule
\end{tabular}
2
"\\begin{table}[H]\n\centering\n"\
3
+df.to_latex(bold_rows=True, column_format='lll')\
\begin{table}[H]
\centering
\begin{tabular}{lll}
\toprule
{} & A & B \\
\midrule
\bottomrule
\end{tabular}
\caption{My Table}
\end{table}
All three beams will have the same cross section and material. We
will
use a standard IPE220, with a second moment of inertia around
the strong axis of 2772 mm4. The young modulus of the steel is
200000 Mpa. As always, the first step is to save all the relevant
data
into variables or dataframes:
1
import pandas as pd
2
import numpy as np
4
Iy=2772*10**4 #mm^4
5
E=200000 #Mpa
7
loads=np.array([20, 18, 13]) #kN/m
8 lengths=np.array([5, 6, 6.5]) #m
3
x=np.linspace(0,lengths[0], 51)
4
M1=loads[0]*lengths[0]*x/2-(loads[0]*x**2)/2
5
fig, ax= plt.subplots()
6
ax.invert_yaxis()
7
ax.plot(M1)
8
plt.savefig("plot1.pdf")
9 plt.show()
Then we can start adding columns. Let's add the loads and the
lengths first:
1
results["l"]=lengths
2
results["q"]=loads
4
display(Latex(
5
"\\begin{table}[H]\n\centering\n"\
6
+results.to_latex(bold_rows=True, column_format='lll')\
\begin{table}[H]
\centering
\begin{tabular}{lll}
\toprule
{} & l & q \\
\midrule
\bottomrule
\end{tabular}
\caption{My Table}
\end{table}
results["$w_{max}$"]=(5/384)*(results.q*results.l**4)/(E*Iy*0.001**3)
display(Latex(
"\\begin{table}[H]\n\centering\n"\
+results.to_latex(bold_rows=True, column_format='lllll',
6
escape=False)\
7
+"\caption{My Table}\n \end{table}"))
\begin{table}[H]
\centering
\begin{tabular}{lllll}
\toprule
\midrule
\bottomrule
\end{tabular}
\caption{My Table}
\end{table}
There are a few things to note here. First, the headers of the new
columns that have been created contain latex syntax. It will look
ugly in jupyter, but once the notebook has been exported in latex
format the table will be rendered correctly. Also, there is a new
argument passed to to_latex() which is escape, set to False. This
prevents pandas from escaping special latex characters in column
names.
Exporting the notebook
Now we come to the final step of this chapter, which is exporting the
notebook that we have just created. The tool that we will use is
called nbconvert. It is a command line interface that comes pre-
installed with jupyter and can be accessed with anaconda prompt.
Maybe you have never used a command line interface, so let's first
explain what it is and what you can do with it.
Using nbconvert
This will update the existing latex file already present in the folder.
Now open it with your favourite LaTeX editor and run it: the result
should be a nicely typesetted document without any python code
showing, and with nice images and tables.
Conclusions
Most engineers know how to use LaTeX to create professional
documents. Being able to perform calculations on a jupyter notebook
and then to export everything in a .tex file can save a lot of time,
and automate something that normally would take hours. I invite you
to try to customize the template file that was used to export the
notebook so that it better suits your needs.
Wrapping up
You have finally reached the end of this book. You should now have a
solid understanding of the python programming language, and how
to use it effectively. If this was the first time you approached
programming, congratulations, the hardest part of the learning
process is now behind you. If instead you were already a seasoned
programmer, hopefully you still found some valuable knowledge in
here. In any case, you now have the tools necessary to solve a large
variety of problems. Remember that the more you code the easier it
gets, so don't stop here!