weac 3.0.2__py3-none-any.whl → 3.1.1__py3-none-any.whl

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/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  WEAC - Weak Layer Anticrack Nucleation Model
3
3
  """
4
4
 
5
- __version__ = "3.0.2"
5
+ __version__ = "3.1.1"
weac/analysis/__init__.py CHANGED
@@ -8,7 +8,7 @@ from .criteria_evaluator import (
8
8
  CoupledCriterionResult,
9
9
  CriteriaEvaluator,
10
10
  FindMinimumForceResult,
11
- SSERRResult,
11
+ SteadyStateResult,
12
12
  )
13
13
  from .plotter import Plotter
14
14
 
@@ -18,6 +18,6 @@ __all__ = [
18
18
  "CoupledCriterionHistory",
19
19
  "CoupledCriterionResult",
20
20
  "FindMinimumForceResult",
21
- "SSERRResult",
21
+ "SteadyStateResult",
22
22
  "Plotter",
23
23
  ]
weac/analysis/analyzer.py CHANGED
@@ -214,7 +214,7 @@ class Analyzer:
214
214
  ], # Convert to t/mm^3
215
215
  "tensile_strength": [
216
216
  layer.tensile_strength for layer in self.sm.slab.layers
217
- ],
217
+ ], # in kPa
218
218
  }
219
219
 
220
220
  # Repeat properties for each grid point in the layer
@@ -225,7 +225,7 @@ class Analyzer:
225
225
  return si
226
226
 
227
227
  @track_analyzer_call
228
- def Sxx(self, Z, phi, dz=2, unit="kPa"):
228
+ def Sxx(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
229
229
  """
230
230
  Compute axial normal stress in slab layers.
231
231
 
@@ -239,6 +239,10 @@ class Analyzer:
239
239
  Element size along z-axis (mm). Default is 2 mm.
240
240
  unit : {'kPa', 'MPa'}, optional
241
241
  Desired output unit. Default is 'kPa'.
242
+ normalize : bool, optional
243
+ Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
244
+ When normalized, the `unit` parameter is ignored and values are returned as ratios.
245
+ Default is False.
242
246
 
243
247
  Returns
244
248
  -------
@@ -258,30 +262,34 @@ class Analyzer:
258
262
  m = Z.shape[1]
259
263
 
260
264
  # Initialize axial normal stress Sxx
261
- Sxx = np.zeros(shape=[n, m])
265
+ Sxx_MPa = np.zeros(shape=[n, m])
262
266
 
263
267
  # Compute axial normal stress Sxx at grid points in MPa
264
268
  for i, z in enumerate(zi):
265
- E = zmesh["E"][i]
269
+ E_MPa = zmesh["E"][i]
266
270
  nu = zmesh["nu"][i]
267
- Sxx[i, :] = E / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
271
+ Sxx_MPa[i, :] = E_MPa / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
268
272
 
269
273
  # Calculate weight load at grid points and superimpose on stress field
270
274
  qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
271
- # Old Implementation: Changed for numerical stability
272
- # for i, qi in enumerate(qt[:-1]):
273
- # Sxx[i, :] += qi * (zi[i + 1] - zi[i])
274
- # Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
275
- # New Implementation: Changed for numerical stability
275
+
276
276
  dz = np.diff(zi)
277
- Sxx[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
278
- Sxx[-1, :] += qt[-1] * dz[-1]
277
+ Sxx_MPa[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
278
+ Sxx_MPa[-1, :] += qt[-1] * dz[-1]
279
+
280
+ # Normalize tensile stresses to tensile strength
281
+ if normalize:
282
+ tensile_strength_kPa = zmesh["tensile_strength"]
283
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
284
+ # Normalize axial normal stress to layers' tensile strength
285
+ normalized_Sxx = Sxx_MPa / tensile_strength_MPa[:, None]
286
+ return normalized_Sxx
279
287
 
280
288
  # Return axial normal stress in specified unit
281
- return convert[unit] * Sxx
289
+ return convert[unit] * Sxx_MPa
282
290
 
283
291
  @track_analyzer_call
284
- def Txz(self, Z, phi, dz=2, unit="kPa"):
292
+ def Txz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
285
293
  """
286
294
  Compute shear stress in slab layers.
287
295
 
@@ -295,6 +303,9 @@ class Analyzer:
295
303
  Element size along z-axis (mm). Default is 2 mm.
296
304
  unit : {'kPa', 'MPa'}, optional
297
305
  Desired output unit. Default is 'kPa'.
306
+ normalize : bool, optional
307
+ Toggle normalization. If True, normalize shear stress values to the tensile strength of each layer (dimensionless).
308
+ When normalized, the `unit` parameter is ignored and values are returned as ratios. Default is False.
298
309
 
299
310
  Returns
300
311
  -------
@@ -332,14 +343,22 @@ class Analyzer:
332
343
 
333
344
  # Integrate -dsxx_dx along z and add cumulative weight load
334
345
  # to obtain shear stress Txz in MPa
335
- Txz = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
336
- Txz += cumulative_trapezoid(qt, zi, initial=0)[:, None]
346
+ Txz_MPa = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
347
+ Txz_MPa += cumulative_trapezoid(qt, zi, initial=0)[:, None]
348
+
349
+ # Normalize shear stresses to tensile strength
350
+ if normalize:
351
+ tensile_strength_kPa = zmesh["tensile_strength"]
352
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
353
+ # Normalize shear stress to layers' tensile strength
354
+ normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
355
+ return normalized_Txz
337
356
 
338
357
  # Return shear stress Txz in specified unit
339
- return convert[unit] * Txz
358
+ return convert[unit] * Txz_MPa
340
359
 
341
360
  @track_analyzer_call
342
- def Szz(self, Z, phi, dz=2, unit="kPa"):
361
+ def Szz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
343
362
  """
344
363
  Compute transverse normal stress in slab layers.
345
364
 
@@ -353,6 +372,10 @@ class Analyzer:
353
372
  Element size along z-axis (mm). Default is 2 mm.
354
373
  unit : {'kPa', 'MPa'}, optional
355
374
  Desired output unit. Default is 'kPa'.
375
+ normalize : bool, optional
376
+ Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
377
+ When normalized, the `unit` parameter is ignored and values are returned as ratios.
378
+ Default is False.
356
379
 
357
380
  Returns
358
381
  -------
@@ -366,7 +389,7 @@ class Analyzer:
366
389
  # Get mesh along z-axis
367
390
  zmesh = self.get_zmesh(dz=dz)
368
391
  zi = zmesh["z"]
369
- rho = zmesh["rho"]
392
+ rho_t_mm3 = zmesh["rho"]
370
393
  qs = self.sm.scenario.surface_load
371
394
  # Get dimensions of stress field (n rows, m columns)
372
395
  n = len(zi)
@@ -387,16 +410,24 @@ class Analyzer:
387
410
  dsxx_dxdx[i, :] = E / (1 - nu**2) * (du0_dxdxdx + z * dpsi_dxdxdx)
388
411
 
389
412
  # Calculate weight load at grid points
390
- qn = rho * G_MM_S2 * np.cos(np.deg2rad(phi))
413
+ qn = -rho_t_mm3 * G_MM_S2 * np.cos(np.deg2rad(phi))
391
414
 
392
415
  # Integrate dsxx_dxdx twice along z to obtain transverse
393
416
  # normal stress Szz in MPa
394
417
  integrand = cumulative_trapezoid(dsxx_dxdx, zi, axis=0, initial=0)
395
- Szz = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
396
- Szz += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
418
+ Szz_MPa = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
419
+ Szz_MPa += cumulative_trapezoid(qn, zi, initial=0)[:, None]
420
+
421
+ # Normalize tensile stresses to tensile strength
422
+ if normalize:
423
+ tensile_strength_kPa = zmesh["tensile_strength"]
424
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
425
+ # Normalize transverse normal stress to layers' tensile strength
426
+ normalized_Szz = Szz_MPa / tensile_strength_MPa[:, None]
427
+ return normalized_Szz
397
428
 
398
- # Return shear stress txz in specified unit
399
- return convert[unit] * Szz
429
+ # Return transverse normal stress Szz in specified unit
430
+ return convert[unit] * Szz_MPa
400
431
 
401
432
  @track_analyzer_call
402
433
  def principal_stress_slab(
@@ -438,6 +469,8 @@ class Analyzer:
438
469
  'min', or if normalization of compressive principal stress
439
470
  is requested.
440
471
  """
472
+ convert = {"kPa": 1e3, "MPa": 1}
473
+
441
474
  # Raise error if specified component is not available
442
475
  if val not in ["min", "max"]:
443
476
  raise ValueError(f"Component {val} not defined.")
@@ -460,9 +493,10 @@ class Analyzer:
460
493
  # Normalize tensile stresses to tensile strength
461
494
  if normalize and val == "max":
462
495
  zmesh = self.get_zmesh(dz=dz)
463
- tensile_strength = zmesh["tensile_strength"]
496
+ tensile_strength_kPa = zmesh["tensile_strength"]
497
+ tensile_strength_converted = tensile_strength_kPa / 1e3 * convert[unit]
464
498
  # Normalize maximum principal stress to layers' tensile strength
465
- normalized_Ps = Ps / tensile_strength[:, None]
499
+ normalized_Ps = Ps / tensile_strength_converted[:, None]
466
500
  return normalized_Ps
467
501
 
468
502
  # Return absolute principal stresses
@@ -95,9 +95,42 @@ class CoupledCriterionResult:
95
95
 
96
96
 
97
97
  @dataclass
98
- class SSERRResult:
98
+ class MaximalStressResult:
99
99
  """
100
- Holds the results of the SSERR evaluation.
100
+ Holds the results of the maximal stress evaluation.
101
+
102
+ Attributes:
103
+ -----------
104
+ principal_stress_kPa: np.ndarray
105
+ The principal stress in kPa.
106
+ Sxx_kPa: np.ndarray
107
+ The axial normal stress in kPa.
108
+ principal_stress_norm: np.ndarray
109
+ The normalized principal stress to the tensile strength of the layers.
110
+ Sxx_norm: np.ndarray
111
+ The normalized axial normal stress to the tensile strength of the layers.
112
+ max_principal_stress_norm: float
113
+ The normalized maximum principal stress to the tensile strength of the layers.
114
+ max_Sxx_norm: float
115
+ The normalized maximum axial normal stress to the tensile strength of the layers.
116
+ slab_tensile_criterion: float
117
+ The slab tensile criterion, i.e. the portion of the slab thickness that is prone
118
+ to fail under tensile stresses in the steady state (between 0 and 1).
119
+ """
120
+
121
+ principal_stress_kPa: np.ndarray
122
+ Sxx_kPa: np.ndarray
123
+ principal_stress_norm: np.ndarray
124
+ Sxx_norm: np.ndarray
125
+ max_principal_stress_norm: float
126
+ max_Sxx_norm: float
127
+ slab_tensile_criterion: float
128
+
129
+
130
+ @dataclass
131
+ class SteadyStateResult:
132
+ """
133
+ Holds the results of the Steady State evaluation.
101
134
 
102
135
  Attributes:
103
136
  -----------
@@ -107,15 +140,21 @@ class SSERRResult:
107
140
  The message of the evaluation.
108
141
  touchdown_distance : float
109
142
  The touchdown distance.
110
- SSERR : float
111
- The Steady-State Energy Release Rate calculated with the
112
- touchdown distance from G_I and G_II.
143
+ energy_release_rate : float
144
+ The steady-state energy release rate calculated with the
145
+ touchdown distance from the differential energy release rate.
146
+ maximal_stress_result: MaximalStressResult
147
+ The maximal stresses in the system at the touchdown distance.
148
+ system: SystemModel
149
+ The modified system model used for the steady state evaluation.
113
150
  """
114
151
 
115
152
  converged: bool
116
153
  message: str
117
154
  touchdown_distance: float
118
- SSERR: float
155
+ energy_release_rate: float
156
+ maximal_stress_result: MaximalStressResult
157
+ system: SystemModel
119
158
 
120
159
 
121
160
  @dataclass
@@ -641,12 +680,12 @@ class CriteriaEvaluator:
641
680
  _recursion_depth=_recursion_depth + 1,
642
681
  )
643
682
 
644
- def evaluate_SSERR(
683
+ def evaluate_SteadyState(
645
684
  self,
646
685
  system: SystemModel,
647
686
  vertical: bool = False,
648
687
  print_call_stats: bool = False,
649
- ) -> SSERRResult:
688
+ ) -> SteadyStateResult:
650
689
  """
651
690
  Evaluates the Touchdown Distance in the Steady State and the Steady State
652
691
  Energy Release Rate.
@@ -671,9 +710,9 @@ class CriteriaEvaluator:
671
710
  UserWarning,
672
711
  )
673
712
  system_copy = copy.deepcopy(system)
674
- system_copy.config.touchdown = True
713
+ system_copy.toggle_touchdown(True)
675
714
  system_copy.update_scenario(scenario_config=ScenarioConfig(phi=0.0))
676
- l_BC = system.slab_touchdown.l_BC
715
+ l_BC = system_copy.slab_touchdown.l_BC
677
716
 
678
717
  segments = [
679
718
  Segment(length=5e3, has_foundation=True, m=0.0),
@@ -684,16 +723,22 @@ class CriteriaEvaluator:
684
723
  phi=0.0, # Slab Touchdown works only for flat slab
685
724
  cut_length=2 * l_BC,
686
725
  )
687
- # system_copy.config.touchdown = True
688
726
  system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
689
727
  touchdown_distance = system_copy.slab_touchdown.touchdown_distance
690
728
  analyzer = Analyzer(system_copy, printing_enabled=print_call_stats)
691
- G, _, _ = analyzer.differential_ERR(unit="J/m^2")
692
- return SSERRResult(
729
+ energy_release_rate, _, _ = analyzer.differential_ERR(unit="J/m^2")
730
+ maximal_stress_result = self._calculate_maximal_stresses(
731
+ system_copy, print_call_stats=print_call_stats
732
+ )
733
+ if print_call_stats:
734
+ analyzer.print_call_stats(message="evaluate_SteadyState Call Statistics")
735
+ return SteadyStateResult(
693
736
  converged=True,
694
- message="SSERR evaluation successful.",
737
+ message="Steady State evaluation successful.",
695
738
  touchdown_distance=touchdown_distance,
696
- SSERR=G,
739
+ energy_release_rate=energy_release_rate,
740
+ maximal_stress_result=maximal_stress_result,
741
+ system=system_copy,
697
742
  )
698
743
 
699
744
  def find_minimum_force(
@@ -1170,3 +1215,55 @@ class CriteriaEvaluator:
1170
1215
 
1171
1216
  # Return the difference from the target
1172
1217
  return g_delta_diff - target
1218
+
1219
+ def _calculate_maximal_stresses(
1220
+ self,
1221
+ system: SystemModel,
1222
+ print_call_stats: bool = False,
1223
+ ) -> MaximalStressResult:
1224
+ """
1225
+ Calculate the maximal stresses in the system.
1226
+
1227
+ Parameters
1228
+ ----------
1229
+ system : SystemModel
1230
+ The system model to analyze.
1231
+ print_call_stats : bool, optional
1232
+ Whether to print analyzer call statistics. Default is False.
1233
+
1234
+ Returns
1235
+ -------
1236
+ MaximalStressResult
1237
+ Object containing both absolute (in kPa) and normalized stress fields,
1238
+ along with maximum normalized stress values.
1239
+ """
1240
+ analyzer = Analyzer(system, printing_enabled=print_call_stats)
1241
+ _, Z, _ = analyzer.rasterize_solution(num=4000, mode="cracked")
1242
+ Sxx_kPa = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=5, unit="kPa")
1243
+ principal_stress_kPa = analyzer.principal_stress_slab(
1244
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa"
1245
+ )
1246
+ Sxx_norm = analyzer.Sxx(
1247
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1248
+ )
1249
+ principal_stress_norm = analyzer.principal_stress_slab(
1250
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1251
+ )
1252
+ max_principal_stress_norm = np.max(principal_stress_norm)
1253
+ max_Sxx_norm = np.max(Sxx_norm)
1254
+ # evaluate for each height level if the slab is prone to fail under tensile stresses
1255
+ height_level_prone_to_fail = np.max(Sxx_norm, axis=1)
1256
+ slab_tensile_criterion = np.mean(height_level_prone_to_fail)
1257
+ if print_call_stats:
1258
+ analyzer.print_call_stats(
1259
+ message="_calculate_maximal_stresses Call Statistics"
1260
+ )
1261
+ return MaximalStressResult(
1262
+ principal_stress_kPa=principal_stress_kPa,
1263
+ Sxx_kPa=Sxx_kPa,
1264
+ principal_stress_norm=principal_stress_norm,
1265
+ Sxx_norm=Sxx_norm,
1266
+ max_principal_stress_norm=max_principal_stress_norm,
1267
+ max_Sxx_norm=max_Sxx_norm,
1268
+ slab_tensile_criterion=slab_tensile_criterion,
1269
+ )
weac/analysis/plotter.py CHANGED
@@ -885,48 +885,56 @@ class Plotter:
885
885
  zmax = np.max(Zsl + scale * Wsl) + pad
886
886
  zmin = np.min(Zsl) - pad
887
887
 
888
- # Compute weak-layer grid coordinates (cm)
889
- Xwl, Zwl = np.meshgrid(1e-1 * xwl, [1e-1 * (zi[-1] + H / 2), zmax])
888
+ # Filter out NaN values from weak layer coordinates
889
+ nanmask = np.isfinite(xwl)
890
+ xwl_finite = xwl[nanmask]
890
891
 
891
- # Assemble weak-layer displacement field (top and bottom)
892
- Uwl = np.vstack([Usl[-1, :], np.zeros(xwl.shape[0])])
893
- Wwl = np.vstack([Wsl[-1, :], np.zeros(xwl.shape[0])])
892
+ # Compute weak-layer grid coordinates (cm) - only for finite xwl
893
+ Xwl, Zwl = np.meshgrid(1e-1 * xwl_finite, [1e-1 * (zi[-1] + H / 2), zmax])
894
+
895
+ # Assemble weak-layer displacement field (top and bottom) - only for finite xwl
896
+ Uwl = np.vstack([Usl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
897
+ Wwl = np.vstack([Wsl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
894
898
 
895
899
  # Compute stress or displacement fields
896
900
  match field:
897
901
  # Horizontal displacements (um)
898
902
  case "u":
899
903
  slab = 1e4 * Usl
900
- weak = 1e4 * Usl[-1, :]
904
+ weak = 1e4 * Usl[-1, nanmask]
901
905
  label = r"$u$ ($\mu$m)"
902
906
  # Vertical deflection (um)
903
907
  case "w":
904
908
  slab = 1e4 * Wsl
905
- weak = 1e4 * Wsl[-1, :]
909
+ weak = 1e4 * Wsl[-1, nanmask]
906
910
  label = r"$w$ ($\mu$m)"
907
- # Axial normal stresses (kPa)
908
911
  case "Sxx":
909
- slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa")
910
- weak = np.zeros(xwl.shape[0])
911
- label = r"$\sigma_{xx}$ (kPa)"
912
+ slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa", normalize=normalize)
913
+ weak = np.zeros(xwl_finite.shape[0])
914
+ label = (
915
+ r"$\sigma_{xx}/\sigma_+$" if normalize else r"$\sigma_{xx}$ (kPa)"
916
+ )
912
917
  # Shear stresses (kPa)
913
918
  case "Txz":
914
- slab = analyzer.Txz(z, phi, dz=dz, unit="kPa")
915
- weak = Tauwl
916
- label = r"$\tau_{xz}$ (kPa)"
919
+ slab = analyzer.Txz(z, phi, dz=dz, unit="kPa", normalize=normalize)
920
+ weak = Tauwl[nanmask]
921
+ label = r"$\tau_{xz}/\sigma_+$" if normalize else r"$\tau_{xz}$ (kPa)"
917
922
  # Transverse normal stresses (kPa)
918
923
  case "Szz":
919
- slab = analyzer.Szz(z, phi, dz=dz, unit="kPa")
920
- weak = Sigmawl
921
- label = r"$\sigma_{zz}$ (kPa)"
924
+ slab = analyzer.Szz(z, phi, dz=dz, unit="kPa", normalize=normalize)
925
+ weak = Sigmawl[nanmask]
926
+ label = (
927
+ r"$\sigma_{zz}/\sigma_+$" if normalize else r"$\sigma_{zz}$ (kPa)"
928
+ )
922
929
  # Principal stresses
923
930
  case "principal":
924
931
  slab = analyzer.principal_stress_slab(
925
932
  z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
926
933
  )
927
- weak = analyzer.principal_stress_weaklayer(
934
+ weak_full = analyzer.principal_stress_weaklayer(
928
935
  z, val="min", unit="kPa", normalize=normalize
929
936
  )
937
+ weak = weak_full[nanmask]
930
938
  if normalize:
931
939
  label = (
932
940
  r"$\sigma_\mathrm{I}/\sigma_+$ (slab), "
@@ -937,11 +945,6 @@ class Plotter:
937
945
  r"$\sigma_\mathrm{I}$ (kPa, slab), "
938
946
  r"$\sigma_\mathrm{I\!I\!I}$ (kPa, weak layer)"
939
947
  )
940
- case _:
941
- raise ValueError(
942
- f"Invalid input '{field}' for field. Valid options are "
943
- "'u', 'w', 'Sxx', 'Txz', 'Szz', or 'principal'"
944
- )
945
948
 
946
949
  # Complement label
947
950
  label += r" $\longrightarrow$"
@@ -968,10 +971,9 @@ class Plotter:
968
971
 
969
972
  # Plot deformed weak-layer _outline
970
973
  if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
971
- nanmask = np.isfinite(xwl)
972
974
  ax.plot(
973
- _outline(Xwl[:, nanmask] + scale * Uwl[:, nanmask]),
974
- _outline(Zwl[:, nanmask] + scale * Wwl[:, nanmask]),
975
+ _outline(Xwl + scale * Uwl),
976
+ _outline(Zwl + scale * Wwl),
975
977
  "k",
976
978
  linewidth=1,
977
979
  )
@@ -1033,6 +1035,369 @@ class Plotter:
1033
1035
 
1034
1036
  return fig
1035
1037
 
1038
+ def plot_visualize_deformation(
1039
+ self,
1040
+ xsl: np.ndarray,
1041
+ xwl: np.ndarray,
1042
+ z: np.ndarray,
1043
+ analyzer: Analyzer,
1044
+ window: float | None = None,
1045
+ weaklayer_proportion: float | None = None,
1046
+ dz: int = 2,
1047
+ levels: int = 300,
1048
+ field: Literal["w", "u", "principal", "Sxx", "Txz", "Szz"] = "w",
1049
+ normalize: bool = True,
1050
+ filename: str = "visualize_deformation",
1051
+ ) -> Figure:
1052
+ """
1053
+ Plot visualize deformation of the slab and weak layer.
1054
+
1055
+ Parameters
1056
+ ----------
1057
+ xsl : np.ndarray
1058
+ Slab x-coordinates.
1059
+ xwl : np.ndarray
1060
+ Weak layer x-coordinates.
1061
+ z : np.ndarray
1062
+ Solution vector.
1063
+ analyzer : Analyzer
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.
1067
+ weaklayer_proportion: float | None, optional
1068
+ Proportion of the plot to allocate to the weak layer. Default is None.
1069
+ dz : int, optional
1070
+ Element size along z-axis (mm). Default is 2 mm.
1071
+ levels : int, optional
1072
+ Number of levels for the colormap. Default is 300.
1073
+ field : str, optional
1074
+ Field to plot ('w', 'u', 'principal', 'Sxx', 'Txz', 'Szz'). Default is 'w'.
1075
+ normalize : bool, optional
1076
+ Toggle normalization. Default is True.
1077
+ filename : str, optional
1078
+ Filename for saving plot. Default is "visualize_deformation".
1079
+
1080
+ Returns
1081
+ -------
1082
+ matplotlib.figure.Figure
1083
+ The generated plot figure.
1084
+ """
1085
+ fig = plt.figure(figsize=(10, 8))
1086
+ ax = fig.add_subplot(111)
1087
+
1088
+ zi = analyzer.get_zmesh(dz=dz)["z"]
1089
+ H = analyzer.sm.slab.H
1090
+ phi = analyzer.sm.scenario.phi
1091
+ system_type = analyzer.sm.scenario.system_type
1092
+ fq = analyzer.sm.fq
1093
+ sigma_comp = (
1094
+ analyzer.sm.weak_layer.sigma_comp
1095
+ ) # Compressive strength of the weak layer [kPa]
1096
+
1097
+ # Compute slab displacements on grid (cm)
1098
+ Usl = np.vstack([fq.u(z, h0=h0, unit="cm") for h0 in zi])
1099
+ Wsl = np.vstack([fq.w(z, unit="cm") for _ in zi])
1100
+ Sigmawl = np.where(np.isfinite(xwl), fq.sig(z, unit="kPa"), np.nan)
1101
+ Tauwl = np.where(np.isfinite(xwl), fq.tau(z, unit="kPa"), np.nan)
1102
+
1103
+ # Put coordinate origin at horizontal center
1104
+ if system_type in ["skier", "skiers"]:
1105
+ xsl = xsl - max(xsl) / 2
1106
+ xwl = xwl - max(xwl) / 2
1107
+
1108
+ # Physical dimensions in cm
1109
+ H_cm = H * 1e-1 # Slab height in cm
1110
+ h_cm = analyzer.sm.weak_layer.h * 1e-1 # Weak layer height in cm
1111
+ crack_h_cm = analyzer.sm.scenario.crack_h * 1e-1 # Crack height in cm
1112
+
1113
+ # Compute slab grid coordinates with vertical origin at top surface (cm)
1114
+ Xsl, Zsl = np.meshgrid(1e-1 * (xsl), 1e-1 * (zi + H / 2))
1115
+
1116
+ # Calculate maximum displacement first (needed for proportion calculation)
1117
+ max_w_displacement = np.nanmax(np.abs(Wsl))
1118
+
1119
+ # Calculate dynamic proportions based on displacement
1120
+ # Weak layer percentage = weak_layer_height / max_displacement (as ratio)
1121
+ # But capped at 40% maximum
1122
+ if weaklayer_proportion is None:
1123
+ if max_w_displacement > 0:
1124
+ weaklayer_proportion = min(0.3, (h_cm / max_w_displacement) * 0.1)
1125
+ else:
1126
+ weaklayer_proportion = 0.3
1127
+
1128
+ # Slab takes the remaining space
1129
+ slab_proportion = 1.0 - weaklayer_proportion
1130
+ cracked_ratio = crack_h_cm / h_cm
1131
+ cracked_proportion = weaklayer_proportion * cracked_ratio
1132
+
1133
+ # Set up plot coordinate system
1134
+ # Plot height is normalized: slab (0 to slab_proportion), weak layer (slab_proportion to slab_proportion+weaklayer_proportion)
1135
+ total_height_plot = (
1136
+ slab_proportion + weaklayer_proportion
1137
+ ) # Total height without displacement
1138
+ # Map physical dimensions to plot coordinates
1139
+ deformation_scale = weaklayer_proportion / h_cm
1140
+
1141
+ # Get x-axis limits spanning all provided x values (deformed and undeformed)
1142
+ xmax = np.max([np.max(Xsl), np.max(Xsl + deformation_scale * Usl)]) + 10.0
1143
+ xmin = np.min([np.min(Xsl), np.min(Xsl + deformation_scale * Usl)]) - 10.0
1144
+
1145
+ # Calculate zmax including maximum deformation
1146
+ zmax = total_height_plot
1147
+
1148
+ # Convert physical coordinates to plot coordinates for slab
1149
+ # Zsl is in cm, we need to map it to plot coordinates (0 to slab_proportion)
1150
+ Zsl_plot = (Zsl / H_cm) * slab_proportion
1151
+
1152
+ # Filter out NaN values from weak layer coordinates
1153
+ nanmask = np.isfinite(xwl)
1154
+ xwl_finite = xwl[nanmask]
1155
+
1156
+ # Compute weak-layer grid coordinates in plot units
1157
+ # Weak layer extends from bottom of slab (slab_proportion) to total height (1.0)
1158
+ Xwl, Zwl_plot = np.meshgrid(
1159
+ 1e-1 * xwl_finite, [slab_proportion, total_height_plot]
1160
+ )
1161
+
1162
+ # Assemble weak-layer displacement field (top and bottom) - only for finite xwl
1163
+ Uwl = np.vstack([Usl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
1164
+ Wwl = np.vstack([Wsl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
1165
+
1166
+ # Convert slab displacements to plot coordinates
1167
+ # Scale factor for displacements:
1168
+ # So scaled displacement in plot units = scale * Wsl
1169
+ Wsl_plot = (
1170
+ deformation_scale * Wsl
1171
+ ) # Already in plot units (proportion of total height)
1172
+ Usl_plot = deformation_scale * Usl # Horizontal displacements also scaled
1173
+ Wwl_plot = deformation_scale * Wwl # Weak layer displacements
1174
+ Uwl_plot = deformation_scale * Uwl # Weak layer horizontal displacements
1175
+
1176
+ # Compute stress or displacement fields
1177
+ match field:
1178
+ # Horizontal displacements (um)
1179
+ case "u":
1180
+ slab = 1e4 * Usl
1181
+ weak = 1e4 * Usl[-1, nanmask]
1182
+ label = r"$u$ ($\mu$m)"
1183
+ # Vertical deflection (um)
1184
+ case "w":
1185
+ slab = 1e4 * Wsl
1186
+ weak = 1e4 * Wsl[-1, nanmask]
1187
+ label = r"$w$ ($\mu$m)"
1188
+ # Axial normal stresses (kPa)
1189
+ case "Sxx":
1190
+ slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa", normalize=normalize)
1191
+ weak = np.zeros(xwl_finite.shape[0])
1192
+ label = (
1193
+ r"$\sigma_{xx}/\sigma_+$" if normalize else r"$\sigma_{xx}$ (kPa)"
1194
+ )
1195
+ # Shear stresses (kPa)
1196
+ case "Txz":
1197
+ slab = analyzer.Txz(z, phi, dz=dz, unit="kPa", normalize=normalize)
1198
+ weak = Tauwl[nanmask]
1199
+ label = r"$\tau_{xz}/\sigma_+$" if normalize else r"$\tau_{xz}$ (kPa)"
1200
+ # Transverse normal stresses (kPa)
1201
+ case "Szz":
1202
+ slab = analyzer.Szz(z, phi, dz=dz, unit="kPa", normalize=normalize)
1203
+ weak = Sigmawl[nanmask]
1204
+ label = (
1205
+ r"$\sigma_{zz}/\sigma_+$" if normalize else r"$\sigma_{zz}$ (kPa)"
1206
+ )
1207
+ # Principal stresses
1208
+ case "principal":
1209
+ slab = analyzer.principal_stress_slab(
1210
+ z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
1211
+ )
1212
+ weak_full = analyzer.principal_stress_weaklayer(
1213
+ z, sc=sigma_comp, val="min", unit="kPa", normalize=normalize
1214
+ )
1215
+ weak = weak_full[nanmask]
1216
+ if normalize:
1217
+ label = (
1218
+ r"$\sigma_\mathrm{I}/\sigma_+$ (slab), "
1219
+ r"$\sigma_\mathrm{I\!I\!I}/\sigma_-$ (weak layer)"
1220
+ )
1221
+ else:
1222
+ label = (
1223
+ r"$\sigma_\mathrm{I}$ (kPa, slab), "
1224
+ r"$\sigma_\mathrm{I\!I\!I}$ (kPa, weak layer)"
1225
+ )
1226
+
1227
+ # Complement label
1228
+ label += r" $\longrightarrow$"
1229
+
1230
+ # Assemble weak-layer output on grid
1231
+ weak = np.vstack([weak, weak])
1232
+
1233
+ # Normalize colormap
1234
+ absmax = np.nanmax(np.abs([slab.min(), slab.max(), weak.min(), weak.max()]))
1235
+ clim = np.round(absmax, _significant_digits(absmax))
1236
+ levels = np.linspace(-clim, clim, num=levels + 1, endpoint=True)
1237
+
1238
+ # Plot baseline
1239
+ ax.axhline(zmax, color="k", linewidth=1)
1240
+
1241
+ # Plot outlines of the undeformed and deformed slab (using plot coordinates)
1242
+ ax.plot(_outline(Xsl), _outline(Zsl_plot), "--", alpha=0.3, linewidth=1)
1243
+ ax.plot(
1244
+ _outline(Xsl + Usl_plot),
1245
+ _outline(Zsl_plot + Wsl_plot),
1246
+ "-",
1247
+ linewidth=1,
1248
+ color="k",
1249
+ )
1250
+
1251
+ # Plot cracked weak-layer outline (where there is no weak layer)
1252
+ xwl_cracked = xsl[~nanmask]
1253
+ Xwl_cracked, Zwl_cracked_plot = np.meshgrid(
1254
+ 1e-1 * xwl_cracked,
1255
+ [slab_proportion + cracked_proportion, total_height_plot],
1256
+ )
1257
+ # No displacements for the cracked weak layer outline (undeformed)
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
+ )
1266
+
1267
+ # Then plot the deformed weak-layer outline where it exists
1268
+ if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
1269
+ ax.plot(
1270
+ _outline(Xwl + Uwl_plot),
1271
+ _outline(Zwl_plot + Wwl_plot),
1272
+ "k",
1273
+ linewidth=1,
1274
+ )
1275
+
1276
+ cmap = plt.get_cmap("RdBu_r")
1277
+ cmap.set_over(_adjust_lightness(cmap(1.0), 0.9))
1278
+ cmap.set_under(_adjust_lightness(cmap(0.0), 0.9))
1279
+
1280
+ # Plot fields (using plot coordinates)
1281
+ ax.contourf(
1282
+ Xsl + Usl_plot,
1283
+ Zsl_plot + Wsl_plot,
1284
+ slab,
1285
+ levels=levels,
1286
+ cmap=cmap,
1287
+ extend="both",
1288
+ )
1289
+ ax.contourf(
1290
+ Xwl + Uwl_plot,
1291
+ Zwl_plot + Wwl_plot,
1292
+ weak,
1293
+ levels=levels,
1294
+ cmap=cmap,
1295
+ extend="both",
1296
+ )
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
+ )
1306
+
1307
+ # Plot setup
1308
+ # Set y-limits to match plot coordinate system (0 to total_height_plot = 1.0)
1309
+ plot_ymin = -0.1
1310
+ plot_ymax = (
1311
+ total_height_plot # Should be 1.0 (slab_proportion + weaklayer_proportion)
1312
+ )
1313
+
1314
+ # Set limits first, then aspect ratio to avoid matplotlib adjusting limits
1315
+ if window is None:
1316
+ ax.set_xlim([xmin, xmax])
1317
+ else:
1318
+ ax.set_xlim([xmax - window, xmax])
1319
+ ax.set_ylim([plot_ymin, plot_ymax])
1320
+ ax.invert_yaxis()
1321
+ ax.use_sticky_edges = False
1322
+
1323
+ # Hide the default y-axis on main axis (we'll use custom axes)
1324
+ ax.yaxis.set_visible(False)
1325
+
1326
+ # Set up dual y-axes
1327
+ # Right axis: slab height in cm (0 at top, H_cm at bottom of slab)
1328
+ ax_right = ax.twinx()
1329
+ slab_height_max = H_cm
1330
+ # Map plot coordinates to physical slab height values
1331
+ # Plot: 0 to slab_proportion (0.6) maps to physical: 0 to H_cm
1332
+ slab_height_ticks = np.linspace(0, slab_height_max, num=5)
1333
+ slab_height_positions_plot = (
1334
+ slab_height_ticks / slab_height_max
1335
+ ) * slab_proportion
1336
+ ax_right.set_yticks(slab_height_positions_plot)
1337
+ ax_right.set_yticklabels([f"{tick:.1f}" for tick in slab_height_ticks])
1338
+ # Ensure right axis ticks and label are on the right side
1339
+ ax_right.yaxis.tick_right()
1340
+ ax_right.yaxis.set_label_position("right")
1341
+ ax_right.set_ylim([plot_ymin, plot_ymax])
1342
+ ax_right.invert_yaxis()
1343
+ ax_right.set_ylabel(
1344
+ r"slab depth [cm] $\longleftarrow$", rotation=90, labelpad=5, loc="top"
1345
+ )
1346
+
1347
+ # Left axis: weak layer height in mm (0 at bottom of slab, h at bottom of weak layer)
1348
+ ax_left = ax.twinx()
1349
+ weak_layer_h_mm = analyzer.sm.weak_layer.h
1350
+ # Map plot coordinates to physical weak layer height values
1351
+ # Plot: slab_proportion (0.6) to total_height_plot (1.0) maps to physical: 0 to h_mm
1352
+ weaklayer_height_ticks = np.linspace(0, weak_layer_h_mm, num=3)
1353
+ # Map from plot coordinates (slab_proportion to 1.0) to physical (0 to h_mm)
1354
+ weaklayer_height_positions_plot = (
1355
+ slab_proportion
1356
+ + (weaklayer_height_ticks / weak_layer_h_mm) * weaklayer_proportion
1357
+ )
1358
+ ax_left.set_yticks(weaklayer_height_positions_plot)
1359
+ ax_left.set_yticklabels([f"{tick:.1f}" for tick in weaklayer_height_ticks])
1360
+ # Move left axis to the left side
1361
+ ax_left.yaxis.tick_left()
1362
+ ax_left.yaxis.set_label_position("left")
1363
+ ax_left.set_ylim([plot_ymin, plot_ymax])
1364
+ ax_left.invert_yaxis()
1365
+ ax_left.set_ylabel(
1366
+ r"weaklayer depth [mm] $\longleftarrow$",
1367
+ rotation=90,
1368
+ labelpad=5,
1369
+ loc="bottom",
1370
+ )
1371
+
1372
+ # Plot labels
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
+ )
1378
+
1379
+ # Show colorbar
1380
+ ticks = np.linspace(levels[0], levels[-1], num=11, endpoint=True)
1381
+ fig.colorbar(
1382
+ ax.contourf(
1383
+ Xsl + Usl_plot,
1384
+ Zsl_plot + Wsl_plot,
1385
+ slab,
1386
+ levels=levels,
1387
+ cmap=cmap,
1388
+ extend="both",
1389
+ ),
1390
+ orientation="horizontal",
1391
+ ticks=ticks,
1392
+ label=label,
1393
+ aspect=35,
1394
+ )
1395
+
1396
+ # Save figure
1397
+ self._save_figure(filename, fig)
1398
+
1399
+ return fig
1400
+
1036
1401
  def plot_stress_envelope(
1037
1402
  self,
1038
1403
  system_model: SystemModel,
weac/components/layer.py CHANGED
@@ -94,6 +94,10 @@ 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)
99
+
100
+
97
101
  class Layer(BaseModel):
98
102
  """
99
103
  Regular slab layer (no foundation springs).
@@ -225,6 +229,9 @@ class WeakLayer(BaseModel):
225
229
  )
226
230
  sigma_c: float = Field(default=6.16, gt=0, description="Tensile strength [kPa]")
227
231
  tau_c: float = Field(default=5.09, gt=0, description="Shear strength [kPa]")
232
+ sigma_comp: float = Field(
233
+ default=2.6, gt=0, description="Compressive strength [kPa]"
234
+ )
228
235
  E_method: Literal["bergfeld", "scapazzo", "gerling"] = Field(
229
236
  default="bergfeld",
230
237
  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, :]
@@ -273,17 +273,17 @@ class UnknownConstantsSolver:
273
273
  Arguments
274
274
  ---------
275
275
  zl : ndarray
276
- Solution vector (6x1) or (6x6) at left end of beam segement.
276
+ Solution vector (6x1) or (6x6) at left end of beam segment.
277
277
  zr : ndarray
278
- Solution vector (6x1) or (6x6) at right end of beam segement.
278
+ Solution vector (6x1) or (6x6) at right end of beam segment.
279
279
  has_foundation : boolean
280
280
  Indicates whether segment has foundation(True) or not (False).
281
281
  Default is False.
282
282
  pos: {'left', 'mid', 'right', 'l', 'm', 'r'}, optional
283
- Determines whether the segement under consideration
284
- is a left boundary segement (left, l), one of the
285
- center segement (mid, m), or a right boundary
286
- segement (right, r). Default is 'mid'.
283
+ Determines whether the segment under consideration
284
+ is a left boundary segment (left, l), one of the
285
+ center segment (mid, m), or a right boundary
286
+ segment (right, r). Default is 'mid'.
287
287
 
288
288
  Returns
289
289
  -------
@@ -310,7 +310,7 @@ class UnknownConstantsSolver:
310
310
  bcs[2],
311
311
  fq.u(zr, h0=0), # ui(xi = li)
312
312
  fq.w(zr), # wi(xi = li)
313
- fq.psi(zr), # psii(xi = li)
313
+ fq.psi(zr), # psi(xi = li)
314
314
  fq.N(zr), # Ni(xi = li)
315
315
  fq.M(zr), # Mi(xi = li)
316
316
  fq.V(zr), # Vi(xi = li)
@@ -321,13 +321,13 @@ class UnknownConstantsSolver:
321
321
  [
322
322
  -fq.u(zl, h0=0), # -ui(xi = 0)
323
323
  -fq.w(zl), # -wi(xi = 0)
324
- -fq.psi(zl), # -psii(xi = 0)
324
+ -fq.psi(zl), # -psi(xi = 0)
325
325
  -fq.N(zl), # -Ni(xi = 0)
326
326
  -fq.M(zl), # -Mi(xi = 0)
327
327
  -fq.V(zl), # -Vi(xi = 0)
328
328
  fq.u(zr, h0=0), # ui(xi = li)
329
329
  fq.w(zr), # wi(xi = li)
330
- fq.psi(zr), # psii(xi = li)
330
+ fq.psi(zr), # psi(xi = li)
331
331
  fq.N(zr), # Ni(xi = li)
332
332
  fq.M(zr), # Mi(xi = li)
333
333
  fq.V(zr), # Vi(xi = li)
@@ -347,7 +347,7 @@ class UnknownConstantsSolver:
347
347
  [
348
348
  -fq.u(zl, h0=0), # -ui(xi = 0)
349
349
  -fq.w(zl), # -wi(xi = 0)
350
- -fq.psi(zl), # -psii(xi = 0)
350
+ -fq.psi(zl), # -psi(xi = 0)
351
351
  -fq.N(zl), # -Ni(xi = 0)
352
352
  -fq.M(zl), # -Mi(xi = 0)
353
353
  -fq.V(zl), # -Vi(xi = 0)
@@ -385,9 +385,9 @@ class UnknownConstantsSolver:
385
385
  Default is False.
386
386
  pos : {'left', 'mid', 'right', 'l', 'm', 'r'}, optional
387
387
  Determines whether the segement under consideration
388
- is a left boundary segement (left, l), one of the
389
- center segement (mid, m), or a right boundary
390
- segement (right, r). Default is 'mid'.
388
+ is a left boundary segment (left, l), one of the
389
+ center segment (mid, m), or a right boundary
390
+ segment (right, r). Default is 'mid'.
391
391
 
392
392
  Returns
393
393
  -------
weac/utils/geldsetzer.py CHANGED
@@ -82,18 +82,23 @@ HAND_HARDNESS = {
82
82
  "F-": 0.67,
83
83
  "F": 1,
84
84
  "F+": 1.33,
85
+ "F-4F": 1.5,
85
86
  "4F-": 1.67,
86
87
  "4F": 2,
87
88
  "4F+": 2.33,
89
+ "4F-1F": 2.5,
88
90
  "1F-": 2.67,
89
91
  "1F": 3,
90
92
  "1F+": 3.33,
93
+ "1F-P": 3.5,
91
94
  "P-": 3.67,
92
95
  "P": 4,
93
96
  "P+": 4.33,
97
+ "P-K": 4.5,
94
98
  "K-": 4.67,
95
99
  "K": 5,
96
100
  "K+": 5.33,
101
+ "K-I": 5.5,
97
102
  "I-": 5.67,
98
103
  "I": 6,
99
104
  "I+": 6.33,
@@ -117,18 +122,23 @@ HAND_HARDNESS_TO_DENSITY = {
117
122
  "F-": 71.7,
118
123
  "F": 103.7,
119
124
  "F+": 118.4,
125
+ "F-4F": 123.15,
120
126
  "4F-": 127.9,
121
127
  "4F": 158.2,
122
128
  "4F+": 163.7,
129
+ "4F-1F": 176.15,
123
130
  "1F-": 188.6,
124
131
  "1F": 208,
125
132
  "1F+": 224.4,
133
+ "1F-P": 238.6,
126
134
  "P-": 252.8,
127
135
  "P": 275.9,
128
136
  "P+": 314.6,
137
+ "P-K": 336.85,
129
138
  "K-": 359.1,
130
139
  "K": 347.4,
131
140
  "K+": 407.8,
141
+ "K-I": 407.8,
132
142
  "I-": 407.8,
133
143
  "I": 407.8,
134
144
  "I+": 407.8,
weac/utils/snow_types.py CHANGED
@@ -65,18 +65,23 @@ class HandHardness(str, Enum):
65
65
  Fm = "F-"
66
66
  F = "F"
67
67
  Fp = "F+"
68
+ F_4F = "F-4F"
68
69
  _4Fm = "4F-"
69
70
  _4F = "4F"
70
71
  _4Fp = "4F+"
72
+ _4F_1F = "4F-1F"
71
73
  _1Fm = "1F-"
72
74
  _1F = "1F"
73
75
  _1Fp = "1F+"
76
+ _1F_P = "1F-P"
74
77
  Pm = "P-"
75
78
  P = "P"
76
79
  Pp = "P+"
80
+ P_K = "P-K"
77
81
  Km = "K-"
78
82
  K = "K"
79
83
  Kp = "K+"
84
+ K_I = "K-I"
80
85
  Im = "I-"
81
86
  I = "I"
82
87
  Ip = "I+"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weac
3
- Version: 3.0.2
3
+ Version: 3.1.1
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
@@ -1,32 +1,32 @@
1
- weac/__init__.py,sha256=mOpFbjyMROJ4BdyLMOUvSfVmzVGLZTYXeIqvGZXGtl4,76
1
+ weac/__init__.py,sha256=kgTwOD6mYTXI3qvj9z5uFDNJH84Toa29jQuKAu-5P_M,76
2
2
  weac/constants.py,sha256=BX8ifZhFciCuzzako1p-2Wh5CWWzR3cPdvyu501UtUs,1194
3
3
  weac/logging_config.py,sha256=GJn4Fs80dDRpbPkSPByCmIOLkEI01dbeuMMSv_IDgoM,1103
4
- weac/analysis/__init__.py,sha256=twF9OYrZiVmBtw6Lyzf5QUhLZNtqFFM2H3OuaYCog1k,486
5
- weac/analysis/analyzer.py,sha256=AR1yxM5trK1c-_lTEVn9e6e96IhWVWSRgHgQcaLy4Io,27089
6
- weac/analysis/criteria_evaluator.py,sha256=WY0FQnfzDfN0h1lI5b4LCUV0bwnvUAVDmS4mKCHGbmo,43599
7
- weac/analysis/plotter.py,sha256=6KzqiXWIDTIFE6qB947eXhtvEAuPp8B4P5idoSX8OwY,66120
4
+ weac/analysis/__init__.py,sha256=FDbmQLC5zmT8OJftqNYgihXc8WYB1b9FasFPGzkKVU8,498
5
+ weac/analysis/analyzer.py,sha256=-yoOqsdQvpNvBc-U2nqB4UEKHGTMBu23lVPJqKxUVcs,29095
6
+ weac/analysis/criteria_evaluator.py,sha256=yiKQV6ccgNnDMEky18zlHT3YfvoxdqCQtnPHKhbnrJA,47624
7
+ weac/analysis/plotter.py,sha256=ibkH4rzQ2pqQVXPRfek722e8eU3vP63uysb-Q-EzfC0,80556
8
8
  weac/components/__init__.py,sha256=94WIUVjPI3U-grN_7v0oi9bZB3Z8rCBcx9wJPOwJBAQ,439
9
9
  weac/components/config.py,sha256=tnOnJ0M-_knZBhdr052nDyyFFAZN0f2hQ68XRuXG6d8,869
10
10
  weac/components/criteria_config.py,sha256=f2agU7nXWURFAw8_68igiSk5aICUxwaB9u3Qasit-Q0,2909
11
- weac/components/layer.py,sha256=sF47006ORRmNnHW7QMk9h-M0vQQR6TL_PKOvInjdNsk,10560
11
+ weac/components/layer.py,sha256=kfghjavzCqzzGrkKzllMMT4qD3F0pcYfk1ZHgM6rvE0,10755
12
12
  weac/components/model_input.py,sha256=oyX4p7PgaAtUXXtpvV8XO_KxwOXtG1TB0x7rXxNfCFA,3330
13
13
  weac/components/scenario_config.py,sha256=Tam-m9DQtdjmTm-lKQ8Dcjqje04ttJS2X3v_Nn7AUHQ,2563
14
14
  weac/components/segment.py,sha256=F279KcAAkRuJKWav_BZ4BanO96WZm4KXtKHinFZki7s,941
15
15
  weac/core/__init__.py,sha256=pRyCKD8XD3qXVUWtFG7N3cS91P5x5d8Jpr1hMEgxQ2U,233
16
16
  weac/core/eigensystem.py,sha256=b7KXi2weCY9IVlH_7lCTXzKSx_pLdWY-x8BPjHL5nKo,13736
17
- weac/core/field_quantities.py,sha256=ci7MvhJ4aYdbW6xxH8vHVgWtk5iypCYv6dZ6KjFNvt8,8964
17
+ weac/core/field_quantities.py,sha256=rr0KvM5Eu9AR43CQrrODJJkXkM2b2N3BWk2dE38f64c,9175
18
18
  weac/core/scenario.py,sha256=vHHe-JDWc-8ECVfy6lFGVtJmIIKRc0J9iUEGR8zckdw,6050
19
19
  weac/core/slab.py,sha256=YwbAf7ogYDn3p0nXfj3IjVqwA6ro3jBSg2wXb_io_gQ,5125
20
20
  weac/core/slab_touchdown.py,sha256=k30FyvclH_Q3mCZNTbJkNhSenv3ja6qSNhhkmi5p04A,14009
21
21
  weac/core/system_model.py,sha256=HAZuquY6p1wf5QuUPA6VOVDIICeCxOaeXqk1dSfEzNY,15513
22
- weac/core/unknown_constants_solver.py,sha256=pzE7soTo7v-jdDECa21pOWRGBp8YeMU3k7egJOemfSY,17766
22
+ weac/core/unknown_constants_solver.py,sha256=kzB8351UC03rVcJWez0FAwdawI2tERbpcgqMRKgYbi8,17753
23
23
  weac/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- weac/utils/geldsetzer.py,sha256=DxvpqmWulBacl-mGeAmAuJcv2xqFBx92PEfNC3aeDzk,3500
24
+ weac/utils/geldsetzer.py,sha256=Rmn_PUyeomPLfxZlj9LDbdBBRDWovavrQenka318TIM,3681
25
25
  weac/utils/misc.py,sha256=lGz0IDDJ_3nvYjSkivPJ5Xscl1D_AmvQLSjaL7SUbKs,3674
26
- weac/utils/snow_types.py,sha256=eX9-5La6Oom7zh6pg5JZ4MZ6nLdWdc7RoUzm5e6b9w8,1483
26
+ weac/utils/snow_types.py,sha256=OKSY8LzfKycSjg7uSUZkSXUzwhcw1D1fZs5S3hVQtUM,1573
27
27
  weac/utils/snowpilot_parser.py,sha256=drOLnUg7uzMJMO9qU9x79xZkqQfFPCCQonVpU9S0x8U,13682
28
- weac-3.0.2.dist-info/licenses/LICENSE,sha256=ojZPWKFHbFGDrlNOvuAKGH9WcKhpLHWZPcQ4SzhK91M,1082
29
- weac-3.0.2.dist-info/METADATA,sha256=p-UwzxwB89KHKa1zQIahEoGoymDvmhzRAieD3xzDPt0,25586
30
- weac-3.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- weac-3.0.2.dist-info/top_level.txt,sha256=8tyXUHPFU4Ba_5kPtpwvXo5l6GjJmOnODVBJFygpdeE,5
32
- weac-3.0.2.dist-info/RECORD,,
28
+ weac-3.1.1.dist-info/licenses/LICENSE,sha256=ojZPWKFHbFGDrlNOvuAKGH9WcKhpLHWZPcQ4SzhK91M,1082
29
+ weac-3.1.1.dist-info/METADATA,sha256=0cdfjFenF40KllOBi5gLkyOSkdA3Ko_1LAdX43AlUK4,25593
30
+ weac-3.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ weac-3.1.1.dist-info/top_level.txt,sha256=8tyXUHPFU4Ba_5kPtpwvXo5l6GjJmOnODVBJFygpdeE,5
32
+ weac-3.1.1.dist-info/RECORD,,
File without changes