schattin commited on
Commit
d7eb045
·
1 Parent(s): add7944

feature(first version): First version to run the simulator with parametes.

Browse files
Files changed (1) hide show
  1. app.py +241 -116
app.py CHANGED
@@ -5,6 +5,7 @@ from typing import Sequence
5
  import matplotlib.pyplot as plt
6
  import numpy as np
7
  import torch
 
8
 
9
  try:
10
  import gradio as gr
@@ -13,41 +14,19 @@ except ModuleNotFoundError: # pragma: no cover - optional dependency for tests
13
 
14
  from surfdisp2k25 import dispsurf2k25_simulator
15
 
16
- MODEL_SIZE = 60
17
- CUSTOM_MODEL_LABEL = "Custom (enter values below)"
18
 
19
- # Preset shear-wave velocity profiles (km/s) sampled over 60 layers.
20
- PRESET_MODELS: dict[str, np.ndarray] = {
21
- "Soft Sedimentary Basin": np.concatenate(
22
- [
23
- np.linspace(0.8, 1.6, 20, dtype=np.float32),
24
- np.linspace(1.6, 2.4, 20, dtype=np.float32),
25
- np.linspace(2.4, 3.2, 20, dtype=np.float32),
26
- ]
27
- ),
28
- "Continental Crust": np.concatenate(
29
- [
30
- np.linspace(2.6, 3.2, 15, dtype=np.float32),
31
- np.linspace(3.2, 3.8, 25, dtype=np.float32),
32
- np.linspace(3.8, 4.5, 20, dtype=np.float32),
33
- ]
34
- ),
35
- "Oceanic Lithosphere": np.concatenate(
36
- [
37
- np.linspace(1.8, 2.4, 15, dtype=np.float32),
38
- np.linspace(2.4, 3.4, 25, dtype=np.float32),
39
- np.linspace(3.4, 4.2, 20, dtype=np.float32),
40
- ]
41
- ),
42
- }
43
-
44
- DEFAULT_CUSTOM_PROFILE = PRESET_MODELS["Continental Crust"]
45
- DEFAULT_TABLE = [[float(v)] for v in DEFAULT_CUSTOM_PROFILE]
46
-
47
- EARTH_MODEL_OPTIONS = {
48
- "Flat Earth (iflsph=0)": 0,
49
- "Spherical Earth (iflsph=1)": 1,
50
- }
51
 
52
  WAVE_TYPE_OPTIONS = {
53
  "Rayleigh waves (iwave=2)": 2,
@@ -70,69 +49,151 @@ def _fail(message: str) -> None:
70
  raise ValidationError(message)
71
 
72
 
73
- def _extract_model_values(
74
- model_label: str, table_values: Sequence[Sequence[float]]
75
- ) -> np.ndarray:
76
- if model_label != CUSTOM_MODEL_LABEL:
77
- return PRESET_MODELS[model_label]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- flattened: list[float] = []
80
- for row in table_values:
81
- if isinstance(row, (list, tuple)):
82
- value = row[0] if row else None
83
- else:
84
- value = row
 
 
85
 
86
- if value in (None, ""):
87
- _fail("All 60 custom model rows must contain a value.")
88
  try:
89
- flattened.append(float(value))
90
- except (TypeError, ValueError) as exc:
91
- _fail("Custom model values must be numeric.")
92
-
93
- values = np.asarray(flattened, dtype=np.float32)
94
- if values.size != MODEL_SIZE:
95
- _fail(
96
- f"Custom model must contain exactly {MODEL_SIZE} values "
97
- f"(received {values.size})."
98
- )
99
- if np.any((values < 0.0) | (values > 8.0)):
100
- _fail("Custom model values must stay between 0 and 8 km/s.")
101
- return values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
 
104
  def _build_theta(
105
- vs_values: np.ndarray, vp_vs_ratio: float, min_depth: float, max_depth: float
106
  ) -> torch.Tensor:
107
  if vp_vs_ratio <= 1.0:
108
  _fail("Vp/Vs ratio must be greater than 1.0.")
109
- if max_depth <= min_depth:
110
- _fail("Max depth must be greater than min depth.")
111
 
112
  n_layers = vs_values.size
113
  if n_layers == 0:
114
  _fail("Model must contain at least one layer.")
115
- if n_layers > 100:
116
- _fail("Models cannot exceed 100 layers.")
117
-
118
- if n_layers == 1:
119
- layer_thickness = 0.0
120
- else:
121
- layer_thickness = (max_depth - min_depth) / (n_layers - 1)
122
 
123
- thickness = np.full(n_layers, layer_thickness, dtype=np.float32)
124
- thickness[-1] = 0.0 # Half-space
 
 
 
 
125
 
126
  theta = np.concatenate(
127
  [
128
  np.array([float(n_layers), float(vp_vs_ratio)], dtype=np.float32),
129
- thickness,
130
  vs_values.astype(np.float32),
131
  ]
132
  )
133
  return torch.from_numpy(theta).unsqueeze(0)
134
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def _make_plot(periods: np.ndarray, velocities: np.ndarray):
137
  fig, ax = plt.subplots(figsize=(7, 4))
138
  ax.plot(periods, velocities, marker="o", markersize=3, linewidth=1.5)
@@ -144,20 +205,56 @@ def _make_plot(periods: np.ndarray, velocities: np.ndarray):
144
  return fig
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def run_simulation(
148
- model_label: str,
149
- custom_values: Sequence[Sequence[float]],
150
- min_depth: float,
151
- max_depth: float,
152
  vp_vs_ratio: float,
153
  p_min: float,
154
  p_max: float,
155
- earth_model_label: str,
156
  wave_type_label: str,
157
  mode: int,
158
  group_label: str,
159
  ):
160
- vs_values = _extract_model_values(model_label, custom_values)
161
 
162
  if p_min <= 0.0 or p_max <= 0.0:
163
  _fail("Periods must be positive numbers.")
@@ -165,9 +262,11 @@ def run_simulation(
165
  _fail("Maximum period must be greater than minimum period.")
166
  if mode < 1:
167
  _fail("Mode number must be at least 1.")
 
 
168
 
169
- theta = _build_theta(vs_values, vp_vs_ratio, min_depth, max_depth)
170
- iflsph = EARTH_MODEL_OPTIONS[earth_model_label]
171
  iwave = WAVE_TYPE_OPTIONS[wave_type_label]
172
  igr = GROUP_VELOCITY_OPTIONS[group_label]
173
 
@@ -176,7 +275,7 @@ def run_simulation(
176
  theta=theta,
177
  p_min=float(p_min),
178
  p_max=float(p_max),
179
- kmax=MODEL_SIZE,
180
  iflsph=iflsph,
181
  iwave=iwave,
182
  mode=int(mode),
@@ -188,52 +287,64 @@ def run_simulation(
188
 
189
  velocities = disp.squeeze(0).detach().cpu().numpy()
190
  periods = np.linspace(p_min, p_max, velocities.size)
191
- plot = _make_plot(periods, velocities)
 
192
 
193
  table = [[float(p), float(v)] for p, v in zip(periods, velocities)]
194
 
195
- layer_thickness = 0.0 if vs_values.size <= 1 else (max_depth - min_depth) / (
196
- vs_values.size - 1
197
- )
 
198
  summary = "\n".join(
199
  [
200
- f"**Model**: {model_label}",
201
- f"**Layers**: {vs_values.size} (Δz ≈ {layer_thickness:.2f} km)",
 
 
 
202
  f"**Vs range**: {vs_values.min():.2f} – {vs_values.max():.2f} km/s",
 
203
  f"**Periods**: {p_min:.2f} – {p_max:.2f} s ({velocities.size} samples)",
204
  f"**Wave type**: {wave_type_label}; mode = {int(mode)}",
205
  f"**Phase velocity range**: {velocities.min():.2f} – {velocities.max():.2f} km/s",
206
  ]
207
  )
208
 
209
- return plot, table, summary
210
 
211
 
212
  if gr is not None:
213
  with gr.Blocks(title="SurfDisp2k25 Simulator") as demo:
214
  gr.Markdown(
215
- "### SurfDisp2k25 dispersion simulator\n"
216
- "Select a preset velocity profile or switch to custom mode to edit the "
217
- "60 Vs values (km/s) sampled between the minimum and maximum depth."
 
 
 
 
 
 
 
 
 
 
 
 
218
  )
219
 
220
  with gr.Row():
221
  with gr.Column():
222
- model_selector = gr.Dropdown(
223
- choices=[*PRESET_MODELS.keys(), CUSTOM_MODEL_LABEL],
224
- value="Continental Crust",
225
- label="Shear-wave velocity model",
226
- )
227
  custom_model = gr.Dataframe(
228
- headers=["Vs (km/s)"],
229
  value=DEFAULT_TABLE,
230
- row_count=(MODEL_SIZE, "fixed"),
231
- col_count=(1, "fixed"),
232
- datatype="float",
233
- label="Custom Vs profile (used when Custom is selected)",
234
  )
235
- min_depth_input = gr.Number(value=0.0, label="Minimum depth (km)")
236
- max_depth_input = gr.Number(value=30.0, label="Maximum depth (km)")
237
  vpvs_input = gr.Slider(
238
  minimum=1.5,
239
  maximum=2.2,
@@ -244,11 +355,6 @@ if gr is not None:
244
  with gr.Column():
245
  p_min_input = gr.Number(value=1.0, label="Minimum period (s)")
246
  p_max_input = gr.Number(value=30.0, label="Maximum period (s)")
247
- earth_model_input = gr.Radio(
248
- choices=list(EARTH_MODEL_OPTIONS.keys()),
249
- value="Flat Earth (iflsph=0)",
250
- label="Earth model",
251
- )
252
  wave_type_input = gr.Radio(
253
  choices=list(WAVE_TYPE_OPTIONS.keys()),
254
  value="Rayleigh waves (iwave=2)",
@@ -256,7 +362,7 @@ if gr is not None:
256
  )
257
  mode_input = gr.Slider(
258
  minimum=1,
259
- maximum=5,
260
  value=1,
261
  step=1,
262
  label="Mode number",
@@ -269,33 +375,52 @@ if gr is not None:
269
  run_button = gr.Button("Run simulation", variant="primary")
270
 
271
  with gr.Row():
 
272
  plot_output = gr.Plot(label="Dispersion curve")
 
 
273
  table_output = gr.Dataframe(
274
  headers=["Period (s)", "Phase velocity (km/s)"],
275
  datatype="float",
276
  col_count=(2, "fixed"),
277
- row_count=(MODEL_SIZE, "dynamic"),
278
  label="Sampled dispersion values",
279
  )
280
 
281
  summary_output = gr.Markdown()
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  run_button.click(
284
  fn=run_simulation,
285
  inputs=[
286
- model_selector,
287
  custom_model,
288
- min_depth_input,
289
- max_depth_input,
290
  vpvs_input,
291
  p_min_input,
292
  p_max_input,
293
- earth_model_input,
294
  wave_type_input,
295
  mode_input,
296
  group_mode_input,
297
  ],
298
- outputs=[plot_output, table_output, summary_output],
299
  )
300
  else: # pragma: no cover - allows importing without gradio installed
301
  demo = None
 
5
  import matplotlib.pyplot as plt
6
  import numpy as np
7
  import torch
8
+ import math
9
 
10
  try:
11
  import gradio as gr
 
14
 
15
  from surfdisp2k25 import dispsurf2k25_simulator
16
 
17
+ DISPERSION_SAMPLES = 60
18
+ MAX_LAYERS = 100
19
 
20
+ # Default layered model with a half-space.
21
+ DEFAULT_CUSTOM_PROFILE: list[tuple[float, float]] = [
22
+ (1.0, 2.0),
23
+ (1.0, 2.5),
24
+ (1.0, 3.0),
25
+ (0.0, 3.5),
26
+ ]
27
+ DEFAULT_THICKNESS = np.asarray([layer[0] for layer in DEFAULT_CUSTOM_PROFILE], dtype=np.float32)
28
+ DEFAULT_VS = np.asarray([layer[1] for layer in DEFAULT_CUSTOM_PROFILE], dtype=np.float32)
29
+ DEFAULT_TABLE = [[float(h), float(vs)] for h, vs in DEFAULT_CUSTOM_PROFILE]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  WAVE_TYPE_OPTIONS = {
32
  "Rayleigh waves (iwave=2)": 2,
 
49
  raise ValidationError(message)
50
 
51
 
52
+ def _normalize_table(table_values: Sequence[Sequence[float]]) -> list[list[float]]:
53
+ if table_values is None: # type: ignore[comparison-overlap]
54
+ return []
55
+ if hasattr(table_values, "to_numpy"):
56
+ return table_values.to_numpy().tolist() # type: ignore[no-any-return]
57
+ if isinstance(table_values, np.ndarray):
58
+ return table_values.tolist()
59
+ return [list(row) if isinstance(row, (list, tuple, np.ndarray)) else [row] for row in table_values] # type: ignore[misc]
60
+
61
+
62
+ def _is_blank(value: object) -> bool:
63
+ if value in (None, ""):
64
+ return True
65
+ if isinstance(value, (float, np.floating)):
66
+ return math.isnan(value)
67
+ return False
68
+
69
+
70
+ def _table_is_empty(table_values: Sequence[Sequence[float]]) -> bool:
71
+ rows = _normalize_table(table_values)
72
+ for row in rows:
73
+ if not isinstance(row, (list, tuple)):
74
+ continue
75
+ if len(row) < 2:
76
+ continue
77
+ if not (_is_blank(row[0]) and _is_blank(row[1])):
78
+ return False
79
+ return True
80
+
81
+
82
+ def _extract_layer_values(
83
+ table_values: Sequence[Sequence[float]] | None,
84
+ ) -> tuple[np.ndarray, np.ndarray]:
85
+ normalized = _normalize_table(table_values)
86
+
87
+ if _table_is_empty(normalized):
88
+ return DEFAULT_THICKNESS.copy(), DEFAULT_VS.copy()
89
+
90
+ thickness_values: list[float] = []
91
+ vs_values: list[float] = []
92
+ for idx, row in enumerate(normalized, start=1):
93
+ if not isinstance(row, (list, tuple)):
94
+ _fail("Each layer must provide thickness and Vs.")
95
 
96
+ if len(row) < 2:
97
+ _fail(f"Layer {idx} must include both thickness and Vs.")
98
+
99
+ raw_thickness, raw_vs = row[0], row[1]
100
+ if _is_blank(raw_thickness) and _is_blank(raw_vs):
101
+ continue # Ignore empty rows for dynamic tables
102
+ if _is_blank(raw_thickness) or _is_blank(raw_vs):
103
+ _fail(f"Layer {idx} must include both thickness and Vs.")
104
 
 
 
105
  try:
106
+ thickness = float(raw_thickness)
107
+ vs = float(raw_vs)
108
+ except (TypeError, ValueError):
109
+ _fail("Layer thickness and Vs must be numeric values.")
110
+
111
+ if thickness < 0.0:
112
+ _fail(f"Layer {idx} thickness must be non-negative.")
113
+ if vs <= 0.0:
114
+ _fail(f"Layer {idx} shear-wave velocity must be greater than 0.")
115
+
116
+ thickness_values.append(thickness)
117
+ vs_values.append(vs)
118
+
119
+ if not thickness_values:
120
+ _fail("Custom model must contain at least one layer.")
121
+
122
+ if len(thickness_values) > MAX_LAYERS:
123
+ _fail(f"Models cannot exceed {MAX_LAYERS} layers.")
124
+
125
+ thickness_array = np.asarray(thickness_values, dtype=np.float32)
126
+ vs_array = np.asarray(vs_values, dtype=np.float32)
127
+
128
+ if thickness_array.size == 0:
129
+ _fail("Custom model must contain at least one layer.")
130
+
131
+ if thickness_array[-1] != 0.0:
132
+ thickness_array[-1] = 0.0
133
+
134
+ if np.any(thickness_array[:-1] <= 0.0):
135
+ _fail("Only the final layer may have zero thickness.")
136
+
137
+ return thickness_array, vs_array
138
 
139
 
140
  def _build_theta(
141
+ thickness_values: np.ndarray, vs_values: np.ndarray, vp_vs_ratio: float
142
  ) -> torch.Tensor:
143
  if vp_vs_ratio <= 1.0:
144
  _fail("Vp/Vs ratio must be greater than 1.0.")
145
+ if thickness_values.size != vs_values.size:
146
+ _fail("Each layer must have a corresponding Vs value.")
147
 
148
  n_layers = vs_values.size
149
  if n_layers == 0:
150
  _fail("Model must contain at least one layer.")
151
+ if n_layers > MAX_LAYERS:
152
+ _fail(f"Models cannot exceed {MAX_LAYERS} layers.")
 
 
 
 
 
153
 
154
+ if np.any(thickness_values < 0.0):
155
+ _fail("Layer thickness values must be non-negative.")
156
+ if thickness_values[-1] != 0.0:
157
+ _fail("The final layer must have zero thickness (half-space).")
158
+ if np.any(vs_values <= 0.0):
159
+ _fail("Shear-wave velocity must be positive.")
160
 
161
  theta = np.concatenate(
162
  [
163
  np.array([float(n_layers), float(vp_vs_ratio)], dtype=np.float32),
164
+ thickness_values.astype(np.float32),
165
  vs_values.astype(np.float32),
166
  ]
167
  )
168
  return torch.from_numpy(theta).unsqueeze(0)
169
 
170
 
171
+ def _make_model_plot(
172
+ thickness_values: np.ndarray, vs_values: np.ndarray
173
+ ):
174
+ min_depth = 0.0
175
+ cumulative_depth = np.cumsum(thickness_values)
176
+ depth_edges = min_depth + np.concatenate(([0.0], cumulative_depth))
177
+ velocity_steps = np.concatenate((vs_values, [vs_values[-1]]))
178
+
179
+ plot_depth = np.concatenate((depth_edges, [depth_edges[-1] + 1.0]))
180
+ plot_velocity = np.concatenate((velocity_steps, [velocity_steps[-1]]))
181
+
182
+ max_depth = depth_edges[-1] + 1.0
183
+ if max_depth <= min_depth:
184
+ max_depth = min_depth + 1.0
185
+
186
+ fig, ax = plt.subplots(figsize=(7, 4))
187
+ ax.step(plot_depth, plot_velocity, where="post", linewidth=1.5)
188
+ ax.set_xlabel("Depth (km)")
189
+ ax.set_ylabel("Shear velocity (km/s)")
190
+ ax.set_title("Layered Vs model")
191
+ ax.set_xlim(min_depth, max_depth)
192
+ ax.grid(True, linestyle="--", linewidth=0.6, alpha=0.5)
193
+ fig.tight_layout()
194
+ return fig
195
+
196
+
197
  def _make_plot(periods: np.ndarray, velocities: np.ndarray):
198
  fig, ax = plt.subplots(figsize=(7, 4))
199
  ax.plot(periods, velocities, marker="o", markersize=3, linewidth=1.5)
 
205
  return fig
206
 
207
 
208
+ def _layers_to_table(thickness: np.ndarray, vs: np.ndarray) -> list[list[float]]:
209
+ return [[float(h), float(v)] for h, v in zip(thickness, vs)]
210
+
211
+
212
+ def _generate_random_model() -> tuple[np.ndarray, np.ndarray]:
213
+ rng = np.random.default_rng()
214
+ n_layers = int(rng.integers(2, 21))
215
+ finite_thickness = rng.uniform(0.1, 3.0, size=n_layers - 1).astype(np.float32)
216
+ vs_values = rng.uniform(2.0, 8.0, size=n_layers - 1).astype(np.float32)
217
+ thickness = np.concatenate([finite_thickness, np.array([0.0], dtype=np.float32)])
218
+ last_vs = float(rng.uniform(2.0, 8.0))
219
+ while abs(last_vs - float(vs_values[-1])) < 1e-3:
220
+ last_vs = float(rng.uniform(2.0, 8.0))
221
+ vs = np.concatenate([vs_values, np.array([last_vs], dtype=np.float32)])
222
+ return thickness, vs
223
+
224
+
225
+ def on_layer_table_change(
226
+ table_values: Sequence[Sequence[float]] | None,
227
+ ):
228
+ if gr is None:
229
+ raise RuntimeError("Gradio is required for interactive updates.")
230
+
231
+ thickness, vs = _extract_layer_values(table_values)
232
+ sanitized_table = _layers_to_table(thickness, vs)
233
+ plot = _make_model_plot(thickness, vs)
234
+
235
+ return sanitized_table, plot
236
+
237
+
238
+ def on_random_model_click():
239
+ if gr is None:
240
+ raise RuntimeError("Gradio is required for interactive updates.")
241
+
242
+ thickness, vs = _generate_random_model()
243
+ table = _layers_to_table(thickness, vs)
244
+ plot = _make_model_plot(thickness, vs)
245
+ return table, plot
246
+
247
+
248
  def run_simulation(
249
+ custom_values: Sequence[Sequence[float]] | None,
 
 
 
250
  vp_vs_ratio: float,
251
  p_min: float,
252
  p_max: float,
 
253
  wave_type_label: str,
254
  mode: int,
255
  group_label: str,
256
  ):
257
+ thickness_values, vs_values = _extract_layer_values(custom_values)
258
 
259
  if p_min <= 0.0 or p_max <= 0.0:
260
  _fail("Periods must be positive numbers.")
 
262
  _fail("Maximum period must be greater than minimum period.")
263
  if mode < 1:
264
  _fail("Mode number must be at least 1.")
265
+ if mode > 3:
266
+ _fail("Mode number cannot exceed 3 in this simulator.")
267
 
268
+ theta = _build_theta(thickness_values, vs_values, vp_vs_ratio)
269
+ iflsph = 0 # Always use flat Earth
270
  iwave = WAVE_TYPE_OPTIONS[wave_type_label]
271
  igr = GROUP_VELOCITY_OPTIONS[group_label]
272
 
 
275
  theta=theta,
276
  p_min=float(p_min),
277
  p_max=float(p_max),
278
+ kmax=DISPERSION_SAMPLES,
279
  iflsph=iflsph,
280
  iwave=iwave,
281
  mode=int(mode),
 
287
 
288
  velocities = disp.squeeze(0).detach().cpu().numpy()
289
  periods = np.linspace(p_min, p_max, velocities.size)
290
+ dispersion_plot = _make_plot(periods, velocities)
291
+ model_plot = _make_model_plot(thickness_values, vs_values)
292
 
293
  table = [[float(p), float(v)] for p, v in zip(periods, velocities)]
294
 
295
+ finite_thickness = thickness_values[:-1] if thickness_values.size > 1 else thickness_values
296
+ total_depth = float(np.sum(finite_thickness))
297
+ max_depth = total_depth
298
+ n_finite_layers = finite_thickness.size
299
  summary = "\n".join(
300
  [
301
+ "**Model**: Custom layered model",
302
+ f"**Layers**: {vs_values.size} (including half-space)",
303
+ f"**Finite thickness layers**: {n_finite_layers}",
304
+ f"**Depth window**: 0.00 – {max_depth:.2f} km",
305
+ f"**Vp/Vs ratio**: {vp_vs_ratio:.2f}",
306
  f"**Vs range**: {vs_values.min():.2f} – {vs_values.max():.2f} km/s",
307
+ f"**Total thickness**: {total_depth:.2f} km",
308
  f"**Periods**: {p_min:.2f} – {p_max:.2f} s ({velocities.size} samples)",
309
  f"**Wave type**: {wave_type_label}; mode = {int(mode)}",
310
  f"**Phase velocity range**: {velocities.min():.2f} – {velocities.max():.2f} km/s",
311
  ]
312
  )
313
 
314
+ return model_plot, dispersion_plot, table, summary
315
 
316
 
317
  if gr is not None:
318
  with gr.Blocks(title="SurfDisp2k25 Simulator") as demo:
319
  gr.Markdown(
320
+ """## SurfDisp2k25 - Interactive Surface Wave Dispersion Simulator (Alpha)
321
+
322
+ This simulator computes surface wave dispersion curves (Love and Rayleigh waves) for layered Earth models.
323
+ You can define layer thicknesses and shear-wave velocities manually or generate a random model.
324
+
325
+ **Parameters**
326
+
327
+ - Minimum/Maximum period (s) - Range of periods used to compute the dispersion curve.
328
+ - Wave type - Choose between Rayleigh (vertical-radial motion) and Love (horizontal shear) waves.
329
+ - Mode number - Number of modes (fundamental plus higher harmonics) to compute.
330
+ - Velocity output - Select whether to compute only phase velocity or both phase and group velocity.
331
+ - Vp/Vs ratio - Defines the P-wave velocity from the S-wave velocity.
332
+
333
+ The simulator displays both the layered Vs model and the resulting dispersion curve.
334
+ This is an alpha version; results may contain numerical artefacts, so use with caution for testing and visualization."""
335
  )
336
 
337
  with gr.Row():
338
  with gr.Column():
339
+ random_button = gr.Button("Generate random model")
 
 
 
 
340
  custom_model = gr.Dataframe(
341
+ headers=["Thickness (km)", "Vs (km/s)"],
342
  value=DEFAULT_TABLE,
343
+ row_count=(len(DEFAULT_TABLE), "dynamic"),
344
+ col_count=(2, "fixed"),
345
+ datatype=["float", "float"],
346
+ label="Custom layered model",
347
  )
 
 
348
  vpvs_input = gr.Slider(
349
  minimum=1.5,
350
  maximum=2.2,
 
355
  with gr.Column():
356
  p_min_input = gr.Number(value=1.0, label="Minimum period (s)")
357
  p_max_input = gr.Number(value=30.0, label="Maximum period (s)")
 
 
 
 
 
358
  wave_type_input = gr.Radio(
359
  choices=list(WAVE_TYPE_OPTIONS.keys()),
360
  value="Rayleigh waves (iwave=2)",
 
362
  )
363
  mode_input = gr.Slider(
364
  minimum=1,
365
+ maximum=3,
366
  value=1,
367
  step=1,
368
  label="Mode number",
 
375
  run_button = gr.Button("Run simulation", variant="primary")
376
 
377
  with gr.Row():
378
+ model_plot_output = gr.Plot(label="Shear-wave velocity profile")
379
  plot_output = gr.Plot(label="Dispersion curve")
380
+
381
+ with gr.Row():
382
  table_output = gr.Dataframe(
383
  headers=["Period (s)", "Phase velocity (km/s)"],
384
  datatype="float",
385
  col_count=(2, "fixed"),
386
+ row_count=(DISPERSION_SAMPLES, "dynamic"),
387
  label="Sampled dispersion values",
388
  )
389
 
390
  summary_output = gr.Markdown()
391
 
392
+ demo.load(
393
+ fn=lambda: _make_model_plot(DEFAULT_THICKNESS, DEFAULT_VS),
394
+ inputs=None,
395
+ outputs=model_plot_output,
396
+ )
397
+
398
+ custom_model.change(
399
+ fn=on_layer_table_change,
400
+ inputs=custom_model,
401
+ outputs=[custom_model, model_plot_output],
402
+ trigger_mode="always_last",
403
+ queue=False,
404
+ )
405
+
406
+ random_button.click(
407
+ fn=on_random_model_click,
408
+ inputs=None,
409
+ outputs=[custom_model, model_plot_output],
410
+ )
411
+
412
  run_button.click(
413
  fn=run_simulation,
414
  inputs=[
 
415
  custom_model,
 
 
416
  vpvs_input,
417
  p_min_input,
418
  p_max_input,
 
419
  wave_type_input,
420
  mode_input,
421
  group_mode_input,
422
  ],
423
+ outputs=[model_plot_output, plot_output, table_output, summary_output],
424
  )
425
  else: # pragma: no cover - allows importing without gradio installed
426
  demo = None