weac 3.0.2__py3-none-any.whl → 3.1.0__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.0"
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,13 +262,13 @@ 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))
@@ -274,14 +278,22 @@ class Analyzer:
274
278
  # Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
275
279
  # New Implementation: Changed for numerical stability
276
280
  dz = np.diff(zi)
277
- Sxx[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
278
- Sxx[-1, :] += qt[-1] * dz[-1]
281
+ Sxx_MPa[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
282
+ Sxx_MPa[-1, :] += qt[-1] * dz[-1]
283
+
284
+ # Normalize tensile stresses to tensile strength
285
+ if normalize:
286
+ tensile_strength_kPa = zmesh["tensile_strength"]
287
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
288
+ # Normalize axial normal stress to layers' tensile strength
289
+ normalized_Sxx = Sxx_MPa / tensile_strength_MPa[:, None]
290
+ return normalized_Sxx
279
291
 
280
292
  # Return axial normal stress in specified unit
281
- return convert[unit] * Sxx
293
+ return convert[unit] * Sxx_MPa
282
294
 
283
295
  @track_analyzer_call
284
- def Txz(self, Z, phi, dz=2, unit="kPa"):
296
+ def Txz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
285
297
  """
286
298
  Compute shear stress in slab layers.
287
299
 
@@ -295,6 +307,9 @@ class Analyzer:
295
307
  Element size along z-axis (mm). Default is 2 mm.
296
308
  unit : {'kPa', 'MPa'}, optional
297
309
  Desired output unit. Default is 'kPa'.
310
+ normalize : bool, optional
311
+ Toggle normalization. If True, normalize shear stress values to the tensile strength of each layer (dimensionless).
312
+ When normalized, the `unit` parameter is ignored and values are returned as ratios. Default is False.
298
313
 
299
314
  Returns
300
315
  -------
@@ -332,14 +347,22 @@ class Analyzer:
332
347
 
333
348
  # Integrate -dsxx_dx along z and add cumulative weight load
334
349
  # 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]
350
+ Txz_MPa = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
351
+ Txz_MPa += cumulative_trapezoid(qt, zi, initial=0)[:, None]
352
+
353
+ # Normalize shear stresses to tensile strength
354
+ if normalize:
355
+ tensile_strength_kPa = zmesh["tensile_strength"]
356
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
357
+ # Normalize shear stress to layers' tensile strength
358
+ normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
359
+ return normalized_Txz
337
360
 
338
361
  # Return shear stress Txz in specified unit
339
- return convert[unit] * Txz
362
+ return convert[unit] * Txz_MPa
340
363
 
341
364
  @track_analyzer_call
342
- def Szz(self, Z, phi, dz=2, unit="kPa"):
365
+ def Szz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
343
366
  """
344
367
  Compute transverse normal stress in slab layers.
345
368
 
@@ -353,6 +376,10 @@ class Analyzer:
353
376
  Element size along z-axis (mm). Default is 2 mm.
354
377
  unit : {'kPa', 'MPa'}, optional
355
378
  Desired output unit. Default is 'kPa'.
379
+ normalize : bool, optional
380
+ Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
381
+ When normalized, the `unit` parameter is ignored and values are returned as ratios.
382
+ Default is False.
356
383
 
357
384
  Returns
358
385
  -------
@@ -392,11 +419,19 @@ class Analyzer:
392
419
  # Integrate dsxx_dxdx twice along z to obtain transverse
393
420
  # normal stress Szz in MPa
394
421
  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]
422
+ Szz_MPa = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
423
+ Szz_MPa += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
397
424
 
398
- # Return shear stress txz in specified unit
399
- return convert[unit] * Szz
425
+ # Normalize tensile stresses to tensile strength
426
+ if normalize:
427
+ tensile_strength_kPa = zmesh["tensile_strength"]
428
+ tensile_strength_MPa = tensile_strength_kPa / 1e3
429
+ # Normalize transverse normal stress to layers' tensile strength
430
+ normalized_Szz = Szz_MPa / tensile_strength_MPa[:, None]
431
+ return normalized_Szz
432
+
433
+ # Return transverse normal stress Szz in specified unit
434
+ return convert[unit] * Szz_MPa
400
435
 
401
436
  @track_analyzer_call
402
437
  def principal_stress_slab(
@@ -438,6 +473,8 @@ class Analyzer:
438
473
  'min', or if normalization of compressive principal stress
439
474
  is requested.
440
475
  """
476
+ convert = {"kPa": 1e3, "MPa": 1}
477
+
441
478
  # Raise error if specified component is not available
442
479
  if val not in ["min", "max"]:
443
480
  raise ValueError(f"Component {val} not defined.")
@@ -460,9 +497,10 @@ class Analyzer:
460
497
  # Normalize tensile stresses to tensile strength
461
498
  if normalize and val == "max":
462
499
  zmesh = self.get_zmesh(dz=dz)
463
- tensile_strength = zmesh["tensile_strength"]
500
+ tensile_strength_kPa = zmesh["tensile_strength"]
501
+ tensile_strength_converted = tensile_strength_kPa / 1e3 * convert[unit]
464
502
  # Normalize maximum principal stress to layers' tensile strength
465
- normalized_Ps = Ps / tensile_strength[:, None]
503
+ normalized_Ps = Ps / tensile_strength_converted[:, None]
466
504
  return normalized_Ps
467
505
 
468
506
  # Return absolute principal stresses
@@ -95,9 +95,38 @@ 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
+ """
117
+
118
+ principal_stress_kPa: np.ndarray
119
+ Sxx_kPa: np.ndarray
120
+ principal_stress_norm: np.ndarray
121
+ Sxx_norm: np.ndarray
122
+ max_principal_stress_norm: float
123
+ max_Sxx_norm: float
124
+
125
+
126
+ @dataclass
127
+ class SteadyStateResult:
128
+ """
129
+ Holds the results of the Steady State evaluation.
101
130
 
102
131
  Attributes:
103
132
  -----------
@@ -107,15 +136,21 @@ class SSERRResult:
107
136
  The message of the evaluation.
108
137
  touchdown_distance : float
109
138
  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.
139
+ energy_release_rate : float
140
+ The steady-state energy release rate calculated with the
141
+ touchdown distance from the differential energy release rate.
142
+ maximal_stress_result: MaximalStressResult
143
+ The maximal stresses in the system at the touchdown distance.
144
+ system: SystemModel
145
+ The modified system model used for the steady state evaluation.
113
146
  """
114
147
 
115
148
  converged: bool
116
149
  message: str
117
150
  touchdown_distance: float
118
- SSERR: float
151
+ energy_release_rate: float
152
+ maximal_stress_result: MaximalStressResult
153
+ system: SystemModel
119
154
 
120
155
 
121
156
  @dataclass
@@ -641,12 +676,12 @@ class CriteriaEvaluator:
641
676
  _recursion_depth=_recursion_depth + 1,
642
677
  )
643
678
 
644
- def evaluate_SSERR(
679
+ def evaluate_SteadyState(
645
680
  self,
646
681
  system: SystemModel,
647
682
  vertical: bool = False,
648
683
  print_call_stats: bool = False,
649
- ) -> SSERRResult:
684
+ ) -> SteadyStateResult:
650
685
  """
651
686
  Evaluates the Touchdown Distance in the Steady State and the Steady State
652
687
  Energy Release Rate.
@@ -688,12 +723,19 @@ class CriteriaEvaluator:
688
723
  system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
689
724
  touchdown_distance = system_copy.slab_touchdown.touchdown_distance
690
725
  analyzer = Analyzer(system_copy, printing_enabled=print_call_stats)
691
- G, _, _ = analyzer.differential_ERR(unit="J/m^2")
692
- return SSERRResult(
726
+ energy_release_rate, _, _ = analyzer.differential_ERR(unit="J/m^2")
727
+ maximal_stress_result = self._calculate_maximal_stresses(
728
+ system_copy, print_call_stats=print_call_stats
729
+ )
730
+ if print_call_stats:
731
+ analyzer.print_call_stats(message="evaluate_SteadyState Call Statistics")
732
+ return SteadyStateResult(
693
733
  converged=True,
694
- message="SSERR evaluation successful.",
734
+ message="Steady State evaluation successful.",
695
735
  touchdown_distance=touchdown_distance,
696
- SSERR=G,
736
+ energy_release_rate=energy_release_rate,
737
+ maximal_stress_result=maximal_stress_result,
738
+ system=system_copy,
697
739
  )
698
740
 
699
741
  def find_minimum_force(
@@ -1170,3 +1212,51 @@ class CriteriaEvaluator:
1170
1212
 
1171
1213
  # Return the difference from the target
1172
1214
  return g_delta_diff - target
1215
+
1216
+ def _calculate_maximal_stresses(
1217
+ self,
1218
+ system: SystemModel,
1219
+ print_call_stats: bool = False,
1220
+ ) -> MaximalStressResult:
1221
+ """
1222
+ Calculate the maximal stresses in the system.
1223
+
1224
+ Parameters
1225
+ ----------
1226
+ system : SystemModel
1227
+ The system model to analyze.
1228
+ print_call_stats : bool, optional
1229
+ Whether to print analyzer call statistics. Default is False.
1230
+
1231
+ Returns
1232
+ -------
1233
+ MaximalStressResult
1234
+ Object containing both absolute (in kPa) and normalized stress fields,
1235
+ along with maximum normalized stress values.
1236
+ """
1237
+ analyzer = Analyzer(system, printing_enabled=print_call_stats)
1238
+ _, Z, _ = analyzer.rasterize_solution(num=4000, mode="cracked")
1239
+ Sxx_kPa = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=5, unit="kPa")
1240
+ principal_stress_kPa = analyzer.principal_stress_slab(
1241
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa"
1242
+ )
1243
+ Sxx_norm = analyzer.Sxx(
1244
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1245
+ )
1246
+ principal_stress_norm = analyzer.principal_stress_slab(
1247
+ Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
1248
+ )
1249
+ max_principal_stress_norm = np.max(principal_stress_norm)
1250
+ max_Sxx_norm = np.max(Sxx_norm)
1251
+ if print_call_stats:
1252
+ analyzer.print_call_stats(
1253
+ message="_calculate_maximal_stresses Call Statistics"
1254
+ )
1255
+ return MaximalStressResult(
1256
+ principal_stress_kPa=principal_stress_kPa,
1257
+ Sxx_kPa=Sxx_kPa,
1258
+ principal_stress_norm=principal_stress_norm,
1259
+ Sxx_norm=Sxx_norm,
1260
+ max_principal_stress_norm=max_principal_stress_norm,
1261
+ max_Sxx_norm=max_Sxx_norm,
1262
+ )
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,354 @@ 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
+ weaklayer_proportion: float | None = None,
1045
+ dz: int = 2,
1046
+ levels: int = 300,
1047
+ field: Literal["w", "u", "principal", "Sxx", "Txz", "Szz"] = "w",
1048
+ normalize: bool = True,
1049
+ filename: str = "visualize_deformation",
1050
+ ) -> Figure:
1051
+ """
1052
+ Plot visualize deformation of the slab and weak layer.
1053
+
1054
+ Parameters
1055
+ ----------
1056
+ xsl : np.ndarray
1057
+ Slab x-coordinates.
1058
+ xwl : np.ndarray
1059
+ Weak layer x-coordinates.
1060
+ z : np.ndarray
1061
+ Solution vector.
1062
+ analyzer : Analyzer
1063
+ Analyzer instance.
1064
+ weaklayer_proportion: float | None, optional
1065
+ Proportion of the plot to allocate to the weak layer. Default is None.
1066
+ dz : int, optional
1067
+ Element size along z-axis (mm). Default is 2 mm.
1068
+ levels : int, optional
1069
+ Number of levels for the colormap. Default is 300.
1070
+ field : str, optional
1071
+ Field to plot ('w', 'u', 'principal', 'Sxx', 'Txz', 'Szz'). Default is 'w'.
1072
+ normalize : bool, optional
1073
+ Toggle normalization. Default is True.
1074
+ filename : str, optional
1075
+ Filename for saving plot. Default is "visualize_deformation".
1076
+
1077
+ Returns
1078
+ -------
1079
+ matplotlib.figure.Figure
1080
+ The generated plot figure.
1081
+ """
1082
+ fig = plt.figure(figsize=(10, 8))
1083
+ ax = fig.add_subplot(111)
1084
+
1085
+ zi = analyzer.get_zmesh(dz=dz)["z"]
1086
+ H = analyzer.sm.slab.H
1087
+ phi = analyzer.sm.scenario.phi
1088
+ system_type = analyzer.sm.scenario.system_type
1089
+ fq = analyzer.sm.fq
1090
+
1091
+ # Compute slab displacements on grid (cm)
1092
+ Usl = np.vstack([fq.u(z, h0=h0, unit="cm") for h0 in zi])
1093
+ Wsl = np.vstack([fq.w(z, unit="cm") for _ in zi])
1094
+ Sigmawl = np.where(np.isfinite(xwl), fq.sig(z, unit="kPa"), np.nan)
1095
+ Tauwl = np.where(np.isfinite(xwl), fq.tau(z, unit="kPa"), np.nan)
1096
+
1097
+ # Put coordinate origin at horizontal center
1098
+ if system_type in ["skier", "skiers"]:
1099
+ xsl = xsl - max(xsl) / 2
1100
+ xwl = xwl - max(xwl) / 2
1101
+
1102
+ # Physical dimensions in cm
1103
+ H_cm = H * 1e-1 # Slab height in cm
1104
+ h_cm = analyzer.sm.weak_layer.h * 1e-1 # Weak layer height in cm
1105
+ crack_h_cm = analyzer.sm.scenario.crack_h * 1e-1 # Crack height in cm
1106
+
1107
+ # Compute slab grid coordinates with vertical origin at top surface (cm)
1108
+ Xsl, Zsl = np.meshgrid(1e-1 * (xsl), 1e-1 * (zi + H / 2))
1109
+
1110
+ # Calculate maximum displacement first (needed for proportion calculation)
1111
+ max_w_displacement = np.nanmax(np.abs(Wsl))
1112
+
1113
+ # Calculate dynamic proportions based on displacement
1114
+ # Weak layer percentage = weak_layer_height / max_displacement (as ratio)
1115
+ # But capped at 40% maximum
1116
+ if weaklayer_proportion is None:
1117
+ if max_w_displacement > 0:
1118
+ weaklayer_proportion = min(0.3, (h_cm / max_w_displacement) * 0.1)
1119
+ else:
1120
+ weaklayer_proportion = 0.3
1121
+
1122
+ # Slab takes the remaining space
1123
+ slab_proportion = 1.0 - weaklayer_proportion
1124
+ cracked_ratio = crack_h_cm / h_cm
1125
+ cracked_proportion = weaklayer_proportion * cracked_ratio
1126
+
1127
+ # Set up plot coordinate system
1128
+ # Plot height is normalized: slab (0 to slab_proportion), weak layer (slab_proportion to slab_proportion+weaklayer_proportion)
1129
+ total_height_plot = (
1130
+ slab_proportion + weaklayer_proportion
1131
+ ) # Total height without displacement
1132
+ # Map physical dimensions to plot coordinates
1133
+ deformation_scale = weaklayer_proportion / h_cm
1134
+
1135
+ # Get x-axis limits spanning all provided x values (deformed and undeformed)
1136
+ xmax = np.max([np.max(Xsl), np.max(Xsl + deformation_scale * Usl)]) + 10.0
1137
+ xmin = np.min([np.min(Xsl), np.min(Xsl + deformation_scale * Usl)]) - 10.0
1138
+
1139
+ # Calculate zmax including maximum deformation
1140
+ zmax = total_height_plot
1141
+
1142
+ # Convert physical coordinates to plot coordinates for slab
1143
+ # Zsl is in cm, we need to map it to plot coordinates (0 to slab_proportion)
1144
+ Zsl_plot = (Zsl / H_cm) * slab_proportion
1145
+
1146
+ # Filter out NaN values from weak layer coordinates
1147
+ nanmask = np.isfinite(xwl)
1148
+ xwl_finite = xwl[nanmask]
1149
+
1150
+ # Compute weak-layer grid coordinates in plot units
1151
+ # Weak layer extends from bottom of slab (slab_proportion) to total height (1.0)
1152
+ Xwl, Zwl_plot = np.meshgrid(
1153
+ 1e-1 * xwl_finite, [slab_proportion, total_height_plot]
1154
+ )
1155
+
1156
+ # Assemble weak-layer displacement field (top and bottom) - only for finite xwl
1157
+ Uwl = np.vstack([Usl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
1158
+ Wwl = np.vstack([Wsl[-1, nanmask], np.zeros(xwl_finite.shape[0])])
1159
+
1160
+ # Convert slab displacements to plot coordinates
1161
+ # Scale factor for displacements:
1162
+ # So scaled displacement in plot units = scale * Wsl
1163
+ Wsl_plot = (
1164
+ deformation_scale * Wsl
1165
+ ) # Already in plot units (proportion of total height)
1166
+ Usl_plot = deformation_scale * Usl # Horizontal displacements also scaled
1167
+ Wwl_plot = deformation_scale * Wwl # Weak layer displacements
1168
+ Uwl_plot = deformation_scale * Uwl # Weak layer horizontal displacements
1169
+
1170
+ # Compute stress or displacement fields
1171
+ match field:
1172
+ # Horizontal displacements (um)
1173
+ case "u":
1174
+ slab = 1e4 * Usl
1175
+ weak = 1e4 * Usl[-1, nanmask]
1176
+ label = r"$u$ ($\mu$m)"
1177
+ # Vertical deflection (um)
1178
+ case "w":
1179
+ slab = 1e4 * Wsl
1180
+ weak = 1e4 * Wsl[-1, nanmask]
1181
+ label = r"$w$ ($\mu$m)"
1182
+ # Axial normal stresses (kPa)
1183
+ case "Sxx":
1184
+ slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa", normalize=normalize)
1185
+ weak = np.zeros(xwl_finite.shape[0])
1186
+ label = (
1187
+ r"$\sigma_{xx}/\sigma_+$" if normalize else r"$\sigma_{xx}$ (kPa)"
1188
+ )
1189
+ # Shear stresses (kPa)
1190
+ case "Txz":
1191
+ slab = analyzer.Txz(z, phi, dz=dz, unit="kPa", normalize=normalize)
1192
+ weak = Tauwl[nanmask]
1193
+ label = r"$\tau_{xz}/\sigma_+$" if normalize else r"$\tau_{xz}$ (kPa)"
1194
+ # Transverse normal stresses (kPa)
1195
+ case "Szz":
1196
+ slab = analyzer.Szz(z, phi, dz=dz, unit="kPa", normalize=normalize)
1197
+ weak = Sigmawl[nanmask]
1198
+ label = (
1199
+ r"$\sigma_{zz}/\sigma_+$" if normalize else r"$\sigma_{zz}$ (kPa)"
1200
+ )
1201
+ # Principal stresses
1202
+ case "principal":
1203
+ slab = analyzer.principal_stress_slab(
1204
+ z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
1205
+ )
1206
+ weak_full = analyzer.principal_stress_weaklayer(
1207
+ z, val="min", unit="kPa", normalize=normalize
1208
+ )
1209
+ weak = weak_full[nanmask]
1210
+ if normalize:
1211
+ label = (
1212
+ r"$\sigma_\mathrm{I}/\sigma_+$ (slab), "
1213
+ r"$\sigma_\mathrm{I\!I\!I}/\sigma_-$ (weak layer)"
1214
+ )
1215
+ else:
1216
+ label = (
1217
+ r"$\sigma_\mathrm{I}$ (kPa, slab), "
1218
+ r"$\sigma_\mathrm{I\!I\!I}$ (kPa, weak layer)"
1219
+ )
1220
+
1221
+ # Complement label
1222
+ label += r" $\longrightarrow$"
1223
+
1224
+ # Assemble weak-layer output on grid
1225
+ weak = np.vstack([weak, weak])
1226
+
1227
+ # Normalize colormap
1228
+ absmax = np.nanmax(np.abs([slab.min(), slab.max(), weak.min(), weak.max()]))
1229
+ clim = np.round(absmax, _significant_digits(absmax))
1230
+ levels = np.linspace(-clim, clim, num=levels + 1, endpoint=True)
1231
+
1232
+ # Plot baseline
1233
+ ax.axhline(zmax, color="k", linewidth=1)
1234
+
1235
+ # Plot outlines of the undeformed and deformed slab (using plot coordinates)
1236
+ ax.plot(_outline(Xsl), _outline(Zsl_plot), "--", alpha=0.3, linewidth=1)
1237
+ ax.plot(
1238
+ _outline(Xsl + Usl_plot),
1239
+ _outline(Zsl_plot + Wsl_plot),
1240
+ "-",
1241
+ linewidth=1,
1242
+ color="k",
1243
+ )
1244
+
1245
+ # Plot cracked weak-layer outline (where there is no weak layer)
1246
+ xwl_cracked = xsl[~nanmask]
1247
+ Xwl_cracked, Zwl_cracked_plot = np.meshgrid(
1248
+ 1e-1 * xwl_cracked,
1249
+ [slab_proportion + cracked_proportion, total_height_plot],
1250
+ )
1251
+ # 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
+ )
1259
+
1260
+ # Then plot the deformed weak-layer outline where it exists
1261
+ if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
1262
+ ax.plot(
1263
+ _outline(Xwl + Uwl_plot),
1264
+ _outline(Zwl_plot + Wwl_plot),
1265
+ "k",
1266
+ linewidth=1,
1267
+ )
1268
+
1269
+ cmap = plt.get_cmap("RdBu_r")
1270
+ cmap.set_over(_adjust_lightness(cmap(1.0), 0.9))
1271
+ cmap.set_under(_adjust_lightness(cmap(0.0), 0.9))
1272
+
1273
+ # Plot fields (using plot coordinates)
1274
+ ax.contourf(
1275
+ Xsl + Usl_plot,
1276
+ Zsl_plot + Wsl_plot,
1277
+ slab,
1278
+ levels=levels,
1279
+ cmap=cmap,
1280
+ extend="both",
1281
+ )
1282
+ ax.contourf(
1283
+ Xwl + Uwl_plot,
1284
+ Zwl_plot + Wwl_plot,
1285
+ weak,
1286
+ levels=levels,
1287
+ cmap=cmap,
1288
+ extend="both",
1289
+ )
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
+ )
1298
+
1299
+ # Plot setup
1300
+ # Set y-limits to match plot coordinate system (0 to total_height_plot = 1.0)
1301
+ plot_ymin = -0.1
1302
+ plot_ymax = (
1303
+ total_height_plot # Should be 1.0 (slab_proportion + weaklayer_proportion)
1304
+ )
1305
+
1306
+ # Set limits first, then aspect ratio to avoid matplotlib adjusting limits
1307
+ ax.set_xlim([xmin, xmax])
1308
+ ax.set_ylim([plot_ymin, plot_ymax])
1309
+ ax.invert_yaxis()
1310
+ ax.use_sticky_edges = False
1311
+
1312
+ # Hide the default y-axis on main axis (we'll use custom axes)
1313
+ ax.yaxis.set_visible(False)
1314
+
1315
+ # Set up dual y-axes
1316
+ # Right axis: slab height in cm (0 at top, H_cm at bottom of slab)
1317
+ ax_right = ax.twinx()
1318
+ slab_height_max = H_cm
1319
+ # Map plot coordinates to physical slab height values
1320
+ # Plot: 0 to slab_proportion (0.6) maps to physical: 0 to H_cm
1321
+ slab_height_ticks = np.linspace(0, slab_height_max, num=5)
1322
+ slab_height_positions_plot = (
1323
+ slab_height_ticks / slab_height_max
1324
+ ) * slab_proportion
1325
+ ax_right.set_yticks(slab_height_positions_plot)
1326
+ ax_right.set_yticklabels([f"{tick:.1f}" for tick in slab_height_ticks])
1327
+ # Ensure right axis ticks and label are on the right side
1328
+ ax_right.yaxis.tick_right()
1329
+ ax_right.yaxis.set_label_position("right")
1330
+ ax_right.set_ylim([plot_ymin, plot_ymax])
1331
+ ax_right.invert_yaxis()
1332
+ ax_right.set_ylabel(
1333
+ r"slab depth [cm] $\longleftarrow$", rotation=90, labelpad=5, loc="top"
1334
+ )
1335
+
1336
+ # Left axis: weak layer height in mm (0 at bottom of slab, h at bottom of weak layer)
1337
+ ax_left = ax.twinx()
1338
+ weak_layer_h_mm = analyzer.sm.weak_layer.h
1339
+ # Map plot coordinates to physical weak layer height values
1340
+ # Plot: slab_proportion (0.6) to total_height_plot (1.0) maps to physical: 0 to h_mm
1341
+ weaklayer_height_ticks = np.linspace(0, weak_layer_h_mm, num=3)
1342
+ # Map from plot coordinates (slab_proportion to 1.0) to physical (0 to h_mm)
1343
+ weaklayer_height_positions_plot = (
1344
+ slab_proportion
1345
+ + (weaklayer_height_ticks / weak_layer_h_mm) * weaklayer_proportion
1346
+ )
1347
+ ax_left.set_yticks(weaklayer_height_positions_plot)
1348
+ ax_left.set_yticklabels([f"{tick:.1f}" for tick in weaklayer_height_ticks])
1349
+ # Move left axis to the left side
1350
+ ax_left.yaxis.tick_left()
1351
+ ax_left.yaxis.set_label_position("left")
1352
+ ax_left.set_ylim([plot_ymin, plot_ymax])
1353
+ ax_left.invert_yaxis()
1354
+ ax_left.set_ylabel(
1355
+ r"weaklayer depth [mm] $\longleftarrow$",
1356
+ rotation=90,
1357
+ labelpad=5,
1358
+ loc="bottom",
1359
+ )
1360
+
1361
+ # Plot labels
1362
+ ax.set_xlabel(r"lateral position $x$ (cm) $\longrightarrow$")
1363
+
1364
+ # Show colorbar
1365
+ ticks = np.linspace(levels[0], levels[-1], num=11, endpoint=True)
1366
+ fig.colorbar(
1367
+ ax.contourf(
1368
+ Xsl + Usl_plot,
1369
+ Zsl_plot + Wsl_plot,
1370
+ slab,
1371
+ levels=levels,
1372
+ cmap=cmap,
1373
+ extend="both",
1374
+ ),
1375
+ orientation="horizontal",
1376
+ ticks=ticks,
1377
+ label=label,
1378
+ aspect=35,
1379
+ )
1380
+
1381
+ # Save figure
1382
+ self._save_figure(filename, fig)
1383
+
1384
+ return fig
1385
+
1036
1386
  def plot_stress_envelope(
1037
1387
  self,
1038
1388
  system_model: SystemModel,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weac
3
- Version: 3.0.2
3
+ Version: 3.1.0
4
4
  Summary: Weak layer anticrack nucleation model
5
5
  Author-email: 2phi GbR <mail@2phi.de>
6
6
  License-Expression: MIT
@@ -1,10 +1,10 @@
1
- weac/__init__.py,sha256=mOpFbjyMROJ4BdyLMOUvSfVmzVGLZTYXeIqvGZXGtl4,76
1
+ weac/__init__.py,sha256=kXYZMl1LWws7VLAGdIwS5HzKkzmlgH3PVBeN8JOoCQg,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=JbtOK35mWlHq6eI8eD2vdZcsClhWl1r6I72-_9mS1Yk,29352
6
+ weac/analysis/criteria_evaluator.py,sha256=B8Y-_qXJX4rKaE8JgKONqaYYjFoqlHv75WeFTNMUvjk,47148
7
+ weac/analysis/plotter.py,sha256=lZXE6mHmIDwe9Y4wWmk_P8Tf0HmV2ruKVwFd5b25qdA,79860
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
@@ -25,8 +25,8 @@ weac/utils/geldsetzer.py,sha256=DxvpqmWulBacl-mGeAmAuJcv2xqFBx92PEfNC3aeDzk,3500
25
25
  weac/utils/misc.py,sha256=lGz0IDDJ_3nvYjSkivPJ5Xscl1D_AmvQLSjaL7SUbKs,3674
26
26
  weac/utils/snow_types.py,sha256=eX9-5La6Oom7zh6pg5JZ4MZ6nLdWdc7RoUzm5e6b9w8,1483
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.0.dist-info/licenses/LICENSE,sha256=ojZPWKFHbFGDrlNOvuAKGH9WcKhpLHWZPcQ4SzhK91M,1082
29
+ weac-3.1.0.dist-info/METADATA,sha256=Lkr3EVyJEoTwmFglEhx8aI01zqjivnTREoJOLuvMDww,25586
30
+ weac-3.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ weac-3.1.0.dist-info/top_level.txt,sha256=8tyXUHPFU4Ba_5kPtpwvXo5l6GjJmOnODVBJFygpdeE,5
32
+ weac-3.1.0.dist-info/RECORD,,
File without changes