Skip to content

Note

Click here to download the full example code

polygons

a coal lease: 295 boreholes, each tagged with the mining method its seam suits, a 48-vertex lease boundary and a 100 m grid. polygons select what lies in the lease and measure how far each point is from its edge, and domains spread from the holes to the grid.

import sys
from pathlib import Path

HERE = Path(__file__).parent if "__file__" in globals() else Path.cwd()
sys.path.insert(0, str(HERE.parents[1]))
import boitata as bt
import matplotlib.pyplot as plt
import numpy as np
from common import ACCENT, GRAY, HIGHLIGHT, INK, LIGHT, map_axes, save

inside the lease

the boundary is a closed Polylines. its single part is the (n, 2) ring that point_in_polygon tests points against, with the even-odd rule.

data = bt.datasets.coal_seam_thickness()
holes, grid, boundary = data["boreholes"], data["grid"], data["boundary"]
print(boundary)
lease = boundary.parts[0][:, :2]
xy, centers = holes.coords[:, :2], grid.coords[:, :2]
inside = bt.point_in_polygon(centers, lease)
print(f"{inside.sum()} of {len(grid)} cells inside, {inside.sum() * 100 * 100 / 1e6:.1f} km²")
print(f"agreement with the INSIDE flag: {np.mean(inside == (grid['INSIDE'] == 1)):.1%}")
print(f"{bt.point_in_polygon(xy, lease).sum()} of {len(xy)} holes inside")


def outline(ax, ring, **kwargs):
    ax.plot(*np.vstack([ring, ring[:1]]).T, **kwargs)

Out:

Polylines(1 features, 1 parts, crs: none)
7162 of 10800 cells inside, 71.6 km²
agreement with the INSIDE flag: 100.0%
295 of 295 holes inside

distance to the boundary

polygon_distance is the plan distance to the nearest edge; with signed=True points inside are negative. a 200 m standoff along the boundary, where no mining is allowed, is the cells within 200 m inside it.

distance = bt.polygon_distance(centers, lease, signed=True)
standoff = (distance > -200) & (distance < 0)
print(f"standoff: {standoff.sum()} cells, {standoff.sum() * 100 * 100 / 1e6:.1f} km²")
to_edge = -bt.polygon_distance(xy, lease, signed=True)
print(
    f"holes from the boundary: {to_edge.min():.1f} to {to_edge.max():.0f} m, {np.sum(to_edge < 200)} within 200 m"
)

fig, ax = plt.subplots(figsize=(7, 4.6), layout="constrained")
bt.plot.section(grid, np.where(inside, -distance, np.nan), ax=ax, colorbar=False, cmap="Greys")
fig.colorbar(ax.collections[0], ax=ax, shrink=0.8, label="Distance inside the boundary (m)")
ax.contour(
    grid.grid(grid.x)[0],
    grid.grid(grid.y)[0],
    grid.grid(distance)[0],
    levels=[-200],
    colors=HIGHLIGHT,
    linewidths=1,
)
outline(ax, lease, color=INK, lw=1)
ax.scatter(*xy.T, s=5, color=ACCENT)
map_axes(ax, "Distance to the lease boundary; 200 m standoff in orange")
save(fig, "distance")

Distance to the lease boundary; 200 m standoff in orange

Out:

standoff: 640 cells, 6.4 km²
holes from the boundary: 0.1 to 3775 m, 23 within 200 m

domains from the holes

assign_domain gives each target the domain of its nearest sample, read from the domain_column of the holes. here the targets are the cells in the lease and the domain is the mining method. labels come back as a list, with a confidence per target: the share of the n nearest holes (5 by default) that carry its label.

cells = grid.filter(inside)
labels, confidence = bt.assign_domain(cells, coords=holes, domain_column="CATEGORY")
methods = bt.Categories(["MECHANIZED", "SELECTIVE", "UNECONOMIC"], colors=[ACCENT, "#9ebad6", LIGHT])
codes = methods.encode(labels)
logged = methods.encode(holes["CATEGORY"])
for name, cell_share, hole_share in zip(
    methods.names, methods.shares(codes), methods.shares(logged), strict=True
):
    print(f"{name:>10}: {cell_share:6.1%} of the lease, {hole_share:6.1%} of the holes")

fig, ax = plt.subplots(figsize=(7, 4.6), layout="constrained")
bt.plot.section(cells, codes, scheme=methods, colorbar=False, ax=ax)
outline(ax, lease, color=GRAY, lw=1)
cmap, norm = bt.plot.category_colors(methods)
ax.scatter(*xy.T, c=logged, cmap=cmap, norm=norm, s=10, edgecolors=INK, linewidths=0.4)
bt.plot.category_legend(methods, ax, loc="upper left", bbox_to_anchor=(1, 1))
map_axes(ax, "Mining method of the nearest hole")
save(fig, "domains")

Mining method of the nearest hole

Out:

MECHANIZED:  69.7% of the lease,  81.0% of the holes
 SELECTIVE:  26.9% of the lease,  16.9% of the holes
UNECONOMIC:   3.4% of the lease,   2.0% of the holes

MECHANIZED holes are 81.0 % of the holes, but their domain covers 69.7 % of the lease, because the infill went where the seam is thick. counting cells instead of holes declusters the shares.

majority vote

method="majority" takes the most frequent label among the n nearest holes instead, and a tie goes to the label of the nearest hole. a lone hole among holes of another method no longer claims its own patch.

majority, agreement = bt.assign_domain(cells, coords=holes, domain_column="CATEGORY", method="majority")
changed = np.array(labels) != np.array(majority)
print(f"{changed.sum()} of {len(cells)} cells change label ({changed.mean():.1%})")
print(f"mean confidence: nearest {confidence.mean():.2f}, majority {agreement.mean():.2f}")
for name, share in zip(methods.names, methods.shares(methods.encode(majority)), strict=True):
    print(f"{name:>10}: {share:6.1%} of the lease")

fig, (a, b) = plt.subplots(1, 2, figsize=(10, 3.8), sharey=True, layout="constrained")
bt.plot.section(cells, methods.encode(majority), scheme=methods, colorbar=False, ax=a)
outline(a, lease, color=GRAY, lw=1)
map_axes(a, "Majority of the 5 nearest holes")
bt.plot.section(cells, changed.astype(float), ax=b, colorbar=False, cmap="Greys", vmin=0, vmax=2)
outline(b, lease, color=GRAY, lw=1)
b.scatter(*xy.T, c=logged, cmap=cmap, norm=norm, s=6, edgecolors=INK, linewidths=0.3)
map_axes(b, "Cells that differ from the nearest hole")
b.set_ylabel("")
save(fig, "majority")

Majority of the 5 nearest holes, Cells that differ from the nearest hole

Out:

1314 of 7162 cells change label (18.3%)
mean confidence: nearest 0.75, majority 0.81
MECHANIZED:  80.4% of the lease
 SELECTIVE:  18.8% of the lease
UNECONOMIC:   0.7% of the lease

the changed cells sit around isolated SELECTIVE holes, now outvoted by their MECHANIZED neighbors, and MECHANIZED grows from 69.7 % to 80.4 % of the lease. majority smooths the map but gives back the declustering: where the drilling is dense, one method wins most votes.

a polygon with a hole

a lease often excludes an area inside it, here a 1.5 km circle around a point in the middle. the exclusion is a second closed part of the lease feature. Polylines decide inside by even-odd counting over a feature's closed parts, so a ring inside another is a hole. contains leaves the exclusion out, area subtracts it and distance measures to the nearest ring, negative inside, so the 200 m standoff now runs along both rings. PolygonSelector, point_in_polygon and BlockModel.subblock take the same Polylines.

center = lease.mean(axis=0)
angle = np.linspace(0, 2 * np.pi, 60, endpoint=False)
exclusion = center + 1500 * np.column_stack([np.cos(angle), np.sin(angle)])
leased = bt.Polylines([lease, exclusion], closed=True, features=[0, 0], attributes={"name": ["lease"]})
kept = leased.contains(centers)
in_exclusion = bt.point_in_polygon(centers, exclusion)
print(
    f"area: {boundary.area()[0] / 1e6:.2f} km² lease, {leased.area()[0] / 1e6:.2f} km² net of the exclusion"
)
print(
    f"cells kept: {kept.sum()}, {np.sum(kept & in_exclusion)} of the {in_exclusion.sum()} in the exclusion zone"
)
print(f"PolygonSelector agrees: {np.array_equal(bt.PolygonSelector(leased).contains(centers), kept)}")
signed = leased.distance(centers, signed=True)
standoff = (signed > -200) & (signed < 0)
print(f"standoff along both rings: {standoff.sum()} cells, {standoff.sum() * 100 * 100 / 1e6:.1f} km²")

fig, (a, b) = plt.subplots(1, 2, figsize=(10, 3.8), sharey=True, layout="constrained")
bt.plot.section(grid, kept.astype(float), ax=a, colorbar=False, cmap="Greys", vmin=0, vmax=2)
bt.plot.section(grid, np.where(kept, -signed, np.nan), ax=b, colorbar=False, cmap="Greys")
b.contour(
    grid.grid(grid.x)[0],
    grid.grid(grid.y)[0],
    grid.grid(signed)[0],
    levels=[-200],
    colors=HIGHLIGHT,
    linewidths=1,
)
for ax, title in ((a, "contains, even-odd"), (b, "Signed distance; 200 m standoff in orange")):
    outline(ax, lease, color=INK, lw=1)
    outline(ax, exclusion, color=INK, lw=1)
    map_axes(ax, title)
b.set_ylabel("")
save(fig, "hole")

contains, even-odd, Signed distance; 200 m standoff in orange

Out:

area: 71.65 km² lease, 64.59 km² net of the exclusion
cells kept: 6458, 0 of the 704 in the exclusion zone
PolygonSelector agrees: True
standoff along both rings: 843 cells, 8.4 km²

Total running time of the script: ( 0 minutes 1.862 seconds)

Download Python source code: example_02_08.py

Download Jupyter notebook: example_02_08.ipynb

Gallery generated by mkdocs-gallery