-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpreprocessing.py
1288 lines (953 loc) · 41.2 KB
/
preprocessing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#This contains all classes required for preprocessing.
#Libraries
import numpy as np
import pandas as pd
import sympy as sp
from sympy.solvers.solveset import linsolve
#Class for Bounday Condition
class BC():
#Defining a counter variable to keep track of number of BC
#Counter serves as BC id
count = 0
#Keeping a list of dof_ids where boundary conditions are applied
dof_ids = []
def __init__(self, n_id, comp, value):
#Increment counter
BC.count += 1
#Set BC id
self.id = BC.count
#Set node object
self.n = Node.ndict[n_id]
#Set BC
self.n.dofs[comp].value = float(value)
#Add DOF id to list
BC.dof_ids.append(self.n.dofs[comp].id)
#Class for DOF
class DOF():
#Defining a counter as class variable
#Keeps count of the number of elements created
#Counter serves as dof id
count = 0
#Defining a list to keep track of all created dof_ids
dof_ids = []
#Defining a dictionary to keep track of created DOF objects
created_dofs = {}
def __init__(self, symbol, value=None):
'''
Initializer for the DOF class
Inputs
symbol - String - ux or uy
value - Float value - By default set to None
'''
#Increment counter
DOF.count += 1
#Setting DOF id
self.id = DOF.count
#Setting the symbol
self.symbol = symbol
#Setting the value
self.value = value
#Add the dof_id to the list
DOF.dof_ids.append(self.id)
#Add the created DOF object to the dictionary
#indexed by id
DOF.created_dofs[self.id] = self
#Class for Bar Element
class Element():
#Defining a counter as class variable
#Keeps count of number of elements created
#Counter serves as element id
count = 0
#Creating a dictionary to store created elements
edict = {}
#Defining number of dofs in element
num_dofs = 4
#Initializer
def __init__(self, n1, n2, A, mat):
'''
Create a bar element between two nodes n1 and n2
Inputs -
n1 - Node - first node of the element
n2 - Node - second node of the element
'''
#Incrementing counter
Element.count += 1
#Setting element id
self.id = Element.count
#Creating a placeholder for material
self.mat = mat
#Setting the area of the element
self.A = A
#Defining a vector of axial displacements of the element
self.u_axial = np.zeros(shape=(Element.num_dofs, 1))
#Defining weights and points for gauss integration
self.gp = [0]
self.w = [2]
#Initialising a list to store the tangent stiffness values
#at the gauss points and initialising the value to be Young's Modulus
self.Et_values_at_gp = [self.mat.E]
#Defining an array to store strains at gauss points
self.eps_gp_arr = np.zeros(shape=(len(self.gp)))
#Defining a list to store stresses at gauss points
self.sig_gp_arr = np.zeros(shape=(len(self.gp)))
#Defining a list to store boolean values that indicate whether
#material has yielded at gauss point
self.yield_flags = np.full(shape=(len(self.gp)), fill_value=False, dtype=bool)
#Creating a dictionary to store nodes belonging to an element
self.n = {}
#Adding node objects to the dictionary
self.n[0] = n1
self.n[1] = n2
#Creating a list to keep track of dof_ids belonging to an element
self.dof_ids = []
#Adding dof_ids to created list
for n in self.n.values():
for d in n.dofs.values():
self.dof_ids.append(d.id)
#Adding the created element to the edict dictionary
Element.edict[self.id] = self
#Computing element geometry
self.compute_geometry()
#Set the transformation matrix for the element
self.set_transformation_matrix()
#Compute the global indices of the element
self.generate_global_indices()
#Generate the global stiffness matrix
self.generate_stiffness_matrix()
#Transform generated stiffness matrix
self.transform_k_local_global()
@classmethod
def create_elements_from_csv(cls, f):
'''
This method acts as an alternative initializer for the Element class
and provides a way to create multiple Element objects from nodal list
in a csv
Inputs:
f - string - path to csv file
Note:
csv file should have columns n1, n2 for node ids
'''
#Creating a dataframe from the imported data from csv
df = pd.read_csv(f)
#Compute the number of rows in dataframe
num_rows = df.shape[0]
#Loop through the rows
for i in range(num_rows):
#Extract the node ids from a single row
n1_id = df.iloc[i]['n1']
n2_id = df.iloc[i]['n2']
#Extracing the area from the row
A = df.iloc[i]['A']
#Extracting the material from the row
mat_name = df.iloc[i]['mat']
#Extracting the node objects corresponding to node ids
n1 = Node.ndict[n1_id]
n2 = Node.ndict[n2_id]
#Extracting material object using mat_name
mat = Material.mat_dict[mat_name]
#Create node by calling initializer
cls(n1, n2, A, mat)
#Deleting dataframe after elements have been created
del df
@classmethod
def define_symbolic_variables(cls):
'''
This method defines all the symbolic variables needed to generate
all the quantities in Element.symbolic_quantities_generator()
'''
#Natural Coordinate
cls.xi = sp.symbols('xi')
#Material and geometry
cls.E = sp.symbols('E')
cls.L = sp.symbols('L')
cls.A = sp.symbols('A')
#Degrees of freedom of bar element
cls.u1 = sp.symbols('u1')
cls.u2 = sp.symbols('u2')
#Defining Jacobian Matrix
cls.J = sp.Matrix([[0.5*cls.L]])
@classmethod
def display_elements(cls):
#Defining column widths and padding
col_width = 7
col_pad = 3
#Computing total cell length - width + padding
cell_width = col_width + col_pad
#Printing note
print('Angles are measured counter clockwise at first node in an element')
print()
#Column names
col_names = ['Elem ID', 'mat', 'n1', 'n2', 'area', 'len', 'deg']
print(''.join(name.ljust(cell_width) for name in col_names))
#Horizontal line below column name
hl = '-'*col_width
print(''.join(hl.ljust(cell_width) for name in col_names))
#Looping through each created element
for e in cls.edict.values():
#Creating a row of the output table
row = []
row.append('{}'.format(e.id))
row.append(e.mat.name)
row.append('{}'.format(e.n[0].id))
row.append('{}'.format(e.n[1].id))
row.append('{:.2f}'.format(e.A))
row.append('{:.2f}'.format(e.L))
row.append('{:.2f}'.format(e.theta_deg))
print(''.join(cell.ljust(cell_width) for cell in row))
#Final print to create space
print()
@classmethod
def set_material(cls, mat):
'''
Sets the material for every single element created
Inputs - Material - material object
'''
#Looping through all the elements and setting material
for e in cls.edict.values():
e.mat = mat
@classmethod
def generate_transformation_matrix_sym(cls):
'''
This method is to set the symbolic transformation matrix
to relate quantities from the local coordinate system to the
global coordinate system
U = Tu
'''
#Defining a symbolic angle
cls.phi = sp.symbols('phi')
#Defining symbolic transformation matrix
#T transforms quantities from local coordinate to global coordiante
cls.T = sp.Matrix(np.zeros(shape=(4, 4)))
cls.T[0, 0] = sp.cos(cls.phi)
cls.T[0, 1] = -sp.sin(cls.phi)
cls.T[1, 0] = sp.sin(cls.phi)
cls.T[1, 1] = sp.cos(cls.phi)
cls.T[2, 2] = sp.cos(cls.phi)
cls.T[2, 3] = -sp.sin(cls.phi)
cls.T[3, 2] = sp.sin(cls.phi)
cls.T[3, 3] = sp.cos(cls.phi)
#Defining the inverse of the transformation matrix
cls.T_inv = cls.T.inv()
@classmethod
def stiffness_integrand_generator(cls):
#Field Variable
u = sp.symbols('u')
#Defining coefficients for linear interpolation
alpha0, alpha1 = sp.symbols(['alpha_0', 'alpha_1'])
#Defining linear interpolation function
u = alpha0 + alpha1*cls.xi
#Substituting the values for xi at the nodes
#Node1
u1e = sp.Eq(cls.u1, u.subs(cls.xi, -1))
#Node2
u2e = sp.Eq(cls.u2, u.subs(cls.xi, 1))
#Solving for the coefficients in terms of the nodal values of the
#degrees of freedom using sp.solvers.solveset.linsolve. Returns a
#finite set.
res = linsolve([u1e, u2e], alpha0, alpha1)
#Extracting the values of the Sympy FiniteSet into the variables
#The elements the FiniteSet can be accessed via .args(index)
#which gives a tuple containing sympy tuple
alpha0_s = res.args[0][0]
alpha1_s = res.args[0][1]
#Substitute the coefficients back into the equation for the field variable
u = u.subs([(alpha0, alpha0_s), (alpha1, alpha1_s)])
#Group the terms of the nodal degrees of freedom
u = sp.collect(sp.expand(u), [cls.u1, cls.u2])
#Collecting the shape functions into separate variables
N1 = u.coeff(cls.u1)
N2 = u.coeff(cls.u2)
#Defining the shape function matrix
cls.N = sp.Matrix([[N1, N2]])
#Defining the onezero matrix
onezero = sp.Matrix([[1]])
#Defining Gamma Matrix - inverse Jacobian
gamma = cls.J.inv()
#Defining DN Matrix
DN = sp.diff(cls.N, cls.xi)
#Computing the transformed strain displacement matrix Bt
cls.Bt = onezero*gamma*DN
#Defining the integrand
cls.stiffness_integrand = cls.E*cls.A*(cls.Bt.T*cls.Bt)*cls.J.det()
@classmethod
def symbolic_quantities_generator(cls):
'''
This method generates all the symbolic quantities needed.
These include the stiffness matrix integrand, dof_vec, strain, stress
'''
#Define necessary symbolic variables
cls.define_symbolic_variables()
#Generate the integrand for calculating stiffness matrix
cls.stiffness_integrand_generator()
#Symbolic element degree of freedom vector
cls.dof_vec_sym = sp.Matrix([[cls.u1], [cls.u2]])
#Symbolic strain vector
cls.eps_sym = cls.Bt*cls.dof_vec_sym
#Symbolic stress vector
cls.sig_sym = cls.E*cls.eps_sym
#Symbolic internal force integrand
cls.int_force_integrand = cls.Bt.T*cls.sig_sym*cls.A*cls.L*0.5
def compute_axial_displacements(self):
'''
This method is basically to compute the axial displacements of the element.
The degrees of freedom of a node are in global coordinates i.e UX and UY
The element formulation is with axial displacements u1 and u2 for both nodes.
Hence we need to apply the transformation u = T_inv*U
U here corresponds to self.dof_vec
'''
#Compute the axial dispalcement vector by inverse transform
self.u_axial = np.matmul(self.T_inv, self.dof_vec)
def compute_dof_vec(self):
'''
Function to compute the degree of freedom vector of the element
'''
#Creating a list to store the the dof values
dof_values_list = []
#Looping through the nodes
for n in self.n.values():
#Append dof value to the list
dof_values_list.append(n.dofs['UX'].value)
dof_values_list.append(n.dofs['UY'].value)
#Converting list to array
self.dof_vec = np.array(dof_values_list)
#Reshaping
dof_vec_shape = (Element.num_dofs, 1)
self.dof_vec = np.reshape(self.dof_vec, newshape=dof_vec_shape)
def compute_geometry(self):
'''
Method to compute the length and angle of the element
'''
#Extracting the coordinates
#Node 1
x1 = self.n[0].x
y1 = self.n[0].y
#Node 2
x2 = self.n[1].x
y2 = self.n[1].y
#Computing length
self.L = np.sqrt((x2-x1)**2 + (y2-y1)**2)
#Computing the angle
m = (y2-y1)/(x2-x1)
self.theta = np.arctan(m)
#Converting angle to degrees for display
self.theta_deg = np.degrees(self.theta)
def compute_internal_force(self):
'''
This method computes the internal force vector for the element.
'''
#Computing the axial internal force by substituting in the symbolic internal force term
temp = self.gauss_integrator(Element.int_force_integrand)
#Convert sympy array to numpy
temp = np.asarray(temp).astype(np.float64)
#The internal force vector returned by the gauss integrator is 2x1 which only contains the axial components
#Zeros are added to convert it from 1d to 2d
self.int_force_axial = np.zeros(shape=(Element.num_dofs, 1))
self.int_force_axial[0, 0] = temp[0, 0]
self.int_force_axial[2, 0] = temp[1, 0]
#Transforming this to the global coordinate system from axial coordinate system
self.int_force_global = np.matmul(self.T, self.int_force_axial)
def compute_strain(self):
'''
This method computes the element strain at all the gauss points
'''
#Looping through all gauss points
for i in range(len(self.gp)):
#Extracting the natural coordinate of the gauss point
xi_val = self.gp[i]
#Defining a list of values that will be used for substitution
sub_list = [(Element.xi, xi_val), (Element.L, self.L),
(Element.u1, self.u_axial[0, 0]),
(Element.u2, self.u_axial[2, 0])]
#Substituting in symbolic strain
eps = Element.eps_sym.subs(sub_list)
#Converting to numpy
eps = np.asarray(eps).astype(np.float64)
#Adding strain value to the list
self.eps_gp_arr[i] = eps[0, 0]
#Check if gauss points have yielded after calculating strains
self.yield_check()
def compute_stress(self):
'''
This method computes the stress in all gauss points in an element
'''
#Loop through all the gauss points
for i in range(len(self.gp)):
#Extract the strain value at the gauss point
eps_val_at_gp = self.eps_gp_arr[i]
#Compute stress at the gauss point based on whether gauss point
#has yielded or not
#If gauss point has yielded use the stress strain polynomial in the plastic region
if self.yield_flags[i] == True:
self.sig_gp_arr[i] = self.mat.sig_poly(eps_val_at_gp)
#If the gauss point has not yielded then use standard Young's modulus
else:
self.sig_gp_arr[i] = self.mat.E*eps_val_at_gp
def gauss_integrator(self, integrand):
'''
Performs gauss integration on integrand
integrand must be a symbolic expression in xi (symbolic)
'''
#Defining a list to store the gauss integrals
gauss_integrals = []
#Looping through number of gauss points
for i in range(len(self.gp)):
#Extracting value of Et and natural coordinate at gauss point
Et_val_at_gp = self.Et_values_at_gp[i]
xi_val = self.gp[i]
sub_list = [(Element.E, Et_val_at_gp),
(Element.xi, xi_val),
(Element.L, self.L),
(Element.A, self.A),
(Element.u1, self.u_axial[0, 0]),
(Element.u2, self.u_axial[2, 0])]
gauss_integral = self.w[i]*integrand.subs(sub_list)
gauss_integrals.append(gauss_integral)
#Calculating total integrand
#Need to provide a start argument for the sum as symbolic matrix of zeros
integral = sum(gauss_integrals, sp.Matrix(np.zeros(shape=integrand.shape)))
return integral
def generate_global_indices(self):
'''
This method generates a matrix of tuples which is the same size
as the local 2d stiffness matrix. Each tuple correponds to the row,col
in the global stiffness matrix where the corresponding term of the stiffness matrix
goes
'''
#Creating a list to store the indices
global_indices_list = []
#Permuting the dof ids to get the global indices
for i in range(Element.num_dofs):
for j in range(Element.num_dofs):
tup = (self.dof_ids[i], self.dof_ids[j])
global_indices_list.append(tup)
#Converting list into array
self.global_indices = np.empty(len(global_indices_list), dtype=object)
self.global_indices[:] = global_indices_list
newshape = (Element.num_dofs, Element.num_dofs)
self.global_indices = np.reshape(self.global_indices, newshape=newshape)
def generate_reduced_indices(self):
'''
This method generates reduced indices for assembly into the reduced stiffness matrix
'''
#Making a copy of the global indices
self.reduced_indices = np.copy(self.global_indices)
#Looping through the tuples
for i in range(Element.num_dofs):
for j in range(Element.num_dofs):
#Each compoment of the reduced_indices array is a tuple of indices
#indicate where that element of the local stiffness matrix fits into the
#global stiffness matrix. Now if any of the dof_ids have a BC applied to them
#we set the tuple to be (None, None). If no bc is applied to that DOF then its
#new position in the reduced global stiffness matrix is computed by subtracting
#from that index the number of dof_ids that are lower than that index
#Setting the reudced indices to None if index is in BC.dof_ids
if any(x in self.reduced_indices[i, j] for x in BC.dof_ids) == True:
self.reduced_indices[i, j] = (None, None)
else:
#Extracting the indices of the tuple
idx1 = self.reduced_indices[i, j][0]
idx2 = self.reduced_indices[i, j][1]
#Figure out how many dof_ids are lesser than idx1 and idx2
count1 = len([k for k in BC.dof_ids if k < idx1])
count2 = len([k for k in BC.dof_ids if k < idx2])
#Updating the index values based on the respective count values
idx1 -= count1
idx2 -= count2
#Inserting the updated values back into reduced_indices matrix
self.reduced_indices[i, j] = (idx1, idx2)
def generate_stiffness_matrix(self):
'''
This method is to generate the element stiffness matrix for the
1D bar via gauss integration
'''
#Compute the symbolic 1d stiffness matrix
self.k_local_1d_s = self.gauss_integrator(Element.stiffness_integrand)
#Convert from symbolic matrix into numpy matrix
self.k_local_1d = np.array(self.k_local_1d_s).astype(np.float64)
#Converting this 1d stiffness matrix into a 2D with zeros
#for the appropriate terms in the y-direction
#This is still in local coordinate system
self.k_local_2d = np.zeros(shape=(4, 4))
self.k_local_2d[0, 0] = self.k_local_1d[0, 0]
self.k_local_2d[0, 2] = self.k_local_1d[0, 1]
self.k_local_2d[2, 0] = self.k_local_1d[1, 0]
self.k_local_2d[2, 2] = self.k_local_1d[1, 1]
#Transformation to global
self.k_global_2d = np.matmul(self.T, np.matmul(self.k_local_2d, self.T.T))
def set_transformation_matrix(self):
'''
This method is called from the initializer for Element class and
basically substitutes the values of angles in the symbolic T matrix
and computes the T matrix and its inverse for the element
'''
#Computing the transformation matrix for element
#by substituting angle
self.T = Element.T.subs(Element.phi, self.theta)
self.T = np.array(self.T).astype(np.float64)
#Computing the inverse of the transformation matrix
self.T_inv = Element.T_inv.subs(Element.phi, self.theta)
self.T_inv = np.array(self.T_inv).astype(np.float64)
def transform_k_local_global(self):
'''
This method transforms the 2D stiffness matrix from the local coordinate
system to the global coordinate system
'''
#Transforming the 2d local stiffness matrix to 2d global stiffness matrix
temp = np.matmul(self.k_local_2d, self.T_inv)
self.k_global_2d = np.matmul(self.T, temp)
def yield_check(self):
'''
This function checks if the material has yielded at the gauss points.
It returns a list of values for Et
'''
#Creating an empty list to store the Et values and which will be returned
Et_val_list = []
#Looping through the list containing strain values at the gauss points
for i in range(self.eps_gp_arr.shape[0]):
#Extracting strain value at a single gauss point
strain_at_gp = self.eps_gp_arr[i]
#If material has yielded at gauss point set the tangent modulus to updated value
#and also update the yield flag to True
if strain_at_gp >= self.mat.yp:
Et_val_list.append(self.mat.Et(strain_at_gp))
self.yield_flags[i] = True
#else set tangent modulus to be Young's Modulus
#and the Yield Flag to be False
else:
Et_val_list.append(self.mat.E)
self.yield_flags[i] = False
#Updating the list of Et values at the gauss points
self.Et_values_at_gp = Et_val_list
#Class for Load
class Load():
#Defining a counter as class variable
#Keeps count of number of loads created
#counter value serves as Load ID
count = 0
#Defining a dictionary to keep track of created Load objects
created_loads = {}
def __init__(self, symbol, value=0.0):
'''
Initializer for the load class
Inputs
symbol - String - FX or FY
value - Float value - Default is None
'''
#Increment counter
Load.count += 1
#Set load id
self.id = Load.count
#Set symbol
self.symbol = symbol
#Set value
self.value = float(value)
#Add created load to dictionary indexed by load id
Load.created_loads[self.id] = self
#Class for Material
class Material():
#Defining a dictionary to store all created material objects
mat_dict = {}
#Initializer
def __init__(self, name, E, **kwargs):
#Setting name of the material
self.name = name
#Setting the Young's Modulus of the mateelement
#typecasts user value for E into float
self.E = float(E)
#Adding material object to dictionary
Material.mat_dict[self.name] = self
#If **kwargs dictionary is not empty
if kwargs:
#Storing the stress strain polynomial in the plastic region
#as sig_poly and also calculating the expression for Tangent
#Modulus as the derivative of sig_poly. Please note sig_poly
#is a function of nominal values and is a function of the
#total strain and not just plastic strain
if 'sig_poly' in kwargs.keys():
self.sig_poly = kwargs['sig_poly']
#Computing tangent modulus
self.Et = self.sig_poly.deriv()
#Storing yield stress
if 'sig_y' in kwargs.keys():
self.sig_y = kwargs['sig_y']
#Storing the yield point
if 'yp' in kwargs.keys():
self.yp = kwargs['yp']
@classmethod
def display_material_data(cls):
#Looping through all the materials created
for mat in cls.mat_dict.values():
print('Material: {}'.format(mat.name))
print('--------------------------------------------------------------')
print('Young\'s Modulus: {}'.format(mat.E))
if hasattr(mat, 'yp'):
print('Yield point: {}'.format(mat.yp))
print()
if hasattr(mat, 'sig_poly'):
print('Stress strain polynomial in plastic region: ')
print(mat.sig_poly)
print()
print('Tangent Modulus: ')
print(mat.Et)
print()
#Class for Node
class Node():
#Defining a counter as class variable
#Keeps count of number of nodes created
#Counter value serves as node id
count = 0
#Creating a dictionary to store created nodes
#keys are the node ids
ndict = {}
#Number of DOFs per node
num_dofs = 2
#Initializer
def __init__(self, x, y):
#Incrementing node counter
Node.count += 1
#Setting node id
self.id = Node.count
#Defining the coordinates of the node
#Typecasting them to float to be sure
self.x = float(x)
self.y = float(y)
#Creating a dictionary to store the dofs
#belonging to current node
self.dofs = {}
#Initializing degrees of freedom for this node
self.dofs['UX'] = DOF(symbol='UX')
self.dofs['UY'] = DOF(symbol='UY')
#Creating a dictionary to store the loads
#belonging to current node
self.loads = {}
#Creating a dictionary to store loads with dof_id
#being the keys
self.loads_by_dof_ids = {}
#Initializing applied forces at this node
self.loads['FX'] = Load(symbol='FX')
self.loads['FY'] = Load(symbol='FY')
#Adding to the loads_by_dof_ids dictionary
self.loads_by_dof_ids[self.dofs['UX'].id] = self.loads['FX']
self.loads_by_dof_ids[self.dofs['UY'].id] = self.loads['FY']
#Adding the created node to ndict
Node.ndict[self.id] = self
#Alternative initializer to create nodes from csv
@classmethod
def create_nodes_from_csv(cls, f):
'''
This method acts as an alternative initializer for the Node class
and provides a way to create multiple node objects from
coordinate data of nodes from a csv
Inputs:
f - string - path to csv file
Note:
csv file should have columns x, y
'''
#Creating a dataframe from the imported data from csv
df = pd.read_csv(f)
#Compute the number of rows in dataframe
num_rows = df.shape[0]
#Loop through the rows
for i in range(num_rows):
#Extract the coordinate from a single row
x = df.iloc[i]['x']
y = df.iloc[i]['y']
#Create node by calling initializer
cls(x, y)
#Deleting dataframe after nodes have been created
del df
#Program to display all nodes
@classmethod
def display_nodes(cls):
#Setting column width and padding
col_width = 12
col_pad = 4
#Computing total cell length - width + padding
cell_width = col_width + col_pad
#Column names
col_names = ['Node ID', 'X', 'Y', 'UX', 'UY', 'FX', 'FY']
print(''.join(name.ljust(cell_width) for name in col_names))
#Horizontal line below column name
hl = '-'*col_width
print(''.join(hl.ljust(cell_width) for name in col_names))
#Looping through each created node
for n in cls.ndict.values():
#Creating a row of the output table
row = []
row.append(str(n.id))
row.append('{:.4f}'.format(n.x))
row.append('{:.4f}'.format(n.y))
#For UX and UY we need to check if the value is None or not
#before formatting its ouput
if n.dofs['UX'].value == None:
row.append(str(n.dofs['UX'].value))
else:
row.append('{:.4E}'.format(n.dofs['UX'].value))
if n.dofs['UY'].value == None:
row.append(str(n.dofs['UY'].value))
else:
row.append('{:.4E}'.format(n.dofs['UY'].value))
row.append('{:.2E}'.format(n.loads['FX'].value))
row.append('{:.2E}'.format(n.loads['FY'].value))
print(''.join(cell.ljust(cell_width) for cell in row))
#Final print to create space
print()
def apply_load(self, comp, value):
'''
Applies the load component at a node
comp is a string
'''
self.loads[comp].value = float(value)
def apply_load_by_dof_id(self, dof_id, value):
'''
sets value for the load at the nodes but uses
dof id to index them
'''
self.loads_by_dof_ids[dof_id].value = value
#Class for Truss
class Truss():
def __init__(self):
#Adding reference to the dictionary of nodes and elements
self.ndict = Node.ndict
self.edict = Element.edict
#Defining the dimension of the global stiffness matrix for the truss
self.global_dimension = len(Node.ndict)*Node.num_dofs
#Defining a flag value to keep track of whether the total reduced force vector
#has been generated.
self.total_reduced_force_has_been_generated = False
def assemble_full_stiffness(self):
'''
Assembles the global stiffness matrix for the truss
'''
#Define a matrix of zeros
k_shape = (self.global_dimension, self.global_dimension)
self.K = np.zeros(shape=k_shape)
#Loop through each element
for e in self.edict.values():
#Looping through the local stiffness matrix
for i in range(e.num_dofs):
for j in range(e.num_dofs):
#Extracting the global indices
gi = e.global_indices[i, j][0]
gj = e.global_indices[i, j][1]
#Subtracting 1 from global indices as
#indexings starts from 0
gi -= 1
gj -= 1
self.K[gi, gj] += e.k_global_2d[i, j]
def assemble_internal_force(self):
'''
This method loops through each element and assembles the global and then