voronoip 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
voronoip-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emerson Charles do Nascimento Marreiros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include voronoip *.py
4
+ recursive-include tests *.py
@@ -0,0 +1,439 @@
1
+ Metadata-Version: 2.4
2
+ Name: voronoip
3
+ Version: 0.1.0
4
+ Summary: Weighted Voronoi Diagrams — multiplicative, additive and power (Laguerre) modes
5
+ Author-email: Emerson Marreiros <ec2763@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/emerson-marreiros/voronoip
8
+ Project-URL: Repository, https://github.com/emerson-marreiros/voronoip
9
+ Project-URL: Issues, https://github.com/emerson-marreiros/voronoip/issues
10
+ Keywords: voronoi,weighted voronoi,computational geometry,diagram,laguerre
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering :: Visualization
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.23
23
+ Requires-Dist: matplotlib>=3.5
24
+ Provides-Extra: full
25
+ Requires-Dist: scipy>=1.9; extra == "full"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-env; extra == "dev"
29
+ Requires-Dist: scipy>=1.9; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # voronoip — Weighted Voronoi Diagrams for Python
35
+
36
+ A lightweight, dependency-light Python library for constructing and
37
+ visualising **weighted Voronoi diagrams**, including:
38
+
39
+ | Mode | Distance function | Effect of larger weight |
40
+ |---|---|---|
41
+ | `"multiplicative"` | `dist(p,g) / w(g)` | larger region |
42
+ | `"additive"` | `dist(p,g) − w(g)` | larger region |
43
+ | `"power"` | `dist(p,g)² − w(g)²` | larger region (power diagram) |
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install voronoip
51
+ ```
52
+
53
+ Or, for local development:
54
+
55
+ ```bash
56
+ pip install numpy scipy matplotlib
57
+ # clone / copy voronoip/ into your project
58
+ ```
59
+
60
+ Dependencies: **numpy**, **scipy** (optional, for `boundary_pixels`),
61
+ **matplotlib** (for visualisation).
62
+
63
+ ---
64
+
65
+ ## Quick start
66
+
67
+ ```python
68
+ import numpy as np
69
+ from voronoip import WeightedVoronoi
70
+
71
+ pts = np.array([[0.2, 0.3],
72
+ [0.7, 0.6],
73
+ [0.5, 0.1],
74
+ [0.1, 0.9]])
75
+ w = np.array([1.0, 2.5, 0.5, 1.8])
76
+
77
+ wv = WeightedVoronoi(pts, w, mode="multiplicative", resolution=512)
78
+ wv.compute()
79
+ wv.plot() # shows an interactive matplotlib figure
80
+ wv.to_png("out.png")
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ⚠️ Two mistakes almost everyone makes at first
86
+
87
+ Before jumping into the examples below, read this. These two mistakes
88
+ account for the vast majority of `TypeError` / `AttributeError` reports
89
+ from new users — both examples further down show exactly how to avoid
90
+ them.
91
+
92
+ ### 1. `compute()` returns the object itself, not a list of regions
93
+
94
+ ```python
95
+ # ❌ WRONG — celulas becomes the WeightedVoronoi object, not a list
96
+ celulas = diagrama.compute()
97
+ for celula in celulas: # TypeError: 'WeightedVoronoi' object is not iterable
98
+ ...
99
+
100
+ # ✅ CORRECT — compute() returns self (useful for chaining);
101
+ # the actual list of regions lives in .regions
102
+ diagrama.compute()
103
+ for regiao in diagrama.regions:
104
+ ...
105
+
106
+ # Also valid, thanks to chaining:
107
+ regioes = diagrama.compute().regions
108
+ ```
109
+
110
+ ### 2. `voronoip` is **raster-based**, not vector-based — there is no `.vertices`
111
+
112
+ If you've used `scipy.spatial.Voronoi` before, you're used to each
113
+ region being a polygon with `.vertices`. `voronoip` works differently:
114
+ it rasterises the diagram onto a pixel grid (`label_grid`), and each
115
+ `VoronoiRegion` is described by a boolean **pixel mask**, not a list of
116
+ polygon corners.
117
+
118
+ ```python
119
+ # ❌ WRONG — VoronoiRegion has no .vertices attribute
120
+ poligono = np.array(regiao.vertices)
121
+ plt.fill(poligono[:, 0], poligono[:, 1])
122
+
123
+ # ✅ CORRECT — let the built-in plot() draw the diagram for you
124
+ fig, ax = diagrama.plot()
125
+
126
+ # ✅ Or, if you need region data programmatically:
127
+ regiao.pixel_mask # (H, W) bool — True where pixels belong to this region
128
+ regiao.area # int — pixel count
129
+ regiao.centroid # (x, y) — mean position of the region
130
+ ```
131
+
132
+ If you specifically need polygon-style output, this is on the roadmap
133
+ (see *Limitations* below) — it is not currently supported.
134
+
135
+ ---
136
+
137
+ ## API reference
138
+
139
+ ### `WeightedVoronoi(points, weights, **kwargs)`
140
+
141
+ | Parameter | Default | Description |
142
+ |---|---|---|
143
+ | `points` | — | `(N, 2)` generator coordinates |
144
+ | `weights` | — | `(N,)` generator weights |
145
+ | `mode` | `"multiplicative"` | distance metric |
146
+ | `resolution` | `512` | pixels along longer axis |
147
+ | `domain` | auto (bounding box + 5 %) | `((xmin,xmax),(ymin,ymax))` |
148
+ | `palette` | `"tab20"` | matplotlib colormap name |
149
+ | `show_generators` | `True` | draw seed points |
150
+ | `show_weights` | `False` | annotate weights |
151
+ | `show_boundaries` | `True` | draw cell edges |
152
+
153
+ > **Tip:** always pass `points` and `weights` as `float` arrays
154
+ > (`np.array([...], dtype=float)` or simply use `1.0` instead of `1`).
155
+ > Integer arrays work in most cases, but mixing them with weight-based
156
+ > division (`mode="multiplicative"`) can produce unexpected integer
157
+ > truncation in edge cases — floats avoid the ambiguity entirely.
158
+
159
+ #### Methods
160
+
161
+ ```python
162
+ wv.compute() # rasterise the diagram (required first) — returns self
163
+
164
+ wv.plot(**kwargs) # returns (fig, ax)
165
+ wv.plot_distance_field() # heat-map of min weighted distance
166
+ wv.plot_comparison() # side-by-side of all 3 modes
167
+
168
+ wv.owner(x, y) # generator index owning (x, y)
169
+ wv.region_of(x, y) # VoronoiRegion containing (x, y)
170
+ wv.nearest_generators(x, y, k=3) # k nearest generators by weighted dist
171
+
172
+ wv.to_png("out.png", dpi=150)
173
+ wv.to_svg("out.svg")
174
+ wv.to_csv("out.csv") # index, x, y, weight, area, centroid
175
+ wv.to_label_array() # (H, W) int ndarray — copy
176
+ ```
177
+
178
+ > **Important:** every query method (`owner`, `region_of`,
179
+ > `nearest_generators`) and every plotting/export method requires
180
+ > `.compute()` to have been called first. Calling them beforehand
181
+ > raises `RuntimeError: Call .compute() before accessing diagram data
182
+ > or plotting.` — this is intentional, not a bug.
183
+
184
+ #### Key attributes (after `compute()`)
185
+
186
+ | Attribute | Type | Description |
187
+ |---|---|---|
188
+ | `label_grid` | `(H, W) int32` | generator index per pixel |
189
+ | `dist_grid` | `(H, W) float64` | minimum weighted distance per pixel |
190
+ | `regions` | `list[VoronoiRegion]` | one object per generator — **this is what you iterate over** |
191
+
192
+ ---
193
+
194
+ ### `VoronoiRegion`
195
+
196
+ ```python
197
+ r = wv.regions[0]
198
+
199
+ r.index # int — generator index
200
+ r.generator # (2,) float — (x, y)
201
+ r.weight # float
202
+ r.pixel_mask # (H, W) bool
203
+ r.color # (R, G, B) tuple
204
+
205
+ r.area # int — number of pixels
206
+ r.centroid # (2,) float — mean (x, y) of mask pixels
207
+ r.boundary_pixels # (K, 2) row/col indices of boundary pixels
208
+ ```
209
+
210
+ > Note that `r.pixel_mask` is the only true source of geometry for a
211
+ > region. `area`, `centroid` and `boundary_pixels` are all *derived*
212
+ > from it — there is no separate vector representation.
213
+
214
+ ---
215
+
216
+ ### `voronoip.generators`
217
+
218
+ ```python
219
+ from voronoip.generators import (
220
+ random_generators, # uniform random
221
+ grid_generators, # regular grid with optional jitter
222
+ poisson_disk_generators, # Bridson blue-noise sampling
223
+ )
224
+
225
+ pts, w = random_generators(n=20, weight_range=(0.5, 2.0), seed=42)
226
+ pts, w = grid_generators(nx=6, ny=6, jitter=0.04, seed=0)
227
+ pts, w = poisson_disk_generators(min_dist=0.1, seed=7)
228
+ ```
229
+
230
+ All functions return `(points, weights)` tuples ready for
231
+ `WeightedVoronoi`.
232
+
233
+ ---
234
+
235
+ ### `voronoip.metrics`
236
+
237
+ ```python
238
+ from voronoip.metrics import (
239
+ multiplicative_weighted_distance, # scalar
240
+ additive_weighted_distance,
241
+ power_distance,
242
+ batch_multiplicative, # vectorised over generators
243
+ batch_additive,
244
+ batch_power,
245
+ )
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Full worked examples
251
+
252
+ The two examples below are deliberately written end-to-end, including
253
+ the result of `.regions` and `.owner()`, so you can copy them as a
254
+ starting template for your own scripts without hitting the two
255
+ mistakes described above.
256
+
257
+ ### Example 1 — Basic diagram with 4 weighted points
258
+
259
+ ```python
260
+ import numpy as np
261
+ import matplotlib.pyplot as plt
262
+ from voronoip import WeightedVoronoi
263
+
264
+ # Points (x, y) — always use floats
265
+ pontos = np.array([
266
+ [1.0, 1.0],
267
+ [5.0, 2.0],
268
+ [3.0, 6.0],
269
+ [7.0, 7.0]
270
+ ])
271
+
272
+ # Weight associated with each point
273
+ pesos = np.array([1.0, 2.0, 0.5, 3.0])
274
+
275
+ # Create the weighted Voronoi object
276
+ vor = WeightedVoronoi(
277
+ points=pontos,
278
+ weights=pesos,
279
+ mode="multiplicative",
280
+ resolution=512,
281
+ show_weights=True # annotate weights directly on the plot
282
+ )
283
+
284
+ # compute() returns self — do NOT reassign it to "regioes"
285
+ vor.compute()
286
+
287
+ # ── Visualization (built-in, no manual polygon drawing needed) ─────
288
+ fig, ax = vor.plot()
289
+ ax.set_title("Diagrama de Voronoi Ponderado - voronoip")
290
+ plt.show()
291
+
292
+ # ── Region data — iterate over .regions, not over vor itself ───────
293
+ print("Regiões:")
294
+ for regiao in vor.regions:
295
+ print(regiao)
296
+ print(f" Área: {regiao.area} px")
297
+ print(f" Centróide: {regiao.centroid}")
298
+ print(f" Peso: {regiao.weight}")
299
+
300
+ # ── Query which region owns an arbitrary point ──────────────────────
301
+ x, y = 4.0, 4.0
302
+ idx = vor.owner(x, y)
303
+ print(f"\nDono do ponto ({x}, {y}): gerador {idx} → {vor.regions[idx]}")
304
+ ```
305
+
306
+ ### Example 2 — Antenna signal coverage (real-world use case)
307
+
308
+ ```python
309
+ import numpy as np
310
+ import matplotlib.pyplot as plt
311
+ from voronoip import WeightedVoronoi
312
+
313
+ # Antenna locations
314
+ antenas = np.array([
315
+ [2.0, 8.0],
316
+ [8.0, 9.0],
317
+ [5.0, 5.0],
318
+ [1.0, 2.0],
319
+ [9.0, 3.0]
320
+ ])
321
+
322
+ # Signal strength (weight) — higher power covers a larger area
323
+ potencia = np.array([5.0, 3.0, 2.0, 1.0, 4.0])
324
+
325
+ diagrama = WeightedVoronoi(
326
+ points=antenas,
327
+ weights=potencia,
328
+ mode="multiplicative",
329
+ resolution=512,
330
+ show_generators=False, # we'll draw the antennas manually below
331
+ show_weights=False,
332
+ )
333
+
334
+ diagrama.compute() # ✅ no reassignment — returns self
335
+
336
+ # ── Visualization ────────────────────────────────────────────────────
337
+ fig, ax = diagrama.plot()
338
+
339
+ # Custom antenna markers (triangles instead of the default dots)
340
+ ax.scatter(antenas[:, 0], antenas[:, 1],
341
+ s=180, c="black", marker="^", zorder=6)
342
+
343
+ # Power labels
344
+ for i, p in enumerate(potencia):
345
+ x, y = antenas[i]
346
+ ax.text(x + 0.15, y, f"P={p}", fontsize=9, zorder=7,
347
+ bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.6, lw=0))
348
+
349
+ ax.set_title("Cobertura de Antenas usando Voronoi Ponderado")
350
+ ax.set_xlabel("X")
351
+ ax.set_ylabel("Y")
352
+ ax.grid(True, alpha=0.3)
353
+ plt.show()
354
+
355
+ # ── Coverage data per antenna — iterate over .regions ───────────────
356
+ print("Cobertura por antena:")
357
+ for regiao in diagrama.regions:
358
+ i = regiao.index
359
+ cx, cy = regiao.centroid
360
+ print(f" Antena {i+1} (P={potencia[i]}) "
361
+ f"→ área: {regiao.area} px "
362
+ f"centróide: ({cx:.2f}, {cy:.2f})")
363
+
364
+ # ── Signal intensity heat-map ───────────────────────────────────────
365
+ fig2, ax2 = diagrama.plot_distance_field(cmap="plasma")
366
+ ax2.set_title("Intensidade de Sinal (distância ponderada)")
367
+ plt.show()
368
+ ```
369
+
370
+ ---
371
+
372
+ ## More examples
373
+
374
+ ### Comparison of all three modes
375
+
376
+ ```python
377
+ wv = WeightedVoronoi(pts, w, mode="multiplicative", resolution=400)
378
+ wv.compute()
379
+ fig, axes = wv.plot_comparison(figsize=(18, 6))
380
+ ```
381
+
382
+ ### Distance field heat-map
383
+
384
+ ```python
385
+ wv.plot_distance_field(cmap="plasma")
386
+ ```
387
+
388
+ ### Querying which region owns a point
389
+
390
+ ```python
391
+ idx = wv.owner(0.5, 0.5)
392
+ region = wv.region_of(0.5, 0.5)
393
+ print(region)
394
+ # VoronoiRegion(index=1, generator=(0.700, 0.600), weight=2.500, area=14832 px)
395
+ ```
396
+
397
+ ### Exporting
398
+
399
+ ```python
400
+ wv.to_png("voronoi.png", dpi=200)
401
+ wv.to_svg("voronoi.svg")
402
+ wv.to_csv("voronoi.csv")
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Limitations
408
+
409
+ - **No polygon/vector output yet.** `voronoip` is raster-first by
410
+ design — regions are pixel masks, not lists of polygon vertices.
411
+ If your use case strictly requires exact vector polygons (e.g. for
412
+ GIS or CAD pipelines), this library is not yet a drop-in
413
+ replacement for `scipy.spatial.Voronoi`.
414
+ - Diagram accuracy (boundary smoothness, area precision) scales with
415
+ `resolution` — low resolutions will show visibly blocky cell edges.
416
+ - `boundary_pixels` requires the optional `scipy` dependency
417
+ (`pip install voronoip[full]`).
418
+
419
+ ---
420
+
421
+ ## Project structure
422
+
423
+ ```
424
+ voronoip/
425
+ ├── __init__.py # public API
426
+ ├── diagram.py # WeightedVoronoi class
427
+ ├── region.py # VoronoiRegion dataclass
428
+ ├── generators.py # random / grid / Poisson-disk seed generators
429
+ └── metrics.py # distance functions + registry
430
+ tests/
431
+ └── test_voronoip.py # full test suite (pytest)
432
+ README.md
433
+ ```
434
+
435
+ ---
436
+
437
+ ## License
438
+
439
+ MIT