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