weac 3.0.0__py3-none-any.whl → 3.0.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/core/scenario.py ADDED
@@ -0,0 +1,200 @@
1
+ """
2
+ This module defines the Scenario class, which encapsulates the physical setup of the model.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Sequence, Union
7
+
8
+ import numpy as np
9
+
10
+ from weac.components import ScenarioConfig, Segment, SystemType, WeakLayer
11
+ from weac.core.slab import Slab
12
+ from weac.utils.misc import decompose_to_normal_tangential
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Scenario:
18
+ """
19
+ Sets up the scenario on which the eigensystem is solved.
20
+
21
+ Parameters
22
+ ---------
23
+ scenario_config: ScenarioConfig
24
+ segments: List[Segment]
25
+ weak_layer: WeakLayer
26
+ slab: Slab
27
+
28
+ Attributes
29
+ ----------
30
+ li : List[float]
31
+ length of segment i [mm]
32
+ ki : List[bool]
33
+ booleans indicating foundation support for segment i
34
+ mi : List[float]
35
+ skier masses (kg) on boundary of segment i and i+1 [kg]
36
+
37
+ system_type : SystemType
38
+ phi : float
39
+ Angle of slab in positive in counter-clockwise direction [deg]
40
+ L : float
41
+ Length of the model [mm]
42
+ crack_h: float
43
+ Height of the crack [mm]
44
+ """
45
+
46
+ # Inputs
47
+ scenario_config: ScenarioConfig
48
+ segments: List[Segment]
49
+ weak_layer: WeakLayer
50
+ slab: Slab
51
+
52
+ # Attributes
53
+ li: np.ndarray # length of segment i [mm]
54
+ ki: np.ndarray # booleans indicating foundation support for segment i
55
+ mi: np.ndarray # skier masses (kg) on boundary of segment i and i+1 [kg]
56
+
57
+ cum_sum_li: np.ndarray # cumulative sum of segment lengths [mm]
58
+
59
+ system_type: SystemType
60
+ phi: float # Angle in [deg]
61
+ surface_load: float # Surface Line-Load [N/mm]
62
+ qw: float # Weight Line-Load [N/mm]
63
+ qn: float # Total Normal Line-Load [N/mm]
64
+ qt: float # Total Tangential Line-Load [N/mm]
65
+ L: float # Length of the model [mm]
66
+ crack_h: float # Height of the crack [mm]
67
+ cut_length: float # Length of the cut [mm]
68
+
69
+ def __init__(
70
+ self,
71
+ scenario_config: ScenarioConfig,
72
+ segments: List[Segment],
73
+ weak_layer: WeakLayer,
74
+ slab: Slab,
75
+ ):
76
+ self.scenario_config = scenario_config
77
+ self.segments = segments
78
+ self.weak_layer = weak_layer
79
+ self.slab = slab
80
+
81
+ self.system_type = scenario_config.system_type
82
+ self.phi = scenario_config.phi
83
+ self.surface_load = scenario_config.surface_load
84
+ self.cut_length = scenario_config.cut_length
85
+
86
+ self._setup_scenario()
87
+ self._calc_normal_load()
88
+ self._calc_tangential_load()
89
+ self._calc_crack_height()
90
+
91
+ def refresh_from_config(self):
92
+ """Pull changed values out of scenario_config
93
+ and recompute derived attributes."""
94
+ self.system_type = self.scenario_config.system_type
95
+ self.phi = self.scenario_config.phi
96
+ self.surface_load = self.scenario_config.surface_load
97
+ self.cut_length = self.scenario_config.cut_length
98
+
99
+ self._setup_scenario()
100
+ self._calc_normal_load()
101
+ self._calc_tangential_load()
102
+ self._calc_crack_height()
103
+
104
+ def get_segment_idx(
105
+ self, x: Union[float, Sequence[float], np.ndarray]
106
+ ) -> Union[int, np.ndarray]:
107
+ """
108
+ Get the segment index for a given x-coordinate or coordinates.
109
+
110
+ Parameters
111
+ ----------
112
+ x: Union[float, Sequence[float], np.ndarray]
113
+ A single x-coordinate or a sequence of x-coordinates.
114
+
115
+ Returns
116
+ -------
117
+ Union[int, np.ndarray]
118
+ The segment index or an array of indices.
119
+ """
120
+ x_arr = np.asarray(x)
121
+ indices = np.digitize(x_arr, self.cum_sum_li)
122
+
123
+ if np.any(x_arr > self.L):
124
+ raise ValueError(f"Coordinate {x_arr} exceeds the slab length.")
125
+
126
+ if x_arr.ndim == 0:
127
+ return int(indices)
128
+
129
+ return indices
130
+
131
+ def _setup_scenario(self):
132
+ self.li = np.array([seg.length for seg in self.segments])
133
+ self.ki = np.array([seg.has_foundation for seg in self.segments])
134
+ # masses that act *between* segments: take all but the last one
135
+ self.mi = np.array([seg.m for seg in self.segments[:-1]])
136
+ self.cum_sum_li = np.cumsum(self.li)
137
+
138
+ # Add dummy segment if only one segment provided
139
+ if len(self.li) == 1:
140
+ self.li = np.append(self.li, 0)
141
+ self.ki = np.append(self.ki, True)
142
+ self.mi = np.append(self.mi, 0)
143
+
144
+ # Calculate the total slab length
145
+ self.L = np.sum(self.li)
146
+
147
+ def _calc_tangential_load(self):
148
+ """
149
+ Total Tangential Load (Surface Load + Weight Load)
150
+
151
+ Returns:
152
+ --------
153
+ qt : float
154
+ Tangential Component of Load [N/mm]
155
+ """
156
+ # Surface Load & Weight Load
157
+ qw = self.slab.qw
158
+ qs = self.surface_load
159
+
160
+ # Normal components of forces
161
+ phi = self.phi
162
+ _, qwt = decompose_to_normal_tangential(qw, phi)
163
+ _, qst = decompose_to_normal_tangential(qs, phi)
164
+ qt = qwt + qst
165
+ self.qt = qt
166
+
167
+ def _calc_normal_load(self):
168
+ """
169
+ Total Normal Load (Surface Load + Weight Load)
170
+
171
+ Returns:
172
+ --------
173
+ qn : float
174
+ Normal Component of Load [N/mm]
175
+ """
176
+ # Surface Load & Weight Load
177
+ qw = self.slab.qw
178
+ qs = self.surface_load
179
+
180
+ # Normal components of forces
181
+ phi = self.phi
182
+ qwn, _ = decompose_to_normal_tangential(qw, phi)
183
+ qsn, _ = decompose_to_normal_tangential(qs, phi)
184
+ qn = qwn + qsn
185
+ self.qn = qn
186
+
187
+ def _calc_crack_height(self):
188
+ """
189
+ Crack Height: Difference between collapsed weak layer and
190
+ Weak Layer (Winkler type) under slab load
191
+
192
+ Example:
193
+ if the collapse layer has a height of 5 and the non-collapsed layer
194
+ has a height of 15 the collapse height is 10
195
+ """
196
+ self.crack_h = self.weak_layer.collapse_height - self.qn / self.weak_layer.kn
197
+ if self.crack_h < 0:
198
+ raise ValueError(
199
+ f"Crack height is negative: {self.crack_h} decrease the surface load"
200
+ )
weac/core/slab.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ This module defines the Slab class, which represents the snow slab and its properties.
3
+ """
4
+
5
+ from typing import List
6
+
7
+ import numpy as np
8
+
9
+ from weac.components import Layer
10
+ from weac.constants import EPS, G_MM_S2
11
+
12
+
13
+ class Slab: # pylint: disable=too-many-instance-attributes,too-few-public-methods
14
+ """
15
+ Parameters of all layers assembled into a slab,
16
+ provided as np.ndarray for easier access.
17
+
18
+ Coordinate frame:
19
+ - z-axis points downward (first index: top layer, last index: bottom layer)
20
+ - z = 0 is set at the mid-point of the slab's thickness
21
+
22
+ Attributes
23
+ ----------
24
+ zi_mid: np.ndarray
25
+ z-coordinate of the layer i mid-point
26
+ zi_bottom: np.ndarray
27
+ z-coordinate of the layer i (boundary towards bottom)
28
+ rhoi: np.ndarray
29
+ densities of the layer i [t/mm^3]
30
+ hi: np.ndarray
31
+ thickness of the layer i [mm]
32
+ Ei: np.ndarray
33
+ Young's modulus of the layer i [MPa]
34
+ Gi: np.ndarray
35
+ Shear Modulus of the layer i [MPa]
36
+ nui: np.ndarray
37
+ Poisson Ratio of the layer i [-]
38
+ H: float
39
+ Total slab thickness (i.e. assembled layers) [mm]
40
+ z_cog: float
41
+ z-coordinate of Center of Gravity [mm]
42
+ qw: float
43
+ Weight Load of the slab [N/mm]
44
+ """
45
+
46
+ # Input data
47
+ layers: List[Layer]
48
+
49
+ rhoi: np.ndarray # densities of the layer i [t/mm^3]
50
+ hi: np.ndarray # thickness of the layer i [mm]
51
+ Ei: np.ndarray # Young's modulus of the layer i [MPa]
52
+ Gi: np.ndarray # Shear Modulus of the layer i [MPa]
53
+ nui: np.ndarray # Poisson Ratio of the layer i [-]
54
+
55
+ # Derived Values
56
+ z0: float # z-coordinate of the top of the slab
57
+ zi_mid: np.ndarray # z-coordinate of the layer i mid-point
58
+ zi_bottom: np.ndarray # z-coordinate of the layer i (boundary towards bottom)
59
+ H: float # Total slab thickness (i.e. assembled layers) [mm]
60
+ z_cog: float # z-coordinate of Center of Gravity [mm]
61
+ qw: float # Weight Load of the slab [N/mm]
62
+
63
+ def __init__(self, layers: List[Layer]) -> None:
64
+ self.layers = layers
65
+ self._calc_slab_params()
66
+
67
+ def calc_vertical_center_of_gravity(self, phi: float):
68
+ """
69
+ Vertical PSTs use triangular slabs (with horizontal cuts on the slab ends)
70
+ Calculate center of gravity of triangular slab segments for vertical PSTs.
71
+
72
+ Parameters
73
+ ----------
74
+ phi : float
75
+ Slope angle [deg]
76
+
77
+ Returns
78
+ -------
79
+ x_cog : float
80
+ Horizontal coordinate of center of gravity [mm]
81
+ z_cog : float
82
+ Vertical coordinate of center of gravity [mm]
83
+ w : float
84
+ Weight of the slab segment that is cut off or added [t]
85
+ """
86
+ # Convert slope angle to radians
87
+ phi = np.deg2rad(phi)
88
+
89
+ # Catch flat-field case
90
+ if abs(phi) < EPS:
91
+ x_cog = 0
92
+ z_cog = 0
93
+ w = 0
94
+ else:
95
+ n = len(self.hi)
96
+ rho = self.rhoi # [t/mm^3]
97
+ hi = self.hi # [mm]
98
+ H = self.H # [mm]
99
+ # Layer coordinates z_i (top to bottom)
100
+ z = np.array([-H / 2 + sum(hi[0:j]) for j in range(n + 1)])
101
+ zi = z[:-1]
102
+ zii = z[1:]
103
+ # Center of gravity of all layers (top to bottom) derived from
104
+ # triangular slab geometry
105
+ zsi = zi + hi / 3 * (3 / 2 * H - zi - 2 * zii) / (H - zi - zii)
106
+ # Surface area of all layers (top to bottom), area = height * base / 2
107
+ # where base varies with slope angle
108
+ Ai = hi / 2 * (H - zi - zii) * np.tan(phi)
109
+ # Center of gravity in vertical direction
110
+ z_cog = sum(zsi * rho * Ai) / sum(rho * Ai)
111
+ # Center of gravity in horizontal direction
112
+ x_cog = (H / 2 - z_cog) * np.tan(phi / 2)
113
+ # Weight of added or cut off slab segments (t)
114
+ w = sum(Ai * rho)
115
+
116
+ # Return center of gravity and weight of slab segment
117
+ return x_cog, z_cog, w
118
+
119
+ def _calc_slab_params(self) -> None:
120
+ rhoi = (
121
+ np.array([ly.rho for ly in self.layers]) * 1e-12
122
+ ) # Layer densities (kg/m^3 -> t/mm^3)
123
+ hi = np.array([ly.h for ly in self.layers]) # Layer thickness
124
+ Ei = np.array([ly.E for ly in self.layers])
125
+ Gi = np.array([ly.G for ly in self.layers])
126
+ nui = np.array([ly.nu for ly in self.layers])
127
+
128
+ H = hi.sum()
129
+ # Vectorized midpoint coordinates per layer (top to bottom)
130
+ # previously: zi_mid = [float(H / 2 - sum(hi[j:n]) + hi[j] / 2) for j in range(n)]
131
+ suffix_cumsum = np.cumsum(hi[::-1])[::-1]
132
+ zi_mid = H / 2 - suffix_cumsum + hi / 2
133
+ zi_bottom = np.cumsum(hi) - H / 2
134
+ z_cog = sum(zi_mid * hi * rhoi) / sum(hi * rhoi)
135
+
136
+ qw = sum(rhoi * G_MM_S2 * hi) # Line load [N/mm]
137
+
138
+ self.rhoi = rhoi
139
+ self.hi = hi
140
+ self.Ei = Ei
141
+ self.Gi = Gi
142
+ self.nui = nui
143
+
144
+ self.zi_mid = zi_mid
145
+ self.zi_bottom = zi_bottom
146
+ self.z0 = -H / 2 # z-coordinate of the top of the slab
147
+ self.H = H
148
+ self.z_cog = z_cog
149
+ self.qw = qw