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,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'")