├── oo_workshop ├── 1276_Codebases.png ├── oo_workshop.ipynb └── molecule_angle.svg ├── oo_project ├── gas_2d.py ├── fast_classes.pyx ├── aux_functions.py └── oo_project.ipynb ├── README.md └── LICENSE.txt /oo_workshop/1276_Codebases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imperialchem/python-prog-oo/master/oo_workshop/1276_Codebases.png -------------------------------------------------------------------------------- /oo_project/gas_2d.py: -------------------------------------------------------------------------------- 1 | #Object-oriented implementation of a hard-disks molecular dynamics simulations 2 | 3 | class Vector(): 4 | '''2D vectors''' 5 | 6 | def __init__(self, i1,i2): 7 | '''Initialise vectors with x and y coordinates''' 8 | self.x = i1 9 | self.y = i2 10 | 11 | def __add__(self,other): 12 | '''Use + sign to implement vector addition''' 13 | return (Vector(self.x+other.x,self.y+other.y)) 14 | 15 | def __sub__(self,other): 16 | '''Use - sign to implement vector "subtraction"''' 17 | return (Vector(self.x-other.x,self.y-other.y)) 18 | 19 | def __mul__(self,number): 20 | '''Use * sign to multiply a vector by a scaler on the left''' 21 | return Vector(self.x*number,self.y*number) 22 | 23 | def __rmul__(self,number): 24 | '''Use * sign to multiply a vector by a scaler on the right''' 25 | return Vector(self.x*number,self.y*number) 26 | 27 | def __truediv__(self,number): 28 | '''Use / to multiply a vector by the inverse of a number''' 29 | return Vector(self.x/number,self.y/number) 30 | 31 | def __repr__(self): 32 | '''Represent a vector by a string of 2 coordinates separated by a space''' 33 | return '{x} {y}'.format(x=self.x, y=self.y) 34 | 35 | def copy(self): 36 | '''Create a new object which is a copy of the current.''' 37 | return Vector(self.x,self.y) 38 | 39 | def dot(self,other): 40 | '''Calculate the dot product between two 2D vectors''' 41 | return self.x*other.x + self.y*other.y 42 | 43 | def norm(self): 44 | '''Calculate the norm of the 2D vector''' 45 | return (self.x**2+self.y**2)**0.5 46 | 47 | class Particle(): 48 | def __init__(self): 49 | pass 50 | 51 | class Simulation(): 52 | def __init__(self): 53 | pass 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An introduction to object-oriented programming in Python 2 | ## Hard-disks (as in 2D hard-spheres) dynamics simulations 3 | 4 | These Jupyter notebooks form the basis of an intermediate course in programming taught at Imperial College's Department of Chemistry 5 | to students following the degree in Chemistry with Molecular Physics. It assumes some familiarity with an imperative 6 | programming style in Python (or another language, but Python basic syntax is not covered here) at the level of our 7 | [introductory programming course](https://github.com/imperialchem/python-prog-intro). 8 | 9 | The course serves also as an introduction to programming atomic/molecular computer simulations, by implementing a hard-disks 10 | dynamics code as an illustration of object-oriented programming. **Note** that the simulation implementation is 11 | (in some ways surprisingly) inefficient. There are better algorithms and programming approaches to solve this problem. 12 | The choice here was to keep the algorithm simple, close to how general molecular dynamics algorithms work, and illustrating 13 | the object oriented paradigm. Having said that, if you identify any obvious way in which the code could be made faster 14 | without increasing its conceptual complexity, please [let us know](mailto:python@imperial.ac.uk). 15 | 16 | The [1st notebook](https://github.com/imperialchem/python-prog-oo/blob/master/oo_workshop/oo_workshop.ipynb) offers 17 | a brief overview of programming styles; motivates the use of an object-oriented paradigm; and covers the basic features 18 | of object-oriented programming in Python. 19 | 20 | The [2nd notebook](https://github.com/imperialchem/python-prog-oo/blob/master/oo_project/oo_project.ipynb) offers a step by step 21 | guide to the implementation of a working object-oriented hard-sphere simulation program, and suggests some avenues for 22 | exploring the simulation results. 23 | 24 | The notebooks and related materials are made available under the [Creative Commons Attribution 4.0 (CC-by) license](https://creativecommons.org/licenses/by/4.0/), 25 | and can be downloaded as a [zip archive](https://github.com/imperialchem/python-prog-oo/archive/master.zip). 26 | In order to use the notebooks interactively in your computer you will need to have a python interpreter (version 3.x) 27 | and the Jupyter notebook installed, both can be obtained, for example, by installing the [Anaconda](https://www.continuum.io/downloads) distribution. 28 | -------------------------------------------------------------------------------- /oo_project/fast_classes.pyx: -------------------------------------------------------------------------------- 1 | #This file redefines a 2D Vector and Particle classes using some tricks to make the code run faster 2 | #You may recognise some of the syntax but do not worry if there are things you don't understand 3 | #For these classes to work, you must have Cython installed on your system 4 | 5 | from cpython cimport bool 6 | 7 | # fast Vector class similar to base Python class but defines the types explicitly 8 | # allowing the compiler to optimise the code 9 | cdef class Vector: 10 | cdef float x, y 11 | def __init__(self, float x, float y): 12 | self.x = x 13 | self.y = y 14 | 15 | def __add__(self,other): 16 | return (Vector(self.x+other.x,self.y+other.y)) 17 | 18 | def __sub__(self,other): 19 | return (Vector(self.x-other.x,self.y-other.y)) 20 | 21 | #Cython does not need self as the first argument 22 | #need to try to check if multiplying on the right or left 23 | def __mul__(arg1,arg2): 24 | try: 25 | result=Vector(arg1.x*arg2,arg1.y*arg2) 26 | except AttributeError: 27 | result=Vector(arg1*arg2.x,arg1*arg2.y) 28 | return result 29 | 30 | def __truediv__(self,number): 31 | return Vector(self.x/number,self.y/number) 32 | 33 | def __repr__(self): 34 | return '{x} {y}'.format(x=self.x, y=self.y) 35 | 36 | def copy(self): 37 | return Vector(self.x,self.y) 38 | 39 | cpdef float dot(self,Vector other): 40 | return self.x*other.x + self.y*other.y 41 | 42 | #faster to use properties than to enable public access 43 | property x: 44 | def __get__(self): 45 | return self.x 46 | def __set__(self,x): 47 | self.x = x 48 | 49 | property y: 50 | def __get__(self): 51 | return self.y 52 | def __set__(self, y): 53 | self.y = y 54 | 55 | # could be used within a fast Particle class 56 | cdef Vector fast_subtract(self, Vector other): 57 | return Vector(self.x-other.x,self.y-other.y) 58 | 59 | cpdef float norm(self): 60 | return (self.x**2+self.y**2)**0.5 61 | 62 | # so that our fast vector can be pickled 63 | def __reduce__(self): 64 | return (rebuild_vec, (self.x,self.y)) 65 | 66 | #enables pickling 67 | def rebuild_vec(x,y): 68 | return Vector(x,y) 69 | 70 | cdef class Particle: 71 | cdef Vector position, momentum 72 | cdef float radius, mass 73 | 74 | def __init__(self, Vector position not None, Vector momentum not None, float radius, float mass): 75 | 76 | self.position = position 77 | self.momentum = momentum 78 | self.radius = radius 79 | self.mass = mass 80 | 81 | property position: 82 | def __get__(self): 83 | return self.position 84 | def __set__(self,p): 85 | self.position = p 86 | 87 | property momentum: 88 | def __get__(self): 89 | return self.momentum 90 | def __set__(self,m): 91 | self.momentum=m 92 | 93 | property radius: 94 | def __get__(self): 95 | return self.radius 96 | def __set__(self,r): 97 | self.radius=r 98 | 99 | property mass: 100 | def __get__(self): 101 | return self.mass 102 | def __set__(self,m): 103 | self.mass=m 104 | 105 | cpdef Vector velocity(self): 106 | return self.momentum/self.mass 107 | 108 | cpdef Particle copy(self): 109 | return Particle(self.position.copy(), self.momentum.copy(), self.radius, self.mass) 110 | 111 | cpdef bool overlap(self, Particle other): 112 | cdef Vector displacement 113 | displacement = self.position.fast_subtract(other.position) 114 | 115 | return displacement.norm() < (self.radius + other.radius) 116 | 117 | #enables pickling 118 | def __reduce__(self): 119 | return (build_particle, (self.position,self.momentum,self.radius,self.mass)) 120 | 121 | #enables pickling 122 | def build_particle(position,momentum,radius,mass): 123 | return Particle(position,momentum,radius,mass) 124 | -------------------------------------------------------------------------------- /oo_project/aux_functions.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.animation as animation 3 | from matplotlib.collections import EllipseCollection 4 | import numpy as np 5 | 6 | def animate_simulation(s,loop=False,display_step=False,interval=10): 7 | '''Animates the trajectory of a simulation object.''' 8 | def add_dots(): 9 | frame.add_collection(dots) 10 | return dots, 11 | 12 | def update_frame(i, dots,text=None): 13 | dots.set_offsets([(p.position.x, p.position.y) for p in s.trajectory[i]]) 14 | 15 | if display_step: 16 | text.set_text(str(i)) 17 | 18 | if text: 19 | return dots,text 20 | else: 21 | return dots, 22 | 23 | particle_sizes = [2*p.radius for p in s.particles] 24 | if len(set(particle_sizes))==1: 25 | particle_sizes = particle_sizes[0] 26 | no_steps = len(s.trajectory) 27 | 28 | fig,frame = plt.subplots() 29 | frame.set(aspect=1,xlim=(0,s.box_length),ylim=(0,s.box_length)) 30 | dots=EllipseCollection(widths=particle_sizes,heights=particle_sizes,angles=0, 31 | units='x', 32 | offsets=[], 33 | transOffset=frame.transData) 34 | 35 | if display_step: 36 | text = frame.text(0.9*s.box_length,0.9*s.box_length,'') 37 | else: 38 | text = None 39 | 40 | #on some systems, particularly macOS, blit option below may cause display glitches 41 | #change to blit=False if needed 42 | frame_ani = animation.FuncAnimation(fig, update_frame, no_steps, init_func=add_dots, 43 | fargs=(dots,text), interval=interval, blit=True, repeat=loop) 44 | 45 | return frame_ani 46 | 47 | 48 | def display_particle(p): 49 | '''Simple function that displays one particle object p in a vacuum.''' 50 | 51 | min_x,max_x=(p.position.x-10*p.radius,p.position.x+10*p.radius) 52 | min_y,max_y=(p.position.y-10*p.radius,p.position.y+10*p.radius) 53 | ax=plt.axes(aspect=1,xlim=(min_x, max_x), ylim=(min_y, max_y)) 54 | ax.add_artist(plt.Circle((p.position.x,p.position.y),radius=p.radius)) 55 | plt.show() 56 | 57 | 58 | def display_particle_motion(particles): 59 | ''' 60 | Animate a list of particle objects, effectively showing 61 | the trajectory of a single particle. 62 | ''' 63 | 64 | no_steps = len(particles) 65 | rad = particles[0].radius 66 | min_x,max_x = min([p.position.x for p in particles]), max([p.position.x for p in particles]) 67 | min_y,max_y = min([p.position.y for p in particles]), max([p.position.y for p in particles]) 68 | 69 | fig=plt.figure() 70 | frame=plt.gca() 71 | frame.set(aspect=1,xlim=(min_x-2*rad,max_x+2*rad),ylim=(min_y-2*rad,max_y+2*rad)) 72 | dot=plt.Circle((0,0),radius=rad) 73 | 74 | def first_dot(): 75 | frame.add_artist(dot) 76 | return dot, 77 | 78 | def update_frame(i,dot): 79 | dot.set_center((particles[i].position.x,particles[i].position.y)) 80 | return dot, 81 | 82 | frame_ani = animation.FuncAnimation(fig, update_frame, no_steps, init_func=first_dot, 83 | fargs=(dot,), interval=10, blit=True, repeat=False) 84 | return frame_ani 85 | 86 | 87 | from ipywidgets import interactive 88 | 89 | def display_vecs(): 90 | def interactive_display(directionx=1.0, directiony=0.0): 91 | plt.figure(figsize=(8,8)) 92 | vec = Vector(np.float64(3),np.float64(2)) 93 | 94 | direction = Vector(directionx,directiony) 95 | normal = direction/direction.norm() 96 | 97 | normal_component = vec.dot(normal) 98 | 99 | remainder = vec - (normal*normal_component) 100 | tangent = remainder/remainder.norm() 101 | 102 | np_vec =np.array( [ [0,0,vec.x,vec.y]]) 103 | np_components = np.array( [ [0,0,normal_component*normal.x, normal_component*normal.y], 104 | [0,0,remainder.x,remainder.y]]) 105 | 106 | X1,Y1,U1,V1 = zip(*np_vec) 107 | X2,Y2,U2,V2 = zip(*np_components) 108 | 109 | ax = plt.gca() 110 | ax.quiver(X1,Y1,U1,V1,angles='xy',scale_units='xy',scale=1, color='r') 111 | ax.quiver(X2,Y2,U2,V2,angles='xy',scale_units='xy',scale=1, linestyle='--', color='b') 112 | 113 | ax.set_xlim([-5,5]) 114 | ax.set_ylim([-5,5]) 115 | plt.draw() 116 | plt.show() 117 | 118 | return interactive(interactive_display, directionx=(-1,1,0.1), directiony=(-1,1,0.1)) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public licenses. 379 | Notwithstanding, Creative Commons may elect to apply one of its public 380 | licenses to material it publishes and in those instances will be 381 | considered the "Licensor." Except for the limited purpose of indicating 382 | that material is shared under a Creative Commons public license or as 383 | otherwise permitted by the Creative Commons policies published at 384 | creativecommons.org/policies, Creative Commons does not authorize the 385 | use of the trademark "Creative Commons" or any other trademark or logo 386 | of Creative Commons without its prior written consent including, 387 | without limitation, in connection with any unauthorized modifications 388 | to any of its public licenses or any other arrangements, 389 | understandings, or agreements concerning use of licensed material. For 390 | the avoidance of doubt, this paragraph does not form part of the public 391 | licenses. 392 | 393 | Creative Commons may be contacted at creativecommons.org. 394 | -------------------------------------------------------------------------------- /oo_workshop/oo_workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to object-oriented programming" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Clyde Fare and João Pedro Malhado, Imperial College London (contact: [python@imperial.ac.uk](mailto:python@imperial.ac.uk))\n", 15 | "\n", 16 | "Notebook is licensed under a [Creative Commons Attribution 4.0 (CC-by) license](http://creativecommons.org/licenses/by/4.0/)." 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Outine\n", 24 | "\n", 25 | "This workshop follows up on the earlier [Introduction to Computer Programming](https://github.com/imperialchem/python-prog-intro) course, by exploring some of the more advanced programming techniques made available in Python. Namely we will be introducing the basic ideas of object-oriented programming, which is a powerful and popular technique to organize code for very large programs.\n", 26 | "\n", 27 | "In later parts of the course we will be using these techniques to implement computer simulations of chemically relevant phenomena, and extract results from these.\n", 28 | "\n", 29 | "### Index\n", 30 | "\n", 31 | "* [Programming styles](#programming_styles)\n", 32 | " * [Procedural of imperative programming](#procedural)\n", 33 | " * [Functional programming](#functional)\n", 34 | " * [Recursive programming](#recursive)\n", 35 | " * [Object-oriented programming](#oo)\n", 36 | "* [Motivational problem](#warm-up)\n", 37 | "* [Defining classes of objects](#classes)\n", 38 | " * [Dot method](#dot)\n", 39 | " * [Norm method](#norm)\n", 40 | " * [Adding vector objects](#addition)\n", 41 | " * [Display objects](#display)\n", 42 | "* [A molecule class](#molecule_class)\n", 43 | "* [General remarks on object-oriented design](#design)" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "## A digression on programming styles " 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "Programming consists of composing sequences of instructions in order to solve some task. For this we use a language, a programming language, to express our reasoning in a form a machine (computer) can interpret. Most programming languages are quite rich, and allow people to express themselves in different ways, and achieve the same result writing different programs. This is much like expressing verbally the same idea using different words.\n", 58 | "\n", 59 | "There are however different styles or types of logical approaches to solve the same problem, and different languages rely on, or are more suited for, different programming paradigms. To illustrate this we will consider several ways in which one could write a program to produce a list of the square root of all integers from 1 to 10." 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "### Procedural or imperative programming " 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "This is probably the most common style of programming, and the one we covered in the previous course. Since you are familiar with this type of programming, we kindly ask you to provide the example for it in writing a program to produce a list of the square roots of all integers between 1 and 10." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "Let us have a closer look at the program you wrote. You probably started by initializing a variable and then used a loop to update the value of this variable in order to achieve the result. The use of variables to store results and the use of loops are the hallmark of the procedural programming style.\n", 88 | "\n", 89 | "Procedural programs are structured as instructions that will modify the value of one or more variables in a sequence that very often will be wrapped in a loop.\n", 90 | "\n", 91 | "One can encapsulate these instructions in functions that help in the organization of the program, but when looking inside them the basic logic will be sequences of instructions changing the value of variables and loops.\n", 92 | "\n", 93 | "The inner workings of contemporary computers resemble most the procedural paradigm and therefore it may be no surprise that the most used programming languages enforce or strongly favour a procedural programming style. Some famous examples being [C](https://en.wikipedia.org/wiki/C_(programming_language), [Fortran](https://en.wikipedia.org/wiki/Fortran) or [BASIC](https://en.wikipedia.org/wiki/BASIC)." 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "### Functional programming " 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "In this type of programming, functions are not just a way to encapsulate instructions, they (rather than variable values) form the central logic of the program. One of the characteristics of this programming style is that functions don't have to receive just data (ex. an integer or a list) as input but they can receive other functions.\n", 108 | "\n", 109 | "Python is not a particularly good language to do functional programming, but it has some of its features. We could write our example program as:\n", 110 | "\n", 111 | " def sqroot(n):\n", 112 | " return n**0.5\n", 113 | "\n", 114 | " list(map(sqroot,range(1,11)))" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "Note that in the above code there are no variables and there are no loops! We define the function *sqroot()*, and use Python's function *map()* to apply it to all the elements resulting from the execution of the function *range()*. *list()* here is not so important and is there so that we can visualize the output as a list.\n", 129 | "\n", 130 | "Note that the function map() is receiving the function sqroot() itself as an argument, not some result of sqroot() operation on some data!\n", 131 | "\n", 132 | "Functional programming is in fashion, with languages like [Haskell](https://en.wikipedia.org/wiki/Haskell_(programming_language)) gaining popularity next to classics like [Lisp](https://en.wikipedia.org/wiki/Lisp_(programming_language))." 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": {}, 138 | "source": [ 139 | "### Recursive programming " 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "In this programming style, the function calls back on itself on a \"simpler\" problem. A solution to our example, where *l* is the list of integers would be:\n", 147 | "\n", 148 | " def rec_list_sqrt(l):\n", 149 | " if l==[]:\n", 150 | " return []\n", 151 | " else:\n", 152 | " return rec_list_sqrt(l[:-1])+[l[-1]**0.5]\n", 153 | " \n", 154 | " rec_list_sqrt(list(range(1,11)))" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "Let us analyse how the function *rec_list_sqrt()* works. We have an *if* statement that branches the code in two different paths. The first path simply states that the function should return an empty list if the input is an empty list. This could seem like a pedantic detail but it is actually crucial for how the function will work. This code path provides a well defined immediate result, termed *base result*.\n", 169 | "\n", 170 | "The other branch of the if statement calls the function rec_list_sqrt() on a new list containing all the elements of the initial input list except the last, and appends the list of a single component with the square root of the last element of the original input. This branch is called *recursion* and it is building our result.\n", 171 | "\n", 172 | "It is perhaps helpful to see the workings of rec_list_sqrt() on a small list:\n", 173 | "\n", 174 | " rec_list_sqrt([1,2,3])\n", 175 | " rec_list_sqrt([1,2])+[3**0.5]\n", 176 | " (rec_list_sqrt([1])+[2**0.5])+[3**0.5]\n", 177 | " ((rec_list_sqrt([])+[1**0.5])+[2**0.5])+[3**0.5]\n", 178 | " (([]+[1**0.5])+[2**0.5])+[3**0.5]\n", 179 | " ([1**0.5]+[2**0.5])+[3**0.5]\n", 180 | " [1**0.5,2**0.5]+[3**0.5]\n", 181 | " [1**0.5,2**0.5,3**0.5]\n", 182 | " \n", 183 | "Up to half way in the trace above, the code is following the recursion path until it reaches the base result, when the recursion stops and the final result is reconstructed. It is crucial that the recursion step will eventually converge to the base result, otherwise the function will keep on recurring in a form of infinite loop.\n", 184 | "\n", 185 | "Although a little awkward at first for people used to other styles of programming, recursive programming is actually quite simple to understand. It however usually generates slower code. Many (but not all) programming languages allow one to call a function within its definition thus allowing to do recursive programming\n", 186 | "\n", 187 | "Do you want to try to write a recursive function to calculate the factorial of a number?" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "### Object-oriented programming " 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "Another programming style, which affects more the higher level structure of the code than the lower level logic, is object-oriented programming. In this case abstract objects, with specific properties and specific functions to act on them, are defined and it is these objects which play a key role in how some algorithm is written.\n", 209 | "\n", 210 | "Many modern programming language allow an object-oriented approach to programming. Some languages where this feature is quite prominent are [C++](https://en.wikipedia.org/wiki/C%2B%2B), [Java](https://en.wikipedia.org/wiki/Java_(programming_language)), [Ada](https://en.wikipedia.org/wiki/Ada_(programming_language)) and ... Python.\n", 211 | "\n", 212 | "In this course we are going to be introducing some of the object-oriented feature of Python and exploring how they can be useful in writing bigger computer programs." 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": {}, 218 | "source": [ 219 | "## A known problem revisited " 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "In the [previous course](https://github.com/imperialchem/python-prog-intro/blob/v2.1/prog_workshop4/prog_workshop4.ipynb) we looked at the problem of reading the .xyz file of a triatomic molecule, and determine its bond angle. We will now revisit that problem to get started and see how to build on from it.\n", 227 | "\n", 228 | "In the original version of the problem we needed to read the file containing the molecule structure. We get a bit of head start and define the structure as a string here." 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "H2S='''S 0.00000000 0.00000000 0.10224900\n", 238 | "H 0.00000000 0.96805900 -0.81799200\n", 239 | "H 0.00000000 -0.96805900 -0.81799200'''" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "\n", 247 | "\n", 248 | "We are interested in determining the bond angle and the bond length of the H2S structure. We saw how using Numpy arrays could greatly simplify the problem, but for the sake of the argument and the exercise we will now use lists to solve the same problem.\n", 249 | "\n", 250 | "*This part of the workshop should not take too long, so if you find yourself stuck, do ask for help!*\n", 251 | "\n", 252 | "The first step could be to convert the string H2S into a list we will call *H2S_xyz*, which has the form:\n", 253 | "\n", 254 | " [['S',x_coord,y_coord,z_coord],\n", 255 | " ['H',x_coord,y_coord,z_coord],\n", 256 | " ['H',x_coord,y_coord,z_coord]]\n", 257 | " \n", 258 | "To parse the string we could use the string methods [.splitlines()](https://docs.python.org/3/library/stdtypes.html#str.splitlines) and/or [.split()](https://docs.python.org/3/library/stdtypes.html#str.split) with a loop construction in order to achieve this. Note that the element symbols in the list above will be strings, but the atom coordinates should be converted to floating point numbers." 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [] 267 | }, 268 | { 269 | "cell_type": "markdown", 270 | "metadata": {}, 271 | "source": [ 272 | "In solving this problem it is useful to think of the chemical bonds as vectors. It is thus useful to define a function *bond()*, which receives 2 lists with the coordinates of 2 atoms, and returns one list with the coordinates of the vector defined by their position." 273 | ] 274 | }, 275 | { 276 | "cell_type": "code", 277 | "execution_count": null, 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "We will now define a few functions that implement some useful vector operations.\n", 287 | "\n", 288 | "First of which the function *length()*, that receives a list that defines a vector, and returns the length of this vector." 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "metadata": {}, 295 | "outputs": [], 296 | "source": [] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "And the function *inner_prod()*, which receives 2 lists as arguments, and return the inner product (or dot product) between them" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "Using the functions defined above, we can now define a new function *bond_length()*, that should calculate a bond length by receiving 3 arguments: the first argument should be a list of the form of *H2S_xyz* defining the structure of the molecule; followed by 2 other integer arguments specifying the position of the atoms between which we want to calculate the bond distance (defined by their position on the list). For example: if we wanted to calculate the bond distance between the S atom the H atom on the left of the picture we should execute\n", 317 | "\n", 318 | " bond_length(H2S_xyz,0,2)" 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": null, 324 | "metadata": {}, 325 | "outputs": [], 326 | "source": [] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "Similarly, we want to define a function *bond_angle()* to calculate the angle between 2 bonds, by receiving the list defining the structure of the molecule and the 3 integers specifying the atoms between which the bonds are formed. Apart from the functions operating on vectors defined above, you will also need the function *acos()* from the math or the numpy modules." 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": null, 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "metadata": {}, 345 | "source": [ 346 | "We have followed a rather structured approach to the solution of the problem (this is a good thing):\n", 347 | "\n", 348 | "* we represented the structure of the molecule by the list H2S_xyz;\n", 349 | "* abstracted the concept of chemical bond and implemented these as vectors;\n", 350 | "* implemented the vectors as Python lists;\n", 351 | "* implemented basic operations on vectors as the functions length() and inner_prod();\n", 352 | "* using these functions we built the higher level functions to solve our problem.\n", 353 | "\n", 354 | "This is a fairly good implementation, general enough so that we can apply it to any other molecule by defining a new structure defining list.\n", 355 | "\n", 356 | "It does however have some fragilities. What happens if you try to run\n", 357 | "\n", 358 | " length(['1','0','0'])" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": null, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": {}, 371 | "source": [ 372 | "As you could have guessed, you obtain an error because although ['1','0','0'] is a perfectly valid Python list, it will not have the same properties of a vector.\n", 373 | "\n", 374 | "Also if you run\n", 375 | "\n", 376 | " H2S_mol=[[0.,0.,0.102249,'S'],[0.,0.968059,-0.817992,'H'],[0.,-0.968059,-0.817992,'H']]\n", 377 | " \n", 378 | " bond_length(H2S_mol,0,2)" 379 | ] 380 | }, 381 | { 382 | "cell_type": "code", 383 | "execution_count": null, 384 | "metadata": {}, 385 | "outputs": [], 386 | "source": [] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "metadata": {}, 391 | "source": [ 392 | "you will see your code breaking, although *H2S_mol* is a fine list and contains information about the structure of the molecule in an equivalent way to *H2S_xyz*.\n", 393 | "\n", 394 | "It can be said that the above examples are just programming errors, and indeed they are! We are however looking for a way to minimize programming errors in larger codes, where a small mistake in one line can be hard to find and render the entire code useless. We also want to reduce the complexity associated with manipulating molecules, - it would be easier if we didn't have to remember that the symbol must come first and followed by the coordinates. \n", 395 | "\n", 396 | "Really we want to be able to define a vector, not a list, that really behaves like a vector, and has the operations we usually associate with vectors. And we want to define a molecular structure in some way, and be able to define operations that are specific to molecular structures (and completely inappropriate for lists of numbers for example).\n", 397 | "\n", 398 | "We would like to be able define abstract **classes** of **objects**, that have specific **attributes** and specific **methods** associated with them. When a programming language allows for this, it allows for *object-oriented programming*. We will see how to do this presently." 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "## Why using object-oriented programming?" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "metadata": { 411 | "slideshow": { 412 | "slide_type": "slide" 413 | } 414 | }, 415 | "source": [ 416 | "Even the relatively simple code such the one we have written above can span several dozens of lines. You may start to appreciate that more sophisticated programs can quickly reach thousands and even millions of lines of codes, with a number of variables of similar order of magnitude, complexity and readability quickly getting out of hand.\n", 417 | "\n", 418 | "The figure below can give you a feeling of how big the code base of current software can be. Note there is some scientific software included in the list.\n", 419 | "\n", 420 | "" 421 | ] 422 | }, 423 | { 424 | "cell_type": "markdown", 425 | "metadata": { 426 | "slideshow": { 427 | "slide_type": "slide" 428 | } 429 | }, 430 | "source": [ 431 | "Given large code bases with 104 - 108 lines of code the most important problem is writing code that can be easily modified, understood and maintained. \n", 432 | "\n", 433 | "Object-oriented programming is a way of organising data and functions into reusable modular units or 'objects'.\n", 434 | "\n", 435 | "The benefits are:\n", 436 | "\n", 437 | "* Code reuse and recycling\n", 438 | "* Encapsulation (different parts of the code not interfering with each other)\n", 439 | "* Easier design\n", 440 | "* Easier modification\n", 441 | "\n", 442 | "Some drawbacks are:\n", 443 | " \n", 444 | "* larger programs\n", 445 | "* some reduced performance\n", 446 | "* some learning curve" 447 | ] 448 | }, 449 | { 450 | "cell_type": "markdown", 451 | "metadata": {}, 452 | "source": [ 453 | "## Defining classes of objects " 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "metadata": {}, 459 | "source": [ 460 | "In order to specify the characteristics we want certain objects to have, we need to define a **class** of objects. This is done via the class statement\n", 461 | "\n", 462 | " class ClassName:\n", 463 | " '''A docstring describing the objects of the class'''\n", 464 | " \n", 465 | "followed by the indented class content. By convention, class names are usually capitalized, and function names written in lower case. (For the type of functionality that we are covering, we don't need to specify any arguments in the class definition. This is used for some more advanced functionality).\n", 466 | "\n", 467 | "Like with functions, we should provide a documentation string describing the class and how to use it.\n", 468 | "\n", 469 | "Although not mandatory, the first thing to define after the class name is usually a function with a strange name '\\_\\_init\\_\\_'. Actually, functions defined inside classes are called **methods**, and we will soon see how they work.\n", 470 | "\n", 471 | " class ClassName:\n", 472 | " '''A docstring describing the objects of the class'''\n", 473 | " \n", 474 | " def __init__(self, arg1, arg2, arg3):\n", 475 | " '''Docstring for the method'''\n", 476 | " \n", 477 | " #code to initialise the class\n", 478 | "\n", 479 | "The \\_\\_init\\_\\_ **method** is run whenever a new object of the class is generated. We call this process, initializing an **instance** of the class.\n", 480 | "\n", 481 | "Inside the class definition, the name *self* is used to refer to the object being acted on (more on this below). The first argument of the \\_\\_init\\_\\_ method is always *self*, followed by the other arguments needed to define the object.\n", 482 | "\n", 483 | "At the moment the discussion is quite abstract, so will illustrate what this means by defining a Vector class. This will allow us to create variables that represent 3D vectors, and methods that operate on them. At a bare minimum, our vector objects should possess three **attributes**, x,y and z, representing the components of the vector. " 484 | ] 485 | }, 486 | { 487 | "cell_type": "markdown", 488 | "metadata": {}, 489 | "source": [ 490 | " class Vector: # <--- class definition\n", 491 | " '''Implements 3D vectors and their behaviour'''\n", 492 | " \n", 493 | " def __init__(self, i1,i2,i3): #<--- initialisation method\n", 494 | " '''Initializes new vector objects by setting the values\n", 495 | " of their of their 3 components\n", 496 | " '''\n", 497 | " self.x = i1\n", 498 | " self.y = i2\n", 499 | " self.z = i3" 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": null, 505 | "metadata": {}, 506 | "outputs": [], 507 | "source": [] 508 | }, 509 | { 510 | "cell_type": "markdown", 511 | "metadata": {}, 512 | "source": [ 513 | "We have just defined our Vector class, and can now create Vector objects. We do this by using the class name, and passing to it the arguments of the \\_\\_init\\_\\_ method except *self*. In this case we need to provide 3 numbers to define a vector.\n", 514 | "\n", 515 | " vec1 = Vector(1,2,3)" 516 | ] 517 | }, 518 | { 519 | "cell_type": "code", 520 | "execution_count": null, 521 | "metadata": {}, 522 | "outputs": [], 523 | "source": [] 524 | }, 525 | { 526 | "cell_type": "markdown", 527 | "metadata": {}, 528 | "source": [ 529 | "The variable vec1 is now an object of the class Vector, or in other words, an **instance** of the class Vector.\n", 530 | "\n", 531 | "**object** = **instance of the class**\n", 532 | "\n", 533 | "We can confirm this by checking its type\n", 534 | "\n", 535 | " type(vec1)" 536 | ] 537 | }, 538 | { 539 | "cell_type": "code", 540 | "execution_count": null, 541 | "metadata": {}, 542 | "outputs": [], 543 | "source": [] 544 | }, 545 | { 546 | "cell_type": "markdown", 547 | "metadata": {}, 548 | "source": [ 549 | "We defined Vector to be initialised with 3 values, and all that the \\_\\_init\\_\\_ method above does is to assign 3 variables (in a slightly different way) with these. Variables defined inside a class in this way are called **attributes**. In our vector class we have defined three attributes *x*, *y* and *z*. \n", 550 | "\n", 551 | "We can access the attributes of a given object by its name followed by a dot (.) followed by the name of the attribute (or method).\n", 552 | "So to access the x attribute we can type:\n", 553 | "\n", 554 | " vec1.x" 555 | ] 556 | }, 557 | { 558 | "cell_type": "code", 559 | "execution_count": null, 560 | "metadata": {}, 561 | "outputs": [], 562 | "source": [] 563 | }, 564 | { 565 | "cell_type": "markdown", 566 | "metadata": {}, 567 | "source": [ 568 | "In Python, the value of attributes of a given object can be changed just as one would do for a variable assignment\n", 569 | "\n", 570 | " vec1.x=0\n", 571 | " vec1.x" 572 | ] 573 | }, 574 | { 575 | "cell_type": "code", 576 | "execution_count": null, 577 | "metadata": {}, 578 | "outputs": [], 579 | "source": [] 580 | }, 581 | { 582 | "cell_type": "markdown", 583 | "metadata": {}, 584 | "source": [ 585 | "When working in the notebook, we can see what attributes (and methods) are associated with a given object by pressing the tab key after the dot\n", 586 | "\n", 587 | " vec1." 588 | ] 589 | }, 590 | { 591 | "cell_type": "code", 592 | "execution_count": null, 593 | "metadata": {}, 594 | "outputs": [], 595 | "source": [] 596 | }, 597 | { 598 | "cell_type": "markdown", 599 | "metadata": {}, 600 | "source": [ 601 | "Once we have defined our class we can create as many objects as we want, i.e. instances of the Vector class.\n", 602 | "\n", 603 | " vec2=Vector(-1,0,1)\n", 604 | " vec3=Vector(1,1,4)" 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": null, 610 | "metadata": {}, 611 | "outputs": [], 612 | "source": [] 613 | }, 614 | { 615 | "cell_type": "markdown", 616 | "metadata": {}, 617 | "source": [ 618 | "and access their attributes\n", 619 | "\n", 620 | " [vec1.x,vec2.y,vec3.z]" 621 | ] 622 | }, 623 | { 624 | "cell_type": "code", 625 | "execution_count": null, 626 | "metadata": {}, 627 | "outputs": [], 628 | "source": [] 629 | }, 630 | { 631 | "cell_type": "markdown", 632 | "metadata": {}, 633 | "source": [ 634 | "We have managed to generate our defined Vectors, but apart from the fact that have 3 distinct attributes, they don't look that much like vectors for now. Namely they don't have any operation associated with them. If we try to multiply a vector by a number, the operation will fail because we haven't yet defined what the result of this operation should be.\n", 635 | "\n", 636 | " 2.5*vec2" 637 | ] 638 | }, 639 | { 640 | "cell_type": "code", 641 | "execution_count": null, 642 | "metadata": {}, 643 | "outputs": [], 644 | "source": [] 645 | }, 646 | { 647 | "cell_type": "markdown", 648 | "metadata": {}, 649 | "source": [ 650 | "We shall address this immediately by defining a method for scaling the vector, that is multiplying the vector by a scalar number. We add functionality to our objects be refining our class definition, in this case by adding a new method to it.\n", 651 | "\n", 652 | " class Vector: # <--- class definition\n", 653 | " '''Implements 3D vectors and their behaviour'''\n", 654 | " \n", 655 | " def __init__(self, i1,i2,i3): #<--- initialisation method\n", 656 | " '''Initializes new vector objects by setting the values\n", 657 | " of their 3 components\n", 658 | " '''\n", 659 | " self.x = i1 #<--- initialisation of attributes\n", 660 | " self.y = i2\n", 661 | " self.z = i3\n", 662 | " \n", 663 | " def scale(self,a): #<--- scale method\n", 664 | " '''Multiplies the vector by a scalar a'''\n", 665 | " self.x=self.x*a\n", 666 | " self.y=self.y*a\n", 667 | " self.z=self.z*a " 668 | ] 669 | }, 670 | { 671 | "cell_type": "code", 672 | "execution_count": 1, 673 | "metadata": {}, 674 | "outputs": [], 675 | "source": [] 676 | }, 677 | { 678 | "cell_type": "markdown", 679 | "metadata": {}, 680 | "source": [ 681 | "We define the scale method just like we define a function, and in this case we need to pass as arguments *self* and the scaling factor *a*. **All methods receive *self* as their first argument** (it is the first argument to \\_\\_init\\_\\_ too). *self* is what we call the object from inside the class. That is, when we are outside our class definition we create an instance and assign it to some variable (above we used vec1 but of course we could pick any name we like), then we access attributes/methods with that variable name, as we have seen. When we are defining methods we need some way to refer to object before it has been created. This is what the *self* keyword is doing.\n", 682 | "\n", 683 | "When we call scale, we want to update the attributes of the vector, such that each component is multiplied by *a*. To access the x,y and z attributes we thus use self.x, self.y and self.z, and update their value." 684 | ] 685 | }, 686 | { 687 | "cell_type": "markdown", 688 | "metadata": {}, 689 | "source": [ 690 | "Since we have updated the class, we need to create new objects with this updated functionality, and we are able to see the new method on by pressing the tab after the dot\n", 691 | "\n", 692 | " vec1=Vector(1,2,3)" 693 | ] 694 | }, 695 | { 696 | "cell_type": "code", 697 | "execution_count": null, 698 | "metadata": {}, 699 | "outputs": [], 700 | "source": [] 701 | }, 702 | { 703 | "cell_type": "markdown", 704 | "metadata": {}, 705 | "source": [ 706 | " vec1." 707 | ] 708 | }, 709 | { 710 | "cell_type": "code", 711 | "execution_count": null, 712 | "metadata": {}, 713 | "outputs": [], 714 | "source": [] 715 | }, 716 | { 717 | "cell_type": "markdown", 718 | "metadata": {}, 719 | "source": [ 720 | "Let us test the new method. Note that we only need to pass the argument *a* when calling it.\n", 721 | "\n", 722 | " vec1.scale(2.5)" 723 | ] 724 | }, 725 | { 726 | "cell_type": "code", 727 | "execution_count": null, 728 | "metadata": {}, 729 | "outputs": [], 730 | "source": [] 731 | }, 732 | { 733 | "cell_type": "markdown", 734 | "metadata": {}, 735 | "source": [ 736 | "We have now covered the basic functionality of how to create abstract objects in Python. There are always other things to learn, but that's basically it for how classes work. It is worth recapping what we covered so far:\n", 737 | " \n", 738 | "* Classes are a means to combine data (attributes) and functionality (methods). \n", 739 | "\n", 740 | "* We define them using the *class* keyword followed by the name of our class. Within the class definition we must include an \\_\\_init\\_\\_ method which is used to initialise instances of the class.\n", 741 | "\n", 742 | "* We create objects (or instances) of the class by typing the name of our class followed by two round brackets and passing the data we want our instance to be initialised with.\n", 743 | "\n", 744 | "* Once we have created an object and set it to a variable we can access attributes and methods of the object using the variable name followed by a dot (.) then the name of the attribute or method." 745 | ] 746 | }, 747 | { 748 | "cell_type": "markdown", 749 | "metadata": {}, 750 | "source": [ 751 | "*This is the point to ask if you don't understand what's going on!*" 752 | ] 753 | }, 754 | { 755 | "cell_type": "markdown", 756 | "metadata": {}, 757 | "source": [ 758 | "### Enriching the Vector class" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "metadata": {}, 764 | "source": [ 765 | "We will now add several methods that will make our objects behave more like vectors. In particular we will implements methods that define the *dot* product between vectors, the *norm* (or length) of a vector, as well as the sum of two vectors and how to *copy* a vector on the way to it. In doing this we will be illustrating further aspects of building classes of objects." 766 | ] 767 | }, 768 | { 769 | "cell_type": "markdown", 770 | "metadata": {}, 771 | "source": [ 772 | "#### dot " 773 | ] 774 | }, 775 | { 776 | "cell_type": "markdown", 777 | "metadata": {}, 778 | "source": [ 779 | "We are interested in implementing a method, which we will call *dot*, to calculate the dot product between 2 vectors. Here's how we want *dot* to behave:\n", 780 | "\n", 781 | " u = Vector(1,2,3)\n", 782 | " v = Vector(4,5,6)\n", 783 | " u.dot(v) == 32\n", 784 | " \n", 785 | "So we are going to create two instances of Vector, u and v, and then call the dot method of u passing in v as the argument to the method. The method will return a number equal to the dot product of the two vectors. \n", 786 | "\n", 787 | "We will be updating the class Vector often, so it is more practical to copy it to a separate file. You can use any regular text editor (such as Notepad on Windows) and save the file with the extension .py, but a better workflow is to have this notebook and the python script with your Vector class both open on JupyterLab (drag the tab title to make it side by side windows).\n", 788 | "\n", 789 | " class Vector:\n", 790 | " '''Implements 3D vectors and their behaviour'''\n", 791 | " \n", 792 | " def __init__(self, i1,i2,i3):\n", 793 | " '''Initializes new vector objects by setting the values\n", 794 | " of their 3 components\n", 795 | " '''\n", 796 | " self.x = i1\n", 797 | " self.y = i2\n", 798 | " self.z = i3\n", 799 | " \n", 800 | " def scale(self,a):\n", 801 | " '''Multiplies the vector by a scalar a'''\n", 802 | " self.x=self.x*a\n", 803 | " self.y=self.y*a\n", 804 | " self.z=self.z*a \n", 805 | "\n", 806 | "We would now import the module we have created that contains the Vector class, using the usual Python import syntax:\n", 807 | "\n", 808 | " from your_class_file import Vector" 809 | ] 810 | }, 811 | { 812 | "cell_type": "code", 813 | "execution_count": null, 814 | "metadata": {}, 815 | "outputs": [], 816 | "source": [] 817 | }, 818 | { 819 | "cell_type": "markdown", 820 | "metadata": {}, 821 | "source": [ 822 | "Usually, external modules only need to be imported once for each session. But because we will be incrementing and modifying our Vector class, we will need to reload the file each time we do so. The IPython session in Jupyter allows us to do this automatically via a special non-Python command (modify your_class_file name below - note no .py extension here)" 823 | ] 824 | }, 825 | { 826 | "cell_type": "code", 827 | "execution_count": null, 828 | "metadata": {}, 829 | "outputs": [], 830 | "source": [ 831 | "%load_ext autoreload\n", 832 | "%autoreload 1\n", 833 | "%aimport your_class_file" 834 | ] 835 | }, 836 | { 837 | "cell_type": "markdown", 838 | "metadata": {}, 839 | "source": [ 840 | "Now add the *dot* method to the Vector class in your file, and check it behaves as intended." 841 | ] 842 | }, 843 | { 844 | "cell_type": "code", 845 | "execution_count": null, 846 | "metadata": {}, 847 | "outputs": [], 848 | "source": [] 849 | }, 850 | { 851 | "cell_type": "markdown", 852 | "metadata": {}, 853 | "source": [ 854 | "#### norm " 855 | ] 856 | }, 857 | { 858 | "cell_type": "markdown", 859 | "metadata": {}, 860 | "source": [ 861 | "Now that we have the *dot* method, we can add to the class another method to return the vector norm." 862 | ] 863 | }, 864 | { 865 | "cell_type": "code", 866 | "execution_count": null, 867 | "metadata": {}, 868 | "outputs": [], 869 | "source": [] 870 | }, 871 | { 872 | "cell_type": "markdown", 873 | "metadata": {}, 874 | "source": [ 875 | "#### vector addition " 876 | ] 877 | }, 878 | { 879 | "cell_type": "markdown", 880 | "metadata": {}, 881 | "source": [ 882 | "In defining the operation corresponding to the addition of two vectors, we will take a few detours on the way.\n", 883 | "\n", 884 | "We will first define a *combine* method. Here's how we want it to behave:\n", 885 | " \n", 886 | " u = Vector(1,2,3)\n", 887 | " v = Vector(4,5,6)\n", 888 | " u.combine(v)\n", 889 | " (u.x,u.y,u.z) == (5,7,9)\n", 890 | " \n", 891 | "So we're going to create two instances of vector, u and v. Then we're going to call the combine method of u and again pass in v. This time the method won't return anything instead it will update u by adding on v.\n", 892 | "\n", 893 | "Modify the file with your Vector class to include a combine method and check it behaves as intended." 894 | ] 895 | }, 896 | { 897 | "cell_type": "code", 898 | "execution_count": null, 899 | "metadata": { 900 | "collapsed": true 901 | }, 902 | "outputs": [], 903 | "source": [] 904 | }, 905 | { 906 | "cell_type": "code", 907 | "execution_count": null, 908 | "metadata": {}, 909 | "outputs": [], 910 | "source": [] 911 | }, 912 | { 913 | "cell_type": "markdown", 914 | "metadata": {}, 915 | "source": [ 916 | "A useful operation would be to be able to generate a copy of our vector, an object of the Vector class. A first uninformed attempt could be to make an assignment to a different variable\n", 917 | "\n", 918 | " w=v\n", 919 | " (w.x,w.y,w.z)" 920 | ] 921 | }, 922 | { 923 | "cell_type": "code", 924 | "execution_count": null, 925 | "metadata": {}, 926 | "outputs": [], 927 | "source": [] 928 | }, 929 | { 930 | "cell_type": "markdown", 931 | "metadata": {}, 932 | "source": [ 933 | "At first glance this seems to have worked just as intended, but note what happens if we now change the original vector v\n", 934 | "\n", 935 | " v.x=10\n", 936 | " w.x" 937 | ] 938 | }, 939 | { 940 | "cell_type": "code", 941 | "execution_count": null, 942 | "metadata": {}, 943 | "outputs": [], 944 | "source": [] 945 | }, 946 | { 947 | "cell_type": "markdown", 948 | "metadata": {}, 949 | "source": [ 950 | "w is in this case not a copy of v, but a \"clone\" of v. It is indeed another name to call the original object by.\n", 951 | "\n", 952 | "To make a copy of the object, we note that we can actually create new instances of a class from inside the class. So the copy method to be added to the class would look like: \n", 953 | "\n", 954 | " def copy(self):\n", 955 | " '''Create a copy of the vector object'''\n", 956 | " return Vector(self.x,self.y,self.z)\n", 957 | "\n", 958 | "This is how the copy method should work.\n", 959 | "\n", 960 | " v=Vector(4,5,6)\n", 961 | " w=v.copy()\n", 962 | " v.x=10\n", 963 | " (w.x,w.y,w.z) == (4,5,6)" 964 | ] 965 | }, 966 | { 967 | "cell_type": "code", 968 | "execution_count": null, 969 | "metadata": {}, 970 | "outputs": [], 971 | "source": [] 972 | }, 973 | { 974 | "cell_type": "markdown", 975 | "metadata": {}, 976 | "source": [ 977 | "With this idea in mind let us improve our *combine* method so that instead of modifying the components of the object on which the method acts upon, to instead return a new instance containing the summed vectors. I.e. change your Vector class so that it behaves in the following way:\n", 978 | "\n", 979 | " u = Vector(1,2,3)\n", 980 | " v = Vector(4,5,6)\n", 981 | " w = u.combine(v)\n", 982 | " \n", 983 | " (u.x,u.y,u.z) == (1,2,3)\n", 984 | " (v.x,v.y,v.z) == (4,5,6)\n", 985 | " (w.x,w.y,w.z) == (5,7,9)" 986 | ] 987 | }, 988 | { 989 | "cell_type": "code", 990 | "execution_count": null, 991 | "metadata": { 992 | "collapsed": true 993 | }, 994 | "outputs": [], 995 | "source": [] 996 | }, 997 | { 998 | "cell_type": "markdown", 999 | "metadata": {}, 1000 | "source": [ 1001 | "We have made some way in defining the sum of two vectors. It might however be nice if instead of writing:\n", 1002 | " \n", 1003 | " u.combine(v)\n", 1004 | "\n", 1005 | "we could instead write:\n", 1006 | " \n", 1007 | " u + v\n", 1008 | " \n", 1009 | "It turns out we can actually do that! There are special methods that let us make use of the same syntax that python uses to modify its own variables. The one we want is the \\_\\_add\\_\\_. Switch the name of your combine method to \\_\\_add\\_\\_ and try it out!" 1010 | ] 1011 | }, 1012 | { 1013 | "cell_type": "code", 1014 | "execution_count": null, 1015 | "metadata": { 1016 | "collapsed": true 1017 | }, 1018 | "outputs": [], 1019 | "source": [] 1020 | }, 1021 | { 1022 | "cell_type": "markdown", 1023 | "metadata": {}, 1024 | "source": [ 1025 | "The method \\_\\_add\\_\\_ controls how the operation '+' behaves in our class, we can control if it behaves like the sum in numbers or the concatenation of lists or strings. There are a few of such special methods whose name start and end with two underscores. The only other one we'll mentioned is the \\_\\_repr\\_\\_ method. But a full list can be found [here](https://docs.python.org/3/reference/datamodel.html#special-method-names)" 1026 | ] 1027 | }, 1028 | { 1029 | "cell_type": "markdown", 1030 | "metadata": {}, 1031 | "source": [ 1032 | "### Object display " 1033 | ] 1034 | }, 1035 | { 1036 | "cell_type": "markdown", 1037 | "metadata": {}, 1038 | "source": [ 1039 | "If we create an instance of Vector and just look at it the value we see something not enormously eye-pleasing:\n", 1040 | "\n", 1041 | " u = Vector(1,2,3)\n", 1042 | " u" 1043 | ] 1044 | }, 1045 | { 1046 | "cell_type": "code", 1047 | "execution_count": null, 1048 | "metadata": {}, 1049 | "outputs": [], 1050 | "source": [] 1051 | }, 1052 | { 1053 | "cell_type": "markdown", 1054 | "metadata": {}, 1055 | "source": [ 1056 | "What's being printed is telling us that u is an instance of the Vector class, and that weird string of symbols and numbers is actually the address in memory where the object is contained. It might be useful though to see at a glance the components of the vector. The special \\_\\_repr\\_\\_ method controls what gets printed when out if we just look at the object itself.\n", 1057 | "\n", 1058 | "Here's how we want our \\_\\_repr\\_\\_ method to behave:\n", 1059 | "\n", 1060 | " u = Vector(1,2,3)\n", 1061 | " u.__repr__() == '1 2 3'\n", 1062 | " \n", 1063 | "It should return a string that contains the values of the components. Modify your Vector class to include the \\_\\_repr\\_\\_ method and check it behaves as expected." 1064 | ] 1065 | }, 1066 | { 1067 | "cell_type": "code", 1068 | "execution_count": null, 1069 | "metadata": { 1070 | "collapsed": true 1071 | }, 1072 | "outputs": [], 1073 | "source": [] 1074 | }, 1075 | { 1076 | "cell_type": "markdown", 1077 | "metadata": {}, 1078 | "source": [ 1079 | "Now see what happens when you look at the value of u:\n", 1080 | " \n", 1081 | " u = Vector(1,2,3)\n", 1082 | " u" 1083 | ] 1084 | }, 1085 | { 1086 | "cell_type": "code", 1087 | "execution_count": null, 1088 | "metadata": {}, 1089 | "outputs": [], 1090 | "source": [] 1091 | }, 1092 | { 1093 | "cell_type": "markdown", 1094 | "metadata": {}, 1095 | "source": [ 1096 | "We have now implemented a decent Vector class with the most important properties of 3D vectors. To have an overview of your work, if you used docstrings appropriately, use the '?' on the notebook to interrogate your class\n", 1097 | "\n", 1098 | " Vector?" 1099 | ] 1100 | }, 1101 | { 1102 | "cell_type": "code", 1103 | "execution_count": 8, 1104 | "metadata": {}, 1105 | "outputs": [], 1106 | "source": [] 1107 | }, 1108 | { 1109 | "cell_type": "markdown", 1110 | "metadata": {}, 1111 | "source": [ 1112 | "You can get a similar result by doing\n", 1113 | "\n", 1114 | " help(Vector)" 1115 | ] 1116 | }, 1117 | { 1118 | "cell_type": "code", 1119 | "execution_count": null, 1120 | "metadata": {}, 1121 | "outputs": [], 1122 | "source": [] 1123 | }, 1124 | { 1125 | "cell_type": "markdown", 1126 | "metadata": {}, 1127 | "source": [ 1128 | "One final note before we move on. In Python *everything* is an instance of a class: integers are actually instances of an integer class, lists are instances of a list class, etc. etc. and they all have attributes and methods. You can always check what methods and attributes are available to any object, by using the tab key after the dot:\n", 1129 | "\n", 1130 | " empty=[]\n", 1131 | " empty." 1132 | ] 1133 | }, 1134 | { 1135 | "cell_type": "code", 1136 | "execution_count": null, 1137 | "metadata": { 1138 | "collapsed": true 1139 | }, 1140 | "outputs": [], 1141 | "source": [] 1142 | }, 1143 | { 1144 | "cell_type": "markdown", 1145 | "metadata": {}, 1146 | "source": [ 1147 | "## Creating a molecule class " 1148 | ] 1149 | }, 1150 | { 1151 | "cell_type": "markdown", 1152 | "metadata": {}, 1153 | "source": [ 1154 | "We will now return to the problem of analysing chemical structure posed in the beginning of the workshop. For this we will use the Vector class we created and will create a new Molecule class.\n", 1155 | "\n", 1156 | "In our vector class our only attributes were three numbers: x,y,z. For a molecule we are going to want a bit more than that. We will want to initialise our molecule with two things, a list of atom symbol strings, and a list of Vectors representing the positions of the atoms.\n", 1157 | "\n", 1158 | "Define a Molecule class and check you can use it as follows:\n", 1159 | "\n", 1160 | " h2o = Molecule(['O','H','H'],\n", 1161 | " [Vector(0,0, 0.119262),\n", 1162 | " Vector(0,0.763239,-0.477047),\n", 1163 | " Vector(0,-0.763239,-0.477047)]\n", 1164 | " )\n", 1165 | " h2o.symbols == ['O','H','H']" 1166 | ] 1167 | }, 1168 | { 1169 | "cell_type": "code", 1170 | "execution_count": null, 1171 | "metadata": { 1172 | "collapsed": true 1173 | }, 1174 | "outputs": [], 1175 | "source": [] 1176 | }, 1177 | { 1178 | "cell_type": "markdown", 1179 | "metadata": {}, 1180 | "source": [ 1181 | "Now implement the *bond_length* method, that will receive the index of 2 atoms in the molecule and will return the distance between them" 1182 | ] 1183 | }, 1184 | { 1185 | "cell_type": "code", 1186 | "execution_count": null, 1187 | "metadata": {}, 1188 | "outputs": [], 1189 | "source": [] 1190 | }, 1191 | { 1192 | "cell_type": "markdown", 1193 | "metadata": {}, 1194 | "source": [ 1195 | "We now want to implement the *bond_angle* method to give the bond angle between three atoms.\n", 1196 | "\n", 1197 | "Does the above defined water molecule have the appropriate equilibrium bond angle?" 1198 | ] 1199 | }, 1200 | { 1201 | "cell_type": "code", 1202 | "execution_count": null, 1203 | "metadata": { 1204 | "collapsed": true 1205 | }, 1206 | "outputs": [], 1207 | "source": [] 1208 | }, 1209 | { 1210 | "cell_type": "markdown", 1211 | "metadata": {}, 1212 | "source": [ 1213 | "Let us test the methods on a slightly bigger molecule.\n", 1214 | " \n", 1215 | " ch4 = Molecule(['C','H','H','H','H'],\n", 1216 | " [Vector(0,0,0),\n", 1217 | " Vector(0.629118,0.629118,0.629118),\n", 1218 | " Vector(-0.629118,-0.629118,0.629118), \n", 1219 | " Vector(0.629118, -0.629118, -0.629118),\n", 1220 | " Vector(-0.629118, 0.629118, -0.629118)]\n", 1221 | " )" 1222 | ] 1223 | }, 1224 | { 1225 | "cell_type": "code", 1226 | "execution_count": null, 1227 | "metadata": {}, 1228 | "outputs": [], 1229 | "source": [] 1230 | }, 1231 | { 1232 | "cell_type": "markdown", 1233 | "metadata": {}, 1234 | "source": [ 1235 | "This a good point to try get a broader picture. Why are we bothering with all of this? We could do all the same things we have done so far with separate variables for each component of each vector and we could use functions instead of methods, as we did in the beginning of the workshop. But if we did that it would be much more complicated to maintain, we would have to keep track of all these myriad of different variables and what they mean and how they fit together, we'd also have to keep track of what variables needed to be passed to which functions. \n", 1236 | "\n", 1237 | "With the Vector and Molecule classes that is all handled for us. In fact once it is defined we don't need to pay attention to how it works at all. We can think of an instance of the Vector class just as a vector. (To think of the instance of Molecule as a molecule is a bit more far fetched, but it is a good representation of molecular structure.) A proper class should provide methods that do all the fundamental things we expect to be able to do with the object that class represents.\n", 1238 | "\n", 1239 | "To illustrate the versatility of the object-oriented approach, now that we have implemented the Vector and Molecule classes, we can easily extend the Molecule class to calculate other common molecular properties.\n", 1240 | "\n", 1241 | "Let us now implement a new attribute charge and a new method to calculate the dipole moment of the molecule\n", 1242 | "\n", 1243 | "$$\\vec{\\mu}=\\sum_i q_i \\vec{r}_i$$\n", 1244 | "\n", 1245 | "where the sum extends to all atoms of the molecule, $q_i$ is the partial charge on atom *i*, and $\\vec{r}_i$ is the position vector of the atom.\n", 1246 | "\n", 1247 | "What is the dipole moment of H2O given the partial charges equal to [-0.68,0.34,0.34]." 1248 | ] 1249 | }, 1250 | { 1251 | "cell_type": "code", 1252 | "execution_count": null, 1253 | "metadata": {}, 1254 | "outputs": [], 1255 | "source": [] 1256 | }, 1257 | { 1258 | "cell_type": "markdown", 1259 | "metadata": {}, 1260 | "source": [ 1261 | "What about the fluoroform molecule?\n", 1262 | "\n", 1263 | " cf3h = Molecule(['C','F','F','F','H'],\n", 1264 | " [Vector(0.,0.,0.335420),\n", 1265 | " Vector(0.,0.,1.425773),\n", 1266 | " Vector(0.,1.249478,-0.127344),\n", 1267 | " Vector(-1.082080,-0.624739,-0.127344),\n", 1268 | " Vector(1.082080,-0.624739,-0.127344)],\n", 1269 | " [1.109,-0.401,-0.401,-0.401,0.093]\n", 1270 | " )" 1271 | ] 1272 | }, 1273 | { 1274 | "cell_type": "code", 1275 | "execution_count": null, 1276 | "metadata": {}, 1277 | "outputs": [], 1278 | "source": [] 1279 | }, 1280 | { 1281 | "cell_type": "markdown", 1282 | "metadata": {}, 1283 | "source": [ 1284 | "It is equally simple to implement a method to determine the centre of mass of the molecule, or the moment of inertia along the Cartesian axes (in this case you would have to displace the molecule such that the centre of mass corresponds to the origin of the reference frame)." 1285 | ] 1286 | }, 1287 | { 1288 | "cell_type": "code", 1289 | "execution_count": null, 1290 | "metadata": {}, 1291 | "outputs": [], 1292 | "source": [] 1293 | }, 1294 | { 1295 | "cell_type": "markdown", 1296 | "metadata": {}, 1297 | "source": [ 1298 | "### Optional: Spin-off an Atom class" 1299 | ] 1300 | }, 1301 | { 1302 | "cell_type": "markdown", 1303 | "metadata": {}, 1304 | "source": [ 1305 | "Now that we have added charges along side the symbols and the Vectors the Molecule class is starting to get a little full. Imagine if we wanted to add magnetic moments, atomic numbers and radii to our atoms- suddenly initialising our molecule would look a bit of a mess. The solution is of course to create an Atom class to represent atom objects and then construct Molecule objects from a list of Atom objects. \n", 1306 | "\n", 1307 | "Have a go building an Atom class and then refactor your molecule class so that it is built from a list of Atoms." 1308 | ] 1309 | }, 1310 | { 1311 | "cell_type": "code", 1312 | "execution_count": null, 1313 | "metadata": { 1314 | "collapsed": true 1315 | }, 1316 | "outputs": [], 1317 | "source": [] 1318 | }, 1319 | { 1320 | "cell_type": "markdown", 1321 | "metadata": {}, 1322 | "source": [ 1323 | "## General advice in object-oriented design " 1324 | ] 1325 | }, 1326 | { 1327 | "cell_type": "markdown", 1328 | "metadata": {}, 1329 | "source": [ 1330 | "To conclude, we present some general advice of choosing how to choose to implement program using objects.\n", 1331 | "\n", 1332 | "When presented with a programming task, the most important stage is that of deciding the design of the program. This is particularly important when doing object-oriented programming, as the programmer is given a wider choice in how to implement his/her code.\n", 1333 | "\n", 1334 | "A rule of thumb to aid in the design choice is based on the natural language description of the problem to be solved. As an example, imagine that we wanted to make the classic [video game Asteroids](https://en.wikipedia.org/wiki/Asteroids_(video_game)). To identify the relevant classes we would try describing it.\n", 1335 | "\n", 1336 | "Wikipedia describes Asteroids as follows:\n", 1337 | "\n", 1338 | "> The objective of Asteroids is to score as many points as possible by destroying asteroids and flying saucers.\n", 1339 | "> The player controls a triangular-shaped ship that can rotate left and right, fire shots straight forward,\n", 1340 | "> and thrust forward. As the ship moves, momentum is not conserved – the ship eventually comes to a stop again\n", 1341 | "> when not thrusting.\n", 1342 | "\n", 1343 | "Look for nouns mentioned that could meaningfully have some kind of 'state' and some set of 'actions' associated with them. These become our objects, the states become attributes and the actions become methods.\n", 1344 | "\n", 1345 | "Thus classes are: \n", 1346 | "\n", 1347 | "* a ship, \n", 1348 | "* an asteroid, \n", 1349 | "* a flying saucer\n", 1350 | "* a bullet \n", 1351 | "\n", 1352 | "Then for each object consider their states and behaviours. For example, the ship class\n", 1353 | " \n", 1354 | " states: position, momentum, orientation\n", 1355 | " behaviours: rotate_left, rotate_right, thrust, fire\n", 1356 | " \n", 1357 | "Below is the outline of a possible Ship class for illustration purpose." 1358 | ] 1359 | }, 1360 | { 1361 | "cell_type": "code", 1362 | "execution_count": null, 1363 | "metadata": {}, 1364 | "outputs": [], 1365 | "source": [ 1366 | "import numpy as np\n", 1367 | "\n", 1368 | "class Ship:\n", 1369 | " '''Object class representing the ship in the Asteroids video game.'''\n", 1370 | " \n", 1371 | " def __init__(self, position=(0,0), momentum=(0,0), orientation=0, bullets=None):\n", 1372 | " '''Initialization of the ship object. The argument list includes arguments\n", 1373 | " called by name, and includes default values. If the class is called as\n", 1374 | " Ship(position=(1,1),orientation=0.85), the attributes will get the values specified\n", 1375 | " in the arguments list, or the default values if they are not specified.\n", 1376 | " '''\n", 1377 | " self.position = np.array(position)\n", 1378 | " self.momentum = np.array(momentum)\n", 1379 | " self.orientation = orientation\n", 1380 | " self.bullets = bullets\n", 1381 | " \n", 1382 | " def rotate_left(self, ang):\n", 1383 | " '''Rotate ship left by ang radians.'''\n", 1384 | " self.orientation = self.orientation - ang\n", 1385 | " \n", 1386 | " def rotate_right(self, ang):\n", 1387 | " '''Rotate ship right by ang radians.'''\n", 1388 | " self.orientation = self.orientatio + ang\n", 1389 | " \n", 1390 | " def thrust(self, w):\n", 1391 | " '''Controls thrust inpulse of the ship.'''\n", 1392 | " x_,y_ = self.momentum\n", 1393 | " self.momentum = np.array([x_+np.sin(self.orientation)*w, y_+np.cos(self.orientation)*w])\n", 1394 | " \n", 1395 | " def fire(self):\n", 1396 | " '''Fire a shot by generating a Bullet object.'''\n", 1397 | " self.bullets.append(Bullet(self.position, self.orientation))\n", 1398 | "\n", 1399 | " def check_collision(self):\n", 1400 | " '''Check whether the shot was a hit.'''\n", 1401 | " pass\n", 1402 | " \n", 1403 | " def update(self, dt):\n", 1404 | " '''Evolve the ship and bullets in time.'''\n", 1405 | " for bullet in self.bullets:\n", 1406 | " bullet.update(dt)\n", 1407 | " self.position = self.position+dt*self.momentum\n", 1408 | " self.check_collision()" 1409 | ] 1410 | }, 1411 | { 1412 | "cell_type": "markdown", 1413 | "metadata": {}, 1414 | "source": [ 1415 | "## Overview" 1416 | ] 1417 | }, 1418 | { 1419 | "cell_type": "markdown", 1420 | "metadata": {}, 1421 | "source": [ 1422 | "In this workshop we have explored different programming paradigms, and in particular we have introduced the object-oriented features of the Python language.\n", 1423 | "\n", 1424 | "We have build a class to implement the behaviour of 3D vectors in Cartesian space, and we have constructed a class representing molecular structure and allowing for the calculation of structure related molecular properties. We contrasted this implementation with an \"unstructured\" approach based on variables and functions. Although the advantages of the object oriented approach are not self-evident for simple codes, its main strength lie in that they are easier to maintain and extend, which make it suitable to manage large programs.\n", 1425 | "\n", 1426 | "Chemical simulation programs can attain a high degree of complexity and sophistication. Based on the skills acquired, we will create next a simple simulation of a 2D gas, and analyse some of its properties." 1427 | ] 1428 | }, 1429 | { 1430 | "cell_type": "markdown", 1431 | "metadata": {}, 1432 | "source": [ 1433 | "## Other Python resources" 1434 | ] 1435 | }, 1436 | { 1437 | "cell_type": "markdown", 1438 | "metadata": {}, 1439 | "source": [ 1440 | "There is an immense amount of documentation available about the Python language and it's applications to different domains from which you can draw upon and further your studies.\n", 1441 | "\n", 1442 | "A good place to start is the [tutorial on classes](https://docs.python.org/3/tutorial/classes.html) in Python's official documentation website.\n", 1443 | "\n", 1444 | "Good reference books could be [Learn Python the hardway](http://learnpythonthehardway.org) or [Learning Scientific Programming with Python](http://scipython.com)." 1445 | ] 1446 | } 1447 | ], 1448 | "metadata": { 1449 | "kernelspec": { 1450 | "display_name": "Python 3 (ipykernel)", 1451 | "language": "python", 1452 | "name": "python3" 1453 | }, 1454 | "language_info": { 1455 | "codemirror_mode": { 1456 | "name": "ipython", 1457 | "version": 3 1458 | }, 1459 | "file_extension": ".py", 1460 | "mimetype": "text/x-python", 1461 | "name": "python", 1462 | "nbconvert_exporter": "python", 1463 | "pygments_lexer": "ipython3", 1464 | "version": "3.11.2" 1465 | } 1466 | }, 1467 | "nbformat": 4, 1468 | "nbformat_minor": 1 1469 | } 1470 | -------------------------------------------------------------------------------- /oo_workshop/molecule_angle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 23 | 28 | 29 | 35 | 40 | 41 | 47 | 52 | 53 | 59 | 64 | 65 | 66 | 68 | 69 | 71 | image/svg+xml 72 | 74 | 75 | 76 | 77 | 78 | 81 | 866 | 870 | 874 | 875 | 876 | -------------------------------------------------------------------------------- /oo_project/oo_project.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Project: Hard disks molecular dynamics simulations" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Clyde Fare and João Pedro Malhado, Imperial College London (contact: [python@imperial.ac.uk](mailto:python@imperial.ac.uk))\n", 15 | "\n", 16 | "Notebook is licensed under a [Creative Commons Attribution 4.0 (CC-by) license](http://creativecommons.org/licenses/by/4.0/)." 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "A computational chemist friend of mine once defined simulations in the following terms:\n", 24 | "\n", 25 | "> Experimental work resembles the actions of a child that receives a big wrapped present, and without opening it, by shaking it in his hands and hearing the sound it makes, has to infer what is inside the box and how it works. Simulation on the other hand, is like starting with an empty box, fill it with pieces that we know well, close and wrap the box, and shake it to hear the sound it makes. By comparing the two sounds, we may be able to understand nature a little better.\n", 26 | "> (Philipp Marquetand, sometime around 2009)\n", 27 | "\n", 28 | "This project is about building a 2D box, filling it with disks, and hearing what sound it makes.\n", 29 | "\n", 30 | "### Index\n", 31 | "\n", 32 | "* [Model outline](#model)\n", 33 | "* [Program outline](#program)\n", 34 | "* [Building a particle class](#particle_class)\n", 35 | " * [Making the particle move](#particle_move)\n", 36 | "* [Building a simulaiton class](#simulation_class)\n", 37 | " * [Recording simulation state](#record_state)\n", 38 | " * [Initial conditions](#initial_conditions)\n", 39 | "* [Boundary collisions](#boundary_collisions)\n", 40 | "* [Particle collisions](#particle_collisions)\n", 41 | " * [2D collisions](#2D_collisions)\n", 42 | " * [Sticky particles](#sticky)\n", 43 | " * [Different particle masses](#different_masses)\n", 44 | "* [Density](#density)\n", 45 | "* [Saving simulations](#saving)\n", 46 | "* [Analysisng physical properties](#report)\n", 47 | "* [Extensions](#extensions)\n", 48 | "* [Bibliography](#bibliography)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "## The model \n", 56 | "\n", 57 | "We are aiming to create a program to describe the motion of an ensemble of classical particles in a 2 dimensional space. Unlike the ideal gas model, we will consider that the particles possess volume (area in 2D) and therefore collide with each other. Apart from these collisions we consider no other interaction between particles. Such a system is identical to that programmed by Nick Brooks as [demonstrations for the Chemical Equilibria course](http://www.imperial.ac.uk/chemistry-teaching/chemicalequilibria/).\n", 58 | "\n", 59 | "Such simulation is one of the simplest examples of a class of simulation methods called [molecular dynamics](https://en.wikipedia.org/wiki/Molecular_dynamics), describing the time evolution of an ensemble of particles according to classical dynamics. In spite of its simplicity, such hard disks model is able to give insight on important results from thermodynamics, reaction kinetics, the structure of dense fluids, or phase transition.\n", 60 | "\n", 61 | "The description of the dynamical state of the system involves the determination of the position $\\vec{r}_i(t)$ and momentum $\\vec{p}_i(t)$ (or velocity) of each particle of the system at each moment of time. For systems of N particles (where N is usually fairly large) it is in general not possible to write a closed form mathematical expression for how these quantities vary in time. The solution to this problem is to use numerical methods by which, starting from a given initial state, the system is propagated forward in time by a small discrete amount $\\Delta t$ (called a time step), and new the position and momentum are calculated for every particle for time $t+\\Delta t$. By repeating this process over and over again, it is possible to obtain a \"picture\" of the system at small intervals of time and thus monitor its evolution.\n", 62 | "\n", 63 | "Our simulation will thus be:\n", 64 | "\n", 65 | "* Create an ensemble of particles with positions in a bound region of space with size L×L, each with a well defined momentum and mass.\n", 66 | "* Sequentially updating the position and momenta of these particles in small time steps, keeping a registry of the configurations taken by the system over time." 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "## Outline of the program design \n", 74 | "\n", 75 | "Our task is thus to write a program to implement the simulation of the system outlined above. The object-oriented approach is actually quite helpful in these early stages of the design in deciding how to structure our program. This exercise is usually better done with pen and paper with a few schematics than in front of a computer keyboard.\n", 76 | "\n", 77 | "In the most simple terms, our simulation can be viewed as an ensemble of particles that need to be propagated in time. We can thus think of two distinct classes of objects:\n", 78 | "\n", 79 | "* Particle class, denoting the state of an individual particle at a given moment in time. This should be a fairly simple class.\n", 80 | "* Simulation class, responsible for the time propagation of an ensemble of Particle objects. This is where most of the work will be done.\n", 81 | "\n", 82 | "The time evolution of the particles should be a relatively simple affair since particles don't interact (except for collisions), each particle should follow uniform linear motion (constant velocity) until an elastic collision occurs with another particle or the container walls, in which case we need to work out how to update the momenta.\n", 83 | "\n", 84 | "Note that this 2 class design is not unique. There could for example be several advantages in creating a third class representing the state of the box as a collection of particles at each time step. A Simulation object would then propagate these box objects in time. We will however stick to two class design.\n", 85 | "\n", 86 | "We shall slowly build all the functionality of the simulation.\n", 87 | "\n", 88 | "* First we will crate a Particle class with the particle attributes.\n", 89 | "* We will then see how to propagate this particle in time in the absence of collisions.\n", 90 | "* The create a Simulation class to propagate an ensemble of non-colliding particles.\n", 91 | "* The most tricky bit of the simulation will be to implement the behaviour when collisions occur. First, collisions with the container walls (easier), then between particles (less simple).\n", 92 | "* After having a working simulation, we will look at some properties of the systems we are simulating." 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "## Some preliminaries\n", 100 | "\n", 101 | "Rather than working directly in the notebook we will build our classes inside a file: gas_2d.py, and then visualise and test the code inside this notebook. Both of these files can be opened in JupyterLab.\n", 102 | "\n", 103 | "Inside gas_2d.py you will find three class definitions, Vector, Particle and Simulation. \n", 104 | "\n", 105 | "We will be making heavy use of the Vector class, which is already filled out and is much the same as the Vector class we developed in the last workshop with a few additional methods that allow us to add and subtract vectors and multiply and divide vectors by numbers using the usual +-/\\* operators. Particle and Simulation are just the outlines of the class. You will need to replace `pass` with code as you as you progress through the project.\n", 106 | "\n", 107 | "First we import our 3 classes." 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "from gas_2d import Vector, Particle, Simulation" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "And because we will be modifying these classes, we setup the notebook to reload the gas_2d.py file automatically." 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "%load_ext autoreload\n", 133 | "%autoreload 1\n", 134 | "%aimport gas_2d" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "We now import some auxiliar functions which will help to visualise the simulation that you will construct." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "%matplotlib qt\n", 151 | "from aux_functions import animate_simulation, display_particle, display_particle_motion, display_vecs" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "## Building a Particle Class " 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "The first class we need to fill out is the Particle class which will unsurprisingly represent... *drum roll* ...particles. Once we can create particle objects we can start building our Simulation class which will represent the total simulation and will include multiple Particle objects.\n", 166 | "\n", 167 | "For our purposes the properties that define a single particle will be position and momentum (both of them are vectors), as well as mass and radius (both of them are scalars). In the skeleton of the class below replace *pass* with code so that the particle class has position/momentum/radius attributes.\n", 168 | "\n", 169 | "We will want a method *velocity* which will return the velocity of the particle (recall that our expanded Vector class allows us to divide vectors by scalars). We will also want a method *copy* which will return an identical copy of the Particle shown below is a skeleton of the class we want to construct. Edit gas_2d.py by filling out the particle class.\n", 170 | "\n", 171 | " class Particle():\n", 172 | " def __init__(self, position, momentum, radius, mass):\n", 173 | " pass\n", 174 | "\n", 175 | " def velocity(self):\n", 176 | " pass\n", 177 | " \n", 178 | " def copy(self):\n", 179 | " pass" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "In the cell below use your particle class to create a particle object and then use the display_particle() auxiliar function on it to display it.\n", 187 | "\n", 188 | "In order to initialise your particle you'll need to create two vectors one for position and one for momentum and provide your particle with a radius and a mass." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "A plot showing your particle should have popped up. Bask in the glory of the particle you have created." 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "## Making our particle move " 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "Ok now we can create individual particles but they aren't doing too much. Let's try generate a trajectory. As our particle is a free particle experiencing no forces the momentum will not change with time. The position however will change. In a small amount of time $\\Delta t$ the position will change via:\n", 217 | "\n", 218 | "$$\\vec{r}(t+\\Delta t)=\\vec{r}(t)+\\vec{v}(t)\\times \\Delta t$$\n", 219 | "\n", 220 | "where $\\vec{r}(t)$ is the position at time $t$, $\\vec{v}(t)$ is velocity and $\\Delta t$ is the time step.\n", 221 | "\n", 222 | "The trajectory of the particle, understood in this context as a sequence of particle positions recorded in consecutive time steps, is obtained by running our simulation for a certain number of time steps. During each step we will use the velocity to update the position and then record the particle using the list.\n", 223 | "\n", 224 | "Fill out the cell so that during each iteration the particle's position is updated, you'll also want to append a copy of the particle object to the trajectory list so that we have a record of the particle at every time step." 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "no_steps = 1000\n", 234 | "dt = 0.01\n", 235 | "\n", 236 | "trajectory = []\n", 237 | "for step in range(no_steps):\n", 238 | " pass" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "Use the display_particle_motion() function on the trajectory list to view the motion of your particle" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": null, 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [ 254 | "display_particle_motion(trajectory)" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "## Building a Simulation class " 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "Great! We're going to take a step sideways now and introduce our main simulation class. The code you wrote above calculated the position of a single particle for multiple time steps.\n", 269 | "\n", 270 | "This time we're going to have many particles but we're only going to calculate their position for a single time step. Below is a skeleton for our simulation class, it will be initialised with a list of particles, a number box_length and another number dt. It includes a method *step* which is where the action takes place. The step method should loop over every particle and update the particle's position using it's momentum and the time step dt.\n", 271 | "\n", 272 | " class Simulation():\n", 273 | " def __init__(self, particles, box_length, dt):\n", 274 | " pass\n", 275 | "\n", 276 | " def step(self):\n", 277 | " pass" 278 | ] 279 | }, 280 | { 281 | "cell_type": "markdown", 282 | "metadata": {}, 283 | "source": [ 284 | "Test your simulation class using the code below (we're printing out the y coordinate of the first particle at the start of the simulation and after we have updated the positions). Do you understand what's being printed out?" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "particles = [Particle(Vector(0.5,0),Vector(0,1),1,1),\n", 294 | " Particle(Vector(0.5,1),Vector(0,-1),1,1),\n", 295 | " Particle(Vector(0,0), Vector(0.5,0.5),1,1)]\n", 296 | "\n", 297 | "s=Simulation(particles, 100,1)\n", 298 | "\n", 299 | "print(s.particles[0].position.y)\n", 300 | "s.step()\n", 301 | "print(s.particles[0].position.y)\n", 302 | "s.step()\n", 303 | "print(s.particles[0].position.y)" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "metadata": {}, 309 | "source": [ 310 | "## Recording the state of the simulation " 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "metadata": {}, 316 | "source": [ 317 | "We also want to record the state of the particles so we can have a look at the trajectories our particles have taken during the simulation and process our data.\n", 318 | "\n", 319 | "The code below shows another simulation class skeleton, which has added a record_state method. This method should loop over each particle and append a copy of the particle to the state list. Once done it should append the state list to the trajectory attribute.\n", 320 | "\n", 321 | "You will want to call the record\\_state method in the initialization method such that the initial state of the system is the first element of the trajectory list. More importantly, you should call record\\_state at the end of step method so that every time we update the particles we record them. Update your Simulation class to include the record\\_state method and call it from your \\_\\_init\\_\\_ and step methods.\n", 322 | "\n", 323 | " class Simulation():\n", 324 | " def __init__(self, particles, box_length, dt):\n", 325 | " self.trajectory = []\n", 326 | "\n", 327 | " def step(self):\n", 328 | " pass\n", 329 | "\n", 330 | " def record_state(self):\n", 331 | " state = []" 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "metadata": {}, 337 | "source": [ 338 | "Test your code using the cell below:" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": null, 344 | "metadata": {}, 345 | "outputs": [], 346 | "source": [ 347 | "particles = [Particle(Vector(10,0),Vector(0,0.75),1,1),\n", 348 | " Particle(Vector(50,10),Vector(0,0.5),1,1),\n", 349 | " Particle(Vector(0,0), Vector(0.5,0.75),1,1)]\n", 350 | "\n", 351 | "no_steps = 300\n", 352 | "\n", 353 | "s=Simulation(particles, 100, 0.5)\n", 354 | "for i in range(no_steps):\n", 355 | " s.step()\n", 356 | "\n", 357 | "animate_simulation(s)" 358 | ] 359 | }, 360 | { 361 | "cell_type": "markdown", 362 | "metadata": {}, 363 | "source": [ 364 | "## Generating initial conditions " 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "metadata": {}, 370 | "source": [ 371 | "The outcome of each simulation will depend strongly on the initial state of the system. Note that each Simulation object is created with initial conditions specified by a list of particle objects.\n", 372 | "\n", 373 | "If we were to create a set of initial conditions with 100 particles, with radius and mass equal to 1, all equally spaced just above the bottom of the box, and with momentum with components along x and y equal to 0.5, we could write\n", 374 | "\n", 375 | " box_length=500\n", 376 | " N=100\n", 377 | " init_ordered=[]\n", 378 | " for p in range(1,N+1):\n", 379 | " pos=Vector(p*(N-1)/box_length,2)\n", 380 | " mom=Vector(0.5,0.5)\n", 381 | " init_ordered.append(Particle(pos,mom,1,1))" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": null, 387 | "metadata": {}, 388 | "outputs": [], 389 | "source": [] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "metadata": {}, 394 | "source": [ 395 | "Create a simulation object with the init_ordered list, make a few simulation steps, and look at your results with animate_simulation()." 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": null, 401 | "metadata": {}, 402 | "outputs": [], 403 | "source": [] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "metadata": {}, 408 | "source": [ 409 | "A useful set of initial conditions will be one where particles have a [random uniform distribution](https://mathworld.wolfram.com/UniformDistribution.html) in the box and uniform random components of momentum in an interval \\[-pmax,pmax\\]. The fuction [uniform()](https://numpy.org/doc/stable/reference/random/generated/numpy.random.uniform.html) from numpy.random submodule can be used in this context.\n", 410 | "\n", 411 | " from numpy.random import uniform\n", 412 | " \n", 413 | " box_length=500\n", 414 | " N=100\n", 415 | " radius=1\n", 416 | " p_max=2\n", 417 | " init_random=[]\n", 418 | " for p in range(1,N+1):\n", 419 | " pos_x=uniform(radius,box_length-radius)\n", 420 | " pos_y=uniform(radius,box_length-radius)\n", 421 | " pos=Vector(pos_x,pos_y)\n", 422 | " mom_x=uniform(-p_max,p_max)\n", 423 | " mom_y=uniform(-p_max,p_max)\n", 424 | " mom=Vector(mom_x,mom_y)\n", 425 | " init_random.append(Particle(pos,mom,radius,1))" 426 | ] 427 | }, 428 | { 429 | "cell_type": "code", 430 | "execution_count": null, 431 | "metadata": {}, 432 | "outputs": [], 433 | "source": [] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "metadata": {}, 438 | "source": [ 439 | "Run a few time steps of a simulation initialized with init_random, and see the result of your simulation." 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": null, 445 | "metadata": {}, 446 | "outputs": [], 447 | "source": [] 448 | }, 449 | { 450 | "cell_type": "markdown", 451 | "metadata": {}, 452 | "source": [ 453 | "You may find yourself using a uniformly distributed random set of initial conditions often, so you may consider to define a function to generate these." 454 | ] 455 | }, 456 | { 457 | "cell_type": "markdown", 458 | "metadata": {}, 459 | "source": [ 460 | "# Collisions" 461 | ] 462 | }, 463 | { 464 | "cell_type": "markdown", 465 | "metadata": {}, 466 | "source": [ 467 | "We're making great progress but our simulation faces two problems:\n", 468 | "\n", 469 | " * Our particles pass through the boundaries of our box\n", 470 | " * Our particles pass through each other.\n", 471 | " \n", 472 | "Let's focus on the boundaries problem to begin with. First execute the simple test we have below involving particles going through the boundary. We'll use this to test our code is behaving the way we want." 473 | ] 474 | }, 475 | { 476 | "cell_type": "markdown", 477 | "metadata": {}, 478 | "source": [ 479 | "## Boundary Collision " 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": null, 485 | "metadata": {}, 486 | "outputs": [], 487 | "source": [ 488 | "def test_box():\n", 489 | " '''Test case for particle collisions with boundary walls.\n", 490 | " Generate 4 particles colliding with each wall and animate the result.\n", 491 | " '''\n", 492 | " box_length = 100\n", 493 | " no_steps = 300\n", 494 | " \n", 495 | " p1 = Particle(position=Vector(10,50), momentum=Vector(-1,0),radius=1,mass=1)\n", 496 | " p2 = Particle(position=Vector(50,90), momentum=Vector(0,1) ,radius=1,mass=1)\n", 497 | " p3 = Particle(position=Vector(50,10), momentum=Vector(0,-1),radius=1,mass=1)\n", 498 | " p4 = Particle(position=Vector(90,50), momentum=Vector(1,0) ,radius=1,mass=1)\n", 499 | "\n", 500 | " box_particles = [p1,p2,p3,p4]\n", 501 | "\n", 502 | " s = Simulation(box_particles, box_length=100, dt=0.1)\n", 503 | " for i in range(300):\n", 504 | " s.step()\n", 505 | " return animate_simulation(s,loop=True)\n", 506 | "\n", 507 | "test_box()" 508 | ] 509 | }, 510 | { 511 | "cell_type": "markdown", 512 | "metadata": {}, 513 | "source": [ 514 | "Ok, let's update our simulation class so that instead of passing through our box they bounce off the edges. Our simulation class will now make use of the previously unused attribute *box_length* and it will need an additional method *apply_box_collisions*.\n", 515 | "\n", 516 | "At the moment our particles' momenta remain unaltered no matter what happens. But we would like the momentum of a particle along the x-axis to reverse if it goes past the sides of our box, likewise we would like the momentum of a particle along the y-axis to reverse if it goes past the top or the bottom of our box.\n", 517 | "\n", 518 | "Try implementing an apply_box_collisions method as outlined in the skeleton below. This method will receive as its argument a particle and then it will check whether that particle is past the boundaries of the box and if it is change it's momentum accordingly.\n", 519 | "\n", 520 | "We'll then add in a call to apply_box_collisions in our step method that loops over all the particles.\n", 521 | "\n", 522 | " class Simulation():\n", 523 | " def __init__(self, particles, box_length, dt): \n", 524 | " pass\n", 525 | "\n", 526 | " def apply_box_collisions(self, particle):\n", 527 | " pass\n", 528 | "\n", 529 | " def step(self):\n", 530 | " pass\n", 531 | "\n", 532 | " def record_state(self):\n", 533 | " pass" 534 | ] 535 | }, 536 | { 537 | "cell_type": "markdown", 538 | "metadata": {}, 539 | "source": [ 540 | "Test your simulation class by executing the cell below" 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": null, 546 | "metadata": {}, 547 | "outputs": [], 548 | "source": [ 549 | "test_box()" 550 | ] 551 | }, 552 | { 553 | "cell_type": "markdown", 554 | "metadata": {}, 555 | "source": [ 556 | "## Particle Collisions " 557 | ] 558 | }, 559 | { 560 | "cell_type": "markdown", 561 | "metadata": {}, 562 | "source": [ 563 | "The final problem, and the most challenging is implementing inter-particle collisions.\n", 564 | "\n", 565 | "The following 2 test cases show us the problem which we will addressing." 566 | ] 567 | }, 568 | { 569 | "cell_type": "code", 570 | "execution_count": null, 571 | "metadata": {}, 572 | "outputs": [], 573 | "source": [ 574 | "def test_1d_collision():\n", 575 | " '''Test case for particle collisions along the coordinate axis.\n", 576 | " Animate 2 particles, one moving and one at rest, colliding along the x axis.\n", 577 | " '''\n", 578 | " box_length = 100\n", 579 | " no_steps = 300\n", 580 | "\n", 581 | " p1 = Particle(position = Vector(10,10), momentum=Vector(1,0),radius=1,mass=1)\n", 582 | " p2 = Particle(position = Vector(20,10), momentum=Vector(0,0) ,radius=1,mass=1)\n", 583 | " \n", 584 | " test_case = [p1,p2]\n", 585 | " s=Simulation(particles=test_case,box_length=100,dt=0.05)\n", 586 | "\n", 587 | " for i in range(500):\n", 588 | " s.step()\n", 589 | " return animate_simulation(s,loop=True)\n", 590 | "\n", 591 | "test_1d_collision()" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "def test_2d_collision():\n", 601 | " '''Test case for general particle collisions in 2D.\n", 602 | " Animate 2 particles, one moving and one at rest, colliding in a diagonal direction.\n", 603 | " '''\n", 604 | " box_length = 100\n", 605 | " no_steps = 300\n", 606 | "\n", 607 | " p1 = Particle(position = Vector(10,10), momentum=Vector(1,0.5),radius=1,mass=1)\n", 608 | " p2 = Particle(position = Vector(20,15), momentum=Vector(0,0) ,radius=1,mass=1)\n", 609 | " \n", 610 | " test_case = [p1,p2]\n", 611 | " s=Simulation(particles=test_case,box_length=100,dt=0.05)\n", 612 | "\n", 613 | " for i in range(500):\n", 614 | " s.step()\n", 615 | " return animate_simulation(s,loop=True)\n", 616 | "\n", 617 | "test_2d_collision()" 618 | ] 619 | }, 620 | { 621 | "cell_type": "markdown", 622 | "metadata": {}, 623 | "source": [ 624 | "### Enhancing the Particle class first" 625 | ] 626 | }, 627 | { 628 | "cell_type": "markdown", 629 | "metadata": {}, 630 | "source": [ 631 | "One thing that will make our life easier is if we add another method to our particle class. In order to check whether two particles are going to collide we need to check whether they overlap. So we'll want to add an overlap method to our Particle class. This method will receive as it's second argument another particle (as always the first argument a method receives is always self). Our method will then return True if the distance between this other particle and itself is less than the sum of the two particle's radii otherwise it will return False. Extend the class in gas_2d.py in accordance with the class skeleton below:\n", 632 | "\n", 633 | " class Particle():\n", 634 | " def __init__(self, position, momentum, radius, mass):\n", 635 | " pass\n", 636 | "\n", 637 | " def velocity(self):\n", 638 | " pass\n", 639 | " \n", 640 | " def copy(self):\n", 641 | " pass\n", 642 | " \n", 643 | " def overlap(self, other_particle):\n", 644 | " pass" 645 | ] 646 | }, 647 | { 648 | "cell_type": "markdown", 649 | "metadata": {}, 650 | "source": [ 651 | "In the cell below create some particle objects and see if your overlap method behaves the way you expect it to." 652 | ] 653 | }, 654 | { 655 | "cell_type": "code", 656 | "execution_count": null, 657 | "metadata": {}, 658 | "outputs": [], 659 | "source": [] 660 | }, 661 | { 662 | "cell_type": "markdown", 663 | "metadata": {}, 664 | "source": [ 665 | "### 1D collisions" 666 | ] 667 | }, 668 | { 669 | "cell_type": "markdown", 670 | "metadata": { 671 | "collapsed": true 672 | }, 673 | "source": [ 674 | "Now we can think about the collisions themselves. We're going to focus on perfectly elastic collisions and to start with we are going to make two additional simplifications: \n", 675 | "\n", 676 | "* We're going to pretend for now that we're dealing with [1 dimensional particles](https://en.wikipedia.org/wiki/Elastic_collision#One-dimensional_Newtonian) that only move along the x-axis.\n", 677 | "* We're going to pretend that the masses of both particles are equal. \n", 678 | "\n", 679 | "Under these conditions the result of an elastic collision is that the momenta of the two particles simply swap over after the collision as illustrated from the following animation (by Simon Steinmann, on [wikimedia commons](https://commons.wikimedia.org/wiki/File:Elastischer_sto%C3%9F.gif)):\n", 680 | "\n", 681 | "\n", 682 | "\n", 683 | "I.e.: \n", 684 | "$$\\vec{p}_1: m\\vec{v}_1 \\rightarrow m\\vec{v}_2$$ \n", 685 | "$$\\vec{p}_2: m\\vec{v}_2 \\rightarrow m\\vec{v}_1$$ " 686 | ] 687 | }, 688 | { 689 | "cell_type": "markdown", 690 | "metadata": {}, 691 | "source": [ 692 | "Our particles are 2D as they have both x and y components for position and momentum, but if we make sure all the particles we're dealing with have the same y position and have 0 y momenta then they are effectively behaving like 1D particles.\n", 693 | "\n", 694 | "Add a method *apply_particle_collision* to your simulation class that receives as its arguments two particles and if they overlap alters their momentum along the x-axis in line with an elastic collision.\n", 695 | "\n", 696 | "\n", 697 | " class Simulation():\n", 698 | " def __init__(self, particles, dt, box_length):\n", 699 | " pass\n", 700 | "\n", 701 | " def apply_particle_collision(self, particle1, particle2):\n", 702 | " pass\n", 703 | "\n", 704 | " def apply_box_collisions(self, particle):\n", 705 | " pass\n", 706 | "\n", 707 | " def step(self):\n", 708 | " pass\n", 709 | "\n", 710 | " def record_state(self):\n", 711 | " pass" 712 | ] 713 | }, 714 | { 715 | "cell_type": "markdown", 716 | "metadata": {}, 717 | "source": [ 718 | "In the cell below create a few particle objects and check that your method behaves the way you expect it to." 719 | ] 720 | }, 721 | { 722 | "cell_type": "code", 723 | "execution_count": null, 724 | "metadata": {}, 725 | "outputs": [], 726 | "source": [] 727 | }, 728 | { 729 | "cell_type": "markdown", 730 | "metadata": {}, 731 | "source": [ 732 | "Now we want our step method to apply particle collisions. But to do that we need to check (loop over) all pairs of particles. Note that we want to avoid colliding particle i with particle j and then also separately colliding particle j with particle i (if we collide the same particles twice we will end up swapping their momenta twice, hence undoing the collision).\n", 733 | "\n", 734 | "There are several different ways of looping over pairs, but perhaps the easiest is to use the function `combinations()` from the itertools library. See example below:" 735 | ] 736 | }, 737 | { 738 | "cell_type": "code", 739 | "execution_count": null, 740 | "metadata": {}, 741 | "outputs": [], 742 | "source": [ 743 | "from itertools import combinations\n", 744 | "\n", 745 | "example_list = [1,2,3,4,5]\n", 746 | "for p1,p2 in combinations(example_list,2):\n", 747 | " print(p1,p2)" 748 | ] 749 | }, 750 | { 751 | "cell_type": "markdown", 752 | "metadata": {}, 753 | "source": [ 754 | "Amend your step method so that it calls apply_particle_collision on all pairs of particles (you will need to import combinations at the top of your gas_2d.py file) and test your solution below:" 755 | ] 756 | }, 757 | { 758 | "cell_type": "code", 759 | "execution_count": null, 760 | "metadata": {}, 761 | "outputs": [], 762 | "source": [ 763 | "test_1d_collision()" 764 | ] 765 | }, 766 | { 767 | "cell_type": "markdown", 768 | "metadata": {}, 769 | "source": [ 770 | "Before tackling 2D collisions, let us take a breath, for our enjoyment generate some particles and create a simulation object *sim_1d* with 200 time steps." 771 | ] 772 | }, 773 | { 774 | "cell_type": "code", 775 | "execution_count": null, 776 | "metadata": {}, 777 | "outputs": [], 778 | "source": [ 779 | "particles = []\n", 780 | "\n", 781 | "sim_1d = None\n", 782 | "for step in range(200):\n", 783 | " pass" 784 | ] 785 | }, 786 | { 787 | "cell_type": "markdown", 788 | "metadata": {}, 789 | "source": [ 790 | "Animate your simulation in the cell below (your code can now simulate a 1D gas!):" 791 | ] 792 | }, 793 | { 794 | "cell_type": "code", 795 | "execution_count": null, 796 | "metadata": {}, 797 | "outputs": [], 798 | "source": [] 799 | }, 800 | { 801 | "cell_type": "markdown", 802 | "metadata": {}, 803 | "source": [ 804 | "## 2D collisions " 805 | ] 806 | }, 807 | { 808 | "cell_type": "markdown", 809 | "metadata": {}, 810 | "source": [ 811 | "Ok back to collisions! Currently we represent our momentum in terms of two directions x and y. What we're going to do for our 2D case is to work out a different representation of the momentum in terms of two different directions. We will choose these directions in such a way that the collision will leave the second component completely unchanged, this essentially turns the 2D problem into the 1D problem we already know how to solve.\n", 812 | "\n", 813 | "What are these two directions? The first is the inter-particle direction at the moment the collision take place. We'll call this the *collision axis*. The other will be at 90 degrees to the collision axis and we'll call the *orthogonal axis*.\n", 814 | "\n", 815 | "What we expect in our simplified case then is that following collision the components of the momentum of the particles along the collision axis will swap over whilst components along the orthogonal axes will remain unchanged.\n", 816 | "\n", 817 | "To make this a bit clearer let's look at the following animation (by Simon Steinmann, from [wikimedia commons](https://commons.wikimedia.org/wiki/File:Elastischer_sto%C3%9F_2D.gif)):\n", 818 | "\n", 819 | "\n", 820 | "\n", 821 | "What you are seeing is one particle in motion and another at rest. At the point where the two particles collide, the animation displays the collision and orthogonal axes as perpendicular black lines. \n", 822 | "\n", 823 | "The momentum of the moving particle (displayed as a red arrow) is expressed as components along these two axes (these are displayed as two blue arrows).\n", 824 | "\n", 825 | "The stationary particle initially has no momentum, which means that both its collision and orthogonal component are initially zero.\n", 826 | "\n", 827 | "During the collision the two particles swap momenta along the collision axis.\n", 828 | "\n", 829 | "In the case illustrated, for the initially moving particle, the collision component of the momentum becomes zero, leaving only its initial orthogonal component contributing to the final momentum. \n", 830 | "\n", 831 | "For the initially stationary particle, the initial orthogonal component is zero so the final momentum is equal to the collision component of initially moving particle prior to collision.\n", 832 | "\n", 833 | "You may need to read this section a few times and stare at the animation for it to make sense. If after doing so it is still unclear please ask a demonstrator!" 834 | ] 835 | }, 836 | { 837 | "cell_type": "markdown", 838 | "metadata": {}, 839 | "source": [ 840 | "### Projecting the momentum along the collision and orthogonal axes" 841 | ] 842 | }, 843 | { 844 | "cell_type": "markdown", 845 | "metadata": {}, 846 | "source": [ 847 | "Luckily our vector class includes all the necessary methods to calculate the collision and orthogonal axes and express the momentum in terms of components along them." 848 | ] 849 | }, 850 | { 851 | "cell_type": "markdown", 852 | "metadata": {}, 853 | "source": [ 854 | "To workout the component of a vector in the direction of a particular axis, we need only calculate the dot product of the vector with that particular axis (a scalar), multiplied by the axis vector itself. Shown below is a figure (by user Acdx, from [wikimedia commons](https://upload.wikimedia.org/wikipedia/commons/f/fd/3D_Vector.svg)) for a 3 dimensional vector **a**, axis vectors **i**,**j**, and **k** and vector components **ax**, **ay** and **az**.\n", 855 | "\n", 856 | "\n", 857 | "\n", 858 | "E.g. to calculate the component of a particular vector *vec* along the x-axis we would compute:\n", 859 | " \n", 860 | " vec = Vector(3,4)\n", 861 | "\n", 862 | " xaxis = Vector(1,0)\n", 863 | " vec_proj_x = xaxis * vec.dot(xaxis)\n", 864 | " vec_proj_x" 865 | ] 866 | }, 867 | { 868 | "cell_type": "code", 869 | "execution_count": null, 870 | "metadata": {}, 871 | "outputs": [], 872 | "source": [] 873 | }, 874 | { 875 | "cell_type": "markdown", 876 | "metadata": {}, 877 | "source": [ 878 | "Likewise to calculate the component of a vector *vec* along the y-axis we would compute:\n", 879 | " \n", 880 | " yaxis = Vector(0,1)\n", 881 | " vec_proj_y = yaxis * vec.dot(yaxis)\n", 882 | " vec_proj_y" 883 | ] 884 | }, 885 | { 886 | "cell_type": "code", 887 | "execution_count": null, 888 | "metadata": {}, 889 | "outputs": [], 890 | "source": [] 891 | }, 892 | { 893 | "cell_type": "markdown", 894 | "metadata": {}, 895 | "source": [ 896 | "Of course for x and y we have an easier option for working out the magnitude of along the x and y axes: we can simply use the .x and .y attributes. However this only works for x and y, whereas our general dot product approach works for any axes with might like to consider.\n", 897 | "\n", 898 | " new_axis = Vector(0.6,0.8)\n", 899 | " vec_proj_new_axis = new_axis * vec.dot(new_axis)\n", 900 | " vec_proj_new_axis" 901 | ] 902 | }, 903 | { 904 | "cell_type": "code", 905 | "execution_count": null, 906 | "metadata": {}, 907 | "outputs": [], 908 | "source": [] 909 | }, 910 | { 911 | "cell_type": "markdown", 912 | "metadata": {}, 913 | "source": [ 914 | "An important point to bear in mind is that an axis vector **must always have a length of 1**. If we have a vector whose direction we would like to use as an axis, we must first normalise it such that it's length is 1 leaving its direction unchanged. To do this we divide the vector by its own length:\n", 915 | "\n", 916 | " normalised_vec = vec/vec.norm()\n", 917 | " print(vec)\n", 918 | " print(normalised_vec)" 919 | ] 920 | }, 921 | { 922 | "cell_type": "code", 923 | "execution_count": null, 924 | "metadata": {}, 925 | "outputs": [], 926 | "source": [] 927 | }, 928 | { 929 | "cell_type": "markdown", 930 | "metadata": {}, 931 | "source": [ 932 | "Having chosen one axis, we can work out the component in the direction perpendicular to it by subtracting the component along our chosen axis from our initial vector.\n", 933 | "\n", 934 | " new_xaxis = Vector(0.6,0.8)\n", 935 | " vec_proj_newx = new_xaxis * vec.dot(new_xaxis)\n", 936 | " vec_proj_newy = vec - vec_proj_newx" 937 | ] 938 | }, 939 | { 940 | "cell_type": "code", 941 | "execution_count": null, 942 | "metadata": {}, 943 | "outputs": [], 944 | "source": [] 945 | }, 946 | { 947 | "cell_type": "markdown", 948 | "metadata": {}, 949 | "source": [ 950 | "We now have assembled all the pieces we need to update our apply_particle_collision method for 2D collisions.\n", 951 | "\n", 952 | "For colliding particles you will want to: \n", 953 | "\n", 954 | "* compute the collision axis\n", 955 | "* compute the projection of the momentum onto the collision and orthogonal axes\n", 956 | "* update the collision projected momentum\n", 957 | "* compute the new total momentum\n", 958 | "\n", 959 | "Have a go updating your simulation class then execute the cell below to test your code." 960 | ] 961 | }, 962 | { 963 | "cell_type": "code", 964 | "execution_count": null, 965 | "metadata": {}, 966 | "outputs": [], 967 | "source": [ 968 | "test_2d_collision()" 969 | ] 970 | }, 971 | { 972 | "cell_type": "markdown", 973 | "metadata": {}, 974 | "source": [ 975 | "Fantastic, we can simulate 2D collisions!" 976 | ] 977 | }, 978 | { 979 | "cell_type": "markdown", 980 | "metadata": {}, 981 | "source": [ 982 | "## Fixing a bug " 983 | ] 984 | }, 985 | { 986 | "cell_type": "markdown", 987 | "metadata": {}, 988 | "source": [ 989 | "We've tested our code in the most obvious way but there is a final test case we need to take a look at.\n", 990 | "\n", 991 | "Execute the cell below and watch the animation of the simulation. What do you think is going on?" 992 | ] 993 | }, 994 | { 995 | "cell_type": "code", 996 | "execution_count": null, 997 | "metadata": {}, 998 | "outputs": [], 999 | "source": [ 1000 | "def test_collision_bug():\n", 1001 | " '''Test case for sticking particles bug. Animate the collision of 5 particles\n", 1002 | " that could result in particles trapping each other.\n", 1003 | " '''\n", 1004 | " box_length = 100\n", 1005 | " no_steps = 300\n", 1006 | "\n", 1007 | " p1 = Particle(position = Vector(45,50), momentum=Vector(1,1),radius=3,mass=1)\n", 1008 | " p2 = Particle(position = Vector(55,60), momentum=Vector(0,0) ,radius=3,mass=1)\n", 1009 | " p3 = Particle(position = Vector(35,39.75), momentum=Vector(2,2),radius=3,mass=1)\n", 1010 | " p4 = Particle(position = Vector(65,70.25), momentum=Vector(-1,-1),radius=3,mass=1)\n", 1011 | " p5 = Particle(position = Vector(54,30), momentum=Vector(0.5,2.5),radius=3,mass=1)\n", 1012 | " \n", 1013 | " test_case = [p1,p2,p3,p4,p5]\n", 1014 | " \n", 1015 | " s=Simulation(particles=test_case,box_length=100,dt=0.25)\n", 1016 | "\n", 1017 | " for i in range(500):\n", 1018 | " s.step()\n", 1019 | " return animate_simulation(s,loop=True, interval=85)\n", 1020 | "\n", 1021 | "test_collision_bug()" 1022 | ] 1023 | }, 1024 | { 1025 | "cell_type": "markdown", 1026 | "metadata": {}, 1027 | "source": [ 1028 | "Our particles are binding even though we haven't implemented attractive interactions?! What the juice?\n", 1029 | "\n", 1030 | "Think about what would happen if you initialised your particles so that they started off moving slowly but already overlapping with one another? What would happen to their momenta on every step?\n", 1031 | "\n", 1032 | "To solve this problem we need to check whether or not two particles are actually moving towards one another before deciding to collide them. As you know how to determine the momentum components along the collision axis, you should be able to add a check to make sure they are moving towards each other prior to switching their momenta.\n", 1033 | "\n", 1034 | "The same problem can manifest along the boundaries (where particles can also get stuck) so you'll want to apply the same solution to your box_collisions method. Modify *apply_box_collisions* and *apply_particle_collisions* to solve the bug and test your solution below:" 1035 | ] 1036 | }, 1037 | { 1038 | "cell_type": "code", 1039 | "execution_count": null, 1040 | "metadata": {}, 1041 | "outputs": [], 1042 | "source": [ 1043 | "test_collision_bug()" 1044 | ] 1045 | }, 1046 | { 1047 | "cell_type": "markdown", 1048 | "metadata": {}, 1049 | "source": [ 1050 | "**Congratulations you've written a particle simulation!**\n", 1051 | "\n", 1052 | "Try out simulations with different initial conditions to see what happens. (Warning if you create a simulation with too many particles it will take a long time to run!)" 1053 | ] 1054 | }, 1055 | { 1056 | "cell_type": "code", 1057 | "execution_count": null, 1058 | "metadata": {}, 1059 | "outputs": [], 1060 | "source": [] 1061 | }, 1062 | { 1063 | "cell_type": "markdown", 1064 | "metadata": {}, 1065 | "source": [ 1066 | "## Allowing for particles with different masses " 1067 | ] 1068 | }, 1069 | { 1070 | "cell_type": "markdown", 1071 | "metadata": {}, 1072 | "source": [ 1073 | "This part of the project offers less guidance, so you're on your own from here!" 1074 | ] 1075 | }, 1076 | { 1077 | "cell_type": "markdown", 1078 | "metadata": {}, 1079 | "source": [ 1080 | "We have assumed so far that all particles have the same mass and hence that:\n", 1081 | "\n", 1082 | "$$\\vec{p'}_1 = \\vec{p}_2 $$\n", 1083 | "$$\\vec{p'}_2 = \\vec{p}_1 $$\n", 1084 | "\n", 1085 | "where the subscripts indicate the particle. $\\vec{p'}$ is the momentum after the collision and $\\vec{p}$ is the momentum before the collision.\n", 1086 | "\n", 1087 | "But there is no reason to make this assumption. The correct formula for a 1D collision in the case of different particle masses is:\n", 1088 | " \n", 1089 | "$$\\vec{p'}_1 = \\frac{(m_1-m_2)\\vec{p}_1+2m_1\\vec{p}_2}{m_1+m_2}$$\n", 1090 | "$$\\vec{p'}_2 = \\frac{(m_2-m_1)\\vec{p}_2+2m_2\\vec{p}_1}{m_1+m_2}$$\n", 1091 | "\n", 1092 | "Update your Simulation so that it can handle cases where particles have different masses and test it out on a suitable test case of your own devising." 1093 | ] 1094 | }, 1095 | { 1096 | "cell_type": "code", 1097 | "execution_count": null, 1098 | "metadata": {}, 1099 | "outputs": [], 1100 | "source": [] 1101 | }, 1102 | { 1103 | "cell_type": "markdown", 1104 | "metadata": {}, 1105 | "source": [ 1106 | "## A note on units and density of the system \n", 1107 | "\n", 1108 | "We have been a bit sloppy with the use of units while building our simulation, in fact we have not addressed this issue at all! When we write dt=0.25, do we mean that the time step is 0.25 femtoseconds, or 0.25 years? Same question arises about box size or particle mass. This is not an issue when it comes to the \"correctness\" of the result, or the qualitative behaviour of the simulated system, but it does become an issue when we want to compare quantities with experiments.\n", 1109 | "\n", 1110 | "We are free to choose our unit system, but when we want to simulate a specific physical system (for example, a container of Xe atoms at T=300K), we must have a particle_radius/box_length ratio that is consistent with the system we want to simulate, or an initial momentum that is consistent with the target temperature.\n", 1111 | "\n", 1112 | "Since we did not pay attention to our choice of parameters, we are not quite sure what physical system we have been simulating. In the analysis below, we will just focus on the qualitative behaviour of the system in an arbitrary system of units.\n", 1113 | "\n", 1114 | "One quantity that is very useful to characterise the system and remains meaningful in any system of units is the density, taken here as the fraction of the area occupied by the particles compared to the total area of the box.\n", 1115 | "\n", 1116 | "* Add a density attribute to your Simulation class and calculate its value in the \\_\\_init\\_\\_ method.\n", 1117 | "\n", 1118 | "Systems with low densities can be seen as gases while high densities will behave more like liquids. For higher densities still, when the particles start being packed in space, the system will have the properties of a solid." 1119 | ] 1120 | }, 1121 | { 1122 | "cell_type": "markdown", 1123 | "metadata": {}, 1124 | "source": [ 1125 | "## Generating a longer simulation" 1126 | ] 1127 | }, 1128 | { 1129 | "cell_type": "markdown", 1130 | "metadata": {}, 1131 | "source": [ 1132 | "Our goal is to generate a larger simulation to try to derive some statistical information of the system. You might have noticed from your trials however that these simulations are somewhat slow. Yes, Python does not generate the fastest programs.\n", 1133 | "\n", 1134 | "We have tried some black magic by coding special fast Vector and Particle classes. If they load, your simulations may accelerate significantly." 1135 | ] 1136 | }, 1137 | { 1138 | "cell_type": "code", 1139 | "execution_count": null, 1140 | "metadata": {}, 1141 | "outputs": [], 1142 | "source": [ 1143 | "import pyximport\n", 1144 | "pyximport.install()\n", 1145 | "\n", 1146 | "from fast_classes import Vector,Particle" 1147 | ] 1148 | }, 1149 | { 1150 | "cell_type": "markdown", 1151 | "metadata": {}, 1152 | "source": [ 1153 | "If your system does not have the necessary components configured and you get an error when importing the classes above, then you need to fall back to the Python classes you defined and wait a little longer for your simulations to run." 1154 | ] 1155 | }, 1156 | { 1157 | "cell_type": "markdown", 1158 | "metadata": {}, 1159 | "source": [ 1160 | "Now create a simulation of 200 particles in 100 length box, with 0.5 for the x/y components of the momenta then simulate 10,000 steps with dt=0.25 (it may take a while)." 1161 | ] 1162 | }, 1163 | { 1164 | "cell_type": "code", 1165 | "execution_count": null, 1166 | "metadata": {}, 1167 | "outputs": [], 1168 | "source": [] 1169 | }, 1170 | { 1171 | "cell_type": "markdown", 1172 | "metadata": {}, 1173 | "source": [ 1174 | "Animate the trajectory on the cell below" 1175 | ] 1176 | }, 1177 | { 1178 | "cell_type": "code", 1179 | "execution_count": null, 1180 | "metadata": {}, 1181 | "outputs": [], 1182 | "source": [] 1183 | }, 1184 | { 1185 | "cell_type": "markdown", 1186 | "metadata": {}, 1187 | "source": [ 1188 | "## Saving your simulation \n", 1189 | "\n", 1190 | "Although not affecting the physical behaviour of the simulated system, it is extremely useful to be able to save the results of our simulations. We will often want to save a simulation so we switch off the notebook but still be able to come back to either analyse or even continue the simulation at a later date.\n", 1191 | "\n", 1192 | "Python allows any object to be saved to disk using the Pickle library. Lets try it out. For this purpose we will create some particles and generate a simulation object *shortsim* with 200 steps" 1193 | ] 1194 | }, 1195 | { 1196 | "cell_type": "code", 1197 | "execution_count": null, 1198 | "metadata": {}, 1199 | "outputs": [], 1200 | "source": [ 1201 | "particles=[]\n", 1202 | "\n", 1203 | "shortsim=None\n", 1204 | "for step in range(200):\n", 1205 | " pass\n" 1206 | ] 1207 | }, 1208 | { 1209 | "cell_type": "markdown", 1210 | "metadata": {}, 1211 | "source": [ 1212 | "Now we'll use pickle to save it (note we will need to import the pickle module). This involves:\n", 1213 | "\n", 1214 | "* Opening a binary file for writing by using the open function with the 'wb' flag.\n", 1215 | "* Calling pickle's dump function to dump our simulation object into the file.\n", 1216 | "* (When using the with construction, the file is closed after writing)\n", 1217 | "\n", 1218 | "Below we pick the file name test_simulation.dat but in general you can choose whichever filename you wish." 1219 | ] 1220 | }, 1221 | { 1222 | "cell_type": "code", 1223 | "execution_count": null, 1224 | "metadata": {}, 1225 | "outputs": [], 1226 | "source": [ 1227 | "import pickle\n", 1228 | "\n", 1229 | "with open('test_simulation.dat','wb') as data_f:\n", 1230 | " pickle.dump(shortsim,data_f)" 1231 | ] 1232 | }, 1233 | { 1234 | "cell_type": "markdown", 1235 | "metadata": {}, 1236 | "source": [ 1237 | "To extract our simulation from the file we have created requires:\n", 1238 | " \n", 1239 | "* Opening a binary file for reading by using the open function with the 'rb' flag.\n", 1240 | "* Calling pickle's load function to extract our simulation object from the file.\n", 1241 | "\n", 1242 | "So to extract the simulation we just saved we would execute:" 1243 | ] 1244 | }, 1245 | { 1246 | "cell_type": "code", 1247 | "execution_count": null, 1248 | "metadata": {}, 1249 | "outputs": [], 1250 | "source": [ 1251 | "with open('test_simulation.dat','rb') as data_f:\n", 1252 | " loaded_sim = pickle.load(data_f)" 1253 | ] 1254 | }, 1255 | { 1256 | "cell_type": "markdown", 1257 | "metadata": {}, 1258 | "source": [ 1259 | "We can now add further steps to the simulation/and or run analysis on it." 1260 | ] 1261 | }, 1262 | { 1263 | "cell_type": "code", 1264 | "execution_count": null, 1265 | "metadata": {}, 1266 | "outputs": [], 1267 | "source": [ 1268 | "for step in range(200):\n", 1269 | " loaded_sim.step()\n", 1270 | " \n", 1271 | "animate_simulation(sim)" 1272 | ] 1273 | }, 1274 | { 1275 | "cell_type": "markdown", 1276 | "metadata": {}, 1277 | "source": [ 1278 | "An important thing to be aware of is that if you update or change your classes in any way (adding attributes or modifying methods), your pickled file will no longer be loadable because your class definition does not match the stored object." 1279 | ] 1280 | }, 1281 | { 1282 | "cell_type": "markdown", 1283 | "metadata": {}, 1284 | "source": [ 1285 | "# Looking at Physical Quantities - Writing your report " 1286 | ] 1287 | }, 1288 | { 1289 | "cell_type": "markdown", 1290 | "metadata": {}, 1291 | "source": [ 1292 | "Write a short report in the form of a Jupyter notebook where you analyse the results of your simulations. Below are some items that should guide your analysis.\n", 1293 | "\n", 1294 | "In your analysis you will probably be writing some functions. You could write these as methods on the simulation class, but it is probably better to write these in the report notebook as functions which receive a simulation object as an argument.\n", 1295 | "\n", 1296 | "Your report might be short, but it should include figures (which can be the result of a code cell, or should be embeded using the menu Edit > Insert Image) and comments on the results you obtain. It can contain short bits of code used for data analysis. Note that the report is written to be *read* by a person, not to be *run* by a machine." 1297 | ] 1298 | }, 1299 | { 1300 | "cell_type": "markdown", 1301 | "metadata": {}, 1302 | "source": [ 1303 | "### Energy as a function of time\n", 1304 | "Using your simulation trajectory, produce a plot of total (kinetic) energy versus time for the simulation." 1305 | ] 1306 | }, 1307 | { 1308 | "cell_type": "markdown", 1309 | "metadata": {}, 1310 | "source": [ 1311 | "### Distribution of speeds\n", 1312 | "\n", 1313 | "It is interesting to investigate how the energy is distributed between particles, and how this distribution evolves in time. Every recorded state defines a distribution of particle velocities and particle speeds. " 1314 | ] 1315 | }, 1316 | { 1317 | "cell_type": "markdown", 1318 | "metadata": {}, 1319 | "source": [ 1320 | "Below is a skeleton for a function *plot_velocity_x* that accepts as its argument an integer defining the particular time step we are interested in and will plot the histogram of the *x* component of velocities present at that time step. (You may need to run %matplotlib to get the interactive plot to work). Fill out this function and use it to investigate how the distribution of velocity components changes with time.\n", 1321 | "\n", 1322 | " from ipywidgets import interact\n", 1323 | " from matplotlib.pyplot import *\n", 1324 | "\n", 1325 | " @interact(t=(0,1000,1))\n", 1326 | " def plot_velocity_x(t):\n", 1327 | " pass\n", 1328 | " \n", 1329 | "To get a better distribution we need better statistics, so a simulation with a higher number of particles will work better. A simulation with a high rate of collisions (higher density) will also equilibrate quicker.\n", 1330 | "\n", 1331 | "Energy is more directly related to speed, and it is also revealing to study how the distribution of speeds changes with time.\n", 1332 | "\n", 1333 | "You may want to try different sets of initial conditions and see how the distributions change." 1334 | ] 1335 | }, 1336 | { 1337 | "cell_type": "markdown", 1338 | "metadata": {}, 1339 | "source": [ 1340 | "When the probability distribution of velocity components, i.e. the probability $\\rho_x$ of finding a particle with *x* component of velocity $v_x$ is Gaussian \n", 1341 | "\n", 1342 | "$$\\rho_x(v_x)=\\sqrt{\\frac{m}{2 \\pi k_B T}} \\exp\\left(-\\frac{m v_x^2}{2k_B T}\\right),$$\n", 1343 | "\n", 1344 | "where $m$ is the particle mass, $k_B$ is Boltzmann's constant and $T$ is the temperature; the distribution of speeds is the Maxwell-Boltzmann distribution which in 2D takes the from:\n", 1345 | " \n", 1346 | "$$\\rho(v)=\\frac{m v}{k_B T} \\exp\\left(-\\frac{m v^2}{2k_B T}\\right).$$ \n", 1347 | "\n", 1348 | "(The 2D Maxwell-Boltzmann distribution is also known as the Rayleigh distribution.)\n", 1349 | "\n", 1350 | "The relationship between the temperature and the average energy for a 2D ideal gas is given by the equipartition theorem as:\n", 1351 | "\n", 1352 | "$$\\langle E \\rangle= k_B T$$\n", 1353 | "\n", 1354 | "Workout the average energy per particle for your simulation and use this value along with the mass you gave your particles to overlay your distributions." 1355 | ] 1356 | }, 1357 | { 1358 | "cell_type": "markdown", 1359 | "metadata": {}, 1360 | "source": [ 1361 | "To make your analysis more quantitative, plot the average and standard deviation of speeds as a function of time.\n", 1362 | "\n", 1363 | "How do the equilibrium value of these quantities depend on the initial conditions?" 1364 | ] 1365 | }, 1366 | { 1367 | "cell_type": "markdown", 1368 | "metadata": {}, 1369 | "source": [ 1370 | "### Distribution of positions\n", 1371 | "\n", 1372 | "Produce a density profile of the system along the *x* direction. To do this choose a fraction of the trajectory where the system is in equilibrium and histogram the *x* position of the particles. Use a bin size smaller than the radius of your particles, and to improve statistics you should accumulate many time steps in a single histogram.\n", 1373 | "\n", 1374 | "Try also systems with a relative high density (for example 0.18 or more). Is the result you obtain what you expected?" 1375 | ] 1376 | }, 1377 | { 1378 | "cell_type": "markdown", 1379 | "metadata": {}, 1380 | "source": [ 1381 | "## Optional (hard!) " 1382 | ] 1383 | }, 1384 | { 1385 | "cell_type": "markdown", 1386 | "metadata": {}, 1387 | "source": [ 1388 | "Some of the tasks suggested here are harder and can take a significant time to complete. **You are not expected to complete any of them, and it is possible to obtain full marks without completing this part of the project**. These tasks are presented as suggestions of ways in which the simulations could be improved. Contact the demonstrators if you plan to implement any of these features." 1389 | ] 1390 | }, 1391 | { 1392 | "cell_type": "markdown", 1393 | "metadata": {}, 1394 | "source": [ 1395 | "### Expansion of the gas\n", 1396 | "\n", 1397 | "Build a simulation where all particles are randomly distributed but only on the left half of the box. The right half of the box is empty. Momenta of the particles can have a random uniform distribution.\n", 1398 | "\n", 1399 | "Run the simulation and observe the result.\n", 1400 | "\n", 1401 | "As a function of time, count the number of particles on each side of the box, and plot them on the same figure. It may also be interesting to plot the x component of the velocity. When does the system reach equilibrium? Does it change, if you change the maximum initial momentum?\n", 1402 | "\n", 1403 | "The initial distribution of speeds chosen does not actually correspond to an gas in equillibrium. Try to build a more realistic simulation of an expanding gas." 1404 | ] 1405 | }, 1406 | { 1407 | "cell_type": "markdown", 1408 | "metadata": {}, 1409 | "source": [ 1410 | "### Mixing" 1411 | ] 1412 | }, 1413 | { 1414 | "cell_type": "markdown", 1415 | "metadata": {}, 1416 | "source": [ 1417 | "You can use the program you developed to study diffusion and mixing. You can start a simulation with different particles on the left and right halves of the box (what distinguishes the particles could be a physical attribute such as mass, or a fictitious one such as colour). Your simulation could look [something like this](http://www.imperial.ac.uk/chemistry-teaching/chemicalequilibria/moleculesmixing2.html). Plot the number of particles of each type on each side of the box as a function of time.\n", 1418 | "\n", 1419 | "What factors affect the speed of mixing?" 1420 | ] 1421 | }, 1422 | { 1423 | "cell_type": "markdown", 1424 | "metadata": {}, 1425 | "source": [ 1426 | "### Pressure on the container walls" 1427 | ] 1428 | }, 1429 | { 1430 | "cell_type": "markdown", 1431 | "metadata": {}, 1432 | "source": [ 1433 | "Can you modify the boundary code so that you can record the pressure being exerted on the box during each time step and hence produce a plot of pressure vs. time? Does your simulation agree with the ideal gas equations?\n", 1434 | "\n", 1435 | "You can plot the pressure for changing system density. You may be able to observe a phase transition." 1436 | ] 1437 | }, 1438 | { 1439 | "cell_type": "markdown", 1440 | "metadata": {}, 1441 | "source": [ 1442 | "### Heat transfer across the boundary" 1443 | ] 1444 | }, 1445 | { 1446 | "cell_type": "markdown", 1447 | "metadata": {}, 1448 | "source": [ 1449 | "At the moment the system we are simulating is isolated: there are no exchanges of mass or energy with the surroundings. We could however allow for the transfer of heat at the boundary, in which case collisions with the walls would no longer be elastic. What would happen if you had walls that were warmer/cooler than the gas?\n", 1450 | "\n", 1451 | "Implement the heat change with the box walls and look how the total energy of the gas changes with time." 1452 | ] 1453 | }, 1454 | { 1455 | "cell_type": "markdown", 1456 | "metadata": {}, 1457 | "source": [ 1458 | "### Simulating properties of the bulk" 1459 | ] 1460 | }, 1461 | { 1462 | "cell_type": "markdown", 1463 | "metadata": {}, 1464 | "source": [ 1465 | "#### Periodic boundary conditions\n", 1466 | "The systems we have been simulating have a very large surface area to volume ratio (or in this case, large perimeter to area ratio). Contrast the particle radius and the box length you have been using, with the radius of a molecule compared with a 1 litre container. Our simulations thus greatly overestimate boundary effects as we saw when plotting the density profile of the system, and are therefore unsuitable to study bulk properties.\n", 1467 | "\n", 1468 | "A very common technique to study bulk properties is to use [periodic boundary conditions](https://en.wikipedia.org/wiki/Periodic_boundary_conditions). In this case the walls are completely suppressed and a particle crossing the boundary on one side of the box is placed on the opposing side of the box, keeping the same momentum. Thus simulating an \"infinite\" box. Special care must be taken measuring distances between particles and checking for collisions across the box boundary.\n", 1469 | "\n", 1470 | "Implement a simulation class with periodic boundary conditions.\n", 1471 | "\n", 1472 | "#### Radial distribution function\n", 1473 | "One quantity often used to describe the structure of fluids is the [radial distribution function *g(r)*](https://en.wikibooks.org/wiki/Molecular_Simulation/Radial_Distribution_Functions). This quantity measures the probability of 2 particles being at a distance *r* from each other. It involves counding how many particles are at a distance *r* from a reference particle, but also accounting for the fact that the number of particles within a circle of radius *r* also increase with *r*.\n", 1474 | "\n", 1475 | "Make a plot of the *g(r)* for one of your simulations." 1476 | ] 1477 | }, 1478 | { 1479 | "cell_type": "markdown", 1480 | "metadata": {}, 1481 | "source": [ 1482 | "### Speeding up the simulations" 1483 | ] 1484 | }, 1485 | { 1486 | "cell_type": "markdown", 1487 | "metadata": {}, 1488 | "source": [ 1489 | "Our simulation is quite slow (you can see where the code is spending its time by including %%prun at the top of a cell and then executing your simulation code). Most of the computational expense is in the particle collisions because there are N(N-1)/2 pairs of particles. One way to reduce this expense is to divide the box up into quadrants, keep track of which quadrant the particles are in and then when executing the particle_collision code only look for collisions between pairs of particles in the same quadrant. Care needs to be taken when thinking about particles on the edges of the quadrants. Can you speed up the code in this way?" 1490 | ] 1491 | }, 1492 | { 1493 | "cell_type": "code", 1494 | "execution_count": null, 1495 | "metadata": {}, 1496 | "outputs": [], 1497 | "source": [ 1498 | "%%prun\n", 1499 | "\n", 1500 | "pass" 1501 | ] 1502 | }, 1503 | { 1504 | "cell_type": "markdown", 1505 | "metadata": {}, 1506 | "source": [ 1507 | "# Bibliography " 1508 | ] 1509 | }, 1510 | { 1511 | "cell_type": "markdown", 1512 | "metadata": {}, 1513 | "source": [ 1514 | "* W. Krauth, [Statistical Mechanics: Algorithms and Computations](http://imp-primo.hosted.exlibrisgroup.com/ICL_VU1:LRSCOP_44IMP:44IMP_ALMA_DS2146738160001591), Oxford University Press, 2006" 1515 | ] 1516 | } 1517 | ], 1518 | "metadata": { 1519 | "anaconda-cloud": {}, 1520 | "kernelspec": { 1521 | "display_name": "Python 3 (ipykernel)", 1522 | "language": "python", 1523 | "name": "python3" 1524 | }, 1525 | "language_info": { 1526 | "codemirror_mode": { 1527 | "name": "ipython", 1528 | "version": 3 1529 | }, 1530 | "file_extension": ".py", 1531 | "mimetype": "text/x-python", 1532 | "name": "python", 1533 | "nbconvert_exporter": "python", 1534 | "pygments_lexer": "ipython3", 1535 | "version": "3.11.2" 1536 | } 1537 | }, 1538 | "nbformat": 4, 1539 | "nbformat_minor": 1 1540 | } 1541 | --------------------------------------------------------------------------------