kirchcig 0.2.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.
kirchcig-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kirchcig contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.4
2
+ Name: kirchcig
3
+ Version: 0.2.0
4
+ Summary: GPU Kirchhoff migration to common-image gathers, and its exact adjoint.
5
+ Author: kirchcig contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zzzzswh/kirchcig
8
+ Project-URL: Repository, https://github.com/zzzzswh/kirchcig
9
+ Project-URL: Issues, https://github.com/zzzzswh/kirchcig/issues
10
+ Project-URL: Changelog, https://github.com/zzzzswh/kirchcig/blob/main/CHANGELOG.md
11
+ Keywords: seismic,kirchhoff,migration,common-image-gather,cuda,cupy,adjoint,least-squares-migration
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Classifier: Environment :: GPU :: NVIDIA CUDA
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.22
25
+ Provides-Extra: eikonal
26
+ Requires-Dist: scikit-fmm; extra == "eikonal"
27
+ Provides-Extra: cuda12
28
+ Requires-Dist: cupy-cuda12x; extra == "cuda12"
29
+ Provides-Extra: cuda11
30
+ Requires-Dist: cupy-cuda11x; extra == "cuda11"
31
+ Provides-Extra: torch
32
+ Requires-Dist: torch; extra == "torch"
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest; extra == "test"
35
+ Requires-Dist: scipy; extra == "test"
36
+ Requires-Dist: scikit-fmm; extra == "test"
37
+ Dynamic: license-file
38
+
39
+ # kirchcig
40
+
41
+ **GPU Kirchhoff migration to common-image gathers (CIGs), with an exact adjoint.**
42
+
43
+ English | [简体中文](https://github.com/zzzzswh/kirchcig/blob/main/README.zh-CN.md)
44
+
45
+ Hand-written CUDA kernels, compiled at runtime by NVRTC through CuPy. PyTorch is an optional zero-copy autograd adapter, not part of the compute path.
46
+
47
+ ![Stacked migrated image and the common-image gather at the scatterer](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/cig.png)
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install "kirchcig[cuda12]" # GPU, CUDA 12.x
53
+ pip install "kirchcig[cuda11]" # GPU, CUDA 11.x
54
+ pip install kirchcig # CPU only, NumPy reference engine
55
+ ```
56
+
57
+ No compiler and no nvcc needed; kernels are built at runtime by NVRTC.
58
+
59
+ | Extra | Pulls in | Gives you |
60
+ |---|---|---|
61
+ | *(none)* | `numpy` | `engine="numpy"`, the reference engine. Runs anywhere, slow. |
62
+ | `cuda12` / `cuda11` | `cupy-cuda12x` / `cupy-cuda11x` | `engine="cuda"`, the GPU engine. Pick the one matching your driver. |
63
+ | `eikonal` | `scikit-fmm` | Traveltimes for a non-constant velocity model. |
64
+ | `torch` | `torch` | `kirchcig.torch`, the autograd wrapper. |
65
+ | `test` | `pytest`, `scipy`, `scikit-fmm` | Test suite, and `op.to_scipy()`. |
66
+
67
+ Combine them: `pip install "kirchcig[cuda12,eikonal,torch]"`.
68
+
69
+ ## Quick start
70
+
71
+ ```python
72
+ import numpy as np
73
+ from kirchcig import migrate
74
+
75
+ # data (ns, nr, nt) prestack traces
76
+ # vel (nx, nz) smooth migration velocity [m/s]
77
+ # srcs (2, ns) source positions, rows are (x, z) [m]
78
+ # recs (2, nr) receiver positions, rows are (x, z) [m]
79
+
80
+ cig = migrate(
81
+ data, vel, srcs, recs,
82
+ dt=0.004, dx=10.0, dz=10.0,
83
+ nh=32, hmax=2000.0, # 32 half-offset bins out to 2000 m
84
+ )
85
+ # cig -> (32, nx, nz)
86
+
87
+ image = cig.sum(0) # (nx, nz) stacked image; nh=1 gives it directly
88
+ ```
89
+
90
+ No data at hand? This runs out of the box:
91
+
92
+ ```python
93
+ from kirchcig import KirchhoffCIG
94
+ op = KirchhoffCIG.demo() # constant velocity, one point scatterer
95
+ assert op.dot_test()
96
+ cig = op.adjoint(op.demo_data())
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ ### The operator pair
102
+
103
+ For inversion you want the operator, not the one-shot function:
104
+
105
+ ```python
106
+ from kirchcig import KirchhoffCIG
107
+
108
+ op = KirchhoffCIG(
109
+ nx=401, nz=201, dx=10.0, dz=10.0,
110
+ srcs=srcs, recs=recs,
111
+ nt=1500, dt=0.004,
112
+ vel=vel,
113
+ nh=32, hmax=2000.0,
114
+ domain="offset", # or "angle"
115
+ engine="cuda", # or "numpy", "auto"
116
+ )
117
+
118
+ cig = op.adjoint(data) # (ns, nr, nt) -> (nh, nx, nz) migration
119
+ data = op.forward(cig) # (nh, nx, nz) -> (ns, nr, nt) demigration
120
+ op.dot_test() # True
121
+ ```
122
+
123
+ `forward` and `adjoint` are exact transposes to accumulator precision, so they drop into any least-squares solver:
124
+
125
+ ```python
126
+ import scipy.sparse.linalg as spla
127
+ cig_lsm = spla.lsqr(op.to_scipy(), data.ravel(), iter_lim=20)[0].reshape(op.shape_model)
128
+ ```
129
+
130
+ ### Angle-domain gathers
131
+
132
+ ```python
133
+ op = KirchhoffCIG(..., domain="angle", nh=30, hmax=60.0) # 30 bins, 0-60 deg
134
+ ```
135
+
136
+ `hmax` is the maximum half opening angle in degrees. Bin indices come from source- and receiver-side emergence angles, computed from the traveltime gradients.
137
+
138
+ ### Anti-alias filtering
139
+
140
+ ```python
141
+ op = KirchhoffCIG(..., aa=True) # default aa_factor=1.0, aa_max=32
142
+ ```
143
+
144
+ Kirchhoff summation aliases wherever the operator's moveout between neighbouring traces exceeds half a period of the highest frequency present. It shows up as steeply dipping, criss-crossing arcs at large offsets and shallow depths; on coarsely sampled data they dominate the image. `aa=True` reads every contribution through a triangle filter whose half-width follows the local operator dip, the standard remedy of Lumley, Claerbout and Bevc (1994) as used by Claerbout's `trimo` and Madagascar's `sfmig2`.
145
+
146
+ ![Single-shot impulse response: aliased, anti-aliased, and a densely sampled reference](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/antialias.png)
147
+
148
+ <sub>One shot, a band-limited spike in every trace. Left: 31 receivers at 100 m, plain summation. Middle: the same data with `aa=True`. Right: 301 receivers at 10 m, no filter. `python examples/antialias.py`.</sub>
149
+
150
+ - `aa_factor` scales the filter width and is the `antialias=` parameter of `trimo` and `sfmig2` (all default to 1.0). 2.0 puts the triangle's first spectral null exactly on the alias frequency, at the cost of resolution on steep dips.
151
+ - `aa_max` caps the half-width in samples (default 32).
152
+ - `aa_stretch` (default on) also covers the time footprint of the image cell, `|dtau/dx| dx + |dtau/dz| dz`. An image sample stands for a `dx * dz` cell; when the depth grid is coarser than the time sampling (`2 dz / v > dt`) each depth sample of a plain summation lands several time samples apart and a demigrated reflector is a comb of interpolated spikes. This is Madagascar's `aastretch`. The source and receiver gradients are summed before the absolute value, so the footprint is only the depth term at the specular point, and the two widths combine as a root mean square. The gradient rides in the traveltime-table element (16 bytes per image point in the offset domain, 32 with angles or weights); `aa_stretch=False` gives the pure trace-axis criterion of `sfmig2`.
153
+ - Operator dips come from differencing the traveltime tables along the trace axis, so it works for eikonal and user-supplied tables alike. The source and receiver axes must be sorted along the line; the constructor warns otherwise.
154
+ - The operator pair stays an exact transpose; `dot_test()` passes with the filter on.
155
+ - Cost: the filter is applied through a three-tap identity on the double integral of the traces, so the price does not depend on the filter width. It does need a float64 copy of the data, `ns * nr * (nt + 2*aa_max + 1) * 8` bytes, and the adjoint reads three float64 taps per contribution instead of one float32 sample. On a V100 that is 1.8x on the adjoint and 2.8x on the forward (see Performance); `python benchmarks/bench.py --aa` measures it on yours.
156
+
157
+ ### Migration aperture
158
+
159
+ ```python
160
+ op = KirchhoffCIG(..., aperture=60.0) # cone half-angle from the vertical [deg]
161
+ op = KirchhoffCIG(..., apt=3000.0) # or a lateral distance [m]; both may be combined
162
+ ```
163
+
164
+ A trace contributes to an image point only if the point lies inside the aperture of *both* its source and its receiver: within `aperture` degrees of the vertical below them (`sfkirmig`'s `aperture=`), or within `apt` metres laterally (`sfmig2`'s `apt=`, in metres here). This suppresses far-aperture swing noise and skips the dropped contributions' work. On the README geometry a 60-degree cone keeps 47% of the trace-image-point pairs (45 degrees: 23%); on the V100 that cut the forward from 25.5 to 17.2 ms while the adjoint did not speed up at all, because its blocks are depth columns that straddle the cone edge, so masked lanes idle while their warp-mates gather and the coalesced table read is still made for every pair. Treat the aperture as an imaging control that comes with a forward speed-up. The cut is hard, as in Madagascar; `op.aperture_masks()` returns the two boolean masks, `python benchmarks/bench.py --aperture 60` prints the kept fraction and the timing.
165
+
166
+ The aperture is applied on the host by pushing the masked traveltime-table entries past the end of the trace, where the kernels already skip. No kernel changes, both engines drop exactly the same contributions, and the pair stays an exact transpose.
167
+
168
+ ### Amplitude weights
169
+
170
+ ```python
171
+ op = KirchhoffCIG(..., weight="obliquity") # sqrt(cos theta) per side
172
+ op = KirchhoffCIG(..., weight=["obliquity", "spreading"]) # times 1/sqrt(t) per side
173
+ op = KirchhoffCIG(..., weight=lambda t, theta, dt: np.cos(theta) ** 2)
174
+ op = KirchhoffCIG(..., weight=(w_srcs, w_recs)) # (ns, nx, nz), (nr, nx, nz)
175
+ ```
176
+
177
+ Every contribution is multiplied by `w_s(src, x, z) * w_r(rec, x, z)`, the product of a source-side and a receiver-side table. Presets are `"obliquity"` (`sqrt(cos theta)`, the product is the geometric mean of the two emergence cosines, the obliquity factor of Kirchhoff modelling; `sfkirmod` uses the arithmetic mean, which agrees to second order) and `"spreading"` (`1 / sqrt(t)`, the 2D Green's function amplitude of one leg up to the velocity); a list multiplies them; a callable is evaluated on each side's traveltime and emergence-angle tables; a pair of arrays is used as is. Both kernels multiply by the same float32 product, so `forward` is `A W`, `adjoint` is `W A^T` and the pair stays an exact transpose. The weight rides in the traveltime-table element, so it costs no extra memory transaction. These cover the obliquity and spreading factors; the full true-amplitude (Bleistein) weights are not separable into two sides and are not provided.
178
+
179
+ ### Half-derivative (rho) filter
180
+
181
+ ```python
182
+ op = KirchhoffCIG(..., halfderiv=True)
183
+ ```
184
+
185
+ Kirchhoff demigration in 2D needs a half-order time derivative. Spreading each image point along its traveltime curve and summing the spread points over a reflector leaves the stationary-phase factor of the one lateral integral behind: a 45-degree phase rotation and an `|omega|^-1/2` spectral tilt. A demigrated horizontal reflector then does not return the wavelet it was built with, and a migrated one carries the rotation the other way. `halfderiv=True` applies `H(omega) = sqrt(1 - rho e^{-i omega})`, the half of the backward difference, to every trace on the way out of `forward` and its exact transpose to the data on the way into `adjoint`. It is the filter Madagascar's `sf_halfint` implements and `sfmig2`, `sfkirchnew` and `sfkirmod` apply, with the same default leak `rho = 1 - 1/nt`; `halfderiv_rho` changes it.
186
+
187
+ It costs one float64 FFT per trace, runs on the engine's device, and is independent of the kernels. `dot_test()` passes with it on. Two properties to know: the discrete filter delays by a quarter sample (its phase is `pi/4 - omega/4`), so migrated reflectors sit `dt/4` shallower in two-way time and round trips are unshifted; and it does nothing about the depth-grid comb (`2 dz / v > dt`), which is what `aa=True` handles.
188
+
189
+ ### PyTorch
190
+
191
+ ```python
192
+ from kirchcig.torch import TorchKirchhoffCIG
193
+
194
+ top = TorchKirchhoffCIG(op)
195
+ cig = top.adjoint(data) # differentiable w.r.t. data
196
+ res = top.forward(cig) - data
197
+ res.pow(2).sum().backward()
198
+ ```
199
+
200
+ Tensors stay on the GPU. The operator is linear, so the backward of `forward` is `adjoint` and vice versa; the backward pass is itself recorded, so second-order derivatives work.
201
+
202
+ ### Custom traveltimes
203
+
204
+ ```python
205
+ op = KirchhoffCIG(..., trav=(trav_srcs, trav_recs))
206
+ # trav_srcs (ns, nx, nz) source-to-image-point traveltimes [s]
207
+ # trav_recs (nr, nx, nz) image-point-to-receiver traveltimes [s]
208
+ ```
209
+
210
+ Otherwise traveltimes come from an eikonal solve (`scikit-fmm`), or analytically for constant velocity. Note the layout: the source/receiver axis comes first, transposed relative to some other libraries, which is what keeps the adjoint reads coalesced.
211
+
212
+ ### Shapes
213
+
214
+ | | Shape | Notes |
215
+ |---|---|---|
216
+ | data | `(ns, nr, nt)` | float32 |
217
+ | model (CIG) | `(nh, nx, nz)` | gather axis outermost |
218
+ | `srcs`, `recs` | `(2, ns)`, `(2, nr)` | rows are `(x, z)` in metres |
219
+ | velocity | `(nx, nz)` or scalar | m/s |
220
+
221
+ ## Examples
222
+
223
+ ```bash
224
+ python examples/plot_cig.py # the cover figure
225
+ python examples/vel_analysis.py # the figure below
226
+ python examples/antialias.py # the anti-aliasing figure above
227
+ python examples/lsqr_migration.py # least-squares migration with SciPy LSQR
228
+ python examples/torch_deep_prior.py # deep-prior LSM
229
+ python benchmarks/bench.py # timings
230
+ ```
231
+
232
+ ![Gathers migrated with three velocities: too low, correct, too high](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/vel_analysis.png)
233
+
234
+ <sub>The same data migrated with three velocities. Flat gathers mean the velocity is right; curvature along the offset axis is what migration velocity analysis measures, and stacking destroys it.</sub>
235
+
236
+ ## Performance
237
+
238
+ Single **Tesla V100-PCIE-32GB** (driver 580.178.04), `nx=401, nz=201, ns=100, nr=200, nt=1500, nh=32`, offset domain. Each operator application evaluates 1.6e9 trace-image-point pairs.
239
+
240
+ | Accumulator | adjoint (migration) | forward (demigration) | dot-test relative error |
241
+ |---|---|---|---|
242
+ | `float64` (default) | 33.9 ms — 47.5 G pair-evals/s | 25.6 ms — 63.0 G pair-evals/s | 9.5e-08 |
243
+ | `float32` * | 33.5 ms — 48.2 G pair-evals/s | 18.8 ms — 85.8 G pair-evals/s | 1.1e-07 |
244
+ | `float64`, `aa=True, aa_stretch=False` | 72.7 ms — 22.2 G pair-evals/s | 67.3 ms — 23.9 G pair-evals/s | 2.0e-08 |
245
+ | `float64`, `aa=True` (with `aa_stretch`) | 62.0 ms — 26.0 G pair-evals/s | 71.2 ms — 22.6 G pair-evals/s | 2.3e-08 |
246
+
247
+ <sub>* the `float32` row has not been re-timed since the adjoint started chunking its source loop, which moved every other adjoint number here; the `float64` rows are the current ones.</sub>
248
+
249
+ Building the operator, including traveltime tables and the one-off NVRTC compile, takes about 1.4 s, 2.1 s with `aa=True`, and 2.6 s with `aa_stretch` as well (the extra work is differencing the tables for the operator dips). Anti-aliasing costs 1.8x on the adjoint (three float64 taps per contribution instead of one float32 sample) and 2.8x on the forward (six shared-memory atomics instead of two, plus the float64 output and its reverse integration). Timings on this machine vary by up to 25% between sessions (the `aa_stretch=False` row measured 98.5 ms on another day), so compare rows measured together. `forward` used to be about twice as fast as `adjoint` — it accumulates one trace per block in shared memory and writes it out once, while `adjoint` does an irregular gather along the traveltime curves — but chunking the adjoint's source loop closed most of that gap, and under `aa=True` the forward is now the slower of the two.
250
+
251
+ `--schunk` sets how many sources the adjoint keeps in registers, so that each receiver-table element is read once per chunk instead of once per source. Sweeping the last row within one session: 118.1 ms at `--schunk 1`, 62.0 ms at the default 4, 59.5 ms at 8, with the forward untouched at 71.1 ms. That 1.9x is most of what took this row from the 128.8 ms it cost in 0.2.0; packing the gradients into the traveltime element is the smaller part of the change (128.8 to 118.1 ms at `--schunk 1`, measured a session apart). One result here is not explained: with both in place, `aa_stretch` comes out *faster* than the plain trace-axis filter — 62.0 against 72.7 ms — although its table element is twice as wide, and the `aa_stretch=False` row has not itself been swept over `schunk`. Until it is, read the two `aa=True` adjoint numbers as about equal rather than as a measured cost of the stretch term.
252
+
253
+ The angle domain pads the element to its widest, 8 floats: traveltime, emergence angle, operator dip and both gradient components. `--domain angle --aa` costs 91.1 ms on the adjoint and 113.7 ms on the forward in the same session, with a dot-test error of 8.4e-08.
254
+
255
+ float64 accumulation is close to free on Volta and other data-centre cards (1:2 FP64:FP32) and buys bit-identical agreement with the NumPy reference engine. On consumer GeForce parts the ratio is about 1:64, so `acc="float32"` is the sensible default there; it costs roughly 1e-7 of relative accuracy.
256
+
257
+ Reproduce with `python benchmarks/bench.py`; `--acc float32`, `--nh`, `--domain`, `--aa`, `--no_aa_stretch`, `--aperture`, `--halfderiv`, `--weight`, `--schunk` and `--engine numpy` are accepted. The first two rows are without anti-aliasing. The operator runs on one device; select it with `cupy.cuda.Device`, or from the PyTorch wrapper by the tensor's device.
258
+
259
+ ## How it works
260
+
261
+ The kernels live as a CUDA C++ string in `kirchcig/_kernels.py`, compiled by `cupy.RawKernel(..., backend="nvrtc")` on first use and cached by CuPy afterwards. CuPy only allocates memory, compiles and launches; all arithmetic is in the kernels.
262
+
263
+ - **Why hand-written.** Kirchhoff migration is an irregular gather along traveltime curves, not a matmul or a convolution. No tensor op expresses it without blowing up memory traffic, and writing the kernel directly is what makes the rest of this list possible.
264
+ - **No atomics in the adjoint.** One thread per image point owns the whole gather axis, so every write is exclusive. Accumulators sit in shared memory as `[nh][block]`, bank-conflict free for any per-thread bin index. Block size is chosen automatically as the largest of {256, 128, 64, 32} that keeps the accumulators within 32 KB; for large `nh` it drops to 32 threads and opts in to the device limit.
265
+ - **Forward: one block per trace**, accumulated in shared memory with cheap shared-memory atomics and written out once.
266
+ - **Exact adjointness by construction.** Both kernels compute the sample index and interpolation weights with the *same* float32 expression, so the pair is the transpose of one sparse matrix; only the summation order differs.
267
+ - **float64 accumulators by default.** A migrated sample sums 10^4 to 10^5 terms. With float64 accumulation the CUDA engine is bit-identical to the NumPy reference; float32 accumulation costs about 1e-7 relative error, and used to buy roughly 1.5x on the adjoint (not re-timed since the source chunking).
268
+ - **One table element per (side, image point).** Traveltime, emergence angle, operator dip, amplitude weight and traveltime gradient are packed in a fixed order into 1, 2, 4 or 8 floats, so whatever the options, a contribution costs one or two aligned 16-byte loads per side. The receiver table is the dominant memory stream of the adjoint (it does not fit in L2 and is re-read for every source), so the adjoint processes sources in register chunks of `schunk` (default 4) and loads each receiver element once per chunk.
269
+ - **Model layout `(nh, nx, nz)`** keeps both the adjoint writeback and the forward model reads coalesced.
270
+ - **Compile-time specialisation.** `nh`, block size, accumulator type and the offset/angle switch are `-D` flags, so `nh` is a true compile-time constant. Changing it costs about a second of NVRTC, once.
271
+ - **Anti-aliasing costs three taps, not a filter loop.** A triangle of half-width `n` applied to a trace `d` equals `(D[i+n-1] - 2 D[i-1] + D[i-n-1]) / n^2` with `D` the double cumulative sum of `d`, so a contribution filtered by any width is still three interpolated reads. The adjoint kernel reads a float64 `D` that CuPy prepares with two cumulative sums; the forward kernel scatters the six transposed taps and CuPy reverse-integrates the result. `D` grows like `nt^2` and the second difference cancels almost all of it, which is why it is float64 and why the forward's trace accumulator is float64 under `aa=True` regardless of `acc`. The operator dip is packed next to the traveltime in the table element, so the extra input is one wider coalesced load, not a second table read.
272
+ - **PyTorch does no numerical work.** `kirchcig.torch` exchanges GPU buffers with CuPy through DLPack (nothing leaves the device; non-default streams are honoured) and registers the pair as `autograd.Function`s. Remove torch and the CUDA engine is unaffected.
273
+
274
+ Large problems are handled by chunking the time axis and splitting the source axis; both are exact, and the test suite checks that a split result equals an unsplit one.
275
+
276
+ ## Limitations
277
+
278
+ - **No true-amplitude weights.** `weight=` covers separable factors (obliquity, spreading, anything of the form `w_s * w_r`); the Bleistein/Schleicher weights that make the migration an inverse rather than an adjoint are not.
279
+ - **Without `aa=True`, the forward does not anti-alias the depth-to-time stretch.** With `2 dz / v > dt` a plain demigrated trace is a comb of interpolated spikes; either choose `dz <= v dt / 2` or turn on `aa` (its `aa_stretch` term handles it).
280
+ - **2D only.** The traveltime tables are the obstacle, not the kernels.
281
+ - **Offset binning uses absolute half-offset**, so positive and negative offsets are not distinguished.
282
+
283
+ ## Related
284
+
285
+ SEG-Y I/O: [segyio](https://github.com/equinor/segyio). Operator algebra and solvers: [PyLops](https://github.com/PyLops/pylops), which kirchcig plugs into via `to_scipy()`. Wave-equation modelling and RTM: [Deepwave](https://github.com/ar4/deepwave).
286
+
287
+ ## Requirements
288
+
289
+ Python >= 3.10 and `numpy`. Optional: `cupy` matching your CUDA version (GPU engine), `scikit-fmm` (eikonal traveltimes), `scipy` (`to_scipy()`), `torch` (autograd wrapper only, not used for compute).
290
+
291
+ Contributions welcome. `pytest -q` runs the dot-product tests under both engines; CUDA tests skip when no GPU is visible.
292
+
293
+ ## Citing
294
+
295
+ <TODO: Zenodo DOI>
296
+
297
+ ## License
298
+
299
+ MIT
300
+
301
+ ---
302
+
303
+ <sub>Keywords: Kirchhoff migration, prestack depth migration, common-image gather, CIG, angle gather, offset gather, GPU seismic imaging, CUDA, CuPy, least-squares migration, LSM, exact adjoint, demigration, migration velocity analysis, MVA, AVO, AVA, PyTorch, seismic inversion.</sub>
@@ -0,0 +1,265 @@
1
+ # kirchcig
2
+
3
+ **GPU Kirchhoff migration to common-image gathers (CIGs), with an exact adjoint.**
4
+
5
+ English | [简体中文](https://github.com/zzzzswh/kirchcig/blob/main/README.zh-CN.md)
6
+
7
+ Hand-written CUDA kernels, compiled at runtime by NVRTC through CuPy. PyTorch is an optional zero-copy autograd adapter, not part of the compute path.
8
+
9
+ ![Stacked migrated image and the common-image gather at the scatterer](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/cig.png)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install "kirchcig[cuda12]" # GPU, CUDA 12.x
15
+ pip install "kirchcig[cuda11]" # GPU, CUDA 11.x
16
+ pip install kirchcig # CPU only, NumPy reference engine
17
+ ```
18
+
19
+ No compiler and no nvcc needed; kernels are built at runtime by NVRTC.
20
+
21
+ | Extra | Pulls in | Gives you |
22
+ |---|---|---|
23
+ | *(none)* | `numpy` | `engine="numpy"`, the reference engine. Runs anywhere, slow. |
24
+ | `cuda12` / `cuda11` | `cupy-cuda12x` / `cupy-cuda11x` | `engine="cuda"`, the GPU engine. Pick the one matching your driver. |
25
+ | `eikonal` | `scikit-fmm` | Traveltimes for a non-constant velocity model. |
26
+ | `torch` | `torch` | `kirchcig.torch`, the autograd wrapper. |
27
+ | `test` | `pytest`, `scipy`, `scikit-fmm` | Test suite, and `op.to_scipy()`. |
28
+
29
+ Combine them: `pip install "kirchcig[cuda12,eikonal,torch]"`.
30
+
31
+ ## Quick start
32
+
33
+ ```python
34
+ import numpy as np
35
+ from kirchcig import migrate
36
+
37
+ # data (ns, nr, nt) prestack traces
38
+ # vel (nx, nz) smooth migration velocity [m/s]
39
+ # srcs (2, ns) source positions, rows are (x, z) [m]
40
+ # recs (2, nr) receiver positions, rows are (x, z) [m]
41
+
42
+ cig = migrate(
43
+ data, vel, srcs, recs,
44
+ dt=0.004, dx=10.0, dz=10.0,
45
+ nh=32, hmax=2000.0, # 32 half-offset bins out to 2000 m
46
+ )
47
+ # cig -> (32, nx, nz)
48
+
49
+ image = cig.sum(0) # (nx, nz) stacked image; nh=1 gives it directly
50
+ ```
51
+
52
+ No data at hand? This runs out of the box:
53
+
54
+ ```python
55
+ from kirchcig import KirchhoffCIG
56
+ op = KirchhoffCIG.demo() # constant velocity, one point scatterer
57
+ assert op.dot_test()
58
+ cig = op.adjoint(op.demo_data())
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### The operator pair
64
+
65
+ For inversion you want the operator, not the one-shot function:
66
+
67
+ ```python
68
+ from kirchcig import KirchhoffCIG
69
+
70
+ op = KirchhoffCIG(
71
+ nx=401, nz=201, dx=10.0, dz=10.0,
72
+ srcs=srcs, recs=recs,
73
+ nt=1500, dt=0.004,
74
+ vel=vel,
75
+ nh=32, hmax=2000.0,
76
+ domain="offset", # or "angle"
77
+ engine="cuda", # or "numpy", "auto"
78
+ )
79
+
80
+ cig = op.adjoint(data) # (ns, nr, nt) -> (nh, nx, nz) migration
81
+ data = op.forward(cig) # (nh, nx, nz) -> (ns, nr, nt) demigration
82
+ op.dot_test() # True
83
+ ```
84
+
85
+ `forward` and `adjoint` are exact transposes to accumulator precision, so they drop into any least-squares solver:
86
+
87
+ ```python
88
+ import scipy.sparse.linalg as spla
89
+ cig_lsm = spla.lsqr(op.to_scipy(), data.ravel(), iter_lim=20)[0].reshape(op.shape_model)
90
+ ```
91
+
92
+ ### Angle-domain gathers
93
+
94
+ ```python
95
+ op = KirchhoffCIG(..., domain="angle", nh=30, hmax=60.0) # 30 bins, 0-60 deg
96
+ ```
97
+
98
+ `hmax` is the maximum half opening angle in degrees. Bin indices come from source- and receiver-side emergence angles, computed from the traveltime gradients.
99
+
100
+ ### Anti-alias filtering
101
+
102
+ ```python
103
+ op = KirchhoffCIG(..., aa=True) # default aa_factor=1.0, aa_max=32
104
+ ```
105
+
106
+ Kirchhoff summation aliases wherever the operator's moveout between neighbouring traces exceeds half a period of the highest frequency present. It shows up as steeply dipping, criss-crossing arcs at large offsets and shallow depths; on coarsely sampled data they dominate the image. `aa=True` reads every contribution through a triangle filter whose half-width follows the local operator dip, the standard remedy of Lumley, Claerbout and Bevc (1994) as used by Claerbout's `trimo` and Madagascar's `sfmig2`.
107
+
108
+ ![Single-shot impulse response: aliased, anti-aliased, and a densely sampled reference](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/antialias.png)
109
+
110
+ <sub>One shot, a band-limited spike in every trace. Left: 31 receivers at 100 m, plain summation. Middle: the same data with `aa=True`. Right: 301 receivers at 10 m, no filter. `python examples/antialias.py`.</sub>
111
+
112
+ - `aa_factor` scales the filter width and is the `antialias=` parameter of `trimo` and `sfmig2` (all default to 1.0). 2.0 puts the triangle's first spectral null exactly on the alias frequency, at the cost of resolution on steep dips.
113
+ - `aa_max` caps the half-width in samples (default 32).
114
+ - `aa_stretch` (default on) also covers the time footprint of the image cell, `|dtau/dx| dx + |dtau/dz| dz`. An image sample stands for a `dx * dz` cell; when the depth grid is coarser than the time sampling (`2 dz / v > dt`) each depth sample of a plain summation lands several time samples apart and a demigrated reflector is a comb of interpolated spikes. This is Madagascar's `aastretch`. The source and receiver gradients are summed before the absolute value, so the footprint is only the depth term at the specular point, and the two widths combine as a root mean square. The gradient rides in the traveltime-table element (16 bytes per image point in the offset domain, 32 with angles or weights); `aa_stretch=False` gives the pure trace-axis criterion of `sfmig2`.
115
+ - Operator dips come from differencing the traveltime tables along the trace axis, so it works for eikonal and user-supplied tables alike. The source and receiver axes must be sorted along the line; the constructor warns otherwise.
116
+ - The operator pair stays an exact transpose; `dot_test()` passes with the filter on.
117
+ - Cost: the filter is applied through a three-tap identity on the double integral of the traces, so the price does not depend on the filter width. It does need a float64 copy of the data, `ns * nr * (nt + 2*aa_max + 1) * 8` bytes, and the adjoint reads three float64 taps per contribution instead of one float32 sample. On a V100 that is 1.8x on the adjoint and 2.8x on the forward (see Performance); `python benchmarks/bench.py --aa` measures it on yours.
118
+
119
+ ### Migration aperture
120
+
121
+ ```python
122
+ op = KirchhoffCIG(..., aperture=60.0) # cone half-angle from the vertical [deg]
123
+ op = KirchhoffCIG(..., apt=3000.0) # or a lateral distance [m]; both may be combined
124
+ ```
125
+
126
+ A trace contributes to an image point only if the point lies inside the aperture of *both* its source and its receiver: within `aperture` degrees of the vertical below them (`sfkirmig`'s `aperture=`), or within `apt` metres laterally (`sfmig2`'s `apt=`, in metres here). This suppresses far-aperture swing noise and skips the dropped contributions' work. On the README geometry a 60-degree cone keeps 47% of the trace-image-point pairs (45 degrees: 23%); on the V100 that cut the forward from 25.5 to 17.2 ms while the adjoint did not speed up at all, because its blocks are depth columns that straddle the cone edge, so masked lanes idle while their warp-mates gather and the coalesced table read is still made for every pair. Treat the aperture as an imaging control that comes with a forward speed-up. The cut is hard, as in Madagascar; `op.aperture_masks()` returns the two boolean masks, `python benchmarks/bench.py --aperture 60` prints the kept fraction and the timing.
127
+
128
+ The aperture is applied on the host by pushing the masked traveltime-table entries past the end of the trace, where the kernels already skip. No kernel changes, both engines drop exactly the same contributions, and the pair stays an exact transpose.
129
+
130
+ ### Amplitude weights
131
+
132
+ ```python
133
+ op = KirchhoffCIG(..., weight="obliquity") # sqrt(cos theta) per side
134
+ op = KirchhoffCIG(..., weight=["obliquity", "spreading"]) # times 1/sqrt(t) per side
135
+ op = KirchhoffCIG(..., weight=lambda t, theta, dt: np.cos(theta) ** 2)
136
+ op = KirchhoffCIG(..., weight=(w_srcs, w_recs)) # (ns, nx, nz), (nr, nx, nz)
137
+ ```
138
+
139
+ Every contribution is multiplied by `w_s(src, x, z) * w_r(rec, x, z)`, the product of a source-side and a receiver-side table. Presets are `"obliquity"` (`sqrt(cos theta)`, the product is the geometric mean of the two emergence cosines, the obliquity factor of Kirchhoff modelling; `sfkirmod` uses the arithmetic mean, which agrees to second order) and `"spreading"` (`1 / sqrt(t)`, the 2D Green's function amplitude of one leg up to the velocity); a list multiplies them; a callable is evaluated on each side's traveltime and emergence-angle tables; a pair of arrays is used as is. Both kernels multiply by the same float32 product, so `forward` is `A W`, `adjoint` is `W A^T` and the pair stays an exact transpose. The weight rides in the traveltime-table element, so it costs no extra memory transaction. These cover the obliquity and spreading factors; the full true-amplitude (Bleistein) weights are not separable into two sides and are not provided.
140
+
141
+ ### Half-derivative (rho) filter
142
+
143
+ ```python
144
+ op = KirchhoffCIG(..., halfderiv=True)
145
+ ```
146
+
147
+ Kirchhoff demigration in 2D needs a half-order time derivative. Spreading each image point along its traveltime curve and summing the spread points over a reflector leaves the stationary-phase factor of the one lateral integral behind: a 45-degree phase rotation and an `|omega|^-1/2` spectral tilt. A demigrated horizontal reflector then does not return the wavelet it was built with, and a migrated one carries the rotation the other way. `halfderiv=True` applies `H(omega) = sqrt(1 - rho e^{-i omega})`, the half of the backward difference, to every trace on the way out of `forward` and its exact transpose to the data on the way into `adjoint`. It is the filter Madagascar's `sf_halfint` implements and `sfmig2`, `sfkirchnew` and `sfkirmod` apply, with the same default leak `rho = 1 - 1/nt`; `halfderiv_rho` changes it.
148
+
149
+ It costs one float64 FFT per trace, runs on the engine's device, and is independent of the kernels. `dot_test()` passes with it on. Two properties to know: the discrete filter delays by a quarter sample (its phase is `pi/4 - omega/4`), so migrated reflectors sit `dt/4` shallower in two-way time and round trips are unshifted; and it does nothing about the depth-grid comb (`2 dz / v > dt`), which is what `aa=True` handles.
150
+
151
+ ### PyTorch
152
+
153
+ ```python
154
+ from kirchcig.torch import TorchKirchhoffCIG
155
+
156
+ top = TorchKirchhoffCIG(op)
157
+ cig = top.adjoint(data) # differentiable w.r.t. data
158
+ res = top.forward(cig) - data
159
+ res.pow(2).sum().backward()
160
+ ```
161
+
162
+ Tensors stay on the GPU. The operator is linear, so the backward of `forward` is `adjoint` and vice versa; the backward pass is itself recorded, so second-order derivatives work.
163
+
164
+ ### Custom traveltimes
165
+
166
+ ```python
167
+ op = KirchhoffCIG(..., trav=(trav_srcs, trav_recs))
168
+ # trav_srcs (ns, nx, nz) source-to-image-point traveltimes [s]
169
+ # trav_recs (nr, nx, nz) image-point-to-receiver traveltimes [s]
170
+ ```
171
+
172
+ Otherwise traveltimes come from an eikonal solve (`scikit-fmm`), or analytically for constant velocity. Note the layout: the source/receiver axis comes first, transposed relative to some other libraries, which is what keeps the adjoint reads coalesced.
173
+
174
+ ### Shapes
175
+
176
+ | | Shape | Notes |
177
+ |---|---|---|
178
+ | data | `(ns, nr, nt)` | float32 |
179
+ | model (CIG) | `(nh, nx, nz)` | gather axis outermost |
180
+ | `srcs`, `recs` | `(2, ns)`, `(2, nr)` | rows are `(x, z)` in metres |
181
+ | velocity | `(nx, nz)` or scalar | m/s |
182
+
183
+ ## Examples
184
+
185
+ ```bash
186
+ python examples/plot_cig.py # the cover figure
187
+ python examples/vel_analysis.py # the figure below
188
+ python examples/antialias.py # the anti-aliasing figure above
189
+ python examples/lsqr_migration.py # least-squares migration with SciPy LSQR
190
+ python examples/torch_deep_prior.py # deep-prior LSM
191
+ python benchmarks/bench.py # timings
192
+ ```
193
+
194
+ ![Gathers migrated with three velocities: too low, correct, too high](https://raw.githubusercontent.com/zzzzswh/kirchcig/main/docs/img/vel_analysis.png)
195
+
196
+ <sub>The same data migrated with three velocities. Flat gathers mean the velocity is right; curvature along the offset axis is what migration velocity analysis measures, and stacking destroys it.</sub>
197
+
198
+ ## Performance
199
+
200
+ Single **Tesla V100-PCIE-32GB** (driver 580.178.04), `nx=401, nz=201, ns=100, nr=200, nt=1500, nh=32`, offset domain. Each operator application evaluates 1.6e9 trace-image-point pairs.
201
+
202
+ | Accumulator | adjoint (migration) | forward (demigration) | dot-test relative error |
203
+ |---|---|---|---|
204
+ | `float64` (default) | 33.9 ms — 47.5 G pair-evals/s | 25.6 ms — 63.0 G pair-evals/s | 9.5e-08 |
205
+ | `float32` * | 33.5 ms — 48.2 G pair-evals/s | 18.8 ms — 85.8 G pair-evals/s | 1.1e-07 |
206
+ | `float64`, `aa=True, aa_stretch=False` | 72.7 ms — 22.2 G pair-evals/s | 67.3 ms — 23.9 G pair-evals/s | 2.0e-08 |
207
+ | `float64`, `aa=True` (with `aa_stretch`) | 62.0 ms — 26.0 G pair-evals/s | 71.2 ms — 22.6 G pair-evals/s | 2.3e-08 |
208
+
209
+ <sub>* the `float32` row has not been re-timed since the adjoint started chunking its source loop, which moved every other adjoint number here; the `float64` rows are the current ones.</sub>
210
+
211
+ Building the operator, including traveltime tables and the one-off NVRTC compile, takes about 1.4 s, 2.1 s with `aa=True`, and 2.6 s with `aa_stretch` as well (the extra work is differencing the tables for the operator dips). Anti-aliasing costs 1.8x on the adjoint (three float64 taps per contribution instead of one float32 sample) and 2.8x on the forward (six shared-memory atomics instead of two, plus the float64 output and its reverse integration). Timings on this machine vary by up to 25% between sessions (the `aa_stretch=False` row measured 98.5 ms on another day), so compare rows measured together. `forward` used to be about twice as fast as `adjoint` — it accumulates one trace per block in shared memory and writes it out once, while `adjoint` does an irregular gather along the traveltime curves — but chunking the adjoint's source loop closed most of that gap, and under `aa=True` the forward is now the slower of the two.
212
+
213
+ `--schunk` sets how many sources the adjoint keeps in registers, so that each receiver-table element is read once per chunk instead of once per source. Sweeping the last row within one session: 118.1 ms at `--schunk 1`, 62.0 ms at the default 4, 59.5 ms at 8, with the forward untouched at 71.1 ms. That 1.9x is most of what took this row from the 128.8 ms it cost in 0.2.0; packing the gradients into the traveltime element is the smaller part of the change (128.8 to 118.1 ms at `--schunk 1`, measured a session apart). One result here is not explained: with both in place, `aa_stretch` comes out *faster* than the plain trace-axis filter — 62.0 against 72.7 ms — although its table element is twice as wide, and the `aa_stretch=False` row has not itself been swept over `schunk`. Until it is, read the two `aa=True` adjoint numbers as about equal rather than as a measured cost of the stretch term.
214
+
215
+ The angle domain pads the element to its widest, 8 floats: traveltime, emergence angle, operator dip and both gradient components. `--domain angle --aa` costs 91.1 ms on the adjoint and 113.7 ms on the forward in the same session, with a dot-test error of 8.4e-08.
216
+
217
+ float64 accumulation is close to free on Volta and other data-centre cards (1:2 FP64:FP32) and buys bit-identical agreement with the NumPy reference engine. On consumer GeForce parts the ratio is about 1:64, so `acc="float32"` is the sensible default there; it costs roughly 1e-7 of relative accuracy.
218
+
219
+ Reproduce with `python benchmarks/bench.py`; `--acc float32`, `--nh`, `--domain`, `--aa`, `--no_aa_stretch`, `--aperture`, `--halfderiv`, `--weight`, `--schunk` and `--engine numpy` are accepted. The first two rows are without anti-aliasing. The operator runs on one device; select it with `cupy.cuda.Device`, or from the PyTorch wrapper by the tensor's device.
220
+
221
+ ## How it works
222
+
223
+ The kernels live as a CUDA C++ string in `kirchcig/_kernels.py`, compiled by `cupy.RawKernel(..., backend="nvrtc")` on first use and cached by CuPy afterwards. CuPy only allocates memory, compiles and launches; all arithmetic is in the kernels.
224
+
225
+ - **Why hand-written.** Kirchhoff migration is an irregular gather along traveltime curves, not a matmul or a convolution. No tensor op expresses it without blowing up memory traffic, and writing the kernel directly is what makes the rest of this list possible.
226
+ - **No atomics in the adjoint.** One thread per image point owns the whole gather axis, so every write is exclusive. Accumulators sit in shared memory as `[nh][block]`, bank-conflict free for any per-thread bin index. Block size is chosen automatically as the largest of {256, 128, 64, 32} that keeps the accumulators within 32 KB; for large `nh` it drops to 32 threads and opts in to the device limit.
227
+ - **Forward: one block per trace**, accumulated in shared memory with cheap shared-memory atomics and written out once.
228
+ - **Exact adjointness by construction.** Both kernels compute the sample index and interpolation weights with the *same* float32 expression, so the pair is the transpose of one sparse matrix; only the summation order differs.
229
+ - **float64 accumulators by default.** A migrated sample sums 10^4 to 10^5 terms. With float64 accumulation the CUDA engine is bit-identical to the NumPy reference; float32 accumulation costs about 1e-7 relative error, and used to buy roughly 1.5x on the adjoint (not re-timed since the source chunking).
230
+ - **One table element per (side, image point).** Traveltime, emergence angle, operator dip, amplitude weight and traveltime gradient are packed in a fixed order into 1, 2, 4 or 8 floats, so whatever the options, a contribution costs one or two aligned 16-byte loads per side. The receiver table is the dominant memory stream of the adjoint (it does not fit in L2 and is re-read for every source), so the adjoint processes sources in register chunks of `schunk` (default 4) and loads each receiver element once per chunk.
231
+ - **Model layout `(nh, nx, nz)`** keeps both the adjoint writeback and the forward model reads coalesced.
232
+ - **Compile-time specialisation.** `nh`, block size, accumulator type and the offset/angle switch are `-D` flags, so `nh` is a true compile-time constant. Changing it costs about a second of NVRTC, once.
233
+ - **Anti-aliasing costs three taps, not a filter loop.** A triangle of half-width `n` applied to a trace `d` equals `(D[i+n-1] - 2 D[i-1] + D[i-n-1]) / n^2` with `D` the double cumulative sum of `d`, so a contribution filtered by any width is still three interpolated reads. The adjoint kernel reads a float64 `D` that CuPy prepares with two cumulative sums; the forward kernel scatters the six transposed taps and CuPy reverse-integrates the result. `D` grows like `nt^2` and the second difference cancels almost all of it, which is why it is float64 and why the forward's trace accumulator is float64 under `aa=True` regardless of `acc`. The operator dip is packed next to the traveltime in the table element, so the extra input is one wider coalesced load, not a second table read.
234
+ - **PyTorch does no numerical work.** `kirchcig.torch` exchanges GPU buffers with CuPy through DLPack (nothing leaves the device; non-default streams are honoured) and registers the pair as `autograd.Function`s. Remove torch and the CUDA engine is unaffected.
235
+
236
+ Large problems are handled by chunking the time axis and splitting the source axis; both are exact, and the test suite checks that a split result equals an unsplit one.
237
+
238
+ ## Limitations
239
+
240
+ - **No true-amplitude weights.** `weight=` covers separable factors (obliquity, spreading, anything of the form `w_s * w_r`); the Bleistein/Schleicher weights that make the migration an inverse rather than an adjoint are not.
241
+ - **Without `aa=True`, the forward does not anti-alias the depth-to-time stretch.** With `2 dz / v > dt` a plain demigrated trace is a comb of interpolated spikes; either choose `dz <= v dt / 2` or turn on `aa` (its `aa_stretch` term handles it).
242
+ - **2D only.** The traveltime tables are the obstacle, not the kernels.
243
+ - **Offset binning uses absolute half-offset**, so positive and negative offsets are not distinguished.
244
+
245
+ ## Related
246
+
247
+ SEG-Y I/O: [segyio](https://github.com/equinor/segyio). Operator algebra and solvers: [PyLops](https://github.com/PyLops/pylops), which kirchcig plugs into via `to_scipy()`. Wave-equation modelling and RTM: [Deepwave](https://github.com/ar4/deepwave).
248
+
249
+ ## Requirements
250
+
251
+ Python >= 3.10 and `numpy`. Optional: `cupy` matching your CUDA version (GPU engine), `scikit-fmm` (eikonal traveltimes), `scipy` (`to_scipy()`), `torch` (autograd wrapper only, not used for compute).
252
+
253
+ Contributions welcome. `pytest -q` runs the dot-product tests under both engines; CUDA tests skip when no GPU is visible.
254
+
255
+ ## Citing
256
+
257
+ <TODO: Zenodo DOI>
258
+
259
+ ## License
260
+
261
+ MIT
262
+
263
+ ---
264
+
265
+ <sub>Keywords: Kirchhoff migration, prestack depth migration, common-image gather, CIG, angle gather, offset gather, GPU seismic imaging, CUDA, CuPy, least-squares migration, LSM, exact adjoint, demigration, migration velocity analysis, MVA, AVO, AVA, PyTorch, seismic inversion.</sub>
@@ -0,0 +1,35 @@
1
+ """kirchcig: GPU Kirchhoff migration to common-image gathers, and its exact adjoint.
2
+
3
+ from kirchcig import migrate, KirchhoffCIG
4
+
5
+ ``migrate`` is the one-shot function; ``KirchhoffCIG`` is the operator pair for
6
+ inversion. The PyTorch wrapper lives in ``kirchcig.torch``.
7
+ """
8
+ from ._version import __version__
9
+ from ._operator import KirchhoffCIG, aperture_mask, migrate, weight_tables
10
+ from ._traveltime import (analytic_traveltime, eikonal_traveltime,
11
+ emergence_angles, traveltime_tables)
12
+ from ._engine_numpy import NumpyEngine
13
+ from ._halfderiv import HalfDerivative
14
+
15
+
16
+ def cuda_available() -> bool:
17
+ """True when the ``cuda`` engine can be used (CuPy + a visible GPU)."""
18
+ from ._engine_cuda import cuda_available as _avail
19
+ return _avail()
20
+
21
+
22
+ __all__ = [
23
+ "__version__",
24
+ "KirchhoffCIG",
25
+ "migrate",
26
+ "aperture_mask",
27
+ "weight_tables",
28
+ "cuda_available",
29
+ "NumpyEngine",
30
+ "HalfDerivative",
31
+ "traveltime_tables",
32
+ "analytic_traveltime",
33
+ "eikonal_traveltime",
34
+ "emergence_angles",
35
+ ]