ohtli 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.
- ohtli-0.1.0/PKG-INFO +81 -0
- ohtli-0.1.0/README.md +58 -0
- ohtli-0.1.0/ohtli/__init__.py +43 -0
- ohtli-0.1.0/ohtli/geometry.py +99 -0
- ohtli-0.1.0/ohtli/metrics.py +83 -0
- ohtli-0.1.0/ohtli/skeleton.py +111 -0
- ohtli-0.1.0/ohtli/viz.py +232 -0
- ohtli-0.1.0/ohtli.egg-info/PKG-INFO +81 -0
- ohtli-0.1.0/ohtli.egg-info/SOURCES.txt +13 -0
- ohtli-0.1.0/ohtli.egg-info/dependency_links.txt +1 -0
- ohtli-0.1.0/ohtli.egg-info/requires.txt +8 -0
- ohtli-0.1.0/ohtli.egg-info/top_level.txt +1 -0
- ohtli-0.1.0/pyproject.toml +41 -0
- ohtli-0.1.0/setup.cfg +4 -0
- ohtli-0.1.0/tests/test_skeleton.py +108 -0
ohtli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ohtli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cálculo y visualización de β-esqueletos mediante fuerza bruta
|
|
5
|
+
Author-email: Sara Luz Valenzuela Camacho <saraluz@gmail.com>
|
|
6
|
+
Maintainer-email: Sara Luz Valenzuela Camacho <saraluz@gmail.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/SaraLuzVC/Tesis-ITAM
|
|
8
|
+
Project-URL: Documentation, https://github.com/SaraLuzVC/Tesis-ITAM/tree/main/ohtli/README.md
|
|
9
|
+
Keywords: beta-skeleton,computational geometry,graph,archaeology
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: networkx>=3.0
|
|
18
|
+
Requires-Dist: matplotlib>=3.7
|
|
19
|
+
Requires-Dist: adjustText>=0.8
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# beta-skeletons
|
|
25
|
+
|
|
26
|
+
Librería para el cálculo y visualización de β-esqueletos mediante fuerza bruta.
|
|
27
|
+
|
|
28
|
+
## Instalación
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install beta-skeletons
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
O en modo desarrollo:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/tu-usuario/beta-skeletons.git
|
|
38
|
+
cd beta-skeletons
|
|
39
|
+
pip install -e ".[dev]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Uso básico
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
from beta_skeletons import adjacency_matrix, build_graph, plot_skeleton
|
|
47
|
+
from beta_skeletons import relative_asymmetry, control_value, plot_metric_bars
|
|
48
|
+
|
|
49
|
+
coords = np.array([[0,0],[1,0],[2,0],[1,1]], dtype=float)
|
|
50
|
+
names = ["A", "B", "C", "D"]
|
|
51
|
+
|
|
52
|
+
# Matriz de adyacencia
|
|
53
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
54
|
+
|
|
55
|
+
# Grafo de NetworkX
|
|
56
|
+
G = build_graph(coords, beta=2.0, labels=names)
|
|
57
|
+
|
|
58
|
+
# Visualización
|
|
59
|
+
ax, G = plot_skeleton(coords, beta=1.0, labels=names)
|
|
60
|
+
|
|
61
|
+
# Métricas
|
|
62
|
+
ra = relative_asymmetry(G)
|
|
63
|
+
cv = control_value(G)
|
|
64
|
+
plot_metric_bars(ra, metric_name="Asimetría relativa")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Estructura del paquete
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
beta_skeletons/
|
|
71
|
+
├── geometry.py # cálculo de lunas y vecindades
|
|
72
|
+
├── skeleton.py # matriz de adyacencia y construcción del grafo
|
|
73
|
+
├── metrics.py # asimetría relativa y valor de control
|
|
74
|
+
└── viz.py # visualizaciones
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Referencia
|
|
78
|
+
|
|
79
|
+
Kirkpatrick, D. G. y Radke, J. D. (1985).
|
|
80
|
+
*A Framework for Computational Morphology*.
|
|
81
|
+
Computational Geometry, pp. 217-248.
|
ohtli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# beta-skeletons
|
|
2
|
+
|
|
3
|
+
Librería para el cálculo y visualización de β-esqueletos mediante fuerza bruta.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install beta-skeletons
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
O en modo desarrollo:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/tu-usuario/beta-skeletons.git
|
|
15
|
+
cd beta-skeletons
|
|
16
|
+
pip install -e ".[dev]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Uso básico
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
from beta_skeletons import adjacency_matrix, build_graph, plot_skeleton
|
|
24
|
+
from beta_skeletons import relative_asymmetry, control_value, plot_metric_bars
|
|
25
|
+
|
|
26
|
+
coords = np.array([[0,0],[1,0],[2,0],[1,1]], dtype=float)
|
|
27
|
+
names = ["A", "B", "C", "D"]
|
|
28
|
+
|
|
29
|
+
# Matriz de adyacencia
|
|
30
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
31
|
+
|
|
32
|
+
# Grafo de NetworkX
|
|
33
|
+
G = build_graph(coords, beta=2.0, labels=names)
|
|
34
|
+
|
|
35
|
+
# Visualización
|
|
36
|
+
ax, G = plot_skeleton(coords, beta=1.0, labels=names)
|
|
37
|
+
|
|
38
|
+
# Métricas
|
|
39
|
+
ra = relative_asymmetry(G)
|
|
40
|
+
cv = control_value(G)
|
|
41
|
+
plot_metric_bars(ra, metric_name="Asimetría relativa")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Estructura del paquete
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
beta_skeletons/
|
|
48
|
+
├── geometry.py # cálculo de lunas y vecindades
|
|
49
|
+
├── skeleton.py # matriz de adyacencia y construcción del grafo
|
|
50
|
+
├── metrics.py # asimetría relativa y valor de control
|
|
51
|
+
└── viz.py # visualizaciones
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Referencia
|
|
55
|
+
|
|
56
|
+
Kirkpatrick, D. G. y Radke, J. D. (1985).
|
|
57
|
+
*A Framework for Computational Morphology*.
|
|
58
|
+
Computational Geometry, pp. 217-248.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
beta_skeletons
|
|
3
|
+
==============
|
|
4
|
+
Librería para el cálculo y visualización de β-esqueletos.
|
|
5
|
+
|
|
6
|
+
Algoritmo de fuerza bruta — O(n^3).
|
|
7
|
+
|
|
8
|
+
Referencia principal:
|
|
9
|
+
Kirkpatrick, D. G. y Radke, J. D. (1985).
|
|
10
|
+
A Framework for Computational Morphology.
|
|
11
|
+
Computational Geometry, pp. 217-248.
|
|
12
|
+
|
|
13
|
+
Uso básico
|
|
14
|
+
----------
|
|
15
|
+
>>> import numpy as np
|
|
16
|
+
>>> from ohtli import adjacency_matrix, build_graph
|
|
17
|
+
>>> coords = np.array([[0,0],[1,0],[2,0],[1,1]])
|
|
18
|
+
>>> adj = adjacency_matrix(coords, beta=1.0)
|
|
19
|
+
>>> G = build_graph(coords, beta=2.0)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .skeleton import adjacency_matrix, build_graph
|
|
23
|
+
from .geometry import point_in_lune, is_beta_neighbor
|
|
24
|
+
from .metrics import relative_asymmetry, control_value
|
|
25
|
+
from .viz import plot_points, plot_lune, plot_skeleton, plot_metric_bars
|
|
26
|
+
|
|
27
|
+
print("Cargando la librería ohtli...")
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"adjacency_matrix",
|
|
31
|
+
"build_graph",
|
|
32
|
+
"point_in_lune",
|
|
33
|
+
"is_beta_neighbor",
|
|
34
|
+
"relative_asymmetry",
|
|
35
|
+
"control_value",
|
|
36
|
+
"plot_points",
|
|
37
|
+
"plot_lune",
|
|
38
|
+
"plot_skeleton",
|
|
39
|
+
"plot_metric_bars",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
__author__ = "Tu nombre"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
geometry.py
|
|
3
|
+
-----------
|
|
4
|
+
Funciones geométricas para el cálculo de β-esqueletos.
|
|
5
|
+
Implementa la definición de Kirkpatrick y Radke (1985) para
|
|
6
|
+
vecindades tipo luna (lune-based neighbourhoods).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _lune_centers_and_radius(p1: np.ndarray, p2: np.ndarray, beta: float):
|
|
13
|
+
"""
|
|
14
|
+
Calcula los centros y radio de los dos discos que definen
|
|
15
|
+
la luna de influencia del par (p1, p2) para un valor beta dado.
|
|
16
|
+
|
|
17
|
+
Para beta >= 1:
|
|
18
|
+
radio = beta * d(p1, p2) / 2
|
|
19
|
+
centros desplazados hacia el interior del segmento.
|
|
20
|
+
|
|
21
|
+
Para beta < 1:
|
|
22
|
+
radio = d(p1, p2) / (2 * beta)
|
|
23
|
+
centros en p1 y p2.
|
|
24
|
+
|
|
25
|
+
Parámetros
|
|
26
|
+
----------
|
|
27
|
+
p1, p2 : np.ndarray de forma (2,)
|
|
28
|
+
beta : float > 0
|
|
29
|
+
|
|
30
|
+
Retorna
|
|
31
|
+
-------
|
|
32
|
+
center1, center2 : np.ndarray de forma (2,)
|
|
33
|
+
r : float
|
|
34
|
+
"""
|
|
35
|
+
d = np.linalg.norm(p2 - p1)
|
|
36
|
+
if d == 0:
|
|
37
|
+
raise ValueError("Los puntos p1 y p2 son idénticos.")
|
|
38
|
+
if beta <= 0:
|
|
39
|
+
raise ValueError("beta debe ser estrictamente positivo.")
|
|
40
|
+
|
|
41
|
+
if beta >= 1:
|
|
42
|
+
r = beta * d / 2
|
|
43
|
+
direction = (p2 - p1) / d
|
|
44
|
+
center1 = p1 + (1 - beta / 2) * (p2 - p1)
|
|
45
|
+
center2 = p2 + (1 - beta / 2) * (p1 - p2)
|
|
46
|
+
else:
|
|
47
|
+
r = d / (2 * beta)
|
|
48
|
+
center1 = p1.copy()
|
|
49
|
+
center2 = p2.copy()
|
|
50
|
+
|
|
51
|
+
return center1, center2, r
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def point_in_lune(pk: np.ndarray, p1: np.ndarray, p2: np.ndarray, beta: float) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Determina si el punto pk está en el interior de la luna
|
|
57
|
+
de influencia del par (p1, p2) para el parámetro beta.
|
|
58
|
+
|
|
59
|
+
Un punto está en la luna si y solo si está dentro de
|
|
60
|
+
ambos discos simultáneamente (intersección estricta).
|
|
61
|
+
|
|
62
|
+
Parámetros
|
|
63
|
+
----------
|
|
64
|
+
pk : np.ndarray de forma (2,) — punto a evaluar
|
|
65
|
+
p1, p2 : np.ndarray de forma (2,) — par de puntos
|
|
66
|
+
beta : float > 0
|
|
67
|
+
|
|
68
|
+
Retorna
|
|
69
|
+
-------
|
|
70
|
+
bool : True si pk está en el interior de la luna
|
|
71
|
+
"""
|
|
72
|
+
center1, center2, r = _lune_centers_and_radius(p1, p2, beta)
|
|
73
|
+
return (np.linalg.norm(pk - center1) < r and
|
|
74
|
+
np.linalg.norm(pk - center2) < r)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_beta_neighbor(coords: np.ndarray, i: int, j: int, beta: float) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Determina si los puntos i y j son β-vecinos en el conjunto coords,
|
|
80
|
+
es decir, si la luna de influencia del par (i, j) está vacía.
|
|
81
|
+
|
|
82
|
+
Parámetros
|
|
83
|
+
----------
|
|
84
|
+
coords : np.ndarray de forma (n, 2)
|
|
85
|
+
i, j : int — índices del par a evaluar
|
|
86
|
+
beta : float > 0
|
|
87
|
+
|
|
88
|
+
Retorna
|
|
89
|
+
-------
|
|
90
|
+
bool : True si la luna está vacía (i y j son β-vecinos)
|
|
91
|
+
"""
|
|
92
|
+
p1, p2 = coords[i], coords[j]
|
|
93
|
+
n = len(coords)
|
|
94
|
+
for k in range(n):
|
|
95
|
+
if k == i or k == j:
|
|
96
|
+
continue
|
|
97
|
+
if point_in_lune(coords[k], p1, p2, beta):
|
|
98
|
+
return False
|
|
99
|
+
return True
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
metrics.py
|
|
3
|
+
----------
|
|
4
|
+
Métricas de análisis sobre grafos de β-esqueletos.
|
|
5
|
+
|
|
6
|
+
Incluye:
|
|
7
|
+
- Asimetría relativa (RA) por nodo
|
|
8
|
+
- Valor de control por nodo
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def relative_asymmetry(G: nx.Graph) -> dict:
|
|
15
|
+
"""
|
|
16
|
+
Calcula la asimetría relativa (RA) para cada nodo del grafo.
|
|
17
|
+
|
|
18
|
+
La RA mide la centralidad de un nodo en términos de profundidad
|
|
19
|
+
media respecto al resto de nodos del grafo.
|
|
20
|
+
|
|
21
|
+
Fórmula:
|
|
22
|
+
RA_i = 2 * (MD_i - 1) / (n - 2)
|
|
23
|
+
|
|
24
|
+
donde MD_i es la profundidad media del nodo i (media de las
|
|
25
|
+
longitudes de caminos más cortos hacia todos los demás nodos).
|
|
26
|
+
|
|
27
|
+
Parámetros
|
|
28
|
+
----------
|
|
29
|
+
G : nx.Graph
|
|
30
|
+
Grafo conectado.
|
|
31
|
+
|
|
32
|
+
Retorna
|
|
33
|
+
-------
|
|
34
|
+
dict : {nodo: valor_RA}
|
|
35
|
+
|
|
36
|
+
Notas
|
|
37
|
+
-----
|
|
38
|
+
Para grafos desconectados, los nodos sin camino hacia algún otro
|
|
39
|
+
nodo tendrán RA indefinida. Se recomienda verificar conectividad
|
|
40
|
+
antes de aplicar esta métrica.
|
|
41
|
+
"""
|
|
42
|
+
n = G.number_of_nodes()
|
|
43
|
+
if n < 3:
|
|
44
|
+
return {node: 0.0 for node in G.nodes()}
|
|
45
|
+
|
|
46
|
+
ra = {}
|
|
47
|
+
for node in G.nodes():
|
|
48
|
+
lengths = nx.single_source_shortest_path_length(G, node)
|
|
49
|
+
total_depth = sum(d for target, d in lengths.items() if target != node)
|
|
50
|
+
md = total_depth / (n - 1)
|
|
51
|
+
ra[node] = 2 * (md - 1) / (n - 2)
|
|
52
|
+
|
|
53
|
+
return ra
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def control_value(G: nx.Graph) -> dict:
|
|
57
|
+
"""
|
|
58
|
+
Calcula el valor de control para cada nodo del grafo.
|
|
59
|
+
|
|
60
|
+
El valor de control de un nodo i es la suma de los inversos
|
|
61
|
+
del grado de cada uno de sus vecinos:
|
|
62
|
+
|
|
63
|
+
CV_i = sum_{j ∈ N(i)} 1 / deg(j)
|
|
64
|
+
|
|
65
|
+
Un nodo con alto valor de control está conectado a vecinos
|
|
66
|
+
de bajo grado, lo que le otorga mayor influencia estructural.
|
|
67
|
+
|
|
68
|
+
Parámetros
|
|
69
|
+
----------
|
|
70
|
+
G : nx.Graph
|
|
71
|
+
|
|
72
|
+
Retorna
|
|
73
|
+
-------
|
|
74
|
+
dict : {nodo: valor_control}
|
|
75
|
+
"""
|
|
76
|
+
cv = {}
|
|
77
|
+
for node in G.nodes():
|
|
78
|
+
neighbors = list(G.neighbors(node))
|
|
79
|
+
if not neighbors:
|
|
80
|
+
cv[node] = 0.0
|
|
81
|
+
else:
|
|
82
|
+
cv[node] = sum(1 / G.degree(nb) for nb in neighbors)
|
|
83
|
+
return cv
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
skeleton.py
|
|
3
|
+
-----------
|
|
4
|
+
Algoritmo de fuerza bruta para calcular β-esqueletos.
|
|
5
|
+
Complejidad temporal: O(n^3).
|
|
6
|
+
|
|
7
|
+
Referencia:
|
|
8
|
+
Kirkpatrick, D. G. y Radke, J. D. (1985).
|
|
9
|
+
A Framework for Computational Morphology.
|
|
10
|
+
Computational Geometry, pp. 217-248.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import networkx as nx
|
|
15
|
+
|
|
16
|
+
from .geometry import is_beta_neighbor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def adjacency_matrix(coords: np.ndarray, beta: float = 1.0) -> np.ndarray:
|
|
20
|
+
"""
|
|
21
|
+
Calcula la matriz de adyacencia del β-esqueleto de un conjunto
|
|
22
|
+
de puntos mediante fuerza bruta.
|
|
23
|
+
|
|
24
|
+
Para cada par (i, j), verifica si la luna de influencia está vacía.
|
|
25
|
+
La matriz resultante es simétrica y sin auto-lazos.
|
|
26
|
+
|
|
27
|
+
Complejidad: O(n^3)
|
|
28
|
+
|
|
29
|
+
Parámetros
|
|
30
|
+
----------
|
|
31
|
+
coords : np.ndarray de forma (n, 2)
|
|
32
|
+
Coordenadas de los n puntos en el plano.
|
|
33
|
+
beta : float > 0
|
|
34
|
+
Parámetro que controla el tamaño de la región de influencia.
|
|
35
|
+
beta = 1 → Grafo de Gabriel
|
|
36
|
+
beta = 2 → Grafo de vecindad relativa (RNG)
|
|
37
|
+
|
|
38
|
+
Retorna
|
|
39
|
+
-------
|
|
40
|
+
np.ndarray de forma (n, n), dtype int
|
|
41
|
+
Matriz de adyacencia binaria y simétrica.
|
|
42
|
+
|
|
43
|
+
Ejemplos
|
|
44
|
+
--------
|
|
45
|
+
>>> import numpy as np
|
|
46
|
+
>>> from beta_skeletons import adjacency_matrix
|
|
47
|
+
>>> coords = np.array([[0,0],[1,0],[2,0]])
|
|
48
|
+
>>> adjacency_matrix(coords, beta=1.0)
|
|
49
|
+
array([[0, 1, 0],
|
|
50
|
+
[1, 0, 1],
|
|
51
|
+
[0, 1, 0]])
|
|
52
|
+
"""
|
|
53
|
+
coords = np.asarray(coords, dtype=float)
|
|
54
|
+
if coords.ndim != 2 or coords.shape[1] != 2:
|
|
55
|
+
raise ValueError("coords debe tener forma (n, 2).")
|
|
56
|
+
if beta <= 0:
|
|
57
|
+
raise ValueError("beta debe ser estrictamente positivo.")
|
|
58
|
+
|
|
59
|
+
n = len(coords)
|
|
60
|
+
matrix = np.zeros((n, n), dtype=int)
|
|
61
|
+
|
|
62
|
+
for i in range(n):
|
|
63
|
+
for j in range(i + 1, n):
|
|
64
|
+
if is_beta_neighbor(coords, i, j, beta):
|
|
65
|
+
matrix[i, j] = 1
|
|
66
|
+
matrix[j, i] = 1
|
|
67
|
+
|
|
68
|
+
return matrix
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_graph(
|
|
72
|
+
coords: np.ndarray,
|
|
73
|
+
beta: float = 1.0,
|
|
74
|
+
labels: list | None = None,
|
|
75
|
+
) -> nx.Graph:
|
|
76
|
+
"""
|
|
77
|
+
Construye el β-esqueleto como un grafo de NetworkX.
|
|
78
|
+
|
|
79
|
+
Cada nodo almacena su posición ('pos') y, opcionalmente,
|
|
80
|
+
su etiqueta ('label').
|
|
81
|
+
|
|
82
|
+
Parámetros
|
|
83
|
+
----------
|
|
84
|
+
coords : np.ndarray de forma (n, 2)
|
|
85
|
+
beta : float > 0
|
|
86
|
+
labels : list de str, opcional
|
|
87
|
+
Nombres de los nodos. Si es None se usan índices enteros.
|
|
88
|
+
|
|
89
|
+
Retorna
|
|
90
|
+
-------
|
|
91
|
+
nx.Graph
|
|
92
|
+
Grafo con atributos 'pos' y 'label' en cada nodo.
|
|
93
|
+
"""
|
|
94
|
+
coords = np.asarray(coords, dtype=float)
|
|
95
|
+
n = len(coords)
|
|
96
|
+
|
|
97
|
+
if labels is not None and len(labels) != n:
|
|
98
|
+
raise ValueError("labels debe tener la misma longitud que coords.")
|
|
99
|
+
|
|
100
|
+
adj = adjacency_matrix(coords, beta=beta)
|
|
101
|
+
G = nx.Graph()
|
|
102
|
+
|
|
103
|
+
for i, (x, y) in enumerate(coords):
|
|
104
|
+
G.add_node(i, pos=(x, y), label=labels[i] if labels is not None else str(i))
|
|
105
|
+
|
|
106
|
+
for i in range(n):
|
|
107
|
+
for j in range(i + 1, n):
|
|
108
|
+
if adj[i, j] == 1:
|
|
109
|
+
G.add_edge(i, j)
|
|
110
|
+
|
|
111
|
+
return G
|
ohtli-0.1.0/ohtli/viz.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
viz.py
|
|
3
|
+
------
|
|
4
|
+
Funciones de visualización para β-esqueletos y sus métricas.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import matplotlib.cm as cm
|
|
10
|
+
import networkx as nx
|
|
11
|
+
from matplotlib.patches import Circle
|
|
12
|
+
from adjustText import adjust_text
|
|
13
|
+
|
|
14
|
+
from .geometry import _lune_centers_and_radius
|
|
15
|
+
from .skeleton import build_graph
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def plot_points(
|
|
19
|
+
coords: np.ndarray,
|
|
20
|
+
labels: list | None = None,
|
|
21
|
+
ax: plt.Axes | None = None,
|
|
22
|
+
title: str = "Conjunto de puntos",
|
|
23
|
+
) -> plt.Axes:
|
|
24
|
+
"""
|
|
25
|
+
Grafica un conjunto de puntos con etiquetas opcionales.
|
|
26
|
+
|
|
27
|
+
Parámetros
|
|
28
|
+
----------
|
|
29
|
+
coords : np.ndarray de forma (n, 2)
|
|
30
|
+
labels : list de str, opcional
|
|
31
|
+
ax : plt.Axes, opcional — si None se crea una figura nueva
|
|
32
|
+
title : str
|
|
33
|
+
|
|
34
|
+
Retorna
|
|
35
|
+
-------
|
|
36
|
+
plt.Axes
|
|
37
|
+
"""
|
|
38
|
+
if ax is None:
|
|
39
|
+
_, ax = plt.subplots(figsize=(10, 10))
|
|
40
|
+
|
|
41
|
+
ax.scatter(coords[:, 0], coords[:, 1], color="steelblue", s=50, zorder=5)
|
|
42
|
+
|
|
43
|
+
if labels is not None:
|
|
44
|
+
texts = [ax.text(x, y, labels[i], fontsize=8)
|
|
45
|
+
for i, (x, y) in enumerate(coords)]
|
|
46
|
+
adjust_text(
|
|
47
|
+
texts, ax=ax,
|
|
48
|
+
arrowprops=dict(arrowstyle="->", color="gray", lw=0.5),
|
|
49
|
+
expand_points=(1.2, 1.2),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
ax.set_xlabel("Longitud")
|
|
53
|
+
ax.set_ylabel("Latitud")
|
|
54
|
+
ax.set_title(title)
|
|
55
|
+
ax.axis("equal")
|
|
56
|
+
ax.grid(True)
|
|
57
|
+
return ax
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plot_lune(
|
|
61
|
+
coords: np.ndarray,
|
|
62
|
+
i: int,
|
|
63
|
+
j: int,
|
|
64
|
+
beta: float = 1.0,
|
|
65
|
+
labels: list | None = None,
|
|
66
|
+
ax: plt.Axes | None = None,
|
|
67
|
+
) -> plt.Axes:
|
|
68
|
+
"""
|
|
69
|
+
Visualiza la luna de influencia del par (i, j) para un beta dado,
|
|
70
|
+
clasificando los puntos restantes según si caen dentro o fuera.
|
|
71
|
+
|
|
72
|
+
Parámetros
|
|
73
|
+
----------
|
|
74
|
+
coords : np.ndarray de forma (n, 2)
|
|
75
|
+
i, j : int — índices del par a visualizar
|
|
76
|
+
beta : float > 0
|
|
77
|
+
labels : list de str, opcional
|
|
78
|
+
ax : plt.Axes, opcional
|
|
79
|
+
|
|
80
|
+
Retorna
|
|
81
|
+
-------
|
|
82
|
+
plt.Axes
|
|
83
|
+
"""
|
|
84
|
+
if ax is None:
|
|
85
|
+
_, ax = plt.subplots(figsize=(10, 10))
|
|
86
|
+
|
|
87
|
+
p1, p2 = coords[i], coords[j]
|
|
88
|
+
center1, center2, r = _lune_centers_and_radius(p1, p2, beta)
|
|
89
|
+
n = len(coords)
|
|
90
|
+
|
|
91
|
+
in_lune, out_lune = [], []
|
|
92
|
+
for k in range(n):
|
|
93
|
+
if k in (i, j):
|
|
94
|
+
continue
|
|
95
|
+
pk = coords[k]
|
|
96
|
+
inside = (np.linalg.norm(pk - center1) < r and
|
|
97
|
+
np.linalg.norm(pk - center2) < r)
|
|
98
|
+
(in_lune if inside else out_lune).append((pk, k))
|
|
99
|
+
|
|
100
|
+
if out_lune:
|
|
101
|
+
pts = np.array([p for p, _ in out_lune])
|
|
102
|
+
ax.scatter(pts[:, 0], pts[:, 1], color="lightgray",
|
|
103
|
+
label="Fuera de la luna", zorder=3)
|
|
104
|
+
|
|
105
|
+
if in_lune:
|
|
106
|
+
pts = np.array([p for p, _ in in_lune])
|
|
107
|
+
ax.scatter(pts[:, 0], pts[:, 1], color="steelblue",
|
|
108
|
+
label="Dentro de la luna", zorder=4)
|
|
109
|
+
|
|
110
|
+
ax.scatter([p1[0], p2[0]], [p1[1], p2[1]],
|
|
111
|
+
color="crimson", zorder=5, label="Par evaluado")
|
|
112
|
+
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], "k--", alpha=0.4)
|
|
113
|
+
|
|
114
|
+
name_i = labels[i] if labels else str(i)
|
|
115
|
+
name_j = labels[j] if labels else str(j)
|
|
116
|
+
offset = r * 0.03
|
|
117
|
+
ax.text(p1[0] + offset, p1[1] + offset, name_i, fontsize=9)
|
|
118
|
+
ax.text(p2[0] + offset, p2[1] + offset, name_j, fontsize=9)
|
|
119
|
+
|
|
120
|
+
ax.add_patch(Circle(center1, r, color="steelblue",
|
|
121
|
+
fill=False, linestyle="--",
|
|
122
|
+
label=f"Disco de {name_i}"))
|
|
123
|
+
ax.add_patch(Circle(center2, r, color="seagreen",
|
|
124
|
+
fill=False, linestyle="--",
|
|
125
|
+
label=f"Disco de {name_j}"))
|
|
126
|
+
|
|
127
|
+
ax.set_xlabel("Longitud")
|
|
128
|
+
ax.set_ylabel("Latitud")
|
|
129
|
+
ax.set_title(f"Luna β = {beta} — '{name_i}' y '{name_j}'")
|
|
130
|
+
ax.axis("equal")
|
|
131
|
+
ax.grid(True)
|
|
132
|
+
ax.legend(fontsize=8)
|
|
133
|
+
return ax
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def plot_skeleton(
|
|
137
|
+
coords: np.ndarray,
|
|
138
|
+
beta: float = 1.0,
|
|
139
|
+
labels: list | None = None,
|
|
140
|
+
node_color: str | list = "skyblue",
|
|
141
|
+
cmap=None,
|
|
142
|
+
colorbar_label: str | None = None,
|
|
143
|
+
ax: plt.Axes | None = None,
|
|
144
|
+
title: str | None = None,
|
|
145
|
+
) -> tuple[plt.Axes, nx.Graph]:
|
|
146
|
+
"""
|
|
147
|
+
Calcula y grafica el β-esqueleto.
|
|
148
|
+
|
|
149
|
+
Parámetros
|
|
150
|
+
----------
|
|
151
|
+
coords : np.ndarray de forma (n, 2)
|
|
152
|
+
beta : float > 0
|
|
153
|
+
labels : list de str, opcional
|
|
154
|
+
node_color : str o list — color fijo o lista de valores numéricos
|
|
155
|
+
cmap : colormap de matplotlib, opcional
|
|
156
|
+
colorbar_label : str, opcional — título de la barra de color
|
|
157
|
+
ax : plt.Axes, opcional
|
|
158
|
+
title : str, opcional
|
|
159
|
+
|
|
160
|
+
Retorna
|
|
161
|
+
-------
|
|
162
|
+
(plt.Axes, nx.Graph)
|
|
163
|
+
"""
|
|
164
|
+
if ax is None:
|
|
165
|
+
_, ax = plt.subplots(figsize=(12, 12))
|
|
166
|
+
|
|
167
|
+
G = build_graph(coords, beta=beta, labels=labels)
|
|
168
|
+
pos = nx.get_node_attributes(G, "pos")
|
|
169
|
+
node_labels = nx.get_node_attributes(G, "label")
|
|
170
|
+
|
|
171
|
+
draw_kwargs = dict(
|
|
172
|
+
pos=pos, ax=ax, with_labels=False,
|
|
173
|
+
node_size=100, edge_color="gray",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if cmap is not None and isinstance(node_color, (list, np.ndarray)):
|
|
177
|
+
norm = plt.Normalize(vmin=min(node_color), vmax=max(node_color))
|
|
178
|
+
draw_kwargs.update(node_color=node_color, cmap=cmap)
|
|
179
|
+
sm = cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
180
|
+
sm.set_array([])
|
|
181
|
+
cbar = plt.colorbar(sm, ax=ax, shrink=0.7)
|
|
182
|
+
if colorbar_label:
|
|
183
|
+
cbar.set_label(colorbar_label)
|
|
184
|
+
else:
|
|
185
|
+
draw_kwargs["node_color"] = node_color
|
|
186
|
+
|
|
187
|
+
nx.draw(G, **draw_kwargs)
|
|
188
|
+
|
|
189
|
+
if labels is not None:
|
|
190
|
+
texts = [ax.text(pos[i][0], pos[i][1], node_labels[i], fontsize=8)
|
|
191
|
+
for i in G.nodes()]
|
|
192
|
+
adjust_text(
|
|
193
|
+
texts,
|
|
194
|
+
arrowprops=dict(arrowstyle="->", color="gray", lw=0.5),
|
|
195
|
+
expand_points=(1.2, 1.2),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
ax.set_title(title or f"β-esqueleto (β = {beta})")
|
|
199
|
+
ax.axis("equal")
|
|
200
|
+
ax.grid(True)
|
|
201
|
+
return ax, G
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def plot_metric_bars(
|
|
205
|
+
metric_values: dict,
|
|
206
|
+
metric_name: str = "Métrica",
|
|
207
|
+
ax: plt.Axes | None = None,
|
|
208
|
+
) -> plt.Axes:
|
|
209
|
+
"""
|
|
210
|
+
Grafica un diagrama de barras de una métrica por nodo.
|
|
211
|
+
|
|
212
|
+
Parámetros
|
|
213
|
+
----------
|
|
214
|
+
metric_values : dict {nodo: valor}
|
|
215
|
+
metric_name : str — nombre de la métrica para etiquetas
|
|
216
|
+
ax : plt.Axes, opcional
|
|
217
|
+
|
|
218
|
+
Retorna
|
|
219
|
+
-------
|
|
220
|
+
plt.Axes
|
|
221
|
+
"""
|
|
222
|
+
if ax is None:
|
|
223
|
+
_, ax = plt.subplots(figsize=(20, 6))
|
|
224
|
+
|
|
225
|
+
nodes, values = zip(*sorted(metric_values.items()))
|
|
226
|
+
ax.bar(nodes, values)
|
|
227
|
+
ax.set_xticks(nodes)
|
|
228
|
+
ax.set_xlabel("Nodo")
|
|
229
|
+
ax.set_ylabel(metric_name)
|
|
230
|
+
ax.set_title(f"{metric_name} por nodo")
|
|
231
|
+
ax.grid(axis="y", linestyle="--", alpha=0.5)
|
|
232
|
+
return ax
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ohtli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cálculo y visualización de β-esqueletos mediante fuerza bruta
|
|
5
|
+
Author-email: Sara Luz Valenzuela Camacho <saraluz@gmail.com>
|
|
6
|
+
Maintainer-email: Sara Luz Valenzuela Camacho <saraluz@gmail.com>
|
|
7
|
+
Project-URL: Homepage, https://github.com/SaraLuzVC/Tesis-ITAM
|
|
8
|
+
Project-URL: Documentation, https://github.com/SaraLuzVC/Tesis-ITAM/tree/main/ohtli/README.md
|
|
9
|
+
Keywords: beta-skeleton,computational geometry,graph,archaeology
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: networkx>=3.0
|
|
18
|
+
Requires-Dist: matplotlib>=3.7
|
|
19
|
+
Requires-Dist: adjustText>=0.8
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# beta-skeletons
|
|
25
|
+
|
|
26
|
+
Librería para el cálculo y visualización de β-esqueletos mediante fuerza bruta.
|
|
27
|
+
|
|
28
|
+
## Instalación
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install beta-skeletons
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
O en modo desarrollo:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/tu-usuario/beta-skeletons.git
|
|
38
|
+
cd beta-skeletons
|
|
39
|
+
pip install -e ".[dev]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Uso básico
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
from beta_skeletons import adjacency_matrix, build_graph, plot_skeleton
|
|
47
|
+
from beta_skeletons import relative_asymmetry, control_value, plot_metric_bars
|
|
48
|
+
|
|
49
|
+
coords = np.array([[0,0],[1,0],[2,0],[1,1]], dtype=float)
|
|
50
|
+
names = ["A", "B", "C", "D"]
|
|
51
|
+
|
|
52
|
+
# Matriz de adyacencia
|
|
53
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
54
|
+
|
|
55
|
+
# Grafo de NetworkX
|
|
56
|
+
G = build_graph(coords, beta=2.0, labels=names)
|
|
57
|
+
|
|
58
|
+
# Visualización
|
|
59
|
+
ax, G = plot_skeleton(coords, beta=1.0, labels=names)
|
|
60
|
+
|
|
61
|
+
# Métricas
|
|
62
|
+
ra = relative_asymmetry(G)
|
|
63
|
+
cv = control_value(G)
|
|
64
|
+
plot_metric_bars(ra, metric_name="Asimetría relativa")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Estructura del paquete
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
beta_skeletons/
|
|
71
|
+
├── geometry.py # cálculo de lunas y vecindades
|
|
72
|
+
├── skeleton.py # matriz de adyacencia y construcción del grafo
|
|
73
|
+
├── metrics.py # asimetría relativa y valor de control
|
|
74
|
+
└── viz.py # visualizaciones
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Referencia
|
|
78
|
+
|
|
79
|
+
Kirkpatrick, D. G. y Radke, J. D. (1985).
|
|
80
|
+
*A Framework for Computational Morphology*.
|
|
81
|
+
Computational Geometry, pp. 217-248.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ohtli/__init__.py
|
|
4
|
+
ohtli/geometry.py
|
|
5
|
+
ohtli/metrics.py
|
|
6
|
+
ohtli/skeleton.py
|
|
7
|
+
ohtli/viz.py
|
|
8
|
+
ohtli.egg-info/PKG-INFO
|
|
9
|
+
ohtli.egg-info/SOURCES.txt
|
|
10
|
+
ohtli.egg-info/dependency_links.txt
|
|
11
|
+
ohtli.egg-info/requires.txt
|
|
12
|
+
ohtli.egg-info/top_level.txt
|
|
13
|
+
tests/test_skeleton.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ohtli
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ohtli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cálculo y visualización de β-esqueletos mediante fuerza bruta"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {file = "LICENSE.txt"}
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Sara Luz Valenzuela Camacho", email = "saraluz@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
maintainers = [
|
|
15
|
+
{ name = "Sara Luz Valenzuela Camacho", email = "saraluz@gmail.com" }
|
|
16
|
+
]
|
|
17
|
+
keywords = ["beta-skeleton", "computational geometry", "graph", "archaeology"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: BSD License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"numpy>=1.24",
|
|
27
|
+
"networkx>=3.0",
|
|
28
|
+
"matplotlib>=3.7",
|
|
29
|
+
"adjustText>=0.8",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = ["pytest", "pytest-cov"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["."]
|
|
37
|
+
include = ["ohtli*"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/SaraLuzVC/Tesis-ITAM"
|
|
41
|
+
Documentation = "https://github.com/SaraLuzVC/Tesis-ITAM/tree/main/ohtli/README.md"
|
ohtli-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests/test_skeleton.py
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from beta_skeletons import (
|
|
8
|
+
adjacency_matrix,
|
|
9
|
+
build_graph,
|
|
10
|
+
point_in_lune,
|
|
11
|
+
is_beta_neighbor,
|
|
12
|
+
relative_asymmetry,
|
|
13
|
+
control_value,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── geometría ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
def test_point_in_lune_center_is_inside():
|
|
20
|
+
"""El punto medio entre p1 y p2 está dentro de la luna para beta=2."""
|
|
21
|
+
p1 = np.array([0.0, 0.0])
|
|
22
|
+
p2 = np.array([4.0, 0.0])
|
|
23
|
+
midpoint = np.array([2.0, 0.0])
|
|
24
|
+
assert point_in_lune(midpoint, p1, p2, beta=2.0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_point_in_lune_far_point_is_outside():
|
|
28
|
+
"""Un punto lejano no está en la luna."""
|
|
29
|
+
p1 = np.array([0.0, 0.0])
|
|
30
|
+
p2 = np.array([1.0, 0.0])
|
|
31
|
+
far = np.array([10.0, 10.0])
|
|
32
|
+
assert not point_in_lune(far, p1, p2, beta=1.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_identical_points_raises():
|
|
36
|
+
from beta_skeletons.geometry import _lune_centers_and_radius
|
|
37
|
+
with pytest.raises(ValueError):
|
|
38
|
+
_lune_centers_and_radius(np.array([0.0, 0.0]), np.array([0.0, 0.0]), beta=1.0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── matriz de adyacencia ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def test_adjacency_matrix_is_symmetric():
|
|
44
|
+
coords = np.array([[0, 0], [1, 0], [2, 0], [1, 1]], dtype=float)
|
|
45
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
46
|
+
np.testing.assert_array_equal(adj, adj.T)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_adjacency_matrix_no_self_loops():
|
|
50
|
+
coords = np.array([[0, 0], [1, 0], [2, 0]], dtype=float)
|
|
51
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
52
|
+
assert np.all(np.diag(adj) == 0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_collinear_points_gabriel():
|
|
56
|
+
"""Para tres puntos colineales, solo los adyacentes deben conectarse (beta=1)."""
|
|
57
|
+
coords = np.array([[0, 0], [1, 0], [2, 0]], dtype=float)
|
|
58
|
+
adj = adjacency_matrix(coords, beta=1.0)
|
|
59
|
+
assert adj[0, 1] == 1
|
|
60
|
+
assert adj[1, 2] == 1
|
|
61
|
+
assert adj[0, 2] == 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_invalid_beta_raises():
|
|
65
|
+
coords = np.array([[0, 0], [1, 0]], dtype=float)
|
|
66
|
+
with pytest.raises(ValueError):
|
|
67
|
+
adjacency_matrix(coords, beta=-1.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── grafo ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
def test_build_graph_node_count():
|
|
73
|
+
coords = np.array([[0, 0], [1, 0], [2, 0]], dtype=float)
|
|
74
|
+
G = build_graph(coords, beta=1.0)
|
|
75
|
+
assert G.number_of_nodes() == 3
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_build_graph_with_labels():
|
|
79
|
+
coords = np.array([[0, 0], [1, 0]], dtype=float)
|
|
80
|
+
G = build_graph(coords, beta=1.0, labels=["A", "B"])
|
|
81
|
+
assert G.nodes[0]["label"] == "A"
|
|
82
|
+
assert G.nodes[1]["label"] == "B"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── métricas ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def test_relative_asymmetry_keys():
|
|
88
|
+
coords = np.array([[0, 0], [1, 0], [2, 0]], dtype=float)
|
|
89
|
+
G = build_graph(coords, beta=1.0)
|
|
90
|
+
ra = relative_asymmetry(G)
|
|
91
|
+
assert set(ra.keys()) == set(G.nodes())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_control_value_keys():
|
|
95
|
+
coords = np.array([[0, 0], [1, 0], [2, 0]], dtype=float)
|
|
96
|
+
G = build_graph(coords, beta=1.0)
|
|
97
|
+
cv = control_value(G)
|
|
98
|
+
assert set(cv.keys()) == set(G.nodes())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_isolated_node_control_value_is_zero():
|
|
102
|
+
import networkx as nx
|
|
103
|
+
G = nx.Graph()
|
|
104
|
+
G.add_node(0)
|
|
105
|
+
G.add_node(1)
|
|
106
|
+
G.add_edge(1, 2)
|
|
107
|
+
cv = control_value(G)
|
|
108
|
+
assert cv[0] == 0.0
|