kib-lap 0.5__cp313-cp313-win_amd64.whl → 0.7.7__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. KIB_LAP/Betonbau/TEST_Rectangular.py +21 -0
  2. KIB_LAP/Betonbau/beam_rectangular.py +4 -0
  3. KIB_LAP/FACHWERKEBEN/Elements.py +209 -0
  4. KIB_LAP/FACHWERKEBEN/InputData.py +118 -0
  5. KIB_LAP/FACHWERKEBEN/Iteration.py +967 -0
  6. KIB_LAP/FACHWERKEBEN/Materials.py +30 -0
  7. KIB_LAP/FACHWERKEBEN/Plotting.py +681 -0
  8. KIB_LAP/FACHWERKEBEN/__init__.py +4 -0
  9. KIB_LAP/FACHWERKEBEN/main.py +27 -0
  10. KIB_LAP/Plattentragwerke/PlateBendingKirchhoff.py +36 -29
  11. KIB_LAP/STABRAUM/InputData.py +13 -2
  12. KIB_LAP/STABRAUM/Output_Data.py +61 -0
  13. KIB_LAP/STABRAUM/Plotting.py +1453 -0
  14. KIB_LAP/STABRAUM/Programm.py +518 -1026
  15. KIB_LAP/STABRAUM/Steifigkeitsmatrix.py +338 -117
  16. KIB_LAP/STABRAUM/main.py +58 -0
  17. KIB_LAP/STABRAUM/results.py +37 -0
  18. KIB_LAP/Scheibe/Assemble_Stiffness.py +246 -0
  19. KIB_LAP/Scheibe/Element_Stiffness.py +362 -0
  20. KIB_LAP/Scheibe/Meshing.py +365 -0
  21. KIB_LAP/Scheibe/Output.py +34 -0
  22. KIB_LAP/Scheibe/Plotting.py +722 -0
  23. KIB_LAP/Scheibe/Shell_Calculation.py +523 -0
  24. KIB_LAP/Scheibe/Testing_Mesh.py +25 -0
  25. KIB_LAP/Scheibe/__init__.py +14 -0
  26. KIB_LAP/Scheibe/main.py +33 -0
  27. KIB_LAP/StabEbenRitz/Biegedrillknicken.py +757 -0
  28. KIB_LAP/StabEbenRitz/Biegedrillknicken_Trigeometry.py +328 -0
  29. KIB_LAP/StabEbenRitz/Querschnittswerte.py +527 -0
  30. KIB_LAP/StabEbenRitz/Stabberechnung_Klasse.py +868 -0
  31. KIB_LAP/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  32. KIB_LAP/plate_buckling_cpp.cp313-win_amd64.pyd +0 -0
  33. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/METADATA +1 -1
  34. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/RECORD +37 -19
  35. Examples/Cross_Section_Thin.py +0 -61
  36. KIB_LAP/Betonbau/Bemessung_Zust_II.py +0 -648
  37. KIB_LAP/Betonbau/Iterative_Design.py +0 -723
  38. KIB_LAP/Plattentragwerke/NumInte.cpp +0 -23
  39. KIB_LAP/Plattentragwerke/NumericalIntegration.cpp +0 -23
  40. KIB_LAP/Plattentragwerke/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  41. KIB_LAP/main.py +0 -2
  42. {Examples → KIB_LAP/StabEbenRitz}/__init__.py +0 -0
  43. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/WHEEL +0 -0
  44. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,967 @@
1
+ # DEPENDENCIES
2
+ import copy # Allows us to create copies of objects in memory
3
+ import math # Math functionality
4
+ import numpy as np # Numpy for working with arrays
5
+ import matplotlib.pyplot as plt # Plotting functionality
6
+ import matplotlib.colors # For colormap functionality
7
+ import ipywidgets as widgets
8
+ from glob import glob # Allows check that file exists before import
9
+ from numpy import genfromtxt # For importing structure data from csv
10
+ import pandas as pd
11
+
12
+ from InputData import Input
13
+ from Materials import Material
14
+ from Elements import Rope_Elements_III
15
+ from Elements import BarElements_I
16
+
17
+ class IterationClass:
18
+ def __init__(self, use_iteration=False):
19
+ print("INIT")
20
+ ##________ Subclasses __________##
21
+ self.Inp = Input()
22
+ self.Mat = Material()
23
+ self.CableNonlinear = Rope_Elements_III(self.Inp)
24
+ self.BarLinear = BarElements_I(self.Inp)
25
+
26
+
27
+ self.swt = False
28
+
29
+ ##___ Iteration parameters _________##
30
+
31
+ self.nForceIncrements = 10
32
+ self.convThreshold = 1 # (N) Threshold on average percentage increase in incremental deflection
33
+
34
+ self.checkSlackAfter = 80
35
+
36
+ # Member Types
37
+
38
+ self.memberType = []
39
+ self.ClassifyMemberType()
40
+
41
+ ##____ Containers_____##
42
+ # Initialise a container to hold the set of global displacements for each external load increment
43
+ self.UG_FINAL = np.empty([self.Inp.nDoF, 0])
44
+
45
+ # Initialise a container to hold the set of internal forces for each external load increment
46
+ self.FI_FINAL = np.empty([self.Inp.nDoF, 0])
47
+
48
+ # Initialise a container to hold the set of axial forces for each external load increment
49
+ self.EXTFORCES = np.empty([self.Inp.nDoF, 0])
50
+
51
+ # Initialise a container to hold the set of axial forces for each external load increment
52
+ self.MBRFORCES = np.empty([len(self.Inp.members), 0])
53
+
54
+ # Initialise global disp vector
55
+ self.UG = np.zeros(
56
+ [self.Inp.nDoF, 1]
57
+ ) # Initialise global displacement vector to zero (undeformed state)
58
+
59
+ # Calculate initial transformation matrices for all members based on undeformed position
60
+ self.TMs = self.calculateTransMatrices(self.UG)
61
+
62
+ # Init point loads to global force vector
63
+
64
+ self.AddPointLoadsGlobal()
65
+
66
+
67
+ # Calculate initial lengths
68
+
69
+ self.calculateInitialLengths()
70
+ self.SelfweigthLoadVector()
71
+
72
+ # Calculate internal force system based on any pre-tension in members
73
+ self.F_pre = self.initPretension()
74
+
75
+ # Initialise a container to store incremental displacements calculated for each iteration [Xa], [Xb] etc.
76
+ self.UG_inc = np.empty([self.Inp.nDoF, 0])
77
+ self.UG_inc = np.append(
78
+ self.UG_inc, self.UG, axis=1
79
+ ) # Add the initial (zero) displacement record
80
+
81
+ # Initialise a container to store incremental internal forces calculated for each iteration [Fa], [Fb] etc.
82
+ self.F_inc = np.empty([self.Inp.nDoF, 0])
83
+ print(self.F_inc)
84
+ print(self.F_pre)
85
+ self.F_inc = np.append(
86
+ self.F_inc, self.F_pre, axis=1
87
+ ) # Add the initial pre-tension force record
88
+
89
+ if use_iteration:
90
+ self.MainConvergenceLoop()
91
+ else:
92
+ self.SolveLinear_NoIteration(treat_cables_as_bars=True)
93
+
94
+ def ClassifyMemberType(self):
95
+ print("Classify Member type")
96
+ for n,m in enumerate(self.Inp.members):
97
+ #Initially assume all members are bars
98
+ self.memberType.append('b')
99
+
100
+
101
+ #Check if member is a cable
102
+ for c in self.Inp.cables:
103
+ if(m[0] in c and m[1] in c):
104
+ self.memberType[n] = 'c'
105
+
106
+ def AddPointLoadsGlobal(self):
107
+ self.forceVector = np.zeros((len(self.Inp.nodes) * 2, 1))
108
+
109
+ if len(self.Inp.forceLocationData) > 0:
110
+ # Split force location data
111
+ try:
112
+ forcedNodes = self.Inp.forceLocationData[:, 0].astype(
113
+ int
114
+ ) # Ensure these are integers)
115
+ xForceIndizes = 2 * forcedNodes - 2
116
+ yForceIndizes = 2 * forcedNodes - 1
117
+
118
+ ForceP = self.Inp.forceLocationData[:, 1].reshape(-1, 1)
119
+ print("FORCE P")
120
+ print(ForceP)
121
+
122
+ # Assign forces to degrees of freedom
123
+ for i in range(0,len(ForceP),1):
124
+
125
+ if self.Inp.forceDirections[i] == "x":
126
+ self.forceVector[xForceIndizes[i]] = ForceP[i]
127
+ elif self.Inp.forceDirections[i] == "y":
128
+ self.forceVector[yForceIndizes[i]] = ForceP[i]
129
+ except:
130
+ pass
131
+
132
+ def calculateInitialLengths(self):
133
+ self.lengths = np.zeros(len(self.Inp.members))
134
+ for n, mbr in enumerate(self.Inp.members):
135
+
136
+ # Calculate undeformed length of member
137
+ node_i = mbr[0] # Node number for node i of this member
138
+ node_j = mbr[1] # Node number for node j of this member
139
+ ix = self.Inp.nodes[node_i - 1][0] # x-coord for node i
140
+ iy = self.Inp.nodes[node_i - 1][1] # y-coord for node i
141
+ jx = self.Inp.nodes[node_j - 1][0] # x-coord for node j
142
+ jy = self.Inp.nodes[node_j - 1][1] # y-coord for node j
143
+
144
+ dx = jx - ix # x-component of vector along member
145
+ dy = jy - iy # y-component of vector along member
146
+ length = math.sqrt(dx**2 + dy**2) # Magnitude of vector (length of member)
147
+ if (length == 0):
148
+ print("Length = 0 at index ", n )
149
+ self.lengths[n] = length
150
+
151
+ def initPretension(self):
152
+ """
153
+ P = axial pre-tension specified for each bar
154
+ Calculate the force vector [F_pre] for each bar [F_pre] = [T'][AA'][P]
155
+ Combine into an overal vector representing the internal force system and return
156
+ """
157
+ self.F_pre = np.array(
158
+ [np.zeros(len(self.forceVector))]
159
+ ).T # Initialse internal force vector
160
+
161
+ for n, mbr in enumerate(self.Inp.members):
162
+ node_i = mbr[0] # Node number for node i of this member
163
+ node_j = mbr[1] # Node number for node j of this member
164
+
165
+ # Index of DoF for this member
166
+ ia = 2 * node_i - 2 # horizontal DoF at node i of this member
167
+ ib = 2 * node_i - 1 # vertical DoF at node i of this member
168
+ ja = 2 * node_j - 2 # horizontal DoF at node j of this member
169
+ jb = 2 * node_j - 1 # vertical DoF at node j of this member
170
+
171
+ # Determine internal pre-tension in global coords
172
+ TM = self.TMs[n, :, :]
173
+ AAp = np.array([[1], [0]])
174
+ P = self.Mat.P0[n]
175
+ F_pre_global = np.matmul(TM.T, AAp) * P
176
+
177
+ # Add member pre-tension to overall record
178
+ self.F_pre[ia, 0] = self.F_pre[ia, 0] + F_pre_global[0][0]
179
+ self.F_pre[ib, 0] = self.F_pre[ib, 0] + F_pre_global[1][0]
180
+ self.F_pre[ja, 0] = self.F_pre[ja, 0] + F_pre_global[2][0]
181
+ self.F_pre[jb, 0] = self.F_pre[jb, 0] + F_pre_global[3][0]
182
+
183
+ return self.F_pre
184
+
185
+ def calculateTransMatrices(self, UG):
186
+ """
187
+ Optimized:
188
+ - Bars ('b'): transformation is constant (undeformed geometry) -> keep initial TM
189
+ - Cables ('c'): update TM each iteration based on deformed geometry
190
+
191
+ Returns array shape (nMembers, 2, 4)
192
+ """
193
+
194
+ nM = len(self.Inp.members)
195
+
196
+ # -------------------------------------------------------
197
+ # Create constant (initial) TMs once (for bars AND cables)
198
+ # -------------------------------------------------------
199
+ if not hasattr(self, "TMs_const") or self.TMs_const is None:
200
+ self.TMs_const = np.zeros((nM, 2, 4), dtype=float)
201
+
202
+ for n, mbr in enumerate(self.Inp.members):
203
+ node_i = int(mbr[0])
204
+ node_j = int(mbr[1])
205
+
206
+ ix = float(self.Inp.nodes[node_i - 1, 0])
207
+ iy = float(self.Inp.nodes[node_i - 1, 1])
208
+ jx = float(self.Inp.nodes[node_j - 1, 0])
209
+ jy = float(self.Inp.nodes[node_j - 1, 1])
210
+
211
+ TM0 = self.CableNonlinear.calculateTransMatrix([ix, iy], [jx, jy])
212
+ self.TMs_const[n, :, :] = TM0
213
+
214
+ # start from constant TMs
215
+ TMs = self.TMs_const.copy()
216
+
217
+ # -------------------------------------------------------
218
+ # Update ONLY cables
219
+ # -------------------------------------------------------
220
+ for n, mbr in enumerate(self.Inp.members):
221
+ if self.memberType[n] != "c":
222
+ continue # bars: keep constant TM
223
+
224
+ node_i = int(mbr[0])
225
+ node_j = int(mbr[1])
226
+
227
+ ia = 2 * node_i - 2
228
+ ib = 2 * node_i - 1
229
+ ja = 2 * node_j - 2
230
+ jb = 2 * node_j - 1
231
+
232
+ # deformed positions = initial + cumulative displacements
233
+ ix = float(self.Inp.nodes[node_i - 1, 0]) + float(UG[ia, 0])
234
+ iy = float(self.Inp.nodes[node_i - 1, 1]) + float(UG[ib, 0])
235
+ jx = float(self.Inp.nodes[node_j - 1, 0]) + float(UG[ja, 0])
236
+ jy = float(self.Inp.nodes[node_j - 1, 1]) + float(UG[jb, 0])
237
+
238
+ TM = self.CableNonlinear.calculateTransMatrix([ix, iy], [jx, jy])
239
+ TMs[n, :, :] = TM
240
+
241
+ return TMs
242
+
243
+ def buildStructureStiffnessMatrix(self, UG,TMs):
244
+ """
245
+ Standard construction of Primary and Structure stiffness matrix
246
+ Construction of non-linear element stiffness matrix handled in a child function
247
+ """
248
+ Kp = np.zeros(
249
+ [self.Inp.nDoF, self.Inp.nDoF]
250
+ ) # Initialise the primary stiffness matrix
251
+
252
+ # store spring stiffness diagonal (for equilibrium check)
253
+ self.Kspring_diag = np.zeros(self.Inp.nDoF, dtype=float)
254
+
255
+
256
+ for n, mbr in enumerate(self.Inp.members):
257
+ node_i = mbr[0] # Node number for node i of this member
258
+ node_j = mbr[1] # Node number for node j of this member
259
+
260
+ # Construct (potentially) non-linear element stiffness matrix
261
+
262
+ # [K11, K12, K21, K22] = self.CableNonlinear.buildElementStiffnessMatrix(
263
+ # n, UG, TMs, self.lengths, self.Mat.P0, self.Mat.E, self.Mat.A
264
+ # )
265
+
266
+ if self.memberType[n] == 'c':
267
+ # cable / nonlinear
268
+ [K11, K12, K21, K22] = self.CableNonlinear.buildElementStiffnessMatrix(
269
+ n, UG, TMs, self.lengths, self.Mat.P0, self.Mat.E, self.Mat.A
270
+ )
271
+ else:
272
+ # bar / linear (Theorie I. Ordnung)
273
+ [K11, K12, K21, K22] = self.BarLinear.buildElementStiffnessMatrix(
274
+ n, UG, None, None, self.Mat.P0, self.Mat.E, self.Mat.A
275
+ )
276
+
277
+
278
+ # Primary stiffness matrix indices associated with each node
279
+ # i.e. node 1 occupies indices 0 and 1 (accessed in Python with [0:2])
280
+ ia = 2 * node_i - 2 # index 0
281
+ ib = 2 * node_i - 1 # index 1
282
+ ja = 2 * node_j - 2 # index 2
283
+ jb = 2 * node_j - 1 # index 3
284
+
285
+ Kp[ia : ib + 1, ia : ib + 1] = Kp[ia : ib + 1, ia : ib + 1] + K11
286
+ Kp[ia : ib + 1, ja : jb + 1] = Kp[ia : ib + 1, ja : jb + 1] + K12
287
+ Kp[ja : jb + 1, ia : ib + 1] = Kp[ja : jb + 1, ia : ib + 1] + K21
288
+ Kp[ja : jb + 1, ja : jb + 1] = Kp[ja : jb + 1, ja : jb + 1] + K22
289
+
290
+ # Add springs
291
+
292
+ if len(self.Inp.springLocationData) > 0:
293
+ # Split force location data
294
+ try:
295
+ forcedNodes = self.Inp.springLocationData[:, 1].astype(
296
+ int
297
+ ) # Ensure these are integers)
298
+ xSpringIndizes = 2 * forcedNodes - 2
299
+ ySpringIndizes = 2 * forcedNodes -1
300
+
301
+ # print("Indizes")
302
+ # print(xSpringIndizes)
303
+ # print(ySpringIndizes)
304
+
305
+ SpringC = self.Inp.springLocationData[:, 2].reshape(-1, 1)
306
+
307
+
308
+ # Assign forces to degrees of freedom
309
+ for i in range(0,len(SpringC),1):
310
+
311
+ if self.Inp.SpringDirections[i] == "x":
312
+ Kp[xSpringIndizes[i]][xSpringIndizes[i]] += SpringC[i]
313
+ self.Kspring_diag[xSpringIndizes[i]] += SpringC[i]
314
+ elif self.Inp.SpringDirections[i] == "y":
315
+ Kp[ySpringIndizes[i]][ySpringIndizes[i]] += SpringC[i]
316
+ self.Kspring_diag[ySpringIndizes[i]] += SpringC[i]
317
+ except:
318
+ pass
319
+
320
+ # Reduce to structure stiffness matrix by deleting rows and columns for restrained DoF
321
+ if (len(self.Inp.restrainedIndex)>0):
322
+ # print("RESTRAINED INDEX")
323
+ # print(self.Inp.restrainedIndex)
324
+ Ks = np.delete(Kp, self.Inp.restrainedIndex, 0) # Delete rows
325
+ Ks = np.delete(Ks, self.Inp.restrainedIndex, 1) # Delete columns
326
+ else:
327
+ Ks = Kp
328
+
329
+
330
+ Ks = np.matrix(
331
+ Ks
332
+ ) # Convert Ks from numpy.ndarray to numpy.matrix to use build in inverter function
333
+
334
+
335
+
336
+
337
+ return Ks
338
+
339
+ def solveDisplacements(self, Ks, F_inequilibrium):
340
+ """
341
+ Standard solving for structural displacements
342
+ """
343
+
344
+ forceVectorRed = copy.copy(
345
+ F_inequilibrium
346
+ ) # Make a copy of forceVector so the copy can be edited, leaving the original unchanged
347
+ if (len(self.Inp.restrainedIndex)>0):
348
+ forceVectorRed = np.delete(
349
+ forceVectorRed, self.Inp.restrainedIndex, 0
350
+ ) # Delete rows corresponding to restrained DoF
351
+ else:
352
+ forceVectorRed = forceVectorRed
353
+
354
+ #U = Ks.I * forceVectorRed
355
+ U = np.linalg.solve(Ks, forceVectorRed)
356
+
357
+
358
+ # Build the global displacement vector inclusing zeros as restrained degrees of freedom
359
+ UG = np.zeros(
360
+ self.Inp.nDoF
361
+ ) # Initialise an array to hold the global displacement vector
362
+ c = 0 # Initialise a counter to track how many restraints have been imposed
363
+ for i in np.arange(self.Inp.nDoF):
364
+ if i in self.Inp.restrainedIndex:
365
+ # Impose zero displacement
366
+ UG[i] = 0
367
+ else:
368
+ # Assign actual displacement
369
+ UG[i] = U[c]
370
+ c = c + 1
371
+
372
+ UG = np.array([UG]).T
373
+
374
+ return UG
375
+
376
+ def SelfweigthLoadVector(self):
377
+
378
+ if(self.swt):
379
+ self.SW_at_supports = np.empty((0,2))
380
+ for n, mbr in enumerate(self.Inp.members):
381
+ node_i = mbr[0] #Node number for node i of this member
382
+ node_j = mbr[1] #Node number for node j of this member
383
+ length = self.lengths[n]
384
+ sw = length*self.Mat.gamma[n] #(N) Self-weight of the member
385
+ F_node = sw/2 #(N) Self-weight distributed into each node
386
+ # print("FNODE")
387
+ # print(F_node)
388
+ iy = 2*node_i-1 #index of y-DoF for node i
389
+ jy = 2*node_j-1 #index of y-DoF for node j
390
+ self.forceVector[iy] = self.forceVector[iy] -F_node
391
+ self.forceVector[jy] = self.forceVector[jy] -F_node
392
+
393
+ #Check if SW needs to be directly added to supports (if elements connect to supports)
394
+ if(iy+1 in self.Inp.restrainedDoF):
395
+ supportSW = np.array([iy, F_node])
396
+ self.SW_at_supports = np.append(self.SW_at_supports, [supportSW], axis=0) #Store y-DoF at support and force to be added
397
+ if(jy+1 in self.Inp.restrainedDoF):
398
+ supportSW = np.array([jy, F_node])
399
+ self.SW_at_supports = np.append(self.SW_at_supports, [supportSW], axis=0) #Store y-DoF at support and force to be added
400
+ print(self.forceVector)
401
+ print(len(self.forceVector))
402
+ else:
403
+ pass
404
+
405
+ def updateInternalForceSystem(self, UG):
406
+ """
407
+ Build internal force vector F_int (global DoF order) for the CURRENT increment UG.
408
+
409
+ - Cable elements ('c'): nonlinear (deformed geometry, AA-matrix, etc.) -> uses self.TMs[n]
410
+ - Bar elements ('b'): linear Theorie I. Ordnung -> uses constant direction cosines from BarLinear
411
+
412
+ IMPORTANT:
413
+ - Pretension P0 is already handled separately via self.F_pre / self.F_inc initial column.
414
+ Therefore for BAR internal force increment we DO NOT add P0 again here (avoid double count).
415
+ """
416
+
417
+ F_int = np.zeros((self.Inp.nDoF, 1), dtype=float)
418
+
419
+ for n, mbr in enumerate(self.Inp.members):
420
+ node_i, node_j = int(mbr[0]), int(mbr[1])
421
+
422
+ # global DoF indices for this member
423
+ ia = 2 * node_i - 2
424
+ ib = 2 * node_i - 1
425
+ ja = 2 * node_j - 2
426
+ jb = 2 * node_j - 1
427
+
428
+ # -------------------------
429
+ # CABLE (nonlinear)
430
+ # -------------------------
431
+ if self.memberType[n] == "c":
432
+ # incremental displacements (global)
433
+ d_ix = float(UG[ia, 0])
434
+ d_iy = float(UG[ib, 0])
435
+ d_jx = float(UG[ja, 0])
436
+ d_jy = float(UG[jb, 0])
437
+
438
+ # current transformation (computed for cumulative shape, stored in self.TMs)
439
+ TM = self.TMs[n, :, :] # shape (2,4)
440
+
441
+ # local incremental displacements
442
+ localDisp = TM @ np.array([[d_ix], [d_iy], [d_jx], [d_jy]], dtype=float)
443
+ u = float(localDisp[0, 0])
444
+ v = float(localDisp[1, 0])
445
+
446
+ # extension from nonlinear geometry
447
+ Lo = float(self.lengths[n])
448
+ e = math.sqrt((Lo + u) ** 2 + v**2) - Lo
449
+
450
+ # AA matrix
451
+ denom = (Lo + e)
452
+ if abs(denom) < 1e-14:
453
+ # numerical guard (should not really happen)
454
+ continue
455
+
456
+ a1 = (Lo + u) / denom
457
+ a2 = v / denom
458
+ AA = np.array([[a1, a2]], dtype=float) # (1,2)
459
+
460
+ # axial load increment (no P0 here; P0 was handled via initPretension)
461
+ P = (float(self.Mat.E[n]) * float(self.Mat.A[n]) / Lo) * e
462
+
463
+ # back to global nodal forces (4x1)
464
+ F_global = (TM.T @ AA.T) * P # (4,1)
465
+
466
+ F_int[ia, 0] += float(F_global[0, 0])
467
+ F_int[ib, 0] += float(F_global[1, 0])
468
+ F_int[ja, 0] += float(F_global[2, 0])
469
+ F_int[jb, 0] += float(F_global[3, 0])
470
+
471
+ # -------------------------
472
+ # BAR (linear, Theorie I. Ordnung)
473
+ # -------------------------
474
+ else:
475
+ # element axial force increment from linear truss theory:
476
+ # N = (EA/L) * [-c -s c s] * u_e
477
+ f_e = self.BarLinear.internal_nodal_forces_global(
478
+ n,
479
+ UG,
480
+ self.Mat.E,
481
+ self.Mat.A,
482
+ P0=None, # do NOT add P0 here (already in self.F_pre)
483
+ ) # shape (4,1)
484
+
485
+ F_int[ia, 0] += float(f_e[0, 0])
486
+ F_int[ib, 0] += float(f_e[1, 0])
487
+ F_int[ja, 0] += float(f_e[2, 0])
488
+ F_int[jb, 0] += float(f_e[3, 0])
489
+
490
+ return F_int
491
+
492
+ def testForConvergence(self, it, threshold, F_ineq):
493
+ """
494
+ Test if structure has converged by comparing the maximum force in the equilibrium
495
+ force vector against a threshold for the simulation.
496
+ """
497
+ notConverged = True # Initialise the convergence flag
498
+ maxIneq = 0
499
+ if it > 0:
500
+ maxIneq = np.max(abs(F_ineq[self.Inp.freeDoF]))
501
+
502
+ if maxIneq < threshold:
503
+ notConverged = False
504
+
505
+ return notConverged, maxIneq
506
+
507
+ def calculateMbrForces(self, UG):
508
+ """
509
+ Calculates the member forces based on change in length of each member
510
+ Takes in the cumulative global displacement vector as UG
511
+ """
512
+
513
+ mbrForces = np.zeros(
514
+ len(self.Inp.members)
515
+ ) # Initialise a container to hold axial forces
516
+
517
+ for n, mbr in enumerate(self.Inp.members):
518
+ node_i = mbr[0] # Node number for node i of this member
519
+ node_j = mbr[1] # Node number for node j of this member
520
+
521
+ # Index of DoF for this member
522
+ ia = 2 * node_i - 2 # horizontal DoF at node i of this member
523
+ ib = 2 * node_i - 1 # vertical DoF at node i of this member
524
+ ja = 2 * node_j - 2 # horizontal DoF at node j of this member
525
+ jb = 2 * node_j - 1 # vertical DoF at node j of this member
526
+
527
+ # New positions = initial pos + cum deflection
528
+ ix = self.Inp.nodes[node_i - 1, 0] + UG[ia, 0]
529
+ iy = self.Inp.nodes[node_i - 1, 1] + UG[ib, 0]
530
+ jx = self.Inp.nodes[node_j - 1, 0] + UG[ja, 0]
531
+ jy = self.Inp.nodes[node_j - 1, 1] + UG[jb, 0]
532
+
533
+ dx = jx - ix # x-component of vector along member
534
+ dy = jy - iy # y-component of vector along member
535
+ newLength = math.sqrt(
536
+ dx**2 + dy**2
537
+ ) # Magnitude of vector (length of member)
538
+
539
+ deltaL = newLength - self.lengths[n] # Change in length
540
+ force = (
541
+ self.Mat.P0[n]
542
+ + deltaL * self.Mat.E[n] * self.Mat.A[n] / self.lengths[n]
543
+ ) # Axial force due to change in length and any pre-tension
544
+ mbrForces[n] = force # Store member force
545
+
546
+ return mbrForces
547
+
548
+ def AdditionalSupportForce(self):
549
+ self.reactionsFlag = False #Initialise a flag so we can plot a message re. reactions when necessary
550
+ if(self.swt):
551
+ if self.SW_at_supports.size>0:
552
+ self.reactionsFlag = True
553
+ for SW in self.SW_at_supports:
554
+ index = int(SW[0]) #Index of the global force vector 'FG' to update
555
+ self.FI_FINAL[index,:] = self.FI_FINAL[index,:] + SW[1] #Add nodal SW force directly to FG
556
+
557
+ def MainConvergenceLoop(self):
558
+ i = 0 # Initialise an iteration counter (zeros out for each load increment)
559
+ inc = 0 # Initialise load increment counter
560
+ notConverged = True # Initialise convergence flag
561
+
562
+ # Init kspring-diagonal for the first iteration
563
+ # It's overwriten in the generation of the stiffness matrix
564
+ # in each loop. Here just for the first run, where the stiffness matrix isn't initialized
565
+ self.Kspring_diag = np.zeros(self.Inp.nDoF, dtype=float)
566
+
567
+
568
+ self.forceIncrement = (
569
+ self.forceVector / self.nForceIncrements
570
+ ) # Determine the force increment for each convergence test
571
+ self.maxForce = (
572
+ self.forceVector
573
+ ) # Define a vector to store the total external force applied
574
+ self.forceVector = (
575
+ self.forceIncrement
576
+ ) # Initialise the forceVector to the first increment of load
577
+
578
+ # print("Force vector")
579
+ # print(self.forceVector)
580
+
581
+
582
+ while notConverged and i < 10000:
583
+
584
+ # Calculate the cumulative internal forces Fi_total = Fa + Fb + Fc + ...
585
+ Fi_total = np.matrix(
586
+ np.sum(self.F_inc, axis=1)
587
+ ).T # Sum across columns of F_inc
588
+
589
+ # Calculate the cumulative incremental displacements UG_total = Xa + Xb + Xc + ...
590
+ UG_total = np.matrix(
591
+ np.sum(self.UG_inc, axis=1)
592
+ ).T # Sum across columns of UG_inc
593
+
594
+ # Inequilibrium force vector used in this iteration F_EXT - Fi_total or externalForces - (cumulative) InternalForceSystem
595
+
596
+ # add spring forces to internal force balance
597
+ # (springs are in K, so their resisting forces must appear in equilibrium)
598
+ F_spring = self.Kspring_diag.reshape(-1, 1) * np.asarray(UG_total, dtype=float)
599
+
600
+
601
+ self.F_inequilibrium = self.forceVector - Fi_total - F_spring
602
+
603
+ # Build the structure stiffness matrix based on current position (using cumulative displacements)
604
+ Ks = self.buildStructureStiffnessMatrix(UG_total,self.TMs)
605
+
606
+ # Solve for global (incremental) displacement vector [Xn] for this iteration
607
+ self.UG = self.solveDisplacements(Ks, self.F_inequilibrium)
608
+
609
+ # Calculate a new transformation matrix for each member based on cum disp up to previous iteration
610
+ self.TMs = self.calculateTransMatrices(UG_total)
611
+
612
+ # if i == 0:
613
+ # print(self.TMs)
614
+
615
+ # Calculate the internal force system based on new incremental displacements, [Fn]
616
+ F_int = self.updateInternalForceSystem(self.UG)
617
+
618
+ # Save incremental displacements and internal forces for this iteration
619
+ self.UG_inc = np.append(self.UG_inc, self.UG, axis=1)
620
+ self.F_inc = np.append(self.F_inc, F_int, axis=1)
621
+
622
+ # Test for convergence
623
+ notConverged, maxIneq = self.testForConvergence(
624
+ i, self.convThreshold, self.F_inequilibrium
625
+ )
626
+
627
+ i += 1
628
+
629
+ # If system has converged, save converged displacements, forces and increment external loading
630
+ if not notConverged:
631
+ self.hasSlackElements = False #Initialise a flag to indicate presence of new slack elements
632
+ mbrForces = self.calculateMbrForces(UG_total) #Calculate member forces based on current set of displacements
633
+
634
+ #Test for compression in cable elements if designated number of converged increments reached
635
+
636
+ if inc > self.checkSlackAfter:
637
+ for m, mbr in enumerate(self.Inp.members):
638
+ if self.memberType[m] == 'c' and mbrForces[m]<0:
639
+ print(f'Compression in cable element from from nodes {mbr[0]} to {mbr[1]}')
640
+ self.hasSlackElements = True #Switch slack elements flag
641
+ self.Mat.A[m] = 0 #Eliminate member stiffness by seting its cross-sectional area to zero
642
+
643
+
644
+
645
+ print(
646
+ f"System has converged for load increment {inc} after {i-1} iterations"
647
+ )
648
+
649
+
650
+ self.UG_FINAL = np.append(
651
+ self.UG_FINAL, UG_total, axis=1
652
+ ) # Add the converged displacement record
653
+ self.UG_inc = np.empty(
654
+ [self.Inp.nDoF, 0]
655
+ ) # Zero out the record of incremental displacements for the next load increment
656
+ self.UG_inc = np.array(
657
+ np.append(self.UG_inc, UG_total, axis=1)
658
+ ) # Add the initial displacement record for next load increment (manually cast as ndarray instead of matrix)
659
+
660
+ self.FI_FINAL = np.append(
661
+ self.FI_FINAL, Fi_total, axis=1
662
+ ) # Add the converged force record
663
+ self.F_inc = np.empty(
664
+ [self.Inp.nDoF, 0]
665
+ ) # Zero out the record of incremental forces for the next load increment
666
+ self.F_inc = np.array(
667
+ np.append(self.F_inc, Fi_total, axis=1)
668
+ ) # Add the initial force record for next load increment (manually cast as ndarray instead of matrix)
669
+
670
+
671
+ self.mbrForces = self.calculateMbrForces(
672
+ self.UG_FINAL[:, -1]
673
+ ) # Calculate the member forces based on change in mbr length
674
+ self.MBRFORCES = np.append(
675
+ self.MBRFORCES, np.matrix(self.mbrForces).T, axis=1
676
+ ) # Add the converged axial forces record
677
+
678
+ self.EXTFORCES = np.append(
679
+ self.EXTFORCES, self.forceVector, axis=1
680
+ ) # Add the external force vector for this load increment
681
+
682
+ # Test if all external loading has been applied
683
+ if abs(sum(self.forceVector).item()) < abs(sum(self.maxForce).item()):
684
+ i = 0 # Reset counter for next load increment
685
+ inc += 1
686
+ self.forceVector = (
687
+ self.forceVector + self.forceIncrement
688
+ ) # Increment the applied load
689
+ notConverged = (
690
+ True # Reset notConverged flag for next load increment
691
+ )
692
+
693
+ self.AdditionalSupportForce()
694
+
695
+ def SolveLinear_NoIteration(self, treat_cables_as_bars=True):
696
+ """
697
+ Linear solve (no iteration, no deformed geometry):
698
+ K * u = F
699
+
700
+ - Builds global stiffness matrix once from undeformed geometry.
701
+ - Solves once for displacements.
702
+ - Computes reactions and member forces.
703
+
704
+ treat_cables_as_bars:
705
+ True -> use linear bar stiffness also for members typed 'c'
706
+ False -> raise error if cables exist (strict linear truss only)
707
+ """
708
+
709
+ # ---------------------------------------------------------
710
+ # 1) Build external load vector (global) once
711
+ # ---------------------------------------------------------
712
+ self.forceVector = np.zeros((len(self.Inp.nodes) * 2, 1), dtype=float)
713
+ self.AddPointLoadsGlobal()
714
+ self.calculateInitialLengths()
715
+ self.SelfweigthLoadVector() # only acts if self.swt=True
716
+
717
+ F = self.forceVector.copy() # global full vector (nDoF,1)
718
+
719
+ # ---------------------------------------------------------
720
+ # 2) Build global stiffness Kp (full) from undeformed geometry
721
+ # ---------------------------------------------------------
722
+ nDoF = self.Inp.nDoF
723
+ Kp = np.zeros((nDoF, nDoF), dtype=float)
724
+
725
+ # Springs: store diagonal like before
726
+ self.Kspring_diag = np.zeros(nDoF, dtype=float)
727
+
728
+ for n, mbr in enumerate(self.Inp.members):
729
+ node_i, node_j = int(mbr[0]), int(mbr[1])
730
+
731
+ if (self.memberType[n] == "c") and (not treat_cables_as_bars):
732
+ raise ValueError(
733
+ f"Member {n+1} (nodes {node_i}-{node_j}) is a cable. "
734
+ "Set treat_cables_as_bars=True or remove cable members for strict linear solve."
735
+ )
736
+
737
+ # Use linear bar element stiffness (undeformed)
738
+ K11, K12, K21, K22 = self.BarLinear.buildElementStiffnessMatrix(
739
+ n, None, None, None, self.Mat.P0, self.Mat.E, self.Mat.A
740
+ )
741
+
742
+ ia = 2 * node_i - 2
743
+ ib = 2 * node_i - 1
744
+ ja = 2 * node_j - 2
745
+ jb = 2 * node_j - 1
746
+
747
+ Kp[ia:ib+1, ia:ib+1] += K11
748
+ Kp[ia:ib+1, ja:jb+1] += K12
749
+ Kp[ja:jb+1, ia:ib+1] += K21
750
+ Kp[ja:jb+1, ja:jb+1] += K22
751
+
752
+ # ---------------------------------------------------------
753
+ # 3) Add springs (same logic as your iterative build)
754
+ # ---------------------------------------------------------
755
+ if len(self.Inp.springLocationData) > 0:
756
+ try:
757
+ forcedNodes = self.Inp.springLocationData[:, 1].astype(int)
758
+ xIdx = 2 * forcedNodes - 2
759
+ yIdx = 2 * forcedNodes - 1
760
+ SpringC = self.Inp.springLocationData[:, 2].reshape(-1)
761
+
762
+ for i in range(len(SpringC)):
763
+ c = float(SpringC[i])
764
+ if self.Inp.SpringDirections[i] == "x":
765
+ Kp[xIdx[i], xIdx[i]] += c
766
+ self.Kspring_diag[xIdx[i]] += c
767
+ elif self.Inp.SpringDirections[i] == "y":
768
+ Kp[yIdx[i], yIdx[i]] += c
769
+ self.Kspring_diag[yIdx[i]] += c
770
+ except:
771
+ pass
772
+
773
+ # ---------------------------------------------------------
774
+ # 4) Reduce and solve
775
+ # ---------------------------------------------------------
776
+ if len(self.Inp.restrainedIndex) > 0:
777
+ free = np.array([i for i in range(nDoF) if i not in self.Inp.restrainedIndex], dtype=int)
778
+ else:
779
+ free = np.arange(nDoF, dtype=int)
780
+
781
+ Kff = Kp[np.ix_(free, free)]
782
+ Ff = F[free, :]
783
+
784
+ uf = np.linalg.solve(Kff, Ff)
785
+
786
+ # build full displacement vector u (restrained are 0)
787
+ u = np.zeros((nDoF, 1), dtype=float)
788
+ u[free, 0] = uf[:, 0]
789
+
790
+ # Save like your usual output containers (single step)
791
+ self.UG_FINAL = u.copy()
792
+ self.EXTFORCES = F.copy()
793
+
794
+ # ---------------------------------------------------------
795
+ # 5) Reactions (full)
796
+ # ---------------------------------------------------------
797
+ R = (Kp @ u) - F # includes spring reactions etc.
798
+ self.FI_FINAL = (Kp @ u) # "internal nodal forces" equivalent
799
+ self.Reactions = R
800
+
801
+ # ---------------------------------------------------------
802
+ # 6) Member forces (linear)
803
+ # include pretension if you want: P0 added in axial_force(..., P0=self.Mat.P0)
804
+ # ---------------------------------------------------------
805
+ mbrN = np.zeros((len(self.Inp.members), 1), dtype=float)
806
+ for n in range(len(self.Inp.members)):
807
+ # linear axial force (tension +)
808
+ N = self.BarLinear.axial_force(n, u, self.Mat.E, self.Mat.A, P0=self.Mat.P0)
809
+ mbrN[n, 0] = N
810
+
811
+ self.MBRFORCES = mbrN.copy()
812
+
813
+ return u, R, mbrN
814
+
815
+
816
+
817
+ def Summarize(self):
818
+ #Generate output statements
819
+ print(f"OUTSTANDING FORCE IMBALANCE")
820
+ for i in np.arange(0,self.Inp.nDoF):
821
+ if i not in self.Inp.restrainedIndex:
822
+ print(f"Remaining force imbalance at DoF {i} is {round(self.F_inequilibrium[i,0]/1000,3)} kN")
823
+
824
+ maxInequality = round(max(abs(self.F_inequilibrium[self.Inp.freeDoF,0])).item()/1000,3)
825
+ print(f"(max = {maxInequality} kN)")
826
+
827
+ print("")
828
+ print("REACTIONS")
829
+
830
+ f_int = self.FI_FINAL[:,-1]
831
+ for i in np.arange(0,len(self.Inp.restrainedIndex)):
832
+ index = self.Inp.restrainedIndex[i]
833
+ print(f"Reaction at DoF {index+1}: {round(f_int[index].item()/1000,2)} kN")
834
+
835
+ # last converged displacement vector
836
+ u_last = np.asarray(self.UG_FINAL[:, -1], dtype=float).reshape(-1, 1) # (nDoF,1)
837
+
838
+ # spring stiffness (diagonal) as vector
839
+ k = np.asarray(self.Kspring_diag, dtype=float).reshape(-1, 1) # (nDoF,1)
840
+
841
+ # elementwise spring forces
842
+ f_springs = k * u_last # (nDoF,1)
843
+
844
+
845
+ try:
846
+ # Federkräfte (letzter Lastschritt)
847
+ f = np.asarray(f_springs, dtype=float).flatten()
848
+
849
+ springno = self.Inp.springLocationData[:, 0].astype(int)
850
+ forcedNodes = self.Inp.springLocationData[:, 1].astype(int)
851
+ dirs = np.asarray(self.Inp.SpringDirections)
852
+
853
+ print("\nSPRING FORCES (per spring):")
854
+ for no, node, d in zip(springno, forcedNodes, dirs):
855
+ d = str(d).strip().lower().replace('"', '')
856
+
857
+ if d == "x":
858
+ dof = 2 * node - 2
859
+ elif d in ("y", "z"):
860
+ dof = 2 * node - 1
861
+ else:
862
+ raise ValueError(f"Unknown spring direction: {d}")
863
+
864
+ print(
865
+ f"Spring {no} | Node {node} | Dir {d} | "
866
+ f"DoF {dof} | Force = {f[dof]/1000:.2f} kN"
867
+ )
868
+
869
+ except Exception as e:
870
+ print("No springs in the system")
871
+ # optional:
872
+ # print(e)
873
+
874
+ print("")
875
+ print("MEMBER FORCES (incl. any pre-tension)")
876
+ for n, mbr in enumerate(self.Inp.members):
877
+ print(f"Force in member {n+1} (nodes {mbr[0]} to {mbr[1]}) is {round(self.mbrForces[n]/1000,2)} kN")
878
+
879
+ print("")
880
+ print("NODAL DISPLACEMENTS")
881
+ ug = self.UG_FINAL[:,-1]
882
+ for n, node in enumerate(self.Inp.nodes):
883
+ ix = 2*(n+1)-2 #horizontal DoF for this node
884
+ iy = 2*(n+1)-1 #vertical DoF for this node
885
+
886
+ ux = round(ug[ix,0],5) #Horizontal nodal displacement
887
+ uy = round(ug[iy,0],5) #Vertical nodal displacement
888
+ print(f"Node {n+1}: Ux = {ux} m, Uy = {uy} m")
889
+
890
+ def SummarizeLinear(self):
891
+ print("LINEAR SOLVE (NO ITERATION)")
892
+
893
+ # -------------------------------------------------
894
+ # REACTIONS
895
+ # -------------------------------------------------
896
+ print("\nREACTIONS (at restrained DoF):")
897
+ for idx in self.Inp.restrainedIndex:
898
+ print(f"DoF {idx+1}: R = {self.Reactions[idx,0]/1000:.2f} kN")
899
+
900
+ # -------------------------------------------------
901
+ # MEMBER FORCES
902
+ # -------------------------------------------------
903
+ print("\nMEMBER FORCES (incl. P0):")
904
+ for n, mbr in enumerate(self.Inp.members):
905
+ print(
906
+ f"Member {n+1} (nodes {mbr[0]}-{mbr[1]}): "
907
+ f"N = {self.MBRFORCES[n,0]/1000:.2f} kN"
908
+ )
909
+
910
+ # -------------------------------------------------
911
+ # NODAL DISPLACEMENTS
912
+ # -------------------------------------------------
913
+ print("\nNODAL DISPLACEMENTS:")
914
+ for n in range(len(self.Inp.nodes)):
915
+ ix = 2 * (n + 1) - 2
916
+ iy = 2 * (n + 1) - 1
917
+ print(
918
+ f"Node {n+1}: "
919
+ f"Ux = {self.UG_FINAL[ix,0]:.6e} m, "
920
+ f"Uy = {self.UG_FINAL[iy,0]:.6e} m"
921
+ )
922
+
923
+ # -------------------------------------------------
924
+ # SPRING FORCES
925
+ # -------------------------------------------------
926
+ if len(self.Inp.springLocationData) == 0:
927
+ print("\nNO SPRINGS IN SYSTEM")
928
+ return
929
+
930
+ print("\nSPRING FORCES:")
931
+
932
+ # displacement vector
933
+ u = self.UG_FINAL.reshape(-1, 1)
934
+
935
+ # diagonal spring stiffness vector
936
+ kdiag = self.Kspring_diag.reshape(-1, 1)
937
+
938
+ # spring forces per DoF
939
+ f_spring = kdiag * u
940
+
941
+ try:
942
+ spring_no = self.Inp.springLocationData[:, 0].astype(int)
943
+ nodes = self.Inp.springLocationData[:, 1].astype(int)
944
+ k_values = self.Inp.springLocationData[:, 2].astype(float)
945
+ directions = np.asarray(self.Inp.SpringDirections)
946
+
947
+ for no, node, k_i, d in zip(spring_no, nodes, k_values, directions):
948
+ d = str(d).strip().lower().replace('"', '')
949
+
950
+ if d == "x":
951
+ dof = 2 * node - 2
952
+ elif d == "y":
953
+ dof = 2 * node - 1
954
+ else:
955
+ raise ValueError(f"Unknown spring direction: {d}")
956
+
957
+ print(
958
+ f"Spring {no} | Node {node} | Dir {d} | "
959
+ f"k = {k_i:.3e} N/m | "
960
+ f"u = {u[dof,0]:.6e} m | "
961
+ f"F = {f_spring[dof,0]/1000:.2f} kN"
962
+ )
963
+
964
+ except Exception as e:
965
+ print("Error while printing spring forces")
966
+ # print(e)
967
+