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,722 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import matplotlib.patches as patches
|
|
4
|
+
import matplotlib.cm as cm
|
|
5
|
+
import matplotlib.colors as colors
|
|
6
|
+
from matplotlib.widgets import Slider
|
|
7
|
+
from scipy.interpolate import griddata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShellPlotter:
|
|
11
|
+
def __init__(self, model):
|
|
12
|
+
"""
|
|
13
|
+
model: ShellCalculation Instanz (hat Meshing, AssembleMatrix, stress_elem_avg, ...)
|
|
14
|
+
"""
|
|
15
|
+
self.m = model
|
|
16
|
+
self.A = self.m.AssembleMatrix # Assembled_Matrices
|
|
17
|
+
|
|
18
|
+
# ---------- helpers ----------
|
|
19
|
+
def _mesh_bounds(self, pad_rel=0.05, pad_abs=0.0):
|
|
20
|
+
"""
|
|
21
|
+
Robuste Auto-Achsen: aus NL min/max + Rand.
|
|
22
|
+
pad_rel: relativer Rand (5% der Größe)
|
|
23
|
+
pad_abs: absoluter Rand (z.B. 0.1 m)
|
|
24
|
+
"""
|
|
25
|
+
NL = np.asarray(self.m.Meshing.NL, dtype=float)
|
|
26
|
+
xmin, ymin = NL.min(axis=0)
|
|
27
|
+
xmax, ymax = NL.max(axis=0)
|
|
28
|
+
dx = max(xmax - xmin, 1e-12)
|
|
29
|
+
dy = max(ymax - ymin, 1e-12)
|
|
30
|
+
pad = max(pad_abs, pad_rel * max(dx, dy))
|
|
31
|
+
return xmin - pad, xmax + pad, ymin - pad, ymax + pad
|
|
32
|
+
|
|
33
|
+
def _element_coords(self, el):
|
|
34
|
+
return np.array([self.m.Meshing.NL[nid - 1] for nid in el], dtype=float)
|
|
35
|
+
|
|
36
|
+
def _point_in_poly(self, x, y, poly):
|
|
37
|
+
"""
|
|
38
|
+
Ray casting: True wenn Punkt (x,y) im Polygon (convex/concave) liegt.
|
|
39
|
+
poly: shape (n,2)
|
|
40
|
+
"""
|
|
41
|
+
inside = False
|
|
42
|
+
n = len(poly)
|
|
43
|
+
for i in range(n):
|
|
44
|
+
x1, y1 = poly[i]
|
|
45
|
+
x2, y2 = poly[(i + 1) % n]
|
|
46
|
+
cond = ((y1 > y) != (y2 > y)) and (x < (x2 - x1) * (y - y1) / (y2 - y1 + 1e-30) + x1)
|
|
47
|
+
if cond:
|
|
48
|
+
inside = not inside
|
|
49
|
+
return inside
|
|
50
|
+
|
|
51
|
+
def _mask_points_in_mesh(self, xy_points):
|
|
52
|
+
"""
|
|
53
|
+
xy_points: array shape (N,2)
|
|
54
|
+
returns: bool mask shape (N,) -> True if point lies inside any element polygon
|
|
55
|
+
"""
|
|
56
|
+
NL = np.asarray(self.m.Meshing.NL, float)
|
|
57
|
+
EL = np.asarray(self.m.Meshing.EL, int)
|
|
58
|
+
|
|
59
|
+
mask = np.zeros((xy_points.shape[0],), dtype=bool)
|
|
60
|
+
|
|
61
|
+
# Optional: Precompute polygons once (speed-up)
|
|
62
|
+
polys = []
|
|
63
|
+
for el in EL:
|
|
64
|
+
poly = np.array([NL[nid - 1] for nid in el], dtype=float)
|
|
65
|
+
polys.append(poly)
|
|
66
|
+
|
|
67
|
+
for k, (x, y) in enumerate(xy_points):
|
|
68
|
+
inside_any = False
|
|
69
|
+
for poly in polys:
|
|
70
|
+
if self._point_in_poly(float(x), float(y), poly):
|
|
71
|
+
inside_any = True
|
|
72
|
+
break
|
|
73
|
+
mask[k] = inside_any
|
|
74
|
+
|
|
75
|
+
return mask
|
|
76
|
+
|
|
77
|
+
def _mask_cut_points_in_mesh(self, cut_direction, cut_position, coords_1d):
|
|
78
|
+
"""
|
|
79
|
+
coords_1d: array der Koordinate entlang der Cut-Achse
|
|
80
|
+
(y bei x=const, x bei y=const)
|
|
81
|
+
returns: bool mask gleicher Länge, True wenn Punkt in irgendeinem Element liegt
|
|
82
|
+
"""
|
|
83
|
+
if cut_direction.lower() == "x":
|
|
84
|
+
xy = np.column_stack([np.full_like(coords_1d, float(cut_position)), coords_1d.astype(float)])
|
|
85
|
+
else:
|
|
86
|
+
xy = np.column_stack([coords_1d.astype(float), np.full_like(coords_1d, float(cut_position))])
|
|
87
|
+
return self._mask_points_in_mesh(xy)
|
|
88
|
+
|
|
89
|
+
# ---------- plots ----------
|
|
90
|
+
def plot_mesh(self, show_node_ids=False, show_elem_ids=False, show_springs=True, spring_scale=1.0):
|
|
91
|
+
fig, ax = plt.subplots()
|
|
92
|
+
ax.set_aspect("equal", adjustable="box")
|
|
93
|
+
ax.grid(True)
|
|
94
|
+
|
|
95
|
+
# elements
|
|
96
|
+
for e, el in enumerate(self.m.Meshing.EL):
|
|
97
|
+
coords = self._element_coords(el)
|
|
98
|
+
ax.add_patch(patches.Polygon(coords, closed=True, fill=False, edgecolor="r", linewidth=1.0))
|
|
99
|
+
|
|
100
|
+
if show_elem_ids:
|
|
101
|
+
c = coords.mean(axis=0)
|
|
102
|
+
ax.text(c[0], c[1], str(e + 1), fontsize=9, ha="center", va="center")
|
|
103
|
+
|
|
104
|
+
# nodes
|
|
105
|
+
if show_node_ids:
|
|
106
|
+
NL = np.asarray(self.m.Meshing.NL, float)
|
|
107
|
+
ax.scatter(NL[:, 0], NL[:, 1], s=15)
|
|
108
|
+
for i, (x, y) in enumerate(NL, start=1):
|
|
109
|
+
ax.text(x, y, str(i), fontsize=9, ha="right", va="bottom")
|
|
110
|
+
|
|
111
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds()
|
|
112
|
+
ax.set_xlim(xmin, xmax)
|
|
113
|
+
ax.set_ylim(ymin, ymax)
|
|
114
|
+
|
|
115
|
+
if show_springs:
|
|
116
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
117
|
+
|
|
118
|
+
plt.show()
|
|
119
|
+
|
|
120
|
+
def plot_deflected_interactive(self, factor0=1000.0, factor_max=5000.0, show_undeformed=True,
|
|
121
|
+
show_springs=True, spring_scale=1.0):
|
|
122
|
+
mesh = self.m.Meshing
|
|
123
|
+
EL = mesh.EL
|
|
124
|
+
NL = mesh.NL
|
|
125
|
+
|
|
126
|
+
fig, ax = plt.subplots()
|
|
127
|
+
plt.subplots_adjust(bottom=0.18)
|
|
128
|
+
ax.set_aspect("equal", adjustable="box")
|
|
129
|
+
ax.grid(True)
|
|
130
|
+
|
|
131
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds(pad_rel=0.08)
|
|
132
|
+
ax.set_xlim(xmin, xmax)
|
|
133
|
+
ax.set_ylim(ymin, ymax)
|
|
134
|
+
|
|
135
|
+
# undeformed
|
|
136
|
+
if show_undeformed:
|
|
137
|
+
for el in EL:
|
|
138
|
+
coords = [NL[nid - 1] for nid in el]
|
|
139
|
+
coords.append(coords[0])
|
|
140
|
+
ax.add_patch(patches.Polygon(coords, closed=True, fill=False, edgecolor="0.7", linewidth=1.0))
|
|
141
|
+
|
|
142
|
+
# deflected artists
|
|
143
|
+
poly_def = []
|
|
144
|
+
for _ in EL:
|
|
145
|
+
p = patches.Polygon([[0, 0]], closed=True, fill=False, edgecolor="r", linewidth=1.5)
|
|
146
|
+
ax.add_patch(p)
|
|
147
|
+
poly_def.append(p)
|
|
148
|
+
|
|
149
|
+
ax_slider = plt.axes([0.15, 0.06, 0.70, 0.03])
|
|
150
|
+
s_factor = Slider(ax_slider, "Scale", 0.0, factor_max, valinit=factor0)
|
|
151
|
+
|
|
152
|
+
# prefetch displacements (interleaved per element column)
|
|
153
|
+
Ue = self.m.AssembleMatrix.disp_element_matrix # shape (8, NoE)
|
|
154
|
+
|
|
155
|
+
def _update(factor):
|
|
156
|
+
for e, el in enumerate(EL):
|
|
157
|
+
coords_def = []
|
|
158
|
+
ue = Ue[:, e]
|
|
159
|
+
for local_i, nid in enumerate(el):
|
|
160
|
+
x0, y0 = NL[nid - 1]
|
|
161
|
+
ux = ue[2 * local_i + 0]
|
|
162
|
+
uy = ue[2 * local_i + 1]
|
|
163
|
+
coords_def.append([x0 + ux * factor, y0 + uy * factor])
|
|
164
|
+
coords_def.append(coords_def[0])
|
|
165
|
+
poly_def[e].set_xy(coords_def)
|
|
166
|
+
fig.canvas.draw_idle()
|
|
167
|
+
|
|
168
|
+
s_factor.on_changed(_update)
|
|
169
|
+
_update(factor0)
|
|
170
|
+
|
|
171
|
+
if show_springs:
|
|
172
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
173
|
+
|
|
174
|
+
plt.show()
|
|
175
|
+
|
|
176
|
+
def plot_inner_element_forces(self, field="sigma_x", show_principal=False, show_springs=True, spring_scale=1.0):
|
|
177
|
+
if not hasattr(self.m, "stress_elem_avg"):
|
|
178
|
+
self.m.CalculateInnerElementForces_Gauss()
|
|
179
|
+
|
|
180
|
+
field_idx = {"sigma_x": 0, "sigma_y": 1, "tau_xy": 2,
|
|
181
|
+
"n_x": 0, "n_y": 1, "n_xy": 2}
|
|
182
|
+
|
|
183
|
+
use_n = field.startswith("n_")
|
|
184
|
+
j = field_idx[field]
|
|
185
|
+
|
|
186
|
+
vals = (self.m.n_elem_avg[:, j] if use_n else self.m.stress_elem_avg[:, j])
|
|
187
|
+
vals = np.asarray(vals, dtype=float)
|
|
188
|
+
|
|
189
|
+
vmin, vmax = float(vals.min()), float(vals.max())
|
|
190
|
+
if np.isclose(vmin, vmax):
|
|
191
|
+
vmin -= 1.0
|
|
192
|
+
vmax += 1.0
|
|
193
|
+
|
|
194
|
+
norm = colors.Normalize(vmin=vmin, vmax=vmax)
|
|
195
|
+
cmap = cm.viridis
|
|
196
|
+
|
|
197
|
+
fig, ax = plt.subplots()
|
|
198
|
+
ax.set_aspect("equal", adjustable="box")
|
|
199
|
+
ax.grid(True)
|
|
200
|
+
|
|
201
|
+
for e, el in enumerate(self.m.Meshing.EL):
|
|
202
|
+
coords = self._element_coords(el)
|
|
203
|
+
polygon = patches.Polygon(coords, closed=True, edgecolor="k", facecolor=cmap(norm(vals[e])))
|
|
204
|
+
ax.add_patch(polygon)
|
|
205
|
+
|
|
206
|
+
if show_principal and (not use_n):
|
|
207
|
+
sx, sy, txy = self.m.stress_elem_avg[e, :]
|
|
208
|
+
s_avg = 0.5 * (sx + sy)
|
|
209
|
+
R = np.sqrt((0.5 * (sx - sy)) ** 2 + txy ** 2)
|
|
210
|
+
s1 = s_avg + R
|
|
211
|
+
s2 = s_avg - R
|
|
212
|
+
theta = 0.5 * np.arctan2(2.0 * txy, (sx - sy))
|
|
213
|
+
|
|
214
|
+
c = coords.mean(axis=0)
|
|
215
|
+
L = 0.15 * max((coords[:, 0].max() - coords[:, 0].min()),
|
|
216
|
+
(coords[:, 1].max() - coords[:, 1].min()), 1e-9)
|
|
217
|
+
c1 = "b" if s1 > 0 else "r"
|
|
218
|
+
c2 = "b" if s2 > 0 else "r"
|
|
219
|
+
ax.arrow(c[0], c[1], L * np.cos(theta), L * np.sin(theta),
|
|
220
|
+
head_width=0.03 * L, head_length=0.03 * L, fc=c1, ec=c1)
|
|
221
|
+
ax.arrow(c[0], c[1], L * np.cos(theta + np.pi / 2), L * np.sin(theta + np.pi / 2),
|
|
222
|
+
head_width=0.03 * L, head_length=0.03 * L, fc=c2, ec=c2)
|
|
223
|
+
|
|
224
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds(pad_rel=0.05)
|
|
225
|
+
ax.set_xlim(xmin, xmax)
|
|
226
|
+
ax.set_ylim(ymin, ymax)
|
|
227
|
+
ax.set_title(field)
|
|
228
|
+
|
|
229
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
230
|
+
sm.set_array([])
|
|
231
|
+
fig.colorbar(sm, ax=ax, orientation="vertical", label=field)
|
|
232
|
+
|
|
233
|
+
if show_springs:
|
|
234
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
235
|
+
|
|
236
|
+
plt.show()
|
|
237
|
+
|
|
238
|
+
def plot_stress_along_cut(self, cut_position, cut_direction="x", field="sigma_x",
|
|
239
|
+
ngrid=250, method="linear",
|
|
240
|
+
restrict_to_mesh=True, show_cut_line=False):
|
|
241
|
+
"""
|
|
242
|
+
Plot along a straight cut line with interpolation of element-center values.
|
|
243
|
+
|
|
244
|
+
IMPORTANT FIX:
|
|
245
|
+
If restrict_to_mesh=True, points on the cut that lie outside the actual mesh
|
|
246
|
+
(e.g. holes / gaps in multi-patch) are removed by geometric masking.
|
|
247
|
+
|
|
248
|
+
cut_direction:
|
|
249
|
+
"x" -> vertical line x = cut_position (plot vs y)
|
|
250
|
+
"y" -> horizontal line y = cut_position (plot vs x)
|
|
251
|
+
|
|
252
|
+
field:
|
|
253
|
+
"sigma_x" | "sigma_y" | "tau_xy" | "n_x" | "n_y" | "n_xy"
|
|
254
|
+
"""
|
|
255
|
+
if not hasattr(self.m, "stress_elem_avg"):
|
|
256
|
+
self.m.CalculateInnerElementForces_Gauss()
|
|
257
|
+
|
|
258
|
+
field_idx = {
|
|
259
|
+
"sigma_x": 0, "sigma_y": 1, "tau_xy": 2,
|
|
260
|
+
"n_x": 0, "n_y": 1, "n_xy": 2
|
|
261
|
+
}
|
|
262
|
+
if field not in field_idx:
|
|
263
|
+
raise ValueError(f"Unknown field '{field}'. Choose from {list(field_idx.keys())}")
|
|
264
|
+
|
|
265
|
+
use_n = field.startswith("n_")
|
|
266
|
+
j = field_idx[field]
|
|
267
|
+
|
|
268
|
+
# Element centers and values
|
|
269
|
+
n_elem = len(self.m.Meshing.EL)
|
|
270
|
+
centroids = np.zeros((n_elem, 2), dtype=float)
|
|
271
|
+
vals = np.zeros(n_elem, dtype=float)
|
|
272
|
+
|
|
273
|
+
for e, el in enumerate(self.m.Meshing.EL):
|
|
274
|
+
coords = self._element_coords(el)
|
|
275
|
+
centroids[e, :] = coords.mean(axis=0)
|
|
276
|
+
vals[e] = float(self.m.n_elem_avg[e, j] if use_n else self.m.stress_elem_avg[e, j])
|
|
277
|
+
|
|
278
|
+
# Interpolation bounds (centroid-based)
|
|
279
|
+
xmin, ymin = centroids.min(axis=0)
|
|
280
|
+
xmax, ymax = centroids.max(axis=0)
|
|
281
|
+
|
|
282
|
+
# clamp cut_position
|
|
283
|
+
if cut_direction.lower() == "x":
|
|
284
|
+
cut_position = float(np.clip(cut_position, xmin, xmax))
|
|
285
|
+
elif cut_direction.lower() == "y":
|
|
286
|
+
cut_position = float(np.clip(cut_position, ymin, ymax))
|
|
287
|
+
else:
|
|
288
|
+
raise ValueError("cut_direction must be 'x' or 'y'")
|
|
289
|
+
|
|
290
|
+
# Interpolation grid
|
|
291
|
+
grid_x, grid_y = np.mgrid[
|
|
292
|
+
xmin:xmax:complex(ngrid),
|
|
293
|
+
ymin:ymax:complex(ngrid)
|
|
294
|
+
]
|
|
295
|
+
grid_z = griddata(centroids, vals, (grid_x, grid_y), method=method)
|
|
296
|
+
|
|
297
|
+
# Extract cut
|
|
298
|
+
if cut_direction.lower() == "x":
|
|
299
|
+
cut_index = int(np.argmin(np.abs(grid_x[:, 0] - cut_position)))
|
|
300
|
+
cut_vals = grid_z[cut_index, :]
|
|
301
|
+
cut_coords = grid_y[cut_index, :]
|
|
302
|
+
xlabel = "y"
|
|
303
|
+
title = f"{field} along x = {cut_position:.6g}"
|
|
304
|
+
else:
|
|
305
|
+
cut_index = int(np.argmin(np.abs(grid_y[0, :] - cut_position)))
|
|
306
|
+
cut_vals = grid_z[:, cut_index]
|
|
307
|
+
cut_coords = grid_x[:, cut_index]
|
|
308
|
+
xlabel = "x"
|
|
309
|
+
title = f"{field} along y = {cut_position:.6g}"
|
|
310
|
+
|
|
311
|
+
# Remove NaNs
|
|
312
|
+
mask = np.isfinite(cut_vals)
|
|
313
|
+
if mask.sum() < 5:
|
|
314
|
+
print("WARNING: Almost no values on cut (NaNs). Try method='nearest' or choose another cut_position.")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
xplot = cut_coords[mask]
|
|
318
|
+
yplot = cut_vals[mask]
|
|
319
|
+
|
|
320
|
+
# IMPORTANT: remove points outside actual mesh (holes / gaps)
|
|
321
|
+
if restrict_to_mesh:
|
|
322
|
+
geom_mask = self._mask_cut_points_in_mesh(cut_direction, cut_position, xplot)
|
|
323
|
+
xplot = xplot[geom_mask]
|
|
324
|
+
yplot = yplot[geom_mask]
|
|
325
|
+
|
|
326
|
+
if len(xplot) < 5:
|
|
327
|
+
print("WARNING: Cut is mostly outside mesh (after geometric masking). Choose another cut_position.")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
vmin = float(np.min(yplot))
|
|
331
|
+
vmax = float(np.max(yplot))
|
|
332
|
+
|
|
333
|
+
plt.figure()
|
|
334
|
+
plt.plot(xplot, yplot, label=title)
|
|
335
|
+
|
|
336
|
+
# Min / Max lines
|
|
337
|
+
plt.axhline(vmin, linestyle="--", linewidth=1, label=f"min = {vmin:.4g}")
|
|
338
|
+
plt.axhline(vmax, linestyle="--", linewidth=1, label=f"max = {vmax:.4g}")
|
|
339
|
+
|
|
340
|
+
# Info box
|
|
341
|
+
info = f"min = {vmin:.4g}\nmax = {vmax:.4g}"
|
|
342
|
+
plt.gca().text(
|
|
343
|
+
0.02, 0.98, info,
|
|
344
|
+
transform=plt.gca().transAxes,
|
|
345
|
+
va="top",
|
|
346
|
+
ha="left",
|
|
347
|
+
bbox=dict(boxstyle="round", alpha=0.8)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
plt.xlabel(xlabel)
|
|
351
|
+
plt.ylabel(field)
|
|
352
|
+
plt.grid(True)
|
|
353
|
+
plt.legend()
|
|
354
|
+
plt.title(title)
|
|
355
|
+
plt.tight_layout()
|
|
356
|
+
plt.show()
|
|
357
|
+
|
|
358
|
+
# Optional: show cut line on mesh (quick visual check)
|
|
359
|
+
if show_cut_line:
|
|
360
|
+
fig, ax = plt.subplots()
|
|
361
|
+
ax.set_aspect("equal", adjustable="box")
|
|
362
|
+
ax.grid(True)
|
|
363
|
+
|
|
364
|
+
# mesh outline
|
|
365
|
+
NL = np.asarray(self.m.Meshing.NL, float)
|
|
366
|
+
EL = np.asarray(self.m.Meshing.EL, int)
|
|
367
|
+
for el in EL:
|
|
368
|
+
coords = np.array([NL[nid - 1] for nid in el], float)
|
|
369
|
+
ax.add_patch(patches.Polygon(coords, closed=True, fill=False, edgecolor="0.7", linewidth=1.0))
|
|
370
|
+
|
|
371
|
+
xmin2, xmax2, ymin2, ymax2 = self._mesh_bounds(pad_rel=0.05)
|
|
372
|
+
ax.set_xlim(xmin2, xmax2)
|
|
373
|
+
ax.set_ylim(ymin2, ymax2)
|
|
374
|
+
|
|
375
|
+
if cut_direction.lower() == "x":
|
|
376
|
+
ax.axvline(cut_position, linestyle="--")
|
|
377
|
+
else:
|
|
378
|
+
ax.axhline(cut_position, linestyle="--")
|
|
379
|
+
|
|
380
|
+
plt.show()
|
|
381
|
+
|
|
382
|
+
# ---------- springs overlay ----------
|
|
383
|
+
def _draw_springs(self, ax, NL, Lref, spring_scale=0.05, show_k=False):
|
|
384
|
+
"""
|
|
385
|
+
Draw spring symbols for boundary conditions from self.A.BC.
|
|
386
|
+
Expects BC columns: No, DOF, cf in [MN/m] (optional)
|
|
387
|
+
"""
|
|
388
|
+
if not hasattr(self.A, "BC"):
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
bc = self.A.BC
|
|
392
|
+
arts = []
|
|
393
|
+
|
|
394
|
+
def spring_poly(L=1.0, nzig=6, amp=0.15):
|
|
395
|
+
xs = np.linspace(0, L, 2 * nzig + 1)
|
|
396
|
+
ys = np.zeros_like(xs)
|
|
397
|
+
for k in range(1, len(xs) - 1):
|
|
398
|
+
ys[k] = amp * (1 if k % 2 else -1)
|
|
399
|
+
return np.column_stack([xs, ys])
|
|
400
|
+
|
|
401
|
+
for i in range(len(bc)):
|
|
402
|
+
node = bc["No"].iloc[i]
|
|
403
|
+
dof = str(bc["DOF"].iloc[i]).strip().lower()
|
|
404
|
+
|
|
405
|
+
if not str(node).isdigit():
|
|
406
|
+
continue
|
|
407
|
+
node = int(node)
|
|
408
|
+
|
|
409
|
+
x, y = NL[node - 1]
|
|
410
|
+
|
|
411
|
+
Ls = spring_scale * 0.25 * Lref
|
|
412
|
+
amp = 0.10 * Ls
|
|
413
|
+
pts = spring_poly(L=Ls, nzig=5, amp=amp)
|
|
414
|
+
|
|
415
|
+
if dof == "x":
|
|
416
|
+
pts[:, 0] *= -1.0
|
|
417
|
+
pts[:, 0] += x
|
|
418
|
+
pts[:, 1] += y
|
|
419
|
+
line, = ax.plot(pts[:, 0], pts[:, 1], linewidth=1.5)
|
|
420
|
+
arts.append(line)
|
|
421
|
+
|
|
422
|
+
wall, = ax.plot([x - Ls, x - Ls], [y - 0.15 * Ls, y + 0.15 * Ls], linewidth=2.0)
|
|
423
|
+
arts.append(wall)
|
|
424
|
+
|
|
425
|
+
if show_k and "cf in [MN/m]" in bc.columns:
|
|
426
|
+
k = bc["cf in [MN/m]"].iloc[i]
|
|
427
|
+
txt = ax.text(x - 1.1 * Ls, y + 0.18 * Ls, f"k={k:g}", fontsize=8, ha="right")
|
|
428
|
+
arts.append(txt)
|
|
429
|
+
|
|
430
|
+
elif dof == "z":
|
|
431
|
+
X = pts[:, 0]
|
|
432
|
+
Y = pts[:, 1]
|
|
433
|
+
pts2 = np.column_stack([-Y, -X]) # rotation -90deg
|
|
434
|
+
pts2[:, 0] += x
|
|
435
|
+
pts2[:, 1] += y
|
|
436
|
+
line, = ax.plot(pts2[:, 0], pts2[:, 1], linewidth=1.5)
|
|
437
|
+
arts.append(line)
|
|
438
|
+
|
|
439
|
+
wall, = ax.plot([x - 0.15 * Ls, x + 0.15 * Ls], [y - Ls, y - Ls], linewidth=2.0)
|
|
440
|
+
arts.append(wall)
|
|
441
|
+
|
|
442
|
+
if show_k and "cf in [MN/m]" in bc.columns:
|
|
443
|
+
k = bc["cf in [MN/m]"].iloc[i]
|
|
444
|
+
txt = ax.text(x + 0.18 * Ls, y - 1.1 * Ls, f"k={k:g}", fontsize=8, va="top")
|
|
445
|
+
arts.append(txt)
|
|
446
|
+
|
|
447
|
+
return arts
|
|
448
|
+
|
|
449
|
+
def _add_springs_to_ax(self, ax, spring_scale=1.0):
|
|
450
|
+
NL = np.asarray(self.m.Meshing.NL, dtype=float)
|
|
451
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds(pad_rel=0.0)
|
|
452
|
+
Lref = 0.15 * max(xmax - xmin, ymax - ymin, 1e-12)
|
|
453
|
+
self._draw_springs(ax, NL, Lref, spring_scale=spring_scale)
|
|
454
|
+
|
|
455
|
+
def plot_mesh_with_node_ids(self,
|
|
456
|
+
show_node_ids=True,
|
|
457
|
+
show_elem_ids=False,
|
|
458
|
+
show_springs=True,
|
|
459
|
+
spring_scale=1.0):
|
|
460
|
+
"""
|
|
461
|
+
Backward compatibility wrapper.
|
|
462
|
+
"""
|
|
463
|
+
return self.plot_mesh(
|
|
464
|
+
show_node_ids=show_node_ids,
|
|
465
|
+
show_elem_ids=show_elem_ids,
|
|
466
|
+
show_springs=show_springs,
|
|
467
|
+
spring_scale=spring_scale
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def plot_load_vector_interactive(self,
|
|
472
|
+
scale0=1.0,
|
|
473
|
+
scale_max=20.0,
|
|
474
|
+
show_node_ids=False,
|
|
475
|
+
show_springs=True,
|
|
476
|
+
spring_scale=1.0,
|
|
477
|
+
show_loaded_labels=True):
|
|
478
|
+
"""
|
|
479
|
+
Backward-compatible wrapper for interactive nodal load plotting.
|
|
480
|
+
"""
|
|
481
|
+
import numpy as np
|
|
482
|
+
import matplotlib.pyplot as plt
|
|
483
|
+
import matplotlib.patches as patches
|
|
484
|
+
from matplotlib.widgets import Slider
|
|
485
|
+
|
|
486
|
+
if not hasattr(self.A, "Load_Vector"):
|
|
487
|
+
raise RuntimeError("Load_Vector not found. Call GenerateLoadVector() first.")
|
|
488
|
+
|
|
489
|
+
NL = np.asarray(self.m.Meshing.NL, dtype=float)
|
|
490
|
+
EL = np.asarray(self.m.Meshing.EL, dtype=int)
|
|
491
|
+
|
|
492
|
+
Fx = self.A.Load_Vector[::2].astype(float)
|
|
493
|
+
Fy = self.A.Load_Vector[1::2].astype(float)
|
|
494
|
+
|
|
495
|
+
nN = NL.shape[0]
|
|
496
|
+
if len(Fx) != nN:
|
|
497
|
+
raise RuntimeError("Load_Vector size does not match number of nodes.")
|
|
498
|
+
|
|
499
|
+
Fmag = np.sqrt(Fx**2 + Fy**2)
|
|
500
|
+
Fmax = float(np.max(Fmag)) if np.max(Fmag) > 0 else 1.0
|
|
501
|
+
|
|
502
|
+
xmin, ymin = NL.min(axis=0)
|
|
503
|
+
xmax, ymax = NL.max(axis=0)
|
|
504
|
+
Lref = 0.15 * max(xmax - xmin, ymax - ymin, 1e-12)
|
|
505
|
+
|
|
506
|
+
fig, ax = plt.subplots()
|
|
507
|
+
plt.subplots_adjust(bottom=0.18)
|
|
508
|
+
ax.set_aspect("equal", adjustable="box")
|
|
509
|
+
ax.grid(True)
|
|
510
|
+
ax.set_title("Nodal load vector")
|
|
511
|
+
|
|
512
|
+
ax.set_xlim(xmin - 0.1*Lref, xmax + 0.1*Lref)
|
|
513
|
+
ax.set_ylim(ymin - 0.1*Lref, ymax + 0.1*Lref)
|
|
514
|
+
|
|
515
|
+
# mesh
|
|
516
|
+
for el in EL:
|
|
517
|
+
coords = [NL[nid - 1] for nid in el]
|
|
518
|
+
ax.add_patch(patches.Polygon(coords, closed=True, fill=False,
|
|
519
|
+
edgecolor="0.7", linewidth=1.0))
|
|
520
|
+
|
|
521
|
+
if show_node_ids:
|
|
522
|
+
for i, (x, y) in enumerate(NL, start=1):
|
|
523
|
+
ax.text(x, y, str(i), fontsize=8, ha="right", va="bottom")
|
|
524
|
+
|
|
525
|
+
loaded = np.where((np.abs(Fx) > 1e-14) | (np.abs(Fy) > 1e-14))[0]
|
|
526
|
+
|
|
527
|
+
arrow_art = [None] * nN
|
|
528
|
+
for i in loaded:
|
|
529
|
+
x, y = NL[i]
|
|
530
|
+
arrow_art[i] = ax.arrow(x, y, 0.0, 0.0,
|
|
531
|
+
head_width=0.05*Lref,
|
|
532
|
+
head_length=0.07*Lref,
|
|
533
|
+
length_includes_head=True)
|
|
534
|
+
|
|
535
|
+
if show_loaded_labels:
|
|
536
|
+
ax.text(x, y, f"{i+1}\n({Fx[i]:.2g},{Fy[i]:.2g})",
|
|
537
|
+
fontsize=7, ha="left", va="bottom")
|
|
538
|
+
|
|
539
|
+
if show_springs:
|
|
540
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
541
|
+
|
|
542
|
+
ax_slider = plt.axes([0.15, 0.06, 0.70, 0.03])
|
|
543
|
+
s_scale = Slider(ax_slider, "Scale", 0.0, scale_max, valinit=scale0)
|
|
544
|
+
|
|
545
|
+
def _update(scale):
|
|
546
|
+
for i in loaded:
|
|
547
|
+
try:
|
|
548
|
+
arrow_art[i].remove()
|
|
549
|
+
except Exception:
|
|
550
|
+
pass
|
|
551
|
+
|
|
552
|
+
x, y = NL[i]
|
|
553
|
+
dx = scale * Lref * Fx[i] / Fmax
|
|
554
|
+
dy = scale * Lref * Fy[i] / Fmax
|
|
555
|
+
|
|
556
|
+
arrow_art[i] = ax.arrow(
|
|
557
|
+
x, y, dx, dy,
|
|
558
|
+
head_width=0.05*Lref,
|
|
559
|
+
head_length=0.07*Lref,
|
|
560
|
+
length_includes_head=True
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
fig.canvas.draw_idle()
|
|
564
|
+
|
|
565
|
+
s_scale.on_changed(_update)
|
|
566
|
+
_update(scale0)
|
|
567
|
+
|
|
568
|
+
plt.show()
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def plot_principal_membrane_forces(self,
|
|
573
|
+
which="n1",
|
|
574
|
+
mode="elem",
|
|
575
|
+
draw_dirs=True,
|
|
576
|
+
dir_scale=1.0,
|
|
577
|
+
show_springs=True,
|
|
578
|
+
spring_scale=1.0):
|
|
579
|
+
"""
|
|
580
|
+
Plot principal membrane forces.
|
|
581
|
+
which: "n1" or "n2"
|
|
582
|
+
mode : "elem" (uses self.m.n_princ_elem_avg) or "node" (uses self.m.n_princ_node + nodal interpolation)
|
|
583
|
+
draw_dirs: draw principal directions (theta)
|
|
584
|
+
dir_scale: scale factor for direction arrows (relative)
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
import numpy as np
|
|
588
|
+
import matplotlib.pyplot as plt
|
|
589
|
+
import matplotlib.patches as patches
|
|
590
|
+
import matplotlib.cm as cm
|
|
591
|
+
import matplotlib.colors as colors
|
|
592
|
+
|
|
593
|
+
# ensure principal results exist
|
|
594
|
+
if not hasattr(self.m, "n_princ_elem_avg"):
|
|
595
|
+
# call solver with principal enabled
|
|
596
|
+
self.m.CalculateInnerElementForces_Gauss(compute_nodal=(mode == "node"), compute_principal=True)
|
|
597
|
+
|
|
598
|
+
NL = np.asarray(self.m.Meshing.NL, float)
|
|
599
|
+
EL = np.asarray(self.m.Meshing.EL, int)
|
|
600
|
+
|
|
601
|
+
idx = 0 if which == "n1" else 1
|
|
602
|
+
title = f"principal membrane force {which}"
|
|
603
|
+
|
|
604
|
+
# ----------------------------
|
|
605
|
+
# values per element (preferred)
|
|
606
|
+
# ----------------------------
|
|
607
|
+
if mode.lower() == "elem":
|
|
608
|
+
data = np.asarray(self.m.n_princ_elem_avg, float) # (n_elem,3) [n1,n2,theta]
|
|
609
|
+
vals = data[:, idx]
|
|
610
|
+
thetas = data[:, 2]
|
|
611
|
+
|
|
612
|
+
vmin, vmax = float(np.min(vals)), float(np.max(vals))
|
|
613
|
+
if np.isclose(vmin, vmax):
|
|
614
|
+
vmin -= 1.0
|
|
615
|
+
vmax += 1.0
|
|
616
|
+
|
|
617
|
+
norm = colors.Normalize(vmin=vmin, vmax=vmax)
|
|
618
|
+
cmap = cm.viridis
|
|
619
|
+
|
|
620
|
+
fig, ax = plt.subplots()
|
|
621
|
+
ax.set_aspect("equal", adjustable="box")
|
|
622
|
+
ax.grid(True)
|
|
623
|
+
|
|
624
|
+
# reference length for arrows
|
|
625
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds(pad_rel=0.0)
|
|
626
|
+
Lref = 0.12 * max(xmax - xmin, ymax - ymin, 1e-12) * dir_scale
|
|
627
|
+
|
|
628
|
+
for e, el in enumerate(EL):
|
|
629
|
+
coords = np.array([NL[nid - 1] for nid in el], float)
|
|
630
|
+
poly = patches.Polygon(coords, closed=True, edgecolor="k", facecolor=cmap(norm(vals[e])))
|
|
631
|
+
ax.add_patch(poly)
|
|
632
|
+
|
|
633
|
+
if draw_dirs:
|
|
634
|
+
c = coords.mean(axis=0)
|
|
635
|
+
th = float(thetas[e])
|
|
636
|
+
|
|
637
|
+
# principal direction (theta) and orthogonal direction
|
|
638
|
+
dx1, dy1 = Lref*np.cos(th), Lref*np.sin(th)
|
|
639
|
+
dx2, dy2 = -Lref*np.sin(th), Lref*np.cos(th)
|
|
640
|
+
|
|
641
|
+
ax.arrow(c[0], c[1], dx1, dy1, head_width=0.12*Lref, head_length=0.12*Lref,
|
|
642
|
+
length_includes_head=True)
|
|
643
|
+
ax.arrow(c[0], c[1], dx2, dy2, head_width=0.12*Lref, head_length=0.12*Lref,
|
|
644
|
+
length_includes_head=True)
|
|
645
|
+
|
|
646
|
+
ax.set_xlim(xmin, xmax)
|
|
647
|
+
ax.set_ylim(ymin, ymax)
|
|
648
|
+
ax.set_title(title)
|
|
649
|
+
|
|
650
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
651
|
+
sm.set_array([])
|
|
652
|
+
fig.colorbar(sm, ax=ax, orientation="vertical", label=which)
|
|
653
|
+
|
|
654
|
+
if show_springs:
|
|
655
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
656
|
+
|
|
657
|
+
plt.show()
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# ----------------------------
|
|
661
|
+
# nodal mode (smooth): node values -> element facecolor via averaging
|
|
662
|
+
# ----------------------------
|
|
663
|
+
if mode.lower() == "node":
|
|
664
|
+
if not hasattr(self.m, "n_princ_node"):
|
|
665
|
+
self.m.CalculateInnerElementForces_Gauss(compute_nodal=True, compute_principal=True)
|
|
666
|
+
|
|
667
|
+
nnode = np.asarray(self.m.n_princ_node, float) # (n_nodes,3)
|
|
668
|
+
node_vals = nnode[:, idx]
|
|
669
|
+
|
|
670
|
+
# element value = mean of its node values (simple, stable)
|
|
671
|
+
vals = np.zeros((EL.shape[0],), float)
|
|
672
|
+
thetas = np.zeros((EL.shape[0],), float)
|
|
673
|
+
for e, el in enumerate(EL):
|
|
674
|
+
ids = np.array(el, int) - 1
|
|
675
|
+
vals[e] = float(np.mean(node_vals[ids]))
|
|
676
|
+
thetas[e] = float(np.mean(nnode[ids, 2])) # averaged theta (ok for structured meshes)
|
|
677
|
+
|
|
678
|
+
vmin, vmax = float(np.min(vals)), float(np.max(vals))
|
|
679
|
+
if np.isclose(vmin, vmax):
|
|
680
|
+
vmin -= 1.0
|
|
681
|
+
vmax += 1.0
|
|
682
|
+
|
|
683
|
+
norm = colors.Normalize(vmin=vmin, vmax=vmax)
|
|
684
|
+
cmap = cm.viridis
|
|
685
|
+
|
|
686
|
+
fig, ax = plt.subplots()
|
|
687
|
+
ax.set_aspect("equal", adjustable="box")
|
|
688
|
+
ax.grid(True)
|
|
689
|
+
|
|
690
|
+
xmin, xmax, ymin, ymax = self._mesh_bounds(pad_rel=0.0)
|
|
691
|
+
Lref = 0.12 * max(xmax - xmin, ymax - ymin, 1e-12) * dir_scale
|
|
692
|
+
|
|
693
|
+
for e, el in enumerate(EL):
|
|
694
|
+
coords = np.array([NL[nid - 1] for nid in el], float)
|
|
695
|
+
poly = patches.Polygon(coords, closed=True, edgecolor="k", facecolor=cmap(norm(vals[e])))
|
|
696
|
+
ax.add_patch(poly)
|
|
697
|
+
|
|
698
|
+
if draw_dirs:
|
|
699
|
+
c = coords.mean(axis=0)
|
|
700
|
+
th = float(thetas[e])
|
|
701
|
+
dx1, dy1 = Lref*np.cos(th), Lref*np.sin(th)
|
|
702
|
+
dx2, dy2 = -Lref*np.sin(th), Lref*np.cos(th)
|
|
703
|
+
ax.arrow(c[0], c[1], dx1, dy1, head_width=0.12*Lref, head_length=0.12*Lref,
|
|
704
|
+
length_includes_head=True)
|
|
705
|
+
ax.arrow(c[0], c[1], dx2, dy2, head_width=0.12*Lref, head_length=0.12*Lref,
|
|
706
|
+
length_includes_head=True)
|
|
707
|
+
|
|
708
|
+
ax.set_xlim(xmin, xmax)
|
|
709
|
+
ax.set_ylim(ymin, ymax)
|
|
710
|
+
ax.set_title(title)
|
|
711
|
+
|
|
712
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
713
|
+
sm.set_array([])
|
|
714
|
+
fig.colorbar(sm, ax=ax, orientation="vertical", label=which)
|
|
715
|
+
|
|
716
|
+
if show_springs:
|
|
717
|
+
self._add_springs_to_ax(ax, spring_scale=spring_scale)
|
|
718
|
+
|
|
719
|
+
plt.show()
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
raise ValueError("mode must be 'elem' or 'node'")
|