Skip to main content

Count ordered extensions of Latin rectangles.

Project description

Latin Rectangles Extension Counter

A Python library for counting ordered extensions of Latin rectangles, with optimized exact routines for adding one row to normalized 2×n rectangles and direct recursive support for adding multiple rows at small n.

Python 3.12+ License: MIT

Overview

A Latin rectangle is an r×n array filled with n different symbols such that each symbol occurs exactly once in each row and at most once in each column.

Extension Problem

Given a 2×n Latin rectangle:

1  2  3  4  5  6  7  8
p[1]  p[2]  p[3]  p[4]  p[5]  p[6]  p[7]  p[8]

where p is a derangement, one common problem is to count how many valid third rows can be added such that the resulting 3×n rectangle remains a Latin rectangle. More generally, the public API counts ordered ways to add rows_to_add further rows.

Key Features

  • High Performance For One Added Row: Quadratic O(n^2) per-derangement time complexity (see Complexity), with exact NTT/CRT convolution available when CRT reconstruction is not too expensive and a cached Touchard formula for repeated cycle-type queries
  • Memory Efficient: Approximate O(n^1.36) memory complexity
  • Consistent Extension API: rows_to_add selects how many ordered rows to add from explicit rows, a normalized derangement, or a cycle type
  • Generalization to k→k+t: Exact counting for extending k×n to (k+t)×n; one-row subproblems use component-wise rook/matching polynomials, while t > 1 uses direct recursion over valid next rows

Installation

Via pip

The library is available on PyPI and can be installed via pip:

pip install latin-rectangles

Via uv Package Manager (Recommended)

Using the uv package manager, you can try out the package without a separate installation step:

# Get the number of extensions for a random derangement for a specific number of columns
uvx latin-rectangles --n 42

If you want to add it to your project, you can use:

# Add to your project's environment using uv
uv add latin-rectangles

Manual Installation

Alternatively, you can clone the repository and install it from source:

git clone https://github.com/ionmich/latin-rectangles.git
cd latin-rectangles

Quick Start

Command Line (CLI) Usage

Generate random derangement:

> uv run python -m latin_rectangles --n 42
🎲 Generated Random Derangement for n=42
📊 Cycle structure: [2, 2, 4, 8, 26]
🔢 Number of extensions after adding 1 row: 185,566,788,772,996,286,199,647,931,971,186,844,003,087,641,029,824

Use specific cycle structure:

> uv run python -m latin_rectangles --c "2,2,4"
⚙️  Specific Cycle Structure for n=8
📊 Cycle structure: [2, 2, 4]
🔢 Number of extensions after adding 1 row: 4,744

Add two rows from a normalized 2×n start:

> uv run python -m latin_rectangles --c "3,4" --rows-to-add 2
⚙️  Specific Cycle Structure for n=7
📊 Cycle structure: [3, 4]
🔢 Number of extensions after adding 2 rows: 83,328

Large counts are summarized by default to keep the CLI usable and to avoid Python's decimal integer string-conversion guard:

> uv run python -m latin_rectangles --n 1700
🎲 Generated Random Derangement for n=1700
📊 Cycle structure: [...]
🔢 Number of extensions after adding 1 row: 4,685 decimal digits (bits=15,562; leading=...; trailing=...; mod 1,000,000,007=...; use --full-output to print all digits)

Use --full-output to print the entire decimal integer, or --max-digits N to raise/lower the exact-printing threshold.

Enumerate all possible cycle structures:

> uv run python -m latin_rectangles --n 8 --all
🔍 All Cycle Structures for n=8
📊 Found 7 possible structures with non-zero extensions:

 1. [2, 2, 2, 2] → 4,752 extensions
 2. [2, 2, 4] → 4,744 extensions
 3. [2, 3, 3] → 4,740 extensions
 4. [2, 6] → 4,740 extensions
 5. [4, 4] → 4,740 extensions
 6. [3, 5] → 4,738 extensions
 7. [8] → 4,738 extensions

Get help

uv run latin-rectangles --help

Python Library Usage

from latin_rectangles import (
  count_extensions,
  count_extensions_from_cycle_type,
  count_extensions_from_derangement,
  count_random_extensions,
  generate_random_derangement,
)

# Method 1: One-liner for random derangement
extensions = count_random_extensions(n=12)
print(f"Extensions: {extensions:,}")

# Method 2: Step-by-step with custom derangement
derangement = generate_random_derangement(n=10)
extensions = count_extensions_from_derangement(derangement, rows_to_add=1)
print(f"Derangement {derangement[1:]} has {extensions:,} extensions")

# Method 3: Using a specific cycle type (e.g., "2,2,4") in one line
cycle_lengths = [2, 2, 4]
extensions = count_extensions_from_cycle_type(cycle_lengths, rows_to_add=1)
print(f"Cycle structure {cycle_lengths} has {extensions:,} extensions")  # 4,744 for n=8

# Method 4: With predefined derangement (1-indexed with dummy 0)
p = [0, 2, 3, 4, 5, 6, 7, 8, 1]  # 8-cycle for n=8
extensions = count_extensions_from_derangement(p)
print(f"8-cycle has {extensions:,} extensions")  # Output: 4,738

# Bonus: General k→k+t extension counting (small example)
# Rows are 1-indexed permutations; the first row may be non-identity (will be standardized).
rows = [
  [0, 1, 2, 3, 4],      # identity
  [0, 2, 1, 4, 3],      # (1 2)(3 4)
  [0, 3, 4, 1, 2],      # (1 3)(2 4)
]
extensions_k = count_extensions(rows, rows_to_add=1)
print(f"Extend 3×4 → 4×4: {extensions_k} ways")

# Add two rows from a normalized 2×7 rectangle with second-row cycle type (3, 4).
two_added = count_extensions_from_cycle_type([3, 4], rows_to_add=2)
print(f"Extend 2×7 → 4×7: {two_added:,} ways")  # 83,328

Algorithm Details

Mathematical Foundation

The algorithms are derived in detail in docs/methods.md, including notation, the rook-polynomial formula, the Touchard identity, the exact NTT/CRT convolution path, and the general k x n -> (k + 1) x n method. The benchmark and plotting workflow is documented in docs/benchmarks.md.

At a high level, the specialized 2 x n -> 3 x n counter uses rook polynomial theory:

  1. Input: A derangement (permutation with no fixed points) representing the second row
  2. Cycle Decomposition: Decompose the derangement into disjoint cycles
  3. Rook Polynomials: Compute rook polynomial for each cycle structure
  4. Polynomial Multiplication: Combine rook polynomials to get the final count

For direct cycle-structure inputs, the implementation also uses Touchard's identity. If the cycle lengths are l_1, ..., l_c, then

E(l_1, ..., l_c) = 1/2 * sum_epsilon M_|epsilon_1 l_1 + ... + epsilon_c l_c|

where M_s is the one-cycle inclusion-exclusion value. The values M_0 = 2 and M_1 = -1 are formal correction terms from the Chebyshev polynomial identity; they are not extension counts for actual 0- or 1-cycles.

This is Touchard's 1934 discordant-permutation formula specialized to the no-fixed-point relative cycle structure of a normalized 2 x n Latin rectangle. The implementation derives the same expression from the rook-polynomial/forbidden-graph view and evaluates it with cached one-cycle values.

Internally, the rook polynomial for one l-cycle stores positive matching numbers r_j, while the Touchard proof uses the reversed signed polynomial q_l(t) = sum_j (-1)^j r_j t^(l-j) and the linear functional F(t^d) = d!. For l >= 2, M_l = F(q_l) is exactly the same inclusion-exclusion count computed from the rook polynomial.

Complexity

  • Per derangement (fixed 2×n Latin rectangle): the default method runs in O(n^2) time due to polynomial multiplications whose total degree sums to n. Memory usage is empirically ~O(n^1.36). The optional use_fft=True path uses exact NTT/CRT convolution for large dense polynomial products and falls back to schoolbook multiplication for small products, skinny products, or products whose coefficient sizes would require too many CRT primes.

  • Repeated cycle-structure queries: the Touchard method reuses cached one-cycle values M_s, which is especially useful when enumerating all cycle structures for a fixed n.

  • Enumerating all relevant cycle types at a fixed n: the number of distinct cycle-type inputs is T(n) = p(n) − p(n − 1), where p(n) is the partition function (partitions of n). Using the Hardy–Ramanujan asymptotic p(n) ≍ (1/(4√3 n)) · exp(C √n) with C = π√(2/3), one gets T(n) = p(n) − p(n − 1) = Θ(exp(C √n) / n^{3/2}). Therefore, the total time to compute extensions for all cycle types scales as O(n^2 · T(n)) = O(√n · exp(C √n)) with the constant C = π√(2/3). Peak memory is still governed by the per-derangement footprint since enumeration can reuse buffers.

API Reference

Core Functions

count_extensions(rows: list[list[int]], rows_to_add: int = 1) -> int

Counts the number of ordered extensions from explicit existing rows.

Parameters:

  • rows: existing Latin rectangle rows as 1-indexed permutations
  • rows_to_add: number of further rows to add; 0 returns 1

Returns: Integer number of ordered extensions

For rows_to_add=1, this uses the general component rook/matching-polynomial method. For larger values it recursively enumerates valid next rows, which is intended for small-n exact work and regression oracles.

The optional use_fft=True argument uses exact NTT/CRT convolution only when the transform route is expected to be reasonable. It does not use rounded floating-point convolution.

count_extensions_from_derangement(permutation: list[int], rows_to_add: int = 1) -> int

Counts ordered extensions from the normalized 2 x n rectangle whose first row is identity and whose second row is the supplied derangement.

For rows_to_add=1, this uses the optimized derangement/cycle-decomposition path. For rows_to_add > 1, it delegates to the recursive explicit-row API.

count_extensions_from_cycle_type(cycle_lengths: list[int], rows_to_add: int = 1, method: str = "auto") -> int

The default method="auto" uses the Touchard formula when its needed one-cycle values are cached or few, and falls back to the rook product for cold dense high-n cycle types. Explicit methods are "touchard", "rook", and "rook_ntt".

The method parameter is only supported for rows_to_add=1. For rows_to_add > 1, this builds a canonical derangement with the requested cycle type and uses direct recursion.

count_random_extensions(n: int) -> int

Convenience function that generates a random derangement and counts its extensions.

Parameters:

  • n: Size of the derangement (must be > 1)

Returns: Number of extensions for the randomly generated derangement

generate_random_derangement(n: int) -> list[int]

Generates a random derangement of size n.

Parameters:

  • n: Size of the derangement

Returns: 1-indexed list representing the derangement

find_cycle_decomposition(permutation: list[int]) -> list[list[int]]

Finds the cycle decomposition of a permutation.

Parameters:

  • permutation: 1-indexed permutation

Returns: List of cycles (each cycle is a list of indices)

Examples

Basic Usage Examples

from latin_rectangles import count_extensions
from latin_rectangles import count_extensions_from_derangement

# Example 1: Single 8-cycle
p_8_cycle = [0, 2, 3, 4, 5, 6, 7, 8, 1]
print(f"8-cycle: {count_extensions_from_derangement(p_8_cycle):,} extensions")
# Output: 8-cycle: 4,738 extensions

# Example 2: Two 4-cycles
p_4_4 = [0, 2, 3, 4, 1, 6, 7, 8, 5]
print(f"4,4-cycles: {count_extensions_from_derangement(p_4_4):,} extensions")
# Output: 4,4-cycles: 4,740 extensions

# Example 3: Four 2-cycles
p_2_2_2_2 = [0, 2, 1, 4, 3, 6, 5, 8, 7]
print(f"2,2,2,2-cycles: {count_extensions_from_derangement(p_2_2_2_2):,} extensions")
# Output: 2,2,2,2-cycles: 4,752 extensions

Advanced Usage

from latin_rectangles import generate_random_derangement, find_cycle_decomposition, count_extensions_from_derangement

# Generate and analyze a random derangement
n = 15
derangement = generate_random_derangement(n)
cycles = find_cycle_decomposition(derangement)
cycle_lengths = sorted([len(c) for c in cycles])
extensions = count_extensions_from_derangement(derangement)

print(f"n={n}")
print(f"Derangement: {derangement[1:]}")
print(f"Cycle structure: {cycle_lengths}")
print(f"Extensions: {extensions:,}")

Batch Processing

from latin_rectangles import count_random_extensions

# Process multiple sizes
results = []
for n in range(5, 21):
    extensions = count_random_extensions(n)
    results.append((n, extensions))
    print(f"n={n:2d}: {extensions:,} extensions")

# Find the size with the most extensions in this batch
max_n, max_extensions = max(results, key=lambda x: x[1])
print(f"Maximum: n={max_n} with {max_extensions:,} extensions")

Development

Running Tests

# Run the test suite
uv run pytest

# Run with coverage
uv run pytest --cov=latin_rectangles

# Run specific test
uv run pytest tests/test_main.py -v

Code Quality

# Type checking
uv run mypy src/

# Linting
uv run ruff check src/

# Formatting
uv run ruff format src/

Benchmarking

# Run performance benchmarks
uv run python benchmark.py

# Analyze complexity
uv run python complexity_analysis.py

Contributing

Contributions are welcome! Please see DEVELOPMENT.md for development guidelines.

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Citation

If you use this library in your research, please cite:

@software{latin_rectangles,
  title={Latin Rectangles Extensions},
  author={Ioannis Michaloliakos},
  year={2025},
  url={https://github.com/ionmich/latin-rectangles}
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

latin_rectangles-0.3.0.tar.gz (498.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

latin_rectangles-0.3.0-py3-none-any.whl (23.2 kB view details)

Uploaded Python 3

File details

Details for the file latin_rectangles-0.3.0.tar.gz.

File metadata

  • Download URL: latin_rectangles-0.3.0.tar.gz
  • Upload date:
  • Size: 498.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.2

File hashes

Hashes for latin_rectangles-0.3.0.tar.gz
Algorithm Hash digest
SHA256 99d7b70ad4afc01d3f5dc8e8aedf9fa091f1e66c23fd47ad342612782065cfbd
MD5 99a70a1b3a8b9567e3baf48edc06b27f
BLAKE2b-256 54aa1609a27ffd8e65ae651c89dcca7fb37134cb2702dddbbc14e9b44d4e53c5

See more details on using hashes here.

File details

Details for the file latin_rectangles-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for latin_rectangles-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3fa124c5715597b3de7adaf59b27a2f332e3ab3a0b22ec7959c3cbd562ba5eda
MD5 42999f3d3ce99168582b19eb94624059
BLAKE2b-256 e35d5a60cd45f17ab883c5b23ae7e0846e5ed75b0af05ca34e845032268533ec

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page