Interpolating values at crossovers#

This notebook shows how to interpolate your data values at crossover points.

[1]:
%load_ext autoreload
%autoreload 2


import logging

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import plotly.io as pio

import airbornegeo

# setup logging to get some additional info from the airbornegeo functions
logging.getLogger("airbornegeo").setLevel("INFO")
logging.basicConfig()
pio.renderers.default = "notebook"

Load data#

This is a subset of the BAS AGAP survey over Antarctica’s Gamburtsev Subglacial Mountains. The file is downloaded and subset in the notebook AGAP_magnetic_survey.

[2]:
data_df = pd.read_csv("data/AGAP_magnetic_survey_processed.csv")
data_df = data_df[
    [
        "easting",
        "northing",
        "height",
        "line",
        "unixtime",
        "distance_along_line",
        "mag",
    ]
]

# for testing limit number of lines
data_df = data_df[data_df.line.isin([149, 162, 47, 64, 80])]
data_df = data_df[
    ~(data_df.line == 47) | ~(data_df.distance_along_line.between(125e3, 134e3))
]
data_df = data_df[~(data_df.line == 64) | ~(data_df.distance_along_line > 497e3)]

# define line types: 0 for survey lines, 1 for tie lines
data_df["line_type"] = np.where(data_df.line >= 142, 1, 0)

data_df.head()
[2]:
easting northing height line unixtime distance_along_line mag line_type
243447 1.078385e+06 324738.995745 4188.1 47 1.230251e+09 0.000000 -60.95 0
243448 1.078375e+06 324738.036077 4188.1 47 1.230251e+09 10.049631 -62.39 0
243449 1.078331e+06 324733.833216 4188.3 47 1.230251e+09 53.589032 -63.66 0
243450 1.078265e+06 324727.295388 4188.6 47 1.230251e+09 120.672054 -64.86 0
243451 1.078198e+06 324720.699627 4188.8 47 1.230251e+09 187.743476 -66.00 0
[3]:
fig, ax = plt.subplots(figsize=(10, 10))
for _line_num, line_data in data_df.groupby("line"):
    # plot points
    ax.scatter(
        line_data.easting,
        line_data.northing,
        s=0.1,
    )
ax.set_aspect("equal")
airbornegeo.add_scalebar(ax, 100e3)
_images/crossovers_04_interpolating_values_4_0.png

Find intersections#

[ ]:
survey = airbornegeo.Survey(
    data_df,
    line_column="line",
    line_type_column="line_type",
    distance_column="distance_along_line",
)
survey.create_intersection_table(method="groups", buffer_dist=200)
inters = survey.intersections
inters
[5]:
ax = data_df.plot.scatter(
    "easting",
    "northing",
    color="k",
    s=0.001,
    marker=".",
    label="line points",
)
ax.legend(markerscale=100)
inters.plot.scatter(
    "easting",
    "northing",
    c="max_dist",
    s=40,
    cmap="magma_r",
    marker="o",
    edgecolor="black",
    ax=ax,
    label="intersection",
)
ax.set_aspect("equal")

airbornegeo.add_scalebar(ax, 100e3)
ax.set_aspect("equal")
_images/crossovers_04_interpolating_values_7_0.png

Add intersections as rows to the dataframe#

We can now add a row to each line of the main dataframe (not the intersections dataframe) which represents the intersection point. We don’t know the data values for the intersection point, so we will need to interpolate the data from the line data on either side of the intersection.

We need to specify the interpolation type (‘linear’, ‘nearest’, ‘quadratic’, ‘cubic’ etc.), which column we want to be interpolated (the magnetic data), and the columns containing the line identifies and the distance along the lines (used for the interpolation).

[ ]:
survey.interpolate_intersections(to_interp="mag", method="cubic")
inters_1 = survey.intersections
inters_1

Inspecting the intersection table below shows that we’ve added a few columns: dist_along_line1 and dist_along_line2, which show where the intersection point is located in units of distance along each line, and line1_interpolation_type and line2_interpolation_type, which inform if the data at the intersection for each line was ‘interpolate’, ‘extrapolated’, or ‘none’ (didn’t work). More on this later.

[7]:
inters_1.head()
[7]:
line1 line2 is_buffered geometry easting northing max_dist dist_along_line1 dist_along_line2 line1_interpolation_type line2_interpolation_type
0 47 149 False POINT (590788 237703) 590788.0 237703.0 12.785759 495443.842139 213907.289441 interpolated interpolated
1 47 162 False POINT (947875 301515) 947875.0 301515.0 1470.097394 135532.706601 33936.180262 interpolated interpolated
3 64 162 False POINT (961606 224280) 961606.0 224280.0 13.818089 134241.591186 112396.881399 interpolated interpolated
4 80 149 False POINT (616278 93894) 616278.0 93894.0 8.205492 497964.801259 67791.479081 interpolated interpolated
5 80 162 False POINT (973552 156679) 973552.0 156679.0 22.552582 135160.650024 181057.830508 interpolated interpolated

Inspecting the data dataframe below shows that we have added 10 rows and a few columns: is_intersection contains booleans for whether the row is an intersection point, intersecting_line keep track of which line the row is intersection, and mag_interpolation_type which keeps track of the interpolationg type used for interpolating the data column we provided, in this case mag.

There are 10 new rows since we have 5 intersections, and two one is added twice (once per each intersecting line).

[8]:
len(data_df[data_df.is_intersection])
[8]:
10
[9]:
data_df[data_df.is_intersection]
[9]:
easting northing height line unixtime distance_along_line mag line_type is_intersection intersecting_line mag_interpolation_type
1910 947875.0 301515.0 NaN 47 NaN 135532.706601 -163.117041 NaN True 162.0 interpolated
7265 590788.0 237703.0 NaN 47 NaN 495443.842139 -53.886664 NaN True 149.0 interpolated
9255 961606.0 224280.0 NaN 64 NaN 134241.591186 -32.713312 NaN True 162.0 interpolated
16293 973552.0 156679.0 NaN 80 NaN 135160.650024 -15.720655 NaN True 162.0 interpolated
21154 616278.0 93894.0 NaN 80 NaN 497964.801259 55.596440 NaN True 149.0 interpolated
22324 616278.0 93894.0 NaN 149 NaN 67791.479081 83.606346 NaN True 80.0 interpolated
24689 590788.0 237703.0 NaN 149 NaN 213907.289441 -45.204287 NaN True 47.0 interpolated
25704 947875.0 301515.0 NaN 162 NaN 33936.180262 -139.415895 NaN True 47.0 interpolated
26876 961606.0 224280.0 NaN 162 NaN 112396.881399 -75.259592 NaN True 64.0 interpolated
27897 973552.0 156679.0 NaN 162 NaN 181057.830508 -0.802466 NaN True 80.0 interpolated

We can inspect each of these interpolated intersections to make sure they worked alright. The first cell below will plot each line one at a time and wait for a keystroke to proceed to the next plot. The next cell lets you plot a single specified line.

These plots show where the intersections (diamonds) fall along the x axis of the plotted line. The y-axis shows the data value (magnetic anomaly in this case), and the vertical position of the diamond indicates the data value for the intersecting line. The vertical offset between the diamond and the line is the crossover error, which we will compute in the next notebook.

For line 162, we see the intersecting line 64 has a higher magnetic anomaly value, while the intersecting line 86 has a lower value.

[10]:
# # if you want to individually inspect each line and its intersections
# airbornegeo.inspect_intersections(
#     data_df,
#     plot_variable=["mag"],
#     line_column="line",
#     x="distance_along_line",
#     # plot_all=True,
# )
[11]:
# if you want to just look at 1 line
airbornegeo.plot_line_and_crosses(
    data_df,
    line=162,
    line_column="line",
    x="distance_along_line",
    y=["mag"],
    y_axes=[1],
    plot_inters=True,
)

Extrapolating values#

If we look at the intersection table, we will see the intersection for lines 64 and 149 has been removed. This was because we set extrapolation to False. This intersection was beyond the end of line line 64 by ~100 m, but we allowed it as an intersection since we used a buffer_dist of 200 m. For line 64, data only exists to the east of the intersection, so interpolation doesn’t work, and it would require extrapolation. Below we show how to use extrapolation.

[ ]:
survey.lines_without_intersections()
[13]:
inters_1
[13]:
line1 line2 is_buffered geometry easting northing max_dist dist_along_line1 dist_along_line2 line1_interpolation_type line2_interpolation_type
0 47 149 False POINT (590788 237703) 590788.0 237703.0 12.785759 495443.842139 213907.289441 interpolated interpolated
1 47 162 False POINT (947875 301515) 947875.0 301515.0 1470.097394 135532.706601 33936.180262 interpolated interpolated
3 64 162 False POINT (961606 224280) 961606.0 224280.0 13.818089 134241.591186 112396.881399 interpolated interpolated
4 80 149 False POINT (616278 93894) 616278.0 93894.0 8.205492 497964.801259 67791.479081 interpolated interpolated
5 80 162 False POINT (973552 156679) 973552.0 156679.0 22.552582 135160.650024 181057.830508 interpolated interpolated
[14]:
# zoom in on buffered intersection
reg = (603e3, 607e3, 159e3, 163e3)

ax = data_df[data_df.line == 149].plot.scatter(
    "easting",
    "northing",
    color="r",
    s=5,
    label="line 149 points",
)
ax = data_df[data_df.line == 64].plot.scatter(
    "easting",
    "northing",
    color="b",
    s=5,
    ax=ax,
    label="line 64 points",
)
ax.legend()
ax.set_xlim(reg[0], reg[1])
ax.set_ylim(reg[2], reg[3])
ax.set_aspect("equal")

airbornegeo.add_scalebar(ax, 200)
ax.set_aspect("equal")
_images/crossovers_04_interpolating_values_21_0.png

We can explicitly set extrapolate to True, which should now gives use a magnetic anomaly value for the intersection of line 64 and 149. We can view these interpolated magnetic anomaly values in profile view. We can also see in the intersection table that for line 64, the interpolation_type is now extrapolated instead of interpolated.

[ ]:
# Reset to baseline for second independent branch
survey.intersections = inters

survey.interpolate_intersections(to_interp="mag", method="cubic", extrapolate=True)
inters_2 = survey.intersections
inters_2
[16]:
# if you want to just look at 1 line
airbornegeo.plot_line_and_crosses(
    data_df,
    line=64,
    line_column="line",
    x="distance_along_line",
    y=["mag"],
    y_axes=[1],
    plot_inters=True,
)

Windowed interpolation#

When lines are really long, interpolation methods like linear or cubic may perform between using only data nearby on either side of the value to be interpolated. We can supply a window_width parameter to use this.

Different interpolation methods have a different number of minimum necessary points to perform the interpolation; for example cubic interpolation requires 4 data points and linear interpolation requires 2.

If the window width is not wide enough to capture enough data on either side, it will be doubled twice to try and capture enough data.

 - : data
 x : intersection point
[ ]: data windows for interpolation
- - - - - - -          x         - - - - - -
                    [     ] # first window
                 [           ] # doubled once
            [                     ] # doubled twice

After the second doubling, if it’s still too narrow to capture enough data, the method will fallback to one which requires fewer points. This fallback order goes from ‘user chosen method’ –> ‘linear’ –> ‘nearest’.

[ ]:
# Reset to baseline for third independent branch
survey.intersections = inters

survey.interpolate_intersections(
    to_interp="mag", method="cubic", extrapolate=True, window_width=500
)
inters_2 = survey.intersections
inters_2
[18]:
data_df[data_df.is_intersection]
[18]:
easting northing height line unixtime distance_along_line mag line_type mag_interpolation_type is_intersection intersecting_line
1910 947875.0 301515.0 NaN 47 NaN 135532.706601 -163.117041 NaN interpolated True 162.0
7265 590788.0 237703.0 NaN 47 NaN 495443.842139 -53.886666 NaN interpolated True 149.0
9255 961606.0 224280.0 NaN 64 NaN 134241.591186 -32.713313 NaN interpolated True 162.0
14424 604414.0 160932.0 NaN 64 NaN 497099.883607 -21.091765 NaN extrapolated True 149.0
16294 973552.0 156679.0 NaN 80 NaN 135160.650024 -15.720641 NaN interpolated True 162.0
21155 616278.0 93894.0 NaN 80 NaN 497964.801259 55.596443 NaN interpolated True 149.0
22325 616278.0 93894.0 NaN 149 NaN 67791.479081 83.606346 NaN interpolated True 80.0
23421 604414.0 160932.0 NaN 149 NaN 135890.938663 -15.550791 NaN interpolated True 64.0
24691 590788.0 237703.0 NaN 149 NaN 213907.289441 -45.204289 NaN interpolated True 47.0
25706 947875.0 301515.0 NaN 162 NaN 33936.180262 -139.415895 NaN interpolated True 47.0
26878 961606.0 224280.0 NaN 162 NaN 112396.881399 -75.259594 NaN interpolated True 64.0
27899 973552.0 156679.0 NaN 162 NaN 181057.830508 -0.802471 NaN interpolated True 80.0
[19]:
ax = data_df.plot.scatter(
    "easting",
    "northing",
    color="k",
    s=0.001,
    marker=".",
    label="line points",
)
ax.legend(markerscale=100)
inters_2.plot.scatter(
    "easting",
    "northing",
    c="max_dist",
    s=40,
    cmap="magma_r",
    marker="o",
    edgecolor="black",
    ax=ax,
    label="intersection",
)
ax.set_aspect("equal")

airbornegeo.add_scalebar(ax, 100e3)
ax.set_aspect("equal")
_images/crossovers_04_interpolating_values_28_0.png

Interpolating other data columns at intersections#

Sometimes it’s useful to interpolate multiple data columns at the intersections. Calling interpolate_intersections() again for a different column keeps any columns interpolated by earlier calls intact at the intersection rows, so you can simply call it once per column. In this case, we additionally interpolate the height variable.

[ ]:
# Reset to baseline for fourth independent branch (interpolating height)
survey.intersections = inters

survey.interpolate_intersections(
    to_interp="height", method="cubic", extrapolate=True, window_width=500
)

# both 'mag' (from the earlier calls above) and 'height' are now populated at
# the intersection rows
survey.data[survey.data.is_intersection].head()