latin-rectangles 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ """Latin rectangles extension counting algorithms."""
2
+
3
+ from .derangements import find_cycle_decomposition, generate_random_derangement
4
+ from .extension_counting import count_extensions
5
+
6
+
7
+ def count_random_extensions(n: int) -> int:
8
+ """
9
+ Generate a random derangement of size n and count its extensions.
10
+
11
+ This is a convenience function that combines random derangement generation
12
+ with extension counting in a single call.
13
+
14
+ Args:
15
+ n: Size of the derangement (must be > 1)
16
+
17
+ Returns:
18
+ Number of extensions for the randomly generated derangement
19
+
20
+ Raises:
21
+ ValueError: If n <= 1 (no derangements exist)
22
+
23
+ Example:
24
+ >>> extensions = count_random_extensions(10)
25
+ >>> print(f"Random derangement for n=10 has {extensions} extensions")
26
+ """
27
+ if n <= 1:
28
+ raise ValueError("n must be greater than 1 for derangements to exist")
29
+
30
+ random_derangement = generate_random_derangement(n)
31
+ return count_extensions(random_derangement)
32
+
33
+
34
+ # Export the main functions
35
+ __all__ = [
36
+ "count_extensions",
37
+ "count_random_extensions",
38
+ "find_cycle_decomposition",
39
+ "generate_random_derangement",
40
+ ]
@@ -0,0 +1,186 @@
1
+ """Entry point for running the latin_rectangles package as a script."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .derangements import find_cycle_decomposition, generate_random_derangement, create_cycle_structure
7
+ from .extension_counting import count_extensions
8
+
9
+
10
+ def count_random_extensions(n: int) -> tuple[int, list[int], int]:
11
+ """
12
+ Generate a random derangement and count its extensions.
13
+
14
+ Args:
15
+ n: Size of the derangement
16
+
17
+ Returns:
18
+ Tuple of (n, cycle_lengths, extensions_count)
19
+ """
20
+ if n <= 1:
21
+ raise ValueError("n must be greater than 1 for derangements to exist")
22
+
23
+ random_p = generate_random_derangement(n)
24
+ random_cycles = find_cycle_decomposition(random_p)
25
+ cycle_lengths = sorted([len(c) for c in random_cycles])
26
+ extensions = count_extensions(random_p)
27
+
28
+ return n, cycle_lengths, extensions
29
+
30
+
31
+ def count_cycle_structure_extensions(cycle_structure: str) -> tuple[int, list[int], int]:
32
+ """
33
+ Create a derangement with specific cycle structure and count its extensions.
34
+
35
+ Args:
36
+ cycle_structure: Comma-separated cycle lengths (e.g., "2,2,4")
37
+
38
+ Returns:
39
+ Tuple of (n, cycle_lengths, extensions_count)
40
+ """
41
+ try:
42
+ cycle_lengths = [int(x.strip()) for x in cycle_structure.split(",")]
43
+ except ValueError:
44
+ raise ValueError("Cycle structure must be comma-separated integers")
45
+
46
+ if not cycle_lengths:
47
+ raise ValueError("Cycle structure cannot be empty")
48
+
49
+ n = sum(cycle_lengths)
50
+ if n <= 1:
51
+ raise ValueError("Total size must be greater than 1")
52
+
53
+ p = create_cycle_structure(cycle_lengths)
54
+ extensions = count_extensions(p)
55
+
56
+ return n, sorted(cycle_lengths), extensions
57
+
58
+
59
+ def generate_all_cycle_structures(n: int) -> list[list[int]]:
60
+ """
61
+ Generate all valid cycle structures (partitions) for a derangement of size n.
62
+ Only includes partitions where all parts are ≥ 2 (no 1-cycles).
63
+
64
+ Args:
65
+ n: Size of the derangement
66
+
67
+ Returns:
68
+ List of cycle structures, each as a sorted list of cycle lengths
69
+ """
70
+ def partitions_with_min_part(target: int, min_part: int, current: list[int]) -> list[list[int]]:
71
+ """Generate partitions of target where all parts are >= min_part."""
72
+ if target == 0:
73
+ return [current[:]]
74
+
75
+ if target < min_part:
76
+ return []
77
+
78
+ result = []
79
+ for part_size in range(min_part, target + 1):
80
+ current.append(part_size)
81
+ result.extend(partitions_with_min_part(target - part_size, part_size, current))
82
+ current.pop()
83
+
84
+ return result
85
+
86
+ if n <= 1:
87
+ return []
88
+
89
+ # Generate all partitions where each part is at least 2
90
+ partitions = partitions_with_min_part(n, 2, [])
91
+
92
+ # Sort each partition for consistent output
93
+ return [sorted(partition) for partition in partitions]
94
+
95
+
96
+ def enumerate_all_extensions(n: int) -> list[tuple[list[int], int]]:
97
+ """
98
+ Enumerate all possible cycle structures for n and count their extensions.
99
+
100
+ Args:
101
+ n: Size of the derangement
102
+
103
+ Returns:
104
+ List of tuples (cycle_structure, extensions_count) sorted by extensions_count
105
+ """
106
+ structures = generate_all_cycle_structures(n)
107
+ results = []
108
+
109
+ for cycle_lengths in structures:
110
+ p = create_cycle_structure(cycle_lengths)
111
+ extensions = count_extensions(p)
112
+ results.append((cycle_lengths, extensions))
113
+
114
+ # Sort by extensions count (descending), then by cycle structure
115
+ results.sort(key=lambda x: (-x[1], x[0]))
116
+ return results
117
+
118
+
119
+ def main() -> None:
120
+ """Main function with command-line interface for Latin rectangle extension counting."""
121
+ parser = argparse.ArgumentParser(
122
+ description="Latin Rectangles Extension Counter",
123
+ formatter_class=argparse.RawDescriptionHelpFormatter,
124
+ epilog="""
125
+ Examples:
126
+ %(prog)s --n 42 # Generate random derangement for n=42
127
+ %(prog)s --c "2,2,4" # Use specific cycle structure: two 2-cycles and one 4-cycle
128
+ %(prog)s --c "8" # Single 8-cycle
129
+ %(prog)s --c "2,2,2,2" # Four 2-cycles
130
+ %(prog)s --n 8 --all # Enumerate all possible cycle structures for n=8
131
+ """,
132
+ )
133
+ # Add --n option for backward compatibility
134
+ parser.add_argument("--n", type=int, help="Size of the derangement (must be > 1)")
135
+ parser.add_argument("--c", type=str, help="Cycle structure as comma-separated integers (e.g., '2,2,4' for two 2-cycles and one 4-cycle)")
136
+ parser.add_argument("--all", action="store_true", help="Enumerate all possible cycle structures for given n (use with --n)")
137
+
138
+ args = parser.parse_args()
139
+
140
+ if args.n and args.c:
141
+ print("❌ Error: Cannot specify both --n and --c arguments", file=sys.stderr)
142
+ sys.exit(1)
143
+
144
+ if args.c and args.all:
145
+ print("❌ Error: Cannot use --all with --c (use --all with --n)", file=sys.stderr)
146
+ sys.exit(1)
147
+
148
+ if not args.n and not args.c:
149
+ parser.print_help()
150
+ sys.exit(1)
151
+
152
+ try:
153
+ if args.n and args.all:
154
+ # Enumerate all cycle structures mode
155
+ results = enumerate_all_extensions(args.n)
156
+ if not results:
157
+ print(f"❌ No valid cycle structures found for n={args.n}")
158
+ sys.exit(1)
159
+
160
+ print(f"🔍 All Cycle Structures for n={args.n}")
161
+ print(f"📊 Found {len(results)} possible structures with non-zero extensions:")
162
+ print()
163
+
164
+ for i, (cycle_structure, extensions) in enumerate(results, 1):
165
+ if extensions > 0: # Only show structures with non-zero extensions
166
+ print(f"{i:2d}. {cycle_structure} → {extensions:,} extensions")
167
+
168
+ elif args.n:
169
+ # Generate random derangement mode
170
+ n_val, cycle_lengths, extensions = count_random_extensions(args.n)
171
+ print(f"🎲 Generated Random Derangement for n={n_val}")
172
+ print(f"📊 Cycle structure: {cycle_lengths}")
173
+ print(f"🔢 Number of extensions: {extensions:,}")
174
+ elif args.c:
175
+ # Specific cycle structure mode
176
+ n_val, cycle_lengths, extensions = count_cycle_structure_extensions(args.c)
177
+ print(f"⚙️ Specific Cycle Structure for n={n_val}")
178
+ print(f"📊 Cycle structure: {cycle_lengths}")
179
+ print(f"🔢 Number of extensions: {extensions:,}")
180
+ except ValueError as e:
181
+ print(f"❌ Error: {e}", file=sys.stderr)
182
+ sys.exit(1)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,107 @@
1
+ """Functions for generating and working with derangements."""
2
+
3
+ import random
4
+
5
+
6
+ def generate_random_derangement(n: int) -> list[int]:
7
+ """
8
+ Quickly generates a random derangement of length n.
9
+ A derangement is a permutation p of {1, ..., n} such that p[i] != i.
10
+
11
+ Args:
12
+ n: The size of the derangement.
13
+
14
+ Returns:
15
+ A list of length n+1 representing the derangement (1-indexed).
16
+
17
+ Raises:
18
+ ValueError: If n=1 (no derangements exist) or n < 0.
19
+ """
20
+ if n == 1:
21
+ raise ValueError("No derangements exist for n=1.")
22
+ if n < 0:
23
+ raise ValueError("n must be non-negative.")
24
+ if n == 0:
25
+ return [0]
26
+
27
+ while True:
28
+ # Create a list of numbers from 1 to n
29
+ p = list(range(1, n + 1))
30
+ # Shuffle the list to get a random permutation
31
+ random.shuffle(p)
32
+
33
+ # Check if it's a derangement (p[i] != i+1 for 0-indexed list)
34
+ is_derangement = True
35
+ for i in range(n):
36
+ if p[i] == i + 1:
37
+ is_derangement = False
38
+ break
39
+
40
+ if is_derangement:
41
+ # Prepend a 0 for 1-based indexing and return
42
+ return [0, *p]
43
+
44
+
45
+ def find_cycle_decomposition(p: list[int]) -> list[list[int]]:
46
+ """
47
+ Finds the cycle decomposition of a permutation.
48
+ Permutation p is 1-indexed, so p[0] is ignored.
49
+
50
+ Args:
51
+ p: 1-indexed permutation where p[0] is ignored.
52
+
53
+ Returns:
54
+ List of cycles, where each cycle is represented as a list of indices.
55
+ """
56
+ n = len(p) - 1
57
+ visited = [False] * (n + 1)
58
+ cycles = []
59
+ for i in range(1, n + 1):
60
+ if not visited[i]:
61
+ current_cycle = []
62
+ j = i
63
+ while not visited[j]:
64
+ visited[j] = True
65
+ current_cycle.append(j)
66
+ j = p[j]
67
+ cycles.append(current_cycle)
68
+ return cycles
69
+
70
+
71
+ def create_cycle_structure(cycle_lengths: list[int]) -> list[int]:
72
+ """
73
+ Create a derangement with a specific cycle structure.
74
+
75
+ Args:
76
+ cycle_lengths: List of desired cycle lengths
77
+
78
+ Returns:
79
+ 1-indexed permutation with the specified cycle structure
80
+
81
+ Raises:
82
+ ValueError: If cycle_lengths contains a 1-cycle (would create fixed point)
83
+ """
84
+ if 1 in cycle_lengths:
85
+ raise ValueError("Cycle structure cannot contain 1-cycles (would create fixed points)")
86
+
87
+ n = sum(cycle_lengths)
88
+ if n == 0:
89
+ return [0]
90
+
91
+ perm = [0] * (n + 1) # 1-indexed with 0 at start
92
+ current_pos = 1
93
+
94
+ for cycle_len in cycle_lengths:
95
+ # Get positions for this cycle
96
+ cycle_positions = list(range(current_pos, current_pos + cycle_len))
97
+
98
+ # Create the cycle: each position points to the next, last points to first
99
+ for i in range(cycle_len):
100
+ perm[cycle_positions[i]] = cycle_positions[(i + 1) % cycle_len]
101
+
102
+ current_pos += cycle_len
103
+
104
+ return perm
105
+
106
+
107
+ __all__ = ["find_cycle_decomposition", "generate_random_derangement", "create_cycle_structure"]
@@ -0,0 +1,49 @@
1
+ """Main extension counting algorithm for Latin rectangles."""
2
+
3
+ import math
4
+
5
+ from .derangements import find_cycle_decomposition
6
+ from .rook_polynomials import get_rook_polynomial_for_cycle, multiply_polynomials
7
+
8
+
9
+ def count_extensions(permutation: list[int]) -> int:
10
+ """
11
+ Calculates the number of ways to extend a 2xn Latin rectangle to a 3xn one.
12
+ This is the most robust and general implementation.
13
+
14
+ Args:
15
+ permutation: A list representing the second row, assuming the first row is
16
+ (1, 2, ..., n). The list should be 1-indexed, so its
17
+ length is n+1 and permutation[0] can be a dummy value.
18
+ It must be a derangement.
19
+
20
+ Returns:
21
+ The integer number of possible third rows.
22
+
23
+ Raises:
24
+ ValueError: If the input permutation is not a derangement.
25
+ """
26
+ n = len(permutation) - 1
27
+ if any(i == val for i, val in enumerate(permutation[1:], 1)):
28
+ raise ValueError("Input permutation must be a derangement (p(i) != i).")
29
+
30
+ # 1. Find the cycle decomposition of the permutation
31
+ cycles = find_cycle_decomposition(permutation)
32
+
33
+ # 2. Get the total rook polynomial by multiplying the polynomials of the sub-problems
34
+ total_rook_poly = [1] # Start with the polynomial "1"
35
+ for cycle in cycles:
36
+ k = len(cycle)
37
+ cycle_rook_poly = get_rook_polynomial_for_cycle(k)
38
+ total_rook_poly = multiply_polynomials(total_rook_poly, cycle_rook_poly)
39
+
40
+ # 3. Apply the Principle of Inclusion-Exclusion to get the final count.
41
+ total_ways = 0
42
+ for k, h_k in enumerate(total_rook_poly):
43
+ term = ((-1) ** k) * h_k * math.factorial(n - k)
44
+ total_ways += term
45
+
46
+ return total_ways
47
+
48
+
49
+ __all__ = ["count_extensions"]
File without changes
@@ -0,0 +1,71 @@
1
+ """Rook polynomial calculations for Latin rectangles."""
2
+
3
+ import math
4
+
5
+ # Memoization cache for rook polynomials to avoid re-computation
6
+ _ROOK_POLY_CACHE: dict[int, list[int]] = {}
7
+
8
+
9
+ def get_rook_polynomial_for_cycle(k: int) -> list[int]:
10
+ """
11
+ Calculates the rook polynomial for the forbidden board of a k-cycle.
12
+ The formula for the j-th coefficient is taken from the Menage problem:
13
+ r_j(k) = (2k / (2k - j)) * C(2k - j, j)
14
+ where C is the binomial coefficient "n-choose-k".
15
+
16
+ Args:
17
+ k: The cycle length.
18
+
19
+ Returns:
20
+ List of coefficients for the rook polynomial.
21
+ """
22
+ if k in _ROOK_POLY_CACHE:
23
+ return _ROOK_POLY_CACHE[k]
24
+
25
+ # The rook polynomial has degree k, so it has k+1 coefficients.
26
+ coeffs = [0] * (k + 1)
27
+
28
+ # r_0 is always 1
29
+ coeffs[0] = 1
30
+
31
+ for j in range(1, k + 1):
32
+ # This handles the case j=2k, where the denominator would be zero.
33
+ # In that situation, the binomial coefficient C(0, 2k) is 0 anyway.
34
+ if (2 * k - j) < j:
35
+ # C(n, k) is 0 if k > n
36
+ coeffs[j] = 0
37
+ continue
38
+
39
+ numerator = 2 * k
40
+ denominator = 2 * k - j
41
+
42
+ # We use integer division `//` as the result is always an integer.
43
+ # This keeps calculations exact and avoids floating point issues.
44
+ term1 = (numerator * math.comb(denominator, j)) // denominator
45
+ coeffs[j] = term1
46
+
47
+ _ROOK_POLY_CACHE[k] = coeffs
48
+ return coeffs
49
+
50
+
51
+ def multiply_polynomials(poly1: list[int], poly2: list[int]) -> list[int]:
52
+ """
53
+ Multiplies two polynomials given as lists of coefficients.
54
+
55
+ Args:
56
+ poly1: First polynomial as list of coefficients.
57
+ poly2: Second polynomial as list of coefficients.
58
+
59
+ Returns:
60
+ Product polynomial as list of coefficients.
61
+ """
62
+ len1, len2 = len(poly1), len(poly2)
63
+ new_len = len1 + len2 - 1
64
+ result_poly = [0] * new_len
65
+ for i in range(len1):
66
+ for j in range(len2):
67
+ result_poly[i + j] += poly1[i] * poly2[j]
68
+ return result_poly
69
+
70
+
71
+ __all__ = ["get_rook_polynomial_for_cycle", "multiply_polynomials"]
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: latin-rectangles
3
+ Version: 0.1.0
4
+ Summary: Count the number of one-row extensions of Latin rectangles.
5
+ Author-email: Ioannis Michaloliakos <ioannis.michalol@ufl.edu>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Latin Rectangles Extension Counter
11
+
12
+ A high-performance Python library for counting the number of ways to extend a 2×n [Latin rectangle](https://en.wikipedia.org/wiki/Latin_rectangle) to a 3×n Latin rectangle using rook polynomial methods and cycle decomposition theory.
13
+
14
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
16
+
17
+ ## Overview
18
+
19
+ 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.
20
+
21
+ ### Extension Problem
22
+
23
+ Given a 2×n Latin rectangle:
24
+
25
+ ```text
26
+ 1 2 3 4 5 6 7 8
27
+ p[1] p[2] p[3] p[4] p[5] p[6] p[7] p[8]
28
+ ```
29
+
30
+ where `p` is a [derangement](https://en.wikipedia.org/wiki/Derangement), the problem is to count how many valid third rows can be added such that the resulting 3×n rectangle remains a Latin rectangle. This library provides an efficient algorithm for computing Latin rectangle extensions.
31
+
32
+ ### Key Features
33
+
34
+ - **High Performance**: Approximate O(n^2) time complexity, tested **up to n=800**.
35
+ - **Memory Efficient**: Approximate O(n^1.36) memory complexity
36
+ - **Mathematically Rigorous**: Based on rook polynomial theory and cycle decomposition
37
+ - **Easy to Use**: Simple command-line interface and Python API
38
+ - **Well Tested**: Comprehensive test suite with complexity analysis
39
+
40
+ ## Installation
41
+
42
+ ```console
43
+ git clone https://github.com/ionmich/latin-rectangles.git
44
+ cd latin-rectangles
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ### Command Line (CLI) Usage
50
+
51
+ Generate random derangement:
52
+
53
+ ```console
54
+ > uv run python -m latin_rectangles --n 42
55
+ 🎲 Generated Random Derangement for n=42
56
+ 📊 Cycle structure: [2, 2, 4, 8, 26]
57
+ 🔢 Number of extensions: 185,566,788,772,996,286,199,647,931,971,186,844,003,087,641,029,824
58
+ ```
59
+
60
+ Use specific cycle structure:
61
+
62
+ ```console
63
+ > uv run python -m latin_rectangles --c "2,2,4"
64
+ ⚙️ Specific Cycle Structure for n=8
65
+ 📊 Cycle structure: [2, 2, 4]
66
+ 🔢 Number of extensions: 4,744
67
+ ```
68
+
69
+ Enumerate all possible cycle structures:
70
+
71
+ ```console
72
+ > uv run python -m latin_rectangles --n 8 --all
73
+ 🔍 All Cycle Structures for n=8
74
+ 📊 Found 7 possible structures with non-zero extensions:
75
+
76
+ 1. [2, 2, 2, 2] → 4,752 extensions
77
+ 2. [2, 2, 4] → 4,744 extensions
78
+ 3. [2, 3, 3] → 4,740 extensions
79
+ 4. [2, 6] → 4,740 extensions
80
+ 5. [4, 4] → 4,740 extensions
81
+ 6. [3, 5] → 4,738 extensions
82
+ 7. [8] → 4,738 extensions
83
+ ```
84
+
85
+ ## Get help
86
+
87
+ ```console
88
+ uv run latin-rectangles --help
89
+ ```
90
+
91
+ ## Python Library Usage
92
+
93
+ ```python
94
+ from latin_rectangles import count_extensions, count_random_extensions
95
+ from latin_rectangles import generate_random_derangement
96
+
97
+ # Method 1: One-liner for random derangement
98
+ extensions = count_random_extensions(n=12)
99
+ print(f"Extensions: {extensions:,}")
100
+
101
+ # Method 2: Step-by-step with custom derangement
102
+ derangement = generate_random_derangement(n=10)
103
+ extensions = count_extensions(derangement)
104
+ print(f"Derangement {derangement[1:]} has {extensions:,} extensions")
105
+
106
+ # Method 3: With predefined derangement (1-indexed with dummy 0)
107
+ p = [0, 2, 3, 4, 5, 6, 7, 8, 1] # 8-cycle for n=8
108
+ extensions = count_extensions(p)
109
+ print(f"8-cycle has {extensions:,} extensions") # Output: 4,738
110
+ ```
111
+
112
+ ## Algorithm Details
113
+
114
+ ### Mathematical Foundation
115
+
116
+ The algorithm leverages **rook polynomial theory** to solve the Latin rectangle extension problem:
117
+
118
+ 1. **Input**: A derangement (permutation with no fixed points) representing the second row
119
+ 2. **Cycle Decomposition**: Decompose the derangement into disjoint cycles
120
+ 3. **Rook Polynomials**: Compute rook polynomial for each cycle structure
121
+ 4. **Polynomial Multiplication**: Combine rook polynomials to get the final count
122
+
123
+ ## API Reference
124
+
125
+ ### Core Functions
126
+
127
+ #### `count_extensions(permutation: list[int]) -> int`
128
+
129
+ Counts the number of extensions for a given derangement.
130
+
131
+ **Parameters:**
132
+
133
+ - `permutation`: 1-indexed list representing a derangement (p[0] is dummy value)
134
+
135
+ **Returns:** Integer number of possible third rows
136
+
137
+ **Raises:** `ValueError` if input is not a derangement
138
+
139
+ #### `count_random_extensions(n: int) -> int`
140
+
141
+ Convenience function that generates a random derangement and counts its extensions.
142
+
143
+ **Parameters:**
144
+
145
+ - `n`: Size of the derangement (must be > 1)
146
+
147
+ **Returns:** Number of extensions for the randomly generated derangement
148
+
149
+ #### `generate_random_derangement(n: int) -> list[int]`
150
+
151
+ Generates a random derangement of size n.
152
+
153
+ **Parameters:**
154
+
155
+ - `n`: Size of the derangement
156
+
157
+ **Returns:** 1-indexed list representing the derangement
158
+
159
+ #### `find_cycle_decomposition(permutation: list[int]) -> list[list[int]]`
160
+
161
+ Finds the cycle decomposition of a permutation.
162
+
163
+ **Parameters:**
164
+
165
+ - `permutation`: 1-indexed permutation
166
+
167
+ **Returns:** List of cycles (each cycle is a list of indices)
168
+
169
+ ## Examples
170
+
171
+ ### Basic Usage Examples
172
+
173
+ ```python
174
+ from latin_rectangles import count_extensions
175
+
176
+ # Example 1: Single 8-cycle
177
+ p_8_cycle = [0, 2, 3, 4, 5, 6, 7, 8, 1]
178
+ print(f"8-cycle: {count_extensions(p_8_cycle):,} extensions")
179
+ # Output: 8-cycle: 4,738 extensions
180
+
181
+ # Example 2: Two 4-cycles
182
+ p_4_4 = [0, 2, 3, 4, 1, 6, 7, 8, 5]
183
+ print(f"4,4-cycles: {count_extensions(p_4_4):,} extensions")
184
+ # Output: 4,4-cycles: 4,740 extensions
185
+
186
+ # Example 3: Four 2-cycles
187
+ p_2_2_2_2 = [0, 2, 1, 4, 3, 6, 5, 8, 7]
188
+ print(f"2,2,2,2-cycles: {count_extensions(p_2_2_2_2):,} extensions")
189
+ # Output: 2,2,2,2-cycles: 4,752 extensions
190
+ ```
191
+
192
+ ### Advanced Usage
193
+
194
+ ```python
195
+ from latin_rectangles import generate_random_derangement, find_cycle_decomposition, count_extensions
196
+
197
+ # Generate and analyze a random derangement
198
+ n = 15
199
+ derangement = generate_random_derangement(n)
200
+ cycles = find_cycle_decomposition(derangement)
201
+ cycle_lengths = sorted([len(c) for c in cycles])
202
+ extensions = count_extensions(derangement)
203
+
204
+ print(f"n={n}")
205
+ print(f"Derangement: {derangement[1:]}")
206
+ print(f"Cycle structure: {cycle_lengths}")
207
+ print(f"Extensions: {extensions:,}")
208
+ ```
209
+
210
+ ### Batch Processing
211
+
212
+ ```python
213
+ from latin_rectangles import count_random_extensions
214
+
215
+ # Process multiple sizes
216
+ results = []
217
+ for n in range(5, 21):
218
+ extensions = count_random_extensions(n)
219
+ results.append((n, extensions))
220
+ print(f"n={n:2d}: {extensions:,} extensions")
221
+
222
+ # Find the size with the most extensions in this batch
223
+ max_n, max_extensions = max(results, key=lambda x: x[1])
224
+ print(f"Maximum: n={max_n} with {max_extensions:,} extensions")
225
+ ```
226
+
227
+ ## Development
228
+
229
+ ### Running Tests
230
+
231
+ ```bash
232
+ # Run the test suite
233
+ uv run pytest
234
+
235
+ # Run with coverage
236
+ uv run pytest --cov=latin_rectangles
237
+
238
+ # Run specific test
239
+ uv run pytest tests/test_main.py -v
240
+ ```
241
+
242
+ ### Code Quality
243
+
244
+ ```bash
245
+ # Type checking
246
+ uv run mypy src/
247
+
248
+ # Linting
249
+ uv run ruff check src/
250
+
251
+ # Formatting
252
+ uv run ruff format src/
253
+ ```
254
+
255
+ ### Benchmarking
256
+
257
+ ```bash
258
+ # Run performance benchmarks
259
+ uv run python benchmark.py
260
+
261
+ # Analyze complexity
262
+ uv run python complexity_analysis.py
263
+ ```
264
+
265
+ ## Contributing
266
+
267
+ Contributions are welcome! Please see [DEVELOPMENT.md](DEVELOPMENT.md) for development guidelines.
268
+
269
+ 1. Fork the repository
270
+ 2. Create a feature branch
271
+ 3. Add tests for new functionality
272
+ 4. Ensure all tests pass
273
+ 5. Submit a pull request
274
+
275
+ ## License
276
+
277
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
278
+
279
+ ## Citation
280
+
281
+ If you use this library in your research, please cite:
282
+
283
+ ```bibtex
284
+ @software{latin_rectangles,
285
+ title={Latin Rectangles Extension Counter},
286
+ author={Ioannis Michaloliakos},
287
+ year={2025},
288
+ url={https://github.com/ionmich/latin-rectangles}
289
+ }
290
+ ```
@@ -0,0 +1,11 @@
1
+ latin_rectangles/__init__.py,sha256=1JsPv1Ts57JLS9jBzlddwJlez5Lu_AMZXTs9hHcc_jU,1161
2
+ latin_rectangles/__main__.py,sha256=uudTFbM8QL7i8RELhIAdZSraEF6YrPLWXrG_tK358eQ,6554
3
+ latin_rectangles/derangements.py,sha256=psrC9BffezFnUrc4ls4JDiN8BgmiM_WGKGSfP0tUjA4,3045
4
+ latin_rectangles/extension_counting.py,sha256=aC8Wi3g6AH0w1Guzw3QD1ZV_ZzohxLbefqaOJ3LLDW4,1757
5
+ latin_rectangles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ latin_rectangles/rook_polynomials.py,sha256=8cCzYevAAhypnAmYgsgDHZtypgYvcWa6f8lMe8hmcPg,2114
7
+ latin_rectangles-0.1.0.dist-info/METADATA,sha256=ur7BUyMCtOTK3fqKw29vdMt63hFrgo7ymP4NM_WU7xA,7798
8
+ latin_rectangles-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ latin_rectangles-0.1.0.dist-info/entry_points.txt,sha256=Y9TXJJPwmtEtDKSTeClqgM-6bYFsviRreYL-R30UUWE,68
10
+ latin_rectangles-0.1.0.dist-info/licenses/LICENSE,sha256=B7vY06ZLK_DfSNp5Pzqi4h5NAPAVzPI8T58yMNr9VDE,1078
11
+ latin_rectangles-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ latin-rectangles = latin_rectangles.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ioannis Michaloliakos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.