Skip to content

feat(math): add finite topological spaces domain with interior, closure, boundary, Kolmogorov quotient, and continuity check (#1920) - #2037

Open
morluto wants to merge 2 commits into
mainfrom
agent/finite-topological-spaces-1920
Open

feat(math): add finite topological spaces domain with interior, closure, boundary, Kolmogorov quotient, and continuity check (#1920)#2037
morluto wants to merge 2 commits into
mainfrom
agent/finite-topological-spaces-1920

Conversation

@morluto

@morluto morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Closes #1920.

Summary

Add the finite_topology_spaces domain 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 FiniteTopologicalSpace values. 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). FiniteTopologicalSpace represents a topology by its specialization preorder: x <= y iff x is in the closure of {y}. The value parses only well-formed spaces: reflexive preorders with in-range indices. FiniteTopologicalMap binds 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' <= x implies f(x') <= f(x)).

Invariances verified by tests

  • The Sierpinski space ({a,b} with a <= b) has Interior({a}) = {a} (open), Interior({b}) = {} (not open), Closure({a}) = {a,b} (up-set of a includes b), Boundary({a}) = {b}.
  • The Sierpinski space is T0 (Kolmogorov quotient has 2 points); the identity map is continuous; the swap map is not.
  • Non-reflexive preorders and out-of-range indices each produce named obstructions.

Validation

  • make check lint + typecheck clean; 14 focused domain tests; all 1136 tests/math, tests/catalog, tests/dispatch tests pass.
  • Frozen admission baselines updated and the finite_topology_spaces schema-snapshot fragment added (5 operations).

Continue this on Linzumi


Open in Devin Review

…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).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Open in Devin Review

Comment on lines +30 to +41
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
@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
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +14 to +18
class SubsetRequest(StrictModel):
"""Operate on a subset of points."""

space: FiniteTopologicalSpace
subset: tuple[int, ...] = Field(default=())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +97 to +101
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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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())
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +140 to +165
_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],
},
},
),
),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +18 to +29
__all__ = [
"FiniteTopologicalMap",
"FiniteTopologicalSpace",
"boundary",
"closure",
"continuous_check",
"from_preorder",
"interior",
"kolmogorov_quotient",
"minimal_neighbourhoods",
"specialization_preorder",
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@morluto morluto left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. points are documented unique but never checked.
  2. Rows need canonical set semantics (sorted, duplicate-free). Duplicate row entries currently survive minimal_neighbourhoods() and can make equivalent points appear different in kolmogorov_quotient() because tuple keys preserve multiplicity.
  3. SubsetRequest validates neither range nor uniqueness. interior() does not validate the subset at all, so subset=(999,) receives a normal empty answer rather than rejection; closure() rejects the same input.
  4. The quotient tool promises a class map, the native kernel computes one, but KolmogorovQuotientResult omits it and the adapter discards it.
  5. FiniteTopologicalMap is named/documented as an immutable continuous map, yet deliberately accepts noncontinuous maps so continuous_check can 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.

morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Deep review summary

Verdict: REQUEST CHANGES — the value accepts relations that are not preorders or topologies, so every downstream result may be mathematically invalid.

FiniteTopologicalSpace checks reflexivity but not transitivity. For example,

points = ("a", "b", "c")
preorder = ((0,), (0, 1), (1, 2))

is accepted although a ≤ b and b ≤ c but not a ≤ c. It therefore does not define the Alexandrov topology claimed by the type. The interior, closure, boundary, quotient, and continuity formulas then apply order-topology theorems to a relation that is not an order. Make transitivity a construction invariant.

There is also an orientation contradiction. The implementation and examples use rows as minimal open neighbourhoods/down-sets, which corresponds to x ≤ y iff y ∈ closure({x}). The value documentation states the opposite convention, x ≤ y iff x ∈ closure({y}). Either transpose the representation and kernels or document one reverse-specialization convention consistently.

Additional boundary problems:

  • point labels are documented unique but not checked;
  • preorder rows need sorted, duplicate-free set semantics;
  • subset requests validate neither range nor uniqueness: interior silently ignores a foreign index while closure raises on the same request;
  • the quotient description promises a class map, the kernel computes it, but the result model discards it;
  • FiniteTopologicalMap is documented as continuous even though noncontinuous maps are intentionally accepted for the checker. Rename it to a point map or make continuity an invariant.

Once a genuine preorder and one explicit orientation convention are enforced, the actual formulas are coherent under the implementation's down-set convention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Finite topological spaces] Add exact specialization preorders, open-set transforms, continuous maps, beat-point cores, and order-complex operations

1 participant