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.
Files changed (69) hide show
  1. {weac-3.1.1 → weac-3.1.2}/CITATION.cff +1 -1
  2. {weac-3.1.1/src/weac.egg-info → weac-3.1.2}/PKG-INFO +1 -1
  3. {weac-3.1.1 → weac-3.1.2}/pyproject.toml +2 -2
  4. {weac-3.1.1 → weac-3.1.2}/src/weac/__init__.py +1 -1
  5. {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/criteria_evaluator.py +21 -4
  6. {weac-3.1.1 → weac-3.1.2}/src/weac/components/layer.py +60 -15
  7. {weac-3.1.1 → weac-3.1.2}/src/weac/core/slab_touchdown.py +5 -4
  8. {weac-3.1.1 → weac-3.1.2}/src/weac/core/system_model.py +1 -2
  9. {weac-3.1.1 → weac-3.1.2/src/weac.egg-info}/PKG-INFO +1 -1
  10. {weac-3.1.1 → weac-3.1.2}/tests/analysis/test_criteria_evaluator.py +40 -0
  11. weac-3.1.2/tests/components/test_layer.py +441 -0
  12. weac-3.1.1/tests/components/test_layer.py +0 -221
  13. {weac-3.1.1 → weac-3.1.2}/LICENSE +0 -0
  14. {weac-3.1.1 → weac-3.1.2}/MANIFEST.in +0 -0
  15. {weac-3.1.1 → weac-3.1.2}/README.md +0 -0
  16. {weac-3.1.1 → weac-3.1.2}/img/bc.png +0 -0
  17. {weac-3.1.1 → weac-3.1.2}/img/layering.png +0 -0
  18. {weac-3.1.1 → weac-3.1.2}/img/logo.png +0 -0
  19. {weac-3.1.1 → weac-3.1.2}/img/model.png +0 -0
  20. {weac-3.1.1 → weac-3.1.2}/img/profiles.png +0 -0
  21. {weac-3.1.1 → weac-3.1.2}/img/systems.png +0 -0
  22. {weac-3.1.1 → weac-3.1.2}/setup.cfg +0 -0
  23. {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/__init__.py +0 -0
  24. {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/analyzer.py +0 -0
  25. {weac-3.1.1 → weac-3.1.2}/src/weac/analysis/plotter.py +0 -0
  26. {weac-3.1.1 → weac-3.1.2}/src/weac/components/__init__.py +0 -0
  27. {weac-3.1.1 → weac-3.1.2}/src/weac/components/config.py +0 -0
  28. {weac-3.1.1 → weac-3.1.2}/src/weac/components/criteria_config.py +0 -0
  29. {weac-3.1.1 → weac-3.1.2}/src/weac/components/model_input.py +0 -0
  30. {weac-3.1.1 → weac-3.1.2}/src/weac/components/scenario_config.py +0 -0
  31. {weac-3.1.1 → weac-3.1.2}/src/weac/components/segment.py +0 -0
  32. {weac-3.1.1 → weac-3.1.2}/src/weac/constants.py +0 -0
  33. {weac-3.1.1 → weac-3.1.2}/src/weac/core/__init__.py +0 -0
  34. {weac-3.1.1 → weac-3.1.2}/src/weac/core/eigensystem.py +0 -0
  35. {weac-3.1.1 → weac-3.1.2}/src/weac/core/field_quantities.py +0 -0
  36. {weac-3.1.1 → weac-3.1.2}/src/weac/core/scenario.py +0 -0
  37. {weac-3.1.1 → weac-3.1.2}/src/weac/core/slab.py +0 -0
  38. {weac-3.1.1 → weac-3.1.2}/src/weac/core/unknown_constants_solver.py +0 -0
  39. {weac-3.1.1 → weac-3.1.2}/src/weac/logging_config.py +0 -0
  40. {weac-3.1.1 → weac-3.1.2}/src/weac/utils/__init__.py +0 -0
  41. {weac-3.1.1 → weac-3.1.2}/src/weac/utils/geldsetzer.py +0 -0
  42. {weac-3.1.1 → weac-3.1.2}/src/weac/utils/misc.py +0 -0
  43. {weac-3.1.1 → weac-3.1.2}/src/weac/utils/snow_types.py +0 -0
  44. {weac-3.1.1 → weac-3.1.2}/src/weac/utils/snowpilot_parser.py +0 -0
  45. {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/SOURCES.txt +0 -0
  46. {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/dependency_links.txt +0 -0
  47. {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/requires.txt +0 -0
  48. {weac-3.1.1 → weac-3.1.2}/src/weac.egg-info/top_level.txt +0 -0
  49. {weac-3.1.1 → weac-3.1.2}/tests/__init__.py +0 -0
  50. {weac-3.1.1 → weac-3.1.2}/tests/analysis/__init__.py +0 -0
  51. {weac-3.1.1 → weac-3.1.2}/tests/analysis/test_analyzer.py +0 -0
  52. {weac-3.1.1 → weac-3.1.2}/tests/components/__init__.py +0 -0
  53. {weac-3.1.1 → weac-3.1.2}/tests/components/test_configs.py +0 -0
  54. {weac-3.1.1 → weac-3.1.2}/tests/core/__init__.py +0 -0
  55. {weac-3.1.1 → weac-3.1.2}/tests/core/test_eigensystem.py +0 -0
  56. {weac-3.1.1 → weac-3.1.2}/tests/core/test_field_quantities.py +0 -0
  57. {weac-3.1.1 → weac-3.1.2}/tests/core/test_scenario.py +0 -0
  58. {weac-3.1.1 → weac-3.1.2}/tests/core/test_slab.py +0 -0
  59. {weac-3.1.1 → weac-3.1.2}/tests/core/test_slab_touchdown.py +0 -0
  60. {weac-3.1.1 → weac-3.1.2}/tests/core/test_system_model.py +0 -0
  61. {weac-3.1.1 → weac-3.1.2}/tests/run_tests.py +0 -0
  62. {weac-3.1.1 → weac-3.1.2}/tests/test_comparison_results.py +0 -0
  63. {weac-3.1.1 → weac-3.1.2}/tests/test_regression_simulation.py +0 -0
  64. {weac-3.1.1 → weac-3.1.2}/tests/utils/__init__.py +0 -0
  65. {weac-3.1.1 → weac-3.1.2}/tests/utils/json_helpers.py +0 -0
  66. {weac-3.1.1 → weac-3.1.2}/tests/utils/test_json_helpers.py +0 -0
  67. {weac-3.1.1 → weac-3.1.2}/tests/utils/test_misc.py +0 -0
  68. {weac-3.1.1 → weac-3.1.2}/tests/utils/test_snowpilot_parser.py +0 -0
  69. {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.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weac
3
- Version: 3.1.1
3
+ Version: 3.1.2
4
4
  Summary: Weak layer anticrack nucleation model
5
5
  Author-email: 2phi GbR <mail@2phi.de>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weac"
7
- version = "3.1.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.1"
126
+ current_version = "3.1.2"
127
127
 
128
128
  [[tool.bumpversion.files]]
129
129
  filename = "pyproject.toml"
@@ -2,4 +2,4 @@
2
2
  WEAC - Weak Layer Anticrack Nucleation Model
3
3
  """
4
4
 
5
- __version__ = "3.1.1"
5
+ __version__ = "3.1.2"
@@ -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
- system_copy.update_scenario(scenario_config=ScenarioConfig(phi=0.0))
715
- l_BC = system_copy.slab_touchdown.l_BC
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=2 * l_BC, has_foundation=False, m=0.0),
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=2 * l_BC,
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
- # TODO: Compressive Strength from Schöttner
98
- # (11 +/- 7) * (rho/rho_0) ^ (5.4 +/- 0.5)
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="sigrist",
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
- if self.tensile_strength_method == "sigrist":
157
- object.__setattr__(
158
- self,
159
- "tensile_strength",
160
- self.tensile_strength
161
- or _sigrist_tensile_strength(self.rho, unit="kPa"),
162
- )
163
- else:
164
- raise ValueError(
165
- f"Invalid tensile_strength_method: {self.tensile_strength_method}"
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 : Literal["A_free_hanging", "B_point_contact", "C_in_contact"]
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: Literal[
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
- if self.config.touchdown:
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weac
3
- Version: 3.1.1
3
+ Version: 3.1.2
4
4
  Summary: Weak layer anticrack nucleation model
5
5
  Author-email: 2phi GbR <mail@2phi.de>
6
6
  License-Expression: MIT
@@ -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