detectability 0.2.1__tar.gz → 0.3.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.
- {detectability-0.2.1/src/detectability.egg-info → detectability-0.3.0}/PKG-INFO +19 -7
- {detectability-0.2.1 → detectability-0.3.0}/README.md +17 -6
- {detectability-0.2.1 → detectability-0.3.0}/pyproject.toml +2 -1
- detectability-0.3.0/src/detectability/__init__.py +7 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/detect.py +26 -6
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/survey.py +47 -12
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/validation.py +18 -0
- {detectability-0.2.1 → detectability-0.3.0/src/detectability.egg-info}/PKG-INFO +19 -7
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_detect.py +11 -2
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_survey.py +40 -0
- detectability-0.2.1/src/detectability/__init__.py +0 -7
- {detectability-0.2.1 → detectability-0.3.0}/LICENSE +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/setup.cfg +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/constants.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/data/harmonica_reference.json +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/data/igrf14coeffs.txt +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/gravity.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/igrf.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/magnetics.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability/rocks.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability.egg-info/SOURCES.txt +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability.egg-info/dependency_links.txt +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability.egg-info/requires.txt +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/src/detectability.egg-info/top_level.txt +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_gravity.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_igrf.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_magnetics.py +0 -0
- {detectability-0.2.1 → detectability-0.3.0}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectability
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pre-survey detectability, modelled and reproducible: forward-modelled gravity and magnetic responses, station spacing and a detectability verdict for near-surface targets.
|
|
5
5
|
Author: Mükremin Nergiz
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -9,6 +9,7 @@ Project-URL: Documentation, https://detectability.mukreminnergiz.com/method
|
|
|
9
9
|
Project-URL: Validation, https://detectability.mukreminnergiz.com/validation
|
|
10
10
|
Project-URL: Repository, https://github.com/mukremin-nergiz/detectability
|
|
11
11
|
Project-URL: Issues, https://github.com/mukremin-nergiz/detectability/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md
|
|
12
13
|
Project-URL: Citation, https://doi.org/10.5281/zenodo.23931815
|
|
13
14
|
Keywords: geophysics,gravity,magnetics,survey design,near-surface,forward modelling,IGRF
|
|
14
15
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -68,7 +69,7 @@ print(report.summary())
|
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
```text
|
|
71
|
-
Detectability 0.
|
|
72
|
+
Detectability 0.3.0: sphere in limestone
|
|
72
73
|
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
73
74
|
stations every 5 m or less, profile at least 153 m long
|
|
74
75
|
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
@@ -82,16 +83,24 @@ Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `d
|
|
|
82
83
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
83
84
|
in, and measured contrasts can be given instead.
|
|
84
85
|
|
|
86
|
+
Methods: `gravity`, `magnetic` (total-field anomaly) and, on request, `gradient`: the reading of a vertical gradiometer
|
|
87
|
+
(lower minus upper sensor, in nT over the baseline), with presets for the Bartington Grad601, Sensys FGM650, Foerster
|
|
88
|
+
FEREX 4.034 and GEM GSM-19G. The defaults are `gravity` and `magnetic`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
evaluate(target, survey, methods=["gravity", "magnetic", "gradient"])
|
|
92
|
+
```
|
|
93
|
+
|
|
85
94
|
## Modules
|
|
86
95
|
|
|
87
96
|
| Module | Contents |
|
|
88
97
|
|---|---|
|
|
89
98
|
| `survey` | `evaluate()`: target and survey in, report and reproduction script out |
|
|
90
99
|
| `gravity` | sphere, horizontal cylinder, right rectangular prism (Nagy; Plouff) and its gradient tensor, vertical cylinder (exact, by a rim integral) and its gradient tensor, vertical step, dyke, 2D polygon (Talwani; Won & Bevis) |
|
|
91
|
-
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation |
|
|
100
|
+
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation (a gradiometer reading is the difference at two sensor heights) |
|
|
92
101
|
| `igrf` | IGRF-14 main field from location and date, from the official IAGA coefficients |
|
|
93
102
|
| `rocks` | typical densities and magnetic susceptibilities with their ranges (Telford et al. 1990) |
|
|
94
|
-
| `detect` | instrument
|
|
103
|
+
| `detect` | instrument presets with their sources (gravimeters, magnetometers, gradiometers), field noise, signal-to-noise ratio, verdict, station spacing, profile length |
|
|
95
104
|
|
|
96
105
|
## Validation
|
|
97
106
|
|
|
@@ -117,6 +126,7 @@ python tools/harmonica_reference.py --check
|
|
|
117
126
|
| Analytical limit | Vertical cylinder, gravity, on its axis | Exact on-axis cylinder formula | 1.7e-13 % | 1.0e-10 % |
|
|
118
127
|
| Independent formula | Horizontal cylinder, gravity | 2D polygon, 720 sides (Talwani; Won & Bevis) | 6.3e-12 % | 0.01 % |
|
|
119
128
|
| Independent formula | Vertical dyke, gravity | 2D polygon (Won & Bevis) | 1.9e-13 % | 1.0e-06 % |
|
|
129
|
+
| Independent formula | Sphere, magnetic gradient (gradiometer reading) | Closed-form vertical-dipole field at both sensors | 7.8e-14 % | 1.0e-10 % |
|
|
120
130
|
| Numerical integration | Prism, gravity | 16³ Gauss–Legendre point masses | 2.9e-06 % | 0.001 % |
|
|
121
131
|
| Numerical integration | Horizontal cylinder, magnetic | 3D dipoles integrated along strike | 2.2e-07 % | 0.01 % |
|
|
122
132
|
| Numerical integration | Prism, magnetic | 16³ Gauss–Legendre point dipoles | 1.3e-12 % | 0.001 % |
|
|
@@ -147,16 +157,18 @@ pytest
|
|
|
147
157
|
Flat topography, a homogeneous host and no regional trend; 2D targets are infinitely long along strike; the profile
|
|
148
158
|
crosses the centre of the target; self-demagnetisation is neglected. Results are expectations under these assumptions,
|
|
149
159
|
not guarantees. Instrument presets come from manufacturers' data sheets (`detect.INSTRUMENTS` lists each figure and its
|
|
150
|
-
source); field noise, which usually dominates, is a stated assumption that you can change.
|
|
160
|
+
source); field noise, which usually dominates, is a stated assumption that you can change. For gradiometers the default
|
|
161
|
+
field noise (0.5 nT) is an assumption, not a published figure: soil, heading and walking noise depend on the site.
|
|
151
162
|
|
|
152
163
|
## Citation
|
|
153
164
|
|
|
154
165
|
See [CITATION.cff](https://github.com/mukremin-nergiz/detectability/blob/main/CITATION.cff), or:
|
|
155
166
|
|
|
156
|
-
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.
|
|
167
|
+
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.3.0) [Computer software].
|
|
157
168
|
> Zenodo. https://doi.org/10.5281/zenodo.23931815
|
|
158
169
|
|
|
159
|
-
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo.
|
|
170
|
+
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo. Changes between
|
|
171
|
+
releases: [CHANGELOG.md](https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md).
|
|
160
172
|
|
|
161
173
|
## Author
|
|
162
174
|
|
|
@@ -35,7 +35,7 @@ print(report.summary())
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
Detectability 0.
|
|
38
|
+
Detectability 0.3.0: sphere in limestone
|
|
39
39
|
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
40
40
|
stations every 5 m or less, profile at least 153 m long
|
|
41
41
|
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
@@ -49,16 +49,24 @@ Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `d
|
|
|
49
49
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
50
50
|
in, and measured contrasts can be given instead.
|
|
51
51
|
|
|
52
|
+
Methods: `gravity`, `magnetic` (total-field anomaly) and, on request, `gradient`: the reading of a vertical gradiometer
|
|
53
|
+
(lower minus upper sensor, in nT over the baseline), with presets for the Bartington Grad601, Sensys FGM650, Foerster
|
|
54
|
+
FEREX 4.034 and GEM GSM-19G. The defaults are `gravity` and `magnetic`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
evaluate(target, survey, methods=["gravity", "magnetic", "gradient"])
|
|
58
|
+
```
|
|
59
|
+
|
|
52
60
|
## Modules
|
|
53
61
|
|
|
54
62
|
| Module | Contents |
|
|
55
63
|
|---|---|
|
|
56
64
|
| `survey` | `evaluate()`: target and survey in, report and reproduction script out |
|
|
57
65
|
| `gravity` | sphere, horizontal cylinder, right rectangular prism (Nagy; Plouff) and its gradient tensor, vertical cylinder (exact, by a rim integral) and its gradient tensor, vertical step, dyke, 2D polygon (Talwani; Won & Bevis) |
|
|
58
|
-
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation |
|
|
66
|
+
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation (a gradiometer reading is the difference at two sensor heights) |
|
|
59
67
|
| `igrf` | IGRF-14 main field from location and date, from the official IAGA coefficients |
|
|
60
68
|
| `rocks` | typical densities and magnetic susceptibilities with their ranges (Telford et al. 1990) |
|
|
61
|
-
| `detect` | instrument
|
|
69
|
+
| `detect` | instrument presets with their sources (gravimeters, magnetometers, gradiometers), field noise, signal-to-noise ratio, verdict, station spacing, profile length |
|
|
62
70
|
|
|
63
71
|
## Validation
|
|
64
72
|
|
|
@@ -84,6 +92,7 @@ python tools/harmonica_reference.py --check
|
|
|
84
92
|
| Analytical limit | Vertical cylinder, gravity, on its axis | Exact on-axis cylinder formula | 1.7e-13 % | 1.0e-10 % |
|
|
85
93
|
| Independent formula | Horizontal cylinder, gravity | 2D polygon, 720 sides (Talwani; Won & Bevis) | 6.3e-12 % | 0.01 % |
|
|
86
94
|
| Independent formula | Vertical dyke, gravity | 2D polygon (Won & Bevis) | 1.9e-13 % | 1.0e-06 % |
|
|
95
|
+
| Independent formula | Sphere, magnetic gradient (gradiometer reading) | Closed-form vertical-dipole field at both sensors | 7.8e-14 % | 1.0e-10 % |
|
|
87
96
|
| Numerical integration | Prism, gravity | 16³ Gauss–Legendre point masses | 2.9e-06 % | 0.001 % |
|
|
88
97
|
| Numerical integration | Horizontal cylinder, magnetic | 3D dipoles integrated along strike | 2.2e-07 % | 0.01 % |
|
|
89
98
|
| Numerical integration | Prism, magnetic | 16³ Gauss–Legendre point dipoles | 1.3e-12 % | 0.001 % |
|
|
@@ -114,16 +123,18 @@ pytest
|
|
|
114
123
|
Flat topography, a homogeneous host and no regional trend; 2D targets are infinitely long along strike; the profile
|
|
115
124
|
crosses the centre of the target; self-demagnetisation is neglected. Results are expectations under these assumptions,
|
|
116
125
|
not guarantees. Instrument presets come from manufacturers' data sheets (`detect.INSTRUMENTS` lists each figure and its
|
|
117
|
-
source); field noise, which usually dominates, is a stated assumption that you can change.
|
|
126
|
+
source); field noise, which usually dominates, is a stated assumption that you can change. For gradiometers the default
|
|
127
|
+
field noise (0.5 nT) is an assumption, not a published figure: soil, heading and walking noise depend on the site.
|
|
118
128
|
|
|
119
129
|
## Citation
|
|
120
130
|
|
|
121
131
|
See [CITATION.cff](https://github.com/mukremin-nergiz/detectability/blob/main/CITATION.cff), or:
|
|
122
132
|
|
|
123
|
-
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.
|
|
133
|
+
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.3.0) [Computer software].
|
|
124
134
|
> Zenodo. https://doi.org/10.5281/zenodo.23931815
|
|
125
135
|
|
|
126
|
-
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo.
|
|
136
|
+
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo. Changes between
|
|
137
|
+
releases: [CHANGELOG.md](https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md).
|
|
127
138
|
|
|
128
139
|
## Author
|
|
129
140
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "detectability"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Pre-survey detectability, modelled and reproducible: forward-modelled gravity and magnetic responses, station spacing and a detectability verdict for near-surface targets."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = ["geophysics", "gravity", "magnetics", "survey design", "near-surface", "forward modelling", "IGRF"]
|
|
@@ -31,6 +31,7 @@ Documentation = "https://detectability.mukreminnergiz.com/method"
|
|
|
31
31
|
Validation = "https://detectability.mukreminnergiz.com/validation"
|
|
32
32
|
Repository = "https://github.com/mukremin-nergiz/detectability"
|
|
33
33
|
Issues = "https://github.com/mukremin-nergiz/detectability/issues"
|
|
34
|
+
Changelog = "https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md"
|
|
34
35
|
Citation = "https://doi.org/10.5281/zenodo.23931815"
|
|
35
36
|
|
|
36
37
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Detectability: pre-survey detectability, modelled and reproducible."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
|
|
5
|
+
from .survey import DEFAULT_METHODS, METHODS, SHAPES, Report, Survey, Target, evaluate # noqa: E402
|
|
6
|
+
|
|
7
|
+
__all__ = ["evaluate", "Target", "Survey", "Report", "SHAPES", "METHODS", "DEFAULT_METHODS", "__version__"]
|
|
@@ -19,11 +19,12 @@ STATIONS_ACROSS = 5
|
|
|
19
19
|
class Instrument:
|
|
20
20
|
"""An instrument preset: its noise, what the figure means and where it comes from (checked 4 Nov 2026)."""
|
|
21
21
|
|
|
22
|
-
method: str # "gravity" or "magnetic
|
|
23
|
-
noise: float # one standard deviation of a reading, in µGal (gravity) or nT (magnetic)
|
|
22
|
+
method: str # "gravity", "magnetic" or "gradient" (magnetic vertical gradient)
|
|
23
|
+
noise: float # one standard deviation of a reading, in µGal (gravity) or nT (magnetic and gradient)
|
|
24
24
|
basis: str # what the manufacturer's figure is
|
|
25
25
|
reference: str # instrument the figure is taken from
|
|
26
26
|
source: str # data sheet or publication
|
|
27
|
+
baseline: float | None = None # m, vertical distance between the two sensors (gradiometers)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
INSTRUMENTS: dict[str, Instrument] = {
|
|
@@ -45,18 +46,37 @@ INSTRUMENTS: dict[str, Instrument] = {
|
|
|
45
46
|
"caesium vapour": Instrument(
|
|
46
47
|
"magnetic", 0.008, "noise < 0.008 nT/√Hz RMS, i.e. at 1 Hz", "Geometrics G-858",
|
|
47
48
|
"https://www.geomatrix.co.uk/land-products/magnetic/g-858/"),
|
|
49
|
+
# Vertical gradiometers: two sensors one above the other; the reading is the lower minus the upper sensor, in nT
|
|
50
|
+
# over the baseline (as Bartington and Foerster report it; nT/m only for a 1 m baseline).
|
|
51
|
+
"Bartington Grad601": Instrument(
|
|
52
|
+
"gradient", 0.1, "noise 100 pT peak-to-peak maximum, taken as one standard deviation to be conservative",
|
|
53
|
+
"Bartington Grad-01-1000L sensor (Grad601)", "https://www.bartingtondownloads.com/wp-content/uploads/DS0060.pdf",
|
|
54
|
+
baseline=1.0),
|
|
55
|
+
"Sensys FGM650": Instrument(
|
|
56
|
+
"gradient", 0.04, "noise < 40 pT rms/√Hz at 1 Hz", "Sensys FGM650 fluxgate gradiometer",
|
|
57
|
+
"https://sensysmagnetometer.com/de/produkte/fgm650-gradiometer/", baseline=0.65),
|
|
58
|
+
"Foerster FEREX 4.034": Instrument(
|
|
59
|
+
"gradient", 0.04, "noise < 40 pT/√Hz at 1 Hz (MG-10 probe)", "Foerster FEREX 4.034 with MG-10-550 probe",
|
|
60
|
+
"https://www.foerstergroup.com/FOERSTER/downloads/product/ferex/FOERSTER_FLY_FEREX4.034_DE.pdf", baseline=0.55),
|
|
61
|
+
"GEM GSM-19G": Instrument(
|
|
62
|
+
"gradient", 0.031, "0.022 nT at 1 Hz per sensor; two sensors in difference give 0.022 × √2",
|
|
63
|
+
"GEM Systems GSM-19G Overhauser gradiometer (one staff section, 0.56 m, per the manual)",
|
|
64
|
+
"https://www.gemsys.ca/wp-content/themes/gemsystems/pdf/GEM_Overhauser_GSM_19_fin.pdf", baseline=0.56),
|
|
48
65
|
}
|
|
49
|
-
# Fluxgate gradiometers (e.g. Bartington Grad601) measure a vertical gradient, not the total-field anomaly modelled
|
|
50
|
-
# here, so they are not offered until a gradiometer response is implemented.
|
|
51
66
|
|
|
52
67
|
GRAVIMETERS_UGAL: dict[str, float] = {k: v.noise for k, v in INSTRUMENTS.items() if v.method == "gravity"}
|
|
53
68
|
MAGNETOMETERS_NT: dict[str, float] = {k: v.noise for k, v in INSTRUMENTS.items() if v.method == "magnetic"}
|
|
69
|
+
GRADIOMETERS_NT: dict[str, float] = {k: v.noise for k, v in INSTRUMENTS.items() if v.method == "gradient"}
|
|
54
70
|
|
|
55
71
|
# Field noise is what remains after corrections and usually dominates the instrument noise.
|
|
56
72
|
DEFAULT_FIELD_NOISE_UGAL = 10.0 # microgravity: tares, drift residuals, terrain, cultural noise
|
|
57
73
|
DEFAULT_FIELD_NOISE_NT = 2.0 # quiet rural site after diurnal correction; urban and industrial sites are far noisier
|
|
74
|
+
# Gradient surveys cancel diurnal variation and the regional field, so their field noise is mainly soil, heading and
|
|
75
|
+
# walking noise. 0.5 nT is an assumption, not a published figure (the Grad601 manual asks for a set-up spot that varies
|
|
76
|
+
# by less than about 0.5 nT over 1 m); change it for the site.
|
|
77
|
+
DEFAULT_FIELD_NOISE_GRADIENT_NT = 0.5
|
|
58
78
|
|
|
59
|
-
UNITS = {"gravity": "µGal", "magnetic": "nT"}
|
|
79
|
+
UNITS = {"gravity": "µGal", "magnetic": "nT", "gradient": "nT"}
|
|
60
80
|
|
|
61
81
|
|
|
62
82
|
def effective_noise(instrument: float, field_noise: float) -> float:
|
|
@@ -143,7 +163,7 @@ def assess(x, anomaly, noise: float, depth: float, method: str = "gravity") -> A
|
|
|
143
163
|
f"Station spacing allows at least {STATIONS_ACROSS} stations across the anomaly width ({width:.3g} m).",
|
|
144
164
|
"Flat topography, homogeneous host and no regional trend; terrain and regional effects reduce the margin.",
|
|
145
165
|
]
|
|
146
|
-
if method
|
|
166
|
+
if method in ("magnetic", "gradient"):
|
|
147
167
|
assumptions.append("Peak is the largest absolute value of the (often two-lobed) anomaly; no remanence unless given.")
|
|
148
168
|
return Assessment(method, peak, float(x[peak_i]), width, shape, noise, snr, verdict, spacing, length, assumptions,
|
|
149
169
|
unit)
|
|
@@ -20,7 +20,8 @@ import numpy as np
|
|
|
20
20
|
from . import __version__, detect, gravity, igrf, magnetics, rocks
|
|
21
21
|
from .constants import SI_TO_UGAL
|
|
22
22
|
|
|
23
|
-
METHODS = ("gravity", "magnetic")
|
|
23
|
+
METHODS = ("gravity", "magnetic", "gradient") # gradient: magnetic vertical gradient (two-sensor gradiometer)
|
|
24
|
+
DEFAULT_METHODS = ("gravity", "magnetic")
|
|
24
25
|
|
|
25
26
|
SHAPES: dict[str, str] = {
|
|
26
27
|
"sphere": "Sphere (cavity, boulder, buried object). size = diameter.",
|
|
@@ -72,6 +73,11 @@ class Survey:
|
|
|
72
73
|
magnetic_field_noise: float = detect.DEFAULT_FIELD_NOISE_NT # nT
|
|
73
74
|
magnetic_instrument_noise: float | None = None # nT, overrides the magnetometer preset
|
|
74
75
|
sensor_height: float = 0.5 # m, magnetometer sensor above ground
|
|
76
|
+
gradiometer: str = "Bartington Grad601"
|
|
77
|
+
gradient_field_noise: float = detect.DEFAULT_FIELD_NOISE_GRADIENT_NT # nT, of the gradiometer reading
|
|
78
|
+
gradient_instrument_noise: float | None = None # nT, overrides the gradiometer preset
|
|
79
|
+
gradiometer_height: float = 0.2 # m, lower sensor above ground (Bartington: about 20 cm in normal use)
|
|
80
|
+
gradiometer_baseline: float | None = None # m between the sensors; default: the gradiometer preset's
|
|
75
81
|
profile_azimuth: float = 0.0 # degrees east of north; 2D targets strike at right angles to it
|
|
76
82
|
latitude: float | None = None # for the main field (IGRF-14); needed for magnetics
|
|
77
83
|
longitude: float | None = None
|
|
@@ -261,7 +267,13 @@ def _main_field(s: Survey):
|
|
|
261
267
|
"inclination": round(f.inclination, 3), "declination": round(f.declination, 3)}
|
|
262
268
|
|
|
263
269
|
|
|
264
|
-
def
|
|
270
|
+
def _gradient(t: Target, x, chi, amb, s: Survey, baseline: float):
|
|
271
|
+
"""Gradiometer reading: total-field anomaly at the lower sensor minus that at the upper sensor, in nT."""
|
|
272
|
+
low, high = replace(s, sensor_height=s.gradiometer_height), replace(s, sensor_height=s.gradiometer_height + baseline)
|
|
273
|
+
return _magnetic(t, x, chi, amb, low) - _magnetic(t, x, chi, amb, high)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def evaluate(target, survey=None, methods=DEFAULT_METHODS) -> Report:
|
|
265
277
|
"""Model ``target`` (a ``Target`` or dict) for each method and assess it for ``survey`` (a ``Survey`` or dict)."""
|
|
266
278
|
t, s = _as(Target, target), _as(Survey, survey)
|
|
267
279
|
t.validate()
|
|
@@ -295,18 +307,36 @@ def evaluate(target, survey=None, methods=METHODS) -> Report:
|
|
|
295
307
|
results.append(MethodResult(method, False, "needs the survey location (latitude, longitude) for the "
|
|
296
308
|
"main field, or field_total/inclination/declination"))
|
|
297
309
|
continue
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
310
|
+
if method == "magnetic":
|
|
311
|
+
inst = s.magnetic_instrument_noise if s.magnetic_instrument_noise is not None else \
|
|
312
|
+
detect.MAGNETOMETERS_NT.get(s.magnetometer)
|
|
313
|
+
if inst is None:
|
|
314
|
+
results.append(MethodResult(method, False, f"unknown magnetometer {s.magnetometer!r}"))
|
|
315
|
+
continue
|
|
316
|
+
noise = detect.effective_noise(inst, s.magnetic_field_noise)
|
|
317
|
+
anomaly = _magnetic(t, x, contrast, amb, s)
|
|
318
|
+
res = MethodResult(method, True, contrast=contrast, contrast_units=units, instrument=s.magnetometer,
|
|
319
|
+
instrument_noise=inst, field_noise=s.magnetic_field_noise, main_field=info)
|
|
320
|
+
else:
|
|
321
|
+
preset = detect.INSTRUMENTS.get(s.gradiometer)
|
|
322
|
+
inst = s.gradient_instrument_noise if s.gradient_instrument_noise is not None else \
|
|
323
|
+
(preset.noise if preset and preset.method == "gradient" else None)
|
|
324
|
+
baseline = s.gradiometer_baseline if s.gradiometer_baseline is not None else \
|
|
325
|
+
(preset.baseline if preset and preset.method == "gradient" else None)
|
|
326
|
+
if inst is None or baseline is None:
|
|
327
|
+
results.append(MethodResult(method, False, f"unknown gradiometer {s.gradiometer!r} (or give "
|
|
328
|
+
"gradient_instrument_noise and gradiometer_baseline)"))
|
|
329
|
+
continue
|
|
330
|
+
if baseline <= 0 or s.gradiometer_height < 0:
|
|
331
|
+
raise ValueError("gradiometer_baseline must be positive and gradiometer_height zero or positive")
|
|
332
|
+
noise = detect.effective_noise(inst, s.gradient_field_noise)
|
|
333
|
+
anomaly = _gradient(t, x, contrast, amb, s, baseline)
|
|
334
|
+
res = MethodResult(method, True, contrast=contrast, contrast_units=units, instrument=s.gradiometer,
|
|
335
|
+
instrument_noise=inst, field_noise=s.gradient_field_noise, main_field=info)
|
|
307
336
|
a = detect.assess(x, anomaly, noise, depth=t.depth_top, method=method)
|
|
308
337
|
preset = detect.INSTRUMENTS.get(res.instrument)
|
|
309
|
-
overridden =
|
|
338
|
+
overridden = {"gravity": s.gravity_instrument_noise, "magnetic": s.magnetic_instrument_noise,
|
|
339
|
+
"gradient": s.gradient_instrument_noise}[method] is not None
|
|
310
340
|
if preset and not overridden:
|
|
311
341
|
a.assumptions.append(f"{res.instrument}: {preset.noise:g} {a.units} per reading ({preset.basis}; "
|
|
312
342
|
f"{preset.reference} data sheet).")
|
|
@@ -315,6 +345,11 @@ def evaluate(target, survey=None, methods=METHODS) -> Report:
|
|
|
315
345
|
if method == "magnetic":
|
|
316
346
|
a.assumptions.append(f"Sensor {s.sensor_height:g} m above ground; profile azimuth {s.profile_azimuth:g}°; "
|
|
317
347
|
"induced magnetisation only, no demagnetisation.")
|
|
348
|
+
if method == "gradient":
|
|
349
|
+
top = s.gradiometer_height + baseline
|
|
350
|
+
a.assumptions.append(f"Reading: lower sensor ({s.gradiometer_height:g} m above ground) minus upper sensor "
|
|
351
|
+
f"({top:g} m), in nT over the {baseline:g} m baseline; diurnal variation and the regional "
|
|
352
|
+
f"field cancel. Profile azimuth {s.profile_azimuth:g}°; induced magnetisation only.")
|
|
318
353
|
res.x, res.anomaly, res.assessment = x.round(4).tolist(), anomaly.tolist(), a
|
|
319
354
|
results.append(res)
|
|
320
355
|
|
|
@@ -240,6 +240,24 @@ def cases() -> list[Case]:
|
|
|
240
240
|
gravity.dyke(x, 3, 15, 4, 500.0, centre=7) * SI_TO_UGAL,
|
|
241
241
|
gravity.polygon(x, [(5, 3), (9, 3), (9, 15), (5, 15)], 500.0) * SI_TO_UGAL))
|
|
242
242
|
|
|
243
|
+
# gradiometer reading of a sphere at the pole: closed-form dipole field at the two sensor heights
|
|
244
|
+
x = np.linspace(-20, 20, 81)
|
|
245
|
+
depth, radius, low, base = 3.0, 0.5, 0.2, 1.0
|
|
246
|
+
m = CHI * POLE.total * 1e-9 / MU0 * 4 / 3 * np.pi * radius**3
|
|
247
|
+
|
|
248
|
+
def _pole_dipole(h):
|
|
249
|
+
z = depth + h # depth of the centre below the sensor
|
|
250
|
+
return MU0 / (4 * np.pi) * m * (2 * z**2 - x**2) / (x**2 + z**2) ** 2.5 * 1e9
|
|
251
|
+
|
|
252
|
+
from .survey import _gradient, Survey, Target
|
|
253
|
+
sphere_target = Target("sphere", depth - radius, 2 * radius)
|
|
254
|
+
reading = _gradient(sphere_target, x, CHI, POLE, Survey(gradiometer_height=low), base)
|
|
255
|
+
out.append(_profile_case(
|
|
256
|
+
"gradient-sphere-pole", "Independent formula", "Sphere, magnetic gradient (gradiometer reading)",
|
|
257
|
+
"Total-field model at the lower minus the upper sensor", "Closed-form vertical-dipole field at both sensors",
|
|
258
|
+
"Radius 0.5 m, centre at 3 m, sensors 0.2 and 1.2 m above ground, at the pole", "nT", 1e-10, x, reading,
|
|
259
|
+
_pole_dipole(low) - _pole_dipole(low + base)))
|
|
260
|
+
|
|
243
261
|
# --- numerical integration -------------------------------------------------------------------------------
|
|
244
262
|
x = np.linspace(-40, 40, 81)
|
|
245
263
|
box = ((-3.0, 3.0), (-4.0, 4.0), (2.0, 6.0))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectability
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pre-survey detectability, modelled and reproducible: forward-modelled gravity and magnetic responses, station spacing and a detectability verdict for near-surface targets.
|
|
5
5
|
Author: Mükremin Nergiz
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -9,6 +9,7 @@ Project-URL: Documentation, https://detectability.mukreminnergiz.com/method
|
|
|
9
9
|
Project-URL: Validation, https://detectability.mukreminnergiz.com/validation
|
|
10
10
|
Project-URL: Repository, https://github.com/mukremin-nergiz/detectability
|
|
11
11
|
Project-URL: Issues, https://github.com/mukremin-nergiz/detectability/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md
|
|
12
13
|
Project-URL: Citation, https://doi.org/10.5281/zenodo.23931815
|
|
13
14
|
Keywords: geophysics,gravity,magnetics,survey design,near-surface,forward modelling,IGRF
|
|
14
15
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -68,7 +69,7 @@ print(report.summary())
|
|
|
68
69
|
```
|
|
69
70
|
|
|
70
71
|
```text
|
|
71
|
-
Detectability 0.
|
|
72
|
+
Detectability 0.3.0: sphere in limestone
|
|
72
73
|
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
73
74
|
stations every 5 m or less, profile at least 153 m long
|
|
74
75
|
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
@@ -82,16 +83,24 @@ Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `d
|
|
|
82
83
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
83
84
|
in, and measured contrasts can be given instead.
|
|
84
85
|
|
|
86
|
+
Methods: `gravity`, `magnetic` (total-field anomaly) and, on request, `gradient`: the reading of a vertical gradiometer
|
|
87
|
+
(lower minus upper sensor, in nT over the baseline), with presets for the Bartington Grad601, Sensys FGM650, Foerster
|
|
88
|
+
FEREX 4.034 and GEM GSM-19G. The defaults are `gravity` and `magnetic`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
evaluate(target, survey, methods=["gravity", "magnetic", "gradient"])
|
|
92
|
+
```
|
|
93
|
+
|
|
85
94
|
## Modules
|
|
86
95
|
|
|
87
96
|
| Module | Contents |
|
|
88
97
|
|---|---|
|
|
89
98
|
| `survey` | `evaluate()`: target and survey in, report and reproduction script out |
|
|
90
99
|
| `gravity` | sphere, horizontal cylinder, right rectangular prism (Nagy; Plouff) and its gradient tensor, vertical cylinder (exact, by a rim integral) and its gradient tensor, vertical step, dyke, 2D polygon (Talwani; Won & Bevis) |
|
|
91
|
-
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation |
|
|
100
|
+
| `magnetics` | total-field anomaly: sphere, horizontal cylinder, prism, vertical cylinder, 2D polygon, dyke (any dip), vertical contact; induced and remanent magnetisation (a gradiometer reading is the difference at two sensor heights) |
|
|
92
101
|
| `igrf` | IGRF-14 main field from location and date, from the official IAGA coefficients |
|
|
93
102
|
| `rocks` | typical densities and magnetic susceptibilities with their ranges (Telford et al. 1990) |
|
|
94
|
-
| `detect` | instrument
|
|
103
|
+
| `detect` | instrument presets with their sources (gravimeters, magnetometers, gradiometers), field noise, signal-to-noise ratio, verdict, station spacing, profile length |
|
|
95
104
|
|
|
96
105
|
## Validation
|
|
97
106
|
|
|
@@ -117,6 +126,7 @@ python tools/harmonica_reference.py --check
|
|
|
117
126
|
| Analytical limit | Vertical cylinder, gravity, on its axis | Exact on-axis cylinder formula | 1.7e-13 % | 1.0e-10 % |
|
|
118
127
|
| Independent formula | Horizontal cylinder, gravity | 2D polygon, 720 sides (Talwani; Won & Bevis) | 6.3e-12 % | 0.01 % |
|
|
119
128
|
| Independent formula | Vertical dyke, gravity | 2D polygon (Won & Bevis) | 1.9e-13 % | 1.0e-06 % |
|
|
129
|
+
| Independent formula | Sphere, magnetic gradient (gradiometer reading) | Closed-form vertical-dipole field at both sensors | 7.8e-14 % | 1.0e-10 % |
|
|
120
130
|
| Numerical integration | Prism, gravity | 16³ Gauss–Legendre point masses | 2.9e-06 % | 0.001 % |
|
|
121
131
|
| Numerical integration | Horizontal cylinder, magnetic | 3D dipoles integrated along strike | 2.2e-07 % | 0.01 % |
|
|
122
132
|
| Numerical integration | Prism, magnetic | 16³ Gauss–Legendre point dipoles | 1.3e-12 % | 0.001 % |
|
|
@@ -147,16 +157,18 @@ pytest
|
|
|
147
157
|
Flat topography, a homogeneous host and no regional trend; 2D targets are infinitely long along strike; the profile
|
|
148
158
|
crosses the centre of the target; self-demagnetisation is neglected. Results are expectations under these assumptions,
|
|
149
159
|
not guarantees. Instrument presets come from manufacturers' data sheets (`detect.INSTRUMENTS` lists each figure and its
|
|
150
|
-
source); field noise, which usually dominates, is a stated assumption that you can change.
|
|
160
|
+
source); field noise, which usually dominates, is a stated assumption that you can change. For gradiometers the default
|
|
161
|
+
field noise (0.5 nT) is an assumption, not a published figure: soil, heading and walking noise depend on the site.
|
|
151
162
|
|
|
152
163
|
## Citation
|
|
153
164
|
|
|
154
165
|
See [CITATION.cff](https://github.com/mukremin-nergiz/detectability/blob/main/CITATION.cff), or:
|
|
155
166
|
|
|
156
|
-
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.
|
|
167
|
+
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.3.0) [Computer software].
|
|
157
168
|
> Zenodo. https://doi.org/10.5281/zenodo.23931815
|
|
158
169
|
|
|
159
|
-
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo.
|
|
170
|
+
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo. Changes between
|
|
171
|
+
releases: [CHANGELOG.md](https://github.com/mukremin-nergiz/detectability/blob/main/CHANGELOG.md).
|
|
160
172
|
|
|
161
173
|
## Author
|
|
162
174
|
|
|
@@ -66,8 +66,17 @@ def test_void_in_limestone_magnetic_example():
|
|
|
66
66
|
def test_instrument_presets_have_sources():
|
|
67
67
|
assert detect.INSTRUMENTS
|
|
68
68
|
for name, inst in detect.INSTRUMENTS.items():
|
|
69
|
-
assert inst.method in ("gravity", "magnetic"), name
|
|
69
|
+
assert inst.method in ("gravity", "magnetic", "gradient"), name
|
|
70
70
|
assert inst.noise > 0 and inst.source.startswith("https://") and inst.reference, name
|
|
71
71
|
assert detect.GRAVIMETERS_UGAL["Scintrex CG-6"] == 5.0
|
|
72
72
|
assert detect.MAGNETOMETERS_NT["Overhauser"] == 0.022
|
|
73
|
-
assert "
|
|
73
|
+
assert "Bartington Grad601" not in detect.MAGNETOMETERS_NT # a gradiometer reading, not the total field
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_gradiometer_presets_have_a_baseline_and_a_source():
|
|
77
|
+
grads = {k: v for k, v in detect.INSTRUMENTS.items() if v.method == "gradient"}
|
|
78
|
+
assert len(grads) >= 3 and set(grads) == set(detect.GRADIOMETERS_NT)
|
|
79
|
+
for name, inst in grads.items():
|
|
80
|
+
assert inst.baseline and 0.3 <= inst.baseline <= 1.5, name
|
|
81
|
+
assert inst.source.startswith("https://") and inst.noise > 0, name
|
|
82
|
+
assert all(v.baseline is None for v in detect.INSTRUMENTS.values() if v.method != "gradient")
|
|
@@ -75,3 +75,43 @@ def test_dataclass_inputs_are_not_modified():
|
|
|
75
75
|
s = Survey(latitude=39.75, longitude=37.02)
|
|
76
76
|
evaluate(Target(**VOID), s)
|
|
77
77
|
assert s.date is None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
SITE = {"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}
|
|
81
|
+
SHALLOW_DYKE = {"shape": "dyke", "depth_top": 0.4, "depth_bottom": 1.2, "size": 0.8, "material": "basalt",
|
|
82
|
+
"host": "sandstone"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_default_methods_unchanged_and_gradient_on_request():
|
|
86
|
+
from detectability import DEFAULT_METHODS, METHODS
|
|
87
|
+
assert DEFAULT_METHODS == ("gravity", "magnetic") and "gradient" in METHODS
|
|
88
|
+
assert [r.method for r in evaluate(SHALLOW_DYKE, SITE).results] == ["gravity", "magnetic"]
|
|
89
|
+
r = evaluate(SHALLOW_DYKE, SITE, ["gradient"]).results[0]
|
|
90
|
+
assert r.available and r.instrument == "Bartington Grad601" and r.assessment.units == "nT"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_gradient_is_the_difference_of_two_sensor_heights():
|
|
94
|
+
import numpy as np
|
|
95
|
+
low = evaluate(SHALLOW_DYKE, {**SITE, "sensor_height": 0.3}, ["magnetic"]).results[0]
|
|
96
|
+
high = evaluate(SHALLOW_DYKE, {**SITE, "sensor_height": 0.95}, ["magnetic"]).results[0]
|
|
97
|
+
g = evaluate(SHALLOW_DYKE, {**SITE, "gradiometer": "Sensys FGM650", "gradiometer_height": 0.3}, ["gradient"])
|
|
98
|
+
assert np.allclose(g.results[0].anomaly, np.subtract(low.anomaly, high.anomaly), rtol=0, atol=1e-12)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_gradient_favours_shallow_targets():
|
|
102
|
+
deep = {"shape": "sphere", "depth_top": 15, "size": 20, "material": "air", "host": "limestone"}
|
|
103
|
+
shallow = evaluate(SHALLOW_DYKE, SITE, ["magnetic", "gradient"])
|
|
104
|
+
assert shallow.recommended == "gradient"
|
|
105
|
+
far = evaluate(deep, SITE, ["magnetic", "gradient"]).results
|
|
106
|
+
assert far[1].assessment.snr < far[0].assessment.snr
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_custom_gradiometer_and_errors():
|
|
110
|
+
import pytest
|
|
111
|
+
r = evaluate(SHALLOW_DYKE, {**SITE, "gradiometer": "home-made", "gradient_instrument_noise": 0.05,
|
|
112
|
+
"gradiometer_baseline": 0.5}, ["gradient"]).results[0]
|
|
113
|
+
assert r.available and r.instrument_noise == 0.05
|
|
114
|
+
assert not evaluate(SHALLOW_DYKE, {**SITE, "gradiometer": "home-made"}, ["gradient"]).results[0].available
|
|
115
|
+
assert not evaluate(SHALLOW_DYKE, {}, ["gradient"]).results[0].available # needs the location
|
|
116
|
+
with pytest.raises(ValueError):
|
|
117
|
+
evaluate(SHALLOW_DYKE, {**SITE, "gradiometer_baseline": 0}, ["gradient"])
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
"""Detectability: pre-survey detectability, modelled and reproducible."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.2.1"
|
|
4
|
-
|
|
5
|
-
from .survey import METHODS, SHAPES, Report, Survey, Target, evaluate # noqa: E402
|
|
6
|
-
|
|
7
|
-
__all__ = ["evaluate", "Target", "Survey", "Report", "SHAPES", "METHODS", "__version__"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|