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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
@@ -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,9 @@
1
+ numpy>=1.21.0
2
+ matplotlib>=3.5.0
3
+
4
+ [dev]
5
+ pytest>=7.0
6
+ pytest-cov
7
+ ruff
8
+ build
9
+ twine
@@ -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)