├── Legendre.py ├── License.txt ├── README.md ├── charge_distribution_2D.py ├── examples.ipynb ├── images ├── line_charge_with_V.png ├── parallel_lines.png └── point_charges.png └── point_charges_2D.py /Legendre.py: -------------------------------------------------------------------------------- 1 | from math import factorial 2 | import numpy as np 3 | 4 | 5 | def pretty_polynomial(polynomial): 6 | """ 7 | Takes a list of coefficients (must be in ascending powers of x) and produces a 'pretty' output. 8 | :param polynomial: the list of coefficients of a polynomial 9 | :return: a string of the polynomial in a more readable format. 10 | """ 11 | s = '' 12 | for i in range(1, len(polynomial)): 13 | if polynomial[i] > 0: 14 | s += ' + ' + str(polynomial[i]) + 'x^' + str(i) 15 | elif polynomial[i] < 0: 16 | s += ' ' + str(polynomial[i]) + 'x^' + str(i) 17 | 18 | # If the constant is nonzero, prepend it to the string. Otherwise, get rid of the '+'. 19 | if polynomial[0]: 20 | s = str(polynomial[0]) + s 21 | else: 22 | s = s[2:] 23 | return s 24 | 25 | 26 | def legendre_polynomial(n: int): 27 | """ 28 | Calculate the coefficients of the nth order Legendre polynomial in x, using 29 | the Rodrigues formula: P_n(x) = 1/(2^n n!) (d/dx)^n [(x^2 - 1)^n]. 30 | 31 | We will treat polynomials as lists in python, where the index in the list corresponds to the power 32 | of x, so the element in the list contains the coefficient of x^i. Thus our polynomials will be 33 | written in ascending powers of x, somewhat contrasting convention. All our operations will therefore 34 | be reduced to list methods. 35 | 36 | :param n: the order of the Legendre polynomial whose coefficients are to be determined 37 | :return: a list of coefficients in ascending powers of x. 38 | """ 39 | # 1. Expand (x^2 - 1) ^ n with the binomial theorem. 40 | expansion = [None] * (n + 1) 41 | for i in range(len(expansion)): 42 | expansion[i] = (-1)**(n - i) * factorial(n) / (factorial(i) * factorial(n - i)) 43 | 44 | # Because the brackets contain x^2, the expansion will only have even powers of x. 45 | # However, we need the odd powers too (which are all zero). 2n + 1 terms in total. 46 | coeffs = [None] * (2*n + 1) 47 | coeffs[::2] = expansion 48 | coeffs[1::2] = [0] * n 49 | 50 | # 2. Differentiate n times. Differentiating is the same as multiplying each element 51 | # by its index (remember this is the power of x), then deleting the first term in the list. 52 | for _ in range(n): 53 | coeffs = [i * coeffs[i] for i in range(1, len(coeffs))] 54 | 55 | # Don't forget the normalising constant in the Rodrigues formula. 56 | # The below code is the same as [x / (2**n * factorial(n)) for x in coeffs]. 57 | return list(map(lambda x: x / (2**n * factorial(n)), coeffs)) 58 | 59 | 60 | def legendre_representation(poly): 61 | """ 62 | Any polynomial can be written in terms of Legendre polynomials. 63 | :param poly: A list or np array of coefficients of the polynomial 64 | :return: A list of coefficients of the Legendre polynomials, 65 | e.g [3, 1, 0, 4] <=> 3P_0(x) + P_1(x) + 4P_3(x) 66 | """ 67 | if not isinstance(poly, np.ndarray): 68 | poly = np.array(poly) 69 | n = len(poly) - 1 70 | legendre_coefficients = [0] * (n + 1) 71 | 72 | while n >= 0: 73 | Pn = legendre_polynomial(n) 74 | legendre_coefficients[n] = poly[n]/Pn[n] 75 | if n == len(poly) - 1: 76 | poly = poly - [legendre_coefficients[n] * x for x in Pn] 77 | else: 78 | poly[:n+1] = poly[:n+1] - [legendre_coefficients[n] * x for x in Pn] 79 | n -= 1 80 | 81 | return legendre_coefficients 82 | 83 | 84 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robert 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 | # ElectrodynamicsPy 2 | 3 | My goal is to write logical, clear code which can either calculate or visualise the various physical quantities that 4 | arise from the study of electrodynamical systems. 5 | 6 | Have a look at the `examples` jupyter notebook to see what kind of stuff one can do with the code. 7 | 8 | ![](https://github.com/surelyourejoking/ElectrodynamicsPy/blob/master/images/point_charges.png) 9 | 10 | ## What I have implemented so far: 11 | 12 | 1. Calculate the field resulting from any point charges in 2D, with a stream plot. 13 | 2. Calculate the potential of point charges in 2D, with a contour plot. 14 | 3. Visualising the field and potential from charge distributions in 2D. 15 | 4. Some (very amateurish) stuff regarding Legendre polynomials. 16 | -------------------------------------------------------------------------------- /charge_distribution_2D.py: -------------------------------------------------------------------------------- 1 | from point_charges_2D import Charge 2 | 3 | 4 | def straight_line_charge(start, end, res=40, Q=10): 5 | """ 6 | A straight line of charge 7 | :param start: the coordinates (as a tuple/list) for the starting point of the line 8 | :param end: the coordinates (as a tuple/list) for the end point of the line 9 | :param res: number of point charges per unit length 10 | :param Q: total charge on the line 11 | """ 12 | length = ((end[1] - start[1])**2 + (end[0] - start[0])**2)**0.5 13 | gradient = (end[1] - start[1]) / (end[0] - start[0]) 14 | intercept = start[1] - gradient * start[0] 15 | 16 | lambd = Q / length 17 | for i in range(int((end[0] - start[0])*res)): 18 | Charge(lambd, [i/res + start[0], gradient * (i/res) + intercept]) 19 | 20 | 21 | def line_charge(parametric_x, parametric_y, trange, res, Q): 22 | """ 23 | Any line of charge, where the line is specified by parametric equations in t. 24 | :param parametric_x: x(t) 25 | :param parametric_y: y(t) 26 | :param trange: the range of t values 27 | :param res: how many point charges should be plotted per unit t 28 | :param Q: the total charge of the line 29 | """ 30 | for t in range(int(trange * res)): 31 | Charge(Q/res, [parametric_x(t/res), parametric_y(t/res)]) 32 | 33 | # Example 34 | """ 35 | import numpy as np 36 | 37 | Charge.reset() 38 | xs = ys = [-2, 2] 39 | 40 | line_charge(parametric_x=lambda t: np.cos(t), parametric_y=lambda t: np.sin(t), trange=2*np.pi, res=100, Q=10 ) 41 | Charge.plot_field(xs, ys) 42 | """ 43 | 44 | 45 | def rectangle_charge(dim, corner, res, Q): 46 | """ 47 | A rectangle of charge 48 | :param dim: the dimensions of the rectangle, as a tuple or list. 49 | :param corner: the coordinates of the lower left corner of the rectangle 50 | :param res: number of point charges per unit length 51 | :param Q: the total charge of the rectangle 52 | """ 53 | sigma = Q / (dim[0] * dim[1] * res**2) 54 | for i in range(int(dim[0] * res)): 55 | for j in range(int(dim[1] * res)): 56 | Charge(sigma, [i/res + corner[0], j/res + corner[1]]) 57 | 58 | # Example 59 | """ 60 | Charge.reset() 61 | xs = ys = [-2, 2] 62 | 63 | rectangle_charge([1, 1], [-0.5, -0.5], res=80, Q=100) 64 | Charge.plot_field(xs, ys) 65 | """ 66 | -------------------------------------------------------------------------------- /images/line_charge_with_V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertmartin8/ElectrodynamicsPy/d474e292d66d81c01d78c2d30fb89e65f4c45432/images/line_charge_with_V.png -------------------------------------------------------------------------------- /images/parallel_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertmartin8/ElectrodynamicsPy/d474e292d66d81c01d78c2d30fb89e65f4c45432/images/parallel_lines.png -------------------------------------------------------------------------------- /images/point_charges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertmartin8/ElectrodynamicsPy/d474e292d66d81c01d78c2d30fb89e65f4c45432/images/point_charges.png -------------------------------------------------------------------------------- /point_charges_2D.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | 5 | class Charge: 6 | # Registry will store the instances of Charge 7 | registry = [] 8 | 9 | def __init__(self, q, pos): 10 | """ 11 | Initialise the charge 12 | :param q: value of charge 13 | :param pos: position of source charge 14 | """ 15 | self.q = q 16 | self.pos = pos 17 | self.registry.append(self) 18 | 19 | def field(self, x, y): 20 | """ 21 | Calculates the electric field of the charge at point (x,y). 22 | :param x: x coordinate in field 23 | :param y: y coordinate in field 24 | :return: Ex and Ey, the horizontal and vertical components of the field at (x,y). 25 | """ 26 | r = np.sqrt((x - self.pos[0])**2 + (y - self.pos[1])**2) 27 | 28 | # Set the minimum value of r to avoid dividing by zero 29 | r[r < 0.005] = 0.005 30 | 31 | Ex = self.q * (x - self.pos[0]) / r**3 32 | Ey = self.q * (y - self.pos[1]) / r**3 33 | 34 | return Ex, Ey 35 | 36 | def potential(self, x, y): 37 | """ 38 | Calculates the electric potential of the charge at point (x,y). 39 | :param x: x coordinate in field 40 | :param y: y coordinate in field 41 | :return: the value of the potential 42 | """ 43 | r = np.sqrt((x - self.pos[0])**2 + (y - self.pos[1])**2) 44 | 45 | # Set the minimum value of r to avoid dividing by zero 46 | r[r < 0.005] = 0.005 47 | 48 | V = self.q / r 49 | return V 50 | 51 | @staticmethod 52 | def E_total(x, y): 53 | """ 54 | Calculates the total electric field at (x,y), by superposing the present fields. 55 | :param x: x coordinate 56 | :param y: y coordinate 57 | :return: the x and y components of the total electric field 58 | """ 59 | Ex_total, Ey_total = 0, 0 60 | for C in Charge.registry: 61 | Ex_total += C.field(x, y)[0] 62 | Ey_total += C.field(x, y)[1] 63 | return [Ex_total, Ey_total] 64 | 65 | @staticmethod 66 | def V_total(x, y): 67 | """ 68 | Calculates the total electric potential at (x,y), by superposing the present potential 69 | :param x: x coordinate 70 | :param y: y coordinate 71 | :return: the total potential at the point 72 | """ 73 | V_total = 0 74 | for C in Charge.registry: 75 | V_total += C.potential(x, y) 76 | return V_total 77 | 78 | @staticmethod 79 | def reset(): 80 | """ 81 | :return: Empties the charge registry 82 | """ 83 | Charge.registry = [] 84 | 85 | @staticmethod 86 | def plot_field(xs, ys, show_charge=True, field=True, potential=False): 87 | """ 88 | Creates basic plots 89 | :param xs: 2d list/tuple of x plotting range 90 | :param ys: 2d list/tuple of y plotting range 91 | :param show_charge: Whether or not the point charges should be displayed 92 | :param field: If true, plots the field lines 93 | :param potential: If true, plots the equipotentials (this is far from perfect). 94 | :return: A stream plot of E and/or contour plot of V. 95 | """ 96 | plt.figure() 97 | if show_charge: 98 | for C in Charge.registry: 99 | # The colour will depend on the charge, and the size will depend on the magnitude 100 | if C.q > 0: 101 | plt.plot(C.pos[0], C.pos[1], 'bo', ms=6*np.sqrt(C.q)) 102 | if C.q < 0: 103 | plt.plot(C.pos[0], C.pos[1], 'ro', ms=6*np.sqrt(-C.q)) 104 | 105 | x, y = np.meshgrid(np.linspace(xs[0], xs[1], 100), 106 | np.linspace(ys[0], ys[1], 100)) 107 | 108 | if field: 109 | Ex, Ey = Charge.E_total(x, y) 110 | plt.streamplot(x, y, Ex, Ey, color='g') 111 | plt.draw() 112 | 113 | if potential: 114 | # I have to multiply by 100 to get the potentials visible. 115 | V = 100 * (Charge.V_total(x, y)) 116 | V[V > 10000] = 10000 117 | plt.contour(x, y, V, 10) 118 | 119 | plt.show() 120 | 121 | # Demonstration 122 | """ 123 | Charge.reset() 124 | A = Charge(1, [0, 0]) 125 | B = Charge(4, [1, 0]) 126 | 127 | xs = ys =[-1, 2.5] 128 | Charge.plot_field(xs, ys, show_charge=False, field=True, potential=True) 129 | """ 130 | --------------------------------------------------------------------------------