weac 3.1.0__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.0 → weac-3.1.2}/CITATION.cff +1 -1
  2. {weac-3.1.0/src/weac.egg-info → weac-3.1.2}/PKG-INFO +2 -2
  3. {weac-3.1.0 → weac-3.1.2}/pyproject.toml +3 -3
  4. {weac-3.1.0 → weac-3.1.2}/src/weac/__init__.py +1 -1
  5. {weac-3.1.0 → weac-3.1.2}/src/weac/analysis/analyzer.py +4 -8
  6. {weac-3.1.0 → weac-3.1.2}/src/weac/analysis/criteria_evaluator.py +30 -6
  7. {weac-3.1.0 → weac-3.1.2}/src/weac/analysis/plotter.py +32 -17
  8. {weac-3.1.0 → weac-3.1.2}/src/weac/components/layer.py +65 -13
  9. {weac-3.1.0 → weac-3.1.2}/src/weac/core/field_quantities.py +27 -23
  10. {weac-3.1.0 → weac-3.1.2}/src/weac/core/slab_touchdown.py +5 -4
  11. {weac-3.1.0 → weac-3.1.2}/src/weac/core/system_model.py +1 -2
  12. {weac-3.1.0 → weac-3.1.2}/src/weac/core/unknown_constants_solver.py +13 -13
  13. {weac-3.1.0 → weac-3.1.2}/src/weac/utils/geldsetzer.py +10 -0
  14. {weac-3.1.0 → weac-3.1.2}/src/weac/utils/snow_types.py +5 -0
  15. {weac-3.1.0 → weac-3.1.2/src/weac.egg-info}/PKG-INFO +2 -2
  16. {weac-3.1.0 → weac-3.1.2}/src/weac.egg-info/requires.txt +1 -1
  17. {weac-3.1.0 → weac-3.1.2}/tests/analysis/test_criteria_evaluator.py +82 -0
  18. weac-3.1.2/tests/components/test_layer.py +441 -0
  19. {weac-3.1.0 → weac-3.1.2}/tests/test_regression_simulation.py +1 -1
  20. weac-3.1.0/tests/components/test_layer.py +0 -221
  21. {weac-3.1.0 → weac-3.1.2}/LICENSE +0 -0
  22. {weac-3.1.0 → weac-3.1.2}/MANIFEST.in +0 -0
  23. {weac-3.1.0 → weac-3.1.2}/README.md +0 -0
  24. {weac-3.1.0 → weac-3.1.2}/img/bc.png +0 -0
  25. {weac-3.1.0 → weac-3.1.2}/img/layering.png +0 -0
  26. {weac-3.1.0 → weac-3.1.2}/img/logo.png +0 -0
  27. {weac-3.1.0 → weac-3.1.2}/img/model.png +0 -0
  28. {weac-3.1.0 → weac-3.1.2}/img/profiles.png +0 -0
  29. {weac-3.1.0 → weac-3.1.2}/img/systems.png +0 -0
  30. {weac-3.1.0 → weac-3.1.2}/setup.cfg +0 -0
  31. {weac-3.1.0 → weac-3.1.2}/src/weac/analysis/__init__.py +0 -0
  32. {weac-3.1.0 → weac-3.1.2}/src/weac/components/__init__.py +0 -0
  33. {weac-3.1.0 → weac-3.1.2}/src/weac/components/config.py +0 -0
  34. {weac-3.1.0 → weac-3.1.2}/src/weac/components/criteria_config.py +0 -0
  35. {weac-3.1.0 → weac-3.1.2}/src/weac/components/model_input.py +0 -0
  36. {weac-3.1.0 → weac-3.1.2}/src/weac/components/scenario_config.py +0 -0
  37. {weac-3.1.0 → weac-3.1.2}/src/weac/components/segment.py +0 -0
  38. {weac-3.1.0 → weac-3.1.2}/src/weac/constants.py +0 -0
  39. {weac-3.1.0 → weac-3.1.2}/src/weac/core/__init__.py +0 -0
  40. {weac-3.1.0 → weac-3.1.2}/src/weac/core/eigensystem.py +0 -0
  41. {weac-3.1.0 → weac-3.1.2}/src/weac/core/scenario.py +0 -0
  42. {weac-3.1.0 → weac-3.1.2}/src/weac/core/slab.py +0 -0
  43. {weac-3.1.0 → weac-3.1.2}/src/weac/logging_config.py +0 -0
  44. {weac-3.1.0 → weac-3.1.2}/src/weac/utils/__init__.py +0 -0
  45. {weac-3.1.0 → weac-3.1.2}/src/weac/utils/misc.py +0 -0
  46. {weac-3.1.0 → weac-3.1.2}/src/weac/utils/snowpilot_parser.py +0 -0
  47. {weac-3.1.0 → weac-3.1.2}/src/weac.egg-info/SOURCES.txt +0 -0
  48. {weac-3.1.0 → weac-3.1.2}/src/weac.egg-info/dependency_links.txt +0 -0
  49. {weac-3.1.0 → weac-3.1.2}/src/weac.egg-info/top_level.txt +0 -0
  50. {weac-3.1.0 → weac-3.1.2}/tests/__init__.py +0 -0
  51. {weac-3.1.0 → weac-3.1.2}/tests/analysis/__init__.py +0 -0
  52. {weac-3.1.0 → weac-3.1.2}/tests/analysis/test_analyzer.py +0 -0
  53. {weac-3.1.0 → weac-3.1.2}/tests/components/__init__.py +0 -0
  54. {weac-3.1.0 → weac-3.1.2}/tests/components/test_configs.py +0 -0
  55. {weac-3.1.0 → weac-3.1.2}/tests/core/__init__.py +0 -0
  56. {weac-3.1.0 → weac-3.1.2}/tests/core/test_eigensystem.py +0 -0
  57. {weac-3.1.0 → weac-3.1.2}/tests/core/test_field_quantities.py +0 -0
  58. {weac-3.1.0 → weac-3.1.2}/tests/core/test_scenario.py +0 -0
  59. {weac-3.1.0 → weac-3.1.2}/tests/core/test_slab.py +0 -0
  60. {weac-3.1.0 → weac-3.1.2}/tests/core/test_slab_touchdown.py +0 -0
  61. {weac-3.1.0 → weac-3.1.2}/tests/core/test_system_model.py +0 -0
  62. {weac-3.1.0 → weac-3.1.2}/tests/run_tests.py +0 -0
  63. {weac-3.1.0 → weac-3.1.2}/tests/test_comparison_results.py +0 -0
  64. {weac-3.1.0 → weac-3.1.2}/tests/utils/__init__.py +0 -0
  65. {weac-3.1.0 → weac-3.1.2}/tests/utils/json_helpers.py +0 -0
  66. {weac-3.1.0 → weac-3.1.2}/tests/utils/test_json_helpers.py +0 -0
  67. {weac-3.1.0 → weac-3.1.2}/tests/utils/test_misc.py +0 -0
  68. {weac-3.1.0 → weac-3.1.2}/tests/utils/test_snowpilot_parser.py +0 -0
  69. {weac-3.1.0 → 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.0
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.0
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
@@ -17,7 +17,7 @@ Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: matplotlib>=3.9.1
20
- Requires-Dist: numpy>=2.0.1
20
+ Requires-Dist: numpy<2.4.0,>=2.3.5
21
21
  Requires-Dist: scipy>=1.14.0
22
22
  Requires-Dist: pydantic>=2.11.7
23
23
  Requires-Dist: snowpylot>=1.1.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weac"
7
- version = "3.1.0"
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"
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "matplotlib>=3.9.1",
23
- "numpy>=2.0.1",
23
+ "numpy>=2.3.5,<2.4.0",
24
24
  "scipy>=1.14.0",
25
25
  "pydantic>=2.11.7",
26
26
  "snowpylot>=1.1.3",
@@ -123,7 +123,7 @@ ignore = [
123
123
  ]
124
124
 
125
125
  [tool.bumpversion]
126
- current_version = "3.1.0"
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.0"
5
+ __version__ = "3.1.2"
@@ -272,11 +272,7 @@ class Analyzer:
272
272
 
273
273
  # Calculate weight load at grid points and superimpose on stress field
274
274
  qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
275
- # Old Implementation: Changed for numerical stability
276
- # for i, qi in enumerate(qt[:-1]):
277
- # Sxx[i, :] += qi * (zi[i + 1] - zi[i])
278
- # Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
279
- # New Implementation: Changed for numerical stability
275
+
280
276
  dz = np.diff(zi)
281
277
  Sxx_MPa[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
282
278
  Sxx_MPa[-1, :] += qt[-1] * dz[-1]
@@ -393,7 +389,7 @@ class Analyzer:
393
389
  # Get mesh along z-axis
394
390
  zmesh = self.get_zmesh(dz=dz)
395
391
  zi = zmesh["z"]
396
- rho = zmesh["rho"]
392
+ rho_t_mm3 = zmesh["rho"]
397
393
  qs = self.sm.scenario.surface_load
398
394
  # Get dimensions of stress field (n rows, m columns)
399
395
  n = len(zi)
@@ -414,13 +410,13 @@ class Analyzer:
414
410
  dsxx_dxdx[i, :] = E / (1 - nu**2) * (du0_dxdxdx + z * dpsi_dxdxdx)
415
411
 
416
412
  # Calculate weight load at grid points
417
- qn = rho * G_MM_S2 * np.cos(np.deg2rad(phi))
413
+ qn = -rho_t_mm3 * G_MM_S2 * np.cos(np.deg2rad(phi))
418
414
 
419
415
  # Integrate dsxx_dxdx twice along z to obtain transverse
420
416
  # normal stress Szz in MPa
421
417
  integrand = cumulative_trapezoid(dsxx_dxdx, zi, axis=0, initial=0)
422
418
  Szz_MPa = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
423
- Szz_MPa += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
419
+ Szz_MPa += cumulative_trapezoid(qn, zi, initial=0)[:, None]
424
420
 
425
421
  # Normalize tensile stresses to tensile strength
426
422
  if normalize:
@@ -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__)
@@ -113,6 +114,9 @@ class MaximalStressResult:
113
114
  The normalized maximum principal stress to the tensile strength of the layers.
114
115
  max_Sxx_norm: float
115
116
  The normalized maximum axial normal stress to the tensile strength of the layers.
117
+ slab_tensile_criterion: float
118
+ The slab tensile criterion, i.e. the portion of the slab thickness that is prone
119
+ to fail under tensile stresses in the steady state (between 0 and 1).
116
120
  """
117
121
 
118
122
  principal_stress_kPa: np.ndarray
@@ -121,6 +125,7 @@ class MaximalStressResult:
121
125
  Sxx_norm: np.ndarray
122
126
  max_principal_stress_norm: float
123
127
  max_Sxx_norm: float
128
+ slab_tensile_criterion: float
124
129
 
125
130
 
126
131
  @dataclass
@@ -679,6 +684,7 @@ class CriteriaEvaluator:
679
684
  def evaluate_SteadyState(
680
685
  self,
681
686
  system: SystemModel,
687
+ mode: TouchdownMode = "C_in_contact",
682
688
  vertical: bool = False,
683
689
  print_call_stats: bool = False,
684
690
  ) -> SteadyStateResult:
@@ -706,20 +712,34 @@ class CriteriaEvaluator:
706
712
  UserWarning,
707
713
  )
708
714
  system_copy = copy.deepcopy(system)
709
- system_copy.config.touchdown = True
710
- system_copy.update_scenario(scenario_config=ScenarioConfig(phi=0.0))
711
- l_BC = system.slab_touchdown.l_BC
715
+ # Evaluate touchdown distance for flat slab
716
+ system_copy.toggle_touchdown(True)
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
712
733
 
713
734
  segments = [
714
735
  Segment(length=5e3, has_foundation=True, m=0.0),
715
- Segment(length=2 * l_BC, has_foundation=False, m=0.0),
736
+ Segment(length=cut_distance, has_foundation=False, m=0.0),
716
737
  ]
717
738
  scenario_config = ScenarioConfig(
718
739
  system_type="vpst-" if vertical else "pst-",
719
740
  phi=0.0, # Slab Touchdown works only for flat slab
720
- cut_length=2 * l_BC,
741
+ cut_length=cut_distance,
721
742
  )
722
- # system_copy.config.touchdown = True
723
743
  system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
724
744
  touchdown_distance = system_copy.slab_touchdown.touchdown_distance
725
745
  analyzer = Analyzer(system_copy, printing_enabled=print_call_stats)
@@ -1248,6 +1268,9 @@ class CriteriaEvaluator:
1248
1268
  )
1249
1269
  max_principal_stress_norm = np.max(principal_stress_norm)
1250
1270
  max_Sxx_norm = np.max(Sxx_norm)
1271
+ # evaluate for each height level if the slab is prone to fail under tensile stresses
1272
+ height_level_prone_to_fail = np.max(Sxx_norm, axis=1)
1273
+ slab_tensile_criterion = np.mean(height_level_prone_to_fail)
1251
1274
  if print_call_stats:
1252
1275
  analyzer.print_call_stats(
1253
1276
  message="_calculate_maximal_stresses Call Statistics"
@@ -1259,4 +1282,5 @@ class CriteriaEvaluator:
1259
1282
  Sxx_norm=Sxx_norm,
1260
1283
  max_principal_stress_norm=max_principal_stress_norm,
1261
1284
  max_Sxx_norm=max_Sxx_norm,
1285
+ slab_tensile_criterion=slab_tensile_criterion,
1262
1286
  )
@@ -1041,6 +1041,7 @@ class Plotter:
1041
1041
  xwl: np.ndarray,
1042
1042
  z: np.ndarray,
1043
1043
  analyzer: Analyzer,
1044
+ window: float | None = None,
1044
1045
  weaklayer_proportion: float | None = None,
1045
1046
  dz: int = 2,
1046
1047
  levels: int = 300,
@@ -1061,6 +1062,8 @@ class Plotter:
1061
1062
  Solution vector.
1062
1063
  analyzer : Analyzer
1063
1064
  Analyzer instance.
1065
+ window: float | None, optional
1066
+ Window size for the plot. Shows the right edge of the slab, where the slab is deformed. Default is None.
1064
1067
  weaklayer_proportion: float | None, optional
1065
1068
  Proportion of the plot to allocate to the weak layer. Default is None.
1066
1069
  dz : int, optional
@@ -1087,6 +1090,9 @@ class Plotter:
1087
1090
  phi = analyzer.sm.scenario.phi
1088
1091
  system_type = analyzer.sm.scenario.system_type
1089
1092
  fq = analyzer.sm.fq
1093
+ sigma_comp = (
1094
+ analyzer.sm.weak_layer.sigma_comp
1095
+ ) # Compressive strength of the weak layer [kPa]
1090
1096
 
1091
1097
  # Compute slab displacements on grid (cm)
1092
1098
  Usl = np.vstack([fq.u(z, h0=h0, unit="cm") for h0 in zi])
@@ -1204,7 +1210,7 @@ class Plotter:
1204
1210
  z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
1205
1211
  )
1206
1212
  weak_full = analyzer.principal_stress_weaklayer(
1207
- z, val="min", unit="kPa", normalize=normalize
1213
+ z, sc=sigma_comp, val="min", unit="kPa", normalize=normalize
1208
1214
  )
1209
1215
  weak = weak_full[nanmask]
1210
1216
  if normalize:
@@ -1249,13 +1255,14 @@ class Plotter:
1249
1255
  [slab_proportion + cracked_proportion, total_height_plot],
1250
1256
  )
1251
1257
  # No displacements for the cracked weak layer outline (undeformed)
1252
- ax.plot(
1253
- _outline(Xwl_cracked),
1254
- _outline(Zwl_cracked_plot),
1255
- "k-",
1256
- alpha=0.3,
1257
- linewidth=1,
1258
- )
1258
+ if xwl_cracked.shape[0] > 0:
1259
+ ax.plot(
1260
+ _outline(Xwl_cracked),
1261
+ _outline(Zwl_cracked_plot),
1262
+ "k-",
1263
+ alpha=0.3,
1264
+ linewidth=1,
1265
+ )
1259
1266
 
1260
1267
  # Then plot the deformed weak-layer outline where it exists
1261
1268
  if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
@@ -1287,14 +1294,15 @@ class Plotter:
1287
1294
  cmap=cmap,
1288
1295
  extend="both",
1289
1296
  )
1290
- ax.contourf(
1291
- Xwl_cracked,
1292
- Zwl_cracked_plot,
1293
- np.zeros((2, xwl_cracked.shape[0])),
1294
- levels=levels,
1295
- cmap=cmap,
1296
- extend="both",
1297
- )
1297
+ if xwl_cracked.shape[0] > 0:
1298
+ ax.contourf(
1299
+ Xwl_cracked,
1300
+ Zwl_cracked_plot,
1301
+ np.zeros((2, xwl_cracked.shape[0])),
1302
+ levels=levels,
1303
+ cmap=cmap,
1304
+ extend="both",
1305
+ )
1298
1306
 
1299
1307
  # Plot setup
1300
1308
  # Set y-limits to match plot coordinate system (0 to total_height_plot = 1.0)
@@ -1304,7 +1312,10 @@ class Plotter:
1304
1312
  )
1305
1313
 
1306
1314
  # Set limits first, then aspect ratio to avoid matplotlib adjusting limits
1307
- ax.set_xlim([xmin, xmax])
1315
+ if window is None:
1316
+ ax.set_xlim([xmin, xmax])
1317
+ else:
1318
+ ax.set_xlim([xmax - window, xmax])
1308
1319
  ax.set_ylim([plot_ymin, plot_ymax])
1309
1320
  ax.invert_yaxis()
1310
1321
  ax.use_sticky_edges = False
@@ -1360,6 +1371,10 @@ class Plotter:
1360
1371
 
1361
1372
  # Plot labels
1362
1373
  ax.set_xlabel(r"lateral position $x$ (cm) $\longrightarrow$")
1374
+ ax.set_title(
1375
+ f"{field}{' (normalized to tensile strength)' if normalize else ''}",
1376
+ size=10,
1377
+ )
1363
1378
 
1364
1379
  # Show colorbar
1365
1380
  ticks = np.linspace(levels[0], levels[-1], num=11, endpoint=True)
@@ -94,6 +94,45 @@ 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
+ 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]
134
+
135
+
97
136
  class Layer(BaseModel):
98
137
  """
99
138
  Regular slab layer (no foundation springs).
@@ -110,6 +149,10 @@ class Layer(BaseModel):
110
149
  Young's modulus E [MPa]. If omitted it is derived from ``rho``.
111
150
  G : float, optional
112
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.
113
156
  """
114
157
 
115
158
  # has to be provided
@@ -125,8 +168,8 @@ class Layer(BaseModel):
125
168
  tensile_strength: float = Field(
126
169
  default=0.0, ge=0, description="Tensile strength [kPa]"
127
170
  )
128
- tensile_strength_method: Literal["sigrist"] = Field(
129
- default="sigrist",
171
+ tensile_strength_method: Literal["sigrist", "adam", "hybrid"] = Field(
172
+ default="hybrid",
130
173
  description="Method to calculate the tensile strength",
131
174
  )
132
175
  E_method: Literal["bergfeld", "scapazzo", "gerling"] = Field(
@@ -149,17 +192,23 @@ class Layer(BaseModel):
149
192
  else:
150
193
  raise ValueError(f"Invalid E_method: {self.E_method}")
151
194
  object.__setattr__(self, "G", self.G or self.E / (2 * (1 + self.nu)))
152
- if self.tensile_strength_method == "sigrist":
153
- object.__setattr__(
154
- self,
155
- "tensile_strength",
156
- self.tensile_strength
157
- or _sigrist_tensile_strength(self.rho, unit="kPa"),
158
- )
159
- else:
160
- raise ValueError(
161
- f"Invalid tensile_strength_method: {self.tensile_strength_method}"
162
- )
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)
163
212
 
164
213
  @model_validator(mode="after")
165
214
  def validate_positive_E_G(self):
@@ -225,6 +274,9 @@ class WeakLayer(BaseModel):
225
274
  )
226
275
  sigma_c: float = Field(default=6.16, gt=0, description="Tensile strength [kPa]")
227
276
  tau_c: float = Field(default=5.09, gt=0, description="Shear strength [kPa]")
277
+ sigma_comp: float = Field(
278
+ default=2.6, gt=0, description="Compressive strength [kPa]"
279
+ )
228
280
  E_method: Literal["bergfeld", "scapazzo", "gerling"] = Field(
229
281
  default="bergfeld",
230
282
  description="Method to calculate the Young's modulus",
@@ -58,19 +58,23 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
58
58
  h0: float = 0,
59
59
  unit: LengthUnit = "mm",
60
60
  ) -> float | np.ndarray:
61
- """Horizontal displacement *u = u₀ + h₀ ψ* at depth h₀."""
61
+ """
62
+ Horizontal displacement `u = u₀ + h₀ ψ` at depth `h₀` (mm).
63
+ """
62
64
  return self._unit_factor(unit) * (Z[0, :] + h0 * self.psi(Z))
63
65
 
64
66
  def du_dx(self, Z: np.ndarray, h0: float) -> float | np.ndarray:
65
- """Derivative u' = u₀' + h₀ ψ'."""
67
+ """Derivative u' = u₀' + h₀ ψ' (-)."""
66
68
  return Z[1, :] + h0 * self.dpsi_dx(Z)
67
69
 
68
70
  def w(self, Z: np.ndarray, unit: LengthUnit = "mm") -> float | np.ndarray:
69
- """Center-line deflection *w*."""
71
+ """
72
+ Center-line (vertical) deflection `w` (mm).
73
+ """
70
74
  return self._unit_factor(unit) * Z[2, :]
71
75
 
72
76
  def dw_dx(self, Z: np.ndarray) -> float | np.ndarray:
73
- """First derivative w'."""
77
+ """First derivative `w'` (-)."""
74
78
  return Z[3, :]
75
79
 
76
80
  def psi(
@@ -78,32 +82,32 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
78
82
  Z: np.ndarray,
79
83
  unit: AngleUnit = "rad",
80
84
  ) -> float | np.ndarray:
81
- """Rotation ψ of the mid-plane."""
85
+ """Rotation `ψ` of the mid-plane (rad)."""
82
86
  factor = self._unit_factor(unit)
83
87
  return factor * Z[4, :]
84
88
 
85
89
  def dpsi_dx(self, Z: np.ndarray) -> float | np.ndarray:
86
- """First derivative ψ′."""
90
+ """First derivative `ψ'` (rad/mm)."""
87
91
  return Z[5, :]
88
92
 
89
93
  def N(self, Z: np.ndarray) -> float | np.ndarray:
90
- """Axial normal force N = A11 u' + B11 psi' in the slab [N]"""
94
+ """Axial normal force `N = A11 u' + B11 ψ'` in the slab [N]"""
91
95
  return self.es.A11 * Z[1, :] + self.es.B11 * Z[5, :]
92
96
 
93
97
  def M(self, Z: np.ndarray) -> float | np.ndarray:
94
- """Bending moment M = B11 u' + D11 psi' in the slab [Nmm]"""
98
+ """Bending moment `M = B11 u' + D11 ψ'` in the slab [Nmm]"""
95
99
  return self.es.B11 * Z[1, :] + self.es.D11 * Z[5, :]
96
100
 
97
101
  def V(self, Z: np.ndarray) -> float | np.ndarray:
98
- """Vertical shear force V = kA55(w' + psi) [N]"""
102
+ """Vertical shear force `V = kA55(w' + ψ)` [N]"""
99
103
  return self.es.kA55 * (Z[3, :] + Z[4, :])
100
104
 
101
105
  def sig(self, Z: np.ndarray, unit: StressUnit = "MPa") -> float | np.ndarray:
102
- """Weak-layer normal stress"""
106
+ """Weak-layer normal stress `sig = -kn * w`"""
103
107
  return -self._unit_factor(unit) * self.es.weak_layer.kn * self.w(Z)
104
108
 
105
109
  def tau(self, Z: np.ndarray, unit: StressUnit = "MPa") -> float | np.ndarray:
106
- """Weak-layer shear stress"""
110
+ """Weak-layer shear stress `tau = -kt * (w' * h/2 - u(h=H/2))`"""
107
111
  return (
108
112
  -self._unit_factor(unit)
109
113
  * self.es.weak_layer.kt
@@ -114,11 +118,11 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
114
118
  )
115
119
 
116
120
  def eps(self, Z: np.ndarray) -> float | np.ndarray:
117
- """Weak-layer normal strain"""
121
+ """Weak-layer normal strain `eps = -w / h`"""
118
122
  return -self.w(Z) / self.es.weak_layer.h
119
123
 
120
124
  def gamma(self, Z: np.ndarray) -> float | np.ndarray:
121
- """Weak-layer shear strain."""
125
+ """Weak-layer shear strain `gamma = (w' * h/2 - u(h=H/2)) / h`"""
122
126
  return (
123
127
  self.dw_dx(Z) / 2 - self.u(Z, h0=self.es.slab.H / 2) / self.es.weak_layer.h
124
128
  )
@@ -176,9 +180,9 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
176
180
 
177
181
  def dz_dxdx(self, z: np.ndarray, phi: float, qs: float) -> np.ndarray:
178
182
  """
179
- Get second derivative z''(x) = K*z'(x) of the solution vector.
183
+ Get second derivative `z''(x) = K*z'(x)` of the solution vector.
180
184
 
181
- z''(x) = [u''(x) u'''(x) w''(x) w'''(x) psi''(x), psi'''(x)]^T
185
+ `z''(x) = [u''(x) u'''(x) w''(x) w'''(x) psi''(x), psi'''(x)]^T`
182
186
 
183
187
  Parameters
184
188
  ----------
@@ -200,7 +204,7 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
200
204
 
201
205
  def du0_dxdx(self, z: np.ndarray, phi: float, qs: float) -> float | np.ndarray:
202
206
  """
203
- Get second derivative of the horiz. centerline displacement u0''(x).
207
+ Get second derivative of the horiz. centerline displacement `u0''(x)` (mm⁻¹).
204
208
 
205
209
  Parameters
206
210
  ----------
@@ -213,13 +217,13 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
213
217
  -------
214
218
  ndarray, float
215
219
  Second derivative of the horizontal centerline displacement
216
- u0''(x) (1/mm).
220
+ `u0''(x)`.
217
221
  """
218
222
  return self.dz_dx(z, phi, qs)[1, :]
219
223
 
220
224
  def dpsi_dxdx(self, z: np.ndarray, phi: float, qs: float) -> float | np.ndarray:
221
225
  """
222
- Get second derivative of the cross-section rotation psi''(x).
226
+ Get second derivative of the cross-section rotation `psi''(x)` (mm⁻²).
223
227
 
224
228
  Parameters
225
229
  ----------
@@ -231,13 +235,13 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
231
235
  Returns
232
236
  -------
233
237
  ndarray, float
234
- Second derivative of the cross-section rotation psi''(x) (1/mm^2).
238
+ Second derivative of the cross-section rotation psi''(x).
235
239
  """
236
240
  return self.dz_dx(z, phi, qs)[5, :]
237
241
 
238
242
  def du0_dxdxdx(self, z: np.ndarray, phi: float, qs: float) -> float | np.ndarray:
239
243
  """
240
- Get third derivative of the horiz. centerline displacement u0'''(x).
244
+ Get third derivative of the horiz. centerline displacement `u0'''(x)` (mm⁻²).
241
245
 
242
246
  Parameters
243
247
  ----------
@@ -250,13 +254,13 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
250
254
  -------
251
255
  ndarray, float
252
256
  Third derivative of the horizontal centerline displacement
253
- u0'''(x) (1/mm^2).
257
+ u0'''(x).
254
258
  """
255
259
  return self.dz_dxdx(z, phi, qs)[1, :]
256
260
 
257
261
  def dpsi_dxdxdx(self, z: np.ndarray, phi: float, qs: float) -> float | np.ndarray:
258
262
  """
259
- Get third derivative of the cross-section rotation psi'''(x).
263
+ Get third derivative of the cross-section rotation `psi'''(x)` (mm⁻³).
260
264
 
261
265
  Parameters
262
266
  ----------
@@ -268,6 +272,6 @@ class FieldQuantities: # pylint: disable=too-many-instance-attributes, too-many
268
272
  Returns
269
273
  -------
270
274
  ndarray, float
271
- Third derivative of the cross-section rotation psi'''(x) (1/mm^3).
275
+ Third derivative of the cross-section rotation psi'''(x).
272
276
  """
273
277
  return self.dz_dxdx(z, phi, qs)[5, :]
@@ -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):