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.
- KIB_LAP/Betonbau/TEST_Rectangular.py +21 -0
- KIB_LAP/Betonbau/beam_rectangular.py +4 -0
- KIB_LAP/FACHWERKEBEN/Elements.py +209 -0
- KIB_LAP/FACHWERKEBEN/InputData.py +118 -0
- KIB_LAP/FACHWERKEBEN/Iteration.py +967 -0
- KIB_LAP/FACHWERKEBEN/Materials.py +30 -0
- KIB_LAP/FACHWERKEBEN/Plotting.py +681 -0
- KIB_LAP/FACHWERKEBEN/__init__.py +4 -0
- KIB_LAP/FACHWERKEBEN/main.py +27 -0
- KIB_LAP/Plattentragwerke/PlateBendingKirchhoff.py +36 -29
- KIB_LAP/STABRAUM/InputData.py +13 -2
- KIB_LAP/STABRAUM/Output_Data.py +61 -0
- KIB_LAP/STABRAUM/Plotting.py +1453 -0
- KIB_LAP/STABRAUM/Programm.py +518 -1026
- KIB_LAP/STABRAUM/Steifigkeitsmatrix.py +338 -117
- KIB_LAP/STABRAUM/main.py +58 -0
- KIB_LAP/STABRAUM/results.py +37 -0
- KIB_LAP/Scheibe/Assemble_Stiffness.py +246 -0
- KIB_LAP/Scheibe/Element_Stiffness.py +362 -0
- KIB_LAP/Scheibe/Meshing.py +365 -0
- KIB_LAP/Scheibe/Output.py +34 -0
- KIB_LAP/Scheibe/Plotting.py +722 -0
- KIB_LAP/Scheibe/Shell_Calculation.py +523 -0
- KIB_LAP/Scheibe/Testing_Mesh.py +25 -0
- KIB_LAP/Scheibe/__init__.py +14 -0
- KIB_LAP/Scheibe/main.py +33 -0
- KIB_LAP/StabEbenRitz/Biegedrillknicken.py +757 -0
- KIB_LAP/StabEbenRitz/Biegedrillknicken_Trigeometry.py +328 -0
- KIB_LAP/StabEbenRitz/Querschnittswerte.py +527 -0
- KIB_LAP/StabEbenRitz/Stabberechnung_Klasse.py +868 -0
- KIB_LAP/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
- KIB_LAP/plate_buckling_cpp.cp313-win_amd64.pyd +0 -0
- {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/METADATA +1 -1
- {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/RECORD +37 -19
- Examples/Cross_Section_Thin.py +0 -61
- KIB_LAP/Betonbau/Bemessung_Zust_II.py +0 -648
- KIB_LAP/Betonbau/Iterative_Design.py +0 -723
- KIB_LAP/Plattentragwerke/NumInte.cpp +0 -23
- KIB_LAP/Plattentragwerke/NumericalIntegration.cpp +0 -23
- KIB_LAP/Plattentragwerke/plate_bending_cpp.cp313-win_amd64.pyd +0 -0
- KIB_LAP/main.py +0 -2
- {Examples → KIB_LAP/StabEbenRitz}/__init__.py +0 -0
- {kib_lap-0.5.dist-info → kib_lap-0.7.7.dist-info}/WHEEL +0 -0
- {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)
|