pytem 0.1.0__py3-none-any.whl

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.
pytem/__init__.py ADDED
@@ -0,0 +1,219 @@
1
+ """
2
+ pyTEM: 1-D layered-earth TEM modelling.
3
+
4
+ Supports circular and square loop geometries, central and offset receivers,
5
+ DLF and Euler transforms, with NumPy / Numba / CuPy backends.
6
+
7
+ ================================================================================
8
+ Package map (table of contents)
9
+ ================================================================================
10
+
11
+ The package is organised in layers. Lower layers hold pure data and physics
12
+ with no project-internal dependencies; higher layers compose them into the
13
+ public forward, inversion, and plotting API. Arrows below read "imports from".
14
+
15
+ Layer 0 - Data and constants
16
+ transform_weights.py MU0 and the digital-filter coefficient tables:
17
+ Key (2009) 201/101-pt Hankel J0/J1 filters, Key
18
+ 81/101-pt Fourier sine/cosine filters, and the
19
+ Euler weights. Exposes the HANKEL_FILTERS,
20
+ FOURIER_FILTERS, EULER_PARAMS registries. No
21
+ internal dependencies; everything else builds on it.
22
+
23
+ Layer 1 - Core physics and backend detection
24
+ recursion.py te_reflection_coeff / te_reflection_coeff_grad:
25
+ the TE reflection coefficient of the
26
+ layer stack and its log-resistivity gradient (NumPy,
27
+ complex). This is the physical heart of the model.
28
+ -> transform_weights
29
+ backends.py CuPy/CUDA detection (HAS_CUDA) and transfer of the
30
+ filter tables to device memory (GPU_HANKEL,
31
+ GPU_FOURIER).
32
+ -> transform_weights
33
+
34
+ Layer 2 - Compute kernels (one physics, three backends)
35
+ kernels_numba.py Numba JIT forward kernels (circular/square, DLF and
36
+ Euler). Sets HAS_NUMBA.
37
+ kernels_gpu.py CuPy/CUDA forward kernels (mirror of kernels_numba).
38
+ -> transform_weights, backends
39
+ kernels_jacobian.py Adjoint upward recursion: forward+backward pass giving
40
+ d(r_TE)/d(ln rho_j) for all layers at once, in both
41
+ Numba and CuPy variants. Backs the analytical
42
+ Jacobian.
43
+ -> transform_weights, backends
44
+
45
+ Layer 3 - Forward modelling
46
+ forward.py Public fwd_circle_* / fwd_square_* functions, the
47
+ analytical half-space references, and the geometry
48
+ builders + backend dispatchers (CUDA > Numba >
49
+ pure Python). Also exports the filter-precompute
50
+ helpers reused by the inversion module.
51
+ -> transform_weights, backends, recursion,
52
+ kernels_numba, kernels_gpu
53
+
54
+ Layer 4 - Inversion
55
+ inversion.py Analytical (getJ_ana) and finite-difference
56
+ (getJ_fd) Jacobians, regularisation helpers (getR,
57
+ getAlpha[s], getRMS), apparent-resistivity
58
+ conversion, and the Gauss-Newton invert() loop.
59
+ -> forward (+ its filter helpers),
60
+ kernels_jacobian, kernels_numba, backends,
61
+ transform_weights
62
+
63
+ Utilities and add-ons (composable, mostly standalone)
64
+ waveform.py Convolution of the step response with a piecewise-
65
+ linear transmitter waveform (setup_waveform,
66
+ convolve_waveform). -> kernels_numba
67
+ system_filter.py Butterworth and WalkTEM cascade H(omega) transfer
68
+ functions fed in as system_filter=. (standalone)
69
+ ip_models.py Complex-resistivity IP models (Pelton, Cole-Cole,
70
+ double-Pelton, MPA) and tem_forward_ip.
71
+ -> transform_weights, recursion
72
+ euler.py Standalone Euler inverse-Laplace transform, used to
73
+ verify the production Euler path. (standalone)
74
+ plotter.py Matplotlib helpers: plot_sounding, plot_model,
75
+ plot_inversion. (standalone)
76
+ data_io.py Field-data import: read_tem_xyz / read_kenbec_xyz /
77
+ read_xyz parse TEM Data Manager and TEMImage-Beta
78
+ `.xyz` exports into TEMData / KenbecTEMData, with
79
+ shared dbdt/snr/to_pytem and line/UTM/distance
80
+ helpers. (standalone)
81
+ survey.py Survey: quick-look sounding, transect and station-
82
+ map plots built on data_io containers. LM/HM are
83
+ always plotted separately (different waveforms).
84
+ -> data_io
85
+
86
+ __init__.py Re-exports the public API listed in __all__.
87
+
88
+ --------------------------------------------------------------------------------
89
+ Dependency flow
90
+ --------------------------------------------------------------------------------
91
+
92
+ transform_weights
93
+ |-> backends
94
+ |-> recursion
95
+ |-> kernels_numba / kernels_gpu / kernels_jacobian
96
+ |
97
+ v
98
+ forward ----> inversion
99
+ ^ ^
100
+ | |
101
+ (waveform, system_filter, ip_models feed in here as options)
102
+
103
+ --------------------------------------------------------------------------------
104
+ Typical call chains
105
+ --------------------------------------------------------------------------------
106
+
107
+ Forward:
108
+ fwd_circle_central() (forward.py)
109
+ _resolve_filters() -> transform_weights tables
110
+ _filter_weights() -> optional system_filter samples
111
+ _build_circular_geometry()-> per-wavenumber weights
112
+ _run_circular() -> CUDA | Numba | pure-Python kernel
113
+ te_reflection_coeff() -> upward recursion (recursion.py / kernels)
114
+ _apply_signal_scaling() -> current and step-off/on/impulse sign
115
+
116
+ Inversion:
117
+ invert() (inversion.py)
118
+ loop:
119
+ forward call (one of fwd_*) (forward.py)
120
+ getJ_ana() -> adjoint kernels (kernels_jacobian.py)
121
+ or getJ_fd() -> repeated forward calls
122
+ _alpha_search() / _gn_solve() / _backtrack() Gauss-Newton step
123
+ optional: convolve_waveform(), system_filter=H, IP rho(omega)
124
+ """
125
+
126
+ __version__ = "0.1.0"
127
+
128
+ from .transform_weights import MU0, HANKEL_FILTERS, FOURIER_FILTERS, EULER_PARAMS
129
+ from .backends import HAS_CUDA
130
+ from .kernels_numba import HAS_NUMBA
131
+ from .recursion import te_reflection_coeff, te_reflection_coeff_grad
132
+
133
+ from .forward import (
134
+ fwd_circle_central,
135
+ fwd_circle_offset,
136
+ fwd_square_central,
137
+ fwd_square_offset,
138
+ fwd_analytical_central,
139
+ fwd_analytical_offset,
140
+ )
141
+
142
+ from .waveform import convolve_waveform, setup_waveform, setup_waveform_matrix, setup_shared_gate_matrices
143
+ from .system_filter import butterworth_filter, cascade_filter
144
+ from .euler import euler_invert
145
+ from .inversion import (getJ_ana, getJ_fd, getR, dbdt_to_apprho, getRMS,
146
+ getAlpha, getAlphas, invert, invert_joint, invert_stations,
147
+ compute_doi)
148
+ from .plotter import (plot_sounding, plot_model, plot_inversion,
149
+ plot_survey_models, plot_survey_responses)
150
+ from .benchmarks import benchmark_step_grids
151
+ from .data_io import (TEMData, KenbecTEMData, TunoeTEMData, read_tem_xyz, read_kenbec_xyz,
152
+ read_xyz, read_usf)
153
+ from .survey import Survey
154
+
155
+ from .ip_models import (
156
+ pelton_res_rho,
157
+ cole_cole_rho,
158
+ double_pelton_rho,
159
+ get_m_taur_MPA,
160
+ mpa_rho,
161
+ tem_forward_ip,
162
+ )
163
+
164
+ __all__ = [
165
+ # Constants & flags
166
+ 'MU0', 'HAS_CUDA', 'HAS_NUMBA',
167
+ 'HANKEL_FILTERS', 'FOURIER_FILTERS', 'EULER_PARAMS',
168
+ # Core
169
+ 'te_reflection_coeff',
170
+ 'te_reflection_coeff_grad',
171
+ # Forward models
172
+ 'fwd_circle_central',
173
+ 'fwd_circle_offset',
174
+ 'fwd_square_central',
175
+ 'fwd_square_offset',
176
+ # Analytical
177
+ 'fwd_analytical_central',
178
+ 'fwd_analytical_offset',
179
+ # Waveform & system filter
180
+ 'convolve_waveform',
181
+ 'setup_waveform',
182
+ 'setup_waveform_matrix',
183
+ 'setup_shared_gate_matrices',
184
+ 'butterworth_filter',
185
+ 'cascade_filter',
186
+ # IP models
187
+ 'pelton_res_rho',
188
+ 'cole_cole_rho',
189
+ 'double_pelton_rho',
190
+ 'get_m_taur_MPA',
191
+ 'mpa_rho',
192
+ 'tem_forward_ip',
193
+ # Euler (verification)
194
+ 'euler_invert',
195
+ # Inversion
196
+ 'getJ_ana',
197
+ 'getJ_fd',
198
+ 'getR',
199
+ 'dbdt_to_apprho',
200
+ 'getRMS',
201
+ 'getAlpha',
202
+ 'getAlphas',
203
+ 'invert',
204
+ 'invert_joint',
205
+ 'compute_doi',
206
+ # Plotting
207
+ 'plot_sounding',
208
+ 'plot_model',
209
+ 'plot_inversion',
210
+ # Field-data import & survey plotting
211
+ 'TEMData',
212
+ 'KenbecTEMData',
213
+ 'TunoeTEMData',
214
+ 'read_tem_xyz',
215
+ 'read_kenbec_xyz',
216
+ 'read_xyz',
217
+ 'read_usf',
218
+ 'Survey',
219
+ ]
pytem/backends.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ backends.py - CuPy/CUDA detection and GPU filter array transfer.
3
+
4
+ Provides:
5
+ - HAS_CUDA flag
6
+ - GPU_HANKEL, GPU_FOURIER registries (CuPy arrays on device)
7
+ """
8
+
9
+ import os
10
+ import glob
11
+ import sys
12
+
13
+ import numpy as np
14
+
15
+ from .transform_weights import (
16
+ _HANKEL_BASE_201, _HANKEL_J0_201, _HANKEL_J1_201,
17
+ _HANKEL_BASE_101, _HANKEL_J0_101, _HANKEL_J1_101,
18
+ _FOURIER_BASE_81, _FOURIER_SIN_81, _FOURIER_COS_81,
19
+ _FOURIER_BASE_101, _FOURIER_SIN_101, _FOURIER_COS_101,
20
+ )
21
+
22
+
23
+ def _register_cuda_dll_dirs():
24
+ """Make CuPy find the CUDA runtime shipped as ``nvidia-*-cu12`` pip wheels.
25
+
26
+ On Windows the wheels install their DLLs under ``site-packages/nvidia/*/bin``.
27
+ CuPy's bundled loader does not search there, and nvrtc loads its builtins
28
+ DLL via the process PATH, so we add those directories to both PATH and the
29
+ DLL search path before CuPy is imported. No-op on non-Windows or when the
30
+ wheels are absent.
31
+ """
32
+ if os.name != "nt":
33
+ return
34
+ bins = []
35
+ for site_dir in {os.path.dirname(os.path.dirname(__file__)), *sys.path}:
36
+ base = os.path.join(site_dir, "nvidia")
37
+ if os.path.isdir(base):
38
+ bins.extend(glob.glob(os.path.join(base, "*", "bin")))
39
+ bins = sorted(set(p for p in bins if os.path.isdir(p)))
40
+ if not bins:
41
+ return
42
+ os.environ["PATH"] = os.pathsep.join(bins) + os.pathsep + os.environ.get("PATH", "")
43
+ for p in bins:
44
+ try:
45
+ os.add_dll_directory(p)
46
+ except (OSError, AttributeError):
47
+ pass
48
+
49
+
50
+ _register_cuda_dll_dirs()
51
+
52
+ try:
53
+ import cupy as cp
54
+ HAS_CUDA = cp.cuda.is_available()
55
+ except ImportError:
56
+ HAS_CUDA = False
57
+
58
+ GPU_HANKEL = {}
59
+ GPU_FOURIER = {}
60
+
61
+ if HAS_CUDA:
62
+ import cupy as cp
63
+
64
+ _GPU_DEVICE = cp.cuda.Device(0)
65
+ _GPU_DEVICE.use()
66
+
67
+ # 201-pt Hankel
68
+ _d_HANKEL_BASE_201 = cp.asarray(_HANKEL_BASE_201)
69
+ _d_HANKEL_J0_201 = cp.asarray(_HANKEL_J0_201, dtype=cp.complex128)
70
+ _d_HANKEL_J1_201 = cp.asarray(_HANKEL_J1_201, dtype=cp.complex128)
71
+
72
+ # 101-pt Hankel
73
+ _d_HANKEL_BASE_101 = cp.asarray(_HANKEL_BASE_101)
74
+ _d_HANKEL_J0_101 = cp.asarray(_HANKEL_J0_101, dtype=cp.complex128)
75
+ _d_HANKEL_J1_101 = cp.asarray(_HANKEL_J1_101, dtype=cp.complex128)
76
+
77
+ # 81-pt Fourier
78
+ _d_FOURIER_BASE_81 = cp.asarray(_FOURIER_BASE_81)
79
+ _d_FOURIER_SIN_81 = cp.asarray(_FOURIER_SIN_81)
80
+ _d_FOURIER_COS_81 = cp.asarray(_FOURIER_COS_81)
81
+
82
+ # 101-pt Fourier
83
+ _d_FOURIER_BASE_101 = cp.asarray(_FOURIER_BASE_101)
84
+ _d_FOURIER_SIN_101 = cp.asarray(_FOURIER_SIN_101)
85
+ _d_FOURIER_COS_101 = cp.asarray(_FOURIER_COS_101)
86
+
87
+ GPU_HANKEL = {
88
+ 'key_201': (_d_HANKEL_BASE_201, _d_HANKEL_J0_201, _d_HANKEL_J1_201),
89
+ 'key_101': (_d_HANKEL_BASE_101, _d_HANKEL_J0_101, _d_HANKEL_J1_101),
90
+ }
91
+ GPU_FOURIER = {
92
+ 'key_81': (_d_FOURIER_BASE_81, _d_FOURIER_SIN_81, _d_FOURIER_COS_81),
93
+ 'key_101': (_d_FOURIER_BASE_101, _d_FOURIER_SIN_101, _d_FOURIER_COS_101),
94
+ }
pytem/benchmarks.py ADDED
@@ -0,0 +1,108 @@
1
+ """Numerical benchmarks for pyTEM forward-model workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from .forward import fwd_circle_offset, fwd_square_offset
11
+ from .waveform import setup_shared_gate_matrices
12
+
13
+
14
+ def benchmark_step_grids(
15
+ systems,
16
+ thicknesses,
17
+ models,
18
+ geometry,
19
+ tx_size,
20
+ rx_x=0.0,
21
+ rx_y=0.0,
22
+ grid_points=(20, 70, 120, 170, 220, 270, 320, 370, 400),
23
+ reference_points=1200,
24
+ repeats=3,
25
+ tolerance=1.0,
26
+ system_filter=None,
27
+ n_quad=5,
28
+ transform='dlf',
29
+ use_numba=True,
30
+ use_cuda=False,
31
+ ):
32
+ """Benchmark shared step-time grids against a high-resolution reference."""
33
+ if geometry not in ('circle_offset', 'square_offset'):
34
+ raise ValueError("geometry must be 'circle_offset' or 'square_offset'.")
35
+ if repeats < 1 or reference_points < 1:
36
+ raise ValueError('repeats and reference_points must be positive.')
37
+
38
+ thicknesses = np.asarray(thicknesses, dtype=float)
39
+ models = {str(name): np.asarray(model, dtype=float)
40
+ for name, model in models.items()}
41
+ points = sorted(set(int(value) for value in grid_points))
42
+ if not points or min(points) < 1:
43
+ raise ValueError('grid_points must contain positive integers.')
44
+
45
+ def predict(resistivities, step_times, gate_matrices):
46
+ options = dict(current=1.0, signal=-1, use_numba=use_numba,
47
+ use_cuda=use_cuda, transform=transform,
48
+ system_filter=system_filter)
49
+ if geometry == 'square_offset':
50
+ response = -fwd_square_offset(
51
+ thicknesses, resistivities, tx_size, rx_x, rx_y, step_times,
52
+ n_quad=n_quad, **options)
53
+ else:
54
+ response = -fwd_circle_offset(
55
+ thicknesses, resistivities, tx_size, rx_x, step_times, **options)
56
+ return {moment: gate_matrices[moment] @ response
57
+ for moment in systems}
58
+
59
+ reference_times, reference_matrices = setup_shared_gate_matrices(
60
+ systems, n_step=reference_points)
61
+ check_times, check_matrices = setup_shared_gate_matrices(
62
+ systems, n_step=2 * reference_points)
63
+ references = {name: predict(model, reference_times, reference_matrices)
64
+ for name, model in models.items()}
65
+ reference_errors = []
66
+ for name in models:
67
+ check = predict(models[name], check_times, check_matrices)
68
+ for moment in systems:
69
+ scale = np.maximum(np.abs(check[moment]),
70
+ max(np.max(np.abs(check[moment])) * 1e-10, 1e-300))
71
+ reference_errors.append(
72
+ np.max(100 * np.abs(references[name][moment] - check[moment]) / scale))
73
+
74
+ rows = []
75
+ for points_count in points:
76
+ started = time.perf_counter()
77
+ step_times, gate_matrices = setup_shared_gate_matrices(
78
+ systems, n_step=points_count)
79
+ setup_seconds = time.perf_counter() - started
80
+ for name, model in models.items():
81
+ predict(model, step_times, gate_matrices)
82
+ durations = []
83
+ prediction = None
84
+ for _ in range(repeats):
85
+ started = time.perf_counter()
86
+ prediction = predict(model, step_times, gate_matrices)
87
+ durations.append(time.perf_counter() - started)
88
+ for moment in systems:
89
+ reference = references[name][moment]
90
+ scale = np.maximum(np.abs(reference),
91
+ max(np.max(np.abs(reference)) * 1e-10, 1e-300))
92
+ error = 100 * np.abs(prediction[moment] - reference) / scale
93
+ rows.append({
94
+ 'Points': points_count,
95
+ 'Model': name,
96
+ 'Moment': moment,
97
+ 'RMS error [%]': np.sqrt(np.mean(error ** 2)),
98
+ 'Max error [%]': error.max(),
99
+ 'Prediction [ms]': 1000 * np.median(durations),
100
+ 'Setup [s]': setup_seconds,
101
+ 'Below tolerance': bool(error.max() <= tolerance),
102
+ })
103
+
104
+ result = pd.DataFrame(rows)
105
+ result.attrs['reference_max_error_percent'] = max(reference_errors, default=0.0)
106
+ result.attrs['reference_points'] = reference_points
107
+ result.attrs['tolerance'] = tolerance
108
+ return result