Spaces:
Sleeping
Sleeping
feature(first version): First version to run the simulator with parametes.
Browse files
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 |
-
|
| 17 |
-
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
if
|
| 77 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
if value in (None, ""):
|
| 87 |
-
_fail("All 60 custom model rows must contain a value.")
|
| 88 |
try:
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
f"
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
def _build_theta(
|
| 105 |
-
|
| 106 |
) -> torch.Tensor:
|
| 107 |
if vp_vs_ratio <= 1.0:
|
| 108 |
_fail("Vp/Vs ratio must be greater than 1.0.")
|
| 109 |
-
if
|
| 110 |
-
_fail("
|
| 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 >
|
| 116 |
-
_fail("Models cannot exceed
|
| 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 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
theta = np.concatenate(
|
| 127 |
[
|
| 128 |
np.array([float(n_layers), float(vp_vs_ratio)], dtype=np.float32),
|
| 129 |
-
|
| 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 |
-
|
| 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 =
|
| 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(
|
| 170 |
-
iflsph =
|
| 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=
|
| 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 |
-
|
|
|
|
| 192 |
|
| 193 |
table = [[float(p), float(v)] for p, v in zip(periods, velocities)]
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
| 198 |
summary = "\n".join(
|
| 199 |
[
|
| 200 |
-
|
| 201 |
-
f"**Layers**: {vs_values.size} (
|
|
|
|
|
|
|
|
|
|
| 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
|
| 210 |
|
| 211 |
|
| 212 |
if gr is not None:
|
| 213 |
with gr.Blocks(title="SurfDisp2k25 Simulator") as demo:
|
| 214 |
gr.Markdown(
|
| 215 |
-
"##
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
)
|
| 219 |
|
| 220 |
with gr.Row():
|
| 221 |
with gr.Column():
|
| 222 |
-
|
| 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=(
|
| 231 |
-
col_count=(
|
| 232 |
-
datatype="float",
|
| 233 |
-
label="Custom
|
| 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=
|
| 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=(
|
| 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
|