feat(math): add finite topological spaces domain with interior, closure, boundary, Kolmogorov quotient, and continuity check (#1920) - #2037
Conversation
…re, boundary, Kolmogorov quotient, and continuity check Add the finite_topology_spaces domain implementing exact, bounded, deterministic finite topological space operations over immutable finite topological spaces represented by specialization preorders: - topology.finite.interior.compute: largest open set contained in a subset - topology.finite.closure.compute: smallest closed set containing a subset via specialization preorder up-set - topology.finite.boundary.compute: closure minus interior - topology.finite.kolmogorov_quotient.compute: T0 quotient identifying points with the same minimal open neighbourhood - topology.finite.continuity_check.compute: continuity check via specialization preorder monotonicity (x' <= x implies f(x') <= f(x)) On a finite set, every topology is Alexandrov, so the topology is equivalently represented by its specialization preorder. The FiniteTopologicalSpace value parses only well-formed spaces with reflexive preorders and in-range indices. Closes #1920
…ests - Import TOOLS and ADMISSIONS in catalog/builtins.py. - Update frozen admission baselines and add the finite_topology_spaces schema-snapshot fragment (5 operations). - Add 14 focused tests (interior, closure, boundary, Kolmogorov quotient, continuity check, validation).
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
| @model_validator(mode="after") | ||
| def require_well_formed(self) -> Self: | ||
| if len(self.preorder) != len(self.points): | ||
| raise ValueError("preorder must have one row per point") | ||
| for row in self.preorder: | ||
| for idx in row: | ||
| if not 0 <= idx < len(self.points): | ||
| raise ValueError("preorder index out of range") | ||
| for i in range(len(self.points)): | ||
| if i not in self.preorder[i]: | ||
| raise ValueError("preorder must be reflexive") | ||
| return self |
There was a problem hiding this comment.
🔴 Spaces that are not really spaces are accepted, producing wrong interior/closure answers
Incoming space descriptions are only checked for self-reference (require_well_formed at src/jacobian/math/finite_topology_spaces/values.py:30-41) and never for the chained-comparison rule they must obey, so inputs that do not describe any actual space are accepted and every interior, closure, boundary and continuity answer computed from them is silently wrong.
Impact: Users can receive confidently reported but mathematically incorrect results for malformed inputs instead of a clear rejection.
Missing transitivity check on the specialization preorder
The model documents preorder as a preorder (row i = the minimal open neighbourhood of point i), but the validator only enforces in-range indices and reflexivity. Transitivity (j in row[i] implies row[j] ⊆ row[i]) is required for the rows to be minimal open neighbourhoods of an Alexandrov topology.
Counterexample accepted today: points (a,b,c) with preorder=((0,), (0,1), (1,2)). Here 0 ∈ row[1] and 1 ∈ row[2] but 0 ∉ row[2]. interior (src/jacobian/math/finite_topology_spaces/operations.py:41-50) returns {2} for the subset {1,2}, yet in the topology generated by these sets the smallest open set containing 2 is {0,1,2}, so {2} is not open and the reported "interior" is not an open set. closure and boundary are wrong for the same reason, and continuous_check tests monotonicity against a relation that is not a preorder.
Sister domain src/jacobian/math/finite_topology/values.py:24-44 enforces the full topology axioms on its input, and AGENTS.md requires the request to encode the advertised mathematical domain, not only the JSON shape.
| @model_validator(mode="after") | |
| def require_well_formed(self) -> Self: | |
| if len(self.preorder) != len(self.points): | |
| raise ValueError("preorder must have one row per point") | |
| for row in self.preorder: | |
| for idx in row: | |
| if not 0 <= idx < len(self.points): | |
| raise ValueError("preorder index out of range") | |
| for i in range(len(self.points)): | |
| if i not in self.preorder[i]: | |
| raise ValueError("preorder must be reflexive") | |
| return self | |
| @model_validator(mode="after") | |
| def require_well_formed(self) -> Self: | |
| if len(self.preorder) != len(self.points): | |
| raise ValueError("preorder must have one row per point") | |
| for row in self.preorder: | |
| for idx in row: | |
| if not 0 <= idx < len(self.points): | |
| raise ValueError("preorder index out of range") | |
| for i in range(len(self.points)): | |
| if i not in self.preorder[i]: | |
| raise ValueError("preorder must be reflexive") | |
| for i, row in enumerate(self.preorder): | |
| down = set(row) | |
| for j in row: | |
| if not set(self.preorder[j]).issubset(down): | |
| raise ValueError("preorder must be transitive") | |
| return self |
Was this helpful? React with 👍 or 👎 to provide feedback.
| class SubsetRequest(StrictModel): | ||
| """Operate on a subset of points.""" | ||
|
|
||
| space: FiniteTopologicalSpace | ||
| subset: tuple[int, ...] = Field(default=()) |
There was a problem hiding this comment.
🔴 Asking for the closure or boundary of a point number that does not exist crashes the request
A request naming a point outside the space is accepted by the input model and then rejected deep inside the computation (raise ValueError at src/jacobian/math/finite_topology_spaces/operations.py:59-60), so an accepted request turns into a host error instead of a typed answer or a validation message.
Impact: Callers get an unhandled server-side failure rather than a clear rejection of their input.
Unvalidated subset indices in SubsetRequest
SubsetRequest (src/jacobian/math/finite_topology_spaces/_models.py:14-18) accepts any tuple[int, ...] for subset with no cross-field check against the space's point count. compute_closure / compute_boundary (src/jacobian/math/finite_topology_spaces/_operations.py:37-44) then call closure, which raises ValueError("subset index out of range"). AGENTS.md states: "A request the model accepts must return a typed domain result; mathematical inapplicability belongs in the request validator or the result, not in a raised backend exception."
The same unvalidated input behaves inconsistently for interior, which silently ignores out-of-range members (src/jacobian/math/finite_topology_spaces/operations.py:41-50), so topology.finite.interior.compute returns an answer while topology.finite.closure.compute fails on the identical request. The fix belongs in the request model: validate that every subset index lies in range(len(space.points)) (and reject duplicates), which also makes the in-kernel raise unreachable.
Prompt for agents
SubsetRequest in src/jacobian/math/finite_topology_spaces/_models.py accepts arbitrary integers in `subset` with no check against the accompanying space. compute_closure and compute_boundary then reach `closure()` in operations.py, which raises ValueError("subset index out of range"), turning an accepted request into a host exception; compute_interior instead silently ignores the same out-of-range indices, so the two operations disagree on identical input. Add a model_validator(mode="after") on SubsetRequest that requires every subset index to be within range(len(space.points)) (and consider rejecting duplicates so the subset is canonical), so the mathematical domain is enforced at the request boundary and the kernel raise becomes unreachable.
Was this helpful? React with 👍 or 👎 to provide feedback.
| nbhd_to_class: dict[tuple[int, ...], list[int]] = {} | ||
| for i, row in enumerate(space.preorder): | ||
| key = tuple(sorted(row)) | ||
| nbhd_to_class.setdefault(key, []).append(i) | ||
| classes = list(nbhd_to_class.values()) |
There was a problem hiding this comment.
🟡 Repeated entries in a point's neighbourhood list can split identical points into separate quotient classes
Points are grouped by their raw, unnormalised neighbourhood list (tuple(sorted(row)) at src/jacobian/math/finite_topology_spaces/operations.py:99), so two points with exactly the same neighbourhood can be put in different groups whenever one list repeats an entry, giving a quotient with too many points.
Impact: The T0 quotient can be reported with more points than it actually has, so the result is mathematically wrong.
Unnormalised preorder rows leak into the class key
FiniteTopologicalSpace (src/jacobian/math/finite_topology_spaces/values.py:30-41) does not require rows to be sorted or duplicate-free, unlike the sibling domain's FiniteTopology (src/jacobian/math/finite_topology/values.py:26-28), so preorder=((0,1,1),(0,1)) is accepted. Both points then have neighbourhood {0,1} and must be identified, but the keys (0,1,1) and (0,1) differ, producing two classes and a non-T0 "quotient". Using frozenset(row) as the key (or canonicalising rows in the validator) fixes it; note that unbounded duplicates also defeat the MAX_POINTS size bound on the input.
| nbhd_to_class: dict[tuple[int, ...], list[int]] = {} | |
| for i, row in enumerate(space.preorder): | |
| key = tuple(sorted(row)) | |
| nbhd_to_class.setdefault(key, []).append(i) | |
| classes = list(nbhd_to_class.values()) | |
| nbhd_to_class: dict[frozenset[int], list[int]] = {} | |
| for i, row in enumerate(space.preorder): | |
| key = frozenset(row) | |
| nbhd_to_class.setdefault(key, []).append(i) | |
| classes = list(nbhd_to_class.values()) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| _op( | ||
| "topology.finite.continuity_check.compute", | ||
| "Check whether a point map is continuous", | ||
| "Return whether a point map between finite topological spaces is " | ||
| "continuous. A map f: X -> Y is continuous iff x' <= x implies " | ||
| "f(x') <= f(x) in the specialization preorders.", | ||
| ContinuousCheckRequest, | ||
| ContinuousCheckResult, | ||
| compute_continuous_check, | ||
| "finite-topology", | ||
| "continuity", | ||
| "exact", | ||
| examples=( | ||
| example( | ||
| "identity_continuous", | ||
| "The identity map is continuous.", | ||
| { | ||
| "point_map": { | ||
| "source": _SPACE, | ||
| "target": _SPACE, | ||
| "point_map": [0, 1], | ||
| }, | ||
| }, | ||
| ), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
🔍 New domain overlaps heavily with the existing finite_topology domain
This domain duplicates concepts already owned by src/jacobian/math/finite_topology: topology.finite.continuity_check.compute covers the same mathematics as the existing topology.is_continuous.compute, and specialization_preorder/minimal_neighbourhoods restate topology.specialization_preorder.compute (src/jacobian/math/finite_topology/_tools.py:31,71). The two domains also use incompatible representations for the same object (open-set families with full topology-axiom validation in src/jacobian/math/finite_topology/values.py:15-44 vs. a preorder here), so agents can get two non-composable notions of a finite space. Worth confirming with the issue whether a second domain is intended rather than extending the existing one with interior/closure/boundary/quotient over FiniteTopology.
Was this helpful? React with 👍 or 👎 to provide feedback.
| __all__ = [ | ||
| "FiniteTopologicalMap", | ||
| "FiniteTopologicalSpace", | ||
| "boundary", | ||
| "closure", | ||
| "continuous_check", | ||
| "from_preorder", | ||
| "interior", | ||
| "kolmogorov_quotient", | ||
| "minimal_neighbourhoods", | ||
| "specialization_preorder", | ||
| ] |
There was a problem hiding this comment.
🔍 New domain is not registered in the root jacobian.math namespace
The package exposes a public __all__ and has an owner-local test_public_api.py, but finite_topology_spaces was not added to src/jacobian/math/__init__.py or to ROOT_MATH_DOMAINS in tests/math/public_api/test_namespace.py:18-43. from jacobian.math import finite_topology_spaces still works as a submodule import so the new test passes, but the cross-owner invariants (no private names, one canonical owner per public callable) never run against this domain. Confirm whether omission from the root namespace is deliberate.
Was this helpful? React with 👍 or 👎 to provide feedback.
morluto
left a comment
There was a problem hiding this comment.
Review verdict: blocked — the value accepts relations that are not preorders/topologies
FiniteTopologicalSpace.require_well_formed() checks only reflexivity. A preorder is reflexive and transitive. For example,
points = ("a", "b", "c")
preorder = ((0,), (0, 1), (1, 2))is accepted even though a <= b and b <= c but not a <= c. It therefore does not define the Alexandrov topology claimed by the type. The interior/closure/continuity kernels then operate on a relation for which the stated order-topology theorems do not apply. Transitivity must be a construction invariant.
There is also a convention contradiction. The implementation/example use rows as minimal open neighbourhoods and open sets as down-sets: in the sample, a <= b, U_b={a,b}, and closure({a})={a,b}. Thus the implemented relation satisfies x <= y iff y in closure({x}). The value documentation instead says x <= y iff x in closure({y}), which is the opposite convention. Either transpose the relation/kernels or document consistently that this is the reverse-specialization convention. Do not mix the two under one wire format.
Additional blockers at the boundary:
pointsare documented unique but never checked.- Rows need canonical set semantics (sorted, duplicate-free). Duplicate row entries currently survive
minimal_neighbourhoods()and can make equivalent points appear different inkolmogorov_quotient()because tuple keys preserve multiplicity. SubsetRequestvalidates neither range nor uniqueness.interior()does not validate the subset at all, sosubset=(999,)receives a normal empty answer rather than rejection;closure()rejects the same input.- The quotient tool promises a class map, the native kernel computes one, but
KolmogorovQuotientResultomits it and the adapter discards it. FiniteTopologicalMapis named/documented as an immutable continuous map, yet deliberately accepts noncontinuous maps socontinuous_checkcan inspect them. Rename it to a finite point map, or make continuity an invariant and remove the checker-shaped operation.
Once a genuine preorder and one explicit orientation convention are enforced, the interior, up-closure, boundary, monotonicity test, and T0 quotient formulas are coherent under the implementation's down-set convention.
Deep review summaryVerdict: REQUEST CHANGES — the value accepts relations that are not preorders or topologies, so every downstream result may be mathematically invalid.
points = ("a", "b", "c")
preorder = ((0,), (0, 1), (1, 2))is accepted although There is also an orientation contradiction. The implementation and examples use rows as minimal open neighbourhoods/down-sets, which corresponds to Additional boundary problems:
Once a genuine preorder and one explicit orientation convention are enforced, the actual formulas are coherent under the implementation's down-set convention. |
Closes #1920.
Summary
Add the
finite_topology_spacesdomain implementing exact, bounded, deterministic finite topological space operations over immutable finite topological spaces represented by specialization preorders. On a finite set, every topology is Alexandrov, so the topology is equivalently represented by its specialization preorder.Design and library choices
The domain uses an exact preorder kernel over immutable
FiniteTopologicalSpacevalues. No simplicial complex framework, homology solver, or general topology framework is introduced. Per the issue, the equivalence between finite topologies and preorders does not make the domain redundant — the public values still need open/closed-set semantics, continuous maps, and quotient operations.Representation (
values.py).FiniteTopologicalSpacerepresents a topology by its specialization preorder:x <= yiff x is in the closure of {y}. The value parses only well-formed spaces: reflexive preorders with in-range indices.FiniteTopologicalMapbinds a source, target, and complete point map.Operations (
operations.py). All functions are deterministic and complete for accepted values.interior.compute— largest open set contained in a subset (points whose minimal open neighbourhood is contained in the subset).closure.compute— smallest closed set containing a subset (up-set in the specialization preorder).boundary.compute— closure minus interior.kolmogorov_quotient.compute— T0 quotient identifying points with the same minimal open neighbourhood.continuity_check.compute— continuity check via specialization preorder monotonicity (x' <= ximpliesf(x') <= f(x)).Invariances verified by tests
Interior({a}) = {a}(open),Interior({b}) = {}(not open),Closure({a}) = {a,b}(up-set of a includes b),Boundary({a}) = {b}.Validation
make checklint + typecheck clean; 14 focused domain tests; all 1136tests/math,tests/catalog,tests/dispatchtests pass.finite_topology_spacesschema-snapshot fragment added (5 operations).Continue this on Linzumi