Worksheet for Computing K-Apollonian and Schmidt Arrangement pictures
Summer 2017, Version 1.0
Coding: Katherine E. Stange
Algorithms: Katherine E. Stange and Daniel Martin
Licensed under Attribution-NonCommercial-ShareAlike 4.0 International
{{{id=19| ################################################ ## Colour and Transparency Schemes Setup ################################################ # Transparency schemes constant_transparency = lambda x, c : x[0] # parameter is constant decreasing_transparency = lambda x, c : c/(x[0]) + x[1] # parameters are upper bound on curvatures, translation increasing_transparency = lambda x, c : x[1] - c/(x[0]) # parameter is upper bound on curvatures, translation # Colour schemes modular_colour = lambda x, c : x[0][ Mod(c*x[1][1] + x[1][2],x[1][0]) ] # Use residue of curvature prime_colour = lambda x, c : x[0][ int(ZZ(c).is_prime()) ] # Two-colours (first two of some scheme) # 4-colour schemes colour_cu = [Color('#cfb87c'), Color('#000000'), Color('#565a5c'), Color('#a2a4a3')] # University of Colorado official colours colour_saul = [Color('#4c3f54'),Color('#d13525'),Color('#f2c057'),Color('#486824')] colour_antique = [Color('#a4cabc'),Color('#eab364'),Color('#b2473e'),Color('#acbd78')] colour_green = [Color('#919636'),Color('#524a3a'),Color('#fffae1'),Color('#5a5f37')] colour_92 = [Color('#fbcd4b'),Color('#a3a599'),Color('#282623'),Color('#88a550')] colour_golden = [Color('#882426'),Color('#cdbea7'),Color('#323030'),Color('#c29545')] colour_pool = [Color('#344d90'),Color('#5cc5ef'),Color('#ffb745'),Color('#e7552c')] colour_distinct = [Color('#52908b'),Color('#e5e2ca'),Color('#ddbc95'),Color('#e7472e')] colour_nightlife = [Color('#00cffa'),Color('#ff0038'),Color('#ffce38'),Color('#020509')] colour_a = [Color('#4c3f54'),Color('#d13525'),Color('#f2c057'),Color('#486824')] colour_b = [Color('#aa3939'),Color('#aa6c39'),Color('#226666'),Color('#2d882d')] colour_c = [Color('#632770'),Color('#923158'),Color('#6c9b34'),Color('#9aa637')] palettes_4 = [colour_cu, colour_saul, colour_antique, colour_green, colour_92, colour_golden, colour_pool, colour_distinct, colour_nightlife, colour_a, colour_b, colour_c] # 5-colour schemes colour_5 = [Color('#270101'),Color('#720017'),Color('#d8d583'),Color('#d9ac2a'),Color('#763f02')] palettes_5 = [colour_5] # 6-colour schemes colour_6 = [Color('#21B6A8'), Color('#177F75'), Color('#B6212D'), Color('#7F171F'), Color('#B67721'), Color('#7F5417')] colour_6alt = [Color('#DD1E2F'), Color('#EBB035'), Color('#06A2CB'), Color('#218559'), Color('#D0C6B1'), Color('#192823')] colour_6seeds = [Color('#B2EEEE'), Color('#7ED6DA'), Color('#3682BC'), Color('#243C5C'), Color('#324E51'), Color('#B8AC7F')] palettes_6 = [colour_6, colour_6alt, colour_6seeds] # 24-colour schemes colour_24 = [Color('#ea4b3d'),Color('#e48022'),Color('#fdce05'),Color('#f0deb5'),Color('#36485e'),Color('#2b2b2b'),Color('#9b59b1'),Color('#376f7e'),Color('#3496d7'),Color('#3396e3'),Color('#2ccc74'),Color('#1bbc9b'),Color('#ebf1ef'),Color('#96a5a5'),Color('#336041'),Color('#735ec5'),Color('#604436'),Color('#60335e'),Color('#ee727c'),Color('#a5c53c'),Color('#f47cc3'),Color('#77302a'),Color('#a28671'),Color('#b7c8f3'),Color('#5165a0')] palettes_24 = [colour_24] # Function colour_demo(n) shows all the possible n-colour palettes def namestr(obj, namespace): return [name for name in namespace if namespace[name] is obj] def colour_demo(n): pic = Graphics() palettes = [] if n == 4: palettes = palettes_4 if n == 5: palettes = palettes_5 if n == 6: palettes = palettes_6 if n == 24: palettes = palettes_24 for i in range(len(palettes[0])): pic += text( str(i), (i,-1) ) for palette in range(len(palettes)): pic += text( namestr(palettes[palette], globals()), (-2, palette) ) for colour in palettes[palette]: pic += disk( (palettes[palette].index(colour), palette), 1/2, (0,2*pi), color=colour ) pic.show(axes=False) # Gradient-disk scheme # Example usage: gradient_disk( (0,0), 1, colour_pool[2], colour_pool[0] ) # fact=100 controls the fineness of the gradient def gradient_disk( coords, radius, color1, color2, fact=100 ): pic = Graphics() num_disks = fact*radius for i in range(0,num_disks+1): pic += disk( coords, radius*(1-i/num_disks), (0,2*pi), color=color1.blend(color2,fraction=i/num_disks) ) return pic ############################################### ## Main Graphing Functions ############################################### # The function precompute_circles obtains the coordinates of the circles in a given range # disc = the root of the field (fundamental-discriminant or fundamental-discriminant/4, e.g. -1 for Gaussian, -2 for Sqrt(2) packing, -3 for Eisenstein etc.) # curvature_bound = the upper bound on curvatures # xmin, xmax, ymin, ymax = give bounds on the centre of the circle def precompute_circles( dis, curvature_bound, xmax, ymax ): coords = [] poly_coeffs = obtain_polycoeffs( dis ) coord_coeffs = obtain_coords( dis ) s_lower = 0 if Mod(dis,4) == Mod(1,4): s_lower = -xmax*curvature_bound for r in range(curvature_bound): r_coords = [] for s in range(s_lower,xmax*curvature_bound): for t in range(-ymax*curvature_bound,ymax*curvature_bound): if Mod(s^2 + poly_coeffs[0]*s*t + poly_coeffs[1]*t^2+t,r) == Mod(0,r): curv = coord_coeffs[0]*r xcoord = coord_coeffs[1]*s + coord_coeffs[2]*t ycoord = coord_coeffs[3]*t + 1 if [xcoord,ycoord,curv] not in r_coords: r_coords.append([xcoord,ycoord,curv]) # first and third scaled if xcoord <> 0: r_coords.append([-xcoord,ycoord,curv]) if ycoord <> 0: r_coords.append([xcoord,-ycoord,curv]) if xcoord <> 0 and ycoord <> 0: r_coords.append([-xcoord,-ycoord,curv]) coords += r_coords return coords # Function filter_circles_rectangular will filter the coordinate list produced by precompute_circles to obtain just those needed for your purposes # In this case, you can specify the curvature lower and upper bounds, and a rectangular region # format of output list element is [ center-coordinates, radius, integer-scaled-curvature ] # the tag filter_down=True will remove all circles contained in others, useful for Apollonian-type circle packings def filter_circles_rectangular( dis, coords, curvature_lower_bound, curvature_upper_bound, xmin, xmax, ymin, ymax, filter_down=False ): rootval = obtain_sqrt(dis) curvature_factor = obtain_coords( dis )[0] circs = [] if filter_down: coords.sort(key = lambda x: x[2]) for coord in coords: if coord[2] >= curvature_lower_bound*curvature_factor and coord[2] <= curvature_upper_bound*curvature_factor: if coord[2] == 0: circs.append( [ (0,0), 0, 0 ] ) if coord[2] > 0 and coord[0]/coord[2] > xmin and coord[0]/coord[2] < xmax: if coord[1]/(coord[2]*rootval) > ymin and coord[1]/(coord[2]*rootval) < ymax: new_coord = [ ( coord[0]/(coord[2]), coord[1]/(coord[2]*rootval) ), 1/(coord[2]*rootval), coord[2]/curvature_factor ] if not filter_down or not containment(new_coord, circs): circs.append( new_coord ) return circs # Another filtering function, this time by curvature lower and upper bound, and a disk region given by center and radius squared def filter_circles_circular( dis, coords, curvature_lower_bound, curvature_upper_bound, xctr, yctr, radius_squared, filter_down=False ): rootval = obtain_sqrt(dis) curvature_factor = obtain_coords( dis )[0] circs = [] if filter_down: coords.sort(key = lambda x: x[2]) for coord in coords: if coord[2] >= curvature_lower_bound*curvature_factor and coord[2] <= curvature_upper_bound*curvature_factor: if coord[2] == 0 and [ (0,0), 0, 0 ] not in circs: circs.append( [ (0,0), 0, 0 ] ) if coord[2] > 0 and (coord[0]/coord[2] - xctr)^2 + (coord[1]/(coord[2]*rootval) - yctr)^2 < radius_squared: new_coord = [ ( coord[0]/coord[2], coord[1]/(coord[2]*rootval) ), 1/(coord[2]*rootval), coord[2]/curvature_factor ] if not filter_down or not containment( new_coord, circs ): circs.append( new_coord ) return circs # This command finally plots a picture # Using disk_colour_scheme = gradient_disk, you must give a parameter list of the form [colour_scheme1, colour_parms1, colour_scheme2, colour_params2, fact] # If using gradient, disk_transparency_scheme is ignored def circle_picture( circle_list, object_type="circle", circle_colour_scheme=modular_colour, circle_colour_parameter_list=[colour_cu,[2,0,1]], circle_transparency_scheme=constant_transparency, circle_transparency_parameter_list=[1], disk_colour_scheme=modular_colour, disk_colour_parameter_list=[colour_cu,[1,0,0]], disk_transparency_scheme=constant_transparency, disk_transparency_parameter_list=[1/2], circle_thickness=1, sort_up=True, line_range=0, dis=-1): sqrtd = sqrt(-dis) if Mod(dis,4) == Mod(1,4): sqrtd = sqrtd/2 pic = Graphics() if sort_up == True: circle_list.sort(key = lambda x: x[2]) if sort_up == False: circle_list.sort(key = lambda x: -x[2]) for circ in circle_list: if (object_type == "circle" or object_type == "combo") and circ[2] == 0: offset_range = floor(line_range/sqrtd) for offset in range(-offset_range, offset_range): pic += line( [(-offset_range,offset*sqrtd),(offset_range,offset*sqrtd)], color = circle_colour_scheme(circle_colour_parameter_list, circ[2]), alpha = circle_transparency_scheme(circle_transparency_parameter_list, circ[2]), thickness=circle_thickness ) if dis == -3: pic += line( [((-1/2)*offset_range, (-sqrtd)*offset_range +offset*sqrtd*2), ( (1/2)*offset_range, (sqrtd)*offset_range +offset*sqrtd*2 )], color = circle_colour_scheme(circle_colour_parameter_list, circ[2]), alpha = circle_transparency_scheme(circle_transparency_parameter_list, circ[2]), thickness=circle_thickness ) pic += line( [((-1/2)*offset_range, (sqrtd)*offset_range +offset*sqrtd*2), ( (1/2)*offset_range, (-sqrtd)*offset_range +offset*sqrtd*2 )], color = circle_colour_scheme(circle_colour_parameter_list, circ[2]), alpha = circle_transparency_scheme(circle_transparency_parameter_list, circ[2]), thickness=circle_thickness ) if circ[2] <> 0: if object_type == "disk" or object_type == "combo": if disk_colour_scheme == gradient_disk: pic += gradient_disk( circ[0], circ[1], disk_colour_parameter_list[0](disk_colour_parameter_list[1], circ[2]), disk_colour_parameter_list[2](disk_colour_parameter_list[3], circ[2]), disk_colour_parameter_list[4] ) if disk_colour_scheme <> gradient_disk: pic += disk( circ[0], circ[1], (0,2*pi), color = disk_colour_scheme(disk_colour_parameter_list, circ[2]), alpha = disk_transparency_scheme(disk_transparency_parameter_list, circ[2]) ) if object_type == "circle" or object_type == "combo": pic += circle( circ[0], circ[1], color = circle_colour_scheme(circle_colour_parameter_list, circ[2]), alpha = circle_transparency_scheme(circle_transparency_parameter_list, circ[2]), thickness=circle_thickness ) return pic ################################## ## Some internal functions ################################## # Helper function that detects if new circle is contained in some already existing circle def containment( new_coord, circs ): contain_flag = False for circ in circs: ctrdst = sqrt( (new_coord[0][0]-circ[0][0])^2 + (new_coord[0][1]-circ[0][1])^2 ) if ctrdst + new_coord[1] <= circ[1]: # if center distance + small radius < big radius contain_flag = True break return contain_flag # This internal function determines the appropriate sqrt needed for scaling pictures def obtain_sqrt( dis ): return sqrt( -1*dis ) # This internal function determines the coefficients of the polynomial based on the field discriminant. # Use as s^2 + l[0]*s*t + l[1]*t^2 + t def obtain_polycoeffs( dis ): if Mod(dis,4) == Mod(1,4): return [1,(1-dis)/4] if Mod(dis,4) <> Mod(1,4): return [0,-dis] # This internal function determines the coordinates of a circle based on the field discriminant. # Use as curv = l[0]*r*sqrt, curv-ctr = (l[1]*s*sqrt + l[2]*t*sqrt, (l[3]*t+1) ) def obtain_coords( dis ): if Mod(dis,4) == Mod(1,4): return [1,1,1/2,(-dis)/2] if Mod(dis,4) <> Mod(1,4): return [2,2,0,2*(-dis)] /// }}}Usage:
Step 1) Precompute circles. The entry "dis" is the square root determining the field (-1 = Gaussian, -3 = Eisenstein etc.). The other entries bound the curvature and the absolute value of the centre coordinates. This is a time-intensive step for detailed pictures, so save the resulting list for future use. So do this once and then do Steps 2-5 as desired using the result.
precompute_circles( dis, curvature_bound, xmax, ymax )
Step 2) Filter circles. You can either specify a rectangle (using xmin,xmax,ymin,ymax) or a circle by specifying center and radius. The rectangle or circle is used as the region in which circle centres are allowed. You must feed in the output of Step 1 (here as "coords"), as well as "dis" as in Step 1. This will filter the circles produced in Step 1 according to your desires. It won't compute new circles, so curvature_bound should be less than or equal to what you specified in Step 1, for example.
filter_circles_rectangular( dis, coords, curvature_lower_bound, curvature_upper_bound, xmin, xmax, ymin, ymax, filter_down=False )
filter_circles_circular( dis, coords, curvature_lower_bound, curvature_upper_bound, xctr, yctr, radius_squared, filter_down=False )
Step 3) Create picture object.
circle_picture( circle_list, object_type="circle", circle_colour_scheme=modular_colour, circle_colour_parameter_list=[colour_cu,[2,0,1]], circle_transparency_scheme=constant_transparency, circle_transparency_parameter_list=[1], disk_colour_scheme=modular_colour, disk_colour_parameter_list=[colour_cu,[1,0,0]], disk_transparency_scheme=constant_transparency, disk_transparency_parameter_list=[1/2], circle_thickness=1, sort_up=True, line_range=0, dis=-1)
Step 4) Display the object. You can add things like "axes=False" to beautify the output.
pic.show(axes=False)
Step 5) Save your image. It will save as pdf (vector graphics) if extension is '.pdf', and as .png if you use that extension, etc.
pic.save('filename-with-path.pdf', axes=False)
Have fun!
{{{id=45| # Gaussian (sqrt(-1)) example, showing interior of one circle of the Schmidt arrangement. gaussian_coords_20 = precompute_circles( -1, 20, 0.5, 1 ) # Precompute up to curvature 20, in a window extending +/- 0.5 horizontally, +/- 1 vertically around the origin gaussian_circs_20 = filter_circles_circular( -1, gaussian_coords_20, 1, 20, 0, 1/2, 0.25 ) # ask for curvs 1 through 20, inside circle ctrd at (0,1/2) with radius 0.25 black_gaussian_doiley = circle_picture( gaussian_circs_20 ) # default style is for black circles black_gaussian_doiley.show( axes=False ) # show with no axes /// }}} {{{id=67| # To choose a colour palette, print out the available schemes of a certain size colour_demo(4) /// }}} {{{id=55| # By adding a few parameters, one can set colour behaviour, in this case modular based on curvature coloured_gaussian_doiley = circle_picture( gaussian_circs_20, circle_colour_scheme=modular_colour, circle_colour_parameter_list = [colour_pool, [4, 1, 0]] ) coloured_gaussian_doiley.show( axes=False ) /// }}} {{{id=54| # One can use gradient-coloured disks instead of circles # Warning, gradient disks look nicer with a high factor, but it takes a long time gradient_gaussian_mandala = circle_picture( gaussian_circs_20, object_type="disk", disk_colour_scheme=gradient_disk, disk_colour_parameter_list = [ modular_colour, [colour_pool, [4, 1, 2]], modular_colour, [colour_pool, [4, 1, 1] ], 1000 ] ) # 1000 makes for a nice smooth gradient gradient_gaussian_mandala.show( axes=False ) /// }}} {{{id=49| # Here's a circle-only snapshot of the Gaussian Schmidt arrangement, with circles fading (using a transparency_scheme) root2val = n(sqrt(2)) gaussian_coords_40 = precompute_circles( -1, 40, 1, 1 ) gaussian_circs_rectangle = filter_circles_rectangular( -1, gaussian_coords_40, 0, 40, -0.1, 1.1, -0.1, 1+0.1 ) gaussian_napkin = circle_picture( gaussian_circs_rectangle, circle_colour_scheme=modular_colour, circle_colour_parameter_list=[colour_saul, [2, 1, 1]], circle_transparency_scheme=increasing_transparency, circle_transparency_parameter_list=[40,1], line_range=3, dis=-1) gaussian_napkin.show(axes=False, xmin=0, xmax=1, ymin=-0, ymax=1) /// Traceback (most recent call last): File "