weac 2.6.4__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.
@@ -0,0 +1,413 @@
1
+ """
2
+ This module defines the system model for the WEAC simulation.
3
+ The system model is the heart of the WEAC simulation. All data sources
4
+ are bundled into the system model. The system model initializes and
5
+ calculates all the parameterizations and passes relevant data to the
6
+ different components.
7
+
8
+ We utilize the pydantic library to define the system model.
9
+ """
10
+
11
+ import copy
12
+ import logging
13
+ from collections.abc import Sequence
14
+ from functools import cached_property
15
+ from typing import List, Optional, Union
16
+
17
+ import numpy as np
18
+
19
+ # from weac.constants import G_MM_S2, LSKI_MM
20
+ from weac.components import (
21
+ Config,
22
+ Layer,
23
+ ModelInput,
24
+ ScenarioConfig,
25
+ Segment,
26
+ WeakLayer,
27
+ )
28
+ from weac.core.eigensystem import Eigensystem
29
+ from weac.core.field_quantities import FieldQuantities
30
+ from weac.core.scenario import Scenario
31
+ from weac.core.slab import Slab
32
+ from weac.core.slab_touchdown import SlabTouchdown
33
+ from weac.core.unknown_constants_solver import UnknownConstantsSolver
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class SystemModel:
39
+ """
40
+ The heart of the WEAC simulation system for avalanche release modeling.
41
+
42
+ This class orchestrates all components of the WEAC simulation, including slab mechanics,
43
+ weak layer properties, touchdown calculations, and the solution of unknown constants
44
+ for beam-on-elastic-foundation problems.
45
+
46
+ The SystemModel follows a lazy evaluation pattern using cached properties, meaning
47
+ expensive calculations (eigensystem, touchdown, unknown constants) are only computed
48
+ when first accessed and then cached for subsequent use.
49
+
50
+ **Extracting Unknown Constants:**
51
+
52
+ The primary output of the SystemModel is the `unknown_constants` matrix, which contains
53
+ the solution constants for the beam segments:
54
+
55
+ ```python
56
+ # Basic usage
57
+ system = SystemModel(model_input=model_input, config=config)
58
+ constants = system.unknown_constants # Shape: (6, N) where N = number of segments
59
+
60
+ # Each column represents the 6 constants for one segment:
61
+ # constants[:, i] = [C1, C2, C3, C4, C5, C6] for segment i
62
+ # These constants define the beam deflection solution within that segment
63
+ ```
64
+
65
+ **Calculation Flow:**
66
+
67
+ 1. **Eigensystem**: Computes eigenvalues/eigenvectors for the beam-foundation system
68
+ 2. **Slab Touchdown** (if enabled): Calculates touchdown behavior and updates segment lengths
69
+ 3. **Unknown Constants**: Solves the linear system for beam deflection constants
70
+
71
+ **Touchdown Behavior:**
72
+
73
+ When `config.touchdown=True`, the system automatically:
74
+ - Calculates touchdown mode (A: free-hanging, B: point contact, C: in contact)
75
+ - Determines touchdown length based on slab-foundation interaction
76
+ - **Redefines scenario segments** to use touchdown length instead of crack length
77
+ - This matches the behavior of the original WEAC implementation
78
+
79
+ **Performance Notes:**
80
+
81
+ - First access to `unknown_constants` triggers all necessary calculations
82
+ - Subsequent accesses return cached results instantly
83
+ - Use `update_*` methods to modify parameters and invalidate caches as needed
84
+
85
+ **Example Usage:**
86
+
87
+ ```python
88
+ from weac.components import ModelInput, Layer, Segment, Config
89
+ from weac.core.system_model import SystemModel
90
+
91
+ # Define system components
92
+ layers = [Layer(rho=200, h=150), Layer(rho=300, h=100)]
93
+ segments = [Segment(length=10000, has_foundation=True, m=0),
94
+ Segment(length=4000, has_foundation=False, m=0)]
95
+
96
+ # Create system
97
+ system = SystemModel(model_input=model_input, config=Config(touchdown=True))
98
+
99
+ # Solve system and extract results
100
+ constants = system.unknown_constants # Solution constants (6 x N_segments)
101
+ touchdown_info = system.slab_touchdown # Touchdown analysis (if enabled)
102
+ eigensystem = system.eigensystem # Eigenvalue problem solution
103
+ ```
104
+
105
+ Attributes:
106
+ config: Configuration settings including touchdown enable/disable
107
+ slab: Slab properties (thickness, material properties per layer)
108
+ weak_layer: Weak layer properties (stiffness, thickness, etc.)
109
+ scenario: Scenario definition (segments, loads, boundary conditions)
110
+ eigensystem: Eigenvalue problem solution (computed lazily)
111
+ slab_touchdown: Touchdown analysis results (computed lazily if enabled)
112
+ unknown_constants: Solution constants matrix (computed lazily)
113
+ """
114
+
115
+ config: Config
116
+ slab: Slab
117
+ weak_layer: WeakLayer
118
+ eigensystem: Eigensystem
119
+ fq: FieldQuantities
120
+
121
+ scenario: Scenario
122
+ slab_touchdown: Optional[SlabTouchdown]
123
+ unknown_constants: np.ndarray
124
+ uncracked_unknown_constants: np.ndarray
125
+
126
+ def __init__(self, model_input: ModelInput, config: Optional[Config] = None):
127
+ if config is None:
128
+ config = Config()
129
+ self.config = config
130
+ self.weak_layer = model_input.weak_layer
131
+ self.slab = Slab(layers=model_input.layers)
132
+ self.scenario = Scenario(
133
+ scenario_config=model_input.scenario_config,
134
+ segments=model_input.segments,
135
+ weak_layer=self.weak_layer,
136
+ slab=self.slab,
137
+ )
138
+ logger.info("Scenario setup")
139
+
140
+ # At this point only the system is initialized
141
+ # The solution to the system (unknown_constants) are only computed
142
+ # when required by the user (at runtime)
143
+
144
+ # Cached properties are invalidated via __dict__.pop in the *invalidate_* helpers.
145
+
146
+ @cached_property
147
+ def fq(self) -> FieldQuantities:
148
+ """Compute the field quantities."""
149
+ return FieldQuantities(eigensystem=self.eigensystem)
150
+
151
+ @cached_property
152
+ def eigensystem(self) -> Eigensystem: # heavy
153
+ """Solve for the eigensystem."""
154
+ logger.info("Solving for Eigensystem")
155
+ return Eigensystem(weak_layer=self.weak_layer, slab=self.slab)
156
+
157
+ @cached_property
158
+ def slab_touchdown(self) -> Optional[SlabTouchdown]:
159
+ """
160
+ Solve for the slab touchdown.
161
+ Modifies the scenario object in place by replacing the undercut segment
162
+ with a new segment of length equal to the touchdown distance if the system is
163
+ a PST or VPST.
164
+ """
165
+ if self.config.touchdown:
166
+ logger.info("Solving for Slab Touchdown")
167
+ slab_touchdown = SlabTouchdown(
168
+ scenario=self.scenario, eigensystem=self.eigensystem
169
+ )
170
+ logger.info(
171
+ "Original cut_length: %s, touchdown_distance: %s",
172
+ self.scenario.cut_length,
173
+ slab_touchdown.touchdown_distance,
174
+ )
175
+
176
+ new_segments = copy.deepcopy(self.scenario.segments)
177
+ if self.scenario.system_type in ("pst-", "vpst-"):
178
+ new_segments[-1].length = slab_touchdown.touchdown_distance
179
+ elif self.scenario.system_type in ("-pst", "-vpst"):
180
+ new_segments[0].length = slab_touchdown.touchdown_distance
181
+
182
+ # Create new scenario with updated segments
183
+ self.scenario = Scenario(
184
+ scenario_config=self.scenario.scenario_config,
185
+ segments=new_segments,
186
+ weak_layer=self.weak_layer,
187
+ slab=self.slab,
188
+ )
189
+ logger.info(
190
+ "Updated scenario with new segment lengths: %s",
191
+ [seg.length for seg in new_segments],
192
+ )
193
+
194
+ return slab_touchdown
195
+ return None
196
+
197
+ @cached_property
198
+ def unknown_constants(self) -> np.ndarray:
199
+ """
200
+ Solve for the unknown constants matrix defining beam deflection in each segment.
201
+
202
+ This is the core solution of the WEAC beam-on-elastic-foundation problem.
203
+ The unknown constants define the deflection, slope, moment, and shear force
204
+ distributions within each beam segment.
205
+
206
+ Returns:
207
+ --------
208
+ np.ndarray: Solution constants matrix of shape (6, N_segments)
209
+ Each column contains the 6 constants for one segment:
210
+ [C1, C2, C3, C4, C5, C6]
211
+
212
+ These constants are used in the general solution:
213
+ u(x) = Σ Ci * φi(x) + up(x)
214
+
215
+ Where φi(x) are the homogeneous solutions and up(x)
216
+ is the particular solution.
217
+
218
+ Notes:
219
+ - For touchdown systems, segment lengths are automatically adjusted
220
+ based on touchdown calculations before solving
221
+ - The solution accounts for boundary conditions, load transmission
222
+ between segments, and foundation support
223
+ - Results are cached after first computation for performance
224
+
225
+ Example:
226
+ ```python
227
+ system = SystemModel(model_input, config)
228
+ C = system.unknown_constants # Shape: (6, 2) for 2-segment system
229
+
230
+ # Constants for first segment
231
+ segment_0_constants = C[:, 0]
232
+
233
+ # Use with eigensystem to compute field quantities
234
+ x = 1000 # Position in mm
235
+ segment_length = system.scenario.li[0]
236
+ ```
237
+ """
238
+ if self.slab_touchdown is not None:
239
+ logger.info("Solving for Unknown Constants")
240
+ return UnknownConstantsSolver.solve_for_unknown_constants(
241
+ scenario=self.scenario,
242
+ eigensystem=self.eigensystem,
243
+ system_type=self.scenario.system_type,
244
+ touchdown_distance=self.slab_touchdown.touchdown_distance,
245
+ touchdown_mode=self.slab_touchdown.touchdown_mode,
246
+ collapsed_weak_layer_kR=self.slab_touchdown.collapsed_weak_layer_kR,
247
+ )
248
+ logger.info("Solving for Unknown Constants")
249
+ return UnknownConstantsSolver.solve_for_unknown_constants(
250
+ scenario=self.scenario,
251
+ eigensystem=self.eigensystem,
252
+ system_type=self.scenario.system_type,
253
+ touchdown_distance=None,
254
+ touchdown_mode=None,
255
+ collapsed_weak_layer_kR=None,
256
+ )
257
+
258
+ @cached_property
259
+ def uncracked_unknown_constants(self) -> np.ndarray:
260
+ """
261
+ Solve for the uncracked unknown constants.
262
+ This is the solution for the case where the slab is cracked nowhere.
263
+ """
264
+ new_segments = copy.deepcopy(self.scenario.segments)
265
+ for _, seg in enumerate(new_segments):
266
+ seg.has_foundation = True
267
+ uncracked_scenario = Scenario(
268
+ scenario_config=self.scenario.scenario_config,
269
+ segments=new_segments,
270
+ weak_layer=self.weak_layer,
271
+ slab=self.slab,
272
+ )
273
+
274
+ logger.info("Solving for Uncracked Unknown Constants")
275
+ if self.slab_touchdown is not None:
276
+ return UnknownConstantsSolver.solve_for_unknown_constants(
277
+ scenario=uncracked_scenario,
278
+ eigensystem=self.eigensystem,
279
+ system_type=self.scenario.system_type,
280
+ touchdown_distance=self.slab_touchdown.touchdown_distance,
281
+ touchdown_mode=self.slab_touchdown.touchdown_mode,
282
+ collapsed_weak_layer_kR=self.slab_touchdown.collapsed_weak_layer_kR,
283
+ )
284
+ return UnknownConstantsSolver.solve_for_unknown_constants(
285
+ scenario=uncracked_scenario,
286
+ eigensystem=self.eigensystem,
287
+ system_type=self.scenario.system_type,
288
+ touchdown_distance=None,
289
+ touchdown_mode=None,
290
+ collapsed_weak_layer_kR=None,
291
+ )
292
+
293
+ # Changes that affect the *weak layer* -> rebuild everything
294
+ def update_weak_layer(self, weak_layer: WeakLayer):
295
+ """Update the weak layer."""
296
+ self.weak_layer = weak_layer
297
+ self.scenario = Scenario(
298
+ scenario_config=self.scenario.scenario_config,
299
+ segments=self.scenario.segments,
300
+ weak_layer=weak_layer,
301
+ slab=self.slab,
302
+ )
303
+ self._invalidate_eigensystem()
304
+
305
+ # Changes that affect the *slab* -> rebuild everything
306
+ def update_layers(self, new_layers: List[Layer]):
307
+ """Update the layers."""
308
+ slab = Slab(layers=new_layers)
309
+ self.slab = slab
310
+ self.scenario = Scenario(
311
+ scenario_config=self.scenario.scenario_config,
312
+ segments=self.scenario.segments,
313
+ weak_layer=self.weak_layer,
314
+ slab=slab,
315
+ )
316
+ self._invalidate_eigensystem()
317
+
318
+ # Changes that affect the *scenario* -> only rebuild C constants
319
+ def update_scenario(
320
+ self,
321
+ segments: Optional[List[Segment]] = None,
322
+ scenario_config: Optional[ScenarioConfig] = None,
323
+ ):
324
+ """
325
+ Update fields on `scenario_config` (if present) or on the
326
+ Scenario object itself, then refresh and invalidate constants.
327
+ """
328
+ logger.debug("Updating Scenario...")
329
+ if segments is None:
330
+ segments = self.scenario.segments
331
+ if scenario_config is None:
332
+ scenario_config = self.scenario.scenario_config
333
+ self.scenario = Scenario(
334
+ scenario_config=scenario_config,
335
+ segments=segments,
336
+ weak_layer=self.weak_layer,
337
+ slab=self.slab,
338
+ )
339
+ if self.config.touchdown:
340
+ self._invalidate_slab_touchdown()
341
+ self._invalidate_constants()
342
+
343
+ def toggle_touchdown(self, touchdown: bool):
344
+ """Toggle the touchdown."""
345
+ if self.config.touchdown != touchdown:
346
+ self.config.touchdown = touchdown
347
+ self._invalidate_slab_touchdown()
348
+ self._invalidate_constants()
349
+
350
+ def _invalidate_eigensystem(self):
351
+ """Invalidate the eigensystem."""
352
+ self.__dict__.pop("eigensystem", None)
353
+ self.__dict__.pop("slab_touchdown", None)
354
+ self.__dict__.pop("fq", None)
355
+ self._invalidate_constants()
356
+
357
+ def _invalidate_slab_touchdown(self):
358
+ """Invalidate the slab touchdown."""
359
+ self.__dict__.pop("slab_touchdown", None)
360
+
361
+ def _invalidate_constants(self):
362
+ """Invalidate the constants."""
363
+ self.__dict__.pop("unknown_constants", None)
364
+ self.__dict__.pop("uncracked_unknown_constants", None)
365
+
366
+ def z(
367
+ self,
368
+ x: Union[float, Sequence[float], np.ndarray],
369
+ C: np.ndarray,
370
+ length: float,
371
+ phi: float,
372
+ has_foundation: bool = True,
373
+ qs: float = 0,
374
+ ) -> np.ndarray:
375
+ """
376
+ Assemble solution vector at positions x.
377
+
378
+ Arguments
379
+ ---------
380
+ x : float or sequence
381
+ Horizontal coordinate (mm). Can be sequence of length N.
382
+ C : ndarray
383
+ Vector of constants (6xN) at positions x.
384
+ length : float
385
+ Segment length (mm).
386
+ phi : float
387
+ Inclination (degrees).
388
+ has_foundation : bool
389
+ Indicates whether segment has foundation (True) or not
390
+ (False). Default is True.
391
+ qs : float
392
+ Surface Load [N/mm]
393
+
394
+ Returns
395
+ -------
396
+ z : ndarray
397
+ Solution vector (6xN) at position x.
398
+ """
399
+ if isinstance(x, (list, tuple, np.ndarray)):
400
+ z = np.concatenate(
401
+ [
402
+ np.dot(self.eigensystem.zh(xi, length, has_foundation), C)
403
+ + self.eigensystem.zp(xi, phi, has_foundation, qs)
404
+ for xi in x
405
+ ],
406
+ axis=1,
407
+ )
408
+ else:
409
+ z = np.dot(
410
+ self.eigensystem.zh(x, length, has_foundation), C
411
+ ) + self.eigensystem.zp(x, phi, has_foundation, qs)
412
+
413
+ return z