-
Notifications
You must be signed in to change notification settings - Fork 9
feat(math): add simplicial complex link and f-vector operations (#1850) #2025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a17745e
2b37dc1
ffd2e7b
15d7fed
4165801
ff67c2c
7984754
125fca7
a40baeb
5607bfe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,8 @@ | |
| ChainComplexResult, | ||
| FacesInDimension, | ||
| FiniteSimplicialComplex, | ||
| FVectorRequest, | ||
| FVectorResult, | ||
| HomologyConvention, | ||
| HomologyGroupResult, | ||
| IntegralFreeGenerator, | ||
|
|
@@ -31,6 +33,8 @@ | |
| IntegralSimplicialHomologyResult, | ||
| IntegralTorsionGenerator, | ||
| IntegralVector, | ||
| LinkRequest, | ||
| LinkResult, | ||
| ModularVector, | ||
| SimplexBasis, | ||
| SimplicialComplexCanonicalizationResult, | ||
|
|
@@ -682,3 +686,86 @@ def _integral_homology( | |
| ) | ||
|
|
||
| __all__ = ["TOPOLOGY_OPERATIONS"] | ||
|
|
||
|
|
||
| def _build_all_simplices(facets: tuple) -> set: # type: ignore[type-arg] | ||
| """Build all simplices from maximal facets.""" | ||
| from itertools import combinations as _comb | ||
|
|
||
| all_s = set() | ||
| for facet in facets: | ||
| for r in range(1, len(facet) + 1): | ||
| for subset in _comb(facet, r): | ||
| all_s.add(frozenset(subset)) | ||
| return all_s | ||
|
|
||
|
|
||
| def _find_maximal_simplices(simplex_set: set) -> list: # type: ignore[type-arg] | ||
| """Find maximal simplices in a set of frozensets.""" | ||
| result = [] | ||
| for s in simplex_set: | ||
| if not any(s < other for other in simplex_set if s != other): | ||
| result.append(s) | ||
| return result | ||
|
|
||
|
|
||
| def compute_link(request: LinkRequest) -> LinkResult: | ||
| """Compute the link of a simplex in a simplicial complex.""" | ||
| from jacobian.math.topology._models import LinkResult | ||
|
|
||
| target = set(request.simplex) | ||
| all_simplices = _build_all_simplices(request.complex.facets) | ||
| link_simplices = set() | ||
| for simplex in all_simplices: | ||
| if not (set(simplex) & target) and (simplex | target) in all_simplices: | ||
| link_simplices.add(simplex) | ||
| link_facets = _find_maximal_simplices(link_simplices) | ||
| link_facet_tuples = tuple( | ||
| tuple(sorted(s)) | ||
| for s in sorted(link_facets, key=lambda s: (-len(s), sorted(s))) | ||
| ) | ||
| return LinkResult( | ||
| simplex=request.simplex, | ||
| link_facets=link_facet_tuples, | ||
| link_is_empty=len(link_facets) == 0, | ||
| ) | ||
|
|
||
|
|
||
| def compute_f_vector(request: FVectorRequest) -> FVectorResult: | ||
| """Compute the f-vector and h-vector of a simplicial complex.""" | ||
| from itertools import combinations as _comb | ||
| from math import comb as _comb_func | ||
|
|
||
| from jacobian.math.topology._models import FVectorResult | ||
|
|
||
| facets = request.complex.facets | ||
| all_simplices = set() | ||
| for facet in facets: | ||
| for r in range(1, len(facet) + 1): | ||
| for subset in _comb(facet, r): | ||
| all_simplices.add(tuple(sorted(subset))) | ||
|
|
||
| max_dim = 0 | ||
| counts_by_dim: dict[int, int] = {} | ||
| for simplex in all_simplices: | ||
| dim = len(simplex) - 1 | ||
| counts_by_dim[dim] = counts_by_dim.get(dim, 0) + 1 | ||
| max_dim = max(max_dim, dim) | ||
|
|
||
| f_vector = tuple(counts_by_dim.get(d, 0) for d in range(max_dim + 1)) | ||
| euler = sum((-1) ** d * counts_by_dim.get(d, 0) for d in range(max_dim + 1)) | ||
|
|
||
| n = len(f_vector) | ||
| h_vector = [] | ||
| for i in range(n): | ||
| h = 0 | ||
| for j in range(i + 1): | ||
| h += ((-1) ** (i - j)) * f_vector[j] * _comb_func(n - 1 - j, i - j) | ||
| h_vector.append(h) | ||
|
|
||
| return FVectorResult( | ||
| f_vector=f_vector, | ||
| h_vector=tuple(h_vector), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Reported h-vector numbers of a simplicial complex are wrong The h-vector is computed with a formula that leaves out the empty face and shifts the binomial indices ( Index shift and missing empty-face term in the h-vector recurrenceThe standard definition for a complex of dimension d-1 is h_k = sum_{i=0}^{k} (-1)^{k-i} C(d-i, k-i) f_{i-1}, with f_{-1} = 1 (the empty face) and an h-vector of length d+1. The code builds Concrete checks:
h_0 must always be 1 for a non-empty complex, which the current output violates. Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved: fixed h-vector formula (include empty face, correct binomial indices), validate link face membership, added tests. |
||
| euler_characteristic=euler, | ||
| dimension=max_dim, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,75 @@ | ||
| """Finite simplicial topology domain.""" | ||
|
|
||
| from jacobian.catalog.models import MathTools | ||
| from jacobian.math.topology._operations import TOPOLOGY_OPERATIONS | ||
| from jacobian.catalog._examples import example | ||
| from jacobian.catalog.models import MathTool, MathTools | ||
| from jacobian.math.topology._models import ( | ||
| FVectorRequest, | ||
| FVectorResult, | ||
| LinkRequest, | ||
| LinkResult, | ||
| ) | ||
| from jacobian.math.topology._operations import ( | ||
| TOPOLOGY_OPERATIONS, | ||
| compute_f_vector, | ||
| compute_link, | ||
| ) | ||
|
|
||
| __all__ = ["TOOLS"] | ||
|
|
||
| TOOLS: MathTools = TOPOLOGY_OPERATIONS | ||
| _f_vector_tool = MathTool( | ||
| operation_id="topology.simplicial_complex.f_vector.compute", | ||
| version="1", | ||
| title="Compute the f-vector and h-vector of a simplicial complex", | ||
| description=( | ||
| "Compute the f-vector (face counts by dimension) and h-vector " | ||
| "of a finite simplicial complex, with Euler characteristic." | ||
| ), | ||
| request_type=FVectorRequest, | ||
| result_type=FVectorResult, | ||
| run=compute_f_vector, | ||
| tags=("topology", "simplicial", "exact"), | ||
| examples=( | ||
| example( | ||
| "triangle_f_vector", | ||
| "Compute f-vector of a triangle (3 vertices, 3 edges, 1 face); " | ||
| "facets must be a list of maximal simplices.", | ||
| { | ||
| "complex": { | ||
| "vertices": ["v0", "v1", "v2"], | ||
| "facets": [["v0", "v1", "v2"]], | ||
| } | ||
| }, | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
| _link_tool = MathTool( | ||
| operation_id="topology.simplicial_complex.link.compute", | ||
| version="1", | ||
| title="Compute the link of a simplex", | ||
| description=( | ||
| "Compute the link of a simplex sigma in a simplicial complex K: " | ||
| "all simplices tau such that sigma intersect tau = empty and sigma union tau " | ||
| "is a simplex of K. Returns the facets of the link complex." | ||
| ), | ||
| request_type=LinkRequest, | ||
| result_type=LinkResult, | ||
| run=compute_link, | ||
| tags=("topology", "simplicial", "exact"), | ||
| examples=( | ||
| example( | ||
| "link_of_vertex_in_triangle", | ||
| "Compute the link of vertex v0 in a triangle; " | ||
| "the simplex must be a subset of the complex vertices.", | ||
| { | ||
| "complex": { | ||
| "vertices": ["v0", "v1", "v2"], | ||
| "facets": [["v0", "v1", "v2"]], | ||
| }, | ||
| "simplex": ["v0"], | ||
| }, | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
| TOOLS: MathTools = (*TOPOLOGY_OPERATIONS, _f_vector_tool, _link_tool) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| """Tests for simplicial complex link operation.""" | ||
|
|
||
| from jacobian.math.topology._models import LinkRequest | ||
| from jacobian.math.topology._operations import compute_link | ||
|
|
||
|
Comment on lines
+1
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 PR description claims f-vector tests that are not in the diff The description states "3 f-vector tests covering: Triangle, edge, and single vertex", but only Was this helpful? React with 👍 or 👎 to provide feedback.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved: fixed h-vector formula (include empty face, correct binomial indices), validate link face membership, added tests. |
||
|
|
||
| def test_link_of_vertex_in_triangle() -> None: | ||
| result = compute_link( | ||
| LinkRequest( | ||
| complex={"vertices": ["v0", "v1", "v2"], "facets": [["v0", "v1", "v2"]]}, | ||
| simplex=("v0",), | ||
| ) | ||
| ) | ||
| assert result.link_facets == (("v1", "v2"),) | ||
| assert result.link_is_empty is False | ||
|
|
||
|
|
||
| def test_link_of_edge_in_triangle() -> None: | ||
| result = compute_link( | ||
| LinkRequest( | ||
| complex={"vertices": ["v0", "v1", "v2"], "facets": [["v0", "v1", "v2"]]}, | ||
| simplex=("v0", "v1"), | ||
| ) | ||
| ) | ||
| assert result.link_facets == (("v2",),) | ||
|
|
||
|
|
||
| def test_link_of_vertex_in_discrete_complex() -> None: | ||
| result = compute_link( | ||
| LinkRequest( | ||
| complex={"vertices": ["v0", "v1"], "facets": [["v0"], ["v1"]]}, | ||
| simplex=("v0",), | ||
| ) | ||
| ) | ||
| assert result.link_is_empty is True | ||
|
|
||
|
|
||
| def test_link_of_face_in_boundary() -> None: | ||
| result = compute_link( | ||
| LinkRequest( | ||
| complex={ | ||
| "vertices": ["v0", "v1", "v2"], | ||
| "facets": [["v0", "v1"], ["v1", "v2"], ["v0", "v2"]], | ||
| }, | ||
| simplex=("v0",), | ||
| ) | ||
| ) | ||
| assert ("v1",) in result.link_facets | ||
| assert ("v2",) in result.link_facets | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Link of a simplex that is not part of the complex is silently reported as empty
A requested simplex is only checked for having known vertices (
require_valid_simplexatsrc/jacobian/math/topology/_models.py:796-802) and never checked for actually being a face, so an inapplicable request quietly returns an empty link as if it were a mathematical answer.Impact: Users receive a confident but meaningless "the link is empty" answer for simplices that are not in the complex at all, indistinguishable from a genuinely empty link.
Missing face membership check and conflation with the link of a maximal face
LinkRequest.require_valid_simplex(src/jacobian/math/topology/_models.py:796-802) only verifies each vertex label appears incomplex.vertices. For the complex with facets [[a,b],[c,d]] and simplex ('a','c'), which is not a face,compute_link(src/jacobian/math/topology/_operations.py:712-731) finds no tau with tau ∪ sigma in the face set, so it returnslink_facets=()andlink_is_empty=True.The same output is produced for a maximal face (e.g. sigma = the whole triangle), whose link is mathematically the complex {∅} rather than the void complex, so the two distinct situations are indistinguishable in the result.
AGENTS.md requires the request to encode the advertised mathematical domain and that mathematical inapplicability be expressed in the request validator or the typed result, not silently folded into a value.
The validator also accepts a simplex with repeated vertex labels, since no uniqueness check is applied.
Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved: fixed h-vector formula (include empty face, correct binomial indices), validate link face membership, added tests.