ertkit 0.1.0__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.
- ertkit/__init__.py +24 -0
- ertkit/_linalg.py +32 -0
- ertkit/_show_versions.py +49 -0
- ertkit/_version.py +24 -0
- ertkit/_warnings.py +18 -0
- ertkit/arrays.py +71 -0
- ertkit/noise.py +134 -0
- ertkit/plot/__init__.py +19 -0
- ertkit/plot/_ves.py +234 -0
- ertkit/py.typed +0 -0
- ertkit/results.py +82 -0
- ertkit/ves/__init__.py +28 -0
- ertkit/ves/data.py +167 -0
- ertkit/ves/forward.py +232 -0
- ertkit/ves/invert.py +632 -0
- ertkit/ves/io.py +179 -0
- ertkit/ves/model.py +86 -0
- ertkit/ves/smooth.py +508 -0
- ertkit/ves/survey.py +119 -0
- ertkit-0.1.0.dist-info/METADATA +156 -0
- ertkit-0.1.0.dist-info/RECORD +23 -0
- ertkit-0.1.0.dist-info/WHEEL +4 -0
- ertkit-0.1.0.dist-info/licenses/LICENSE +21 -0
ertkit/ves/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Vertical electrical sounding (VES): layered-earth modeling and inversion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ertkit.ves.data import Sounding
|
|
6
|
+
from ertkit.ves.forward import forward, hankel_j0, potential, resistivity_transform
|
|
7
|
+
from ertkit.ves.invert import LayerFit, invert_few_layer
|
|
8
|
+
from ertkit.ves.io import read_csv, write_csv
|
|
9
|
+
from ertkit.ves.model import LayeredModel
|
|
10
|
+
from ertkit.ves.smooth import DOI_THRESHOLD, SmoothFit, invert_smooth
|
|
11
|
+
from ertkit.ves.survey import Survey
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DOI_THRESHOLD",
|
|
15
|
+
"LayerFit",
|
|
16
|
+
"LayeredModel",
|
|
17
|
+
"SmoothFit",
|
|
18
|
+
"Sounding",
|
|
19
|
+
"Survey",
|
|
20
|
+
"forward",
|
|
21
|
+
"hankel_j0",
|
|
22
|
+
"invert_few_layer",
|
|
23
|
+
"invert_smooth",
|
|
24
|
+
"potential",
|
|
25
|
+
"read_csv",
|
|
26
|
+
"resistivity_transform",
|
|
27
|
+
"write_csv",
|
|
28
|
+
]
|
ertkit/ves/data.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Observed or synthetic VES data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from ertkit._warnings import DataWarning
|
|
12
|
+
from ertkit.noise import ErrorModel
|
|
13
|
+
from ertkit.ves.forward import forward
|
|
14
|
+
from ertkit.ves.model import LayeredModel
|
|
15
|
+
from ertkit.ves.survey import Survey
|
|
16
|
+
|
|
17
|
+
__all__ = ["Sounding"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, eq=False, kw_only=True, slots=True)
|
|
21
|
+
class Sounding:
|
|
22
|
+
r"""Apparent resistivities of a sounding with their error model.
|
|
23
|
+
|
|
24
|
+
Readings that cannot be inverted in log space (transfer resistance
|
|
25
|
+
:math:`R \le 0`, or :math:`|R|` below three times the absolute error) are
|
|
26
|
+
removed with a :class:`~ertkit.DataWarning`.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
survey : Survey
|
|
31
|
+
Electrode half-spacings of the readings.
|
|
32
|
+
rhoa : array_like
|
|
33
|
+
Apparent resistivity in ohm meters, one value per reading.
|
|
34
|
+
error : ErrorModel or None, optional
|
|
35
|
+
Measurement-error model. Default (or None): ``ErrorModel()``, 3%
|
|
36
|
+
relative.
|
|
37
|
+
|
|
38
|
+
Raises
|
|
39
|
+
------
|
|
40
|
+
TypeError
|
|
41
|
+
If ``survey`` or ``error`` has the wrong type.
|
|
42
|
+
ValueError
|
|
43
|
+
If ``rhoa`` does not have one finite value per reading, or if no
|
|
44
|
+
reading is usable.
|
|
45
|
+
|
|
46
|
+
Warns
|
|
47
|
+
-----
|
|
48
|
+
DataWarning
|
|
49
|
+
If readings are removed.
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
The data weights use the observed transfer resistances, while
|
|
54
|
+
:meth:`synthetic` draws noise with standard deviations computed from the
|
|
55
|
+
noise-free ones; with an absolute error part, readings that happen to come
|
|
56
|
+
out low are therefore weighted slightly less.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
>>> from ertkit.ves import Sounding, Survey
|
|
61
|
+
>>> survey = Survey.schlumberger(ab2=[3.0, 10.0, 30.0], mn2=0.5)
|
|
62
|
+
>>> Sounding(survey=survey, rhoa=[120.0, 80.0, 150.0]).n_data
|
|
63
|
+
3
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
survey: Survey
|
|
67
|
+
rhoa: NDArray[np.float64]
|
|
68
|
+
error: ErrorModel = field(default_factory=ErrorModel)
|
|
69
|
+
|
|
70
|
+
def __post_init__(self) -> None:
|
|
71
|
+
# Checked through plain objects: callers may pass anything at runtime.
|
|
72
|
+
survey_obj: object = self.survey
|
|
73
|
+
error_obj: object = self.error
|
|
74
|
+
if not isinstance(survey_obj, Survey):
|
|
75
|
+
raise TypeError(f"survey must be a Survey, got {type(survey_obj).__name__}")
|
|
76
|
+
if error_obj is None:
|
|
77
|
+
object.__setattr__(self, "error", ErrorModel())
|
|
78
|
+
elif not isinstance(error_obj, ErrorModel):
|
|
79
|
+
raise TypeError(
|
|
80
|
+
f"error must be an ErrorModel, got {type(error_obj).__name__}"
|
|
81
|
+
)
|
|
82
|
+
rhoa = np.array(self.rhoa, dtype=np.float64, copy=True, ndmin=1)
|
|
83
|
+
if rhoa.shape != (self.survey.n_readings,):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"need {self.survey.n_readings} apparent resistivities, got shape {rhoa.shape}"
|
|
86
|
+
)
|
|
87
|
+
if not np.all(np.isfinite(rhoa)):
|
|
88
|
+
raise ValueError("apparent resistivities must be finite")
|
|
89
|
+
keep = self.error.usable(rhoa / self.survey.geometric_factor)
|
|
90
|
+
if not np.any(keep):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"no reading is usable (all R <= 0 or below the noise level)"
|
|
93
|
+
)
|
|
94
|
+
survey = self.survey
|
|
95
|
+
if not np.all(keep):
|
|
96
|
+
dropped = np.flatnonzero(~keep)
|
|
97
|
+
spacings = ", ".join(f"{v:.4g}" for v in self.survey.ab2[dropped])
|
|
98
|
+
warnings.warn(
|
|
99
|
+
f"removed {dropped.size} reading(s) with R <= 0 or |R| below three "
|
|
100
|
+
f"times the absolute error: indices {dropped.tolist()} "
|
|
101
|
+
f"(AB/2 = {spacings} m)",
|
|
102
|
+
DataWarning,
|
|
103
|
+
stacklevel=3,
|
|
104
|
+
)
|
|
105
|
+
survey = Survey(ab2=survey.ab2[keep], mn2=survey.mn2[keep])
|
|
106
|
+
rhoa = rhoa[keep]
|
|
107
|
+
rhoa.setflags(write=False)
|
|
108
|
+
object.__setattr__(self, "survey", survey)
|
|
109
|
+
object.__setattr__(self, "rhoa", rhoa)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def synthetic(
|
|
113
|
+
cls,
|
|
114
|
+
model: LayeredModel,
|
|
115
|
+
survey: Survey,
|
|
116
|
+
error: ErrorModel | None = None,
|
|
117
|
+
rng: int | np.random.Generator | None = None,
|
|
118
|
+
) -> Sounding:
|
|
119
|
+
r"""Simulate noisy data from a layered model.
|
|
120
|
+
|
|
121
|
+
Noise is drawn from the error model in log space:
|
|
122
|
+
:math:`\ln \rho_a^{\mathrm{obs}} = \ln \rho_a + s \, \varepsilon`, with
|
|
123
|
+
:math:`\varepsilon \sim N(0, 1)` and :math:`s` evaluated at the
|
|
124
|
+
noise-free data.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
model : LayeredModel
|
|
129
|
+
The true model.
|
|
130
|
+
survey : Survey
|
|
131
|
+
Electrode half-spacings.
|
|
132
|
+
error : ErrorModel, optional
|
|
133
|
+
Error model used both to draw the noise and to weight the data.
|
|
134
|
+
Default ``ErrorModel()``.
|
|
135
|
+
rng : int, numpy.random.Generator or None, optional
|
|
136
|
+
Seed or generator, normalized with :func:`numpy.random.default_rng`.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
Sounding
|
|
141
|
+
"""
|
|
142
|
+
error = ErrorModel() if error is None else error
|
|
143
|
+
generator = np.random.default_rng(rng)
|
|
144
|
+
clean = forward(model, survey)
|
|
145
|
+
s = error.log_std(clean / survey.geometric_factor)
|
|
146
|
+
noisy = clean * np.exp(s * generator.standard_normal(clean.size))
|
|
147
|
+
return cls(survey=survey, rhoa=noisy, error=error)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def n_data(self) -> int:
|
|
151
|
+
"""Number of readings."""
|
|
152
|
+
return int(self.rhoa.size)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def resistance(self) -> NDArray[np.float64]:
|
|
156
|
+
r"""Transfer resistance :math:`R = \rho_a / K`, in ohms."""
|
|
157
|
+
return self.rhoa / self.survey.geometric_factor
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def log_data(self) -> NDArray[np.float64]:
|
|
161
|
+
r"""The inverted data :math:`d = \ln \rho_a`."""
|
|
162
|
+
return np.log(self.rhoa)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def log_std(self) -> NDArray[np.float64]:
|
|
166
|
+
r"""Standard deviation of :math:`\ln \rho_a` for each reading."""
|
|
167
|
+
return self.error.log_std(self.resistance)
|
ertkit/ves/forward.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
r"""Forward modeling of vertical electrical soundings over a layered earth.
|
|
2
|
+
|
|
3
|
+
The potential on the surface at distance :math:`r` from a point source of
|
|
4
|
+
current :math:`I` is
|
|
5
|
+
|
|
6
|
+
.. math::
|
|
7
|
+
|
|
8
|
+
V(r) = \frac{I}{2\pi} \int_0^\infty \mathcal{T}_1(\lambda) J_0(\lambda r) \, d\lambda,
|
|
9
|
+
|
|
10
|
+
where :math:`\mathcal{T}_1` is the resistivity transform of the layered earth
|
|
11
|
+
(Pekeris 1940; Koefoed 1979). The integral is evaluated with the 120-point
|
|
12
|
+
digital linear filter of Guptasarma and Singh (1997), taken from libdlf
|
|
13
|
+
(https://github.com/emsig/libdlf; filter values licensed CC BY 4.0). Its
|
|
14
|
+
relative error is below about 1e-6 for resistivity contrasts up to 1e4 between
|
|
15
|
+
layers, and grows in proportion to the contrast beyond that.
|
|
16
|
+
|
|
17
|
+
The functions here accept complex resistivities and thicknesses, so that
|
|
18
|
+
derivatives can be computed by the complex-step method.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from functools import cache
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import libdlf
|
|
28
|
+
import numpy as np
|
|
29
|
+
from numpy.typing import ArrayLike, NDArray
|
|
30
|
+
|
|
31
|
+
from ertkit.ves.model import LayeredModel
|
|
32
|
+
from ertkit.ves.survey import Survey
|
|
33
|
+
|
|
34
|
+
__all__ = ["forward", "hankel_j0", "potential", "resistivity_transform"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cache
|
|
38
|
+
def _filter() -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
39
|
+
"""Base and J0 weights of the Guptasarma and Singh (1997) 120-point filter."""
|
|
40
|
+
base, weights = libdlf.hankel.gupt_120_1997()
|
|
41
|
+
return np.asarray(base, dtype=np.float64), np.asarray(weights, dtype=np.float64)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _tanh(z: NDArray[Any]) -> NDArray[Any]:
|
|
45
|
+
"""Hyperbolic tangent that is safe for complex arguments with Re(z) >= 0.
|
|
46
|
+
|
|
47
|
+
numpy's complex ``tanh`` overflows for large real parts on some platforms
|
|
48
|
+
(Windows). Since :math:`\\lambda h` always has a positive real part here,
|
|
49
|
+
:math:`\\tanh z = -\\operatorname{expm1}(-2z)/(2 + \\operatorname{expm1}(-2z))`,
|
|
50
|
+
which cannot overflow, is used for complex input.
|
|
51
|
+
"""
|
|
52
|
+
if not np.iscomplexobj(z):
|
|
53
|
+
return np.tanh(z)
|
|
54
|
+
em = np.expm1(-2.0 * z)
|
|
55
|
+
return np.asarray(-em / (2.0 + em))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resistivity_transform(
|
|
59
|
+
lam: ArrayLike, resistivity: ArrayLike, thickness: ArrayLike
|
|
60
|
+
) -> NDArray[Any]:
|
|
61
|
+
r"""Resistivity transform :math:`\mathcal{T}_1(\lambda)` of a layered earth.
|
|
62
|
+
|
|
63
|
+
Computed by the recurrence
|
|
64
|
+
|
|
65
|
+
.. math::
|
|
66
|
+
|
|
67
|
+
\mathcal{T}_N = \rho_N, \qquad
|
|
68
|
+
\mathcal{T}_i = \frac{\mathcal{T}_{i+1} + \rho_i \tanh(\lambda h_i)}
|
|
69
|
+
{1 + \mathcal{T}_{i+1} \tanh(\lambda h_i)/\rho_i},
|
|
70
|
+
\qquad i = N-1, \dots, 1.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
lam : array_like
|
|
75
|
+
Hankel-transform variable :math:`\lambda`, in 1/m; any shape.
|
|
76
|
+
resistivity : array_like
|
|
77
|
+
Layer resistivities in ohm meters, length :math:`N`.
|
|
78
|
+
thickness : array_like
|
|
79
|
+
Thicknesses in meters of the upper :math:`N - 1` layers.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
ndarray
|
|
84
|
+
:math:`\mathcal{T}_1(\lambda)` in ohm meters, with the shape of ``lam``.
|
|
85
|
+
Complex if any input is complex.
|
|
86
|
+
"""
|
|
87
|
+
rho = np.asarray(resistivity)
|
|
88
|
+
h = np.asarray(thickness)
|
|
89
|
+
lam = np.asarray(lam)
|
|
90
|
+
if rho.ndim != 1 or h.shape != (rho.size - 1,):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"need {rho.size - 1} thicknesses for {rho.size} resistivities, "
|
|
93
|
+
f"got shape {h.shape}"
|
|
94
|
+
)
|
|
95
|
+
out = np.broadcast_to(rho[-1], lam.shape).astype(np.result_type(lam, rho, h, 1.0))
|
|
96
|
+
for i in range(rho.size - 2, -1, -1):
|
|
97
|
+
# A thickness with no imaginary part (all but the perturbed one, in a
|
|
98
|
+
# complex-step derivative) takes the much faster real tanh.
|
|
99
|
+
h_i = h[i].real if np.iscomplexobj(h) and h[i].imag == 0 else h[i]
|
|
100
|
+
t = _tanh(lam * h_i)
|
|
101
|
+
out = (out + rho[i] * t) / (1.0 + out * t / rho[i])
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def hankel_j0(
|
|
106
|
+
kernel: Callable[[NDArray[np.float64]], NDArray[Any]], r: ArrayLike
|
|
107
|
+
) -> NDArray[Any]:
|
|
108
|
+
r"""Zeroth-order Hankel transform by digital linear filtering.
|
|
109
|
+
|
|
110
|
+
Approximates
|
|
111
|
+
|
|
112
|
+
.. math::
|
|
113
|
+
|
|
114
|
+
F(r) = \int_0^\infty f(\lambda) J_0(\lambda r) \, d\lambda
|
|
115
|
+
\approx \frac{1}{r} \sum_j f(b_j / r) \, w_j,
|
|
116
|
+
|
|
117
|
+
with the base :math:`b_j` and weights :math:`w_j` of the 120-point filter
|
|
118
|
+
of Guptasarma and Singh (1997).
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
kernel : callable
|
|
123
|
+
The function :math:`f`. It receives an array of :math:`\lambda` values
|
|
124
|
+
of shape ``(len(r), 120)`` and must return an array of the same shape.
|
|
125
|
+
r : array_like
|
|
126
|
+
Positive distances in meters, one-dimensional.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
ndarray
|
|
131
|
+
:math:`F(r)`, one value per distance.
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
ValueError
|
|
136
|
+
If a distance is not positive and finite.
|
|
137
|
+
|
|
138
|
+
Notes
|
|
139
|
+
-----
|
|
140
|
+
Filter values from libdlf (https://github.com/emsig/libdlf), licensed
|
|
141
|
+
CC BY 4.0.
|
|
142
|
+
"""
|
|
143
|
+
base, weights = _filter()
|
|
144
|
+
r = np.atleast_1d(np.asarray(r, dtype=np.float64))
|
|
145
|
+
if r.ndim != 1 or np.any(~(r > 0)) or not np.all(np.isfinite(r)):
|
|
146
|
+
raise ValueError("distances must be a one-dimensional array of positive values")
|
|
147
|
+
lam = base[np.newaxis, :] / r[:, np.newaxis]
|
|
148
|
+
values = np.asarray(kernel(lam))
|
|
149
|
+
# An explicit sum rather than a matrix product: some BLAS builds (Accelerate
|
|
150
|
+
# with numpy 2.0) raise spurious floating-point warnings in matmul.
|
|
151
|
+
return np.asarray((values * weights).sum(axis=-1) / r)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _rhoa(
|
|
155
|
+
resistivity: NDArray[Any],
|
|
156
|
+
thickness: NDArray[Any],
|
|
157
|
+
ab2: NDArray[np.float64],
|
|
158
|
+
mn2: NDArray[np.float64],
|
|
159
|
+
) -> NDArray[Any]:
|
|
160
|
+
"""Apparent resistivity of symmetric soundings; complex inputs allowed."""
|
|
161
|
+
near = ab2 - mn2
|
|
162
|
+
far = ab2 + mn2
|
|
163
|
+
r = np.concatenate([near, far])
|
|
164
|
+
v = hankel_j0(lambda lam: resistivity_transform(lam, resistivity, thickness), r) / (
|
|
165
|
+
2.0 * np.pi
|
|
166
|
+
)
|
|
167
|
+
v_near, v_far = v[: ab2.size], v[ab2.size :]
|
|
168
|
+
# Delta V = V(AM) - V(BM) - V(AN) + V(BN) = 2 [V(near) - V(far)] for I = 1 A.
|
|
169
|
+
k = 2.0 * np.pi / (2.0 / near - 2.0 / far)
|
|
170
|
+
return np.asarray(k * 2.0 * (v_near - v_far))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def potential(
|
|
174
|
+
model: LayeredModel, r: ArrayLike, current: float = 1.0
|
|
175
|
+
) -> NDArray[np.float64]:
|
|
176
|
+
"""Surface potential at distance ``r`` from a point current source.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
model : LayeredModel
|
|
181
|
+
The layered earth.
|
|
182
|
+
r : array_like
|
|
183
|
+
Distances in meters from the source, positive.
|
|
184
|
+
current : float, optional
|
|
185
|
+
Source current in amperes. Default 1.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
ndarray of float
|
|
190
|
+
Potential in volts, one value per distance.
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
ValueError
|
|
195
|
+
If a distance is not positive and finite.
|
|
196
|
+
"""
|
|
197
|
+
r = np.atleast_1d(np.asarray(r, dtype=np.float64))
|
|
198
|
+
if np.any(~(r > 0)) or not np.all(np.isfinite(r)):
|
|
199
|
+
raise ValueError("distances must be positive and finite")
|
|
200
|
+
integral = hankel_j0(
|
|
201
|
+
lambda lam: resistivity_transform(lam, model.resistivity, model.thickness), r
|
|
202
|
+
)
|
|
203
|
+
return np.asarray(current * integral.real / (2.0 * np.pi), dtype=np.float64)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def forward(model: LayeredModel, survey: Survey) -> NDArray[np.float64]:
|
|
207
|
+
"""Apparent resistivities of a sounding over a layered earth.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
model : LayeredModel
|
|
212
|
+
The layered earth.
|
|
213
|
+
survey : Survey
|
|
214
|
+
The readings, with their half-spacings.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
ndarray of float
|
|
219
|
+
Apparent resistivity in ohm meters, one value per reading. The finite
|
|
220
|
+
potential-electrode spacing is modeled exactly.
|
|
221
|
+
|
|
222
|
+
Examples
|
|
223
|
+
--------
|
|
224
|
+
>>> import numpy as np
|
|
225
|
+
>>> from ertkit.ves import LayeredModel, Survey, forward
|
|
226
|
+
>>> survey = Survey.schlumberger(ab2=[3.0, 30.0, 300.0], mn2=0.5)
|
|
227
|
+
>>> model = LayeredModel(resistivity=[100.0], thickness=[])
|
|
228
|
+
>>> np.allclose(forward(model, survey), 100.0)
|
|
229
|
+
True
|
|
230
|
+
"""
|
|
231
|
+
rhoa = _rhoa(model.resistivity, model.thickness, survey.ab2, survey.mn2)
|
|
232
|
+
return np.asarray(rhoa.real, dtype=np.float64)
|