detectability 0.2.1__py3-none-any.whl
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/__init__.py +7 -0
- detectability/constants.py +6 -0
- detectability/data/harmonica_reference.json +589 -0
- detectability/data/igrf14coeffs.txt +199 -0
- detectability/detect.py +149 -0
- detectability/gravity.py +266 -0
- detectability/igrf.py +163 -0
- detectability/magnetics.py +199 -0
- detectability/rocks.py +68 -0
- detectability/survey.py +323 -0
- detectability/validation.py +404 -0
- detectability-0.2.1.dist-info/METADATA +166 -0
- detectability-0.2.1.dist-info/RECORD +16 -0
- detectability-0.2.1.dist-info/WHEEL +5 -0
- detectability-0.2.1.dist-info/licenses/LICENSE +28 -0
- detectability-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Validation: every model against a result computed in a different way.
|
|
2
|
+
|
|
3
|
+
The same cases run as tests (each must stay within its tolerance), feed the website's Validation page, and print as a
|
|
4
|
+
Markdown table:
|
|
5
|
+
|
|
6
|
+
python -m detectability.validation
|
|
7
|
+
|
|
8
|
+
Errors are the largest absolute difference along the profile as a percentage of the largest reference value (or, for a
|
|
9
|
+
single value, the relative difference).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import datetime as _dt
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from importlib import resources
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from . import gravity, igrf, magnetics
|
|
22
|
+
from .constants import G, SI_TO_UGAL
|
|
23
|
+
from .magnetics import MU0, Ambient
|
|
24
|
+
|
|
25
|
+
MID = Ambient(48567.0, 58.373, 6.313) # Sivas, Türkiye, 2026 (IGRF-14)
|
|
26
|
+
POLE = Ambient(50000.0, 90.0, 0.0) # vertical field and magnetisation
|
|
27
|
+
CHI = 0.01
|
|
28
|
+
|
|
29
|
+
# British Geological Survey IGRF-14 web service (geomag.bgs.ac.uk), retrieved 3 Nov 2026; rounded to 1 nT and 0.001°.
|
|
30
|
+
BGS = [
|
|
31
|
+
("Sivas, Türkiye", 39.75, 37.02, 0.0, "2026-07-01", 48567, 6.313, 58.373),
|
|
32
|
+
("Cape Town, South Africa", -33.9, 18.4, 0.0, "2026-07-01", 24996, -26.673, -64.631),
|
|
33
|
+
("Canadian Arctic", 70.0, -100.0, 0.0, "2026-07-01", 57831, -0.041, 85.147),
|
|
34
|
+
("Amazon, 5 km altitude", 0.0, -60.0, 5.0, "2026-07-01", 26671, -16.84, 8.983),
|
|
35
|
+
("London, United Kingdom", 51.5, -0.1, 0.0, "2012-03-15", 48635, -1.345, 66.469),
|
|
36
|
+
]
|
|
37
|
+
IGRF_TOLERANCE_NT, IGRF_TOLERANCE_DEG = 1.0, 0.002
|
|
38
|
+
|
|
39
|
+
# Independent code: the same targets computed with Harmonica (Fatiando a Terra Project), a separate open-source package.
|
|
40
|
+
# tools/harmonica_reference.py computes them and stores data/harmonica_reference.json, so neither the library nor the
|
|
41
|
+
# website has to install Harmonica; CI installs it, recomputes the file and checks it. Profiles run north (azimuth 0),
|
|
42
|
+
# so the profile frame (along, across, down) is (north, east, down). A prism 200 km long stands in for a 2D dyke.
|
|
43
|
+
HARMONICA_FILE = "harmonica_reference.json"
|
|
44
|
+
_FIELD = (MID.total, MID.inclination, MID.declination)
|
|
45
|
+
HARMONICA_CASES = {
|
|
46
|
+
"harmonica-prism-gravity": {"source": "prism", "along": (-3.0, 3.0), "across": (-4.0, 4.0), "depth": (2.0, 6.0),
|
|
47
|
+
"drho": -2200.0, "x": (-40.0, 40.0, 81)},
|
|
48
|
+
"harmonica-prism-magnetic": {"source": "prism", "along": (-3.0, 2.0), "across": (-2.5, 3.5), "depth": (4.0, 7.0),
|
|
49
|
+
"chi": CHI, "field": _FIELD, "height": 0.5, "x": (-30.0, 30.0, 61)},
|
|
50
|
+
"harmonica-sphere-gravity": {"source": "sphere", "depth": 12.0, "radius": 3.0, "drho": -2600.0,
|
|
51
|
+
"x": (-60.0, 60.0, 121)},
|
|
52
|
+
"harmonica-sphere-magnetic": {"source": "sphere", "depth": 12.0, "radius": 3.0, "chi": CHI, "field": _FIELD,
|
|
53
|
+
"height": 0.5, "x": (-60.0, 60.0, 121)},
|
|
54
|
+
"harmonica-dyke-gravity": {"source": "prism", "along": (5.0, 9.0), "across": (-1e5, 1e5), "depth": (3.0, 15.0),
|
|
55
|
+
"drho": 500.0, "x": (-40.0, 40.0, 81)},
|
|
56
|
+
"harmonica-dyke-magnetic": {"source": "prism", "along": (-2.0, 2.0), "across": (-1e5, 1e5), "depth": (3.0, 20.0),
|
|
57
|
+
"chi": CHI, "field": _FIELD, "height": 0.5, "x": (-50.0, 50.0, 101)},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Case:
|
|
63
|
+
key: str
|
|
64
|
+
check: str # kind of check
|
|
65
|
+
geometry: str # target and method
|
|
66
|
+
model: str # how Detectability computes it
|
|
67
|
+
reference: str # the independent result
|
|
68
|
+
setting: str # the example used
|
|
69
|
+
units: str
|
|
70
|
+
tolerance: float # %: the test fails above this
|
|
71
|
+
error: float # %
|
|
72
|
+
x: np.ndarray | None = None
|
|
73
|
+
model_values: np.ndarray | None = None
|
|
74
|
+
reference_values: np.ndarray | None = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def passed(self) -> bool:
|
|
78
|
+
return self.error <= self.tolerance
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _profile_error(model, ref) -> float:
|
|
82
|
+
model, ref = np.asarray(model, float), np.asarray(ref, float)
|
|
83
|
+
return float(np.max(np.abs(model - ref)) / np.max(np.abs(ref)) * 100.0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _value_error(model: float, ref: float) -> float:
|
|
87
|
+
return float(abs(model - ref) / abs(ref) * 100.0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _point_masses(x, box, drho, n=16):
|
|
91
|
+
"""Gauss-Legendre sum of point masses filling a box ((x1, x2), (y1, y2), (z1, z2)); profile along y = 0."""
|
|
92
|
+
nodes, weights = np.polynomial.legendre.leggauss(n)
|
|
93
|
+
pts = [((hi - lo) / 2 * nodes + (hi + lo) / 2, (hi - lo) / 2 * weights) for lo, hi in box]
|
|
94
|
+
X, Y, Z = np.meshgrid(pts[0][0], pts[1][0], pts[2][0], indexing="ij")
|
|
95
|
+
Wt = np.einsum("i,j,k->ijk", pts[0][1], pts[1][1], pts[2][1])
|
|
96
|
+
return np.array([np.sum(G * drho * Wt * Z / ((X - xi) ** 2 + Y**2 + Z**2) ** 1.5) for xi in x])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _point_dipoles(x, box, chi, ambient, azimuth, height=0.0, n=16):
|
|
100
|
+
"""Gauss-Legendre sum of point dipoles filling a box given in profile coordinates (along, across, depth)."""
|
|
101
|
+
nodes, weights = np.polynomial.legendre.leggauss(n)
|
|
102
|
+
pts = [((hi - lo) / 2 * nodes + (hi + lo) / 2, (hi - lo) / 2 * weights) for lo, hi in box]
|
|
103
|
+
az = np.radians(azimuth)
|
|
104
|
+
along, across = np.array([np.cos(az), np.sin(az), 0.0]), np.array([-np.sin(az), np.cos(az), 0.0])
|
|
105
|
+
m = magnetics.magnetisation(chi, ambient)
|
|
106
|
+
f = magnetics.unit_vector(ambient.inclination, ambient.declination)
|
|
107
|
+
A, C, Z = np.meshgrid(pts[0][0], pts[1][0], pts[2][0], indexing="ij")
|
|
108
|
+
Wt = np.einsum("i,j,k->ijk", pts[0][1], pts[1][1], pts[2][1]).ravel()
|
|
109
|
+
src = A.ravel()[:, None] * along + C.ravel()[:, None] * across + Z.ravel()[:, None] * np.array([0, 0, 1.0])
|
|
110
|
+
out = []
|
|
111
|
+
for xi in x:
|
|
112
|
+
r = xi * along - np.array([0, 0, height]) - src
|
|
113
|
+
dist = np.linalg.norm(r, axis=1, keepdims=True)
|
|
114
|
+
rhat = r / dist
|
|
115
|
+
b = MU0 / (4 * np.pi) * (3 * (rhat @ m)[:, None] * rhat - m) / dist**3
|
|
116
|
+
out.append(np.sum((b @ f) * Wt) * 1e9)
|
|
117
|
+
return np.array(out)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _dipoles_along_strike(x, z, radius, chi, ambient, azimuth, n=20001):
|
|
121
|
+
m = magnetics.magnetisation(chi, ambient) * np.pi * radius**2
|
|
122
|
+
f = magnetics.unit_vector(ambient.inclination, ambient.declination)
|
|
123
|
+
a = np.radians(azimuth)
|
|
124
|
+
along, strike = np.array([np.cos(a), np.sin(a), 0.0]), np.array([-np.sin(a), np.cos(a), 0.0])
|
|
125
|
+
theta = np.linspace(-np.pi / 2, np.pi / 2, n)[1:-1]
|
|
126
|
+
s, ds = z * np.tan(theta), z / np.cos(theta) ** 2 * (theta[1] - theta[0])
|
|
127
|
+
out = []
|
|
128
|
+
for xi in x:
|
|
129
|
+
r = xi * along - s[:, None] * strike - np.array([0.0, 0.0, z])
|
|
130
|
+
dist = np.linalg.norm(r, axis=1, keepdims=True)
|
|
131
|
+
rhat = r / dist
|
|
132
|
+
b = MU0 / (4 * np.pi) * (3 * (rhat @ m)[:, None] * rhat - m) / dist**3
|
|
133
|
+
out.append(np.sum((b @ f) * ds) * 1e9)
|
|
134
|
+
return np.array(out)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _disc_line_masses(x, radius, z1, z2, drho, n=200):
|
|
138
|
+
"""Vertical cylinder by Gauss-Legendre over its cross-section in polar coordinates about the axis, each node a
|
|
139
|
+
vertical line mass in closed form; profile along y = 0. Independent of the rim integral used by the model."""
|
|
140
|
+
r, wr = np.polynomial.legendre.leggauss(n)
|
|
141
|
+
r, wr = radius / 2 * (r + 1), radius / 2 * wr
|
|
142
|
+
t, wt = np.polynomial.legendre.leggauss(2 * n)
|
|
143
|
+
t, wt = np.pi * (t + 1), np.pi * wt
|
|
144
|
+
rr, tt = np.meshgrid(r, t, indexing="ij")
|
|
145
|
+
w = np.outer(wr, wt) * rr
|
|
146
|
+
out = []
|
|
147
|
+
for xi in x:
|
|
148
|
+
s2 = (xi - rr * np.cos(tt)) ** 2 + (rr * np.sin(tt)) ** 2
|
|
149
|
+
out.append(np.sum(w * (1 / np.sqrt(s2 + z1**2) - 1 / np.sqrt(s2 + z2**2))))
|
|
150
|
+
return G * drho * np.array(out)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cylinder_dipoles(x, radius, z1, z2, chi, ambient, azimuth, height=0.0, n=32):
|
|
154
|
+
"""Vertical cylinder filled with Gauss-Legendre point dipoles (polar about the axis, and depth)."""
|
|
155
|
+
r, wr = np.polynomial.legendre.leggauss(n)
|
|
156
|
+
r, wr = radius / 2 * (r + 1), radius / 2 * wr
|
|
157
|
+
t, wt = np.polynomial.legendre.leggauss(2 * n)
|
|
158
|
+
t, wt = np.pi * (t + 1), np.pi * wt
|
|
159
|
+
z, wz = np.polynomial.legendre.leggauss(n)
|
|
160
|
+
z, wz = (z2 - z1) / 2 * z + (z1 + z2) / 2, (z2 - z1) / 2 * wz
|
|
161
|
+
rr, tt, zz = np.meshgrid(r, t, z, indexing="ij")
|
|
162
|
+
w = (np.einsum("i,j,k->ijk", wr, wt, wz) * rr).ravel()
|
|
163
|
+
a = np.radians(azimuth)
|
|
164
|
+
along, across = np.array([np.cos(a), np.sin(a), 0.0]), np.array([-np.sin(a), np.cos(a), 0.0])
|
|
165
|
+
src = ((rr * np.cos(tt)).ravel()[:, None] * along + (rr * np.sin(tt)).ravel()[:, None] * across
|
|
166
|
+
+ zz.ravel()[:, None] * np.array([0.0, 0.0, 1.0]))
|
|
167
|
+
m = magnetics.magnetisation(chi, ambient)
|
|
168
|
+
f = magnetics.unit_vector(ambient.inclination, ambient.declination)
|
|
169
|
+
out = []
|
|
170
|
+
for xi in x:
|
|
171
|
+
rel = xi * along - np.array([0.0, 0.0, height]) - src
|
|
172
|
+
dist = np.linalg.norm(rel, axis=1, keepdims=True)
|
|
173
|
+
rhat = rel / dist
|
|
174
|
+
b = MU0 / (4 * np.pi) * (3 * (rhat @ m)[:, None] * rhat - m) / dist**3
|
|
175
|
+
out.append(np.sum((b @ f) * w) * 1e9)
|
|
176
|
+
return np.array(out)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _poisson(dgz_dz, chi, ambient, drho):
|
|
180
|
+
"""Poisson's relation for vertical field and magnetisation: dT = mu0 M / (4 pi G drho) * dgz/dz, in nT."""
|
|
181
|
+
return MU0 * (chi * ambient.total * 1e-9 / MU0) / (4 * np.pi * G * drho) * dgz_dz * 1e9
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _dgz_dz(fn, top, bottom, d=1e-4):
|
|
185
|
+
"""Vertical derivative of a gravity profile: moving the sensor down equals moving the body up."""
|
|
186
|
+
return (fn(top - d, bottom - d) - fn(top + d, bottom + d)) / (2 * d)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _profile_case(key, check, geometry, model, reference, setting, units, tolerance, x, mv, rv) -> Case:
|
|
190
|
+
return Case(key, check, geometry, model, reference, setting, units, tolerance, _profile_error(mv, rv),
|
|
191
|
+
np.asarray(x, float), np.asarray(mv, float), np.asarray(rv, float))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cases() -> list[Case]:
|
|
195
|
+
out: list[Case] = []
|
|
196
|
+
|
|
197
|
+
# --- analytical limits -----------------------------------------------------------------------------------
|
|
198
|
+
x = np.linspace(-300, 300, 121)
|
|
199
|
+
a, z = 2.0, 100.0
|
|
200
|
+
mass = (2 * a) ** 3 * -2600.0
|
|
201
|
+
out.append(_profile_case(
|
|
202
|
+
"prism-point-mass", "Analytical limit", "Prism, gravity, far from the sensor", "Nagy (1966) closed form",
|
|
203
|
+
"Point mass of the same mass", "4 m cube, centre at 100 m, −2,600 kg/m³", "µGal", 0.01, x,
|
|
204
|
+
gravity.prism(x, 0.0, -a, a, -a, a, z - a, z + a, -2600.0) * SI_TO_UGAL,
|
|
205
|
+
G * mass * z / (x**2 + z**2) ** 1.5 * SI_TO_UGAL))
|
|
206
|
+
|
|
207
|
+
t, L = 2.0, 1e5
|
|
208
|
+
out.append(Case("prism-slab", "Analytical limit", "Prism, gravity, wide and thin", "Nagy (1966) closed form",
|
|
209
|
+
"Infinite Bouguer slab, 2πGΔρt", "200 km wide, 2 m thick, top at 10 m, +1,000 kg/m³", "µGal", 0.1,
|
|
210
|
+
_value_error(gravity.prism(0.0, 0.0, -L, L, -L, L, 10.0, 10.0 + t, 1000.0),
|
|
211
|
+
2 * np.pi * G * 1000.0 * t)))
|
|
212
|
+
|
|
213
|
+
out.append(Case("step-slab", "Analytical limit", "Vertical step, gravity, far over the slab", "Faulted-slab closed form",
|
|
214
|
+
"Infinite Bouguer slab, 2πGΔρt", "Slab from 5 to 25 m, +300 kg/m³, 1,000 km from the edge", "µGal",
|
|
215
|
+
0.01, _value_error(gravity.vertical_step(1e6, 5.0, 25.0, 300.0), 2 * np.pi * G * 300.0 * 20.0)))
|
|
216
|
+
|
|
217
|
+
top, bottom, R = 5.0, 30.0, 3.0
|
|
218
|
+
exact = 2 * np.pi * G * -2600.0 * ((bottom - top) - np.hypot(bottom, R) + np.hypot(top, R))
|
|
219
|
+
out.append(Case("vertical-cylinder-axis", "Analytical limit", "Vertical cylinder, gravity, on its axis",
|
|
220
|
+
"Rim integral (closed form in depth and distance)", "Exact on-axis cylinder formula",
|
|
221
|
+
"Shaft of 6 m diameter from 5 to 30 m, −2,600 kg/m³", "µGal", 1e-10,
|
|
222
|
+
_value_error(gravity.vertical_cylinder(0.0, top, bottom, R, -2600.0), exact)))
|
|
223
|
+
|
|
224
|
+
# --- independent closed forms ----------------------------------------------------------------------------
|
|
225
|
+
x = np.linspace(-60, 60, 121)
|
|
226
|
+
z, R, n = 8.0, 2.0, 720
|
|
227
|
+
Rn = R * np.sqrt(2 * np.pi / (n * np.sin(2 * np.pi / n)))
|
|
228
|
+
ang = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
229
|
+
circle = np.column_stack([Rn * np.cos(ang), z + Rn * np.sin(ang)])
|
|
230
|
+
out.append(_profile_case(
|
|
231
|
+
"cylinder-polygon", "Independent formula", "Horizontal cylinder, gravity", "Line-mass closed form",
|
|
232
|
+
"2D polygon, 720 sides (Talwani; Won & Bevis)", "Tunnel of 4 m diameter, axis at 8 m, −2,600 kg/m³", "µGal",
|
|
233
|
+
0.01, x, gravity.horizontal_cylinder(x, z, R, -2600.0) * SI_TO_UGAL,
|
|
234
|
+
gravity.polygon(x, circle, -2600.0) * SI_TO_UGAL))
|
|
235
|
+
|
|
236
|
+
x = np.linspace(-40, 40, 81)
|
|
237
|
+
out.append(_profile_case(
|
|
238
|
+
"dyke-polygon", "Independent formula", "Vertical dyke, gravity", "Difference of two vertical steps",
|
|
239
|
+
"2D polygon (Won & Bevis)", "4 m wide dyke from 3 to 15 m, +500 kg/m³", "µGal", 1e-6, x,
|
|
240
|
+
gravity.dyke(x, 3, 15, 4, 500.0, centre=7) * SI_TO_UGAL,
|
|
241
|
+
gravity.polygon(x, [(5, 3), (9, 3), (9, 15), (5, 15)], 500.0) * SI_TO_UGAL))
|
|
242
|
+
|
|
243
|
+
# --- numerical integration -------------------------------------------------------------------------------
|
|
244
|
+
x = np.linspace(-40, 40, 81)
|
|
245
|
+
box = ((-3.0, 3.0), (-4.0, 4.0), (2.0, 6.0))
|
|
246
|
+
out.append(_profile_case(
|
|
247
|
+
"prism-gravity-quadrature", "Numerical integration", "Prism, gravity", "Nagy (1966) closed form",
|
|
248
|
+
"16³ Gauss–Legendre point masses", "6 × 8 m room from 2 to 6 m, −2,200 kg/m³", "µGal", 0.001, x,
|
|
249
|
+
gravity.prism(x, 0.0, *box[0], *box[1], *box[2], -2200.0) * SI_TO_UGAL,
|
|
250
|
+
_point_masses(x, box, -2200.0) * SI_TO_UGAL))
|
|
251
|
+
|
|
252
|
+
x = np.linspace(-40, 40, 41)
|
|
253
|
+
out.append(_profile_case(
|
|
254
|
+
"cylinder-magnetic-strike", "Numerical integration", "Horizontal cylinder, magnetic", "2D line dipole",
|
|
255
|
+
"3D dipoles integrated along strike", "Pipe of 2 m diameter, axis at 6 m, 0.01 SI, main field of Sivas, "
|
|
256
|
+
"profile azimuth 30°", "nT", 0.01, x, magnetics.horizontal_cylinder(x, 6.0, 1.0, CHI, MID, azimuth=30.0),
|
|
257
|
+
_dipoles_along_strike(x, 6.0, 1.0, CHI, MID, 30.0)))
|
|
258
|
+
|
|
259
|
+
x = np.linspace(-30, 30, 61)
|
|
260
|
+
box = ((-3.0, 2.0), (-2.5, 3.5), (4.0, 7.0))
|
|
261
|
+
out.append(_profile_case(
|
|
262
|
+
"prism-magnetic-quadrature", "Numerical integration", "Prism, magnetic",
|
|
263
|
+
"Gradient tensor and Poisson's relation", "16³ Gauss–Legendre point dipoles",
|
|
264
|
+
"5 × 6 m block from 4 to 7 m, 0.01 SI, sensor 0.5 m above ground, azimuth 30°", "nT", 0.001, x,
|
|
265
|
+
magnetics.prism(x, *box[0], *box[1], *box[2], CHI, MID, azimuth=30.0, height=0.5),
|
|
266
|
+
_point_dipoles(x, box, CHI, MID, 30.0, height=0.5)))
|
|
267
|
+
|
|
268
|
+
x = np.linspace(-30, 30, 61)
|
|
269
|
+
out.append(_profile_case(
|
|
270
|
+
"vertical-cylinder-quadrature", "Numerical integration", "Vertical cylinder, gravity",
|
|
271
|
+
"Rim integral (closed form in depth and distance)", "Gauss–Legendre over the cross-section, line masses in depth",
|
|
272
|
+
"Shaft of 6 m diameter from 5 to 30 m, −2,600 kg/m³", "µGal", 1e-8, x,
|
|
273
|
+
gravity.vertical_cylinder(x, 5.0, 30.0, 3.0, -2600.0) * SI_TO_UGAL,
|
|
274
|
+
_disc_line_masses(x, 3.0, 5.0, 30.0, -2600.0) * SI_TO_UGAL))
|
|
275
|
+
|
|
276
|
+
out.append(_profile_case(
|
|
277
|
+
"vertical-cylinder-magnetic-quadrature", "Numerical integration", "Vertical cylinder, magnetic",
|
|
278
|
+
"Gradient tensor (rim integral) and Poisson's relation", "Gauss–Legendre point dipoles over the volume",
|
|
279
|
+
"Shaft of 6 m diameter from 5 to 30 m, 0.01 SI, sensor 0.5 m above ground, azimuth 30°", "nT", 1e-6, x,
|
|
280
|
+
magnetics.vertical_cylinder(x, 5.0, 30.0, 3.0, CHI, MID, azimuth=30.0, height=0.5),
|
|
281
|
+
_cylinder_dipoles(x, 3.0, 5.5, 30.5, CHI, MID, 30.0)))
|
|
282
|
+
|
|
283
|
+
# --- Poisson's relation: the magnetic codes against the independent gravity codes -------------------------
|
|
284
|
+
x = np.linspace(-60, 60, 121)
|
|
285
|
+
out.append(_profile_case(
|
|
286
|
+
"sphere-poisson", "Poisson's relation", "Sphere, gravity ↔ magnetic", "Magnetic dipole",
|
|
287
|
+
"Vertical derivative of the gravity sphere", "Radius 3 m, centre at 12 m, at the pole", "nT", 0.001, x,
|
|
288
|
+
magnetics.sphere(x, 12.0, 3.0, CHI, POLE),
|
|
289
|
+
_poisson(_dgz_dz(lambda a, b: gravity.sphere(x, a, 3.0, 1000.0), 12.0, 12.0) , CHI, POLE, 1000.0)))
|
|
290
|
+
|
|
291
|
+
x = np.linspace(-50, 50, 101)
|
|
292
|
+
for key, geometry, dip in (("dyke-poisson", "Vertical dyke, gravity ↔ magnetic", 90.0),
|
|
293
|
+
("dipping-dyke-poisson", "Dipping dyke (60°), gravity ↔ magnetic", 60.0)):
|
|
294
|
+
out.append(_profile_case(
|
|
295
|
+
key, "Poisson's relation", geometry, "Magnetic 2D polygon (surface charge)",
|
|
296
|
+
"Vertical derivative of the gravity dyke", "4 m wide, from 3 to 20 m, at the pole", "nT", 1e-4, x,
|
|
297
|
+
magnetics.dyke(x, 3.0, 20.0, 4.0, CHI, POLE, dip=dip),
|
|
298
|
+
_poisson(_dgz_dz(lambda a, b: gravity.dyke(x, a, b, 4.0, 1000.0, dip=dip), 3.0, 20.0), CHI, POLE, 1000.0)))
|
|
299
|
+
|
|
300
|
+
x = np.linspace(-40, 40, 81)
|
|
301
|
+
out.append(_profile_case(
|
|
302
|
+
"vertical-cylinder-poisson", "Poisson's relation", "Vertical cylinder, gravity ↔ magnetic",
|
|
303
|
+
"Gradient tensor (rim integral)", "Vertical derivative of the gravity cylinder",
|
|
304
|
+
"Shaft of 6 m diameter from 2 to 30 m, at the pole", "nT", 1e-4, x,
|
|
305
|
+
magnetics.vertical_cylinder(x, 2.0, 30.0, 3.0, CHI, POLE),
|
|
306
|
+
_poisson(_dgz_dz(lambda a, b: gravity.vertical_cylinder(x, a, b, 3.0, 1000.0), 2.0, 30.0), CHI, POLE, 1000.0)))
|
|
307
|
+
|
|
308
|
+
x = np.linspace(-80, 80, 161)
|
|
309
|
+
out.append(_profile_case(
|
|
310
|
+
"contact-poisson", "Poisson's relation", "Vertical contact, gravity ↔ magnetic", "Magnetic 2D polygon",
|
|
311
|
+
"Vertical derivative of the gravity step", "Slab from 5 to 40 m, at the pole", "nT", 1e-4, x,
|
|
312
|
+
magnetics.contact(x, 5.0, 40.0, CHI, POLE),
|
|
313
|
+
_poisson(_dgz_dz(lambda a, b: gravity.vertical_step(x, a, b, 1000.0), 5.0, 40.0), CHI, POLE, 1000.0)))
|
|
314
|
+
|
|
315
|
+
# --- independent code ---------------------------------------------------------------------------------------
|
|
316
|
+
out.extend(_harmonica_cases())
|
|
317
|
+
return out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def harmonica_reference() -> dict | None:
|
|
321
|
+
"""The stored Harmonica values and the versions they were computed with (None if the file is missing)."""
|
|
322
|
+
try:
|
|
323
|
+
return json.loads(resources.files("detectability").joinpath("data", HARMONICA_FILE).read_text("utf-8"))
|
|
324
|
+
except FileNotFoundError:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _harmonica_cases() -> list[Case]:
|
|
329
|
+
ref = harmonica_reference()
|
|
330
|
+
if ref is None:
|
|
331
|
+
return []
|
|
332
|
+
h = f"Harmonica {ref['harmonica'].lstrip('v')}"
|
|
333
|
+
rows = {
|
|
334
|
+
"harmonica-prism-gravity": ("Prism, gravity", "Nagy (1966) closed form", f"{h} prism_gravity",
|
|
335
|
+
"6 × 8 m room from 2 to 6 m, −2,200 kg/m³", "µGal", 1e-8),
|
|
336
|
+
"harmonica-prism-magnetic": ("Prism, magnetic", "Gradient tensor and Poisson's relation", f"{h} prism_magnetic",
|
|
337
|
+
"5 × 6 m block from 4 to 7 m, 0.01 SI, main field of Sivas, sensor 0.5 m above "
|
|
338
|
+
"ground", "nT", 1e-8),
|
|
339
|
+
"harmonica-sphere-gravity": ("Sphere, gravity", "Point mass", f"{h} point_gravity",
|
|
340
|
+
"Radius 3 m, centre at 12 m, −2,600 kg/m³", "µGal", 1e-10),
|
|
341
|
+
"harmonica-sphere-magnetic": ("Sphere, magnetic", "Magnetic dipole", f"{h} dipole_magnetic",
|
|
342
|
+
"Radius 3 m, centre at 12 m, 0.01 SI, main field of Sivas, sensor 0.5 m above "
|
|
343
|
+
"ground", "nT", 1e-10),
|
|
344
|
+
"harmonica-dyke-gravity": ("Vertical dyke, gravity", "Difference of two vertical steps (2D)",
|
|
345
|
+
f"{h} prism_gravity, 200 km along strike", "4 m wide dyke from 3 to 15 m, "
|
|
346
|
+
"+500 kg/m³", "µGal", 1e-5),
|
|
347
|
+
"harmonica-dyke-magnetic": ("Vertical dyke, magnetic", "2D polygon (surface charge)",
|
|
348
|
+
f"{h} prism_magnetic, 200 km along strike", "4 m wide, from 3 to 20 m, 0.01 SI, "
|
|
349
|
+
"main field of Sivas, sensor 0.5 m above ground", "nT", 1e-5),
|
|
350
|
+
}
|
|
351
|
+
amb = Ambient(*_FIELD)
|
|
352
|
+
out = []
|
|
353
|
+
for key, c in HARMONICA_CASES.items():
|
|
354
|
+
x = np.linspace(c["x"][0], c["x"][1], int(c["x"][2]))
|
|
355
|
+
if key == "harmonica-prism-gravity":
|
|
356
|
+
model = gravity.prism(x, 0.0, *c["along"], *c["across"], *c["depth"], c["drho"]) * SI_TO_UGAL
|
|
357
|
+
elif key == "harmonica-prism-magnetic":
|
|
358
|
+
model = magnetics.prism(x, *c["along"], *c["across"], *c["depth"], c["chi"], amb, height=c["height"])
|
|
359
|
+
elif key == "harmonica-sphere-gravity":
|
|
360
|
+
model = gravity.sphere(x, c["depth"], c["radius"], c["drho"]) * SI_TO_UGAL
|
|
361
|
+
elif key == "harmonica-sphere-magnetic":
|
|
362
|
+
model = magnetics.sphere(x, c["depth"], c["radius"], c["chi"], amb, height=c["height"])
|
|
363
|
+
elif key == "harmonica-dyke-gravity":
|
|
364
|
+
width = c["along"][1] - c["along"][0]
|
|
365
|
+
model = gravity.dyke(x, *c["depth"], width, c["drho"], centre=sum(c["along"]) / 2) * SI_TO_UGAL
|
|
366
|
+
else:
|
|
367
|
+
width = c["along"][1] - c["along"][0]
|
|
368
|
+
model = magnetics.dyke(x, *c["depth"], width, c["chi"], amb, centre=sum(c["along"]) / 2,
|
|
369
|
+
height=c["height"])
|
|
370
|
+
geometry, how, reference, setting, units, tol = rows[key]
|
|
371
|
+
out.append(_profile_case(key, "Independent code", geometry, how, reference, setting, units, tol, x, model,
|
|
372
|
+
np.asarray(ref["cases"][key], float)))
|
|
373
|
+
return out
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def igrf_rows() -> list[dict]:
|
|
377
|
+
rows = []
|
|
378
|
+
for site, lat, lon, alt, date, F, D, I in BGS:
|
|
379
|
+
f = igrf.field(lat, lon, _dt.date.fromisoformat(date), alt)
|
|
380
|
+
rows.append({"site": site, "date": date, "F": F, "D": D, "I": I, "F_model": f.total,
|
|
381
|
+
"D_model": f.declination, "I_model": f.inclination,
|
|
382
|
+
"dF": f.total - F, "dD": f.declination - D, "dI": f.inclination - I})
|
|
383
|
+
return rows
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _pct(v: float) -> str:
|
|
387
|
+
return f"{v:.1e} %" if v < 0.001 else f"{v:.3g} %"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def markdown_table() -> str:
|
|
391
|
+
lines = ["| Check | Geometry and method | Reference | Max. relative error | Tolerance |",
|
|
392
|
+
"|---|---|---|---:|---:|"]
|
|
393
|
+
for c in cases():
|
|
394
|
+
lines.append(f"| {c.check} | {c.geometry} | {c.reference} | {_pct(c.error)} | {_pct(c.tolerance)} |")
|
|
395
|
+
rows = igrf_rows()
|
|
396
|
+
dF = max(abs(r["dF"]) for r in rows)
|
|
397
|
+
dA = max(max(abs(r["dD"]), abs(r["dI"])) for r in rows)
|
|
398
|
+
lines.append(f"| Reference service | IGRF-14 main field, 5 sites | British Geological Survey IGRF-14 | "
|
|
399
|
+
f"{dF:.2f} nT, {dA:.4f}° | {IGRF_TOLERANCE_NT:g} nT, {IGRF_TOLERANCE_DEG:g}° |")
|
|
400
|
+
return "\n".join(lines)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
print(markdown_table())
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: detectability
|
|
3
|
+
Version: 0.2.1
|
|
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
|
+
Author: Mükremin Nergiz
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://detectability.mukreminnergiz.com
|
|
8
|
+
Project-URL: Documentation, https://detectability.mukreminnergiz.com/method
|
|
9
|
+
Project-URL: Validation, https://detectability.mukreminnergiz.com/validation
|
|
10
|
+
Project-URL: Repository, https://github.com/mukremin-nergiz/detectability
|
|
11
|
+
Project-URL: Issues, https://github.com/mukremin-nergiz/detectability/issues
|
|
12
|
+
Project-URL: Citation, https://doi.org/10.5281/zenodo.23931815
|
|
13
|
+
Keywords: geophysics,gravity,magnetics,survey design,near-surface,forward modelling,IGRF
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: Education
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
30
|
+
Provides-Extra: validation
|
|
31
|
+
Requires-Dist: harmonica==0.7.0; extra == "validation"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Detectability
|
|
35
|
+
|
|
36
|
+
[](https://github.com/mukremin-nergiz/detectability/actions/workflows/tests.yml)
|
|
37
|
+
[](https://github.com/mukremin-nergiz/detectability/blob/main/LICENSE)
|
|
38
|
+
[](https://doi.org/10.5281/zenodo.23931815)
|
|
39
|
+
|
|
40
|
+
**Pre-survey detectability, modelled and reproducible.** Describe a buried target and a survey. Detectability models the gravity and
|
|
41
|
+
magnetic response of the target, compares it with instrument and field noise, and gives a verdict for each method
|
|
42
|
+
(clearly detectable, marginal, not detectable), with the recommended station spacing and profile length, every
|
|
43
|
+
assumption, and a Python script that reproduces the result.
|
|
44
|
+
|
|
45
|
+
- Web tool: <https://detectability.mukreminnergiz.com>
|
|
46
|
+
- Method and validation: <https://detectability.mukreminnergiz.com/method>
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install detectability
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Python 3.10 or newer; the only dependency is NumPy. The development version: `pip install "git+https://github.com/mukremin-nergiz/detectability"`.
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
A 20 m air-filled cave whose top is 15 m deep in limestone, surveyed near Sivas, Türkiye:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from detectability import evaluate
|
|
62
|
+
|
|
63
|
+
report = evaluate(
|
|
64
|
+
{"shape": "sphere", "depth_top": 15, "size": 20, "material": "air", "host": "limestone"},
|
|
65
|
+
{"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"},
|
|
66
|
+
)
|
|
67
|
+
print(report.summary())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Detectability 0.2.1: sphere in limestone
|
|
72
|
+
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
73
|
+
stations every 5 m or less, profile at least 153 m long
|
|
74
|
+
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
75
|
+
Recommended method: gravity
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`report.script()` returns a standalone script that recomputes the report; `report.to_dict()` gives the full result,
|
|
79
|
+
including the modelled profiles and the assumptions.
|
|
80
|
+
|
|
81
|
+
Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `dyke` (any dip) and `contact`. The main
|
|
82
|
+
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
83
|
+
in, and measured contrasts can be given instead.
|
|
84
|
+
|
|
85
|
+
## Modules
|
|
86
|
+
|
|
87
|
+
| Module | Contents |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `survey` | `evaluate()`: target and survey in, report and reproduction script out |
|
|
90
|
+
| `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 |
|
|
92
|
+
| `igrf` | IGRF-14 main field from location and date, from the official IAGA coefficients |
|
|
93
|
+
| `rocks` | typical densities and magnetic susceptibilities with their ranges (Telford et al. 1990) |
|
|
94
|
+
| `detect` | instrument and field noise, signal-to-noise ratio, verdict, station spacing, profile length |
|
|
95
|
+
|
|
96
|
+
## Validation
|
|
97
|
+
|
|
98
|
+
Every model is checked against a result computed in a different way. Each row below is also a test: it fails if the
|
|
99
|
+
error exceeds the tolerance. The error is the largest difference along the profile as a percentage of the largest
|
|
100
|
+
reference value. Figures for each row: <https://detectability.mukreminnergiz.com/validation>.
|
|
101
|
+
|
|
102
|
+
The "Independent code" rows compare with [Harmonica](https://www.fatiando.org/harmonica) 0.7.0 (Fatiando a Terra
|
|
103
|
+
Project, [doi:10.5281/zenodo.13308312](https://doi.org/10.5281/zenodo.13308312)), a separate open-source package. Its
|
|
104
|
+
values are stored in `src/detectability/data/harmonica_reference.json`, so the library does not depend on it; to
|
|
105
|
+
recompute and check them:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install -e ".[validation]"
|
|
109
|
+
python tools/harmonica_reference.py --check
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Check | Geometry and method | Reference | Max. relative error | Tolerance |
|
|
113
|
+
|---|---|---|---:|---:|
|
|
114
|
+
| Analytical limit | Prism, gravity, far from the sensor | Point mass of the same mass | 1.9e-05 % | 0.01 % |
|
|
115
|
+
| Analytical limit | Prism, gravity, wide and thin | Infinite Bouguer slab, 2πGΔρt | 0.0099 % | 0.1 % |
|
|
116
|
+
| Analytical limit | Vertical step, gravity, far over the slab | Infinite Bouguer slab, 2πGΔρt | 4.8e-04 % | 0.01 % |
|
|
117
|
+
| Analytical limit | Vertical cylinder, gravity, on its axis | Exact on-axis cylinder formula | 1.7e-13 % | 1.0e-10 % |
|
|
118
|
+
| Independent formula | Horizontal cylinder, gravity | 2D polygon, 720 sides (Talwani; Won & Bevis) | 6.3e-12 % | 0.01 % |
|
|
119
|
+
| Independent formula | Vertical dyke, gravity | 2D polygon (Won & Bevis) | 1.9e-13 % | 1.0e-06 % |
|
|
120
|
+
| Numerical integration | Prism, gravity | 16³ Gauss–Legendre point masses | 2.9e-06 % | 0.001 % |
|
|
121
|
+
| Numerical integration | Horizontal cylinder, magnetic | 3D dipoles integrated along strike | 2.2e-07 % | 0.01 % |
|
|
122
|
+
| Numerical integration | Prism, magnetic | 16³ Gauss–Legendre point dipoles | 1.3e-12 % | 0.001 % |
|
|
123
|
+
| Numerical integration | Vertical cylinder, gravity | Gauss–Legendre over the cross-section, line masses in depth | 5.5e-13 % | 1.0e-08 % |
|
|
124
|
+
| Numerical integration | Vertical cylinder, magnetic | Gauss–Legendre point dipoles over the volume | 5.8e-13 % | 1.0e-06 % |
|
|
125
|
+
| Poisson's relation | Sphere, gravity ↔ magnetic | Vertical derivative of the gravity sphere | 1.4e-08 % | 0.001 % |
|
|
126
|
+
| Poisson's relation | Vertical dyke, gravity ↔ magnetic | Vertical derivative of the gravity dyke | 2.2e-08 % | 1.0e-04 % |
|
|
127
|
+
| Poisson's relation | Dipping dyke (60°), gravity ↔ magnetic | Vertical derivative of the gravity dyke | 2.6e-08 % | 1.0e-04 % |
|
|
128
|
+
| Poisson's relation | Vertical cylinder, gravity ↔ magnetic | Vertical derivative of the gravity cylinder | 3.6e-08 % | 1.0e-04 % |
|
|
129
|
+
| Poisson's relation | Vertical contact, gravity ↔ magnetic | Vertical derivative of the gravity step | 3.9e-06 % | 1.0e-04 % |
|
|
130
|
+
| Independent code | Prism, gravity | Harmonica 0.7.0 prism_gravity | 3.0e-12 % | 1.0e-08 % |
|
|
131
|
+
| Independent code | Prism, magnetic | Harmonica 0.7.0 prism_magnetic | 4.8e-13 % | 1.0e-08 % |
|
|
132
|
+
| Independent code | Sphere, gravity | Harmonica 0.7.0 point_gravity | 3.9e-14 % | 1.0e-10 % |
|
|
133
|
+
| Independent code | Sphere, magnetic | Harmonica 0.7.0 dipole_magnetic | 8.8e-14 % | 1.0e-10 % |
|
|
134
|
+
| Independent code | Vertical dyke, gravity | Harmonica 0.7.0 prism_gravity, 200 km along strike | 3.6e-07 % | 1.0e-05 % |
|
|
135
|
+
| Independent code | Vertical dyke, magnetic | Harmonica 0.7.0 prism_magnetic, 200 km along strike | 5.1e-07 % | 1.0e-05 % |
|
|
136
|
+
| Reference service | IGRF-14 main field, 5 sites | British Geological Survey IGRF-14 | 0.38 nT, 0.0005° | 1 nT, 0.002° |
|
|
137
|
+
|
|
138
|
+
Regenerate this table with `python -m detectability.validation`. Run all tests (including these) with:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pip install -e ".[test]"
|
|
142
|
+
pytest
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
Flat topography, a homogeneous host and no regional trend; 2D targets are infinitely long along strike; the profile
|
|
148
|
+
crosses the centre of the target; self-demagnetisation is neglected. Results are expectations under these assumptions,
|
|
149
|
+
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.
|
|
151
|
+
|
|
152
|
+
## Citation
|
|
153
|
+
|
|
154
|
+
See [CITATION.cff](https://github.com/mukremin-nergiz/detectability/blob/main/CITATION.cff), or:
|
|
155
|
+
|
|
156
|
+
> Nergiz, M. (2026). *Detectability: Pre-survey detectability, modelled and reproducible* (Version 0.2.1) [Computer software].
|
|
157
|
+
> Zenodo. https://doi.org/10.5281/zenodo.23931815
|
|
158
|
+
|
|
159
|
+
The DOI above always resolves to the latest version; each release also has its own DOI on Zenodo.
|
|
160
|
+
|
|
161
|
+
## Author
|
|
162
|
+
|
|
163
|
+
Mükremin Nergiz, Geophysical Engineer & Software Developer (Independent Researcher, Sivas, Türkiye).
|
|
164
|
+
ORCID [0009-0001-6789-2774](https://orcid.org/0009-0001-6789-2774) · <https://mukreminnergiz.com>
|
|
165
|
+
|
|
166
|
+
Licence: BSD-3-Clause.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
detectability/__init__.py,sha256=cO_ngIky4b0Un6TiEtLHGTiLX0OUZRE_Ht9XJsJE3Zw,272
|
|
2
|
+
detectability/constants.py,sha256=t4Le7jC1QFbG5LJZ6h9eWn5FAiarX6HBzEv2DbaoQHw,277
|
|
3
|
+
detectability/detect.py,sha256=Ncs8ISd8_OiVkRA1HP7sO5Ct07Fw65AIlt3AjzyntXc,7415
|
|
4
|
+
detectability/gravity.py,sha256=f6c4x79L4bGobn9XNpRhFY3ymZy_ari2s8HweH8xfAQ,14364
|
|
5
|
+
detectability/igrf.py,sha256=BZWUGSR-Wg6PNVopdg764Nuqb74maDpsEFL8SXSNsZE,6889
|
|
6
|
+
detectability/magnetics.py,sha256=RfIBMSvNDMj92QNq4NrRsg0y_s5Sdl9NXR9Hr92Nuo4,10780
|
|
7
|
+
detectability/rocks.py,sha256=Vb5JuiSEUSAZqry00bj8vo2Ee5meUOePv__MjPAjQtE,2823
|
|
8
|
+
detectability/survey.py,sha256=aNzuSRolAu0NmMyBAa_L7nJ8iRzekWjHOVk1JlWkj2g,15063
|
|
9
|
+
detectability/validation.py,sha256=OxdQ8DnQ37nsnmtSPIt7NClBkF0ijwED5Rt0PFbj1lA,22315
|
|
10
|
+
detectability/data/harmonica_reference.json,sha256=rvofao_ePp7csTT_F7TkPbCW1lN4ua-MKxzZ9iNRY9o,13919
|
|
11
|
+
detectability/data/igrf14coeffs.txt,sha256=j42IQDAo_E7pLE842XtG4Kh-LPxJYEW0PJ4mwdawkDw,42411
|
|
12
|
+
detectability-0.2.1.dist-info/licenses/LICENSE,sha256=JhugoJkggCbcANfpdnaa_eXT5lgZxlxRlwvV_jQygH0,1503
|
|
13
|
+
detectability-0.2.1.dist-info/METADATA,sha256=zPSOJN0Y0Co6888hfZD7C4-EYMtmpSt-whtZe7Uqtok,9551
|
|
14
|
+
detectability-0.2.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
detectability-0.2.1.dist-info/top_level.txt,sha256=Xs154_L3v581zFA1ogt3Lkfw_3HIFHAKnykT-Na4JI4,14
|
|
16
|
+
detectability-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Mükremin Nergiz
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
detectability
|