Calibration

Calibrate magnetometer and accelerometer readings

  • Author(s): Phil Underwood

Implementation Notes

Software and Dependencies:

Axes

There are several axis conventions used.

  • World coordinates: this represents the “real world”, X is due east, Y is due North, Z is Up

  • Device coordinates: this represents the device. Y is the primary axis - the direction of travel of direction or direction of a pointer. Z is up, and X is to the right if Y is facing away from you and Z is up.

class mag_cal.Calibration(mag_axes: str = '+X+Y+Z', grav_axes: str = None)

Object representing a magnetometer and accelerometer calibration

Create an object representing the calibration coefficients for a combined magnetometer and accelerometer

Parameters:
  • mag_axes (str) – A string representing how your magnetometer is mounted with respect to your device. For each axis XYZ of your device (see above for axis descriptions), state the corresponding axis of your sensor. Add a + or - to let us know if it is inverted. So for a sensor that is correctly mounted it will be "+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as "-Y+X-Z"

  • grav_axes (str) – Same format as mag_axes but for the accelerometer. Default is copy of mag_axes.

ELLIPSOID = 0

Fit to Ellipsoid

AXIS_CORRECTION = 1

Fit to ellipsoid then correct any axis misalignment

NON_LINEAR = 2

as per AXIS_CORRECTION and then do correction for non-linear effects

FAST_NON_LINEAR = 3

as per AXIS_CORRECTION and then do quick non-linear correction

calibrate(mag_data: ndarray, grav_data: ndarray, routine: int = 3)

Perform a full calibration, with an algorithm depending on the value of routine. If you select a routine other than ELLIPSOID you must provide at least one run of at least four shots in the same direction with varying amonts of roll. Ideally two sets of eight readings, but this is not vital.

Parameters:
  • mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)

  • grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)

  • routine

    what level of calibration to perform:

    • ELLIPSOID: Simplest form of calibration, very fast, does not require any sets of readings to be aligned. Does not correct for misalignment between pointer and sensors.

    • AXIS_CORRECTION: This routine performs the ELLIPSOID and then applies a rotation to offset any misalignment between the pointer and sensors (and also misalignment between accelerometer and magnetometer if relevant). This process will automatically identify which shots have been taken in the same direction

    • NON_LINEAR: Performs calibration as per AXIS_CORRECTION, then uses an optimisation process to account for non-linear sensor response. See fit_non_linear for details.

    • FAST_NON_LINEAR: Performs calibration as per AXIS_CORRECTION, then uses a least-squares process to account for non-linear sensor response. A lot faster than NON_LINEAR, but slightly less accurate. See fit_non_linear_quick for details

Returns:

Measure of error: percentage error of fit for ELLIPSOID, standard deviation of error in degrees for other methods. Normally <1 degree is acceptable, <0.5 degrees is good.

get_angles(mag, grav) Tuple[ndarray, ndarray, ndarray]

Get device azimuth(bearing), inclination, and roll, given the magnetic and gravity readings

Parameters:
  • mag (np.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (np.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

Returns:

(azimuth, inclination, roll) in degrees

as_dict() Dict

Convert the current calibration to a dictionary, suitable for serialising via json.

Returns:

The calibration as a dictionary

Return type:

dict

classmethod from_dict(dct: Dict) Calibration

Create a Calibration object based on a dict previously produced by as_dict

Parameters:

dct (dict) – dict to instantiate

Returns:

Calibration object

fit_ellipsoid(mag_data: ndarray, grav_data: ndarray) Tuple[float, float]

Take multiple sets of readings in various directions. You can then use this function to determine an ideal set of calibration coefficients

Parameters:
  • mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)

  • grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)

Returns:

(mag_accuracy, grav_accuracy) How well the calibrated model fits the data. Lower numbers are better

fit_to_axis(data, axis='Y') float

Take multiple reading with the device pointing in the same direction, but rotated around axis. You can repeat this with several directions. This function will take this data and ensure that your sensors aligned relative to each other. This function requires that you have run Calibration.fit_ellipsoid beforehand.

Parameters:
  • data – A list of paired magnetic and gravity readings e.g.: [(mag_data1, grav_data1), (mag_data2, grav_data2), ...], where mag_data1 and grav_data1 are (N,3) numpy arrays of readings around the axis in the first direction, and mag_data2 and grav_data2 are (M,3) numpy arrays of readings around the specified axis in another direction.

  • axis – Axis you have rotated your device around. Defaults to "Y"

Returns:

Average standard deviation of readings in degrees, after calibration

fit_non_linear(data, axis: str = 'Y', param_count: int = 3, sensor: int = 1)

Compensate for devices which do not have a linear response between the magnetic or gravity field and their output. It is recommended to use a param_count of either 1, 3 or 5 for this function; odd numbers generally get better results, but there is a significant risk of overfitting if higher numbers are used. See Underwood, Phil (2021) Non-linear Calibration of a Digital Compass and Accelerometer, Cave Radio Electronics Group Journal 114, pp7-10. June 2021 for more details on the algorithm used. This function uses a Nelder-Mead minimisation process to find optimal values for each non-linear parameter.

This function requires that you have run both Calibration.fit_ellipsoid and Calibration.fit_to_axis beforehand.

Parameters:
  • data – A list of paired magnetic and gravity readings e.g.: [(mag_data1, grav_data1), (mag_data2, grav_data2), ...], where mag_data1 and grav_data1 is a (N,3) numpy array of readings around the axis in the first direction, and mag_data2 and grav_data2 is a (M,3) numpy array of readings around the specified axis in another direction.

  • axis (str) – Axis along which the device has been rotated for the given readings.

  • param_count (int) – Number of parameters to use per sensor axis. Larger numbers will take substantially more time to calculate. Default is 3

  • sensor – Whether to calibrate the magnetometer, accelerometer or both. Must be one of Calibration.MAGNETOMETER, Calibration.ACCELEROMETER, or Calibration.BOTH. Default is MAGNETOMETER, as accelerometers are generally quite well-behaved.

Returns:

Standard deviation of accuracy of calibration in degrees

fit_non_linear_quick(data, param_count: int = 3)

Compensate for devices which do not have a linear response between the magnetic or gravity field and their output. It is recommended to use a param_count of either 1, 3 or 5 for this function; odd numbers generally get better results, but there is a significant risk of overfitting if higher numbers are used. See Underwood, Phil (2021) Non-linear Calibration of a Digital Compass and Accelerometer, Cave Radio Electronics Group Journal 114, pp7-10. June 2021 for more details on the algorithm used. This function uses a least squares method to rapidly find a good set of parameters to use. It is much faster than fit_non_linear, but gives slightly less good results. Note it will only calibrate the magnetometer and is unable currently to apply a non-linear correction to the accelerometer. It will also only calibrate for rotations around the Y axis.

This function requires that you have run both Calibration.fit_ellipsoid and Calibration.fit_to_axis beforehand.

Parameters:
  • data – A list of paired magnetic and gravity readings e.g.: [(mag_data1, grav_data1), (mag_data2, grav_data2), ...], where mag_data1 and grav_data1 is a (N,3) numpy array of readings around the axis in the first direction, and mag_data2 and grav_data2 is a (M,3) numpy array of readings around the specified axis in another direction.

  • param_count (int) – Number of parameters to use per sensor axis. Larger numbers will take substantially more time to calculate. Default is 3

Returns:

Standard deviation of accuracy of calibration in degrees

accuracy(data) float

Calculate average accuracy for a set of multiple readings taken

Parameters:

data – A list of paired magnetic and gravity readings e.g.: [(mag_data1, grav_data1), (mag_data2, grav_data2)], where mag_data1 and grav_data1 is a (N,3) numpy array of readings around the axis in the first direction, and mag_data2 and grav_data2 is a (M,3) numpy array of readings around the specified axis in another direction.

Returns:

Average standard deviation of readings in degrees

uniformity(mag_data, grav_data)

Check the uniformity of the data - how well the calibrated data points fit on a sphere of radius 1.0

Parameters:
  • mag_data (np.ndarray) – Numpy array of magnetic readings of shape (N,3)

  • grav_data (np.ndarray) – Numpy array of gravity readings of shape (M,3)

Returns:

(mag_accuracy, grav_accuracy) How well the calibrated model fits the data. Lower numbers are better

get_orientation_vector(mag, grav)
Given a set of magnetic readings and gravity readings, get the orientation

of the device in world coordinates.

Parameters:
  • mag (np.array) – Numpy array of shape(N,3) or (3,)

  • grav (np.array) – Numpy array of shape(N,3) or (3,)

Returns:

Device orientation in world coordinates.

Return type:

Numpy array of same shape as mag, with device orientation.

get_orientation_matrix(mag, grav) ndarray

Get the device orientation as an orthonormal matrix, given the magnetic and gravity readings

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

Returns:

Orthonormal matrix converting device coordinates to real world coordinates

Return type:

numpy.ndarray

static matrix_to_angles(matrix: ndarray)

Extract the rotation angles from a matrix. Angles are “zxy” rotations (azimuth, pitch, roll)

Parameters:

matrix (np.ndarray) –

Returns:

azimuth, pitch, roll

classmethod angles_to_matrix(azimuth: ndarray, inclination: ndarray, roll: ndarray)

Create a rotation matrix from angles “zxy” i.e azimmuth, inclination, roll. This is the invers of matrix_to_angles

Parameters:
  • azimuth – float or np.array of floats for azimuth

  • inclination – pitch(rotation around x axis)

  • roll – roll around y axis

Returns:

find_similar_shots(mag: ndarray, grav: ndarray, precision=30, min_run=4)

Find runs of shots that are within precision degrees of each other

Parameters:
  • mag – numpy array of magnetic data

  • grav – numpy array of accelerometer data

  • precision – number of degrees shots should be within

  • min_run – minimum length of run to find

Returns:

list of start and finish indices for each run

get_field_strengths(mag, grav)

Get field strength for magnetic and gravity components

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

Returns:

mag_field, grav_field

Return type:

numpy.ndarrays if multiple readings given or floats

get_dips(mag, grav)

Get the magnetic field dip

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

Returns:

dip angle(s) in degrees

Return type:

numpy.ndarrays if multiple readings given or floats

set_expected_mean_dip(mag, grav)

Store an expected dip and standard deviations. This will be used for magnetic and (gravitational!!) anomaly detection

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

set_field_characteristics(mag, grav)

Store magnetic and gravity field strengths and also dip angles. This will be used for magnetic and (gravitational!!) anomaly detection

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, either as numpy array or sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, either as numpy array or sequence of 3 floats

Returns:

raise_if_anomaly(mag, grav, strictness: Strictness = (2.0, 2.0, 3.0))

Raises an error if magnetic field strength and dip are not similar to during calibration

Parameters:
  • mag (numpy.ndarray) – Magnetic readings, sequence of 3 floats

  • grav (numpy.ndarray) – Gravity readings, sequence of 3 floats

  • strictness (Strictness) –

    NamedTuple containing the following entries

    • mag: Acceptable percentage difference in magnetic field strength

    • grav: Acceptable percentage difference in gravity field strength

    • dip: Acceptable difference in dip in degrees

    If not specified will default to 2% for mag and grav and 3° for dip

Returns:

None

Raises:
  • MagneticAnomalyError if magnetic field strength too big or small.

  • GravityAnomalyError if gravity field strength too big or small - usually occurs if movement during read

  • DipAnomalyError if magnetic field dip too big or small

class mag_cal.Axes(axes)

This represents the orientation of a sensor in respect to the device orientation

Parameters:

axes (str) – A string representing how your sensor is mounted with respect to your device. For each axis XYZ of your device (see note regarding Axes above), state the corresponding axis of your sensor. Add a + or - in front to let us know if it is inverted. So for a sensor that is correctly mounted it will be "+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as "-Y+X+Z"

fix_axes(data: ndarray)

Transform raw data from sensor coordinates to raw device coordinates :param data: raw sensor data as a np.array((…,3)) :return: np.array with same dimensions as data

class mag_cal.Sensor(axes='+X+Y+Z')

This represents the calibration coefficients for a single sensor

Create a sensor object, with axes set

Parameters:

axes (str) – A string representing how your sensor is mounted with respect to your device. For each axis XYZ of your device (see above for axis descriptions), state the corresponding axis of your sensor. Add a + or - to let us know if it is inverted. So for a sensor that is correctly mounted it will be "+X+Y+Z". If the sensors Y axis points to the left of the device, the X is forwards and Z is down, specify this as "-Y+X+Z". Default is +X+Y+Z

fit_ellipsoid(data: ndarray) float

Take multiple sets of readings in various directions. You can then use this function to determine an ideal set of calibration coefficients.

Parameters:

data (np.ndarray) – Numpy array of readings of shape (N,3)

Returns:

accuracy - How well the calibrated model fits the data; lower numbers are better.

Return type:

float

align_along_axis(data, axis='Y')

Calibrate for any mechanical placement error.

Parameters:
  • data (List[np.ndarray]) – A list of sets of data - numpy array of shape (N,3). Each set should have been taken with the device pointing at a fixed target, but rotated through different angles

  • axis (str) – Axis that the device has been rotated around, must be one of “X”, “Y”, or “Z”. Default is “Y”.

Returns:

None

apply(data: ndarray) ndarray

Take a set of raw data and apply the calibration to it

Parameters:

data (np.ndarray) – Numpy array of shape (3,) or (N,3)

Returns:

numpy array with same shape as data

set_non_linear_params(params: ndarray)

Set the parameters for the radial basis functions

Parameters:

params (np.ndarray) – Numpy matrix or vector with N*3 elements, where N is the number of parameters per axis

set_linear()

Disable non-linear adjustments

uniformity(data: ndarray)

Check the uniformity of the data points after calibration. This is measured as the standard deviation of the absolute magnitude of each measurement

Parameters:

data (np.ndarray) – Numpy array of shape (N,3)

Returns:

Calculated uniformity as above

as_dict()

Return the calibration parameters as a dict, can be reloaded using Sensor.from_dict

Returns:

Dict containing all the calibration data for this sensor

classmethod from_dict(dct: dict) Sensor

Create a new Sensor instance from the given dict, which should have been created using as_dict.

Parameters:

dct (dict) – Dict of values as created by as_dict

Returns:

New Sensor object, initialised with given data

get_field_strength(data)

Get the field strength from this sensor, using the original units that the data was provided in. :param numpy.ndarray data: Sensor readings, either as numpy array or sequence of 3 floats :return: Single float or numpy array of field strengths

set_expected_field_strengths(data)

Store an expected field strength and standard deviations. This will be used for magnetic and (gravitational!!) anomaly detection :param numpy.ndarray data: Sensor readings, either as numpy array or sequence of 3 floats

raise_if_anomaly(data, tolerance=0.05)

Raise SensorAnomaly if given reading is not within expected range

Parameters:
  • data – sequence of 3 floats

  • tolerance – Amount if difference in field strength to accept

Raises:

SensorError if field strength is greater than tolerance different from the mean

exception mag_cal.NotCalibrated

Error to raise when calibration has not been done yet

class mag_cal.Strictness(mag, grav, dip)

Create new instance of Strictness(mag, grav, dip)

dip

Alias for field number 2

grav

Alias for field number 1

mag

Alias for field number 0