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
@@ -1,5 +1,14 @@
1
- from Steifigkeitsmatrix import *
2
- from InputData import Input
1
+ # STABRAUM-Dependencies
2
+ try:
3
+ from Steifigkeitsmatrix import *
4
+ from InputData import Input
5
+ from results import AnalysisResults
6
+ except:
7
+ from KIB_LAP.STABRAUM.Steifigkeitsmatrix import *
8
+ from KIB_LAP.STABRAUM.InputData import Input
9
+ from KIB_LAP.STABRAUM.results import AnalysisResults
10
+
11
+
3
12
  import sympy as sp
4
13
 
5
14
  import matplotlib.pyplot as plt
@@ -10,6 +19,7 @@ import matplotlib.patches as mpatches
10
19
  from matplotlib.widgets import Slider
11
20
  from mpl_toolkits.mplot3d.art3d import Line3DCollection
12
21
 
22
+
13
23
  def _set_axes_equal(ax, extra: float = 0.0):
14
24
  """
15
25
  Erzwingt identische numerische Achsenlimits.
@@ -64,8 +74,96 @@ class mainloop:
64
74
  self.MTS_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
65
75
  self.N_el_i_store = np.zeros((len(self.Inp.members), 2, 1))
66
76
 
67
- ##_____ Class Functions____________##
68
- self.MainConvergence()
77
+ # ------------------------------------------------------------
78
+ # DEBUG / SINGULARITÄTSTEST
79
+ # ------------------------------------------------------------
80
+ def check_singularity(self, K=None, tol_row=1e-10, tol_rank=1e-8, verbose=True):
81
+ """
82
+ Prüft typische Ursachen für Singularität:
83
+ - Rank-Defizit
84
+ - Nullzeilen/Nullspalten (DOFs ohne Steifigkeit)
85
+ - isolierte Knoten (nicht in members)
86
+ - 0-Längen-Elemente
87
+ Gibt ein Dict mit Diagnosen zurück.
88
+ """
89
+ if K is None:
90
+ K = self.GesMat
91
+
92
+ info = {}
93
+
94
+ # -------- Rank --------
95
+ try:
96
+ r = np.linalg.matrix_rank(K, tol=tol_rank)
97
+ except TypeError:
98
+ # falls numpy Version keine tol als kw hat
99
+ r = np.linalg.matrix_rank(K)
100
+ n = K.shape[0]
101
+ info["rank"] = int(r)
102
+ info["n"] = int(n)
103
+ info["deficit"] = int(n - r)
104
+
105
+ # -------- Nullzeilen/Nullspalten --------
106
+ row_norm = np.linalg.norm(K, axis=1)
107
+ col_norm = np.linalg.norm(K, axis=0)
108
+ zero_rows = np.where(row_norm < tol_row)[0]
109
+ zero_cols = np.where(col_norm < tol_row)[0]
110
+
111
+ info["zero_rows"] = zero_rows.tolist()
112
+ info["zero_cols"] = zero_cols.tolist()
113
+
114
+ # -------- DOF Mapping (nur wenn 7 dof/node) --------
115
+ def gdof_to_node_ldof(gdof):
116
+ return int(gdof // 7 + 1), int(gdof % 7)
117
+
118
+ info["zero_rows_nodes"] = [gdof_to_node_ldof(i) for i in zero_rows]
119
+ info["zero_cols_nodes"] = [gdof_to_node_ldof(i) for i in zero_cols]
120
+
121
+ # -------- isolierte Knoten --------
122
+ try:
123
+ na = list(self.Inp.members["na"])
124
+ ne = list(self.Inp.members["ne"])
125
+ used = set(na) | set(ne)
126
+ all_nodes = set(range(1, len(self.Inp.nodes["x[m]"]) + 1))
127
+ isolated = sorted(all_nodes - used)
128
+ except Exception:
129
+ isolated = []
130
+ info["isolated_nodes"] = isolated
131
+
132
+ # -------- 0-Längen-Elemente --------
133
+ zero_len_elems = []
134
+ try:
135
+ for eidx, (a, e) in enumerate(
136
+ zip(self.Inp.members["na"], self.Inp.members["ne"]), start=1
137
+ ):
138
+ xa = self.Inp.nodes["x[m]"][a - 1]
139
+ ya = self.Inp.nodes["y[m]"][a - 1]
140
+ za = self.Inp.nodes["z[m]"][a - 1]
141
+ xb = self.Inp.nodes["x[m]"][e - 1]
142
+ yb = self.Inp.nodes["y[m]"][e - 1]
143
+ zb = self.Inp.nodes["z[m]"][e - 1]
144
+ L = float(np.sqrt((xa - xb) ** 2 + (ya - yb) ** 2 + (za - zb) ** 2))
145
+ if L < 1e-12:
146
+ zero_len_elems.append((eidx, int(a), int(e)))
147
+ except Exception:
148
+ pass
149
+ info["zero_length_elements"] = zero_len_elems
150
+
151
+ # -------- Ausgabe --------
152
+ if verbose:
153
+ print("\n=== Singularity check ===")
154
+ print(f"size n = {n}, rank = {r}, deficit = {n-r}")
155
+ print(f"zero rows: {len(zero_rows)}, zero cols: {len(zero_cols)}")
156
+ if len(zero_rows) > 0:
157
+ print("first zero rows (gdof -> node,ldof):")
158
+ for gdof in zero_rows[:30]:
159
+ node, ldof = gdof_to_node_ldof(gdof)
160
+ print(f" gdof {gdof:4d} -> node {node}, ldof {ldof}")
161
+ if len(isolated) > 0:
162
+ print("isolated nodes:", isolated)
163
+ if len(zero_len_elems) > 0:
164
+ print("zero-length elements (eid, na, ne):", zero_len_elems)
165
+
166
+ return info
69
167
 
70
168
  def CalculateTransMat(self):
71
169
  print("Calculate Transmatrices")
@@ -185,7 +283,7 @@ class mainloop:
185
283
  )
186
284
 
187
285
  K_el_i = np.matmul(
188
- self.TransMats[i].T, np.matmul(K_el_i, self.TransMats[i])
286
+ self.TransMats[i], np.matmul(K_el_i, self.TransMats[i].T)
189
287
  )
190
288
 
191
289
  self.K_el_i_store[i] = K_el_i
@@ -236,174 +334,124 @@ class mainloop:
236
334
 
237
335
  def LocalLoadVectorLine(self):
238
336
  """
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
337
+ Lokale Festendkräfte aus Linienlasten (lokales System!).
338
+ Speichert in self.S_loc_elem_line und gibt dieses Array zurück.
246
339
  """
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)
340
+ n_elem = len(self.Inp.members["na"])
341
+ self.S_loc_elem_line = np.zeros((14, n_elem), dtype=float)
251
342
 
252
343
  res_mbr = self.Inp.ElementLoads["Member"]
253
344
  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
- )
345
+ res_line_b = self.Inp.ElementLoads["qze"] # aktuell nicht benutzt
274
346
 
275
- S_loc = self.S_loc_elem_line[:, j]
347
+ for k in range(len(res_mbr)):
348
+ e = int(res_mbr[k] - 1)
276
349
 
277
- # global line loadings
350
+ L = float(self.member_length[e])
351
+ q = float(res_line_a[k]) # TODO: falls qza != qze -> konsistente Formeln
278
352
 
279
- S_glob_elem[:, j] = np.matmul(np.transpose(TransMatj), S_loc)
353
+ # Lokale Festendkräfte (deine Konvention beibehalten)
354
+ # Vz
355
+ self.S_loc_elem_line[3, e] += +q * L / 2.0
356
+ self.S_loc_elem_line[10, e] += +q * L / 2.0
280
357
 
281
- return S_glob_elem
358
+ # My
359
+ self.S_loc_elem_line[4, e] += -q * L**2 / 12.0
360
+ self.S_loc_elem_line[11, e] += +q * L**2 / 12.0
361
+
362
+ return self.S_loc_elem_line
282
363
 
283
364
  def LocalLoadVectorTemp(self):
284
365
  """
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
366
+ Lokale Festendkräfte aus Temperatur (lokales System!).
367
+ Speichert in self.S_loc_elem_temp und gibt dieses Array zurück.
292
368
  """
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
369
+ n_elem = len(self.Inp.members["na"])
370
+ self.S_loc_elem_temp = np.zeros((14, n_elem), dtype=float)
297
371
 
298
372
  res_mbr = self.Inp.TemperatureForces["Member"]
299
-
300
373
  res_tem_dT = self.Inp.TemperatureForces["dT[K]"]
301
374
  res_tem_dTz = self.Inp.TemperatureForces["dTz[K]"]
302
375
  res_tem_dTy = self.Inp.TemperatureForces["dTy[K]"]
303
376
 
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
- )
377
+ for k in range(len(res_mbr)):
378
+ e = int(res_mbr[k] - 1)
314
379
 
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
- )
380
+ EA = self.ElemStem.E * self.ElemStem.A
381
+ EIz = self.ElemStem.E * self.ElemStem.I_z
382
+ EIy = self.ElemStem.E * self.ElemStem.I_y
321
383
 
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
- )
384
+ dT = float(res_tem_dT[k])
385
+ dTz = float(res_tem_dTz[k])
386
+ dTy = float(res_tem_dTy[k])
328
387
 
329
- S_loc = self.S_loc_elem_temp[:, j]
388
+ # N
389
+ self.S_loc_elem_temp[0, e] += -EA * 1e-5 * dT
390
+ self.S_loc_elem_temp[7, e] += +EA * 1e-5 * dT
330
391
 
331
- # global temperature loadings
392
+ # Mz
393
+ self.S_loc_elem_temp[2, e] += -EIz * 1e-5 * dTy
394
+ self.S_loc_elem_temp[9, e] += +EIz * 1e-5 * dTy
332
395
 
333
- S_glob_elem[:, j] = np.matmul(np.transpose(TransMatj), S_loc)
396
+ # My
397
+ self.S_loc_elem_temp[4, e] += -EIy * 1e-5 * dTz
398
+ self.S_loc_elem_temp[11, e] += +EIy * 1e-5 * dTz
334
399
 
335
- return S_glob_elem
400
+ return self.S_loc_elem_temp
336
401
 
337
402
  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)
403
+ """
404
+ Globaler Lastvektor:
405
+ - Element-Festendkräfte (Temp + Linienlast) lokal -> global via T
406
+ - Knotenlasten (global) addieren
407
+ """
408
+ # erzeugt/updated self.S_loc_elem_temp / self.S_loc_elem_line
409
+ self.LocalLoadVectorTemp()
410
+ self.LocalLoadVectorLine()
346
411
 
347
- F_glob = np.zeros(self.Inp.nDoF)
412
+ n_elem = len(self.Inp.members["na"])
413
+ F_glob = np.zeros(self.Inp.nDoF, dtype=float)
348
414
 
349
415
  na_memb = self.Inp.members["na"]
350
416
  ne_memb = self.Inp.members["ne"]
351
417
 
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]
418
+ for e in range(n_elem):
419
+ node_i = int(na_memb[e])
420
+ node_j = int(ne_memb[e])
356
421
 
357
- dof_Na = 7 * (node_i - 1)
358
- dof_Ne = 7 * (node_j - 1)
422
+ base_i = 7 * (node_i - 1)
423
+ base_j = 7 * (node_j - 1)
359
424
 
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]
425
+ F_e_loc = (self.S_loc_elem_temp[:, e] + self.S_loc_elem_line[:, e]).reshape(
426
+ 14, 1
427
+ )
380
428
 
381
- F_glob[dof_Mza] += F_loc_temp[2, i]
382
- F_glob[dof_Mze] += F_loc_temp[9, i]
429
+ T = self.TransMats[e] # local -> global
430
+ F_e_glob = (T @ F_e_loc).ravel() # (14,)
383
431
 
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]
432
+ F_glob[base_i : base_i + 7] += F_e_glob[0:7]
433
+ F_glob[base_j : base_j + 7] += F_e_glob[7:14]
386
434
 
387
- # Input of Nodal Forces
435
+ # Knotenlasten (global)
388
436
  res_nodes = self.Inp.NodalForces["Node"]
389
437
  res_dof = self.Inp.NodalForces["Dof"]
390
438
  res_forc = self.Inp.NodalForces["Value[MN/MNm]"]
391
439
 
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]
440
+ for k in range(len(res_nodes)):
441
+ node = int(res_nodes[k])
442
+ base = 7 * (node - 1)
443
+ dof = str(res_dof[k]).strip().lower()
444
+
445
+ if dof == "fx":
446
+ idx = base + 0
447
+ elif dof == "fy":
448
+ idx = base + 1
449
+ elif dof == "fz":
450
+ idx = base + 3
451
+ else:
452
+ continue
404
453
 
405
- print("Force vector ", F_glob)
406
- print("Local force vectors ", self.S_loc_elem_temp)
454
+ F_glob[idx] += float(res_forc[k])
407
455
 
408
456
  return F_glob
409
457
 
@@ -431,973 +479,417 @@ class mainloop:
431
479
  return u_el
432
480
 
433
481
  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
482
  """
503
- Plot the undeformed 3D structure.
483
+ Berechnet lokale Schnittgrößen an linkem und rechtem Schnittufer:
504
484
 
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]
485
+ - f_glob = K_el_global @ u_el_global (wie bei dir gespeichert)
486
+ - f_loc = T.T @ f_glob (global -> local, da T local->global)
487
+ - f_eff = f_loc - (f0_temp_loc + f0_line_loc)
524
488
 
525
- X = [ix, jx]
526
- Y = [iy, jy]
527
- Z = [iz, jz]
489
+ Konvention:
490
+ - Linkes Schnittufer: s_L = - f_I,loc
491
+ - Rechtes Schnittufer: s_R = + f_J,loc
492
+ - Zug positiv
528
493
 
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):
494
+ Ergebnis in self.*_el_i_store[i] jeweils als (2,1): [links; rechts]
540
495
  """
541
- Berechnet einen normierten orthogonalen Vektor zur Strukturlinie von (xi, zi) nach (xj, zj)
542
- mittels Kreuzprodukt.
543
496
 
544
- Parameter:
545
- - xi, zi: Koordinaten des Startknotens
546
- - xj, zj: Koordinaten des Endknotens
497
+ n_elem = len(self.Inp.members["na"])
498
+ s_el = np.zeros((14, n_elem), dtype=float)
547
499
 
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])
500
+ # Hole (lokale) Festendkräfte. Falls du die Arrays noch nicht als Attribute speicherst,
501
+ # werden sie hier neu erzeugt.
502
+ F_loc_temp = self.LocalLoadVectorTemp()
503
+ F_loc_line = self.LocalLoadVectorLine()
554
504
 
555
- # Einheitsvektor entlang der y-Achse
556
- y_unit = np.array([0, 1, 0])
505
+ for e in range(n_elem):
557
506
 
558
- # Kreuzprodukt berechnen
559
- perp_vector = np.cross(v, y_unit) # Ergebnis ist ebenfalls ein 3D-Vektor
507
+ # 1) Element-Endkräfte aus K*u (im Koordinatensystem von K_el_i_store & u_el)
508
+ f_glob = (self.K_el_i_store[e] @ self.u_el[:, e]).reshape(14, 1)
509
+ s_el[:, e] = f_glob.ravel()
560
510
 
561
- # Extrahiere die x und z Komponenten
562
- perp_vector_2d = perp_vector[[0, 2]]
511
+ # 2) global -> local
512
+ T = self.TransMats[e] # local -> global
513
+ f_loc = T.T @ f_glob # global -> local
563
514
 
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
- )
515
+ # 3) lokale Festendkräfte (Temp + Linienlast)
516
+ f0_loc = (F_loc_temp[:, e] + F_loc_line[:, e]).reshape(14, 1)
570
517
 
571
- unit_vector = perp_vector_2d / norm
518
+ # 4) wirksame lokale Endkräfte
519
+ f_eff = f_loc - f0_loc
572
520
 
573
- # Für positives und negatives My
574
- unit_vector_pos = unit_vector
575
- unit_vector_neg = -unit_vector
521
+ # 5) Schnittufer-Abbildung: links = -f_I, rechts = +f_J
522
+ # Indizes laut Layout:
523
+ # [0] Na [1] Vya [2] Mza [3] Vza [4] Mya [5] Mxa [6] Mwa
524
+ # [7] Nb [8] Vyb [9] Mzb [10]Vzb [11]Myb [12]Mxb [13]Mwb
576
525
 
577
- return unit_vector_pos, unit_vector_neg
526
+ N_L, N_R = -f_eff[0, 0], f_eff[7, 0]
527
+ Vy_L, Vy_R = -f_eff[1, 0], f_eff[8, 0]
528
+ Mz_L, Mz_R = -f_eff[2, 0], f_eff[9, 0]
529
+ Vz_L, Vz_R = -f_eff[3, 0], f_eff[10, 0]
530
+ My_L, My_R = -f_eff[4, 0], f_eff[11, 0]
531
+ Mx_L, Mx_R = -f_eff[5, 0], f_eff[12, 0]
578
532
 
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
- )
533
+ self.N_el_i_store[e] = np.array([N_L, N_R]).reshape(2, 1)
534
+ self.VY_el_i_store[e] = np.array([Vy_L, Vy_R]).reshape(2, 1)
535
+ self.VZ_el_i_store[e] = np.array([Vz_L, Vz_R]).reshape(2, 1)
636
536
 
637
- return conn_x, conn_z
537
+ self.MZ_el_i_store[e] = np.array([Mz_L, Mz_R]).reshape(2, 1)
538
+ self.MY_el_i_store[e] = np.array([My_L, My_R]).reshape(2, 1)
539
+ self.MX_el_i_store[e] = np.array([Mx_L, Mx_R]).reshape(2, 1)
638
540
 
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
541
+ return s_el
676
542
 
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):
543
+ def SpringsData(self):
702
544
  """
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
545
+ Baut Koppel-Federn zwischen zwei Knoten in die globale Matrix self.GesMat ein.
546
+ CSV: node_a,node_e,dof,cp/cm[MN,m]
547
+ Annahmen:
548
+ - node_* ist 1-basiert
549
+ - dof ist 0-basiert (0..6)
550
+ - 7 DoF pro Knoten
716
551
  """
552
+ # Wenn Input die Datei nicht geladen hat: nichts tun
553
+ if not hasattr(self.Inp, "SpringsData"):
554
+ return
555
+ if self.Inp.SpringsData is None or len(self.Inp.SpringsData) == 0:
556
+ return
717
557
 
718
- if N_local == 0:
719
- return # Keine Kraft zu plotten
558
+ for _, row in self.Inp.SpringsData.iterrows():
559
+ na = int(row["node_a"])
560
+ ne = int(row["node_e"])
561
+ dof = int(row["dof"])
562
+ k = float(row["cp[MN]"])
720
563
 
721
- # Kleine Verbindungslinie vom Knoten zum Start des Pfeils
722
- connection_length = 0.05 * scale # Anpassbarer Wert
564
+ ia = 7 * (na - 1) + dof
565
+ ie = 7 * (ne - 1) + dof
723
566
 
724
- conn_x = xi + connection_length * unit_vector[0] * N_local * scale
725
- conn_z = zi + connection_length * unit_vector[1] * N_local * scale
567
+ # 2x2 Feder-Block
568
+ self.GesMat[ia, ia] += k
569
+ self.GesMat[ia, ie] -= k
570
+ self.GesMat[ie, ia] -= k
571
+ self.GesMat[ie, ie] += k
726
572
 
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
573
 
732
- # Berechnung des Vektors, skaliert durch Moment und Maßstabsfaktor
733
- vec = scale * N_local * unit_vector
734
574
 
735
- # Farbwahl basierend auf dem Vorzeichen des Moments
736
- color = "blue" if N_local >= 0 else "red"
575
+ def MainConvergence(self):
737
576
 
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)
577
+ self.TransMats = self.CalculateTransMat()
578
+ self.GesMat = self.BuildStructureStiffnessMatrix()
742
579
 
743
- # Berechnung des Endpunkts für die Textplatzierung
744
- x_end = conn_x
745
- z_end = conn_z
580
+ self.RestraintData()
581
+ self.SpringsData() # HIER
582
+ # <<< HIER: Singularitäts-Check >>>
583
+ self.check_singularity(self.GesMat, verbose=True)
746
584
 
747
- # Moment-Text
748
- moment_text = f"N = {N_local:.3f} MNm"
585
+ self.FGes = self.GlobalLoadVector()
749
586
 
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
- )
587
+ self.u_ges = self.SolveDisplacement()
759
588
 
760
- return conn_x, conn_z
589
+ self.u_el = self.StoreLocalDisplacements()
761
590
 
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
591
+ self.s_el = self.CalculateLocalInnerForces()
799
592
 
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):
593
+ def run(self) -> AnalysisResults:
825
594
  """
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
595
+ Führt die komplette Berechnung aus und gibt ein Results-Objekt zurück.
831
596
  """
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]
597
+ self.MainConvergence()
838
598
 
839
- ix = nodes["x[m]"][node_i - 1]
840
- iz = nodes["z[m]"][node_i - 1]
599
+ return AnalysisResults(
600
+ Inp=self.Inp,
601
+ u_ges=self.u_ges,
602
+ GesMat=self.GesMat,
603
+ FGes=self.FGes,
604
+ TransMats=self.TransMats,
605
+ K_el_i_store=self.K_el_i_store,
606
+ u_el=self.u_el,
607
+ s_el=self.s_el,
608
+ N_el_i_store=self.N_el_i_store,
609
+ VY_el_i_store=self.VY_el_i_store,
610
+ VZ_el_i_store=self.VZ_el_i_store,
611
+ MX_el_i_store=self.MX_el_i_store,
612
+ MY_el_i_store=self.MY_el_i_store,
613
+ MZ_el_i_store=self.MZ_el_i_store,
614
+ member_length=np.array(self.member_length, dtype=float),
615
+ )
841
616
 
842
- jx = nodes["x[m]"][node_j - 1]
843
- jz = nodes["z[m]"][node_j - 1]
617
+ def check_global_equilibrium(self):
618
+ r = (
619
+ self.GesMat @ self.u_ges - self.FGes
620
+ ).ravel() # Reaktionsvektor auf allen DOFs
844
621
 
845
- X = [ix, jx]
846
- Z = [iz, jz]
622
+ # welche DOFs sind "gelagert"? (klassisch: Cp groß oder Fix)
623
+ # Bei dir: RestraintData addiert Cp auf Diagonale.
624
+ # -> als Näherung: DOF ist gelagert, wenn Cp > 0 in Input.
625
+ restrained = np.zeros_like(r, dtype=bool)
626
+ for _, row in self.Inp.RestraintData.iterrows():
627
+ gdof = 7 * (int(row["Node"]) - 1) + int(row["Dof"])
628
+ restrained[gdof] = True
847
629
 
848
- ax.plot(X, Z, color="black", lw=1.0)
630
+ free = ~restrained
849
631
 
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)
632
+ print("max residual on FREE dofs:", np.max(np.abs(r[free])))
633
+ print("sum residual on FREE dofs:", np.sum(r[free]))
855
634
 
856
- return fig, ax
635
+ def debug_single_fx(self, node=2, Fx=1.0):
636
+ # setze alle Lasten leer
637
+ self.Inp.ElementLoads = self.Inp.ElementLoads.iloc[0:0].copy()
638
+ self.Inp.TemperatureForces = self.Inp.TemperatureForces.iloc[0:0].copy()
639
+ self.Inp.NodalForces = self.Inp.NodalForces.iloc[0:0].copy()
857
640
 
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).
641
+ # eine Last
642
+ import pandas as pd
861
643
 
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"]
644
+ self.Inp.NodalForces = pd.DataFrame(
645
+ [{"Node": node, "Dof": "Fx", "Value[MN/MNm]": Fx}]
868
646
  )
869
647
 
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
- )
648
+ self.MainConvergence()
879
649
 
880
- # Achsenlimits automatisch anpassen und gleiches Seitenverhältnis
881
- ax.set_aspect("equal", adjustable="datalim")
882
- ax.relim()
883
- ax.autoscale_view()
650
+ ux = self.u_ges[7 * (node - 1) + 0]
651
+ uy = self.u_ges[7 * (node - 1) + 1]
652
+ uz = self.u_ges[7 * (node - 1) + 3]
653
+ print("ux, uy, uz =", ux, uy, uz)
654
+
655
+ def sum_reactions_fx(self):
656
+ r = (self.GesMat @ self.u_ges - self.FGes).ravel()
657
+
658
+ Rx = 0.0
659
+ for _, row in self.Inp.RestraintData.iterrows():
660
+ node = int(row["Node"])
661
+ dof = int(row["Dof"])
662
+ if dof == 0: # ux-DOF
663
+ Rx += r[7 * (node - 1) + 0]
664
+
665
+ Fx = 0.0
666
+ for _, row in self.Inp.NodalForces.iterrows():
667
+ if str(row["Dof"]).strip().lower() == "fx":
668
+ Fx += float(row["Value[MN/MNm]"])
669
+
670
+ print("Sum Fx (applied) =", Fx)
671
+ print("Sum Rx (support) =", Rx)
672
+ print("Fx + Rx =", Fx + Rx)
673
+
674
+ def sum_spring_reactions_fx(self):
675
+ u = self.u_ges.ravel()
676
+
677
+ Rx = 0.0
678
+ Fx = 0.0
679
+
680
+ # aufgebrachte nodale Fx
681
+ for _, row in self.Inp.NodalForces.iterrows():
682
+ if str(row["Dof"]).strip().lower() == "fx":
683
+ Fx += float(row["Value[MN/MNm]"])
684
+
685
+ # Federkräfte aus RestraintData: k * u an genau diesen DOFs
686
+ for _, row in self.Inp.RestraintData.iterrows():
687
+ node = int(row["Node"])
688
+ dof = int(row["Dof"])
689
+ k = float(row["Cp[MN/m]/[MNm/m]"])
690
+ gdof = 7 * (node - 1) + dof
691
+
692
+ if dof == 0: # ux
693
+ Rx += k * u[gdof]
694
+
695
+ print("Sum Fx (applied) =", Fx)
696
+ print("Sum spring Rx =", Rx)
697
+ print("Fx - Rx =", Fx - Rx)
698
+
699
+ ####________________________Iteration Theorie II.Order___________________________####
700
+ def BuildGeometricStiffnessMatrix(self):
701
+ """
702
+ Baut globale geometrische Steifigkeitsmatrix Kg(u) aus aktuellen Schnittgrößen.
703
+ Voraussetzung: self.u_ges, self.u_el, self.s_el bzw. *_store sind aktuell.
704
+ """
705
+ nD = self.Inp.nDoF
706
+ Kg_glob = np.zeros((nD, nD), dtype=float)
707
+
708
+ na_memb = self.Inp.members["na"].to_numpy()
709
+ ne_memb = self.Inp.members["ne"].to_numpy()
710
+
711
+ # Default: keine qy/qz (wenn du es noch nicht sauber mapst)
712
+ # Du kannst qy/qz später elementweise aus ElementLoads ableiten.
713
+ qy = 0.0
714
+ qz = 0.0
715
+ yq = 0.0
716
+ zq = 0.0
717
+
718
+ # Schwerpunkt/Schubmittelpunkt des Querschnitts
719
+ # (muss aus Input kommen; hier: fallback = 0)
720
+ yM = 0.0
721
+ zM = 0.0
722
+
723
+ for e in range(len(na_memb)):
724
+ ni = int(na_memb[e])
725
+ nj = int(ne_memb[e])
726
+ L = float(self.member_length[e])
727
+
728
+ # Schnittgrößen aus deinen Stores (links/rechts)
729
+ N_L, N_R = float(self.N_el_i_store[e][0, 0]), float(
730
+ self.N_el_i_store[e][1, 0]
731
+ )
732
+ My_L, My_R = float(self.MY_el_i_store[e][0, 0]), float(
733
+ self.MY_el_i_store[e][1, 0]
734
+ )
735
+ Mz_L, Mz_R = float(self.MZ_el_i_store[e][0, 0]), float(
736
+ self.MZ_el_i_store[e][1, 0]
737
+ )
738
+ Mx_L, Mx_R = float(self.MX_el_i_store[e][0, 0]), float(
739
+ self.MX_el_i_store[e][1, 0]
740
+ )
884
741
 
885
- return fig, ax
742
+ # Für Kg brauchst du i.d.R. konstante N, lineare M-Verläufe:
743
+ Nbar = 0.5 * (N_L + N_R)
744
+
745
+ # Mr: je nach Definition (Torsion/Warping). Als Start: mittleres Torsionsmoment
746
+ Mr = 0.5 * (Mx_L + Mx_R)
747
+
748
+ # lokale Kg nach deiner Formel
749
+ Kg_loc = self.ElemStem.Kg_theory_II_order(
750
+ L=L,
751
+ N=Nbar,
752
+ My_a=My_L,
753
+ My_b=My_R,
754
+ Mz_a=Mz_L,
755
+ Mz_b=Mz_R,
756
+ Mr=Mr,
757
+ qy=qy,
758
+ qz=qz,
759
+ yq=yq,
760
+ zq=zq,
761
+ yM=yM,
762
+ zM=zM,
763
+ )
886
764
 
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).
765
+ # local -> global
766
+ T = self.TransMats[e]
767
+ Kg_e_glob = T @ Kg_loc @ T.T
890
768
 
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
- )
769
+ # Assembling in globale Matrix
770
+ bi = 7 * (ni - 1)
771
+ bj = 7 * (nj - 1)
898
772
 
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
- )
773
+ Kg_glob[bi : bi + 7, bi : bi + 7] += Kg_e_glob[0:7, 0:7]
774
+ Kg_glob[bi : bi + 7, bj : bj + 7] += Kg_e_glob[0:7, 7:14]
775
+ Kg_glob[bj : bj + 7, bi : bi + 7] += Kg_e_glob[7:14, 0:7]
776
+ Kg_glob[bj : bj + 7, bj : bj + 7] += Kg_e_glob[7:14, 7:14]
908
777
 
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
- )
778
+ return Kg_glob
981
779
 
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
- ):
780
+ def SolveSecondOrder(self, max_iter=30, tol=1e-8, verbose=True):
1009
781
  """
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.
782
+ II.-Ordnung: Picard-Iteration
783
+ 1) K = Ke + Kg(u_k)
784
+ 2) löse u_{k+1}
785
+ 3) update Schnittgrößen, Kg, ...
1013
786
  """
787
+ self.TransMats = self.CalculateTransMat()
1014
788
 
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
- )
789
+ # 1) Ke aufbauen (wie bisher)
790
+ Ke_glob = self.BuildStructureStiffnessMatrix()
791
+ self.GesMat = Ke_glob.copy()
792
+
793
+ # Lager/Federn in Ke
794
+ self.RestraintData()
795
+
796
+ # Lastvektor (global) einmal
797
+ self.FGes = self.GlobalLoadVector()
1318
798
 
1319
- # ── alle momentan gezeichneten Artists merken (fürs Löschen) ───────
1320
- base_children = set(ax.get_children()) # alles, was NICHT neu erzeugt wird
799
+ # Start: lineare Lösung
800
+ u = np.linalg.solve(self.GesMat, self.FGes)
801
+ if verbose:
802
+ print("II-Order: initial linear solve done.")
1321
803
 
1322
- # ---------- UI-Platz reservieren -----------------------------------
1323
- fig.subplots_adjust(bottom=0.25)
804
+ for it in range(1, max_iter + 1):
805
+ self.u_ges = u
806
+ self.u_el = self.StoreLocalDisplacements()
807
+ self.s_el = self.CalculateLocalInnerForces() # update N,My,Mz,...
1324
808
 
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])
809
+ Kg_glob = self.BuildGeometricStiffnessMatrix()
1328
810
 
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)
811
+ # Totalmatrix neu: Ke + Kg + restraints
812
+ Ktot = Ke_glob + Kg_glob
1332
813
 
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()
814
+ # restraints nochmal addieren (weil Ktot neu)
815
+ self.GesMat = Ktot
816
+ self.RestraintData()
1339
817
 
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]")])
818
+ # lösen
819
+ u_new = np.linalg.solve(self.GesMat, self.FGes)
1345
820
 
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])
821
+ # Konvergenz
822
+ rel = np.linalg.norm(u_new - u) / (np.linalg.norm(u_new) + 1e-16)
823
+ if verbose:
824
+ print(f"II-Order iter {it}: rel_du = {rel:.3e}")
1349
825
 
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)
826
+ u = u_new
827
+ if rel < tol:
828
+ break
1352
829
 
1353
- _set_axes_equal(ax)
1354
- fig.canvas.draw_idle()
830
+ # final speichern
831
+ self.u_ges = u
832
+ self.u_el = self.StoreLocalDisplacements()
833
+ self.s_el = self.CalculateLocalInnerForces()
834
+ return u
1355
835
 
1356
- # ---------- Callbacks ----------------------------------------------
1357
- def _update_view(_):
1358
- ax.view_init(elev=s_elev.val, azim=s_azim.val)
1359
- fig.canvas.draw_idle()
836
+ #####_______________________ BUCKLING ___________________________#####
837
+ def get_free_dofs_mask(self):
838
+ restrained = np.zeros(self.Inp.nDoF, dtype=bool)
839
+ for _, row in self.Inp.RestraintData.iterrows():
840
+ gdof = 7 * (int(row["Node"]) - 1) + int(row["Dof"])
841
+ restrained[gdof] = True
842
+ return ~restrained
843
+
844
+ def BucklingEigen_alpha_crit(self, use_second_order_prestate=False, verbose=True):
845
+ """
846
+ Linear buckling: (Ke + alpha Kg) phi = 0
847
+ -> solve Ke phi = -alpha Kg phi
848
+ Returns: eigenvalues alpha (sorted), eigenvectors in full dof size (optional)
849
+ """
850
+ self.TransMats = self.CalculateTransMat()
1360
851
 
1361
- def _update_scale(_):
1362
- _redraw_moments(s_scale.val)
852
+ # Ke inkl. Lager
853
+ Ke = self.BuildStructureStiffnessMatrix()
854
+ self.GesMat = Ke.copy()
855
+ self.RestraintData()
1363
856
 
1364
- s_elev.on_changed(_update_view)
1365
- s_azim.on_changed(_update_view)
1366
- s_scale.on_changed(_update_scale)
857
+ # Vorzustand bestimmen (für Kg):
858
+ self.FGes = self.GlobalLoadVector()
1367
859
 
1368
- # ---------- Initiale Ansicht ---------------------------------------
1369
- _update_view(None) # stellt Kamera
1370
- # (Momenten-Plot ist schon mit scale_init gezeichnet)
860
+ if use_second_order_prestate:
861
+ self.SolveSecondOrder(max_iter=30, tol=1e-8, verbose=False)
862
+ else:
863
+ self.u_ges = np.linalg.solve(self.GesMat, self.FGes)
864
+ self.u_el = self.StoreLocalDisplacements()
865
+ self.s_el = self.CalculateLocalInnerForces()
1371
866
 
1372
- return fig, ax, (s_elev, s_azim, s_scale)
867
+ Kg = self.BuildGeometricStiffnessMatrix()
1373
868
 
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
869
+ free = self.get_free_dofs_mask()
870
+ Ke_ff = self.GesMat[np.ix_(free, free)]
871
+ Kg_ff = Kg[np.ix_(free, free)]
1380
872
 
1381
- # ── Instanz erzeugen (rechnet nur einmal) ───────────────────────────
1382
- calc = mainloop()
873
+ # EVP: Ke_ff^{-1} (-Kg_ff) v = alpha v
874
+ # -> solve A v = alpha v with A = inv(Ke_ff) @ (-Kg_ff)
875
+ A = np.linalg.solve(Ke_ff, -Kg_ff)
1383
876
 
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")
877
+ # Eigenwerte
878
+ eigvals, eigvecs = np.linalg.eig(A)
1388
879
 
1389
- # 2-D-Normalkräfte ---------------------------------------------------
1390
- fig2, ax2 = calc.plot_structure_with_normalforces_2d(scale=5)
1391
- ax2.invert_yaxis()
880
+ # reell filtern & sortieren (kleinster positiver)
881
+ eigvals = np.real_if_close(eigvals, tol=1e-6)
882
+ eigvals_real = np.real(eigvals)
1392
883
 
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
- )
884
+ # sinnvolle Kandidaten: alpha > 0
885
+ pos = eigvals_real[eigvals_real > 1e-9]
886
+ pos_sorted = np.sort(pos)
1398
887
 
1399
- calc.plot_My_3d_interactive(scale_init=1e-3, text=True)
888
+ if verbose:
889
+ if len(pos_sorted) == 0:
890
+ print("No positive alpha found (check sign convention of N/Kg).")
891
+ else:
892
+ print("alpha_crit =", pos_sorted[0])
893
+ print("next alphas:", pos_sorted[:5])
1400
894
 
1401
- # ── genau EIN Aufruf ────────────────────────────────────────────────
1402
- plt.show() # Fenster offen → hier läuft die Event-Schleife
1403
- # Fenster zu → show() kehrt zurück → Script endet
895
+ return pos_sorted, (eigvals_real, eigvecs)