Skip to content

Commit f35299f

Browse files
committed
feat(math): add simplicial complex f-vector operation
Add topology.simplicial_complex.f_vector.compute to the topology domain. Computes the f-vector (face counts by dimension), h-vector, and Euler characteristic of a finite simplicial complex from its maximal facets. Partially addresses #1798.
1 parent efce533 commit f35299f

7 files changed

Lines changed: 141 additions & 6 deletions

File tree

src/jacobian/math/topology/_admission.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@
2525
AdmissionDecision.KEEP,
2626
"distinct exact bounded mathematical value or invariant with material computational or reliability leverage",
2727
),
28+
OperationAdmission(
29+
"topology.simplicial_complex.f_vector.compute",
30+
AdmissionDecision.KEEP,
31+
"exact f-vector, h-vector, and Euler characteristic of a simplicial complex",
32+
),
2833
)

src/jacobian/math/topology/_models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,3 +785,21 @@ def require_complete_integral_dimension_range(self) -> Self:
785785
"face_closure",
786786
"simplicial_complex_digest",
787787
]
788+
789+
790+
class FVectorRequest(StrictModel):
791+
"""Request the f-vector and h-vector of a simplicial complex."""
792+
793+
complex: SimplicialComplexRequest
794+
795+
796+
class FVectorResult(TopologyExactResult):
797+
"""The f-vector and h-vector of a simplicial complex."""
798+
799+
f_vector: tuple[int, ...]
800+
h_vector: tuple[int, ...]
801+
euler_characteristic: int
802+
dimension: int
803+
804+
805+
__all__.extend(["FVectorRequest", "FVectorResult"])

src/jacobian/math/topology/_operations.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
ChainComplexResult,
2424
FacesInDimension,
2525
FiniteSimplicialComplex,
26+
FVectorRequest,
27+
FVectorResult,
2628
HomologyConvention,
2729
HomologyGroupResult,
2830
IntegralFreeGenerator,
@@ -682,3 +684,47 @@ def _integral_homology(
682684
)
683685

684686
__all__ = ["TOPOLOGY_OPERATIONS"]
687+
688+
689+
def compute_f_vector(request: FVectorRequest) -> FVectorResult:
690+
"""Compute the f-vector and h-vector of a simplicial complex."""
691+
facets = request.complex.facets
692+
693+
# Build all simplices from facets
694+
from itertools import combinations as _comb
695+
696+
all_simplices: set[tuple[str, ...]] = set()
697+
for facet in facets:
698+
n = len(facet)
699+
for r in range(1, n + 1):
700+
for subset in _comb(facet, r):
701+
all_simplices.add(tuple(sorted(subset)))
702+
703+
# Count by dimension
704+
max_dim = 0
705+
counts_by_dim: dict[int, int] = {}
706+
for simplex in all_simplices:
707+
dim = len(simplex) - 1
708+
counts_by_dim[dim] = counts_by_dim.get(dim, 0) + 1
709+
max_dim = max(max_dim, dim)
710+
711+
f_vector = tuple(counts_by_dim.get(d, 0) for d in range(max_dim + 1))
712+
euler = sum((-1) ** d * counts_by_dim.get(d, 0) for d in range(max_dim + 1))
713+
714+
# Compute h-vector from f-vector
715+
from math import comb as _comb_func
716+
717+
n = len(f_vector)
718+
h_vector = []
719+
for i in range(n):
720+
h = 0
721+
for j in range(i + 1):
722+
h += ((-1) ** (i - j)) * f_vector[j] * _comb_func(n - 1 - j, i - j)
723+
h_vector.append(h)
724+
725+
return FVectorResult(
726+
f_vector=f_vector,
727+
h_vector=tuple(h_vector),
728+
euler_characteristic=euler,
729+
dimension=max_dim,
730+
)
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,37 @@
11
"""Finite simplicial topology domain."""
22

3-
from jacobian.catalog.models import MathTools
4-
from jacobian.math.topology._operations import TOPOLOGY_OPERATIONS
3+
from jacobian.catalog._examples import example
4+
from jacobian.catalog.models import MathTool, MathTools
5+
from jacobian.math.topology._models import FVectorRequest, FVectorResult
6+
from jacobian.math.topology._operations import TOPOLOGY_OPERATIONS, compute_f_vector
57

68
__all__ = ["TOOLS"]
79

8-
TOOLS: MathTools = TOPOLOGY_OPERATIONS
10+
_f_vector_tool = MathTool(
11+
operation_id="topology.simplicial_complex.f_vector.compute",
12+
version="1",
13+
title="Compute the f-vector and h-vector of a simplicial complex",
14+
description=(
15+
"Compute the f-vector (face counts by dimension) and h-vector "
16+
"of a finite simplicial complex, with Euler characteristic."
17+
),
18+
request_type=FVectorRequest,
19+
result_type=FVectorResult,
20+
run=compute_f_vector,
21+
tags=("topology", "simplicial", "exact"),
22+
examples=(
23+
example(
24+
"triangle_f_vector",
25+
"Compute f-vector of a triangle (3 vertices, 3 edges, 1 face); "
26+
"facets must be a list of simplices.",
27+
{
28+
"complex": {
29+
"vertices": ["v0", "v1", "v2"],
30+
"facets": [["v0", "v1", "v2"]],
31+
}
32+
},
33+
),
34+
),
35+
)
36+
37+
TOOLS: MathTools = (*TOPOLOGY_OPERATIONS, _f_vector_tool)

tests/catalog/operation_schema_snapshots/topology.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"input_schema": "sha256:5696974624ef54213bd7f17dedfd0b85bca3c218eb03f32dfa2c425f6b141b1a",
1111
"output_schema": "sha256:c737a222d0d9965766a9a58c4477784f6876a0696b4950b919b448f1fd6d950a"
1212
},
13+
"topology.simplicial_complex.f_vector.compute": {
14+
"input_schema": "sha256:cda6b5cff62d3c4ea1facadf31c5e3f437a18995a5f239785fd55032fbcaac35",
15+
"output_schema": "sha256:4d6d236e58516aecf41c20352328af26f6ac9c9259c9764ad9aa163b76ab38cf"
16+
},
1317
"topology.simplicial_homology.compute": {
1418
"input_schema": "sha256:fb63f528a8a29fc0764b550ac7a631896c7736f706ab2a73d7c7621da4ee4a58",
1519
"output_schema": "sha256:37e3af857a4f953425c5ff1755dc2aa29ab238bb8bca27fbec5484f40ff45441"

tests/catalog/test_admission.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ def test_every_frozen_candidate_has_exactly_one_admission_decision() -> None:
2727
reviewed_ids = [record.operation_id for record in OPERATION_ADMISSIONS]
2828

2929
assert REVIEWED_BASE_REVISION == "61589543bbbff546edbc51d34a07887982fa4ad6"
30-
assert len(candidate_ids) == len(set(candidate_ids)) == 399
30+
assert len(candidate_ids) == len(set(candidate_ids)) == 400
3131
assert reviewed_ids == sorted(reviewed_ids)
3232
assert set(reviewed_ids) == set(candidate_ids)
3333
assert all(record.rationale.strip() for record in OPERATION_ADMISSIONS)
3434
assert Counter(record.decision for record in OPERATION_ADMISSIONS) == {
35-
AdmissionDecision.KEEP: 239,
35+
AdmissionDecision.KEEP: 240,
3636
AdmissionDecision.NATIVE_ONLY: 56,
3737
AdmissionDecision.DROP: 104,
3838
}
@@ -46,7 +46,7 @@ def test_public_catalog_contains_only_admitted_atomic_operations() -> None:
4646
}
4747

4848
assert {tool.operation_id for tool in BUILTIN_TOOLS} == expected
49-
assert len(BUILTIN_TOOLS) == 239
49+
assert len(BUILTIN_TOOLS) == 240
5050

5151

5252
def test_catalog_construction_fails_closed_on_duplicate_candidates() -> None:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Tests for simplicial complex f-vector operation."""
2+
3+
from jacobian.math.topology._models import FVectorRequest
4+
from jacobian.math.topology._operations import compute_f_vector
5+
6+
7+
def test_triangle() -> None:
8+
result = compute_f_vector(
9+
FVectorRequest(
10+
complex={"vertices": ["v0", "v1", "v2"], "facets": [["v0", "v1", "v2"]]}
11+
)
12+
)
13+
assert result.f_vector == (3, 3, 1)
14+
assert result.euler_characteristic == 1
15+
assert result.dimension == 2
16+
17+
18+
def test_edge() -> None:
19+
result = compute_f_vector(
20+
FVectorRequest(complex={"vertices": ["v0", "v1"], "facets": [["v0", "v1"]]})
21+
)
22+
assert result.f_vector == (2, 1)
23+
assert result.euler_characteristic == 1
24+
assert result.dimension == 1
25+
26+
27+
def test_single_vertex() -> None:
28+
result = compute_f_vector(
29+
FVectorRequest(complex={"vertices": ["v0"], "facets": [["v0"]]})
30+
)
31+
assert result.f_vector == (1,)
32+
assert result.euler_characteristic == 1
33+
assert result.dimension == 0

0 commit comments

Comments
 (0)