seisvtk 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.
- seisvtk-0.1.0/LICENSE +21 -0
- seisvtk-0.1.0/PKG-INFO +90 -0
- seisvtk-0.1.0/README.md +56 -0
- seisvtk-0.1.0/pyproject.toml +55 -0
- seisvtk-0.1.0/setup.cfg +4 -0
- seisvtk-0.1.0/src/seisvtk/__init__.py +0 -0
- seisvtk-0.1.0/src/seisvtk/io/__init__.py +1 -0
- seisvtk-0.1.0/src/seisvtk/io/browse.py +128 -0
- seisvtk-0.1.0/src/seisvtk/io/convert_segy.py +67 -0
- seisvtk-0.1.0/src/seisvtk/io/deviation.py +131 -0
- seisvtk-0.1.0/src/seisvtk/io/horizon.py +157 -0
- seisvtk-0.1.0/src/seisvtk/io/ingest.py +188 -0
- seisvtk-0.1.0/src/seisvtk/io/loaders.py +141 -0
- seisvtk-0.1.0/src/seisvtk/io/logging_config.py +9 -0
- seisvtk-0.1.0/src/seisvtk/io/segy2zarr.py +170 -0
- seisvtk-0.1.0/src/seisvtk/io/segy_scan.py +121 -0
- seisvtk-0.1.0/src/seisvtk/io/survey.py +193 -0
- seisvtk-0.1.0/src/seisvtk/io/tables.py +131 -0
- seisvtk-0.1.0/src/seisvtk/io/volume.py +144 -0
- seisvtk-0.1.0/src/seisvtk/io/well.py +275 -0
- seisvtk-0.1.0/src/seisvtk/viz/actors.py +30 -0
- seisvtk-0.1.0/src/seisvtk/viz/app.py +790 -0
- seisvtk-0.1.0/src/seisvtk/viz/bridge.py +26 -0
- seisvtk-0.1.0/src/seisvtk/viz/colormaps.py +83 -0
- seisvtk-0.1.0/src/seisvtk/viz/horizon_actor.py +81 -0
- seisvtk-0.1.0/src/seisvtk/viz/interactor.py +62 -0
- seisvtk-0.1.0/src/seisvtk/viz/render.py +64 -0
- seisvtk-0.1.0/src/seisvtk/viz/scene.py +18 -0
- seisvtk-0.1.0/src/seisvtk/viz/service.py +128 -0
- seisvtk-0.1.0/src/seisvtk/viz/slice_scene.py +390 -0
- seisvtk-0.1.0/src/seisvtk/viz/well_actor.py +70 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/PKG-INFO +90 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/SOURCES.txt +47 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/dependency_links.txt +1 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/entry_points.txt +2 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/requires.txt +18 -0
- seisvtk-0.1.0/src/seisvtk.egg-info/top_level.txt +1 -0
- seisvtk-0.1.0/tests/test_browse.py +80 -0
- seisvtk-0.1.0/tests/test_deviation.py +80 -0
- seisvtk-0.1.0/tests/test_horizon.py +68 -0
- seisvtk-0.1.0/tests/test_ingest.py +67 -0
- seisvtk-0.1.0/tests/test_loaders.py +117 -0
- seisvtk-0.1.0/tests/test_segy.py +159 -0
- seisvtk-0.1.0/tests/test_service.py +74 -0
- seisvtk-0.1.0/tests/test_slice_scene.py +44 -0
- seisvtk-0.1.0/tests/test_survey.py +96 -0
- seisvtk-0.1.0/tests/test_tables.py +53 -0
- seisvtk-0.1.0/tests/test_volume.py +53 -0
- seisvtk-0.1.0/tests/test_well.py +155 -0
seisvtk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aashrit
|
|
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.
|
seisvtk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seisvtk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Server-side 3D seismic cross-section viewer (trame + VTK) with a VTK-free I/O core
|
|
5
|
+
Author-email: Aashrit <aashrith12345@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aashrith48/seisvtk
|
|
8
|
+
Project-URL: Repository, https://github.com/aashrith48/seisvtk
|
|
9
|
+
Keywords: seismic,visualization,vtk,trame,segy,zarr,geoscience
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: pandas
|
|
20
|
+
Requires-Dist: segyio
|
|
21
|
+
Requires-Dist: lasio
|
|
22
|
+
Requires-Dist: zarr>=3
|
|
23
|
+
Provides-Extra: viz
|
|
24
|
+
Requires-Dist: vtk; extra == "viz"
|
|
25
|
+
Requires-Dist: trame; extra == "viz"
|
|
26
|
+
Requires-Dist: trame-vtk; extra == "viz"
|
|
27
|
+
Requires-Dist: trame-vuetify; extra == "viz"
|
|
28
|
+
Provides-Extra: s3
|
|
29
|
+
Requires-Dist: fsspec; extra == "s3"
|
|
30
|
+
Requires-Dist: s3fs; extra == "s3"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# seisvtk
|
|
36
|
+
|
|
37
|
+
Server-side 3D seismic cross-section viewer (trame + VTK) with a lightweight,
|
|
38
|
+
VTK-free I/O core. The viewer renders inline / crossline / Z slices of a cube and
|
|
39
|
+
streams the rendered frames to the browser, so the client stays thin and the cube
|
|
40
|
+
is read **out-of-core** — only the chunks a visible slice touches are loaded, from
|
|
41
|
+
local disk or straight from object storage (`s3://`). Wells (including deviated)
|
|
42
|
+
and horizons drape on the same grid.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install seisvtk # I/O core: SEG-Y -> Zarr, wells, horizons, geometry
|
|
48
|
+
pip install "seisvtk[viz]" # + the trame/VTK viewer
|
|
49
|
+
pip install "seisvtk[s3]" # + read cubes directly from s3://
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The core has no VTK dependency, so a data producer (e.g. a pipeline step that
|
|
53
|
+
only converts SEG-Y and writes overlays) installs just `seisvtk`. For a **headless
|
|
54
|
+
/ CPU** render host, install the `vtk-osmesa` wheel in place of `vtk` (see
|
|
55
|
+
`docker/Dockerfile`).
|
|
56
|
+
|
|
57
|
+
## Use
|
|
58
|
+
|
|
59
|
+
Serve a single cube:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m seisvtk.viz.app path/to/cube.zarr --mode property --port 8910
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Serve a scene manifest (a cube plus well/horizon overlays — `s3://` cubes work):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
seisvtk-viewer scene.json --port 8910
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Convert a SEG-Y to a chunked Zarr from Python:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from seisvtk.io.loaders import convert_segy
|
|
75
|
+
convert_segy("survey.segy", "survey.zarr") # geometry auto-detected from headers
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What's in the box
|
|
79
|
+
|
|
80
|
+
- **I/O (`seisvtk.io`, no VTK):** streaming SEG-Y→Zarr conversion with survey-geometry
|
|
81
|
+
recovery; an out-of-core `Volume` reader (local or `s3://`); readers for wells
|
|
82
|
+
(LAS/CSV, deviation surveys, checkshots), horizons, and the survey affine
|
|
83
|
+
(`GridTransform`).
|
|
84
|
+
- **Viewer (`seisvtk.viz`):** a trame app streaming inline/crossline/Z slice planes,
|
|
85
|
+
well tubes coloured by a log, and horizon surfaces; amplitude and property
|
|
86
|
+
colormaps; vertical exaggeration; runtime cube swap.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[MIT](LICENSE) © 2026 Aashrit
|
seisvtk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# seisvtk
|
|
2
|
+
|
|
3
|
+
Server-side 3D seismic cross-section viewer (trame + VTK) with a lightweight,
|
|
4
|
+
VTK-free I/O core. The viewer renders inline / crossline / Z slices of a cube and
|
|
5
|
+
streams the rendered frames to the browser, so the client stays thin and the cube
|
|
6
|
+
is read **out-of-core** — only the chunks a visible slice touches are loaded, from
|
|
7
|
+
local disk or straight from object storage (`s3://`). Wells (including deviated)
|
|
8
|
+
and horizons drape on the same grid.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install seisvtk # I/O core: SEG-Y -> Zarr, wells, horizons, geometry
|
|
14
|
+
pip install "seisvtk[viz]" # + the trame/VTK viewer
|
|
15
|
+
pip install "seisvtk[s3]" # + read cubes directly from s3://
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The core has no VTK dependency, so a data producer (e.g. a pipeline step that
|
|
19
|
+
only converts SEG-Y and writes overlays) installs just `seisvtk`. For a **headless
|
|
20
|
+
/ CPU** render host, install the `vtk-osmesa` wheel in place of `vtk` (see
|
|
21
|
+
`docker/Dockerfile`).
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
Serve a single cube:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m seisvtk.viz.app path/to/cube.zarr --mode property --port 8910
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Serve a scene manifest (a cube plus well/horizon overlays — `s3://` cubes work):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
seisvtk-viewer scene.json --port 8910
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Convert a SEG-Y to a chunked Zarr from Python:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from seisvtk.io.loaders import convert_segy
|
|
41
|
+
convert_segy("survey.segy", "survey.zarr") # geometry auto-detected from headers
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What's in the box
|
|
45
|
+
|
|
46
|
+
- **I/O (`seisvtk.io`, no VTK):** streaming SEG-Y→Zarr conversion with survey-geometry
|
|
47
|
+
recovery; an out-of-core `Volume` reader (local or `s3://`); readers for wells
|
|
48
|
+
(LAS/CSV, deviation surveys, checkshots), horizons, and the survey affine
|
|
49
|
+
(`GridTransform`).
|
|
50
|
+
- **Viewer (`seisvtk.viz`):** a trame app streaming inline/crossline/Z slice planes,
|
|
51
|
+
well tubes coloured by a log, and horizon surfaces; amplitude and property
|
|
52
|
+
colormaps; vertical exaggeration; runtime cube swap.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
[MIT](LICENSE) © 2026 Aashrit
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "seisvtk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Server-side 3D seismic cross-section viewer (trame + VTK) with a VTK-free I/O core"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Aashrit", email = "aashrith12345@gmail.com" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
keywords = ["seismic", "visualization", "vtk", "trame", "segy", "zarr", "geoscience"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Core = the VTK-free I/O layer (seisvtk.io): read/convert SEG-Y, Zarr, wells,
|
|
23
|
+
# horizons, survey geometry. This is all a producer (e.g. a one-shot pipeline
|
|
24
|
+
# node) needs, and it stays light — no VTK, no trame.
|
|
25
|
+
dependencies = [
|
|
26
|
+
"numpy",
|
|
27
|
+
"pandas",
|
|
28
|
+
"segyio",
|
|
29
|
+
"lasio",
|
|
30
|
+
"zarr>=3",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/aashrith48/seisvtk"
|
|
35
|
+
Repository = "https://github.com/aashrith48/seisvtk"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
seisvtk-viewer = "seisvtk.viz.service:main"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
# Rendering/serving: the trame+VTK viewer (seisvtk.viz). Add this only where you
|
|
42
|
+
# actually render. Use the vtk-osmesa wheel instead of vtk for headless/CPU hosts.
|
|
43
|
+
viz = ["vtk", "trame", "trame-vtk", "trame-vuetify"]
|
|
44
|
+
# Reading a cube straight from object storage (s3://…) via an fsspec backend.
|
|
45
|
+
s3 = ["fsspec", "s3fs"]
|
|
46
|
+
test = ["pytest"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
pythonpath = ["src"]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
|
seisvtk-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Server-side directory listing for the viewer's file-picker popup.
|
|
2
|
+
|
|
3
|
+
The trame viewer renders where the data lives, so files are chosen by browsing
|
|
4
|
+
the *server's* filesystem (its own disk when run locally), not by uploading from
|
|
5
|
+
the browser — which only yields bytes, never a path, and cannot handle multi-GB
|
|
6
|
+
cubes. :func:`browse` turns a directory into a sorted, typed entry list the popup
|
|
7
|
+
renders; it is pure and unit-tested, with the trame dialog kept in the app layer.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import string
|
|
13
|
+
|
|
14
|
+
# A Zarr "file" is really a directory; treat ``*.zarr`` as a selectable store
|
|
15
|
+
# rather than a folder to descend into.
|
|
16
|
+
STORE_SUFFIXES = (".zarr",)
|
|
17
|
+
|
|
18
|
+
_ICON = {"up": "mdi-arrow-up-left", "drive": "mdi-harddisk",
|
|
19
|
+
"dir": "mdi-folder", "store": "mdi-database", "file": "mdi-file-outline"}
|
|
20
|
+
# Vuetify theme colours, so the list reads at a glance (folders warm, data cool).
|
|
21
|
+
_COLOR = {"up": "grey", "drive": "blue-grey", "dir": "amber-darken-1",
|
|
22
|
+
"store": "blue", "file": "blue-grey-lighten-1"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _entry(name, path, kind):
|
|
26
|
+
return {"name": name, "path": path, "type": kind,
|
|
27
|
+
"icon": _ICON[kind], "color": _COLOR[kind]}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _crumbs(path: str) -> list[dict]:
|
|
31
|
+
"""Breadcrumb trail for *path*: ``[{label, path}]`` from "This PC" down."""
|
|
32
|
+
crumbs = [{"label": "This PC", "path": ""}]
|
|
33
|
+
if not path:
|
|
34
|
+
return crumbs
|
|
35
|
+
path = os.path.abspath(path)
|
|
36
|
+
drive, tail = os.path.splitdrive(path)
|
|
37
|
+
if drive:
|
|
38
|
+
acc = drive + os.sep
|
|
39
|
+
crumbs.append({"label": drive, "path": acc})
|
|
40
|
+
else:
|
|
41
|
+
acc = os.sep
|
|
42
|
+
crumbs.append({"label": "/", "path": acc})
|
|
43
|
+
for part in [p for p in tail.split(os.sep) if p]:
|
|
44
|
+
acc = os.path.join(acc, part)
|
|
45
|
+
crumbs.append({"label": part, "path": acc})
|
|
46
|
+
return crumbs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_drives() -> list[str]:
|
|
50
|
+
"""Mounted Windows drive roots (``['C:\\\\', 'D:\\\\', ...]``); ``[]`` off Windows.
|
|
51
|
+
|
|
52
|
+
Uses the ``GetLogicalDrives`` bitmask, which reports assigned drive letters
|
|
53
|
+
instantly without spinning up removable media — probing ``A:\\`` or an empty
|
|
54
|
+
optical drive with ``os.path.exists`` can block for tens of seconds.
|
|
55
|
+
"""
|
|
56
|
+
if os.name != "nt":
|
|
57
|
+
return []
|
|
58
|
+
import ctypes
|
|
59
|
+
|
|
60
|
+
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
|
|
61
|
+
return [f"{string.ascii_uppercase[i]}:\\" for i in range(26) if bitmask & (1 << i)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_drive_root(path: str) -> bool:
|
|
65
|
+
drive, tail = os.path.splitdrive(os.path.abspath(path))
|
|
66
|
+
return bool(drive) and tail in ("", os.sep, "/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parent(path: str) -> str | None:
|
|
70
|
+
"""The directory above *path*; ``""`` at a drive root (→ drive list), None at top."""
|
|
71
|
+
if _is_drive_root(path):
|
|
72
|
+
return "" # one level up from C:\ is "pick a drive"
|
|
73
|
+
up = os.path.dirname(os.path.abspath(path).rstrip("\\/"))
|
|
74
|
+
return up or None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _drives_view() -> dict:
|
|
78
|
+
"""The top level: a row per drive (Windows) or root (POSIX)."""
|
|
79
|
+
roots = list_drives() or ["/"]
|
|
80
|
+
entries = [_entry(r, r, "drive") for r in roots]
|
|
81
|
+
return {"cwd": "", "parent": None, "entries": entries, "crumbs": _crumbs("")}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def browse(path: str | None, *, exts=None) -> dict:
|
|
85
|
+
"""List *path* for the picker: ``{cwd, parent, entries}``.
|
|
86
|
+
|
|
87
|
+
Each entry is ``{name, path, type, icon}`` with ``type`` one of ``up`` (the
|
|
88
|
+
parent row), ``dir`` (navigable folder), ``store`` (a selectable ``*.zarr``),
|
|
89
|
+
or ``file`` (selectable). Files are kept only if their extension is in ``exts``
|
|
90
|
+
(lowercase, dotted, e.g. ``{'.segy', '.sgy'}``); ``None`` keeps all. Folders
|
|
91
|
+
are always listed so the user can navigate. Hidden/system entries and
|
|
92
|
+
unreadable children are skipped rather than raising. An empty/None *path*
|
|
93
|
+
yields the drive list.
|
|
94
|
+
"""
|
|
95
|
+
if not path:
|
|
96
|
+
return _drives_view()
|
|
97
|
+
path = os.path.abspath(path)
|
|
98
|
+
entries: list[dict] = []
|
|
99
|
+
parent = _parent(path)
|
|
100
|
+
if parent is not None:
|
|
101
|
+
entries.append(_entry("..", parent, "up"))
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
children = list(os.scandir(path))
|
|
105
|
+
except OSError: # permission denied, gone, etc.
|
|
106
|
+
children = []
|
|
107
|
+
|
|
108
|
+
dirs, stores, files = [], [], []
|
|
109
|
+
exts = {e.lower() for e in exts} if exts else None
|
|
110
|
+
for e in children:
|
|
111
|
+
name = e.name
|
|
112
|
+
if name.startswith(".") or name.startswith("$"):
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
is_dir = e.is_dir()
|
|
116
|
+
except OSError:
|
|
117
|
+
continue
|
|
118
|
+
if is_dir:
|
|
119
|
+
if name.lower().endswith(STORE_SUFFIXES):
|
|
120
|
+
stores.append(_entry(name, e.path, "store"))
|
|
121
|
+
else:
|
|
122
|
+
dirs.append(_entry(name, e.path, "dir"))
|
|
123
|
+
elif exts is None or os.path.splitext(name)[1].lower() in exts:
|
|
124
|
+
files.append(_entry(name, e.path, "file"))
|
|
125
|
+
|
|
126
|
+
key = lambda d: d["name"].lower()
|
|
127
|
+
entries += sorted(dirs, key=key) + sorted(stores, key=key) + sorted(files, key=key)
|
|
128
|
+
return {"cwd": path, "parent": parent, "entries": entries, "crumbs": _crumbs(path)}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from seisvtk.io.segy_scan import detect_geometry, open_auto
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
#Logging
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import segyio
|
|
10
|
+
import zarr
|
|
11
|
+
from seisvtk.io.logging_config import setup_logging
|
|
12
|
+
from seisvtk.io.segy2zarr import convert_segy_to_zarr
|
|
13
|
+
|
|
14
|
+
#Find relative path for output files
|
|
15
|
+
REPO_PATH = Path(__file__).resolve().parents[2]
|
|
16
|
+
output_folder=REPO_PATH / "Testing" / "Output"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
#Logger setup
|
|
24
|
+
logger = logging.getLogger(__spec__.name if __spec__ else __name__)
|
|
25
|
+
setup_logging()
|
|
26
|
+
|
|
27
|
+
#Argument Parser (Setup sys.argv to receive script arguments)
|
|
28
|
+
parser = argparse.ArgumentParser(description="Tool to automatically convert a SEGY to optimized format")
|
|
29
|
+
parser.add_argument('filepath', help="Full file path to .segy file")
|
|
30
|
+
parser.add_argument('-o','--out',help="Full output path for converted file")
|
|
31
|
+
|
|
32
|
+
parser.add_argument('--auto', action='store_true', help='Autmatically scan SEGY for header information and geometry')
|
|
33
|
+
|
|
34
|
+
parser.add_argument('--iline', type=int, help='Byte location for INLINE values in traces')
|
|
35
|
+
parser.add_argument('--xline', type=int, help='Byte location for CROSSLINE values in traces')
|
|
36
|
+
|
|
37
|
+
#Parse sys.argv
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
#Normalizing output path
|
|
41
|
+
out = os.path.join(output_folder,args.out+'.zarr')
|
|
42
|
+
|
|
43
|
+
#Automatically scan SEGY for geometry
|
|
44
|
+
# Resolve iline/xline bytes from either --auto or the manual flags.
|
|
45
|
+
if args.auto:
|
|
46
|
+
geom = detect_geometry(args.filepath)
|
|
47
|
+
logger.info(f'Scanned SEGY Geometry: {geom}')
|
|
48
|
+
iline, xline = geom["iline_byte"], geom["xline_byte"]
|
|
49
|
+
elif args.iline is not None and args.xline is not None:
|
|
50
|
+
iline, xline = args.iline, args.xline
|
|
51
|
+
logger.info(f'Using ILINE:{iline} XLINE:{xline} byte locations')
|
|
52
|
+
else:
|
|
53
|
+
parser.error('--iline and --xline are required when --auto is not set.')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
convert_segy_to_zarr(args.filepath, iline, xline, out, logger=logger)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
main()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Well deviation surveys: turn a trajectory file into XY/TVD along the hole.
|
|
2
|
+
|
|
3
|
+
A well is rarely a clean vertical line; to drape it on a cube we need the
|
|
4
|
+
horizontal position at every logged depth. Surveys come in two shapes:
|
|
5
|
+
|
|
6
|
+
* **Directional** — ``(MD, inclination, azimuth)``. The classic minimum-curvature
|
|
7
|
+
method (:func:`minimum_curvature`) integrates these angle stations into
|
|
8
|
+
east/north/TVD offsets from the wellhead. This is the industry-standard
|
|
9
|
+
survey-to-position calculation.
|
|
10
|
+
* **XYZ** — ``(MD, X, Y, Z)`` positions already computed by the acquisition
|
|
11
|
+
software; read straight through.
|
|
12
|
+
|
|
13
|
+
Both land in a :class:`Deviation`: measured depth plus east/north offsets from
|
|
14
|
+
the first station and true vertical depth, which :mod:`seisvtk.io.well` combines
|
|
15
|
+
with a wellhead ``(X, Y)`` and a time-depth relation to place the log.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from seisvtk.io.tables import pick_column, read_table, to_float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Deviation:
|
|
28
|
+
"""A trajectory sampled on measured depth.
|
|
29
|
+
|
|
30
|
+
``east``/``north`` are horizontal offsets from the first station (metres in
|
|
31
|
+
the survey's own units); ``tvd`` is true vertical depth below it. Add the
|
|
32
|
+
wellhead ``(X, Y)`` to ``east``/``north`` for absolute position.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
md: np.ndarray
|
|
36
|
+
east: np.ndarray
|
|
37
|
+
north: np.ndarray
|
|
38
|
+
tvd: np.ndarray
|
|
39
|
+
|
|
40
|
+
def at(self, md) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
41
|
+
"""``(east, north, tvd)`` offsets at measured depths ``md``.
|
|
42
|
+
|
|
43
|
+
Linear within the surveyed interval and *linearly extrapolated* along the
|
|
44
|
+
end tangents outside it (not clamped): a log sample above the first
|
|
45
|
+
station continues up the opening trajectory — vertical for a survey that
|
|
46
|
+
starts vertical — and one below the last station continues down the final
|
|
47
|
+
tangent, rather than freezing horizontally at the last station.
|
|
48
|
+
"""
|
|
49
|
+
md = np.asarray(md, float)
|
|
50
|
+
return (self._interp(md, self.east),
|
|
51
|
+
self._interp(md, self.north),
|
|
52
|
+
self._interp(md, self.tvd))
|
|
53
|
+
|
|
54
|
+
def _interp(self, md, vals) -> np.ndarray:
|
|
55
|
+
t = np.interp(md, self.md, vals)
|
|
56
|
+
if self.md.size >= 2: # extend the end tangents
|
|
57
|
+
s0 = (vals[1] - vals[0]) / (self.md[1] - self.md[0])
|
|
58
|
+
s1 = (vals[-1] - vals[-2]) / (self.md[-1] - self.md[-2])
|
|
59
|
+
t = np.where(md < self.md[0], vals[0] + s0 * (md - self.md[0]), t)
|
|
60
|
+
t = np.where(md > self.md[-1], vals[-1] + s1 * (md - self.md[-1]), t)
|
|
61
|
+
return t
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def minimum_curvature(md, inc_deg, azi_deg) -> Deviation:
|
|
65
|
+
"""Minimum-curvature trajectory from angle stations.
|
|
66
|
+
|
|
67
|
+
``inc_deg`` is inclination from vertical (0 = straight down) and ``azi_deg``
|
|
68
|
+
is azimuth clockwise from north, both in degrees. Returns cumulative
|
|
69
|
+
east/north/TVD offsets from the first station (which sits at the origin).
|
|
70
|
+
"""
|
|
71
|
+
md = np.asarray(md, float)
|
|
72
|
+
inc = np.radians(np.asarray(inc_deg, float))
|
|
73
|
+
azi = np.radians(np.asarray(azi_deg, float))
|
|
74
|
+
n = md.size
|
|
75
|
+
east = np.zeros(n)
|
|
76
|
+
north = np.zeros(n)
|
|
77
|
+
tvd = np.zeros(n)
|
|
78
|
+
|
|
79
|
+
for i in range(1, n):
|
|
80
|
+
dmd = md[i] - md[i - 1]
|
|
81
|
+
i1, i2 = inc[i - 1], inc[i]
|
|
82
|
+
a1, a2 = azi[i - 1], azi[i]
|
|
83
|
+
# dogleg angle between the two station directions
|
|
84
|
+
cos_dl = np.cos(i2 - i1) - np.sin(i1) * np.sin(i2) * (1.0 - np.cos(a2 - a1))
|
|
85
|
+
dl = np.arccos(np.clip(cos_dl, -1.0, 1.0))
|
|
86
|
+
rf = 1.0 if dl < 1e-9 else (2.0 / dl) * np.tan(dl / 2.0) # ratio factor
|
|
87
|
+
half = dmd / 2.0 * rf
|
|
88
|
+
north[i] = north[i - 1] + half * (np.sin(i1) * np.cos(a1) + np.sin(i2) * np.cos(a2))
|
|
89
|
+
east[i] = east[i - 1] + half * (np.sin(i1) * np.sin(a1) + np.sin(i2) * np.sin(a2))
|
|
90
|
+
tvd[i] = tvd[i - 1] + half * (np.cos(i1) + np.cos(i2))
|
|
91
|
+
|
|
92
|
+
return Deviation(md, east, north, tvd)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def read_deviation(path: str, *, kind: str = "directional",
|
|
96
|
+
cols: tuple[int, ...] | None = None,
|
|
97
|
+
delimiter: str | None = None) -> Deviation:
|
|
98
|
+
"""Read a deviation survey file into a :class:`Deviation`.
|
|
99
|
+
|
|
100
|
+
``kind="directional"`` expects ``(MD, inc, azi)`` columns (default columns
|
|
101
|
+
1,2,3); ``kind="xyz"`` expects ``(MD, X, Y, Z)`` (default 1,2,3,4) and the
|
|
102
|
+
XY are stored as offsets from the first row so the model stays wellhead-
|
|
103
|
+
relative. ``cols`` overrides the 1-based column numbers; ``delimiter`` is
|
|
104
|
+
sniffed when ``None``. Rows with a non-finite value are dropped and the
|
|
105
|
+
survey is sorted by measured depth, since minimum-curvature accumulates down
|
|
106
|
+
the hole and :meth:`Deviation.at` needs ``md`` ascending.
|
|
107
|
+
"""
|
|
108
|
+
df = read_table(path, delimiter=delimiter)
|
|
109
|
+
|
|
110
|
+
if kind == "directional":
|
|
111
|
+
c = cols or (1, 2, 3)
|
|
112
|
+
md, (inc, azi) = _clean_survey(df, c[0], (c[1], c[2]))
|
|
113
|
+
return minimum_curvature(md, inc, azi)
|
|
114
|
+
|
|
115
|
+
if kind == "xyz":
|
|
116
|
+
c = cols or (1, 2, 3, 4)
|
|
117
|
+
md, (x, y, z) = _clean_survey(df, c[0], (c[1], c[2], c[3]))
|
|
118
|
+
return Deviation(md, x - x[0], y - y[0], z - z[0])
|
|
119
|
+
|
|
120
|
+
raise ValueError(f"kind must be 'directional' or 'xyz', got {kind!r}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _clean_survey(df, md_col, value_cols):
|
|
124
|
+
"""Pull MD + value columns, drop non-finite rows, sort ascending by MD."""
|
|
125
|
+
md = to_float(df[pick_column(df, md_col)])
|
|
126
|
+
values = [to_float(df[pick_column(df, c)]) for c in value_cols]
|
|
127
|
+
good = np.isfinite(md)
|
|
128
|
+
for v in values:
|
|
129
|
+
good &= np.isfinite(v)
|
|
130
|
+
order = np.argsort(md[good], kind="stable")
|
|
131
|
+
return md[good][order], tuple(v[good][order] for v in values)
|