knowledgecomplex 0.1.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.
- knowledgecomplex/__init__.py +91 -0
- knowledgecomplex/analysis.py +1177 -0
- knowledgecomplex/audit.py +173 -0
- knowledgecomplex/clique.py +399 -0
- knowledgecomplex/codecs/__init__.py +9 -0
- knowledgecomplex/codecs/markdown.py +220 -0
- knowledgecomplex/diff.py +429 -0
- knowledgecomplex/exceptions.py +38 -0
- knowledgecomplex/filtration.py +226 -0
- knowledgecomplex/graph.py +1107 -0
- knowledgecomplex/io.py +167 -0
- knowledgecomplex/ontologies/README.md +182 -0
- knowledgecomplex/ontologies/__init__.py +24 -0
- knowledgecomplex/ontologies/brand.py +67 -0
- knowledgecomplex/ontologies/operations.py +44 -0
- knowledgecomplex/ontologies/research.py +73 -0
- knowledgecomplex/queries/boundary.sparql +17 -0
- knowledgecomplex/queries/closure.sparql +18 -0
- knowledgecomplex/queries/coboundary.sparql +16 -0
- knowledgecomplex/queries/degree.sparql +12 -0
- knowledgecomplex/queries/skeleton.sparql +17 -0
- knowledgecomplex/queries/star.sparql +19 -0
- knowledgecomplex/queries/vertices.sparql +13 -0
- knowledgecomplex/resources/kc_core.ttl +140 -0
- knowledgecomplex/resources/kc_core_shapes.ttl +191 -0
- knowledgecomplex/schema.py +1142 -0
- knowledgecomplex/viz.py +786 -0
- knowledgecomplex-0.1.0.dist-info/METADATA +442 -0
- knowledgecomplex-0.1.0.dist-info/RECORD +31 -0
- knowledgecomplex-0.1.0.dist-info/WHEEL +4 -0
- knowledgecomplex-0.1.0.dist-info/licenses/LICENSE +201 -0
knowledgecomplex/viz.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"""knowledgecomplex.viz — NetworkX export and visualization helpers.
|
|
2
|
+
|
|
3
|
+
Two complementary views of a knowledge complex are provided:
|
|
4
|
+
|
|
5
|
+
**Hasse diagram** (``plot_hasse``, ``plot_hasse_star``, ``plot_hasse_skeleton``)
|
|
6
|
+
Every element (vertex, edge, face) becomes a graph node. Directed edges
|
|
7
|
+
represent the boundary operator, pointing from each element to its boundary
|
|
8
|
+
elements (higher dimension → lower dimension). Faces have out-degree 3
|
|
9
|
+
and in-degree 0; edges have out-degree 2; vertices have out-degree 0.
|
|
10
|
+
Nodes are colored by type and sized by dimension.
|
|
11
|
+
|
|
12
|
+
**Geometric realization** (``plot_geometric``, ``plot_geometric_interactive``)
|
|
13
|
+
Only KC vertices become points in 3D space. KC edges become line segments
|
|
14
|
+
connecting their two boundary vertices. KC faces become filled,
|
|
15
|
+
semi-transparent triangular patches spanning their three boundary vertices.
|
|
16
|
+
This is the classical geometric realization of the abstract simplicial
|
|
17
|
+
complex — the view a topologist would draw.
|
|
18
|
+
|
|
19
|
+
``to_networkx`` exports a ``DiGraph`` that backs the Hasse plots.
|
|
20
|
+
``verify_networkx`` validates that a DiGraph satisfies simplicial complex
|
|
21
|
+
cardinality and closure invariants at runtime.
|
|
22
|
+
|
|
23
|
+
Requires optional dependencies::
|
|
24
|
+
|
|
25
|
+
pip install knowledgecomplex[viz] # matplotlib + networkx
|
|
26
|
+
pip install knowledgecomplex[viz-interactive] # + plotly for interactive 3D
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import warnings
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from knowledgecomplex.graph import KnowledgeComplex
|
|
35
|
+
|
|
36
|
+
_DIM_BY_KIND = {"vertex": 0, "edge": 1, "face": 2}
|
|
37
|
+
_SIZE_BY_DIM = {0: 400, 1: 200, 2: 100}
|
|
38
|
+
|
|
39
|
+
_INSTALL_HINT = (
|
|
40
|
+
"networkx and matplotlib are required for visualization.\n"
|
|
41
|
+
"Install them with: pip install knowledgecomplex[viz]"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_PLOTLY_HINT = (
|
|
45
|
+
"plotly is required for interactive 3D visualization.\n"
|
|
46
|
+
"Install it with: pip install knowledgecomplex[viz-interactive]"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _require_nx():
|
|
51
|
+
try:
|
|
52
|
+
import networkx as nx
|
|
53
|
+
return nx
|
|
54
|
+
except ImportError:
|
|
55
|
+
raise ImportError(_INSTALL_HINT) from None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _require_mpl():
|
|
59
|
+
try:
|
|
60
|
+
import matplotlib
|
|
61
|
+
import matplotlib.pyplot as plt
|
|
62
|
+
return matplotlib, plt
|
|
63
|
+
except ImportError:
|
|
64
|
+
raise ImportError(_INSTALL_HINT) from None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _require_plotly():
|
|
68
|
+
try:
|
|
69
|
+
import plotly.graph_objects as go
|
|
70
|
+
return go
|
|
71
|
+
except ImportError:
|
|
72
|
+
raise ImportError(_PLOTLY_HINT) from None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── NetworkX export ─────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_networkx(kc: "KnowledgeComplex") -> Any:
|
|
79
|
+
"""Convert a KnowledgeComplex to a directed networkx DiGraph.
|
|
80
|
+
|
|
81
|
+
Every element (vertex, edge, face) becomes a node. Directed edges
|
|
82
|
+
represent the boundary operator ``kc:boundedBy``, pointing **from each
|
|
83
|
+
element to its boundary elements** (higher dimension → lower dimension).
|
|
84
|
+
|
|
85
|
+
In the resulting DiGraph:
|
|
86
|
+
|
|
87
|
+
- **Face** nodes have out-degree 3 (→ 3 boundary edges) and in-degree 0.
|
|
88
|
+
- **Edge** nodes have out-degree 2 (→ 2 boundary vertices).
|
|
89
|
+
- **Vertex** nodes have out-degree 0 (empty boundary).
|
|
90
|
+
|
|
91
|
+
Each node carries attributes:
|
|
92
|
+
|
|
93
|
+
- ``type``: element type name (e.g. ``"Node"``, ``"Link"``)
|
|
94
|
+
- ``kind``: ``"vertex"``, ``"edge"``, or ``"face"``
|
|
95
|
+
- ``dim``: 0, 1, or 2
|
|
96
|
+
- ``uri``: file URI if present, else ``None``
|
|
97
|
+
- All model-namespace attributes from the element
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
kc : KnowledgeComplex
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
networkx.DiGraph
|
|
106
|
+
"""
|
|
107
|
+
nx = _require_nx()
|
|
108
|
+
G = nx.DiGraph(name=kc._schema._namespace)
|
|
109
|
+
|
|
110
|
+
for elem_id in kc.element_ids():
|
|
111
|
+
elem = kc.element(elem_id)
|
|
112
|
+
type_name = elem.type
|
|
113
|
+
kind = kc._schema._types.get(type_name, {}).get("kind", "vertex")
|
|
114
|
+
attrs = {
|
|
115
|
+
"type": type_name,
|
|
116
|
+
"kind": kind,
|
|
117
|
+
"dim": _DIM_BY_KIND.get(kind, 0),
|
|
118
|
+
"uri": elem.uri,
|
|
119
|
+
**elem.attrs,
|
|
120
|
+
}
|
|
121
|
+
G.add_node(elem_id, **attrs)
|
|
122
|
+
|
|
123
|
+
# Directed boundary edges: element → boundary element (high dim → low dim)
|
|
124
|
+
for elem_id in kc.element_ids():
|
|
125
|
+
for boundary_id in kc.boundary(elem_id):
|
|
126
|
+
G.add_edge(elem_id, boundary_id)
|
|
127
|
+
|
|
128
|
+
return G
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── Verification ────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def verify_networkx(G: Any) -> bool:
|
|
135
|
+
"""Validate that a DiGraph satisfies simplicial complex invariants.
|
|
136
|
+
|
|
137
|
+
Checks cardinality constraints and boundary closure:
|
|
138
|
+
|
|
139
|
+
- Every node has ``kind`` and ``dim`` attributes.
|
|
140
|
+
- **Vertices** (dim=0): out-degree = 0.
|
|
141
|
+
- **Edges** (dim=1): out-degree = 2, both targets are vertices (dim=0).
|
|
142
|
+
- **Faces** (dim=2): out-degree = 3, all targets are edges (dim=1).
|
|
143
|
+
- **Closed-triangle**: for each face, the 3 boundary edges share exactly
|
|
144
|
+
3 distinct vertices (forming a closed triangle, not an open fan).
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
G : networkx.DiGraph
|
|
149
|
+
A DiGraph produced by :func:`to_networkx`.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
bool
|
|
154
|
+
``True`` if all invariants hold.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
ValueError
|
|
159
|
+
On the first invariant violation, with a descriptive message.
|
|
160
|
+
TypeError
|
|
161
|
+
If *G* is not a ``DiGraph``.
|
|
162
|
+
"""
|
|
163
|
+
nx = _require_nx()
|
|
164
|
+
if not isinstance(G, nx.DiGraph):
|
|
165
|
+
raise TypeError(f"Expected nx.DiGraph, got {type(G).__name__}")
|
|
166
|
+
|
|
167
|
+
for node in G.nodes:
|
|
168
|
+
data = G.nodes[node]
|
|
169
|
+
if "kind" not in data or "dim" not in data:
|
|
170
|
+
raise ValueError(f"Node '{node}' missing 'kind' or 'dim' attribute")
|
|
171
|
+
|
|
172
|
+
dim = data["dim"]
|
|
173
|
+
out_deg = G.out_degree(node)
|
|
174
|
+
successors = list(G.successors(node))
|
|
175
|
+
|
|
176
|
+
if dim == 0: # vertex
|
|
177
|
+
if out_deg != 0:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Vertex '{node}' has out-degree {out_deg}, expected 0"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif dim == 1: # edge
|
|
183
|
+
if out_deg != 2:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Edge '{node}' has out-degree {out_deg}, expected 2"
|
|
186
|
+
)
|
|
187
|
+
for s in successors:
|
|
188
|
+
if G.nodes[s].get("dim") != 0:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Edge '{node}' boundary target '{s}' is not a vertex "
|
|
191
|
+
f"(dim={G.nodes[s].get('dim')})"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
elif dim == 2: # face
|
|
195
|
+
if out_deg != 3:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Face '{node}' has out-degree {out_deg}, expected 3"
|
|
198
|
+
)
|
|
199
|
+
for s in successors:
|
|
200
|
+
if G.nodes[s].get("dim") != 1:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"Face '{node}' boundary target '{s}' is not an edge "
|
|
203
|
+
f"(dim={G.nodes[s].get('dim')})"
|
|
204
|
+
)
|
|
205
|
+
# Closed-triangle: 3 edges must share exactly 3 distinct vertices
|
|
206
|
+
face_vertices = set()
|
|
207
|
+
for edge_node in successors:
|
|
208
|
+
for v in G.successors(edge_node):
|
|
209
|
+
face_vertices.add(v)
|
|
210
|
+
if len(face_vertices) != 3:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Face '{node}' boundary edges span {len(face_vertices)} "
|
|
213
|
+
f"distinct vertices, expected 3 (closed triangle)"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── Color mapping ──────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def type_color_map(kc: "KnowledgeComplex") -> dict[str, str]:
|
|
223
|
+
"""Build a type-name to hex-color mapping from the schema's type registry.
|
|
224
|
+
|
|
225
|
+
Uses matplotlib's ``tab10`` colormap (or ``tab20`` if > 10 types)
|
|
226
|
+
for distinct, visually separable colors.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
kc : KnowledgeComplex
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
dict[str, str]
|
|
235
|
+
Mapping from type name to hex color string.
|
|
236
|
+
"""
|
|
237
|
+
_, plt = _require_mpl()
|
|
238
|
+
import matplotlib.colors as mcolors
|
|
239
|
+
|
|
240
|
+
type_names = sorted(kc._schema._types.keys())
|
|
241
|
+
cmap_name = "tab10" if len(type_names) <= 10 else "tab20"
|
|
242
|
+
cmap = plt.get_cmap(cmap_name)
|
|
243
|
+
|
|
244
|
+
colors = {}
|
|
245
|
+
for i, name in enumerate(type_names):
|
|
246
|
+
colors[name] = mcolors.to_hex(cmap(i % cmap.N))
|
|
247
|
+
return colors
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ── Hasse diagram helpers ──────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _prepare_ax(ax, figsize):
|
|
254
|
+
"""Create or reuse a matplotlib Axes."""
|
|
255
|
+
_, plt = _require_mpl()
|
|
256
|
+
if ax is None:
|
|
257
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize)
|
|
258
|
+
else:
|
|
259
|
+
fig = ax.get_figure()
|
|
260
|
+
return fig, ax
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _layout(G):
|
|
264
|
+
"""Choose a 2D layout for the graph (converts DiGraph to undirected)."""
|
|
265
|
+
nx = _require_nx()
|
|
266
|
+
if len(G) == 0:
|
|
267
|
+
return {}
|
|
268
|
+
undirected = G.to_undirected() if G.is_directed() else G
|
|
269
|
+
try:
|
|
270
|
+
return nx.kamada_kawai_layout(undirected)
|
|
271
|
+
except Exception:
|
|
272
|
+
return nx.spring_layout(undirected, seed=42)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ── Hasse diagram plots ───────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def plot_hasse(
|
|
279
|
+
kc: "KnowledgeComplex",
|
|
280
|
+
*,
|
|
281
|
+
ax: Any = None,
|
|
282
|
+
figsize: tuple[float, float] = (10, 8),
|
|
283
|
+
with_labels: bool = True,
|
|
284
|
+
node_size_by_dim: bool = True,
|
|
285
|
+
) -> tuple[Any, Any]:
|
|
286
|
+
"""Plot the Hasse diagram of the complex with type-based color coding.
|
|
287
|
+
|
|
288
|
+
Every element (vertex, edge, face) is drawn as a node. Directed arrows
|
|
289
|
+
represent the boundary operator, pointing from each element to its
|
|
290
|
+
boundary elements (higher dimension → lower dimension). Nodes are colored
|
|
291
|
+
by type and sized by dimension (vertices largest, faces smallest).
|
|
292
|
+
|
|
293
|
+
This is **not** a geometric picture of the complex — it is the partially
|
|
294
|
+
ordered set of simplices. For a geometric view where vertices are points,
|
|
295
|
+
edges are line segments, and faces are filled triangles, see
|
|
296
|
+
:func:`plot_geometric`.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
kc : KnowledgeComplex
|
|
301
|
+
ax : matplotlib Axes, optional
|
|
302
|
+
Axes to draw on. Created if not provided.
|
|
303
|
+
figsize : tuple
|
|
304
|
+
Figure size if creating a new figure.
|
|
305
|
+
with_labels : bool
|
|
306
|
+
Show element ID labels on nodes.
|
|
307
|
+
node_size_by_dim : bool
|
|
308
|
+
Scale node size by dimension (vertex=large, face=small).
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
(fig, ax)
|
|
313
|
+
The matplotlib Figure and Axes.
|
|
314
|
+
"""
|
|
315
|
+
nx = _require_nx()
|
|
316
|
+
_, plt = _require_mpl()
|
|
317
|
+
|
|
318
|
+
G = to_networkx(kc)
|
|
319
|
+
fig, ax = _prepare_ax(ax, figsize)
|
|
320
|
+
pos = _layout(G)
|
|
321
|
+
colors = type_color_map(kc)
|
|
322
|
+
|
|
323
|
+
if len(G) == 0:
|
|
324
|
+
ax.set_title("Empty complex")
|
|
325
|
+
ax.axis("off")
|
|
326
|
+
return fig, ax
|
|
327
|
+
|
|
328
|
+
node_colors = [colors.get(G.nodes[n].get("type", ""), "#999999") for n in G]
|
|
329
|
+
if node_size_by_dim:
|
|
330
|
+
node_sizes = [_SIZE_BY_DIM.get(G.nodes[n].get("dim", 0), 200) for n in G]
|
|
331
|
+
else:
|
|
332
|
+
node_sizes = 300
|
|
333
|
+
|
|
334
|
+
nx.draw_networkx_edges(
|
|
335
|
+
G, pos, ax=ax, edge_color="#cccccc", width=1.5,
|
|
336
|
+
arrows=True, arrowstyle="-|>", arrowsize=12,
|
|
337
|
+
connectionstyle="arc3,rad=0.05",
|
|
338
|
+
)
|
|
339
|
+
nx.draw_networkx_nodes(
|
|
340
|
+
G, pos, ax=ax,
|
|
341
|
+
node_color=node_colors,
|
|
342
|
+
node_size=node_sizes,
|
|
343
|
+
edgecolors="#333333",
|
|
344
|
+
linewidths=0.5,
|
|
345
|
+
)
|
|
346
|
+
if with_labels:
|
|
347
|
+
nx.draw_networkx_labels(G, pos, ax=ax, font_size=8)
|
|
348
|
+
|
|
349
|
+
# Legend
|
|
350
|
+
from matplotlib.lines import Line2D
|
|
351
|
+
legend_handles = []
|
|
352
|
+
for type_name in sorted(colors):
|
|
353
|
+
kind = kc._schema._types.get(type_name, {}).get("kind", "?")
|
|
354
|
+
legend_handles.append(
|
|
355
|
+
Line2D([0], [0], marker="o", color="w",
|
|
356
|
+
markerfacecolor=colors[type_name], markersize=10,
|
|
357
|
+
label=f"{type_name} ({kind})")
|
|
358
|
+
)
|
|
359
|
+
if legend_handles:
|
|
360
|
+
ax.legend(handles=legend_handles, loc="best", fontsize=8)
|
|
361
|
+
|
|
362
|
+
ax.set_title(f"Hasse Diagram: {kc._schema._namespace}")
|
|
363
|
+
ax.axis("off")
|
|
364
|
+
return fig, ax
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def plot_hasse_star(
|
|
368
|
+
kc: "KnowledgeComplex",
|
|
369
|
+
id: str,
|
|
370
|
+
*,
|
|
371
|
+
ax: Any = None,
|
|
372
|
+
figsize: tuple[float, float] = (10, 8),
|
|
373
|
+
with_labels: bool = True,
|
|
374
|
+
) -> tuple[Any, Any]:
|
|
375
|
+
"""Plot the Hasse diagram with the star of an element highlighted.
|
|
376
|
+
|
|
377
|
+
Elements in ``St(id)`` are drawn in full color with directed arrows;
|
|
378
|
+
all other elements are dimmed to light gray. This is the Hasse-diagram
|
|
379
|
+
view — see :func:`plot_hasse` for details on what that means.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
kc : KnowledgeComplex
|
|
384
|
+
id : str
|
|
385
|
+
Element whose star to highlight.
|
|
386
|
+
ax : matplotlib Axes, optional
|
|
387
|
+
figsize : tuple
|
|
388
|
+
with_labels : bool
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
(fig, ax)
|
|
393
|
+
"""
|
|
394
|
+
nx = _require_nx()
|
|
395
|
+
_, plt = _require_mpl()
|
|
396
|
+
|
|
397
|
+
G = to_networkx(kc)
|
|
398
|
+
fig, ax = _prepare_ax(ax, figsize)
|
|
399
|
+
pos = _layout(G)
|
|
400
|
+
colors = type_color_map(kc)
|
|
401
|
+
star_ids = kc.star(id)
|
|
402
|
+
|
|
403
|
+
highlighted = [n for n in G if n in star_ids]
|
|
404
|
+
dimmed = [n for n in G if n not in star_ids]
|
|
405
|
+
|
|
406
|
+
if dimmed:
|
|
407
|
+
nx.draw_networkx_nodes(
|
|
408
|
+
G, pos, nodelist=dimmed, ax=ax,
|
|
409
|
+
node_color="#dddddd", node_size=150,
|
|
410
|
+
edgecolors="#cccccc", linewidths=0.5,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
star_edges = [(u, v) for u, v in G.edges() if u in star_ids and v in star_ids]
|
|
414
|
+
dim_edges = [(u, v) for u, v in G.edges() if (u, v) not in set(star_edges)]
|
|
415
|
+
if dim_edges:
|
|
416
|
+
nx.draw_networkx_edges(
|
|
417
|
+
G, pos, edgelist=dim_edges, ax=ax, edge_color="#eeeeee", width=1.0,
|
|
418
|
+
arrows=True, arrowstyle="-|>", arrowsize=8,
|
|
419
|
+
)
|
|
420
|
+
if star_edges:
|
|
421
|
+
nx.draw_networkx_edges(
|
|
422
|
+
G, pos, edgelist=star_edges, ax=ax, edge_color="#666666", width=2.0,
|
|
423
|
+
arrows=True, arrowstyle="-|>", arrowsize=14,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if highlighted:
|
|
427
|
+
h_colors = [colors.get(G.nodes[n].get("type", ""), "#999999") for n in highlighted]
|
|
428
|
+
h_sizes = [_SIZE_BY_DIM.get(G.nodes[n].get("dim", 0), 200) for n in highlighted]
|
|
429
|
+
nx.draw_networkx_nodes(
|
|
430
|
+
G, pos, nodelist=highlighted, ax=ax,
|
|
431
|
+
node_color=h_colors, node_size=h_sizes,
|
|
432
|
+
edgecolors="#333333", linewidths=1.0,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if with_labels:
|
|
436
|
+
nx.draw_networkx_labels(G, pos, ax=ax, font_size=8)
|
|
437
|
+
|
|
438
|
+
ax.set_title(f"Hasse Star({id})")
|
|
439
|
+
ax.axis("off")
|
|
440
|
+
return fig, ax
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def plot_hasse_skeleton(
|
|
444
|
+
kc: "KnowledgeComplex",
|
|
445
|
+
k: int,
|
|
446
|
+
*,
|
|
447
|
+
ax: Any = None,
|
|
448
|
+
figsize: tuple[float, float] = (10, 8),
|
|
449
|
+
with_labels: bool = True,
|
|
450
|
+
) -> tuple[Any, Any]:
|
|
451
|
+
"""Plot the Hasse diagram of the k-skeleton only.
|
|
452
|
+
|
|
453
|
+
Shows only elements of dimension ≤ k, with directed boundary arrows.
|
|
454
|
+
This is the Hasse-diagram view — see :func:`plot_hasse` for details.
|
|
455
|
+
|
|
456
|
+
k=0: vertices only, k=1: vertices + edges, k=2: everything.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
kc : KnowledgeComplex
|
|
461
|
+
k : int
|
|
462
|
+
Maximum dimension (0, 1, or 2).
|
|
463
|
+
ax : matplotlib Axes, optional
|
|
464
|
+
figsize : tuple
|
|
465
|
+
with_labels : bool
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
(fig, ax)
|
|
470
|
+
"""
|
|
471
|
+
nx = _require_nx()
|
|
472
|
+
_, plt = _require_mpl()
|
|
473
|
+
|
|
474
|
+
G = to_networkx(kc)
|
|
475
|
+
skel_ids = kc.skeleton(k)
|
|
476
|
+
subG = G.subgraph(skel_ids).copy()
|
|
477
|
+
|
|
478
|
+
fig, ax = _prepare_ax(ax, figsize)
|
|
479
|
+
pos = _layout(subG)
|
|
480
|
+
colors = type_color_map(kc)
|
|
481
|
+
|
|
482
|
+
if len(subG) == 0:
|
|
483
|
+
ax.set_title(f"Hasse Skeleton({k}) — empty")
|
|
484
|
+
ax.axis("off")
|
|
485
|
+
return fig, ax
|
|
486
|
+
|
|
487
|
+
node_colors = [colors.get(subG.nodes[n].get("type", ""), "#999999") for n in subG]
|
|
488
|
+
node_sizes = [_SIZE_BY_DIM.get(subG.nodes[n].get("dim", 0), 200) for n in subG]
|
|
489
|
+
|
|
490
|
+
nx.draw_networkx_edges(
|
|
491
|
+
subG, pos, ax=ax, edge_color="#cccccc", width=1.5,
|
|
492
|
+
arrows=True, arrowstyle="-|>", arrowsize=12,
|
|
493
|
+
)
|
|
494
|
+
nx.draw_networkx_nodes(
|
|
495
|
+
subG, pos, ax=ax,
|
|
496
|
+
node_color=node_colors,
|
|
497
|
+
node_size=node_sizes,
|
|
498
|
+
edgecolors="#333333",
|
|
499
|
+
linewidths=0.5,
|
|
500
|
+
)
|
|
501
|
+
if with_labels:
|
|
502
|
+
nx.draw_networkx_labels(subG, pos, ax=ax, font_size=8)
|
|
503
|
+
|
|
504
|
+
ax.set_title(f"Hasse Skeleton({k})")
|
|
505
|
+
ax.axis("off")
|
|
506
|
+
return fig, ax
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ── Deprecated aliases ─────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def plot_complex(kc, **kwargs):
|
|
513
|
+
"""Deprecated: use :func:`plot_hasse` instead."""
|
|
514
|
+
warnings.warn("plot_complex is deprecated, use plot_hasse", DeprecationWarning, stacklevel=2)
|
|
515
|
+
return plot_hasse(kc, **kwargs)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def plot_star(kc, id, **kwargs):
|
|
519
|
+
"""Deprecated: use :func:`plot_hasse_star` instead."""
|
|
520
|
+
warnings.warn("plot_star is deprecated, use plot_hasse_star", DeprecationWarning, stacklevel=2)
|
|
521
|
+
return plot_hasse_star(kc, id, **kwargs)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def plot_skeleton(kc, k, **kwargs):
|
|
525
|
+
"""Deprecated: use :func:`plot_hasse_skeleton` instead."""
|
|
526
|
+
warnings.warn("plot_skeleton is deprecated, use plot_hasse_skeleton", DeprecationWarning, stacklevel=2)
|
|
527
|
+
return plot_hasse_skeleton(kc, k, **kwargs)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ── Geometric realization helpers ──────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _face_vertices(kc: "KnowledgeComplex", face_id: str) -> list[str]:
|
|
534
|
+
"""Get the 3 vertices of a face by walking boundary twice.
|
|
535
|
+
|
|
536
|
+
boundary(face) → 3 edges → boundary(each edge) → deduplicate → 3 vertices.
|
|
537
|
+
"""
|
|
538
|
+
verts: set[str] = set()
|
|
539
|
+
for edge_id in kc.boundary(face_id):
|
|
540
|
+
verts |= kc.boundary(edge_id)
|
|
541
|
+
return sorted(verts)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _vertex_positions_3d(
|
|
545
|
+
kc: "KnowledgeComplex",
|
|
546
|
+
) -> dict[str, tuple[float, float, float]]:
|
|
547
|
+
"""Compute 3D positions for KC vertices using force-directed layout.
|
|
548
|
+
|
|
549
|
+
Builds a networkx graph of only KC vertices, with an edge between
|
|
550
|
+
vertices that share a KC edge, then runs spring_layout in 3D.
|
|
551
|
+
"""
|
|
552
|
+
nx = _require_nx()
|
|
553
|
+
G = nx.Graph()
|
|
554
|
+
|
|
555
|
+
# Add vertex nodes
|
|
556
|
+
vertex_ids = list(kc.skeleton(0))
|
|
557
|
+
for vid in vertex_ids:
|
|
558
|
+
G.add_node(vid)
|
|
559
|
+
|
|
560
|
+
# Connect vertices that share a KC edge
|
|
561
|
+
edge_ids = kc.skeleton(1) - kc.skeleton(0)
|
|
562
|
+
for eid in edge_ids:
|
|
563
|
+
boundary = list(kc.boundary(eid))
|
|
564
|
+
if len(boundary) == 2:
|
|
565
|
+
G.add_edge(boundary[0], boundary[1])
|
|
566
|
+
|
|
567
|
+
if len(G) == 0:
|
|
568
|
+
return {}
|
|
569
|
+
|
|
570
|
+
pos = nx.spring_layout(G, dim=3, seed=42)
|
|
571
|
+
return {vid: tuple(pos[vid]) for vid in pos}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# ── Geometric realization: matplotlib ──────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def plot_geometric(
|
|
578
|
+
kc: "KnowledgeComplex",
|
|
579
|
+
*,
|
|
580
|
+
ax: Any = None,
|
|
581
|
+
figsize: tuple[float, float] = (10, 8),
|
|
582
|
+
with_labels: bool = True,
|
|
583
|
+
) -> tuple[Any, Any]:
|
|
584
|
+
"""Plot the geometric realization of the complex in 3D.
|
|
585
|
+
|
|
586
|
+
KC vertices become points in 3D space (positioned by force-directed
|
|
587
|
+
layout). KC edges become line segments connecting their two boundary
|
|
588
|
+
vertices. KC faces become filled, semi-transparent triangular patches
|
|
589
|
+
spanning their three boundary vertices.
|
|
590
|
+
|
|
591
|
+
This is the classical geometric realization — the view a topologist
|
|
592
|
+
would draw. For the Hasse diagram where every element is a node and
|
|
593
|
+
boundary relations are directed edges, see :func:`plot_hasse`.
|
|
594
|
+
|
|
595
|
+
Parameters
|
|
596
|
+
----------
|
|
597
|
+
kc : KnowledgeComplex
|
|
598
|
+
ax : matplotlib Axes3D, optional
|
|
599
|
+
A 3D axes to draw on. Created if not provided.
|
|
600
|
+
figsize : tuple
|
|
601
|
+
Figure size if creating a new figure.
|
|
602
|
+
with_labels : bool
|
|
603
|
+
Show vertex ID labels.
|
|
604
|
+
|
|
605
|
+
Returns
|
|
606
|
+
-------
|
|
607
|
+
(fig, ax)
|
|
608
|
+
The matplotlib Figure and Axes3D.
|
|
609
|
+
"""
|
|
610
|
+
_, plt = _require_mpl()
|
|
611
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
612
|
+
|
|
613
|
+
colors = type_color_map(kc)
|
|
614
|
+
pos = _vertex_positions_3d(kc)
|
|
615
|
+
|
|
616
|
+
if ax is None:
|
|
617
|
+
fig = plt.figure(figsize=figsize)
|
|
618
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
619
|
+
else:
|
|
620
|
+
fig = ax.get_figure()
|
|
621
|
+
|
|
622
|
+
if not pos:
|
|
623
|
+
ax.set_title("Empty complex")
|
|
624
|
+
return fig, ax
|
|
625
|
+
|
|
626
|
+
# Draw faces as filled triangles
|
|
627
|
+
face_ids = kc.skeleton(2) - kc.skeleton(1)
|
|
628
|
+
for fid in face_ids:
|
|
629
|
+
verts = _face_vertices(kc, fid)
|
|
630
|
+
if len(verts) == 3 and all(v in pos for v in verts):
|
|
631
|
+
tri = [pos[v] for v in verts]
|
|
632
|
+
face_type = kc.element(fid).type
|
|
633
|
+
color = colors.get(face_type, "#999999")
|
|
634
|
+
poly = Poly3DCollection([tri], alpha=0.25, facecolor=color,
|
|
635
|
+
edgecolor=color, linewidths=0.5)
|
|
636
|
+
ax.add_collection3d(poly)
|
|
637
|
+
|
|
638
|
+
# Draw edges as line segments
|
|
639
|
+
edge_ids = kc.skeleton(1) - kc.skeleton(0)
|
|
640
|
+
for eid in edge_ids:
|
|
641
|
+
boundary = list(kc.boundary(eid))
|
|
642
|
+
if len(boundary) == 2 and all(v in pos for v in boundary):
|
|
643
|
+
p0, p1 = pos[boundary[0]], pos[boundary[1]]
|
|
644
|
+
edge_type = kc.element(eid).type
|
|
645
|
+
color = colors.get(edge_type, "#999999")
|
|
646
|
+
ax.plot3D(
|
|
647
|
+
[p0[0], p1[0]], [p0[1], p1[1]], [p0[2], p1[2]],
|
|
648
|
+
color=color, linewidth=2,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Draw vertices as scatter points
|
|
652
|
+
vertex_ids = list(kc.skeleton(0))
|
|
653
|
+
for vid in vertex_ids:
|
|
654
|
+
if vid in pos:
|
|
655
|
+
x, y, z = pos[vid]
|
|
656
|
+
vtype = kc.element(vid).type
|
|
657
|
+
color = colors.get(vtype, "#999999")
|
|
658
|
+
ax.scatter3D(x, y, z, color=color, s=80, edgecolors="#333333",
|
|
659
|
+
linewidths=0.5, zorder=5, depthshade=False)
|
|
660
|
+
|
|
661
|
+
# Labels
|
|
662
|
+
if with_labels:
|
|
663
|
+
for vid in vertex_ids:
|
|
664
|
+
if vid in pos:
|
|
665
|
+
x, y, z = pos[vid]
|
|
666
|
+
ax.text(x, y, z, f" {vid}", fontsize=7)
|
|
667
|
+
|
|
668
|
+
# Legend
|
|
669
|
+
from matplotlib.lines import Line2D
|
|
670
|
+
legend_handles = []
|
|
671
|
+
for type_name in sorted(colors):
|
|
672
|
+
kind = kc._schema._types.get(type_name, {}).get("kind", "?")
|
|
673
|
+
legend_handles.append(
|
|
674
|
+
Line2D([0], [0], marker="o", color="w",
|
|
675
|
+
markerfacecolor=colors[type_name], markersize=8,
|
|
676
|
+
label=f"{type_name} ({kind})")
|
|
677
|
+
)
|
|
678
|
+
if legend_handles:
|
|
679
|
+
ax.legend(handles=legend_handles, loc="best", fontsize=7)
|
|
680
|
+
|
|
681
|
+
ax.set_title(f"Geometric Realization: {kc._schema._namespace}")
|
|
682
|
+
return fig, ax
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# ── Geometric realization: plotly ──────────────────────────────────────────
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def plot_geometric_interactive(
|
|
689
|
+
kc: "KnowledgeComplex",
|
|
690
|
+
) -> Any:
|
|
691
|
+
"""Plot an interactive 3D geometric realization of the complex.
|
|
692
|
+
|
|
693
|
+
Same geometry as :func:`plot_geometric` — KC vertices are points, KC edges
|
|
694
|
+
are line segments, KC faces are filled triangles — but rendered with
|
|
695
|
+
Plotly for interactive rotation, zoom, and hover inspection.
|
|
696
|
+
|
|
697
|
+
Requires plotly::
|
|
698
|
+
|
|
699
|
+
pip install knowledgecomplex[viz-interactive]
|
|
700
|
+
|
|
701
|
+
Parameters
|
|
702
|
+
----------
|
|
703
|
+
kc : KnowledgeComplex
|
|
704
|
+
|
|
705
|
+
Returns
|
|
706
|
+
-------
|
|
707
|
+
plotly.graph_objects.Figure
|
|
708
|
+
Call ``.show()`` to display or ``.write_html("file.html")`` to save.
|
|
709
|
+
"""
|
|
710
|
+
go = _require_plotly()
|
|
711
|
+
|
|
712
|
+
colors = type_color_map(kc)
|
|
713
|
+
pos = _vertex_positions_3d(kc)
|
|
714
|
+
fig = go.Figure()
|
|
715
|
+
|
|
716
|
+
if not pos:
|
|
717
|
+
fig.update_layout(title="Empty complex")
|
|
718
|
+
return fig
|
|
719
|
+
|
|
720
|
+
# Faces as Mesh3d triangles
|
|
721
|
+
face_ids = kc.skeleton(2) - kc.skeleton(1)
|
|
722
|
+
for fid in face_ids:
|
|
723
|
+
verts = _face_vertices(kc, fid)
|
|
724
|
+
if len(verts) == 3 and all(v in pos for v in verts):
|
|
725
|
+
xs = [pos[v][0] for v in verts]
|
|
726
|
+
ys = [pos[v][1] for v in verts]
|
|
727
|
+
zs = [pos[v][2] for v in verts]
|
|
728
|
+
face_type = kc.element(fid).type
|
|
729
|
+
color = colors.get(face_type, "#999999")
|
|
730
|
+
fig.add_trace(go.Mesh3d(
|
|
731
|
+
x=xs, y=ys, z=zs,
|
|
732
|
+
i=[0], j=[1], k=[2],
|
|
733
|
+
color=color, opacity=0.3,
|
|
734
|
+
hoverinfo="text",
|
|
735
|
+
hovertext=f"{fid} ({face_type})",
|
|
736
|
+
showlegend=False,
|
|
737
|
+
))
|
|
738
|
+
|
|
739
|
+
# Edges as line segments
|
|
740
|
+
edge_ids = kc.skeleton(1) - kc.skeleton(0)
|
|
741
|
+
for eid in edge_ids:
|
|
742
|
+
boundary = list(kc.boundary(eid))
|
|
743
|
+
if len(boundary) == 2 and all(v in pos for v in boundary):
|
|
744
|
+
p0, p1 = pos[boundary[0]], pos[boundary[1]]
|
|
745
|
+
edge_type = kc.element(eid).type
|
|
746
|
+
color = colors.get(edge_type, "#999999")
|
|
747
|
+
fig.add_trace(go.Scatter3d(
|
|
748
|
+
x=[p0[0], p1[0]], y=[p0[1], p1[1]], z=[p0[2], p1[2]],
|
|
749
|
+
mode="lines",
|
|
750
|
+
line=dict(color=color, width=4),
|
|
751
|
+
hoverinfo="text",
|
|
752
|
+
hovertext=f"{eid} ({edge_type})",
|
|
753
|
+
showlegend=False,
|
|
754
|
+
))
|
|
755
|
+
|
|
756
|
+
# Vertices as markers
|
|
757
|
+
vertex_ids = [v for v in kc.skeleton(0) if v in pos]
|
|
758
|
+
xs = [pos[v][0] for v in vertex_ids]
|
|
759
|
+
ys = [pos[v][1] for v in vertex_ids]
|
|
760
|
+
zs = [pos[v][2] for v in vertex_ids]
|
|
761
|
+
vtypes = [kc.element(v).type for v in vertex_ids]
|
|
762
|
+
vcolors = [colors.get(t, "#999999") for t in vtypes]
|
|
763
|
+
hover = [f"{vid} ({vt})" for vid, vt in zip(vertex_ids, vtypes)]
|
|
764
|
+
|
|
765
|
+
fig.add_trace(go.Scatter3d(
|
|
766
|
+
x=xs, y=ys, z=zs,
|
|
767
|
+
mode="markers+text",
|
|
768
|
+
marker=dict(size=6, color=vcolors, line=dict(width=1, color="#333333")),
|
|
769
|
+
text=vertex_ids,
|
|
770
|
+
textposition="top center",
|
|
771
|
+
textfont=dict(size=8),
|
|
772
|
+
hoverinfo="text",
|
|
773
|
+
hovertext=hover,
|
|
774
|
+
showlegend=False,
|
|
775
|
+
))
|
|
776
|
+
|
|
777
|
+
fig.update_layout(
|
|
778
|
+
title=f"Geometric Realization: {kc._schema._namespace}",
|
|
779
|
+
scene=dict(
|
|
780
|
+
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title=""),
|
|
781
|
+
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title=""),
|
|
782
|
+
zaxis=dict(showgrid=False, zeroline=False, showticklabels=False, title=""),
|
|
783
|
+
),
|
|
784
|
+
showlegend=False,
|
|
785
|
+
)
|
|
786
|
+
return fig
|