pytem 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.
- pytem-0.1.0/PKG-INFO +316 -0
- pytem-0.1.0/README.md +285 -0
- pytem-0.1.0/pyproject.toml +34 -0
- pytem-0.1.0/pytem/__init__.py +219 -0
- pytem-0.1.0/pytem/backends.py +94 -0
- pytem-0.1.0/pytem/benchmarks.py +108 -0
- pytem-0.1.0/pytem/data_io.py +941 -0
- pytem-0.1.0/pytem/euler.py +54 -0
- pytem-0.1.0/pytem/forward.py +866 -0
- pytem-0.1.0/pytem/gerda_io.py +805 -0
- pytem-0.1.0/pytem/inversion.py +1724 -0
- pytem-0.1.0/pytem/ip_models.py +158 -0
- pytem-0.1.0/pytem/kernels_gpu.py +251 -0
- pytem-0.1.0/pytem/kernels_jacobian.py +764 -0
- pytem-0.1.0/pytem/kernels_numba.py +315 -0
- pytem-0.1.0/pytem/plotter.py +279 -0
- pytem-0.1.0/pytem/recursion.py +131 -0
- pytem-0.1.0/pytem/survey.py +287 -0
- pytem-0.1.0/pytem/system_filter.py +56 -0
- pytem-0.1.0/pytem/transform_weights.py +469 -0
- pytem-0.1.0/pytem/waveform.py +612 -0
- pytem-0.1.0/pytem.egg-info/PKG-INFO +316 -0
- pytem-0.1.0/pytem.egg-info/SOURCES.txt +26 -0
- pytem-0.1.0/pytem.egg-info/dependency_links.txt +1 -0
- pytem-0.1.0/pytem.egg-info/requires.txt +21 -0
- pytem-0.1.0/pytem.egg-info/top_level.txt +1 -0
- pytem-0.1.0/setup.cfg +4 -0
- pytem-0.1.0/setup.py +3 -0
pytem-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 1-D layered-earth time-domain electromagnetic (TEM) forward modelling and inversion
|
|
5
|
+
Author-email: Paul McLachlan <pamcl@dtu.dk>
|
|
6
|
+
Project-URL: Homepage, https://github.com/pmc93/PyTEM
|
|
7
|
+
Keywords: geophysics,TEM,TDEM,electromagnetics,inversion,groundwater
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: numba
|
|
19
|
+
Provides-Extra: gpu
|
|
20
|
+
Requires-Dist: cupy-cuda12x; extra == "gpu"
|
|
21
|
+
Provides-Extra: maps
|
|
22
|
+
Requires-Dist: pyproj; extra == "maps"
|
|
23
|
+
Requires-Dist: utm; extra == "maps"
|
|
24
|
+
Requires-Dist: contextily; extra == "maps"
|
|
25
|
+
Requires-Dist: matplotlib-scalebar; extra == "maps"
|
|
26
|
+
Provides-Extra: gerda
|
|
27
|
+
Requires-Dist: fdb; extra == "gerda"
|
|
28
|
+
Requires-Dist: utm; extra == "gerda"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: pytem[gerda,gpu,maps]; extra == "all"
|
|
31
|
+
|
|
32
|
+
# PyTEM
|
|
33
|
+
|
|
34
|
+
1-D layered-earth Time-Domain Electromagnetic (TEM) modelling and inversion in Python.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install pytem
|
|
38
|
+
pip install "pytem[gpu]" # CUDA (CuPy); also [maps], [gerda], [all]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Overview
|
|
44
|
+
|
|
45
|
+
pyTEM computes the vertical dB/dt step-off response of a 1-D horizontally layered resistivity model beneath a grounded loop source. It supports four transmitter/receiver geometries, two transform methods, three compute backends, a full regularised inversion, induced polarisation models, instrument system filters, and transmitter waveform convolution.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Package structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pytem/
|
|
53
|
+
├── transform_weights.py # DLF coefficients and Euler weights (static data)
|
|
54
|
+
├── recursion.py # TE reflection coefficient — NumPy reference
|
|
55
|
+
├── backends.py # CUDA/CuPy detection; GPU transform weight arrays
|
|
56
|
+
├── kernels_numba.py # Numba JIT forward kernels
|
|
57
|
+
├── kernels_gpu.py # CuPy GPU forward kernels
|
|
58
|
+
├── kernels_jacobian.py # Numba + GPU analytical Jacobian kernels
|
|
59
|
+
├── euler.py # Standalone Euler ILT reference
|
|
60
|
+
├── forward.py # Public forward model API
|
|
61
|
+
├── system_filter.py # Instrument frequency-domain filter
|
|
62
|
+
├── waveform.py # Waveform convolution
|
|
63
|
+
├── inversion.py # Jacobian + regularised Gauss-Newton inversion
|
|
64
|
+
├── ip_models.py # Induced polarisation complex resistivity models
|
|
65
|
+
├── plotter.py # Plotting utilities
|
|
66
|
+
└── __init__.py # Public API
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Module descriptions
|
|
72
|
+
|
|
73
|
+
### `transform_weights.py`
|
|
74
|
+
Pure data module — no computation. Stores the pre-optimised Digital Linear Filter (DLF) coefficients from Key (2009, 2012) and the Euler–Stehfest acceleration weights:
|
|
75
|
+
|
|
76
|
+
| Registry key | Points | Use |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `key_101` | 101 | Hankel J0/J1 (fast, default) |
|
|
79
|
+
| `key_201` | 201 | Hankel J0/J1 (more accurate) |
|
|
80
|
+
| `key_81` | 81 | Fourier sine/cosine (fast, default) |
|
|
81
|
+
| `key_101` | 101 | Fourier sine/cosine (more accurate) |
|
|
82
|
+
|
|
83
|
+
Euler–Stehfest weights are stored at orders 8, 11, 15, and 19 (Abate & Whitt 1995). Also stores `MU0 = 4π × 10⁻⁷`.
|
|
84
|
+
|
|
85
|
+
Exports: `MU0`, `HANKEL_FILTERS`, `FOURIER_FILTERS`, `EULER_PARAMS`
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### `recursion.py`
|
|
90
|
+
Reference NumPy implementation of the Wait (1954) upward TE-mode recursion and its adjoint gradient. Used as a ground truth for testing other backends.
|
|
91
|
+
|
|
92
|
+
Exports: `te_reflection_coeff`, `te_reflection_coeff_grad`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### `backends.py`
|
|
97
|
+
Detects whether CuPy (CUDA) is available at import time. If CUDA is present, pre-transfers all transform weight arrays to device memory so forward calls pay no Host-to-Device transfer cost.
|
|
98
|
+
|
|
99
|
+
Exports: `HAS_CUDA`
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### `kernels_numba.py`
|
|
104
|
+
Numba `@njit`-compiled scalar kernels for the TEM forward model. One kernel per geometry × transform combination:
|
|
105
|
+
|
|
106
|
+
- `_tem_circular_jit` — circular loop, Fourier DLF
|
|
107
|
+
- `_tem_circular_euler_jit` — circular loop, Euler ILT
|
|
108
|
+
- `_tem_square_jit` — square loop, Fourier DLF
|
|
109
|
+
- `_tem_square_euler_jit` — square loop, Euler ILT
|
|
110
|
+
|
|
111
|
+
All kernels accept a `filter_weights` array `(n_t, n_eval) complex128` for the system filter. The inner loop runs in prange over gate times for CPU parallelism.
|
|
112
|
+
|
|
113
|
+
Exports: `HAS_NUMBA`, and the JIT kernels when Numba is available.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `kernels_gpu.py`
|
|
118
|
+
CuPy (CUDA) equivalents of the Numba kernels. The full `(n_t, n_f, K)` tensor is batched in a single CuPy operation to saturate GPU occupancy. Guarded by `HAS_CUDA`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `kernels_jacobian.py`
|
|
123
|
+
Numba JIT and CuPy implementations of the **analytical Jacobian** kernels. Each uses the adjoint Wait recursion: one forward pass stores intermediate values, one backward pass accumulates `∂r_TE / ∂(ln ρ_j)` for all layers simultaneously, at the cost of a single forward call.
|
|
124
|
+
|
|
125
|
+
Same set of geometry × transform combinations as the forward kernels. All accept `filter_weights` so the system filter is automatically included in the gradient (since `H(ω)` is independent of resistivity).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `euler.py`
|
|
130
|
+
Standalone reference implementation of the Euler–Maclaurin inverse Laplace transform (`euler_invert`). Used for verification only; the production path uses precomputed weights from `transform_weights.py`.
|
|
131
|
+
|
|
132
|
+
Exports: `euler_invert`
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `forward.py`
|
|
137
|
+
The main public forward model. Dispatches to CUDA > Numba > NumPy automatically.
|
|
138
|
+
|
|
139
|
+
| Function | Geometry |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `fwd_circle_central` | Circular loop, Rx at centre |
|
|
142
|
+
| `fwd_circle_offset` | Circular loop, Rx at radial offset |
|
|
143
|
+
| `fwd_square_central` | Square loop, Rx at centre |
|
|
144
|
+
| `fwd_square_offset` | Square loop, Rx at (x, y) offset |
|
|
145
|
+
| `fwd_analytical_central` | Magnetic dipole analytical approximation, central |
|
|
146
|
+
| `fwd_analytical_offset` | Magnetic dipole analytical approximation, offset |
|
|
147
|
+
|
|
148
|
+
All functions share the same keyword arguments:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
fwd_circle_central(
|
|
152
|
+
thicknesses, # (N-1,) layer thicknesses [m]
|
|
153
|
+
resistivities, # (N,) resistivities [Ohm.m]
|
|
154
|
+
tx_radius, # float equivalent circle radius [m]
|
|
155
|
+
times, # (n_t,) gate times [s]
|
|
156
|
+
use_numba=True,
|
|
157
|
+
use_cuda=True,
|
|
158
|
+
system_filter=None, # callable H(omega) -> complex, or None
|
|
159
|
+
transform='dlf', # 'dlf' or 'euler'
|
|
160
|
+
hankel_filter='key_101',
|
|
161
|
+
fourier_filter='key_81',
|
|
162
|
+
euler_order=11,
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Also exports `_precompute_filter_dlf` and `_precompute_filter_euler` (used internally by `inversion.py`).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `system_filter.py`
|
|
171
|
+
Instrument frequency-domain transfer functions. A filter `H(omega)` is a callable that takes an array of angular frequencies and returns complex weights. It is applied inside the forward transform before taking the imaginary/real part.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
H = butterworth_filter(f_low=None, f_high=3e4, order=1)
|
|
175
|
+
H = cascade_filter(filtfreq=3e4) # two cascaded 1st-order Butterworth LP
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Exports: `butterworth_filter`, `cascade_filter`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### `waveform.py`
|
|
183
|
+
Convolves a pre-computed step-off response with a piecewise-linear transmitter waveform using Gauss-Legendre quadrature per waveform segment:
|
|
184
|
+
|
|
185
|
+
$$G(t) = -\int \frac{dI}{d\tau}\, S(t - \tau)\, d\tau$$
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
result = convolve_waveform(
|
|
189
|
+
step_times, # dense time grid [s]
|
|
190
|
+
step_response, # step-off response on that grid
|
|
191
|
+
waveform_times, # waveform break points [s]
|
|
192
|
+
waveform_currents,# current at each break point [A]
|
|
193
|
+
gate_times, # output gate centre times [s]
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Exports: `convolve_waveform`
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### `inversion.py`
|
|
202
|
+
All inversion machinery, built around a regularised Gauss-Newton loop that minimises:
|
|
203
|
+
|
|
204
|
+
$$\phi(\mathbf{m}) = \|\mathbf{W}(\ln \mathbf{d}_\text{obs} - \ln \mathbf{d}_\text{pred}(\mathbf{m}))\|^2 + \alpha\, \mathbf{m}^T \mathbf{R}\, \mathbf{m}$$
|
|
205
|
+
|
|
206
|
+
**Public utilities:**
|
|
207
|
+
|
|
208
|
+
| Function | Purpose |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `getJ_ana` | Analytical Jacobian via adjoint Wait recursion |
|
|
211
|
+
| `getJ_fd` | Finite-difference Jacobian (N+1 forward calls) |
|
|
212
|
+
| `getR` | First-order roughness matrix with damping |
|
|
213
|
+
| `getRMS` | Noise-normalised RMS misfit |
|
|
214
|
+
| `getAlpha` | Single log-spaced regularisation parameter |
|
|
215
|
+
| `getAlphas` | Depth-weighted regularisation vector |
|
|
216
|
+
| `dbdt_to_apprho` | Convert dB/dt to apparent resistivity |
|
|
217
|
+
| `invert` | Full regularised inversion loop |
|
|
218
|
+
|
|
219
|
+
**Private helpers:** `_gn_solve`, `_backtrack`, `_alpha_search`
|
|
220
|
+
|
|
221
|
+
**`invert()` key options:**
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
result = pytem.invert(
|
|
225
|
+
obs_data, thicknesses, log_resistivities, tx_radius, times,
|
|
226
|
+
analytical_j=True, # use getJ_ana (faster) or getJ_fd
|
|
227
|
+
system_filter=H, # frequency-domain instrument filter
|
|
228
|
+
waveform_times=wf_t, # transmitter waveform
|
|
229
|
+
waveform_currents=wf_I,
|
|
230
|
+
noise_std=0.02,
|
|
231
|
+
maxit=20,
|
|
232
|
+
use_numba=True,
|
|
233
|
+
)
|
|
234
|
+
# result keys: 'resistivities', 'thicknesses', 'rms_history',
|
|
235
|
+
# 'model_history', 'sensitivity', 'obs_data', 'times'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
When both `analytical_j=True` and a waveform are provided, the waveform Jacobian is formed analytically using the chain rule: `∂G_i/∂(ln ρ_j) = conv(∂F/∂(ln ρ_j), w)_i`, avoiding N+1 full waveform convolution calls.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### `ip_models.py`
|
|
243
|
+
Complex resistivity models for induced polarisation (IP). Each returns `rho(omega)` (complex) at a given angular frequency, suitable for passing into `te_reflection_coeff`.
|
|
244
|
+
|
|
245
|
+
| Function | Model |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `pelton_res_rho` | Pelton et al. (1978) |
|
|
248
|
+
| `cole_cole_rho` | Cole & Cole (1941) |
|
|
249
|
+
| `double_pelton_rho` | Double Pelton (two relaxation terms) |
|
|
250
|
+
| `mpa_rho` | Maximum Phase Angle (Fiandaca et al. 2018) |
|
|
251
|
+
| `get_m_taur_MPA` | Iterative MPA → Cole-Cole conversion |
|
|
252
|
+
| `tem_forward_ip` | Full TEM forward with per-layer IP |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `plotter.py`
|
|
257
|
+
Standalone plotting functions (no class required):
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
ax = plot_sounding(times, obs, mod, labels=['Observed', 'Modelled'])
|
|
261
|
+
ax = plot_model(thicknesses, resistivities)
|
|
262
|
+
fig, axs = plot_inversion(times, obs_data, mod_data, thicknesses,
|
|
263
|
+
best_rho, rms_history)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
All functions accept an optional `ax` argument to plot into an existing axes.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Data flow
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
resistivities + thicknesses
|
|
274
|
+
│
|
|
275
|
+
▼
|
|
276
|
+
recursion.py ← Wait recursion (TE reflection coeff)
|
|
277
|
+
│
|
|
278
|
+
▼
|
|
279
|
+
forward.py ← Hankel + Fourier/Euler DLF transforms
|
|
280
|
+
│ system_filter applied in frequency domain
|
|
281
|
+
│ dispatch: CUDA > Numba > NumPy
|
|
282
|
+
▼
|
|
283
|
+
step-off dB/dt
|
|
284
|
+
│
|
|
285
|
+
├──── waveform.py ─── convolve with transmitter waveform
|
|
286
|
+
│
|
|
287
|
+
▼
|
|
288
|
+
inversion.py
|
|
289
|
+
├── getJ_ana / getJ_fd ← Jacobian
|
|
290
|
+
├── _alpha_search ← regularisation ladder
|
|
291
|
+
├── _gn_solve ← normal equations
|
|
292
|
+
└── _backtrack ← bounds enforcement
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Compute backends
|
|
298
|
+
|
|
299
|
+
| Backend | When active | Strength |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| NumPy | always | Portable, easy to debug |
|
|
302
|
+
| Numba JIT | `use_numba=True` and Numba installed | Fast scalar loops, CPU SIMD, prange parallelism |
|
|
303
|
+
| CuPy (CUDA) | `use_cuda=True` and CUDA available | Fully batched GPU; fastest for large problems |
|
|
304
|
+
|
|
305
|
+
Priority: CUDA > Numba > NumPy. Set `use_numba=False, use_cuda=False` to force NumPy.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## References
|
|
310
|
+
|
|
311
|
+
- Wait, J. R. (1954). Mutual coupling of loops lying on the ground. *Geophysics*, 19, 290–296.
|
|
312
|
+
- Key, K. (2009). 1D inversion of multicomponent, multifrequency marine CSEM data. *Geophysics*, 74(2), F9–F20.
|
|
313
|
+
- Key, K. (2012). Is the fast Hankel transform faster than quadrature? *Geophysics*, 77(3), F21–F30.
|
|
314
|
+
- Abate, J., & Whitt, W. (1995). Numerical inversion of Laplace transforms of probability distributions. *ORSA Journal on Computing*, 7(1), 36–43.
|
|
315
|
+
- Pelton, W. H., et al. (1978). Mineral discrimination and removal of inductive coupling with multifrequency IP. *Geophysics*, 43(3), 588–609.
|
|
316
|
+
- Fiandaca, G., et al. (2018). Re-parameterisations of the Cole–Cole model. *Geophysical Journal International*, 214(2), 1160–1173.
|
pytem-0.1.0/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# PyTEM
|
|
2
|
+
|
|
3
|
+
1-D layered-earth Time-Domain Electromagnetic (TEM) modelling and inversion in Python.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install pytem
|
|
7
|
+
pip install "pytem[gpu]" # CUDA (CuPy); also [maps], [gerda], [all]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
pyTEM computes the vertical dB/dt step-off response of a 1-D horizontally layered resistivity model beneath a grounded loop source. It supports four transmitter/receiver geometries, two transform methods, three compute backends, a full regularised inversion, induced polarisation models, instrument system filters, and transmitter waveform convolution.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Package structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pytem/
|
|
22
|
+
├── transform_weights.py # DLF coefficients and Euler weights (static data)
|
|
23
|
+
├── recursion.py # TE reflection coefficient — NumPy reference
|
|
24
|
+
├── backends.py # CUDA/CuPy detection; GPU transform weight arrays
|
|
25
|
+
├── kernels_numba.py # Numba JIT forward kernels
|
|
26
|
+
├── kernels_gpu.py # CuPy GPU forward kernels
|
|
27
|
+
├── kernels_jacobian.py # Numba + GPU analytical Jacobian kernels
|
|
28
|
+
├── euler.py # Standalone Euler ILT reference
|
|
29
|
+
├── forward.py # Public forward model API
|
|
30
|
+
├── system_filter.py # Instrument frequency-domain filter
|
|
31
|
+
├── waveform.py # Waveform convolution
|
|
32
|
+
├── inversion.py # Jacobian + regularised Gauss-Newton inversion
|
|
33
|
+
├── ip_models.py # Induced polarisation complex resistivity models
|
|
34
|
+
├── plotter.py # Plotting utilities
|
|
35
|
+
└── __init__.py # Public API
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Module descriptions
|
|
41
|
+
|
|
42
|
+
### `transform_weights.py`
|
|
43
|
+
Pure data module — no computation. Stores the pre-optimised Digital Linear Filter (DLF) coefficients from Key (2009, 2012) and the Euler–Stehfest acceleration weights:
|
|
44
|
+
|
|
45
|
+
| Registry key | Points | Use |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `key_101` | 101 | Hankel J0/J1 (fast, default) |
|
|
48
|
+
| `key_201` | 201 | Hankel J0/J1 (more accurate) |
|
|
49
|
+
| `key_81` | 81 | Fourier sine/cosine (fast, default) |
|
|
50
|
+
| `key_101` | 101 | Fourier sine/cosine (more accurate) |
|
|
51
|
+
|
|
52
|
+
Euler–Stehfest weights are stored at orders 8, 11, 15, and 19 (Abate & Whitt 1995). Also stores `MU0 = 4π × 10⁻⁷`.
|
|
53
|
+
|
|
54
|
+
Exports: `MU0`, `HANKEL_FILTERS`, `FOURIER_FILTERS`, `EULER_PARAMS`
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### `recursion.py`
|
|
59
|
+
Reference NumPy implementation of the Wait (1954) upward TE-mode recursion and its adjoint gradient. Used as a ground truth for testing other backends.
|
|
60
|
+
|
|
61
|
+
Exports: `te_reflection_coeff`, `te_reflection_coeff_grad`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### `backends.py`
|
|
66
|
+
Detects whether CuPy (CUDA) is available at import time. If CUDA is present, pre-transfers all transform weight arrays to device memory so forward calls pay no Host-to-Device transfer cost.
|
|
67
|
+
|
|
68
|
+
Exports: `HAS_CUDA`
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `kernels_numba.py`
|
|
73
|
+
Numba `@njit`-compiled scalar kernels for the TEM forward model. One kernel per geometry × transform combination:
|
|
74
|
+
|
|
75
|
+
- `_tem_circular_jit` — circular loop, Fourier DLF
|
|
76
|
+
- `_tem_circular_euler_jit` — circular loop, Euler ILT
|
|
77
|
+
- `_tem_square_jit` — square loop, Fourier DLF
|
|
78
|
+
- `_tem_square_euler_jit` — square loop, Euler ILT
|
|
79
|
+
|
|
80
|
+
All kernels accept a `filter_weights` array `(n_t, n_eval) complex128` for the system filter. The inner loop runs in prange over gate times for CPU parallelism.
|
|
81
|
+
|
|
82
|
+
Exports: `HAS_NUMBA`, and the JIT kernels when Numba is available.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `kernels_gpu.py`
|
|
87
|
+
CuPy (CUDA) equivalents of the Numba kernels. The full `(n_t, n_f, K)` tensor is batched in a single CuPy operation to saturate GPU occupancy. Guarded by `HAS_CUDA`.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### `kernels_jacobian.py`
|
|
92
|
+
Numba JIT and CuPy implementations of the **analytical Jacobian** kernels. Each uses the adjoint Wait recursion: one forward pass stores intermediate values, one backward pass accumulates `∂r_TE / ∂(ln ρ_j)` for all layers simultaneously, at the cost of a single forward call.
|
|
93
|
+
|
|
94
|
+
Same set of geometry × transform combinations as the forward kernels. All accept `filter_weights` so the system filter is automatically included in the gradient (since `H(ω)` is independent of resistivity).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### `euler.py`
|
|
99
|
+
Standalone reference implementation of the Euler–Maclaurin inverse Laplace transform (`euler_invert`). Used for verification only; the production path uses precomputed weights from `transform_weights.py`.
|
|
100
|
+
|
|
101
|
+
Exports: `euler_invert`
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### `forward.py`
|
|
106
|
+
The main public forward model. Dispatches to CUDA > Numba > NumPy automatically.
|
|
107
|
+
|
|
108
|
+
| Function | Geometry |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `fwd_circle_central` | Circular loop, Rx at centre |
|
|
111
|
+
| `fwd_circle_offset` | Circular loop, Rx at radial offset |
|
|
112
|
+
| `fwd_square_central` | Square loop, Rx at centre |
|
|
113
|
+
| `fwd_square_offset` | Square loop, Rx at (x, y) offset |
|
|
114
|
+
| `fwd_analytical_central` | Magnetic dipole analytical approximation, central |
|
|
115
|
+
| `fwd_analytical_offset` | Magnetic dipole analytical approximation, offset |
|
|
116
|
+
|
|
117
|
+
All functions share the same keyword arguments:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
fwd_circle_central(
|
|
121
|
+
thicknesses, # (N-1,) layer thicknesses [m]
|
|
122
|
+
resistivities, # (N,) resistivities [Ohm.m]
|
|
123
|
+
tx_radius, # float equivalent circle radius [m]
|
|
124
|
+
times, # (n_t,) gate times [s]
|
|
125
|
+
use_numba=True,
|
|
126
|
+
use_cuda=True,
|
|
127
|
+
system_filter=None, # callable H(omega) -> complex, or None
|
|
128
|
+
transform='dlf', # 'dlf' or 'euler'
|
|
129
|
+
hankel_filter='key_101',
|
|
130
|
+
fourier_filter='key_81',
|
|
131
|
+
euler_order=11,
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Also exports `_precompute_filter_dlf` and `_precompute_filter_euler` (used internally by `inversion.py`).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `system_filter.py`
|
|
140
|
+
Instrument frequency-domain transfer functions. A filter `H(omega)` is a callable that takes an array of angular frequencies and returns complex weights. It is applied inside the forward transform before taking the imaginary/real part.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
H = butterworth_filter(f_low=None, f_high=3e4, order=1)
|
|
144
|
+
H = cascade_filter(filtfreq=3e4) # two cascaded 1st-order Butterworth LP
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Exports: `butterworth_filter`, `cascade_filter`
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `waveform.py`
|
|
152
|
+
Convolves a pre-computed step-off response with a piecewise-linear transmitter waveform using Gauss-Legendre quadrature per waveform segment:
|
|
153
|
+
|
|
154
|
+
$$G(t) = -\int \frac{dI}{d\tau}\, S(t - \tau)\, d\tau$$
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
result = convolve_waveform(
|
|
158
|
+
step_times, # dense time grid [s]
|
|
159
|
+
step_response, # step-off response on that grid
|
|
160
|
+
waveform_times, # waveform break points [s]
|
|
161
|
+
waveform_currents,# current at each break point [A]
|
|
162
|
+
gate_times, # output gate centre times [s]
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Exports: `convolve_waveform`
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `inversion.py`
|
|
171
|
+
All inversion machinery, built around a regularised Gauss-Newton loop that minimises:
|
|
172
|
+
|
|
173
|
+
$$\phi(\mathbf{m}) = \|\mathbf{W}(\ln \mathbf{d}_\text{obs} - \ln \mathbf{d}_\text{pred}(\mathbf{m}))\|^2 + \alpha\, \mathbf{m}^T \mathbf{R}\, \mathbf{m}$$
|
|
174
|
+
|
|
175
|
+
**Public utilities:**
|
|
176
|
+
|
|
177
|
+
| Function | Purpose |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `getJ_ana` | Analytical Jacobian via adjoint Wait recursion |
|
|
180
|
+
| `getJ_fd` | Finite-difference Jacobian (N+1 forward calls) |
|
|
181
|
+
| `getR` | First-order roughness matrix with damping |
|
|
182
|
+
| `getRMS` | Noise-normalised RMS misfit |
|
|
183
|
+
| `getAlpha` | Single log-spaced regularisation parameter |
|
|
184
|
+
| `getAlphas` | Depth-weighted regularisation vector |
|
|
185
|
+
| `dbdt_to_apprho` | Convert dB/dt to apparent resistivity |
|
|
186
|
+
| `invert` | Full regularised inversion loop |
|
|
187
|
+
|
|
188
|
+
**Private helpers:** `_gn_solve`, `_backtrack`, `_alpha_search`
|
|
189
|
+
|
|
190
|
+
**`invert()` key options:**
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
result = pytem.invert(
|
|
194
|
+
obs_data, thicknesses, log_resistivities, tx_radius, times,
|
|
195
|
+
analytical_j=True, # use getJ_ana (faster) or getJ_fd
|
|
196
|
+
system_filter=H, # frequency-domain instrument filter
|
|
197
|
+
waveform_times=wf_t, # transmitter waveform
|
|
198
|
+
waveform_currents=wf_I,
|
|
199
|
+
noise_std=0.02,
|
|
200
|
+
maxit=20,
|
|
201
|
+
use_numba=True,
|
|
202
|
+
)
|
|
203
|
+
# result keys: 'resistivities', 'thicknesses', 'rms_history',
|
|
204
|
+
# 'model_history', 'sensitivity', 'obs_data', 'times'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
When both `analytical_j=True` and a waveform are provided, the waveform Jacobian is formed analytically using the chain rule: `∂G_i/∂(ln ρ_j) = conv(∂F/∂(ln ρ_j), w)_i`, avoiding N+1 full waveform convolution calls.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### `ip_models.py`
|
|
212
|
+
Complex resistivity models for induced polarisation (IP). Each returns `rho(omega)` (complex) at a given angular frequency, suitable for passing into `te_reflection_coeff`.
|
|
213
|
+
|
|
214
|
+
| Function | Model |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `pelton_res_rho` | Pelton et al. (1978) |
|
|
217
|
+
| `cole_cole_rho` | Cole & Cole (1941) |
|
|
218
|
+
| `double_pelton_rho` | Double Pelton (two relaxation terms) |
|
|
219
|
+
| `mpa_rho` | Maximum Phase Angle (Fiandaca et al. 2018) |
|
|
220
|
+
| `get_m_taur_MPA` | Iterative MPA → Cole-Cole conversion |
|
|
221
|
+
| `tem_forward_ip` | Full TEM forward with per-layer IP |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### `plotter.py`
|
|
226
|
+
Standalone plotting functions (no class required):
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
ax = plot_sounding(times, obs, mod, labels=['Observed', 'Modelled'])
|
|
230
|
+
ax = plot_model(thicknesses, resistivities)
|
|
231
|
+
fig, axs = plot_inversion(times, obs_data, mod_data, thicknesses,
|
|
232
|
+
best_rho, rms_history)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
All functions accept an optional `ax` argument to plot into an existing axes.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Data flow
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
resistivities + thicknesses
|
|
243
|
+
│
|
|
244
|
+
▼
|
|
245
|
+
recursion.py ← Wait recursion (TE reflection coeff)
|
|
246
|
+
│
|
|
247
|
+
▼
|
|
248
|
+
forward.py ← Hankel + Fourier/Euler DLF transforms
|
|
249
|
+
│ system_filter applied in frequency domain
|
|
250
|
+
│ dispatch: CUDA > Numba > NumPy
|
|
251
|
+
▼
|
|
252
|
+
step-off dB/dt
|
|
253
|
+
│
|
|
254
|
+
├──── waveform.py ─── convolve with transmitter waveform
|
|
255
|
+
│
|
|
256
|
+
▼
|
|
257
|
+
inversion.py
|
|
258
|
+
├── getJ_ana / getJ_fd ← Jacobian
|
|
259
|
+
├── _alpha_search ← regularisation ladder
|
|
260
|
+
├── _gn_solve ← normal equations
|
|
261
|
+
└── _backtrack ← bounds enforcement
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Compute backends
|
|
267
|
+
|
|
268
|
+
| Backend | When active | Strength |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| NumPy | always | Portable, easy to debug |
|
|
271
|
+
| Numba JIT | `use_numba=True` and Numba installed | Fast scalar loops, CPU SIMD, prange parallelism |
|
|
272
|
+
| CuPy (CUDA) | `use_cuda=True` and CUDA available | Fully batched GPU; fastest for large problems |
|
|
273
|
+
|
|
274
|
+
Priority: CUDA > Numba > NumPy. Set `use_numba=False, use_cuda=False` to force NumPy.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## References
|
|
279
|
+
|
|
280
|
+
- Wait, J. R. (1954). Mutual coupling of loops lying on the ground. *Geophysics*, 19, 290–296.
|
|
281
|
+
- Key, K. (2009). 1D inversion of multicomponent, multifrequency marine CSEM data. *Geophysics*, 74(2), F9–F20.
|
|
282
|
+
- Key, K. (2012). Is the fast Hankel transform faster than quadrature? *Geophysics*, 77(3), F21–F30.
|
|
283
|
+
- Abate, J., & Whitt, W. (1995). Numerical inversion of Laplace transforms of probability distributions. *ORSA Journal on Computing*, 7(1), 36–43.
|
|
284
|
+
- Pelton, W. H., et al. (1978). Mineral discrimination and removal of inductive coupling with multifrequency IP. *Geophysics*, 43(3), 588–609.
|
|
285
|
+
- Fiandaca, G., et al. (2018). Re-parameterisations of the Cole–Cole model. *Geophysical Journal International*, 214(2), 1160–1173.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytem"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "1-D layered-earth time-domain electromagnetic (TEM) forward modelling and inversion"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Paul McLachlan", email = "pamcl@dtu.dk" }]
|
|
12
|
+
keywords = ["geophysics", "TEM", "TDEM", "electromagnetics", "inversion", "groundwater"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["numpy", "scipy", "pandas", "matplotlib", "numba"]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
gpu = ["cupy-cuda12x"]
|
|
23
|
+
maps = ["pyproj", "utm", "contextily", "matplotlib-scalebar"]
|
|
24
|
+
gerda = ["fdb", "utm"]
|
|
25
|
+
all = ["pytem[gpu,maps,gerda]"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/pmc93/PyTEM"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["pytem"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.dynamic]
|
|
34
|
+
version = { attr = "pytem.__version__" }
|