Skip to main content

Weighted Voronoi Diagrams — multiplicative, additive and power (Laguerre) modes

Project description

voronoip — Weighted Voronoi Diagrams for Python

A lightweight, dependency-light Python library for constructing and visualising weighted Voronoi diagrams, including:

Mode Distance function Effect of larger weight
"multiplicative" dist(p,g) / w(g) larger region
"additive" dist(p,g) − w(g) larger region
"power" dist(p,g)² − w(g)² larger region (power diagram)

Installation

pip install voronoip

Or, for local development:

pip install numpy scipy matplotlib
# clone / copy voronoip/ into your project

Dependencies: numpy, scipy (optional, for boundary_pixels), matplotlib (for visualisation).


Quick start

import numpy as np
from voronoip import WeightedVoronoi

pts = np.array([[0.2, 0.3],
                [0.7, 0.6],
                [0.5, 0.1],
                [0.1, 0.9]])
w   = np.array([1.0, 2.5, 0.5, 1.8])

wv = WeightedVoronoi(pts, w, mode="multiplicative", resolution=512)
wv.compute()
wv.plot()          # shows an interactive matplotlib figure
wv.to_png("out.png")

Two mistakes almost everyone makes at first

Before jumping into the examples below, read this. These two mistakes account for the vast majority of TypeError / AttributeError reports from new users — both examples further down show exactly how to avoid them.

1. compute() returns the object itself, not a list of regions

# WRONG — celulas becomes the WeightedVoronoi object, not a list
celulas = diagrama.compute()
for celula in celulas:        # TypeError: 'WeightedVoronoi' object is not iterable
    ...

# CORRECT — compute() returns self (useful for chaining);
#    the actual list of regions lives in .regions
diagrama.compute()
for regiao in diagrama.regions:
    ...

# Also valid, thanks to chaining:
regioes = diagrama.compute().regions

2. voronoip is raster-based, not vector-based — there is no .vertices

If you've used scipy.spatial.Voronoi before, you're used to each region being a polygon with .vertices. voronoip works differently: it rasterises the diagram onto a pixel grid (label_grid), and each VoronoiRegion is described by a boolean pixel mask, not a list of polygon corners.

# WRONG — VoronoiRegion has no .vertices attribute
poligono = np.array(regiao.vertices)
plt.fill(poligono[:, 0], poligono[:, 1])

# CORRECT — let the built-in plot() draw the diagram for you
fig, ax = diagrama.plot()

# Or, if you need region data programmatically:
regiao.pixel_mask     # (H, W) bool — True where pixels belong to this region
regiao.area            # int — pixel count
regiao.centroid         # (x, y) — mean position of the region

If you specifically need polygon-style output, this is on the roadmap (see Limitations below) — it is not currently supported.


API reference

WeightedVoronoi(points, weights, **kwargs)

Parameter Default Description
points (N, 2) generator coordinates
weights (N,) generator weights
mode "multiplicative" distance metric
resolution 512 pixels along longer axis
domain auto (bounding box + 5 %) ((xmin,xmax),(ymin,ymax))
palette "tab20" matplotlib colormap name
show_generators True draw seed points
show_weights False annotate weights
show_boundaries True draw cell edges

Tip: always pass points and weights as float arrays (np.array([...], dtype=float) or simply use 1.0 instead of 1). Integer arrays work in most cases, but mixing them with weight-based division (mode="multiplicative") can produce unexpected integer truncation in edge cases — floats avoid the ambiguity entirely.

Methods

wv.compute()                     # rasterise the diagram (required first) — returns self

wv.plot(**kwargs)                # returns (fig, ax)
wv.plot_distance_field()         # heat-map of min weighted distance
wv.plot_comparison()             # side-by-side of all 3 modes

wv.owner(x, y)                   # generator index owning (x, y)
wv.region_of(x, y)               # VoronoiRegion containing (x, y)
wv.nearest_generators(x, y, k=3) # k nearest generators by weighted dist

wv.to_png("out.png", dpi=150)
wv.to_svg("out.svg")
wv.to_csv("out.csv")             # index, x, y, weight, area, centroid
wv.to_label_array()              # (H, W) int ndarray — copy

Important: every query method (owner, region_of, nearest_generators) and every plotting/export method requires .compute() to have been called first. Calling them beforehand raises RuntimeError: Call .compute() before accessing diagram data or plotting. — this is intentional, not a bug.

Key attributes (after compute())

Attribute Type Description
label_grid (H, W) int32 generator index per pixel
dist_grid (H, W) float64 minimum weighted distance per pixel
regions list[VoronoiRegion] one object per generator — this is what you iterate over

VoronoiRegion

r = wv.regions[0]

r.index          # int — generator index
r.generator      # (2,) float — (x, y)
r.weight         # float
r.pixel_mask     # (H, W) bool
r.color          # (R, G, B) tuple

r.area           # int — number of pixels
r.centroid       # (2,) float — mean (x, y) of mask pixels
r.boundary_pixels # (K, 2) row/col indices of boundary pixels

Note that r.pixel_mask is the only true source of geometry for a region. area, centroid and boundary_pixels are all derived from it — there is no separate vector representation.


voronoip.generators

from voronoip.generators import (
    random_generators,           # uniform random
    grid_generators,             # regular grid with optional jitter
    poisson_disk_generators,     # Bridson blue-noise sampling
)

pts, w = random_generators(n=20, weight_range=(0.5, 2.0), seed=42)
pts, w = grid_generators(nx=6, ny=6, jitter=0.04, seed=0)
pts, w = poisson_disk_generators(min_dist=0.1, seed=7)

All functions return (points, weights) tuples ready for WeightedVoronoi.


voronoip.metrics

from voronoip.metrics import (
    multiplicative_weighted_distance,  # scalar
    additive_weighted_distance,
    power_distance,
    batch_multiplicative,              # vectorised over generators
    batch_additive,
    batch_power,
)

Full worked examples

The two examples below are deliberately written end-to-end, including the result of .regions and .owner(), so you can copy them as a starting template for your own scripts without hitting the two mistakes described above.

Example 1 — Basic diagram with 4 weighted points

import numpy as np
import matplotlib.pyplot as plt
from voronoip import WeightedVoronoi

# Points (x, y) — always use floats
pontos = np.array([
    [1.0, 1.0],
    [5.0, 2.0],
    [3.0, 6.0],
    [7.0, 7.0]
])

# Weight associated with each point
pesos = np.array([1.0, 2.0, 0.5, 3.0])

# Create the weighted Voronoi object
vor = WeightedVoronoi(
    points=pontos,
    weights=pesos,
    mode="multiplicative",
    resolution=512,
    show_weights=True       # annotate weights directly on the plot
)

# compute() returns self — do NOT reassign it to "regioes"
vor.compute()

# ── Visualization (built-in, no manual polygon drawing needed) ─────
fig, ax = vor.plot()
ax.set_title("Diagrama de Voronoi Ponderado - voronoip")
plt.show()

# ── Region data — iterate over .regions, not over vor itself ───────
print("Regiões:")
for regiao in vor.regions:
    print(regiao)
    print(f"  Área:      {regiao.area} px")
    print(f"  Centróide: {regiao.centroid}")
    print(f"  Peso:      {regiao.weight}")

# ── Query which region owns an arbitrary point ──────────────────────
x, y = 4.0, 4.0
idx = vor.owner(x, y)
print(f"\nDono do ponto ({x}, {y}): gerador {idx}{vor.regions[idx]}")

Example 2 — Antenna signal coverage (real-world use case)

import numpy as np
import matplotlib.pyplot as plt
from voronoip import WeightedVoronoi

# Antenna locations
antenas = np.array([
    [2.0, 8.0],
    [8.0, 9.0],
    [5.0, 5.0],
    [1.0, 2.0],
    [9.0, 3.0]
])

# Signal strength (weight) — higher power covers a larger area
potencia = np.array([5.0, 3.0, 2.0, 1.0, 4.0])

diagrama = WeightedVoronoi(
    points=antenas,
    weights=potencia,
    mode="multiplicative",
    resolution=512,
    show_generators=False,   # we'll draw the antennas manually below
    show_weights=False,
)

diagrama.compute()           # no reassignment — returns self

# ── Visualization ────────────────────────────────────────────────────
fig, ax = diagrama.plot()

# Custom antenna markers (triangles instead of the default dots)
ax.scatter(antenas[:, 0], antenas[:, 1],
           s=180, c="black", marker="^", zorder=6)

# Power labels
for i, p in enumerate(potencia):
    x, y = antenas[i]
    ax.text(x + 0.15, y, f"P={p}", fontsize=9, zorder=7,
            bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.6, lw=0))

ax.set_title("Cobertura de Antenas usando Voronoi Ponderado")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.grid(True, alpha=0.3)
plt.show()

# ── Coverage data per antenna — iterate over .regions ───────────────
print("Cobertura por antena:")
for regiao in diagrama.regions:
    i = regiao.index
    cx, cy = regiao.centroid
    print(f"  Antena {i+1} (P={potencia[i]}) "
          f"→ área: {regiao.area} px  "
          f"centróide: ({cx:.2f}, {cy:.2f})")

# ── Signal intensity heat-map ───────────────────────────────────────
fig2, ax2 = diagrama.plot_distance_field(cmap="plasma")
ax2.set_title("Intensidade de Sinal (distância ponderada)")
plt.show()

More examples

Comparison of all three modes

wv = WeightedVoronoi(pts, w, mode="multiplicative", resolution=400)
wv.compute()
fig, axes = wv.plot_comparison(figsize=(18, 6))

Distance field heat-map

wv.plot_distance_field(cmap="plasma")

Querying which region owns a point

idx = wv.owner(0.5, 0.5)
region = wv.region_of(0.5, 0.5)
print(region)
# VoronoiRegion(index=1, generator=(0.700, 0.600), weight=2.500, area=14832 px)

Exporting

wv.to_png("voronoi.png", dpi=200)
wv.to_svg("voronoi.svg")
wv.to_csv("voronoi.csv")

Limitations

  • No polygon/vector output yet. voronoip is raster-first by design — regions are pixel masks, not lists of polygon vertices. If your use case strictly requires exact vector polygons (e.g. for GIS or CAD pipelines), this library is not yet a drop-in replacement for scipy.spatial.Voronoi.
  • Diagram accuracy (boundary smoothness, area precision) scales with resolution — low resolutions will show visibly blocky cell edges.
  • boundary_pixels requires the optional scipy dependency (pip install voronoip[full]).

Project structure

voronoip/
├── __init__.py      # public API
├── diagram.py       # WeightedVoronoi class
├── region.py         # VoronoiRegion dataclass
├── generators.py     # random / grid / Poisson-disk seed generators
└── metrics.py         # distance functions + registry
tests/
└── test_voronoip.py  # full test suite (pytest)
README.md

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

voronoip-0.2.0.tar.gz (22.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

voronoip-0.2.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file voronoip-0.2.0.tar.gz.

File metadata

  • Download URL: voronoip-0.2.0.tar.gz
  • Upload date:
  • Size: 22.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for voronoip-0.2.0.tar.gz
Algorithm Hash digest
SHA256 0a254eac6d1fc8d9b75812cb37f4849079d700f02f0b13175a7b3bacc928dbde
MD5 43997a157a47ece0587e48cea3c4a8f5
BLAKE2b-256 03b05fc4991df8b4716e2de2a93d05d1d27c8c58976052ffe3798e14f682be87

See more details on using hashes here.

File details

Details for the file voronoip-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: voronoip-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for voronoip-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 589e4d85d784a950938829b9e0b595fb092a91626282684af0d0be6b95619645
MD5 334a2804f8051f11dee59c3d312c8303
BLAKE2b-256 d38716a530cf423182b943fa8b62e19c92a35ebd4ee7cfc45859001fd698dd93

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page