Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/jacobian/domains/combinatorics/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
PolynomialCoefficientRecurrenceEvaluationRequest,
RationalGeneratingFunctionCoefficientsRequest,
)
from jacobian.math.combinatorics import PolynomialCoefficientRecurrenceTableRequest
from jacobian.providers import flint_runtime

_ENTRYPOINT = "jacobian_checkers.recurrence_series"
Expand Down Expand Up @@ -87,6 +88,16 @@ def _combinatorics_runtime(
replay_method="standard-library Fraction polynomial recurrence replay",
reason=_REASON,
),
ExactReplayCheckerDeclaration(
"combinatorics.recurrence.p_recursive.table_residuals.compute",
PolynomialCoefficientRecurrenceTableRequest,
"check_polynomial_coefficient_recurrence_table_residuals",
"combinatorics.p-recursive.submitted-table-residual-replay",
entrypoint_module=_ENTRYPOINT,
provider_runtime_factory=_combinatorics_runtime,
replay_method="standard-library Fraction submitted-table residual replay",
reason=_REASON,
),
ExactReplayCheckerDeclaration(
"combinatorics.generating_function.coefficients.compute",
RationalGeneratingFunctionCoefficientsRequest,
Expand Down
45 changes: 45 additions & 0 deletions src/jacobian/domains/combinatorics/recurrence.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
evaluate_linear_recurrence,
evaluate_polynomial_coefficient_recurrence,
)
from jacobian.math.combinatorics import (
PolynomialCoefficientRecurrenceTableRequest,
PolynomialCoefficientRecurrenceTableResult,
recurrence_table_residuals,
)

RECURRENCE_CAPABILITIES = (
combinatorics_operation(
Expand Down Expand Up @@ -166,6 +171,46 @@
),
),
),
combinatorics_operation(
"combinatorics.recurrence.p_recursive.table_residuals.compute",
"Compute residuals for a submitted P-recursive table",
(
"Compute every exact residual of a bounded caller-supplied rational "
"table against sum p_j(n)a_(n-j)=0 without generating or repairing terms."
),
PolynomialCoefficientRecurrenceTableRequest,
PolynomialCoefficientRecurrenceTableResult,
recurrence_table_residuals,
"combinatorics",
"recurrence",
"p-recursive",
"submitted-table",
"exact-rational",
invocation_examples=(
example(
"factorial_table_residuals",
"Check a supplied factorial prefix against a_n=n*a_(n-1).",
{
"coefficient_polynomials": [
[{"num": "1", "den": "1"}],
[
{"num": "0", "den": "1"},
{"num": "-1", "den": "1"},
],
],
"values": [
{"num": value, "den": "1"}
for value in ("1", "1", "2", "6", "24", "120")
],
"coefficient_convention": (
"SUM_P_J_OF_N_TIMES_A_N_MINUS_J_EQUALS_ZERO_FOR_J_FROM_0"
),
"polynomial_convention": "ASCENDING_POWERS_OF_N",
"table_convention": "VALUES_A_0_THROUGH_A_N_IN_ORDER",
},
),
),
),
combinatorics_operation(
"combinatorics.generating_function.coefficients.compute",
"Compute a rational generating-function coefficient prefix",
Expand Down
15 changes: 15 additions & 0 deletions src/jacobian/math/combinatorics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Provider-independent exact combinatorics values and functions."""

from jacobian.math.combinatorics.recurrence_tables import (
IndexedRecurrenceResidual,
PolynomialCoefficientRecurrenceTableRequest,
PolynomialCoefficientRecurrenceTableResult,
recurrence_table_residuals,
)

__all__ = [
"IndexedRecurrenceResidual",
"PolynomialCoefficientRecurrenceTableRequest",
"PolynomialCoefficientRecurrenceTableResult",
"recurrence_table_residuals",
Comment on lines +10 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Register the new public combinatorics module

Add combinatorics to jacobian.math.__all__ and to the public namespace/import-isolation manifests. This package declares a supported public surface, but import jacobian.math does not expose it like the other domain modules, and the unchanged PUBLIC_API table means its symbols and isolation behavior receive none of the required API checks.

AGENTS.md reference: AGENTS.md:L65-L68

Useful? React with 👍 / 👎.

]
156 changes: 156 additions & 0 deletions src/jacobian/math/combinatorics/recurrence_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Exact residuals for caller-supplied P-recursive tables."""

from __future__ import annotations

from fractions import Fraction
from typing import Literal, Self

from pydantic import Field, StrictBool, StrictInt, model_validator

from jacobian.contracts.combinatorics import (
MAX_COMBINATORICS_INPUT_RATIONAL_DIGITS,
MAX_COMBINATORICS_RESULT_RATIONAL_DIGITS,
MAX_LINEAR_RECURRENCE_INDEX,
MAX_LINEAR_RECURRENCE_ORDER,
MAX_P_RECURSIVE_POLYNOMIAL_DEGREE,
)
from jacobian.contracts.exact import CanonicalRational, require_bounded_rational
from jacobian.contracts.results import ContractModel


class IndexedRecurrenceResidual(ContractModel):
index: StrictInt = Field(ge=1, le=MAX_LINEAR_RECURRENCE_INDEX)
value: CanonicalRational


class PolynomialCoefficientRecurrenceTableRequest(ContractModel):
"""One complete finite table and one polynomial-coefficient recurrence."""

coefficient_polynomials: tuple[tuple[CanonicalRational, ...], ...] = Field(
min_length=2, max_length=MAX_LINEAR_RECURRENCE_ORDER + 1
)
values: tuple[CanonicalRational, ...] = Field(
min_length=2, max_length=MAX_LINEAR_RECURRENCE_INDEX + 1
)
coefficient_convention: Literal[
"SUM_P_J_OF_N_TIMES_A_N_MINUS_J_EQUALS_ZERO_FOR_J_FROM_0"
]
polynomial_convention: Literal["ASCENDING_POWERS_OF_N"]
table_convention: Literal["VALUES_A_0_THROUGH_A_N_IN_ORDER"]

@model_validator(mode="after")
def require_complete_bounded_table(self) -> Self:
order = len(self.coefficient_polynomials) - 1
if len(self.values) <= order:
raise ValueError(
"values must include the initial range and at least one checked step"
)
for polynomial in self.coefficient_polynomials:
if (
not polynomial
or len(polynomial) > MAX_P_RECURSIVE_POLYNOMIAL_DEGREE + 1
):
raise ValueError("coefficient polynomial degree is outside the bound")
if polynomial[-1].as_fraction() == 0:
raise ValueError("coefficient polynomial must omit trailing zero terms")
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Accept the canonical zero coefficient polynomial

Allow a singleton zero polynomial ([{"num":"0","den":"1"}]) and reject a trailing zero only when the vector has multiple coefficients. Zero lag polynomials are valid in sum_j p_j(n)a_(n-j)=0—for example, a_n-a_(n-2)=0 requires p_1=0—but this validator rejects every such table even though the existing P-recursive contract and independent checker both accept [0], silently narrowing the advertised recurrence domain.

AGENTS.md reference: AGENTS.md:L55-L58

Useful? React with 👍 / 👎.

for coefficient in polynomial:
require_bounded_rational(
coefficient,
max_digits=MAX_COMBINATORICS_INPUT_RATIONAL_DIGITS,
label="recurrence polynomial coefficient",
)
for value in self.values:
require_bounded_rational(
value,
max_digits=MAX_COMBINATORICS_INPUT_RATIONAL_DIGITS,
label="submitted recurrence table value",
)
return self
Comment on lines +64 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate the complete ledger against the inline size limit

Add an aggregate result-size check before accepting the request, or publish this result durably. A maximum-order, maximum-degree table with many pairwise-coprime bounded denominators can produce hundreds of individually valid residuals whose combined encoding exceeds the 10 MiB InlinePublication limit; this validator checks only each input rational, so the producer performs the expensive complete replay and then fails during publication instead of rejecting the request up front.

AGENTS.md reference: AGENTS.md:L150-L153

Useful? React with 👍 / 👎.



class PolynomialCoefficientRecurrenceTableResult(ContractModel):
"""Complete exact residual ledger for the supplied finite table."""

coefficient_convention: Literal[
"SUM_P_J_OF_N_TIMES_A_N_MINUS_J_EQUALS_ZERO_FOR_J_FROM_0"
]
polynomial_convention: Literal["ASCENDING_POWERS_OF_N"]
table_convention: Literal["VALUES_A_0_THROUGH_A_N_IN_ORDER"]
recurrence_order: StrictInt = Field(ge=1, le=MAX_LINEAR_RECURRENCE_ORDER)
term_count: StrictInt = Field(ge=2, le=MAX_LINEAR_RECURRENCE_INDEX + 1)
residuals: tuple[IndexedRecurrenceResidual, ...] = Field(
min_length=1, max_length=MAX_LINEAR_RECURRENCE_INDEX
)
satisfies_recurrence: StrictBool
first_failure_index: StrictInt | None = Field(
default=None, ge=1, le=MAX_LINEAR_RECURRENCE_INDEX
)

@model_validator(mode="after")
def require_complete_consistent_ledger(self) -> Self:
expected = tuple(range(self.recurrence_order, self.term_count))
if tuple(item.index for item in self.residuals) != expected:
raise ValueError("residuals must cover every checked table index")
failures = tuple(
item.index for item in self.residuals if item.value.as_fraction() != 0
)
if self.satisfies_recurrence != (not failures):
raise ValueError("satisfies_recurrence must agree with residuals")
if self.first_failure_index != (failures[0] if failures else None):
raise ValueError("first_failure_index must identify the first failure")
return self


def _evaluate(polynomial: tuple[Fraction, ...], index: int) -> Fraction:
return sum(
(coefficient * index**power for power, coefficient in enumerate(polynomial)),
Fraction(),
)


def recurrence_table_residuals(
request: PolynomialCoefficientRecurrenceTableRequest,
) -> PolynomialCoefficientRecurrenceTableResult:
polynomials = tuple(
tuple(value.as_fraction() for value in polynomial)
for polynomial in request.coefficient_polynomials
)
values = tuple(value.as_fraction() for value in request.values)
Comment on lines +114 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expose computational rationals from the native API

Keep CanonicalRational conversion in the capability adapter and make the public jacobian.math.combinatorics kernel accept and return computational values such as Fraction-based domain values. As exported here, native callers must construct Pydantic wire requests containing decimal strings, and the function immediately converts those strings to Fraction before converting results back to wire models, making the supported native API itself a wire boundary rather than a computational API.

AGENTS.md reference: AGENTS.md:L92-L96

Useful? React with 👍 / 👎.

order = len(polynomials) - 1
residuals = []
failures = []
for index in range(order, len(values)):
residual = sum(
(
_evaluate(polynomials[offset], index) * values[index - offset]
for offset in range(order + 1)
),
Fraction(),
)
wire = CanonicalRational.from_fraction(residual)
require_bounded_rational(
wire,
max_digits=MAX_COMBINATORICS_RESULT_RATIONAL_DIGITS,
label="submitted recurrence residual",
)
residuals.append(IndexedRecurrenceResidual(index=index, value=wire))
if residual:
failures.append(index)
return PolynomialCoefficientRecurrenceTableResult(
coefficient_convention=request.coefficient_convention,
polynomial_convention=request.polynomial_convention,
table_convention=request.table_convention,
recurrence_order=order,
term_count=len(values),
residuals=tuple(residuals),
satisfies_recurrence=not failures,
first_failure_index=failures[0] if failures else None,
)


__all__ = [
"IndexedRecurrenceResidual",
"PolynomialCoefficientRecurrenceTableRequest",
"PolynomialCoefficientRecurrenceTableResult",
"recurrence_table_residuals",
]
100 changes: 100 additions & 0 deletions src/jacobian_checkers/recurrence_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,94 @@ def _replay_polynomial_coefficient_recurrence(
)


def _replay_polynomial_coefficient_recurrence_table(
source: dict[str, Any], result: dict[str, Any]
) -> bool:
expected_source = {
"coefficient_polynomials",
"values",
"coefficient_convention",
"polynomial_convention",
"table_convention",
}
expected_result = {
"coefficient_convention",
"polynomial_convention",
"table_convention",
"recurrence_order",
"term_count",
"residuals",
"satisfies_recurrence",
"first_failure_index",
}
if set(source) != expected_source or set(result) != expected_result:
return False
if (
source["coefficient_convention"] != _P_RECURSIVE_CONVENTION
or source["polynomial_convention"] != "ASCENDING_POWERS_OF_N"
or source["table_convention"] != "VALUES_A_0_THROUGH_A_N_IN_ORDER"
or result["coefficient_convention"] != source["coefficient_convention"]
or result["polynomial_convention"] != source["polynomial_convention"]
or result["table_convention"] != source["table_convention"]
):
return False
raw_polynomials = source["coefficient_polynomials"]
if not isinstance(raw_polynomials, list) or not 2 <= len(raw_polynomials) <= 17:
return False
polynomials = [_canonical_polynomial(item) for item in raw_polynomials]
values = _fractions(source["values"], minimum=2, maximum=513, max_digits=64)
order = len(polynomials) - 1
if len(values) <= order:
return False
expected = []
for index in range(order, len(values)):
residual = sum(
(
sum(
(
coefficient * index**power
for power, coefficient in enumerate(polynomials[offset])
),
Fraction(),
)
* values[index - offset]
for offset in range(order + 1)
),
Fraction(),
)
if not _bounded_replay_fraction(residual):
return False
expected.append(residual)
raw_residuals = result["residuals"]
if not isinstance(raw_residuals, list) or len(raw_residuals) != len(expected):
return False
for item, index, residual in zip(
raw_residuals, range(order, len(values)), expected, strict=True
):
if (
not isinstance(item, dict)
or set(item) != {"index", "value"}
or type(item["index"]) is not int
or item["index"] != index
or _fraction(item["value"], max_digits=32_768) != residual
):
return False
failures = [
index
for index, residual in zip(range(order, len(values)), expected, strict=True)
if residual != 0
]
return (
type(result["recurrence_order"]) is int
and result["recurrence_order"] == order
and type(result["term_count"]) is int
and result["term_count"] == len(values)
and type(result["satisfies_recurrence"]) is bool
and result["satisfies_recurrence"] == (not failures)
and result["first_failure_index"] == (failures[0] if failures else None)
)


def _replay_rational_series(
source: dict[str, Any],
result: dict[str, Any],
Expand Down Expand Up @@ -517,6 +605,17 @@ def check_polynomial_coefficient_recurrence_evaluation(
)


def check_polynomial_coefficient_recurrence_table_residuals(
request: object,
) -> dict[str, Any]:
return _run(
request,
operation_id="combinatorics.recurrence.p_recursive.table_residuals.compute",
witness_format="combinatorics.p-recursive.submitted-table-residual-replay",
replay=_replay_polynomial_coefficient_recurrence_table,
)


def check_rational_generating_function_coefficients(
request: object,
) -> dict[str, Any]:
Expand All @@ -531,5 +630,6 @@ def check_rational_generating_function_coefficients(
__all__ = [
"check_linear_recurrence_evaluation",
"check_polynomial_coefficient_recurrence_evaluation",
"check_polynomial_coefficient_recurrence_table_residuals",
"check_rational_generating_function_coefficients",
]
Loading