|
16 | 16 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
17 | 17 | # License for the specific language governing permissions and limitations under
|
18 | 18 | # the License.
|
| 19 | +from copy import deepcopy |
19 | 20 | from concurrent.futures import ProcessPoolExecutor
|
20 | 21 |
|
| 22 | +import numpy as np |
| 23 | + |
21 | 24 | try:
|
22 | 25 | import tqdm
|
23 | 26 | import tqdm.contrib.concurrent
|
24 | 27 | except ImportError:
|
25 | 28 | tqdm = None
|
26 | 29 |
|
27 |
| -from emg3d import io, solver |
| 30 | +from emg3d import io, solver, utils |
28 | 31 |
|
29 | 32 |
|
30 | 33 | def process_map(fn, *iterables, max_workers, **kwargs):
|
@@ -143,3 +146,314 @@ def solve(inp):
|
143 | 146 | return fname, fname
|
144 | 147 | else:
|
145 | 148 | return efield, info
|
| 149 | + |
| 150 | + |
| 151 | +@utils._requires('empymod') |
| 152 | +def layered(inp): |
| 153 | + """Returns response or gradient using layered models; for a `process_map`. |
| 154 | +
|
| 155 | + Used within a Simulation to call empymod in parallel for layered models. |
| 156 | + Depending on the input it returns either the forward responses or the |
| 157 | + finite-difference gradient. |
| 158 | +
|
| 159 | + The parameters section describes the content of the input dict. |
| 160 | +
|
| 161 | + Parameters |
| 162 | + ---------- |
| 163 | + model : Model |
| 164 | + The model; a :class:`emg3d.models.Model` instance. Must be isotropic or |
| 165 | + VTI. |
| 166 | +
|
| 167 | + src : Tx* |
| 168 | + Any dipole or point source of the available sources, e.g., |
| 169 | + :class:`emg3d.electrodes.TxElectricDipole`. |
| 170 | +
|
| 171 | + receivers : dict of Rx* |
| 172 | + Receiver dict (:attr:`emg3d.surveys.Survey.receivers`). |
| 173 | +
|
| 174 | + frequencies : dict |
| 175 | + Frequency dict (:attr:`emg3d.surveys.Survey.frequencies`). |
| 176 | +
|
| 177 | + empymod_opts : dict |
| 178 | + Options passed to empymod ({src;rec}pts, {h;f}t, {h;f}targ, xdirect, |
| 179 | + loop). |
| 180 | +
|
| 181 | + observed : DataArray |
| 182 | + Observed data of this source. |
| 183 | +
|
| 184 | + layered_opts : dict |
| 185 | + Options passed to :attr:`emg3d.models.Model.extract_1d`. |
| 186 | +
|
| 187 | + gradient : bool |
| 188 | + If False, the electromagnetic responses are returned; if True, the |
| 189 | + gradient is returned. |
| 190 | +
|
| 191 | + If True, the following things _have_ to be provided: ``observed``, |
| 192 | + ``weights``, and ``residual``; otherwise a zero gradient is returned. |
| 193 | +
|
| 194 | + weights : DataArray, optional |
| 195 | + Data weights corresponding to the data; only required if |
| 196 | + ``gradient=True``. |
| 197 | +
|
| 198 | + residual : DataArray |
| 199 | + Residuals using the current model; only required if ``gradient=True``. |
| 200 | +
|
| 201 | +
|
| 202 | + Returns |
| 203 | + ------- |
| 204 | + out : ndarray |
| 205 | + If ``gradient=False``, the output are the electromagnetic responses |
| 206 | + (synthetic data) of shape (nrec, nfreq). |
| 207 | +
|
| 208 | + If ``gradient=True``, the output is the finite-difference gradient of |
| 209 | + shape (3, nx, ny, nz). |
| 210 | +
|
| 211 | + """ |
| 212 | + |
| 213 | + # Extract input. |
| 214 | + model = inp['model'] |
| 215 | + src = inp['src'] |
| 216 | + receivers = inp['receivers'] |
| 217 | + frequencies = np.array([f for f in inp['frequencies'].values()]) |
| 218 | + empymod_opts = inp['empymod_opts'] |
| 219 | + observed = inp['observed'] |
| 220 | + lopts = deepcopy(inp['layered_opts']) |
| 221 | + gradient = inp['gradient'] |
| 222 | + |
| 223 | + # Get method and set to return_imat. |
| 224 | + method = lopts.pop('method') |
| 225 | + lopts['return_imat'] = True |
| 226 | + |
| 227 | + # Collect rec-independent empymod options. |
| 228 | + empymod_opts = { |
| 229 | + # User input ({src;rec}pts, {h;f}t, {h;f}targ, xdirect, loop). |
| 230 | + # Contains also verb from simulation class. |
| 231 | + **empymod_opts, |
| 232 | + # |
| 233 | + # Source properties, same for all receivers. |
| 234 | + 'src': src.coordinates, |
| 235 | + 'msrc': src.xtype != 'electric', |
| 236 | + 'strength': src.strength, |
| 237 | + # |
| 238 | + # Enforced properties (not implemented). |
| 239 | + 'signal': None, |
| 240 | + 'epermV': None, |
| 241 | + 'mpermV': None, |
| 242 | + 'squeeze': True, |
| 243 | + } |
| 244 | + |
| 245 | + # Create some flags. |
| 246 | + epsilon_r = model.epsilon_r is not None |
| 247 | + mu_r = model.mu_r is not None |
| 248 | + vti = model.case == 'VTI' |
| 249 | + |
| 250 | + # Pre-allocate output array. |
| 251 | + if gradient: |
| 252 | + # Gradient. |
| 253 | + out = np.zeros((3, *model.shape)) |
| 254 | + |
| 255 | + # Get weights and residual if the gradient is wanted. |
| 256 | + weights = inp.get('weights', None) |
| 257 | + residual = inp.get('residual', None) |
| 258 | + if weights is None or residual is None or observed is None: |
| 259 | + return out |
| 260 | + |
| 261 | + else: |
| 262 | + # Responses. |
| 263 | + out = np.full((len(receivers), frequencies.size), np.nan+1j*np.nan) |
| 264 | + |
| 265 | + # Loop over receivers. |
| 266 | + for i, (rkey, rec) in enumerate(receivers.items()): |
| 267 | + |
| 268 | + # Check observed data, limit to finite values if provided. |
| 269 | + if observed is not None: |
| 270 | + fi = np.isfinite(observed.loc[rkey, :].data) |
| 271 | + if fi.sum() == 0: |
| 272 | + continue |
| 273 | + freqs = frequencies[fi] |
| 274 | + |
| 275 | + # Generating obs data for all. |
| 276 | + else: |
| 277 | + fi = np.ones(frequencies.size, dtype=bool) |
| 278 | + freqs = frequencies |
| 279 | + |
| 280 | + # Get 1D model. |
| 281 | + # Note: if method='source', this would be faster outside the loop. |
| 282 | + oned, imat = model.extract_1d(**_get_points(method, src, rec), **lopts) |
| 283 | + |
| 284 | + # Collect input. |
| 285 | + empymod_inp = { |
| 286 | + **empymod_opts, |
| 287 | + 'rec': rec.coordinates, |
| 288 | + 'mrec': rec.xtype != 'electric', |
| 289 | + 'depth': oned.grid.nodes_z[1:-1], |
| 290 | + 'freqtime': freqs, |
| 291 | + 'epermH': None if not epsilon_r else oned.epsilon_r[0, 0, :], |
| 292 | + 'mpermH': None if not mu_r else oned.mu_r[0, 0, :], |
| 293 | + } |
| 294 | + |
| 295 | + # Get horizontal and vertical conductivities. |
| 296 | + map2cond = oned.map.backward |
| 297 | + cond_h = map2cond(oned.property_x[0, 0, :]) |
| 298 | + cond_v = None if not vti else map2cond(oned.property_z[0, 0, :]) |
| 299 | + |
| 300 | + # Compute gradient. |
| 301 | + if gradient: |
| 302 | + |
| 303 | + # Get misfit of this src-rec pair. |
| 304 | + obs = observed.loc[rkey, :].data[fi] |
| 305 | + wgt = weights.loc[rkey, :].data[fi] |
| 306 | + res = residual.loc[rkey, :].data[fi] |
| 307 | + misfit = np.sum(wgt*(res.conj()*res)).real/2 |
| 308 | + |
| 309 | + # Get horizontal gradient. |
| 310 | + out[0, ...] += _fd_gradient(cond_h, cond_v, obs, wgt, misfit, |
| 311 | + empymod_inp, imat, vertical=False) |
| 312 | + |
| 313 | + # Get vertical gradient if VTI. |
| 314 | + if vti: |
| 315 | + out[2, ...] += _fd_gradient(cond_h, cond_v, obs, wgt, misfit, |
| 316 | + empymod_inp, imat, vertical=True) |
| 317 | + |
| 318 | + # Compute response. |
| 319 | + else: |
| 320 | + out[i, fi] = _empymod_fwd(cond_h, cond_v, empymod_inp) |
| 321 | + |
| 322 | + return out |
| 323 | + |
| 324 | + |
| 325 | +@utils._requires('empymod') |
| 326 | +def _empymod_fwd(cond_h, cond_v, empymod_inp): |
| 327 | + """Thin wrapper for empymod.bipole(). |
| 328 | +
|
| 329 | + Parameters |
| 330 | + ---------- |
| 331 | + cond_h, cond_v : ndarray |
| 332 | + Horizontal and vertical conductivities (S/m). ``cond_v`` can be None, |
| 333 | + in which case an isotropic medium is assumed. |
| 334 | +
|
| 335 | + empymod_inp : dict |
| 336 | + Passed through to :func:`empymod.model.bipole`. Any parameter except |
| 337 | + for ``res`` and ``aniso``. |
| 338 | +
|
| 339 | +
|
| 340 | + Returns |
| 341 | + ------- |
| 342 | + resp : EMArray |
| 343 | + Electromagnetic field as returned from :func:`empymod.model.bipole`. |
| 344 | +
|
| 345 | +
|
| 346 | + """ |
| 347 | + from empymod import bipole |
| 348 | + aniso = None if cond_v is None else np.sqrt(cond_h/cond_v) |
| 349 | + return bipole(res=1/cond_h, aniso=aniso, **empymod_inp) |
| 350 | + |
| 351 | + |
| 352 | +def _get_points(method, src, rec): |
| 353 | + """Returns correct method and points for model.extract_1d. |
| 354 | +
|
| 355 | + Parameters |
| 356 | + ---------- |
| 357 | + method : str |
| 358 | + All methods accepted by :attr:`emg3d.models.Model.extract_1d` plus |
| 359 | + ``'source'``, ``'receiver'``. |
| 360 | +
|
| 361 | + src, rec : {Tx*, Rx*) |
| 362 | + Any of the available point and dipole sources or receivers, e.g., |
| 363 | + :class:`emg3d.electrodes.TxElectricDipole`. |
| 364 | +
|
| 365 | + Returns |
| 366 | + ------- |
| 367 | + out : dict |
| 368 | + Can be passed directly to :attr:`emg3d.models.Model.extract_1d` for the |
| 369 | + parameters ``method``, ``p0``, and ``p1``. |
| 370 | +
|
| 371 | + """ |
| 372 | + |
| 373 | + # Get default points. |
| 374 | + p0 = src.center[:2] |
| 375 | + p1 = rec.center[:2] |
| 376 | + |
| 377 | + # If source or receiver, we re-set one point and rename the method |
| 378 | + if method == 'source': |
| 379 | + p1 = p0 |
| 380 | + method = 'midpoint' |
| 381 | + |
| 382 | + elif method == 'receiver': |
| 383 | + p0 = p1 |
| 384 | + method = 'midpoint' |
| 385 | + |
| 386 | + return {'method': method, 'p0': p0, 'p1': p1} |
| 387 | + |
| 388 | + |
| 389 | +def _fd_gradient(cond_h, cond_v, data, weight, misfit, empymod_inp, imat, |
| 390 | + vertical): |
| 391 | + """Computes the finite-difference gradient using empymod. |
| 392 | +
|
| 393 | + The finite difference is obtained by adding a relative difference of 0.01 % |
| 394 | + to the layer (currently hard-coded). |
| 395 | +
|
| 396 | + Parameters |
| 397 | + ---------- |
| 398 | + cond_h, cond_v : ndarray |
| 399 | + Horizontal and vertical conductivities (S/m). ``cond_v`` can be None, |
| 400 | + in which case an isotropic medium is assumed. |
| 401 | +
|
| 402 | + data : ndarray |
| 403 | + Observed data. |
| 404 | +
|
| 405 | + weight : ndarray |
| 406 | + Weights corresponding to these data. |
| 407 | +
|
| 408 | + misfit : float |
| 409 | + Misfit using the current model. |
| 410 | +
|
| 411 | + empymod_inp : dict |
| 412 | + Passed through to :func:`empymod.model.bipole`. Any parameter except |
| 413 | + for ``res`` and ``aniso``. |
| 414 | +
|
| 415 | + imat : ndarray |
| 416 | + Interpolation matrix as returned by |
| 417 | + :attr:`emg3d.models.Model.extract_1d`. |
| 418 | +
|
| 419 | + vertical : bool |
| 420 | + If True, the gradient for the vertical conductivities is assumed, |
| 421 | + otherwise the gradient for the horizontal conductivities. |
| 422 | + If ``vertical=True``, ``cond_v`` cannot be None (not checked, will fail |
| 423 | + with an AttributeError). |
| 424 | +
|
| 425 | +
|
| 426 | + Returns |
| 427 | + ------- |
| 428 | + gradient : ndarray of shape (nx, ny, nz) |
| 429 | + Gradient. |
| 430 | +
|
| 431 | +
|
| 432 | + """ |
| 433 | + # Relative difference fixed to 0.01 %; could be made an input parameter. |
| 434 | + rel_diff = 0.0001 |
| 435 | + |
| 436 | + # Loop over layers and compute FD gradient for each. |
| 437 | + grad = np.zeros(cond_h.size) |
| 438 | + for iz in range(cond_h.size): |
| 439 | + |
| 440 | + # Get 1D model. |
| 441 | + cond_p = cond_h.copy() if not vertical else cond_v.copy() |
| 442 | + |
| 443 | + # Add relative difference to the layer. |
| 444 | + delta = cond_p[iz] * rel_diff |
| 445 | + cond_p[iz] += delta |
| 446 | + |
| 447 | + # Call empymod. |
| 448 | + if vertical: |
| 449 | + response = _empymod_fwd(cond_h, cond_p, empymod_inp) |
| 450 | + else: |
| 451 | + response = _empymod_fwd(cond_p, cond_v, empymod_inp) |
| 452 | + |
| 453 | + # Calculate gradient and add it. |
| 454 | + residual = response - data |
| 455 | + fd_misfit = np.sum(weight*(residual.conj()*residual)).real/2 |
| 456 | + grad[iz] = (fd_misfit - misfit)/delta |
| 457 | + |
| 458 | + # Bring back to full grid and return. |
| 459 | + return imat[..., None] * grad[None, :] |
0 commit comments