Skip to content

feat(math): add universal algebra domain with term evaluation, equation profiles, subalgebras, congruences, and quotients (#1912) - #2034

Open
morluto wants to merge 2 commits into
mainfrom
agent/finite-universal-algebra-1912
Open

feat(math): add universal algebra domain with term evaluation, equation profiles, subalgebras, congruences, and quotients (#1912)#2034
morluto wants to merge 2 commits into
mainfrom
agent/finite-universal-algebra-1912

Conversation

@morluto

@morluto morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Closes #1912.

Summary

Add the universal_algebra domain implementing exact, bounded, deterministic universal-algebra operations over immutable single-sorted finite algebra values. This is the general finite-algebra layer above #1686's one-binary-operation magma tools and below many domain-specific algebra families.

Design and library choices

The domain uses an exact enumeration kernel over immutable finite-algebra values. No theorem prover, model finder, variety classifier, or equational presentation/free-algebra constructor is introduced. Per the issue, this does not propose a general first-order logic language, many-sorted framework, or arbitrary callback system.

Representation (values.py). FiniteAlgebra binds a finite unique carrier, a FiniteSignature (tuple of OperationSymbols with unique IDs and bounded arities), and complete operation tables (one per operation, indexed by the dense Cartesian product of input positions in row-major order). The value parses only well-formed algebras: unique carrier labels, correct table cell counts, and in-range table outputs. A FlatTerm is a closed source-bound AST (flat node list with parent/child indices) for finite-algebra terms.

Operations (operations.py). All functions are deterministic and complete for accepted values.

  • term.evaluate.compute — exact bottom-up term evaluation under a complete variable assignment.
  • equation.profile.compute — evaluate s = t over all assignments. Return HOLDS with satisfying count, or FAILS with first counterassignment and exact left/right values. Generalizes magma identity calculation to an arbitrary finite signature.
  • subalgebra.generated.compute — least subalgebra containing a generating set by finite closure under all basic operations and nullary constants.
  • congruence.check.compute — exact compatibility check of a carrier partition against all basic operations.
  • quotient.compute — quotient algebra A/theta induced by a congruence with block-wise operations.

Invariances verified by tests

  • AND(0, 1) = 0 and AND(1, 1) = 1 in the 2-element Boolean algebra.
  • AND(x, x) = x holds (idempotence) with 2 satisfying assignments; AND(x, y) = x fails.
  • The generated subalgebra of {0} is {0} (closed); of {0, 1} is {0, 1}.
  • The universal partition {{0, 1}} and equality partition {{0}, {1}} are both congruences.
  • The quotient by the universal congruence is a one-element algebra.
  • Duplicate carrier, wrong table size, and out-of-range outputs 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 (KEEP 239→244, candidates 399→404) and the universal_algebra schema-snapshot fragment added (5 operations).

Continue this on Linzumi


Open in Devin Review

…on profiles, subalgebras, congruences, and quotients

Add the universal_algebra domain implementing exact, bounded, deterministic
universal-algebra operations over immutable finite algebra values:

- universal_algebra.term.evaluate.compute: exact bottom-up term evaluation
  over a finite algebra with complete operation tables
- universal_algebra.equation.profile.compute: evaluate s = t over all
  assignments, returning HOLDS with satisfying count or FAILS with first
  counterassignment and exact left/right values
- universal_algebra.subalgebra.generated.compute: least subalgebra
  containing a generating set by finite closure under all basic operations
  and nullary constants
- universal_algebra.congruence.check.compute: exact compatibility check of
  a carrier partition against all basic operations
- universal_algebra.quotient.compute: quotient algebra induced by a
  congruence with block-wise operations

The FiniteAlgebra value parses only well-formed single-sorted finite
algebras with complete operation tables. Operation and argument axes are
exact and ordered. The domain does not propose a theorem prover, model
finder, variety classifier, or equational presentation/free-algebra
constructor.

Closes #1912
- Import TOOLS and ADMISSIONS in catalog/builtins.py.
- Update frozen admission baselines (KEEP 239->244, candidates 399->404)
  and add the universal_algebra schema-snapshot fragment (5 operations).
- Add 14 focused tests (term evaluation, equation profile, generated
  subalgebra, congruence check, quotient, 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 11 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +188 to +193
for args in iproduct(range(block_count), repeat=symbol.arity):
cell_index = 0
for arg in args:
cell_index = cell_index * n + arg
output = algebra.tables[op_idx][cell_index]
table.append(block_of[output])

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.

🔴 Quotient algebra returns wrong operation tables

Quotient operation tables are built by feeding block numbers straight into the original element table (algebra.tables[op_idx][cell_index] at src/jacobian/math/universal_algebra/operations.py:188-193) instead of a representative element of each block, so the returned quotient is mathematically wrong whenever blocks and elements do not coincide.
Impact: Users get incorrect quotient algebra tables for most non-trivial partitions, silently and with no error.

Block indices used as carrier indices when addressing the original table

For an operation of arity r, the code enumerates args over range(block_count) but computes cell_index in base n = len(carrier) and then reads the original table at that index. The original table is indexed by carrier elements, not blocks, so this only produces correct results in the accidental cases block_count == n (the identity partition, where block i happens to be {i} in order) or block_count == 1 with the first block containing element 0 — which are exactly the two cases covered by the tests at tests/math/universal_algebra/test_universal_algebra.py:183-188.

Example: carrier size 3, partition ((0,1),(2,)), binary op. The quotient table for (B0,B1) should be block_of(f(rep(B0), rep(B1))) = block_of(f(0,2)), i.e. original cell 0*3+2 = 2; the code instead reads cell 0*3+1 = 1, i.e. f(0,1).

The correct construction picks a representative per block (e.g. min(block)), computes cell_index from the representatives in base n, and maps the output through block_of.

Suggested change
for args in iproduct(range(block_count), repeat=symbol.arity):
cell_index = 0
for arg in args:
cell_index = cell_index * n + arg
output = algebra.tables[op_idx][cell_index]
table.append(block_of[output])
representatives = [min(block) for block in partition]
for args in iproduct(range(block_count), repeat=symbol.arity):
cell_index = 0
for arg in args:
cell_index = cell_index * n + representatives[arg]
output = algebra.tables[op_idx][cell_index]
table.append(block_of[output])
Open in Devin Review

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

Comment on lines +57 to +61
class SubalgebraRequest(StrictModel):
"""Compute the least subalgebra containing the generating set."""

algebra: FiniteAlgebra
generators: 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.

🔴 Subalgebra generators outside the carrier crash the operation

Generating elements are never checked against the algebra size before being used to look up operation results (algebra.tables[op_idx][cell_index] at src/jacobian/math/universal_algebra/operations.py:109-113), so a request naming a non-existent element aborts with an internal error instead of a typed result.
Impact: A validly-shaped subalgebra request with an out-of-range generator raises a host exception rather than returning a mathematical answer or a validation error.

Missing request validation on `SubalgebraRequest.generators`

SubalgebraRequest (src/jacobian/math/universal_algebra/_models.py:57-61) accepts any tuple of ints; EvaluateRequest does validate its assignment against the carrier range (src/jacobian/math/universal_algebra/_models.py:20-25) and CongruenceRequest validates partition elements (src/jacobian/math/universal_algebra/_models.py:78-89), so this omission is inconsistent.

With carrier=("0","1") and generators=(5,), the closure loop computes cell_index = 5*2+5 = 15 for a binary operation whose table has 4 cells, raising IndexError. AGENTS.md requires that an accepted request return a typed domain result, with mathematical inapplicability handled in the request validator.

Suggested change
class SubalgebraRequest(StrictModel):
"""Compute the least subalgebra containing the generating set."""
algebra: FiniteAlgebra
generators: tuple[int, ...] = Field(default=())
class SubalgebraRequest(StrictModel):
"""Compute the least subalgebra containing the generating set."""
algebra: FiniteAlgebra
generators: tuple[int, ...] = Field(default=())
@model_validator(mode="after")
def require_valid_generators(self) -> Self:
n = len(self.algebra.carrier)
if any(not 0 <= g < n for g in self.generators):
raise ValueError("generator out of carrier range")
return self
Open in Devin Review

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

Comment on lines +99 to +103
class QuotientRequest(StrictModel):
"""Compute the quotient algebra A/theta."""

algebra: FiniteAlgebra
partition: tuple[tuple[int, ...], ...]

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.

🔴 Quotient requests with malformed partitions crash instead of returning a result

The partition supplied for a quotient is accepted without any range or disjointness check (partition: tuple[tuple[int, ...], ...] at src/jacobian/math/universal_algebra/_models.py:99-103), so a partition mentioning a non-existent element can abort the call with an internal lookup error.
Impact: A well-formed-looking quotient request can fail with a host exception instead of a mathematical result or a clear validation message.

Cover check in `congruence_check` can be satisfied by out-of-range elements

CongruenceRequest validates element range and block disjointness (src/jacobian/math/universal_algebra/_models.py:78-89), but QuotientRequest does not. quotient delegates to congruence_check, whose only structural guard is len(block_of) != n (src/jacobian/math/universal_algebra/operations.py:139-140). With carrier=("0","1") and partition=((0, 5),), block_of = {0: 0, 5: 0} has size 2 == n, so the guard passes, and the compatibility loop then evaluates block_of[1], raising KeyError. Adding the same validator used by CongruenceRequest (and/or making congruence_check verify the keys are exactly range(n)) fixes it.

Suggested change
class QuotientRequest(StrictModel):
"""Compute the quotient algebra A/theta."""
algebra: FiniteAlgebra
partition: tuple[tuple[int, ...], ...]
class QuotientRequest(StrictModel):
"""Compute the quotient algebra A/theta."""
algebra: FiniteAlgebra
partition: tuple[tuple[int, ...], ...]
@model_validator(mode="after")
def require_partition_covers_carrier(self) -> Self:
n = len(self.algebra.carrier)
seen: set[int] = set()
for block in self.partition:
for elem in block:
if not 0 <= elem < n:
raise ValueError("partition element out of carrier range")
if elem in seen:
raise ValueError("partition blocks must be disjoint")
seen.add(elem)
return self
Open in Devin Review

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

Comment on lines +87 to +91
@model_validator(mode="after")
def require_valid_root(self) -> Self:
if self.root >= len(self.nodes):
raise ValueError("root index out of range")
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.

🔴 Malformed terms make evaluation crash with an internal error

Term node references and node kinds are never checked when a term is parsed (only the root index is checked, at src/jacobian/math/universal_algebra/values.py:87-91), so a request with a child index out of range, a self-referencing node, an unknown kind, or a variable not covered by the assignment aborts with an internal error.
Impact: Accepted-looking term evaluation and equation requests can fail as host errors rather than returning a mathematical value or a validation message.

Unvalidated flat-term structure and assignment completeness

FlatTerm validates only root < len(nodes). Consequences in evaluate_term (src/jacobian/math/universal_algebra/operations.py:29-52):

  • A child index >= len(nodes) raises IndexError at term.nodes[index].
  • A node whose children include its own index (or any cycle) recurses forever, raising RecursionError.
  • Term.kind is a free-form str (src/jacobian/math/universal_algebra/values.py:74), so kind="foo" reaches the unknown node kind ValueError.
  • EvaluateRequest validates assignment ranges but not completeness, so a term using variable_id=3 with assignment=(0,) raises incomplete assignment; likewise equation_profile builds assignments only for range(variable_count) (src/jacobian/math/universal_algebra/operations.py:68-70), so a term referencing a higher variable index raises.

AGENTS.md requires the request model to encode the advertised mathematical domain (completeness, well-formedness) so that an accepted request always returns a typed result. Fixes: constrain kind to a literal, validate every child index against len(nodes) and acyclicity/topological ordering plus arity in FlatTerm, and validate assignment/variable coverage in EvaluateRequest and EquationProfileRequest.

Prompt for agents
FlatTerm/Term in src/jacobian/math/universal_algebra/values.py accept structurally invalid terms: `kind` is an unconstrained str, child indices are not checked against the node list, cycles are permitted, and children counts are not checked against operation arity. evaluate_term in operations.py then raises IndexError, RecursionError or ValueError for such inputs. Additionally EvaluateRequest (_models.py) validates assignment ranges but not that every variable referenced by the term has an assigned value, and EquationProfileRequest does not check that all term variable ids are < variable_count, so evaluate_term raises 'incomplete assignment'. Per AGENTS.md every accepted request must return a typed domain result, so these conditions must be rejected by the request/value validators (constrain kind to a Literal, validate child indices in range and acyclic/topologically ordered, and validate variable coverage).
Open in Devin Review

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

Comment on lines +143 to +148
for op_idx, symbol in enumerate(algebra.operations):
if symbol.arity == 0:
continue
for x in iproduct(range(n), repeat=symbol.arity):
for y in iproduct(range(n), repeat=symbol.arity):
if all(block_of[x[j]] == block_of[y[j]] for j in range(symbol.arity)):

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.

🔴 Congruence and quotient checks can run for effectively unbounded time

The congruence compatibility test loops over every pair of argument tuples (iproduct(range(n), repeat=symbol.arity) nested twice at src/jacobian/math/universal_algebra/operations.py:146-147), which for the largest accepted algebra is on the order of a trillion iterations, so the call never finishes.
Impact: A request within the documented limits can hang the server indefinitely.

n^(2r) enumeration with n up to 32 and r up to 4

MAX_CARRIER_SIZE = 32 and MAX_ARITY = 4 (src/jacobian/math/universal_algebra/values.py:18-20) permit n^(2*arity) = 32^8 ≈ 1.1e12 iterations per operation, times up to 16 operations. quotient runs the same check first (src/jacobian/math/universal_algebra/operations.py:171).

The compatibility check can be done in n^arity * arity work by, for each argument tuple, comparing against tuples where one coordinate is replaced by another element of the same block (or simply by iterating over block-index tuples and their representatives). Additionally, AGENTS.md requires an explicit, tested work budget: either derive a bound and reject requests above it, or reduce the enumeration.

The same class of problem affects EquationProfileRequest.variable_count, which is only Field(ge=1) (src/jacobian/math/universal_algebra/_models.py:40); equation_profile enumerates n ** variable_count assignments, so variable_count=20 with n=32 is unbounded work.

Prompt for agents
congruence_check in src/jacobian/math/universal_algebra/operations.py enumerates all pairs (x, y) of argument tuples, i.e. n^(2*arity) iterations, with n up to MAX_CARRIER_SIZE=32 and arity up to MAX_ARITY=4 (values.py). That is ~1e12 iterations per operation, so an accepted request can hang. quotient() invokes the same check. Rework the compatibility test to a bounded form (for each argument tuple, vary one coordinate over its block, or iterate over block-index tuples with representatives), and/or add an explicit named work budget validated on the request. Separately, EquationProfileRequest.variable_count in _models.py has no upper bound while equation_profile enumerates n**variable_count assignments; add a bounded budget there too.
Open in Devin Review

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

Comment on lines +95 to +98
for symbol in algebra.operations:
if symbol.arity == 0:
for output in algebra.tables[algebra.operations.index(symbol)]:
carrier_set.add(output)

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.

🟡 Nullary constants are looked up by symbol equality, which can pick the wrong table

When seeding the subalgebra with constants, the operation's table is located by searching for an equal operation symbol (algebra.operations.index(symbol) at src/jacobian/math/universal_algebra/operations.py:97), so two symbols with the same name and arity make the wrong constant be used.
Impact: For algebras with repeated operation symbols, the generated subalgebra can contain wrong elements.

`list.index` matches by value, not position

OperationSymbol is a pydantic model compared by field values, and FiniteAlgebra does not require unique operation_ids (src/jacobian/math/universal_algebra/values.py:44-61), so two distinct nullary symbols with the same id/arity but different tables collapse to the first index. The enclosing loop should use enumerate(algebra.operations) like the closure loop at src/jacobian/math/universal_algebra/operations.py:104.

Suggested change
for symbol in algebra.operations:
if symbol.arity == 0:
for output in algebra.tables[algebra.operations.index(symbol)]:
carrier_set.add(output)
for op_idx, symbol in enumerate(algebra.operations):
if symbol.arity == 0:
for output in algebra.tables[op_idx]:
carrier_set.add(output)
Open in Devin Review

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

Comment on lines +64 to +69
def compute_congruence(request: CongruenceRequest) -> CongruenceResult:
result = congruence_check(request.algebra, request.partition)
return CongruenceResult(
is_congruence=result["is_congruence"], # type: ignore[arg-type]
obstruction=result.get("obstruction"), # type: ignore[arg-type]
)

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.

🔍 Congruence obstruction details are computed but dropped at the wire boundary

congruence_check returns rich obstruction data (operation, x, y) on failure, but CongruenceResult only carries is_congruence and an obstruction string, so compute_congruence discards the witness. The declared tool description advertises a compatibility check; if the witness is meant to be user-visible (as the equation profile exposes its counterassignment), the result model should include it. Also note quotient turns a non-congruence partition into a raised ValueError rather than a typed result, which is a different pattern from congruence_check returning an obstruction.

Open in Devin Review

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

Comment on lines +106 to +111
class QuotientResult(StrictModel):
"""The quotient algebra carrier, operations, and tables."""

carrier: tuple[str, ...]
operations: tuple[tuple[str, int], ...]
tables: tuple[tuple[int, ...], ...]

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.

🔍 Quotient result does not carry the block membership map or validate table sizes

QuotientResult reports carrier labels B0..Bk, the original (operation_id, arity) pairs and flattened tables, but nothing tells the caller which original elements belong to which block, so the quotient homomorphism is not recoverable from the result. Additionally the tables are indexed in base block_count while FiniteAlgebra would validate them in base len(carrier); the result is a loose tuple-of-tuples with no cell-count validation, so a caller cannot round-trip it into a FiniteAlgebra without re-deriving the indexing convention.

Open in Devin Review

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

Comment on lines +44 to +61
@model_validator(mode="after")
def require_well_formed(self) -> Self:
if len(self.carrier) > MAX_CARRIER_SIZE:
raise ValueError("carrier size exceeds the bounded budget")
if len(set(self.carrier)) != len(self.carrier):
raise ValueError("carrier labels must be unique")
if len(self.tables) != len(self.operations):
raise ValueError("tables must have one entry per operation symbol")
for symbol, table in zip(self.operations, self.tables, strict=True):
expected_cells = len(self.carrier) ** symbol.arity
if len(table) != expected_cells:
raise ValueError(
f"operation {symbol.operation_id} table has wrong cell count"
)
for output in table:
if not 0 <= output < len(self.carrier):
raise ValueError("table output out of carrier range")
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.

🔍 Signature does not enforce unique operation ids despite the stated contract

The PR description says FiniteSignature carries "unique IDs", but the implemented FiniteAlgebra validator only checks carrier uniqueness, table counts and output ranges — duplicate operation_id values are accepted. This is what makes the operations.index(symbol) lookup ambiguous and also means the quotient result can report two identically named operations.

Open in Devin Review

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

Comment on lines +182 to +188
class TestQuotient:
def test_trivial_quotient(self) -> None:
result = compute_quotient(
QuotientRequest(algebra=_boolean_algebra(), partition=((0, 1),))
)
assert result.carrier == ("B0",)
assert len(result.operations) == 2

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.

🔍 Tests cover only degenerate quotient/congruence cases

All quotient and congruence tests use a 2-element algebra with either the universal partition (1 block) or the identity partition (2 blocks, in order). Both are exactly the cases where the block-index-as-element table indexing coincidentally produces correct output, which is why the incorrect quotient table construction is not detected. A 3-element algebra with a 2-block partition (e.g. Z/3 with blocks {0,1},{2} or a genuine congruence on a 4-element algebra) would be the defining-invariant test: quotient tables must agree with block_of(f(reps)) for every block tuple.

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 — quotient operations are indexed by block IDs as though they were carrier elements

1. quotient() computes the wrong operation table

For quotient arguments (B_i1, ..., B_ir), the original operation must be evaluated on one representative from each block. The code instead feeds the block indices themselves into the original dense table:

cell_index = cell_index * n + arg

A minimal counterexample is a four-element algebra with unary identity f(x)=x and the congruence ((0,1),(2,3)). The quotient operation must be identity on two blocks, table (0,1). This code evaluates original elements 0 and 1, both in block 0, and returns (0,0).

Choose a deterministic representative partition[arg][0] for every quotient argument, evaluate the original table at those carrier indices, then map the result through block_of. Add a quotient well-definedness/reconstruction test independent of block ordering.

2. The “partition” boundary admits empty blocks and QuotientRequest has no partition validation

CongruenceRequest checks disjointness/range but not coverage despite its validator name, and it never rejects empty blocks. ((0,1), ()) covers a two-element carrier, so congruence_check() returns true and quotient() creates a spurious empty quotient class. A partition must have nonempty, pairwise-disjoint blocks whose union is the carrier. Reuse one exact partition value/validator for both check and quotient.

3. FlatTerm is not a safe term representation

It validates only the root. Child indices may be negative/out of range; nodes may self-reference or form cycles; arbitrary kind/field combinations are accepted. A one-node application whose child is itself recurses until RecursionError. Validate the complete reachable graph as an acyclic, well-founded term DAG (or require children to precede parents), validate node variants structurally, and bound node count/depth.

EvaluateRequest also does not enforce its claimed complete assignment: extras are ignored and missing variables fail only during execution. EquationProfileRequest does not require term variable IDs below variable_count.

4. The advertised bounded completeness is not bounded

variable_count has no upper/work bound, so equation profiling accepts |A|^k enumeration for arbitrary k. Congruence checking accepts up to 32^(2*4) argument pairs (over one trillion per operation). Bounds must constrain the actual derived work, not only carrier/arity independently.

5. Generated-subalgebra metadata is wrong

generators are not range/uniqueness validated, and is_closed compares final set size with the raw tuple length. Duplicate generators can make a closed set report false; an empty generator tuple with nullary constants always reports true even though constants were added. Define whether this field means “the input set was already closed” or “the returned set is closed”, then compute and validate that exact predicate.

The basic table encoding and term-evaluation indexing are sound once inputs are well-founded. The quotient bug is independently merge-blocking.

morluto commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Deep review summary

Verdict: REQUEST CHANGES — quotient tables are mathematically wrong, and several accepted inputs can crash or exceed any defensible work bound.

1. quotient() indexes the original table with block IDs

A quotient operation must evaluate the original operation on one representative from each block, then map the output back to a block. The code feeds block numbers directly into the original carrier table.

For a four-element algebra with unary identity and partition ((0,1),(2,3)), the quotient identity table must be (0,1). The implementation evaluates original elements 0 and 1, both in block 0, and returns (0,0).

Choose a deterministic representative per block, evaluate at those carrier indices, and then apply block_of. Add tests independent of block ordering.

2. The partition boundary is incomplete

CongruenceRequest permits empty blocks and does not require exact carrier coverage. QuotientRequest has no equivalent validator at all. Inputs such as ((0,1), ()) create a spurious empty quotient class; malformed out-of-range partitions can pass the size check and later raise KeyError.

Use one exact partition value requiring nonempty, pairwise-disjoint blocks whose union is exactly the carrier.

3. FlatTerm is not a safe term representation

Only the root is checked. Child indices may be invalid, terms may self-reference or cycle, kind is unconstrained, and field combinations are not variant-checked. Accepted inputs can raise IndexError, RecursionError, or late ValueError. Validate a bounded, acyclic, well-founded term DAG and enforce variable/assignment coverage in both evaluation and equation profiling.

4. The claimed bounded completeness is not bounded

variable_count has no derived-work bound, so equation profiling accepts |A|^k work for arbitrary k. Congruence checking may enumerate up to 32^(2·4) argument pairs per operation, over one trillion. Bound the actual work product, not just carrier size and arity separately.

5. Generated-subalgebra metadata is wrong

Generators are not range/uniqueness validated, and is_closed compares final set size with the raw generator tuple length. Duplicates and nullary constants make the result incorrect. Define and replay the exact predicate represented by this field.

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 universal algebra] Add exact signatures, term operations, generated subalgebras, congruences, quotients, homomorphisms, and products

1 participant