kib-lap 0.5__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 (64) hide show
  1. Examples/Cross_Section_Thin.py +61 -0
  2. Examples/__init__.py +0 -0
  3. KIB_LAP/Betonbau/Bemessung_Polygon.py +667 -0
  4. KIB_LAP/Betonbau/Bemessung_Zust_II.py +648 -0
  5. KIB_LAP/Betonbau/Cross_Section_Kappa.py +925 -0
  6. KIB_LAP/Betonbau/Druckglied_KGV.py +179 -0
  7. KIB_LAP/Betonbau/Iterative_Design.py +723 -0
  8. KIB_LAP/Betonbau/Materialkennwerte_Beton.py +196 -0
  9. KIB_LAP/Betonbau/Querschnittsbreite.py +194 -0
  10. KIB_LAP/Betonbau/Querschnittsbreite_Kreis.py +63 -0
  11. KIB_LAP/Betonbau/__init__.py +2 -0
  12. KIB_LAP/Betonbau/beam_plate_T.py +921 -0
  13. KIB_LAP/Betonbau/beam_plate_T_reverse.py +915 -0
  14. KIB_LAP/Betonbau/beam_rectangular.py +635 -0
  15. KIB_LAP/Betonbau/beam_sub_section.py +9 -0
  16. KIB_LAP/Dynamik/Cross_Section_Properties.py +155 -0
  17. KIB_LAP/Dynamik/Deformation_Method.py +587 -0
  18. KIB_LAP/Dynamik/Duhamel_SDOF.py +221 -0
  19. KIB_LAP/Dynamik/FFT.py +87 -0
  20. KIB_LAP/Dynamik/Kontinuum_Eigenmodes.py +418 -0
  21. KIB_LAP/Dynamik/Kontinuum_Schwingung.py +757 -0
  22. KIB_LAP/Dynamik/Pendulum_Spring_Linearized.py +91 -0
  23. KIB_LAP/Dynamik/Pendulum_Spring_Problem.py +94 -0
  24. KIB_LAP/Dynamik/__init__.py +0 -0
  25. KIB_LAP/Examples/Cross_Section_Thin.py +61 -0
  26. KIB_LAP/Examples/Cross_Section_Thin_2.py +14 -0
  27. KIB_LAP/Examples/Plattentragwerke.py +39 -0
  28. KIB_LAP/Examples/Plattentragwerke_2.py +60 -0
  29. KIB_LAP/Examples/ShearDesign.py +28 -0
  30. KIB_LAP/Examples/__init__.py +0 -0
  31. KIB_LAP/Plattenbeulen/Plate_Design.py +276 -0
  32. KIB_LAP/Plattenbeulen/Ritz_Optimiert.py +658 -0
  33. KIB_LAP/Plattenbeulen/__init__.py +2 -0
  34. KIB_LAP/Plattenbeulen/dist/__init__.py +0 -0
  35. KIB_LAP/Plattenbeulen/plate_buckling.cpp +561 -0
  36. KIB_LAP/Plattenbeulen/plate_buckling_cpp.cp313-win_amd64.pyd +0 -0
  37. KIB_LAP/Plattenbeulen/plate_buckling_cpp.cpp +561 -0
  38. KIB_LAP/Plattenbeulen/setup.py +35 -0
  39. KIB_LAP/Plattentragwerke/Functions.cpp +326 -0
  40. KIB_LAP/Plattentragwerke/Functions.h +41 -0
  41. KIB_LAP/Plattentragwerke/NumInte.cpp +23 -0
  42. KIB_LAP/Plattentragwerke/NumericalIntegration.cpp +23 -0
  43. KIB_LAP/Plattentragwerke/PlateBendingKirchhoff.py +843 -0
  44. KIB_LAP/Plattentragwerke/__init__.py +1 -0
  45. KIB_LAP/Plattentragwerke/plate_bending.cpp +341 -0
  46. KIB_LAP/Plattentragwerke/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  47. KIB_LAP/Plattentragwerke/setup.py +39 -0
  48. KIB_LAP/Querschnittswerte/Querschnitt_Duenn.py +526 -0
  49. KIB_LAP/Querschnittswerte/__init__.py +1 -0
  50. KIB_LAP/STABRAUM/InputData.py +92 -0
  51. KIB_LAP/STABRAUM/Programm.py +1403 -0
  52. KIB_LAP/STABRAUM/Steifigkeitsmatrix.py +275 -0
  53. KIB_LAP/STABRAUM/__init__.py +3 -0
  54. KIB_LAP/Stahlbau/__init__.py +0 -0
  55. KIB_LAP/Verbundbau/Verbundtraeger_Bemessung.py +766 -0
  56. KIB_LAP/Verbundbau/__init__.py +0 -0
  57. KIB_LAP/__init__.py +4 -0
  58. KIB_LAP/main.py +2 -0
  59. KIB_LAP/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  60. KIB_LAP/plate_buckling_cpp.cp313-win_amd64.pyd +0 -0
  61. kib_lap-0.5.dist-info/METADATA +25 -0
  62. kib_lap-0.5.dist-info/RECORD +64 -0
  63. kib_lap-0.5.dist-info/WHEEL +5 -0
  64. kib_lap-0.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1403 @@
1
+ from Steifigkeitsmatrix import *
2
+ from InputData import Input
3
+ import sympy as sp
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+
8
+ from mpl_toolkits.mplot3d import Axes3D # nötig für 3D-Plots
9
+ import matplotlib.patches as mpatches
10
+ from matplotlib.widgets import Slider
11
+ from mpl_toolkits.mplot3d.art3d import Line3DCollection
12
+
13
+ def _set_axes_equal(ax, extra: float = 0.0):
14
+ """
15
+ Erzwingt identische numerische Achsenlimits.
16
+ optional: 'extra' = zusätzlicher Rand als Prozentsatz (0-1).
17
+ """
18
+ import numpy as np
19
+
20
+ # aktuelle Grenzen holen
21
+ x_limits = np.array(ax.get_xlim3d())
22
+ y_limits = np.array(ax.get_ylim3d())
23
+ z_limits = np.array(ax.get_zlim3d())
24
+
25
+ # Spannweiten & gemeinsames Maximum
26
+ ranges = np.array([np.ptp(lim) for lim in (x_limits, y_limits, z_limits)])
27
+ max_range = ranges.max()
28
+
29
+ # sind alle Punkte (fast) in einer Ebene? -> Mindestspanne ansetzen
30
+ if max_range == 0:
31
+ max_range = 1.0 # beliebiger Würfel von 1 m
32
+
33
+ # Mittelpunkt­koordinaten
34
+ mids = np.array([lim.mean() for lim in (x_limits, y_limits, z_limits)])
35
+
36
+ half = (1 + extra) * max_range / 2
37
+ ax.set_xlim3d(mids[0] - half, mids[0] + half)
38
+ ax.set_ylim3d(mids[1] - half, mids[1] + half)
39
+ ax.set_zlim3d(mids[2] - half, mids[2] + half)
40
+
41
+ # Darstellungswürfel in aktuellen MPL-Versionen
42
+ try:
43
+ ax.set_box_aspect((1, 1, 1))
44
+ except AttributeError:
45
+ pass
46
+
47
+
48
+ class mainloop:
49
+ def __init__(self):
50
+ ##________ Subclasses __________##
51
+ self.Inp = Input()
52
+ self.ElemStem = ElemStema()
53
+ ##_____ Class variables ___________##
54
+ self.K_el_i_store = np.zeros((len(self.Inp.members), 14, 14))
55
+
56
+ self.MY_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
57
+ self.VZ_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
58
+
59
+ self.MZ_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
60
+ self.VY_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
61
+ self.MX_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
62
+
63
+ self.MTP_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
64
+ self.MTS_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
65
+ self.N_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
66
+
67
+ ##_____ Class Functions____________##
68
+ self.MainConvergence()
69
+
70
+ def CalculateTransMat(self):
71
+ print("Calculate Transmatrices")
72
+
73
+ TransformationMatrices = np.zeros((len(self.Inp.members), 14, 14))
74
+ na_memb = self.Inp.members["na"]
75
+ ne_memb = self.Inp.members["ne"]
76
+ for i in range(len(self.Inp.members["na"])):
77
+
78
+ node_i = na_memb[i] # Node number for node i of this member
79
+ node_j = ne_memb[i] # Node number for node j of this member
80
+
81
+ # Index of DoF for this member
82
+ ia = 2 * node_i - 2 # horizontal DoF at node i of this member
83
+ ib = 2 * node_i - 1 # vertical DoF at node i of this member
84
+ ja = 2 * node_j - 2 # horizontal DoF at node j of this member
85
+ jb = 2 * node_j - 1 # vertical DoF at node j of this member
86
+
87
+ # New positions = initial pos + cum deflection
88
+ ix = self.Inp.nodes["x[m]"][node_i - 1]
89
+ iy = self.Inp.nodes["y[m]"][node_i - 1]
90
+ iz = self.Inp.nodes["z[m]"][node_i - 1]
91
+
92
+ jx = self.Inp.nodes["x[m]"][node_j - 1]
93
+ jy = self.Inp.nodes["y[m]"][node_j - 1]
94
+ jz = self.Inp.nodes["z[m]"][node_j - 1]
95
+
96
+ TM = self.ElemStem.TransformationMatrix([ix, iy, iz], [jx, jy, jz])
97
+
98
+ TransformationMatrices[i, :, :] = TM
99
+ print("Transmat")
100
+ print(TransformationMatrices[0])
101
+ return TransformationMatrices
102
+
103
+ def BuildStructureStiffnessMatrix(self):
104
+ """
105
+ Standard construction of Primary and Structure stiffness matrix
106
+ Construction of non-linear element stiffness matrix handled in a child function
107
+ """
108
+ Kp = np.zeros(
109
+ [self.Inp.nDoF, self.Inp.nDoF]
110
+ ) # Initialise the primary stiffness matrix
111
+ self.member_length = []
112
+
113
+ na_memb = self.Inp.members["na"]
114
+ ne_memb = self.Inp.members["ne"]
115
+ crosssec_members = self.Inp.members["cs"]
116
+
117
+ for i in range(0, len(self.Inp.members["na"]), 1):
118
+ node_i = na_memb[i] # Node number for node i of this member
119
+ node_j = ne_memb[i] # Node number for node j of this member
120
+
121
+ # New positions = initial pos + cum deflection
122
+ ix = self.Inp.nodes["x[m]"][node_i - 1]
123
+ iy = self.Inp.nodes["y[m]"][node_i - 1]
124
+ iz = self.Inp.nodes["z[m]"][node_i - 1]
125
+
126
+ jx = self.Inp.nodes["x[m]"][node_j - 1]
127
+ jy = self.Inp.nodes["y[m]"][node_j - 1]
128
+ jz = self.Inp.nodes["z[m]"][node_j - 1]
129
+
130
+ dx = abs(ix - jx)
131
+ dy = abs(iy - jy)
132
+ dz = abs(iz - jz)
133
+
134
+ length = np.sqrt(dx**2 + dy**2 + dz**2)
135
+ self.member_length.append(length)
136
+
137
+ num_cs = crosssec_members[i]
138
+ mat_num_i = self.Inp.CrossSection.loc[
139
+ self.Inp.CrossSection["No"] == num_cs, "material"
140
+ ].iloc[0]
141
+
142
+ I_y_i = self.Inp.CrossSection.loc[
143
+ self.Inp.CrossSection["No"] == num_cs, "Iy"
144
+ ].iloc[0]
145
+ I_z_i = self.Inp.CrossSection.loc[
146
+ self.Inp.CrossSection["No"] == num_cs, "Iz"
147
+ ].iloc[0]
148
+ A_i = self.Inp.CrossSection.loc[
149
+ self.Inp.CrossSection["No"] == num_cs, "A"
150
+ ].iloc[0]
151
+ I_w_i = self.Inp.CrossSection.loc[
152
+ self.Inp.CrossSection["No"] == num_cs, "Iw"
153
+ ].iloc[0]
154
+ I_T_i = self.Inp.CrossSection.loc[
155
+ self.Inp.CrossSection["No"] == num_cs, "It"
156
+ ].iloc[0]
157
+
158
+ print("Material NUmber")
159
+ print(mat_num_i)
160
+
161
+ E_i = self.Inp.Material.loc[self.Inp.Material["No"] == mat_num_i, "E"].iloc[
162
+ 0
163
+ ]
164
+ G_i = self.Inp.Material.loc[self.Inp.Material["No"] == mat_num_i, "G"].iloc[
165
+ 0
166
+ ]
167
+ print("Material E_i")
168
+ print(E_i)
169
+
170
+ K_el_i = self.ElemStem.insert_elements(
171
+ S=0,
172
+ E=E_i,
173
+ G=G_i,
174
+ A=A_i,
175
+ I_y=I_y_i,
176
+ I_z=I_z_i,
177
+ I_omega=I_w_i,
178
+ I_T=I_T_i,
179
+ cv=0,
180
+ z1=0,
181
+ cw=0,
182
+ z2=0,
183
+ c_thet=0,
184
+ l=length,
185
+ )
186
+
187
+ K_el_i = np.matmul(
188
+ self.TransMats[i].T, np.matmul(K_el_i, self.TransMats[i])
189
+ )
190
+
191
+ self.K_el_i_store[i] = K_el_i
192
+
193
+ K_11 = K_el_i[0:7, 0:7]
194
+ K_12 = K_el_i[0:7, 7:14]
195
+ K_21 = K_el_i[7:14, 0:7]
196
+ K_22 = K_el_i[7:14, 7:14]
197
+
198
+ Kp[
199
+ 7 * (node_i - 1) : 7 * (node_i - 1) + 7,
200
+ 7 * (node_i - 1) : 7 * (node_i - 1) + 7,
201
+ ] += K_11
202
+
203
+ Kp[
204
+ 7 * (node_i - 1) : 7 * (node_i - 1) + 7,
205
+ 7 * (node_j - 1) : 7 * (node_j - 1) + 7,
206
+ ] += K_12
207
+
208
+ Kp[
209
+ 7 * (node_j - 1) : 7 * (node_j - 1) + 7,
210
+ 7 * (node_i - 1) : 7 * (node_i - 1) + 7,
211
+ ] += K_21
212
+
213
+ Kp[
214
+ 7 * (node_j - 1) : 7 * (node_j - 1) + 7,
215
+ 7 * (node_j - 1) : 7 * (node_j - 1) + 7,
216
+ ] += K_22
217
+
218
+ return Kp
219
+
220
+ def RestraintData(self):
221
+ """
222
+ This functions implements the restraint data, which is loaded from the \n
223
+ input file. \n
224
+ There are 7 DOF's per node. \n
225
+ Therefore the restrained DOF in the global stiffness matrix can be expressed by: \n
226
+ GDOF = 7 * (node-1) + DOF \n
227
+ """
228
+ res_nodes = self.Inp.RestraintData["Node"]
229
+ res_dof = self.Inp.RestraintData["Dof"]
230
+ res_stif = self.Inp.RestraintData["Cp[MN/m]/[MNm/m]"]
231
+
232
+ for i in range(len(res_dof)):
233
+ glob_dof = 7 * (res_nodes[i] - 1) + res_dof[i]
234
+ print("restrain ", glob_dof)
235
+ self.GesMat[glob_dof, glob_dof] += res_stif[i]
236
+
237
+ def LocalLoadVectorLine(self):
238
+ """
239
+ This function calculates the local loadvectors for each element \n
240
+ and transforms them into the global coordinate system. \n
241
+ Each local force vector is stored in a separate local force vector, \n
242
+ which is taken into account, when the inner forces are calculated afterwards \n
243
+ Members of the local load vector are: \n
244
+ - Temperature loading \n
245
+ - Member forces \n
246
+ """
247
+ self.S_loc_elem_line = np.zeros((14, len(self.Inp.members)))
248
+ S_glob_elem = np.zeros((14, len(self.Inp.members)))
249
+
250
+ # Element Loading (line loads, single loads)
251
+
252
+ res_mbr = self.Inp.ElementLoads["Member"]
253
+ res_line_a = self.Inp.ElementLoads["qza"]
254
+ res_line_b = self.Inp.ElementLoads["qze"]
255
+
256
+ for i in range(0, len(res_mbr)):
257
+ j = int(res_mbr[i] - 1)
258
+ print("JJJ")
259
+ print(j)
260
+ print(res_mbr)
261
+ TransMatj = self.TransMats[j]
262
+
263
+ self.S_loc_elem_line[3, j] = (
264
+ +res_line_a[i] * self.member_length[j] / 2
265
+ ) # VZ
266
+ self.S_loc_elem_line[10, j] = +res_line_a[i] * self.member_length[j] / 2
267
+
268
+ self.S_loc_elem_line[4, j] = (
269
+ -res_line_a[i] * self.member_length[j] ** 2 / 12
270
+ ) # MY
271
+ self.S_loc_elem_line[11, j] = (
272
+ +res_line_a[i] * self.member_length[j] ** 2 / 12
273
+ )
274
+
275
+ S_loc = self.S_loc_elem_line[:, j]
276
+
277
+ # global line loadings
278
+
279
+ S_glob_elem[:, j] = np.matmul(np.transpose(TransMatj), S_loc)
280
+
281
+ return S_glob_elem
282
+
283
+ def LocalLoadVectorTemp(self):
284
+ """
285
+ This function calculates the local loadvectors for each element \n
286
+ and transforms them into the global coordinate system. \n
287
+ Each local force vector is stored in a separate local force vector, \n
288
+ which is taken into account, when the inner forces are calculated afterwards \n
289
+ Members of the local load vector are: \n
290
+ - Temperature loading \n
291
+ - Member forces \n
292
+ """
293
+ self.S_loc_elem_temp = np.zeros((14, len(self.Inp.members)))
294
+ S_glob_elem = np.zeros((14, len(self.Inp.members)))
295
+
296
+ # Temperature Loading
297
+
298
+ res_mbr = self.Inp.TemperatureForces["Member"]
299
+
300
+ res_tem_dT = self.Inp.TemperatureForces["dT[K]"]
301
+ res_tem_dTz = self.Inp.TemperatureForces["dTz[K]"]
302
+ res_tem_dTy = self.Inp.TemperatureForces["dTy[K]"]
303
+
304
+ for i in range(0, len(res_mbr)):
305
+ j = int(res_mbr[i] - 1)
306
+ TransMatj = self.TransMats[j]
307
+ # Local temperature loadings
308
+ self.S_loc_elem_temp[0, j] = (
309
+ -self.ElemStem.E * self.ElemStem.A * 1e-5 * res_tem_dT[i]
310
+ ) # N
311
+ self.S_loc_elem_temp[7, j] = (
312
+ +self.ElemStem.E * self.ElemStem.A * 1e-5 * res_tem_dT[i]
313
+ )
314
+
315
+ self.S_loc_elem_temp[2, j] = (
316
+ -self.ElemStem.E * self.ElemStem.I_z * 1e-5 * res_tem_dTy[i]
317
+ ) # MZ
318
+ self.S_loc_elem_temp[9, j] = (
319
+ +self.ElemStem.E * self.ElemStem.I_z * 1e-5 * res_tem_dTy[i]
320
+ )
321
+
322
+ self.S_loc_elem_temp[4, j] = (
323
+ -self.ElemStem.E * self.ElemStem.I_y * 1e-5 * res_tem_dTz[i]
324
+ ) # MY
325
+ self.S_loc_elem_temp[11, j] = (
326
+ +self.ElemStem.E * self.ElemStem.I_y * 1e-5 * res_tem_dTz[i]
327
+ )
328
+
329
+ S_loc = self.S_loc_elem_temp[:, j]
330
+
331
+ # global temperature loadings
332
+
333
+ S_glob_elem[:, j] = np.matmul(np.transpose(TransMatj), S_loc)
334
+
335
+ return S_glob_elem
336
+
337
+ def GlobalLoadVector(self):
338
+ F_loc_temp = self.LocalLoadVectorTemp()
339
+ F_loc_line = self.LocalLoadVectorLine()
340
+
341
+ print("Local element temp")
342
+ print(F_loc_temp)
343
+
344
+ print("Local element line")
345
+ print(F_loc_line)
346
+
347
+ F_glob = np.zeros(self.Inp.nDoF)
348
+
349
+ na_memb = self.Inp.members["na"]
350
+ ne_memb = self.Inp.members["ne"]
351
+
352
+ # Input of Member Forces
353
+ for i in range(0, len(self.Inp.members)):
354
+ node_i = na_memb[i]
355
+ node_j = ne_memb[i]
356
+
357
+ dof_Na = 7 * (node_i - 1)
358
+ dof_Ne = 7 * (node_j - 1)
359
+
360
+ dof_Vya = 7 * (node_i - 1) + 1
361
+ dof_Vye = 7 * (node_j - 1) + 1
362
+
363
+ dof_Vza = 7 * (node_i - 1) + 3
364
+ dof_Vze = 7 * (node_j - 1) + 3
365
+
366
+ dof_Mza = 7 * (node_i - 1) + 2
367
+ dof_Mze = 7 * (node_j - 1) + 2
368
+
369
+ dof_Mya = 7 * (node_i - 1) + 2
370
+ dof_Mye = 7 * (node_j - 1) + 2
371
+
372
+ F_glob[dof_Na] += F_loc_temp[0, i] + F_loc_line[0, i]
373
+ F_glob[dof_Ne] += F_loc_temp[7, i] + F_loc_line[7, i]
374
+
375
+ F_glob[dof_Vya] += F_loc_temp[1, i]
376
+ F_glob[dof_Vye] += F_loc_temp[8, i]
377
+
378
+ F_glob[dof_Vza] += F_loc_temp[3, i] + F_loc_line[3, i]
379
+ F_glob[dof_Vze] += F_loc_temp[10, i] + F_loc_line[10, i]
380
+
381
+ F_glob[dof_Mza] += F_loc_temp[2, i]
382
+ F_glob[dof_Mze] += F_loc_temp[9, i]
383
+
384
+ F_glob[dof_Mya] += F_loc_temp[4, i] + F_loc_line[4, i]
385
+ F_glob[dof_Mye] += F_loc_temp[11, i] + F_loc_line[11, i]
386
+
387
+ # Input of Nodal Forces
388
+ res_nodes = self.Inp.NodalForces["Node"]
389
+ res_dof = self.Inp.NodalForces["Dof"]
390
+ res_forc = self.Inp.NodalForces["Value[MN/MNm]"]
391
+
392
+ for i in range(0, len(res_nodes)):
393
+ node_i = res_nodes[i]
394
+ glob_index = (node_i - 1) * 7
395
+
396
+ if res_dof[i] == "Fx" or res_dof[i] == "fx" or res_dof[i] == "FX":
397
+ glob_index += 0
398
+ elif res_dof[i] == "Fy" or res_dof[i] == "fy" or res_dof[i] == "FY":
399
+ glob_index += 1
400
+ elif res_dof[i] == "Fz" or res_dof[i] == "fz" or res_dof[i] == "FZ":
401
+ glob_index += 3
402
+
403
+ F_glob[glob_index] += res_forc[i]
404
+
405
+ print("Force vector ", F_glob)
406
+ print("Local force vectors ", self.S_loc_elem_temp)
407
+
408
+ return F_glob
409
+
410
+ def SolveDisplacement(self):
411
+ u_glob = np.linalg.solve(self.GesMat, self.FGes)
412
+ return u_glob
413
+
414
+ def StoreLocalDisplacements(self):
415
+ u_el = np.zeros(
416
+ [14, len(self.Inp.members["na"])]
417
+ ) # Initialise the primary stiffness matrix
418
+
419
+ na_memb = self.Inp.members["na"]
420
+ ne_memb = self.Inp.members["ne"]
421
+
422
+ for i in range(0, len(self.Inp.members["na"]), 1):
423
+ numa = 7 * (na_memb[i] - 1)
424
+ nume = 7 * (ne_memb[i] - 1)
425
+
426
+ u_el[0:7, i] = self.u_ges[numa : numa + 7]
427
+ u_el[7:14, i] = self.u_ges[nume : nume + 7]
428
+
429
+ # u_el[:,i] = np.matmul(self.TransMats[i],u_el[:,i])
430
+
431
+ return u_el
432
+
433
+ def CalculateLocalInnerForces(self):
434
+ s_el = np.zeros([14, len(self.Inp.members["na"])])
435
+
436
+ for i in range(0, len(self.Inp.members["na"]), 1):
437
+ s_el[:, i] = np.matmul(self.K_el_i_store[i], self.u_el[:, i])
438
+ self.MZ_el_i_store[i] = np.array([s_el[2, i] * (-1), s_el[9, i]]).reshape(
439
+ 2, 1
440
+ ) # Left is *(-1)
441
+ self.MY_el_i_store[i] = np.array([s_el[4, i] * (-1), s_el[11, i]]).reshape(
442
+ 2, 1
443
+ ) # Left is *(-1)
444
+ self.MX_el_i_store[i] = np.array([s_el[5, i] * (-1), s_el[12, i]]).reshape(
445
+ 2, 1
446
+ ) # Left is *(-1)
447
+
448
+ self.N_el_i_store[i] = np.array([s_el[0, i] * (-1), s_el[7, i]]).reshape(
449
+ 2, 1
450
+ ) # Left is *(-1)
451
+ self.VY_el_i_store[i] = np.array([s_el[1, i] * (-1), s_el[8, i]]).reshape(
452
+ 2, 1
453
+ ) # Left is *(-1)
454
+ self.VZ_el_i_store[i] = np.array([s_el[3, i] * (-1), s_el[10, i]]).reshape(
455
+ 2, 1
456
+ ) # Left is *(-1)
457
+
458
+ # Explicit transformation in local coordinates
459
+
460
+ self.N_el_i_store[i] = (
461
+ self.N_el_i_store[i] * self.TransMats[i][0, 0]
462
+ + self.VY_el_i_store[i] * self.TransMats[i][0, 1]
463
+ + self.VZ_el_i_store[i] * self.TransMats[i][0, 3]
464
+ - np.array(
465
+ [-self.S_loc_elem_temp[0, i], self.S_loc_elem_temp[7, i]]
466
+ ).reshape(
467
+ 2, 1
468
+ ) # Left is * (-1)
469
+ # - np.array([-self.S_loc_elem_line[0, i], self.S_loc_elem_line[7, i]]).reshape(2, 1) # Left is * (-1)
470
+ )
471
+
472
+ self.MY_el_i_store[i] = (
473
+ self.MX_el_i_store[i] * self.TransMats[i][4, 5]
474
+ + self.MY_el_i_store[i] * self.TransMats[i][4, 4]
475
+ + self.MZ_el_i_store[i] * self.TransMats[i][4, 2]
476
+ - np.array(
477
+ [-self.S_loc_elem_temp[4, i], self.S_loc_elem_temp[11, i]]
478
+ ).reshape(
479
+ 2, 1
480
+ ) # Left is * (-1)
481
+ # - np.array([-self.S_loc_elem_line[4, i], self.S_loc_elem_line[11, i]]).reshape(2, 1) # Left is * (-1)
482
+ )
483
+
484
+ return s_el
485
+
486
+ def MainConvergence(self):
487
+
488
+ self.TransMats = self.CalculateTransMat()
489
+ self.GesMat = self.BuildStructureStiffnessMatrix()
490
+
491
+ self.RestraintData()
492
+
493
+ self.FGes = self.GlobalLoadVector()
494
+
495
+ self.u_ges = self.SolveDisplacement()
496
+
497
+ self.u_el = self.StoreLocalDisplacements()
498
+
499
+ self.s_el = self.CalculateLocalInnerForces()
500
+
501
+ def plot_structure_3d(self, nodes, na_memb, ne_memb):
502
+ """
503
+ Plot the undeformed 3D structure.
504
+
505
+ Parameters:
506
+ nodes: dict with keys ['x[m]', 'y[m]', 'z[m]']
507
+ na_memb, ne_memb: lists with node IDs for start and end of members
508
+ """
509
+ fig = plt.figure()
510
+ ax = fig.add_subplot(projection="3d")
511
+
512
+ # Plot all members
513
+ for i in range(len(na_memb)):
514
+ node_i = na_memb[i]
515
+ node_j = ne_memb[i]
516
+
517
+ ix = nodes["x[m]"][node_i - 1]
518
+ iy = nodes["y[m]"][node_i - 1]
519
+ iz = nodes["z[m]"][node_i - 1]
520
+
521
+ jx = nodes["x[m]"][node_j - 1]
522
+ jy = nodes["y[m]"][node_j - 1]
523
+ jz = nodes["z[m]"][node_j - 1]
524
+
525
+ X = [ix, jx]
526
+ Y = [iy, jy]
527
+ Z = [iz, jz]
528
+
529
+ ax.plot(X, Y, Z, color="black", lw=1.0)
530
+
531
+ # Achsenbeschriftung und Titel
532
+ ax.set_xlabel("X [m]")
533
+ ax.set_ylabel("Y [m]")
534
+ ax.set_zlabel("Z [m]")
535
+ ax.set_title("Unverformte Struktur")
536
+
537
+ return fig, ax
538
+
539
+ def calculate_orthogonal_unit_vector_cross_product(self, xi, zi, xj, zj):
540
+ """
541
+ Berechnet einen normierten orthogonalen Vektor zur Strukturlinie von (xi, zi) nach (xj, zj)
542
+ mittels Kreuzprodukt.
543
+
544
+ Parameter:
545
+ - xi, zi: Koordinaten des Startknotens
546
+ - xj, zj: Koordinaten des Endknotens
547
+
548
+ Rückgabe:
549
+ - unit_vector_pos: NumPy-Array des normierten orthogonalen Vektors für positives My
550
+ - unit_vector_neg: NumPy-Array des normierten orthogonalen Vektors für negatives My
551
+ """
552
+ # Richtungsvektor in 3D (y-Komponente ist 0)
553
+ v = np.array([xj - xi, 0, zj - zi])
554
+
555
+ # Einheitsvektor entlang der y-Achse
556
+ y_unit = np.array([0, 1, 0])
557
+
558
+ # Kreuzprodukt berechnen
559
+ perp_vector = np.cross(v, y_unit) # Ergebnis ist ebenfalls ein 3D-Vektor
560
+
561
+ # Extrahiere die x und z Komponenten
562
+ perp_vector_2d = perp_vector[[0, 2]]
563
+
564
+ # Normierung
565
+ norm = np.linalg.norm(perp_vector_2d)
566
+ if norm == 0:
567
+ raise ValueError(
568
+ "Die Strukturlinie hat keine Länge. Start- und Endknoten sind identisch."
569
+ )
570
+
571
+ unit_vector = perp_vector_2d / norm
572
+
573
+ # Für positives und negatives My
574
+ unit_vector_pos = unit_vector
575
+ unit_vector_neg = -unit_vector
576
+
577
+ return unit_vector_pos, unit_vector_neg
578
+
579
+ def plot_moment_my_2d(self, ax, xi, zi, my_local, unit_vector, scale=1):
580
+ """
581
+ Plot eines Biegemoments My in 2D (x-z-Ebene) orthogonal zur Strukturlinie.
582
+
583
+ Parameter:
584
+ ax : matplotlib.axes.Axes
585
+ Die 2D-Achse zum Plotten
586
+ xi, zi : float
587
+ Startpunkt (globale Koordinaten in x und z)
588
+ my_local : float
589
+ Lokaler My-Wert
590
+ unit_vector : array-like, shape (2,)
591
+ Normalisierter orthogonaler Vektor für das Moment
592
+ scale : float, optional
593
+ Maßstabsfaktor für die Pfeillänge
594
+ """
595
+ if my_local == 0:
596
+ return # Kein Moment zu plotten
597
+
598
+ # Kleine Verbindungslinie vom Knoten zum Start des Pfeils
599
+ connection_length = 0.05 * scale # Anpassbarer Wert
600
+
601
+ conn_x = xi + connection_length * unit_vector[0] * my_local * scale
602
+ conn_z = zi + connection_length * unit_vector[1] * my_local * scale
603
+
604
+ if my_local >= 0:
605
+ ax.plot([xi, conn_x], [zi, conn_z], color="blue", linewidth=1)
606
+ if my_local < 0:
607
+ ax.plot([xi, conn_x], [zi, conn_z], color="red", linewidth=1)
608
+
609
+ # Berechnung des Vektors, skaliert durch Moment und Maßstabsfaktor
610
+ vec = scale * my_local * unit_vector
611
+
612
+ # Farbwahl basierend auf dem Vorzeichen des Moments
613
+ color = "blue" if my_local >= 0 else "red"
614
+
615
+ # Plotten des Pfeils
616
+ # ax.arrow(conn_x, conn_z, vec[0], vec[1],
617
+ # head_width=0.05 * scale, head_length=0.1 * scale,
618
+ # fc=color, ec=color, length_includes_head=True)
619
+
620
+ # Berechnung des Endpunkts für die Textplatzierung
621
+ x_end = conn_x
622
+ z_end = conn_z
623
+
624
+ # Moment-Text
625
+ moment_text = f"My = {my_local:.3f} MNm"
626
+
627
+ # Textversatz für bessere Sichtbarkeit
628
+ text_offset = 0 # 0.05 * scale
629
+ ax.text(
630
+ x_end + text_offset * unit_vector[0],
631
+ z_end + text_offset * unit_vector[1],
632
+ moment_text,
633
+ color=color,
634
+ fontsize=8,
635
+ )
636
+
637
+ return conn_x, conn_z
638
+
639
+ def plot_all_My_2d(self, ax, nodes, na_memb, ne_memb, My_el_i_store, scale=0.4):
640
+ """
641
+ Plot My-Momente für alle Elemente an ihren Knoten in 2D (x-z-Ebene).
642
+
643
+ Parameter:
644
+ ax : matplotlib.axes.Axes
645
+ Die 2D-Achse zum Plotten
646
+ nodes : dict
647
+ Dictionary mit Knotenkordinaten
648
+ na_memb, ne_memb : Listen
649
+ Listen von Knotennummern für die Elemente
650
+ My_el_i_store : ndarray
651
+ Array von My-Werten pro Element und Knoten
652
+ scale : float, optional
653
+ Maßstabsfaktor für die Pfeile
654
+ """
655
+ for i in range(len(na_memb)):
656
+ node_i = na_memb[i]
657
+ node_j = ne_memb[i]
658
+
659
+ ix = nodes["x[m]"][node_i - 1]
660
+ iz = nodes["z[m]"][node_i - 1]
661
+
662
+ jx = nodes["x[m]"][node_j - 1]
663
+ jz = nodes["z[m]"][node_j - 1]
664
+
665
+ # Berechne die Einheitsvektoren für positives und negatives My mittels Kreuzprodukt
666
+ try:
667
+ unit_vector_pos, unit_vector_neg = (
668
+ self.calculate_orthogonal_unit_vector_cross_product(ix, iz, jx, jz)
669
+ )
670
+ print(
671
+ f"Element {i+1}: unit_vector_pos = {unit_vector_pos}, unit_vector_neg = {unit_vector_neg}"
672
+ )
673
+ except ValueError as e:
674
+ print(f"Fehler bei Mitglied {i+1}: {e}")
675
+ continue
676
+
677
+ # Lokales My an Knoten a / b
678
+ My_a = My_el_i_store[i, 0, 0] # [i,0] -> Knoten a
679
+ My_b = My_el_i_store[i, 1, 0] # [i,1] -> Knoten b
680
+
681
+ try:
682
+ # 1) Pfeil am Knoten a
683
+ conn_ix, conn_iz = self.plot_moment_my_2d(
684
+ ax, ix, iz, My_a, unit_vector_pos, scale=scale
685
+ )
686
+ # 2) Pfeil am Knoten b
687
+ conn_jx, conn_jz = self.plot_moment_my_2d(
688
+ ax, jx, jz, My_b, unit_vector_pos, scale=scale
689
+ )
690
+ ax.plot(
691
+ [conn_ix, conn_jx], [conn_iz, conn_jz], color="black", linewidth=1
692
+ )
693
+ except:
694
+ pass
695
+
696
+ # Hinzufügen einer Legende zur Unterscheidung
697
+ red_patch = mpatches.Patch(color="blue", label="Positives My")
698
+ blue_patch = mpatches.Patch(color="red", label="Negatives My")
699
+ ax.legend(handles=[red_patch, blue_patch])
700
+
701
+ def plot_normalforce_N_2d(self, ax, xi, zi, N_local, unit_vector, scale=1):
702
+ """
703
+ Plot eines Biegemoments My in 2D (x-z-Ebene) orthogonal zur Strukturlinie.
704
+
705
+ Parameter:
706
+ ax : matplotlib.axes.Axes
707
+ Die 2D-Achse zum Plotten
708
+ xi, zi : float
709
+ Startpunkt (globale Koordinaten in x und z)
710
+ N_local : float
711
+ Lokaler N-Wert
712
+ unit_vector : array-like, shape (2,)
713
+ Normalisierter orthogonaler Vektor für das Moment
714
+ scale : float, optional
715
+ Maßstabsfaktor für die Pfeillänge
716
+ """
717
+
718
+ if N_local == 0:
719
+ return # Keine Kraft zu plotten
720
+
721
+ # Kleine Verbindungslinie vom Knoten zum Start des Pfeils
722
+ connection_length = 0.05 * scale # Anpassbarer Wert
723
+
724
+ conn_x = xi + connection_length * unit_vector[0] * N_local * scale
725
+ conn_z = zi + connection_length * unit_vector[1] * N_local * scale
726
+
727
+ if N_local >= 0:
728
+ ax.plot([xi, conn_x], [zi, conn_z], color="blue", linewidth=1)
729
+ if N_local < 0:
730
+ ax.plot([xi, conn_x], [zi, conn_z], color="red", linewidth=1)
731
+
732
+ # Berechnung des Vektors, skaliert durch Moment und Maßstabsfaktor
733
+ vec = scale * N_local * unit_vector
734
+
735
+ # Farbwahl basierend auf dem Vorzeichen des Moments
736
+ color = "blue" if N_local >= 0 else "red"
737
+
738
+ # Plotten des Pfeils
739
+ # ax.arrow(conn_x, conn_z, vec[0], vec[1],
740
+ # head_width=0.05 * scale, head_length=0.1 * scale,
741
+ # fc=color, ec=color, length_includes_head=True)
742
+
743
+ # Berechnung des Endpunkts für die Textplatzierung
744
+ x_end = conn_x
745
+ z_end = conn_z
746
+
747
+ # Moment-Text
748
+ moment_text = f"N = {N_local:.3f} MNm"
749
+
750
+ # Textversatz für bessere Sichtbarkeit
751
+ text_offset = 0 # 0.05 * scale
752
+ ax.text(
753
+ x_end + text_offset * unit_vector[0],
754
+ z_end + text_offset * unit_vector[1],
755
+ moment_text,
756
+ color=color,
757
+ fontsize=8,
758
+ )
759
+
760
+ return conn_x, conn_z
761
+
762
+ def plot_all_N_2d(self, ax, nodes, na_memb, ne_memb, N_el_i_store, scale=0.4):
763
+ """
764
+ Plot N-Momente für alle Elemente an ihren Knoten in 2D (x-z-Ebene).
765
+
766
+ Parameter:
767
+ ax : matplotlib.axes.Axes
768
+ Die 2D-Achse zum Plotten
769
+ nodes : dict
770
+ Dictionary mit Knotenkordinaten
771
+ na_memb, ne_memb : Listen
772
+ Listen von Knotennummern für die Elemente
773
+ N_el_i_store : ndarray
774
+ Array von N-Werten pro Element und Knoten
775
+ scale : float, optional
776
+ Maßstabsfaktor für die Pfeile
777
+ """
778
+ for i in range(len(na_memb)):
779
+ node_i = na_memb[i]
780
+ node_j = ne_memb[i]
781
+
782
+ ix = nodes["x[m]"][node_i - 1]
783
+ iz = nodes["z[m]"][node_i - 1]
784
+
785
+ jx = nodes["x[m]"][node_j - 1]
786
+ jz = nodes["z[m]"][node_j - 1]
787
+
788
+ # Berechne die Einheitsvektoren für positives und negatives My mittels Kreuzprodukt
789
+ try:
790
+ unit_vector_pos, unit_vector_neg = (
791
+ self.calculate_orthogonal_unit_vector_cross_product(ix, iz, jx, jz)
792
+ )
793
+ print(
794
+ f"Element {i+1}: unit_vector_pos = {unit_vector_pos}, unit_vector_neg = {unit_vector_neg}"
795
+ )
796
+ except ValueError as e:
797
+ print(f"Fehler bei Mitglied {i+1}: {e}")
798
+ continue
799
+
800
+ # Lokales My an Knoten a / b
801
+ N_a = N_el_i_store[i, 0, 0] # [i,0] -> Knoten a
802
+ N_b = N_el_i_store[i, 1, 0] # [i,1] -> Knoten b
803
+
804
+ try:
805
+ # 1) Pfeil am Knoten a
806
+ conn_ix, conn_iz = self.plot_normalforce_N_2d(
807
+ ax, ix, iz, N_a, unit_vector_pos, scale=scale
808
+ )
809
+ # 2) Pfeil am Knoten b
810
+ conn_jx, conn_jz = self.plot_normalforce_N_2d(
811
+ ax, jx, jz, N_b, unit_vector_pos, scale=scale
812
+ )
813
+ ax.plot(
814
+ [conn_ix, conn_jx], [conn_iz, conn_jz], color="black", linewidth=1
815
+ )
816
+ except:
817
+ pass
818
+
819
+ # Hinzufügen einer Legende zur Unterscheidung
820
+ red_patch = mpatches.Patch(color="blue", label="Positives N")
821
+ blue_patch = mpatches.Patch(color="red", label="Negatives N")
822
+ ax.legend(handles=[red_patch, blue_patch])
823
+
824
+ def plot_structure_2d(self, nodes, na_memb, ne_memb):
825
+ """
826
+ Plot der unverformten 2D-Struktur in der x-z-Ebene.
827
+
828
+ Parameter:
829
+ nodes: dict mit den Schlüsseln ['x[m]', 'y[m]', 'z[m]']
830
+ na_memb, ne_memb: Listen mit Knoten-IDs für Start und Ende der Elemente
831
+ """
832
+ fig, ax = plt.subplots(figsize=(10, 8))
833
+
834
+ # Alle Elemente plotten
835
+ for i in range(len(na_memb)):
836
+ node_i = na_memb[i]
837
+ node_j = ne_memb[i]
838
+
839
+ ix = nodes["x[m]"][node_i - 1]
840
+ iz = nodes["z[m]"][node_i - 1]
841
+
842
+ jx = nodes["x[m]"][node_j - 1]
843
+ jz = nodes["z[m]"][node_j - 1]
844
+
845
+ X = [ix, jx]
846
+ Z = [iz, jz]
847
+
848
+ ax.plot(X, Z, color="black", lw=1.0)
849
+
850
+ # Achsenbeschriftung und Titel
851
+ ax.set_xlabel("X [m]")
852
+ ax.set_ylabel("Z [m]")
853
+ ax.set_title("Unverformte Struktur (x-z-Ebene)")
854
+ ax.grid(True)
855
+
856
+ return fig, ax
857
+
858
+ def plot_structure_with_moments_2d(self, scale=0.4):
859
+ """
860
+ Kombinierte Methode zum Plotten der Struktur und der Biegemomente My in 2D (x-z-Ebene).
861
+
862
+ Parameter:
863
+ scale : float, optional
864
+ Skalierungsfaktor für die Momente
865
+ """
866
+ fig, ax = self.plot_structure_2d(
867
+ self.Inp.nodes, self.Inp.members["na"], self.Inp.members["ne"]
868
+ )
869
+
870
+ # Plot der Biegemomente My mittels Kreuzprodukt
871
+ self.plot_all_My_2d(
872
+ ax,
873
+ self.Inp.nodes,
874
+ self.Inp.members["na"],
875
+ self.Inp.members["ne"],
876
+ self.MY_el_i_store,
877
+ scale=scale,
878
+ )
879
+
880
+ # Achsenlimits automatisch anpassen und gleiches Seitenverhältnis
881
+ ax.set_aspect("equal", adjustable="datalim")
882
+ ax.relim()
883
+ ax.autoscale_view()
884
+
885
+ return fig, ax
886
+
887
+ def plot_structure_with_normalforces_2d(self, scale=0.4):
888
+ """
889
+ Kombinierte Methode zum Plotten der Struktur und der Biegemomente My in 2D (x-z-Ebene).
890
+
891
+ Parameter:
892
+ scale : float, optional
893
+ Skalierungsfaktor für die Momente
894
+ """
895
+ fig, ax = self.plot_structure_2d(
896
+ self.Inp.nodes, self.Inp.members["na"], self.Inp.members["ne"]
897
+ )
898
+
899
+ # Plot der Biegemomente My mittels Kreuzprodukt
900
+ self.plot_all_N_2d(
901
+ ax,
902
+ self.Inp.nodes,
903
+ self.Inp.members["na"],
904
+ self.Inp.members["ne"],
905
+ self.N_el_i_store,
906
+ scale=scale,
907
+ )
908
+
909
+ # Achsenlimits automatisch anpassen und gleiches Seitenverhältnis
910
+ ax.set_aspect("equal", adjustable="datalim")
911
+ ax.relim()
912
+ ax.autoscale_view()
913
+
914
+ return fig, ax
915
+
916
+ def plot_structure_deformed_3d(
917
+ self,
918
+ scale: float = 1.0,
919
+ show_undeformed: bool = True,
920
+ node_labels: bool = False,
921
+ undeformed_kwargs: dict | None = None,
922
+ deformed_kwargs: dict | None = None,
923
+ ):
924
+ """
925
+ Zeichnet das räumliche Tragwerk samt skalierter Verformungen.
926
+
927
+ Parameters
928
+ ----------
929
+ scale : float, optional
930
+ Maßstabsfaktor für die Verschiebungen.
931
+ show_undeformed : bool, optional
932
+ Wenn True, wird das unverformte System zusätzlich angezeigt.
933
+ node_labels : bool, optional
934
+ Beschriftet die Knoten mit ihrer Nummer.
935
+ undeformed_kwargs / deformed_kwargs : dict, optional
936
+ Extra-Keyword-Argumente für die Linien (Farbe, Linienstärke …).
937
+ """
938
+ if undeformed_kwargs is None:
939
+ undeformed_kwargs = dict(color="lightgray", lw=1.0, zorder=1)
940
+ if deformed_kwargs is None:
941
+ deformed_kwargs = dict(color="blue", lw=2.0, zorder=3)
942
+
943
+ fig = plt.figure(figsize=(10, 8))
944
+ ax: Axes3D = fig.add_subplot(projection="3d")
945
+
946
+ na = self.Inp.members["na"]
947
+ ne = self.Inp.members["ne"]
948
+
949
+ # Kurzfunktionen für Verschiebungen
950
+ def ux(n):
951
+ return self.u_ges[7 * (n - 1) + 0] # u_x
952
+
953
+ def uy(n):
954
+ return self.u_ges[7 * (n - 1) + 1] # u_y
955
+
956
+ def uz(n):
957
+ return self.u_ges[7 * (n - 1) + 3] # u_z
958
+
959
+ # 1) Unverformte Struktur
960
+ if show_undeformed:
961
+ for a, e in zip(na, ne):
962
+ xa, ya, za = (
963
+ self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]")
964
+ )
965
+ xe, ye, ze = (
966
+ self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]")
967
+ )
968
+ ax.plot([xa, xe], [ya, ye], [za, ze], **undeformed_kwargs)
969
+
970
+ # 2) Verformte Struktur
971
+ for a, e in zip(na, ne):
972
+ xa, ya, za = (self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]"))
973
+ xe, ye, ze = (self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]"))
974
+
975
+ ax.plot(
976
+ [xa + scale * ux(a), xe + scale * ux(e)],
977
+ [ya + scale * uy(a), ye + scale * uy(e)],
978
+ [za + scale * uz(a), ze + scale * uz(e)],
979
+ **deformed_kwargs,
980
+ )
981
+
982
+ # 3) Knotennummern (optional)
983
+ if node_labels:
984
+ for n in range(1, len(self.Inp.nodes) + 1):
985
+ x0 = self.Inp.nodes["x[m]"][n - 1] + scale * ux(n)
986
+ y0 = self.Inp.nodes["y[m]"][n - 1] + scale * uy(n)
987
+ z0 = self.Inp.nodes["z[m]"][n - 1] + scale * uz(n)
988
+ ax.text(x0, y0, z0, f"{n}", fontsize=8, ha="center", va="center")
989
+
990
+ # Achsen und Optik
991
+ ax.set_xlabel("X [m]")
992
+ ax.set_ylabel("Y [m]")
993
+ ax.set_zlabel("Z [m]")
994
+ ax.set_title(f"Verformte Struktur 3-D (Skalierung {scale:g})")
995
+ ax.set_box_aspect((1, 1, 1)) # gleiches Seitenverhältnis
996
+ fig.tight_layout()
997
+
998
+ # Optional: Blickwinkel anpassen (z. B. isometrisch)
999
+ # ax.view_init(elev=20, azim=-60)
1000
+
1001
+ return fig, ax
1002
+
1003
+ def plot_structure_deformed_3d_interactive(
1004
+ self,
1005
+ scale_init: float = 1.0,
1006
+ show_undeformed: bool = True,
1007
+ node_labels: bool = False,
1008
+ ):
1009
+ """
1010
+ Öffnet ein 3-D-Fenster mit interaktiven Slidern:
1011
+ • Elevation (θ) • Azimut (φ) • Skalierung s der Verformungen
1012
+ Die x-, y-, z-Achsen werden stets auf identische Länge gebracht.
1013
+ """
1014
+
1015
+ # ─── Kurzfunktionen für Verschiebungen ────────────────────────────
1016
+ ux = lambda n: self.u_ges[7 * (n - 1) + 0] # DOF 0
1017
+ uy = lambda n: self.u_ges[7 * (n - 1) + 1] # DOF 1
1018
+ uz = lambda n: self.u_ges[7 * (n - 1) + 3] # DOF 3
1019
+
1020
+ # ─── Grundplot anlegen ───────────────────────────────────────────
1021
+ import matplotlib.pyplot as plt
1022
+
1023
+ fig = plt.figure(figsize=(8, 6))
1024
+ ax = fig.add_subplot(projection="3d")
1025
+
1026
+ # 1) nach dem allerersten Zeichnen
1027
+ _set_axes_equal(ax)
1028
+
1029
+ na, ne = self.Inp.members["na"], self.Inp.members["ne"]
1030
+
1031
+ # Listen zum schnellen Updaten
1032
+ deformed_lines: list[Line3D] = []
1033
+
1034
+ # ── Linien zeichnen (undeformed + deformed) ───────────────────────
1035
+ for a, e in zip(na, ne):
1036
+ xa0, ya0, za0 = (self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]"))
1037
+ xe0, ye0, ze0 = (self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]"))
1038
+
1039
+ if show_undeformed:
1040
+ ax.plot(
1041
+ [xa0, xe0],
1042
+ [ya0, ye0],
1043
+ [za0, ze0],
1044
+ color="lightgray",
1045
+ lw=1,
1046
+ zorder=1,
1047
+ )
1048
+
1049
+ # deformed-Linie initial
1050
+ xd = [xa0 + scale_init * ux(a), xe0 + scale_init * ux(e)]
1051
+ yd = [ya0 + scale_init * uy(a), ye0 + scale_init * uy(e)]
1052
+ zd = [za0 + scale_init * uz(a), ze0 + scale_init * uz(e)]
1053
+
1054
+ (ld,) = ax.plot(xd, yd, zd, color="tab:blue", lw=2, zorder=3)
1055
+ deformed_lines.append(ld)
1056
+
1057
+ # Knotennummern (optional) – als separate Textobjekte
1058
+ text_elems = []
1059
+ if node_labels:
1060
+ for n in range(1, len(self.Inp.nodes) + 1):
1061
+ x0, y0, z0 = (
1062
+ self.Inp.nodes[k][n - 1] for k in ("x[m]", "y[m]", "z[m]")
1063
+ )
1064
+ t = ax.text(
1065
+ x0 + scale_init * ux(n),
1066
+ y0 + scale_init * uy(n),
1067
+ z0 + scale_init * uz(n),
1068
+ f"{n}",
1069
+ fontsize=8,
1070
+ ha="center",
1071
+ va="center",
1072
+ zorder=4,
1073
+ )
1074
+ text_elems.append(t)
1075
+
1076
+ # ─── Achsenformatierung ───────────────────────────────────────────
1077
+ ax.set_xlabel("X [m]")
1078
+ ax.set_ylabel("Y [m]")
1079
+ ax.set_zlabel("Z [m]")
1080
+ ax.set_title("Verformte Struktur – interaktiv")
1081
+ try:
1082
+ ax.set_box_aspect((1, 1, 1)) # Matplotlib ≥ 3.3
1083
+ except AttributeError:
1084
+ _set_axes_equal(ax)
1085
+
1086
+ # ─── Slider-UI unter dem Plot platzieren ──────────────────────────
1087
+ fig.subplots_adjust(bottom=0.25) # Platz für drei Slider
1088
+
1089
+ # Achsen-Koordinaten [links, unten, breite, höhe]
1090
+ ax_elev = fig.add_axes([0.13, 0.15, 0.74, 0.03])
1091
+ ax_azim = fig.add_axes([0.13, 0.10, 0.74, 0.03])
1092
+ ax_scale = fig.add_axes([0.13, 0.05, 0.74, 0.03])
1093
+
1094
+ s_elev = Slider(ax_elev, "Elev (°)", -90, 90, valinit=20, valstep=1)
1095
+ s_azim = Slider(ax_azim, "Azim (°)", -180, 180, valinit=-60, valstep=1)
1096
+ s_scale = Slider(ax_scale, "Scale", 0.0, scale_init * 10000, valinit=scale_init)
1097
+
1098
+ # ─── Callback-Funktionen ──────────────────────────────────────────
1099
+ def _update_view(_):
1100
+ ax.view_init(elev=s_elev.val, azim=s_azim.val)
1101
+ fig.canvas.draw_idle()
1102
+
1103
+ def _update_scale(_):
1104
+ s = s_scale.val
1105
+ for (a, e), line in zip(zip(na, ne), deformed_lines):
1106
+ xa0, ya0, za0 = (
1107
+ self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]")
1108
+ )
1109
+ xe0, ye0, ze0 = (
1110
+ self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]")
1111
+ )
1112
+ # neue Koordinaten
1113
+ line.set_data_3d(
1114
+ [xa0 + s * ux(a), xe0 + s * ux(e)],
1115
+ [ya0 + s * uy(a), ye0 + s * uy(e)],
1116
+ [za0 + s * uz(a), ze0 + s * uz(e)],
1117
+ )
1118
+ # Knotentexte mit skalieren
1119
+ if node_labels:
1120
+ for n, txt in enumerate(text_elems, start=1):
1121
+ x0, y0, z0 = (
1122
+ self.Inp.nodes[k][n - 1] for k in ("x[m]", "y[m]", "z[m]")
1123
+ )
1124
+ txt.set_position((x0 + s * ux(n), y0 + s * uy(n)))
1125
+ txt.set_3d_properties(z0 + s * uz(n), zdir="z")
1126
+ # Limits neu auf Würfel setzen
1127
+ try:
1128
+ ax.set_box_aspect((1, 1, 1))
1129
+ except AttributeError:
1130
+ _set_axes_equal(ax)
1131
+ fig.canvas.draw_idle()
1132
+
1133
+ # Slider verbinden
1134
+ s_elev.on_changed(_update_view)
1135
+ s_azim.on_changed(_update_view)
1136
+ s_scale.on_changed(_update_scale)
1137
+
1138
+ # Startansicht
1139
+ _update_view(None)
1140
+ _update_scale(None)
1141
+
1142
+ return fig, ax, (s_elev, s_azim, s_scale)
1143
+
1144
+ def orthogonal_unit_vector_3d(self,pt_i, pt_j, prefer_axis="y"):
1145
+ """
1146
+ Liefert einen normierten Vektor w, der senkrecht auf der
1147
+ Stabachse v = (pt_j - pt_i) steht.
1148
+
1149
+ Parameters
1150
+ ----------
1151
+ pt_i, pt_j : array-like (3,)
1152
+ Globale xyz-Koordinaten der Knoten i und j.
1153
+ prefer_axis : {"x","y","z"}, optional
1154
+ Welcher globale Achsvektor soll primär verwendet werden,
1155
+ um das Kreuzprodukt zu bilden? Wähle eine Achse, die
1156
+ im Regelfall nicht parallel zur Elementachse v ist.
1157
+
1158
+ Returns
1159
+ -------
1160
+ w : ndarray (3,)
1161
+ Normierter Orthogonalvektor.
1162
+ """
1163
+ v = np.asarray(pt_j) - np.asarray(pt_i)
1164
+ v_norm = np.linalg.norm(v)
1165
+ if v_norm == 0:
1166
+ raise ValueError("Elementlänge 0 – identische Knoten?")
1167
+
1168
+ v = v / v_norm
1169
+
1170
+ # Globalen Hilfsvektor wählen
1171
+ axes = {"x": np.array([1, 0, 0]),
1172
+ "y": np.array([0, 1, 0]),
1173
+ "z": np.array([0, 0, 1])}
1174
+ u = axes.get(prefer_axis, axes["y"])
1175
+
1176
+ # Prüfen, ob v und u (fast) parallel sind
1177
+ if abs(np.dot(v, u)) > 0.95: # nahezu kollinear
1178
+ u = axes["z"] if prefer_axis != "z" else axes["x"]
1179
+
1180
+ w = np.cross(v, u)
1181
+ w_norm = np.linalg.norm(w)
1182
+ if w_norm == 0:
1183
+ raise ValueError("Konnte keinen Orthogonal­vektor berechnen.")
1184
+ return w / w_norm
1185
+
1186
+ def plot_moment_my_3d(self,ax, x0, y0, z0, m_val, w, scale=1.0,
1187
+ color_pos="tab:blue", color_neg="tab:red",
1188
+ text=True):
1189
+ """
1190
+ Zeichnet einen Momentenpfeil (My) im 3-D-Plot.
1191
+
1192
+ Parameter
1193
+ ---------
1194
+ ax : mpl_toolkits.mplot3d.Axes3D
1195
+ x0,y0,z0 : float
1196
+ Koordinaten des Ausgangsknotens (global).
1197
+ m_val : float
1198
+ Momentwert (positiv / negativ gemäß System).
1199
+ w : ndarray (3,)
1200
+ Normierter Orthogonalvektor (Ausgabe der o.g. Funktion).
1201
+ scale : float
1202
+ Skalierungs­faktor für die Pfeillänge.
1203
+ """
1204
+ if m_val == 0:
1205
+ return None # nichts zu plotten
1206
+
1207
+ # Farbe
1208
+ col = color_pos if m_val >= 0 else color_neg
1209
+
1210
+ # kleine Verbindungslinie vom Knoten nach außen
1211
+ link_len = 0.05 * scale
1212
+ P1 = np.array([x0, y0, z0])
1213
+ P2 = P1 + link_len * w * abs(m_val)
1214
+
1215
+ # Pfeil
1216
+ vec = scale * m_val * w
1217
+ Q = P2 + vec
1218
+
1219
+ # Plotten
1220
+ ax.plot([P1[0], P2[0]], [P1[1], P2[1]], [P1[2], P2[2]], color=col, lw=1)
1221
+ ax.quiver(P2[0], P2[1], P2[2],
1222
+ vec[0], vec[1], vec[2],
1223
+ arrow_length_ratio=0.15, color=col, linewidth=1)
1224
+
1225
+ # Text
1226
+ if text:
1227
+ txt = f"My={m_val:.2f}"
1228
+ ax.text(Q[0], Q[1], Q[2], txt, fontsize=8, color=col)
1229
+
1230
+ return (P1, P2, Q) # falls du später noch updaten willst
1231
+
1232
+ def plot_My_3d(
1233
+ self,
1234
+ scale: float = 1,
1235
+ show_axes: bool = True,
1236
+ show_stabs: bool = True,
1237
+ text: bool = True,
1238
+ prefer_axis: str = "y",
1239
+ ):
1240
+ """
1241
+ Erstellt EINEN separaten 3-D-Plot, in dem ausschließlich die
1242
+ My-Momente (Biegung um lokale y-Achse) dargestellt werden.
1243
+
1244
+ Parameter
1245
+ ---------
1246
+ scale : float globaler Faktor für die Pfeillängen (Moment * scale)
1247
+ show_axes : bool Achsenbeschriftungen + Würfelanzeige?
1248
+ show_stabs : bool graue Stabachsen als Anhalt zeichnen?
1249
+ text : bool Momentwert als Text an Pfeilspitze?
1250
+ prefer_axis : {"x","y","z"} welche globale Achse zur Bildung
1251
+ des Orthogonal­vektors primär verwenden?
1252
+ """
1253
+ fig = plt.figure(figsize=(8, 6))
1254
+ ax = fig.add_subplot(projection="3d")
1255
+ ax.set_title("My-Momente 3-D")
1256
+
1257
+ na, ne = self.Inp.members["na"], self.Inp.members["ne"]
1258
+
1259
+ # ----- 1. optional: Stabachsen als helle Linien --------------------
1260
+ if show_stabs:
1261
+ segs = []
1262
+ for a, e in zip(na, ne):
1263
+ Pi = [self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]")]
1264
+ Pj = [self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]")]
1265
+ segs.append([Pi, Pj])
1266
+ lc = Line3DCollection(segs, colors="lightgray", linewidths=1, zorder=0)
1267
+ ax.add_collection3d(lc)
1268
+
1269
+ # ----- 2. Momente plotten ------------------------------------------
1270
+ for idx, (a, e) in enumerate(zip(na, ne)):
1271
+ Pi = np.array([self.Inp.nodes[k][a - 1] for k in ("x[m]", "y[m]", "z[m]")])
1272
+ Pj = np.array([self.Inp.nodes[k][e - 1] for k in ("x[m]", "y[m]", "z[m]")])
1273
+
1274
+ w = self.orthogonal_unit_vector_3d(Pi, Pj, prefer_axis=prefer_axis)
1275
+
1276
+ My_a = float(self.MY_el_i_store[idx, 0, 0])
1277
+ My_b = float(self.MY_el_i_store[idx, 1, 0])
1278
+
1279
+ self.plot_moment_my_3d(ax, *Pi, My_a, w, scale=scale, text=text)
1280
+ self.plot_moment_my_3d(ax, *Pj, My_b, w, scale=scale, text=text)
1281
+
1282
+ # ----- 3. Optik -----------------------------------------------------
1283
+ if show_axes:
1284
+ ax.set_xlabel("X [m]")
1285
+ ax.set_ylabel("Y [m]")
1286
+ ax.set_zlabel("Z [m]")
1287
+ else: # Achsen ausblenden
1288
+ ax.set_axis_off()
1289
+
1290
+ # kubische Limits
1291
+ _set_axes_equal(ax)
1292
+ fig.tight_layout()
1293
+
1294
+ return fig, ax
1295
+
1296
+ def plot_My_3d_interactive(
1297
+ self,
1298
+ scale_init: float = 1e-3,
1299
+ show_axes: bool = True,
1300
+ show_stabs: bool = True,
1301
+ text: bool = True,
1302
+ prefer_axis: str = "y",
1303
+ ):
1304
+ """
1305
+ Interaktive 3-D-Darstellung der My-Momente mit drei Slidern:
1306
+ • Elevation (θ) • Azimut (φ) • Skalierung s
1307
+ """
1308
+ import matplotlib.pyplot as plt
1309
+
1310
+ # ---------- Grundplot zeichnen -------------------------------------
1311
+ fig, ax = self.plot_My_3d(
1312
+ scale=scale_init,
1313
+ show_axes=show_axes,
1314
+ show_stabs=show_stabs,
1315
+ text=text,
1316
+ prefer_axis=prefer_axis,
1317
+ )
1318
+
1319
+ # ── alle momentan gezeichneten Artists merken (fürs Löschen) ───────
1320
+ base_children = set(ax.get_children()) # alles, was NICHT neu erzeugt wird
1321
+
1322
+ # ---------- UI-Platz reservieren -----------------------------------
1323
+ fig.subplots_adjust(bottom=0.25)
1324
+
1325
+ ax_elev = fig.add_axes([0.14, 0.15, 0.72, 0.03])
1326
+ ax_azim = fig.add_axes([0.14, 0.10, 0.72, 0.03])
1327
+ ax_scale = fig.add_axes([0.14, 0.05, 0.72, 0.03])
1328
+
1329
+ s_elev = Slider(ax_elev, "Elev (°)", -90, 90, valinit=20, valstep=1)
1330
+ s_azim = Slider(ax_azim, "Azim (°)", -180, 180, valinit=-60, valstep=1)
1331
+ s_scale = Slider(ax_scale, "Scale", 0.0, scale_init*100, valinit=scale_init)
1332
+
1333
+ # ---------- Helfer: Momente neu zeichnen ---------------------------
1334
+ def _redraw_moments(scale):
1335
+ # 1) alles löschen, was nach dem Basisset erzeugt wurde
1336
+ for art in list(ax.get_children()):
1337
+ if art not in base_children:
1338
+ art.remove()
1339
+
1340
+ # 2) neu zeichnen (ohne Achsen neu zu beschriften)
1341
+ na, ne = self.Inp.members["na"], self.Inp.members["ne"]
1342
+ for idx, (a, e) in enumerate(zip(na, ne)):
1343
+ Pi = np.array([self.Inp.nodes[k][a - 1] for k in ("x[m]","y[m]","z[m]")])
1344
+ Pj = np.array([self.Inp.nodes[k][e - 1] for k in ("x[m]","y[m]","z[m]")])
1345
+
1346
+ w = self.orthogonal_unit_vector_3d(Pi, Pj, prefer_axis=prefer_axis)
1347
+ My_a = float(self.MY_el_i_store[idx, 0, 0])
1348
+ My_b = float(self.MY_el_i_store[idx, 1, 0])
1349
+
1350
+ self.plot_moment_my_3d(ax, *Pi, My_a, w, scale=scale, text=text)
1351
+ self.plot_moment_my_3d(ax, *Pj, My_b, w, scale=scale, text=text)
1352
+
1353
+ _set_axes_equal(ax)
1354
+ fig.canvas.draw_idle()
1355
+
1356
+ # ---------- Callbacks ----------------------------------------------
1357
+ def _update_view(_):
1358
+ ax.view_init(elev=s_elev.val, azim=s_azim.val)
1359
+ fig.canvas.draw_idle()
1360
+
1361
+ def _update_scale(_):
1362
+ _redraw_moments(s_scale.val)
1363
+
1364
+ s_elev.on_changed(_update_view)
1365
+ s_azim.on_changed(_update_view)
1366
+ s_scale.on_changed(_update_scale)
1367
+
1368
+ # ---------- Initiale Ansicht ---------------------------------------
1369
+ _update_view(None) # stellt Kamera
1370
+ # (Momenten-Plot ist schon mit scale_init gezeichnet)
1371
+
1372
+ return fig, ax, (s_elev, s_azim, s_scale)
1373
+
1374
+ # Beispielhafte Hauptfunktion
1375
+ if __name__ == "__main__":
1376
+ import matplotlib
1377
+ # Für Desktop-Skripte (nicht im Notebook) ein GUI-Backend wählen:
1378
+ matplotlib.use("TkAgg") # oder "QtAgg"
1379
+ import matplotlib.pyplot as plt
1380
+
1381
+ # ── Instanz erzeugen (rechnet nur einmal) ───────────────────────────
1382
+ calc = mainloop()
1383
+
1384
+ # 2-D-Momente --------------------------------------------------------
1385
+ fig1, ax1 = calc.plot_structure_with_moments_2d(scale=5)
1386
+ ax1.invert_yaxis()
1387
+ fig1.savefig("plots/ClampHinged_Inclination.eps", format="eps")
1388
+
1389
+ # 2-D-Normalkräfte ---------------------------------------------------
1390
+ fig2, ax2 = calc.plot_structure_with_normalforces_2d(scale=5)
1391
+ ax2.invert_yaxis()
1392
+
1393
+ # 3-D-Plot mit Slidern ----------------------------------------------
1394
+ fig3, ax3, sliders = calc.plot_structure_deformed_3d_interactive(
1395
+ scale_init=10,
1396
+ node_labels=True,
1397
+ )
1398
+
1399
+ calc.plot_My_3d_interactive(scale_init=1e-3, text=True)
1400
+
1401
+ # ── genau EIN Aufruf ────────────────────────────────────────────────
1402
+ plt.show() # Fenster offen → hier läuft die Event-Schleife
1403
+ # Fenster zu → show() kehrt zurück → Script endet