Deduplicating intersections: block_size vs min_spacing

Deduplicating intersections: block_size vs min_spacing#

When lines cross each other repeatedly in a small area (e.g. at very low angles, or with proximity_dist), create_intersection_table can return many near-duplicate crossovers. Two parameters thin them, keeping the best (lowest max_dist, preferring true intersections over proximity ones) intersection of each line pair per neighborhood:

  • ``block_size``: 2D spatial window β€” drops an intersection if a better one of the same line pair is within this straight-line distance.

  • ``min_spacing``: 1D along-line window β€” drops an intersection if a better one of the same line pair is within this distance along both lines.

The difference matters when a line doubles back (a β€œhairpin”) and re-crosses another line: the two crossings are spatially close, but far apart along the hairpin line, so they constrain different parts of that line and are both worth keeping for levelling. block_size drops one of them; min_spacing keeps both.

If both parameters are passed, they act as one combined filter in a single pass: an intersection is dropped if it fails either test against an already-kept intersection.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import airbornegeo

Synthetic survey#

Three lines:

  • Line 1: straight horizontal line.

  • Line 2: a hairpin which crosses line 1 twice, only 400 m apart spatially, but ~6.4 km apart along line 2.

  • Line 3: a wiggly low-angle line which grazes line 1, crossing it several times within a couple of kilometres β€” true duplicates we want to thin.

[ ]:
# line 1: straight
xa = np.arange(0, 14000, 50.0)
line1 = pd.DataFrame({"easting": xa, "northing": 0.0, "line": 1})

# line 2: hairpin, down 6 km at x=3000, across 400 m, back up at x=3400
d = np.arange(0, 12400, 50.0)
line2 = pd.DataFrame(
    {
        "easting": np.where(
            d < 6000, 3000.0, np.where(d < 6400, 3000 + (d - 6000), 3400.0)
        ),
        "northing": np.where(
            d < 6000, 3000 - d, np.where(d < 6400, -3000.0, -3000 + (d - 6400))
        ),
        "line": 2,
    }
)

# line 3: low-angle graze of line 1 with a wiggle -> several crossings
xc = np.arange(6000, 13000, 50.0)
line3 = pd.DataFrame(
    {
        "easting": xc,
        "northing": 100 * np.sin(xc / 300) + 0.04 * (xc - 9500),
        "line": 3,
    }
)

data = pd.concat([line1, line2, line3], ignore_index=True)

_fig, ax = plt.subplots(figsize=(8, 5))
for name, df in data.groupby("line"):
    ax.plot(df.easting, df.northing, label=f"line {name}")
ax.legend()
ax.set_aspect("equal")
_images/crossovers_07_deduplicating_intersections_3_0.png
[ ]:
survey = airbornegeo.Survey(data, line_column="line")

No deduplication#

Without either parameter we get all the crossings: 2 hairpin crossings (lines 1/2) and several graze crossings (lines 1/3).

[ ]:
def plot_inters(inters, title):
    _fig, ax = plt.subplots(figsize=(8, 5))
    for name, df in data.groupby("line"):
        ax.plot(df.easting, df.northing, lw=0.8, label=f"line {name}")
    ax.scatter(
        inters.easting,
        inters.northing,
        c="black",
        zorder=3,
        label="intersections",
    )
    ax.legend()
    ax.set_aspect("equal")
    ax.set_title(title)
[ ]:
survey.create_intersection_table(method="network", progressbar=False)
inters_none = survey.intersections
plot_inters(inters_none, f"no deduplication: {len(inters_none)} intersections")
inters_none

block_size only (2D)#

A 1 km spatial window correctly thins the graze crossings, but also wrongly collapses the two hairpin crossings into one, since they are only 400 m apart spatially.

[ ]:
survey.create_intersection_table(method="network", block_size=1000, progressbar=False)
inters_block_size = survey.intersections
plot_inters(
    inters_block_size, f"block_size=1000: {len(inters_block_size)} intersections"
)
inters_block_size

min_spacing only (1D)#

A 2 km along-line window thins the graze crossings (they are within 2 km of each other along both lines), but keeps both hairpin crossings, since they are ~6.4 km apart along line 2. The line1_along_dist / line2_along_dist columns show the distance along each line used for this test.

[ ]:
survey.create_intersection_table(method="network", min_spacing=2000, progressbar=False)
inters_min_spacing = survey.intersections
plot_inters(
    inters_min_spacing, f"min_spacing=2000: {len(inters_min_spacing)} intersections"
)
inters_min_spacing

Both together#

The two act as a single combined filter: an intersection is dropped if it is within block_size spatially or within min_spacing along both lines of a better intersection. A small block_size (e.g. to catch numerically-duplicate points) combines naturally with a larger min_spacing. Note a min_spacing larger than block_size is typical, since along-line distance is always at least the straight-line distance.

[ ]:
survey.create_intersection_table(
    method="network",
    block_size=200,
    min_spacing=2000,
    progressbar=False,
)
inters_both = survey.intersections
plot_inters(
    inters_both, f"block_size=200, min_spacing=2000: {len(inters_both)} intersections"
)
inters_both

Summary#

  • Use block_size when you want at most one crossover of a line pair per spatial neighborhood, regardless of line topology.

  • Use min_spacing when crossovers should be thinned by how much of each line they constrain β€” it keeps spatially-close re-crossings (hairpins, loops) which are far apart along one of the lines.

  • Passing both applies both criteria in one pass; an intersection must clear both to be kept.

Note that since distance along a line is always at least the straight-line distance, min_spacing=s only ever drops a subset of what block_size=s would drop β€” its value is that you can use a large min_spacing without collapsing genuinely distinct re-crossings.