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
pointsandweightsasfloatarrays (np.array([...], dtype=float)or simply use1.0instead of1). 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 raisesRuntimeError: 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_maskis the only true source of geometry for a region.area,centroidandboundary_pixelsare 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.
voronoipis 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 forscipy.spatial.Voronoi. - Diagram accuracy (boundary smoothness, area precision) scales with
resolution— low resolutions will show visibly blocky cell edges. boundary_pixelsrequires the optionalscipydependency (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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a254eac6d1fc8d9b75812cb37f4849079d700f02f0b13175a7b3bacc928dbde
|
|
| MD5 |
43997a157a47ece0587e48cea3c4a8f5
|
|
| BLAKE2b-256 |
03b05fc4991df8b4716e2de2a93d05d1d27c8c58976052ffe3798e14f682be87
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
589e4d85d784a950938829b9e0b595fb092a91626282684af0d0be6b95619645
|
|
| MD5 |
334a2804f8051f11dee59c3d312c8303
|
|
| BLAKE2b-256 |
d38716a530cf423182b943fa8b62e19c92a35ebd4ee7cfc45859001fd698dd93
|