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.
Files changed (36) hide show
  1. {detectability-0.5.0/src/detectability.egg-info → detectability-0.6.0}/PKG-INFO +25 -2
  2. {detectability-0.5.0 → detectability-0.6.0}/README.md +24 -1
  3. {detectability-0.5.0 → detectability-0.6.0}/pyproject.toml +1 -1
  4. detectability-0.6.0/src/detectability/__init__.py +9 -0
  5. detectability-0.6.0/src/detectability/limits.py +121 -0
  6. {detectability-0.5.0 → detectability-0.6.0/src/detectability.egg-info}/PKG-INFO +25 -2
  7. {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/SOURCES.txt +2 -0
  8. detectability-0.6.0/tests/test_limits.py +93 -0
  9. detectability-0.5.0/src/detectability/__init__.py +0 -7
  10. {detectability-0.5.0 → detectability-0.6.0}/LICENSE +0 -0
  11. {detectability-0.5.0 → detectability-0.6.0}/setup.cfg +0 -0
  12. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/constants.py +0 -0
  13. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/ert_reference.json +0 -0
  14. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/harmonica_reference.json +0 -0
  15. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/data/igrf14coeffs.txt +0 -0
  16. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/detect.py +0 -0
  17. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/ert.py +0 -0
  18. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/gpr.py +0 -0
  19. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/gravity.py +0 -0
  20. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/igrf.py +0 -0
  21. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/magnetics.py +0 -0
  22. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/rocks.py +0 -0
  23. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/survey.py +0 -0
  24. {detectability-0.5.0 → detectability-0.6.0}/src/detectability/validation.py +0 -0
  25. {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/dependency_links.txt +0 -0
  26. {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/requires.txt +0 -0
  27. {detectability-0.5.0 → detectability-0.6.0}/src/detectability.egg-info/top_level.txt +0 -0
  28. {detectability-0.5.0 → detectability-0.6.0}/tests/test_detect.py +0 -0
  29. {detectability-0.5.0 → detectability-0.6.0}/tests/test_ert.py +0 -0
  30. {detectability-0.5.0 → detectability-0.6.0}/tests/test_ert_pygimli.py +0 -0
  31. {detectability-0.5.0 → detectability-0.6.0}/tests/test_gpr.py +0 -0
  32. {detectability-0.5.0 → detectability-0.6.0}/tests/test_gravity.py +0 -0
  33. {detectability-0.5.0 → detectability-0.6.0}/tests/test_igrf.py +0 -0
  34. {detectability-0.5.0 → detectability-0.6.0}/tests/test_magnetics.py +0 -0
  35. {detectability-0.5.0 → detectability-0.6.0}/tests/test_survey.py +0 -0
  36. {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.5.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.5.0: sphere in limestone
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.5.0: sphere in limestone
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.5.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.5.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.5.0: sphere in limestone
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