lp2graph 0.3.0__py3-none-any.whl

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.
Files changed (80) hide show
  1. lp2graph/__init__.py +54 -0
  2. lp2graph/cli.py +238 -0
  3. lp2graph/codec/__init__.py +41 -0
  4. lp2graph/codec/latex.py +884 -0
  5. lp2graph/codec/normalize.py +82 -0
  6. lp2graph/core/__init__.py +35 -0
  7. lp2graph/core/graph.py +183 -0
  8. lp2graph/core/loader.py +63 -0
  9. lp2graph/core/model.py +437 -0
  10. lp2graph/core/validate.py +237 -0
  11. lp2graph/export/__init__.py +13 -0
  12. lp2graph/export/dgl.py +51 -0
  13. lp2graph/export/latex.py +126 -0
  14. lp2graph/export/networkx_adapter.py +50 -0
  15. lp2graph/export/pyg.py +79 -0
  16. lp2graph/export/pyomo_stub.py +81 -0
  17. lp2graph/metrics/__init__.py +58 -0
  18. lp2graph/metrics/classification.py +113 -0
  19. lp2graph/metrics/flags.py +122 -0
  20. lp2graph/metrics/result.py +26 -0
  21. lp2graph/metrics/structural.py +236 -0
  22. lp2graph/mining/__init__.py +47 -0
  23. lp2graph/mining/cluster/__init__.py +65 -0
  24. lp2graph/mining/cluster/agglomerative.py +82 -0
  25. lp2graph/mining/cluster/distance.py +65 -0
  26. lp2graph/mining/cluster/operator.py +218 -0
  27. lp2graph/mining/cluster/silhouette.py +88 -0
  28. lp2graph/mining/cluster/stability.py +178 -0
  29. lp2graph/mining/cluster/taxonomy.py +268 -0
  30. lp2graph/mining/corpusmgr/__init__.py +70 -0
  31. lp2graph/mining/corpusmgr/dedup.py +183 -0
  32. lp2graph/mining/corpusmgr/manager.py +79 -0
  33. lp2graph/mining/corpusmgr/manifest.py +82 -0
  34. lp2graph/mining/corpusmgr/record.py +101 -0
  35. lp2graph/mining/corpusmgr/select.py +128 -0
  36. lp2graph/mining/homologize/__init__.py +82 -0
  37. lp2graph/mining/homologize/concept.py +134 -0
  38. lp2graph/mining/homologize/entity.py +217 -0
  39. lp2graph/mining/homologize/lemmatize.py +80 -0
  40. lp2graph/mining/homologize/signature.py +166 -0
  41. lp2graph/mining/homologize/thesaurus.py +70 -0
  42. lp2graph/mining/homologize/tokenize.py +255 -0
  43. lp2graph/mining/homologize/vectorize.py +141 -0
  44. lp2graph/mining/ingest/__init__.py +59 -0
  45. lp2graph/mining/ingest/code_importers.py +104 -0
  46. lp2graph/mining/ingest/dispatch.py +148 -0
  47. lp2graph/mining/ingest/latex_normalizer.py +243 -0
  48. lp2graph/mining/ingest/pyomo_importer.py +297 -0
  49. lp2graph/mining/ingest/result.py +124 -0
  50. lp2graph/mining/isomorphism/__init__.py +26 -0
  51. lp2graph/mining/isomorphism/report.py +178 -0
  52. lp2graph/mining/label/__init__.py +70 -0
  53. lp2graph/mining/label/classifier.py +161 -0
  54. lp2graph/mining/label/features.py +35 -0
  55. lp2graph/mining/label/guardrails.py +176 -0
  56. lp2graph/mining/label/loop.py +314 -0
  57. lp2graph/mining/label/rules.py +92 -0
  58. lp2graph/mining/label/store.py +164 -0
  59. lp2graph/mining/label/vocab.py +64 -0
  60. lp2graph/mining/provenance.py +90 -0
  61. lp2graph/mining/versions.py +51 -0
  62. lp2graph/nl/__init__.py +15 -0
  63. lp2graph/nl/describe.py +301 -0
  64. lp2graph/render/__init__.py +11 -0
  65. lp2graph/render/palette.py +80 -0
  66. lp2graph/render/svg.py +220 -0
  67. lp2graph/solve/__init__.py +50 -0
  68. lp2graph/solve/grounder.py +405 -0
  69. lp2graph/solve/instance.py +76 -0
  70. lp2graph/transform/__init__.py +30 -0
  71. lp2graph/transform/bigm.py +173 -0
  72. lp2graph/views/__init__.py +17 -0
  73. lp2graph/views/ground.py +477 -0
  74. lp2graph/views/hybrid.py +202 -0
  75. lp2graph/views/schema.py +208 -0
  76. lp2graph-0.3.0.dist-info/METADATA +206 -0
  77. lp2graph-0.3.0.dist-info/RECORD +80 -0
  78. lp2graph-0.3.0.dist-info/WHEEL +4 -0
  79. lp2graph-0.3.0.dist-info/entry_points.txt +2 -0
  80. lp2graph-0.3.0.dist-info/licenses/LICENSE +205 -0
@@ -0,0 +1,26 @@
1
+ """Result type returned by every metric."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class MetricResult:
11
+ """The output of a single metric.
12
+
13
+ ``name`` is a stable machine identifier; ``value`` is whatever the
14
+ metric produces (int, float, bool, dict). ``explanation`` is a short
15
+ one-line human-readable description. ``data`` carries optional
16
+ auxiliary fields (e.g. the diameter path along with the diameter
17
+ length).
18
+ """
19
+
20
+ name: str
21
+ value: Any
22
+ explanation: str = ""
23
+ data: dict[str, Any] = field(default_factory=dict)
24
+
25
+
26
+ __all__ = ["MetricResult"]
@@ -0,0 +1,236 @@
1
+ """Structural metrics over typed graphs.
2
+
3
+ Adapted from joernmht/raiLPminerExperimentation
4
+ (railpminer/analysis/metrics.py and railpminer/visualization/diameter.py),
5
+ MIT License. The algorithms are the same; the surface targets the new
6
+ internal :class:`~lp2graph.core.graph.Graph` rather than NetworkX
7
+ directly.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import defaultdict, deque
13
+
14
+ from lp2graph.core.graph import Graph
15
+ from lp2graph.core.model import Formulation
16
+ from lp2graph.metrics.result import MetricResult
17
+
18
+
19
+ def node_counts_by_class(g: Graph) -> MetricResult:
20
+ """Count nodes by their high-level class.
21
+
22
+ Returns a dict like ``{"variable": 4, "constraint": 6, ...}``.
23
+ """
24
+ counts: dict[str, int] = defaultdict(int)
25
+ for n in g.nodes:
26
+ counts[n.cls] += 1
27
+ return MetricResult(
28
+ name="node_counts_by_class",
29
+ value=dict(counts),
30
+ explanation="Count of nodes grouped by class.",
31
+ )
32
+
33
+
34
+ def edge_density(g: Graph) -> MetricResult:
35
+ """Edge density: |E| / max(1, |V| * (|V| - 1))."""
36
+ n = len(g)
37
+ e = len(g.edges)
38
+ density = 0.0 if n <= 1 else e / (n * (n - 1))
39
+ return MetricResult(
40
+ name="edge_density",
41
+ value=density,
42
+ explanation="Directed-graph edge density.",
43
+ data={"nodes": n, "edges": e},
44
+ )
45
+
46
+
47
+ def constraint_variable_ratio(g: Graph) -> MetricResult:
48
+ """Number of constraint nodes divided by number of variable nodes.
49
+
50
+ Operates on the schema or hybrid view. For ground views, instance
51
+ nodes are counted instead.
52
+ """
53
+ constraints = len([n for n in g.nodes if n.cls in ("constraint", "instance_constraint")])
54
+ variables = len([n for n in g.nodes if n.cls in ("variable", "instance_variable")])
55
+ if variables == 0:
56
+ ratio = float("inf") if constraints > 0 else 0.0
57
+ else:
58
+ ratio = constraints / variables
59
+ return MetricResult(
60
+ name="constraint_variable_ratio",
61
+ value=ratio,
62
+ explanation="Constraint-to-variable count ratio.",
63
+ data={"constraints": constraints, "variables": variables},
64
+ )
65
+
66
+
67
+ def minimal_size(g: Graph) -> MetricResult:
68
+ """The product |constraints| · |variables| (the dense incidence-matrix size).
69
+
70
+ A weak proxy for problem size; useful for relative comparison across
71
+ formulations.
72
+ """
73
+ constraints = len([n for n in g.nodes if n.cls in ("constraint", "instance_constraint")])
74
+ variables = len([n for n in g.nodes if n.cls in ("variable", "instance_variable")])
75
+ cv = max(constraints, 1) * max(variables, 1)
76
+ return MetricResult(
77
+ name="minimal_size",
78
+ value=cv,
79
+ explanation="Product of constraint count and variable count.",
80
+ data={"constraints": constraints, "variables": variables},
81
+ )
82
+
83
+
84
+ def model_coherence(g: Graph) -> MetricResult:
85
+ """1 if the underlying undirected graph is connected, else 0.
86
+
87
+ A coherent model has every variable reached by some path through
88
+ constraints. Disconnected components usually signal a missing
89
+ coupling.
90
+ """
91
+ if len(g) <= 1:
92
+ return MetricResult(name="model_coherence", value=1, explanation="Trivially connected.")
93
+ seen: set[str] = set()
94
+ start = g.nodes[0].id
95
+ queue = deque([start])
96
+ seen.add(start)
97
+ adj: dict[str, set[str]] = defaultdict(set)
98
+ for e in g.edges:
99
+ adj[e.src].add(e.dst)
100
+ adj[e.dst].add(e.src)
101
+ while queue:
102
+ n = queue.popleft()
103
+ for m in adj[n]:
104
+ if m not in seen:
105
+ seen.add(m)
106
+ queue.append(m)
107
+ coherent = 1 if len(seen) == len(g) else 0
108
+ return MetricResult(
109
+ name="model_coherence",
110
+ value=coherent,
111
+ explanation="1 if undirected graph is connected, else 0.",
112
+ data={"reached": len(seen), "total": len(g)},
113
+ )
114
+
115
+
116
+ def model_completeness(f: Formulation) -> MetricResult:
117
+ """1 if the formulation is a recoverable model, else 0.
118
+
119
+ A *complete* model declares an objective together with at least one
120
+ variable and at least one constraint -- the minimal evidence that a
121
+ whole model, rather than a fragment, was extracted. This is the
122
+ companion well-formedness indicator to :func:`model_coherence`; unlike
123
+ coherence (a graph property) it reads the canonical
124
+ :class:`~lp2graph.core.model.Formulation` directly.
125
+ """
126
+ complete = (
127
+ 1 if f.objective is not None and len(f.variables) >= 1 and len(f.constraints) >= 1 else 0
128
+ )
129
+ return MetricResult(
130
+ name="model_completeness",
131
+ value=complete,
132
+ explanation="1 if the model declares an objective, >=1 variable and >=1 constraint, else 0.",
133
+ data={
134
+ "has_objective": f.objective is not None,
135
+ "n_variables": len(f.variables),
136
+ "n_constraints": len(f.constraints),
137
+ },
138
+ )
139
+
140
+
141
+ def graph_diameter(g: Graph) -> MetricResult:
142
+ """Diameter of the largest connected component (undirected, unweighted).
143
+
144
+ Returns the diameter and one diameter-realizing path. If the graph
145
+ has fewer than two nodes, returns 0.
146
+ """
147
+ if len(g) < 2:
148
+ return MetricResult(name="graph_diameter", value=0, explanation="Too small.")
149
+ adj: dict[str, set[str]] = defaultdict(set)
150
+ for e in g.edges:
151
+ adj[e.src].add(e.dst)
152
+ adj[e.dst].add(e.src)
153
+
154
+ # Find the largest connected component.
155
+ seen_global: set[str] = set()
156
+ components: list[set[str]] = []
157
+ for n in g.nodes:
158
+ if n.id in seen_global:
159
+ continue
160
+ comp: set[str] = set()
161
+ queue = deque([n.id])
162
+ seen_global.add(n.id)
163
+ comp.add(n.id)
164
+ while queue:
165
+ x = queue.popleft()
166
+ for y in adj[x]:
167
+ if y not in seen_global:
168
+ seen_global.add(y)
169
+ comp.add(y)
170
+ queue.append(y)
171
+ components.append(comp)
172
+ largest = max(components, key=len)
173
+ if len(largest) < 2:
174
+ return MetricResult(name="graph_diameter", value=0, explanation="LCC has < 2 nodes.")
175
+
176
+ # Two-pass BFS approximation gives the exact diameter for trees and
177
+ # an exact answer for arbitrary graphs only when followed by full
178
+ # BFS from every node. Here we do the full BFS for correctness; the
179
+ # graphs we care about are small (template-scale).
180
+ best = 0
181
+ best_path: list[str] = []
182
+ nodes = sorted(largest) # deterministic
183
+ for src in nodes:
184
+ dist = {src: 0}
185
+ prev: dict[str, str | None] = {src: None}
186
+ queue = deque([src])
187
+ while queue:
188
+ x = queue.popleft()
189
+ for y in adj[x]:
190
+ if y not in dist:
191
+ dist[y] = dist[x] + 1
192
+ prev[y] = x
193
+ queue.append(y)
194
+ target = max(dist, key=lambda k: (dist[k], k))
195
+ if dist[target] > best:
196
+ best = dist[target]
197
+ # Reconstruct path.
198
+ path: list[str] = []
199
+ cur: str | None = target
200
+ while cur is not None:
201
+ path.append(cur)
202
+ cur = prev[cur]
203
+ best_path = list(reversed(path))
204
+ return MetricResult(
205
+ name="graph_diameter",
206
+ value=best,
207
+ explanation="Longest shortest path in the largest connected component.",
208
+ data={"path": best_path},
209
+ )
210
+
211
+
212
+ def structural_summary(g: Graph) -> dict[str, MetricResult]:
213
+ """Convenience: compute every structural metric at once."""
214
+ return {
215
+ m.name: m
216
+ for m in (
217
+ node_counts_by_class(g),
218
+ edge_density(g),
219
+ constraint_variable_ratio(g),
220
+ minimal_size(g),
221
+ model_coherence(g),
222
+ graph_diameter(g),
223
+ )
224
+ }
225
+
226
+
227
+ __all__ = [
228
+ "constraint_variable_ratio",
229
+ "edge_density",
230
+ "graph_diameter",
231
+ "minimal_size",
232
+ "model_coherence",
233
+ "model_completeness",
234
+ "node_counts_by_class",
235
+ "structural_summary",
236
+ ]
@@ -0,0 +1,47 @@
1
+ """LP mining extensions for lp2graph.
2
+
3
+ This subpackage implements the modules the *LP Mining with LP2Graph*
4
+ method needs on top of the deterministic core library:
5
+
6
+ - :mod:`lp2graph.mining.ingest` (M1) — heterogeneous ingestion front-end
7
+ turning solver code and non-canonical LaTeX into validated
8
+ ``Formulation`` objects, with source-span provenance.
9
+ - :mod:`lp2graph.mining.homologize` (M2) — lexical homologizer and
10
+ TF-IDF concept vectorizer.
11
+ - :mod:`lp2graph.mining.cluster` (M3) — the cluster-and-name operator
12
+ ``CN`` and the bottom-up multi-level taxonomy induction.
13
+ - :mod:`lp2graph.mining.label` (M4) — the two-stage labeling service with
14
+ a versioned, self-growing closed-loop store.
15
+ - :mod:`lp2graph.mining.corpusmgr` (M5) — corpus & provenance manager.
16
+ - :mod:`lp2graph.mining.isomorphism` (M6) — intra-cluster schema-graph
17
+ isomorphism reporting.
18
+
19
+ Every module is deterministic: frozen resources are versioned in
20
+ :mod:`lp2graph.mining.versions` and stamped into emitted records.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from lp2graph.mining.provenance import ProvenanceMap, Rewrite, SourceSpan
26
+ from lp2graph.mining.versions import (
27
+ CLUSTERING_VERSION,
28
+ LABEL_LEXICON_VERSION,
29
+ LEXICON_VERSION,
30
+ REWRITE_RULES_VERSION,
31
+ THESAURUS_VERSION,
32
+ VOCABULARY_VERSION,
33
+ WORDNET_VERSION,
34
+ )
35
+
36
+ __all__ = [
37
+ "CLUSTERING_VERSION",
38
+ "LABEL_LEXICON_VERSION",
39
+ "LEXICON_VERSION",
40
+ "REWRITE_RULES_VERSION",
41
+ "THESAURUS_VERSION",
42
+ "VOCABULARY_VERSION",
43
+ "WORDNET_VERSION",
44
+ "ProvenanceMap",
45
+ "Rewrite",
46
+ "SourceSpan",
47
+ ]
@@ -0,0 +1,65 @@
1
+ """M3 — cluster-and-name operator and multi-level taxonomy induction.
2
+
3
+ Public surface:
4
+
5
+ - :func:`CN` / :class:`ClusterConfig` / :class:`NamedClustering` — the core
6
+ cluster-and-name operator.
7
+ - :func:`induce` / :class:`Taxonomy` / :class:`LevelResult` — the bottom-up
8
+ Level V → C → M passes plus the text-only domain / solution-approach
9
+ clusterings.
10
+ - :func:`stability_report` / :class:`StabilityReport` — silhouette, bootstrap
11
+ ARI, and sensitivity diagnostics.
12
+
13
+ Everything is deterministic given the versioned :class:`ClusterConfig`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from lp2graph.mining.cluster.distance import (
19
+ cosine_distance,
20
+ cosine_similarity,
21
+ distance_matrix,
22
+ )
23
+ from lp2graph.mining.cluster.operator import (
24
+ CN,
25
+ UNASSIGNED,
26
+ Algorithm,
27
+ ClusterConfig,
28
+ NamedClustering,
29
+ )
30
+ from lp2graph.mining.cluster.silhouette import silhouette_samples, silhouette_score
31
+ from lp2graph.mining.cluster.stability import (
32
+ StabilityReport,
33
+ adjusted_rand_index,
34
+ bootstrap_ari,
35
+ sensitivity,
36
+ stability_report,
37
+ )
38
+ from lp2graph.mining.cluster.taxonomy import (
39
+ LevelResult,
40
+ Taxonomy,
41
+ induce,
42
+ model_feature_document,
43
+ )
44
+
45
+ __all__ = [
46
+ "CN",
47
+ "UNASSIGNED",
48
+ "Algorithm",
49
+ "ClusterConfig",
50
+ "LevelResult",
51
+ "NamedClustering",
52
+ "StabilityReport",
53
+ "Taxonomy",
54
+ "adjusted_rand_index",
55
+ "bootstrap_ari",
56
+ "cosine_distance",
57
+ "cosine_similarity",
58
+ "distance_matrix",
59
+ "induce",
60
+ "model_feature_document",
61
+ "sensitivity",
62
+ "silhouette_samples",
63
+ "silhouette_score",
64
+ "stability_report",
65
+ ]
@@ -0,0 +1,82 @@
1
+ """Deterministic average-linkage agglomerative clustering (M3).
2
+
3
+ A small, dependency-free clustering core used as the default backend for the
4
+ ``CN`` operator. Two stopping modes:
5
+
6
+ - **threshold** — keep merging the two closest clusters until the closest
7
+ average-linkage distance exceeds ``distance_threshold``; this discovers the
8
+ number of clusters (and naturally leaves singletons), the HDBSCAN-style
9
+ behavior the method asks for without the dependency.
10
+ - **fixed K** — merge until exactly ``k`` clusters remain.
11
+
12
+ Determinism is the whole point: ties (equal merge distances) are broken by
13
+ the lexicographically smallest cluster-index pair, so the dendrogram — and
14
+ every partition cut from it — is reproducible across runs and machines.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Sequence
20
+
21
+
22
+ def _labels_from_groups(groups: list[list[int]], n: int) -> tuple[int, ...]:
23
+ """Assign each point a cluster id, ids ordered by smallest member index."""
24
+ ordered = sorted(groups, key=min)
25
+ labels = [-1] * n
26
+ for cid, group in enumerate(ordered):
27
+ for idx in group:
28
+ labels[idx] = cid
29
+ return tuple(labels)
30
+
31
+
32
+ def _average_linkage(dist: Sequence[Sequence[float]], a: Sequence[int], b: Sequence[int]) -> float:
33
+ total = 0.0
34
+ for i in a:
35
+ for j in b:
36
+ total += dist[i][j]
37
+ return total / (len(a) * len(b))
38
+
39
+
40
+ def agglomerative(
41
+ dist: Sequence[Sequence[float]],
42
+ *,
43
+ distance_threshold: float | None = 0.7,
44
+ k: int | None = None,
45
+ ) -> tuple[int, ...]:
46
+ """Cluster points described by precomputed distance matrix ``dist``.
47
+
48
+ Provide exactly one stopping rule: ``k`` (fixed cluster count) or
49
+ ``distance_threshold`` (merge while the closest pair is within it).
50
+ Returns a tuple of cluster ids aligned to the input order.
51
+ """
52
+ if (k is None) == (distance_threshold is None):
53
+ raise ValueError("provide exactly one of 'k' or 'distance_threshold'")
54
+ n = len(dist)
55
+ if n == 0:
56
+ return ()
57
+ if n == 1:
58
+ return (0,)
59
+
60
+ groups: list[list[int]] = [[i] for i in range(n)]
61
+
62
+ while len(groups) > 1:
63
+ if k is not None and len(groups) <= k:
64
+ break
65
+ # Find the closest pair of groups; deterministic tie-break by (i, j).
66
+ best: tuple[float, int, int] | None = None
67
+ for i in range(len(groups)):
68
+ for j in range(i + 1, len(groups)):
69
+ d = _average_linkage(dist, groups[i], groups[j])
70
+ if best is None or d < best[0]:
71
+ best = (d, i, j)
72
+ assert best is not None
73
+ merge_d, gi, gj = best
74
+ if k is None and distance_threshold is not None and merge_d > distance_threshold:
75
+ break
76
+ groups[gi] = groups[gi] + groups[gj]
77
+ del groups[gj]
78
+
79
+ return _labels_from_groups(groups, n)
80
+
81
+
82
+ __all__ = ["agglomerative"]
@@ -0,0 +1,65 @@
1
+ """Cosine distance over TF-IDF concept vectors (M3).
2
+
3
+ The clustering operator works in cosine-distance space. Because the M2
4
+ vectorizer already L2-normalizes its output, cosine similarity is just the
5
+ dot product and cosine distance is ``1 - dot``. Zero vectors (entities whose
6
+ text yielded no in-vocabulary concept) are at distance 1 from everything and
7
+ are routed to the explicit *unassigned* part upstream.
8
+
9
+ Pure Python, no numpy dependency, fully order-deterministic.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+
16
+ Vector = Sequence[float]
17
+
18
+
19
+ def dot(a: Vector, b: Vector) -> float:
20
+ """Dot product of two equal-length vectors."""
21
+ if len(a) != len(b):
22
+ raise ValueError(f"vector length mismatch: {len(a)} vs {len(b)}")
23
+ return sum(x * y for x, y in zip(a, b, strict=True))
24
+
25
+
26
+ def cosine_similarity(a: Vector, b: Vector) -> float:
27
+ """Cosine similarity assuming ``a``/``b`` are L2-normalized (or zero)."""
28
+ return dot(a, b)
29
+
30
+
31
+ def cosine_distance(a: Vector, b: Vector) -> float:
32
+ """Cosine distance ``1 - cosine_similarity`` clamped to ``[0, 2]``."""
33
+ d = 1.0 - cosine_similarity(a, b)
34
+ if d < 0.0:
35
+ return 0.0
36
+ if d > 2.0:
37
+ return 2.0
38
+ return d
39
+
40
+
41
+ def is_zero(v: Vector) -> bool:
42
+ """True if every coordinate is exactly zero (no concept signal)."""
43
+ return all(x == 0.0 for x in v)
44
+
45
+
46
+ def distance_matrix(vectors: Sequence[Vector]) -> list[list[float]]:
47
+ """Symmetric pairwise cosine-distance matrix with a zero diagonal."""
48
+ n = len(vectors)
49
+ mat = [[0.0] * n for _ in range(n)]
50
+ for i in range(n):
51
+ for j in range(i + 1, n):
52
+ d = cosine_distance(vectors[i], vectors[j])
53
+ mat[i][j] = d
54
+ mat[j][i] = d
55
+ return mat
56
+
57
+
58
+ __all__ = [
59
+ "Vector",
60
+ "cosine_distance",
61
+ "cosine_similarity",
62
+ "distance_matrix",
63
+ "dot",
64
+ "is_zero",
65
+ ]