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 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__" }