"""
Module containing code to determine positions
"""
import pandas as pd
import numpy as np
import lmfit
_beacons = [
("LP5", 0, 0, 0),
("N1", 0, 1000, 0),
("E1", 1000, 0, 0),
("S1", 0, -1000, 0),
("W1", -1000, 0, 0),
]
# this is a default set of beacons. This module variable will be overwritten when used
# in conjunction with the datalogger.
default_beacons = pd.DataFrame(_beacons, columns=["beacon", "x", "y", "z"])
def _distance_to_beacons(
x: float,
y: float,
z: float,
beacon_IDs: list,
reference_beacons: pd.DataFrame = None,
):
"""
Vectorized version of :func:`distance_to_beacon`
required for fitting models
"""
distances = []
for beacon_ID in beacon_IDs:
d = distance_to_beacon(
x=x, y=y, z=z, beacon_ID=beacon_ID, reference_beacons=reference_beacons
)
distances.append(d)
return np.array(distances)
[docs]def distance_to_beacon(
x: float, y: float, z: float, beacon_ID: int, reference_beacons: pd.DataFrame = None
) -> float:
"""
Calculates the distance from a position to a beacon specified by beacon_ID.
"""
if reference_beacons is None:
reference_beacons = default_beacons
ref_beac = reference_beacons.loc[int(beacon_ID)]
# _, xb, yb, zb =
xb = ref_beac["x"]
yb = ref_beac["y"]
zb = ref_beac["z"]
return np.sqrt((x - xb) ** 2 + (y - yb) ** 2 + (z - zb) ** 2)
[docs]def fit_observations(
distances: list,
beacon_names: list,
depth: float = None,
x_guess: float = 0,
y_guess: float = 0,
beacon_log=default_beacons,
) -> dict:
"""
Fits distance observations and returns the estimated x, y, z coordinates.
Parameters
----------
distances
observed distances in m
beacon_names
names of the beacons to which the reference measurements where made.
depth
supports different in put formats
None -> will attempt to determine depth based on observation..
float -> will take the depth as a known value.
">0" -> will attempt to determine the height above sea level based on
observations
"<0" -> will attempt to determine depth below sea level based on observations.
Returns
-------
coordinates
a dictionary containing the keys, x, y, and z
"""
beacon_IDs = determine_beacon_IDs(beacon_names, beacon_log)
def _wrapped_distance_to_beacons(x: float, y: float, z: float, beacon_IDs: list):
return _distance_to_beacons(
x=x, y=y, z=z, beacon_IDs=beacon_IDs, reference_beacons=beacon_log
)
dist_model = lmfit.Model(_wrapped_distance_to_beacons, independent_vars=["beacon_IDs"])
dist_model.set_param_hint("x", value=x_guess, vary=True)
dist_model.set_param_hint("y", value=y_guess, vary=True)
if depth is None:
# initial guess is set negative so that in face of ambiguity we will find a
# coordinate below sea level
dist_model.set_param_hint("z", value=-20, vary=True)
elif depth == "<0":
dist_model.set_param_hint("z", value=-20, vary=True, max=0)
elif depth == ">0":
# initial guess is positive
dist_model.set_param_hint("z", value=20, vary=True, min=0)
else:
dist_model.set_param_hint("z", value=-depth, vary=False)
fit_results = dist_model.fit(distances, beacon_IDs=beacon_IDs)
return fit_results
[docs]def determine_beacon_IDs(beacon_names, beacons: pd.DataFrame):
"""
Determines the ID (integer index) of beacons specified by name.
"""
beacon_IDs = []
for beacon_name in beacon_names:
assert isinstance(beacon_name, str)
ID = beacons.index[beacons["beacon"] == beacon_name][0]
beacon_IDs.append(ID)
return beacon_IDs