├── .gitignore ├── LICENSE ├── README.md ├── arcsine ├── arcsine.png ├── arcsine.py ├── random_walk.png └── tosses.py ├── bertrand ├── bertrand.py ├── bertrand1.png ├── bertrand2.png └── bertrand3.png ├── diamond_square ├── cloud.png ├── diamond_square.py └── terrain.png ├── forest_fire ├── circle-overlap.png ├── circle-overlap.py ├── circle-overlap.svg ├── forest.gif ├── forest_fire.mp4 ├── forest_fire.py ├── forest_fire_sm.gif └── forest_fire_still.png ├── goldbach_comet ├── goldbach_comet.png └── goldbach_comet.py ├── lorenz ├── lorenz.png └── lorenz.py ├── maze ├── README.md ├── ca_maze.py ├── ca_maze1.gif ├── df_maze-example.png ├── df_maze.py ├── make_df_maze.py ├── maze.svg ├── maze3.svg └── maze3_solution.svg ├── modular_multiplication_table ├── modmult-12-12.png ├── modmult-12-12_labelled.png ├── modmult-256-256.png ├── modmult-64-8.png └── multiplication_table.py ├── mystery_curve ├── mystery_curve.py ├── mystery_curve_20.png ├── mystery_curve_3.png └── mystery_curve_6.png ├── poisson_disc_sampled_noise ├── periodogram.py ├── periodograms.png ├── poisson.png ├── poisson.py ├── poisson_noise_proc.py ├── uniform.png └── uniform_noise.py ├── prime_visualizations ├── klauber_triangle.png ├── klauber_triangle.py ├── ulam_spiral.png └── ulam_spiral.py ├── primes_last_digits ├── last_digits_hmap.py ├── plot_prime_digits.py ├── prime-last-digits.py ├── prime_digits.png └── prime_digits_hmap.png ├── reuleaux ├── README.md ├── make_reuleaux_construction.py ├── reuleaux-11.svg ├── reuleaux-3.png ├── reuleaux-3.svg └── reuleaux-5.svg └── wilberforce_pendulum ├── wilberforce.py ├── wilberforce_theta-z_plot.png ├── wilberforce_theta-z_polar_plot.png └── wilberforce_z-t_plot.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.pyo 4 | *.db 5 | .DS_Store 6 | .coverage 7 | maze/ca_frames/* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Hill 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scipython_maths 2 | A repository of small maths projects explored with Python. 3 | 4 | The projects in this repository are described in more detail on the [scipython blog](https://scipython.com/blog/) which accompanies my book, [Learning Scientific Programming with Python](https://www.amazon.com/Learning-Scientific-Programming-Python-Christian/dp/1107075416/) 5 | -------------------------------------------------------------------------------- /arcsine/arcsine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/arcsine/arcsine.png -------------------------------------------------------------------------------- /arcsine/arcsine.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import rc 3 | import matplotlib.pyplot as plt 4 | 5 | # Demonstrate that the distribution for the number of times "heads" leads 6 | # "tails" in the sequential tossing of ntosses coins follows the "arcsine 7 | # law". The maths behind this code is described in the scipython blog 8 | # article at https://scipython.com/blog/the-arcsine-law/ 9 | # Christian Hill, March 2017. 10 | 11 | rc('font', **{'family': 'serif', 'serif': ['Computer Modern'], 'size': 16}) 12 | rc('text', usetex=True) 13 | 14 | # Number of coin tosses in each trial sequence. 15 | ntosses = 1000 16 | # Number of trials of ntosses to repeat. 17 | ntrials = 10000 18 | 19 | def coin_tosses(ntosses): 20 | """Return a running score for ntosses coin tosses. 21 | 22 | Each toss scores +1 for a head and -1 for a tail. 23 | 24 | """ 25 | 26 | return np.cumsum(np.random.choice([-1,1], size=ntosses)) 27 | 28 | def n_times_ahead(ntosses): 29 | """Return the number of times "heads" leads in N coin tosses. 30 | 31 | Simulate ntosses tosses of a fair coin and return the number of times 32 | during this sequence that the cumulative number of "heads" results exceeds 33 | the number of "tails" results. 34 | 35 | """ 36 | 37 | tosses = coin_tosses(ntosses) 38 | return sum(tosses>0) 39 | 40 | # Number of tosses out of ntosses that "heads" leads over "tails" for each 41 | # of ntrials trials. 42 | n_ahead = np.array([n_times_ahead(ntosses) for i in range(ntrials)]) 43 | 44 | # Plot a histogram in nbins bins and the arcsine distribution. 45 | nbins = 20 46 | bins = np.linspace(0, ntosses, nbins) 47 | hist, bin_edges = np.histogram(n_ahead, bins=bins, normed=True) 48 | bin_centres = (bin_edges[:-1] + bin_edges[1:]) / 2 49 | 50 | dpi = 72 51 | plt.figure(figsize=(600/dpi, 450/dpi), dpi=dpi) 52 | 53 | # bar widths in units of the x-axis. 54 | bar_width = ntosses/nbins * 0.5 55 | plt.bar(bin_centres, hist, align='center', width=bar_width, facecolor='r', 56 | edgecolor=None, alpha=0.7) 57 | 58 | # The arcsine distribution 59 | x = np.linspace(0, 1, 100) 60 | plt.plot(x*ntosses, 1/np.pi/np.sqrt(x*(1-x))/ntosses, color='g', lw=2) 61 | 62 | plt.xlabel('Number of times ``heads" leads') 63 | plt.savefig('arcsine.png', dpi=dpi) 64 | plt.show() 65 | -------------------------------------------------------------------------------- /arcsine/random_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/arcsine/random_walk.png -------------------------------------------------------------------------------- /arcsine/tosses.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import rc 3 | import matplotlib.pylab as plt 4 | 5 | # Simulate a coin-tossing experiment in which, in each of ntrials trials 6 | # a fair coin is tossed ntosses times and a record kept, on each toss, of 7 | # the difference between the total number of heads and total number of tails 8 | # seen. The maths behind this code is described in the scipython blog 9 | # article at https://scipython.com/blog/the-arcsine-law/ 10 | # Christian Hill, March 2017. 11 | 12 | rc('font', **{'family': 'serif', 'serif': ['Computer Modern'], 'size': 16}) 13 | rc('text', usetex=True) 14 | 15 | def coin_tosses(ntosses): 16 | return np.cumsum(np.random.choice([-1,1], size=ntosses)) 17 | 18 | ntrials = 10 19 | ntosses = 1000 20 | 21 | for i in range(ntrials): 22 | plt.plot(range(ntosses), coin_tosses(ntosses), c='r', alpha=0.4) 23 | plt.axhline(c='k') 24 | plt.xlabel('Toss number') 25 | plt.ylabel(r'$n_\mathrm{heads}-n_\mathrm{tails}$') 26 | plt.savefig('random_walk.png') 27 | plt.show() 28 | -------------------------------------------------------------------------------- /bertrand/bertrand.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.patches import Circle 4 | from matplotlib.lines import Line2D 5 | 6 | # A stochastic analysis of selection methods in Betrand's Paradox. 7 | # A description of this problem is available on the blog page at 8 | # https://scipython.com/blog/bertrands-paradox/ 9 | # Christian Hill, April 2018. 10 | 11 | TAU = 2 * np.pi 12 | 13 | # Fractional RGB values for light grey. 14 | GREY = (0.2,0.2,0.2) 15 | # Don't plot more than this number of chords because they overlap too much 16 | # and obscure the point we're trying to make. 17 | NCHORDS_TO_PLOT = 1000 18 | # Do the statistics using a sample size of nchords 19 | nchords = 10000 20 | # The circle radius. Doesn't matter what it is. 21 | r = 1 22 | # The critical side length of the equilateral triangle inscribed in the circle. 23 | # We are testing if a chord is longer than this length. 24 | tlen = r * np.sqrt(3) 25 | 26 | def setup_axes(): 27 | """Set up the two Axes with the circle and correct limits, aspect.""" 28 | 29 | fig, axes = plt.subplots(nrows=1, ncols=2, subplot_kw={'aspect': 'equal'}) 30 | for ax in axes: 31 | circle = Circle((0,0), r, facecolor='none') 32 | ax.add_artist(circle) 33 | ax.set_xlim((-r,r)) 34 | ax.set_ylim((-r,r)) 35 | ax.axis('off') 36 | return fig, axes 37 | 38 | def bertrand1(): 39 | """Generate random chords and midpoints using "Method 1". 40 | 41 | Pairs of (uniformly-distributed) random points on the unit circle are 42 | selected and joined as chords. 43 | 44 | """ 45 | 46 | angles = np.random.random((nchords,2)) * TAU 47 | chords = np.array((r * np.cos(angles), r * np.sin(angles))) 48 | chords = np.swapaxes(chords, 0, 1) 49 | # The midpoints of the chords 50 | midpoints = np.mean(chords, axis=2).T 51 | return chords, midpoints 52 | 53 | def get_chords_from_midpoints(midpoints): 54 | """Return the chords with the provided midpoints. 55 | 56 | Methods 2 and 3 share this code for retrieving the chord end points from 57 | the midpoints. 58 | 59 | """ 60 | 61 | # We should probably watch out for the edge-case of a "vertical" chord 62 | # (y0=0), but it's rather unlikely over 10000 trials, so don't bother. 63 | chords = np.zeros((nchords, 2, 2)) 64 | for i, (x0, y0) in enumerate(midpoints.T): 65 | # y = mx + c is the equation of the chord. 66 | m = -x0/y0 67 | c = y0 + x0**2/y0 68 | # Solve the quadratic equation determining where the chord intersects 69 | # the circle to find its endpoints. 70 | A, B, C = m**2 + 1, 2*m*c, c**2 - r**2 71 | d = np.sqrt(B**2 - 4*A*C) 72 | x = np.array( ((-B + d), (-B - d))) / 2 / A 73 | y = m*x + c 74 | chords[i] = (x, y) 75 | return chords 76 | 77 | def bertrand2(): 78 | """Generate random chords and midpoints using "Method 2". 79 | 80 | First select a random radius of the circle, and then choose a point 81 | at random (uniformly-distributed) on this radius to be the midpoint of 82 | the chosed chord. 83 | 84 | """ 85 | 86 | angles = np.random.random(nchords) * TAU 87 | radii = np.random.random(nchords) * r 88 | midpoints = np.array((radii * np.cos(angles), radii * np.sin(angles))) 89 | chords = get_chords_from_midpoints(midpoints) 90 | return chords, midpoints 91 | 92 | def bertrand3(): 93 | """Generate random chords and midpoints using "Method 3". 94 | 95 | Select a point at random (uniformly distributed) within the circle, and 96 | consider this point to be the midpoint of the chosed chord. 97 | 98 | """ 99 | 100 | # To ensure the points are uniformly distributed within the circle we 101 | # need to weight the radial distance by the square root of the random 102 | # number chosen on (0,1]: there should be a greater probability for points 103 | # further out from the centre, where there is more room for them. 104 | angles = np.random.random(nchords) * TAU 105 | radii = np.sqrt(np.random.random(nchords)) * r 106 | midpoints = np.array((radii * np.cos(angles), radii * np.sin(angles))) 107 | chords = get_chords_from_midpoints(midpoints) 108 | return chords, midpoints 109 | 110 | bertrand_methods = {1: bertrand1, 2: bertrand2, 3: bertrand3} 111 | 112 | def plot_bertrand(method_number): 113 | # Plot the chords and their midpoints on separate Axes for the selected 114 | # method of picking a chord randomly. 115 | 116 | chords, midpoints = bertrand_methods[method_number]() 117 | 118 | # Here's where we will keep track of which chords are longer than tlen 119 | success = [False] * nchords 120 | 121 | fig, axes = setup_axes() 122 | for i, chord in enumerate(chords): 123 | x, y = chord 124 | if np.hypot(x[0]-x[1], y[0]-y[1]) > tlen: 125 | success[i] = True 126 | if i < NCHORDS_TO_PLOT: 127 | line = Line2D(*chord, color=GREY, alpha=0.1) 128 | axes[0].add_line(line) 129 | axes[1].scatter(*midpoints, s=0.2, color=GREY) 130 | fig.suptitle('Method {}'.format(method_number)) 131 | 132 | prob = np.sum(success)/nchords 133 | print('Bertrand, method {} probability: {}'.format(method_number, prob)) 134 | plt.savefig('bertrand{}.png'.format(method_number)) 135 | plt.show() 136 | 137 | plot_bertrand(1) 138 | plot_bertrand(2) 139 | plot_bertrand(3) 140 | -------------------------------------------------------------------------------- /bertrand/bertrand1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/bertrand/bertrand1.png -------------------------------------------------------------------------------- /bertrand/bertrand2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/bertrand/bertrand2.png -------------------------------------------------------------------------------- /bertrand/bertrand3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/bertrand/bertrand3.png -------------------------------------------------------------------------------- /diamond_square/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/diamond_square/cloud.png -------------------------------------------------------------------------------- /diamond_square/diamond_square.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # Create a cloud-like image based on noise generated by the "diamond-square" 5 | # algorithm. The maths behind this code is described in the scipython blog 6 | # article at 7 | # https://scipython.com/blog/cloud-images-using-the-diamond-square-algorithm/ 8 | # Christian Hill, March 2016. 9 | 10 | DPI = 100 11 | 12 | # The array must be square with edge length 2**n + 1 13 | n = 10 14 | N = 2**n + 1 15 | # f scales the random numbers at each stage of the algorithm 16 | f = 1.0 17 | 18 | # Initialise the array with random numbers at its corners 19 | arr = np.zeros((N, N)) 20 | arr[0::N-1,0::N-1] = np.random.uniform(-1, 1, (2,2)) 21 | side = N-1 22 | 23 | nsquares = 1 24 | while side > 1: 25 | sideo2 = side // 2 26 | 27 | # Diamond step 28 | for ix in range(nsquares): 29 | for iy in range(nsquares): 30 | x0, x1, y0, y1 = ix*side, (ix+1)*side, iy*side, (iy+1)*side 31 | xc, yc = x0 + sideo2, y0 + sideo2 32 | # Set this pixel to the mean of its "diamond" neighbours plus 33 | # a random offset. 34 | arr[yc,xc] = (arr[y0,x0] + arr[y0,x1] + arr[y1,x0] + arr[y1,x1])/4 35 | arr[yc,xc] += f * np.random.uniform(-1,1) 36 | 37 | # Square step: NB don't do this step until the pixels from the preceding 38 | # diamond step have been set. 39 | for iy in range(2*nsquares+1): 40 | yc = sideo2 * iy 41 | for ix in range(nsquares+1): 42 | xc = side * ix + sideo2 * (1 - iy % 2) 43 | if not (0 <= xc < N and 0 <= yc < N): 44 | continue 45 | tot, ntot = 0., 0 46 | # Set this pixel to the mean of its "square" neighbours plus 47 | # a random offset. At the edges, it has only three neighbours 48 | for (dx, dy) in ((-1,0), (1,0), (0,-1), (0,1)): 49 | xs, ys = xc + dx*sideo2, yc + dy*sideo2 50 | if not (0 <= xs < N and 0 <= ys < N): 51 | continue 52 | else: 53 | tot += arr[ys, xs] 54 | ntot += 1 55 | arr[yc, xc] += tot / ntot + f * np.random.uniform(-1,1) 56 | side = sideo2 57 | nsquares *= 2 58 | f /= 2 59 | 60 | fig = plt.figure(figsize=(N/DPI, N/DPI), dpi=DPI) 61 | plt.imshow(arr, cmap=plt.cm.terrain, interpolation='bicubic') 62 | plt.axis('off') 63 | 64 | plt.savefig('terrain.png', dpi=DPI) 65 | plt.show() 66 | -------------------------------------------------------------------------------- /diamond_square/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/diamond_square/terrain.png -------------------------------------------------------------------------------- /forest_fire/circle-overlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/forest_fire/circle-overlap.png -------------------------------------------------------------------------------- /forest_fire/circle-overlap.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import brentq 3 | 4 | def intersection_area(d, R, r): 5 | """Return the area of intersection of two circles. 6 | 7 | The circles have radii R and r, and their centres are separated by d. 8 | 9 | """ 10 | 11 | if d <= abs(R-r): 12 | # One circle is entirely enclosed in the other. 13 | return np.pi * min(R, r)**2 14 | if d >= r + R: 15 | # The circles don't overlap at all. 16 | return 0 17 | 18 | r2, R2, d2 = r**2, R**2, d**2 19 | alpha = np.arccos((d2 + r2 - R2) / (2*d*r)) 20 | beta = np.arccos((d2 + R2 - r2) / (2*d*R)) 21 | return ( r2 * alpha + R2 * beta - 22 | 0.5 * (r2 * np.sin(2*alpha) + R2 * np.sin(2*beta)) 23 | ) 24 | 25 | def find_d(A, R, r): 26 | """ 27 | Find the distance between the centres of two circles giving overlap area A. 28 | 29 | """ 30 | 31 | # A cannot be larger than the area of the smallest circle! 32 | if A > np.pi * min(r, R)**2: 33 | raise ValueError("Intersection area can't be larger than the area" 34 | " of the smallest circle") 35 | if A == 0: 36 | # If the circles don't overlap, place them next to each other 37 | return R+r 38 | 39 | if A < 0: 40 | raise ValueError('Negative intersection area') 41 | 42 | def f(d, A, R, r): 43 | return intersection_area(d, R, r) - A 44 | 45 | a, b = abs(R-r), R+r 46 | d = brentq(f, a, b, args=(A, R, r)) 47 | return d 48 | 49 | r, R = 0.5, 1.5 50 | A = np.pi * r**2 51 | print(intersection_area(1, R, r) / A) 52 | print(intersection_area(np.sqrt(2), R, r) / A) 53 | -------------------------------------------------------------------------------- /forest_fire/circle-overlap.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /forest_fire/forest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/forest_fire/forest.gif -------------------------------------------------------------------------------- /forest_fire/forest_fire.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/forest_fire/forest_fire.mp4 -------------------------------------------------------------------------------- /forest_fire/forest_fire.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib import animation 4 | from matplotlib import colors 5 | 6 | # Create a forest fire animation based on a simple cellular automaton model. 7 | # The maths behind this code is described in the scipython blog article 8 | # at https://scipython.com/blog/the-forest-fire-model/ 9 | # Christian Hill, January 2016. 10 | # Updated January 2020. 11 | 12 | # Displacements from a cell to its eight nearest neighbours 13 | neighbourhood = ((-1,-1), (-1,0), (-1,1), (0,-1), (0, 1), (1,-1), (1,0), (1,1)) 14 | EMPTY, TREE, FIRE = 0, 1, 2 15 | # Colours for visualization: brown for EMPTY, dark green for TREE and orange 16 | # for FIRE. Note that for the colormap to work, this list and the bounds list 17 | # must be one larger than the number of different values in the array. 18 | colors_list = [(0.2,0,0), (0,0.5,0), (1,0,0), 'orange'] 19 | cmap = colors.ListedColormap(colors_list) 20 | bounds = [0,1,2,3] 21 | norm = colors.BoundaryNorm(bounds, cmap.N) 22 | 23 | def iterate(X): 24 | """Iterate the forest according to the forest-fire rules.""" 25 | 26 | # The boundary of the forest is always empty, so only consider cells 27 | # indexed from 1 to nx-2, 1 to ny-2 28 | X1 = np.zeros((ny, nx)) 29 | for ix in range(1,nx-1): 30 | for iy in range(1,ny-1): 31 | if X[iy,ix] == EMPTY and np.random.random() <= p: 32 | X1[iy,ix] = TREE 33 | if X[iy,ix] == TREE: 34 | X1[iy,ix] = TREE 35 | for dx,dy in neighbourhood: 36 | # The diagonally-adjacent trees are further away, so 37 | # only catch fire with a reduced probability: 38 | if abs(dx) == abs(dy) and np.random.random() < 0.573: 39 | continue 40 | if X[iy+dy,ix+dx] == FIRE: 41 | X1[iy,ix] = FIRE 42 | break 43 | else: 44 | if np.random.random() <= f: 45 | X1[iy,ix] = FIRE 46 | return X1 47 | 48 | # The initial fraction of the forest occupied by trees. 49 | forest_fraction = 0.2 50 | # Probability of new tree growth per empty cell, and of lightning strike. 51 | p, f = 0.05, 0.0001 52 | # Forest size (number of cells in x and y directions). 53 | nx, ny = 100, 100 54 | # Initialize the forest grid. 55 | X = np.zeros((ny, nx)) 56 | X[1:ny-1, 1:nx-1] = np.random.randint(0, 2, size=(ny-2, nx-2)) 57 | X[1:ny-1, 1:nx-1] = np.random.random(size=(ny-2, nx-2)) < forest_fraction 58 | 59 | fig = plt.figure(figsize=(25/3, 6.25)) 60 | ax = fig.add_subplot(111) 61 | ax.set_axis_off() 62 | im = ax.imshow(X, cmap=cmap, norm=norm)#, interpolation='nearest') 63 | 64 | # The animation function: called to produce a frame for each generation. 65 | def animate(i): 66 | im.set_data(animate.X) 67 | animate.X = iterate(animate.X) 68 | # Bind our grid to the identifier X in the animate function's namespace. 69 | animate.X = X 70 | 71 | # Interval between frames (ms). 72 | interval = 100 73 | anim = animation.FuncAnimation(fig, animate, interval=interval, frames=200) 74 | #anim.save("forest_fire.mp4") 75 | plt.show() 76 | -------------------------------------------------------------------------------- /forest_fire/forest_fire_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/forest_fire/forest_fire_sm.gif -------------------------------------------------------------------------------- /forest_fire/forest_fire_still.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/forest_fire/forest_fire_still.png -------------------------------------------------------------------------------- /goldbach_comet/goldbach_comet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/goldbach_comet/goldbach_comet.png -------------------------------------------------------------------------------- /goldbach_comet/goldbach_comet.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from matplotlib import rc 3 | import matplotlib.pyplot as plt 4 | 5 | # Plot the "Goldbach Comet" illustrating the number of ways 6 | # the numbers 3–nmax can be written as the sum of two primes. Mathematical 7 | # details are available on my blog article at 8 | # https://scipython.com/blog/the-goldbach-comet/ 9 | # Christian Hill, April 2017. 10 | 11 | rc('font', **{'family': 'serif', 'serif': ['Computer Modern'], 'size': 16}) 12 | rc('text', usetex=True) 13 | 14 | nmax = 2000 15 | # Odd prime numbers up to nmax. 16 | odd_primes = np.array([n for n in range(3, nmax) if all( 17 | (n % m) != 0 for m in range(2,int(np.sqrt(n))+1))]) 18 | 19 | def get_g(n): 20 | g = 0 21 | for p in odd_primes: 22 | if p > n//2: 23 | break 24 | if n-p in odd_primes: 25 | g += 1 26 | return g 27 | 28 | imax = nmax//2 - 1 29 | idx = np.arange(imax) 30 | def get_n_from_index(i): 31 | return 2*(i+2) 32 | n = get_n_from_index(np.arange(imax)) 33 | 34 | g = np.zeros(imax, dtype=int) 35 | for i in idx: 36 | g[i] = get_g(n[i]) 37 | 38 | 39 | i_0 = idx[((n%6)==0)] 40 | i_2 = idx[((n%6)==2)] 41 | i_4 = idx[((n%6)==4)] 42 | 43 | plt.scatter(n[i_0], g[i_0], marker='+', c='b', alpha=0.5, 44 | label=r'$n=0\;(\mathrm{mod}\;6)$') 45 | plt.scatter(n[i_2], g[i_2], marker='+', c='g', alpha=0.5, 46 | label=r'$n=2\;(\mathrm{mod}\;6)$') 47 | plt.scatter(n[i_4], g[i_4], marker='+', c='r', alpha=0.5, 48 | label=r'$n=4\;(\mathrm{mod}\;6)$') 49 | plt.xlim(0, nmax) 50 | plt.ylim(0, np.max(g[i_0])) 51 | plt.xlabel(r'$n$') 52 | plt.ylabel(r'$g(n)$') 53 | plt.legend(loc='upper left', scatterpoints=1) 54 | plt.savefig('goldbach_comet.png') 55 | plt.show() 56 | 57 | 58 | -------------------------------------------------------------------------------- /lorenz/lorenz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/lorenz/lorenz.png -------------------------------------------------------------------------------- /lorenz/lorenz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.integrate import solve_ivp 3 | import matplotlib.pyplot as plt 4 | from mpl_toolkits.mplot3d import Axes3D 5 | 6 | # Create an image of the Lorenz attractor. 7 | # The maths behind this code is described in the scipython blog article 8 | # at https://scipython.com/blog/the-lorenz-attractor/ 9 | # Christian Hill, January 2016. 10 | # Updated, January 2021 to use scipy.integrate.solve_ivp. 11 | 12 | WIDTH, HEIGHT, DPI = 1000, 750, 100 13 | 14 | # Lorenz paramters and initial conditions. 15 | sigma, beta, rho = 10, 2.667, 28 16 | u0, v0, w0 = 0, 1, 1.05 17 | 18 | # Maximum time point and total number of time points. 19 | tmax, n = 100, 10000 20 | 21 | def lorenz(t, X, sigma, beta, rho): 22 | """The Lorenz equations.""" 23 | u, v, w = X 24 | up = -sigma*(u - v) 25 | vp = rho*u - v - u*w 26 | wp = -beta*w + u*v 27 | return up, vp, wp 28 | 29 | # Integrate the Lorenz equations. 30 | soln = solve_ivp(lorenz, (0, tmax), (u0, v0, w0), args=(sigma, beta, rho), 31 | dense_output=True) 32 | # Interpolate solution onto the time grid, t. 33 | t = np.linspace(0, tmax, n) 34 | x, y, z = soln.sol(t) 35 | 36 | # Plot the Lorenz attractor using a Matplotlib 3D projection. 37 | fig = plt.figure(facecolor='k', figsize=(WIDTH/DPI, HEIGHT/DPI)) 38 | ax = fig.gca(projection='3d') 39 | ax.set_facecolor('k') 40 | fig.subplots_adjust(left=0, right=1, bottom=0, top=1) 41 | 42 | # Make the line multi-coloured by plotting it in segments of length s which 43 | # change in colour across the whole time series. 44 | s = 10 45 | cmap = plt.cm.winter 46 | for i in range(0,n-s,s): 47 | ax.plot(x[i:i+s+1], y[i:i+s+1], z[i:i+s+1], color=cmap(i/n), alpha=0.4) 48 | 49 | # Remove all the axis clutter, leaving just the curve. 50 | ax.set_axis_off() 51 | 52 | plt.savefig('lorenz.png', dpi=DPI) 53 | plt.show() 54 | -------------------------------------------------------------------------------- /maze/README.md: -------------------------------------------------------------------------------- 1 | The script `make_df_maze.py` creates a maze using the depth-first algorithm as described at https://scipython.com/blog/making-a-maze/. 2 | Change the dimensions by altering the variables `nx` and `ny`. 3 | 4 | For example with `nx = ny = 40`: 5 | 6 |

7 | Sample depth-first 40x40 maze 8 |

9 | 10 | `ca_maze.py` creates the frames for an animation of the growth of a maze using the cellular automaton algorithm described at https://scipython.com/blog/maze-generation-by-cellular-automaton/. The frames are written to the subdirectory `ca_frames/` and the maze size is again set by the variables `nx` and `ny`. The frames can be put together into an animated gif with [Imagemagick](https://www.imagemagick.org/script/index.php)'s `convert` utility. For example: 11 | 12 |

13 | Sample cellular automaton maze generation 14 |

15 | -------------------------------------------------------------------------------- /maze/ca_maze.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.signal import convolve2d 3 | import matplotlib.pyplot as plt 4 | 5 | # Create a maze using the cellular automaton approach described at 6 | # https://scipython.com/blog/maze-generation-by-cellular-automaton/ 7 | # The frames for animation of the growth of the maze are saved to 8 | # the subdirectory ca_frames/. 9 | # Christian Hill, January 2018. 10 | 11 | def ca_step(X): 12 | """Evolve the maze by a single CA step.""" 13 | 14 | K = np.ones((3, 3)) 15 | n = convolve2d(X, K, mode='same', boundary='wrap') - X 16 | return (n == 3) | (X & ((n > 0) & (n < 6))) 17 | 18 | # Maze size 19 | nx, ny = 200, 150 20 | X = np.zeros((ny, nx), dtype=np.bool) 21 | # Size of initial random area (must be even numbers) 22 | mx, my = 20, 16 23 | 24 | # Initialize a patch with a random mx x my region 25 | r = np.random.random((my, mx)) > 0.75 26 | X[ny//2-my//2:ny//2+my//2, nx//2-mx//2:nx//2+mx//2] = r 27 | 28 | # Total number of iterations 29 | nit = 400 30 | # Make an image every ipf iterations 31 | ipf = 10 32 | 33 | # Figure dimensions (pixels) and resolution (dpi) 34 | width, height, dpi = 600, 450, 10 35 | fig = plt.figure(figsize=(width/dpi, height/dpi), dpi=dpi) 36 | ax = fig.add_subplot(111) 37 | 38 | for i in range(nit): 39 | X = ca_step(X) 40 | if not i % ipf: 41 | print('{}/{}'.format(i,nit)) 42 | im = ax.imshow(X, cmap=plt.cm.binary, interpolation='nearest') 43 | plt.axis('off') 44 | plt.savefig('ca_frames/_img{:04d}.png'.format(i), dpi=dpi) 45 | plt.cla() 46 | 47 | -------------------------------------------------------------------------------- /maze/ca_maze1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/maze/ca_maze1.gif -------------------------------------------------------------------------------- /maze/df_maze-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/maze/df_maze-example.png -------------------------------------------------------------------------------- /maze/df_maze.py: -------------------------------------------------------------------------------- 1 | # df_maze.py 2 | import random 3 | 4 | 5 | # Create a maze using the depth-first algorithm described at 6 | # https://scipython.com/blog/making-a-maze/ 7 | # Christian Hill, April 2017. 8 | 9 | class Cell: 10 | """A cell in the maze. 11 | 12 | A maze "Cell" is a point in the grid which may be surrounded by walls to 13 | the north, east, south or west. 14 | 15 | """ 16 | 17 | # A wall separates a pair of cells in the N-S or W-E directions. 18 | wall_pairs = {'N': 'S', 'S': 'N', 'E': 'W', 'W': 'E'} 19 | # A mapping of cardinal directions to coordinate differences. 20 | delta = {'W': (-1, 0), 'E': (1, 0), 'S': (0, 1), 'N': (0, -1)} 21 | 22 | def __init__(self, x, y): 23 | """Initialize the cell at (x,y). At first it is surrounded by walls.""" 24 | 25 | self.x, self.y = x, y 26 | self.walls = {'N': True, 'S': True, 'E': True, 'W': True} 27 | 28 | def __repr__(self): 29 | """return a string representation of a cell""" 30 | return f'({self.x}, {self.y})' 31 | 32 | def has_all_walls(self): 33 | """Does this cell still have all its walls?""" 34 | 35 | return all(self.walls.values()) 36 | 37 | def knock_down_wall(self, other, wall): 38 | """Knock down the wall between cells self and other.""" 39 | 40 | self.walls[wall] = False 41 | other.walls[Cell.wall_pairs[wall]] = False 42 | 43 | 44 | class Maze: 45 | """A Maze, represented as a grid of cells.""" 46 | 47 | def __init__(self, nx, ny, ix=0, iy=0): 48 | """Initialize the maze grid. 49 | The maze consists of nx x ny cells and will be constructed starting 50 | at the cell indexed at (ix, iy). 51 | 52 | """ 53 | 54 | self.nx, self.ny = nx, ny 55 | self.ix, self.iy = ix, iy 56 | self.maze_map = [[Cell(x, y) for y in range(ny)] for x in range(nx)] 57 | 58 | self.add_begin_end = False 59 | self.add_treasure = False 60 | self.treasure_x = random.randint(0, self.nx-1) 61 | self.treasure_y = random.randint(0, self.ny-1) 62 | 63 | # Give the coordinates of walls that you do *not* wish to be 64 | # present in the output here. 65 | self.excluded_walls = [((nx-1, ny), (nx, ny)), 66 | ((0, 0), (0, 1))] 67 | 68 | # Store the solution to the maze 69 | self.solution = None 70 | 71 | def cell_at(self, x, y): 72 | """Return the Cell object at (x,y).""" 73 | 74 | return self.maze_map[x][y] 75 | 76 | 77 | def __str__(self): 78 | """Return a (crude) string representation of the maze.""" 79 | 80 | maze_rows = ['-' * self.nx * 2] 81 | for y in range(self.ny): 82 | maze_row = ['|'] 83 | for x in range(self.nx): 84 | if self.maze_map[x][y].walls['E']: 85 | maze_row.append(' |') 86 | else: 87 | maze_row.append(' ') 88 | maze_rows.append(''.join(maze_row)) 89 | maze_row = ['|'] 90 | for x in range(self.nx): 91 | if self.maze_map[x][y].walls['S']: 92 | maze_row.append('-+') 93 | else: 94 | maze_row.append(' +') 95 | maze_rows.append(''.join(maze_row)) 96 | return '\n'.join(maze_rows) 97 | 98 | 99 | def write_svg(self, filename, solution=False): 100 | """Write an SVG image of the maze to filename.""" 101 | 102 | aspect_ratio = self.nx / self.ny 103 | # Pad the maze all around by this amount. 104 | padding = 10 105 | # Height and width of the maze image (excluding padding), in pixels 106 | height = 500 107 | width = int(height * aspect_ratio) 108 | # Scaling factors mapping maze coordinates to image coordinates 109 | scy, scx = height / self.ny, width / self.nx 110 | 111 | def write_wall(f, x1, y1, x2, y2): 112 | """Write a single wall to the SVG image file handle f.""" 113 | 114 | if ((x1, y1), (x2, y2)) in self.excluded_walls: 115 | print(f'Excluding wall at {((x1, y1), (x2, y2))}') 116 | return 117 | sx1, sy1, sx2, sy2 = x1*scx, y1*scy, x2*scx, y2*scy 118 | print('' 119 | .format(sx1, sy1, sx2, sy2), file=f) 120 | 121 | def add_cell_rect(f, x, y, colour): 122 | pad = 5 123 | print(f'', file=f) 125 | 126 | def add_path_segment(f, cell, next_cell): 127 | sx1, sy1 = scx * (cell.x + 0.5), scy * (cell.y + 0.5) 128 | sx2, sy2 = scx * (next_cell.x + 0.5), scy * (next_cell.y + 0.5) 129 | print(f'', 130 | file=f) 131 | 132 | # Write the SVG image file for maze 133 | with open(filename, 'w') as f: 134 | # SVG preamble and styles. 135 | print('', file=f) 136 | print('' 139 | .format(width + 2 * padding, height + 2 * padding, 140 | -padding, -padding, width + 2 * padding, height + 2 * padding), 141 | file=f) 142 | print('\n\n', file=f) 147 | # Draw the "South" and "East" walls of each cell, if present (these 148 | # are the "North" and "West" walls of a neighbouring cell in 149 | # general, of course). 150 | for x in range(self.nx): 151 | for y in range(self.ny): 152 | if self.cell_at(x, y).walls['S']: 153 | x1, y1, x2, y2 = x, y+1, x+1, y+1 154 | write_wall(f, x1, y1, x2, y2) 155 | if self.cell_at(x, y).walls['E']: 156 | x1, y1, x2, y2 = x+1, y, x+1, y+1 157 | write_wall(f, x1, y1, x2, y2) 158 | 159 | # Draw the North and West maze border, which won't have been drawn 160 | # by the procedure above. 161 | for x in range(self.nx): 162 | write_wall(f, x, 0, x+1, 0) 163 | for y in range(self.ny): 164 | write_wall(f, 0, y, 0, y+1) 165 | 166 | #print(''.format(width), file=f) 167 | #print(''.format(height), file=f) 168 | 169 | if self.add_begin_end: 170 | add_cell_rect(f, 0, 0, 'green') 171 | add_cell_rect(f, self.nx - 1, self.ny - 1, 'red') 172 | if self.add_treasure: 173 | add_cell_rect(f, self.treasure_x, self.treasure_y, 'yellow') 174 | 175 | if solution: 176 | if self.solution is None: 177 | print('Error: There is no solution stored.') 178 | else: 179 | for i, cell in enumerate(self.solution[:-1]): 180 | next_cell = self.solution[i+1] 181 | add_path_segment(f, cell, next_cell) 182 | 183 | print('', file=f) 184 | 185 | 186 | def find_valid_neighbours(self, cell): 187 | """Return a list of unvisited neighbours to cell.""" 188 | 189 | neighbours = [] 190 | for direction, (dx, dy) in Cell.delta.items(): 191 | x2, y2 = cell.x + dx, cell.y + dy 192 | if (0 <= x2 < self.nx) and (0 <= y2 < self.ny): 193 | neighbour = self.cell_at(x2, y2) 194 | if neighbour.has_all_walls(): 195 | neighbours.append((direction, neighbour)) 196 | return neighbours 197 | 198 | 199 | def get_solution(self): 200 | return self.solution 201 | 202 | 203 | def make_maze(self): 204 | # Total number of cells. 205 | n = self.nx * self.ny 206 | cell_stack = [] 207 | current_cell = self.cell_at(self.ix, self.iy) 208 | # Total number of visited cells during maze construction. 209 | nv = 1 210 | 211 | while nv < n: 212 | neighbours = self.find_valid_neighbours(current_cell) 213 | 214 | if not neighbours: 215 | # We've reached a dead end: backtrack. 216 | current_cell = cell_stack.pop() 217 | continue 218 | 219 | # Choose a random neighbouring cell and move to it. 220 | direction, next_cell = random.choice(neighbours) 221 | current_cell.knock_down_wall(next_cell, direction) 222 | cell_stack.append(current_cell) 223 | current_cell = next_cell 224 | 225 | # Store the solution if we are at the exit cell 226 | if (current_cell.x == self.nx - 1) and \ 227 | (current_cell.y == self.ny - 1): 228 | self.solution = cell_stack.copy() 229 | self.solution.append(next_cell) 230 | nv += 1 231 | -------------------------------------------------------------------------------- /maze/make_df_maze.py: -------------------------------------------------------------------------------- 1 | from df_maze import Maze 2 | 3 | # Maze dimensions (ncols, nrows) 4 | nx, ny = 15, 15 5 | # Maze entry position 6 | ix, iy = 0, 0 7 | 8 | maze = Maze(nx, ny, ix, iy) 9 | maze.add_begin_end = True 10 | maze.add_treasure = False 11 | maze.make_maze() 12 | 13 | print(maze) 14 | maze.write_svg('maze3.svg') 15 | maze.write_svg('maze3_solution.svg', solution=True) 16 | -------------------------------------------------------------------------------- /maze/maze.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /maze/maze3.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /maze/maze3_solution.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | -------------------------------------------------------------------------------- /modular_multiplication_table/modmult-12-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/modular_multiplication_table/modmult-12-12.png -------------------------------------------------------------------------------- /modular_multiplication_table/modmult-12-12_labelled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/modular_multiplication_table/modmult-12-12_labelled.png -------------------------------------------------------------------------------- /modular_multiplication_table/modmult-256-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/modular_multiplication_table/modmult-256-256.png -------------------------------------------------------------------------------- /modular_multiplication_table/modmult-64-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/modular_multiplication_table/modmult-64-8.png -------------------------------------------------------------------------------- /modular_multiplication_table/multiplication_table.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy as np 3 | import matplotlib 4 | import matplotlib.pyplot as plt 5 | from matplotlib.ticker import FuncFormatter 6 | 7 | # Create a colour-coded multiplication table based on modular arithmetic. 8 | # The maths behind this code is described in the scipython blog article at 9 | # https://scipython.com/blog/visulaizing-modular-multiplication-tables/ 10 | # Christian Hill, May 2016. 11 | 12 | def multiplication_table(n, N=None, number_labels=True): 13 | """Create and plot an image of a multiplication table modulo n 14 | 15 | The table is of ij % n for i, j = 1, 2, ..., N-1. If not supplied, 16 | N defaults to n. If N is a mutiple of n, the pattern is repeated 17 | across the created image. The "rainbow" colormap is used, but zeros 18 | (corresponding to factors of n) are displayed in white. 19 | 20 | """ 21 | 22 | if not N: 23 | N=n 24 | 25 | # A multiplication table (modulo n) 26 | arr = np.fromfunction(lambda i,j:(i+1)*(j+1) % n, (N-1,N-1)) 27 | 28 | # Select a colormap, but we'll set 0 values to white 29 | cmap = matplotlib.cm.get_cmap('rainbow') 30 | cmap.set_under('w') 31 | 32 | fig, ax = plt.subplots() 33 | # Plot an image of the multiplication table in colours for values greater 34 | # than 1. Zero values get plotted in white thanks to set_under, above. 35 | ax.imshow(arr, interpolation='nearest', cmap=cmap, vmin=1) 36 | 37 | # Make sure the tick marks are correct (start at 1) 38 | tick_formatter = FuncFormatter(lambda v, pos: str(int(v+1))) 39 | ax.xaxis.set_major_formatter(tick_formatter) 40 | ax.yaxis.set_major_formatter(tick_formatter) 41 | 42 | # For small n, write the value in each box of the array image. 43 | if number_labels and N < 21: 44 | for i in range(N-1): 45 | for j in range(N-1): 46 | ax.annotate(s=str((i+1)*(j+1)%n), xy=(i,j), ha='center', 47 | va='center') 48 | 49 | # The user supplies n (and optionally N) as command line arguments 50 | n = int(sys.argv[1]) 51 | try: 52 | N = int(sys.argv[2]) 53 | except IndexError: 54 | N = None 55 | 56 | number_labels = False 57 | multiplication_table(n, N, number_labels) 58 | plt.savefig('modmult-{}-{}.png'.format(N if N else n, n)) 59 | plt.show() 60 | -------------------------------------------------------------------------------- /mystery_curve/mystery_curve.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | # Create a pleasing curve in the complex plane based on the formula 6 | # f(t) = e^(it)[1 - e^(ikt)/2 + i.e^(-ikt)/3]. 7 | # The maths behind this code is described in the scipython blog 8 | # article at https://scipython.com/blog/the-mystery-curve/ 9 | # Christian Hill, May 2016. 10 | 11 | def f(t, k): 12 | """Return the "Mystery Curve" for parameter k on a grid of t values.""" 13 | 14 | def P(z): 15 | return 1 - z / 2 - 1 / z**3 / 3j 16 | return np.exp(1j*t) * P(np.exp(k*1j*t)) 17 | 18 | # k is supplied as a command line argument. 19 | k = int(sys.argv[1]) 20 | 21 | # Choose a grid of t values at a suitable resolution so that the curve. 22 | # is well-represented. 23 | t = np.linspace(0, 2*np.pi, 200*k+1); 24 | 25 | u = f(t, k) 26 | 27 | # Plot the Mystery Curve in a pleasing colour, removing the axis clutter. 28 | fig, ax = plt.subplots(facecolor='w') 29 | ax.plot(np.real(u), np.imag(u), lw=2, color='m', alpha=0.5) 30 | ax.set_aspect('equal') 31 | plt.axis('off') 32 | 33 | plt.savefig('mystery_curve_{}.png'.format(k)) 34 | plt.show() 35 | 36 | -------------------------------------------------------------------------------- /mystery_curve/mystery_curve_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/mystery_curve/mystery_curve_20.png -------------------------------------------------------------------------------- /mystery_curve/mystery_curve_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/mystery_curve/mystery_curve_3.png -------------------------------------------------------------------------------- /mystery_curve/mystery_curve_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/mystery_curve/mystery_curve_6.png -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/periodogram.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from poisson import PoissonDisc 4 | 5 | # Generate periodogram images for uniformly-distributed noise and 6 | # Poisson disc-sampled ("blue") noise in two dimensions. 7 | # For mathematical details, please see the blog articles at 8 | # https://scipython.com/blog/poisson-disc-sampling-in-python/ 9 | # https://scipython.com/blog/power-spectra-for-blue-and-uniform-noise/ 10 | # Christian Hill, March 2017. 11 | 12 | class UniformNoise(): 13 | """A class for generating uniformly distributed, 2D noise.""" 14 | 15 | def __init__(self, width=50, height=50, n=None): 16 | """Initialise the size of the domain and number of points to sample.""" 17 | 18 | self.width, self.height = width, height 19 | if n is None: 20 | n = int(width * height) 21 | self.n = n 22 | 23 | def reset(self): 24 | pass 25 | 26 | def sample(self): 27 | return np.array([np.random.uniform(0, width, size=self.n), 28 | np.random.uniform(0, height, size=self.n)]).T 29 | 30 | # domain size, minimum distance between samples for Poisson disc method... 31 | width = height = 100 32 | r = 2 33 | poisson_disc = PoissonDisc(width, height, r) 34 | # Expected number of samples from Poisson disc method... 35 | n = int(width * height / np.pi / poisson_disc.a**2) 36 | # ... use the same for uniform noise. 37 | uniform_noise = UniformNoise(width, height, n) 38 | 39 | # Number of sampling runs to do (to remove noise from the noise in the power 40 | # spectrum). 41 | N = 100 42 | # Sampling parameter, when putting the sample points onto the domain 43 | M = 5 44 | 45 | fig, ax = plt.subplots(nrows=2, ncols=2) 46 | 47 | for j, noise in enumerate((poisson_disc, uniform_noise)): 48 | print(noise.__class__.__name__) 49 | spec = np.zeros((height * M, width * M)) 50 | for i in range(N): 51 | print('{}/{}'.format(i+1, N)) 52 | noise.reset() 53 | samples = np.array(noise.sample()) 54 | domain = np.zeros((height * M, width * M)) 55 | for pt in samples: 56 | coords = int(pt[1] * M), int(pt[0] * M) 57 | domain[coords] = 1 58 | 59 | # Do the Fourier Trasform, shift the frequencies and add to the 60 | # running total. 61 | f = np.fft.fft2(domain) 62 | fshift = np.fft.fftshift(f) 63 | spec += np.log(np.abs(fshift)) 64 | 65 | # Plot the a set of random points and the power spectrum. 66 | ax[0][j].imshow(domain, cmap=plt.cm.Greys) 67 | ax[1][j].imshow(spec, cmap=plt.cm.Greys_r) 68 | # Remove axis ticks and annotations 69 | for k in (0,1): 70 | ax[k][j].tick_params(which='both', bottom='off', left='off', 71 | top='off', right='off', labelbottom='off', labelleft='off') 72 | 73 | plt.savefig('periodograms.png') 74 | plt.show() 75 | -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/periodograms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/poisson_disc_sampled_noise/periodograms.png -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/poisson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/poisson_disc_sampled_noise/poisson.png -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/poisson.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # For mathematical details of this algorithm, please see the blog 5 | # article at https://scipython.com/blog/poisson-disc-sampling-in-python/ 6 | # Christian Hill, March 2017. 7 | 8 | class PoissonDisc(): 9 | """A class for generating two-dimensional Possion (blue) noise).""" 10 | 11 | def __init__(self, width=50, height=50, r=1, k=30): 12 | self.width, self.height = width, height 13 | self.r = r 14 | self.k = k 15 | 16 | # Cell side length 17 | self.a = r/np.sqrt(2) 18 | # Number of cells in the x- and y-directions of the grid 19 | self.nx, self.ny = int(width / self.a) + 1, int(height / self.a) + 1 20 | 21 | self.reset() 22 | 23 | def reset(self): 24 | """Reset the cells dictionary.""" 25 | 26 | # A list of coordinates in the grid of cells 27 | coords_list = [(ix, iy) for ix in range(self.nx) 28 | for iy in range(self.ny)] 29 | # Initilalize the dictionary of cells: each key is a cell's coordinates 30 | # the corresponding value is the index of that cell's point's 31 | # coordinates in the samples list (or None if the cell is empty). 32 | self.cells = {coords: None for coords in coords_list} 33 | 34 | def get_cell_coords(self, pt): 35 | """Get the coordinates of the cell that pt = (x,y) falls in.""" 36 | 37 | return int(pt[0] // self.a), int(pt[1] // self.a) 38 | 39 | def get_neighbours(self, coords): 40 | """Return the indexes of points in cells neighbouring cell at coords. 41 | For the cell at coords = (x,y), return the indexes of points in the 42 | cells with neighbouring coordinates illustrated below: ie those cells 43 | that could contain points closer than r. 44 | 45 | ooo 46 | ooooo 47 | ooXoo 48 | ooooo 49 | ooo 50 | 51 | """ 52 | 53 | dxdy = [(-1,-2),(0,-2),(1,-2),(-2,-1),(-1,-1),(0,-1),(1,-1),(2,-1), 54 | (-2,0),(-1,0),(1,0),(2,0),(-2,1),(-1,1),(0,1),(1,1),(2,1), 55 | (-1,2),(0,2),(1,2),(0,0)] 56 | neighbours = [] 57 | for dx, dy in dxdy: 58 | neighbour_coords = coords[0] + dx, coords[1] + dy 59 | if not (0 <= neighbour_coords[0] < self.nx and 60 | 0 <= neighbour_coords[1] < self.ny): 61 | # We're off the grid: no neighbours here. 62 | continue 63 | neighbour_cell = self.cells[neighbour_coords] 64 | if neighbour_cell is not None: 65 | # This cell is occupied: store the index of the contained point 66 | neighbours.append(neighbour_cell) 67 | return neighbours 68 | 69 | def point_valid(self, pt): 70 | """Is pt a valid point to emit as a sample? 71 | 72 | It must be no closer than r from any other point: check the cells in 73 | its immediate neighbourhood. 74 | 75 | """ 76 | 77 | cell_coords = self.get_cell_coords(pt) 78 | for idx in self.get_neighbours(cell_coords): 79 | nearby_pt = self.samples[idx] 80 | # Squared distance between candidate point, pt, and this nearby_pt. 81 | distance2 = (nearby_pt[0]-pt[0])**2 + (nearby_pt[1]-pt[1])**2 82 | if distance2 < self.r**2: 83 | # The points are too close, so pt is not a candidate. 84 | return False 85 | # All points tested: if we're here, pt is valid 86 | return True 87 | 88 | def get_point(self, refpt): 89 | """Try to find a candidate point near refpt to emit in the sample. 90 | 91 | We draw up to k points from the annulus of inner radius r, outer radius 92 | 2r around the reference point, refpt. If none of them are suitable 93 | (because they're too close to existing points in the sample), return 94 | False. Otherwise, return the pt. 95 | 96 | """ 97 | 98 | i = 0 99 | while i < self.k: 100 | i += 1 101 | rho = np.sqrt(np.random.uniform(r**2, 4 * r**2)) 102 | theta = np.random.uniform(0, 2*np.pi) 103 | pt = refpt[0] + rho*np.cos(theta), refpt[1] + rho*np.sin(theta) 104 | if not (0 <= pt[0] < self.width and 0 <= pt[1] < self.height): 105 | # This point falls outside the domain, so try again. 106 | continue 107 | if self.point_valid(pt): 108 | return pt 109 | # We failed to find a suitable point in the vicinity of refpt. 110 | return False 111 | 112 | def sample(self): 113 | """Poisson disc random sampling in 2D. 114 | 115 | Draw random samples on the domain width x height such that no two 116 | samples are closer than r apart. The parameter k determines the 117 | maximum number of candidate points to be chosen around each reference 118 | point before removing it from the "active" list. 119 | 120 | """ 121 | 122 | # Pick a random point to start with. 123 | pt = (np.random.uniform(0, self.width), 124 | np.random.uniform(0, self.height)) 125 | self.samples = [pt] 126 | # Our first sample is indexed at 0 in the samples list... 127 | self.cells[self.get_cell_coords(pt)] = 0 128 | # and it is active, in the sense that we're going to look for more 129 | # points in its neighbourhood. 130 | active = [0] 131 | 132 | # As long as there are points in the active list, keep looking for 133 | # samples. 134 | while active: 135 | # choose a random "reference" point from the active list. 136 | idx = np.random.choice(active) 137 | refpt = self.samples[idx] 138 | # Try to pick a new point relative to the reference point. 139 | pt = self.get_point(refpt) 140 | if pt: 141 | # Point pt is valid: add it to samples list and mark as active 142 | self.samples.append(pt) 143 | nsamples = len(self.samples) - 1 144 | active.append(nsamples) 145 | self.cells[self.get_cell_coords(pt)] = nsamples 146 | else: 147 | # We had to give up looking for valid points near refpt, so 148 | # remove it from the list of "active" points. 149 | active.remove(idx) 150 | 151 | return self.samples 152 | -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/poisson_noise_proc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # Procedural algorithm for the generation of two-dimensional Poission-disc 5 | # sampled ("blue") noise. For mathematical details, please see the blog 6 | # article at https://scipython.com/blog/poisson-disc-sampling-in-python/ 7 | # Christian Hill, March 2017. 8 | 9 | # Choose up to k points around each reference point as candidates for a new 10 | # sample point 11 | k = 30 12 | 13 | # Minimum distance between samples 14 | r = 1.7 15 | 16 | width, height = 60, 45 17 | 18 | # Cell side length 19 | a = r/np.sqrt(2) 20 | # Number of cells in the x- and y-directions of the grid 21 | nx, ny = int(width / a) + 1, int(height / a) + 1 22 | 23 | # A list of coordinates in the grid of cells 24 | coords_list = [(ix, iy) for ix in range(nx) for iy in range(ny)] 25 | # Initilalize the dictionary of cells: each key is a cell's coordinates, the 26 | # corresponding value is the index of that cell's point's coordinates in the 27 | # samples list (or None if the cell is empty). 28 | cells = {coords: None for coords in coords_list} 29 | 30 | def get_cell_coords(pt): 31 | """Get the coordinates of the cell that pt = (x,y) falls in.""" 32 | 33 | return int(pt[0] // a), int(pt[1] // a) 34 | 35 | def get_neighbours(coords): 36 | """Return the indexes of points in cells neighbouring cell at coords. 37 | 38 | For the cell at coords = (x,y), return the indexes of points in the cells 39 | with neighbouring coordinates illustrated below: ie those cells that could 40 | contain points closer than r. 41 | 42 | ooo 43 | ooooo 44 | ooXoo 45 | ooooo 46 | ooo 47 | 48 | """ 49 | 50 | dxdy = [(-1,-2),(0,-2),(1,-2),(-2,-1),(-1,-1),(0,-1),(1,-1),(2,-1), 51 | (-2,0),(-1,0),(1,0),(2,0),(-2,1),(-1,1),(0,1),(1,1),(2,1), 52 | (-1,2),(0,2),(1,2),(0,0)] 53 | neighbours = [] 54 | for dx, dy in dxdy: 55 | neighbour_coords = coords[0] + dx, coords[1] + dy 56 | if not (0 <= neighbour_coords[0] < nx and 57 | 0 <= neighbour_coords[1] < ny): 58 | # We're off the grid: no neighbours here. 59 | continue 60 | neighbour_cell = cells[neighbour_coords] 61 | if neighbour_cell is not None: 62 | # This cell is occupied: store this index of the contained point. 63 | neighbours.append(neighbour_cell) 64 | return neighbours 65 | 66 | def point_valid(pt): 67 | """Is pt a valid point to emit as a sample? 68 | 69 | It must be no closer than r from any other point: check the cells in its 70 | immediate neighbourhood. 71 | 72 | """ 73 | 74 | cell_coords = get_cell_coords(pt) 75 | for idx in get_neighbours(cell_coords): 76 | nearby_pt = samples[idx] 77 | # Squared distance between or candidate point, pt, and this nearby_pt. 78 | distance2 = (nearby_pt[0]-pt[0])**2 + (nearby_pt[1]-pt[1])**2 79 | if distance2 < r**2: 80 | # The points are too close, so pt is not a candidate. 81 | return False 82 | # All points tested: if we're here, pt is valid 83 | return True 84 | 85 | def get_point(k, refpt): 86 | """Try to find a candidate point relative to refpt to emit in the sample. 87 | 88 | We draw up to k points from the annulus of inner radius r, outer radius 2r 89 | around the reference point, refpt. If none of them are suitable (because 90 | they're too close to existing points in the sample), return False. 91 | Otherwise, return the pt. 92 | 93 | """ 94 | i = 0 95 | while i < k: 96 | rho, theta = np.random.uniform(r, 2*r), np.random.uniform(0, 2*np.pi) 97 | pt = refpt[0] + rho*np.cos(theta), refpt[1] + rho*np.sin(theta) 98 | if not (0 < pt[0] < width and 0 < pt[1] < height): 99 | # This point falls outside the domain, so try again. 100 | continue 101 | if point_valid(pt): 102 | return pt 103 | i += 1 104 | # We failed to find a suitable point in the vicinity of refpt. 105 | return False 106 | 107 | # Pick a random point to start with. 108 | pt = (np.random.uniform(0, width), np.random.uniform(0, height)) 109 | samples = [pt] 110 | # Our first sample is indexed at 0 in the samples list... 111 | cells[get_cell_coords(pt)] = 0 112 | # ... and it is active, in the sense that we're going to look for more points 113 | # in its neighbourhood. 114 | active = [0] 115 | 116 | nsamples = 1 117 | # As long as there are points in the active list, keep trying to find samples. 118 | while active: 119 | # choose a random "reference" point from the active list. 120 | idx = np.random.choice(active) 121 | refpt = samples[idx] 122 | # Try to pick a new point relative to the reference point. 123 | pt = get_point(k, refpt) 124 | if pt: 125 | # Point pt is valid: add it to the samples list and mark it as active 126 | samples.append(pt) 127 | nsamples += 1 128 | active.append(len(samples)-1) 129 | cells[get_cell_coords(pt)] = len(samples) - 1 130 | else: 131 | # We had to give up looking for valid points near refpt, so remove it 132 | # from the list of "active" points. 133 | active.remove(idx) 134 | 135 | plt.scatter(*zip(*samples), color='r', alpha=0.6, lw=0) 136 | plt.xlim(0, width) 137 | plt.ylim(0, height) 138 | plt.axis('off') 139 | plt.savefig('poisson.png') 140 | plt.show() 141 | 142 | -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/uniform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/poisson_disc_sampled_noise/uniform.png -------------------------------------------------------------------------------- /poisson_disc_sampled_noise/uniform_noise.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # A short script to generate an image of two-dimensional, uniformly- 5 | # distributed noise, illustrating "clustering". 6 | # For more details, please see the blog article at: 7 | # https://scipython.com/blog/poisson-disc-sampling-in-python/ 8 | # Christian Hill, March 2017. 9 | 10 | width, height = 60, 45 11 | N = width * height // 4 12 | plt.scatter(np.random.uniform(0,width,N), np.random.uniform(0,height,N), 13 | c='g', alpha=0.6, lw=0) 14 | plt.xlim(0,width) 15 | plt.ylim(0,height) 16 | plt.axis('off') 17 | plt.savefig('uniform.png') 18 | plt.show() 19 | -------------------------------------------------------------------------------- /prime_visualizations/klauber_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/prime_visualizations/klauber_triangle.png -------------------------------------------------------------------------------- /prime_visualizations/klauber_triangle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import matplotlib.cm as cm 4 | 5 | # Visualize prime numbers as a "Klauber triangle" 6 | # The maths behind this code is described in the scipython blog 7 | # article at https://scipython.com/blog/the-klauber-triangle/ 8 | # Christian Hill, November 2016. 9 | 10 | n = 200 11 | ncols = 2*n+1 12 | nmax = n**2 13 | 14 | # Prime numbers up to and including n**2. 15 | primes = np.array([n for n in range(2,n**2+1) if all( 16 | (n % m) != 0 for m in range(2,int(np.sqrt(n))+1))]) 17 | a = np.zeros(nmax) 18 | a[primes-1]=1 19 | 20 | arr = np.zeros((n, ncols)) 21 | for i in range(n): 22 | arr[i,(n-i):(n+i+1)] = a[i**2:i**2+2*i+1] 23 | 24 | fig, ax = plt.subplots() 25 | ax.matshow(arr, cmap=cm.binary) 26 | ax.axis('off') 27 | # Ensure the Axes are centred in the figure 28 | ax.set_position([0.1,0.1,0.8,0.8]) 29 | plt.savefig('klauber_triangle.png') 30 | plt.show() 31 | -------------------------------------------------------------------------------- /prime_visualizations/ulam_spiral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/prime_visualizations/ulam_spiral.png -------------------------------------------------------------------------------- /prime_visualizations/ulam_spiral.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import matplotlib.cm as cm 4 | 5 | # Visualize prime numbers as an "Ulam spiral" 6 | # The maths behind this code is described in the scipython blog 7 | # article at https://scipython.com/blog/the-ulam-spiral/ 8 | # Christian Hill, October 2016. 9 | 10 | def make_spiral(arr): 11 | nrows, ncols= arr.shape 12 | idx = np.arange(nrows*ncols).reshape(nrows,ncols)[::-1] 13 | spiral_idx = [] 14 | while idx.size: 15 | spiral_idx.append(idx[0]) 16 | # Remove the first row (the one we've just appended to spiral). 17 | idx = idx[1:] 18 | # Rotate the rest of the array anticlockwise 19 | idx = idx.T[::-1] 20 | # Make a flat array of indices spiralling into the array. 21 | spiral_idx = np.hstack(spiral_idx) 22 | # Index into a flattened version of our target array with spiral indices. 23 | spiral = np.empty_like(arr) 24 | spiral.flat[spiral_idx] = arr.flat[::-1] 25 | return spiral 26 | 27 | # edge size of the square array. 28 | w = 251 29 | # Prime numbers up to and including w**2. 30 | primes = np.array([n for n in range(2,w**2+1) if all( 31 | (n % m) != 0 for m in range(2,int(np.sqrt(n))+1))]) 32 | # Create an array of boolean values: 1 for prime, 0 for composite 33 | arr = np.zeros(w**2, dtype='u1') 34 | arr[primes-1] = 1 35 | # Spiral the values clockwise out from the centre 36 | arr = make_spiral(arr.reshape((w,w))) 37 | 38 | plt.matshow(arr, cmap=cm.binary) 39 | plt.axis('off') 40 | plt.savefig('ulam_spiral.png') 41 | plt.show() 42 | -------------------------------------------------------------------------------- /primes_last_digits/last_digits_hmap.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import FuncFormatter 4 | 5 | # Create a heatmap summarizing the probabilities associated with 6 | # the last digits of consecutive prime numbers. More details 7 | # at https://scipython.com/blog/do-consecutive-primes-avoid-sharing-the-same-last-digit/ 8 | # Christian Hill, March 2016. 9 | 10 | # First 10,000,000 primes 11 | digit_count = {1: {1: 446808, 3: 756072, 9: 526953, 7: 769924}, 12 | 3: {1: 593196, 3: 422302, 9: 769915, 7: 714795}, 13 | 9: {1: 820369, 3: 640076, 9: 446032, 7: 593275}, 14 | 7: {1: 639384, 3: 681759, 9: 756852, 7: 422289}} 15 | last_digits = [1,3,7,9] 16 | 17 | hmap = np.empty((4,4)) 18 | for i, d1 in enumerate(last_digits): 19 | total = sum(digit_count[d1].values()) 20 | for j, d2 in enumerate(last_digits): 21 | hmap[i,j] = digit_count[d1][d2] / total * 100 22 | 23 | fig = plt.figure() 24 | ax = fig.add_axes([0.1,0.3,0.8,0.6]) 25 | im = ax.imshow(hmap, interpolation='nearest', cmap=plt.cm.YlOrRd, origin='lower') 26 | tick_labels = [str(d) for d in last_digits] 27 | ax.set_xticks(range(4)) 28 | ax.set_xticklabels(tick_labels) 29 | ax.set_xlabel('Last digit of second prime') 30 | ax.set_yticks(range(4)) 31 | ax.set_yticklabels(tick_labels) 32 | ax.set_ylabel('Last digit of first prime') 33 | 34 | cbar_axes = fig.add_axes([0.1,0.1,0.8,0.05]) 35 | 36 | cbar = plt.colorbar(im, orientation='horizontal', cax=cbar_axes) 37 | cbar.ax.set_xlabel('Probability /%') 38 | 39 | plt.savefig('prime_digits_hmap.png') 40 | plt.show() 41 | -------------------------------------------------------------------------------- /primes_last_digits/plot_prime_digits.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # Create a set of bar charts summarizing the probabilities associated with 5 | # the last digits of consecutive prime numbers. More details 6 | # at https://scipython.com/blog/do-consecutive-primes-avoid-sharing-the-same-last-digit/ 7 | # Christian Hill, March 2016. 8 | 9 | # Dictionary of consecutive digit counts for the first 10,000,000 primes 10 | digit_count = {1: {1: 446808, 3: 756072, 9: 526953, 7: 769924}, 11 | 3: {1: 593196, 3: 422302, 9: 769915, 7: 714795}, 12 | 9: {1: 820369, 3: 640076, 9: 446032, 7: 593275}, 13 | 7: {1: 639384, 3: 681759, 9: 756852, 7: 422289}} 14 | fig, ax = plt.subplots(nrows=2, ncols=2, facecolor='#dddddd') 15 | 16 | xticks = [0,1,2,3] 17 | last_digits = [1,3,7,9] 18 | for i, d in enumerate(last_digits): 19 | ir, ic = i // 2, i % 2 20 | this_ax = ax[ir,ic] 21 | this_ax.patch.set_alpha(1) 22 | count = np.array([digit_count[d][j] for j in last_digits]) 23 | total = sum(count) 24 | prob = count / total * 100 25 | this_ax.bar(xticks, prob, align='center', color='maroon', ec='maroon', 26 | alpha=0.7) 27 | this_ax.set_title('Last digit of prime: {:d}'.format(d), fontsize=14) 28 | this_ax.set_xticklabels(['{:d}'.format(j) for j in last_digits]) 29 | this_ax.set_xticks(xticks) 30 | this_ax.set_yticks([0,10,20,30,40]) 31 | this_ax.set_ylim(0,35) 32 | this_ax.set_yticks([]) 33 | for j, pr in enumerate(prob): 34 | this_ax.annotate('{:.1f}%'.format(pr), xy=(j, pr-2), ha='center', 35 | va='top', color='w', fontsize=12) 36 | this_ax.set_xlabel('Next prime ends in') 37 | this_ax.set_frame_on(False) 38 | this_ax.tick_params(axis='x', length=0) 39 | this_ax.tick_params(axis='y', length=0) 40 | fig.subplots_adjust(wspace=0.2, hspace=0.7, left=0, bottom=0.1, right=1, 41 | top=0.95) 42 | plt.savefig('prime_digits.png') 43 | plt.show() 44 | 45 | 46 | -------------------------------------------------------------------------------- /primes_last_digits/prime-last-digits.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | 4 | # Prime numbers greater than 5 end in 1, 3, 7 or 9. It seems that a prime 5 | # ending in one digit is less likely to be followed by another ending in the 6 | # same digit. Return a dictionary summarizing this distribution. More details 7 | # at https://scipython.com/blog/do-consecutive-primes-avoid-sharing-the-same-last-digit/ 8 | # Christian Hill, March 2016. 9 | 10 | def approx_nth_prime(n): 11 | """Return an upper bound for the value of the nth prime""" 12 | 13 | return n * (math.log(n) + math.log(math.log(n))) 14 | 15 | nmax = 10000000 16 | pmax = approx_nth_prime(nmax) 17 | print('The {:d}th prime is approximately {:d}'.format(nmax,int(pmax))) 18 | N = int(math.sqrt(pmax)) + 1 19 | print('Our sieve will therefore contain primes up to', N) 20 | 21 | def primes_up_to(N): 22 | """A generator yielding all primes less than N.""" 23 | 24 | yield 2 25 | # Only consider odd numbers up to N, starting at 3 26 | bsieve = [True] * ((N-1)//2) 27 | for i,bp in enumerate(bsieve): 28 | p = 2*i + 3 29 | if bp: 30 | yield p 31 | # Mark off all multiples of p as composite 32 | for m in range(i, (N-1)//2, p): 33 | bsieve[m] = False 34 | 35 | gen_primes = primes_up_to(N) 36 | sieve = list(gen_primes) 37 | 38 | def is_prime(n, imax): 39 | """Return True if n is prime, else return False. 40 | 41 | imax is the maximum index in the sieve of potential prime factors that 42 | needs to be considered; this should be the index of the first prime number 43 | larger than the square root of n. 44 | 45 | """ 46 | return not any(n % p == 0 for p in sieve[:imax]) 47 | 48 | 49 | digit_count = {1: {1: 0, 3: 0, 7: 0, 9: 0}, 50 | 3: {1: 0, 3: 0, 7: 0, 9: 0}, 51 | 7: {1: 0, 3: 0, 7: 0, 9: 0}, 52 | 9: {1: 0, 3: 0, 7: 0, 9: 0}} 53 | 54 | # nprimes is the number of prime numbers encountered 55 | nprimes = 0 56 | # the most recent prime number considered (we start with the first prime number 57 | # which ends with 1,3,7 or 9 and is followed by a number ending with one of 58 | # these digits, 7 since 2, 3 and 5 are somewhat special cases. 59 | last_prime = 7 60 | # The current prime number to consider, initially the one after 7 which is 11 61 | n = 11 62 | # The index of the maximum prime in our sieve we need to consider when testing 63 | # for primality: initially 2, since sieve[2] = 5 is the nearest prime larger 64 | # than sqrt(11). plim is this largest prime from the sieve. 65 | imax = 2 66 | plim = sieve[imax] 67 | start_time = time.time() 68 | 69 | while nprimes <= nmax: 70 | # Output a progress indicator 71 | if not nprimes % 1000: 72 | print(nprimes) 73 | 74 | if is_prime(n, imax): 75 | # n is prime: update the dictionary of last digits 76 | digit_count[last_prime % 10][n % 10] += 1 77 | last_prime = n 78 | nprimes += 1 79 | # Move on to the next candidate (skip even numbers) 80 | n += 2 81 | 82 | # Update imax and plim if necessary 83 | if math.sqrt(n) >= plim: 84 | imax += 1 85 | plim = sieve[imax] 86 | end_time = time.time() 87 | print(digit_count) 88 | print('Time taken: {:.2f} s'.format(end_time - start_time)) 89 | -------------------------------------------------------------------------------- /primes_last_digits/prime_digits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/primes_last_digits/prime_digits.png -------------------------------------------------------------------------------- /primes_last_digits/prime_digits_hmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/primes_last_digits/prime_digits_hmap.png -------------------------------------------------------------------------------- /reuleaux/README.md: -------------------------------------------------------------------------------- 1 | This code constructs Reuleaux polygons as described at https://scipython.com/blog/constructing-reuleaux-polygons/. 2 | 3 |

4 | A Reuleaux triangle 5 |

6 | -------------------------------------------------------------------------------- /reuleaux/make_reuleaux_construction.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math 3 | 4 | # Create SVG images of Reuleaux polygons, as described at 5 | # https://scipython.com/blog/constructing-reuleaux-polygons/ 6 | # Christian Hill, June 2018. 7 | 8 | # Image size (pixels) 9 | SIZE = 600 10 | 11 | def draw_poly(n, a, phi=0, show_centres=False, colour='#888', 12 | filename='reuleaux.svg'): 13 | """Draw a Reuleaux polygon with n vertices. 14 | 15 | a is the side-length of the straight-sided inscribed polygon, phi is the 16 | phase, describing the rotation of the polygon as depicted. If show_centres 17 | is True, markers are placed at the centres of the constructing circles. 18 | colour is the fill colour of the polygon and filename the name of the SVG 19 | file created. Note that n must be odd. 20 | 21 | """ 22 | 23 | if not n % 2: 24 | sys.exit('Error in draw_poly: n must be odd') 25 | 26 | fo = open(filename, 'w') 27 | # The SVG preamble and styles. 28 | print('\n' 29 | 30 | '' 32 | .format(SIZE, SIZE), file=fo) 33 | print(""" 34 | 35 | 52 | 53 | """ % colour, file=fo) 54 | 55 | c0x = c0y = SIZE // 2 56 | # Calculate the radius of each of the constructing circles. 57 | alpha = math.pi * (1 - 1/n) 58 | r = a / 2 / math.sin(alpha/2) 59 | 60 | if show_centres: 61 | print(''.format(c0x, c0y), 62 | file=fo) 63 | 64 | # Caclulate the (x, y) positions of the polygon's vertices. 65 | v = [] 66 | for i in range(n): 67 | # The centre, (cx, cy), of this constructing circle. 68 | cx = c0x + r * math.cos(2*i*math.pi/n + phi) 69 | cy = c0y + r * math.sin(2*i*math.pi/n + phi) 70 | v.append((cx, cy)) 71 | if show_centres: 72 | print('' 73 | .format(cx, cy), file=fo) 74 | print(''.format( 75 | cx, cy, a), file=fo) 76 | 77 | def make_A(x,y): 78 | """Return the SVG arc path designation for the side ending at (x,y).""" 79 | 80 | return 'A {},{},0,0,1,{},{}'.format(a,a,x,y) 81 | 82 | d = 'M {},{}'.format(v[0][0], v[0][1]) 83 | for i in range(n): 84 | x, y = v[(i+1)%n] 85 | d += ' ' + make_A(x, y) 86 | print(''.format(d), file=fo) 87 | 88 | print('', file=fo) 89 | fo.close() 90 | 91 | draw_poly(3, 175, colour='#eea', filename='reuleaux-3.svg') 92 | draw_poly(5, 175, math.pi/3, filename='reuleaux-5.svg') 93 | draw_poly(11, 175, colour='#aee', filename='reuleaux-11.svg') 94 | -------------------------------------------------------------------------------- /reuleaux/reuleaux-11.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /reuleaux/reuleaux-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/reuleaux/reuleaux-3.png -------------------------------------------------------------------------------- /reuleaux/reuleaux-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /reuleaux/reuleaux-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /wilberforce_pendulum/wilberforce.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.integrate import solve_ivp 3 | from matplotlib import rc 4 | import matplotlib.pyplot as plt 5 | import matplotlib 6 | 7 | # Plot parameters related to the Wilberforce pendulum 8 | # Mathematical details are available on my blog article, 9 | # at https://scipython.com/blog/the-wilberforce-pendulum/ 10 | # Christian Hill, January 2016. 11 | # Updated (January 2020) to use solve_ivp instead of odeint. 12 | 13 | # Use LaTeX throughout the figure for consistency. 14 | rc('font', **{'family': 'serif', 'serif': ['Computer Modern'], 'size': 16}) 15 | rc('text', usetex=True) 16 | 17 | # Parameters for the system 18 | omega = 2.314 # rad.s-1 19 | epsilon = 9.27e-3 # N 20 | m = 0.4905 # kg 21 | I = 1.39e-4 # kg.m2 22 | 23 | def deriv(t, y, omega, epsilon, m, I): 24 | """Return the first derivatives of y = z, zdot, theta, thetadot.""" 25 | z, zdot, theta, thetadot = y 26 | dzdt = zdot 27 | dzdotdt = -omega**2 * z - epsilon / 2 / m * theta 28 | dthetadt = thetadot 29 | dthetadotdt = -omega**2 * theta - epsilon / 2 / I * z 30 | return dzdt, dzdotdt, dthetadt, dthetadotdt 31 | 32 | # Initial conditions: theta=2pi, z=zdot=thetadot=0 33 | y0 = [0, 0, 2*np.pi, 0] 34 | 35 | # Do the numerical integration of the equations of motion up to tmax secs. 36 | tmax = 40 37 | soln = solve_ivp(deriv, (0, tmax), y0, args=(omega, epsilon, m, I), dense_output=True) 38 | # The time grid in s 39 | t = np.linspace(0, tmax, 2000) 40 | # Unpack z and theta as a function of time 41 | z, theta = soln.sol(t)[0], soln.sol(t)[2] 42 | 43 | # Plot z vs. t and theta vs. t on axes which share a time (x) axis 44 | fig, ax_z = plt.subplots() 45 | l_z, = ax_z.plot(t, z, 'g', label=r'$z$') 46 | ax_z.set_xlabel('time /s') 47 | ax_z.set_ylabel(r'$z /\mathrm{m}$') 48 | ax_theta = ax_z.twinx() 49 | l_theta, = ax_theta.plot(t, theta, 'orange', label=r'$\theta$') 50 | ax_theta.set_ylabel(r'$\theta /\mathrm{rad}$') 51 | 52 | # Add a single legend for the lines of both twinned axes 53 | lines = (l_z, l_theta) 54 | labels = [line.get_label() for line in lines] 55 | plt.legend(lines, labels) 56 | plt.tight_layout() 57 | plt.savefig('wilberforce_z-t_plot.png') 58 | plt.show() 59 | 60 | # Plot theta vs. z on a cartesian plot 61 | fig, ax1 = plt.subplots() 62 | ax1.plot(z, theta, 'r', alpha=0.8) 63 | ax1.set_xlabel(r'$z /\mathrm{m}$') 64 | ax1.set_ylabel(r'$\theta /\mathrm{rad}$') 65 | plt.tight_layout() 66 | plt.savefig('wilberforce_theta-z_plot.png') 67 | plt.show() 68 | 69 | # Plot z vs. theta on a polar plot 70 | fig, ax2 = plt.subplots(subplot_kw={'projection': 'polar'}) 71 | ax2.plot(theta, z, 'b', alpha=0.8) 72 | plt.tight_layout() 73 | plt.savefig('wilberforce_theta-z_polar_plot.png') 74 | plt.show() 75 | -------------------------------------------------------------------------------- /wilberforce_pendulum/wilberforce_theta-z_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/wilberforce_pendulum/wilberforce_theta-z_plot.png -------------------------------------------------------------------------------- /wilberforce_pendulum/wilberforce_theta-z_polar_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/wilberforce_pendulum/wilberforce_theta-z_polar_plot.png -------------------------------------------------------------------------------- /wilberforce_pendulum/wilberforce_z-t_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/wilberforce_pendulum/wilberforce_z-t_plot.png --------------------------------------------------------------------------------