seiscode-models 0.1.0__tar.gz
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.
- seiscode_models-0.1.0/LICENSE +21 -0
- seiscode_models-0.1.0/PKG-INFO +31 -0
- seiscode_models-0.1.0/README.md +1 -0
- seiscode_models-0.1.0/pyproject.toml +45 -0
- seiscode_models-0.1.0/setup.cfg +4 -0
- seiscode_models-0.1.0/src/seiscode_models/__init__.py +42 -0
- seiscode_models-0.1.0/src/seiscode_models/models/__init__.py +82 -0
- seiscode_models-0.1.0/src/seiscode_models/models/geological.py +92 -0
- seiscode_models-0.1.0/src/seiscode_models/models/primitives.py +82 -0
- seiscode_models-0.1.0/src/seiscode_models/utils/__init__.py +0 -0
- seiscode_models-0.1.0/src/seiscode_models/utils/dtypes.py +45 -0
- seiscode_models-0.1.0/src/seiscode_models/utils/io.py +68 -0
- seiscode_models-0.1.0/src/seiscode_models/utils/validation.py +32 -0
- seiscode_models-0.1.0/src/seiscode_models/utils/visualization.py +39 -0
- seiscode_models-0.1.0/src/seiscode_models.egg-info/PKG-INFO +31 -0
- seiscode_models-0.1.0/src/seiscode_models.egg-info/SOURCES.txt +22 -0
- seiscode_models-0.1.0/src/seiscode_models.egg-info/dependency_links.txt +1 -0
- seiscode_models-0.1.0/src/seiscode_models.egg-info/requires.txt +9 -0
- seiscode_models-0.1.0/src/seiscode_models.egg-info/top_level.txt +1 -0
- seiscode_models-0.1.0/tests/test_dtypes.py +24 -0
- seiscode_models-0.1.0/tests/test_geological.py +27 -0
- seiscode_models-0.1.0/tests/test_io.py +33 -0
- seiscode_models-0.1.0/tests/test_primitives.py +35 -0
- seiscode_models-0.1.0/tests/test_visualization.py +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Damien Pageot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seiscode-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Synthetic subsurface velocity model generators for seismic testing
|
|
5
|
+
Author-email: Damien Pageot <damien.pageot@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PageotD/seiscode-models
|
|
8
|
+
Project-URL: Repository, https://github.com/PageotD/seiscode-models
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.21.0
|
|
22
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
|
+
Requires-Dist: build; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# SeisCode Models
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SeisCode Models
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "seiscode-models"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Synthetic subsurface velocity model generators for seismic testing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Damien Pageot", email = "damien.pageot@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.21.0",
|
|
28
|
+
"matplotlib>=3.5.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7.0",
|
|
34
|
+
"pytest-cov",
|
|
35
|
+
"ruff",
|
|
36
|
+
"build",
|
|
37
|
+
"twine",
|
|
38
|
+
# "sphinx",
|
|
39
|
+
# "sphinx-rtd-theme",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/PageotD/seiscode-models"
|
|
45
|
+
Repository = "https://github.com/PageotD/seiscode-models"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""SeisCode Models: Synthetic subsurface velocity model generators."""
|
|
2
|
+
from .models.primitives import (
|
|
3
|
+
gradient_background,
|
|
4
|
+
add_gaussian_anomaly,
|
|
5
|
+
add_fault_offset,
|
|
6
|
+
)
|
|
7
|
+
from .models.geological import (
|
|
8
|
+
dipping_layers,
|
|
9
|
+
sinusoidal_interface,
|
|
10
|
+
faulted_layers,
|
|
11
|
+
checkerboard,
|
|
12
|
+
salt_dome,
|
|
13
|
+
low_velocity_layer,
|
|
14
|
+
heterogeneous_anomaly_model
|
|
15
|
+
)
|
|
16
|
+
from .utils.dtypes import parse_dtype
|
|
17
|
+
from .utils.validation import validate_model
|
|
18
|
+
from .utils.io import (
|
|
19
|
+
export_model,
|
|
20
|
+
load_model
|
|
21
|
+
)
|
|
22
|
+
from .utils.visualization import quick_view
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"gradient_background",
|
|
28
|
+
"add_gaussian_anomaly",
|
|
29
|
+
"add_fault_offset",
|
|
30
|
+
"parse_dtype",
|
|
31
|
+
"validate_model",
|
|
32
|
+
"dipping_layers",
|
|
33
|
+
"sinusoidal_interface",
|
|
34
|
+
"faulted_layers",
|
|
35
|
+
"checkerboard",
|
|
36
|
+
"salt_dome",
|
|
37
|
+
"low_velocity_layer",
|
|
38
|
+
"heterogeneous_anomaly_model",
|
|
39
|
+
"export_model",
|
|
40
|
+
"load_model",
|
|
41
|
+
"quick_view"
|
|
42
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Primitive building blocks for subsurface velocity models with dtype support."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..utils.dtypes import parse_dtype
|
|
4
|
+
|
|
5
|
+
def gradient_background(nx: int, nz: int, v0: float = 1500.0, v1: float = 4500.0, dtype=np.float32) -> np.ndarray:
|
|
6
|
+
"""Generate a linear velocity gradient with depth.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
nx : int
|
|
11
|
+
Number of cells along x (horizontal direction).
|
|
12
|
+
nz : int
|
|
13
|
+
Number of cells along z (vertical direction, positive downward).
|
|
14
|
+
v0 : float, optional
|
|
15
|
+
Velocity at the surface, in m/s. Default is 1500.0.
|
|
16
|
+
v1 : float, optional
|
|
17
|
+
Velocity at the bottom of the model, in m/s. Default is 4500.0.
|
|
18
|
+
dtype : str or np.dtype, optional
|
|
19
|
+
Data type precision ('f32', 'f64', etc.). Default is np.float32.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
ndarray (nx, nz)
|
|
24
|
+
Velocity field in m/s with the requested dtype.
|
|
25
|
+
"""
|
|
26
|
+
dt = parse_dtype(dtype)
|
|
27
|
+
z = np.linspace(0.0, 1.0, nz, dtype=dt)
|
|
28
|
+
v_col = v0 + (v1 - v0) * z
|
|
29
|
+
return np.broadcast_to(v_col, (nx, nz)).copy()
|
|
30
|
+
|
|
31
|
+
def add_gaussian_anomaly(v: np.ndarray, cx: float, cz: float, sigma_x: float, sigma_z: float, dv: float, vmin: float = 200.0, vmax: float = 8000.0) -> np.ndarray:
|
|
32
|
+
"""Add a Gaussian velocity anomaly to an existing model.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
v : ndarray (nx, nz)
|
|
37
|
+
Velocity field to modify (not modified in place).
|
|
38
|
+
cx, cz : float
|
|
39
|
+
Center position along x and z, in cells.
|
|
40
|
+
sigma_x, sigma_z : float
|
|
41
|
+
Gaussian standard deviation along x and z, in cells.
|
|
42
|
+
dv : float
|
|
43
|
+
Amplitude of the anomaly at the center, in m/s.
|
|
44
|
+
vmin, vmax : float, optional
|
|
45
|
+
Clipping velocity bounds, in m/s.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
ndarray (nx, nz)
|
|
50
|
+
Copy of v with the anomaly added, preserving dtype and clipped.
|
|
51
|
+
"""
|
|
52
|
+
nx, nz = v.shape
|
|
53
|
+
dt = v.dtype
|
|
54
|
+
ii = np.arange(nx, dtype=dt)[:, None]
|
|
55
|
+
jj = np.arange(nz, dtype=dt)[None, :]
|
|
56
|
+
g = dv * np.exp(-((ii - cx) ** 2 / (2 * sigma_x ** 2) + (jj - cz) ** 2 / (2 * sigma_z ** 2)))
|
|
57
|
+
return np.clip(v + g, vmin, vmax).astype(dt)
|
|
58
|
+
|
|
59
|
+
def add_fault_offset(v: np.ndarray, ix_fault: int, throw: int) -> np.ndarray:
|
|
60
|
+
"""Shift layers on either side of a vertical fault.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
v : ndarray (nx, nz)
|
|
65
|
+
Stratified velocity field to shift.
|
|
66
|
+
ix_fault : int
|
|
67
|
+
Column index of the fault.
|
|
68
|
+
throw : int
|
|
69
|
+
Fault throw in cells along z.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
ndarray (nx, nz)
|
|
74
|
+
Shifted field preserving original dtype.
|
|
75
|
+
"""
|
|
76
|
+
out = v.copy()
|
|
77
|
+
left, right = slice(None, ix_fault), slice(ix_fault, None)
|
|
78
|
+
if throw >= 0:
|
|
79
|
+
out[left, throw:] = v[left, :out.shape[1] - throw]
|
|
80
|
+
else:
|
|
81
|
+
out[right, :throw] = v[right, -throw:]
|
|
82
|
+
return out
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Geological velocity models supporting f32/f64 precision."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .primitives import gradient_background, add_gaussian_anomaly, add_fault_offset
|
|
4
|
+
from ..utils.dtypes import parse_dtype
|
|
5
|
+
|
|
6
|
+
def dipping_layers(nx: int = 201, nz: int = 51, dx: float = 1.0, dz: float = 1.0,
|
|
7
|
+
v: tuple = (1800.0, 2400.0, 3200.0, 4200.0), dip_deg: float = 8.0, dtype=np.float32) -> np.ndarray:
|
|
8
|
+
"""Generate dipping planar layers with the same dip angle."""
|
|
9
|
+
dt = parse_dtype(dtype)
|
|
10
|
+
v_arr = np.asarray(v, dtype=dt)
|
|
11
|
+
x = np.arange(nx, dtype=dt) * dx
|
|
12
|
+
dip = np.deg2rad(dip_deg)
|
|
13
|
+
v_out = np.full((nx, nz), v_arr[-1], dtype=dt)
|
|
14
|
+
depths = [0.0]
|
|
15
|
+
for k in range(1, len(v_arr)):
|
|
16
|
+
depths.append(depths[-1] + 10.0 * k)
|
|
17
|
+
for k in range(len(v_arr) - 2, -1, -1):
|
|
18
|
+
z_int = depths[k] + (x - x.mean()) * np.tan(dip)
|
|
19
|
+
k_int = np.clip((z_int / dz).astype(int), 0, nz - 1)
|
|
20
|
+
for i in range(nx):
|
|
21
|
+
v_out[i, :k_int[i]] = v_arr[k]
|
|
22
|
+
return v_out
|
|
23
|
+
|
|
24
|
+
def sinusoidal_interface(nx: int = 201, nz: int = 51, period: float = 100.0, amplitude: float = 8.0,
|
|
25
|
+
v_top: float = 2000.0, v_bottom: float = 3500.0, dtype=np.float32) -> np.ndarray:
|
|
26
|
+
"""Generate two layers separated by a sinusoidal interface."""
|
|
27
|
+
dt = parse_dtype(dtype)
|
|
28
|
+
x = np.arange(nx, dtype=dt)
|
|
29
|
+
z_int = nz * 0.45 + amplitude * np.sin(2 * np.pi * x / period)
|
|
30
|
+
k_int = z_int.astype(int)
|
|
31
|
+
v = np.full((nx, nz), v_bottom, dtype=dt)
|
|
32
|
+
for i in range(nx):
|
|
33
|
+
v[i, :k_int[i]] = v_top
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
def faulted_layers(nx: int = 201, nz: int = 51, ix_fault: int = 120, throw: int = 8,
|
|
37
|
+
v: tuple = (1800.0, 2400.0, 3200.0, 4200.0), dtype=np.float32) -> np.ndarray:
|
|
38
|
+
"""Generate stratigraphic layers with a normal fault."""
|
|
39
|
+
dt = parse_dtype(dtype)
|
|
40
|
+
base = np.zeros((nx, nz), dtype=dt)
|
|
41
|
+
bounds = [0, 12, 28, 40, nz]
|
|
42
|
+
vs = np.asarray(v, dtype=dt)
|
|
43
|
+
for k in range(4):
|
|
44
|
+
base[:, bounds[k]:bounds[k + 1]] = vs[k]
|
|
45
|
+
return add_fault_offset(base, ix_fault, throw).astype(dt)
|
|
46
|
+
|
|
47
|
+
def checkerboard(nx: int = 201, nz: int = 51, cell: int = 20, v0: float = 3000.0,
|
|
48
|
+
dv: float = 600.0, dtype=np.float32) -> np.ndarray:
|
|
49
|
+
"""Generate a checkerboard pattern for resolution testing."""
|
|
50
|
+
dt = parse_dtype(dtype)
|
|
51
|
+
ii = np.arange(nx, dtype=dt)[:, None]
|
|
52
|
+
jj = np.arange(nz, dtype=dt)[None, :]
|
|
53
|
+
pat = ((ii // cell) + (jj // cell)) % 2
|
|
54
|
+
return np.where(pat == 0, v0 + dv, v0 - dv).astype(dt)
|
|
55
|
+
|
|
56
|
+
def salt_dome(nx: int = 201, nz: int = 51, cx: int = 140, width: float = 40.0,
|
|
57
|
+
v_salt: float = 4500.0, v_sed: float = 2500.0, dtype=np.float32) -> np.ndarray:
|
|
58
|
+
"""Generate a salt dome model with semi-elliptical shape."""
|
|
59
|
+
dt = parse_dtype(dtype)
|
|
60
|
+
v = gradient_background(nx, nz, v_sed, 4000.0, dtype=dt)
|
|
61
|
+
ii = np.arange(nx, dtype=dt)[:, None]
|
|
62
|
+
jj = np.arange(nz, dtype=dt)[None, :]
|
|
63
|
+
r = ((ii - cx) / width) ** 2 + np.clip((nz * 0.9 - jj) / (nz * 0.9), 0, None) ** 2
|
|
64
|
+
v[r < 1.0] = v_salt
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
def low_velocity_layer(nx: int = 201, nz: int = 51, k0: int = 20, thickness: int = 8,
|
|
68
|
+
v_slow: float = 1200.0, dtype=np.float32) -> np.ndarray:
|
|
69
|
+
"""Generate a model with a buried low-velocity channel."""
|
|
70
|
+
dt = parse_dtype(dtype)
|
|
71
|
+
v = gradient_background(nx, nz, 1800.0, 4500.0, dtype=dt)
|
|
72
|
+
v[:, k0:k0 + thickness] = v_slow
|
|
73
|
+
return v
|
|
74
|
+
|
|
75
|
+
def heterogeneous_anomaly_model(nx: int = 384, nz: int = 122, seed: int = 7, dtype=np.float32) -> np.ndarray:
|
|
76
|
+
"""Generate a complex synthetic model."""
|
|
77
|
+
dt = parse_dtype(dtype)
|
|
78
|
+
rng = np.random.default_rng(seed)
|
|
79
|
+
v = gradient_background(nx, nz, 1500.0, 4800.0, dtype=dt)
|
|
80
|
+
v = add_gaussian_anomaly(v, nx * 0.72, nz * 0.35, 30.0, 12.0, 1200.0)
|
|
81
|
+
v = add_gaussian_anomaly(v, nx * 0.25, nz * 0.55, 25.0, 10.0, -700.0)
|
|
82
|
+
v = add_gaussian_anomaly(v, nx * 0.5, nz * 0.75, 40.0, 15.0, 900.0)
|
|
83
|
+
noise = rng.normal(0.0, 1.0, (nx, nz)).astype(dt)
|
|
84
|
+
k = 7
|
|
85
|
+
pad = k // 2
|
|
86
|
+
padded = np.pad(noise, pad, mode="edge")
|
|
87
|
+
smooth = np.zeros_like(noise)
|
|
88
|
+
for di in range(k):
|
|
89
|
+
for dj in range(k):
|
|
90
|
+
smooth += padded[di:di + nx, dj:dj + nz]
|
|
91
|
+
smooth /= (k * k)
|
|
92
|
+
return np.clip(v * (1.0 + 0.05 * smooth / smooth.std()), 500.0, 6000.0).astype(dt)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Primitive building blocks for subsurface velocity models with dtype support."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..utils.dtypes import parse_dtype
|
|
4
|
+
|
|
5
|
+
def gradient_background(nx: int, nz: int, v0: float = 1500.0, v1: float = 4500.0, dtype=np.float32) -> np.ndarray:
|
|
6
|
+
"""Generate a linear velocity gradient with depth.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
nx : int
|
|
11
|
+
Number of cells along x (horizontal direction).
|
|
12
|
+
nz : int
|
|
13
|
+
Number of cells along z (vertical direction, positive downward).
|
|
14
|
+
v0 : float, optional
|
|
15
|
+
Velocity at the surface, in m/s. Default is 1500.0.
|
|
16
|
+
v1 : float, optional
|
|
17
|
+
Velocity at the bottom of the model, in m/s. Default is 4500.0.
|
|
18
|
+
dtype : str or np.dtype, optional
|
|
19
|
+
Data type precision ('f32', 'f64', etc.). Default is np.float32.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
ndarray (nx, nz)
|
|
24
|
+
Velocity field in m/s with the requested dtype.
|
|
25
|
+
"""
|
|
26
|
+
dt = parse_dtype(dtype)
|
|
27
|
+
z = np.linspace(0.0, 1.0, nz, dtype=dt)
|
|
28
|
+
v_col = v0 + (v1 - v0) * z
|
|
29
|
+
return np.broadcast_to(v_col, (nx, nz)).copy()
|
|
30
|
+
|
|
31
|
+
def add_gaussian_anomaly(v: np.ndarray, cx: float, cz: float, sigma_x: float, sigma_z: float, dv: float, vmin: float = 200.0, vmax: float = 8000.0) -> np.ndarray:
|
|
32
|
+
"""Add a Gaussian velocity anomaly to an existing model.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
v : ndarray (nx, nz)
|
|
37
|
+
Velocity field to modify (not modified in place).
|
|
38
|
+
cx, cz : float
|
|
39
|
+
Center position along x and z, in cells.
|
|
40
|
+
sigma_x, sigma_z : float
|
|
41
|
+
Gaussian standard deviation along x and z, in cells.
|
|
42
|
+
dv : float
|
|
43
|
+
Amplitude of the anomaly at the center, in m/s.
|
|
44
|
+
vmin, vmax : float, optional
|
|
45
|
+
Clipping velocity bounds, in m/s.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
ndarray (nx, nz)
|
|
50
|
+
Copy of v with the anomaly added, preserving dtype and clipped.
|
|
51
|
+
"""
|
|
52
|
+
nx, nz = v.shape
|
|
53
|
+
dt = v.dtype
|
|
54
|
+
ii = np.arange(nx, dtype=dt)[:, None]
|
|
55
|
+
jj = np.arange(nz, dtype=dt)[None, :]
|
|
56
|
+
g = dv * np.exp(-((ii - cx) ** 2 / (2 * sigma_x ** 2) + (jj - cz) ** 2 / (2 * sigma_z ** 2)))
|
|
57
|
+
return np.clip(v + g, vmin, vmax).astype(dt)
|
|
58
|
+
|
|
59
|
+
def add_fault_offset(v: np.ndarray, ix_fault: int, throw: int) -> np.ndarray:
|
|
60
|
+
"""Shift layers on either side of a vertical fault.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
v : ndarray (nx, nz)
|
|
65
|
+
Stratified velocity field to shift.
|
|
66
|
+
ix_fault : int
|
|
67
|
+
Column index of the fault.
|
|
68
|
+
throw : int
|
|
69
|
+
Fault throw in cells along z.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
ndarray (nx, nz)
|
|
74
|
+
Shifted field preserving original dtype.
|
|
75
|
+
"""
|
|
76
|
+
out = v.copy()
|
|
77
|
+
left, right = slice(None, ix_fault), slice(ix_fault, None)
|
|
78
|
+
if throw >= 0:
|
|
79
|
+
out[left, throw:] = v[left, :out.shape[1] - throw]
|
|
80
|
+
else:
|
|
81
|
+
out[right, :throw] = v[right, -throw:]
|
|
82
|
+
return out
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Precision management utilities for velocity models."""
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_dtype(dtype: Union[str, np.dtype] = np.float32) -> np.dtype:
|
|
8
|
+
"""Parse and validate the requested floating-point precision dtype.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
dtype : str or np.dtype, optional
|
|
13
|
+
Target precision, e.g., 'f32', 'float32', 'f64', 'float64',
|
|
14
|
+
or np.float32, np.float64. Default is np.float32.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
np.dtype
|
|
19
|
+
Normalized NumPy floating-point dtype.
|
|
20
|
+
|
|
21
|
+
Raises
|
|
22
|
+
------
|
|
23
|
+
ValueError
|
|
24
|
+
If the dtype is not supported or not a floating-point type.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(dtype, str):
|
|
27
|
+
dtype_lower = dtype.lower()
|
|
28
|
+
|
|
29
|
+
if dtype_lower in ("f32", "float32"):
|
|
30
|
+
return np.dtype(np.float32)
|
|
31
|
+
elif dtype_lower in ("f64", "float64"):
|
|
32
|
+
return np.dtype(np.float64)
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Unsupported string dtype: '{dtype}'. Use 'f32' or 'f64'."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
dt = np.dtype(dtype)
|
|
39
|
+
|
|
40
|
+
if dt == np.dtype(np.float32) or dt == np.dtype(np.float64):
|
|
41
|
+
return dt
|
|
42
|
+
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Unsupported dtype: {dt}. Only float32 (f32) and float64 (f64) are allowed."
|
|
45
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Input/Output utilities for exporting velocity models to seismic solvers."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Union, Tuple
|
|
5
|
+
from .validation import validate_model
|
|
6
|
+
|
|
7
|
+
def export_model(
|
|
8
|
+
v: np.ndarray,
|
|
9
|
+
filepath: Union[str, Path],
|
|
10
|
+
format: str = "bin",
|
|
11
|
+
dx: float = 1.0,
|
|
12
|
+
dz: float = 1.0
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Export a velocity model to a file compatible with seismic solvers.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
v : ndarray (nx, nz)
|
|
19
|
+
Velocity model array.
|
|
20
|
+
filepath : str or Path
|
|
21
|
+
Destination file path.
|
|
22
|
+
format : str, optional
|
|
23
|
+
Export format:
|
|
24
|
+
- 'bin' or 'raw': Flat binary file (C-contiguous or Fortran-contiguous depending on solver needs).
|
|
25
|
+
- 'npy': Standard NumPy binary format preserving dtype and shape.
|
|
26
|
+
Default is 'bin'.
|
|
27
|
+
dx, dz : float, optional
|
|
28
|
+
Grid spacing (validated before export).
|
|
29
|
+
"""
|
|
30
|
+
validate_model(v, dx=dx, dz=dz)
|
|
31
|
+
filepath = Path(filepath)
|
|
32
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
fmt = format.lower()
|
|
35
|
+
if fmt in ("bin", "raw"):
|
|
36
|
+
# Enregistrement en binaire brut (souvent attendu en float32/float64 pur par les solveurs C/Fortran)
|
|
37
|
+
v.tofile(filepath)
|
|
38
|
+
elif fmt == "npy":
|
|
39
|
+
np.save(filepath, v)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Unsupported export format: '{format}'. Use 'bin', 'raw', or 'npy'.")
|
|
42
|
+
|
|
43
|
+
def load_model(filepath: Union[str, Path], shape: Tuple[int, int], dtype=np.float32) -> np.ndarray:
|
|
44
|
+
"""Load a velocity model from a raw binary file.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
filepath : str or Path
|
|
49
|
+
Path to the binary file.
|
|
50
|
+
shape : tuple of int
|
|
51
|
+
Expected shape (nx, nz) of the model.
|
|
52
|
+
dtype : str or np.dtype, optional
|
|
53
|
+
Data type of the binary file. Default is np.float32.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
ndarray (nx, nz)
|
|
58
|
+
Loaded velocity model.
|
|
59
|
+
"""
|
|
60
|
+
filepath = Path(filepath)
|
|
61
|
+
if filepath.suffix == ".npy":
|
|
62
|
+
return np.load(filepath)
|
|
63
|
+
|
|
64
|
+
v = np.fromfile(filepath, dtype=dtype)
|
|
65
|
+
expected_size = shape[0] * shape[1]
|
|
66
|
+
if v.size != expected_size:
|
|
67
|
+
raise ValueError(f"File size mismatch: expected {expected_size} elements for shape {shape}, got {v.size}.")
|
|
68
|
+
return v.reshape(shape)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Model validation utilities."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def validate_model(v: np.ndarray, dx: float = 1.0, dz: float = 1.0, v_min: float = 200.0, v_max: float = 8000.0) -> None:
|
|
5
|
+
"""Validate a velocity model array and its parameters.
|
|
6
|
+
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
v : ndarray
|
|
10
|
+
Velocity model array (nx, nz).
|
|
11
|
+
dx, dz : float
|
|
12
|
+
Grid spacing in meters.
|
|
13
|
+
v_min, v_max : float
|
|
14
|
+
Acceptable velocity limits in m/s.
|
|
15
|
+
|
|
16
|
+
Raises
|
|
17
|
+
------
|
|
18
|
+
ValueError
|
|
19
|
+
If the model structure, dtype, or bounds are invalid.
|
|
20
|
+
"""
|
|
21
|
+
if not isinstance(v, np.ndarray):
|
|
22
|
+
raise ValueError("Model must be a NumPy array.")
|
|
23
|
+
if v.ndim != 2:
|
|
24
|
+
raise ValueError(f"Model must be 2D, got {v.ndim}D.")
|
|
25
|
+
if v.dtype not in (np.dtype(np.float32), np.dtype(np.float64)):
|
|
26
|
+
raise ValueError(f"Model dtype must be float32 or float64, got {v.dtype}.")
|
|
27
|
+
if not np.all(np.isfinite(v)):
|
|
28
|
+
raise ValueError("Model contains non-finite values (NaN or Inf).")
|
|
29
|
+
if np.any(v < v_min) or np.any(v > v_max):
|
|
30
|
+
raise ValueError(f"Velocities out of range [{v_min}, {v_max}] m/s.")
|
|
31
|
+
if dx <= 0 or dz <= 0:
|
|
32
|
+
raise ValueError("Grid spacing dx and dz must be positive.")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Visualization utilities compatible with Python 3.8+."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .validation import validate_model
|
|
4
|
+
|
|
5
|
+
def quick_view(
|
|
6
|
+
velocities: np.ndarray,
|
|
7
|
+
dx: float = 1.0,
|
|
8
|
+
dz: float = 1.0,
|
|
9
|
+
title: str = "Velocity model"
|
|
10
|
+
) -> None:
|
|
11
|
+
"""Display a velocity field with depth positive downward.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
velocities : ndarray (nx, nz)
|
|
16
|
+
Velocity field to display, in m/s.
|
|
17
|
+
dx : float, optional
|
|
18
|
+
Grid spacing along x, in meters. Default is 1.0.
|
|
19
|
+
dz : float, optional
|
|
20
|
+
Grid spacing along z, in meters. Default is 1.0.
|
|
21
|
+
title : str, optional
|
|
22
|
+
Figure title. Default is "Velocity model".
|
|
23
|
+
"""
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
validate_model(velocities, dx=dx, dz=dz)
|
|
26
|
+
|
|
27
|
+
nx, nz = velocities.shape
|
|
28
|
+
fig, ax = plt.subplots(figsize=(12, 6))
|
|
29
|
+
im = ax.imshow(
|
|
30
|
+
velocities.T,
|
|
31
|
+
extent=[0, (nx - 1) * dx, (nz - 1) * dz, 0],
|
|
32
|
+
aspect="auto",
|
|
33
|
+
cmap="viridis"
|
|
34
|
+
)
|
|
35
|
+
ax.set_xlabel("x (m)")
|
|
36
|
+
ax.set_ylabel("Depth z (m)")
|
|
37
|
+
ax.set_title(title)
|
|
38
|
+
fig.colorbar(im, ax=ax, label="Velocity (m/s)")
|
|
39
|
+
plt.show()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seiscode-models
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Synthetic subsurface velocity model generators for seismic testing
|
|
5
|
+
Author-email: Damien Pageot <damien.pageot@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PageotD/seiscode-models
|
|
8
|
+
Project-URL: Repository, https://github.com/PageotD/seiscode-models
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.21.0
|
|
22
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
|
+
Requires-Dist: build; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# SeisCode Models
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/seiscode_models/__init__.py
|
|
5
|
+
src/seiscode_models.egg-info/PKG-INFO
|
|
6
|
+
src/seiscode_models.egg-info/SOURCES.txt
|
|
7
|
+
src/seiscode_models.egg-info/dependency_links.txt
|
|
8
|
+
src/seiscode_models.egg-info/requires.txt
|
|
9
|
+
src/seiscode_models.egg-info/top_level.txt
|
|
10
|
+
src/seiscode_models/models/__init__.py
|
|
11
|
+
src/seiscode_models/models/geological.py
|
|
12
|
+
src/seiscode_models/models/primitives.py
|
|
13
|
+
src/seiscode_models/utils/__init__.py
|
|
14
|
+
src/seiscode_models/utils/dtypes.py
|
|
15
|
+
src/seiscode_models/utils/io.py
|
|
16
|
+
src/seiscode_models/utils/validation.py
|
|
17
|
+
src/seiscode_models/utils/visualization.py
|
|
18
|
+
tests/test_dtypes.py
|
|
19
|
+
tests/test_geological.py
|
|
20
|
+
tests/test_io.py
|
|
21
|
+
tests/test_primitives.py
|
|
22
|
+
tests/test_visualization.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
seiscode_models
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Tests for precision dtype utilities."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
from seiscode_models.utils.dtypes import parse_dtype
|
|
5
|
+
|
|
6
|
+
class TestParseDtype:
|
|
7
|
+
def test_default_dtype(self):
|
|
8
|
+
assert parse_dtype() == np.dtype(np.float32)
|
|
9
|
+
|
|
10
|
+
def test_string_inputs(self):
|
|
11
|
+
assert parse_dtype("f32") == np.dtype(np.float32)
|
|
12
|
+
assert parse_dtype("float32") == np.dtype(np.float32)
|
|
13
|
+
assert parse_dtype("f64") == np.dtype(np.float64)
|
|
14
|
+
assert parse_dtype("float64") == np.dtype(np.float64)
|
|
15
|
+
|
|
16
|
+
def test_numpy_dtype_inputs(self):
|
|
17
|
+
assert parse_dtype(np.float32) == np.dtype(np.float32)
|
|
18
|
+
assert parse_dtype(np.float64) == np.dtype(np.float64)
|
|
19
|
+
|
|
20
|
+
def test_invalid_inputs(self):
|
|
21
|
+
with pytest.raises(ValueError, match="Unsupported string dtype"):
|
|
22
|
+
parse_dtype("int32")
|
|
23
|
+
with pytest.raises(ValueError, match="Unsupported dtype"):
|
|
24
|
+
parse_dtype(np.int32)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for geological models and precision behavior."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
import seiscode_models as sm
|
|
5
|
+
|
|
6
|
+
class TestGeologicalModels:
|
|
7
|
+
@pytest.mark.parametrize("model_func", [
|
|
8
|
+
sm.dipping_layers,
|
|
9
|
+
sm.sinusoidal_interface,
|
|
10
|
+
sm.faulted_layers,
|
|
11
|
+
sm.checkerboard,
|
|
12
|
+
sm.salt_dome,
|
|
13
|
+
sm.low_velocity_layer,
|
|
14
|
+
sm.heterogeneous_anomaly_model,
|
|
15
|
+
])
|
|
16
|
+
def test_model_generation_and_dtype(self, model_func):
|
|
17
|
+
# Test f32
|
|
18
|
+
v_f32 = model_func(nx=50, nz=30, dtype="f32")
|
|
19
|
+
assert v_f32.shape == (50, 30)
|
|
20
|
+
assert v_f32.dtype == np.float32
|
|
21
|
+
sm.validate_model(v_f32)
|
|
22
|
+
|
|
23
|
+
# Test f64
|
|
24
|
+
v_f64 = model_func(nx=50, nz=30, dtype="f64")
|
|
25
|
+
assert v_f64.shape == (50, 30)
|
|
26
|
+
assert v_f64.dtype == np.float64
|
|
27
|
+
sm.validate_model(v_f64)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tests for model I/O export and import utilities."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
import seiscode_models as sm
|
|
5
|
+
|
|
6
|
+
class TestModelIO:
|
|
7
|
+
def test_export_import_bin(self, tmp_path):
|
|
8
|
+
v = sm.gradient_background(40, 20, dtype="f32")
|
|
9
|
+
file_path = tmp_path / "model.bin"
|
|
10
|
+
|
|
11
|
+
sm.export_model(v, file_path, format="bin")
|
|
12
|
+
assert file_path.exists()
|
|
13
|
+
|
|
14
|
+
v_loaded = sm.load_model(file_path, shape=(40, 20), dtype=np.float32)
|
|
15
|
+
assert v_loaded.shape == (40, 20)
|
|
16
|
+
assert v_loaded.dtype == np.float32
|
|
17
|
+
assert np.allclose(v, v_loaded)
|
|
18
|
+
|
|
19
|
+
def test_export_import_npy(self, tmp_path):
|
|
20
|
+
v = sm.salt_dome(30, 30, dtype="f64")
|
|
21
|
+
file_path = tmp_path / "model.npy"
|
|
22
|
+
|
|
23
|
+
sm.export_model(v, file_path, format="npy")
|
|
24
|
+
assert file_path.exists()
|
|
25
|
+
|
|
26
|
+
v_loaded = sm.load_model(file_path, shape=(30, 30))
|
|
27
|
+
assert v_loaded.dtype == np.float64
|
|
28
|
+
assert np.allclose(v, v_loaded)
|
|
29
|
+
|
|
30
|
+
def test_invalid_format(self, tmp_path):
|
|
31
|
+
v = sm.gradient_background(10, 10)
|
|
32
|
+
with pytest.raises(ValueError, match="Unsupported export format"):
|
|
33
|
+
sm.export_model(v, tmp_path / "model.xyz", format="xyz")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Tests for primitive functions and dtype preservation."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
import seiscode_models as sm
|
|
5
|
+
|
|
6
|
+
class TestGradientBackground:
|
|
7
|
+
def test_shape_and_dtype(self):
|
|
8
|
+
v_f32 = sm.gradient_background(50, 30, dtype="f32")
|
|
9
|
+
assert v_f32.shape == (50, 30)
|
|
10
|
+
assert v_f32.dtype == np.float32
|
|
11
|
+
|
|
12
|
+
v_f64 = sm.gradient_background(50, 30, dtype="f64")
|
|
13
|
+
assert v_f64.dtype == np.float64
|
|
14
|
+
|
|
15
|
+
def test_gradient_values(self):
|
|
16
|
+
v = sm.gradient_background(10, 10, v0=1000.0, v1=3000.0)
|
|
17
|
+
assert np.allclose(v[:, 0], 1000.0)
|
|
18
|
+
assert np.allclose(v[:, -1], 3000.0)
|
|
19
|
+
|
|
20
|
+
class TestAddGaussianAnomaly:
|
|
21
|
+
def test_dtype_preserved(self):
|
|
22
|
+
v = sm.gradient_background(40, 40, dtype=np.float64)
|
|
23
|
+
v_out = sm.add_gaussian_anomaly(v, 20, 20, 5.0, 5.0, 500.0)
|
|
24
|
+
assert v_out.dtype == np.float64
|
|
25
|
+
assert v_out.shape == (40, 40)
|
|
26
|
+
|
|
27
|
+
class TestValidation:
|
|
28
|
+
def test_valid_model(self):
|
|
29
|
+
v = sm.gradient_background(20, 20, dtype="f32")
|
|
30
|
+
sm.validate_model(v) # Ne doit pas lever d'exception
|
|
31
|
+
|
|
32
|
+
def test_invalid_dtype(self):
|
|
33
|
+
v = sm.gradient_background(20, 20).astype(np.int32)
|
|
34
|
+
with pytest.raises(ValueError, match="Model dtype must be float32 or float64"):
|
|
35
|
+
sm.validate_model(v)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Tests for visualization utilities."""
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import seiscode_models as sm
|
|
6
|
+
|
|
7
|
+
class TestQuickView:
|
|
8
|
+
@patch("matplotlib.pyplot.show")
|
|
9
|
+
def test_quick_view_execution(self, mock_show):
|
|
10
|
+
v = sm.gradient_background(30, 20)
|
|
11
|
+
# Ne doit pas lever d'exception et doit appeler matplotlib.pyplot.show
|
|
12
|
+
sm.quick_view(v, dx=1.5, dz=1.5, title="Test Model")
|
|
13
|
+
mock_show.assert_called_once()
|
|
14
|
+
|
|
15
|
+
def test_quick_view_invalid_model(self):
|
|
16
|
+
v = np.array([1, 2, 3]) # 1D invalide
|
|
17
|
+
with pytest.raises(ValueError, match="Model must be 2D"):
|
|
18
|
+
sm.quick_view(v)
|