weac 3.1.1__tar.gz → 3.1.2__tar.gz
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-3.1.1 → weac-3.1.2}/CITATION.cff +1 -1
- {weac-3.1.1/src/weac.egg-info → weac-3.1.2}/PKG-INFO +1 -1
- {weac-3.1.1 → weac-3.1.2}/pyproject.toml +2 -2
- {weac-3.1.1 → weac-3.1.2}/src/weac/__init__.py +1 -1
- {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/criteria_evaluator.py +21 -4
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/layer.py +60 -15
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/slab_touchdown.py +5 -4
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/system_model.py +1 -2
- {weac-3.1.1 → weac-3.1.2/src/weac.egg-info}/PKG-INFO +1 -1
- {weac-3.1.1 → weac-3.1.2}/tests/analysis/test_criteria_evaluator.py +40 -0
- weac-3.1.2/tests/components/test_layer.py +441 -0
- weac-3.1.1/tests/components/test_layer.py +0 -221
- {weac-3.1.1 → weac-3.1.2}/LICENSE +0 -0
- {weac-3.1.1 → weac-3.1.2}/MANIFEST.in +0 -0
- {weac-3.1.1 → weac-3.1.2}/README.md +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/bc.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/layering.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/logo.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/model.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/profiles.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/img/systems.png +0 -0
- {weac-3.1.1 → weac-3.1.2}/setup.cfg +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/analyzer.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/plotter.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/config.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/criteria_config.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/model_input.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/scenario_config.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/components/segment.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/constants.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/eigensystem.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/field_quantities.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/scenario.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/slab.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/core/unknown_constants_solver.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/logging_config.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/utils/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/utils/geldsetzer.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/utils/misc.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/utils/snow_types.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac/utils/snowpilot_parser.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/SOURCES.txt +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/dependency_links.txt +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/requires.txt +0 -0
- {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/top_level.txt +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/analysis/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/analysis/test_analyzer.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/components/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/components/test_configs.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_eigensystem.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_field_quantities.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_scenario.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_slab.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_slab_touchdown.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/core/test_system_model.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/run_tests.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/test_comparison_results.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/test_regression_simulation.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/__init__.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/json_helpers.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/test_json_helpers.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/test_misc.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/test_snowpilot_parser.py +0 -0
- {weac-3.1.1 → weac-3.1.2}/tests/utils/weac_reference_runner.py +0 -0
|
@@ -8,7 +8,7 @@ authors:
|
|
|
8
8
|
- family-names: "Weissgraeber"
|
|
9
9
|
given-names: "Philipp"
|
|
10
10
|
orcid: "https://orcid.org/0000-0001-8320-8672"
|
|
11
|
-
version: 3.1.
|
|
11
|
+
version: 3.1.2
|
|
12
12
|
date-released: 2021-12-30
|
|
13
13
|
identifiers:
|
|
14
14
|
- description: Collection of archived snapshots of all versions of WEAC
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weac"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.2"
|
|
8
8
|
authors = [{ name = "2phi GbR", email = "mail@2phi.de" }]
|
|
9
9
|
description = "Weak layer anticrack nucleation model"
|
|
10
10
|
readme = "README.md"
|
|
@@ -123,7 +123,7 @@ ignore = [
|
|
|
123
123
|
]
|
|
124
124
|
|
|
125
125
|
[tool.bumpversion]
|
|
126
|
-
current_version = "3.1.
|
|
126
|
+
current_version = "3.1.2"
|
|
127
127
|
|
|
128
128
|
[[tool.bumpversion.files]]
|
|
129
129
|
filename = "pyproject.toml"
|
|
@@ -24,6 +24,7 @@ from weac.components import (
|
|
|
24
24
|
WeakLayer,
|
|
25
25
|
)
|
|
26
26
|
from weac.constants import RHO_ICE
|
|
27
|
+
from weac.core.slab_touchdown import TouchdownMode
|
|
27
28
|
from weac.core.system_model import SystemModel
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
@@ -683,6 +684,7 @@ class CriteriaEvaluator:
|
|
|
683
684
|
def evaluate_SteadyState(
|
|
684
685
|
self,
|
|
685
686
|
system: SystemModel,
|
|
687
|
+
mode: TouchdownMode = "C_in_contact",
|
|
686
688
|
vertical: bool = False,
|
|
687
689
|
print_call_stats: bool = False,
|
|
688
690
|
) -> SteadyStateResult:
|
|
@@ -710,18 +712,33 @@ class CriteriaEvaluator:
|
|
|
710
712
|
UserWarning,
|
|
711
713
|
)
|
|
712
714
|
system_copy = copy.deepcopy(system)
|
|
715
|
+
# Evaluate touchdown distance for flat slab
|
|
713
716
|
system_copy.toggle_touchdown(True)
|
|
714
|
-
|
|
715
|
-
|
|
717
|
+
segments = [
|
|
718
|
+
Segment(length=5e3, has_foundation=True, m=0.0),
|
|
719
|
+
Segment(length=5e3, has_foundation=False, m=0.0),
|
|
720
|
+
]
|
|
721
|
+
system_copy.update_scenario(
|
|
722
|
+
segments=segments, scenario_config=ScenarioConfig(phi=0.0)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
cut_distance = 0
|
|
726
|
+
match mode:
|
|
727
|
+
case "C_in_contact":
|
|
728
|
+
cut_distance = 2 * system_copy.slab_touchdown.l_BC
|
|
729
|
+
case "B_point_contact":
|
|
730
|
+
cut_distance = system_copy.slab_touchdown.l_BC - 1e-3
|
|
731
|
+
case "A_free_hanging":
|
|
732
|
+
cut_distance = system_copy.slab_touchdown.l_AB - 1e-3
|
|
716
733
|
|
|
717
734
|
segments = [
|
|
718
735
|
Segment(length=5e3, has_foundation=True, m=0.0),
|
|
719
|
-
Segment(length=
|
|
736
|
+
Segment(length=cut_distance, has_foundation=False, m=0.0),
|
|
720
737
|
]
|
|
721
738
|
scenario_config = ScenarioConfig(
|
|
722
739
|
system_type="vpst-" if vertical else "pst-",
|
|
723
740
|
phi=0.0, # Slab Touchdown works only for flat slab
|
|
724
|
-
cut_length=
|
|
741
|
+
cut_length=cut_distance,
|
|
725
742
|
)
|
|
726
743
|
system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
|
|
727
744
|
touchdown_distance = system_copy.slab_touchdown.touchdown_distance
|
|
@@ -94,8 +94,43 @@ def _sigrist_tensile_strength(rho, unit: Literal["kPa", "MPa"] = "kPa"):
|
|
|
94
94
|
return convert[unit] * 240 * (rho / RHO_ICE) ** 2.44
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
def _adam_tensile_strength(rho, unit: Literal["kPa", "MPa"] = "kPa"):
|
|
98
|
+
"""
|
|
99
|
+
Estimate the tensile strength of a slab layer from its density.
|
|
100
|
+
|
|
101
|
+
Uses the density parametrization of Adam (2025).
|
|
102
|
+
|
|
103
|
+
Arguments
|
|
104
|
+
---------
|
|
105
|
+
rho : ndarray, float
|
|
106
|
+
Layer density (kg/m^3).
|
|
107
|
+
unit : str, optional
|
|
108
|
+
Desired output unit of the layer strength. Default is 'kPa'.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
ndarray
|
|
113
|
+
Tensile strength in specified unit.
|
|
114
|
+
"""
|
|
115
|
+
convert = {"kPa": 1e3, "MPa": 1}
|
|
116
|
+
TS_0 = 1.0 # [MPa]
|
|
117
|
+
kappa = 3.45 # [-]
|
|
118
|
+
# Adam's equation is given in MPa
|
|
119
|
+
return TS_0 * (rho / RHO_ICE) ** kappa * convert[unit]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# # TODO: Compressive Strength from Schöttner
|
|
123
|
+
# def _schotter_compressive_strength(rho, unit: Literal["kPa", "MPa"] = "kPa"):
|
|
124
|
+
# """
|
|
125
|
+
# Estimate the compressive strength of a slab layer from its density.
|
|
126
|
+
# On the compressive strength of weak snow layers of depth hoar - Schöttner (2025).
|
|
127
|
+
|
|
128
|
+
# Uses the density parametrization of Schöttner (2025).
|
|
129
|
+
# """
|
|
130
|
+
# convert = {"kPa": 1e3, "MPa": 1}
|
|
131
|
+
# CS_0 = 11.0 # [MPa]
|
|
132
|
+
# CS_1 = 5.4 # [-]
|
|
133
|
+
# return CS_0 * (rho / RHO_ICE) ** CS_1 * convert[unit]
|
|
99
134
|
|
|
100
135
|
|
|
101
136
|
class Layer(BaseModel):
|
|
@@ -114,6 +149,10 @@ class Layer(BaseModel):
|
|
|
114
149
|
Young's modulus E [MPa]. If omitted it is derived from ``rho``.
|
|
115
150
|
G : float, optional
|
|
116
151
|
Shear modulus G [MPa]. If omitted it is derived from ``E`` and ``nu``.
|
|
152
|
+
tensile_strength: float
|
|
153
|
+
Tensile strength [kPa].
|
|
154
|
+
tensile_strength_method: Literal["sigrist", "adam", "hybrid"]
|
|
155
|
+
Method to calculate the tensile strength.
|
|
117
156
|
"""
|
|
118
157
|
|
|
119
158
|
# has to be provided
|
|
@@ -129,8 +168,8 @@ class Layer(BaseModel):
|
|
|
129
168
|
tensile_strength: float = Field(
|
|
130
169
|
default=0.0, ge=0, description="Tensile strength [kPa]"
|
|
131
170
|
)
|
|
132
|
-
tensile_strength_method: Literal["sigrist"] = Field(
|
|
133
|
-
default="
|
|
171
|
+
tensile_strength_method: Literal["sigrist", "adam", "hybrid"] = Field(
|
|
172
|
+
default="hybrid",
|
|
134
173
|
description="Method to calculate the tensile strength",
|
|
135
174
|
)
|
|
136
175
|
E_method: Literal["bergfeld", "scapazzo", "gerling"] = Field(
|
|
@@ -153,17 +192,23 @@ class Layer(BaseModel):
|
|
|
153
192
|
else:
|
|
154
193
|
raise ValueError(f"Invalid E_method: {self.E_method}")
|
|
155
194
|
object.__setattr__(self, "G", self.G or self.E / (2 * (1 + self.nu)))
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
195
|
+
|
|
196
|
+
if not self.tensile_strength:
|
|
197
|
+
if self.tensile_strength_method == "sigrist":
|
|
198
|
+
ts_value = _sigrist_tensile_strength(self.rho, unit="kPa")
|
|
199
|
+
elif self.tensile_strength_method == "adam":
|
|
200
|
+
ts_value = _adam_tensile_strength(self.rho, unit="kPa")
|
|
201
|
+
elif self.tensile_strength_method == "hybrid":
|
|
202
|
+
# Use Sigrist for rho < 250, Adam for rho >= 250
|
|
203
|
+
if self.rho < 250:
|
|
204
|
+
ts_value = _sigrist_tensile_strength(self.rho, unit="kPa")
|
|
205
|
+
else:
|
|
206
|
+
ts_value = _adam_tensile_strength(self.rho, unit="kPa")
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Invalid tensile_strength_method: {self.tensile_strength_method}"
|
|
210
|
+
)
|
|
211
|
+
object.__setattr__(self, "tensile_strength", ts_value)
|
|
167
212
|
|
|
168
213
|
@model_validator(mode="after")
|
|
169
214
|
def validate_positive_E_G(self):
|
|
@@ -20,6 +20,9 @@ from weac.core.unknown_constants_solver import UnknownConstantsSolver
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
TouchdownMode = Literal["A_free_hanging", "B_point_contact", "C_in_contact"]
|
|
24
|
+
|
|
25
|
+
|
|
23
26
|
class SlabTouchdown: # pylint: disable=too-many-instance-attributes,too-few-public-methods
|
|
24
27
|
"""
|
|
25
28
|
Handling the touchdown situation in a PST.
|
|
@@ -56,7 +59,7 @@ class SlabTouchdown: # pylint: disable=too-many-instance-attributes,too-few-pub
|
|
|
56
59
|
Length of the crack for transition of stage A to stage B [mm]
|
|
57
60
|
l_BC : float
|
|
58
61
|
Length of the crack for transition of stage B to stage C [mm]
|
|
59
|
-
touchdown_mode :
|
|
62
|
+
touchdown_mode : TouchdownMode
|
|
60
63
|
Type of touchdown mode
|
|
61
64
|
touchdown_distance : float
|
|
62
65
|
Length of the touchdown segment [mm]
|
|
@@ -74,9 +77,7 @@ class SlabTouchdown: # pylint: disable=too-many-instance-attributes,too-few-pub
|
|
|
74
77
|
straight_scenario: Scenario
|
|
75
78
|
l_AB: float
|
|
76
79
|
l_BC: float
|
|
77
|
-
touchdown_mode:
|
|
78
|
-
"A_free_hanging", "B_point_contact", "C_in_contact"
|
|
79
|
-
] # Three types of contact with collapsed weak layer
|
|
80
|
+
touchdown_mode: TouchdownMode # Three types of contact with collapsed weak layer
|
|
80
81
|
touchdown_distance: float
|
|
81
82
|
collapsed_weak_layer_kR: float | None = None
|
|
82
83
|
|
|
@@ -335,8 +335,7 @@ class SystemModel:
|
|
|
335
335
|
weak_layer=self.weak_layer,
|
|
336
336
|
slab=self.slab,
|
|
337
337
|
)
|
|
338
|
-
|
|
339
|
-
self._invalidate_slab_touchdown()
|
|
338
|
+
self._invalidate_slab_touchdown()
|
|
340
339
|
self._invalidate_constants()
|
|
341
340
|
|
|
342
341
|
def toggle_touchdown(self, touchdown: bool):
|
|
@@ -272,6 +272,46 @@ class TestCriteriaEvaluator(unittest.TestCase):
|
|
|
272
272
|
self.assertIsInstance(new_segments, list)
|
|
273
273
|
self.assertTrue(all(isinstance(s, Segment) for s in new_segments))
|
|
274
274
|
|
|
275
|
+
def test_evaluate_SteadyState_modes(self):
|
|
276
|
+
"""Test evaluate_SteadyState with various modes."""
|
|
277
|
+
test_cases = [
|
|
278
|
+
("C_in_contact", "C_in_contact"),
|
|
279
|
+
("B_point_contact", "B_point_contact"),
|
|
280
|
+
("A_free_hanging", "A_free_hanging"),
|
|
281
|
+
(None, "C_in_contact"), # default mode
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
for mode_param, expected_mode in test_cases:
|
|
285
|
+
with self.subTest(mode=mode_param):
|
|
286
|
+
segments = [
|
|
287
|
+
Segment(length=self.segments_length, has_foundation=True, m=0),
|
|
288
|
+
Segment(length=self.segments_length, has_foundation=True, m=0),
|
|
289
|
+
]
|
|
290
|
+
system = SystemModel(
|
|
291
|
+
model_input=ModelInput(
|
|
292
|
+
layers=self.layers,
|
|
293
|
+
weak_layer=self.weak_layer,
|
|
294
|
+
segments=segments,
|
|
295
|
+
scenario_config=ScenarioConfig(phi=self.phi),
|
|
296
|
+
),
|
|
297
|
+
config=Config(touchdown=True),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if mode_param is None:
|
|
301
|
+
results: SteadyStateResult = self.evaluator.evaluate_SteadyState(
|
|
302
|
+
system
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
results: SteadyStateResult = self.evaluator.evaluate_SteadyState(
|
|
306
|
+
system, mode=mode_param
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
self.assertTrue(results.converged)
|
|
310
|
+
self.assertEqual(
|
|
311
|
+
results.system.slab_touchdown.touchdown_mode,
|
|
312
|
+
expected_mode,
|
|
313
|
+
)
|
|
314
|
+
|
|
275
315
|
|
|
276
316
|
if __name__ == "__main__":
|
|
277
317
|
unittest.main()
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for Layer and WeakLayer components.
|
|
3
|
+
|
|
4
|
+
Tests validation, automatic property calculations, and edge cases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from weac.components.layer import (
|
|
13
|
+
Layer,
|
|
14
|
+
WeakLayer,
|
|
15
|
+
_adam_tensile_strength,
|
|
16
|
+
_bergfeld_youngs_modulus,
|
|
17
|
+
_gerling_youngs_modulus,
|
|
18
|
+
_scapozza_youngs_modulus,
|
|
19
|
+
_sigrist_tensile_strength,
|
|
20
|
+
)
|
|
21
|
+
from weac.constants import NU
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestLayerPropertyCalculations(unittest.TestCase):
|
|
25
|
+
"""Test the layer property calculation functions."""
|
|
26
|
+
|
|
27
|
+
def test_bergfeld_calculation(self):
|
|
28
|
+
"""Test Bergfeld Young's modulus calculation."""
|
|
29
|
+
# Test with standard ice density
|
|
30
|
+
E = _bergfeld_youngs_modulus(rho=917.0) # Ice density
|
|
31
|
+
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
32
|
+
self.assertTrue(np.isscalar(E), "Result should be a scalar")
|
|
33
|
+
|
|
34
|
+
# Test with typical snow densities
|
|
35
|
+
E_light = _bergfeld_youngs_modulus(rho=100.0)
|
|
36
|
+
E_heavy = _bergfeld_youngs_modulus(rho=400.0)
|
|
37
|
+
self.assertLess(E_light, E_heavy, "Heavier snow should have higher modulus")
|
|
38
|
+
|
|
39
|
+
def test_scapozza_calculation(self):
|
|
40
|
+
"""Test Scapozza Young's modulus calculation."""
|
|
41
|
+
E = _scapozza_youngs_modulus(rho=200.0)
|
|
42
|
+
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
43
|
+
|
|
44
|
+
def test_gerling_calculation(self):
|
|
45
|
+
"""Test Gerling Young's modulus calculation."""
|
|
46
|
+
E = _gerling_youngs_modulus(rho=250.0)
|
|
47
|
+
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestTensileStrengthCalculations(unittest.TestCase):
|
|
51
|
+
"""Test tensile strength calculation functions."""
|
|
52
|
+
|
|
53
|
+
def test_sigrist_calculation_kPa(self):
|
|
54
|
+
"""Test Sigrist tensile strength calculation in kPa."""
|
|
55
|
+
# Test with typical snow density
|
|
56
|
+
ts = _sigrist_tensile_strength(rho=200.0, unit="kPa")
|
|
57
|
+
self.assertGreater(ts, 0, "Tensile strength should be positive")
|
|
58
|
+
self.assertTrue(np.isscalar(ts), "Result should be a scalar")
|
|
59
|
+
|
|
60
|
+
# Test with different densities
|
|
61
|
+
ts_light = _sigrist_tensile_strength(rho=100.0, unit="kPa")
|
|
62
|
+
ts_heavy = _sigrist_tensile_strength(rho=400.0, unit="kPa")
|
|
63
|
+
self.assertLess(ts_light, ts_heavy, "Heavier snow should have higher strength")
|
|
64
|
+
|
|
65
|
+
def test_sigrist_calculation_MPa(self):
|
|
66
|
+
"""Test Sigrist tensile strength calculation in MPa."""
|
|
67
|
+
ts_kPa = _sigrist_tensile_strength(rho=200.0, unit="kPa")
|
|
68
|
+
ts_MPa = _sigrist_tensile_strength(rho=200.0, unit="MPa")
|
|
69
|
+
self.assertAlmostEqual(
|
|
70
|
+
ts_kPa, ts_MPa * 1000, places=5, msg="Unit conversion should be correct"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_adam_calculation_kPa(self):
|
|
74
|
+
"""Test Adam tensile strength calculation in kPa."""
|
|
75
|
+
# Test with typical snow density
|
|
76
|
+
ts = _adam_tensile_strength(rho=300.0, unit="kPa")
|
|
77
|
+
self.assertGreater(ts, 0, "Tensile strength should be positive")
|
|
78
|
+
self.assertTrue(np.isscalar(ts), "Result should be a scalar")
|
|
79
|
+
|
|
80
|
+
# Test with different densities
|
|
81
|
+
ts_light = _adam_tensile_strength(rho=150.0, unit="kPa")
|
|
82
|
+
ts_heavy = _adam_tensile_strength(rho=450.0, unit="kPa")
|
|
83
|
+
self.assertLess(ts_light, ts_heavy, "Heavier snow should have higher strength")
|
|
84
|
+
|
|
85
|
+
def test_adam_calculation_MPa(self):
|
|
86
|
+
"""Test Adam tensile strength calculation in MPa."""
|
|
87
|
+
ts_kPa = _adam_tensile_strength(rho=300.0, unit="kPa")
|
|
88
|
+
ts_MPa = _adam_tensile_strength(rho=300.0, unit="MPa")
|
|
89
|
+
self.assertAlmostEqual(
|
|
90
|
+
ts_kPa, ts_MPa * 1000, places=5, msg="Unit conversion should be correct"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_sigrist_vs_adam_comparison(self):
|
|
94
|
+
"""Compare Sigrist and Adam formulations at different densities."""
|
|
95
|
+
# At low densities, compare the formulations
|
|
96
|
+
rho_low = 150.0
|
|
97
|
+
ts_sigrist = _sigrist_tensile_strength(rho=rho_low, unit="kPa")
|
|
98
|
+
ts_adam = _adam_tensile_strength(rho=rho_low, unit="kPa")
|
|
99
|
+
# Both should give positive values
|
|
100
|
+
self.assertGreater(ts_sigrist, 0)
|
|
101
|
+
self.assertGreater(ts_adam, 0)
|
|
102
|
+
|
|
103
|
+
# At high densities
|
|
104
|
+
rho_high = 400.0
|
|
105
|
+
ts_sigrist_high = _sigrist_tensile_strength(rho=rho_high, unit="kPa")
|
|
106
|
+
ts_adam_high = _adam_tensile_strength(rho=rho_high, unit="kPa")
|
|
107
|
+
self.assertGreater(ts_sigrist_high, 0)
|
|
108
|
+
self.assertGreater(ts_adam_high, 0)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestLayerTensileStrength(unittest.TestCase):
|
|
112
|
+
"""Test Layer class tensile strength functionality."""
|
|
113
|
+
|
|
114
|
+
def test_layer_default_tensile_strength_method(self):
|
|
115
|
+
"""Test that default method is 'hybrid'."""
|
|
116
|
+
layer = Layer(rho=200.0, h=100.0)
|
|
117
|
+
self.assertEqual(
|
|
118
|
+
layer.tensile_strength_method,
|
|
119
|
+
"hybrid",
|
|
120
|
+
"Default method should be 'hybrid'",
|
|
121
|
+
)
|
|
122
|
+
self.assertGreater(
|
|
123
|
+
layer.tensile_strength, 0, "Tensile strength should be calculated"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_layer_sigrist_method(self):
|
|
127
|
+
"""Test Layer with explicit Sigrist method."""
|
|
128
|
+
layer = Layer(rho=200.0, h=100.0, tensile_strength_method="sigrist")
|
|
129
|
+
expected_ts = _sigrist_tensile_strength(rho=200.0, unit="kPa")
|
|
130
|
+
self.assertAlmostEqual(
|
|
131
|
+
layer.tensile_strength,
|
|
132
|
+
expected_ts,
|
|
133
|
+
places=5,
|
|
134
|
+
msg="Tensile strength should match Sigrist calculation",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def test_layer_adam_method(self):
|
|
138
|
+
"""Test Layer with explicit Adam method."""
|
|
139
|
+
layer = Layer(rho=300.0, h=100.0, tensile_strength_method="adam")
|
|
140
|
+
expected_ts = _adam_tensile_strength(rho=300.0, unit="kPa")
|
|
141
|
+
self.assertAlmostEqual(
|
|
142
|
+
layer.tensile_strength,
|
|
143
|
+
expected_ts,
|
|
144
|
+
places=5,
|
|
145
|
+
msg="Tensile strength should match Adam calculation",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_layer_hybrid_method_low_density(self):
|
|
149
|
+
"""Test hybrid method uses Sigrist for density < 250."""
|
|
150
|
+
rho = 200.0 # Below 250 threshold
|
|
151
|
+
layer = Layer(rho=rho, h=100.0, tensile_strength_method="hybrid")
|
|
152
|
+
expected_ts = _sigrist_tensile_strength(rho=rho, unit="kPa")
|
|
153
|
+
self.assertAlmostEqual(
|
|
154
|
+
layer.tensile_strength,
|
|
155
|
+
expected_ts,
|
|
156
|
+
places=5,
|
|
157
|
+
msg="Hybrid should use Sigrist for rho < 250",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def test_layer_hybrid_method_high_density(self):
|
|
161
|
+
"""Test hybrid method uses Adam for density >= 250."""
|
|
162
|
+
rho = 300.0 # Above 250 threshold
|
|
163
|
+
layer = Layer(rho=rho, h=100.0, tensile_strength_method="hybrid")
|
|
164
|
+
expected_ts = _adam_tensile_strength(rho=rho, unit="kPa")
|
|
165
|
+
self.assertAlmostEqual(
|
|
166
|
+
layer.tensile_strength,
|
|
167
|
+
expected_ts,
|
|
168
|
+
places=5,
|
|
169
|
+
msg="Hybrid should use Adam for rho >= 250",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def test_layer_hybrid_method_at_threshold(self):
|
|
173
|
+
"""Test hybrid method behavior exactly at 250 kg/m³."""
|
|
174
|
+
rho = 250.0 # Exactly at threshold
|
|
175
|
+
layer = Layer(rho=rho, h=100.0, tensile_strength_method="hybrid")
|
|
176
|
+
expected_ts = _adam_tensile_strength(rho=rho, unit="kPa")
|
|
177
|
+
self.assertAlmostEqual(
|
|
178
|
+
layer.tensile_strength,
|
|
179
|
+
expected_ts,
|
|
180
|
+
places=5,
|
|
181
|
+
msg="Hybrid should use Adam for rho = 250",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def test_layer_custom_tensile_strength(self):
|
|
185
|
+
"""Test that custom tensile strength overrides calculation."""
|
|
186
|
+
custom_ts = 50.0
|
|
187
|
+
layer = Layer(
|
|
188
|
+
rho=200.0,
|
|
189
|
+
h=100.0,
|
|
190
|
+
tensile_strength=custom_ts,
|
|
191
|
+
tensile_strength_method="sigrist",
|
|
192
|
+
)
|
|
193
|
+
self.assertEqual(
|
|
194
|
+
layer.tensile_strength,
|
|
195
|
+
custom_ts,
|
|
196
|
+
"Custom tensile strength should override calculation",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestTensileStrengthPhysicalConsistency(unittest.TestCase):
|
|
201
|
+
"""Test physical consistency of tensile strength calculations."""
|
|
202
|
+
|
|
203
|
+
def test_density_strength_relationship(self):
|
|
204
|
+
"""Test that higher density leads to higher tensile strength."""
|
|
205
|
+
layer_light = Layer(rho=150.0, h=100.0)
|
|
206
|
+
layer_heavy = Layer(rho=350.0, h=100.0)
|
|
207
|
+
|
|
208
|
+
self.assertLess(
|
|
209
|
+
layer_light.tensile_strength,
|
|
210
|
+
layer_heavy.tensile_strength,
|
|
211
|
+
"Heavier snow should have higher tensile strength",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def test_hybrid_continuity_around_threshold(self):
|
|
215
|
+
"""Test continuity of hybrid method around 250 kg/m³ threshold."""
|
|
216
|
+
# Test just below threshold
|
|
217
|
+
layer_below = Layer(rho=249.0, h=100.0, tensile_strength_method="hybrid")
|
|
218
|
+
# Test just above threshold
|
|
219
|
+
layer_above = Layer(rho=251.0, h=100.0, tensile_strength_method="hybrid")
|
|
220
|
+
|
|
221
|
+
# Both should have positive strength
|
|
222
|
+
self.assertGreater(layer_below.tensile_strength, 0)
|
|
223
|
+
self.assertGreater(layer_above.tensile_strength, 0)
|
|
224
|
+
|
|
225
|
+
# Values should be reasonably close (within an order of magnitude)
|
|
226
|
+
# This is a loose check since the formulations differ
|
|
227
|
+
ratio = layer_above.tensile_strength / layer_below.tensile_strength
|
|
228
|
+
self.assertLess(
|
|
229
|
+
ratio, 10.0, "Strength shouldn't jump by more than 10x at threshold"
|
|
230
|
+
)
|
|
231
|
+
self.assertGreater(
|
|
232
|
+
ratio, 0.1, "Strength shouldn't drop by more than 10x at threshold"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def test_all_methods_give_positive_strength(self):
|
|
236
|
+
"""Test that all methods produce positive tensile strength."""
|
|
237
|
+
rho_values = [100.0, 200.0, 300.0, 400.0]
|
|
238
|
+
methods = ["sigrist", "adam", "hybrid"]
|
|
239
|
+
|
|
240
|
+
for rho in rho_values:
|
|
241
|
+
for method in methods:
|
|
242
|
+
layer = Layer(rho=rho, h=100.0, tensile_strength_method=method)
|
|
243
|
+
self.assertGreater(
|
|
244
|
+
layer.tensile_strength,
|
|
245
|
+
0,
|
|
246
|
+
f"Method {method} with rho={rho} should give positive strength",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def test_tensile_strength_density_monotonicity(self):
|
|
250
|
+
"""Test that tensile strength increases monotonically with density."""
|
|
251
|
+
densities = [100.0, 150.0, 200.0, 250.0, 300.0, 350.0, 400.0]
|
|
252
|
+
methods = ["sigrist", "adam", "hybrid"]
|
|
253
|
+
|
|
254
|
+
for method in methods:
|
|
255
|
+
strengths = [
|
|
256
|
+
Layer(rho=rho, h=100.0, tensile_strength_method=method).tensile_strength
|
|
257
|
+
for rho in densities
|
|
258
|
+
]
|
|
259
|
+
# Check that each strength is greater than the previous
|
|
260
|
+
for i in range(1, len(strengths)):
|
|
261
|
+
self.assertGreater(
|
|
262
|
+
strengths[i],
|
|
263
|
+
strengths[i - 1],
|
|
264
|
+
f"Strength should increase with density for {method} method",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TestLayer(unittest.TestCase):
|
|
269
|
+
"""Test the Layer class functionality."""
|
|
270
|
+
|
|
271
|
+
def test_layer_creation_with_required_fields(self):
|
|
272
|
+
"""Test creating a layer with only required fields."""
|
|
273
|
+
layer = Layer(rho=200.0, h=100.0)
|
|
274
|
+
|
|
275
|
+
# Check required fields
|
|
276
|
+
self.assertEqual(layer.rho, 200.0)
|
|
277
|
+
self.assertEqual(layer.h, 100.0)
|
|
278
|
+
|
|
279
|
+
# Check auto-calculated fields
|
|
280
|
+
self.assertIsNotNone(layer.E, "Young's modulus should be auto-calculated")
|
|
281
|
+
self.assertIsNotNone(layer.G, "Shear modulus should be auto-calculated")
|
|
282
|
+
self.assertGreater(layer.E, 0, "Young's modulus should be positive")
|
|
283
|
+
self.assertGreater(layer.G, 0, "Shear modulus should be positive")
|
|
284
|
+
|
|
285
|
+
# Check default Poisson's ratio
|
|
286
|
+
self.assertEqual(layer.nu, NU, "Default Poisson's ratio should be 0.25")
|
|
287
|
+
|
|
288
|
+
def test_layer_creation_with_all_fields(self):
|
|
289
|
+
"""Test creating a layer with all fields specified."""
|
|
290
|
+
layer = Layer(rho=250.0, h=150.0, nu=0.3, E=50.0, G=20.0)
|
|
291
|
+
|
|
292
|
+
self.assertEqual(layer.rho, 250.0)
|
|
293
|
+
self.assertEqual(layer.h, 150.0)
|
|
294
|
+
self.assertEqual(layer.nu, 0.3)
|
|
295
|
+
self.assertEqual(layer.E, 50.0, "Specified E should override auto-calculation")
|
|
296
|
+
self.assertEqual(layer.G, 20.0, "Specified G should override auto-calculation")
|
|
297
|
+
|
|
298
|
+
def test_layer_validation_errors(self):
|
|
299
|
+
"""Test that invalid layer parameters raise ValidationError."""
|
|
300
|
+
# Negative density
|
|
301
|
+
with self.assertRaises(ValidationError):
|
|
302
|
+
Layer(rho=-100.0, h=100.0)
|
|
303
|
+
|
|
304
|
+
# Zero thickness
|
|
305
|
+
with self.assertRaises(ValidationError):
|
|
306
|
+
Layer(rho=200.0, h=0.0)
|
|
307
|
+
|
|
308
|
+
# Invalid Poisson's ratio (>= 0.5)
|
|
309
|
+
with self.assertRaises(ValidationError):
|
|
310
|
+
Layer(rho=200.0, h=100.0, nu=0.5)
|
|
311
|
+
|
|
312
|
+
# Negative Young's modulus
|
|
313
|
+
with self.assertRaises(ValidationError):
|
|
314
|
+
Layer(rho=200.0, h=100.0, E=-10.0)
|
|
315
|
+
|
|
316
|
+
def test_shear_modulus_calculation(self):
|
|
317
|
+
"""Test automatic shear modulus calculation from E and nu."""
|
|
318
|
+
layer = Layer(rho=200.0, h=100.0, nu=0.25, E=100.0)
|
|
319
|
+
|
|
320
|
+
# G = E / (2 * (1 + nu))
|
|
321
|
+
expected_G = 100.0 / (2 * (1 + 0.25))
|
|
322
|
+
self.assertAlmostEqual(layer.G, expected_G, places=5)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TestWeakLayer(unittest.TestCase):
|
|
326
|
+
"""Test the WeakLayer class functionality."""
|
|
327
|
+
|
|
328
|
+
def test_weak_layer_creation_minimal(self):
|
|
329
|
+
"""Test creating a weak layer with minimal required fields."""
|
|
330
|
+
wl = WeakLayer(rho=50.0, h=10.0)
|
|
331
|
+
|
|
332
|
+
# Check required fields
|
|
333
|
+
self.assertEqual(wl.rho, 50.0)
|
|
334
|
+
self.assertEqual(wl.h, 10.0)
|
|
335
|
+
|
|
336
|
+
# Check auto-calculated fields
|
|
337
|
+
self.assertIsNotNone(wl.E, "Young's modulus should be auto-calculated")
|
|
338
|
+
self.assertIsNotNone(wl.G, "Shear modulus should be auto-calculated")
|
|
339
|
+
self.assertIsNotNone(wl.kn, "Normal stiffness should be auto-calculated")
|
|
340
|
+
self.assertIsNotNone(wl.kt, "Shear stiffness should be auto-calculated")
|
|
341
|
+
self.assertGreater(wl.E, 0, "Young's modulus should be positive")
|
|
342
|
+
self.assertGreater(wl.G, 0, "Shear modulus should be positive")
|
|
343
|
+
self.assertGreater(wl.kn, 0, "Normal stiffness should be positive")
|
|
344
|
+
self.assertGreater(wl.kt, 0, "Shear stiffness should be positive")
|
|
345
|
+
|
|
346
|
+
# Check default fracture properties
|
|
347
|
+
self.assertEqual(wl.G_c, 1.0)
|
|
348
|
+
self.assertEqual(wl.G_Ic, 0.56)
|
|
349
|
+
self.assertEqual(wl.G_IIc, 0.79)
|
|
350
|
+
|
|
351
|
+
def test_weak_layer_stiffness_calculations(self):
|
|
352
|
+
"""Test weak layer stiffness calculations."""
|
|
353
|
+
wl = WeakLayer(rho=100.0, h=20.0, E=10.0, nu=0.2)
|
|
354
|
+
|
|
355
|
+
# kn = E_plane / h = E / (1 - nu²) / h
|
|
356
|
+
E_plane = 10.0 / (1 - 0.2**2)
|
|
357
|
+
expected_kn = E_plane / 20.0
|
|
358
|
+
self.assertAlmostEqual(wl.kn, expected_kn, places=5)
|
|
359
|
+
|
|
360
|
+
# kt = G / h
|
|
361
|
+
expected_G = 10.0 / (2 * (1 + 0.2))
|
|
362
|
+
expected_kt = expected_G / 20.0
|
|
363
|
+
self.assertAlmostEqual(wl.kt, expected_kt, places=5)
|
|
364
|
+
|
|
365
|
+
def test_weak_layer_custom_stiffnesses(self):
|
|
366
|
+
"""Test weak layer with custom stiffness values."""
|
|
367
|
+
wl = WeakLayer(rho=80.0, h=15.0, kn=5.0, kt=3.0)
|
|
368
|
+
|
|
369
|
+
self.assertEqual(wl.kn, 5.0, "Custom kn should override calculation")
|
|
370
|
+
self.assertEqual(wl.kt, 3.0, "Custom kt should override calculation")
|
|
371
|
+
|
|
372
|
+
def test_weak_layer_fracture_properties(self):
|
|
373
|
+
"""Test weak layer fracture property validation."""
|
|
374
|
+
wl = WeakLayer(rho=90.0, h=25.0, G_c=2.5, G_Ic=1.5, G_IIc=1.8)
|
|
375
|
+
|
|
376
|
+
self.assertEqual(wl.G_c, 2.5)
|
|
377
|
+
self.assertEqual(wl.G_Ic, 1.5)
|
|
378
|
+
self.assertEqual(wl.G_IIc, 1.8)
|
|
379
|
+
|
|
380
|
+
def test_weak_layer_validation_errors(self):
|
|
381
|
+
"""Test weak layer validation errors."""
|
|
382
|
+
# Negative fracture energy
|
|
383
|
+
with self.assertRaises(ValidationError):
|
|
384
|
+
WeakLayer(rho=100.0, h=20.0, G_c=-1.0)
|
|
385
|
+
|
|
386
|
+
# Zero thickness
|
|
387
|
+
with self.assertRaises(ValidationError):
|
|
388
|
+
WeakLayer(rho=100.0, h=0.0)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TestLayerPhysicalConsistency(unittest.TestCase):
|
|
392
|
+
"""Test physical consistency of layer calculations."""
|
|
393
|
+
|
|
394
|
+
def test_layer_density_modulus_relationship(self):
|
|
395
|
+
"""Test that higher density leads to higher modulus."""
|
|
396
|
+
layer_light = Layer(rho=150.0, h=100.0)
|
|
397
|
+
layer_heavy = Layer(rho=350.0, h=100.0)
|
|
398
|
+
|
|
399
|
+
self.assertLess(
|
|
400
|
+
layer_light.E,
|
|
401
|
+
layer_heavy.E,
|
|
402
|
+
"Heavier snow should have higher Young's modulus",
|
|
403
|
+
)
|
|
404
|
+
self.assertLess(
|
|
405
|
+
layer_light.G,
|
|
406
|
+
layer_heavy.G,
|
|
407
|
+
"Heavier snow should have higher shear modulus",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def test_weak_layer_thickness_stiffness_relationship(self):
|
|
411
|
+
"""Test that thicker weak layers have lower stiffness."""
|
|
412
|
+
wl_thin = WeakLayer(rho=100.0, h=10.0)
|
|
413
|
+
wl_thick = WeakLayer(rho=100.0, h=30.0)
|
|
414
|
+
|
|
415
|
+
self.assertGreater(
|
|
416
|
+
wl_thin.kn,
|
|
417
|
+
wl_thick.kn,
|
|
418
|
+
"Thinner weak layer should have higher normal stiffness",
|
|
419
|
+
)
|
|
420
|
+
self.assertGreater(
|
|
421
|
+
wl_thin.kt,
|
|
422
|
+
wl_thick.kt,
|
|
423
|
+
"Thinner weak layer should have higher shear stiffness",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def test_poisson_ratio_bounds(self):
|
|
427
|
+
"""Test Poisson's ratio physical bounds."""
|
|
428
|
+
# Test upper bound (must be < 0.5 for positive definite stiffness)
|
|
429
|
+
with self.assertRaises(ValidationError):
|
|
430
|
+
Layer(rho=200.0, h=100.0, nu=0.5)
|
|
431
|
+
|
|
432
|
+
with self.assertRaises(ValidationError):
|
|
433
|
+
Layer(rho=200.0, h=100.0, nu=0.6)
|
|
434
|
+
|
|
435
|
+
# Test lower bound (must be >= 0)
|
|
436
|
+
with self.assertRaises(ValidationError):
|
|
437
|
+
Layer(rho=200.0, h=100.0, nu=-0.1)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
unittest.main(verbosity=2)
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for Layer and WeakLayer components.
|
|
3
|
-
|
|
4
|
-
Tests validation, automatic property calculations, and edge cases.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import unittest
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
from pydantic import ValidationError
|
|
11
|
-
|
|
12
|
-
from weac.components.layer import (
|
|
13
|
-
Layer,
|
|
14
|
-
WeakLayer,
|
|
15
|
-
_bergfeld_youngs_modulus,
|
|
16
|
-
_gerling_youngs_modulus,
|
|
17
|
-
_scapozza_youngs_modulus,
|
|
18
|
-
)
|
|
19
|
-
from weac.constants import NU
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class TestLayerPropertyCalculations(unittest.TestCase):
|
|
23
|
-
"""Test the layer property calculation functions."""
|
|
24
|
-
|
|
25
|
-
def test_bergfeld_calculation(self):
|
|
26
|
-
"""Test Bergfeld Young's modulus calculation."""
|
|
27
|
-
# Test with standard ice density
|
|
28
|
-
E = _bergfeld_youngs_modulus(rho=917.0) # Ice density
|
|
29
|
-
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
30
|
-
self.assertTrue(np.isscalar(E), "Result should be a scalar")
|
|
31
|
-
|
|
32
|
-
# Test with typical snow densities
|
|
33
|
-
E_light = _bergfeld_youngs_modulus(rho=100.0)
|
|
34
|
-
E_heavy = _bergfeld_youngs_modulus(rho=400.0)
|
|
35
|
-
self.assertLess(E_light, E_heavy, "Heavier snow should have higher modulus")
|
|
36
|
-
|
|
37
|
-
def test_scapozza_calculation(self):
|
|
38
|
-
"""Test Scapozza Young's modulus calculation."""
|
|
39
|
-
E = _scapozza_youngs_modulus(rho=200.0)
|
|
40
|
-
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
41
|
-
|
|
42
|
-
def test_gerling_calculation(self):
|
|
43
|
-
"""Test Gerling Young's modulus calculation."""
|
|
44
|
-
E = _gerling_youngs_modulus(rho=250.0)
|
|
45
|
-
self.assertGreater(E, 0, "Young's modulus should be positive")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class TestLayer(unittest.TestCase):
|
|
49
|
-
"""Test the Layer class functionality."""
|
|
50
|
-
|
|
51
|
-
def test_layer_creation_with_required_fields(self):
|
|
52
|
-
"""Test creating a layer with only required fields."""
|
|
53
|
-
layer = Layer(rho=200.0, h=100.0)
|
|
54
|
-
|
|
55
|
-
# Check required fields
|
|
56
|
-
self.assertEqual(layer.rho, 200.0)
|
|
57
|
-
self.assertEqual(layer.h, 100.0)
|
|
58
|
-
|
|
59
|
-
# Check auto-calculated fields
|
|
60
|
-
self.assertIsNotNone(layer.E, "Young's modulus should be auto-calculated")
|
|
61
|
-
self.assertIsNotNone(layer.G, "Shear modulus should be auto-calculated")
|
|
62
|
-
self.assertGreater(layer.E, 0, "Young's modulus should be positive")
|
|
63
|
-
self.assertGreater(layer.G, 0, "Shear modulus should be positive")
|
|
64
|
-
|
|
65
|
-
# Check default Poisson's ratio
|
|
66
|
-
self.assertEqual(layer.nu, NU, "Default Poisson's ratio should be 0.25")
|
|
67
|
-
|
|
68
|
-
def test_layer_creation_with_all_fields(self):
|
|
69
|
-
"""Test creating a layer with all fields specified."""
|
|
70
|
-
layer = Layer(rho=250.0, h=150.0, nu=0.3, E=50.0, G=20.0)
|
|
71
|
-
|
|
72
|
-
self.assertEqual(layer.rho, 250.0)
|
|
73
|
-
self.assertEqual(layer.h, 150.0)
|
|
74
|
-
self.assertEqual(layer.nu, 0.3)
|
|
75
|
-
self.assertEqual(layer.E, 50.0, "Specified E should override auto-calculation")
|
|
76
|
-
self.assertEqual(layer.G, 20.0, "Specified G should override auto-calculation")
|
|
77
|
-
|
|
78
|
-
def test_layer_validation_errors(self):
|
|
79
|
-
"""Test that invalid layer parameters raise ValidationError."""
|
|
80
|
-
# Negative density
|
|
81
|
-
with self.assertRaises(ValidationError):
|
|
82
|
-
Layer(rho=-100.0, h=100.0)
|
|
83
|
-
|
|
84
|
-
# Zero thickness
|
|
85
|
-
with self.assertRaises(ValidationError):
|
|
86
|
-
Layer(rho=200.0, h=0.0)
|
|
87
|
-
|
|
88
|
-
# Invalid Poisson's ratio (>= 0.5)
|
|
89
|
-
with self.assertRaises(ValidationError):
|
|
90
|
-
Layer(rho=200.0, h=100.0, nu=0.5)
|
|
91
|
-
|
|
92
|
-
# Negative Young's modulus
|
|
93
|
-
with self.assertRaises(ValidationError):
|
|
94
|
-
Layer(rho=200.0, h=100.0, E=-10.0)
|
|
95
|
-
|
|
96
|
-
def test_shear_modulus_calculation(self):
|
|
97
|
-
"""Test automatic shear modulus calculation from E and nu."""
|
|
98
|
-
layer = Layer(rho=200.0, h=100.0, nu=0.25, E=100.0)
|
|
99
|
-
|
|
100
|
-
# G = E / (2 * (1 + nu))
|
|
101
|
-
expected_G = 100.0 / (2 * (1 + 0.25))
|
|
102
|
-
self.assertAlmostEqual(layer.G, expected_G, places=5)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class TestWeakLayer(unittest.TestCase):
|
|
106
|
-
"""Test the WeakLayer class functionality."""
|
|
107
|
-
|
|
108
|
-
def test_weak_layer_creation_minimal(self):
|
|
109
|
-
"""Test creating a weak layer with minimal required fields."""
|
|
110
|
-
wl = WeakLayer(rho=50.0, h=10.0)
|
|
111
|
-
|
|
112
|
-
# Check required fields
|
|
113
|
-
self.assertEqual(wl.rho, 50.0)
|
|
114
|
-
self.assertEqual(wl.h, 10.0)
|
|
115
|
-
|
|
116
|
-
# Check auto-calculated fields
|
|
117
|
-
self.assertIsNotNone(wl.E, "Young's modulus should be auto-calculated")
|
|
118
|
-
self.assertIsNotNone(wl.G, "Shear modulus should be auto-calculated")
|
|
119
|
-
self.assertIsNotNone(wl.kn, "Normal stiffness should be auto-calculated")
|
|
120
|
-
self.assertIsNotNone(wl.kt, "Shear stiffness should be auto-calculated")
|
|
121
|
-
self.assertGreater(wl.E, 0, "Young's modulus should be positive")
|
|
122
|
-
self.assertGreater(wl.G, 0, "Shear modulus should be positive")
|
|
123
|
-
self.assertGreater(wl.kn, 0, "Normal stiffness should be positive")
|
|
124
|
-
self.assertGreater(wl.kt, 0, "Shear stiffness should be positive")
|
|
125
|
-
|
|
126
|
-
# Check default fracture properties
|
|
127
|
-
self.assertEqual(wl.G_c, 1.0)
|
|
128
|
-
self.assertEqual(wl.G_Ic, 0.56)
|
|
129
|
-
self.assertEqual(wl.G_IIc, 0.79)
|
|
130
|
-
|
|
131
|
-
def test_weak_layer_stiffness_calculations(self):
|
|
132
|
-
"""Test weak layer stiffness calculations."""
|
|
133
|
-
wl = WeakLayer(rho=100.0, h=20.0, E=10.0, nu=0.2)
|
|
134
|
-
|
|
135
|
-
# kn = E_plane / h = E / (1 - nu²) / h
|
|
136
|
-
E_plane = 10.0 / (1 - 0.2**2)
|
|
137
|
-
expected_kn = E_plane / 20.0
|
|
138
|
-
self.assertAlmostEqual(wl.kn, expected_kn, places=5)
|
|
139
|
-
|
|
140
|
-
# kt = G / h
|
|
141
|
-
expected_G = 10.0 / (2 * (1 + 0.2))
|
|
142
|
-
expected_kt = expected_G / 20.0
|
|
143
|
-
self.assertAlmostEqual(wl.kt, expected_kt, places=5)
|
|
144
|
-
|
|
145
|
-
def test_weak_layer_custom_stiffnesses(self):
|
|
146
|
-
"""Test weak layer with custom stiffness values."""
|
|
147
|
-
wl = WeakLayer(rho=80.0, h=15.0, kn=5.0, kt=3.0)
|
|
148
|
-
|
|
149
|
-
self.assertEqual(wl.kn, 5.0, "Custom kn should override calculation")
|
|
150
|
-
self.assertEqual(wl.kt, 3.0, "Custom kt should override calculation")
|
|
151
|
-
|
|
152
|
-
def test_weak_layer_fracture_properties(self):
|
|
153
|
-
"""Test weak layer fracture property validation."""
|
|
154
|
-
wl = WeakLayer(rho=90.0, h=25.0, G_c=2.5, G_Ic=1.5, G_IIc=1.8)
|
|
155
|
-
|
|
156
|
-
self.assertEqual(wl.G_c, 2.5)
|
|
157
|
-
self.assertEqual(wl.G_Ic, 1.5)
|
|
158
|
-
self.assertEqual(wl.G_IIc, 1.8)
|
|
159
|
-
|
|
160
|
-
def test_weak_layer_validation_errors(self):
|
|
161
|
-
"""Test weak layer validation errors."""
|
|
162
|
-
# Negative fracture energy
|
|
163
|
-
with self.assertRaises(ValidationError):
|
|
164
|
-
WeakLayer(rho=100.0, h=20.0, G_c=-1.0)
|
|
165
|
-
|
|
166
|
-
# Zero thickness
|
|
167
|
-
with self.assertRaises(ValidationError):
|
|
168
|
-
WeakLayer(rho=100.0, h=0.0)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
class TestLayerPhysicalConsistency(unittest.TestCase):
|
|
172
|
-
"""Test physical consistency of layer calculations."""
|
|
173
|
-
|
|
174
|
-
def test_layer_density_modulus_relationship(self):
|
|
175
|
-
"""Test that higher density leads to higher modulus."""
|
|
176
|
-
layer_light = Layer(rho=150.0, h=100.0)
|
|
177
|
-
layer_heavy = Layer(rho=350.0, h=100.0)
|
|
178
|
-
|
|
179
|
-
self.assertLess(
|
|
180
|
-
layer_light.E,
|
|
181
|
-
layer_heavy.E,
|
|
182
|
-
"Heavier snow should have higher Young's modulus",
|
|
183
|
-
)
|
|
184
|
-
self.assertLess(
|
|
185
|
-
layer_light.G,
|
|
186
|
-
layer_heavy.G,
|
|
187
|
-
"Heavier snow should have higher shear modulus",
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
def test_weak_layer_thickness_stiffness_relationship(self):
|
|
191
|
-
"""Test that thicker weak layers have lower stiffness."""
|
|
192
|
-
wl_thin = WeakLayer(rho=100.0, h=10.0)
|
|
193
|
-
wl_thick = WeakLayer(rho=100.0, h=30.0)
|
|
194
|
-
|
|
195
|
-
self.assertGreater(
|
|
196
|
-
wl_thin.kn,
|
|
197
|
-
wl_thick.kn,
|
|
198
|
-
"Thinner weak layer should have higher normal stiffness",
|
|
199
|
-
)
|
|
200
|
-
self.assertGreater(
|
|
201
|
-
wl_thin.kt,
|
|
202
|
-
wl_thick.kt,
|
|
203
|
-
"Thinner weak layer should have higher shear stiffness",
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
def test_poisson_ratio_bounds(self):
|
|
207
|
-
"""Test Poisson's ratio physical bounds."""
|
|
208
|
-
# Test upper bound (must be < 0.5 for positive definite stiffness)
|
|
209
|
-
with self.assertRaises(ValidationError):
|
|
210
|
-
Layer(rho=200.0, h=100.0, nu=0.5)
|
|
211
|
-
|
|
212
|
-
with self.assertRaises(ValidationError):
|
|
213
|
-
Layer(rho=200.0, h=100.0, nu=0.6)
|
|
214
|
-
|
|
215
|
-
# Test lower bound (must be >= 0)
|
|
216
|
-
with self.assertRaises(ValidationError):
|
|
217
|
-
Layer(rho=200.0, h=100.0, nu=-0.1)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if __name__ == "__main__":
|
|
221
|
-
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|