Skip to content

Commit ceb3f74

Browse files
authored
Merge pull request #5 from BlockScience/dev
parametric sequence and clean up html
2 parents 0fea456 + 98d63c5 commit ceb3f74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+462
-46865
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ venv/
1212
MANIFEST
1313
htmlcov/
1414
.coverage
15+
site/

docs/api/parametric.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Parametric Sequences
2+
3+
::: knowledgecomplex.parametric

docs/tutorial.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,36 @@ filt[1] # set of element IDs at step 1
254254
filt2 = Filtration.from_function(kc, lambda eid: some_score(eid))
255255
```
256256

257-
## 7. Clique inference
257+
## 7. Parametric sequences
258+
259+
A `ParametricSequence` views a single complex through a parameterized filter. The complex holds all elements across all parameter values; the filter selects which are "active" at each value. Unlike a Filtration, subcomplexes can shrink — elements can appear and disappear.
260+
261+
```python
262+
from knowledgecomplex import ParametricSequence
263+
264+
# Complex has people with active_from/active_until attributes
265+
seq = ParametricSequence(
266+
kc,
267+
values=["Q1", "Q2", "Q3", "Q4"],
268+
filter=lambda elem, t: elem.attrs.get("active_from", "0") <= t < elem.attrs.get("active_until", "9999"),
269+
)
270+
271+
seq["Q2"] # set of element IDs active at Q2
272+
seq.birth("carol") # "Q2" — first value where carol appears
273+
seq.death("bob") # "Q3" — first value where bob disappears
274+
seq.active_at("bob") # ["Q1", "Q2"]
275+
seq.new_at(1) # elements appearing at Q2
276+
seq.removed_at(2) # elements disappearing at Q3 (bob left)
277+
seq.is_monotone # False — people can leave
278+
seq.subcomplex_at(0) # is the Q1 slice boundary-closed?
279+
280+
for value, ids in seq:
281+
print(f"{value}: {len(ids)} elements")
282+
```
283+
284+
The complex is the territory; the parameterized filter is the map.
285+
286+
## 8. Clique inference
258287

259288
Discover higher-order structure hiding in the edge graph:
260289

@@ -271,7 +300,7 @@ added = infer_faces(kc, "coverage")
271300
preview = infer_faces(kc, "coverage", dry_run=True)
272301
```
273302

274-
## 8. Export and load
303+
## 9. Export and load
275304

276305
```python
277306
# Export schema + instance to a directory
@@ -292,7 +321,7 @@ save_graph(kc, "data.jsonld", format="json-ld")
292321
load_graph(kc, "data.ttl") # additive loading
293322
```
294323

295-
## 9. Verification and audit
324+
## 10. Verification and audit
296325

297326
```python
298327
# Throwing verification
@@ -317,7 +346,7 @@ report = audit_file("data/instance.ttl", shapes="data/shapes.ttl",
317346
ontology="data/ontology.ttl")
318347
```
319348

320-
## 10. Pre-built ontologies
349+
## 11. Pre-built ontologies
321350

322351
Three ontologies ship with the package:
323352

examples/08_temporal_sweep/temporal_sweep.py

Lines changed: 38 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -69,74 +69,50 @@
6969
print(f"Built timeline: {len(kc.element_ids())} total elements across all time")
7070
print()
7171

72-
# ── Manual parameterized sweep ─────────────────────────────────────────────
72+
# ── Parameterized sweep using ParametricSequence ──────────────────────────
7373

74-
# Since we store active_from/active_until as string attributes, we can
75-
# query for elements active at a specific time by comparing attribute values.
74+
from knowledgecomplex import ParametricSequence
75+
76+
def active_filter(elem, t):
77+
"""Element is active at time t if active_from <= t < active_until."""
78+
af = elem.attrs.get("active_from", "0")
79+
au = elem.attrs.get("active_until", "9999")
80+
return af <= t < au
81+
82+
seq = ParametricSequence(kc, values=["1", "2", "3", "4", "5"], filter=active_filter)
7683

7784
print("=== Active subcomplex at each quarter ===")
78-
for t in ["1", "2", "3", "4", "5"]:
79-
# Get active people at time t
80-
active_people = set()
81-
for pid in kc.element_ids(type="Person"):
82-
elem = kc.element(pid)
83-
af = elem.attrs.get("active_from", "0")
84-
au = elem.attrs.get("active_until", "9999")
85-
if af <= t < au:
86-
active_people.add(pid)
87-
88-
# Get active edges at time t
89-
active_edges = set()
90-
for eid in kc.element_ids(type="WorksWith"):
91-
elem = kc.element(eid)
92-
af = elem.attrs.get("active_from", "0")
93-
au = elem.attrs.get("active_until", "9999")
94-
if af <= t < au:
95-
# Only include if both endpoints are active
96-
boundary = kc.boundary(eid)
97-
if boundary <= active_people:
98-
active_edges.add(eid)
99-
100-
# Get active faces
101-
active_faces = set()
102-
for fid in kc.element_ids(type="Squad"):
103-
boundary = kc.boundary(fid)
104-
if boundary <= active_edges:
105-
active_faces.add(fid)
106-
107-
active = active_people | active_edges | active_faces
108-
is_sub = kc.is_subcomplex(active)
109-
110-
print(f" Q{t}: {len(active_people)} people, "
111-
f"{len(active_edges)} collabs, "
112-
f"{len(active_faces)} squads "
113-
f"(valid subcomplex: {is_sub})")
114-
print(f" people: {sorted(active_people)}")
115-
116-
# Show who's new and who left
117-
if t != "1":
118-
prev_t = str(int(t) - 1)
119-
prev_people = set()
120-
for pid in kc.element_ids(type="Person"):
121-
elem = kc.element(pid)
122-
af = elem.attrs.get("active_from", "0")
123-
au = elem.attrs.get("active_until", "9999")
124-
if af <= prev_t < au:
125-
prev_people.add(pid)
126-
joined = active_people - prev_people
127-
left = prev_people - active_people
128-
if joined:
129-
print(f" joined: {sorted(joined)}")
130-
if left:
131-
print(f" left: {sorted(left)}")
85+
for t, active in seq:
86+
print(f" Q{t}: {len(active)} elements "
87+
f"(valid subcomplex: {seq.subcomplex_at(seq.values.index(t))})")
88+
print(f" {sorted(active)}")
89+
90+
i = seq.values.index(t)
91+
new = seq.new_at(i)
92+
removed = seq.removed_at(i)
93+
if new:
94+
print(f" joined: {sorted(new)}")
95+
if removed:
96+
print(f" left: {sorted(removed)}")
13297
print()
13398

99+
# ── Lifecycle queries ──────────────────────────────────────────────────────
100+
101+
print("=== Lifecycle ===")
102+
for person in ["alice", "bob", "carol", "dave", "eve"]:
103+
birth = seq.birth(person)
104+
death = seq.death(person)
105+
active = seq.active_at(person)
106+
print(f" {person:6s} birth=Q{birth} death={'Q'+death if death else 'still active':14s} active={active}")
107+
print()
108+
134109
# ── Key insight ────────────────────────────────────────────────────────────
135110

136-
print("=== Key insight ===")
137-
print(" This is NOT a filtration — the subcomplex at Q4 is not a superset")
138-
print(" of Q3 (bob left). But each time-slice is a valid subcomplex,")
139-
print(" and the full complex contains the complete history.")
111+
print(f"=== Key insight ===")
112+
print(f" is_monotone: {seq.is_monotone}")
113+
print(f" This is NOT a filtration — bob leaves at Q3, so Q3 is not a")
114+
print(f" superset of Q2. But the complex holds the complete history,")
115+
print(f" and the parameterized filter slices it at any time.")
140116
print()
141-
print(" The complex is the territory; the time-slice queries are the maps.")
117+
print(f" The complex is the territory; the parameterized filter is the map.")
142118
print("Done.")

knowledgecomplex/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from knowledgecomplex.schema import SchemaBuilder, vocab, text, TextDescriptor, Codec
1212
from knowledgecomplex.graph import KnowledgeComplex, Element
1313
from knowledgecomplex.filtration import Filtration
14+
from knowledgecomplex.parametric import ParametricSequence
1415
from knowledgecomplex.exceptions import ValidationError, SchemaError, UnknownQueryError
1516
from knowledgecomplex.audit import AuditReport, AuditViolation, audit_file
1617
from knowledgecomplex.io import save_graph, load_graph, dump_graph
@@ -60,6 +61,7 @@
6061
"KnowledgeComplex", "Element",
6162
# Filtrations
6263
"Filtration",
64+
"ParametricSequence",
6365
# Exceptions
6466
"ValidationError", "SchemaError", "UnknownQueryError",
6567
# File I/O

knowledgecomplex/parametric.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""
2+
knowledgecomplex.parametric — Parametric sequences over knowledge complexes.
3+
4+
A ParametricSequence represents a single complex viewed through a
5+
parameterized filter. Each parameter value selects a subcomplex —
6+
the sequence of subcomplexes can grow, shrink, or change arbitrarily
7+
as the parameter varies.
8+
9+
Unlike :class:`~knowledgecomplex.filtration.Filtration`, which enforces
10+
monotone nesting, a ParametricSequence is observational — it computes
11+
slices lazily from a filter function.
12+
"""
13+
14+
from __future__ import annotations
15+
from typing import Any, Callable, Iterator, TYPE_CHECKING
16+
17+
if TYPE_CHECKING:
18+
from knowledgecomplex.graph import KnowledgeComplex, Element
19+
20+
21+
class ParametricSequence:
22+
"""
23+
A complex viewed through a parameterized filter.
24+
25+
One complex holds all elements. A filter function decides which
26+
elements are active at each parameter value. The result is a
27+
sequence of subcomplexes indexed by parameter values.
28+
29+
Parameters
30+
----------
31+
kc : KnowledgeComplex
32+
The complex containing all elements.
33+
values : list
34+
Ordered parameter values (e.g. ``["Q1", "Q2", "Q3", "Q4"]``).
35+
filter : Callable[[Element, value], bool]
36+
Returns True if the element is active at the given parameter value.
37+
38+
Example
39+
-------
40+
>>> seq = ParametricSequence(kc, values=["1","2","3","4"],
41+
... filter=lambda elem, t: elem.attrs.get("active_from","0") <= t)
42+
>>> seq[0] # element IDs active at "1"
43+
>>> seq.birth("carol") # first value where carol appears
44+
"""
45+
46+
def __init__(
47+
self,
48+
kc: "KnowledgeComplex",
49+
values: list,
50+
filter: Callable[["Element", Any], bool],
51+
) -> None:
52+
self._kc = kc
53+
self._values = list(values)
54+
self._filter = filter
55+
self._cache: dict[int, frozenset[str]] = {}
56+
57+
def _compute(self, index: int) -> frozenset[str]:
58+
"""Compute and cache the element set at a given index."""
59+
if index not in self._cache:
60+
value = self._values[index]
61+
ids = frozenset(
62+
eid for eid in self._kc.element_ids()
63+
if self._filter(self._kc.element(eid), value)
64+
)
65+
self._cache[index] = ids
66+
return self._cache[index]
67+
68+
def __repr__(self) -> str:
69+
return f"ParametricSequence(steps={len(self._values)}, monotone={self.is_monotone})"
70+
71+
# --- Indexing ---
72+
73+
def __getitem__(self, key: int | Any) -> set[str]:
74+
if isinstance(key, int):
75+
return set(self._compute(key))
76+
# Try to look up by parameter value
77+
try:
78+
index = self._values.index(key)
79+
except ValueError:
80+
raise KeyError(f"Parameter value {key!r} not in values list")
81+
return set(self._compute(index))
82+
83+
def __len__(self) -> int:
84+
return len(self._values)
85+
86+
def __iter__(self) -> Iterator[tuple[Any, set[str]]]:
87+
for i, value in enumerate(self._values):
88+
yield value, set(self._compute(i))
89+
90+
# --- Properties ---
91+
92+
@property
93+
def complex(self) -> "KnowledgeComplex":
94+
"""The parent KnowledgeComplex."""
95+
return self._kc
96+
97+
@property
98+
def values(self) -> list:
99+
"""The ordered parameter values."""
100+
return list(self._values)
101+
102+
@property
103+
def is_monotone(self) -> bool:
104+
"""True if every step is a superset of the previous (filtration-like)."""
105+
for i in range(1, len(self._values)):
106+
if not (self._compute(i - 1) <= self._compute(i)):
107+
return False
108+
return True
109+
110+
# --- Queries ---
111+
112+
def birth(self, element_id: str) -> Any:
113+
"""Return the first parameter value where the element appears.
114+
115+
Raises
116+
------
117+
ValueError
118+
If the element does not appear at any parameter value.
119+
"""
120+
for i, value in enumerate(self._values):
121+
if element_id in self._compute(i):
122+
return value
123+
raise ValueError(f"Element '{element_id}' not found at any parameter value")
124+
125+
def death(self, element_id: str) -> Any | None:
126+
"""Return the first parameter value where the element disappears.
127+
128+
Returns None if the element is present at all values after its birth,
129+
or if it never appears.
130+
"""
131+
appeared = False
132+
for i in range(len(self._values)):
133+
present = element_id in self._compute(i)
134+
if present:
135+
appeared = True
136+
elif appeared:
137+
return self._values[i]
138+
return None
139+
140+
def active_at(self, element_id: str) -> list:
141+
"""Return the list of parameter values where the element is present."""
142+
return [
143+
self._values[i]
144+
for i in range(len(self._values))
145+
if element_id in self._compute(i)
146+
]
147+
148+
def new_at(self, index: int) -> set[str]:
149+
"""Return elements appearing at step index that were not in step index-1."""
150+
current = self._compute(index)
151+
if index == 0:
152+
return set(current)
153+
previous = self._compute(index - 1)
154+
return set(current - previous)
155+
156+
def removed_at(self, index: int) -> set[str]:
157+
"""Return elements present at step index-1 that are absent at step index."""
158+
if index == 0:
159+
return set()
160+
current = self._compute(index)
161+
previous = self._compute(index - 1)
162+
return set(previous - current)
163+
164+
def subcomplex_at(self, index: int) -> bool:
165+
"""Check if the slice at index is a valid subcomplex (closed under boundary)."""
166+
return self._kc.is_subcomplex(set(self._compute(index)))

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ nav:
3030
- Algebraic Topology: api/analysis.md
3131
- Clique Inference: api/clique.md
3232
- Filtrations: api/filtration.md
33+
- Parametric Sequences: api/parametric.md
3334
- Diffs & Sequences: api/diff.md
3435
- File I/O: api/io.md
3536
- Codecs: api/codecs.md

0 commit comments

Comments
 (0)