detectability 0.5.0__tar.gz → 0.6.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.5.0/src/detectability.egg-info → detectability-0.6.0}/PKG-INFO +25 -2
- {detectability-0.5.0 → detectability-0.6.0}/README.md +24 -1
- {detectability-0.5.0 → detectability-0.6.0}/pyproject.toml +1 -1
- detectability-0.6.0/src/detectability/__init__.py +9 -0
- detectability-0.6.0/src/detectability/limits.py +121 -0
- {detectability-0.5.0 → detectability-0.6.0/src/detectability.egg-info}/PKG-INFO +25 -2
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/SOURCES.txt +2 -0
- detectability-0.6.0/tests/test_limits.py +93 -0
- detectability-0.5.0/src/detectability/__init__.py +0 -7
- {detectability-0.5.0 → detectability-0.6.0}/LICENSE +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/setup.cfg +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/constants.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/ert_reference.json +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/harmonica_reference.json +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/igrf14coeffs.txt +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/detect.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/ert.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/gpr.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/gravity.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/igrf.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/magnetics.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/rocks.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/survey.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability/validation.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/dependency_links.txt +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/requires.txt +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/top_level.txt +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_detect.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_ert.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_ert_pygimli.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_gpr.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_gravity.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_igrf.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_magnetics.py +0 -0
- {detectability-0.5.0 → detectability-0.6.0}/tests/test_survey.py +0 -0
- {detectability-0.5.0 → detectability-0.6.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.6.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
|
|
@@ -71,7 +71,7 @@ print(report.summary())
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
```text
|
|
74
|
-
Detectability 0.
|
|
74
|
+
Detectability 0.6.0: sphere in limestone
|
|
75
75
|
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
76
76
|
stations every 5 m or less, profile at least 153 m long
|
|
77
77
|
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
@@ -81,6 +81,29 @@ Recommended method: gravity
|
|
|
81
81
|
`report.script()` returns a standalone script that recomputes the report; `report.to_dict()` gives the full result,
|
|
82
82
|
including the modelled profiles and the assumptions.
|
|
83
83
|
|
|
84
|
+
### How deep before a method loses it?
|
|
85
|
+
|
|
86
|
+
`depth_limit()` asks the inverse question — the one a survey plan turns on. Leave `depth_top` out: it is what the
|
|
87
|
+
search solves for.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from detectability import depth_limit
|
|
91
|
+
|
|
92
|
+
for method in ("gravity", "magnetic", "gpr"):
|
|
93
|
+
print(depth_limit({"shape": "sphere", "size": 20, "material": "air", "host": "limestone"},
|
|
94
|
+
{"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}, method=method))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
gravity: detectable (S/N >= 5) down to 25.7 m
|
|
99
|
+
magnetic: never reaches S/N 5; S/N 2.88 even at 0.5 m
|
|
100
|
+
gpr: detectable (S/N >= 5) down to 25.2 m
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Pass `threshold=detect.MARGINAL` for the lower threshold, and `search=(shallow, deep)` to bound the sweep.
|
|
104
|
+
Sweep the target size and you have a detection-limit chart; the website draws one at
|
|
105
|
+
[/limits](https://detectability.mukreminnergiz.com/limits).
|
|
106
|
+
|
|
84
107
|
Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `dyke` (any dip) and `contact`. The main
|
|
85
108
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
86
109
|
in, and measured contrasts can be given instead.
|
|
@@ -35,7 +35,7 @@ print(report.summary())
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
Detectability 0.
|
|
38
|
+
Detectability 0.6.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
|
|
@@ -45,6 +45,29 @@ Recommended method: gravity
|
|
|
45
45
|
`report.script()` returns a standalone script that recomputes the report; `report.to_dict()` gives the full result,
|
|
46
46
|
including the modelled profiles and the assumptions.
|
|
47
47
|
|
|
48
|
+
### How deep before a method loses it?
|
|
49
|
+
|
|
50
|
+
`depth_limit()` asks the inverse question — the one a survey plan turns on. Leave `depth_top` out: it is what the
|
|
51
|
+
search solves for.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from detectability import depth_limit
|
|
55
|
+
|
|
56
|
+
for method in ("gravity", "magnetic", "gpr"):
|
|
57
|
+
print(depth_limit({"shape": "sphere", "size": 20, "material": "air", "host": "limestone"},
|
|
58
|
+
{"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}, method=method))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
gravity: detectable (S/N >= 5) down to 25.7 m
|
|
63
|
+
magnetic: never reaches S/N 5; S/N 2.88 even at 0.5 m
|
|
64
|
+
gpr: detectable (S/N >= 5) down to 25.2 m
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pass `threshold=detect.MARGINAL` for the lower threshold, and `search=(shallow, deep)` to bound the sweep.
|
|
68
|
+
Sweep the target size and you have a detection-limit chart; the website draws one at
|
|
69
|
+
[/limits](https://detectability.mukreminnergiz.com/limits).
|
|
70
|
+
|
|
48
71
|
Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `dyke` (any dip) and `contact`. The main
|
|
49
72
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
50
73
|
in, and measured contrasts can be given instead.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "detectability"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.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"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Detectability: pre-survey detectability, modelled and reproducible."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.6.0"
|
|
4
|
+
|
|
5
|
+
from .limits import Limit, depth_limit # noqa: E402
|
|
6
|
+
from .survey import DEFAULT_METHODS, METHODS, SHAPES, Report, Survey, Target, evaluate # noqa: E402
|
|
7
|
+
|
|
8
|
+
__all__ = ["evaluate", "depth_limit", "Limit", "Target", "Survey", "Report",
|
|
9
|
+
"SHAPES", "METHODS", "DEFAULT_METHODS", "__version__"]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""How deep a method still sees a target: the depth limit, solved for instead of assumed.
|
|
2
|
+
|
|
3
|
+
``evaluate`` answers "can this method see the target at this depth?". The inverse question is the one that shapes a
|
|
4
|
+
survey plan: *how deep can the target be before the method loses it?*
|
|
5
|
+
|
|
6
|
+
>>> from detectability import depth_limit
|
|
7
|
+
>>> limit = depth_limit({"shape": "sphere", "size": 20, "material": "air", "host": "limestone"},
|
|
8
|
+
... {"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}, method="gravity")
|
|
9
|
+
>>> print(limit)
|
|
10
|
+
|
|
11
|
+
``depth_top`` on the target is ignored, and may be left out entirely: it is what the search solves for. If the shape
|
|
12
|
+
also needs ``depth_bottom``, the body keeps its thickness as it moves down.
|
|
13
|
+
|
|
14
|
+
The search makes no assumption that signal-to-noise falls smoothly with depth, because it does not always: the GPR
|
|
15
|
+
antenna frequency, when left to the default, is chosen from the target depth, so the response can step. A coarse
|
|
16
|
+
geometric scan brackets the **shallowest** crossing of the threshold and a bisection refines it. The shallowest
|
|
17
|
+
crossing is the honest answer — beyond it the method can no longer be relied on, whatever happens deeper.
|
|
18
|
+
|
|
19
|
+
Each limit costs ``steps`` plus about twenty forward models. That is milliseconds for gravity, magnetics and GPR.
|
|
20
|
+
For ERT, where a single forward model inverts a mesh and takes tens of seconds, a limit takes tens of minutes; pass a
|
|
21
|
+
narrow ``search`` range and a small ``steps`` if you ask for one at all.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, replace
|
|
27
|
+
|
|
28
|
+
from . import detect
|
|
29
|
+
from .survey import NEEDS_BOTTOM, Survey, Target, _as, evaluate
|
|
30
|
+
|
|
31
|
+
__all__ = ["Limit", "depth_limit"]
|
|
32
|
+
|
|
33
|
+
SEARCH = (0.5, 2000.0) # m below ground: shallow enough for utilities, deep enough for an ore body
|
|
34
|
+
STEPS = 24 # coarse scan points, geometric over the search range
|
|
35
|
+
REFINE = 20 # bisections; takes a 24-point scan of 0.5-2000 m below 0.1 % in depth
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Limit:
|
|
40
|
+
"""The greatest depth at which ``method`` still reaches ``threshold`` for a given target."""
|
|
41
|
+
|
|
42
|
+
method: str
|
|
43
|
+
threshold: float
|
|
44
|
+
depth: float | None # m to the top of the target; None when the method never reaches the threshold
|
|
45
|
+
snr_shallowest: float | None # signal-to-noise at the shallowest depth searched, for context
|
|
46
|
+
search: tuple[float, float]
|
|
47
|
+
beyond_search: bool = False # still detected at the deepest depth searched: the limit is at least that
|
|
48
|
+
available: bool = True
|
|
49
|
+
reason: str = ""
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
if not self.available:
|
|
53
|
+
return f"{self.method}: not assessed ({self.reason})"
|
|
54
|
+
if self.depth is None:
|
|
55
|
+
return (f"{self.method}: never reaches S/N {self.threshold:g}; "
|
|
56
|
+
f"S/N {self.snr_shallowest:.3g} even at {self.search[0]:g} m")
|
|
57
|
+
at_least = "at least " if self.beyond_search else ""
|
|
58
|
+
return f"{self.method}: detectable (S/N >= {self.threshold:g}) down to {at_least}{self.depth:.3g} m"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _at(target: Target, depth: float) -> Target:
|
|
62
|
+
"""The same body, moved so its top sits at ``depth``; thickness kept where the shape needs a bottom."""
|
|
63
|
+
moved = replace(target, depth_top=depth)
|
|
64
|
+
if target.shape in NEEDS_BOTTOM:
|
|
65
|
+
thickness = (target.depth_bottom - target.depth_top) if target.depth_bottom else max(target.size or 1.0, 1.0)
|
|
66
|
+
moved.depth_bottom = depth + thickness
|
|
67
|
+
return moved
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def depth_limit(target, survey=None, method: str = "gravity", threshold: float = detect.CLEAR,
|
|
71
|
+
search: tuple[float, float] = SEARCH, steps: int = STEPS, refine: int = REFINE) -> Limit:
|
|
72
|
+
"""Greatest depth at which ``method`` still reaches ``threshold`` for ``target`` under ``survey``.
|
|
73
|
+
|
|
74
|
+
``threshold`` defaults to the clearly-detectable signal-to-noise (5); pass ``detect.MARGINAL`` for the lower one.
|
|
75
|
+
The result's ``depth`` is None when the method never reaches the threshold, even at the shallowest depth searched.
|
|
76
|
+
"""
|
|
77
|
+
shallow, deep = float(search[0]), float(search[1])
|
|
78
|
+
if not 0 < shallow < deep:
|
|
79
|
+
raise ValueError(f"search must be (shallow, deep) with 0 < shallow < deep; got {search!r}")
|
|
80
|
+
if steps < 2:
|
|
81
|
+
raise ValueError("steps must be at least 2")
|
|
82
|
+
|
|
83
|
+
if isinstance(target, dict) and "depth_top" not in target:
|
|
84
|
+
target = {**target, "depth_top": shallow} # it is what the search solves for; any value will do
|
|
85
|
+
t, s = _as(Target, target), _as(Survey, survey)
|
|
86
|
+
t.validate()
|
|
87
|
+
|
|
88
|
+
def snr(depth: float) -> tuple[float, bool, str]:
|
|
89
|
+
result = evaluate(_at(t, depth), s, methods=[method]).results[0]
|
|
90
|
+
if not result.available:
|
|
91
|
+
return 0.0, False, result.reason
|
|
92
|
+
return result.assessment.snr, True, ""
|
|
93
|
+
|
|
94
|
+
first, ok, why = snr(shallow)
|
|
95
|
+
if not ok:
|
|
96
|
+
return Limit(method, threshold, None, None, (shallow, deep), available=False, reason=why)
|
|
97
|
+
if first < threshold:
|
|
98
|
+
return Limit(method, threshold, None, first, (shallow, deep))
|
|
99
|
+
|
|
100
|
+
# Coarse scan for the shallowest crossing, then bisect inside that bracket.
|
|
101
|
+
ratio = (deep / shallow) ** (1 / (steps - 1))
|
|
102
|
+
lo, lo_depth = shallow, shallow
|
|
103
|
+
for i in range(1, steps):
|
|
104
|
+
probe = shallow * ratio ** i
|
|
105
|
+
value, ok, _ = snr(probe)
|
|
106
|
+
if not ok or value < threshold:
|
|
107
|
+
hi = probe
|
|
108
|
+
break
|
|
109
|
+
lo_depth = probe
|
|
110
|
+
else:
|
|
111
|
+
return Limit(method, threshold, deep, first, (shallow, deep), beyond_search=True)
|
|
112
|
+
|
|
113
|
+
lo = lo_depth
|
|
114
|
+
for _ in range(refine):
|
|
115
|
+
mid = (lo * hi) ** 0.5
|
|
116
|
+
value, ok, _ = snr(mid)
|
|
117
|
+
if ok and value >= threshold:
|
|
118
|
+
lo = mid
|
|
119
|
+
else:
|
|
120
|
+
hi = mid
|
|
121
|
+
return Limit(method, threshold, (lo * hi) ** 0.5, first, (shallow, deep))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectability
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -71,7 +71,7 @@ print(report.summary())
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
```text
|
|
74
|
-
Detectability 0.
|
|
74
|
+
Detectability 0.6.0: sphere in limestone
|
|
75
75
|
- gravity: peak 114 µGal, noise 11.2 µGal, S/N 10.2: clearly detectable
|
|
76
76
|
stations every 5 m or less, profile at least 153 m long
|
|
77
77
|
- magnetic: peak 0.462 nT, noise 2 nT, S/N 0.231: not detectable
|
|
@@ -81,6 +81,29 @@ Recommended method: gravity
|
|
|
81
81
|
`report.script()` returns a standalone script that recomputes the report; `report.to_dict()` gives the full result,
|
|
82
82
|
including the modelled profiles and the assumptions.
|
|
83
83
|
|
|
84
|
+
### How deep before a method loses it?
|
|
85
|
+
|
|
86
|
+
`depth_limit()` asks the inverse question — the one a survey plan turns on. Leave `depth_top` out: it is what the
|
|
87
|
+
search solves for.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from detectability import depth_limit
|
|
91
|
+
|
|
92
|
+
for method in ("gravity", "magnetic", "gpr"):
|
|
93
|
+
print(depth_limit({"shape": "sphere", "size": 20, "material": "air", "host": "limestone"},
|
|
94
|
+
{"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}, method=method))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
gravity: detectable (S/N >= 5) down to 25.7 m
|
|
99
|
+
magnetic: never reaches S/N 5; S/N 2.88 even at 0.5 m
|
|
100
|
+
gpr: detectable (S/N >= 5) down to 25.2 m
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Pass `threshold=detect.MARGINAL` for the lower threshold, and `search=(shallow, deep)` to bound the sweep.
|
|
104
|
+
Sweep the target size and you have a detection-limit chart; the website draws one at
|
|
105
|
+
[/limits](https://detectability.mukreminnergiz.com/limits).
|
|
106
|
+
|
|
84
107
|
Target shapes: `sphere`, `horizontal_cylinder`, `vertical_cylinder`, `prism`, `dyke` (any dip) and `contact`. The main
|
|
85
108
|
geomagnetic field comes from IGRF-14 for the survey location and date. Typical densities and susceptibilities are built
|
|
86
109
|
in, and measured contrasts can be given instead.
|
|
@@ -8,6 +8,7 @@ src/detectability/ert.py
|
|
|
8
8
|
src/detectability/gpr.py
|
|
9
9
|
src/detectability/gravity.py
|
|
10
10
|
src/detectability/igrf.py
|
|
11
|
+
src/detectability/limits.py
|
|
11
12
|
src/detectability/magnetics.py
|
|
12
13
|
src/detectability/rocks.py
|
|
13
14
|
src/detectability/survey.py
|
|
@@ -26,6 +27,7 @@ tests/test_ert_pygimli.py
|
|
|
26
27
|
tests/test_gpr.py
|
|
27
28
|
tests/test_gravity.py
|
|
28
29
|
tests/test_igrf.py
|
|
30
|
+
tests/test_limits.py
|
|
29
31
|
tests/test_magnetics.py
|
|
30
32
|
tests/test_survey.py
|
|
31
33
|
tests/test_validation.py
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The depth limit must agree with the assessment it inverts: detectable just above it, not just below."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from detectability import Target, depth_limit, detect, evaluate
|
|
6
|
+
|
|
7
|
+
SURVEY = {"latitude": 39.75, "longitude": 37.02, "date": "2026-07-01"}
|
|
8
|
+
CAVITY = {"shape": "sphere", "size": 20, "material": "air", "host": "limestone"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def snr_at(target, depth, method):
|
|
12
|
+
return evaluate({**target, "depth_top": depth}, SURVEY, methods=[method]).results[0].assessment.snr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("method", ["gravity", "gpr"])
|
|
16
|
+
def test_limit_brackets_the_threshold(method):
|
|
17
|
+
limit = depth_limit(CAVITY, SURVEY, method=method)
|
|
18
|
+
assert limit.available and limit.depth is not None and not limit.beyond_search
|
|
19
|
+
assert snr_at(CAVITY, limit.depth * 0.99, method) >= detect.CLEAR
|
|
20
|
+
assert snr_at(CAVITY, limit.depth * 1.01, method) < detect.CLEAR
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_marginal_reaches_deeper_than_clear():
|
|
24
|
+
clear = depth_limit(CAVITY, SURVEY, method="gravity")
|
|
25
|
+
marginal = depth_limit(CAVITY, SURVEY, method="gravity", threshold=detect.MARGINAL)
|
|
26
|
+
assert marginal.depth > clear.depth
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_a_bigger_body_is_seen_deeper():
|
|
30
|
+
small = depth_limit({**CAVITY, "size": 10}, SURVEY, method="gravity")
|
|
31
|
+
large = depth_limit({**CAVITY, "size": 40}, SURVEY, method="gravity")
|
|
32
|
+
assert large.depth > small.depth
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_depth_top_is_ignored_and_optional():
|
|
36
|
+
without = depth_limit(CAVITY, SURVEY, method="gravity")
|
|
37
|
+
with_depth = depth_limit({**CAVITY, "depth_top": 999}, SURVEY, method="gravity")
|
|
38
|
+
as_object = depth_limit(Target(shape="sphere", depth_top=1, size=20, material="air", host="limestone"),
|
|
39
|
+
SURVEY, method="gravity")
|
|
40
|
+
assert without.depth == pytest.approx(with_depth.depth, rel=1e-9)
|
|
41
|
+
assert without.depth == pytest.approx(as_object.depth, rel=1e-9)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_never_detected_reports_the_shallowest_signal():
|
|
45
|
+
limit = depth_limit(CAVITY, SURVEY, method="magnetic")
|
|
46
|
+
assert limit.depth is None and limit.available
|
|
47
|
+
assert 0 < limit.snr_shallowest < detect.CLEAR
|
|
48
|
+
assert "never reaches" in str(limit)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_still_detected_at_the_bottom_of_the_search():
|
|
52
|
+
limit = depth_limit(CAVITY, SURVEY, method="gravity", search=(0.5, 5.0))
|
|
53
|
+
assert limit.beyond_search and limit.depth == 5.0
|
|
54
|
+
assert "at least" in str(limit)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_unassessable_method_says_why():
|
|
58
|
+
limit = depth_limit({"shape": "sphere", "size": 20, "material": "magnetite", "host": "granite"},
|
|
59
|
+
SURVEY, method="gravity")
|
|
60
|
+
assert not limit.available and limit.depth is None
|
|
61
|
+
assert "no density for 'magnetite'" in limit.reason and "not assessed" in str(limit)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_body_keeps_its_thickness_while_it_moves_down():
|
|
65
|
+
dyke = {"shape": "dyke", "size": 10, "depth_top": 5, "depth_bottom": 45,
|
|
66
|
+
"material": "air", "host": "limestone"}
|
|
67
|
+
limit = depth_limit(dyke, SURVEY, method="gravity")
|
|
68
|
+
assert limit.depth is not None
|
|
69
|
+
# The 40 m thickness must be kept at the limit, or the answer would be about a different body.
|
|
70
|
+
assert snr_at({**dyke, "depth_bottom": limit.depth * 0.99 + 40}, limit.depth * 0.99, "gravity") >= detect.CLEAR
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_a_narrow_search_still_lands_on_the_same_answer():
|
|
74
|
+
wide = depth_limit(CAVITY, SURVEY, method="gravity")
|
|
75
|
+
narrow = depth_limit(CAVITY, SURVEY, method="gravity", search=(20.0, 30.0), steps=6)
|
|
76
|
+
assert narrow.depth == pytest.approx(wide.depth, rel=1e-3)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("search", [(0, 100), (-1, 10), (100, 10), (10, 10)])
|
|
80
|
+
def test_a_bad_search_range_is_refused(search):
|
|
81
|
+
with pytest.raises(ValueError, match="search must be"):
|
|
82
|
+
depth_limit(CAVITY, SURVEY, method="gravity", search=search)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_too_few_steps_is_refused():
|
|
86
|
+
with pytest.raises(ValueError, match="steps must be"):
|
|
87
|
+
depth_limit(CAVITY, SURVEY, method="gravity", steps=1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_an_invalid_target_is_refused():
|
|
91
|
+
with pytest.raises(ValueError):
|
|
92
|
+
depth_limit({"shape": "sphere", "size": -5, "material": "air", "host": "limestone"},
|
|
93
|
+
SURVEY, method="gravity")
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
"""Detectability: pre-survey detectability, modelled and reproducible."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.5.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__"]
|
|
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
|
|
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
|