Metadata-Version: 2.4
Name: voronoip
Version: 0.2.0
Summary: Weighted Voronoi Diagrams — multiplicative, additive and power (Laguerre) modes
Author-email: Emerson Marreiros <ec2763@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/emerson-marreiros/voronoip
Project-URL: Repository, https://github.com/emerson-marreiros/voronoip
Project-URL: Issues, https://github.com/emerson-marreiros/voronoip/issues
Keywords: voronoi,weighted voronoi,computational geometry,diagram,laguerre
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: matplotlib>=3.5
Provides-Extra: full
Requires-Dist: scipy>=1.9; extra == "full"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-env; extra == "dev"
Requires-Dist: scipy>=1.9; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install voronoip
```

Or, for local development:

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

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

---

## Quick start

```python
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

```python
# 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.

```python
# 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

```python
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`

```python
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`

```python
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`

```python
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

```python
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)

```python
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

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

### Distance field heat-map

```python
wv.plot_distance_field(cmap="plasma")
```

### Querying which region owns a point

```python
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

```python
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
