Skip to content

Commit 8f52770

Browse files
authored
Tensile Strength Evaluation as part of Steady State Analysis (#34)
This pull request refactors and improves the stress and steady-state evaluation logic in the `weac.analysis` package. It introduces new result data classes and fixes plotting issues related to stress calculations and visualization. ### Refactoring and Data Model Improvements * Introduced `MaximalStressResult` and `SteadyStateResult` data classes, replacing the old `SSERRResult` for clearer and more comprehensive result encapsulation. The new classes include normalized stress fields and system state at touchdown. (`[[1]](diffhunk://#diff-cd31ff029b0e336203154640e70d582b1554ca04f41c94048eaec4dee7daab6dL98-R129)`, `[[2]](diffhunk://#diff-cd31ff029b0e336203154640e70d582b1554ca04f41c94048eaec4dee7daab6dL110-R153)`, `[[3]](diffhunk://#diff-1b897e0700621752c15b1115da95c4b2a2d61aa57dde50d6b780bf490269b16cL11-R11)`, `[[4]](diffhunk://#diff-1b897e0700621752c15b1115da95c4b2a2d61aa57dde50d6b780bf490269b16cL21-R21)`) * Updated method names and references from `SSERR` to `SteadyState` for consistency and clarity, including renaming the evaluation method and result types. (`[[1]](diffhunk://#diff-cd31ff029b0e336203154640e70d582b1554ca04f41c94048eaec4dee7daab6dL644-R684)`, `[[2]](diffhunk://#diff-cd31ff029b0e336203154640e70d582b1554ca04f41c94048eaec4dee7daab6dL691-R734)`, `[[3]](diffhunk://#diff-1b897e0700621752c15b1115da95c4b2a2d61aa57dde50d6b780bf490269b16cL11-R11)`, `[[4]](diffhunk://#diff-1b897e0700621752c15b1115da95c4b2a2d61aa57dde50d6b780bf490269b16cL21-R21)`) ### Stress Calculation Enhancements * Refactored the `Sxx` method in `analyzer.py` to support normalization of axial normal stress to tensile strength, improving numerical stability and result interpretability. (`[[1]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440L228-R228)`, `[[2]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440R242-R243)`, `[[3]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440L261-R269)`, `[[4]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440L277-R291)`) * Improved normalization logic in `principal_stress_slab` to correctly convert tensile strength units and normalize principal stresses. (`[[1]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440R451-R452)`, `[[2]](diffhunk://#diff-8468e03357e32957d46ec295646154a4a64e750fa249655a80c6620d9ad32440L463-R478)`) * Added `_calculate_maximal_stresses` helper method to centralize maximal stress computation for steady-state evaluation. (`[src/weac/analysis/criteria_evaluator.pyR1211-R1241](diffhunk://#diff-cd31ff029b0e336203154640e70d582b1554ca04f41c94048eaec4dee7daab6dR1211-R1241)`) ### Plotting and Visualization Fixes * Updated `plot_deformed` in `plotter.py` to robustly handle NaN values in weak-layer coordinates, ensuring correct plotting and preventing errors when visualizing stress/displacement fields. (`[[1]](diffhunk://#diff-737cdaa926ddb7c8e3803d8568adddbe44745a23baca2d6bd39fc27e8a41a1dcL888-R934)`, `[[2]](diffhunk://#diff-737cdaa926ddb7c8e3803d8568adddbe44745a23baca2d6bd39fc27e8a41a1dcL971-R978)`) These changes collectively enhance the clarity of stress analysis and steady-state evaluation in the codebase. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Per-layer stress normalization option and a new deformation visualization routine with selectable fields and normalization. * **Bug Fixes** * Robust handling and masking of NaN/invalid weak-layer coordinates in deformation plotting. * **Refactor** * Steady‑state evaluation payload reorganized and renamed: includes energy-release metric, detailed maximal-stress metrics, and system info. * **Tests** * Added tests for stress unit conversion and normalization consistency. * **Chores** * Gitignore reinstates .weac-reference and adds a dev/ entry. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 2235508 + 59dd3a7 commit 8f52770

10 files changed

Lines changed: 645 additions & 93 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,7 @@ scratch/
5656
temp*
5757
old*
5858

59-
.weac-reference/
59+
.weac-reference/
60+
61+
# Folder for development and local testing
62+
dev/

demo/demo.ipynb

Lines changed: 53 additions & 24 deletions
Large diffs are not rendered by default.

src/weac/analysis/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
CoupledCriterionResult,
99
CriteriaEvaluator,
1010
FindMinimumForceResult,
11-
SSERRResult,
11+
SteadyStateResult,
1212
)
1313
from .plotter import Plotter
1414

@@ -18,6 +18,6 @@
1818
"CoupledCriterionHistory",
1919
"CoupledCriterionResult",
2020
"FindMinimumForceResult",
21-
"SSERRResult",
21+
"SteadyStateResult",
2222
"Plotter",
2323
]

src/weac/analysis/analyzer.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def get_zmesh(self, dz=2):
214214
], # Convert to t/mm^3
215215
"tensile_strength": [
216216
layer.tensile_strength for layer in self.sm.slab.layers
217-
],
217+
], # in kPa
218218
}
219219

220220
# Repeat properties for each grid point in the layer
@@ -225,7 +225,7 @@ def get_zmesh(self, dz=2):
225225
return si
226226

227227
@track_analyzer_call
228-
def Sxx(self, Z, phi, dz=2, unit="kPa"):
228+
def Sxx(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
229229
"""
230230
Compute axial normal stress in slab layers.
231231
@@ -239,6 +239,10 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
239239
Element size along z-axis (mm). Default is 2 mm.
240240
unit : {'kPa', 'MPa'}, optional
241241
Desired output unit. Default is 'kPa'.
242+
normalize : bool, optional
243+
Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
244+
When normalized, the `unit` parameter is ignored and values are returned as ratios.
245+
Default is False.
242246
243247
Returns
244248
-------
@@ -258,13 +262,13 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
258262
m = Z.shape[1]
259263

260264
# Initialize axial normal stress Sxx
261-
Sxx = np.zeros(shape=[n, m])
265+
Sxx_MPa = np.zeros(shape=[n, m])
262266

263267
# Compute axial normal stress Sxx at grid points in MPa
264268
for i, z in enumerate(zi):
265-
E = zmesh["E"][i]
269+
E_MPa = zmesh["E"][i]
266270
nu = zmesh["nu"][i]
267-
Sxx[i, :] = E / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
271+
Sxx_MPa[i, :] = E_MPa / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
268272

269273
# Calculate weight load at grid points and superimpose on stress field
270274
qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
@@ -274,14 +278,22 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
274278
# Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
275279
# New Implementation: Changed for numerical stability
276280
dz = np.diff(zi)
277-
Sxx[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
278-
Sxx[-1, :] += qt[-1] * dz[-1]
281+
Sxx_MPa[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
282+
Sxx_MPa[-1, :] += qt[-1] * dz[-1]
283+
284+
# Normalize tensile stresses to tensile strength
285+
if normalize:
286+
tensile_strength_kPa = zmesh["tensile_strength"]
287+
tensile_strength_MPa = tensile_strength_kPa / 1e3
288+
# Normalize axial normal stress to layers' tensile strength
289+
normalized_Sxx = Sxx_MPa / tensile_strength_MPa[:, None]
290+
return normalized_Sxx
279291

280292
# Return axial normal stress in specified unit
281-
return convert[unit] * Sxx
293+
return convert[unit] * Sxx_MPa
282294

283295
@track_analyzer_call
284-
def Txz(self, Z, phi, dz=2, unit="kPa"):
296+
def Txz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
285297
"""
286298
Compute shear stress in slab layers.
287299
@@ -295,6 +307,9 @@ def Txz(self, Z, phi, dz=2, unit="kPa"):
295307
Element size along z-axis (mm). Default is 2 mm.
296308
unit : {'kPa', 'MPa'}, optional
297309
Desired output unit. Default is 'kPa'.
310+
normalize : bool, optional
311+
Toggle normalization. If True, normalize shear stress values to the tensile strength of each layer (dimensionless).
312+
When normalized, the `unit` parameter is ignored and values are returned as ratios. Default is False.
298313
299314
Returns
300315
-------
@@ -332,14 +347,22 @@ def Txz(self, Z, phi, dz=2, unit="kPa"):
332347

333348
# Integrate -dsxx_dx along z and add cumulative weight load
334349
# 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]
350+
Txz_MPa = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
351+
Txz_MPa += cumulative_trapezoid(qt, zi, initial=0)[:, None]
352+
353+
# Normalize shear stresses to tensile strength
354+
if normalize:
355+
tensile_strength_kPa = zmesh["tensile_strength"]
356+
tensile_strength_MPa = tensile_strength_kPa / 1e3
357+
# Normalize shear stress to layers' tensile strength
358+
normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
359+
return normalized_Txz
337360

338361
# Return shear stress Txz in specified unit
339-
return convert[unit] * Txz
362+
return convert[unit] * Txz_MPa
340363

341364
@track_analyzer_call
342-
def Szz(self, Z, phi, dz=2, unit="kPa"):
365+
def Szz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
343366
"""
344367
Compute transverse normal stress in slab layers.
345368
@@ -353,6 +376,10 @@ def Szz(self, Z, phi, dz=2, unit="kPa"):
353376
Element size along z-axis (mm). Default is 2 mm.
354377
unit : {'kPa', 'MPa'}, optional
355378
Desired output unit. Default is 'kPa'.
379+
normalize : bool, optional
380+
Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
381+
When normalized, the `unit` parameter is ignored and values are returned as ratios.
382+
Default is False.
356383
357384
Returns
358385
-------
@@ -392,11 +419,19 @@ def Szz(self, Z, phi, dz=2, unit="kPa"):
392419
# Integrate dsxx_dxdx twice along z to obtain transverse
393420
# normal stress Szz in MPa
394421
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]
422+
Szz_MPa = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
423+
Szz_MPa += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
397424

398-
# Return shear stress txz in specified unit
399-
return convert[unit] * Szz
425+
# Normalize tensile stresses to tensile strength
426+
if normalize:
427+
tensile_strength_kPa = zmesh["tensile_strength"]
428+
tensile_strength_MPa = tensile_strength_kPa / 1e3
429+
# Normalize transverse normal stress to layers' tensile strength
430+
normalized_Szz = Szz_MPa / tensile_strength_MPa[:, None]
431+
return normalized_Szz
432+
433+
# Return transverse normal stress Szz in specified unit
434+
return convert[unit] * Szz_MPa
400435

401436
@track_analyzer_call
402437
def principal_stress_slab(
@@ -438,6 +473,8 @@ def principal_stress_slab(
438473
'min', or if normalization of compressive principal stress
439474
is requested.
440475
"""
476+
convert = {"kPa": 1e3, "MPa": 1}
477+
441478
# Raise error if specified component is not available
442479
if val not in ["min", "max"]:
443480
raise ValueError(f"Component {val} not defined.")
@@ -460,9 +497,10 @@ def principal_stress_slab(
460497
# Normalize tensile stresses to tensile strength
461498
if normalize and val == "max":
462499
zmesh = self.get_zmesh(dz=dz)
463-
tensile_strength = zmesh["tensile_strength"]
500+
tensile_strength_kPa = zmesh["tensile_strength"]
501+
tensile_strength_converted = tensile_strength_kPa / 1e3 * convert[unit]
464502
# Normalize maximum principal stress to layers' tensile strength
465-
normalized_Ps = Ps / tensile_strength[:, None]
503+
normalized_Ps = Ps / tensile_strength_converted[:, None]
466504
return normalized_Ps
467505

468506
# Return absolute principal stresses

src/weac/analysis/criteria_evaluator.py

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,38 @@ class CoupledCriterionResult:
9595

9696

9797
@dataclass
98-
class SSERRResult:
98+
class MaximalStressResult:
9999
"""
100-
Holds the results of the SSERR evaluation.
100+
Holds the results of the maximal stress evaluation.
101+
102+
Attributes:
103+
-----------
104+
principal_stress_kPa: np.ndarray
105+
The principal stress in kPa.
106+
Sxx_kPa: np.ndarray
107+
The axial normal stress in kPa.
108+
principal_stress_norm: np.ndarray
109+
The normalized principal stress to the tensile strength of the layers.
110+
Sxx_norm: np.ndarray
111+
The normalized axial normal stress to the tensile strength of the layers.
112+
max_principal_stress_norm: float
113+
The normalized maximum principal stress to the tensile strength of the layers.
114+
max_Sxx_norm: float
115+
The normalized maximum axial normal stress to the tensile strength of the layers.
116+
"""
117+
118+
principal_stress_kPa: np.ndarray
119+
Sxx_kPa: np.ndarray
120+
principal_stress_norm: np.ndarray
121+
Sxx_norm: np.ndarray
122+
max_principal_stress_norm: float
123+
max_Sxx_norm: float
124+
125+
126+
@dataclass
127+
class SteadyStateResult:
128+
"""
129+
Holds the results of the Steady State evaluation.
101130
102131
Attributes:
103132
-----------
@@ -107,15 +136,21 @@ class SSERRResult:
107136
The message of the evaluation.
108137
touchdown_distance : float
109138
The touchdown distance.
110-
SSERR : float
111-
The Steady-State Energy Release Rate calculated with the
112-
touchdown distance from G_I and G_II.
139+
energy_release_rate : float
140+
The steady-state energy release rate calculated with the
141+
touchdown distance from the differential energy release rate.
142+
maximal_stress_result: MaximalStressResult
143+
The maximal stresses in the system at the touchdown distance.
144+
system: SystemModel
145+
The modified system model used for the steady state evaluation.
113146
"""
114147

115148
converged: bool
116149
message: str
117150
touchdown_distance: float
118-
SSERR: float
151+
energy_release_rate: float
152+
maximal_stress_result: MaximalStressResult
153+
system: SystemModel
119154

120155

121156
@dataclass
@@ -641,12 +676,12 @@ def evaluate_coupled_criterion(
641676
_recursion_depth=_recursion_depth + 1,
642677
)
643678

644-
def evaluate_SSERR(
679+
def evaluate_SteadyState(
645680
self,
646681
system: SystemModel,
647682
vertical: bool = False,
648683
print_call_stats: bool = False,
649-
) -> SSERRResult:
684+
) -> SteadyStateResult:
650685
"""
651686
Evaluates the Touchdown Distance in the Steady State and the Steady State
652687
Energy Release Rate.
@@ -688,12 +723,19 @@ def evaluate_SSERR(
688723
system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
689724
touchdown_distance = system_copy.slab_touchdown.touchdown_distance
690725
analyzer = Analyzer(system_copy, printing_enabled=print_call_stats)
691-
G, _, _ = analyzer.differential_ERR(unit="J/m^2")
692-
return SSERRResult(
726+
energy_release_rate, _, _ = analyzer.differential_ERR(unit="J/m^2")
727+
maximal_stress_result = self._calculate_maximal_stresses(
728+
system_copy, print_call_stats=print_call_stats
729+
)
730+
if print_call_stats:
731+
analyzer.print_call_stats(message="evaluate_SteadyState Call Statistics")
732+
return SteadyStateResult(
693733
converged=True,
694-
message="SSERR evaluation successful.",
734+
message="Steady State evaluation successful.",
695735
touchdown_distance=touchdown_distance,
696-
SSERR=G,
736+
energy_release_rate=energy_release_rate,
737+
maximal_stress_result=maximal_stress_result,
738+
system=system_copy,
697739
)
698740

699741
def find_minimum_force(
@@ -1170,3 +1212,51 @@ def _fracture_toughness_exceedance(
11701212

11711213
# Return the difference from the target
11721214
return g_delta_diff - target
1215+
1216+
def _calculate_maximal_stresses(
1217+
self,
1218+
system: SystemModel,
1219+
print_call_stats: bool = False,
1220+
) -> MaximalStressResult:
1221+
"""
1222+
Calculate the maximal stresses in the system.
1223+
1224+
Parameters
1225+
----------
1226+
system : SystemModel
1227+
The system model to analyze.
1228+
print_call_stats : bool, optional
1229+
Whether to print analyzer call statistics. Default is False.
1230+
1231+
Returns
1232+
-------
1233+
MaximalStressResult
1234+
Object containing both absolute (in kPa) and normalized stress fields,
1235+
along with maximum normalized stress values.
1236+
"""
1237+
analyzer = Analyzer(system, printing_enabled=print_call_stats)
1238+
_, Z, _ = analyzer.rasterize_solution(num=4000, mode="cracked")
1239+
Sxx_kPa = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=5, unit="kPa")
1240+
principal_stress_kPa = analyzer.principal_stress_slab(
1241+
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa"
1242+
)
1243+
Sxx_norm = analyzer.Sxx(
1244+
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1245+
)
1246+
principal_stress_norm = analyzer.principal_stress_slab(
1247+
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1248+
)
1249+
max_principal_stress_norm = np.max(principal_stress_norm)
1250+
max_Sxx_norm = np.max(Sxx_norm)
1251+
if print_call_stats:
1252+
analyzer.print_call_stats(
1253+
message="_calculate_maximal_stresses Call Statistics"
1254+
)
1255+
return MaximalStressResult(
1256+
principal_stress_kPa=principal_stress_kPa,
1257+
Sxx_kPa=Sxx_kPa,
1258+
principal_stress_norm=principal_stress_norm,
1259+
Sxx_norm=Sxx_norm,
1260+
max_principal_stress_norm=max_principal_stress_norm,
1261+
max_Sxx_norm=max_Sxx_norm,
1262+
)

0 commit comments

Comments
 (0)