Skip to content

Commit 2e4b0f3

Browse files
authored
Minor/fixes (#52)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved slab tensile criterion calculation with directional propagation logic for low-density level assessment. * **Documentation** * Clarified configuration parameter documentation regarding weak-snow handling and weak-layer behavior. * **Tests** * Added test coverage for energy release rate calculations. * Enhanced test suite to verify directional assessment logic in stability analysis. * Updated stress calculation test expectations. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/2phi/weac/pull/52?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 2a595d3 + ecb877b commit 2e4b0f3

8 files changed

Lines changed: 95 additions & 31 deletions

File tree

TODO.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
## Major
44

5+
- [ ] Layer & Slab using pipelines from Mary-Kate + Attributes (value, calculated [bool], pipeline, uncertainty)
6+
- [ ] Uncertainties propagation
57
- [ ] Use Classes for Boundary Types
68
- [ ] Automatically figure out type of system
79
- [ ] Automatically set boundary conditions based on system
810

911
## Minor
1012

13+
- [ ] Swap to Pytest from Unittest
1114
- [ ] resolve fracture criterion also when lower than strength criterion
1215
- [ ] Florian CriterionEvaluator: clarify and fix damping behavior (find_minimum_force / evaluate_coupled_criterion)
1316
- Expected behavior

src/weac/analysis/criteria_evaluator.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,12 @@ def evaluate_SteadyState(
721721
-----------
722722
system: SystemModel
723723
The system model.
724+
mode: TouchdownMode, optional
725+
Touchdown evaluation mode. The three supported modes are:
726+
``"C_in_contact"`` for a cut distance of ``2 * l_BC``,
727+
``"B_point_contact"`` for a cut distance just shorter than ``l_BC``,
728+
and ``"A_free_hanging"`` for a cut distance just shorter than ``l_AB``.
729+
Defaults to ``"C_in_contact"``.
724730
vertical: bool, optional
725731
Whether to evaluate the system in a vertical configuration.
726732
Defaults to False.
@@ -1301,15 +1307,30 @@ def _calculate_maximal_stresses(
13011307
)
13021308
max_principal_stress_norm = np.max(principal_stress_norm)
13031309
max_Sxx_norm = np.max(Sxx_norm)
1304-
# Count height levels as failure-prone when tensile stress exceeds the layer
1305-
# strength or the slab density is below the configured weak-snow threshold.
1306-
# zmesh rho is t/mm^3, layer rho is kg/m^3
1310+
# zmesh rows are ordered from slab top to bottom. Low-density levels only
1311+
# fail through downward growth from tensile failures above; when they do,
1312+
# they are excluded from the slab tensile criterion denominator.
1313+
# zmesh rho is t/mm^3, layer rho is kg/m^3.
13071314
zmesh = analyzer.get_zmesh(dz=1)
13081315
rho_kg_m3 = zmesh["rho"] * 1e12
13091316
tensile_exceeds = np.max(Sxx_norm, axis=1) > 1
13101317
low_density = rho_kg_m3 <= self.criteria_config.low_density_threshold_kg_m3
1311-
height_level_prone_to_fail = tensile_exceeds | low_density
1312-
slab_tensile_criterion = np.mean(height_level_prone_to_fail)
1318+
height_level_prone_to_fail = np.zeros_like(tensile_exceeds, dtype=bool)
1319+
all_above_prone_to_fail = True
1320+
for index, is_low_density in enumerate(low_density):
1321+
if is_low_density:
1322+
height_level_prone_to_fail[index] = all_above_prone_to_fail
1323+
else:
1324+
height_level_prone_to_fail[index] = tensile_exceeds[index]
1325+
all_above_prone_to_fail &= height_level_prone_to_fail[index]
1326+
1327+
low_density_prone_to_fail = low_density & height_level_prone_to_fail
1328+
load_bearing_levels = ~low_density_prone_to_fail
1329+
slab_tensile_criterion = (
1330+
np.mean(height_level_prone_to_fail[load_bearing_levels])
1331+
if np.any(load_bearing_levels)
1332+
else 1.0
1333+
)
13131334
if print_call_stats:
13141335
analyzer.print_call_stats(
13151336
message="_calculate_maximal_stresses Call Statistics"

src/weac/components/criteria_config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ class CriteriaConfig(BaseModel):
5050
Order of magnitude for stress envelope. Default is 1.0.
5151
low_density_threshold_kg_m3 : float
5252
Slab density threshold in kg/m^3 below which a layer is treated as weak snow
53-
and counted as prone to tensile failure in the slab tensile criterion.
53+
and excluded from the slab tensile criterion percentage when broken through
54+
directional growth from above.
5455
"""
5556

5657
fn: float = Field(
@@ -94,6 +95,6 @@ class CriteriaConfig(BaseModel):
9495
gt=0,
9596
description=(
9697
"Slab density threshold in kg/m^3 below which a layer is treated as weak "
97-
"snow in the slab tensile criterion"
98+
"snow and conditionally excluded from the slab tensile criterion percentage"
9899
),
99100
)

src/weac/core/field_quantities.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,14 @@ def sig(self, Z: np.ndarray, unit: StressUnit = "MPa") -> float | np.ndarray:
107107
return -self._unit_factor(unit) * self.es.weak_layer.kn * self.w(Z)
108108

109109
def tau(self, Z: np.ndarray, unit: StressUnit = "MPa") -> float | np.ndarray:
110-
"""Weak-layer shear stress `tau = -kt * (w' * h/2 - u(h=H/2))`"""
110+
"""Weak-layer shear stress `tau = kt * h * (w' / 2 - u(h=H/2) / h)`"""
111111
return (
112-
-self._unit_factor(unit)
112+
self._unit_factor(unit)
113113
* self.es.weak_layer.kt
114+
* self.es.weak_layer.h
114115
* (
115-
self.dw_dx(Z) * self.es.weak_layer.h / 2
116-
- self.u(Z, h0=self.es.slab.H / 2)
116+
self.dw_dx(Z) / 2
117+
- self.u(Z, h0=self.es.slab.H / 2) / self.es.weak_layer.h
117118
)
118119
)
119120

@@ -122,7 +123,7 @@ def eps(self, Z: np.ndarray) -> float | np.ndarray:
122123
return -self.w(Z) / self.es.weak_layer.h
123124

124125
def gamma(self, Z: np.ndarray) -> float | np.ndarray:
125-
"""Weak-layer shear strain `gamma = (w' * h/2 - u(h=H/2)) / h`"""
126+
"""Weak-layer shear strain `gamma = w' / 2 - u(h=H/2) / h`"""
126127
return (
127128
self.dw_dx(Z) / 2 - self.u(Z, h0=self.es.slab.H / 2) / self.es.weak_layer.h
128129
)

tests/analysis/test_analyzer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,37 @@ def test_energy_release_rates_shapes(self):
156156
self.assertEqual(Gdif.shape, (4,))
157157
self.assertTrue(np.isfinite(Gdif).all())
158158

159+
def test_energy_release_rate_integrands_non_negative(self):
160+
"""Test that ERR integrands are non-negative for matching stress/strain."""
161+
slope_angle = 20.0
162+
system = SystemModel(
163+
model_input=ModelInput(
164+
scenario_config=ScenarioConfig(phi=slope_angle, system_type="skier"),
165+
layers=[Layer()],
166+
weak_layer=WeakLayer(),
167+
segments=[Segment(), Segment()],
168+
),
169+
config=Config(),
170+
)
171+
analyzer = Analyzer(system_model=system, printing_enabled=False)
172+
173+
z_uncracked = np.array([[0.0], [0.0], [1.0], [0.2], [0.0], [0.0]])
174+
175+
def constant_solution(x):
176+
return np.repeat(z_uncracked, np.size(np.atleast_1d(x)), axis=1)
177+
178+
mode_i = analyzer._integrand_GI( # pylint: disable=protected-access
179+
np.array([0.0, 1.0]), constant_solution, constant_solution
180+
)
181+
mode_ii = analyzer._integrand_GII( # pylint: disable=protected-access
182+
np.array([0.0, 1.0]), constant_solution, constant_solution
183+
)
184+
185+
self.assertTrue(np.all(mode_i >= 0), "Mode I integrand should be non-negative")
186+
self.assertTrue(
187+
np.all(mode_ii >= 0), "Mode II integrand should be non-negative"
188+
)
189+
159190
def test_internal_and_external_potentials_pst(self):
160191
"""Test internal and external potentials for PST."""
161192
# Ensure PST-specific methods run

tests/analysis/test_criteria_evaluator.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,19 @@ def test_stress_envelope_adam_unpublished(self):
6868
self.assertGreater(result[0], 0)
6969

7070
@patch("weac.analysis.criteria_evaluator.Analyzer")
71-
def test_calculate_maximal_stresses_uses_configured_low_density_threshold(
71+
def test_calculate_maximal_stresses_applies_directional_low_density_exclusion(
7272
self, mock_analyzer_cls
73-
):
74-
"""Test that the slab tensile criterion uses the configured density cutoff."""
75-
sxx_kpa = np.zeros((3, 1))
76-
principal_stress_kPa = np.zeros((3, 1))
77-
sxx_norm = np.full((3, 1), 0.5)
78-
principal_stress_norm = np.full((3, 1), 0.5)
73+
): # pylint: disable=protected-access
74+
"""Test that weak snow is excluded only after downward failure growth."""
75+
sxx_kpa = np.zeros((4, 1))
76+
principal_stress_kPa = np.zeros((4, 1))
77+
sxx_norm = np.array([[1.5], [0.5], [0.5], [0.5]])
78+
principal_stress_norm = np.full((4, 1), 0.5)
7979

8080
mock_analyzer = mock_analyzer_cls.return_value
8181
mock_analyzer.rasterize_solution.return_value = (
8282
None,
83-
np.array([0, 1, 2]),
83+
np.array([0, 1, 2, 3]),
8484
None,
8585
)
8686
mock_analyzer.Sxx.side_effect = (
@@ -92,20 +92,22 @@ def test_calculate_maximal_stresses_uses_configured_low_density_threshold(
9292
)
9393
)
9494
mock_analyzer.get_zmesh.return_value = {
95-
"rho": np.array([90.0, 110.0, 130.0]) * 1e-12
95+
"rho": np.array([130.0, 90.0, 90.0, 130.0]) * 1e-12
9696
}
9797
system = SimpleNamespace(scenario=SimpleNamespace(phi=30.0))
9898

9999
# Access the helper directly so the test can isolate the density-threshold logic.
100-
default_result = CriteriaEvaluator(
100+
top_broken_result = CriteriaEvaluator(
101101
CriteriaConfig()
102102
)._calculate_maximal_stresses(system=system) # pylint: disable=protected-access
103-
tuned_result = CriteriaEvaluator(
104-
CriteriaConfig(low_density_threshold_kg_m3=120)
103+
104+
sxx_norm = np.array([[0.5], [0.5], [0.5], [1.5]])
105+
top_unbroken_result = CriteriaEvaluator(
106+
CriteriaConfig()
105107
)._calculate_maximal_stresses(system=system) # pylint: disable=protected-access
106108

107-
self.assertAlmostEqual(default_result.slab_tensile_criterion, 1 / 3)
108-
self.assertAlmostEqual(tuned_result.slab_tensile_criterion, 2 / 3)
109+
self.assertAlmostEqual(top_broken_result.slab_tensile_criterion, 1 / 2)
110+
self.assertAlmostEqual(top_unbroken_result.slab_tensile_criterion, 1 / 4)
109111

110112
def test_find_minimum_force_convergence(self):
111113
"""Test the convergence of find_minimum_force."""

tests/analysis/test_slab_tensile_comparisons.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ def _setup_from_cm(*layers: tuple[float, float]) -> SetupDefinition:
8080
),
8181
ComparisonCase(
8282
name="case_2",
83-
setup_a=_setup_from_cm((50, 75), (20, 225)),
84-
setup_b=_setup_from_cm((30, 75), (20, 225)),
83+
setup_a=_setup_from_cm((30, 75), (20, 225)),
84+
setup_b=_setup_from_cm((50, 75), (20, 225)),
8585
),
8686
ComparisonCase(
8787
name="case_3",
@@ -115,8 +115,13 @@ def _setup_from_cm(*layers: tuple[float, float]) -> SetupDefinition:
115115
),
116116
ComparisonCase(
117117
name="case_9",
118-
setup_a=_setup_from_cm((15, 275), (40, 75)),
119-
setup_b=_setup_from_cm((40, 75), (15, 275)),
118+
setup_a=_setup_from_cm((40, 75), (15, 275)),
119+
setup_b=_setup_from_cm((15, 275), (40, 75)),
120+
),
121+
ComparisonCase(
122+
name="case_10",
123+
setup_a=_setup_from_cm((30, 75), (20, 275)),
124+
setup_b=_setup_from_cm((50, 75), (20, 275)),
120125
),
121126
)
122127

tests/core/test_field_quantities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def test_weak_layer_shear_stress(self):
272272
H = self.fq.es.slab.H
273273
u_surface = self.fq.u(self.Z, h0=H / 2)
274274

275-
expected = -self.fq.es.weak_layer.kt * (self.Z[3, :] * h / 2 - u_surface)
275+
expected = self.fq.es.weak_layer.kt * (self.Z[3, :] * h / 2 - u_surface)
276276
np.testing.assert_array_almost_equal(
277277
tau,
278278
expected,

0 commit comments

Comments
 (0)