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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. KIB_LAP/Betonbau/TEST_Rectangular.py +21 -0
  2. KIB_LAP/Betonbau/beam_rectangular.py +4 -0
  3. KIB_LAP/FACHWERKEBEN/Elements.py +209 -0
  4. KIB_LAP/FACHWERKEBEN/InputData.py +118 -0
  5. KIB_LAP/FACHWERKEBEN/Iteration.py +967 -0
  6. KIB_LAP/FACHWERKEBEN/Materials.py +30 -0
  7. KIB_LAP/FACHWERKEBEN/Plotting.py +681 -0
  8. KIB_LAP/FACHWERKEBEN/__init__.py +4 -0
  9. KIB_LAP/FACHWERKEBEN/main.py +27 -0
  10. KIB_LAP/Plattentragwerke/PlateBendingKirchhoff.py +36 -29
  11. KIB_LAP/STABRAUM/InputData.py +13 -2
  12. KIB_LAP/STABRAUM/Output_Data.py +61 -0
  13. KIB_LAP/STABRAUM/Plotting.py +1453 -0
  14. KIB_LAP/STABRAUM/Programm.py +518 -1026
  15. KIB_LAP/STABRAUM/Steifigkeitsmatrix.py +338 -117
  16. KIB_LAP/STABRAUM/main.py +58 -0
  17. KIB_LAP/STABRAUM/results.py +37 -0
  18. KIB_LAP/Scheibe/Assemble_Stiffness.py +246 -0
  19. KIB_LAP/Scheibe/Element_Stiffness.py +362 -0
  20. KIB_LAP/Scheibe/Meshing.py +365 -0
  21. KIB_LAP/Scheibe/Output.py +34 -0
  22. KIB_LAP/Scheibe/Plotting.py +722 -0
  23. KIB_LAP/Scheibe/Shell_Calculation.py +523 -0
  24. KIB_LAP/Scheibe/Testing_Mesh.py +25 -0
  25. KIB_LAP/Scheibe/__init__.py +14 -0
  26. KIB_LAP/Scheibe/main.py +33 -0
  27. KIB_LAP/StabEbenRitz/Biegedrillknicken.py +757 -0
  28. KIB_LAP/StabEbenRitz/Biegedrillknicken_Trigeometry.py +328 -0
  29. KIB_LAP/StabEbenRitz/Querschnittswerte.py +527 -0
  30. KIB_LAP/StabEbenRitz/Stabberechnung_Klasse.py +868 -0
  31. KIB_LAP/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  32. KIB_LAP/plate_buckling_cpp.cp313-win_amd64.pyd +0 -0
  33. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/METADATA +1 -1
  34. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/RECORD +37 -19
  35. Examples/Cross_Section_Thin.py +0 -61
  36. KIB_LAP/Betonbau/Bemessung_Zust_II.py +0 -648
  37. KIB_LAP/Betonbau/Iterative_Design.py +0 -723
  38. KIB_LAP/Plattentragwerke/NumInte.cpp +0 -23
  39. KIB_LAP/Plattentragwerke/NumericalIntegration.cpp +0 -23
  40. KIB_LAP/Plattentragwerke/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
  41. KIB_LAP/main.py +0 -2
  42. {Examples → KIB_LAP/StabEbenRitz}/__init__.py +0 -0
  43. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/WHEEL +0 -0
  44. {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1453 @@
1
+ # ============================================================
2
+ # Plotting.py
3
+ # Reines Plotting-Modul für STABRAUM
4
+ # Erwartet ein AnalysisResults-Objekt: res = calc.run()
5
+ # ============================================================
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ import matplotlib
11
+
12
+ matplotlib.use("QtAgg") # oder TkAgg
13
+
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.patches as mpatches
16
+ from matplotlib.patches import FancyArrowPatch
17
+ from matplotlib.widgets import Slider
18
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection, Line3DCollection
19
+ from matplotlib.widgets import CheckButtons, TextBox
20
+
21
+
22
+ # ------------------------------------------------------------
23
+ # Hilfsfunktion: gleiche Achsenskalierung im 3D
24
+ # ------------------------------------------------------------
25
+ def set_axes_equal_3d(ax, extra: float = 0.0):
26
+ x_limits = np.array(ax.get_xlim3d(), dtype=float)
27
+ y_limits = np.array(ax.get_ylim3d(), dtype=float)
28
+ z_limits = np.array(ax.get_zlim3d(), dtype=float)
29
+
30
+ ranges = np.array([np.ptp(lim) for lim in (x_limits, y_limits, z_limits)], dtype=float)
31
+ max_range = float(max(ranges.max(), 1e-9))
32
+
33
+ mids = np.array([lim.mean() for lim in (x_limits, y_limits, z_limits)], dtype=float)
34
+ half = (1.0 + float(extra)) * max_range / 2.0
35
+
36
+ ax.set_xlim3d(mids[0] - half, mids[0] + half)
37
+ ax.set_ylim3d(mids[1] - half, mids[1] + half)
38
+ ax.set_zlim3d(mids[2] - half, mids[2] + half)
39
+
40
+ try:
41
+ ax.set_box_aspect((1, 1, 1))
42
+ except Exception:
43
+ pass
44
+
45
+
46
+ # ============================================================
47
+ # StructurePlotter
48
+ # ============================================================
49
+ class StructurePlotter:
50
+ """
51
+ Reines Plotting.
52
+ Erwartet ein AnalysisResults-Objekt (res = calc.run()).
53
+ """
54
+
55
+ # ----------------------------
56
+ # init
57
+ # ----------------------------
58
+ def __init__(self, res):
59
+ self.res = res
60
+ self.Inp = res.Inp
61
+
62
+ self.nodes = self.Inp.nodes
63
+ self.na = self.Inp.members["na"]
64
+ self.ne = self.Inp.members["ne"]
65
+
66
+ # --------------------------------------------------------
67
+ # Geometrie-Helfer
68
+ # --------------------------------------------------------
69
+ def _pt(self, n: int) -> np.ndarray:
70
+ return np.array(
71
+ [
72
+ float(self.nodes["x[m]"][n - 1]),
73
+ float(self.nodes["y[m]"][n - 1]),
74
+ float(self.nodes["z[m]"][n - 1]),
75
+ ],
76
+ dtype=float,
77
+ )
78
+
79
+ def _tangent(self, a: int, e: int):
80
+ Pi, Pj = self._pt(int(a)), self._pt(int(e))
81
+ v = Pj - Pi
82
+ L = float(np.linalg.norm(v))
83
+ if L < 1e-15:
84
+ raise ValueError("Elementlänge ~ 0")
85
+ return Pi, Pj, v / L, L
86
+
87
+ def _stable_normal(self, t: np.ndarray, prefer="y") -> np.ndarray:
88
+ axes = {
89
+ "x": np.array([1.0, 0.0, 0.0]),
90
+ "y": np.array([0.0, 1.0, 0.0]),
91
+ "z": np.array([0.0, 0.0, 1.0]),
92
+ }
93
+ u = axes.get(prefer, axes["y"])
94
+ if abs(float(np.dot(t, u))) > 0.95:
95
+ u = axes["z"] if prefer != "z" else axes["x"]
96
+
97
+ w = np.cross(t, u)
98
+ n = float(np.linalg.norm(w))
99
+ if n < 1e-15:
100
+ raise ValueError("Kein Orthogonalvektor")
101
+ return w / n
102
+
103
+ def _orth_unit_2d(self, xi, zi, xj, zj) -> np.ndarray:
104
+ """
105
+ Orthogonaler Einheitsvektor zur Stabachse in x-z-Ebene.
106
+ """
107
+ v = np.array([float(xj - xi), 0.0, float(zj - zi)], dtype=float)
108
+ y_unit = np.array([0.0, 1.0, 0.0], dtype=float)
109
+ perp = np.cross(v, y_unit)[[0, 2]]
110
+ n = float(np.linalg.norm(perp))
111
+ if n < 1e-15:
112
+ raise ValueError("Elementlänge ~ 0")
113
+ return perp / n
114
+
115
+ def _field_map(self):
116
+ return {
117
+ "N": self.res.N_el_i_store,
118
+ "VY": self.res.VY_el_i_store,
119
+ "VZ": self.res.VZ_el_i_store,
120
+ "MX": self.res.MX_el_i_store,
121
+ "MY": self.res.MY_el_i_store,
122
+ "MZ": self.res.MZ_el_i_store,
123
+ }
124
+
125
+ # --------------------------------------------------------
126
+ # Dynamische Artists (2D)
127
+ # --------------------------------------------------------
128
+ def _mark_dyn(self, artist):
129
+ try:
130
+ artist._dyn = True
131
+ except Exception:
132
+ pass
133
+ return artist
134
+
135
+ def _clear_dyn(self, ax):
136
+ for ln in list(getattr(ax, "lines", [])):
137
+ if getattr(ln, "_dyn", False):
138
+ ln.remove()
139
+ for p in list(getattr(ax, "patches", [])):
140
+ if getattr(p, "_dyn", False):
141
+ p.remove()
142
+ for t in list(getattr(ax, "texts", [])):
143
+ if getattr(t, "_dyn", False):
144
+ t.remove()
145
+
146
+ # --------------------------------------------------------
147
+ # Basic 2D arrows
148
+ # --------------------------------------------------------
149
+ def _draw_force_arrow(self, ax, x, z, dx, dz, color="k", lw=1.8, head=10):
150
+ arr = FancyArrowPatch(
151
+ (x, z),
152
+ (x + dx, z + dz),
153
+ arrowstyle="-|>",
154
+ mutation_scale=head,
155
+ linewidth=lw,
156
+ color=color,
157
+ )
158
+ ax.add_patch(arr)
159
+ self._mark_dyn(arr)
160
+ return arr
161
+
162
+ def _draw_moment_double_arrow(self, ax, x, z, M, radius=0.08, color="purple", lw=1.8):
163
+ # Kleiner Kreis + Pfeil andeuten
164
+ th = np.linspace(0.0, 2 * np.pi, 120)
165
+ xs = x + radius * np.cos(th)
166
+ zs = z + radius * np.sin(th)
167
+ ln = ax.plot(xs, zs, color=color, lw=lw)[0]
168
+ self._mark_dyn(ln)
169
+
170
+ # Richtungspfeil am Kreis
171
+ # wähle Punkt bei 45°
172
+ t0 = np.pi / 4
173
+ px = x + radius * np.cos(t0)
174
+ pz = z + radius * np.sin(t0)
175
+ # Tangente
176
+ tx = -np.sin(t0)
177
+ tz = np.cos(t0)
178
+ sgn = 1.0 if float(M) >= 0.0 else -1.0
179
+ arr = FancyArrowPatch(
180
+ (px, pz),
181
+ (px + sgn * 0.6 * radius * tx, pz + sgn * 0.6 * radius * tz),
182
+ arrowstyle="-|>",
183
+ mutation_scale=10,
184
+ linewidth=lw,
185
+ color=color,
186
+ )
187
+ ax.add_patch(arr)
188
+ self._mark_dyn(arr)
189
+ return ln, arr
190
+
191
+ # --------------------------------------------------------
192
+ # Reactions / supports helpers (STABRAUM-Style)
193
+ # --------------------------------------------------------
194
+ def _reaction_vector(self) -> np.ndarray:
195
+ """
196
+ Reaktionsvektor r = K*u - F (o.ä.)
197
+ Du hattest das schon in deiner OutputData. Hier robust:
198
+ - bevorzugt res.Reactions, falls vorhanden
199
+ - sonst versucht: r = GesMat @ u_ges - FGes
200
+ """
201
+ if hasattr(self.res, "Reactions") and self.res.Reactions is not None:
202
+ r = np.asarray(self.res.Reactions, dtype=float).reshape(-1)
203
+ return r
204
+
205
+ if hasattr(self.res, "GesMat") and hasattr(self.res, "u_ges") and hasattr(self.res, "FGes"):
206
+ K = np.asarray(self.res.GesMat, dtype=float)
207
+ u = np.asarray(self.res.u_ges, dtype=float).reshape(-1)
208
+ F = np.asarray(self.res.FGes, dtype=float).reshape(-1)
209
+ return (K @ u - F).reshape(-1)
210
+
211
+ # fallback
212
+ return np.zeros(int(len(self.nodes["x[m]"]) * 7), dtype=float)
213
+
214
+ def _support_nodes(self):
215
+ """
216
+ RestraintData erwartet: Spalten 'Node' usw.
217
+ """
218
+ df = getattr(self.Inp, "RestraintData", None)
219
+ if df is None:
220
+ return []
221
+ cols = [str(c).strip() for c in df.columns]
222
+ if "Node" not in cols:
223
+ return []
224
+ # unique nodes, >0
225
+ out = []
226
+ for n in df["Node"].tolist():
227
+ try:
228
+ nn = int(n)
229
+ if nn not in out:
230
+ out.append(nn)
231
+ except Exception:
232
+ pass
233
+ return out
234
+
235
+ def _length_ref_xz(self, frac=0.03) -> float:
236
+ xs = np.asarray(self.nodes["x[m]"], dtype=float)
237
+ zs = np.asarray(self.nodes["z[m]"], dtype=float)
238
+ span = max(float(xs.max() - xs.min()), float(zs.max() - zs.min()), 1e-9)
239
+ return float(frac) * span
240
+
241
+ # ========================================================
242
+ # Springs: Parsing & 3D drawing (TRANSLATION + ROTATION)
243
+ # ========================================================
244
+ def _get_springs_df(self):
245
+ df = getattr(self.Inp, "SpringsData", None)
246
+ if df is None:
247
+ df = getattr(self.Inp, "Springs", None)
248
+ return df
249
+
250
+ @staticmethod
251
+ def _spring_dof_kind(dof: int):
252
+ """
253
+ Mapping passend zu deinem 7-DoF Layout (wie in deinen anderen Funktionen):
254
+ Transl: 0=FX/ux, 1=FY/uy, 3=FZ/uz
255
+ Rot : 5=MX, 4=MY, 2=MZ
256
+ """
257
+ dof = int(dof)
258
+ trans = {0: ("TX", np.array([1.0, 0.0, 0.0])),
259
+ 1: ("TY", np.array([0.0, 1.0, 0.0])),
260
+ 3: ("TZ", np.array([0.0, 0.0, 1.0]))}
261
+ rot = {5: ("RX", np.array([1.0, 0.0, 0.0])),
262
+ 4: ("RY", np.array([0.0, 1.0, 0.0])),
263
+ 2: ("RZ", np.array([0.0, 0.0, 1.0]))}
264
+ if dof in trans:
265
+ return "trans", trans[dof][0], trans[dof][1]
266
+ if dof in rot:
267
+ return "rot", rot[dof][0], rot[dof][1]
268
+ return None, f"DOF{dof}", np.array([1.0, 0.0, 0.0])
269
+
270
+ @staticmethod
271
+ def _pick_perp_basis(axis_hat: np.ndarray):
272
+ axis_hat = np.asarray(axis_hat, dtype=float)
273
+ axis_hat = axis_hat / (np.linalg.norm(axis_hat) + 1e-16)
274
+ ex = np.array([1.0, 0.0, 0.0], dtype=float)
275
+ ey = np.array([0.0, 1.0, 0.0], dtype=float)
276
+ ez = np.array([0.0, 0.0, 1.0], dtype=float)
277
+
278
+ h = ex if abs(float(np.dot(axis_hat, ex))) < 0.9 else ey
279
+ u = np.cross(axis_hat, h)
280
+ nu = float(np.linalg.norm(u))
281
+ if nu < 1e-14:
282
+ h = ez
283
+ u = np.cross(axis_hat, h)
284
+ nu = float(np.linalg.norm(u))
285
+
286
+ u_hat = u / (nu + 1e-16)
287
+ v_hat = np.cross(axis_hat, u_hat)
288
+ v_hat = v_hat / (np.linalg.norm(v_hat) + 1e-16)
289
+ return u_hat, v_hat
290
+
291
+ def _draw_trans_spring_3d(self, ax, P0, axis_hat, size, nzig=7, amp_frac=0.18, color="purple", lw=1.6):
292
+ """
293
+ Zickzack-Feder entlang axis_hat, Start bei P0.
294
+ """
295
+ P0 = np.asarray(P0, dtype=float).reshape(3,)
296
+ axis_hat = np.asarray(axis_hat, dtype=float).reshape(3,)
297
+ axis_hat = axis_hat / (np.linalg.norm(axis_hat) + 1e-16)
298
+
299
+ u_hat, _ = self._pick_perp_basis(axis_hat)
300
+ amp = float(amp_frac) * float(size)
301
+
302
+ t = np.linspace(0.0, 1.0, 2 * int(nzig) + 1)
303
+ pts = []
304
+ for i, ti in enumerate(t):
305
+ P = P0 + (ti * float(size)) * axis_hat
306
+ if 0 < i < len(t) - 1:
307
+ sgn = 1.0 if (i % 2 == 0) else -1.0
308
+ P = P + sgn * amp * u_hat
309
+ pts.append(P)
310
+
311
+ segs = [[pts[i], pts[i + 1]] for i in range(len(pts) - 1)]
312
+ coll = Line3DCollection(segs, colors=color, linewidths=lw)
313
+ ax.add_collection3d(coll)
314
+ return coll
315
+
316
+ def _draw_rot_spring_3d(self, ax, P0, axis_hat, radius, turns=1.25, n=120, color="purple", lw=1.6):
317
+ """
318
+ Rotationsfeder als Spirale in Ebene senkrecht zu axis_hat.
319
+ """
320
+ P0 = np.asarray(P0, dtype=float).reshape(3,)
321
+ axis_hat = np.asarray(axis_hat, dtype=float).reshape(3,)
322
+ axis_hat = axis_hat / (np.linalg.norm(axis_hat) + 1e-16)
323
+
324
+ u_hat, v_hat = self._pick_perp_basis(axis_hat)
325
+
326
+ th = np.linspace(0.0, 2 * np.pi * float(turns), int(n))
327
+ # leichte Radialänderung für "Federlook"
328
+ r = float(radius) * (0.75 + 0.25 * (th / (th.max() + 1e-16)))
329
+
330
+ pts = [P0 + r[i] * np.cos(th[i]) * u_hat + r[i] * np.sin(th[i]) * v_hat for i in range(len(th))]
331
+ segs = [[pts[i], pts[i + 1]] for i in range(len(pts) - 1)]
332
+ coll = Line3DCollection(segs, colors=color, linewidths=lw)
333
+ ax.add_collection3d(coll)
334
+ return coll
335
+
336
+ def _draw_springs_3d(
337
+ self,
338
+ ax,
339
+ # Größen (relativ zur Modellspannweite)
340
+ size_frac=0.06,
341
+ rot_radius_frac=0.03,
342
+ # Sichtbarkeit
343
+ show_trans=True,
344
+ show_rot=True,
345
+ # Datenquellen
346
+ include_springsdata=True,
347
+ include_restraints=True,
348
+ # Filter
349
+ k_tol=1e-12,
350
+ # Darstellung
351
+ color="purple",
352
+ label=False,
353
+ label_fs=8,
354
+ ):
355
+ """
356
+ Zeichnet Federn in 3D aus zwei Quellen:
357
+
358
+ (A) SpringsData (CSV):
359
+ Spalten: node_a, node_e, dof, cp/cm[MN,m] (k-Spalte optional/robust)
360
+ - node_a==node_e -> Feder am Knoten
361
+ - sonst -> Feder am Mittelpunkt zwischen node_a und node_e
362
+
363
+ (B) RestraintData:
364
+ Spalten: Node, kx/ky/kz/krx/kry/krz (robust, case-insensitive)
365
+ - Feder immer am Knoten
366
+
367
+ DOF-Mapping (dein 7-DoF Layout / STABRAUM):
368
+ transl: ux=0, uy=1, uz=3
369
+ rot : rx=5, ry=4, rz=2
370
+
371
+ Benötigt folgende Helper-Methoden in deiner Klasse:
372
+ - self._pt(n) -> np.array([x,y,z])
373
+ - self._spring_dof_kind(dof) -> (kind, label, axis_vec)
374
+ - self._draw_trans_spring_3d(ax, P0, axis_vec, size, color=...)
375
+ - self._draw_rot_spring_3d(ax, P0, axis_vec, radius, color=...)
376
+ - self._get_springs_df() -> DataFrame|None (deine SpringsData)
377
+ - self._iter_restraint_springs(k_tol=...) -> yields (node, dof_int, k)
378
+ """
379
+
380
+ # ---------------------------
381
+ # Modell-Spannweite für Maßstab
382
+ # ---------------------------
383
+ xs = np.asarray(self.nodes["x[m]"], dtype=float)
384
+ ys = np.asarray(self.nodes["y[m]"], dtype=float)
385
+ zs = np.asarray(self.nodes["z[m]"], dtype=float)
386
+ span = max(
387
+ float(xs.max() - xs.min()),
388
+ float(ys.max() - ys.min()),
389
+ float(zs.max() - zs.min()),
390
+ 1e-9,
391
+ )
392
+
393
+ size = float(size_frac) * span
394
+ rr = float(rot_radius_frac) * span
395
+
396
+ # ---------------------------
397
+ # Jobs sammeln: (P0, dof, labelprefix)
398
+ # ---------------------------
399
+ jobs = [] # list[tuple[np.ndarray, int, str|None]]
400
+
401
+ # ---------- (A) SpringsData ----------
402
+ if include_springsdata:
403
+ df = None
404
+ try:
405
+ df = self._get_springs_df()
406
+ except Exception:
407
+ df = None
408
+
409
+ if df is not None:
410
+ df = df.copy()
411
+ df.columns = [str(c).strip().replace("\ufeff", "") for c in df.columns]
412
+
413
+ need = ["node_a", "node_e", "dof"]
414
+ if all(c in df.columns for c in need):
415
+ # k-Spalte robust finden (optional)
416
+ k_col = None
417
+ for cand in [
418
+ "cp/cm[MN,m]",
419
+ "cp/cm[MN,m ]",
420
+ "cp/cm",
421
+ "k",
422
+ "K",
423
+ "stiffness",
424
+ "Value",
425
+ "value",
426
+ ]:
427
+ if cand in df.columns:
428
+ k_col = cand
429
+ break
430
+
431
+ # Leere Zeilen raus (CSV-Leerzeilen)
432
+ if k_col is not None:
433
+ df = df.dropna(subset=["node_a", "node_e", "dof", k_col])
434
+ else:
435
+ df = df.dropna(subset=["node_a", "node_e", "dof"])
436
+
437
+ for _, row in df.iterrows():
438
+ try:
439
+ na = int(row["node_a"])
440
+ ne = int(row["node_e"])
441
+ dof = int(float(row["dof"]))
442
+ except Exception:
443
+ continue
444
+
445
+ # optionaler k-filter
446
+ if k_col is not None:
447
+ try:
448
+ kval = float(row[k_col])
449
+ except Exception:
450
+ kval = 0.0
451
+ if abs(kval) <= float(k_tol):
452
+ continue
453
+
454
+ try:
455
+ Pa = self._pt(na)
456
+ Pe = self._pt(ne)
457
+ except Exception:
458
+ continue
459
+
460
+ P0 = Pa if na == ne else 0.5 * (Pa + Pe)
461
+ jobs.append((np.asarray(P0, dtype=float), dof, None))
462
+
463
+ # ---------- (B) RestraintData ----------
464
+ if include_restraints:
465
+ try:
466
+ for n, dof, k in self._iter_restraint_springs(k_tol=k_tol):
467
+ try:
468
+ P0 = self._pt(int(n))
469
+ except Exception:
470
+ continue
471
+ jobs.append((np.asarray(P0, dtype=float), int(dof), f"N{int(n)}"))
472
+ except Exception:
473
+ pass
474
+
475
+ if len(jobs) == 0:
476
+ return []
477
+
478
+ # ---------------------------
479
+ # Zeichnen
480
+ # ---------------------------
481
+ artists = []
482
+
483
+ for P0, dof, lab_prefix in jobs:
484
+ try:
485
+ kind, lab, axis = self._spring_dof_kind(int(dof))
486
+ except Exception:
487
+ continue
488
+
489
+ if kind == "trans" and not show_trans:
490
+ continue
491
+ if kind == "rot" and not show_rot:
492
+ continue
493
+
494
+ axis = np.asarray(axis, dtype=float)
495
+ na = float(np.linalg.norm(axis))
496
+ if na < 1e-15:
497
+ continue
498
+ axis = axis / na
499
+
500
+ # kleiner Offset damit Feder nicht "im Knotenpunkt" klebt
501
+ Pbase = P0 + 0.15 * size * axis
502
+
503
+ if kind == "trans":
504
+ try:
505
+ art = self._draw_trans_spring_3d(ax, Pbase, axis, size=size, color=color)
506
+ artists.append(art)
507
+ except Exception:
508
+ pass
509
+
510
+ elif kind == "rot":
511
+ try:
512
+ art = self._draw_rot_spring_3d(ax, Pbase, axis, radius=rr, color=color)
513
+ artists.append(art)
514
+ except Exception:
515
+ pass
516
+
517
+ if label:
518
+ txt = lab if lab_prefix is None else f"{lab_prefix}:{lab}"
519
+ try:
520
+ ax.text(
521
+ float(Pbase[0]),
522
+ float(Pbase[1]),
523
+ float(Pbase[2]),
524
+ txt,
525
+ fontsize=int(label_fs),
526
+ color=color,
527
+ )
528
+ except Exception:
529
+ pass
530
+
531
+ return artists
532
+
533
+
534
+ # ========================================================
535
+ # 2D Struktur (x-z) + Nummerierung
536
+ # ========================================================
537
+ def plot_structure_2d(self, node_labels: bool = False, elem_labels: bool = False):
538
+ fig, ax = plt.subplots(figsize=(10, 6))
539
+
540
+ # Stäbe zeichnen
541
+ for idx, (a, e) in enumerate(zip(self.na, self.ne), start=1):
542
+ Pi, Pj = self._pt(int(a)), self._pt(int(e))
543
+ ax.plot([Pi[0], Pj[0]], [Pi[2], Pj[2]], color="black", lw=1.0)
544
+
545
+ # Elementnummer am Mittelpunkt
546
+ if elem_labels:
547
+ xm = 0.5 * (Pi[0] + Pj[0])
548
+ zm = 0.5 * (Pi[2] + Pj[2])
549
+ ax.text(
550
+ xm,
551
+ zm,
552
+ f"E{idx}",
553
+ fontsize=9,
554
+ ha="center",
555
+ va="center",
556
+ bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="gray", lw=0.5),
557
+ )
558
+
559
+ # Knotennummern
560
+ if node_labels:
561
+ n_nodes = len(self.nodes["x[m]"])
562
+ for n in range(1, n_nodes + 1):
563
+ P = self._pt(n)
564
+ ax.plot(P[0], P[2], marker="o", markersize=3, color="black")
565
+ ax.text(
566
+ P[0],
567
+ P[2],
568
+ f"N{n}",
569
+ fontsize=9,
570
+ ha="left",
571
+ va="bottom",
572
+ bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="gray", lw=0.5),
573
+ )
574
+
575
+ ax.set_xlabel("X [m]")
576
+ ax.set_ylabel("Z [m]")
577
+ ax.set_title("Unverformte Struktur (x-z)")
578
+ ax.grid(True)
579
+ ax.set_aspect("equal", adjustable="datalim")
580
+ ax.relim()
581
+ ax.autoscale_view()
582
+ return fig, ax
583
+
584
+ # ========================================================
585
+ # 2D Endwerte (statisch)
586
+ # ========================================================
587
+ def plot_endforces_2d(self, kind="MY", scale=5.0, invert_y=False, node_labels=False, elem_labels=False):
588
+ fig, ax = self.plot_structure_2d(node_labels=node_labels, elem_labels=elem_labels)
589
+ Q = self._field_map()[kind.upper()] # (nElem,2,1)
590
+
591
+ for i, (a, e) in enumerate(zip(self.na, self.ne)):
592
+ Pi, Pj = self._pt(int(a)), self._pt(int(e))
593
+ ix, iz = Pi[0], Pi[2]
594
+ jx, jz = Pj[0], Pj[2]
595
+
596
+ try:
597
+ u = self._orth_unit_2d(ix, iz, jx, jz)
598
+ except ValueError:
599
+ continue
600
+
601
+ qa = float(Q[i, 0, 0])
602
+ qb = float(Q[i, 1, 0])
603
+
604
+ def endpt(x, z, val):
605
+ if abs(val) < 1e-15:
606
+ return x, z
607
+ col = "blue" if val >= 0 else "red"
608
+ link = 0.05 * float(scale)
609
+ cx = x + link * u[0] * val * float(scale)
610
+ cz = z + link * u[1] * val * float(scale)
611
+ ax.plot([x, cx], [z, cz], color=col, lw=1)
612
+ ax.text(cx, cz, f"{kind}={val:.3f}", color=col, fontsize=8)
613
+ return cx, cz
614
+
615
+ ca = endpt(ix, iz, qa)
616
+ cb = endpt(jx, jz, qb)
617
+ ax.plot([ca[0], cb[0]], [ca[1], cb[1]], color="black", lw=1)
618
+
619
+ ax.legend(handles=[mpatches.Patch(color="blue", label=f"{kind} ≥ 0"),
620
+ mpatches.Patch(color="red", label=f"{kind} < 0")])
621
+
622
+ if invert_y:
623
+ ax.invert_yaxis()
624
+
625
+ ax.relim()
626
+ ax.autoscale_view()
627
+ return fig, ax
628
+
629
+ # ========================================================
630
+ # 2D Endwerte (DYNAMISCH, Slider für scale)
631
+ # ========================================================
632
+ def plot_endforces_2d_interactive(self, kind="MY", scale_init=5.0, invert_y=False, node_labels=False, elem_labels=False):
633
+ fig, ax = self.plot_structure_2d(node_labels=node_labels, elem_labels=elem_labels)
634
+ Q = self._field_map()[kind.upper()] # (nElem,2,1)
635
+
636
+ smin = 0.0
637
+ smax = max(float(scale_init) * 50.0, 10.0)
638
+
639
+ ax.legend(handles=[mpatches.Patch(color="blue", label=f"{kind} ≥ 0"),
640
+ mpatches.Patch(color="red", label=f"{kind} < 0")])
641
+
642
+ def _clear_dyn_local():
643
+ self._clear_dyn(ax)
644
+
645
+ def draw(scale):
646
+ _clear_dyn_local()
647
+ scale = float(scale)
648
+
649
+ for i, (a, e) in enumerate(zip(self.na, self.ne)):
650
+ Pi, Pj = self._pt(int(a)), self._pt(int(e))
651
+ ix, iz = Pi[0], Pi[2]
652
+ jx, jz = Pj[0], Pj[2]
653
+
654
+ try:
655
+ u = self._orth_unit_2d(ix, iz, jx, jz)
656
+ except ValueError:
657
+ continue
658
+
659
+ qa = float(Q[i, 0, 0])
660
+ qb = float(Q[i, 1, 0])
661
+
662
+ def endpt(x, z, val):
663
+ if abs(val) < 1e-15:
664
+ return x, z
665
+ col = "blue" if val >= 0 else "red"
666
+ link = 0.05 * scale
667
+ cx = x + link * u[0] * val * scale
668
+ cz = z + link * u[1] * val * scale
669
+ ln = ax.plot([x, cx], [z, cz], color=col, lw=1)[0]
670
+ self._mark_dyn(ln)
671
+ txt = ax.text(cx, cz, f"{kind}={val:.3f}", color=col, fontsize=8)
672
+ self._mark_dyn(txt)
673
+ return cx, cz
674
+
675
+ ca = endpt(ix, iz, qa)
676
+ cb = endpt(jx, jz, qb)
677
+ ln2 = ax.plot([ca[0], cb[0]], [ca[1], cb[1]], color="black", lw=1)[0]
678
+ self._mark_dyn(ln2)
679
+
680
+ ax.relim()
681
+ ax.autoscale_view()
682
+ ax.set_aspect("equal", adjustable="datalim")
683
+ if invert_y:
684
+ ax.invert_yaxis()
685
+ fig.canvas.draw_idle()
686
+
687
+ fig.subplots_adjust(bottom=0.20)
688
+ ax_scale = fig.add_axes([0.15, 0.08, 0.70, 0.03])
689
+ s_scale = Slider(ax_scale, "Scale", smin, smax, valinit=float(scale_init))
690
+ s_scale.on_changed(lambda _: draw(s_scale.val))
691
+ draw(scale_init)
692
+ return fig, ax, s_scale
693
+
694
+ # ========================================================
695
+ # 3D verformte Struktur (interaktiv) + Nummerierung + FEDERN (NEU)
696
+ # ========================================================
697
+ def plot_structure_deformed_3d_interactive(
698
+ self,
699
+ scale_init=1.0,
700
+ show_undeformed=True,
701
+ node_labels=False,
702
+ elem_labels=False,
703
+ # NEW: springs
704
+ show_springs=True,
705
+ show_rot_springs=True,
706
+ springs_size_frac=0.06,
707
+ springs_rot_radius_frac=0.03,
708
+ springs_label=False,
709
+ springs_k_tol=1e-12,
710
+ ):
711
+ def ux(n):
712
+ return float(self.res.u_ges[7 * (n - 1) + 0])
713
+
714
+ def uy(n):
715
+ return float(self.res.u_ges[7 * (n - 1) + 1])
716
+
717
+ def uz(n):
718
+ return float(self.res.u_ges[7 * (n - 1) + 3])
719
+
720
+ fig = plt.figure(figsize=(9, 7))
721
+ ax = fig.add_subplot(projection="3d")
722
+
723
+ # unverformte Stäbe
724
+ if show_undeformed:
725
+ segs = []
726
+ for a, e in zip(self.na, self.ne):
727
+ segs.append([self._pt(int(a)), self._pt(int(e))])
728
+ ax.add_collection3d(Line3DCollection(segs, colors="lightgray", linewidths=1, zorder=0))
729
+
730
+ # Springs (NEU): symbolisch, unabhängig vom Deformationsscale
731
+ if show_springs or show_rot_springs:
732
+ self._draw_springs_3d(
733
+ ax,
734
+ size_frac=float(springs_size_frac),
735
+ rot_radius_frac=float(springs_rot_radius_frac),
736
+ show_trans=bool(show_springs),
737
+ show_rot=bool(show_rot_springs),
738
+ color="purple",
739
+ label=bool(springs_label),
740
+ k_tol=float(springs_k_tol),
741
+ )
742
+
743
+ # deformierte Linien
744
+ deformed_lines = []
745
+ for a, e in zip(self.na, self.ne):
746
+ a, e = int(a), int(e)
747
+ Pi, Pj = self._pt(a), self._pt(e)
748
+
749
+ xd = [Pi[0] + float(scale_init) * ux(a), Pj[0] + float(scale_init) * ux(e)]
750
+ yd = [Pi[1] + float(scale_init) * uy(a), Pj[1] + float(scale_init) * uy(e)]
751
+ zd = [Pi[2] + float(scale_init) * uz(a), Pj[2] + float(scale_init) * uz(e)]
752
+ (ld,) = ax.plot(xd, yd, zd, lw=2, zorder=3)
753
+ deformed_lines.append((a, e, ld))
754
+
755
+ # Texte
756
+ node_texts = []
757
+ elem_texts = []
758
+
759
+ if node_labels:
760
+ n_nodes = len(self.nodes["x[m]"])
761
+ for n in range(1, n_nodes + 1):
762
+ P = self._pt(n)
763
+ txt = ax.text(
764
+ P[0] + float(scale_init) * ux(n),
765
+ P[1] + float(scale_init) * uy(n),
766
+ P[2] + float(scale_init) * uz(n),
767
+ f"N{n}",
768
+ fontsize=8,
769
+ zorder=4,
770
+ )
771
+ node_texts.append((n, txt))
772
+
773
+ if elem_labels:
774
+ for idx, (a, e) in enumerate(zip(self.na, self.ne), start=1):
775
+ a, e = int(a), int(e)
776
+ Pi, Pj = self._pt(a), self._pt(e)
777
+ xm = 0.5 * (Pi[0] + Pj[0]) + float(scale_init) * 0.5 * (ux(a) + ux(e))
778
+ ym = 0.5 * (Pi[1] + Pj[1]) + float(scale_init) * 0.5 * (uy(a) + uy(e))
779
+ zm = 0.5 * (Pi[2] + Pj[2]) + float(scale_init) * 0.5 * (uz(a) + uz(e))
780
+ txt = ax.text(xm, ym, zm, f"E{idx}", fontsize=8, zorder=4)
781
+ elem_texts.append((idx, a, e, txt))
782
+
783
+ ax.set_xlabel("X [m]")
784
+ ax.set_ylabel("Y [m]")
785
+ ax.set_zlabel("Z [m]")
786
+ ax.set_title("Verformte Struktur 3D (interaktiv) + Federn")
787
+
788
+ set_axes_equal_3d(ax, extra=0.05)
789
+
790
+ fig.subplots_adjust(bottom=0.18)
791
+ ax_scale = fig.add_axes([0.15, 0.08, 0.7, 0.03])
792
+ s_scale = Slider(ax_scale, "Scale", 0.0, float(scale_init) * 10000.0, valinit=float(scale_init))
793
+
794
+ def update(_):
795
+ s = float(s_scale.val)
796
+
797
+ for a, e, line in deformed_lines:
798
+ Pi, Pj = self._pt(a), self._pt(e)
799
+ line.set_data_3d(
800
+ [Pi[0] + s * ux(a), Pj[0] + s * ux(e)],
801
+ [Pi[1] + s * uy(a), Pj[1] + s * uy(e)],
802
+ [Pi[2] + s * uz(a), Pj[2] + s * uz(e)],
803
+ )
804
+
805
+ for n, txt in node_texts:
806
+ P = self._pt(n)
807
+ txt.set_position((P[0] + s * ux(n), P[1] + s * uy(n)))
808
+ txt.set_3d_properties(P[2] + s * uz(n), zdir="z")
809
+
810
+ for idx, a, e, txt in elem_texts:
811
+ Pi, Pj = self._pt(a), self._pt(e)
812
+ xm = 0.5 * (Pi[0] + Pj[0]) + s * 0.5 * (ux(a) + ux(e))
813
+ ym = 0.5 * (Pi[1] + Pj[1]) + s * 0.5 * (uy(a) + uy(e))
814
+ zm = 0.5 * (Pi[2] + Pj[2]) + s * 0.5 * (uz(a) + uz(e))
815
+ txt.set_position((xm, ym))
816
+ txt.set_3d_properties(zm, zdir="z")
817
+
818
+ set_axes_equal_3d(ax, extra=0.05)
819
+ fig.canvas.draw_idle()
820
+
821
+ s_scale.on_changed(update)
822
+ update(None)
823
+ return fig, ax, s_scale
824
+
825
+ # ========================================================
826
+ # 3D Schnittgrößen-Diagramm entlang der Stäbe (DYNAMISCH) + FEDERN (NEU)
827
+ # ========================================================
828
+ def plot_diagram_3d_interactive(
829
+ self,
830
+ kind="MY",
831
+ scale_init=1.0,
832
+ width_frac=0.03,
833
+ prefer_axis="y",
834
+ show_structure=True,
835
+ show_end_labels=True,
836
+ label_offset_frac=0.02,
837
+ font_size=8,
838
+ robust_ref=True,
839
+ keep_view=True,
840
+ margin_frac=0.05,
841
+ # NEW: springs
842
+ show_springs=True,
843
+ show_rot_springs=True,
844
+ springs_size_frac=0.06,
845
+ springs_rot_radius_frac=0.03,
846
+ springs_label=False,
847
+ springs_k_tol=1e-12,
848
+ ):
849
+ """
850
+ Festes 3D-Diagramm als Polygon-Band pro Element.
851
+ - Positiv: Blau, Negativ: Rot (Split bei Vorzeichenwechsel)
852
+ - Nur 1 Diagramm-Linie pro Element (Mittellinie)
853
+ - Einheit bleibt exakt wie in deinem Solver (MN bzw. MNm).
854
+ - Scale per TextBox (+ optional Slider)
855
+ - Node/Elem IDs per CheckButtons
856
+ - NEU: Federn (Translation + Rotation) symbolisch
857
+ """
858
+ kind = str(kind).upper()
859
+ Q = self._field_map()[kind] # (nElem,2,1)
860
+
861
+ vals = np.abs(Q[:, :, 0]).ravel()
862
+ if vals.size == 0:
863
+ qref = 1.0
864
+ else:
865
+ qref = float(np.percentile(vals, 95)) if robust_ref else float(vals.max())
866
+ qref = max(qref, 1e-12)
867
+
868
+ xs = np.asarray(self.nodes["x[m]"], dtype=float)
869
+ ys = np.asarray(self.nodes["y[m]"], dtype=float)
870
+ zs = np.asarray(self.nodes["z[m]"], dtype=float)
871
+ span = max(float(xs.max() - xs.min()), float(ys.max() - ys.min()), float(zs.max() - zs.min()), 1e-9)
872
+
873
+ mx = float(margin_frac) * span
874
+ xlim = (float(xs.min() - mx), float(xs.max() + mx))
875
+ ylim = (float(ys.min() - mx), float(ys.max() + mx))
876
+ zlim = (float(zs.min() - mx), float(zs.max() + mx))
877
+
878
+ width = float(width_frac) * span
879
+ label_off = float(label_offset_frac) * span
880
+
881
+ fig = plt.figure(figsize=(11, 7))
882
+ ax = fig.add_subplot(projection="3d")
883
+
884
+ if show_structure:
885
+ segs = []
886
+ for a, e in zip(self.na, self.ne):
887
+ segs.append([self._pt(int(a)), self._pt(int(e))])
888
+ ax.add_collection3d(Line3DCollection(segs, colors="lightgray", linewidths=1, zorder=0))
889
+
890
+ # NEW: springs (symbolic)
891
+ if show_springs or show_rot_springs:
892
+ self._draw_springs_3d(
893
+ ax,
894
+ size_frac=float(springs_size_frac),
895
+ rot_radius_frac=float(springs_rot_radius_frac),
896
+ show_trans=bool(show_springs),
897
+ show_rot=bool(show_rot_springs),
898
+ color="purple",
899
+ label=bool(springs_label),
900
+ k_tol=float(springs_k_tol),
901
+ )
902
+
903
+ ax.set_xlim(xlim)
904
+ ax.set_ylim(ylim)
905
+ ax.set_zlim(zlim)
906
+ try:
907
+ ax.set_box_aspect((1, 1, 1))
908
+ except Exception:
909
+ pass
910
+
911
+ node_id_texts = []
912
+ elem_id_texts = []
913
+
914
+ n_nodes = len(self.nodes["x[m]"])
915
+ for n in range(1, n_nodes + 1):
916
+ P = self._pt(n)
917
+ node_id_texts.append(ax.text(P[0], P[1], P[2], f"N{n}", fontsize=8, visible=False))
918
+
919
+ for idx, (a, e) in enumerate(zip(self.na, self.ne), start=1):
920
+ Pi, Pj = self._pt(int(a)), self._pt(int(e))
921
+ Pm = 0.5 * (Pi + Pj)
922
+ elem_id_texts.append(ax.text(Pm[0], Pm[1], Pm[2], f"E{idx}", fontsize=8, visible=False))
923
+
924
+ polys = []
925
+ lines = []
926
+ end_texts = []
927
+
928
+ def clear_dynamic():
929
+ nonlocal polys, lines, end_texts
930
+ for p in polys:
931
+ try:
932
+ p.remove()
933
+ except Exception:
934
+ pass
935
+ for ln in lines:
936
+ try:
937
+ ln.remove()
938
+ except Exception:
939
+ pass
940
+ for t in end_texts:
941
+ try:
942
+ t.remove()
943
+ except Exception:
944
+ pass
945
+ polys, lines, end_texts = [], [], []
946
+
947
+ def col_for(val):
948
+ return "blue" if float(val) >= 0.0 else "red"
949
+
950
+ def split_parts(Pi, Pj, Mi, Mj):
951
+ Mi = float(Mi)
952
+ Mj = float(Mj)
953
+ if Mi == 0.0 or Mj == 0.0 or (Mi > 0 and Mj > 0) or (Mi < 0 and Mj < 0):
954
+ return [(Pi, Pj, Mi, Mj, col_for(0.5 * (Mi + Mj)))]
955
+ s0 = -Mi / (Mj - Mi)
956
+ s0 = min(max(float(s0), 0.0), 1.0)
957
+ P0 = Pi + s0 * (Pj - Pi)
958
+ return [
959
+ (Pi, P0, Mi, 0.0, col_for(Mi)),
960
+ (P0, Pj, 0.0, Mj, col_for(Mj)),
961
+ ]
962
+
963
+ state = {"scale": float(scale_init)}
964
+
965
+ def draw():
966
+ clear_dynamic()
967
+
968
+ scale = float(state["scale"])
969
+ alpha = scale / qref
970
+
971
+ prev_w = None
972
+ for i, (a, e) in enumerate(zip(self.na, self.ne)):
973
+ a, e = int(a), int(e)
974
+ Pi, Pj, t_hat, L = self._tangent(a, e)
975
+
976
+ w = self._stable_normal(t_hat, prefer=prefer_axis)
977
+ if prev_w is not None and float(np.dot(w, prev_w)) < 0.0:
978
+ w = -w
979
+ prev_w = w
980
+
981
+ v = np.cross(t_hat, w)
982
+ nv = float(np.linalg.norm(v))
983
+ if nv < 1e-15:
984
+ continue
985
+ v = v / nv
986
+
987
+ Mi = float(Q[i, 0, 0])
988
+ Mj = float(Q[i, 1, 0])
989
+
990
+ for PA, PB, Ma, Mb, c in split_parts(Pi, Pj, Mi, Mj):
991
+ p1 = PA + 0.0 * width * v
992
+ p2 = PA + 0.0 * width * v + (alpha * Ma) * w
993
+ p3 = PB + 0.0 * width * v + (alpha * Mb) * w
994
+ p4 = PB + 0.0 * width * v
995
+
996
+ poly = Poly3DCollection(
997
+ [[p1, p2, p3, p4]],
998
+ alpha=0.25,
999
+ facecolor=c,
1000
+ edgecolor=c,
1001
+ linewidths=1.0,
1002
+ )
1003
+ ax.add_collection3d(poly)
1004
+ polys.append(poly)
1005
+
1006
+ q2 = PA + (alpha * Ma) * w
1007
+ q3 = PB + (alpha * Mb) * w
1008
+ ln = ax.plot([q2[0], q3[0]], [q2[1], q3[1]], [q2[2], q3[2]], color=c, lw=2.0)[0]
1009
+ lines.append(ln)
1010
+
1011
+ if show_end_labels:
1012
+ off = label_off * w
1013
+ end_texts.append(
1014
+ ax.text(Pi[0] + off[0], Pi[1] + off[1], Pi[2] + off[2], f"N{a}\n{Mi:+.3f}", fontsize=int(font_size))
1015
+ )
1016
+ end_texts.append(
1017
+ ax.text(Pj[0] + off[0], Pj[1] + off[1], Pj[2] + off[2], f"N{e}\n{Mj:+.3f}", fontsize=int(font_size))
1018
+ )
1019
+
1020
+ ax.set_xlabel("X [m]")
1021
+ ax.set_ylabel("Y [m]")
1022
+ ax.set_zlabel("Z [m]")
1023
+ ax.set_title(f"{kind} Diagramm 3D (Polygon) | base | ref={qref:.3g}")
1024
+
1025
+ if keep_view:
1026
+ ax.set_xlim(xlim)
1027
+ ax.set_ylim(ylim)
1028
+ ax.set_zlim(zlim)
1029
+
1030
+ fig.canvas.draw_idle()
1031
+
1032
+ fig.subplots_adjust(left=0.05, right=0.83, bottom=0.12)
1033
+
1034
+ ax_chk = fig.add_axes([0.85, 0.78, 0.13, 0.12])
1035
+ for sp in ax_chk.spines.values():
1036
+ sp.set_visible(False)
1037
+ ax_chk.set_xticks([])
1038
+ ax_chk.set_yticks([])
1039
+ ax_chk.set_title("Labels", fontsize=9)
1040
+
1041
+ chk = CheckButtons(ax_chk, ["Node IDs", "Elem IDs"], [False, False])
1042
+
1043
+ def on_chk(label):
1044
+ if label == "Node IDs":
1045
+ vis = not node_id_texts[0].get_visible() if node_id_texts else False
1046
+ for t in node_id_texts:
1047
+ t.set_visible(vis)
1048
+ elif label == "Elem IDs":
1049
+ vis = not elem_id_texts[0].get_visible() if elem_id_texts else False
1050
+ for t in elem_id_texts:
1051
+ t.set_visible(vis)
1052
+ fig.canvas.draw_idle()
1053
+
1054
+ chk.on_clicked(on_chk)
1055
+
1056
+ ax_box = fig.add_axes([0.15, 0.04, 0.18, 0.05])
1057
+ box = TextBox(ax_box, "Scale", initial=str(scale_init))
1058
+
1059
+ ax_scale = fig.add_axes([0.36, 0.05, 0.40, 0.03])
1060
+ s_scale = Slider(ax_scale, " ", 0.0, float(scale_init) * 200.0, valinit=float(scale_init))
1061
+
1062
+ def on_box_submit(text):
1063
+ try:
1064
+ v = float(str(text).replace(",", "."))
1065
+ except Exception:
1066
+ return
1067
+ state["scale"] = v
1068
+ s_scale.set_val(v)
1069
+ draw()
1070
+
1071
+ box.on_submit(on_box_submit)
1072
+
1073
+ def on_slider(_):
1074
+ state["scale"] = float(s_scale.val)
1075
+ box.set_val(str(state["scale"]))
1076
+ draw()
1077
+
1078
+ s_scale.on_changed(on_slider)
1079
+
1080
+ draw()
1081
+ return fig, ax, (box, s_scale), chk
1082
+
1083
+ def _get_restraints_df(self):
1084
+ df = getattr(self.Inp, "RestraintData", None)
1085
+ return df
1086
+
1087
+
1088
+ def _iter_restraint_springs(self, k_tol=1e-12):
1089
+ """
1090
+ Liefert Iterator über Restraint-Data Federn:
1091
+ yield (node, dof_int, k_value)
1092
+
1093
+ Erwartete Spalten (robust):
1094
+ Node,
1095
+ kx, ky, kz, krx, kry, krz (oder Varianten wie kX, kY, ...)
1096
+
1097
+ DOF Mapping (dein 7-DoF Layout):
1098
+ transl: ux=0, uy=1, uz=3
1099
+ rot : rx=5, ry=4, rz=2
1100
+ """
1101
+ df = self._get_restraints_df()
1102
+ if df is None:
1103
+ return
1104
+
1105
+ df = df.copy()
1106
+ df.columns = [str(c).strip().replace("\ufeff", "") for c in df.columns]
1107
+
1108
+ if "Node" not in df.columns:
1109
+ return
1110
+
1111
+ # robust column pick
1112
+ def pick(*names):
1113
+ for n in names:
1114
+ if n in df.columns:
1115
+ return n
1116
+ # try case-insensitive
1117
+ low = {c.lower(): c for c in df.columns}
1118
+ for n in names:
1119
+ if n.lower() in low:
1120
+ return low[n.lower()]
1121
+ return None
1122
+
1123
+ col_kx = pick("kx", "kX")
1124
+ col_ky = pick("ky", "kY")
1125
+ col_kz = pick("kz", "kZ")
1126
+ col_krx = pick("krx", "kRx", "kRX")
1127
+ col_kry = pick("kry", "kRy", "kRY")
1128
+ col_krz = pick("krz", "kRz", "kRZ")
1129
+
1130
+ # DOF map
1131
+ dof_map = []
1132
+ if col_kx: dof_map.append((0, col_kx))
1133
+ if col_ky: dof_map.append((1, col_ky))
1134
+ if col_kz: dof_map.append((3, col_kz))
1135
+ if col_krx: dof_map.append((5, col_krx))
1136
+ if col_kry: dof_map.append((4, col_kry))
1137
+ if col_krz: dof_map.append((2, col_krz))
1138
+
1139
+ for _, row in df.iterrows():
1140
+ try:
1141
+ n = int(row["Node"])
1142
+ except Exception:
1143
+ continue
1144
+
1145
+ for dof, col in dof_map:
1146
+ try:
1147
+ k = float(row[col])
1148
+ except Exception:
1149
+ continue
1150
+ if abs(k) <= float(k_tol):
1151
+ continue
1152
+ yield n, int(dof), float(k)
1153
+
1154
+
1155
+
1156
+ # ========================================================
1157
+ # 2D Support reactions (interaktiv)
1158
+ # ========================================================
1159
+ def plot_support_reactions_2d_interactive(
1160
+ self,
1161
+ invert_y=False,
1162
+ node_labels=True,
1163
+ elem_labels=False,
1164
+ show_forces=True,
1165
+ show_moments=True,
1166
+ scale_force_init=0.8,
1167
+ moment_radius_init=0.08,
1168
+ moment_scale_init=1.0,
1169
+ moment_kind_prefer="MY",
1170
+ robust_ref=True,
1171
+ Lref_frac=0.03,
1172
+ slider_force_max=10.0,
1173
+ ):
1174
+ fig, ax = self.plot_structure_2d(node_labels=node_labels, elem_labels=elem_labels)
1175
+ ax.set_title("Auflagerreaktionen (interaktiv)")
1176
+
1177
+ r = self._reaction_vector()
1178
+ support_nodes = self._support_nodes()
1179
+ if not support_nodes:
1180
+ ax.text(0.5, 0.5, "Keine Auflagerknoten in RestraintData gefunden.",
1181
+ transform=ax.transAxes, ha="center", va="center")
1182
+ return fig, ax, None
1183
+
1184
+ Lref = self._length_ref_xz(frac=Lref_frac)
1185
+
1186
+ Fvals = []
1187
+ for n in support_nodes:
1188
+ gdof = 7 * (n - 1)
1189
+ Fvals += [abs(float(r[gdof + 0])), abs(float(r[gdof + 3]))]
1190
+ Fvals = np.asarray(Fvals, dtype=float)
1191
+ if Fvals.size == 0:
1192
+ Fref = 1.0
1193
+ else:
1194
+ Fref = float(np.percentile(Fvals, 95)) if robust_ref else float(Fvals.max())
1195
+ Fref = max(Fref, 1e-12)
1196
+
1197
+ def pick_moment_components(gdof_base):
1198
+ Mz = float(r[gdof_base + 2])
1199
+ My = float(r[gdof_base + 4])
1200
+ Mx = float(r[gdof_base + 5])
1201
+ return Mx, My, Mz
1202
+
1203
+ def choose_moment(Mx, My, Mz):
1204
+ pref = moment_kind_prefer.upper()
1205
+ if pref == "MX":
1206
+ return Mx, "Mx"
1207
+ if pref == "MZ":
1208
+ return Mz, "Mz"
1209
+ return My, "My"
1210
+
1211
+ def draw(scale_force, moment_radius, moment_scale):
1212
+ self._clear_dyn(ax)
1213
+
1214
+ scale_force = float(scale_force)
1215
+ moment_radius = float(moment_radius)
1216
+ moment_scale = float(moment_scale)
1217
+
1218
+ alphaF = (scale_force * Lref) / Fref
1219
+
1220
+ for n in support_nodes:
1221
+ P = self._pt(int(n))
1222
+ x, z = float(P[0]), float(P[2])
1223
+ gdof = 7 * (int(n) - 1)
1224
+
1225
+ Rx = float(r[gdof + 0])
1226
+ Rz = float(r[gdof + 3])
1227
+
1228
+ Mx, My, Mz = pick_moment_components(gdof)
1229
+ Mplot, Mlab = choose_moment(Mx, My, Mz)
1230
+
1231
+ if show_forces and (abs(Rx) > 1e-15 or abs(Rz) > 1e-15):
1232
+ self._draw_force_arrow(ax, x, z, alphaF * Rx, alphaF * Rz, color="green")
1233
+ self._mark_dyn(ax.text(x, z, f"R{n}", fontsize=8, color="green", ha="right", va="top"))
1234
+ self._mark_dyn(ax.text(x, z, f"\nRx={Rx:+.3f} MN\nRz={Rz:+.3f} MN",
1235
+ fontsize=8, color="green", ha="left", va="top"))
1236
+
1237
+ if show_moments and abs(Mplot) > 1e-15:
1238
+ rr = moment_radius * moment_scale
1239
+ self._draw_moment_double_arrow(ax, x, z, Mplot, radius=rr, color="purple")
1240
+ self._mark_dyn(ax.text(x + rr, z + rr, f"{Mlab}={Mplot:+.3f} MNm", fontsize=8, color="purple"))
1241
+
1242
+ ax.relim()
1243
+ ax.autoscale_view()
1244
+ ax.set_aspect("equal", adjustable="datalim")
1245
+ if invert_y:
1246
+ ax.invert_yaxis()
1247
+ fig.canvas.draw_idle()
1248
+
1249
+ fig.subplots_adjust(bottom=0.25)
1250
+
1251
+ ax_sF = fig.add_axes([0.15, 0.14, 0.70, 0.03])
1252
+ s_force = Slider(ax_sF, "Scale Force", 0.0, float(slider_force_max), valinit=float(scale_force_init))
1253
+
1254
+ ax_sR = fig.add_axes([0.15, 0.09, 0.70, 0.03])
1255
+ s_rad = Slider(ax_sR, "Moment Radius", 0.0, float(moment_radius_init) * 20.0, valinit=float(moment_radius_init))
1256
+
1257
+ ax_sM = fig.add_axes([0.15, 0.04, 0.70, 0.03])
1258
+ s_msc = Slider(ax_sM, "Moment Scale", 0.0, float(moment_scale_init) * 20.0, valinit=float(moment_scale_init))
1259
+
1260
+ def update(_):
1261
+ draw(s_force.val, s_rad.val, s_msc.val)
1262
+
1263
+ s_force.on_changed(update)
1264
+ s_rad.on_changed(update)
1265
+ s_msc.on_changed(update)
1266
+
1267
+ draw(scale_force_init, moment_radius_init, moment_scale_init)
1268
+ return fig, ax, (s_force, s_rad, s_msc)
1269
+
1270
+ # ========================================================
1271
+ # 2D nodal loads (deine robuste Version)
1272
+ # ========================================================
1273
+ def plot_nodal_loads_2d_interactive(
1274
+ self,
1275
+ invert_y=False,
1276
+ node_labels=True,
1277
+ elem_labels=False,
1278
+ show_forces=True,
1279
+ show_moments=True,
1280
+ scale_force_init=0.8,
1281
+ moment_radius_init=0.08,
1282
+ moment_scale_init=1.0,
1283
+ moment_kind_prefer="MY",
1284
+ robust_ref=True,
1285
+ Lref_frac=0.03,
1286
+ slider_force_max=10.0,
1287
+ debug=False,
1288
+ ):
1289
+ """
1290
+ Knotenlasten (interaktiv) – robust gegen verschiedene DoF-Schreibweisen
1291
+ und verschiedene Value-Spaltennamen.
1292
+
1293
+ Erwartetes Format (lang):
1294
+ Node | Dof | Value[...]
1295
+
1296
+ Unterstützte Dof-Strings:
1297
+ Fx,Fy,Fz,Mx,My,Mz
1298
+ X,Y,Z / Ux,Uy,Uz / Rx,Ry,Rz
1299
+ numerisch: 0,1,3,5,4,2 (Mapping passend zu deinem 7-DoF-Layout)
1300
+ """
1301
+ fig, ax = self.plot_structure_2d(node_labels=node_labels, elem_labels=elem_labels)
1302
+ ax.set_title("Knotenlasten (interaktiv) – aus NodalForces.csv")
1303
+
1304
+ dfL = getattr(self.Inp, "NodalForces", None)
1305
+ if dfL is None:
1306
+ dfL = getattr(self.Inp, "NodalForcesData", None)
1307
+ if dfL is None:
1308
+ ax.text(0.5, 0.5, "Keine NodalForces Tabelle in Inp gefunden.",
1309
+ transform=ax.transAxes, ha="center", va="center")
1310
+ return fig, ax, None
1311
+
1312
+ dfL = dfL.copy()
1313
+ dfL.columns = [str(c).strip().replace("\ufeff", "") for c in dfL.columns]
1314
+
1315
+ if "Node" not in dfL.columns or "Dof" not in dfL.columns:
1316
+ ax.text(0.5, 0.5, f"NodalForces: Spalten fehlen. Gefunden: {dfL.columns.tolist()}",
1317
+ transform=ax.transAxes, ha="center", va="center")
1318
+ return fig, ax, None
1319
+
1320
+ value_candidates = ["Value[MN/MNm]", "Value", "value", "P[N]", "P", "F", "Load"]
1321
+ val_col = next((c for c in value_candidates if c in dfL.columns), None)
1322
+ if val_col is None:
1323
+ ax.text(0.5, 0.5, f"NodalForces: keine Value-Spalte gefunden.\nSpalten: {dfL.columns.tolist()}",
1324
+ transform=ax.transAxes, ha="center", va="center")
1325
+ return fig, ax, None
1326
+
1327
+ dfL = dfL.dropna(subset=["Node", "Dof", val_col])
1328
+
1329
+ if debug:
1330
+ print("NodalForces columns:", dfL.columns.tolist())
1331
+ print("Using value column:", val_col)
1332
+ print(dfL.head())
1333
+
1334
+ def norm_dof(dof_raw):
1335
+ s = str(dof_raw).strip().upper()
1336
+ aliases = {
1337
+ "FX": "FX", "FY": "FY", "FZ": "FZ",
1338
+ "MX": "MX", "MY": "MY", "MZ": "MZ",
1339
+ "X": "FX", "Y": "FY", "Z": "FZ",
1340
+ "UX": "FX", "UY": "FY", "UZ": "FZ",
1341
+ "RX": "MX", "RY": "MY", "RZ": "MZ",
1342
+ }
1343
+ if s in aliases:
1344
+ return aliases[s]
1345
+ try:
1346
+ d = int(float(s))
1347
+ num_map = {0: "FX", 1: "FY", 3: "FZ", 5: "MX", 4: "MY", 2: "MZ"}
1348
+ return num_map.get(d, None)
1349
+ except Exception:
1350
+ return None
1351
+
1352
+ data = {}
1353
+ for _, row in dfL.iterrows():
1354
+ try:
1355
+ n = int(row["Node"])
1356
+ except Exception:
1357
+ continue
1358
+
1359
+ dof = norm_dof(row["Dof"])
1360
+ if dof is None:
1361
+ if debug:
1362
+ print(f"Warnung: unbekannter Dof '{row['Dof']}' (Node {n})")
1363
+ continue
1364
+
1365
+ try:
1366
+ val = float(row[val_col])
1367
+ except Exception:
1368
+ continue
1369
+
1370
+ if n not in data:
1371
+ data[n] = {"FX": 0.0, "FY": 0.0, "FZ": 0.0, "MX": 0.0, "MY": 0.0, "MZ": 0.0}
1372
+ data[n][dof] += val
1373
+
1374
+ nodes = sorted(data.keys())
1375
+ if len(nodes) == 0:
1376
+ ax.text(0.5, 0.5, "Keine gültigen Knotenlasten gefunden (DoF/Value prüfen).",
1377
+ transform=ax.transAxes, ha="center", va="center")
1378
+ return fig, ax, None
1379
+
1380
+ Fx = np.array([data[n]["FX"] for n in nodes], dtype=float)
1381
+ Fz = np.array([data[n]["FZ"] for n in nodes], dtype=float)
1382
+ Mx = np.array([data[n]["MX"] for n in nodes], dtype=float)
1383
+ My = np.array([data[n]["MY"] for n in nodes], dtype=float)
1384
+ Mz = np.array([data[n]["MZ"] for n in nodes], dtype=float)
1385
+
1386
+ Lref = self._length_ref_xz(frac=Lref_frac)
1387
+ Fabs = np.hstack([np.abs(Fx), np.abs(Fz)])
1388
+ if Fabs.size == 0:
1389
+ Fref = 1.0
1390
+ else:
1391
+ Fref = float(np.percentile(Fabs, 95)) if robust_ref else float(Fabs.max())
1392
+ Fref = max(Fref, 1e-12)
1393
+
1394
+ def choose_moment(mx, my, mz):
1395
+ pref = str(moment_kind_prefer).upper()
1396
+ if pref == "MX":
1397
+ return float(mx), "Mx"
1398
+ if pref == "MZ":
1399
+ return float(mz), "Mz"
1400
+ return float(my), "My"
1401
+
1402
+ def draw(scale_force, moment_radius, moment_scale):
1403
+ self._clear_dyn(ax)
1404
+
1405
+ scale_force = float(scale_force)
1406
+ moment_radius = float(moment_radius)
1407
+ moment_scale = float(moment_scale)
1408
+
1409
+ alphaF = (scale_force * Lref) / Fref
1410
+
1411
+ for n, fx, fz, mx, my, mz in zip(nodes, Fx, Fz, Mx, My, Mz):
1412
+ P = self._pt(int(n))
1413
+ x, z = float(P[0]), float(P[2])
1414
+
1415
+ if show_forces and (abs(fx) > 1e-15 or abs(fz) > 1e-15):
1416
+ self._draw_force_arrow(ax, x, z, alphaF * fx, alphaF * fz, color="orange")
1417
+ self._mark_dyn(ax.text(x, z, f"F{n}", fontsize=8, color="orange", ha="right", va="top"))
1418
+ self._mark_dyn(ax.text(x, z, f"\nFx={fx:+.3f} MN\nFz={fz:+.3f} MN",
1419
+ fontsize=8, color="orange", ha="left", va="top"))
1420
+
1421
+ Mplot, Mlab = choose_moment(mx, my, mz)
1422
+ if show_moments and abs(Mplot) > 1e-15:
1423
+ rr = moment_radius * moment_scale
1424
+ self._draw_moment_double_arrow(ax, x, z, Mplot, radius=rr, color="brown")
1425
+ self._mark_dyn(ax.text(x + rr, z + rr, f"{Mlab}={Mplot:+.3f} MNm", fontsize=8, color="brown"))
1426
+
1427
+ ax.relim()
1428
+ ax.autoscale_view()
1429
+ ax.set_aspect("equal", adjustable="datalim")
1430
+ if invert_y:
1431
+ ax.invert_yaxis()
1432
+ fig.canvas.draw_idle()
1433
+
1434
+ fig.subplots_adjust(bottom=0.25)
1435
+
1436
+ ax_sF = fig.add_axes([0.15, 0.14, 0.70, 0.03])
1437
+ s_force = Slider(ax_sF, "Scale Force", 0.0, float(slider_force_max), valinit=float(scale_force_init))
1438
+
1439
+ ax_sR = fig.add_axes([0.15, 0.09, 0.70, 0.03])
1440
+ s_rad = Slider(ax_sR, "Moment Radius", 0.0, float(moment_radius_init) * 20.0, valinit=float(moment_radius_init))
1441
+
1442
+ ax_sM = fig.add_axes([0.15, 0.04, 0.70, 0.03])
1443
+ s_msc = Slider(ax_sM, "Moment Scale", 0.0, float(moment_scale_init) * 20.0, valinit=float(moment_scale_init))
1444
+
1445
+ def update(_):
1446
+ draw(s_force.val, s_rad.val, s_msc.val)
1447
+
1448
+ s_force.on_changed(update)
1449
+ s_rad.on_changed(update)
1450
+ s_msc.on_changed(update)
1451
+
1452
+ draw(scale_force_init, moment_radius_init, moment_scale_init)
1453
+ return fig, ax, (s_force, s_rad, s_msc)