def PBD_4_7(v, check=True, existence=False): r""" Return a `(v,\{4,7\})`-PBD For all `v` such that `n\equiv 1\pmod{3}` and `n\neq 10,19, 31` there exists a `(v,\{4,7\})`-PBD. This is proved in Proposition IX.4.5 from [BJL99]_, which this method implements. This construction of PBD is used by the construction of Kirkman Triple Systems. EXAMPLE:: sage: from sage.combinat.designs.resolvable_bibd import PBD_4_7 sage: PBD_4_7(22) Pairwise Balanced Design on 22 points with sets of sizes in set([4, 7]) TESTS: All values `\leq 300`:: sage: for i in range(1,300,3): ....: if i not in [10,19,31]: ....: assert PBD_4_7(i,existence=True) ....: _ = PBD_4_7(i,check=True) """ if v % 3 != 1 or v in [10, 19, 31]: if existence: return Unknown raise NotImplementedError if existence: return True from .group_divisible_designs import GroupDivisibleDesign from .group_divisible_designs import GDD_4_2 from .bibd import PairwiseBalancedDesign from .bibd import balanced_incomplete_block_design if v == 22: # Beth/Jungnickel/Lenz: take KTS(15) and extend each of the 7 classes # with a new point. Make those new points a 7-set. KTS15 = kirkman_triple_system(15) blocks = [ S + [i + 15] for i, classs in enumerate(KTS15._classes) for S in classs ] + [list(range(15, 22))] elif v == 34: # [BJL99] (p527,vol1), but originally Brouwer A = [(0, 0), (1, 1), (2, 0), (4, 1)] B = [(0, 0), (1, 0), (4, 2)] C = [(0, 0), (2, 2), (5, 0)] D = [(0, 0), (0, 1), (0, 2)] A = [[(x + i, y + j) for x, y in A] for i in range(9) for j in range(3)] B = [[(x + i, y + i + j) for x, y in B] + [27 + j] for i in range(9) for j in range(3)] C = [[(x + i + j, y + 2 * i + j) for x, y in C] + [30 + j] for i in range(9) for j in range(3)] D = [[(x + i, y + i) for x, y in D] + [33] for i in range(9)] blocks = [[ int(x) if not isinstance(x, tuple) else (x[1] % 3) * 9 + (x[0] % 9) for x in S ] for S in A + B + C + D + [list(range(27, 34))]] elif v == 46: # [BJL99] (p527,vol1), but originally Brouwer A = [(1, 0), (3, 0), (9, 0), (0, 1)] B = [(2, 0), (6, 0), (5, 0), (0, 1)] C = [(0, 0), (1, 1), (4, 2)] D = [(0, 0), (2, 1), (7, 2)] E = [(0, 0), (0, 1), (0, 2)] A = [[(x + i, y + j) for x, y in A] for i in range(13) for j in range(3)] B = [[(x + i, y + j) for x, y in B] for i in range(13) for j in range(3)] C = [[(x + i, y + j) for x, y in C] + [39 + j] for i in range(13) for j in range(3)] D = [[(x + i, y + j) for x, y in D] + [42 + j] for i in range(13) for j in range(3)] E = [[(x + i, y + i) for x, y in E] + [45] for i in range(13)] blocks = [[ int(x) if not isinstance(x, tuple) else (x[1] % 3) * 13 + (x[0] % 13) for x in S ] for S in A + B + C + D + E + [list(range(39, 46))]] elif v == 58: # [BJL99] (p527,vol1), but originally Brouwer A = [(0, 0), (1, 0), (4, 0), (5, 1)] B = [(0, 0), (2, 0), (8, 0), (11, 1)] C = [(0, 0), (5, 0), (2, 1), (12, 1)] D = [(0, 0), (8, 1), (7, 2)] E = [(0, 0), (6, 1), (4, 2)] F = [(0, 0), (0, 1), (0, 2)] A = [[(x + i, y + j) for x, y in A] for i in range(17) for j in range(3)] B = [[(x + i, y + j) for x, y in B] for i in range(17) for j in range(3)] C = [[(x + i, y + j) for x, y in C] for i in range(17) for j in range(3)] D = [[(x + i, y + j) for x, y in D] + [51 + j] for i in range(17) for j in range(3)] E = [[(x + i, y + j) for x, y in E] + [54 + j] for i in range(17) for j in range(3)] F = [[(x + i, y + i) for x, y in F] + [57] for i in range(17)] blocks = [[ int(x) if not isinstance(x, tuple) else (x[1] % 3) * 17 + (x[0] % 17) for x in S ] for S in A + B + C + D + E + F + [list(range(51, 58))]] elif v == 70: # [BJL99] (p527,vol1), but originally Brouwer A = [(0, 0), (1, 0), (5, 1), (13, 1)] B = [(0, 0), (4, 0), (20, 1), (10, 1)] C = [(0, 0), (16, 0), (17, 1), (19, 1)] D = [(0, 0), (2, 1), (8, 1), (11, 1)] E = [(0, 0), (3, 2), (9, 1)] F = [(0, 0), (7, 0), (14, 1)] H = [(0, 0), (0, 1), (0, 2)] A = [[(x + i, y + j) for x, y in A] for i in range(21) for j in range(3)] B = [[(x + i, y + j) for x, y in B] for i in range(21) for j in range(3)] C = [[(x + i, y + j) for x, y in C] for i in range(21) for j in range(3)] D = [[(x + i, y + j) for x, y in D] for i in range(21) for j in range(3)] E = [[(x + i, y + j) for x, y in E] + [63 + j] for i in range(21) for j in range(3)] F = [[(x + 3 * i + j, y + ii + j) for x, y in F] + [66 + j] for i in range(7) for j in range(3) for ii in range(3)] H = [[(x + i, y + i) for x, y in H] + [69] for i in range(21)] blocks = [[ int(x) if not isinstance(x, tuple) else (x[1] % 3) * 21 + (x[0] % 21) for x in S ] for S in A + B + C + D + E + F + H + [list(range(63, 70))]] elif v == 82: # This construction is Theorem IX.3.16 from [BJL99] (p.627). # # A (15,{4},{3})-GDD from a (16,4)-BIBD from .group_divisible_designs import group_divisible_design from .orthogonal_arrays import transversal_design GDD = group_divisible_design(3 * 5, K=[4], G=[3], check=False) TD = transversal_design(5, 5) # A (75,{4},{15})-GDD GDD2 = [[3 * B[x // 3] + x % 3 for x in BB] for B in TD for BB in GDD] # We now complete the (75,{4},{15})-GDD into a (82,{4,7})-PBD. For this, # we add 7 new points that are added to all groups of size 15. # # On these groups a (15+7,{4,7})-PBD is pasted, in such a way that the 7 # new points are a set of the final PBD PBD22 = PBD_4_7(15 + 7) S = next(SS for SS in PBD22 if len(SS) == 7) # a set of size 7 PBD22.relabel({ v: i for i, v in enumerate([i for i in range(15 + 7) if i not in S] + S) }) for B in PBD22: if B == S: continue for i in range(5): GDD2.append([x + i * 15 if x < 15 else x + 60 for x in B]) GDD2.append(list(range(75, 82))) blocks = GDD2 elif v == 94: # IX.4.5.l from [BJL99]. # # take 4 parallel lines from an affine plane of order 7, and a 5th # one. This is a (31,{4,5,7})-BIBD. And 94=3*31+1. from sage.combinat.designs.block_design import AffineGeometryDesign AF = AffineGeometryDesign(2, 1, 7) parall = [] plus_one = None for S in AF: if all(all(x not in SS for x in S) for SS in parall): parall.append(S) elif plus_one is None: plus_one = S if len(parall) == 4 and plus_one is not None: break X = set(sum(parall, plus_one)) S_4_5_7 = [X.intersection(S) for S in AF] S_4_5_7 = [S for S in S_4_5_7 if len(S) > 1] S_4_5_7 = PairwiseBalancedDesign(X, blocks=S_4_5_7, K=[4, 5, 7], check=False) S_4_5_7.relabel() return PBD_4_7_from_Y(S_4_5_7, check=check) elif v == 127 or v == 142: # IX.4.5.o from [BJL99]. # # Attach two or seven infinite points to a (40,4)-RBIBD to get a # (42,{4,5},{1,2,7})-GDD or a (47,{4,5},{1,2,7})-GDD points_to_add = 2 if v == 127 else 7 rBIBD4 = v_4_1_rbibd(40) GDD = [ S + [40 + i] if i < points_to_add else S for i, classs in enumerate(rBIBD4._classes) for S in classs ] if points_to_add == 7: GDD.append(list(range(40, 40 + points_to_add))) groups = [[x] for x in range(40 + points_to_add)] else: groups = [[x] for x in range(40)] groups.append(list(range(40, 40 + points_to_add))) GDD = GroupDivisibleDesign(40 + points_to_add, groups=groups, blocks=GDD, K=[2, 4, 5, 7], check=False, copy=False) return PBD_4_7_from_Y(GDD, check=check) elif v % 6 == 1 and GDD_4_2((v - 1) // 6, existence=True): # VII.5.17 from [BJL99] gdd = GDD_4_2((v - 1) // 6) return PBD_4_7_from_Y(gdd, check=check) elif v == 202: # IV.4.5.p from [BJL99] PBD = PBD_4_7(22, check=False) PBD = PBD_4_7_from_Y(PBD, check=False) return PBD_4_7_from_Y(PBD, check=check) elif balanced_incomplete_block_design(v, 4, existence=True): return balanced_incomplete_block_design(v, 4) elif balanced_incomplete_block_design(v, 7, existence=True): return balanced_incomplete_block_design(v, 7) else: from sage.combinat.designs.orthogonal_arrays import orthogonal_array # IX.4.5.m from [BJL99]. # # This construction takes a TD(5,g) and truncates its last column to # size u: it yields a (4g+u,{4,5},{g,u})-GDD. If there exists a # (3g+1,{4,7})-PBD and a (3u+1,{4,7})-PBD, then we can apply the x->3x+1 # construction on the truncated transversal design (which is a GDD). # # We write vv = 4g+u while satisfying the hypotheses. vv = (v - 1) // 3 for g in range((vv + 5 - 1) // 5, vv // 4 + 1): u = vv - 4 * g if (orthogonal_array(5, g, existence=True) and PBD_4_7(3 * g + 1, existence=True) and PBD_4_7(3 * u + 1, existence=True)): from .orthogonal_arrays import transversal_design domain = set(range(vv)) GDD = transversal_design(5, g) GDD = GroupDivisibleDesign( vv, groups=[[x for x in gr if x in domain] for gr in GDD.groups()], blocks=[[x for x in B if x in domain] for B in GDD], G=set([g, u]), K=[4, 5], check=False) return PBD_4_7_from_Y(GDD, check=check) return PairwiseBalancedDesign(v, blocks=blocks, K=[4, 7], check=check, copy=False)
def PBD_4_7(v,check=True, existence=False): r""" Return a `(v,\{4,7\})`-PBD For all `v` such that `n\equiv 1\pmod{3}` and `n\neq 10,19, 31` there exists a `(v,\{4,7\})`-PBD. This is proved in Proposition IX.4.5 from [BJL99]_, which this method implements. This construction of PBD is used by the construction of Kirkman Triple Systems. EXAMPLE:: sage: from sage.combinat.designs.resolvable_bibd import PBD_4_7 sage: PBD_4_7(22) Pairwise Balanced Design on 22 points with sets of sizes in set([4, 7]) TESTS: All values `\leq 300`:: sage: for i in range(1,300,3): ....: if i not in [10,19,31]: ....: assert PBD_4_7(i,existence=True) ....: _ = PBD_4_7(i,check=True) """ if v%3 != 1 or v in [10,19,31]: if existence: return Unknown raise NotImplementedError if existence: return True from group_divisible_designs import GroupDivisibleDesign from group_divisible_designs import GDD_4_2 from bibd import PairwiseBalancedDesign from bibd import balanced_incomplete_block_design if v == 22: # Beth/Jungnickel/Lenz: take KTS(15) and extend each of the 7 classes # with a new point. Make those new points a 7-set. KTS15 = kirkman_triple_system(15) blocks = [S+[i+15] for i,classs in enumerate(KTS15._classes) for S in classs]+[range(15,22)] elif v == 34: # [BJL99] (p527,vol1), but originally Brouwer A = [(0,0),(1,1),(2,0),(4,1)] B = [(0,0),(1,0),(4,2)] C = [(0,0),(2,2),(5,0)] D = [(0,0),(0,1),(0,2)] A = [[(x+i, y+j) for x,y in A] for i in range(9) for j in range(3)] B = [[(x+i, y+i+j) for x,y in B]+[27+j] for i in range(9) for j in range(3)] C = [[(x+i+j,y+2*i+j) for x,y in C]+[30+j] for i in range(9) for j in range(3)] D = [[(x+i, y+i) for x,y in D]+[33] for i in range(9)] blocks = [[int(x) if not isinstance(x,tuple) else (x[1]%3)*9+(x[0]%9) for x in S] for S in A+B+C+D+[range(27,34)]] elif v == 46: # [BJL99] (p527,vol1), but originally Brouwer A = [(1,0),(3,0),(9,0),(0,1)] B = [(2,0),(6,0),(5,0),(0,1)] C = [(0,0),(1,1),(4,2)] D = [(0,0),(2,1),(7,2)] E = [(0,0),(0,1),(0,2)] A = [[(x+i, y+j) for x,y in A] for i in range(13) for j in range(3)] B = [[(x+i, y+j) for x,y in B] for i in range(13) for j in range(3)] C = [[(x+i, y+j) for x,y in C]+[39+j] for i in range(13) for j in range(3)] D = [[(x+i, y+j) for x,y in D]+[42+j] for i in range(13) for j in range(3)] E = [[(x+i, y+i) for x,y in E]+[45] for i in range(13)] blocks = [[int(x) if not isinstance(x,tuple) else (x[1]%3)*13+(x[0]%13) for x in S] for S in A+B+C+D+E+[range(39,46)]] elif v == 58: # [BJL99] (p527,vol1), but originally Brouwer A = [(0,0),(1,0),(4,0),( 5,1)] B = [(0,0),(2,0),(8,0),(11,1)] C = [(0,0),(5,0),(2,1),(12,1)] D = [(0,0),(8,1),(7,2)] E = [(0,0),(6,1),(4,2)] F = [(0,0),(0,1),(0,2)] A = [[(x+i, y+j) for x,y in A] for i in range(17) for j in range(3)] B = [[(x+i, y+j) for x,y in B] for i in range(17) for j in range(3)] C = [[(x+i, y+j) for x,y in C] for i in range(17) for j in range(3)] D = [[(x+i, y+j) for x,y in D]+[51+j] for i in range(17) for j in range(3)] E = [[(x+i, y+j) for x,y in E]+[54+j] for i in range(17) for j in range(3)] F = [[(x+i, y+i) for x,y in F]+[57] for i in range(17)] blocks = [[int(x) if not isinstance(x,tuple) else (x[1]%3)*17+(x[0]%17) for x in S] for S in A+B+C+D+E+F+[range(51,58)]] elif v == 70: # [BJL99] (p527,vol1), but originally Brouwer A = [(0,0),(1,0),(5,1),(13,1)] B = [(0,0),(4,0),(20,1),(10,1)] C = [(0,0),(16,0),(17,1),(19,1)] D = [(0,0),(2,1),(8,1),(11,1)] E = [(0,0),(3,2),(9,1)] F = [(0,0),(7,0),(14,1)] H = [(0,0),(0,1),(0,2)] A = [[(x+i, y+j) for x,y in A] for i in range(21) for j in range(3)] B = [[(x+i, y+j) for x,y in B] for i in range(21) for j in range(3)] C = [[(x+i, y+j) for x,y in C] for i in range(21) for j in range(3)] D = [[(x+i, y+j) for x,y in D] for i in range(21) for j in range(3)] E = [[(x+i, y+j) for x,y in E]+[63+j] for i in range(21) for j in range(3)] F = [[(x+3*i+j, y+ii+j) for x,y in F]+[66+j] for i in range( 7) for j in range(3) for ii in range(3)] H = [[(x+i, y+i) for x,y in H]+[69] for i in range(21)] blocks = [[int(x) if not isinstance(x,tuple) else (x[1]%3)*21+(x[0]%21) for x in S] for S in A+B+C+D+E+F+H+[range(63,70)]] elif v == 82: # This construction is Theorem IX.3.16 from [BJL99] (p.627). # # A (15,{4},{3})-GDD from a (16,4)-BIBD from group_divisible_designs import group_divisible_design from orthogonal_arrays import transversal_design GDD = group_divisible_design(3*5,K=[4],G=[3],check=False) TD = transversal_design(5,5) # A (75,{4},{15})-GDD GDD2 = [[3*B[x//3]+x%3 for x in BB] for B in TD for BB in GDD] # We now complete the (75,{4},{15})-GDD into a (82,{4,7})-PBD. For this, # we add 7 new points that are added to all groups of size 15. # # On these groups a (15+7,{4,7})-PBD is pasted, in such a way that the 7 # new points are a set of the final PBD PBD22 = PBD_4_7(15+7) S = next(SS for SS in PBD22 if len(SS) == 7) # a set of size 7 PBD22.relabel({v:i for i,v in enumerate([i for i in range(15+7) if i not in S] + S)}) for B in PBD22: if B == S: continue for i in range(5): GDD2.append([x+i*15 if x<15 else x+60 for x in B]) GDD2.append(range(75,82)) blocks = GDD2 elif v == 94: # IX.4.5.l from [BJL99]. # # take 4 parallel lines from an affine plane of order 7, and a 5th # one. This is a (31,{4,5,7})-BIBD. And 94=3*31+1. from sage.combinat.designs.block_design import AffineGeometryDesign AF = AffineGeometryDesign(2,1,7) parall = [] plus_one = None for S in AF: if all(all(x not in SS for x in S) for SS in parall): parall.append(S) elif plus_one is None: plus_one = S if len(parall) == 4 and plus_one is not None: break X = set(sum(parall,plus_one)) S_4_5_7 = [X.intersection(S) for S in AF] S_4_5_7 = [S for S in S_4_5_7 if len(S)>1] S_4_5_7 = PairwiseBalancedDesign(X, blocks = S_4_5_7, K = [4,5,7], check=False) S_4_5_7.relabel() return PBD_4_7_from_Y(S_4_5_7,check=check) elif v == 127 or v == 142: # IX.4.5.o from [BJL99]. # # Attach two or seven infinite points to a (40,4)-RBIBD to get a # (42,{4,5},{1,2,7})-GDD or a (47,{4,5},{1,2,7})-GDD points_to_add = 2 if v == 127 else 7 rBIBD4 = v_4_1_rbibd(40) GDD = [S+[40+i] if i<points_to_add else S for i,classs in enumerate(rBIBD4._classes) for S in classs] if points_to_add == 7: GDD.append(range(40,40+points_to_add)) groups = [[x] for x in range(40+points_to_add)] else: groups = [[x] for x in range(40)] groups.append(range(40,40+points_to_add)) GDD = GroupDivisibleDesign(40+points_to_add, groups = groups, blocks = GDD, K = [2,4,5,7], check = False, copy = False) return PBD_4_7_from_Y(GDD,check=check) elif v%6 == 1 and GDD_4_2((v-1)/6,existence=True): # VII.5.17 from [BJL99] gdd = GDD_4_2((v-1)/6) return PBD_4_7_from_Y(gdd,check=check) elif v == 202: # IV.4.5.p from [BJL99] PBD = PBD_4_7(22,check=False) PBD = PBD_4_7_from_Y(PBD,check=False) return PBD_4_7_from_Y(PBD,check=check) elif balanced_incomplete_block_design(v,4,existence=True): return balanced_incomplete_block_design(v,4) elif balanced_incomplete_block_design(v,7,existence=True): return balanced_incomplete_block_design(v,7) else: from sage.combinat.designs.orthogonal_arrays import orthogonal_array # IX.4.5.m from [BJL99]. # # This construction takes a TD(5,g) and truncates its last column to # size u: it yields a (4g+u,{4,5},{g,u})-GDD. If there exists a # (3g+1,{4,7})-PBD and a (3u+1,{4,7})-PBD, then we can apply the x->3x+1 # construction on the truncated transversal design (which is a GDD). # # We write vv = 4g+u while satisfying the hypotheses. vv = (v-1)/3 for g in range((vv+5-1)//5,vv//4+1): u = vv-4*g if (orthogonal_array(5,g,existence=True) and PBD_4_7(3*g+1,existence=True) and PBD_4_7(3*u+1,existence=True)): from orthogonal_arrays import transversal_design domain = set(range(vv)) GDD = transversal_design(5,g) GDD = GroupDivisibleDesign(vv, groups = [[x for x in gr if x in domain] for gr in GDD.groups()], blocks = [[x for x in B if x in domain] for B in GDD], G = set([g,u]), K = [4,5], check=False) return PBD_4_7_from_Y(GDD,check=check) return PairwiseBalancedDesign(v, blocks = blocks, K = [4,7], check = check, copy = False)
def OrthogonalArrayBlockGraph(k, n, OA=None): r""" Return the graph of an `OA(k,n)`. The intersection graph of the blocks of a transversal design with parameters `(k,n)`, or `TD(k,n)` for short, is a strongly regular graph (unless it is a complete graph). Its parameters `(v,k',\lambda,\mu)` are determined by the parameters `k,n` via: .. MATH:: v=n^2, k'=k(n-1), \lambda=(k-1)(k-2)+n-2, \mu=k(k-1) As transversal designs and orthogonal arrays (OA for short) are equivalent objects, this graph can also be built from the blocks of an `OA(k,n)`, two of them being adjacent if one of their coordinates match. For more information on these graphs, see `Andries Brouwer's page on Orthogonal Array graphs <https://www.win.tue.nl/~aeb/graphs/OA.html>`_. .. WARNING:: - Brouwer's website uses the notation `OA(n,k)` instead of `OA(k,n)` - For given parameters `k` and `n` there can be many `OA(k,n)` : the graphs returned are not uniquely defined by their parameters (see the examples below). - If the function is called only with the parameter ``k`` and ``n`` the results might be different with two versions of Sage, or even worse : some could not be available anymore. .. SEEALSO:: :mod:`sage.combinat.designs.orthogonal_arrays` INPUT: - ``k,n`` (integers) - ``OA`` -- An orthogonal array. If set to ``None`` (default) then :func:`~sage.combinat.designs.orthogonal_arrays.orthogonal_array` is called to compute an `OA(k,n)`. EXAMPLES:: sage: G = graphs.OrthogonalArrayBlockGraph(5,5); G OA(5,5): Graph on 25 vertices sage: G.is_strongly_regular(parameters=True) (25, 20, 15, 20) sage: G = graphs.OrthogonalArrayBlockGraph(4,10); G OA(4,10): Graph on 100 vertices sage: G.is_strongly_regular(parameters=True) (100, 36, 14, 12) Two graphs built from different orthogonal arrays are also different:: sage: k=4;n=10 sage: OAa = designs.orthogonal_arrays.build(k,n) sage: OAb = [[(x+1)%n for x in R] for R in OAa] sage: set(map(tuple,OAa)) == set(map(tuple,OAb)) False sage: Ga = graphs.OrthogonalArrayBlockGraph(k,n,OAa) sage: Gb = graphs.OrthogonalArrayBlockGraph(k,n,OAb) sage: Ga == Gb False As ``OAb`` was obtained from ``OAa`` by a relabelling the two graphs are isomorphic:: sage: Ga.is_isomorphic(Gb) True But there are examples of `OA(k,n)` for which the resulting graphs are not isomorphic:: sage: oa0 = [[0, 0, 1], [0, 1, 3], [0, 2, 0], [0, 3, 2], ....: [1, 0, 3], [1, 1, 1], [1, 2, 2], [1, 3, 0], ....: [2, 0, 0], [2, 1, 2], [2, 2, 1], [2, 3, 3], ....: [3, 0, 2], [3, 1, 0], [3, 2, 3], [3, 3, 1]] sage: oa1 = [[0, 0, 1], [0, 1, 0], [0, 2, 3], [0, 3, 2], ....: [1, 0, 3], [1, 1, 2], [1, 2, 0], [1, 3, 1], ....: [2, 0, 0], [2, 1, 1], [2, 2, 2], [2, 3, 3], ....: [3, 0, 2], [3, 1, 3], [3, 2, 1], [3, 3, 0]] sage: g0 = graphs.OrthogonalArrayBlockGraph(3,4,oa0) sage: g1 = graphs.OrthogonalArrayBlockGraph(3,4,oa1) sage: g0.is_isomorphic(g1) False But nevertheless isospectral:: sage: g0.spectrum() [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, -3, -3, -3, -3, -3, -3] sage: g1.spectrum() [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, -3, -3, -3, -3, -3, -3] Note that the graph ``g0`` is actually isomorphic to the affine polar graph `VO^+(4,2)`:: sage: graphs.AffineOrthogonalPolarGraph(4,2,'+').is_isomorphic(g0) True TESTS:: sage: G = graphs.OrthogonalArrayBlockGraph(4,6) Traceback (most recent call last): ... NotImplementedError: I don't know how to build an OA(4,6)! sage: G = graphs.OrthogonalArrayBlockGraph(8,2) Traceback (most recent call last): ... ValueError: There is no OA(8,2). Beware, Brouwer's website uses OA(n,k) instead of OA(k,n) ! """ if n > 1 and k >= n + 2: raise ValueError( "There is no OA({},{}). Beware, Brouwer's website uses OA(n,k) instead of OA(k,n) !" .format(k, n)) from itertools import combinations if OA is None: from sage.combinat.designs.orthogonal_arrays import orthogonal_array OA = orthogonal_array(k, n) else: assert len(OA) == n**2 assert n == 0 or k == len(OA[0]) OA = map(tuple, OA) d = [[[] for j in range(n)] for i in range(k)] for R in OA: for i, x in enumerate(R): d[i][x].append(R) g = Graph() for l in d: for ll in l: g.add_edges(combinations(ll, 2)) g.name("OA({},{})".format(k, n)) return g
def OrthogonalArrayBlockGraph(k,n,OA=None): r""" Returns the graph of an `OA(k,n)`. The intersection graph of the blocks of a transversal design with parameters `(k,n)`, or `TD(k,n)` for short, is a strongly regular graph (unless it is a complete graph). Its parameters `(v,k',\lambda,\mu)` are determined by the parameters `k,n` via: .. MATH:: v=n^2, k'=k(n-1), \lambda=(k-1)(k-2)+n-2, \mu=k(k-1) As transversal designs and orthogonal arrays (OA for short) are equivalent objects, this graph can also be built from the blocks of an `OA(k,n)`, two of them being adjacent if one of their coordinates match. For more information on these graphs, see `Andries Brouwer's page on Orthogonal Array graphs <www.win.tue.nl/~aeb/graphs/OA.html>`_. .. WARNING:: - Brouwer's website uses the notation `OA(n,k)` instead of `OA(k,n)` - For given parameters `k` and `n` there can be many `OA(k,n)` : the graphs returned are not uniquely defined by their parameters (see the examples below). - If the function is called only with the parameter ``k`` and ``n`` the results might be different with two versions of Sage, or even worse : some could not be available anymore. .. SEEALSO:: :mod:`sage.combinat.designs.orthogonal_arrays` INPUT: - ``k,n`` (integers) - ``OA`` -- An orthogonal array. If set to ``None`` (default) then :func:`~sage.combinat.designs.orthogonal_arrays.orthogonal_array` is called to compute an `OA(k,n)`. EXAMPLES:: sage: G = graphs.OrthogonalArrayBlockGraph(5,5); G OA(5,5): Graph on 25 vertices sage: G.is_strongly_regular(parameters=True) (25, 20, 15, 20) sage: G = graphs.OrthogonalArrayBlockGraph(4,10); G OA(4,10): Graph on 100 vertices sage: G.is_strongly_regular(parameters=True) (100, 36, 14, 12) Two graphs built from different orthogonal arrays are also different:: sage: k=4;n=10 sage: OAa = designs.orthogonal_arrays.build(k,n) sage: OAb = [[(x+1)%n for x in R] for R in OAa] sage: set(map(tuple,OAa)) == set(map(tuple,OAb)) False sage: Ga = graphs.OrthogonalArrayBlockGraph(k,n,OAa) sage: Gb = graphs.OrthogonalArrayBlockGraph(k,n,OAb) sage: Ga == Gb False As ``OAb`` was obtained from ``OAa`` by a relabelling the two graphs are isomorphic:: sage: Ga.is_isomorphic(Gb) True But there are examples of `OA(k,n)` for which the resulting graphs are not isomorphic:: sage: oa0 = [[0, 0, 1], [0, 1, 3], [0, 2, 0], [0, 3, 2], ....: [1, 0, 3], [1, 1, 1], [1, 2, 2], [1, 3, 0], ....: [2, 0, 0], [2, 1, 2], [2, 2, 1], [2, 3, 3], ....: [3, 0, 2], [3, 1, 0], [3, 2, 3], [3, 3, 1]] sage: oa1 = [[0, 0, 1], [0, 1, 0], [0, 2, 3], [0, 3, 2], ....: [1, 0, 3], [1, 1, 2], [1, 2, 0], [1, 3, 1], ....: [2, 0, 0], [2, 1, 1], [2, 2, 2], [2, 3, 3], ....: [3, 0, 2], [3, 1, 3], [3, 2, 1], [3, 3, 0]] sage: g0 = graphs.OrthogonalArrayBlockGraph(3,4,oa0) sage: g1 = graphs.OrthogonalArrayBlockGraph(3,4,oa1) sage: g0.is_isomorphic(g1) False But nevertheless isospectral:: sage: g0.spectrum() [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, -3, -3, -3, -3, -3, -3] sage: g1.spectrum() [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, -3, -3, -3, -3, -3, -3] Note that the graph ``g0`` is actually isomorphic to the affine polar graph `VO^+(4,2)`:: sage: graphs.AffineOrthogonalPolarGraph(4,2,'+').is_isomorphic(g0) True TESTS:: sage: G = graphs.OrthogonalArrayBlockGraph(4,6) Traceback (most recent call last): ... NotImplementedError: I don't know how to build an OA(4,6)! sage: G = graphs.OrthogonalArrayBlockGraph(8,2) Traceback (most recent call last): ... ValueError: There is no OA(8,2). Beware, Brouwer's website uses OA(n,k) instead of OA(k,n) ! """ if n>1 and k>=n+2: raise ValueError("There is no OA({},{}). Beware, Brouwer's website uses OA(n,k) instead of OA(k,n) !".format(k,n)) from itertools import combinations if OA is None: from sage.combinat.designs.orthogonal_arrays import orthogonal_array OA = orthogonal_array(k,n) else: assert len(OA) == n**2 assert n == 0 or k == len(OA[0]) OA = map(tuple,OA) d = [[[] for j in range(n)] for i in range(k)] for R in OA: for i,x in enumerate(R): d[i][x].append(R) g = Graph() for l in d: for ll in l: g.add_edges(combinations(ll,2)) g.name("OA({},{})".format(k,n)) return g
def mutually_orthogonal_latin_squares(k,n, partitions = False, check = True, existence=False): r""" Return `k` Mutually Orthogonal `n\times n` Latin Squares (MOLS). For more information on Mutually Orthogonal Latin Squares, see :mod:`~sage.combinat.designs.latin_squares`. INPUT: - ``k`` (integer) -- number of MOLS. If ``k=None`` it is set to the largest value available. - ``n`` (integer) -- size of the latin square. - ``partition`` (boolean) -- a Latin Square can be seen as 3 partitions of the `n^2` cells of the array into `n` sets of size `n`, respectively : * The partition of rows * The partition of columns * The partition of number (cells numbered with 0, cells numbered with 1, ...) These partitions have the additional property that any two sets from different partitions intersect on exactly one element. When ``partition`` is set to ``True``, this function returns a list of `k+2` partitions satisfying this intersection property instead of the `k+2` MOLS (though the data is exactly the same in both cases). - ``existence`` (boolean) -- instead of building the design, return: - ``True`` -- meaning that Sage knows how to build the design - ``Unknown`` -- meaning that Sage does not know how to build the design, but that the design may exist (see :mod:`sage.misc.unknown`). - ``False`` -- meaning that the design does not exist. .. NOTE:: When ``k=None`` and ``existence=True`` the function returns an integer, i.e. the largest `k` such that we can build a `k` MOLS of order `n`. - ``check`` -- (boolean) Whether to check that output is correct before returning it. As this is expected to be useless (but we are cautious guys), you may want to disable it whenever you want speed. Set to ``True`` by default. EXAMPLES:: sage: designs.mutually_orthogonal_latin_squares(4,5) [ [0 2 4 1 3] [0 3 1 4 2] [0 4 3 2 1] [0 1 2 3 4] [4 1 3 0 2] [3 1 4 2 0] [2 1 0 4 3] [4 0 1 2 3] [3 0 2 4 1] [1 4 2 0 3] [4 3 2 1 0] [3 4 0 1 2] [2 4 1 3 0] [4 2 0 3 1] [1 0 4 3 2] [2 3 4 0 1] [1 3 0 2 4], [2 0 3 1 4], [3 2 1 0 4], [1 2 3 4 0] ] sage: designs.mutually_orthogonal_latin_squares(3,7) [ [0 2 4 6 1 3 5] [0 3 6 2 5 1 4] [0 4 1 5 2 6 3] [6 1 3 5 0 2 4] [5 1 4 0 3 6 2] [4 1 5 2 6 3 0] [5 0 2 4 6 1 3] [3 6 2 5 1 4 0] [1 5 2 6 3 0 4] [4 6 1 3 5 0 2] [1 4 0 3 6 2 5] [5 2 6 3 0 4 1] [3 5 0 2 4 6 1] [6 2 5 1 4 0 3] [2 6 3 0 4 1 5] [2 4 6 1 3 5 0] [4 0 3 6 2 5 1] [6 3 0 4 1 5 2] [1 3 5 0 2 4 6], [2 5 1 4 0 3 6], [3 0 4 1 5 2 6] ] sage: designs.mutually_orthogonal_latin_squares(2,5,partitions=True) [[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]], [[0, 5, 10, 15, 20], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24]], [[0, 8, 11, 19, 22], [3, 6, 14, 17, 20], [1, 9, 12, 15, 23], [4, 7, 10, 18, 21], [2, 5, 13, 16, 24]], [[0, 9, 13, 17, 21], [2, 6, 10, 19, 23], [4, 8, 12, 16, 20], [1, 5, 14, 18, 22], [3, 7, 11, 15, 24]]] What is the maximum number of MOLS of size 8 that Sage knows how to build?:: sage: designs.orthogonal_arrays.largest_available_k(8)-2 7 If you only want to know if Sage is able to build a given set of MOLS, query the ``orthogonal_arrays.*`` functions:: sage: designs.orthogonal_arrays.is_available(5+2, 5) # 5 MOLS of order 5 False sage: designs.orthogonal_arrays.is_available(4+2,6) # 4 MOLS of order 6 False Sage, however, is not able to prove that the second MOLS do not exist:: sage: designs.orthogonal_arrays.exists(4+2,6) # 4 MOLS of order 6 Unknown If you ask for such a MOLS then you will respectively get an informative ``EmptySetError`` or ``NotImplementedError``:: sage: designs.mutually_orthogonal_latin_squares(5, 5) Traceback (most recent call last): ... EmptySetError: There exist at most n-1 MOLS of size n if n>=2. sage: designs.mutually_orthogonal_latin_squares(4,6) Traceback (most recent call last): ... NotImplementedError: I don't know how to build 4 MOLS of order 6 TESTS: The special case `n=1`:: sage: designs.mutually_orthogonal_latin_squares(3, 1) [[0], [0], [0]] sage: designs.mutually_orthogonal_latin_squares(None, 1) Traceback (most recent call last): ... ValueError: there are no bound on k when 0<=n<=1 sage: designs.mutually_orthogonal_latin_squares(2,10) [ [1 8 9 0 2 4 6 3 5 7] [1 7 6 5 0 9 8 2 3 4] [7 2 8 9 0 3 5 4 6 1] [8 2 1 7 6 0 9 3 4 5] [6 1 3 8 9 0 4 5 7 2] [9 8 3 2 1 7 0 4 5 6] [5 7 2 4 8 9 0 6 1 3] [0 9 8 4 3 2 1 5 6 7] [0 6 1 3 5 8 9 7 2 4] [2 0 9 8 5 4 3 6 7 1] [9 0 7 2 4 6 8 1 3 5] [4 3 0 9 8 6 5 7 1 2] [8 9 0 1 3 5 7 2 4 6] [6 5 4 0 9 8 7 1 2 3] [2 3 4 5 6 7 1 8 9 0] [3 4 5 6 7 1 2 8 0 9] [3 4 5 6 7 1 2 0 8 9] [5 6 7 1 2 3 4 0 9 8] [4 5 6 7 1 2 3 9 0 8], [7 1 2 3 4 5 6 9 8 0] ] """ from sage.combinat.designs.orthogonal_arrays import orthogonal_array from sage.matrix.constructor import Matrix from sage.arith.all import factor from .database import MOLS_constructions # Is k is None we find the largest available if k is None: from sage.misc.superseded import deprecation deprecation(17034,"please use designs.orthogonal_arrays.largest_available_k instead of k=None") if n == 0 or n == 1: if existence: from sage.rings.infinity import Infinity return Infinity raise ValueError("there are no bound on k when 0<=n<=1") k = orthogonal_array(None,n,existence=True) - 2 if existence: return k if existence: from sage.misc.superseded import deprecation deprecation(17034,"please use designs.orthogonal_arrays.is_available/exists instead of existence=True") if n == 1: if existence: return True matrices = [Matrix([[0]])]*k elif k >= n: if existence: return False raise EmptySetError("There exist at most n-1 MOLS of size n if n>=2.") elif n in MOLS_constructions and k <= MOLS_constructions[n][0]: if existence: return True _, construction = MOLS_constructions[n] matrices = construction()[:k] elif orthogonal_array(k+2,n,existence=True) is not Unknown: # Forwarding non-existence results if orthogonal_array(k+2,n,existence=True): if existence: return True else: if existence: return False raise EmptySetError("There does not exist {} MOLS of order {}!".format(k,n)) # make sure that the first two columns are "11, 12, ..., 1n, 21, 22, ..." OA = sorted(orthogonal_array(k+2,n,check=False)) # We first define matrices as lists of n^2 values matrices = [[] for _ in range(k)] for L in OA: for i in range(2,k+2): matrices[i-2].append(L[i]) # The real matrices matrices = [[M[i*n:(i+1)*n] for i in range(n)] for M in matrices] matrices = [Matrix(M) for M in matrices] else: if existence: return Unknown raise NotImplementedError("I don't know how to build {} MOLS of order {}".format(k,n)) if check: assert are_mutually_orthogonal_latin_squares(matrices) # partitions have been requested but have not been computed yet if partitions is True: partitions = [[[i*n+j for j in range(n)] for i in range(n)], [[j*n+i for j in range(n)] for i in range(n)]] for m in matrices: partition = [[] for i in range(n)] for i in range(n): for j in range(n): partition[m[i,j]].append(i*n+j) partitions.append(partition) if partitions: return partitions else: return matrices
def mutually_orthogonal_latin_squares(n,k, partitions = False, check = True, existence=False, who_asked=tuple()): r""" Returns `k` Mutually Orthogonal `n\times n` Latin Squares (MOLS). For more information on Latin Squares and MOLS, see :mod:`~sage.combinat.designs.latin_squares` or the :wikipedia:`Latin_square`, or even the :wikipedia:`Wikipedia entry on MOLS <Graeco-Latin_square#Mutually_orthogonal_Latin_squares>`. INPUT: - ``n`` (integer) -- size of the latin square. - ``k`` (integer) -- number of MOLS. If ``k=None`` it is set to the largest value available. - ``partition`` (boolean) -- a Latin Square can be seen as 3 partitions of the `n^2` cells of the array into `n` sets of size `n`, respectively : * The partition of rows * The partition of columns * The partition of number (cells numbered with 0, cells numbered with 1, ...) These partitions have the additional property that any two sets from different partitions intersect on exactly one element. When ``partition`` is set to ``True``, this function returns a list of `k+2` partitions satisfying this intersection property instead of the `k+2` MOLS (though the data is exactly the same in both cases). - ``existence`` (boolean) -- instead of building the design, returns: - ``True`` -- meaning that Sage knows how to build the design - ``Unknown`` -- meaning that Sage does not know how to build the design, but that the design may exist (see :mod:`sage.misc.unknown`). - ``False`` -- meaning that the design does not exist. .. NOTE:: When ``k=None`` and ``existence=True`` the function returns an integer, i.e. the largest `k` such that we can build a `k` MOLS of order `n`. - ``check`` -- (boolean) Whether to check that output is correct before returning it. As this is expected to be useless (but we are cautious guys), you may want to disable it whenever you want speed. Set to ``True`` by default. - ``who_asked`` (internal use only) -- because of the equivalence between OA/TD/MOLS, each of the three constructors calls the others. We must keep track of who calls who in order to avoid infinite loops. ``who_asked`` is the tuple of the other functions that were called before this one. EXAMPLES:: sage: designs.mutually_orthogonal_latin_squares(5,4) [ [0 2 4 1 3] [0 3 1 4 2] [0 4 3 2 1] [0 1 2 3 4] [4 1 3 0 2] [3 1 4 2 0] [2 1 0 4 3] [4 0 1 2 3] [3 0 2 4 1] [1 4 2 0 3] [4 3 2 1 0] [3 4 0 1 2] [2 4 1 3 0] [4 2 0 3 1] [1 0 4 3 2] [2 3 4 0 1] [1 3 0 2 4], [2 0 3 1 4], [3 2 1 0 4], [1 2 3 4 0] ] sage: designs.mutually_orthogonal_latin_squares(7,3) [ [0 2 4 6 1 3 5] [0 3 6 2 5 1 4] [0 4 1 5 2 6 3] [6 1 3 5 0 2 4] [5 1 4 0 3 6 2] [4 1 5 2 6 3 0] [5 0 2 4 6 1 3] [3 6 2 5 1 4 0] [1 5 2 6 3 0 4] [4 6 1 3 5 0 2] [1 4 0 3 6 2 5] [5 2 6 3 0 4 1] [3 5 0 2 4 6 1] [6 2 5 1 4 0 3] [2 6 3 0 4 1 5] [2 4 6 1 3 5 0] [4 0 3 6 2 5 1] [6 3 0 4 1 5 2] [1 3 5 0 2 4 6], [2 5 1 4 0 3 6], [3 0 4 1 5 2 6] ] sage: designs.mutually_orthogonal_latin_squares(5,2,partitions=True) [[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]], [[0, 5, 10, 15, 20], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24]], [[0, 8, 11, 19, 22], [3, 6, 14, 17, 20], [1, 9, 12, 15, 23], [4, 7, 10, 18, 21], [2, 5, 13, 16, 24]], [[0, 9, 13, 17, 21], [2, 6, 10, 19, 23], [4, 8, 12, 16, 20], [1, 5, 14, 18, 22], [3, 7, 11, 15, 24]]] What is the maximum number of MOLS of size 8 that Sage knows how to build?:: sage: designs.mutually_orthogonal_latin_squares(8,None,existence=True) 7 If you only want to know if Sage is able to build a given set of MOLS, just set the argument ``existence`` to ``True``:: sage: designs.mutually_orthogonal_latin_squares(5, 5, existence=True) False sage: designs.mutually_orthogonal_latin_squares(6, 4, existence=True) Unknown If you ask for such a MOLS then you will respecively get an informative ``EmptySetError`` or ``NotImplementedError``:: sage: designs.mutually_orthogonal_latin_squares(5, 5) Traceback (most recent call last): ... EmptySetError: There exist at most n-1 MOLS of size n if n>=2. sage: designs.mutually_orthogonal_latin_squares(6, 4) Traceback (most recent call last): ... NotImplementedError: I don't know how to build these MOLS! TESTS: The special case `n=1`:: sage: designs.mutually_orthogonal_latin_squares(1, 3) [[0], [0], [0]] sage: designs.mutually_orthogonal_latin_squares(1, None, existence=True) +Infinity sage: designs.mutually_orthogonal_latin_squares(1, None) Traceback (most recent call last): ... ValueError: there are no bound on k when n=1. sage: designs.mutually_orthogonal_latin_squares(10,2,existence=True) True sage: designs.mutually_orthogonal_latin_squares(10,2) [ [1 8 9 0 2 4 6 3 5 7] [1 7 6 5 0 9 8 2 3 4] [7 2 8 9 0 3 5 4 6 1] [8 2 1 7 6 0 9 3 4 5] [6 1 3 8 9 0 4 5 7 2] [9 8 3 2 1 7 0 4 5 6] [5 7 2 4 8 9 0 6 1 3] [0 9 8 4 3 2 1 5 6 7] [0 6 1 3 5 8 9 7 2 4] [2 0 9 8 5 4 3 6 7 1] [9 0 7 2 4 6 8 1 3 5] [4 3 0 9 8 6 5 7 1 2] [8 9 0 1 3 5 7 2 4 6] [6 5 4 0 9 8 7 1 2 3] [2 3 4 5 6 7 1 8 9 0] [3 4 5 6 7 1 2 8 0 9] [3 4 5 6 7 1 2 0 8 9] [5 6 7 1 2 3 4 0 9 8] [4 5 6 7 1 2 3 9 0 8], [7 1 2 3 4 5 6 9 8 0] ] """ from sage.combinat.designs.orthogonal_arrays import orthogonal_array from sage.matrix.constructor import Matrix from sage.rings.arith import factor from database import MOLS_constructions # Is k is None we find the largest available if k is None: if n == 1: if existence: from sage.rings.infinity import Infinity return Infinity raise ValueError("there are no bound on k when n=1.") k = orthogonal_array(None,n,existence=True) - 2 if existence: return k if n == 1: if existence: return True matrices = [Matrix([[0]])]*k elif k >= n: if existence: return False raise EmptySetError("There exist at most n-1 MOLS of size n if n>=2.") elif n in MOLS_constructions and k <= MOLS_constructions[n][0]: if existence: return True _, construction = MOLS_constructions[n] matrices = construction()[:k] elif (orthogonal_array not in who_asked and orthogonal_array(k+2,n,existence=True,who_asked = who_asked+(mutually_orthogonal_latin_squares,)) is not Unknown): # Forwarding non-existence results if orthogonal_array(k+2,n,existence=True,who_asked = who_asked+(mutually_orthogonal_latin_squares,)): if existence: return True else: if existence: return False raise EmptySetError("These MOLS do not exist!") OA = orthogonal_array(k+2,n,check=False, who_asked = who_asked+(mutually_orthogonal_latin_squares,)) OA.sort() # make sure that the first two columns are "11, 12, ..., 1n, 21, 22, ..." # We first define matrices as lists of n^2 values matrices = [[] for _ in range(k)] for L in OA: for i in range(2,k+2): matrices[i-2].append(L[i]) # The real matrices matrices = [[M[i*n:(i+1)*n] for i in range(n)] for M in matrices] matrices = [Matrix(M) for M in matrices] else: if existence: return Unknown raise NotImplementedError("I don't know how to build these MOLS!") if check: assert are_mutually_orthogonal_latin_squares(matrices) # partitions have been requested but have not been computed yet if partitions is True: partitions = [[[i*n+j for j in range(n)] for i in range(n)], [[j*n+i for j in range(n)] for i in range(n)]] for m in matrices: partition = [[] for i in range(n)] for i in range(n): for j in range(n): partition[m[i,j]].append(i*n+j) partitions.append(partition) if partitions: return partitions else: return matrices
def mutually_orthogonal_latin_squares(n, k, partitions=False, check=True, existence=False, who_asked=tuple()): r""" Returns `k` Mutually Orthogonal `n\times n` Latin Squares (MOLS). For more information on Latin Squares and MOLS, see :mod:`~sage.combinat.designs.latin_squares` or the :wikipedia:`Latin_square`, or even the :wikipedia:`Wikipedia entry on MOLS <Graeco-Latin_square#Mutually_orthogonal_Latin_squares>`. INPUT: - ``n`` (integer) -- size of the latin square. - ``k`` (integer) -- number of MOLS. If ``k=None`` it is set to the largest value available. - ``partition`` (boolean) -- a Latin Square can be seen as 3 partitions of the `n^2` cells of the array into `n` sets of size `n`, respectively : * The partition of rows * The partition of columns * The partition of number (cells numbered with 0, cells numbered with 1, ...) These partitions have the additional property that any two sets from different partitions intersect on exactly one element. When ``partition`` is set to ``True``, this function returns a list of `k+2` partitions satisfying this intersection property instead of the `k+2` MOLS (though the data is exactly the same in both cases). - ``existence`` (boolean) -- instead of building the design, returns: - ``True`` -- meaning that Sage knows how to build the design - ``Unknown`` -- meaning that Sage does not know how to build the design, but that the design may exist (see :mod:`sage.misc.unknown`). - ``False`` -- meaning that the design does not exist. .. NOTE:: When ``k=None`` and ``existence=True`` the function returns an integer, i.e. the largest `k` such that we can build a `k` MOLS of order `n`. - ``check`` -- (boolean) Whether to check that output is correct before returning it. As this is expected to be useless (but we are cautious guys), you may want to disable it whenever you want speed. Set to ``True`` by default. - ``who_asked`` (internal use only) -- because of the equivalence between OA/TD/MOLS, each of the three constructors calls the others. We must keep track of who calls who in order to avoid infinite loops. ``who_asked`` is the tuple of the other functions that were called before this one. EXAMPLES:: sage: designs.mutually_orthogonal_latin_squares(5,4) [ [0 2 4 1 3] [0 3 1 4 2] [0 4 3 2 1] [0 1 2 3 4] [4 1 3 0 2] [3 1 4 2 0] [2 1 0 4 3] [4 0 1 2 3] [3 0 2 4 1] [1 4 2 0 3] [4 3 2 1 0] [3 4 0 1 2] [2 4 1 3 0] [4 2 0 3 1] [1 0 4 3 2] [2 3 4 0 1] [1 3 0 2 4], [2 0 3 1 4], [3 2 1 0 4], [1 2 3 4 0] ] sage: designs.mutually_orthogonal_latin_squares(7,3) [ [0 2 4 6 1 3 5] [0 3 6 2 5 1 4] [0 4 1 5 2 6 3] [6 1 3 5 0 2 4] [5 1 4 0 3 6 2] [4 1 5 2 6 3 0] [5 0 2 4 6 1 3] [3 6 2 5 1 4 0] [1 5 2 6 3 0 4] [4 6 1 3 5 0 2] [1 4 0 3 6 2 5] [5 2 6 3 0 4 1] [3 5 0 2 4 6 1] [6 2 5 1 4 0 3] [2 6 3 0 4 1 5] [2 4 6 1 3 5 0] [4 0 3 6 2 5 1] [6 3 0 4 1 5 2] [1 3 5 0 2 4 6], [2 5 1 4 0 3 6], [3 0 4 1 5 2 6] ] sage: designs.mutually_orthogonal_latin_squares(5,2,partitions=True) [[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]], [[0, 5, 10, 15, 20], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24]], [[0, 8, 11, 19, 22], [3, 6, 14, 17, 20], [1, 9, 12, 15, 23], [4, 7, 10, 18, 21], [2, 5, 13, 16, 24]], [[0, 9, 13, 17, 21], [2, 6, 10, 19, 23], [4, 8, 12, 16, 20], [1, 5, 14, 18, 22], [3, 7, 11, 15, 24]]] What is the maximum number of MOLS of size 8 that Sage knows how to build?:: sage: designs.mutually_orthogonal_latin_squares(8,None,existence=True) 7 If you only want to know if Sage is able to build a given set of MOLS, just set the argument ``existence`` to ``True``:: sage: designs.mutually_orthogonal_latin_squares(5, 5, existence=True) False sage: designs.mutually_orthogonal_latin_squares(6, 4, existence=True) Unknown If you ask for such a MOLS then you will respecively get an informative ``EmptySetError`` or ``NotImplementedError``:: sage: designs.mutually_orthogonal_latin_squares(5, 5) Traceback (most recent call last): ... EmptySetError: There exist at most n-1 MOLS of size n if n>=2. sage: designs.mutually_orthogonal_latin_squares(6, 4) Traceback (most recent call last): ... NotImplementedError: I don't know how to build these MOLS! TESTS: The special case `n=1`:: sage: designs.mutually_orthogonal_latin_squares(1, 3) [[0], [0], [0]] sage: designs.mutually_orthogonal_latin_squares(1, None, existence=True) +Infinity sage: designs.mutually_orthogonal_latin_squares(1, None) Traceback (most recent call last): ... ValueError: there are no bound on k when n=1. sage: designs.mutually_orthogonal_latin_squares(10,2,existence=True) True sage: designs.mutually_orthogonal_latin_squares(10,2) [ [1 8 9 0 2 4 6 3 5 7] [1 7 6 5 0 9 8 2 3 4] [7 2 8 9 0 3 5 4 6 1] [8 2 1 7 6 0 9 3 4 5] [6 1 3 8 9 0 4 5 7 2] [9 8 3 2 1 7 0 4 5 6] [5 7 2 4 8 9 0 6 1 3] [0 9 8 4 3 2 1 5 6 7] [0 6 1 3 5 8 9 7 2 4] [2 0 9 8 5 4 3 6 7 1] [9 0 7 2 4 6 8 1 3 5] [4 3 0 9 8 6 5 7 1 2] [8 9 0 1 3 5 7 2 4 6] [6 5 4 0 9 8 7 1 2 3] [2 3 4 5 6 7 1 8 9 0] [3 4 5 6 7 1 2 8 0 9] [3 4 5 6 7 1 2 0 8 9] [5 6 7 1 2 3 4 0 9 8] [4 5 6 7 1 2 3 9 0 8], [7 1 2 3 4 5 6 9 8 0] ] """ from sage.combinat.designs.orthogonal_arrays import orthogonal_array from sage.matrix.constructor import Matrix from sage.rings.arith import factor from database import MOLS_constructions # Is k is None we find the largest available if k is None: if n == 1: if existence: from sage.rings.infinity import Infinity return Infinity raise ValueError("there are no bound on k when n=1.") k = orthogonal_array(None, n, existence=True) - 2 if existence: return k if n == 1: if existence: return True matrices = [Matrix([[0]])] * k elif k >= n: if existence: return False raise EmptySetError("There exist at most n-1 MOLS of size n if n>=2.") elif n in MOLS_constructions and k <= MOLS_constructions[n][0]: if existence: return True _, construction = MOLS_constructions[n] matrices = construction()[:k] elif (orthogonal_array not in who_asked and orthogonal_array( k + 2, n, existence=True, who_asked=who_asked + (mutually_orthogonal_latin_squares, )) is not Unknown): # Forwarding non-existence results if orthogonal_array(k + 2, n, existence=True, who_asked=who_asked + (mutually_orthogonal_latin_squares, )): if existence: return True else: if existence: return False raise EmptySetError("These MOLS do not exist!") OA = orthogonal_array(k + 2, n, check=False, who_asked=who_asked + (mutually_orthogonal_latin_squares, )) OA.sort( ) # make sure that the first two columns are "11, 12, ..., 1n, 21, 22, ..." # We first define matrices as lists of n^2 values matrices = [[] for _ in range(k)] for L in OA: for i in range(2, k + 2): matrices[i - 2].append(L[i]) # The real matrices matrices = [[M[i * n:(i + 1) * n] for i in range(n)] for M in matrices] matrices = [Matrix(M) for M in matrices] else: if existence: return Unknown raise NotImplementedError("I don't know how to build these MOLS!") if check: assert are_mutually_orthogonal_latin_squares(matrices) # partitions have been requested but have not been computed yet if partitions is True: partitions = [[[i * n + j for j in range(n)] for i in range(n)], [[j * n + i for j in range(n)] for i in range(n)]] for m in matrices: partition = [[] for i in range(n)] for i in range(n): for j in range(n): partition[m[i, j]].append(i * n + j) partitions.append(partition) if partitions: return partitions else: return matrices