Sompote commited on
Commit
1843b43
·
verified ·
1 Parent(s): 519a0b4

Upload 2 files

Browse files
Files changed (2) hide show
  1. src/app.py +77 -8
  2. src/pvd_consolidation.py +304 -43
src/app.py CHANGED
@@ -1,12 +1,12 @@
1
  """
2
  Streamlit App for PVD Consolidation Analysis
3
- Shows settlement vs time curve
4
  """
5
 
6
  import streamlit as st
7
  import numpy as np
8
  import matplotlib.pyplot as plt
9
- from pvd_consolidation import PVDConsolidation, SoilLayer, PVDProperties
10
 
11
  # Page configuration
12
  st.set_page_config(
@@ -101,13 +101,75 @@ if well_resistance == "Custom qw":
101
  else:
102
  qw = 1e12 # Very large value for negligible resistance
103
 
104
- pvd = PVDProperties(dw=dw, ds=ds, De=De, L_drain=L_drain, qw=qw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  # Analysis Parameters
107
  st.sidebar.subheader("Analysis Parameters")
108
- surcharge = st.sidebar.number_input(
109
- "Surcharge (kPa)", min_value=1.0, value=100.0, step=10.0
110
- )
111
  t_max = st.sidebar.number_input("Max Time (years)", min_value=0.1, value=2.0, step=0.1)
112
  dt = st.sidebar.number_input(
113
  "Time Step (years)", min_value=0.001, value=0.01, step=0.001, format="%.3f"
@@ -117,8 +179,15 @@ dt = st.sidebar.number_input(
117
  if st.sidebar.button("🚀 Run Analysis", type="primary"):
118
  with st.spinner("Calculating consolidation..."):
119
  try:
120
- # Create analysis
121
- analysis = PVDConsolidation(layers, pvd, surcharge, dt)
 
 
 
 
 
 
 
122
 
123
  # Calculate settlement vs time
124
  time, settlement = analysis.settlement_vs_time(t_max=t_max, n_points=200)
 
1
  """
2
  Streamlit App for PVD Consolidation Analysis
3
+ Shows settlement vs time curve with vacuum and staged loading support
4
  """
5
 
6
  import streamlit as st
7
  import numpy as np
8
  import matplotlib.pyplot as plt
9
+ from pvd_consolidation import PVDConsolidation, SoilLayer, PVDProperties, LoadingStage
10
 
11
  # Page configuration
12
  st.set_page_config(
 
101
  else:
102
  qw = 1e12 # Very large value for negligible resistance
103
 
104
+ # Vacuum depth loss option
105
+ vacuum_depth_loss = st.sidebar.checkbox(
106
+ "Vacuum loss with depth",
107
+ value=True,
108
+ help="If checked: vacuum decreases with depth (u(z) = u_surface × exp(-z/L)). If unchecked: uniform vacuum at all depths.",
109
+ )
110
+
111
+ pvd = PVDProperties(
112
+ dw=dw, ds=ds, De=De, L_drain=L_drain, qw=qw, vacuum_depth_loss=vacuum_depth_loss
113
+ )
114
+
115
+ # Loading Configuration
116
+ st.sidebar.subheader("Loading Configuration")
117
+
118
+ loading_type = st.sidebar.radio(
119
+ "Loading Type",
120
+ ["Single Stage", "Multi-Stage"],
121
+ help="Single stage: One loading condition. Multi-stage: Multiple loading stages over time.",
122
+ )
123
+
124
+ if loading_type == "Single Stage":
125
+ st.sidebar.markdown("**Single Stage Loading**")
126
+ surcharge = st.sidebar.number_input(
127
+ "Surcharge (kPa)", min_value=0.0, value=100.0, step=10.0
128
+ )
129
+ vacuum = st.sidebar.number_input(
130
+ "Vacuum (kPa)",
131
+ min_value=0.0,
132
+ value=0.0,
133
+ step=10.0,
134
+ help="Vacuum pressure (positive value). 80 kPa vacuum ≈ 80 kPa surcharge",
135
+ )
136
+ loading_stages = None
137
+
138
+ else: # Multi-Stage
139
+ st.sidebar.markdown("**Multi-Stage Loading**")
140
+ n_stages = st.sidebar.number_input(
141
+ "Number of Stages", min_value=1, max_value=10, value=2
142
+ )
143
+
144
+ loading_stages = []
145
+ for i in range(n_stages):
146
+ st.sidebar.markdown(f"**Stage {i + 1}**")
147
+ col1, col2, col3 = st.sidebar.columns(3)
148
+
149
+ with col1:
150
+ start_time = st.number_input(
151
+ f"Time (y)",
152
+ min_value=0.0,
153
+ value=float(i * 0.5),
154
+ step=0.1,
155
+ key=f"time_{i}",
156
+ )
157
+ with col2:
158
+ stage_surcharge = st.number_input(
159
+ f"Surcharge", min_value=0.0, value=50.0, step=10.0, key=f"sur_{i}"
160
+ )
161
+ with col3:
162
+ stage_vacuum = st.number_input(
163
+ f"Vacuum", min_value=0.0, value=0.0, step=10.0, key=f"vac_{i}"
164
+ )
165
+
166
+ loading_stages.append(LoadingStage(start_time, stage_surcharge, stage_vacuum))
167
+
168
+ surcharge = 0.0 # Not used in multi-stage
169
+ vacuum = 0.0
170
 
171
  # Analysis Parameters
172
  st.sidebar.subheader("Analysis Parameters")
 
 
 
173
  t_max = st.sidebar.number_input("Max Time (years)", min_value=0.1, value=2.0, step=0.1)
174
  dt = st.sidebar.number_input(
175
  "Time Step (years)", min_value=0.001, value=0.01, step=0.001, format="%.3f"
 
179
  if st.sidebar.button("🚀 Run Analysis", type="primary"):
180
  with st.spinner("Calculating consolidation..."):
181
  try:
182
+ # Create analysis with vacuum and staged loading support
183
+ if loading_stages:
184
+ analysis = PVDConsolidation(
185
+ layers, pvd, loading_stages=loading_stages, dt=dt
186
+ )
187
+ else:
188
+ analysis = PVDConsolidation(
189
+ layers, pvd, surcharge=surcharge, vacuum=vacuum, dt=dt
190
+ )
191
 
192
  # Calculate settlement vs time
193
  time, settlement = analysis.settlement_vs_time(t_max=t_max, n_points=200)
src/pvd_consolidation.py CHANGED
@@ -12,6 +12,22 @@ from dataclasses import dataclass
12
  from typing import List, Tuple, Dict, Any
13
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  @dataclass
16
  class SoilLayer:
17
  """Soil layer properties"""
@@ -36,6 +52,13 @@ class PVDProperties:
36
  De: float # Equivalent diameter of unit cell (m)
37
  L_drain: float # Total drain spacing for two-way drainage (m)
38
  qw: float # Well discharge capacity (m³/year)
 
 
 
 
 
 
 
39
 
40
 
41
  class PVDConsolidation:
@@ -47,7 +70,9 @@ class PVDConsolidation:
47
  self,
48
  soil_layers: List[SoilLayer],
49
  pvd: PVDProperties,
50
- surcharge: float,
 
 
51
  dt: float = 0.01,
52
  ):
53
  """
@@ -60,15 +85,33 @@ class PVDConsolidation:
60
  pvd : PVDProperties
61
  PVD installation properties
62
  surcharge : float
63
- Applied surcharge load (kPa)
 
 
 
 
64
  dt : float
65
  Time step for finite difference (years)
66
  """
67
  self.layers = soil_layers
68
  self.pvd = pvd
69
- self.surcharge = surcharge
70
  self.dt = dt
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # Calculate total thickness
73
  self.total_thickness = sum(layer.thickness for layer in soil_layers)
74
 
@@ -119,6 +162,127 @@ class PVDConsolidation:
119
  self.n_nodes = len(self.z_coords)
120
  self.dz = np.diff(self.z_coords)
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  def calculate_pvd_factors_layer(self, layer_idx: int) -> Tuple[float, float, float]:
123
  """
124
  Calculate PVD influence factors for a specific layer
@@ -265,6 +429,9 @@ class PVDConsolidation:
265
  """
266
  Calculate degree of consolidation in vertical direction
267
 
 
 
 
268
  Parameters:
269
  -----------
270
  t : float
@@ -278,18 +445,52 @@ class PVDConsolidation:
278
  Uv = np.zeros(self.n_nodes)
279
 
280
  for i in range(self.n_nodes):
281
- z = self.z_coords[i]
282
  Cv = self.Cv_profile[i]
283
- H = self.total_thickness
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  # Time factor
286
- Tv = Cv * t / H**2
 
 
 
287
 
288
  # Calculate Uv using Terzaghi's solution
289
- if Tv < 0.217:
290
- Uv[i] = np.sqrt(4 * Tv / np.pi)
 
 
 
 
291
  else:
292
- Uv[i] = 1 - (8 / np.pi**2) * np.exp(-(np.pi**2) * Tv / 4)
 
 
 
 
293
 
294
  Uv[i] = min(Uv[i], 1.0)
295
 
@@ -332,6 +533,7 @@ class PVDConsolidation:
332
  def calculate_settlement(self, t: float) -> Tuple[float, np.ndarray]:
333
  """
334
  Calculate total settlement at time t
 
335
 
336
  Parameters:
337
  -----------
@@ -345,48 +547,107 @@ class PVDConsolidation:
345
  layer_settlements : ndarray
346
  Settlement for each layer (m)
347
  """
348
- U = self.calculate_total_U(t)
349
-
350
  layer_settlements = np.zeros(len(self.layers))
351
 
352
- for i, layer in enumerate(self.layers):
353
- # Find nodes in this layer
354
- layer_mask = self.layer_indices == i
355
- U_layer = np.mean(U[layer_mask])
356
-
357
- # Calculate final settlement for this layer
358
- sigma_final = layer.sigma_ini + self.surcharge
359
-
360
- # Settlement components
361
- if sigma_final <= layer.sigma_p:
362
- # Recompression only
363
- Sc_final = (
364
- layer.RR * np.log10(sigma_final / layer.sigma_ini) * layer.thickness
365
- )
366
- else:
367
- if layer.sigma_ini >= layer.sigma_p:
368
- # Virgin compression only
369
- Sc_final = (
370
- layer.CR
371
- * np.log10(sigma_final / layer.sigma_ini)
372
- * layer.thickness
 
 
 
 
 
373
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  else:
375
- # Both recompression and virgin compression
376
- Sc_recomp = (
377
  layer.RR
378
- * np.log10(layer.sigma_p / layer.sigma_ini)
379
- * layer.thickness
380
- )
381
- Sc_virgin = (
382
- layer.CR
383
- * np.log10(sigma_final / layer.sigma_p)
384
  * layer.thickness
385
  )
386
- Sc_final = Sc_recomp + Sc_virgin
387
 
388
- # Settlement at time t
389
- layer_settlements[i] = U_layer * Sc_final
390
 
391
  total_settlement = np.sum(layer_settlements)
392
 
 
12
  from typing import List, Tuple, Dict, Any
13
 
14
 
15
+ @dataclass
16
+ class LoadingStage:
17
+ """Loading stage definition for staged loading"""
18
+
19
+ start_time: float # Time when this stage starts (years)
20
+ surcharge: float # Surcharge load (kPa)
21
+ vacuum: float # Vacuum pressure (kPa, positive value, will be applied as negative)
22
+
23
+ def __post_init__(self):
24
+ """Validate inputs"""
25
+ if self.vacuum < 0:
26
+ raise ValueError(
27
+ "Vacuum should be specified as positive value (e.g., 80 for -80 kPa)"
28
+ )
29
+
30
+
31
  @dataclass
32
  class SoilLayer:
33
  """Soil layer properties"""
 
52
  De: float # Equivalent diameter of unit cell (m)
53
  L_drain: float # Total drain spacing for two-way drainage (m)
54
  qw: float # Well discharge capacity (m³/year)
55
+ r_influence: float = None # Vacuum influence radius (m), default = De/4
56
+ vacuum_depth_loss: bool = True # Use vacuum loss with depth (default: True)
57
+
58
+ def __post_init__(self):
59
+ """Set default vacuum influence radius"""
60
+ if self.r_influence is None:
61
+ self.r_influence = self.De / 4
62
 
63
 
64
  class PVDConsolidation:
 
70
  self,
71
  soil_layers: List[SoilLayer],
72
  pvd: PVDProperties,
73
+ surcharge: float = 0.0,
74
+ vacuum: float = 0.0,
75
+ loading_stages: List[LoadingStage] = None,
76
  dt: float = 0.01,
77
  ):
78
  """
 
85
  pvd : PVDProperties
86
  PVD installation properties
87
  surcharge : float
88
+ Applied surcharge load (kPa) - for single-stage loading
89
+ vacuum : float
90
+ Applied vacuum pressure (kPa, positive value) - for single-stage loading
91
+ loading_stages : List[LoadingStage]
92
+ List of loading stages for multi-stage loading (overrides surcharge/vacuum)
93
  dt : float
94
  Time step for finite difference (years)
95
  """
96
  self.layers = soil_layers
97
  self.pvd = pvd
 
98
  self.dt = dt
99
 
100
+ # Setup loading
101
+ if loading_stages is not None:
102
+ self.loading_stages = sorted(loading_stages, key=lambda x: x.start_time)
103
+ self.use_staged_loading = True
104
+ else:
105
+ # Single stage loading
106
+ self.loading_stages = [
107
+ LoadingStage(start_time=0.0, surcharge=surcharge, vacuum=vacuum)
108
+ ]
109
+ self.use_staged_loading = False
110
+
111
+ # For compatibility
112
+ self.surcharge = surcharge
113
+ self.vacuum = vacuum
114
+
115
  # Calculate total thickness
116
  self.total_thickness = sum(layer.thickness for layer in soil_layers)
117
 
 
162
  self.n_nodes = len(self.z_coords)
163
  self.dz = np.diff(self.z_coords)
164
 
165
+ def get_current_loading(self, t: float) -> Tuple[float, float]:
166
+ """
167
+ Get current surcharge and vacuum at time t
168
+
169
+ Parameters:
170
+ -----------
171
+ t : float
172
+ Current time (years)
173
+
174
+ Returns:
175
+ --------
176
+ surcharge, vacuum : float, float
177
+ Current surcharge and vacuum values (kPa)
178
+ """
179
+ # Find the active loading stage
180
+ active_stage = self.loading_stages[0]
181
+ for stage in self.loading_stages:
182
+ if t >= stage.start_time:
183
+ active_stage = stage
184
+ else:
185
+ break
186
+
187
+ return active_stage.surcharge, active_stage.vacuum
188
+
189
+ def calculate_vacuum_at_radius(self, r: float, vacuum_magnitude: float) -> float:
190
+ """
191
+ Calculate vacuum pressure at radial distance r from drain
192
+
193
+ u(r) = u_drain × exp(-r/r_influence)
194
+
195
+ Parameters:
196
+ -----------
197
+ r : float
198
+ Radial distance from drain (m)
199
+ vacuum_magnitude : float
200
+ Vacuum at drain (kPa, positive value)
201
+
202
+ Returns:
203
+ --------
204
+ vacuum_at_r : float
205
+ Vacuum pressure at distance r (kPa, positive value)
206
+ """
207
+ if vacuum_magnitude == 0:
208
+ return 0.0
209
+
210
+ # Vacuum decreases exponentially with distance
211
+ vacuum_at_r = vacuum_magnitude * np.exp(-r / self.pvd.r_influence)
212
+
213
+ return vacuum_at_r
214
+
215
+ def calculate_layer_average_vacuum(
216
+ self, layer_idx: int, vacuum_surface: float
217
+ ) -> float:
218
+ """
219
+ Calculate average vacuum pressure for a specific soil layer
220
+
221
+ If vacuum_depth_loss = True:
222
+ Vacuum is full at surface and decreases with depth
223
+ u(z) = u_surface × exp(-z/L_drain)
224
+
225
+ If vacuum_depth_loss = False:
226
+ Uniform vacuum throughout (full vacuum at all depths within L_drain)
227
+
228
+ Parameters:
229
+ -----------
230
+ layer_idx : int
231
+ Layer index
232
+ vacuum_surface : float
233
+ Vacuum at surface (kPa, positive value)
234
+
235
+ Returns:
236
+ --------
237
+ avg_vacuum : float
238
+ Average vacuum for this layer (kPa)
239
+ """
240
+ if vacuum_surface == 0:
241
+ return 0.0
242
+
243
+ layer = self.layers[layer_idx]
244
+
245
+ # Find layer depth range
246
+ depth_top = sum(self.layers[i].thickness for i in range(layer_idx))
247
+ depth_bottom = depth_top + layer.thickness
248
+
249
+ # Check if layer is within drain length
250
+ if depth_top >= self.pvd.L_drain:
251
+ # Layer is completely below drain length - no vacuum effect
252
+ return 0.0
253
+
254
+ # Option 1: Uniform vacuum (no depth loss)
255
+ if not self.pvd.vacuum_depth_loss:
256
+ # Full vacuum throughout the layer (within drain length)
257
+ return vacuum_surface
258
+
259
+ # Option 2: Vacuum loss with depth
260
+ # Adjust bottom depth if it exceeds drain length
261
+ if depth_bottom > self.pvd.L_drain:
262
+ depth_bottom = self.pvd.L_drain
263
+ effective_thickness = depth_bottom - depth_top
264
+ else:
265
+ effective_thickness = layer.thickness
266
+
267
+ # Integrate vacuum over layer thickness
268
+ # u(z) = u_surface × exp(-z/L_drain)
269
+ # Average = (1/h) ∫[z_top to z_bottom] u_surface × exp(-z/L_drain) dz
270
+
271
+ L = self.pvd.L_drain
272
+
273
+ # Analytical integration:
274
+ # ∫ u_surface × exp(-z/L) dz = -u_surface × L × exp(-z/L) + C
275
+
276
+ integral_bottom = -vacuum_surface * L * np.exp(-depth_bottom / L)
277
+ integral_top = -vacuum_surface * L * np.exp(-depth_top / L)
278
+
279
+ integral = integral_bottom - integral_top
280
+
281
+ # Average over effective layer thickness
282
+ avg_vacuum = integral / effective_thickness
283
+
284
+ return avg_vacuum
285
+
286
  def calculate_pvd_factors_layer(self, layer_idx: int) -> Tuple[float, float, float]:
287
  """
288
  Calculate PVD influence factors for a specific layer
 
429
  """
430
  Calculate degree of consolidation in vertical direction
431
 
432
+ For layers below PVD: drainage path is from layer midpoint to bottom of PVD
433
+ For layers within/above PVD: normal two-way drainage
434
+
435
  Parameters:
436
  -----------
437
  t : float
 
445
  Uv = np.zeros(self.n_nodes)
446
 
447
  for i in range(self.n_nodes):
448
+ depth = self.z_coords[i]
449
  Cv = self.Cv_profile[i]
450
+ layer_idx = self.layer_indices[i]
451
+
452
+ # Determine drainage path length H
453
+ if depth > self.pvd.L_drain:
454
+ # Below PVD: drainage from this point UP to bottom of PVD
455
+ # For layer below PVD, use distance from layer midpoint to PVD bottom
456
+
457
+ # Find layer boundaries
458
+ cumulative_depth = 0
459
+ for idx in range(layer_idx):
460
+ cumulative_depth += self.layers[idx].thickness
461
+ layer_top = cumulative_depth
462
+ layer_bottom = cumulative_depth + self.layers[layer_idx].thickness
463
+ layer_midpoint = (layer_top + layer_bottom) / 2
464
+
465
+ # Drainage path: from layer midpoint to PVD bottom
466
+ H = abs(layer_midpoint - self.pvd.L_drain)
467
+
468
+ # One-way drainage (upward only to PVD)
469
+ drainage_type = "one-way"
470
+ else:
471
+ # Within or above PVD: normal two-way drainage
472
+ H = self.total_thickness
473
+ drainage_type = "two-way"
474
 
475
  # Time factor
476
+ if H > 0:
477
+ Tv = Cv * t / H**2
478
+ else:
479
+ Tv = 1e10 # Very large, instant drainage
480
 
481
  # Calculate Uv using Terzaghi's solution
482
+ if drainage_type == "one-way":
483
+ # One-way drainage (for layers below PVD)
484
+ if Tv < 0.05:
485
+ Uv[i] = np.sqrt(4 * Tv / np.pi)
486
+ else:
487
+ Uv[i] = 1 - (8 / np.pi**2) * np.exp(-(np.pi**2) * Tv / 4)
488
  else:
489
+ # Two-way drainage (for layers within PVD zone)
490
+ if Tv < 0.217:
491
+ Uv[i] = np.sqrt(4 * Tv / np.pi)
492
+ else:
493
+ Uv[i] = 1 - (8 / np.pi**2) * np.exp(-(np.pi**2) * Tv / 4)
494
 
495
  Uv[i] = min(Uv[i], 1.0)
496
 
 
533
  def calculate_settlement(self, t: float) -> Tuple[float, np.ndarray]:
534
  """
535
  Calculate total settlement at time t
536
+ For staged loading: calculates settlement contribution from each stage
537
 
538
  Parameters:
539
  -----------
 
547
  layer_settlements : ndarray
548
  Settlement for each layer (m)
549
  """
 
 
550
  layer_settlements = np.zeros(len(self.layers))
551
 
552
+ # For each loading stage, calculate its contribution to settlement
553
+ for stage_idx, stage in enumerate(self.loading_stages):
554
+ if t < stage.start_time:
555
+ # This stage hasn't started yet
556
+ continue
557
+
558
+ # Time since this stage started
559
+ t_stage = t - stage.start_time
560
+
561
+ # Calculate consolidation degree for this stage's time duration
562
+ U_stage = self.calculate_total_U(t_stage)
563
+
564
+ # Calculate settlement for each layer from this load increment
565
+ for i, layer in enumerate(self.layers):
566
+ # Find nodes in this layer
567
+ layer_mask = self.layer_indices == i
568
+ U_layer = np.mean(U_stage[layer_mask])
569
+
570
+ # Get layer-specific vacuum (decreases with depth)
571
+ # Previous stage vacuum for this layer
572
+ if stage_idx == 0:
573
+ avg_vacuum_prev_layer = 0.0
574
+ else:
575
+ prev_stage = self.loading_stages[stage_idx - 1]
576
+ avg_vacuum_prev_layer = self.calculate_layer_average_vacuum(
577
+ i, prev_stage.vacuum
578
  )
579
+
580
+ # Current stage vacuum for this layer
581
+ avg_vacuum_current_layer = self.calculate_layer_average_vacuum(
582
+ i, stage.vacuum
583
+ )
584
+
585
+ # Previous stage load for this layer
586
+ if stage_idx == 0:
587
+ prev_surcharge = 0.0
588
+ sigma_prev_layer = 0.0
589
+ else:
590
+ prev_surcharge = self.loading_stages[stage_idx - 1].surcharge
591
+ sigma_prev_layer = prev_surcharge + avg_vacuum_prev_layer
592
+
593
+ # Current stage load for this layer
594
+ sigma_current_layer = stage.surcharge + avg_vacuum_current_layer
595
+
596
+ # Load increment for this layer
597
+ delta_sigma_layer = sigma_current_layer - sigma_prev_layer
598
+
599
+ if abs(delta_sigma_layer) < 1e-6:
600
+ # No significant load change in this layer
601
+ continue
602
+
603
+ # Stress before this stage
604
+ sigma_before = layer.sigma_ini + sigma_prev_layer
605
+
606
+ # Stress after this stage (if fully consolidated)
607
+ sigma_after = layer.sigma_ini + sigma_current_layer
608
+
609
+ # Calculate ultimate settlement for this load increment
610
+ Sc_increment = 0.0
611
+
612
+ if delta_sigma_layer > 0:
613
+ # Loading increment
614
+ if sigma_after <= layer.sigma_p:
615
+ # All in recompression range
616
+ Sc_increment = (
617
+ layer.RR
618
+ * np.log10(sigma_after / sigma_before)
619
+ * layer.thickness
620
+ )
621
+ elif sigma_before >= layer.sigma_p:
622
+ # All in virgin compression range
623
+ Sc_increment = (
624
+ layer.CR
625
+ * np.log10(sigma_after / sigma_before)
626
+ * layer.thickness
627
+ )
628
+ else:
629
+ # Crosses from recompression to virgin compression
630
+ Sc_recomp = (
631
+ layer.RR
632
+ * np.log10(layer.sigma_p / sigma_before)
633
+ * layer.thickness
634
+ )
635
+ Sc_virgin = (
636
+ layer.CR
637
+ * np.log10(sigma_after / layer.sigma_p)
638
+ * layer.thickness
639
+ )
640
+ Sc_increment = Sc_recomp + Sc_virgin
641
  else:
642
+ # Unloading (e.g., vacuum removal) - use recompression/swelling
643
+ Sc_increment = (
644
  layer.RR
645
+ * np.log10(sigma_after / sigma_before)
 
 
 
 
 
646
  * layer.thickness
647
  )
 
648
 
649
+ # Add this stage's contribution (with consolidation degree)
650
+ layer_settlements[i] += U_layer * Sc_increment
651
 
652
  total_settlement = np.sum(layer_settlements)
653