weac 3.0.0__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 +1 -1
- 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/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/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-3.0.0.dist-info → weac-3.0.1.dist-info}/METADATA +4 -4
- weac-3.0.1.dist-info/RECORD +32 -0
- weac-3.0.1.dist-info/licenses/LICENSE +21 -0
- weac-3.0.0.dist-info/RECORD +0 -8
- weac-3.0.0.dist-info/licenses/LICENSE +0 -24
- {weac-3.0.0.dist-info → weac-3.0.1.dist-info}/WHEEL +0 -0
- {weac-3.0.0.dist-info → weac-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the Analyzer class, which is used to analyze the results of the WEAC model.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Standard library imports
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from functools import partial, wraps
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
# Third party imports
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy.integrate import cumulative_trapezoid, quad
|
|
15
|
+
|
|
16
|
+
from weac.constants import G_MM_S2
|
|
17
|
+
|
|
18
|
+
# Module imports
|
|
19
|
+
from weac.core.system_model import SystemModel
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def track_analyzer_call(func):
|
|
25
|
+
"""Decorator to track call count and execution time of Analyzer methods."""
|
|
26
|
+
|
|
27
|
+
@wraps(func)
|
|
28
|
+
def wrapper(self, *args, **kwargs):
|
|
29
|
+
"""Wrapper that adds tracking functionality."""
|
|
30
|
+
|
|
31
|
+
start_time = time.perf_counter()
|
|
32
|
+
result = func(self, *args, **kwargs)
|
|
33
|
+
duration = time.perf_counter() - start_time
|
|
34
|
+
|
|
35
|
+
func_name = func.__name__
|
|
36
|
+
self.call_stats[func_name]["count"] += 1
|
|
37
|
+
self.call_stats[func_name]["total_time"] += duration
|
|
38
|
+
|
|
39
|
+
logger.debug(
|
|
40
|
+
"Analyzer method '%s' called. Execution time: %.4f seconds.",
|
|
41
|
+
func_name,
|
|
42
|
+
duration,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Analyzer:
|
|
51
|
+
"""
|
|
52
|
+
Provides methods for the analysis of layered slabs on compliant
|
|
53
|
+
elastic foundations.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
sm: SystemModel
|
|
57
|
+
printing_enabled: bool = True
|
|
58
|
+
|
|
59
|
+
def __init__(self, system_model: SystemModel, printing_enabled: bool = True):
|
|
60
|
+
self.sm = system_model
|
|
61
|
+
self.call_stats = defaultdict(lambda: {"count": 0, "total_time": 0.0})
|
|
62
|
+
self.printing_enabled = printing_enabled
|
|
63
|
+
|
|
64
|
+
def get_call_stats(self):
|
|
65
|
+
"""Returns the call statistics."""
|
|
66
|
+
return self.call_stats
|
|
67
|
+
|
|
68
|
+
def print_call_stats(self, message: str = "Analyzer Call Statistics"):
|
|
69
|
+
"""Prints the call statistics in a readable format."""
|
|
70
|
+
if self.printing_enabled:
|
|
71
|
+
print(f"--- {message} ---")
|
|
72
|
+
if not self.call_stats:
|
|
73
|
+
print("No methods have been called.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
sorted_stats = sorted(
|
|
77
|
+
self.call_stats.items(),
|
|
78
|
+
key=lambda item: item[1]["total_time"],
|
|
79
|
+
reverse=True,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
for func_name, stats in sorted_stats:
|
|
83
|
+
count = stats["count"]
|
|
84
|
+
total_time = stats["total_time"]
|
|
85
|
+
avg_time = total_time / count if count > 0 else 0
|
|
86
|
+
print(
|
|
87
|
+
f"- {func_name}: "
|
|
88
|
+
f"called {count} times, "
|
|
89
|
+
f"total time {total_time:.4f}s, "
|
|
90
|
+
f"avg time {avg_time:.4f}s"
|
|
91
|
+
)
|
|
92
|
+
print("---------------------------------")
|
|
93
|
+
|
|
94
|
+
@track_analyzer_call
|
|
95
|
+
def rasterize_solution(
|
|
96
|
+
self,
|
|
97
|
+
mode: Literal["cracked", "uncracked"] = "cracked",
|
|
98
|
+
num: int = 4000,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Compute rasterized solution vector.
|
|
102
|
+
|
|
103
|
+
Parameters:
|
|
104
|
+
---------
|
|
105
|
+
mode : Literal["cracked", "uncracked"]
|
|
106
|
+
Mode of the solution.
|
|
107
|
+
num : int
|
|
108
|
+
Number of grid points.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
xs : ndarray
|
|
113
|
+
Grid point x-coordinates at which solution vector
|
|
114
|
+
is discretized.
|
|
115
|
+
zs : ndarray
|
|
116
|
+
Matrix with solution vectors as columns at grid
|
|
117
|
+
points xs.
|
|
118
|
+
x_founded : ndarray
|
|
119
|
+
Grid point x-coordinates that lie on a foundation.
|
|
120
|
+
"""
|
|
121
|
+
ki = self.sm.scenario.ki
|
|
122
|
+
match mode:
|
|
123
|
+
case "cracked":
|
|
124
|
+
C = self.sm.unknown_constants
|
|
125
|
+
case "uncracked":
|
|
126
|
+
ki = np.full(len(ki), True)
|
|
127
|
+
C = self.sm.uncracked_unknown_constants
|
|
128
|
+
phi = self.sm.scenario.phi
|
|
129
|
+
li = self.sm.scenario.li
|
|
130
|
+
qs = self.sm.scenario.surface_load
|
|
131
|
+
|
|
132
|
+
# Drop zero-length segments
|
|
133
|
+
li = abs(li)
|
|
134
|
+
isnonzero = li > 0
|
|
135
|
+
C, ki, li = C[:, isnonzero], ki[isnonzero], li[isnonzero]
|
|
136
|
+
|
|
137
|
+
# Compute number of plot points per segment (+1 for last segment)
|
|
138
|
+
ni = np.ceil(li / li.sum() * num).astype("int")
|
|
139
|
+
ni[-1] += 1
|
|
140
|
+
|
|
141
|
+
# Provide cumulated length and plot point lists
|
|
142
|
+
lic = np.insert(np.cumsum(li), 0, 0)
|
|
143
|
+
nic = np.insert(np.cumsum(ni), 0, 0)
|
|
144
|
+
|
|
145
|
+
# Initialize arrays
|
|
146
|
+
issupported = np.full(ni.sum(), True)
|
|
147
|
+
xs = np.full(ni.sum(), np.nan)
|
|
148
|
+
zs = np.full([6, xs.size], np.nan)
|
|
149
|
+
|
|
150
|
+
# Loop through segments
|
|
151
|
+
for i, length in enumerate(li):
|
|
152
|
+
# Get local x-coordinates of segment i
|
|
153
|
+
endpoint = i == li.size - 1
|
|
154
|
+
xi = np.linspace(0, length, num=ni[i], endpoint=endpoint)
|
|
155
|
+
# Compute start and end coordinates of segment i
|
|
156
|
+
x0 = lic[i]
|
|
157
|
+
# Assemble global coordinate vector
|
|
158
|
+
xs[nic[i] : nic[i + 1]] = x0 + xi
|
|
159
|
+
# Mask coordinates not on foundation (including endpoints)
|
|
160
|
+
if not ki[i]:
|
|
161
|
+
issupported[nic[i] : nic[i + 1]] = False
|
|
162
|
+
# Compute segment solution
|
|
163
|
+
zi = self.sm.z(xi, C[:, [i]], length, phi, ki[i], qs=qs)
|
|
164
|
+
# Assemble global solution matrix
|
|
165
|
+
zs[:, nic[i] : nic[i + 1]] = zi
|
|
166
|
+
|
|
167
|
+
# Make sure cracktips are included
|
|
168
|
+
transmissionbool = [ki[j] or ki[j + 1] for j, _ in enumerate(ki[:-1])]
|
|
169
|
+
for i, truefalse in enumerate(transmissionbool, start=1):
|
|
170
|
+
issupported[nic[i]] = truefalse
|
|
171
|
+
|
|
172
|
+
# Assemble vector of coordinates on foundation
|
|
173
|
+
xs_supported = np.full(ni.sum(), np.nan)
|
|
174
|
+
xs_supported[issupported] = xs[issupported]
|
|
175
|
+
|
|
176
|
+
return xs, zs, xs_supported
|
|
177
|
+
|
|
178
|
+
@track_analyzer_call
|
|
179
|
+
def get_zmesh(self, dz=2):
|
|
180
|
+
"""
|
|
181
|
+
Get z-coordinates of grid points and corresponding elastic properties.
|
|
182
|
+
|
|
183
|
+
Arguments
|
|
184
|
+
---------
|
|
185
|
+
dz : float, optional
|
|
186
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
mesh : ndarray
|
|
191
|
+
Mesh along z-axis. Columns are a list of z-coordinates (mm) of
|
|
192
|
+
grid points along z-axis with at least two grid points (top,
|
|
193
|
+
bottom) per layer, Young's modulus of each grid point, shear
|
|
194
|
+
modulus of each grid point, and Poisson's ratio of each grid
|
|
195
|
+
point.
|
|
196
|
+
"""
|
|
197
|
+
# Get z-coordinates of slab layers
|
|
198
|
+
z = np.concatenate([[self.sm.slab.z0], self.sm.slab.zi_bottom])
|
|
199
|
+
# Compute number of grid points per layer
|
|
200
|
+
nlayer = np.ceil((z[1:] - z[:-1]) / dz).astype(np.int32) + 1
|
|
201
|
+
# Calculate grid points as list of z-coordinates (mm)
|
|
202
|
+
zi = np.hstack(
|
|
203
|
+
[
|
|
204
|
+
np.linspace(z[i], z[i + 1], n, endpoint=True)
|
|
205
|
+
for i, n in enumerate(nlayer)
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
# Extract elastic properties for each layer, reversing to match z order
|
|
209
|
+
layer_properties = {
|
|
210
|
+
"E": [layer.E for layer in self.sm.slab.layers],
|
|
211
|
+
"nu": [layer.nu for layer in self.sm.slab.layers],
|
|
212
|
+
"rho": [
|
|
213
|
+
layer.rho * 1e-12 for layer in self.sm.slab.layers
|
|
214
|
+
], # Convert to t/mm^3
|
|
215
|
+
"tensile_strength": [
|
|
216
|
+
layer.tensile_strength for layer in self.sm.slab.layers
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Repeat properties for each grid point in the layer
|
|
221
|
+
si = {"z": zi}
|
|
222
|
+
for prop, values in layer_properties.items():
|
|
223
|
+
si[prop] = np.repeat(values, nlayer)
|
|
224
|
+
|
|
225
|
+
return si
|
|
226
|
+
|
|
227
|
+
@track_analyzer_call
|
|
228
|
+
def Sxx(self, Z, phi, dz=2, unit="kPa"):
|
|
229
|
+
"""
|
|
230
|
+
Compute axial normal stress in slab layers.
|
|
231
|
+
|
|
232
|
+
Arguments
|
|
233
|
+
----------
|
|
234
|
+
Z : ndarray
|
|
235
|
+
Solution vector [u(x) u'(x) w(x) w'(x) psi(x), psi'(x)]^T
|
|
236
|
+
phi : float
|
|
237
|
+
Inclination (degrees). Counterclockwise positive.
|
|
238
|
+
dz : float, optional
|
|
239
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
240
|
+
unit : {'kPa', 'MPa'}, optional
|
|
241
|
+
Desired output unit. Default is 'kPa'.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
ndarray, float
|
|
246
|
+
Axial slab normal stress in specified unit.
|
|
247
|
+
"""
|
|
248
|
+
# Unit conversion dict
|
|
249
|
+
convert = {"kPa": 1e3, "MPa": 1}
|
|
250
|
+
|
|
251
|
+
# Get mesh along z-axis
|
|
252
|
+
zmesh = self.get_zmesh(dz=dz)
|
|
253
|
+
zi = zmesh["z"]
|
|
254
|
+
rho = zmesh["rho"]
|
|
255
|
+
|
|
256
|
+
# Get dimensions of stress field (n rows, m columns)
|
|
257
|
+
n = len(zmesh["z"])
|
|
258
|
+
m = Z.shape[1]
|
|
259
|
+
|
|
260
|
+
# Initialize axial normal stress Sxx
|
|
261
|
+
Sxx = np.zeros(shape=[n, m])
|
|
262
|
+
|
|
263
|
+
# Compute axial normal stress Sxx at grid points in MPa
|
|
264
|
+
for i, z in enumerate(zi):
|
|
265
|
+
E = zmesh["E"][i]
|
|
266
|
+
nu = zmesh["nu"][i]
|
|
267
|
+
Sxx[i, :] = E / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
|
|
268
|
+
|
|
269
|
+
# Calculate weight load at grid points and superimpose on stress field
|
|
270
|
+
qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
|
|
271
|
+
# Old Implementation: Changed for numerical stability
|
|
272
|
+
# for i, qi in enumerate(qt[:-1]):
|
|
273
|
+
# Sxx[i, :] += qi * (zi[i + 1] - zi[i])
|
|
274
|
+
# Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
|
|
275
|
+
# New Implementation: Changed for numerical stability
|
|
276
|
+
dz = np.diff(zi)
|
|
277
|
+
Sxx[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
|
|
278
|
+
Sxx[-1, :] += qt[-1] * dz[-1]
|
|
279
|
+
|
|
280
|
+
# Return axial normal stress in specified unit
|
|
281
|
+
return convert[unit] * Sxx
|
|
282
|
+
|
|
283
|
+
@track_analyzer_call
|
|
284
|
+
def Txz(self, Z, phi, dz=2, unit="kPa"):
|
|
285
|
+
"""
|
|
286
|
+
Compute shear stress in slab layers.
|
|
287
|
+
|
|
288
|
+
Arguments
|
|
289
|
+
----------
|
|
290
|
+
Z : ndarray
|
|
291
|
+
Solution vector [u(x) u'(x) w(x) w'(x) psi(x), psi'(x)]^T
|
|
292
|
+
phi : float
|
|
293
|
+
Inclination (degrees). Counterclockwise positive.
|
|
294
|
+
dz : float, optional
|
|
295
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
296
|
+
unit : {'kPa', 'MPa'}, optional
|
|
297
|
+
Desired output unit. Default is 'kPa'.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
ndarray
|
|
302
|
+
Shear stress at grid points in the slab in specified unit.
|
|
303
|
+
"""
|
|
304
|
+
# Unit conversion dict
|
|
305
|
+
convert = {"kPa": 1e3, "MPa": 1}
|
|
306
|
+
# Get mesh along z-axis
|
|
307
|
+
zmesh = self.get_zmesh(dz=dz)
|
|
308
|
+
zi = zmesh["z"]
|
|
309
|
+
rho = zmesh["rho"]
|
|
310
|
+
qs = self.sm.scenario.surface_load
|
|
311
|
+
|
|
312
|
+
# Get dimensions of stress field (n rows, m columns)
|
|
313
|
+
n = len(zi)
|
|
314
|
+
m = Z.shape[1]
|
|
315
|
+
|
|
316
|
+
# Get second derivatives of centerline displacement u0 and
|
|
317
|
+
# cross-section rotaiton psi of all grid points along the x-axis
|
|
318
|
+
du0_dxdx = self.sm.fq.du0_dxdx(Z, phi, qs=qs)
|
|
319
|
+
dpsi_dxdx = self.sm.fq.dpsi_dxdx(Z, phi, qs=qs)
|
|
320
|
+
|
|
321
|
+
# Initialize first derivative of axial normal stress sxx w.r.t. x
|
|
322
|
+
dsxx_dx = np.zeros(shape=[n, m])
|
|
323
|
+
|
|
324
|
+
# Calculate first derivative of sxx at z-grid points
|
|
325
|
+
for i, z in enumerate(zi):
|
|
326
|
+
E = zmesh["E"][i]
|
|
327
|
+
nu = zmesh["nu"][i]
|
|
328
|
+
dsxx_dx[i, :] = E / (1 - nu**2) * (du0_dxdx + z * dpsi_dxdx)
|
|
329
|
+
|
|
330
|
+
# Calculate weight load at grid points
|
|
331
|
+
qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
|
|
332
|
+
|
|
333
|
+
# Integrate -dsxx_dx along z and add cumulative weight load
|
|
334
|
+
# to obtain shear stress Txz in MPa
|
|
335
|
+
Txz = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
|
|
336
|
+
Txz += cumulative_trapezoid(qt, zi, initial=0)[:, None]
|
|
337
|
+
|
|
338
|
+
# Return shear stress Txz in specified unit
|
|
339
|
+
return convert[unit] * Txz
|
|
340
|
+
|
|
341
|
+
@track_analyzer_call
|
|
342
|
+
def Szz(self, Z, phi, dz=2, unit="kPa"):
|
|
343
|
+
"""
|
|
344
|
+
Compute transverse normal stress in slab layers.
|
|
345
|
+
|
|
346
|
+
Arguments
|
|
347
|
+
----------
|
|
348
|
+
Z : ndarray
|
|
349
|
+
Solution vector [u(x) u'(x) w(x) w'(x) psi(x), psi'(x)]^T
|
|
350
|
+
phi : float
|
|
351
|
+
Inclination (degrees). Counterclockwise positive.
|
|
352
|
+
dz : float, optional
|
|
353
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
354
|
+
unit : {'kPa', 'MPa'}, optional
|
|
355
|
+
Desired output unit. Default is 'kPa'.
|
|
356
|
+
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
ndarray, float
|
|
360
|
+
Transverse normal stress at grid points in the slab in
|
|
361
|
+
specified unit.
|
|
362
|
+
"""
|
|
363
|
+
# Unit conversion dict
|
|
364
|
+
convert = {"kPa": 1e3, "MPa": 1}
|
|
365
|
+
|
|
366
|
+
# Get mesh along z-axis
|
|
367
|
+
zmesh = self.get_zmesh(dz=dz)
|
|
368
|
+
zi = zmesh["z"]
|
|
369
|
+
rho = zmesh["rho"]
|
|
370
|
+
qs = self.sm.scenario.surface_load
|
|
371
|
+
# Get dimensions of stress field (n rows, m columns)
|
|
372
|
+
n = len(zi)
|
|
373
|
+
m = Z.shape[1]
|
|
374
|
+
|
|
375
|
+
# Get third derivatives of centerline displacement u0 and
|
|
376
|
+
# cross-section rotaiton psi of all grid points along the x-axis
|
|
377
|
+
du0_dxdxdx = self.sm.fq.du0_dxdxdx(Z, phi, qs=qs)
|
|
378
|
+
dpsi_dxdxdx = self.sm.fq.dpsi_dxdxdx(Z, phi, qs=qs)
|
|
379
|
+
|
|
380
|
+
# Initialize second derivative of axial normal stress sxx w.r.t. x
|
|
381
|
+
dsxx_dxdx = np.zeros(shape=[n, m])
|
|
382
|
+
|
|
383
|
+
# Calculate second derivative of sxx at z-grid points
|
|
384
|
+
for i, z in enumerate(zi):
|
|
385
|
+
E = zmesh["E"][i]
|
|
386
|
+
nu = zmesh["nu"][i]
|
|
387
|
+
dsxx_dxdx[i, :] = E / (1 - nu**2) * (du0_dxdxdx + z * dpsi_dxdxdx)
|
|
388
|
+
|
|
389
|
+
# Calculate weight load at grid points
|
|
390
|
+
qn = rho * G_MM_S2 * np.cos(np.deg2rad(phi))
|
|
391
|
+
|
|
392
|
+
# Integrate dsxx_dxdx twice along z to obtain transverse
|
|
393
|
+
# normal stress Szz in MPa
|
|
394
|
+
integrand = cumulative_trapezoid(dsxx_dxdx, zi, axis=0, initial=0)
|
|
395
|
+
Szz = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
|
|
396
|
+
Szz += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
|
|
397
|
+
|
|
398
|
+
# Return shear stress txz in specified unit
|
|
399
|
+
return convert[unit] * Szz
|
|
400
|
+
|
|
401
|
+
@track_analyzer_call
|
|
402
|
+
def principal_stress_slab(
|
|
403
|
+
self,
|
|
404
|
+
Z,
|
|
405
|
+
phi: float,
|
|
406
|
+
dz: float = 2,
|
|
407
|
+
unit: Literal["kPa", "MPa"] = "kPa",
|
|
408
|
+
val: Literal["min", "max"] = "max",
|
|
409
|
+
normalize: bool = False,
|
|
410
|
+
):
|
|
411
|
+
"""
|
|
412
|
+
Compute maximum or minimum principal stress in slab layers.
|
|
413
|
+
|
|
414
|
+
Arguments
|
|
415
|
+
---------
|
|
416
|
+
Z : ndarray
|
|
417
|
+
Solution vector [u(x) u'(x) w(x) w'(x) psi(x), psi'(x)]^T
|
|
418
|
+
phi : float
|
|
419
|
+
Inclination (degrees). Counterclockwise positive.
|
|
420
|
+
dz : float, optional
|
|
421
|
+
Element size along z-axis (mm). Default is 2 mm.
|
|
422
|
+
unit : {'kPa', 'MPa'}, optional
|
|
423
|
+
Desired output unit. Default is 'kPa'.
|
|
424
|
+
val : str, optional
|
|
425
|
+
Maximum 'max' or minimum 'min' principal stress. Default is 'max'.
|
|
426
|
+
normalize : bool
|
|
427
|
+
Toggle layerwise normalization to strength.
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
ndarray
|
|
432
|
+
Maximum or minimum principal stress in specified unit.
|
|
433
|
+
|
|
434
|
+
Raises
|
|
435
|
+
------
|
|
436
|
+
ValueError
|
|
437
|
+
If specified principal stress component is neither 'max' nor
|
|
438
|
+
'min', or if normalization of compressive principal stress
|
|
439
|
+
is requested.
|
|
440
|
+
"""
|
|
441
|
+
# Raise error if specified component is not available
|
|
442
|
+
if val not in ["min", "max"]:
|
|
443
|
+
raise ValueError(f"Component {val} not defined.")
|
|
444
|
+
|
|
445
|
+
# Multiplier selection dict
|
|
446
|
+
m = {"max": 1, "min": -1}
|
|
447
|
+
|
|
448
|
+
# Get axial normal stresses, shear stresses, transverse normal stresses
|
|
449
|
+
Sxx = self.Sxx(Z=Z, phi=phi, dz=dz, unit=unit)
|
|
450
|
+
Txz = self.Txz(Z=Z, phi=phi, dz=dz, unit=unit)
|
|
451
|
+
Szz = self.Szz(Z=Z, phi=phi, dz=dz, unit=unit)
|
|
452
|
+
|
|
453
|
+
# Calculate principal stress
|
|
454
|
+
Ps = (Sxx + Szz) / 2 + m[val] * np.sqrt((Sxx - Szz) ** 2 + 4 * Txz**2) / 2
|
|
455
|
+
|
|
456
|
+
# Raise error if normalization of compressive stresses is attempted
|
|
457
|
+
if normalize and val == "min":
|
|
458
|
+
raise ValueError("Can only normalize tensile stresses.")
|
|
459
|
+
|
|
460
|
+
# Normalize tensile stresses to tensile strength
|
|
461
|
+
if normalize and val == "max":
|
|
462
|
+
zmesh = self.get_zmesh(dz=dz)
|
|
463
|
+
tensile_strength = zmesh["tensile_strength"]
|
|
464
|
+
# Normalize maximum principal stress to layers' tensile strength
|
|
465
|
+
normalized_Ps = Ps / tensile_strength[:, None]
|
|
466
|
+
return normalized_Ps
|
|
467
|
+
|
|
468
|
+
# Return absolute principal stresses
|
|
469
|
+
return Ps
|
|
470
|
+
|
|
471
|
+
@track_analyzer_call
|
|
472
|
+
def principal_stress_weaklayer(
|
|
473
|
+
self,
|
|
474
|
+
Z,
|
|
475
|
+
sc: float = 2.6,
|
|
476
|
+
unit: Literal["kPa", "MPa"] = "kPa",
|
|
477
|
+
val: Literal["min", "max"] = "min",
|
|
478
|
+
normalize: bool = False,
|
|
479
|
+
):
|
|
480
|
+
"""
|
|
481
|
+
Compute maximum or minimum principal stress in the weak layer.
|
|
482
|
+
|
|
483
|
+
Arguments
|
|
484
|
+
---------
|
|
485
|
+
Z : ndarray
|
|
486
|
+
Solution vector [u(x) u'(x) w(x) w'(x) psi(x), psi'(x)]^T
|
|
487
|
+
sc : float
|
|
488
|
+
Weak-layer compressive strength. Default is 2.6 kPa.
|
|
489
|
+
unit : {'kPa', 'MPa'}, optional
|
|
490
|
+
Desired output unit. Default is 'kPa'.
|
|
491
|
+
val : str, optional
|
|
492
|
+
Maximum 'max' or minimum 'min' principal stress. Default is 'min'.
|
|
493
|
+
normalize : bool
|
|
494
|
+
Toggle layerwise normalization to strength.
|
|
495
|
+
|
|
496
|
+
Returns
|
|
497
|
+
-------
|
|
498
|
+
ndarray
|
|
499
|
+
Maximum or minimum principal stress in specified unit.
|
|
500
|
+
|
|
501
|
+
Raises
|
|
502
|
+
------
|
|
503
|
+
ValueError
|
|
504
|
+
If specified principal stress component is neither 'max' nor
|
|
505
|
+
'min', or if normalization of tensile principal stress
|
|
506
|
+
is requested.
|
|
507
|
+
"""
|
|
508
|
+
# Raise error if specified component is not available
|
|
509
|
+
if val not in ["min", "max"]:
|
|
510
|
+
raise ValueError(f"Component {val} not defined.")
|
|
511
|
+
|
|
512
|
+
# Multiplier selection dict
|
|
513
|
+
m = {"max": 1, "min": -1}
|
|
514
|
+
|
|
515
|
+
# Get weak-layer normal and shear stresses
|
|
516
|
+
sig = self.sm.fq.sig(Z, unit=unit)
|
|
517
|
+
tau = self.sm.fq.tau(Z, unit=unit)
|
|
518
|
+
|
|
519
|
+
# Calculate principal stress
|
|
520
|
+
ps = sig / 2 + m[val] * np.sqrt(sig**2 + 4 * tau**2) / 2
|
|
521
|
+
|
|
522
|
+
# Raise error if normalization of tensile stresses is attempted
|
|
523
|
+
if normalize and val == "max":
|
|
524
|
+
raise ValueError("Can only normalize compressive stresses.")
|
|
525
|
+
|
|
526
|
+
# Normalize compressive stresses to compressive strength
|
|
527
|
+
if normalize and val == "min":
|
|
528
|
+
return ps / sc
|
|
529
|
+
|
|
530
|
+
# Return absolute principal stresses
|
|
531
|
+
return ps
|
|
532
|
+
|
|
533
|
+
@track_analyzer_call
|
|
534
|
+
def incremental_ERR(
|
|
535
|
+
self, tolerance: float = 1e-6, unit: Literal["kJ/m^2", "J/m^2"] = "kJ/m^2"
|
|
536
|
+
) -> np.ndarray:
|
|
537
|
+
"""
|
|
538
|
+
Compute incremental energy release rate (ERR) of all cracks.
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
ndarray
|
|
543
|
+
List of total, mode I, and mode II energy release rates.
|
|
544
|
+
"""
|
|
545
|
+
li = self.sm.scenario.li
|
|
546
|
+
ki = self.sm.scenario.ki
|
|
547
|
+
k0 = np.ones_like(ki, dtype=bool)
|
|
548
|
+
C_uncracked = self.sm.uncracked_unknown_constants
|
|
549
|
+
C_cracked = self.sm.unknown_constants
|
|
550
|
+
phi = self.sm.scenario.phi
|
|
551
|
+
qs = self.sm.scenario.surface_load
|
|
552
|
+
|
|
553
|
+
# Reduce inputs to segments with crack advance
|
|
554
|
+
iscrack = k0 & ~ki
|
|
555
|
+
C_uncracked, C_cracked, li = (
|
|
556
|
+
C_uncracked[:, iscrack],
|
|
557
|
+
C_cracked[:, iscrack],
|
|
558
|
+
li[iscrack],
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Compute total crack lenght and initialize outputs
|
|
562
|
+
da = li.sum() if li.sum() > 0 else np.nan
|
|
563
|
+
Ginc1, Ginc2 = 0, 0
|
|
564
|
+
|
|
565
|
+
# Loop through segments with crack advance
|
|
566
|
+
for j, length in enumerate(li):
|
|
567
|
+
# Uncracked (0) and cracked (1) solutions at integration points
|
|
568
|
+
z_uncracked = partial(
|
|
569
|
+
self.sm.z,
|
|
570
|
+
C=C_uncracked[:, [j]],
|
|
571
|
+
length=length,
|
|
572
|
+
phi=phi,
|
|
573
|
+
has_foundation=True,
|
|
574
|
+
qs=qs,
|
|
575
|
+
)
|
|
576
|
+
z_cracked = partial(
|
|
577
|
+
self.sm.z,
|
|
578
|
+
C=C_cracked[:, [j]],
|
|
579
|
+
length=length,
|
|
580
|
+
phi=phi,
|
|
581
|
+
has_foundation=False,
|
|
582
|
+
qs=qs,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Mode I (1) and II (2) integrands at integration points
|
|
586
|
+
intGI = partial(
|
|
587
|
+
self._integrand_GI, z_uncracked=z_uncracked, z_cracked=z_cracked
|
|
588
|
+
)
|
|
589
|
+
intGII = partial(
|
|
590
|
+
self._integrand_GII, z_uncracked=z_uncracked, z_cracked=z_cracked
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Segment contributions to total crack opening integral
|
|
594
|
+
Ginc1 += quad(intGI, 0, length, epsabs=tolerance, epsrel=tolerance)[0] / (
|
|
595
|
+
2 * da
|
|
596
|
+
)
|
|
597
|
+
Ginc2 += quad(intGII, 0, length, epsabs=tolerance, epsrel=tolerance)[0] / (
|
|
598
|
+
2 * da
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
convert = {"kJ/m^2": 1, "J/m^2": 1e3}
|
|
602
|
+
return np.array([Ginc1 + Ginc2, Ginc1, Ginc2]).flatten() * convert[unit]
|
|
603
|
+
|
|
604
|
+
@track_analyzer_call
|
|
605
|
+
def differential_ERR(
|
|
606
|
+
self, unit: Literal["kJ/m^2", "J/m^2"] = "kJ/m^2"
|
|
607
|
+
) -> np.ndarray:
|
|
608
|
+
"""
|
|
609
|
+
Compute differential energy release rate of all crack tips.
|
|
610
|
+
|
|
611
|
+
Returns
|
|
612
|
+
-------
|
|
613
|
+
ndarray
|
|
614
|
+
List of total, mode I, and mode II energy release rates.
|
|
615
|
+
"""
|
|
616
|
+
li = self.sm.scenario.li
|
|
617
|
+
ki = self.sm.scenario.ki
|
|
618
|
+
C = self.sm.unknown_constants
|
|
619
|
+
phi = self.sm.scenario.phi
|
|
620
|
+
qs = self.sm.scenario.surface_load
|
|
621
|
+
|
|
622
|
+
# Get number and indices of segment transitions
|
|
623
|
+
ntr = len(li) - 1
|
|
624
|
+
itr = np.arange(ntr)
|
|
625
|
+
|
|
626
|
+
# Identify supported-free and free-supported transitions as crack tips
|
|
627
|
+
iscracktip = [ki[j] != ki[j + 1] for j in range(ntr)]
|
|
628
|
+
|
|
629
|
+
# Transition indices of crack tips and total number of crack tips
|
|
630
|
+
ict = itr[iscracktip]
|
|
631
|
+
nct = len(ict)
|
|
632
|
+
|
|
633
|
+
# Initialize energy release rate array
|
|
634
|
+
Gdif = np.zeros([3, nct])
|
|
635
|
+
|
|
636
|
+
# Compute energy relase rate of all crack tips
|
|
637
|
+
for j, idx in enumerate(ict):
|
|
638
|
+
# Solution at crack tip
|
|
639
|
+
z = self.sm.z(
|
|
640
|
+
li[idx], C[:, [idx]], li[idx], phi, has_foundation=ki[idx], qs=qs
|
|
641
|
+
)
|
|
642
|
+
# Mode I and II differential energy release rates
|
|
643
|
+
Gdif[1:, j] = np.concatenate(
|
|
644
|
+
(self.sm.fq.Gi(z, unit=unit), self.sm.fq.Gii(z, unit=unit))
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Sum mode I and II contributions
|
|
648
|
+
Gdif[0, :] = Gdif[1, :] + Gdif[2, :]
|
|
649
|
+
|
|
650
|
+
# Adjust contributions for center cracks
|
|
651
|
+
if nct > 1:
|
|
652
|
+
avgmask = np.full(nct, True) # Initialize mask
|
|
653
|
+
avgmask[[0, -1]] = ki[[0, -1]] # Do not weight edge cracks
|
|
654
|
+
Gdif[:, avgmask] *= 0.5 # Weigth with half crack length
|
|
655
|
+
|
|
656
|
+
# Return total differential energy release rate of all crack tips
|
|
657
|
+
return Gdif.sum(axis=1)
|
|
658
|
+
|
|
659
|
+
def _integrand_GI(
|
|
660
|
+
self, x: float | np.ndarray, z_uncracked, z_cracked
|
|
661
|
+
) -> float | np.ndarray:
|
|
662
|
+
"""
|
|
663
|
+
Mode I integrand for energy release rate calculation.
|
|
664
|
+
"""
|
|
665
|
+
sig_uncracked = self.sm.fq.sig(z_uncracked(x))
|
|
666
|
+
eps_cracked = self.sm.fq.eps(z_cracked(x))
|
|
667
|
+
return sig_uncracked * eps_cracked * self.sm.weak_layer.h
|
|
668
|
+
|
|
669
|
+
def _integrand_GII(
|
|
670
|
+
self, x: float | np.ndarray, z_uncracked, z_cracked
|
|
671
|
+
) -> float | np.ndarray:
|
|
672
|
+
"""
|
|
673
|
+
Mode II integrand for energy release rate calculation.
|
|
674
|
+
"""
|
|
675
|
+
tau_uncracked = self.sm.fq.tau(z_uncracked(x))
|
|
676
|
+
gamma_cracked = self.sm.fq.gamma(z_cracked(x))
|
|
677
|
+
return tau_uncracked * gamma_cracked * self.sm.weak_layer.h
|
|
678
|
+
|
|
679
|
+
@track_analyzer_call
|
|
680
|
+
def total_potential(self):
|
|
681
|
+
"""
|
|
682
|
+
Returns total differential potential.
|
|
683
|
+
Currently only implemented for PST systems.
|
|
684
|
+
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
Pi : float
|
|
688
|
+
Total differential potential (Nmm).
|
|
689
|
+
"""
|
|
690
|
+
Pi_int = self._internal_potential()
|
|
691
|
+
Pi_ext = self._external_potential()
|
|
692
|
+
|
|
693
|
+
return Pi_int + Pi_ext
|
|
694
|
+
|
|
695
|
+
def _external_potential(self):
|
|
696
|
+
"""
|
|
697
|
+
Compute total external potential (pst only).
|
|
698
|
+
|
|
699
|
+
Returns
|
|
700
|
+
-------
|
|
701
|
+
Pi_ext : float
|
|
702
|
+
Total external potential [Nmm].
|
|
703
|
+
"""
|
|
704
|
+
if self.sm.scenario.system_type not in ["pst-", "-pst"]:
|
|
705
|
+
logger.error("Input error: Only pst-setup implemented at the moment.")
|
|
706
|
+
raise NotImplementedError("Only pst-setup implemented at the moment.")
|
|
707
|
+
|
|
708
|
+
# Rasterize solution
|
|
709
|
+
xq, zq, xb = self.rasterize_solution(mode="cracked", num=2000)
|
|
710
|
+
_ = xq, xb
|
|
711
|
+
# Compute displacements where weight loads are applied
|
|
712
|
+
w0 = self.sm.fq.w(zq)
|
|
713
|
+
us = self.sm.fq.u(zq, h0=self.sm.slab.z_cog)
|
|
714
|
+
# Get weight loads
|
|
715
|
+
qn = self.sm.scenario.qn
|
|
716
|
+
qt = self.sm.scenario.qt
|
|
717
|
+
# use +/- and us[0]/us[-1] according to system and phi
|
|
718
|
+
# compute total external potential
|
|
719
|
+
Pi_ext = (
|
|
720
|
+
-qn * (self.sm.scenario.li[0] + self.sm.scenario.li[1]) * np.average(w0)
|
|
721
|
+
- qn
|
|
722
|
+
* (self.sm.scenario.L - (self.sm.scenario.li[0] + self.sm.scenario.li[1]))
|
|
723
|
+
* self.sm.scenario.crack_h
|
|
724
|
+
)
|
|
725
|
+
# Ensure
|
|
726
|
+
ub = us[0] if self.sm.scenario.system_type in ["-pst"] else us[-1]
|
|
727
|
+
Pi_ext += (
|
|
728
|
+
-qt * (self.sm.scenario.li[0] + self.sm.scenario.li[1]) * np.average(us)
|
|
729
|
+
- qt
|
|
730
|
+
* (self.sm.scenario.L - (self.sm.scenario.li[0] + self.sm.scenario.li[1]))
|
|
731
|
+
* ub
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
return Pi_ext
|
|
735
|
+
|
|
736
|
+
def _internal_potential(self):
|
|
737
|
+
"""
|
|
738
|
+
Compute total internal potential (pst only).
|
|
739
|
+
|
|
740
|
+
Returns
|
|
741
|
+
-------
|
|
742
|
+
Pi_int : float
|
|
743
|
+
Total internal potential [Nmm].
|
|
744
|
+
"""
|
|
745
|
+
if self.sm.scenario.system_type not in ["pst-", "-pst"]:
|
|
746
|
+
logger.error("Input error: Only pst-setup implemented at the moment.")
|
|
747
|
+
raise NotImplementedError("Only pst-setup implemented at the moment.")
|
|
748
|
+
|
|
749
|
+
# Extract system parameters
|
|
750
|
+
L = self.sm.scenario.L
|
|
751
|
+
system_type = self.sm.scenario.system_type
|
|
752
|
+
A11 = self.sm.eigensystem.A11
|
|
753
|
+
B11 = self.sm.eigensystem.B11
|
|
754
|
+
D11 = self.sm.eigensystem.D11
|
|
755
|
+
kA55 = self.sm.eigensystem.kA55
|
|
756
|
+
kn = self.sm.weak_layer.kn
|
|
757
|
+
kt = self.sm.weak_layer.kt
|
|
758
|
+
|
|
759
|
+
# Rasterize solution
|
|
760
|
+
xq, zq, xb = self.rasterize_solution(mode="cracked", num=2000)
|
|
761
|
+
|
|
762
|
+
# Compute section forces
|
|
763
|
+
N, M, V = self.sm.fq.N(zq), self.sm.fq.M(zq), self.sm.fq.V(zq)
|
|
764
|
+
|
|
765
|
+
# Drop parts of the solution that are not a foundation
|
|
766
|
+
zweak = zq[:, ~np.isnan(xb)]
|
|
767
|
+
xweak = xb[~np.isnan(xb)]
|
|
768
|
+
|
|
769
|
+
# Compute weak layer displacements
|
|
770
|
+
wweak = self.sm.fq.w(zweak)
|
|
771
|
+
uweak = self.sm.fq.u(zweak, h0=self.sm.slab.H / 2)
|
|
772
|
+
|
|
773
|
+
# Compute stored energy of the slab (monte-carlo integration)
|
|
774
|
+
n = len(xq)
|
|
775
|
+
nweak = len(xweak)
|
|
776
|
+
# energy share from moment, shear force, wl normal and tangential springs
|
|
777
|
+
Pi_int = (
|
|
778
|
+
L / 2 / n / A11 * np.sum([Ni**2 for Ni in N])
|
|
779
|
+
+ L / 2 / n / (D11 - B11**2 / A11) * np.sum([Mi**2 for Mi in M])
|
|
780
|
+
+ L / 2 / n / kA55 * np.sum([Vi**2 for Vi in V])
|
|
781
|
+
+ L * kn / 2 / nweak * np.sum([wi**2 for wi in wweak])
|
|
782
|
+
+ L * kt / 2 / nweak * np.sum([ui**2 for ui in uweak])
|
|
783
|
+
)
|
|
784
|
+
# energy share from substitute rotation spring
|
|
785
|
+
if system_type in ["pst-"]:
|
|
786
|
+
Pi_int += 1 / 2 * M[-1] * (self.sm.fq.psi(zq)[-1]) ** 2
|
|
787
|
+
elif system_type in ["-pst"]:
|
|
788
|
+
Pi_int += 1 / 2 * M[0] * (self.sm.fq.psi(zq)[0]) ** 2
|
|
789
|
+
|
|
790
|
+
return Pi_int
|