weac 2.6.4__py3-none-any.whl → 3.0.1__py3-none-any.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.
- weac/__init__.py +2 -14
- weac/analysis/__init__.py +23 -0
- weac/analysis/analyzer.py +790 -0
- weac/analysis/criteria_evaluator.py +1169 -0
- weac/analysis/plotter.py +1922 -0
- weac/components/__init__.py +21 -0
- weac/components/config.py +33 -0
- weac/components/criteria_config.py +86 -0
- weac/components/layer.py +284 -0
- weac/components/model_input.py +103 -0
- weac/components/scenario_config.py +72 -0
- weac/components/segment.py +31 -0
- weac/constants.py +37 -0
- weac/core/__init__.py +10 -0
- weac/core/eigensystem.py +405 -0
- weac/core/field_quantities.py +273 -0
- weac/core/scenario.py +200 -0
- weac/core/slab.py +149 -0
- weac/core/slab_touchdown.py +363 -0
- weac/core/system_model.py +413 -0
- weac/core/unknown_constants_solver.py +444 -0
- weac/logging_config.py +39 -0
- weac/utils/__init__.py +0 -0
- weac/utils/geldsetzer.py +166 -0
- weac/utils/misc.py +127 -0
- weac/utils/snow_types.py +82 -0
- weac/utils/snowpilot_parser.py +332 -0
- {weac-2.6.4.dist-info → weac-3.0.1.dist-info}/METADATA +196 -64
- weac-3.0.1.dist-info/RECORD +32 -0
- weac-3.0.1.dist-info/licenses/LICENSE +21 -0
- weac/eigensystem.py +0 -658
- weac/inverse.py +0 -51
- weac/layered.py +0 -64
- weac/mixins.py +0 -2083
- weac/plot.py +0 -675
- weac/tools.py +0 -334
- weac-2.6.4.dist-info/RECORD +0 -12
- weac-2.6.4.dist-info/licenses/LICENSE +0 -24
- {weac-2.6.4.dist-info → weac-3.0.1.dist-info}/WHEEL +0 -0
- {weac-2.6.4.dist-info → weac-3.0.1.dist-info}/top_level.txt +0 -0
weac/analysis/plotter.py
ADDED
|
@@ -0,0 +1,1922 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides plotting functions for visualizing the results of the WEAC model.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Standard library imports
|
|
6
|
+
import colorsys
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import List, Literal, Optional
|
|
10
|
+
|
|
11
|
+
# Third party imports
|
|
12
|
+
import matplotlib.colors as mc
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import numpy as np
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
from matplotlib.patches import Patch, Polygon, Rectangle
|
|
17
|
+
from scipy.optimize import brentq
|
|
18
|
+
|
|
19
|
+
from weac.analysis.analyzer import Analyzer
|
|
20
|
+
from weac.analysis.criteria_evaluator import (
|
|
21
|
+
CoupledCriterionResult,
|
|
22
|
+
CriteriaEvaluator,
|
|
23
|
+
FindMinimumForceResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Module imports
|
|
27
|
+
from weac.components.layer import WeakLayer
|
|
28
|
+
from weac.core.scenario import Scenario
|
|
29
|
+
from weac.core.slab import Slab
|
|
30
|
+
from weac.core.system_model import SystemModel
|
|
31
|
+
from weac.utils.misc import isnotebook
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
LABELSTYLE = {
|
|
36
|
+
"backgroundcolor": "w",
|
|
37
|
+
"horizontalalignment": "center",
|
|
38
|
+
"verticalalignment": "center",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
COLORS = np.array(
|
|
42
|
+
[ # TUD color palette
|
|
43
|
+
["#DCDCDC", "#B5B5B5", "#898989", "#535353"], # gray
|
|
44
|
+
["#5D85C3", "#005AA9", "#004E8A", "#243572"], # blue
|
|
45
|
+
["#009CDA", "#0083CC", "#00689D", "#004E73"], # ocean
|
|
46
|
+
["#50B695", "#009D81", "#008877", "#00715E"], # teal
|
|
47
|
+
["#AFCC50", "#99C000", "#7FAB16", "#6A8B22"], # green
|
|
48
|
+
["#DDDF48", "#C9D400", "#B1BD00", "#99A604"], # lime
|
|
49
|
+
["#FFE05C", "#FDCA00", "#D7AC00", "#AE8E00"], # yellow
|
|
50
|
+
["#F8BA3C", "#F5A300", "#D28700", "#BE6F00"], # sand
|
|
51
|
+
["#EE7A34", "#EC6500", "#CC4C03", "#A94913"], # orange
|
|
52
|
+
["#E9503E", "#E6001A", "#B90F22", "#961C26"], # red
|
|
53
|
+
["#C9308E", "#A60084", "#951169", "#732054"], # magenta
|
|
54
|
+
["#804597", "#721085", "#611C73", "#4C226A"], # purple
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _outline(grid):
|
|
60
|
+
"""Extract _outline values of a 2D array (matrix, grid)."""
|
|
61
|
+
top = grid[0, :-1]
|
|
62
|
+
right = grid[:-1, -1]
|
|
63
|
+
bot = grid[-1, :0:-1]
|
|
64
|
+
left = grid[::-1, 0]
|
|
65
|
+
|
|
66
|
+
return np.hstack([top, right, bot, left])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _significant_digits(decimal: float) -> int:
|
|
70
|
+
"""Return the number of significant digits for a given decimal."""
|
|
71
|
+
if decimal == 0:
|
|
72
|
+
return 1
|
|
73
|
+
try:
|
|
74
|
+
sig_digits = -int(np.floor(np.log10(decimal)))
|
|
75
|
+
except ValueError:
|
|
76
|
+
sig_digits = 3
|
|
77
|
+
return sig_digits
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _tight_central_distribution(limit, samples=100, tightness=1.5):
|
|
81
|
+
"""
|
|
82
|
+
Provide values within a given interval distributed tightly around 0.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
limit : float
|
|
87
|
+
Maximum and minimum of value range.
|
|
88
|
+
samples : int, optional
|
|
89
|
+
Number of values. Default is 100.
|
|
90
|
+
tightness : int, optional
|
|
91
|
+
Degree of value densification at center. 1.0 corresponds
|
|
92
|
+
to equal spacing. Default is 1.5.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
ndarray
|
|
97
|
+
Array of values more tightly spaced around 0.
|
|
98
|
+
"""
|
|
99
|
+
stop = limit ** (1 / tightness)
|
|
100
|
+
levels = np.linspace(0, stop, num=int(samples / 2), endpoint=True) ** tightness
|
|
101
|
+
return np.unique(np.hstack([-levels[::-1], levels]))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _adjust_lightness(color, amount=0.5):
|
|
105
|
+
"""
|
|
106
|
+
Adjust color lightness.
|
|
107
|
+
|
|
108
|
+
Arguments
|
|
109
|
+
----------
|
|
110
|
+
color : str or tuple
|
|
111
|
+
Matplotlib colorname, hex string, or RGB value tuple.
|
|
112
|
+
amount : float, optional
|
|
113
|
+
Amount of lightening: >1 lightens, <1 darkens. Default is 0.5.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
tuple
|
|
118
|
+
RGB color tuple.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
c = mc.cnames[color]
|
|
122
|
+
except KeyError:
|
|
123
|
+
c = color
|
|
124
|
+
c = colorsys.rgb_to_hls(*mc.to_rgb(c))
|
|
125
|
+
return colorsys.hls_to_rgb(c[0], max(0, min(1, amount * c[1])), c[2])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MidpointNormalize(mc.Normalize):
|
|
129
|
+
"""Colormap normalization to a specified midpoint. Default is 0."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, vmin, vmax, midpoint=0, clip=False):
|
|
132
|
+
"""Initialize normalization."""
|
|
133
|
+
self.midpoint = midpoint
|
|
134
|
+
mc.Normalize.__init__(self, vmin, vmax, clip)
|
|
135
|
+
|
|
136
|
+
def __call__(self, value, clip=None):
|
|
137
|
+
"""Apply normalization."""
|
|
138
|
+
x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1]
|
|
139
|
+
return np.ma.masked_array(np.interp(value, x, y))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Plotter:
|
|
143
|
+
"""
|
|
144
|
+
Modern plotting class for WEAC simulations with support for multiple system comparisons.
|
|
145
|
+
|
|
146
|
+
This class provides comprehensive visualization capabilities for weak layer anticrack
|
|
147
|
+
nucleation simulations, including single system analysis and multi-system comparisons.
|
|
148
|
+
|
|
149
|
+
Features:
|
|
150
|
+
- Single and multi-system plotting
|
|
151
|
+
- System override functionality for selective plotting
|
|
152
|
+
- Comprehensive dashboard creation
|
|
153
|
+
- Modern matplotlib styling
|
|
154
|
+
- Jupyter notebook integration
|
|
155
|
+
- Automatic plot directory management
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
plot_dir: str = "plots",
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Initialize the plotter.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
system : SystemModel, optional
|
|
168
|
+
Single system model for analysis
|
|
169
|
+
systems : List[SystemModel], optional
|
|
170
|
+
List of system models for comparison
|
|
171
|
+
labels : List[str], optional
|
|
172
|
+
Labels for each system in plots
|
|
173
|
+
colors : List[str], optional
|
|
174
|
+
Colors for each system in plots
|
|
175
|
+
plot_dir : str, default "plots"
|
|
176
|
+
Directory to save plots
|
|
177
|
+
"""
|
|
178
|
+
self.labels = LABELSTYLE
|
|
179
|
+
self.colors = COLORS
|
|
180
|
+
|
|
181
|
+
# Set up plot directory
|
|
182
|
+
self.plot_dir = plot_dir
|
|
183
|
+
os.makedirs(self.plot_dir, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
# Set up matplotlib style
|
|
186
|
+
self._setup_matplotlib_style()
|
|
187
|
+
|
|
188
|
+
# Cache analyzers for performance
|
|
189
|
+
self._analyzers = {}
|
|
190
|
+
|
|
191
|
+
def _setup_matplotlib_style(self):
|
|
192
|
+
"""Set up modern matplotlib styling."""
|
|
193
|
+
plt.style.use("default")
|
|
194
|
+
plt.rcParams.update(
|
|
195
|
+
{
|
|
196
|
+
"figure.figsize": (12, 8),
|
|
197
|
+
"figure.dpi": 100,
|
|
198
|
+
"savefig.dpi": 300,
|
|
199
|
+
"savefig.bbox": "tight",
|
|
200
|
+
"font.size": 11,
|
|
201
|
+
"axes.titlesize": 14,
|
|
202
|
+
"axes.labelsize": 12,
|
|
203
|
+
"xtick.labelsize": 10,
|
|
204
|
+
"ytick.labelsize": 10,
|
|
205
|
+
"legend.fontsize": 10,
|
|
206
|
+
"lines.linewidth": 2,
|
|
207
|
+
"axes.grid": True,
|
|
208
|
+
"grid.alpha": 0.3,
|
|
209
|
+
"axes.axisbelow": True,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _get_analyzer(self, system: SystemModel) -> Analyzer:
|
|
214
|
+
"""Get cached analyzer for a system."""
|
|
215
|
+
system_id = id(system)
|
|
216
|
+
if system_id not in self._analyzers:
|
|
217
|
+
self._analyzers[system_id] = Analyzer(system_model=system)
|
|
218
|
+
return self._analyzers[system_id]
|
|
219
|
+
|
|
220
|
+
def _get_systems_to_plot(
|
|
221
|
+
self,
|
|
222
|
+
system_model: Optional[SystemModel] = None,
|
|
223
|
+
system_models: Optional[List[SystemModel]] = None,
|
|
224
|
+
) -> List[SystemModel]:
|
|
225
|
+
"""Determine which systems to plot based on override parameters."""
|
|
226
|
+
if system_model is not None and system_models is not None:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"Provide either 'system_model' or 'system_models', not both"
|
|
229
|
+
)
|
|
230
|
+
if isinstance(system_model, SystemModel):
|
|
231
|
+
return [system_model]
|
|
232
|
+
if isinstance(system_models, list):
|
|
233
|
+
return system_models
|
|
234
|
+
raise ValueError(
|
|
235
|
+
"Must provide either 'system_model' or 'system_models' as a "
|
|
236
|
+
"SystemModel or list of SystemModels"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _save_figure(self, filename: str, fig: Optional[Figure] = None):
|
|
240
|
+
"""Save figure with proper formatting."""
|
|
241
|
+
if fig is None:
|
|
242
|
+
fig = plt.gcf()
|
|
243
|
+
|
|
244
|
+
filepath = os.path.join(self.plot_dir, f"{filename}.png")
|
|
245
|
+
fig.savefig(filepath, dpi=300, bbox_inches="tight", facecolor="white")
|
|
246
|
+
|
|
247
|
+
if not isnotebook():
|
|
248
|
+
plt.close(fig)
|
|
249
|
+
|
|
250
|
+
def plot_slab_profile(
|
|
251
|
+
self,
|
|
252
|
+
weak_layers: List[WeakLayer] | WeakLayer,
|
|
253
|
+
slabs: List[Slab] | Slab,
|
|
254
|
+
filename: str = "slab_profile",
|
|
255
|
+
labels: Optional[List[str] | str] = None,
|
|
256
|
+
colors: Optional[List[str]] = None,
|
|
257
|
+
):
|
|
258
|
+
"""
|
|
259
|
+
Plot slab layer profiles for comparison.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
weak_layers : List[WeakLayer] | WeakLayer
|
|
264
|
+
The weak layer or layers to plot.
|
|
265
|
+
slabs : List[Slab] | Slab
|
|
266
|
+
The slab or slabs to plot.
|
|
267
|
+
filename : str, optional
|
|
268
|
+
Filename for saving plot
|
|
269
|
+
labels : list of str, optional
|
|
270
|
+
Labels for each system.
|
|
271
|
+
colors : list of str, optional
|
|
272
|
+
Colors for each system.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
matplotlib.figure.Figure
|
|
277
|
+
The generated plot figure.
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(weak_layers, WeakLayer):
|
|
280
|
+
weak_layers = [weak_layers]
|
|
281
|
+
if isinstance(slabs, Slab):
|
|
282
|
+
slabs = [slabs]
|
|
283
|
+
|
|
284
|
+
if labels is None:
|
|
285
|
+
labels = [f"System {i + 1}" for i in range(len(weak_layers))]
|
|
286
|
+
elif isinstance(labels, str):
|
|
287
|
+
labels = [labels] * len(slabs)
|
|
288
|
+
elif len(labels) != len(slabs):
|
|
289
|
+
raise ValueError("Number of labels must match number of slabs")
|
|
290
|
+
|
|
291
|
+
if colors is None:
|
|
292
|
+
plot_colors = [self.colors[i, 0] for i in range(len(slabs))]
|
|
293
|
+
else:
|
|
294
|
+
plot_colors = colors
|
|
295
|
+
|
|
296
|
+
# Plot Setup
|
|
297
|
+
plt.rcdefaults()
|
|
298
|
+
plt.rc("font", family="serif", size=8)
|
|
299
|
+
plt.rc("mathtext", fontset="cm")
|
|
300
|
+
|
|
301
|
+
fig = plt.figure(figsize=(8 / 3, 4))
|
|
302
|
+
ax1 = fig.gca()
|
|
303
|
+
|
|
304
|
+
# Plot 1: Layer thickness and density
|
|
305
|
+
max_height = 0
|
|
306
|
+
for i, slab in enumerate(slabs):
|
|
307
|
+
total_height = slab.H + weak_layers[i].h
|
|
308
|
+
max_height = max(max_height, total_height)
|
|
309
|
+
|
|
310
|
+
for i, (weak_layer, slab, label, color) in enumerate(
|
|
311
|
+
zip(weak_layers, slabs, labels, plot_colors)
|
|
312
|
+
):
|
|
313
|
+
# Plot weak layer
|
|
314
|
+
wl_y = [-weak_layer.h, 0]
|
|
315
|
+
wl_x = [weak_layer.rho, weak_layer.rho]
|
|
316
|
+
ax1.fill_betweenx(wl_y, 0, wl_x, color="red", alpha=0.8, hatch="///")
|
|
317
|
+
|
|
318
|
+
# Plot slab layers
|
|
319
|
+
x_coords = []
|
|
320
|
+
y_coords = []
|
|
321
|
+
current_height = 0
|
|
322
|
+
|
|
323
|
+
# As slab.layers is top-down
|
|
324
|
+
for layer in reversed(slab.layers):
|
|
325
|
+
x_coords.extend([layer.rho, layer.rho])
|
|
326
|
+
y_coords.extend([current_height, current_height + layer.h])
|
|
327
|
+
current_height += layer.h
|
|
328
|
+
|
|
329
|
+
ax1.fill_betweenx(
|
|
330
|
+
y_coords, 0, x_coords, color=color, alpha=0.7, label=label
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Set axis labels
|
|
334
|
+
ax1.set_xlabel(r"$\longleftarrow$ Density $\rho$ (kg/m$^3$)")
|
|
335
|
+
ax1.set_ylabel(r"Height above weak layer (mm) $\longrightarrow$")
|
|
336
|
+
|
|
337
|
+
ax1.set_title("Slab Density Profile")
|
|
338
|
+
|
|
339
|
+
handles, slab_labels = ax1.get_legend_handles_labels()
|
|
340
|
+
weak_layer_patch = Patch(
|
|
341
|
+
facecolor="red", alpha=0.8, hatch="///", label="Weak Layer"
|
|
342
|
+
)
|
|
343
|
+
ax1.legend(
|
|
344
|
+
handles=[weak_layer_patch] + handles, labels=["Weak Layer"] + slab_labels
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
ax1.grid(True, alpha=0.3)
|
|
348
|
+
ax1.set_xlim(500, 0)
|
|
349
|
+
ax1.set_ylim(-min(weak_layer.h for weak_layer in weak_layers), max_height)
|
|
350
|
+
|
|
351
|
+
if filename:
|
|
352
|
+
self._save_figure(filename, fig)
|
|
353
|
+
|
|
354
|
+
return fig
|
|
355
|
+
|
|
356
|
+
def plot_rotated_slab_profile(
|
|
357
|
+
self,
|
|
358
|
+
weak_layer: WeakLayer,
|
|
359
|
+
slab: Slab,
|
|
360
|
+
angle: float = 0,
|
|
361
|
+
weight: float = 0,
|
|
362
|
+
slab_width: float = 200,
|
|
363
|
+
filename: str = "rotated_slab_profile",
|
|
364
|
+
title: str = "Rotated Slab Profile",
|
|
365
|
+
):
|
|
366
|
+
"""
|
|
367
|
+
Plot a rectangular slab profile with layers stacked vertically, colored by density,
|
|
368
|
+
and rotated by the specified angle.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
weak_layer : WeakLayer
|
|
373
|
+
The weak layer to plot at the bottom.
|
|
374
|
+
slab : Slab
|
|
375
|
+
The slab with layers to plot.
|
|
376
|
+
angle : float, optional
|
|
377
|
+
Rotation angle in degrees. Default is 0.
|
|
378
|
+
slab_width : float, optional
|
|
379
|
+
Width of the slab rectangle in mm. Default is 200.
|
|
380
|
+
filename : str, optional
|
|
381
|
+
Filename for saving plot. Default is "rotated_slab_profile".
|
|
382
|
+
title : str, optional
|
|
383
|
+
Plot title. Default is "Rotated Slab Profile".
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
matplotlib.figure.Figure
|
|
388
|
+
The generated plot figure.
|
|
389
|
+
"""
|
|
390
|
+
# Plot Setup
|
|
391
|
+
plt.rcdefaults()
|
|
392
|
+
plt.rc("font", family="serif", size=10)
|
|
393
|
+
plt.rc("mathtext", fontset="cm")
|
|
394
|
+
|
|
395
|
+
fig = plt.figure(figsize=(8, 6), dpi=300)
|
|
396
|
+
ax = fig.gca()
|
|
397
|
+
|
|
398
|
+
# Calculate total height
|
|
399
|
+
total_height = slab.H + weak_layer.h
|
|
400
|
+
|
|
401
|
+
# Create density-based colormap
|
|
402
|
+
all_densities = [weak_layer.rho] + [layer.rho for layer in slab.layers]
|
|
403
|
+
min_density = min(all_densities)
|
|
404
|
+
max_density = max(all_densities)
|
|
405
|
+
|
|
406
|
+
# Normalize densities for color mapping
|
|
407
|
+
norm = mc.Normalize(vmin=min_density, vmax=max_density)
|
|
408
|
+
cmap = plt.get_cmap("viridis") # You can change this to any colormap
|
|
409
|
+
|
|
410
|
+
# Function to create sloped layer (parallelogram)
|
|
411
|
+
def create_sloped_layer(x, y, width, height, angle_rad):
|
|
412
|
+
"""Create a layer that follows the slope angle"""
|
|
413
|
+
# Calculate horizontal offset for the slope
|
|
414
|
+
slope_offset = width * np.sin(angle_rad)
|
|
415
|
+
|
|
416
|
+
# Create parallelogram corners
|
|
417
|
+
# Bottom edge is horizontal, top edge is shifted by slope_offset
|
|
418
|
+
corners = np.array(
|
|
419
|
+
[
|
|
420
|
+
[x, y], # Bottom left
|
|
421
|
+
[x + width, y + slope_offset], # Bottom right
|
|
422
|
+
[x + width, y + height + slope_offset], # Top right (shifted)
|
|
423
|
+
[x, y + height], # Top left (shifted)
|
|
424
|
+
]
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return corners
|
|
428
|
+
|
|
429
|
+
# Convert angle to radians
|
|
430
|
+
angle_rad = np.radians(angle)
|
|
431
|
+
|
|
432
|
+
# Start from bottom (weak layer)
|
|
433
|
+
current_y = 0
|
|
434
|
+
|
|
435
|
+
# Plot weak layer
|
|
436
|
+
wl_corners = create_sloped_layer(
|
|
437
|
+
0, current_y, slab_width, weak_layer.h, angle_rad
|
|
438
|
+
)
|
|
439
|
+
wl_color = cmap(norm(weak_layer.rho))
|
|
440
|
+
wl_patch = Polygon(
|
|
441
|
+
wl_corners,
|
|
442
|
+
facecolor=wl_color,
|
|
443
|
+
edgecolor="black",
|
|
444
|
+
linewidth=1,
|
|
445
|
+
alpha=0.8,
|
|
446
|
+
hatch="///",
|
|
447
|
+
)
|
|
448
|
+
ax.add_patch(wl_patch)
|
|
449
|
+
|
|
450
|
+
# Add density label for weak layer
|
|
451
|
+
wl_center = np.mean(wl_corners, axis=0)
|
|
452
|
+
ax.text(
|
|
453
|
+
wl_center[0],
|
|
454
|
+
wl_center[1],
|
|
455
|
+
f"{weak_layer.rho:.0f}\nkg/m³",
|
|
456
|
+
ha="center",
|
|
457
|
+
va="center",
|
|
458
|
+
fontsize=8,
|
|
459
|
+
fontweight="bold",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
current_y += weak_layer.h
|
|
463
|
+
|
|
464
|
+
# Plot slab layers (from bottom to top)
|
|
465
|
+
top_layer_corners = None
|
|
466
|
+
for _i, layer in enumerate(reversed(slab.layers)):
|
|
467
|
+
layer_corners = create_sloped_layer(
|
|
468
|
+
0, current_y, slab_width, layer.h, angle_rad
|
|
469
|
+
)
|
|
470
|
+
layer_color = cmap(norm(layer.rho))
|
|
471
|
+
layer_patch = Polygon(
|
|
472
|
+
layer_corners,
|
|
473
|
+
facecolor=layer_color,
|
|
474
|
+
edgecolor="black",
|
|
475
|
+
linewidth=1,
|
|
476
|
+
alpha=0.8,
|
|
477
|
+
)
|
|
478
|
+
ax.add_patch(layer_patch)
|
|
479
|
+
|
|
480
|
+
# Add density label for slab layer
|
|
481
|
+
layer_center = np.mean(layer_corners, axis=0)
|
|
482
|
+
ax.text(
|
|
483
|
+
layer_center[0],
|
|
484
|
+
layer_center[1],
|
|
485
|
+
f"{layer.rho:.0f}\nkg/m³",
|
|
486
|
+
ha="center",
|
|
487
|
+
va="center",
|
|
488
|
+
fontsize=8,
|
|
489
|
+
fontweight="bold",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
current_y += layer.h
|
|
493
|
+
# Keep track of the top layer corners for arrow placement
|
|
494
|
+
top_layer_corners = layer_corners
|
|
495
|
+
|
|
496
|
+
# Add weight arrow if weight > 0 and we have layers
|
|
497
|
+
if weight > 0 and top_layer_corners is not None:
|
|
498
|
+
# Calculate midpoint of top edge of highest layer
|
|
499
|
+
# Top edge is between points 2 and 3 (top right and top left)
|
|
500
|
+
top_left = top_layer_corners[3]
|
|
501
|
+
top_right = top_layer_corners[2]
|
|
502
|
+
arrow_start_x = (top_left[0] + top_right[0]) / 2
|
|
503
|
+
arrow_start_y = (top_left[1] + top_right[1]) / 2
|
|
504
|
+
|
|
505
|
+
# Scale arrow based on weight (0-400 maps to 0-100, above 400 = 100)
|
|
506
|
+
max_arrow_height = 100
|
|
507
|
+
arrow_height = min(weight * max_arrow_height / 400, max_arrow_height)
|
|
508
|
+
arrow_width = arrow_height * 0.3 # Arrow width proportional to height
|
|
509
|
+
|
|
510
|
+
# Create arrow pointing downward
|
|
511
|
+
arrow_tip_x = arrow_start_x
|
|
512
|
+
arrow_tip_y = arrow_start_y
|
|
513
|
+
|
|
514
|
+
# Arrow shaft (rectangular part)
|
|
515
|
+
shaft_width = arrow_width * 0.3
|
|
516
|
+
shaft_left = arrow_start_x - shaft_width / 2
|
|
517
|
+
shaft_right = arrow_start_x + shaft_width / 2
|
|
518
|
+
shaft_top = arrow_start_y + arrow_height
|
|
519
|
+
shaft_bottom = arrow_tip_y + arrow_width * 0.4
|
|
520
|
+
|
|
521
|
+
# Arrow head (triangular part)
|
|
522
|
+
head_left = arrow_start_x - arrow_width / 2
|
|
523
|
+
head_right = arrow_start_x + arrow_width / 2
|
|
524
|
+
head_top = shaft_bottom
|
|
525
|
+
|
|
526
|
+
# Draw arrow shaft
|
|
527
|
+
shaft_corners = np.array(
|
|
528
|
+
[
|
|
529
|
+
[shaft_left, shaft_top],
|
|
530
|
+
[shaft_right, shaft_top],
|
|
531
|
+
[shaft_right, shaft_bottom],
|
|
532
|
+
[shaft_left, shaft_bottom],
|
|
533
|
+
]
|
|
534
|
+
)
|
|
535
|
+
shaft_patch = Polygon(
|
|
536
|
+
shaft_corners,
|
|
537
|
+
facecolor="red",
|
|
538
|
+
edgecolor="darkred",
|
|
539
|
+
linewidth=2,
|
|
540
|
+
alpha=0.8,
|
|
541
|
+
)
|
|
542
|
+
ax.add_patch(shaft_patch)
|
|
543
|
+
|
|
544
|
+
# Draw arrow head
|
|
545
|
+
head_corners = np.array(
|
|
546
|
+
[
|
|
547
|
+
[head_left, head_top],
|
|
548
|
+
[head_right, head_top],
|
|
549
|
+
[arrow_tip_x, arrow_tip_y],
|
|
550
|
+
]
|
|
551
|
+
)
|
|
552
|
+
head_patch = Polygon(
|
|
553
|
+
head_corners,
|
|
554
|
+
facecolor="red",
|
|
555
|
+
edgecolor="darkred",
|
|
556
|
+
linewidth=2,
|
|
557
|
+
alpha=0.8,
|
|
558
|
+
)
|
|
559
|
+
ax.add_patch(head_patch)
|
|
560
|
+
|
|
561
|
+
# Add weight label
|
|
562
|
+
ax.text(
|
|
563
|
+
arrow_start_x + arrow_width * 0.7,
|
|
564
|
+
arrow_start_y - arrow_height / 2,
|
|
565
|
+
f"{weight:.0f} kg",
|
|
566
|
+
ha="left",
|
|
567
|
+
va="center",
|
|
568
|
+
fontsize=10,
|
|
569
|
+
fontweight="bold",
|
|
570
|
+
color="darkred",
|
|
571
|
+
bbox={
|
|
572
|
+
"boxstyle": "round,pad=0.3",
|
|
573
|
+
"facecolor": "white",
|
|
574
|
+
"alpha": 0.8,
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Calculate plot limits to accommodate rotated rectangle
|
|
579
|
+
margin = max(slab_width, total_height) * 0.2
|
|
580
|
+
|
|
581
|
+
# Find the bounds of all rotated rectangles
|
|
582
|
+
all_corners = []
|
|
583
|
+
current_y = 0
|
|
584
|
+
|
|
585
|
+
# Weak layer corners
|
|
586
|
+
wl_corners = create_sloped_layer(
|
|
587
|
+
0, current_y, slab_width, weak_layer.h, angle_rad
|
|
588
|
+
)
|
|
589
|
+
all_corners.extend(wl_corners)
|
|
590
|
+
current_y += weak_layer.h
|
|
591
|
+
|
|
592
|
+
# Slab layer corners
|
|
593
|
+
for layer in reversed(slab.layers):
|
|
594
|
+
layer_corners = create_sloped_layer(
|
|
595
|
+
0, current_y, slab_width, layer.h, angle_rad
|
|
596
|
+
)
|
|
597
|
+
all_corners.extend(layer_corners)
|
|
598
|
+
current_y += layer.h
|
|
599
|
+
|
|
600
|
+
all_corners = np.array(all_corners)
|
|
601
|
+
min_x, max_x = all_corners[:, 0].min(), all_corners[:, 0].max()
|
|
602
|
+
min_y, max_y = all_corners[:, 1].min(), all_corners[:, 1].max()
|
|
603
|
+
|
|
604
|
+
# Set axis limits with margin
|
|
605
|
+
ax.set_xlim(min_x - margin, max_x + margin)
|
|
606
|
+
ax.set_ylim(min_y - margin, max_y + margin)
|
|
607
|
+
|
|
608
|
+
# Set labels and title
|
|
609
|
+
ax.set_xlabel("Width (mm)")
|
|
610
|
+
ax.set_ylabel("Height (mm)")
|
|
611
|
+
ax.set_title(f"{title}\nSlope Angle: {angle}°")
|
|
612
|
+
|
|
613
|
+
# Add colorbar
|
|
614
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
615
|
+
sm.set_array([])
|
|
616
|
+
cbar = plt.colorbar(sm, ax=ax)
|
|
617
|
+
cbar.set_label("Density (kg/m³)")
|
|
618
|
+
|
|
619
|
+
# Add legend
|
|
620
|
+
weak_layer_patch = Patch(
|
|
621
|
+
facecolor=cmap(norm(weak_layer.rho)),
|
|
622
|
+
hatch="///",
|
|
623
|
+
edgecolor="black",
|
|
624
|
+
label="Weak Layer",
|
|
625
|
+
)
|
|
626
|
+
slab_patch = Patch(facecolor="gray", edgecolor="black", label="Slab Layers")
|
|
627
|
+
ax.legend(handles=[weak_layer_patch, slab_patch], loc="upper right")
|
|
628
|
+
|
|
629
|
+
# Equal aspect ratio and grid
|
|
630
|
+
ax.set_aspect("equal")
|
|
631
|
+
ax.grid(True, alpha=0.3)
|
|
632
|
+
|
|
633
|
+
# Remove axis ticks for cleaner look
|
|
634
|
+
ax.tick_params(axis="both", which="major", labelsize=8)
|
|
635
|
+
|
|
636
|
+
plt.tight_layout()
|
|
637
|
+
|
|
638
|
+
if filename:
|
|
639
|
+
self._save_figure(filename, fig)
|
|
640
|
+
|
|
641
|
+
return fig
|
|
642
|
+
|
|
643
|
+
def plot_section_forces(
|
|
644
|
+
self,
|
|
645
|
+
system_model: Optional[SystemModel] = None,
|
|
646
|
+
system_models: Optional[List[SystemModel]] = None,
|
|
647
|
+
filename: str = "section_forces",
|
|
648
|
+
labels: Optional[List[str]] = None,
|
|
649
|
+
colors: Optional[List[str]] = None,
|
|
650
|
+
):
|
|
651
|
+
"""
|
|
652
|
+
Plot section forces (N, M, V) for comparison.
|
|
653
|
+
|
|
654
|
+
Parameters
|
|
655
|
+
----------
|
|
656
|
+
system_model : SystemModel, optional
|
|
657
|
+
Single system to plot (overrides default)
|
|
658
|
+
system_models : List[SystemModel], optional
|
|
659
|
+
Multiple systems to plot (overrides default)
|
|
660
|
+
filename : str, optional
|
|
661
|
+
Filename for saving plot
|
|
662
|
+
labels : list of str, optional
|
|
663
|
+
Labels for each system.
|
|
664
|
+
colors : list of str, optional
|
|
665
|
+
Colors for each system.
|
|
666
|
+
"""
|
|
667
|
+
systems_to_plot = self._get_systems_to_plot(system_model, system_models)
|
|
668
|
+
|
|
669
|
+
if labels is None:
|
|
670
|
+
labels = [f"System {i + 1}" for i in range(len(systems_to_plot))]
|
|
671
|
+
if colors is None:
|
|
672
|
+
plot_colors = [self.colors[i, 0] for i in range(len(systems_to_plot))]
|
|
673
|
+
else:
|
|
674
|
+
plot_colors = colors
|
|
675
|
+
|
|
676
|
+
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
|
|
677
|
+
|
|
678
|
+
for i, system in enumerate(systems_to_plot):
|
|
679
|
+
analyzer = self._get_analyzer(system)
|
|
680
|
+
x, z, _ = analyzer.rasterize_solution()
|
|
681
|
+
fq = system.fq
|
|
682
|
+
|
|
683
|
+
# Convert x to meters for plotting
|
|
684
|
+
x_m = x / 1000
|
|
685
|
+
|
|
686
|
+
# Plot axial force N
|
|
687
|
+
N = fq.N(z)
|
|
688
|
+
axes[0].plot(x_m, N, color=plot_colors[i], label=labels[i], linewidth=2)
|
|
689
|
+
|
|
690
|
+
# Plot bending moment M
|
|
691
|
+
M = fq.M(z)
|
|
692
|
+
axes[1].plot(x_m, M, color=plot_colors[i], label=labels[i], linewidth=2)
|
|
693
|
+
|
|
694
|
+
# Plot shear force V
|
|
695
|
+
V = fq.V(z)
|
|
696
|
+
axes[2].plot(x_m, V, color=plot_colors[i], label=labels[i], linewidth=2)
|
|
697
|
+
|
|
698
|
+
# Formatting
|
|
699
|
+
axes[0].set_ylabel("N (N)")
|
|
700
|
+
axes[0].set_title("Axial Force")
|
|
701
|
+
axes[0].legend()
|
|
702
|
+
axes[0].grid(True, alpha=0.3)
|
|
703
|
+
|
|
704
|
+
axes[1].set_ylabel("M (Nmm)")
|
|
705
|
+
axes[1].set_title("Bending Moment")
|
|
706
|
+
axes[1].legend()
|
|
707
|
+
axes[1].grid(True, alpha=0.3)
|
|
708
|
+
|
|
709
|
+
axes[2].set_xlabel("Distance (m)")
|
|
710
|
+
axes[2].set_ylabel("V (N)")
|
|
711
|
+
axes[2].set_title("Shear Force")
|
|
712
|
+
axes[2].legend()
|
|
713
|
+
axes[2].grid(True, alpha=0.3)
|
|
714
|
+
|
|
715
|
+
plt.tight_layout()
|
|
716
|
+
|
|
717
|
+
if filename:
|
|
718
|
+
self._save_figure(filename, fig)
|
|
719
|
+
|
|
720
|
+
return fig
|
|
721
|
+
|
|
722
|
+
def plot_energy_release_rates(
|
|
723
|
+
self,
|
|
724
|
+
system_model: Optional[SystemModel] = None,
|
|
725
|
+
system_models: Optional[List[SystemModel]] = None,
|
|
726
|
+
filename: str = "ERR",
|
|
727
|
+
labels: Optional[List[str]] = None,
|
|
728
|
+
colors: Optional[List[str]] = None,
|
|
729
|
+
):
|
|
730
|
+
"""
|
|
731
|
+
Plot energy release rates (G_I, G_II) for comparison.
|
|
732
|
+
|
|
733
|
+
Parameters
|
|
734
|
+
----------
|
|
735
|
+
system_model : SystemModel, optional
|
|
736
|
+
Single system to plot (overrides default)
|
|
737
|
+
system_models : List[SystemModel], optional
|
|
738
|
+
Multiple systems to plot (overrides default)
|
|
739
|
+
filename : str, optional
|
|
740
|
+
Filename for saving plot
|
|
741
|
+
labels : list of str, optional
|
|
742
|
+
Labels for each system.
|
|
743
|
+
colors : list of str, optional
|
|
744
|
+
Colors for each system.
|
|
745
|
+
"""
|
|
746
|
+
systems_to_plot = self._get_systems_to_plot(system_model, system_models)
|
|
747
|
+
|
|
748
|
+
if labels is None:
|
|
749
|
+
labels = [f"System {i + 1}" for i in range(len(systems_to_plot))]
|
|
750
|
+
if colors is None:
|
|
751
|
+
plot_colors = [self.colors[i, 0] for i in range(len(systems_to_plot))]
|
|
752
|
+
else:
|
|
753
|
+
plot_colors = colors
|
|
754
|
+
|
|
755
|
+
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
|
|
756
|
+
|
|
757
|
+
for i, system in enumerate(systems_to_plot):
|
|
758
|
+
analyzer = self._get_analyzer(system)
|
|
759
|
+
x, z, _ = analyzer.rasterize_solution()
|
|
760
|
+
fq = system.fq
|
|
761
|
+
|
|
762
|
+
# Convert x to meters for plotting
|
|
763
|
+
x_m = x / 1000
|
|
764
|
+
|
|
765
|
+
# Plot Mode I energy release rate
|
|
766
|
+
G_I = fq.Gi(z, unit="kJ/m^2")
|
|
767
|
+
axes[0].plot(x_m, G_I, color=plot_colors[i], label=labels[i], linewidth=2)
|
|
768
|
+
|
|
769
|
+
# Plot Mode II energy release rate
|
|
770
|
+
G_II = fq.Gii(z, unit="kJ/m^2")
|
|
771
|
+
axes[1].plot(x_m, G_II, color=plot_colors[i], label=labels[i], linewidth=2)
|
|
772
|
+
|
|
773
|
+
# Formatting
|
|
774
|
+
axes[0].set_ylabel("G_I (kJ/m²)")
|
|
775
|
+
axes[0].set_title("Mode I Energy Release Rate")
|
|
776
|
+
axes[0].legend()
|
|
777
|
+
axes[0].grid(True, alpha=0.3)
|
|
778
|
+
|
|
779
|
+
axes[1].set_xlabel("Distance (m)")
|
|
780
|
+
axes[1].set_ylabel("G_II (kJ/m²)")
|
|
781
|
+
axes[1].set_title("Mode II Energy Release Rate")
|
|
782
|
+
axes[1].legend()
|
|
783
|
+
axes[1].grid(True, alpha=0.3)
|
|
784
|
+
|
|
785
|
+
plt.tight_layout()
|
|
786
|
+
|
|
787
|
+
if filename:
|
|
788
|
+
self._save_figure(filename, fig)
|
|
789
|
+
|
|
790
|
+
return fig
|
|
791
|
+
|
|
792
|
+
def plot_deformed(
|
|
793
|
+
self,
|
|
794
|
+
xsl: np.ndarray,
|
|
795
|
+
xwl: np.ndarray,
|
|
796
|
+
z: np.ndarray,
|
|
797
|
+
analyzer: Analyzer,
|
|
798
|
+
dz: int = 2,
|
|
799
|
+
scale: int = 100,
|
|
800
|
+
window: float = np.inf,
|
|
801
|
+
pad: int = 2,
|
|
802
|
+
levels: int = 300,
|
|
803
|
+
aspect: int = 2,
|
|
804
|
+
field: Literal["w", "u", "principal", "Sxx", "Txz", "Szz"] = "w",
|
|
805
|
+
normalize: bool = True,
|
|
806
|
+
filename: str = "deformed_slab",
|
|
807
|
+
) -> Figure:
|
|
808
|
+
"""
|
|
809
|
+
Plot deformed slab with field contours.
|
|
810
|
+
|
|
811
|
+
Parameters
|
|
812
|
+
----------
|
|
813
|
+
xsl : np.ndarray
|
|
814
|
+
Slab x-coordinates.
|
|
815
|
+
xwl : np.ndarray
|
|
816
|
+
Weak layer x-coordinates.
|
|
817
|
+
z : np.ndarray
|
|
818
|
+
Solution vector.
|
|
819
|
+
analyzer : Analyzer
|
|
820
|
+
Analyzer instance.
|
|
821
|
+
dz : int, optional
|
|
822
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
823
|
+
scale : int, optional
|
|
824
|
+
Deformation scale factor. Default is 100.
|
|
825
|
+
window : float, optional
|
|
826
|
+
Plot window width. Default is inf.
|
|
827
|
+
pad : int, optional
|
|
828
|
+
Padding around plot. Default is 2.
|
|
829
|
+
levels : int, optional
|
|
830
|
+
Number of contour levels. Default is 300.
|
|
831
|
+
aspect : int, optional
|
|
832
|
+
Aspect ratio. Default is 2.
|
|
833
|
+
field : str, optional
|
|
834
|
+
Field to plot ('w', 'u', 'principal', 'Sxx', 'Txz', 'Szz'). Default is 'w'.
|
|
835
|
+
normalize : bool, optional
|
|
836
|
+
Toggle normalization. Default is True.
|
|
837
|
+
filename : str, optional
|
|
838
|
+
Filename for saving plot. Default is "deformed_slab".
|
|
839
|
+
|
|
840
|
+
Returns
|
|
841
|
+
-------
|
|
842
|
+
matplotlib.figure.Figure
|
|
843
|
+
The generated plot figure.
|
|
844
|
+
"""
|
|
845
|
+
fig = plt.figure(figsize=(10, 8))
|
|
846
|
+
ax = fig.add_subplot(111)
|
|
847
|
+
|
|
848
|
+
zi = analyzer.get_zmesh(dz=dz)["z"]
|
|
849
|
+
H = analyzer.sm.slab.H
|
|
850
|
+
phi = analyzer.sm.scenario.phi
|
|
851
|
+
system_type = analyzer.sm.scenario.system_type
|
|
852
|
+
fq = analyzer.sm.fq
|
|
853
|
+
|
|
854
|
+
# Compute slab displacements on grid (cm)
|
|
855
|
+
Usl = np.vstack([fq.u(z, h0=h0, unit="cm") for h0 in zi])
|
|
856
|
+
Wsl = np.vstack([fq.w(z, unit="cm") for _ in zi])
|
|
857
|
+
Sigmawl = np.where(np.isfinite(xwl), fq.sig(z, unit="kPa"), np.nan)
|
|
858
|
+
Tauwl = np.where(np.isfinite(xwl), fq.tau(z, unit="kPa"), np.nan)
|
|
859
|
+
|
|
860
|
+
# Put coordinate origin at horizontal center
|
|
861
|
+
if system_type in ["skier", "skiers"]:
|
|
862
|
+
xsl = xsl - max(xsl) / 2
|
|
863
|
+
xwl = xwl - max(xwl) / 2
|
|
864
|
+
|
|
865
|
+
# Compute slab grid coordinates with vertical origin at top surface (cm)
|
|
866
|
+
Xsl, Zsl = np.meshgrid(1e-1 * (xsl), 1e-1 * (zi + H / 2))
|
|
867
|
+
|
|
868
|
+
# Get x-coordinate of maximum deflection w (cm) and derive plot limits
|
|
869
|
+
xfocus = xsl[np.max(np.argmax(Wsl, axis=1))] / 10
|
|
870
|
+
xmax = np.min([np.max([Xsl, Xsl + scale * Usl]) + pad, xfocus + window / 2])
|
|
871
|
+
xmin = np.max([np.min([Xsl, Xsl + scale * Usl]) - pad, xfocus - window / 2])
|
|
872
|
+
|
|
873
|
+
# Scale shown weak-layer thickness with to max deflection and add padding
|
|
874
|
+
if analyzer.sm.config.touchdown:
|
|
875
|
+
zmax = (
|
|
876
|
+
np.max(Zsl)
|
|
877
|
+
+ (analyzer.sm.weak_layer.h * 1e-1 * scale)
|
|
878
|
+
- (analyzer.sm.scenario.crack_h * 1e-1 * scale)
|
|
879
|
+
)
|
|
880
|
+
zmax = min(zmax, np.max(Zsl + scale * Wsl))
|
|
881
|
+
else:
|
|
882
|
+
zmax = np.max(Zsl + scale * Wsl) + pad
|
|
883
|
+
zmin = np.min(Zsl) - pad
|
|
884
|
+
|
|
885
|
+
# Compute weak-layer grid coordinates (cm)
|
|
886
|
+
Xwl, Zwl = np.meshgrid(1e-1 * xwl, [1e-1 * (zi[-1] + H / 2), zmax])
|
|
887
|
+
|
|
888
|
+
# Assemble weak-layer displacement field (top and bottom)
|
|
889
|
+
Uwl = np.vstack([Usl[-1, :], np.zeros(xwl.shape[0])])
|
|
890
|
+
Wwl = np.vstack([Wsl[-1, :], np.zeros(xwl.shape[0])])
|
|
891
|
+
|
|
892
|
+
# Compute stress or displacement fields
|
|
893
|
+
match field:
|
|
894
|
+
# Horizontal displacements (um)
|
|
895
|
+
case "u":
|
|
896
|
+
slab = 1e4 * Usl
|
|
897
|
+
weak = 1e4 * Usl[-1, :]
|
|
898
|
+
label = r"$u$ ($\mu$m)"
|
|
899
|
+
# Vertical deflection (um)
|
|
900
|
+
case "w":
|
|
901
|
+
slab = 1e4 * Wsl
|
|
902
|
+
weak = 1e4 * Wsl[-1, :]
|
|
903
|
+
label = r"$w$ ($\mu$m)"
|
|
904
|
+
# Axial normal stresses (kPa)
|
|
905
|
+
case "Sxx":
|
|
906
|
+
slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa")
|
|
907
|
+
weak = np.zeros(xwl.shape[0])
|
|
908
|
+
label = r"$\sigma_{xx}$ (kPa)"
|
|
909
|
+
# Shear stresses (kPa)
|
|
910
|
+
case "Txz":
|
|
911
|
+
slab = analyzer.Txz(z, phi, dz=dz, unit="kPa")
|
|
912
|
+
weak = Tauwl
|
|
913
|
+
label = r"$\tau_{xz}$ (kPa)"
|
|
914
|
+
# Transverse normal stresses (kPa)
|
|
915
|
+
case "Szz":
|
|
916
|
+
slab = analyzer.Szz(z, phi, dz=dz, unit="kPa")
|
|
917
|
+
weak = Sigmawl
|
|
918
|
+
label = r"$\sigma_{zz}$ (kPa)"
|
|
919
|
+
# Principal stresses
|
|
920
|
+
case "principal":
|
|
921
|
+
slab = analyzer.principal_stress_slab(
|
|
922
|
+
z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
|
|
923
|
+
)
|
|
924
|
+
weak = analyzer.principal_stress_weaklayer(
|
|
925
|
+
z, val="min", unit="kPa", normalize=normalize
|
|
926
|
+
)
|
|
927
|
+
if normalize:
|
|
928
|
+
label = (
|
|
929
|
+
r"$\sigma_\mathrm{I}/\sigma_+$ (slab), "
|
|
930
|
+
r"$\sigma_\mathrm{I\!I\!I}/\sigma_-$ (weak layer)"
|
|
931
|
+
)
|
|
932
|
+
else:
|
|
933
|
+
label = (
|
|
934
|
+
r"$\sigma_\mathrm{I}$ (kPa, slab), "
|
|
935
|
+
r"$\sigma_\mathrm{I\!I\!I}$ (kPa, weak layer)"
|
|
936
|
+
)
|
|
937
|
+
case _:
|
|
938
|
+
raise ValueError(
|
|
939
|
+
f"Invalid input '{field}' for field. Valid options are "
|
|
940
|
+
"'u', 'w', 'Sxx', 'Txz', 'Szz', or 'principal'"
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Complement label
|
|
944
|
+
label += r" $\longrightarrow$"
|
|
945
|
+
|
|
946
|
+
# Assemble weak-layer output on grid
|
|
947
|
+
weak = np.vstack([weak, weak])
|
|
948
|
+
|
|
949
|
+
# Normalize colormap
|
|
950
|
+
absmax = np.nanmax(np.abs([slab.min(), slab.max(), weak.min(), weak.max()]))
|
|
951
|
+
clim = np.round(absmax, _significant_digits(absmax))
|
|
952
|
+
levels = np.linspace(-clim, clim, num=levels + 1, endpoint=True)
|
|
953
|
+
# nanmax = np.nanmax([slab.max(), weak.max()])
|
|
954
|
+
# nanmin = np.nanmin([slab.min(), weak.min()])
|
|
955
|
+
# norm = MidpointNormalize(vmin=nanmin, vmax=nanmax)
|
|
956
|
+
|
|
957
|
+
# Plot baseline
|
|
958
|
+
ax.axhline(zmax, color="k", linewidth=1)
|
|
959
|
+
|
|
960
|
+
# Plot outlines of the undeformed and deformed slab
|
|
961
|
+
ax.plot(_outline(Xsl), _outline(Zsl), "k--", alpha=0.3, linewidth=1)
|
|
962
|
+
ax.plot(
|
|
963
|
+
_outline(Xsl + scale * Usl), _outline(Zsl + scale * Wsl), "k", linewidth=1
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Plot deformed weak-layer _outline
|
|
967
|
+
if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
|
|
968
|
+
nanmask = np.isfinite(xwl)
|
|
969
|
+
ax.plot(
|
|
970
|
+
_outline(Xwl[:, nanmask] + scale * Uwl[:, nanmask]),
|
|
971
|
+
_outline(Zwl[:, nanmask] + scale * Wwl[:, nanmask]),
|
|
972
|
+
"k",
|
|
973
|
+
linewidth=1,
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
cmap = plt.get_cmap("RdBu_r")
|
|
977
|
+
cmap.set_over(_adjust_lightness(cmap(1.0), 0.9))
|
|
978
|
+
cmap.set_under(_adjust_lightness(cmap(0.0), 0.9))
|
|
979
|
+
|
|
980
|
+
# Plot fields
|
|
981
|
+
ax.contourf(
|
|
982
|
+
Xsl + scale * Usl,
|
|
983
|
+
Zsl + scale * Wsl,
|
|
984
|
+
slab,
|
|
985
|
+
levels=levels,
|
|
986
|
+
cmap=cmap,
|
|
987
|
+
extend="both",
|
|
988
|
+
)
|
|
989
|
+
ax.contourf(
|
|
990
|
+
Xwl + scale * Uwl,
|
|
991
|
+
Zwl + scale * Wwl,
|
|
992
|
+
weak,
|
|
993
|
+
levels=levels,
|
|
994
|
+
cmap=cmap,
|
|
995
|
+
extend="both",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
# Plot setup
|
|
999
|
+
ax.axis("scaled")
|
|
1000
|
+
ax.set_xlim([xmin, xmax])
|
|
1001
|
+
ax.set_ylim([zmin, zmax])
|
|
1002
|
+
ax.set_aspect(aspect)
|
|
1003
|
+
ax.invert_yaxis()
|
|
1004
|
+
ax.use_sticky_edges = False
|
|
1005
|
+
|
|
1006
|
+
# Plot labels
|
|
1007
|
+
ax.set_xlabel(r"lateral position $x$ (cm) $\longrightarrow$")
|
|
1008
|
+
ax.set_ylabel("depth below surface\n" + r"$\longleftarrow $ $d$ (cm)")
|
|
1009
|
+
ax.set_title(rf"${scale}\!\times\!$ scaled deformations (cm)", size=10)
|
|
1010
|
+
|
|
1011
|
+
# Show colorbar
|
|
1012
|
+
ticks = np.linspace(levels[0], levels[-1], num=11, endpoint=True)
|
|
1013
|
+
fig.colorbar(
|
|
1014
|
+
ax.contourf(
|
|
1015
|
+
Xsl + scale * Usl,
|
|
1016
|
+
Zsl + scale * Wsl,
|
|
1017
|
+
slab,
|
|
1018
|
+
levels=levels,
|
|
1019
|
+
cmap=cmap,
|
|
1020
|
+
extend="both",
|
|
1021
|
+
),
|
|
1022
|
+
orientation="horizontal",
|
|
1023
|
+
ticks=ticks,
|
|
1024
|
+
label=label,
|
|
1025
|
+
aspect=35,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
# Save figure
|
|
1029
|
+
self._save_figure(filename, fig)
|
|
1030
|
+
|
|
1031
|
+
return fig
|
|
1032
|
+
|
|
1033
|
+
def plot_stress_envelope(
|
|
1034
|
+
self,
|
|
1035
|
+
system_model: SystemModel,
|
|
1036
|
+
criteria_evaluator: CriteriaEvaluator,
|
|
1037
|
+
all_envelopes: bool = False,
|
|
1038
|
+
filename: Optional[str] = None,
|
|
1039
|
+
):
|
|
1040
|
+
"""
|
|
1041
|
+
Plot stress envelope in τ-σ space.
|
|
1042
|
+
|
|
1043
|
+
Parameters
|
|
1044
|
+
----------
|
|
1045
|
+
system_model : SystemModel
|
|
1046
|
+
System to plot
|
|
1047
|
+
criteria_evaluator : CriteriaEvaluator
|
|
1048
|
+
Criteria evaluator to use for the stress envelope
|
|
1049
|
+
all_envelopes : bool, optional
|
|
1050
|
+
Whether to plot all four quadrants of the envelope
|
|
1051
|
+
filename : str, optional
|
|
1052
|
+
Filename for saving plot
|
|
1053
|
+
"""
|
|
1054
|
+
analyzer = self._get_analyzer(system_model)
|
|
1055
|
+
_, z, _ = analyzer.rasterize_solution(num=10000)
|
|
1056
|
+
fq = system_model.fq
|
|
1057
|
+
|
|
1058
|
+
# Calculate stresses
|
|
1059
|
+
sigma = np.abs(fq.sig(z, unit="kPa"))
|
|
1060
|
+
tau = fq.tau(z, unit="kPa")
|
|
1061
|
+
|
|
1062
|
+
fig, ax = plt.subplots(figsize=(4, 8 / 3))
|
|
1063
|
+
|
|
1064
|
+
# Plot stress path
|
|
1065
|
+
ax.plot(sigma, tau, "b-", linewidth=2, label="Stress Path")
|
|
1066
|
+
ax.scatter(
|
|
1067
|
+
sigma[0], tau[0], color="green", s=10, marker="o", label="Start", zorder=5
|
|
1068
|
+
)
|
|
1069
|
+
ax.scatter(
|
|
1070
|
+
sigma[-1], tau[-1], color="red", s=10, marker="s", label="End", zorder=5
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# --- Programmatic Envelope Calculation ---
|
|
1074
|
+
weak_layer = system_model.weak_layer
|
|
1075
|
+
|
|
1076
|
+
# Define a function to find the root for a given tau
|
|
1077
|
+
def find_sigma_for_tau(tau_val, sigma_c, method: Optional[str] = None):
|
|
1078
|
+
# Target function to find the root of: envelope(sigma, tau) - 1 = 0
|
|
1079
|
+
def envelope_root_func(sigma_val):
|
|
1080
|
+
return (
|
|
1081
|
+
criteria_evaluator.stress_envelope(
|
|
1082
|
+
sigma_val, tau_val, weak_layer, method=method
|
|
1083
|
+
)
|
|
1084
|
+
- 1
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
try:
|
|
1088
|
+
search_upper_bound = sigma_c * 1.1
|
|
1089
|
+
sigma_root = brentq(
|
|
1090
|
+
envelope_root_func,
|
|
1091
|
+
a=0,
|
|
1092
|
+
b=search_upper_bound,
|
|
1093
|
+
xtol=1e-6,
|
|
1094
|
+
rtol=1e-6,
|
|
1095
|
+
)
|
|
1096
|
+
return sigma_root
|
|
1097
|
+
except ValueError:
|
|
1098
|
+
return np.nan
|
|
1099
|
+
|
|
1100
|
+
# Calculate the corresponding sigma for each tau
|
|
1101
|
+
if all_envelopes:
|
|
1102
|
+
methods = [
|
|
1103
|
+
"mede_s-RG1",
|
|
1104
|
+
"mede_s-RG2",
|
|
1105
|
+
"mede_s-FCDH",
|
|
1106
|
+
"schottner",
|
|
1107
|
+
"adam_unpublished",
|
|
1108
|
+
]
|
|
1109
|
+
else:
|
|
1110
|
+
methods = [criteria_evaluator.criteria_config.stress_envelope_method]
|
|
1111
|
+
|
|
1112
|
+
colors = self.colors
|
|
1113
|
+
colors = np.array(colors)
|
|
1114
|
+
colors = np.tile(colors, (len(methods), 1))
|
|
1115
|
+
|
|
1116
|
+
max_sigma = 0
|
|
1117
|
+
max_tau = 0
|
|
1118
|
+
for i, method in enumerate(methods):
|
|
1119
|
+
# Calculate tau_c for the given method to define tau_range
|
|
1120
|
+
config = criteria_evaluator.criteria_config
|
|
1121
|
+
density = weak_layer.rho
|
|
1122
|
+
tau_c = 0.0 # fallback
|
|
1123
|
+
sigma_c = 0.0
|
|
1124
|
+
if method == "adam_unpublished":
|
|
1125
|
+
scaling_factor = config.scaling_factor
|
|
1126
|
+
order_of_magnitude = config.order_of_magnitude
|
|
1127
|
+
if scaling_factor > 1:
|
|
1128
|
+
order_of_magnitude = 0.7
|
|
1129
|
+
scaling_factor = max(scaling_factor, 0.55)
|
|
1130
|
+
|
|
1131
|
+
tau_c = 5.09 * (scaling_factor**order_of_magnitude)
|
|
1132
|
+
sigma_c = 6.16 * (scaling_factor**order_of_magnitude)
|
|
1133
|
+
elif method == "schottner":
|
|
1134
|
+
rho_ice = 916.7
|
|
1135
|
+
sigma_y = 2000
|
|
1136
|
+
sigma_c_adam = 6.16
|
|
1137
|
+
tau_c_adam = 5.09
|
|
1138
|
+
order_of_magnitude = config.order_of_magnitude
|
|
1139
|
+
sigma_c = sigma_y * 13 * (density / rho_ice) ** order_of_magnitude
|
|
1140
|
+
tau_c = tau_c_adam * (sigma_c / sigma_c_adam)
|
|
1141
|
+
sigma_c = sigma_y * 13 * (density / rho_ice) ** order_of_magnitude
|
|
1142
|
+
elif method == "mede_s-RG1":
|
|
1143
|
+
tau_c = 3.53 # This is tau_T from Mede's paper
|
|
1144
|
+
sigma_c = 7.00
|
|
1145
|
+
elif method == "mede_s-RG2":
|
|
1146
|
+
tau_c = 1.22 # This is tau_T from Mede's paper
|
|
1147
|
+
sigma_c = 2.33
|
|
1148
|
+
elif method == "mede_s-FCDH":
|
|
1149
|
+
tau_c = 0.61 # This is tau_T from Mede's paper
|
|
1150
|
+
sigma_c = 1.49
|
|
1151
|
+
|
|
1152
|
+
tau_range = np.linspace(0, tau_c, 100)
|
|
1153
|
+
sigma_envelope = np.array(
|
|
1154
|
+
[find_sigma_for_tau(t, sigma_c, method) for t in tau_range]
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
# Remove nan values where no root was found
|
|
1158
|
+
valid_points = ~np.isnan(sigma_envelope)
|
|
1159
|
+
valid_tau_range = tau_range[valid_points]
|
|
1160
|
+
sigma_envelope = sigma_envelope[valid_points]
|
|
1161
|
+
|
|
1162
|
+
max_sigma = max(max_sigma, np.max(sigma_envelope))
|
|
1163
|
+
max_tau = max(max_tau, np.max(np.abs(valid_tau_range)))
|
|
1164
|
+
ax.plot(
|
|
1165
|
+
sigma_envelope,
|
|
1166
|
+
valid_tau_range,
|
|
1167
|
+
"--",
|
|
1168
|
+
linewidth=2,
|
|
1169
|
+
label=method,
|
|
1170
|
+
color=colors[i, 0],
|
|
1171
|
+
)
|
|
1172
|
+
ax.plot(
|
|
1173
|
+
-sigma_envelope, valid_tau_range, "--", linewidth=2, color=colors[i, 0]
|
|
1174
|
+
)
|
|
1175
|
+
ax.plot(
|
|
1176
|
+
-sigma_envelope,
|
|
1177
|
+
-valid_tau_range,
|
|
1178
|
+
"--",
|
|
1179
|
+
linewidth=2,
|
|
1180
|
+
color=colors[i, 0],
|
|
1181
|
+
)
|
|
1182
|
+
ax.plot(
|
|
1183
|
+
sigma_envelope, -valid_tau_range, "--", linewidth=2, color=colors[i, 0]
|
|
1184
|
+
)
|
|
1185
|
+
ax.scatter(0, tau_c, color="black", s=10, marker="o")
|
|
1186
|
+
ax.text(0, tau_c, r"$\tau_c$", color="black", ha="center", va="bottom")
|
|
1187
|
+
ax.scatter(sigma_c, 0, color="black", s=10, marker="o")
|
|
1188
|
+
ax.text(sigma_c, 0, r"$\sigma_c$", color="black", ha="left", va="center")
|
|
1189
|
+
|
|
1190
|
+
# Formatting
|
|
1191
|
+
ax.set_xlabel("Compressive Strength σ (kPa)")
|
|
1192
|
+
ax.set_ylabel("Shear Strength τ (kPa)")
|
|
1193
|
+
ax.set_title("Weak Layer Stress Envelope")
|
|
1194
|
+
ax.legend()
|
|
1195
|
+
ax.grid(True, alpha=0.3)
|
|
1196
|
+
ax.axhline(y=0, color="k", linewidth=0.5)
|
|
1197
|
+
ax.axvline(x=0, color="k", linewidth=0.5)
|
|
1198
|
+
|
|
1199
|
+
max_tau = max(max_tau, float(np.max(np.abs(tau))))
|
|
1200
|
+
max_sigma = max(max_sigma, float(np.max(np.abs(sigma))))
|
|
1201
|
+
ax.set_xlim(0, max_sigma * 1.1)
|
|
1202
|
+
ax.set_ylim(-max_tau * 1.1, max_tau * 1.1)
|
|
1203
|
+
|
|
1204
|
+
plt.tight_layout()
|
|
1205
|
+
|
|
1206
|
+
if filename:
|
|
1207
|
+
self._save_figure(filename, fig)
|
|
1208
|
+
|
|
1209
|
+
return fig
|
|
1210
|
+
|
|
1211
|
+
def plot_err_envelope(
|
|
1212
|
+
self,
|
|
1213
|
+
system_model: SystemModel,
|
|
1214
|
+
criteria_evaluator: CriteriaEvaluator,
|
|
1215
|
+
filename: str = "err_envelope",
|
|
1216
|
+
) -> Figure:
|
|
1217
|
+
"""Plot the ERR envelope."""
|
|
1218
|
+
analyzer = self._get_analyzer(system_model)
|
|
1219
|
+
|
|
1220
|
+
incr_energy = analyzer.incremental_ERR(unit="J/m^2")
|
|
1221
|
+
G_I = incr_energy[1]
|
|
1222
|
+
G_II = incr_energy[2]
|
|
1223
|
+
|
|
1224
|
+
fig, ax = plt.subplots(figsize=(4, 8 / 3))
|
|
1225
|
+
|
|
1226
|
+
# Plot stress path
|
|
1227
|
+
ax.scatter(
|
|
1228
|
+
np.abs(G_I),
|
|
1229
|
+
np.abs(G_II),
|
|
1230
|
+
color="blue",
|
|
1231
|
+
s=50,
|
|
1232
|
+
marker="o",
|
|
1233
|
+
label="Incremental ERR",
|
|
1234
|
+
zorder=5,
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
G_Ic = system_model.weak_layer.G_Ic
|
|
1238
|
+
G_IIc = system_model.weak_layer.G_IIc
|
|
1239
|
+
ax.scatter(0, G_IIc, color="black", s=100, marker="o", zorder=5)
|
|
1240
|
+
ax.text(
|
|
1241
|
+
0.01,
|
|
1242
|
+
G_IIc + 0.02,
|
|
1243
|
+
r"$G_{IIc}$",
|
|
1244
|
+
color="black",
|
|
1245
|
+
ha="left",
|
|
1246
|
+
va="center",
|
|
1247
|
+
)
|
|
1248
|
+
ax.scatter(G_Ic, 0, color="black", s=100, marker="o", zorder=5)
|
|
1249
|
+
ax.text(
|
|
1250
|
+
G_Ic + 0.01,
|
|
1251
|
+
0.01,
|
|
1252
|
+
r"$G_{Ic}$",
|
|
1253
|
+
color="black",
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
# --- Programmatic Envelope Calculation ---
|
|
1257
|
+
weak_layer = system_model.weak_layer
|
|
1258
|
+
|
|
1259
|
+
# Define a function to find the root for a given G_II
|
|
1260
|
+
def find_GI_for_GII(GII_val):
|
|
1261
|
+
# Target function to find the root of: envelope(sigma, tau) - 1 = 0
|
|
1262
|
+
def envelope_root_func(GI_val):
|
|
1263
|
+
return (
|
|
1264
|
+
criteria_evaluator.fracture_toughness_envelope(
|
|
1265
|
+
GI_val,
|
|
1266
|
+
GII_val,
|
|
1267
|
+
weak_layer,
|
|
1268
|
+
)
|
|
1269
|
+
- 1
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
try:
|
|
1273
|
+
GI_root = brentq(envelope_root_func, a=0, b=50, xtol=1e-6, rtol=1e-6)
|
|
1274
|
+
return GI_root
|
|
1275
|
+
except ValueError:
|
|
1276
|
+
return np.nan
|
|
1277
|
+
|
|
1278
|
+
# Generate a range of G values in the positive quadrant
|
|
1279
|
+
GII_max = system_model.weak_layer.G_IIc * 1.1
|
|
1280
|
+
GII_range = np.linspace(0, GII_max, 100)
|
|
1281
|
+
|
|
1282
|
+
GI_envelope = np.array([find_GI_for_GII(t) for t in GII_range])
|
|
1283
|
+
|
|
1284
|
+
# Remove nan values where no root was found
|
|
1285
|
+
valid_points = ~np.isnan(GI_envelope)
|
|
1286
|
+
valid_GII_range = GII_range[valid_points]
|
|
1287
|
+
GI_envelope = GI_envelope[valid_points]
|
|
1288
|
+
|
|
1289
|
+
ax.plot(
|
|
1290
|
+
GI_envelope,
|
|
1291
|
+
valid_GII_range,
|
|
1292
|
+
"--",
|
|
1293
|
+
linewidth=2,
|
|
1294
|
+
label="Fracture Toughness Envelope",
|
|
1295
|
+
color="red",
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
# Formatting
|
|
1299
|
+
ax.set_xlabel("GI (J/m²)")
|
|
1300
|
+
ax.set_ylabel("GII (J/m²)")
|
|
1301
|
+
ax.set_title("Fracture Toughness Envelope")
|
|
1302
|
+
ax.legend()
|
|
1303
|
+
ax.grid(True, alpha=0.3)
|
|
1304
|
+
ax.axhline(y=0, color="k", linewidth=0.5)
|
|
1305
|
+
ax.axvline(x=0, color="k", linewidth=0.5)
|
|
1306
|
+
ax.set_xlim(0, max(np.abs(GI_envelope)) * 1.1)
|
|
1307
|
+
ax.set_ylim(0, max(np.abs(valid_GII_range)) * 1.1)
|
|
1308
|
+
|
|
1309
|
+
plt.tight_layout()
|
|
1310
|
+
|
|
1311
|
+
self._save_figure(filename, fig)
|
|
1312
|
+
|
|
1313
|
+
return fig
|
|
1314
|
+
|
|
1315
|
+
def plot_analysis(
|
|
1316
|
+
self,
|
|
1317
|
+
system: SystemModel,
|
|
1318
|
+
criteria_evaluator: CriteriaEvaluator,
|
|
1319
|
+
min_force_result: FindMinimumForceResult,
|
|
1320
|
+
min_crack_length: float,
|
|
1321
|
+
coupled_criterion_result: CoupledCriterionResult,
|
|
1322
|
+
dz: int = 2,
|
|
1323
|
+
deformation_scale: float = 100.0,
|
|
1324
|
+
window: int = np.inf,
|
|
1325
|
+
levels: int = 300,
|
|
1326
|
+
filename: str = "analysis",
|
|
1327
|
+
) -> Figure:
|
|
1328
|
+
"""
|
|
1329
|
+
Plot deformed slab with field contours.
|
|
1330
|
+
|
|
1331
|
+
Parameters
|
|
1332
|
+
----------
|
|
1333
|
+
field : str, default 'w'
|
|
1334
|
+
Field to plot ('w', 'u', 'principal', 'sigma', 'tau')
|
|
1335
|
+
system_model : SystemModel, optional
|
|
1336
|
+
System to plot (uses first system if not specified)
|
|
1337
|
+
filename : str, optional
|
|
1338
|
+
Filename for saving plot
|
|
1339
|
+
"""
|
|
1340
|
+
fig = plt.figure(figsize=(12, 10))
|
|
1341
|
+
ax = fig.add_subplot(111)
|
|
1342
|
+
|
|
1343
|
+
logger.debug("System Segments: %s", system.scenario.segments)
|
|
1344
|
+
analyzer = Analyzer(system)
|
|
1345
|
+
xsl, z, xwl = analyzer.rasterize_solution(mode="cracked", num=200)
|
|
1346
|
+
|
|
1347
|
+
zi = analyzer.get_zmesh(dz=dz)["z"]
|
|
1348
|
+
H = analyzer.sm.slab.H
|
|
1349
|
+
h = system.weak_layer.h
|
|
1350
|
+
system_type = analyzer.sm.scenario.system_type
|
|
1351
|
+
fq = analyzer.sm.fq
|
|
1352
|
+
|
|
1353
|
+
# Generate a window size which fits the plots
|
|
1354
|
+
window = min(window, np.max(xwl) - np.min(xwl), 10000)
|
|
1355
|
+
|
|
1356
|
+
# Calculate scaling factors for proper aspect ratio and relative heights
|
|
1357
|
+
# 7:1 aspect ratio: vertical extent = window / 7
|
|
1358
|
+
total_vertical_extent = window / 7.0
|
|
1359
|
+
|
|
1360
|
+
# Slab should appear 2x taller than weak layer
|
|
1361
|
+
# So slab gets 2/3 of vertical space, weak layer gets 1/3
|
|
1362
|
+
slab_display_height = (2 / 3) * total_vertical_extent
|
|
1363
|
+
weak_layer_display_height = (1 / 3) * total_vertical_extent
|
|
1364
|
+
|
|
1365
|
+
# Calculate separate scaling factors for coordinates
|
|
1366
|
+
slab_z_scale = slab_display_height / H
|
|
1367
|
+
weak_layer_z_scale = weak_layer_display_height / h
|
|
1368
|
+
|
|
1369
|
+
# Deformation scaling (separate from coordinate scaling)
|
|
1370
|
+
scale = deformation_scale
|
|
1371
|
+
|
|
1372
|
+
# Compute slab displacements on grid (cm)
|
|
1373
|
+
Usl = np.vstack([fq.u(z, h0=h0, unit="cm") for h0 in zi])
|
|
1374
|
+
Wsl = np.vstack([fq.w(z, unit="cm") for _ in zi])
|
|
1375
|
+
Sigmawl = np.where(np.isfinite(xwl), fq.sig(z, unit="kPa"), np.nan)
|
|
1376
|
+
Tauwl = np.where(np.isfinite(xwl), fq.tau(z, unit="kPa"), np.nan)
|
|
1377
|
+
|
|
1378
|
+
# Put coordinate origin at horizontal center
|
|
1379
|
+
if system_type in ["skier", "skiers"]:
|
|
1380
|
+
xsl = xsl - max(xsl) / 2
|
|
1381
|
+
xwl = xwl - max(xwl) / 2
|
|
1382
|
+
|
|
1383
|
+
# Compute slab grid coordinates with vertical origin at top surface (cm)
|
|
1384
|
+
Xsl, Zsl = np.meshgrid(1e-1 * (xsl), 1e-1 * slab_z_scale * (zi - H / 2))
|
|
1385
|
+
|
|
1386
|
+
# Get x-coordinate of maximum deflection w (cm) and derive plot limits
|
|
1387
|
+
xmax = np.min([np.max([Xsl, Xsl + scale * Usl]), 1e-1 * window / 2])
|
|
1388
|
+
xmin = np.max([np.min([Xsl, Xsl + scale * Usl]), -1e-1 * window / 2])
|
|
1389
|
+
|
|
1390
|
+
# Compute weak-layer grid coordinates (cm)
|
|
1391
|
+
# Position weak layer below the slab
|
|
1392
|
+
Xwl, Zwl = np.meshgrid(
|
|
1393
|
+
1e-1 * xwl,
|
|
1394
|
+
[
|
|
1395
|
+
0, # Top of weak layer (at bottom of slab)
|
|
1396
|
+
1e-1 * weak_layer_z_scale * h, # Bottom of weak layer
|
|
1397
|
+
],
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
# Assemble weak-layer displacement field (top and bottom)
|
|
1401
|
+
Uwl = np.vstack([Usl[-1, :], np.zeros(xwl.shape[0])])
|
|
1402
|
+
Wwl = np.vstack([Wsl[-1, :], np.zeros(xwl.shape[0])])
|
|
1403
|
+
|
|
1404
|
+
stress_envelope = criteria_evaluator.stress_envelope(
|
|
1405
|
+
Sigmawl, Tauwl, system.weak_layer
|
|
1406
|
+
)
|
|
1407
|
+
stress_envelope[np.isnan(stress_envelope)] = np.nanmax(stress_envelope)
|
|
1408
|
+
|
|
1409
|
+
# Assemble weak-layer output on grid
|
|
1410
|
+
weak = np.vstack([stress_envelope, stress_envelope])
|
|
1411
|
+
|
|
1412
|
+
# Normalize colormap
|
|
1413
|
+
levels = np.linspace(0, 1, num=levels + 1, endpoint=True)
|
|
1414
|
+
|
|
1415
|
+
# Plot outlines of the undeformed and deformed slab
|
|
1416
|
+
ax.plot(
|
|
1417
|
+
_outline(Xsl),
|
|
1418
|
+
_outline(Zsl),
|
|
1419
|
+
linestyle="--",
|
|
1420
|
+
color="yellow",
|
|
1421
|
+
alpha=0.3,
|
|
1422
|
+
linewidth=1,
|
|
1423
|
+
)
|
|
1424
|
+
ax.plot(
|
|
1425
|
+
_outline(Xsl + scale * Usl),
|
|
1426
|
+
_outline(Zsl + scale * Wsl),
|
|
1427
|
+
color="blue",
|
|
1428
|
+
linewidth=1,
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
# Plot deformed weak-layer _outline
|
|
1432
|
+
nanmask = np.isfinite(xwl)
|
|
1433
|
+
ax.plot(
|
|
1434
|
+
_outline(Xwl[:, nanmask] + scale * Uwl[:, nanmask]),
|
|
1435
|
+
_outline(Zwl[:, nanmask] + scale * Wwl[:, nanmask]),
|
|
1436
|
+
"k",
|
|
1437
|
+
linewidth=1,
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
cmap = plt.get_cmap("RdBu_r")
|
|
1441
|
+
cmap.set_over(_adjust_lightness(cmap(1.0), 0.9))
|
|
1442
|
+
cmap.set_under(_adjust_lightness(cmap(0.0), 0.9))
|
|
1443
|
+
|
|
1444
|
+
ax.contourf(
|
|
1445
|
+
Xwl + scale * Uwl,
|
|
1446
|
+
Zwl + scale * Wwl,
|
|
1447
|
+
weak,
|
|
1448
|
+
levels=levels,
|
|
1449
|
+
cmap=cmap,
|
|
1450
|
+
extend="both",
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
# Plot setup
|
|
1454
|
+
ax.axis("scaled")
|
|
1455
|
+
ax.set_xlim([xmin, xmax])
|
|
1456
|
+
ax.invert_yaxis()
|
|
1457
|
+
ax.use_sticky_edges = False
|
|
1458
|
+
|
|
1459
|
+
# Set up custom y-axis ticks to show real scaled heights
|
|
1460
|
+
# Calculate the actual extent of the plot
|
|
1461
|
+
slab_top = 1e-1 * slab_z_scale * (zi[0] - H / 2) # Top of slab
|
|
1462
|
+
slab_bottom = 1e-1 * slab_z_scale * (zi[-1] - H / 2) # Bottom of slab
|
|
1463
|
+
weak_layer_bottom = 1e-1 * weak_layer_z_scale * h # Bottom of weak layer
|
|
1464
|
+
|
|
1465
|
+
# Create tick positions and labels
|
|
1466
|
+
y_ticks = []
|
|
1467
|
+
y_labels = []
|
|
1468
|
+
|
|
1469
|
+
# Slab ticks (show actual slab heights in mm)
|
|
1470
|
+
num_slab_ticks = 5
|
|
1471
|
+
slab_tick_positions = np.linspace(slab_bottom, slab_top, num_slab_ticks)
|
|
1472
|
+
slab_height_ticks = np.linspace(
|
|
1473
|
+
0, -H, num_slab_ticks
|
|
1474
|
+
) # Actual slab heights in mm
|
|
1475
|
+
|
|
1476
|
+
for pos, height in zip(slab_tick_positions, slab_height_ticks):
|
|
1477
|
+
y_ticks.append(pos)
|
|
1478
|
+
y_labels.append(f"{height:.0f}")
|
|
1479
|
+
|
|
1480
|
+
# Weak layer ticks (show actual weak layer heights in mm)
|
|
1481
|
+
num_wl_ticks = 3
|
|
1482
|
+
wl_tick_positions = np.linspace(0, weak_layer_bottom, num_wl_ticks)
|
|
1483
|
+
wl_height_ticks = np.linspace(
|
|
1484
|
+
0, h, num_wl_ticks
|
|
1485
|
+
) # Actual weak layer heights in mm
|
|
1486
|
+
|
|
1487
|
+
for pos, height in zip(wl_tick_positions, wl_height_ticks):
|
|
1488
|
+
y_ticks.append(pos)
|
|
1489
|
+
y_labels.append(f"{height:.0f}")
|
|
1490
|
+
|
|
1491
|
+
# Set the custom ticks
|
|
1492
|
+
ax.set_yticks(y_ticks)
|
|
1493
|
+
ax.set_yticklabels(y_labels)
|
|
1494
|
+
|
|
1495
|
+
# Add grid lines for better readability
|
|
1496
|
+
ax.grid(True, alpha=0.3)
|
|
1497
|
+
|
|
1498
|
+
# Add horizontal line to separate slab and weak layer
|
|
1499
|
+
ax.axhline(y=slab_bottom, color="black", linewidth=1, alpha=0.5, linestyle="--")
|
|
1500
|
+
|
|
1501
|
+
# === ADD ANALYSIS ANNOTATIONS ===
|
|
1502
|
+
|
|
1503
|
+
# 1. Vertical lines for min_crack_length (centered at x=0)
|
|
1504
|
+
min_crack_length_cm = min_crack_length / 10 # Convert mm to cm
|
|
1505
|
+
ax.plot(
|
|
1506
|
+
[-min_crack_length_cm / 2, -min_crack_length_cm / 2],
|
|
1507
|
+
[0, weak_layer_bottom],
|
|
1508
|
+
color="orange",
|
|
1509
|
+
linewidth=1,
|
|
1510
|
+
alpha=0.7,
|
|
1511
|
+
label=f"Crack Propagation: ±{min_crack_length / 2:.0f}mm",
|
|
1512
|
+
)
|
|
1513
|
+
ax.plot(
|
|
1514
|
+
[min_crack_length_cm / 2, min_crack_length_cm / 2],
|
|
1515
|
+
[0, weak_layer_bottom],
|
|
1516
|
+
color="orange",
|
|
1517
|
+
linewidth=1,
|
|
1518
|
+
alpha=0.7,
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
base_square_size = (1e-1 * window) / 25 # Base size for scaling
|
|
1522
|
+
segment_position = 0 # Track cumulative position
|
|
1523
|
+
square_spacing = 2.0 # Space above slab for squares
|
|
1524
|
+
|
|
1525
|
+
# Collect weight information for legend
|
|
1526
|
+
weight_legend_items = []
|
|
1527
|
+
|
|
1528
|
+
for segment in system.scenario.segments:
|
|
1529
|
+
segment_position += segment.length
|
|
1530
|
+
if segment.m > 0: # If there's a weight at this segment
|
|
1531
|
+
# Convert position to cm and center at x=0
|
|
1532
|
+
square_x = (segment_position / 10) - (1e-1 * max(xsl))
|
|
1533
|
+
square_y = slab_top - square_spacing # Position above slab
|
|
1534
|
+
|
|
1535
|
+
# Calculate square side length based on cube root of weight (volume scaling)
|
|
1536
|
+
actual_side_length = base_square_size * (segment.m / 100) ** (1 / 3)
|
|
1537
|
+
|
|
1538
|
+
# Draw actual skier weight square (filled, blue)
|
|
1539
|
+
actual_square = Rectangle(
|
|
1540
|
+
(square_x - actual_side_length / 2, square_y - actual_side_length),
|
|
1541
|
+
actual_side_length,
|
|
1542
|
+
actual_side_length,
|
|
1543
|
+
facecolor="blue",
|
|
1544
|
+
alpha=0.7,
|
|
1545
|
+
edgecolor="blue",
|
|
1546
|
+
linewidth=1,
|
|
1547
|
+
)
|
|
1548
|
+
ax.add_patch(actual_square)
|
|
1549
|
+
|
|
1550
|
+
# Add to weight legend
|
|
1551
|
+
weight_legend_items.append(
|
|
1552
|
+
(f"Actual: {segment.m:.0f} kg", "blue", True)
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
# Draw critical weight square (outline only, green)
|
|
1556
|
+
critical_weight = min_force_result.critical_skier_weight
|
|
1557
|
+
critical_side_length = base_square_size * (critical_weight / 100) ** (
|
|
1558
|
+
1 / 3
|
|
1559
|
+
)
|
|
1560
|
+
critical_square = Rectangle(
|
|
1561
|
+
(
|
|
1562
|
+
square_x - critical_side_length / 2,
|
|
1563
|
+
square_y - critical_side_length,
|
|
1564
|
+
),
|
|
1565
|
+
critical_side_length,
|
|
1566
|
+
critical_side_length,
|
|
1567
|
+
facecolor="none",
|
|
1568
|
+
alpha=0.7,
|
|
1569
|
+
edgecolor="green",
|
|
1570
|
+
linewidth=1,
|
|
1571
|
+
)
|
|
1572
|
+
ax.add_patch(critical_square)
|
|
1573
|
+
|
|
1574
|
+
# Add to weight legend (only once)
|
|
1575
|
+
if not any("Critical" in item[0] for item in weight_legend_items):
|
|
1576
|
+
weight_legend_items.append(
|
|
1577
|
+
(f"Critical: {critical_weight:.0f} kg", "green", False)
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
# 3. Coupled criterion result square (centered at x=0)
|
|
1581
|
+
coupled_weight = coupled_criterion_result.critical_skier_weight
|
|
1582
|
+
coupled_side_length = base_square_size * (coupled_weight / 100) ** (1 / 3)
|
|
1583
|
+
coupled_square = Rectangle(
|
|
1584
|
+
(-coupled_side_length / 2, slab_top - square_spacing - coupled_side_length),
|
|
1585
|
+
coupled_side_length,
|
|
1586
|
+
coupled_side_length,
|
|
1587
|
+
facecolor="none",
|
|
1588
|
+
alpha=0.7,
|
|
1589
|
+
edgecolor="red",
|
|
1590
|
+
linewidth=1,
|
|
1591
|
+
)
|
|
1592
|
+
ax.add_patch(coupled_square)
|
|
1593
|
+
|
|
1594
|
+
# Add to weight legend
|
|
1595
|
+
weight_legend_items.append((f"Coupled: {coupled_weight:.0f} kg", "red", False))
|
|
1596
|
+
|
|
1597
|
+
# 4. Vertical line for coupled criterion result (spans weak layer only)
|
|
1598
|
+
cc_crack_length = coupled_criterion_result.crack_length / 10
|
|
1599
|
+
ax.plot(
|
|
1600
|
+
[cc_crack_length / 2, cc_crack_length / 2],
|
|
1601
|
+
[0, weak_layer_bottom],
|
|
1602
|
+
color="red",
|
|
1603
|
+
linewidth=1,
|
|
1604
|
+
alpha=0.7,
|
|
1605
|
+
)
|
|
1606
|
+
ax.plot(
|
|
1607
|
+
[-cc_crack_length / 2, -cc_crack_length / 2],
|
|
1608
|
+
[0, weak_layer_bottom],
|
|
1609
|
+
color="red",
|
|
1610
|
+
linewidth=1,
|
|
1611
|
+
alpha=0.7,
|
|
1612
|
+
label=f"Crack Nucleation: ±{coupled_criterion_result.crack_length / 2:.0f}mm",
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
# Calculate and set proper y-axis limits to include squares
|
|
1616
|
+
# Find the maximum extent of squares and text above the slab
|
|
1617
|
+
max_weight = max(
|
|
1618
|
+
[segment.m for segment in system.scenario.segments if segment.m > 0]
|
|
1619
|
+
+ [
|
|
1620
|
+
min_force_result.critical_skier_weight,
|
|
1621
|
+
coupled_criterion_result.critical_skier_weight,
|
|
1622
|
+
]
|
|
1623
|
+
)
|
|
1624
|
+
max_square_size = base_square_size * (max_weight / 100) ** (1 / 3)
|
|
1625
|
+
|
|
1626
|
+
# Calculate plot limits for inverted y-axis
|
|
1627
|
+
# Top of plot (smallest y-value): above the squares and text
|
|
1628
|
+
plot_top = slab_top - 3 * max_square_size - 5 # Include text space
|
|
1629
|
+
|
|
1630
|
+
# Bottom of plot (largest y-value): below weak layer
|
|
1631
|
+
plot_bottom = weak_layer_bottom + 1.0
|
|
1632
|
+
|
|
1633
|
+
# Set y-limits [bottom, top] for inverted axis
|
|
1634
|
+
ax.set_ylim([plot_bottom, plot_top])
|
|
1635
|
+
|
|
1636
|
+
weight_legend_handles = []
|
|
1637
|
+
weight_legend_labels = []
|
|
1638
|
+
|
|
1639
|
+
for label, color, filled in weight_legend_items:
|
|
1640
|
+
if filled:
|
|
1641
|
+
# Filled square for actual weights
|
|
1642
|
+
patch = Patch(facecolor=color, edgecolor=color, alpha=0.7)
|
|
1643
|
+
else:
|
|
1644
|
+
# Outline only square for critical/coupled weights
|
|
1645
|
+
patch = Patch(facecolor="none", edgecolor=color, alpha=0.7, linewidth=1)
|
|
1646
|
+
|
|
1647
|
+
weight_legend_handles.append(patch)
|
|
1648
|
+
weight_legend_labels.append(label)
|
|
1649
|
+
|
|
1650
|
+
# Plot labels
|
|
1651
|
+
ax.set_xlabel(r"lateral position $x$ (cm) $\longrightarrow$")
|
|
1652
|
+
ax.set_ylabel("Layer Height (mm)\n" + r"$\longleftarrow $ Slab | Weak Layer")
|
|
1653
|
+
|
|
1654
|
+
# Add primary legend for annotations (crack lengths)
|
|
1655
|
+
legend1 = ax.legend(loc="upper right", fontsize=8)
|
|
1656
|
+
|
|
1657
|
+
# Add the first legend back (matplotlib only shows the last legend by default)
|
|
1658
|
+
ax.add_artist(legend1)
|
|
1659
|
+
|
|
1660
|
+
# Show colorbar
|
|
1661
|
+
ticks = np.linspace(levels[0], levels[-1], num=11, endpoint=True)
|
|
1662
|
+
fig.colorbar(
|
|
1663
|
+
ax.contourf(
|
|
1664
|
+
Xwl + scale * Uwl,
|
|
1665
|
+
Zwl + scale * Wwl,
|
|
1666
|
+
weak,
|
|
1667
|
+
levels=levels,
|
|
1668
|
+
cmap=cmap,
|
|
1669
|
+
extend="both",
|
|
1670
|
+
),
|
|
1671
|
+
orientation="horizontal",
|
|
1672
|
+
ticks=ticks,
|
|
1673
|
+
label="Stress Criterion: Failure > 1",
|
|
1674
|
+
aspect=35,
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
# Save figure
|
|
1678
|
+
self._save_figure(filename, fig)
|
|
1679
|
+
|
|
1680
|
+
return fig
|
|
1681
|
+
|
|
1682
|
+
# === PLOT WRAPPERS ===========================================================
|
|
1683
|
+
|
|
1684
|
+
def plot_displacements(
|
|
1685
|
+
self,
|
|
1686
|
+
analyzer: Analyzer,
|
|
1687
|
+
x: np.ndarray,
|
|
1688
|
+
z: np.ndarray,
|
|
1689
|
+
filename: str = "displacements",
|
|
1690
|
+
) -> Figure:
|
|
1691
|
+
"""Wrap for displacements plot."""
|
|
1692
|
+
data = [
|
|
1693
|
+
[x / 10, analyzer.sm.fq.u(z, unit="mm"), r"$u_0\ (\mathrm{mm})$"],
|
|
1694
|
+
[x / 10, -analyzer.sm.fq.w(z, unit="mm"), r"$-w\ (\mathrm{mm})$"],
|
|
1695
|
+
[x / 10, analyzer.sm.fq.psi(z, unit="deg"), r"$\psi\ (^\circ)$ "],
|
|
1696
|
+
]
|
|
1697
|
+
self._plot_data(
|
|
1698
|
+
scenario=analyzer.sm.scenario,
|
|
1699
|
+
ax1label=r"Displacements",
|
|
1700
|
+
ax1data=data,
|
|
1701
|
+
filename=filename,
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
def plot_stresses(
|
|
1705
|
+
self,
|
|
1706
|
+
analyzer: Analyzer,
|
|
1707
|
+
x: np.ndarray,
|
|
1708
|
+
z: np.ndarray,
|
|
1709
|
+
filename: str = "stresses",
|
|
1710
|
+
) -> Figure:
|
|
1711
|
+
"""Wrap stress plot."""
|
|
1712
|
+
data = [
|
|
1713
|
+
[x / 10, analyzer.sm.fq.tau(z, unit="kPa"), r"$\tau$"],
|
|
1714
|
+
[x / 10, analyzer.sm.fq.sig(z, unit="kPa"), r"$\sigma$"],
|
|
1715
|
+
]
|
|
1716
|
+
self._plot_data(
|
|
1717
|
+
scenario=analyzer.sm.scenario,
|
|
1718
|
+
ax1label=r"Stress (kPa)",
|
|
1719
|
+
ax1data=data,
|
|
1720
|
+
filename=filename,
|
|
1721
|
+
)
|
|
1722
|
+
|
|
1723
|
+
def plot_stress_criteria(
|
|
1724
|
+
self, analyzer: Analyzer, x: np.ndarray, stress: np.ndarray
|
|
1725
|
+
) -> Figure:
|
|
1726
|
+
"""Wrap plot of stress and energy criteria."""
|
|
1727
|
+
data = [[x / 10, stress, r"$\sigma/\sigma_\mathrm{c}$"]]
|
|
1728
|
+
self._plot_data(
|
|
1729
|
+
scenario=analyzer.sm.scenario,
|
|
1730
|
+
ax1label=r"Criteria",
|
|
1731
|
+
ax1data=data,
|
|
1732
|
+
filename="crit",
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
def plot_ERR_comp(
|
|
1736
|
+
self,
|
|
1737
|
+
analyzer: Analyzer,
|
|
1738
|
+
da: np.ndarray,
|
|
1739
|
+
Gdif: np.ndarray,
|
|
1740
|
+
Ginc: np.ndarray,
|
|
1741
|
+
mode: int = 0,
|
|
1742
|
+
) -> Figure:
|
|
1743
|
+
"""Wrap energy release rate plot."""
|
|
1744
|
+
data = [
|
|
1745
|
+
[da / 10, 1e3 * Gdif[mode, :], r"$\mathcal{G}$"],
|
|
1746
|
+
[da / 10, 1e3 * Ginc[mode, :], r"$\bar{\mathcal{G}}$"],
|
|
1747
|
+
]
|
|
1748
|
+
self._plot_data(
|
|
1749
|
+
scenario=analyzer.sm.scenario,
|
|
1750
|
+
xlabel=r"Crack length $\Delta a$ (cm)",
|
|
1751
|
+
ax1label=r"Energy release rate (J/m$^2$)",
|
|
1752
|
+
ax1data=data,
|
|
1753
|
+
filename="err",
|
|
1754
|
+
vlines=False,
|
|
1755
|
+
)
|
|
1756
|
+
|
|
1757
|
+
def plot_ERR_modes(
|
|
1758
|
+
self, analyzer: Analyzer, da: np.ndarray, G: np.ndarray, kind: str = "inc"
|
|
1759
|
+
) -> Figure:
|
|
1760
|
+
"""Wrap energy release rate plot."""
|
|
1761
|
+
label = r"$\bar{\mathcal{G}}$" if kind == "inc" else r"$\mathcal{G}$"
|
|
1762
|
+
data = [
|
|
1763
|
+
[da / 10, 1e3 * G[2, :], label + r"$_\mathrm{I\!I}$"],
|
|
1764
|
+
[da / 10, 1e3 * G[1, :], label + r"$_\mathrm{I}$"],
|
|
1765
|
+
[da / 10, 1e3 * G[0, :], label + r"$_\mathrm{I+I\!I}$"],
|
|
1766
|
+
]
|
|
1767
|
+
self._plot_data(
|
|
1768
|
+
scenario=analyzer.sm.scenario,
|
|
1769
|
+
xlabel=r"Crack length $a$ (cm)",
|
|
1770
|
+
ax1label=r"Energy release rate (J/m$^2$)",
|
|
1771
|
+
ax1data=data,
|
|
1772
|
+
filename="modes",
|
|
1773
|
+
vlines=False,
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
def plot_fea_disp(
|
|
1777
|
+
self, analyzer: Analyzer, x: np.ndarray, z: np.ndarray, fea: np.ndarray
|
|
1778
|
+
) -> Figure:
|
|
1779
|
+
"""Wrap displacements plot."""
|
|
1780
|
+
data = [
|
|
1781
|
+
[fea[:, 0] / 10, -np.flipud(fea[:, 1]), r"FEA $u_0$"],
|
|
1782
|
+
[fea[:, 0] / 10, np.flipud(fea[:, 2]), r"FEA $w_0$"],
|
|
1783
|
+
# [fea[:, 0]/10, -np.flipud(fea[:, 3]), r'FEA $u(z=-h/2)$'],
|
|
1784
|
+
# [fea[:, 0]/10, np.flipud(fea[:, 4]), r'FEA $w(z=-h/2)$'],
|
|
1785
|
+
[fea[:, 0] / 10, np.flipud(np.rad2deg(fea[:, 5])), r"FEA $\psi$"],
|
|
1786
|
+
[x / 10, analyzer.sm.fq.u(z, z0=0), r"$u_0$"],
|
|
1787
|
+
[x / 10, -analyzer.sm.fq.w(z), r"$-w$"],
|
|
1788
|
+
[x / 10, np.rad2deg(analyzer.sm.fq.psi(z)), r"$\psi$"],
|
|
1789
|
+
]
|
|
1790
|
+
self._plot_data(
|
|
1791
|
+
scenario=analyzer.sm.scenario,
|
|
1792
|
+
ax1label=r"Displacements (mm)",
|
|
1793
|
+
ax1data=data,
|
|
1794
|
+
filename="fea_disp",
|
|
1795
|
+
labelpos=-50,
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
def plot_fea_stress(
|
|
1799
|
+
self, analyzer: Analyzer, xb: np.ndarray, zb: np.ndarray, fea: np.ndarray
|
|
1800
|
+
) -> Figure:
|
|
1801
|
+
"""Wrap stress plot."""
|
|
1802
|
+
data = [
|
|
1803
|
+
[fea[:, 0] / 10, 1e3 * np.flipud(fea[:, 2]), r"FEA $\sigma_2$"],
|
|
1804
|
+
[fea[:, 0] / 10, 1e3 * np.flipud(fea[:, 3]), r"FEA $\tau_{12}$"],
|
|
1805
|
+
[xb / 10, analyzer.sm.fq.tau(zb, unit="kPa"), r"$\tau$"],
|
|
1806
|
+
[xb / 10, analyzer.sm.fq.sig(zb, unit="kPa"), r"$\sigma$"],
|
|
1807
|
+
]
|
|
1808
|
+
self._plot_data(
|
|
1809
|
+
scenario=analyzer.sm.scenario,
|
|
1810
|
+
ax1label=r"Stress (kPa)",
|
|
1811
|
+
ax1data=data,
|
|
1812
|
+
filename="fea_stress",
|
|
1813
|
+
labelpos=-50,
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
# === BASE PLOT FUNCTION ======================================================
|
|
1817
|
+
|
|
1818
|
+
def _plot_data(
|
|
1819
|
+
self,
|
|
1820
|
+
scenario: Scenario,
|
|
1821
|
+
filename: str,
|
|
1822
|
+
ax1data,
|
|
1823
|
+
ax1label,
|
|
1824
|
+
ax2data=None,
|
|
1825
|
+
ax2label=None,
|
|
1826
|
+
labelpos=None,
|
|
1827
|
+
vlines=True,
|
|
1828
|
+
xlabel=r"Horizontal position $x$ (cm)",
|
|
1829
|
+
) -> Figure:
|
|
1830
|
+
"""Plot data. Base function."""
|
|
1831
|
+
# Figure setup
|
|
1832
|
+
plt.rcdefaults()
|
|
1833
|
+
plt.rc("font", family="serif", size=10)
|
|
1834
|
+
plt.rc("mathtext", fontset="cm")
|
|
1835
|
+
|
|
1836
|
+
# Create figure
|
|
1837
|
+
fig = plt.figure(figsize=(4, 8 / 3))
|
|
1838
|
+
ax1 = fig.gca()
|
|
1839
|
+
|
|
1840
|
+
# Axis limits
|
|
1841
|
+
ax1.autoscale(axis="x", tight=True)
|
|
1842
|
+
|
|
1843
|
+
# Set axis labels
|
|
1844
|
+
ax1.set_xlabel(xlabel + r" $\longrightarrow$")
|
|
1845
|
+
ax1.set_ylabel(ax1label + r" $\longrightarrow$")
|
|
1846
|
+
|
|
1847
|
+
# Plot x-axis
|
|
1848
|
+
ax1.axhline(0, linewidth=0.5, color="gray")
|
|
1849
|
+
|
|
1850
|
+
ki = scenario.ki
|
|
1851
|
+
li = scenario.li
|
|
1852
|
+
mi = scenario.mi
|
|
1853
|
+
|
|
1854
|
+
# Plot vertical separators
|
|
1855
|
+
if vlines:
|
|
1856
|
+
ax1.axvline(0, linewidth=0.5, color="gray")
|
|
1857
|
+
for i, f in enumerate(ki):
|
|
1858
|
+
if not f:
|
|
1859
|
+
ax1.axvspan(
|
|
1860
|
+
sum(li[:i]) / 10,
|
|
1861
|
+
sum(li[: i + 1]) / 10,
|
|
1862
|
+
facecolor="gray",
|
|
1863
|
+
alpha=0.05,
|
|
1864
|
+
zorder=100,
|
|
1865
|
+
)
|
|
1866
|
+
for i, m in enumerate(mi, start=1):
|
|
1867
|
+
if m > 0:
|
|
1868
|
+
ax1.axvline(sum(li[:i]) / 10, linewidth=0.5, color="gray")
|
|
1869
|
+
else:
|
|
1870
|
+
ax1.autoscale(axis="y", tight=True)
|
|
1871
|
+
|
|
1872
|
+
# Calculate labelposition
|
|
1873
|
+
if not labelpos:
|
|
1874
|
+
x = ax1data[0][0]
|
|
1875
|
+
labelpos = int(0.95 * len(x[~np.isnan(x)]))
|
|
1876
|
+
|
|
1877
|
+
# Fill left y-axis
|
|
1878
|
+
i = 0
|
|
1879
|
+
for x, y, label in ax1data:
|
|
1880
|
+
i += 1
|
|
1881
|
+
if label == "" or "FEA" in label:
|
|
1882
|
+
# line, = ax1.plot(x, y, 'k:', linewidth=1)
|
|
1883
|
+
ax1.plot(x, y, linewidth=3, color="white")
|
|
1884
|
+
(line,) = ax1.plot(x, y, ":", linewidth=1) # , color='black'
|
|
1885
|
+
thislabelpos = -2
|
|
1886
|
+
x, y = x[~np.isnan(x)], y[~np.isnan(x)]
|
|
1887
|
+
xtx = (x[thislabelpos - 1] + x[thislabelpos]) / 2
|
|
1888
|
+
ytx = (y[thislabelpos - 1] + y[thislabelpos]) / 2
|
|
1889
|
+
ax1.text(xtx, ytx, label, color=line.get_color(), **LABELSTYLE)
|
|
1890
|
+
else:
|
|
1891
|
+
# Plot line
|
|
1892
|
+
ax1.plot(x, y, linewidth=3, color="white")
|
|
1893
|
+
(line,) = ax1.plot(x, y, linewidth=1)
|
|
1894
|
+
# Line label
|
|
1895
|
+
x, y = x[~np.isnan(x)], y[~np.isnan(x)]
|
|
1896
|
+
if len(x) > 0:
|
|
1897
|
+
xtx = (x[labelpos - 10 * i - 1] + x[labelpos - 10 * i]) / 2
|
|
1898
|
+
ytx = (y[labelpos - 10 * i - 1] + y[labelpos - 10 * i]) / 2
|
|
1899
|
+
ax1.text(xtx, ytx, label, color=line.get_color(), **LABELSTYLE)
|
|
1900
|
+
|
|
1901
|
+
# Fill right y-axis
|
|
1902
|
+
if ax2data:
|
|
1903
|
+
# Create right y-axis
|
|
1904
|
+
ax2 = ax1.twinx()
|
|
1905
|
+
# Set axis label
|
|
1906
|
+
ax2.set_ylabel(ax2label + r" $\longrightarrow$")
|
|
1907
|
+
# Fill
|
|
1908
|
+
for x, y, label in ax2data:
|
|
1909
|
+
# Plot line
|
|
1910
|
+
ax2.plot(x, y, linewidth=3, color="white")
|
|
1911
|
+
(line,) = ax2.plot(x, y, linewidth=1, color=COLORS[8, 0])
|
|
1912
|
+
# Line label
|
|
1913
|
+
x, y = x[~np.isnan(x)], y[~np.isnan(x)]
|
|
1914
|
+
xtx = (x[labelpos - 1] + x[labelpos]) / 2
|
|
1915
|
+
ytx = (y[labelpos - 1] + y[labelpos]) / 2
|
|
1916
|
+
ax2.text(xtx, ytx, label, color=line.get_color(), **LABELSTYLE)
|
|
1917
|
+
|
|
1918
|
+
# Save figure
|
|
1919
|
+
if filename:
|
|
1920
|
+
self._save_figure(filename, fig)
|
|
1921
|
+
|
|
1922
|
+
return fig
|