MC²Plus – Oil India Ltd. – IIT Kharagpur Energy Innovation Challenge 2026
Track 1: AI-Driven Subsurface Intelligence
Recovers a clean, interpretable subsurface image from noisy real seismic data without ever needing clean training data, while preserving faults — the structural features where hydrocarbons are trapped — then runs automated fault, horizon, and facies interpretation on top.
Trained and evaluated on the F3 Netherlands block (Alaudah et al. 2019, Zenodo 3755060) — 401 inlines × 701 crosslines × 255 depth samples of real post-stack seismic amplitude with ground-truth lithostratigraphic facies labels.
On held-out test inlines with known noise added to real F3 geology, the denoiser reaches 13.66 dB SNR / 0.900 SSIM, against 10.98 dB for f-x deconvolution, 10.32 dB for this project's previous denoiser, and 6.02 dB for the untouched input. Fault picks agree with clean-section picks at 0.643 Dice, up from 0.345 on the noisy input — so the denoising is measurably helping the interpretation rather than erasing it. Fault preservation comes from the estimator itself (a posterior mean that defers to the observed amplitude wherever context fails to predict it) rather than from a tuned penalty term.
# 1. Install dependencies
pip install -r requirements.txt
# 2. Download the real F3 Netherlands dataset (~1 GB from Zenodo)
python data/download_f3.py
# 3a. Train the denoiser. Validation mode: known noise is added to real F3
# sections, the model sees only the contaminated data, and it is scored
# against the originals — real geology with a reference.
python -m deepseis.denoise --config configs/denoise.yaml
# 3b. Field mode: train on the raw survey, which is the model that ships.
python -m deepseis.denoise --config configs/denoise.yaml --field \
--run-dir runs/denoise_field
# 3c. Train the interpretation heads on the denoised survey. The facies head
# trains across many inlines and is scored on held-out ones.
python -m deepseis.train --config configs/default.yaml \
--blindspot runs/denoise_field/blindspot.pt
# 4. Score the denoiser on the held-out test block against classical baselines
python -m deepseis.evaluate --checkpoint runs/denoise/blindspot.pt --split test
# ...and against the previous model, and a structure-term ablation:
python -m deepseis.train --config configs/default.yaml --train-legacy-denoiser
python -m deepseis.denoise --config configs/denoise.yaml --no-structure \
--run-dir runs/denoise_nostruct
python -m deepseis.evaluate --checkpoint runs/denoise/blindspot.pt --split test \
--legacy runs/default/denoiser.pt \
--compare no_structure=runs/denoise_nostruct/blindspot.pt
# 5. Launch the dashboard
streamlit run app/dashboard.py
# Open http://localhost:8501Run the test suite: pytest tests/
| File | Shape | Contents |
|---|---|---|
data/raw/f3/data/train/train_seismic.npy |
(401, 701, 255) | Real post-stack seismic amplitude |
data/raw/f3/data/train/train_labels.npy |
(401, 701, 255) | 6-class facies labels (0–5) |
The denoiser is self-supervised — it trains directly on the noisy F3 amplitude, learning this survey's noise characteristics with no clean reference needed.
FaultSeg trains on a freshly generated synthetic volume and is applied to the real denoised data — exactly the workflow from the FaultSeg3D paper (Wu et al. 2019). No real fault ground-truth exists for F3, so quantitative Dice is not reported; qualitative fault-probability overlays are shown in the dashboard.
F3 facies classes (Alaudah et al. 2019):
0 Upper North Sea · 1 Middle North Sea · 2 Lower North Sea · 3 Rijnland/Chalk · 4 Jurassic · 5 Triassic
The facies head trains on denoised sections drawn from across the training block and is
scored on held-out inlines, using the same buffered split layout as the denoiser
(deepseis/facies.py). It previously reported 94.4% pixel accuracy on inline 0 — the one
inline it had been fitted on — which could not distinguish fitting from generalising in
either direction.
Held-out mean IoU is 0.892, over all six units, on inlines 101–130 — a block the head never saw, behind a 15-line buffer:
| Unit | Held-out IoU |
|---|---|
| Upper North Sea | 0.979 |
| Middle North Sea | 0.868 |
| Lower North Sea | 0.962 |
| Rijnland/Chalk | 0.877 |
| Jurassic | 0.826 |
| Triassic | 0.839 |
Every unit clears 0.82, including the two rarest. On the training block it reaches 97.8% pixel accuracy and 0.940 mean IoU — the gap between that and 0.892 is what generalisation costs, and it is small.
Getting that number to mean anything required fixing the split, and the story is worth recording because two earlier versions of it were wrong in opposite directions.
F3's facies are spatially segregated along the inline axis: all six units occur only in inlines 0–168, Triassic disappearing around 168 and Jurassic thinning to nothing by ~235. The denoiser's survey-wide split (validation at 307–342) therefore lands entirely in a region where two of the six units do not exist. Under that split they could not be scored at all — so a first draft of this README reported them at 0.0 IoU, an artefact of counting absent-from-truth classes as zeroes, and a second draft reported their generalisation as unmeasured, which was true but only because the split was wrong.
The facies split is now confined to the interval where the full stratigraphic column is
present (train.facies_split_ranges): train 0–85, validation 101–130, test 146–169, with
15-line buffers, and every partition verified to contain all six units. It is still a spatial
hold-out — just a smaller one that asks a question the data can answer. The units that
"couldn't generalise" score 0.826 and 0.839.
The same bug had a second effect worth recording, because it is the kind that flatters a
model rather than embarrassing it. Mean IoU was averaged over whichever classes were
non-NaN that epoch, so a model that gave up on a hard class saw it drop out of the
denominator instead of contributing a zero — the score went up for getting worse, jumping
from 0.573 to 0.739 between two epochs. The denominator is now pinned to the classes present
in the held-out truth (facies.mean_iou_over, tested in tests/test_facies.py).
One thing the fixed metric does not penalise, and which the report records separately so it
cannot hide: the head still predicts Jurassic and Triassic somewhere in a block where neither
occurs. Those are false positives, and excluding absent classes from mean IoU is the
conventional treatment, but classes_absent_but_predicted in facies_metrics.json names them
and the Facies tab flags them in place.
The previous denoiser was a masking-based Noise2Void model. Measured on F3 it removed
1.8% of the input variance and left corr(input, output) = 0.992, while deleting 70–75%
of the energy above 0.6 Nyquist and almost nothing below 0.4. That is a mild low-pass
filter, not a denoiser, and its headline leakage score of 0.334 was mostly earned by barely
touching the data — a filter that removes nothing scores a perfect 0.000 on that metric.
Three things caused it, and none were fixable by tuning:
- The masking destroyed the training data.
masking.modedefaulted toauto, whose own estimator routed 76% of F3 patches to StructN2V; StructN2V then blinded a full-height column per masked pixel, so with 82 masked pixels in a 64×64 patch, 97.9% of the input was replaced by a random shuffle. Three-quarters of training was noise-to-noise regression, and the only function that minimises that is a smooth low-order guess. - Supervision reached 2% of pixels. Masked N2V computes its loss only at masked locations, and feeds a corrupted input at training time and a clean one at inference.
- Everything was fitted on one inline.
volume[0].T, one of 401, then served across the whole survey — with no held-out data anywhere in the project.
A blind-spot architecture, not a blind-spot mask (models/blindspot.py). The receptive
field is restricted so the prediction at a pixel provably cannot see that pixel, using
half-plane convolutions over the four rotations of the input (Laine et al. 2019). Nothing is
masked, the loss is evaluated at every pixel, and train and inference see identical inputs.
The blindness is verified exhaustively by autograd in tests/test_blindspot.py rather than
assumed — the previous blind spot was silently not blind, and nothing caught it.
It predicts a distribution, then folds the observation back in (losses/nll.py). The
network outputs a per-pixel Gaussian (mu, sigma_p) over the clean value given the
surroundings, and is trained by maximum likelihood on the noisy data. Crucially the output
is not mu: a blind-spot network can only ever produce E[x | context excluding the pixel],
which is intrinsically over-smoothed because it throws away the most informative measurement
of the pixel — the pixel. Combining prior and observation through the noise model recovers
E[x | context AND pixel]:
x_hat = (mu · sigma_n² + y · sigma_p²) / (sigma_p² + sigma_n²)
This is what makes it fault-preserving, and it is a property of the estimator rather than a
penalty term. Where context predicts a sample well — conformable bedding — sigma_p is small
and the noise is removed. Where context fails — fault cuts, pinch-outs, channel margins,
exactly the features being interpreted — sigma_p is large and the observed amplitude is kept.
Bias-free and normalisation-free. Every convolution has bias=False and every activation
is positively homogeneous, so f(a·x) = a·f(x) exactly (Mohan et al. 2020). Amplitude-scale
train/serve mismatch becomes structurally impossible rather than a bug to be reintroduced,
and removing GroupNorm (which rescaled by whatever else was in the patch, destroying
relative amplitude) makes the network exactly translation equivariant. That in turn removes
the need for patch-and-average inference — itself a post-hoc smoothing operator — so a
section is denoised in a single pass.
The noise level is measured, not learned (noise_estimate.py). The training likelihood
constrains only sigma_p² + sigma_n², so the split between "unpredictable" and "noise" is
close to unidentifiable — and that split is exactly what sets how much gets removed. A
partly-trained model here had the total variance right to within 15% while splitting it
almost backwards (sigma_p² = 0.151 against a true 0.037; sigma_n² = 0.115 against a true
0.187), which put 57% of the weight on the raw observation where the correct split puts 17%.
Measuring sigma_n from the F-K spectrum instead was worth +3.1 dB.
Two estimators are needed, because the obvious one fails silently on real data. White noise is flat across the F-K plane, so the high-frequency/high-wavenumber corner holds a noise floor and nothing else — accurate to 1% on sections with known added noise. But processed seismic is band-limited on purpose: F3's corner sits five orders of magnitude below its passband, so reading a noise level there measures the anti-alias filter, returns ~0.005 against a section amplitude of 0.87, and would turn the denoiser into an identity. The fallback reads outside the geological dip fan within the passband, which survives band-limiting. The choice between them is made by testing whether the corner's spectrum is flat (a real noise floor, ratio 0.97–1.00 across a 30× range of noise levels) or still rolling off (a stopband, 0.05).
Three weaknesses in the old evaluation are fixed. Sections come from a held-out test block, separated from training by a 30-line buffer — adjacent F3 inlines correlate at 0.93, so a random split would have made "held-out" meaningless. Scoring uses a reference on real geology: known noise is added to real F3 sections, the model is trained self-supervised on the contaminated data alone, and scored against the originals, so PSNR/SSIM/SNR are exact on real structure rather than on a synthetic model of it. And the controls are baselines worth losing to — f-x deconvolution and structure-oriented smoothing, not just an identity and two blurs.
Field mode (
--field) trains on the raw survey with no injected noise. It is the model the dashboard serves, and it has no reference, so it is selected on held-out predictive likelihood. Not on leakage: that is minimised by removing nothing, and a field run selecting on it kept its epoch-0 checkpoint over every later one.
Held-out test block (inlines 373–400, behind two 30-line buffers), 8 sections, known
noise added to real F3 geology. +/- is the spread across sections. faultDice is the
agreement of the fault head's picks with its picks on the clean section — i.e. how much of
the interpretation survives.
| method | SNR dB | +/- | PSNR | SSIM | leakage | removed | resid. coh | faultDice |
|---|---|---|---|---|---|---|---|---|
| blind-spot + per-section σₙ | 13.79 | 0.96 | 34.27 | 0.901 | 0.114 | 0.199 | 0.162 | 0.654 |
| blind-spot + orthogonalization | 13.72 | 0.94 | 34.21 | 0.900 | 0.059 | 0.207 | 0.157 | 0.647 |
| blind-spot | 13.66 | 0.94 | 34.15 | 0.900 | 0.100 | 0.209 | 0.162 | 0.643 |
| f-x deconvolution | 10.98 | 0.75 | 31.47 | 0.848 | 0.370 | 0.134 | 0.178 | 0.597 |
| structure-oriented smoothing | 10.45 | 0.50 | 30.94 | 0.829 | 0.422 | 0.103 | 0.136 | 0.520 |
| 3×3 median | 10.39 | 0.60 | 30.87 | 0.831 | 0.179 | 0.212 | 0.194 | 0.565 |
| previous denoiser (masking N2V) | 10.32 | 0.43 | 30.81 | 0.807 | 0.148 | 0.260 | 0.186 | 0.531 |
| gaussian σ=1.0 | 10.25 | 0.52 | 30.74 | 0.851 | 0.436 | 0.212 | 0.236 | 0.515 |
| gaussian σ=0.5 | 9.61 | 0.06 | 30.09 | 0.803 | 0.604 | 0.040 | 0.138 | 0.496 |
| identity (removes nothing) | 6.02 | 0.02 | 26.51 | 0.690 | 0.000 | 0.000 | 0.000 | 0.345 |
+3.34 dB over the previous denoiser and +2.68 dB over f-x deconvolution, with SSIM 0.900 against 0.807 and 0.848. It removes 21% of the input variance at a leakage of 0.100, where the previous model removed 26% at 0.148 — more removed and less of it was signal.
The fault-Dice column is the one that answers the project's actual claim. Fault picks on the noisy section agree with the clean-section picks at 0.345; denoising raises that to 0.643, against 0.597 for f-x decon and 0.531 for the previous model. Denoising is helping the interpretation rather than quietly erasing what is being interpreted, and that is now a measured number rather than a qualitative overlay.
Two optional refinements were measured rather than assumed, and both earn a small keep: re-estimating σₙ per section (+0.13 dB, since the true noise level varies 0.40–0.54 between lines) and Chen–Fomel orthogonalization (leakage 0.100 → 0.059 with SNR still improving slightly, so it is recovering signal rather than gaming its own metric).
The project's signature feature used to be a "fault-preservation loss" with an ON/OFF toggle.
Rebuilt versions of both terms are in losses/geology.py — a coherence-trough penalty that
punishes healing a fault, and a dip-fan spectral term with a proper taper — and each is
tested to rise monotonically as a fault is blurred out and to charge nothing for removing
incoherent noise. They are nonetheless off by default, because an ablation on a matched
schedule says they do nothing:
| SNR dB | SSIM | leakage | faultDice | |
|---|---|---|---|---|
| structure terms off | 13.82 | 0.903 | 0.111 | 0.657 |
| structure terms on | 13.66 | 0.900 | 0.100 | 0.656 |
Identical seed, steps, and learning-rate curve, stopped at the same epoch; OFF was equal or better at all six matched epochs. The gap is well inside the ±0.97 dB spread across sections, so the honest reading is "no measurable benefit" rather than "OFF is better".
This is not really a failure of the terms — it is a sign they are redundant here. The posterior-mean estimator already keeps the observed amplitude wherever context cannot predict it, which is what a fault is, so a penalty for the same behaviour has little left to correct. They may still pay on data with strong coherent noise, which is what the dip-fan term is shaped for, so the code and its tests stay.
Reporting this cost the project its headline demo. It is the right answer anyway: the previous ON/OFF comparison was 0.334 vs 0.360 leakage on a single seed with no error bars, and the direction of that difference was not established either.
On the spectral profile: the blind-spot model keeps 0.94 / 0.84 of the two lowest F-K bands and almost nothing above 0.6 Nyquist. That looks like a low-pass until you check where F3's signal is — the survey is band-limited by its processing chain, with power falling off a cliff past 0.4 Nyquist, so the band being emptied contains the added noise and nothing else. The same profile on data whose signal reached those frequencies would be a problem.
Trained on the raw survey with nothing added, the same architecture removes about 0.6% of the input variance. That is not a failure of the model; it is what a calibrated denoiser should do to this data. F3's published volume is migrated, stacked, commercially processed seismic, and the measured incoherent noise level is correspondingly low. The demonstration of denoising capability therefore has to come from the semi-synthetic control above — real geology, known noise — and any figure claiming a dramatic visual improvement on raw F3 should be treated with suspicion.
Field-mode training also stops early on its own: held-out predictive likelihood peaked at epoch 2 and got worse for three consecutive epochs while the training likelihood kept improving. The held-out selection correctly kept epoch 2. With very little noise to model, there is not much for the network to learn beyond the geology it is already being shown.
Training budgets used here. The reported semi-synthetic model is the best of 6 epochs (360 steps) on a CPU, selected on held-out SNR; it had plateaued (14.16 → 14.22 → 14.23 dB over its last three epochs). The ablation below is run on the identical schedule and stopped at the same epoch so the comparison is matched. These are not converged-to-death numbers — a longer run on a GPU would likely add a few tenths of a dB — but the margins over every baseline are large enough that more training does not change any conclusion.
| Tab | What you see |
|---|---|
| 🧮 Denoise | F3 input → denoised → the difference section, with the per-pixel uncertainty map and the leakage / energy-removed / residual-coherence numbers. Below it: a controlled experiment (add known noise to a held-out inline, denoise, compare against the original, with live sliders for the noise level) and the full benchmark table read straight from the evaluation report |
| 🧩 Fault segmentation | Fault-probability overlays on noisy vs. denoised F3 input |
| 🗺️ F3 Survey explorer | Inline slider — scrub through all 401 real F3 inlines live |
| 🌊 Facies | Held-out IoU per unit up front, then the in-sample map vs. published labels and error map — labelled as in-sample |
| 📤 Export | Download denoised section as SEG-Y or .npy |
| 🔬 Diagnostics | Selectable section (controlled experiment or raw F3), the no-reference metric set, F-K spectra before/after on a shared colour scale, energy kept per F-K band, and the signal-leakage map |
The Denoise tab is deliberately blunt about the fact that raw F3 barely changes: the app says
so in place, and puts the controlled experiment directly underneath so the capability can be
seen rather than asserted. The benchmark table is loaded from runs/denoise/evaluation_final.json
rather than typed in, so the app cannot quietly disagree with this README.
- Load real F3
.npyvolume — SEG-Y (.sgy) and FaultSeg3D.datalso supported - Self-supervised blind-spot denoiser — architectural blind spot, verified by autograd
- Per-pixel uncertainty and posterior-mean output — structure preservation from the estimator
- Reference-free noise-level estimation, with detection of band-limited data
- Bias-free, normalisation-free network; single-pass full-section inference
- Multi-section training with buffered train/val/test splits along the inline axis
- Structure-preservation terms — coherence-trough and dip-fan, each behaviourally tested (available, but off by default: ablated and found to give no measurable benefit)
- Evaluation against f-x deconvolution, structure-oriented smoothing, and the previous model
- FaultSeg-style fault segmentation — synthetic-trained, applied to real denoised F3 data
- 6-class facies segmentation trained across the survey, scored on held-out inlines
- SEG-Y export with in-dashboard download button
- 6-tab Streamlit dashboard with a controlled-noise demo and the live benchmark table,
covered by headless smoke tests (
tests/test_dashboard.py)
- Local signal-and-noise orthogonalization post-process (Chen & Fomel 2015)
- Fault-pick agreement scoring — does denoising preserve interpretability, quantitatively
- Physics-guided diffusion refiner (DDPM, Laplacian-smoothness prior)
- Horizon tracking (amplitude-peak auto-tracking, fault-aware)
- F-K spectrum viewer (before/after denoising)
- Jacobian sensitivity map (
masking/jacobian_explain.py) — built to audit the legacy masking denoiser's blind spot. Removed from the dashboard: the current model's sensitivity to its own pixel is exactly zero by construction, andtests/test_blindspot.pyasserts that at every pixel rather than at one the viewer happens to click
The masking denoiser (models/unet.py, masking/) is retained so the previous model can be
loaded and scored beside the current one, which deepseis.evaluate --legacy does.
deepseis/
├── configs/
│ ├── denoise.yaml # blind-spot denoiser: data splits, model, losses, eval
│ └── default.yaml # interpretation heads + legacy denoiser
├── data/
│ ├── download_f3.py # downloads F3 from Zenodo, auto-patches config
│ └── samples/faultseg3d/ # bundled FaultSeg3D validation pair (.dat)
├── deepseis/
│ ├── io/ # dataset.py (splits, sampling) · segy.py · synthetic.py
│ │ # noise.py · patches.py
│ ├── models/ # blindspot.py · unet.py (legacy) · faultseg.py
│ │ # facies.py · diffusion.py
│ ├── losses/ # nll.py · geology.py · reconstruction.py (legacy)
│ │ # edge_preserve.py (legacy) · frequency.py (legacy)
│ ├── masking/ # noise2void.py · struct_n2v.py · jacobian_explain.py
│ ├── interpretation/ # horizon.py
│ ├── denoise.py # train / infer the blind-spot denoiser
│ ├── noise_estimate.py # reference-free sigma_n from the F-K spectrum
│ ├── baselines.py # f-x decon · structure-oriented smoothing · blurs
│ ├── postprocess.py # local signal-and-noise orthogonalization
│ ├── evaluate.py # held-out scoring against every baseline
│ └── train.py · infer.py · metrics.py
├── app/dashboard.py # Streamlit 6-tab dashboard
├── Dockerfile # builds and launches the full pipeline in one command
└── tests/ # pytest test suite
docker build -t deepseis .
docker run -p 8501:8501 deepseis
# Open http://localhost:8501The container automatically downloads F3 data, trains all models, then launches the dashboard.
The failures this project has had were not crashes — they were components that ran cleanly while doing the opposite of what they claimed. A blind spot that was not blind. An edge loss that rewarded raising gradient magnitude, so the model it supervised emitted more energy than it was given. A masking mode that destroyed 98% of its own training data. Each survived because nothing tested the property, only that the code ran and the loss fell.
So the suite asserts properties, not smoke:
| Test | Property |
|---|---|
test_blindspot.py |
Every pixel's prediction has exactly zero gradient w.r.t. that pixel — checked exhaustively, for both heads, all input channels, and odd section sizes. Plus: the network does use its context (a constant output would also be perfectly blind), and all four half-planes are reached. |
test_geology_losses.py |
Each structure term is zero on an untouched section, rises monotonically as a fault is blurred out, and does not charge for removing incoherent noise. |
test_baselines.py |
The dip estimator recovers dips it is given, and every baseline actually improves SNR — a weak baseline would flatter the model. |
test_noise_estimate.py |
The estimator recovers known noise levels, is invariant to signal amplitude, and detects band-limited data instead of reporting a filter stopband as silence. |
test_dataset.py |
Splits are disjoint and buffered, crosslines are truncated to the training block, normalisation is one global constant, and injected noise is fixed per section (resampling it would quietly turn the benchmark into the easier Noise2Noise problem). |
test_denoise.py |
Translation and scale equivariance, tiled inference matching single-pass, and checkpoints carrying their normalisation constants. |
test_evaluate.py |
Scoring touches only the requested split, the reference is the clean section, and the stored normalisation is used rather than recomputed. |
test_facies.py |
Held-out scoring uses unseen sections, patches combine by vote rather than by averaging class indices, and mean IoU has a fixed denominator so a model cannot raise its score by giving up on a hard class. |
test_masking.py |
The legacy blind spot never donates a pixel to itself, and StructN2V's blind region stays a bounded segment inside a hard budget. |
test_dashboard.py |
The page executes end to end with no exception, every tab renders, the export flow actually produces a file, and the controlled demo hits the benchmark result — which catches serving-path normalisation bugs that leave the page looking perfectly fine. |
Four of these caught real defects during the rebuild:
- the dip estimator used the standard
0.5·arctan2(2·jxy, jyy−jxx)orientation shortcut, which returns the gradient direction rather than the structure direction — worth ~4 dB, and it was making the structure-oriented baseline look weak; - the orthogonalization update was the widely quoted
s + w·n0form, which does not actually orthogonalize (the projection form recovers leaked signal exactly where that one is break-even); - mean IoU had a moving denominator, so the facies head's score rose when it gave up on a class;
- double normalisation at three separate call sites — the dashboard's controlled demo,
the dashboard's main denoise path, and the facies head's training data.
serve_sectionapplies the checkpoint's stored scale, and each caller had already divided by roughly the same number. It is silent by construction: the network is scale equivariant so its own output absorbs the extra factor, but σₙ is a fixed physical noise level, so only the ratio shifts and the denoiser quietly under-removes. Measured cost: 3.17 dB and 4× less noise removed, with the page rendering perfectly and no error raised anywhere. The facies head had been fitted on the doubly-scaled sections and scored 0.93 against its own broken input — and 0.13 once served correctly.
That last one is the archetype for this whole project, which is why serve_section now
carries a runtime guard that warns when it is handed a section whose amplitude is suspiciously
close to unity for the checkpoint it is being served by. The failures here do not crash. They
return a worse number that still looks entirely plausible.
Krull et al. 2019 (Noise2Void) · Laine et al. 2019 (blind-spot architecture, Bayesian
combination) · Batson & Royer 2019 (Noise2Self) · Mohan et al. 2020 (bias-free denoising
networks) · Canales 1984 (f-x deconvolution) · Chen & Fomel 2015 (local signal-and-noise
orthogonalization) · Wu et al. 2019 (FaultSeg3D) · Alaudah et al. 2019 (F3 benchmark) ·
Birnie et al. 2021/2022 · Li et al. 2024/2025
See DeepSeis_Project_Spec.md §15 for the full bibliography.