├── .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 |
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 |
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 |
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('', 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 |
272 |
--------------------------------------------------------------------------------
/maze/maze3.svg:
--------------------------------------------------------------------------------
1 |
2 |
271 |
--------------------------------------------------------------------------------
/maze/maze3_solution.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 | '', 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 |
38 |
--------------------------------------------------------------------------------
/reuleaux/reuleaux-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scipython/scipython-maths/bc4bd5e7f307ccdf0d7e5aef63f925b9c20d3076/reuleaux/reuleaux-3.png
--------------------------------------------------------------------------------
/reuleaux/reuleaux-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/reuleaux/reuleaux-5.svg:
--------------------------------------------------------------------------------
1 |
2 |
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
--------------------------------------------------------------------------------