Tutorials

Tutorial 1 Loop

Tutorial1-Loop

Tutorial 1 - The Loop class

This first tutorial aims to present the loop class to the user. The class itself represents a single closed or grounded loop of any shape in the field used for the calculation of magnetic or electric fields.

Only a few steps are necessary to initialize a loop instance, but many configurations are possible. In this sript the common methods of the loop class are presented and explained.

Start off with COMET

In [1]:
%matplotlib inline
# first we import the pyhed module from comet
from comet import pyhed as ph
# and the numeric library numpy
import numpy as np

The logger controls the console output (red background). The verbosity increases with lower values for the log level. default is 30 (warning) which means everything less important than a warning is prevented from appearing in the console. There are more verbose levels, including 20 (info), 16 (progress), 13 (trace) and 10 (debug) or more silent like 40 (error) and 50 (critical), however the last two are usually only printed when the code is to exit anyway.

In [2]:
ph.log.setLevel(20) # INFO

Defining the loop geometry

The loop is a general class that can have any shape and represents either a source or transmitter. The basic shape is defined additionally to the number of segments or maximum length for the dipole discretization. Predefined shapes utility functions are (see Loop API documentation):

  • buildCircle - perfect circular loop
  • buildRectangle/buildSquare - rectangle or square
  • buildFig8/buildFig8Circle - figure-of-eight loops
  • buildLoop - arbitrarily shaped loop
  • buildDipole/buildLine - single dipoles or a line of dipoles

We assume a 40m * 20m figure-of-eight loop, consisting of segments with a maximum length of 3m.

In [17]:
loop = ph.loop.buildFig8([[-20, -10], [20, 10]], max_length=2)
print(loop) # Show some information like positions & angles
### LOOP ###
looptype: figure8
pos:
[[-20.          -9.09090909   0.        ]
 [-20.          -7.27272727   0.        ]
 [-20.          -5.45454545   0.        ]
 ...
 [-15.45454545 -10.           0.        ]
 [-17.27272727 -10.           0.        ]
 [-19.09090909 -10.           0.        ]] (77, 3)
phi:
[1.57079633 1.57079633 1.57079633 ... 3.14159265 3.14159265 3.14159265] (77,)
ds:
[1.81818182 1.81818182 1.81818182 ... 1.81818182 1.81818182 1.81818182] (77,)
In [18]:
# We can have a look at the loop by
ax = loop.show()
# and see the position of the segments

Define 1D resistivity and frequency

Those values are set and stored in the config of the loop class. The config is a small class that allows to share the values if multiple loops are handled. By default it contains a 1000 $\Omega$m halfspace with a Larmor frequency of 2000 Hz. Mostly, the B field is used which is defined by a TE mode for an ungrounded source.

In [19]:
print(loop.config)
### LOOP CONFIGURATION ###
case:                     homogeneous halfspace
resistivity distribution: [1000.] [Ohm*m] (1)
layer thickness:          [] [m] (0)
frequency:                2000.00 [Hz]
current:                  1.0 [A]
field type:               magnetic, B
field mode:               te
In [20]:
# We set the model to a two-layer case (2nd layer infinite)
loop.setModel([1000, 10], d=[10])
# and can access those properties
print("rho =", loop.config.rho, ", d =", loop.config.d)
rho = [1000   10] , d = [10]
In [21]:
loop.setFrequency(2200)  # loop.config.f
loop.setFType('B')  # sets the field type ('B', 'H', or 'E')
print(loop.config)
### LOOP CONFIGURATION ###
case:                     layered halfspace
resistivity distribution: [1000   10] [Ohm*m] (2)
layer thickness:          [10] [m] (1)
frequency:                2200.00 [Hz]
current:                  1.0 [A]
field type:               magnetic, B
field mode:               te

Define mesh and export fields

The dipole positions of the loop don't have to be a part of any mesh. In order to calculate a field first coordinates the have to be given to the class object. The easiest way to do this is via a simple array. It is recommended to use pygimli Mesh for more sophisticated models or if values have to be interpolated afterwards.

first example: simple array

In [22]:
# If you want the field on the two coordinates (5, 5, -5) and
# (50, 50, -5), you would use the loop object like this:
coords = np.array([[5, 5, -5], [50, 50, -5]]).T
# shape: (3, n) for n points
loop.setLoopMesh(coords)
field = loop.calculate(num_cpu=1)
28/11/19 - 18:29:15 - comet - INFO - detecting non-grounded loop, set mode to "te"
28/11/19 - 18:29:15 - comet - INFO - Calculating loop field of closed figure8, 77 dipoles at (-0.0, 0.0, 0.0)
28/11/19 - 18:29:15 - comet - INFO - config: layered earth, 2 layers @ 2200.0 Hz, magnetic, B
28/11/19 - 18:29:15 - comet - INFO - Calculate directly on main core.
28/11/19 - 18:29:15 - comet - INFO - total field calculation time: 0.09 seconds
In [23]:
# the field values are stored in the variable loop.field
# we get another array of shape (3, n) for n points
# with complex valued vector field for each input coordinate
print('loop.field (shape =', loop.field.shape, '):')
print(loop.field)
loop.field (shape = (3, 2) ):
[[ 2.72089524e-08+3.42016952e-10j -1.05065987e-11-1.40105779e-11j]
 [-1.00860450e-08-2.90866750e-11j -3.83289637e-11-3.06967041e-11j]
 [ 4.66120951e-08-1.88077662e-10j -9.14102794e-11-9.68668618e-12j]]
In [24]:
# for more complex calculations we need to define more points or a mesh
# this can be done using the appropriate methods of the loops class:
# tetgen is needed for that.
loop.createLoopMesh()
# the mesh contains the source and is stored in loop.loopmesh
print(loop.loopmesh) # show mesh numbers
28/11/19 - 18:29:21 - comet - INFO - createLoopMesh()
calling: "tetgen"
  _default_LoopMesh_figure8_77.poly  -pzQAfq1.20a339.305730k
tetgen is finished: True
28/11/19 - 18:29:27 - comet - INFO - loopmesh.save: _default_LoopMesh_figure8_77.bms
Mesh: Nodes: 20001 Cells: 100891 Boundaries: 209646
In [25]:
# now we calculate the new field for the mesh, this time with more cores
field = loop.calculate(num_cpu=4)
28/11/19 - 18:29:32 - comet - INFO - detecting non-grounded loop, set mode to "te"
28/11/19 - 18:29:32 - comet - INFO - Calculating loop field of closed figure8, 77 dipoles at (-0.0, 0.0, 0.0)
28/11/19 - 18:29:32 - comet - INFO - config: layered earth, 2 layers @ 2200.0 Hz, magnetic, B
28/11/19 - 18:29:32 - comet - INFO - Start calculating in 4 parallel processes.
calc dipoles: [##########################################################] 77/77
28/11/19 - 18:29:56 - comet - INFO - Waiting for other processes to finish.
28/11/19 - 18:29:57 - comet - INFO - total field calculation time: 24.94 seconds
In [26]:
# for visualization the field can be exported as vtk.
# Note that this works only with a mesh, not with pure coordinates 
loop.exportVTK('loop.vtk')
28/11/19 - 18:30:05 - comet - INFO - save vectorfield to vtk ...
28/11/19 - 18:30:06 - comet - INFO - file name: loop.vtk (0.657 sec)

One can use Paraview (paraview.org) for visualization of the resulting magnetic field.

field_2.png

Tutorial 2 Survey and FID

Tutorial2-Survey

Tutorial 2 - the Survey and FID classes

The tutorial for the Survey class is covering the definition and adjustments for simple FID experiments as well as the site parameters as magnetic field, tx and rx definition, pulse moments and many more.

Main Focus is on the import of a dataset using simple arrays. Thus tutorial explains all the steps needed and shows where COMET stores the different information.

Two complex classes are needed directly for this and presented here. Furthermore the loop class presented in the first example are used.

This script does not calculate anything extensive. It's only purpose is to give you a feeling on how to use the survey and fid class and where to put your input information.

In [1]:
%matplotlib inline
# import main modules from comet and other classes
from comet import pyhed as ph
from comet import snmr
import numpy as np # numerics
import matplotlib.pyplot as plt # plotting

ph.log.setLevel(20) # INFO, see Loop tutorial

Importing data 1

There are several ways to import data. Here we show you the manually way, convinience functions are available for MRSMatlab files. The basic function for importing raw data need at least four arrays:

  • data (1D vector, complex),
  • error (1D vector, complex),
  • times (1D vector, float)
  • pulses (1D vector, float)

  • optionally phases (1D vector, float)

In this tutorial we take some previously saved vectors from a csv file (you have to modify the import with respect to your data file format). Numpy has some easy and very flexible options when it comes to importing ascii files, e.g.

data = np.genfromtxt('datafile', **kwargs)

See genfromtxt API doc for options.

In [2]:
# Here we simply import an npz (numpy compressed vector) file.
# importing those we get numpy arrays of various shapes (see console)
data_npz = np.load('tut2_data.npz')
# Data
data = data_npz['data']
print('data', data.shape, data.dtype)
# Error
error = data_npz['error']
print('error', error.shape, error.dtype)
# Pulses
pulses = data_npz['pulses']
print('pulses', pulses.shape, pulses.dtype)
# Phases (not needed for rotated amplitude inversion)
phases = data_npz['phases']
print('phases', phases.shape, phases.dtype)
# time vector
times = data_npz['times']
print('times', times.shape, times.dtype)
data (24, 975) complex128
error (24, 975) float64
pulses (24,) float64
phases (24,) float64
times (975,) float64

Importing data 2

Now we have several arrays and need to store them in the correct objects for COMET to use them. This is the main purpose of the survey class and the FID's. Every FID represents one measurement in the field, the survey represents the field and all the global configurations. The survey class also manages the loop classes introduced in the first tutorial.

In [3]:
# Now we create an empty survey class representing our site
site = snmr.survey.Survey()
print(site)
SNMR Survey: 0 loops , 0 soundings

The survey class is empty. It holds no references to loops or measurements so far. In order to put our data in, we need to define our measurements. This needs all the loops used on this site so we can refer to them when initializing the FID's. We now quickly add some loops to the Survey (Loop classes are explained in detail in the first tutorial).

In [4]:
# We start defining three half-overlapping loops
# (see Tutorial 1 on Loop classes)
loop0 = ph.loop.buildRectangle([[-40, -20], [0, 20]], max_length=2)
loop1 = ph.loop.buildRectangle([[-20, -20], [20, 20]], max_length=2)
loop2 = ph.loop.buildRectangle([[0, -20], [40, 20]], max_length=2)
# Three 40 * 40 square loops from -40 to 40 meter in x

# let's add them to our manager survey class and have a look
site.addLoop([loop0, loop1, loop2])
print(site)
print(site.loops)
SNMR Survey: 3 loops , 0 soundings
[closed rectangle, 84 dipoles at (-20.0, -0.0, 0.0), 40.0 x 40.0 m
 closed rectangle, 84 dipoles at (-0.0, -0.0, 0.0), 40.0 x 40.0 m
 closed rectangle, 84 dipoles at (20.0, -0.0, 0.0), 40.0 x 40.0 m]
In [5]:
ph.plot.showLoopLayout(*site.loops);

The FID class

FID stands for free induction decay. See also its API Documentation. Finally we define which loop is used as transmitter and receiver to define our NMR experiments.

In [6]:
site.createSounding(tx=0, rx=0)
# A coincident sounding using the first (in Python 0!) loop as receiver and transmitter
# let's take all combinations:
for tx_i in range(3):
    for rx_i in range(3):
        site.createSounding(tx=tx_i, rx=rx_i)

print(site.fids)
# Note that the sounding using tx=0 and rx=0 is not created again.
[FID(Tx: 0, Rx: 0), FID(Tx: 0, Rx: 1), FID(Tx: 0, Rx: 2), FID(Tx: 1, Rx: 0), FID(Tx: 1, Rx: 1), FID(Tx: 1, Rx: 2), FID(Tx: 2, Rx: 0), FID(Tx: 2, Rx: 1), FID(Tx: 2, Rx: 2)]

As we are setting everthing up, we can might as well take a small detour to define our earth and resistivity.

For computing the NMR kernel, we require the strength and orientation of the magnetic field, given by the total field and its inclination and the declination of the profile. Note that this will set the frequency of the loops to the corresponding Larmor frequency.

In [7]:
# earth magnetic field at site
site.setEarth(mag=48000e-9, incl=60, decl=0)
print(site.earth)

# resistivtiy distribution at site
# 1 Layer case (1000 Ohmm, 10m thickness) + halfspace (10 Ohmm)
# take larmor frequency from earth magnetic field
config = ph.config(rho=[1000, 10], d=[10], f=site.earth.larmor)
# config is also a class, however it is little more than a container...
# ... but it can be piped to the survey class
site.setLoopConfig(config)
# This sets the resistivity configuration for all loops at once
# let's check the first loop right away
print(site.loops[0].config)
### EARTH ###
Inclination: 1.047 rad (60°  0'  0")
Declination: 0.000 rad ( 0°  0'  0")
Magnitude:   48000e-9 Tesla
### LOOP CONFIGURATION ###
case:                     layered halfspace
resistivity distribution: [1000   10] [Ohm*m] (2)
layer thickness:          [10] [m] (1)
frequency:                2043.69 [Hz]
current:                  1.0 [A]
field type:               magnetic, B
field mode:               te

Finally the data

The FID's are the target for our array imported above. We show this at the first FID as example.

In [8]:
# we just grab the first one
fid = site.fids[0]
print(fid)
FID(Tx: 0, Rx: 0)
In [9]:
# Set data, error and times, phases (unrotated)
fid.setRawDataErrorAndTimes(data, error, times, phases=phases,
                            rotated=False)
# Pulse moments (in Ampere-seconds, NOT amperes!)
fid.setPulses(pulses)
# Note that if no pulse vector is given, a default one is available 
# (e.g. for synthetic studies)

# For gating the data, define the number of desired gates
# (sometimesthe first gates are empty and therefore deleted, 
# so you might end up a few gates short)
fid.gating(num_gates=42)
print(len(fid.gates), "gates")
# plot example for one fid
# make figure with one axis to plot stuff into
fig, ax = plt.subplots()
# fills a given plot axis with a gated fid plot
ax, cb = ph.plot.drawFid(ax, fid, to_plot='abs')
36 gates

Some last things

There is more to the FID class. Here are a few examples on how to change the settings.

In [10]:
# There are a lot of setter functions, e.g.

fid.setFrequencyOffset(2.3) # in Hertz

# definition of time gates, if gating is not used
fid.setGates([0, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0], midpoints=False)
# first gate between 0 and 0.1, second between 0.1 and 0.2 and so on...
# Note that instead of the gate centers its boundaries are set as
# the gate length is needed to compute individual noise levels.

# set pulse (s) and deadtime (s) of the measuring device
fid.setPulseDuration(0.04, deadtime_device=0.005)
# the effective deadtime is calculated internally summing up the deadtime
# of the device and half of the pulse duration
print('Effective deadtime: {:.3f} s'.format(fid.deadtime))

# Define the transmitter/receiver indices and turns of the loop
fid.setTx(0, turns=1)  # stored in fid.tx_index and fid.tx_turns

# same for rx
fid.setRx(1, turns=1)  # stored in fid.rx_index and fid.rx_turns

# Note: set tx and rx can be used if the loops of the survey class are imported after the FID data,
# however usually you define them at the creation of the fids via "createSounding(tx=...)"
# as shown above.
print(fid)
Effective deadtime: 0.025 s
FID(Tx: 0, Rx: 1)

Tutorial 3 Kernel

Tutorial3-Kernel

Tutorial 3 - The Kernel class

After importing/defining everything in the Survey and FID classes the kernel class is used for the computation of the kernel functions.

If a simple synthetic case is conducted, the class remains hidden, however it is important to know how it works and what it does understand COMET.

In [3]:
# make sure graphics are plotted
%matplotlib inline 
# import comet submodules
from comet import pyhed as ph
from comet import snmr
import numpy as np # numerics
import matplotlib.pyplot as plt # plotting

ph.log.setLevel(20)

Setting up the geometry

We first initialize some material that is covered by the first two tutorials. For the kernel, we assume three half overlapping 20x20m sized loops. All 9 combinations for FID's are considered.

In [4]:
loop0 = ph.loop.buildRectangle([[-40, -20], [0, 20]], max_length=2)
loop1 = ph.loop.buildRectangle([[-20, -20], [20, 20]], max_length=2)
loop2 = ph.loop.buildRectangle([[0, -20], [40, 20]], max_length=2)
site = snmr.survey.Survey()
site.addLoop([loop0, loop1, loop2])
site.setEarth(mag=48000e-9, incl=60, decl=0)
config = ph.config(rho=[1000, 10], d=[10], f=site.earth.larmor)
site.setLoopConfig(config)
for tx_i in range(3):
    for rx_i in range(3):
        site.createSounding(tx=tx_i, rx=rx_i)

print(site)
SNMR Survey: 3 loops , 9 soundings
In [6]:
# Now we create a 1D kernel for the first FID
kern = site.createKernel(fid=0, dimension=1)
# This returns a Kernel class. let's take a look
print(kern)
# It contains references to the survey, loops and FIDs.
# Changes of survey parameters are made there and piped to the kernel.
survey: SNMR Survey: 3 loops , 9 soundings
fid: FID(Tx: 0, Rx: 0)
earth: earth, incl: 60.0°, decl: 0.0°, mag: 48000 nT
Tx: closed rectangle, 84 dipoles at (-20.0, -0.0, 0.0), 40.0 x 40.0 m
config: layered earth, 2 layers @ 2043.6869791707677 Hz, magnetic, B
Rx: coincident
dimension: 1\nKernel Matrix not calculated yet.
In [ ]:
# Let's have a look at the features of the kernel class,
# e.g. z discretization for a 1D kernel.
kern.createZVector(90, -60, min_thk=0.5)  ## better keywords?

This creates a z-vector in 90 steps up to 60 meter depth. The layers are spaced using a sinus hyperbolicus function (more linear compared to log, which is nicer at larger depth). The min_thk specifies the thickness of the final kernel discretization. Small layers around z = 0 are combined until each layer is at least 0.5m thick, which is better for inversions later. By summing up the values for the small thicknesses we obtain a more accurate kernel function in the shallow subsurface. By kern.setZVector() the z vector can be set arbitrarily.

In [8]:
print(kern.zvec) # z always positive upwards
[  0.          -0.5573965   -1.11068032  -1.65288414  -2.23159808
  -2.80657824  -3.31988333  -3.91791712  -4.61594236  -5.14552247
  -5.73339912  -6.38623268  -7.11141949  -7.91717558  -8.81262984
  -9.80792736 -10.3464936  -10.91434444 -11.51308711 -12.1444163
 -12.8101189  -13.51207911 -14.25228376 -15.03282791 -15.85592081
 -16.72389212 -17.63919855 -18.60443076 -19.62232074 -20.69574951
 -21.8277553  -23.02154211 -24.28048884 -25.60815878 -27.00830976
 -28.48490476 -30.0421231  -31.68437233 -33.41630064 -35.24281007
 -37.16907034 -39.20053353 -41.34294946 -43.602382   -45.98522623
 -48.49822651 -51.14849562 -53.94353485 -56.89125525 -60.        ]
In [10]:
# With this a mesh can be generated.
kern.create1DKernelMesh()
# There are a lot of options, however we build a default mesh.
print(kern.kernelMesh1D)
Mesh: Nodes: 12734 Cells: 25360 Boundaries: 38093
In [11]:
# Now we calulate the kernel function using 4 CPUs
kern.calculate(num_cpu=4)
# First, it requests a magnetic field for all involved loops (here only 0).
# If it does not exist none already, it will trigger the computation 
# following the supply-and-demand principle.
# This in turn requires a mesh, so it will be created.
# Last, the kernel for FID 0 is computed.
# Even the calls 
# kern.create1DKernelMesh()
# kern.createZVector(...) 
# are not needed, because they will be triggered using default parameters.
27/11/19 - 08:39:03 - comet - INFO - Calculating magnetic field of tx for 1D-kernel on nodes. ...
27/11/19 - 08:39:03 - comet - INFO - No Loop Mesh given or found. Creating.
27/11/19 - 08:39:03 - comet - INFO - createLoopMesh()
calling: "tetgen"
  _default_LoopMesh_rectangle_84.poly  -pzQAfq1.20a674.180326k
tetgen is finished: True
27/11/19 - 08:39:14 - comet - INFO - loopmesh.save: _default_LoopMesh_rectangle_84.bms
27/11/19 - 08:39:14 - comet - INFO - detecting non-grounded loop, set mode to "te"
27/11/19 - 08:39:14 - comet - INFO - Calculating loop field of closed rectangle, 84 dipoles at (-20.0, -0.0, 0.0), 40.0 x 40.0 m
27/11/19 - 08:39:14 - comet - INFO - config: layered earth, 2 layers @ 2043.6869791707677 Hz, magnetic, B
27/11/19 - 08:39:14 - comet - INFO - Start calculating in 4 parallel processes.
27/11/19 - 08:40:41 - comet - INFO - total field calculation time: 98.36 seconds
27/11/19 - 08:40:41 - comet - INFO - Get 90 slices... start calculating in 4 parallel processes.
27/11/19 - 08:41:07 - comet - INFO - overall calculation time: 124.3987 seconds
In [13]:
# Have a look at the kernels properties 
print(kern)
survey: SNMR Survey: 3 loops , 9 soundings
fid: FID(Tx: 0, Rx: 0)
earth: earth, incl: 60.0°, decl: 0.0°, mag: 48000 nT
Tx: closed rectangle, 84 dipoles at (-20.0, -0.0, 0.0), 40.0 x 40.0 m
config: layered earth, 2 layers @ 2043.6869791707677 Hz, magnetic, B
Rx: coincident
dimension: 1
Kernel Matrix pulse Moments: 24 
Kernel Matrix layers: 90
In [16]:
# and at the kernel matrix itself
kern.show(toplot=['real', 'imag', '0D']);
# alternatively, amplitude and phase kernels can be plotted
kern.show(toplot=['amp', 'phase']);
27/11/19 - 08:45:24 - comet - INFO - Kernel.show(1D): pcolormesh
27/11/19 - 08:45:24 - comet - INFO - Kernel.show(1D): pcolormesh
In [17]:
kern.save(savename='kern_fid_0')
# saves kernel class including mesh and kernel matrix, but excluding survey
# and FID (just the references are saved == indices to the correct loops
# and the FID). A backup of the pulse moments is saved with the kernel but
# discarded, once the survey is loaded and connected back to the kernel.
27/11/19 - 08:47:11 - comet - INFO - Save Kernel: kern_fid_0
27/11/19 - 08:47:11 - comet - INFO - save kernelmesh1D: kern_fid_0_kern1D.bms
In [19]:
# At a later stage, we can create a new kernel by loading
kern2 = snmr.kernel.Kernel(name='kern_fid_0')
print(kern2)
# Note the empty survey class and a None FID, no loops are known,
# plotting and inversion would be possible, but a recalculation fails
27/11/19 - 08:48:05 - comet - INFO - load file: c:\Guenther.T\src\comet\tutorials\kern_fid_0.npz
survey: SNMR Survey: 0 loops , 0 soundings
fid: None
earth: earth, incl: 60.0°, decl: 2.0°, mag: 48000 nT
Tx: None
Rx: coincident
dimension: 1
Kernel Matrix pulse Moments: 24 
Kernel Matrix layers: 90
In [23]:
# We can fix this by reconnecting with survey and FID
kern2.setSurvey(site, fid=0)
print(kern2)
# now we are back at the previous stage (kern2==kern)
survey: SNMR Survey: 3 loops , 9 soundings
fid: FID(Tx: 0, Rx: 0)
earth: earth, incl: 60.0°, decl: 0.0°, mag: 48000 nT
Tx: closed rectangle, 84 dipoles at (-20.0, -0.0, 0.0), 40.0 x 40.0 m
config: layered earth, 2 layers @ 2043.6869791707677 Hz, magnetic, B
Rx: coincident
dimension: 1
Kernel Matrix pulse Moments: 24 
Kernel Matrix layers: 90