Magnetic Dipole App#
Now that we have learned how to interact with geoh5
and to create a custom user-interface with ui.json
,
we can start implementing our first application. We are going to create a a magnetic dipole simulator from scratch,
which could become a useful tool to interpret magnetic data maps.
Background#
Python needs to import external libraries for certain classes and functions to be recognized when running the code. That is done below.
# Import the necessary packages
from __future__ import annotations
import json
import numpy as np
from geoh5py.data import Data
from geoh5py.objects import ObjectBase, Points
from geoh5py.ui_json import InputFile, constants, templates
from geoh5py.ui_json.utils import monitored_directory_copy
from geoh5py.workspace import Workspace
from training import assets_path
The physics#
The strength of the magnetic field flux of a single dipole is:
where \(\mu_0\) is a constant (\(4 \pi \times 10^{-7}\)), \(\mathbf{m}\) is the magnetic dipole moment (susceptibility \(\times\) volume), and \(\mathbf{r}\) is the vector between the dipole and the locations.
In a geophysical context however, we normally get Total Magnetic Intensity (TMI) data from magnetometers. The conversion can be approximated by a projection of the fields onto the direction of Earth’s inducing field.
Arrays#
We will make use a few useful methods on numpy
arrays.
The
sum()
method applied to a specific axis of an array, collapsing the dimension.The
dot()
method performs the dot product between two arraysThe transpose (
.T
) method to get the dimensions of arrays to align:
\([1 \times 3] \cdot [M \times 3].T \rightarrow [1 \times M] \).
Array broadcasting (repeat) of a 1D array along a second dimension using the
[:, None]
indexing:
\([N \times 3] \;/\;([1 \times N][: None]) \rightarrow [N \times 3] \)
To-Do#
In order to compute the physics of dipoles, we will need:
Functions:
Converting dip/azimuth angles to vector components
Computing the fields of a dipole(s)
Converting vector fields to TMI
Inputs parameters:
Receivers (observation) locations
Source dipole locations
Dipole moment values
Dipole inclination angles
Dipole declination angles
Earth’s field inclination
Earth’s field declination
Functions#
Inclination / declination to vector conversion#
We require a function to convert inclination/declination angles to Cartesian vectors. The same function will come handy later when computing the TMI projection. We need to perform the following conversion:
\( \hat x = \sin(\phi) * \cos(\theta)\)
\( \hat y = \sin(\phi) * \sin(\theta) \)
\( \hat z = \cos(\phi) \)
where \(\phi\) is the angle on the horizontal plane, and \(\theta\) is the vertical angles. Both are positive counter-clockwise with reference from the \(\hat x\)-axis, so we also need to convert the angles from the geographic convention (degree clockwise from North).
def inclination_declination_2_xyz(inclination, declination):
"""Convert inclination and declination angles (degrees) to unit vector (xyz)."""
theta = np.deg2rad((450 - declination) % 360)
phi = np.deg2rad(90 + inclination)
xyz = np.c_[np.sin(phi) * np.cos(theta), np.sin(phi) * np.sin(theta), np.cos(phi)]
return xyz
Dipole functions#
We can begin with writing the function needed to compute the magnetic field from a single dipole.
The function should be able to take as argument
- Coordinates of the source and receivers (defining 'r'),
- Parameters defining the strength (moment) and orientation (inc, dec) of the dipole source.
def b_field(source, locations, moment, inclination, declination):
"""
Compute the magnetic field components of a dipole on an array of locations.
:param source: Location of a point dipole, shape(1, 3).
:param locations: Array of observation locations, shape(n, 3).
:param moment: Dipole moment of the source (A.m^2)
:param inclination: Dipole horizontal angle, clockwise from North
:param declination: Dipole vertical angle from horizontal, positive down
:return: Array of magnetic field components, shape(n, 3)
"""
# Convert the inclination and declination to Cartesian vector
m = moment * inclination_declination_2_xyz(inclination, declination)
# Compute the radial components
rad = source - locations
# Compute |r|
dist = np.sum(rad**2.0, axis=1) ** 0.5
# mu_0 / 4 pi * 1e9 for nT
constant = 100
fields = constant * (
((np.dot(m, rad.T).T * 3 * rad) / dist[:, None] ** 5) - (m / dist[:, None] ** 3)
)
return fields
We can now test our b_field
function with existing objects present in our suncity.geoh5
project.
with Workspace(assets_path() / "suncity.geoh5") as workspace:
# get variables
source = workspace.get_entity("SunCityLocation")[0].vertices
output_entity = workspace.get_entity("SunCity")[0]
locations = output_entity.centroids
m = 50**3 * 0.05 * 57000
# compute fields
field = b_field(source, locations, m, -67, -17)
# write back to workspace
output_entity.add_data(
{
"bx": {"values": field[:, 0]},
"by": {"values": field[:, 1]},
"bz": {"values": field[:, 2]},
}
)
The b_field
function returns an array for the three components of the
magnetic field due to the dipole source.
TMI projection#
Let’s write a simple function to get the TMI (projected) data instead.
def tmi_projection(b_components, earth_field):
"""Project magnetic field onto Earth's field."""
h0 = inclination_declination_2_xyz(earth_field[0], earth_field[1])
return np.dot(h0, b_components.T)
and test it for the inducing field parameters at our test site at Suncity (Inc: -62.11, Dec: -17.9)
.
with Workspace(assets_path() / "suncity.geoh5") as workspace:
# get variables
source = workspace.get_entity("SunCityLocation")[0].vertices
output_entity = workspace.get_entity("SunCity")[0]
locations = output_entity.centroids
m = 50**3 * 0.05 * 57000
# compute fields
earth_field = np.array([-45, 0])
field = b_field(source, locations, m, earth_field[0], earth_field[1])
tmi = tmi_projection(field, earth_field)
# write back to workspace
output_entity.add_data({"tmi_45": {"values": tmi}})
/home/docs/checkouts/readthedocs.org/user_builds/mirageoscience-python-training/conda/release-0.1.0/lib/python3.10/site-packages/geoh5py/data/numeric_data.py:114: UserWarning: Input 'values' converted to a 1D array.
warn("Input 'values' converted to a 1D array.")
The Simulator#
In this section, we are going to string together the functions defined above and generalize our approach so that we can deal with different types of geoh5
objects. Let’s wrap all the previous functions into a magnetic_simulator
class.
def magnetic_simulator(
sources: ObjectBase,
receivers: ObjectBase,
moments: Data | float,
inclinations: Data | float,
declinations: Data | float,
earth_inc: float,
earth_dec: float,
):
"""
Compute the magnetic field components of dipoles on a geoh5py object.
:param sources: Points object of dipole locations.
:param receivers: Array or Points object of observation locations.
:param moments: Value or Data of dipole moments.
:param inclinations: Value or Data of dipole inclination angles.
:param declinations: Value or Data of dipole declination angles.
:param earth_inc: Earth's field inclination angle.
:param earth_dec: Earth's field declination angle.
:return b_field: List of Data entities.
"""
def locations(entity):
if hasattr(entity, "centroids"):
return entity.centroids
return entity.vertices
# Extract dipole coordinates
dipoles = locations(sources)
# Extract receiver coordinates
observations = locations(receivers)
def vectorize(entity):
if isinstance(entity, Data):
return entity.values
return np.ones(dipoles.shape[0]) * entity
# Get dipole moment values
mom = vectorize(moments)
# Get dipole inclination values
inc = vectorize(inclinations)
# Get dipole declination values
dec = vectorize(declinations)
# Loop over the dipoles
fields = np.zeros_like(observations)
for source, moment, inclination, declination in zip(dipoles, mom, inc, dec):
fields += b_field(source, observations, moment, inclination, declination)
tmi = tmi_projection(fields, (earth_inc, earth_dec))
# Add data to receiver object
data = receivers.add_data(
{
"b_x": {"values": fields[:, 0]},
"b_y": {"values": fields[:, 1]},
"b_z": {"values": fields[:, 2]},
"tmi": {"values": tmi},
}
)
return data
We now have a generic container to compute TMI data based on any type of ANALYST object.
You are now invited to edit your Points object to add more vertices and create data arrays for moment, azimuth and dips. We can now run the simulation by simply giving those entities to our class.
with Workspace(assets_path() / "suncity.geoh5") as workspace:
# get variables
source = workspace.get_entity("SunCityLocation")[0]
output_entity = workspace.get_entity("SunCity")[0]
m = 50**3 * 0.05 * 57000
#
data = magnetic_simulator(source, output_entity, m, -67, -17, -67, -17)
print(data)
[<geoh5py.data.float_data.FloatData object at 0x7f4ebd3ec6a0>, <geoh5py.data.float_data.FloatData object at 0x7f4ebd39f8b0>, <geoh5py.data.float_data.FloatData object at 0x7f4ebd39f9a0>, <geoh5py.data.float_data.FloatData object at 0x7f4ebd39e980>]
The User-Interface (ui.json)#
In the previous section, we have introduced functionality to compute the magnetic field from dipole locations. We are now going to create a user-interface (UI) to be able to run this program from a Geoscience ANALYST session. Review the UIjson section for background.
We need a ui.json to provide inputs for the following 7 parameters:
Object
defining the dipoles (sources)Object
defining the receiversData
orfloat
value for the dipole momentsData
orfloat
value for the dipole inclination anglesData
orfloat
value for the dipole declination anglesFloat
value for Earth’s field inclination angleFloat
value for Earth’s field declination angle
Let’s create a UI.json with forms for each one of those inputs
mag_ui = constants.default_ui_json.copy()
mag_ui["title"] = "Magnetic Dipole App"
mag_ui.update(
{
"sources": templates.object_parameter(label="Dipoles", mesh_type="", value=""),
"receivers": templates.object_parameter(
label="Receivers", mesh_type="", value=""
),
"moments": templates.data_parameter(
label="Dipole Moment", parent="sources", value=1.0
),
"inclination": templates.data_parameter(
label="Dipole Inclination", parent="sources", value=-62.11
),
"declination": templates.data_parameter(
label="Dipole Declination", parent="sources", value=-17.9
),
"earth_inc": templates.float_parameter(
label="Earth's field Inclination", value=-62.11
),
"earth_dec": templates.float_parameter(
label="Earth's field Declination", value=-17.9
),
}
)
# Some modifications
for label in ["moments", "inclination", "declination"]:
mag_ui[label]["isValue"] = True
mag_ui[label]["property"] = ""
We now need tell which “program” that ANALYST can call.
mag_ui["conda_environment"] = "python-training"
mag_ui["run_command"] = "mag_dipole_app"
Internally, when executed from ANALYST, this is what is going to happen
conda activate python-training
python mag_dipole
Then your mag_dipole.run should be able to do the rest: compute and store the result.
Let’s write out our ui.json to file.
with open(assets_path() / "magnetic_dipole.ui.json", "w", encoding="utf-8") as file:
json.dump(mag_ui, file, indent=4)
Try running the UI through ANALYST with a drag & drop of the file to the viewport.
The Driver (Running with Geoscience ANALYST)#
In the previous section, we created our simulation program and a user-interface using the UI json format. As a final step, we need to define a driver
program that can be called from ANALYST.
We are going to take advantage of the
geoh5py.ui_json.InputFile
class to handle value conversions between geoh5 and python, as well as the monitored_directory_copy
function to handle the live link with ANALYST.
def run(file: str):
"""
Run the mag_dipole simulation from InputFile.
"""
ifile = InputFile.read_ui_json(file).data
with ifile["geoh5"].open(mode="r+"):
magnetic_simulator(
ifile["sources"],
ifile["receivers"],
ifile["moments"],
ifile["inclination"],
ifile["declination"],
ifile["earth_inc"],
ifile["earth_dec"],
)
if ifile["monitoring_directory"] is not None:
monitored_directory_copy(ifile["monitoring_directory"], ifile["receivers"])
The final step requires to make this program available to python to be executed from ANALYST.
Do the following:
Use the
File\Download as\Python *.py
dropdown menu to convert this notebook to apy
file.Rename the file to
mag_dipole_app.py
.Remove everything in the file except the
the imports,
the blocks of functions
def
the following lines (without #’s)
if __name__ == "__main__": file = sys.argv[1] run(file)
This part becomes the entry point of the python interpreter when running the
mag_dipole_app.py
script. At the end, your file should look like this.
Executing in ANALYST#
We now have everything we need to run our program with custom user-interface. The last step consists in moving our ui.json
and py
file to ANALYST Script Directory. After re-opening of the workspace, your program will be visible in the drop-down menu.
Et voila!
Copyright (c) 2022 Mira Geoscience Ltd.