|
39 | 39 | _MAX_RESIDUE_EXPONENT = 32 |
40 | 40 | _MAX_RESIDUE_ASSIGNMENTS = 4_096 |
41 | 41 | _MAX_POLYNOMIAL_RESIDUE_MODULUS = 1_000_000 |
| 42 | +_MAX_FINITE_GROUP_ORDER = 4_096 |
| 43 | +_MAX_FINITE_GROUP_RANK = 6 |
| 44 | +_MAX_FINITE_GROUP_FACTOR_SIZE = 256 |
42 | 45 |
|
43 | 46 | BoundedInteger = Annotated[ |
44 | 47 | str, |
@@ -294,6 +297,43 @@ def require_canonical_bounded_polynomial(self) -> Self: |
294 | 297 | return self |
295 | 298 |
|
296 | 299 |
|
| 300 | +class FiniteAbelianGroupFactorizationRequest(ContractModel): |
| 301 | + """Two bounded integer-vector factors in a product of cyclic groups.""" |
| 302 | + |
| 303 | + moduli: tuple[StrictInt, ...] = Field( |
| 304 | + min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 305 | + ) |
| 306 | + left: tuple[tuple[StrictInt, ...], ...] = Field( |
| 307 | + min_length=1, max_length=_MAX_FINITE_GROUP_FACTOR_SIZE |
| 308 | + ) |
| 309 | + right: tuple[tuple[StrictInt, ...], ...] = Field( |
| 310 | + min_length=1, max_length=_MAX_FINITE_GROUP_FACTOR_SIZE |
| 311 | + ) |
| 312 | + |
| 313 | + @model_validator(mode="after") |
| 314 | + def require_bounded_product_group(self) -> Self: |
| 315 | + if any(modulus < 2 or modulus > 1_000_000 for modulus in self.moduli): |
| 316 | + raise ValueError("cyclic moduli must be between 2 and 1,000,000") |
| 317 | + if math.prod(self.moduli) > _MAX_FINITE_GROUP_ORDER: |
| 318 | + raise ValueError("finite abelian group exceeds the 4,096-element bound") |
| 319 | + if len(self.left) * len(self.right) > _MAX_FINITE_GROUP_ORDER: |
| 320 | + raise ValueError("factor Cartesian product exceeds the group-order bound") |
| 321 | + if any( |
| 322 | + len(element) != len(self.moduli) |
| 323 | + for factor in (self.left, self.right) |
| 324 | + for element in factor |
| 325 | + ): |
| 326 | + raise ValueError("every factor element must match the group rank") |
| 327 | + if any( |
| 328 | + abs(coordinate) > 1_000_000 |
| 329 | + for factor in (self.left, self.right) |
| 330 | + for element in factor |
| 331 | + for coordinate in element |
| 332 | + ): |
| 333 | + raise ValueError("factor coordinates exceed the input bound") |
| 334 | + return self |
| 335 | + |
| 336 | + |
297 | 337 | class ChineseRemainderRequest(ContractModel): |
298 | 338 | """A finite system of integer congruences with parallel residues and moduli.""" |
299 | 339 |
|
@@ -541,6 +581,90 @@ def bind_complete_residue_image(self) -> Self: |
541 | 581 | return self |
542 | 582 |
|
543 | 583 |
|
| 584 | +class FiniteAbelianRepresentationCount(ContractModel): |
| 585 | + representation_count: StrictInt = Field(ge=0, le=_MAX_FINITE_GROUP_ORDER) |
| 586 | + element_count: StrictInt = Field(ge=1, le=_MAX_FINITE_GROUP_ORDER) |
| 587 | + |
| 588 | + |
| 589 | +class FiniteAbelianRepresentationWitness(ContractModel): |
| 590 | + element: tuple[StrictInt, ...] = Field( |
| 591 | + min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 592 | + ) |
| 593 | + left: tuple[StrictInt, ...] = Field(min_length=1, max_length=_MAX_FINITE_GROUP_RANK) |
| 594 | + right: tuple[StrictInt, ...] = Field( |
| 595 | + min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 596 | + ) |
| 597 | + other_left: tuple[StrictInt, ...] | None = Field( |
| 598 | + default=None, min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 599 | + ) |
| 600 | + other_right: tuple[StrictInt, ...] | None = Field( |
| 601 | + default=None, min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 602 | + ) |
| 603 | + |
| 604 | + |
| 605 | +class FiniteAbelianGroupFactorizationResult(ContractModel): |
| 606 | + """Complete unique-representation summary for ``G = left + right``.""" |
| 607 | + |
| 608 | + semantics_version: Literal["finite-abelian-group-factorization.v1"] |
| 609 | + moduli: tuple[StrictInt, ...] = Field( |
| 610 | + min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 611 | + ) |
| 612 | + normalized_left: tuple[tuple[StrictInt, ...], ...] = Field( |
| 613 | + min_length=1, max_length=_MAX_FINITE_GROUP_FACTOR_SIZE |
| 614 | + ) |
| 615 | + normalized_right: tuple[tuple[StrictInt, ...], ...] = Field( |
| 616 | + min_length=1, max_length=_MAX_FINITE_GROUP_FACTOR_SIZE |
| 617 | + ) |
| 618 | + group_order: StrictInt = Field(ge=2, le=_MAX_FINITE_GROUP_ORDER) |
| 619 | + pair_count: StrictInt = Field(ge=1, le=_MAX_FINITE_GROUP_ORDER) |
| 620 | + distinct_sum_count: StrictInt = Field(ge=1, le=_MAX_FINITE_GROUP_ORDER) |
| 621 | + representation_histogram: tuple[FiniteAbelianRepresentationCount, ...] = Field( |
| 622 | + min_length=1 |
| 623 | + ) |
| 624 | + is_exact_factorization: StrictBool |
| 625 | + first_missing: tuple[StrictInt, ...] | None = Field( |
| 626 | + default=None, min_length=1, max_length=_MAX_FINITE_GROUP_RANK |
| 627 | + ) |
| 628 | + first_duplicate: FiniteAbelianRepresentationWitness | None = None |
| 629 | + |
| 630 | + @model_validator(mode="after") |
| 631 | + def bind_factorization_summary(self) -> Self: |
| 632 | + if self.group_order != math.prod(self.moduli): |
| 633 | + raise ValueError("group order must equal the product of cyclic moduli") |
| 634 | + if self.pair_count != len(self.normalized_left) * len(self.normalized_right): |
| 635 | + raise ValueError("pair count must equal the factor Cartesian-product size") |
| 636 | + counts = tuple( |
| 637 | + item.representation_count for item in self.representation_histogram |
| 638 | + ) |
| 639 | + if counts != tuple(sorted(set(counts))): |
| 640 | + raise ValueError( |
| 641 | + "representation histogram counts must be unique and increasing" |
| 642 | + ) |
| 643 | + if ( |
| 644 | + sum(item.element_count for item in self.representation_histogram) |
| 645 | + != self.group_order |
| 646 | + ): |
| 647 | + raise ValueError("representation histogram must cover the complete group") |
| 648 | + if ( |
| 649 | + sum( |
| 650 | + item.representation_count * item.element_count |
| 651 | + for item in self.representation_histogram |
| 652 | + ) |
| 653 | + != self.pair_count |
| 654 | + ): |
| 655 | + raise ValueError("representation histogram must cover every factor pair") |
| 656 | + expected = self.pair_count == self.group_order and all( |
| 657 | + item.representation_count == 1 for item in self.representation_histogram |
| 658 | + ) |
| 659 | + if self.is_exact_factorization != expected: |
| 660 | + raise ValueError( |
| 661 | + "factorization decision does not match the complete histogram" |
| 662 | + ) |
| 663 | + if self.is_exact_factorization and (self.first_missing or self.first_duplicate): |
| 664 | + raise ValueError("exact factorizations cannot carry failure witnesses") |
| 665 | + return self |
| 666 | + |
| 667 | + |
544 | 668 | def _evaluate_normalized_modular_polynomial( |
545 | 669 | terms: tuple[NormalizedModularPolynomialTerm, ...], |
546 | 670 | assignment: tuple[int, ...], |
|
0 commit comments