weac 3.0.1__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/analysis/plotter.py CHANGED
@@ -6,7 +6,7 @@ This module provides plotting functions for visualizing the results of the WEAC
6
6
  import colorsys
7
7
  import logging
8
8
  import os
9
- from typing import List, Literal, Optional
9
+ from typing import Literal
10
10
 
11
11
  # Third party imports
12
12
  import matplotlib.colors as mc
@@ -219,9 +219,9 @@ class Plotter:
219
219
 
220
220
  def _get_systems_to_plot(
221
221
  self,
222
- system_model: Optional[SystemModel] = None,
223
- system_models: Optional[List[SystemModel]] = None,
224
- ) -> List[SystemModel]:
222
+ system_model: SystemModel | None = None,
223
+ system_models: list[SystemModel] | None = None,
224
+ ) -> list[SystemModel]:
225
225
  """Determine which systems to plot based on override parameters."""
226
226
  if system_model is not None and system_models is not None:
227
227
  raise ValueError(
@@ -236,7 +236,7 @@ class Plotter:
236
236
  "SystemModel or list of SystemModels"
237
237
  )
238
238
 
239
- def _save_figure(self, filename: str, fig: Optional[Figure] = None):
239
+ def _save_figure(self, filename: str, fig: Figure | None = None):
240
240
  """Save figure with proper formatting."""
241
241
  if fig is None:
242
242
  fig = plt.gcf()
@@ -249,20 +249,20 @@ class Plotter:
249
249
 
250
250
  def plot_slab_profile(
251
251
  self,
252
- weak_layers: List[WeakLayer] | WeakLayer,
253
- slabs: List[Slab] | Slab,
252
+ weak_layers: list[WeakLayer] | WeakLayer,
253
+ slabs: list[Slab] | Slab,
254
254
  filename: str = "slab_profile",
255
- labels: Optional[List[str] | str] = None,
256
- colors: Optional[List[str]] = None,
255
+ labels: list[str] | str | None = None,
256
+ colors: list[str] | None = None,
257
257
  ):
258
258
  """
259
259
  Plot slab layer profiles for comparison.
260
260
 
261
261
  Parameters
262
262
  ----------
263
- weak_layers : List[WeakLayer] | WeakLayer
263
+ weak_layers : list[WeakLayer] | WeakLayer
264
264
  The weak layer or layers to plot.
265
- slabs : List[Slab] | Slab
265
+ slabs : list[Slab] | Slab
266
266
  The slab or slabs to plot.
267
267
  filename : str, optional
268
268
  Filename for saving plot
@@ -281,6 +281,9 @@ class Plotter:
281
281
  if isinstance(slabs, Slab):
282
282
  slabs = [slabs]
283
283
 
284
+ if len(weak_layers) != len(slabs):
285
+ raise ValueError("Number of weak layers must match number of slabs")
286
+
284
287
  if labels is None:
285
288
  labels = [f"System {i + 1}" for i in range(len(weak_layers))]
286
289
  elif isinstance(labels, str):
@@ -642,11 +645,11 @@ class Plotter:
642
645
 
643
646
  def plot_section_forces(
644
647
  self,
645
- system_model: Optional[SystemModel] = None,
646
- system_models: Optional[List[SystemModel]] = None,
648
+ system_model: SystemModel | None = None,
649
+ system_models: list[SystemModel] | None = None,
647
650
  filename: str = "section_forces",
648
- labels: Optional[List[str]] = None,
649
- colors: Optional[List[str]] = None,
651
+ labels: list[str] | None = None,
652
+ colors: list[str] | None = None,
650
653
  ):
651
654
  """
652
655
  Plot section forces (N, M, V) for comparison.
@@ -655,7 +658,7 @@ class Plotter:
655
658
  ----------
656
659
  system_model : SystemModel, optional
657
660
  Single system to plot (overrides default)
658
- system_models : List[SystemModel], optional
661
+ system_models : list[SystemModel], optional
659
662
  Multiple systems to plot (overrides default)
660
663
  filename : str, optional
661
664
  Filename for saving plot
@@ -721,11 +724,11 @@ class Plotter:
721
724
 
722
725
  def plot_energy_release_rates(
723
726
  self,
724
- system_model: Optional[SystemModel] = None,
725
- system_models: Optional[List[SystemModel]] = None,
727
+ system_model: SystemModel | None = None,
728
+ system_models: list[SystemModel] | None = None,
726
729
  filename: str = "ERR",
727
- labels: Optional[List[str]] = None,
728
- colors: Optional[List[str]] = None,
730
+ labels: list[str] | None = None,
731
+ colors: list[str] | None = None,
729
732
  ):
730
733
  """
731
734
  Plot energy release rates (G_I, G_II) for comparison.
@@ -734,7 +737,7 @@ class Plotter:
734
737
  ----------
735
738
  system_model : SystemModel, optional
736
739
  Single system to plot (overrides default)
737
- system_models : List[SystemModel], optional
740
+ system_models : list[SystemModel], optional
738
741
  Multiple systems to plot (overrides default)
739
742
  filename : str, optional
740
743
  Filename for saving plot
@@ -882,48 +885,56 @@ class Plotter:
882
885
  zmax = np.max(Zsl + scale * Wsl) + pad
883
886
  zmin = np.min(Zsl) - pad
884
887
 
885
- # Compute weak-layer grid coordinates (cm)
886
- 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]
887
891
 
888
- # Assemble weak-layer displacement field (top and bottom)
889
- Uwl = np.vstack([Usl[-1, :], np.zeros(xwl.shape[0])])
890
- 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])])
891
898
 
892
899
  # Compute stress or displacement fields
893
900
  match field:
894
901
  # Horizontal displacements (um)
895
902
  case "u":
896
903
  slab = 1e4 * Usl
897
- weak = 1e4 * Usl[-1, :]
904
+ weak = 1e4 * Usl[-1, nanmask]
898
905
  label = r"$u$ ($\mu$m)"
899
906
  # Vertical deflection (um)
900
907
  case "w":
901
908
  slab = 1e4 * Wsl
902
- weak = 1e4 * Wsl[-1, :]
909
+ weak = 1e4 * Wsl[-1, nanmask]
903
910
  label = r"$w$ ($\mu$m)"
904
- # Axial normal stresses (kPa)
905
911
  case "Sxx":
906
- slab = analyzer.Sxx(z, phi, dz=dz, unit="kPa")
907
- weak = np.zeros(xwl.shape[0])
908
- 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
+ )
909
917
  # Shear stresses (kPa)
910
918
  case "Txz":
911
- slab = analyzer.Txz(z, phi, dz=dz, unit="kPa")
912
- weak = Tauwl
913
- 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)"
914
922
  # Transverse normal stresses (kPa)
915
923
  case "Szz":
916
- slab = analyzer.Szz(z, phi, dz=dz, unit="kPa")
917
- weak = Sigmawl
918
- 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
+ )
919
929
  # Principal stresses
920
930
  case "principal":
921
931
  slab = analyzer.principal_stress_slab(
922
932
  z, phi, dz=dz, val="max", unit="kPa", normalize=normalize
923
933
  )
924
- weak = analyzer.principal_stress_weaklayer(
934
+ weak_full = analyzer.principal_stress_weaklayer(
925
935
  z, val="min", unit="kPa", normalize=normalize
926
936
  )
937
+ weak = weak_full[nanmask]
927
938
  if normalize:
928
939
  label = (
929
940
  r"$\sigma_\mathrm{I}/\sigma_+$ (slab), "
@@ -934,11 +945,6 @@ class Plotter:
934
945
  r"$\sigma_\mathrm{I}$ (kPa, slab), "
935
946
  r"$\sigma_\mathrm{I\!I\!I}$ (kPa, weak layer)"
936
947
  )
937
- case _:
938
- raise ValueError(
939
- f"Invalid input '{field}' for field. Valid options are "
940
- "'u', 'w', 'Sxx', 'Txz', 'Szz', or 'principal'"
941
- )
942
948
 
943
949
  # Complement label
944
950
  label += r" $\longrightarrow$"
@@ -965,10 +971,9 @@ class Plotter:
965
971
 
966
972
  # Plot deformed weak-layer _outline
967
973
  if system_type in ["-pst", "pst-", "-vpst", "vpst-"]:
968
- nanmask = np.isfinite(xwl)
969
974
  ax.plot(
970
- _outline(Xwl[:, nanmask] + scale * Uwl[:, nanmask]),
971
- _outline(Zwl[:, nanmask] + scale * Wwl[:, nanmask]),
975
+ _outline(Xwl + scale * Uwl),
976
+ _outline(Zwl + scale * Wwl),
972
977
  "k",
973
978
  linewidth=1,
974
979
  )
@@ -1030,12 +1035,360 @@ class Plotter:
1030
1035
 
1031
1036
  return fig
1032
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
+
1033
1386
  def plot_stress_envelope(
1034
1387
  self,
1035
1388
  system_model: SystemModel,
1036
1389
  criteria_evaluator: CriteriaEvaluator,
1037
1390
  all_envelopes: bool = False,
1038
- filename: Optional[str] = None,
1391
+ filename: str | None = None,
1039
1392
  ):
1040
1393
  """
1041
1394
  Plot stress envelope in τ-σ space.
@@ -1074,7 +1427,7 @@ class Plotter:
1074
1427
  weak_layer = system_model.weak_layer
1075
1428
 
1076
1429
  # Define a function to find the root for a given tau
1077
- def find_sigma_for_tau(tau_val, sigma_c, method: Optional[str] = None):
1430
+ def find_sigma_for_tau(tau_val, sigma_c, method: str | None = None):
1078
1431
  # Target function to find the root of: envelope(sigma, tau) - 1 = 0
1079
1432
  def envelope_root_func(sigma_val):
1080
1433
  return (
@@ -14,7 +14,6 @@ field_name: type = Field(..., gt=0, description="Description")
14
14
 
15
15
  import json
16
16
  import logging
17
- from typing import List
18
17
 
19
18
  from pydantic import BaseModel, ConfigDict, Field, model_validator
20
19
 
@@ -49,13 +48,13 @@ class ModelInput(BaseModel):
49
48
  default_factory=lambda: WeakLayer(rho=125, h=20, E=1.0),
50
49
  description="Weak layer",
51
50
  )
52
- layers: List[Layer] = Field(
53
- default_factory=lambda: [Layer(rho=250, h=100)], description="List of layers"
51
+ layers: list[Layer] = Field(
52
+ default_factory=lambda: [Layer(rho=250, h=100)], description="list of layers"
54
53
  )
55
54
  scenario_config: ScenarioConfig = Field(
56
55
  default_factory=ScenarioConfig, description="Scenario configuration"
57
56
  )
58
- segments: List[Segment] = Field(
57
+ segments: list[Segment] = Field(
59
58
  default_factory=lambda: [
60
59
  Segment(length=5000, has_foundation=True, m=100),
61
60
  Segment(length=5000, has_foundation=True, m=0),