Polyergic

Positronium! ...ish

Jul 31st, 2012
203
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.68 KB | None | 0 0
  1. #!/usr/bin/python
  2.  
  3. #
  4. # Honors project for PHYS 4700 - Electricity & Magnetism (milestone 13)
  5. # Shad Sterling <[email protected]>
  6. #
  7. # Green particles have negative charge, Red particles have positive charge
  8. # Blue arrows represent velocity, Yellow arrows represent acceleration
  9. # Grey grid arrows are scaled electric field vectors
  10. #
  11. # Add particles by clicking on empty space (new particles will alternate charge)
  12. # Move particles by drag & drop (next new particle will have opposite charge as moved particle)
  13. # Remove particles by clicking without dragging (next new particle will have same charge)
  14. # Advance motion by clicking on any motion arrow (velocity or acceleration)
  15. # Continuously advance motion by dragging on a motion arrow and holding button down
  16. #
  17.  
  18.  
  19. from __future__ import print_function, division
  20. from visual import *
  21. import random
  22. import time
  23. import gc
  24. import inspect
  25. import pprint
  26.  
  27. arena_size = 65   # arena size in meters (significantly smaller sizes are problematic)
  28. arena_grid = 24   # number of gridpoints across each dimension of the arena
  29. step_time = 0.05  # simulation time per step in seconds
  30. frame_time = 1/4  # minimum real time per step in seconds when continuously stepping
  31. #drag_time = 0.1   # sleep time for drag position updates
  32.  
  33.  
  34. class Arena:
  35.     grid_base = 0.25     # minimum length for grid vectors as proportion of grid spacing
  36.     grid_max = 1/4       # maximum length for grid vectors as proportion of arena dimension
  37.     rebound_ratio = 0.1  # proportion of velocity reflected from boundary
  38.     coulomb_constant = 8.9875517873681764 * 10**9 # N*m*m/C/C
  39.  
  40.     def __init__( self, size, grid, scene ):
  41.         self.size = size # size of arena in each dimension in meters
  42.         self.boundary = size/2
  43.         self.particles = []
  44.         self.grid = []
  45.         scene.autoscale = false
  46.         scene.autocenter = false
  47.         scene.range = self.boundary
  48.         self.regrid( grid ) # setup indicator grid
  49.         self.accel_limit = self.size
  50.  
  51.     def regrid( self, grid ):
  52.         for point in self.grid:
  53.             point.hide()
  54.         self.grid_size = ceil(abs(grid)) # gridpoint count in each dimension
  55.         self.grid_spacing = self.size/(self.grid_size+1) # distance in meters between gridpoints (no points on edges)
  56.         self.grid_base = self.grid_spacing * Arena.grid_base
  57.         self.grid_max = self.size * Arena.grid_max
  58.         start = -self.boundary + self.grid_spacing
  59.         stop = self.boundary - self.grid_spacing*1.5
  60.         x = start
  61.         y = self.boundary - self.grid_spacing
  62.         i = self.grid_size ** 2 #number of gridpoints
  63.         while i > 0:
  64.             self.grid.append( GridPoint( self, vector( x, y, 0 ) ) )
  65.             if x < stop:
  66.                 x += self.grid_spacing
  67.             else:
  68.                 x = start
  69.                 y -= self.grid_spacing
  70.             i = i - 1
  71.         for particle in self.particles:
  72.             show = particle.Avatar.visible
  73.             particle.Avatar.hide()
  74.             particle.avatar = Avatar( particle )
  75.             if show:
  76.                 particle.Avatar.show()
  77.  
  78.     def capacitor( self, count, length, width, pos, angle ):
  79.         angle = math.pi/6
  80.         start = rotate( (length/2,0,0), math.pi/2-angle, (0,0,1) )
  81.         side = (count-1)/2
  82.         next = -start/side
  83.         position = start + pos # backward for type safety
  84.         gap = rotate( (width/2,0,0), math.pi*2-angle, (0,0,1) )
  85.         while 0 < count:
  86.             self.electron( position-gap )
  87.             self.positron( position+gap )
  88.             position = position + next
  89.             count -= 1
  90.  
  91.     def circle( self, count, radius, pos, angle ):
  92.         inc = math.pi/count
  93.         while 0 < count:
  94.             offset = rotate( (radius,0,0), angle, (0,0,1) )
  95.             self.positron( offset+pos )
  96.             angle += inc
  97.             offset = rotate( (radius,0,0), angle, (0,0,1) )
  98.             self.electron( offset+pos )
  99.             angle += inc
  100.             count -= 1
  101.  
  102.     def positronium( self, radius, angle, pos=(0,0,0), clockwise = false ):
  103.         offset = rotate( (radius,0,0), angle, (0,0,1) )
  104.         p = self.positron( offset+pos )
  105.         n = self.electron( -offset+pos )
  106.         f = Particle.force_electric( p, n )
  107.         d = 1
  108.         if clockwise:
  109.             d = -d
  110.         p.acceleration = f/p.mass
  111.         a = p.acceleration * sqrt( p.acceleration.mag * radius )
  112.         p.velocity = rotate( a, d*math.pi/2, (0,0,1) )
  113.         n.acceleration = -f/n.mass
  114.         a = n.acceleration * sqrt( n.acceleration.mag * radius )
  115.         n.velocity = rotate( a, d*math.pi/2, (0,0,1) )
  116.  
  117.     def electron( self, position, velocity=(0,0,0) ):
  118.         new = Particle.electron( self, position, velocity )
  119.         self.add( new )
  120.         return new
  121.  
  122.     def positron( self, position, velocity=(0,0,0) ):
  123.         new = Particle.positron( self, position, velocity )
  124.         self.add( new )
  125.         return new
  126.  
  127.     def add( self, particle ):
  128.         self.particles.append( particle )
  129.         particle.reveal()
  130.  
  131.     def remove( self, particle ):
  132.         particle.hide()
  133.         self.particles.remove( particle )
  134.  
  135.     def bound( self, particle ):
  136.         i = 0
  137.         while i < 2:
  138.             if particle.position[i] > self.boundary:
  139.                 particle.position[i] = self.boundary
  140.                 particle.velocity[i] = -abs( particle.velocity[i] * self.rebound_ratio )
  141.             elif particle.position[i] < -self.boundary:
  142.                 particle.position[i] = -self.boundary
  143.                 particle.velocity[i] = abs( particle.velocity[i] * self.rebound_ratio )
  144.             i = i + 1
  145.  
  146.     def update( self ):
  147.         for point in self.grid:
  148.             point.update()
  149.         for particle in self.particles:
  150.             particle.update() # update acceleration
  151.  
  152.     def step( self, step_time ):
  153.         for particle in self.particles:
  154.             particle.step( step_time ) # update position & velocity
  155.             self.bound( particle )     # constrain to arena bounds
  156.         self.update()
  157.  
  158.     def sync( self ):
  159.         for particle in self.particles:
  160.             particle.sync()
  161.         for point in self.grid:
  162.             point.sync()
  163.  
  164. class GridPoint:
  165.     arrow_radius = 0.1
  166.     arrow_scale = 10 ** 10
  167.     color_electric = color.gray(0.7)
  168.  
  169.     def __init__( self, arena, position ):
  170.         self.arena = arena
  171.         self.position = position
  172.         self.avatar = arrow( pos = position, fixedwidth = true, color = GridPoint.color_electric,
  173.                              shaftwidth = GridPoint.arrow_radius * self.arena.grid_spacing,
  174.                              role = "grid" )
  175.         self.electric = vector( 0,0,0 )
  176.    
  177.     def update( self ):
  178.         electric = vector( 0,0,0 )
  179.         for particle in self.arena.particles:
  180.             range = self.position - particle.position
  181.             if 0 != particle.charge:
  182.                 electric += Arena.coulomb_constant * range * particle.charge / (range.mag**3)
  183.         electric.mag = self.arena.grid_base + electric.mag * GridPoint.arrow_scale
  184.         if electric.mag > self.arena.grid_max:
  185.             electric.mag = 0
  186.         self.electric = electric
  187.         self.in_sync = false
  188.  
  189.     def sync( self ):
  190.         if not self.in_sync:
  191.             self.avatar.axis = self.electric
  192.             self.in_sync = true
  193.  
  194. class Particle:
  195.     electron_mass = 9.10938291 * 10**(-31) # kilograms
  196.     electron_charge = -1.602176565 * 10**(-19) # Coulombs
  197.  
  198.     @classmethod
  199.     def electron( self, arena, position, velocity=(0,0,0) ):
  200.         return self( arena, Particle.electron_mass, Particle.electron_charge, position, velocity )
  201.    
  202.     @classmethod
  203.     def positron( self, arena, position, velocity=(0,0,0) ):
  204.         return self( arena, Particle.electron_mass, -Particle.electron_charge, position, velocity )
  205.  
  206.     @classmethod
  207.     def force_electric( self, p1, p2 ):
  208.         range = p1.position - p2.position
  209.         return Arena.coulomb_constant * range * p1.charge * p2.charge / (range.mag**3)
  210.  
  211.     def __init__( self, arena, mass, charge, position, velocity, acceleration=(0,0,0) ):
  212.         self.arena = arena
  213.         self.mass = mass
  214.         self.charge = charge
  215.         self.position = vector( position )
  216.         self.velocity = vector( velocity )
  217.         self.acceleration = vector( acceleration )
  218.         self.avatar = Avatar( self )
  219.         self.in_sync = true #true when avatar matches
  220.  
  221.     def __str__( self ):
  222.         return "{ Particle "+str(id(self))+": "+str(self.mass)+"kg "+str(self.charge)+"C @"\
  223.                +str(self.position)+" & "+str(id(self.avatar))+"=>"+str(id(self.avatar.position))+" }"
  224.  
  225.     def sync( self ): # synchronize avatar
  226.         if not self.in_sync:
  227.             self.avatar.update()
  228.             self.in_sync = true
  229.  
  230.     def step( self, step_time ): # step position & velocity
  231.         self.position += self.velocity * step_time
  232.         self.velocity += self.acceleration * step_time
  233.         self.in_sync = false #avatar is outdated
  234.  
  235.     def update( self ): # recalculate acceleration
  236.         electric = vector( 0, 0, 0 )
  237.         for particle in self.arena.particles:
  238.             if 0 != particle.charge and self is not particle:
  239.                 force = Particle.force_electric( self, particle )
  240.                 electric += force
  241.         self.acceleration = electric / self.mass
  242.         if self.acceleration.mag > self.arena.accel_limit:
  243.             self.acceleration.mag = self.arena.accel_limit
  244.         self.in_sync = false #avatar is outdated
  245.  
  246.     def move( self, new_position ):
  247.         self.position = vector(new_position)
  248.         self.in_sync = false #avatar is outdated
  249.  
  250.     def hide( self ):
  251.         self.avatar.hide()
  252.  
  253.     def reveal( self ):
  254.         self.avatar.reveal()
  255.  
  256. class Avatar:
  257.     avatar_radius = 0.5 # for avatars of point particles
  258.     color_negative = (0,.9,0) # negative particles are (almost) green
  259.     color_positive = (1,.1,.1) # positive particles are (almost) red
  260.     arrow_radius = 0.35
  261.     arrow_scale = 1 # proportional length of velocity & acceleration arrows
  262.     color_velocity = (0,.2,1) #velocity arrows are (almost) blue
  263.     color_acceleration = color.yellow #acceleration arrows are yellow
  264.  
  265.     @classmethod
  266.     def color_of( self, charge ):
  267.         if charge < 0:
  268.             return Avatar.color_negative
  269.         elif charge > 0:
  270.             return Avatar.color_positive
  271.  
  272.     @classmethod
  273.     def arrow_of( self, base, motion ): # velocity arrow
  274.         m = vector( motion )
  275.         m.mag = m.mag * Avatar.arrow_scale + base
  276.         return m
  277.  
  278.     def __init__( self, particle ):
  279.         self.radius = Avatar.avatar_radius * particle.arena.grid_spacing
  280.         self.arrow_width = Avatar.arrow_radius * particle.arena.grid_spacing
  281.         self.position = visual.sphere( color = Avatar.color_of( particle.charge ),
  282.                                        radius = self.radius, role = "particle", real = particle )
  283.         self.velocity = visual.arrow( shaftwidth = self.arrow_width, fixedwidth = true,
  284.                                       color = Avatar.color_velocity, role = "motion" )
  285.         self.acceleration = visual.arrow( shaftwidth = self.arrow_width, fixedwidth = true,
  286.                                           color = Avatar.color_acceleration, role = "motion" )
  287.         self.represents = particle
  288.         self.update()
  289.  
  290.     def __str__( self ):
  291.         return "{ Avatar "+str(id(self))+": "+str(id(self.position))+" "+str(id(self.velocity))+" "\
  292.                +str(id(self.acceleration))+" }"
  293.  
  294.     def update( self ):
  295.         self.position.pos = self.represents.position
  296.         self.velocity.pos = self.represents.position
  297.         self.velocity.axis = Avatar.arrow_of( self.radius, self.represents.velocity )
  298.         self.acceleration.pos = self.represents.position
  299.         self.acceleration.axis = Avatar.arrow_of( self.radius, self.represents.acceleration )
  300.  
  301.     def hide( self ):
  302.         self.position.visible = false
  303.         self.velocity.visible = false
  304.         self.acceleration.visible = false
  305.  
  306.     def reveal( self ):
  307.         self.position.visible = true
  308.         self.velocity.visible = true
  309.         self.acceleration.visible = true
  310.  
  311.  
  312.  
  313. arena = Arena( arena_size, arena_grid, scene )
  314. #arena.capacitor( ceil(arena_grid/3), arena_size*2/3, arena_size/2, (0,0,0), 2*math.pi*random.random() )
  315. #arena.positron( (0,0,0), rotate( (arena_size/4/Avatar.arrow_scale,0,0), 2*math.pi*random.random(), (0,0,1) ) )
  316. #arena.circle( 1, arena_size/20, (0,0,0), 2*math.pi*random.random() )
  317. #arena.circle( ceil(arena_grid/3), arena_size/6, (0,0,0), 2*math.pi*random.random() )
  318. arena.positronium( arena_size/7.85, 2*math.pi*random.random() )
  319.  
  320. negative = true
  321. target = None
  322. event = None
  323. step = false
  324. changed = true
  325. drag = false # dragging a particle
  326. run = false # stepping continuously (~dragging motion)
  327.  
  328. #ticksum = 0
  329. #tickcount = 0
  330. #tickmisscount = 0
  331. #tickinterval = frame_time
  332.  
  333. while true:
  334.     if changed:
  335.         tickstart = time.time()
  336.         if step:
  337.             #print( "calculating step" )
  338.             arena.step( step_time )
  339.         arena.update() #recalculate forces
  340.         gc.collect()
  341.         changed = false
  342.         #tickend = time.time()
  343.         #ticktime = tickend - tickstart # tick time (not including waiting)
  344.         #tickcount = tickcount + 1
  345.         #ticksum = ticksum + ticktime
  346.         #tickavg = ticksum/tickcount # average seconds per tick
  347.         #if ticktime > tickinterval:
  348.         #   tickmisscount += 1
  349.         #tickmissrate = tickmisscount / tickcount
  350.         #tickinterval = max( frame_time, tickavg * 1.35 )
  351.         #tickwait = max( 0, tickinterval - ticktime )
  352.         #print( "tick", tickcount, "took", ticktime, "avg", tickavg, "sum", ticksum, "interval",
  353.         #      tickinterval, "missed", tickmisscount, "rate", tickmissrate, "wait", tickwait )
  354.         #time.sleep( tickwait )
  355.         arena.sync() #update avatars
  356.         #print( "refresh complete" )
  357.     #else:
  358.         #print( "refresh skipped" )
  359.     if 0 == scene.mouse.events and (drag or run):
  360.         #print( "dragging..." )
  361.         #time.sleep( drag_time )
  362.         newpos = vector( scene.mouse.pos[0], scene.mouse.pos[1], 0 )
  363.     else:
  364.         #print( "dequeing event..." )
  365.         event = scene.mouse.getevent()
  366.         newpos = vector( event.pos[0], event.pos[1], 0 )
  367.     if event.press:
  368.         if None != event.pick:
  369.             role = event.pick.role
  370.         else:
  371.             role = None
  372.         if "particle" == role:
  373.             target = event.pick.real
  374.             #print( "press:  "+str(id(event.pick))+" selects "+str(target) )
  375.         elif "motion" == role:
  376.             #print( "press:  "+str(id(event.pick))+" steps once" )
  377.             step = true
  378.             changed = true
  379.             run = true
  380.         elif None != target:
  381.             #print( "press:  "+str(id(event.pick))+" replaces "+str(target)+" at "+srt(newpos) )
  382.             target.move( newpos )
  383.             arena.add( target )
  384.             negative = target.charge > 0
  385.             changed = true
  386.             drag = true
  387.         elif negative:
  388.             target = arena.electron( newpos )
  389.             #print( "press:  "+str(id(event.pick))+" creates "+str(target) )
  390.             negative = false
  391.             changed = true
  392.             drag = true
  393.         else:
  394.             target = arena.positron( newpos )
  395.             #print( "press:  "+str(id(event.pick))+" creates "+str(target) )
  396.             negative = true
  397.             changed = true
  398.             drag = true
  399.     elif event.drag:
  400.         if None != target:
  401.             #print( "drag:  "+str(id(event.pick))+" moves "+str(target)+" to "+str(newpos) )
  402.             target.move( newpos )
  403.             changed = true
  404.             drag = true
  405.         elif run:
  406.             #print( "drag:  "+str(id(event.pick))+" steps again" )
  407.             step = true
  408.             changed = true
  409.         else: # how would this happen?
  410.             #print( "drag:  "+str(id(event.pick))+" remains idle\t\t(I sense a disturbance...)" )
  411.             changed = false
  412.     elif event.release:
  413.         if None != target:
  414.             if false == drag:
  415.                 #print( "release:  "+str(id(event.pick))+" removes "+str(target) )
  416.                 arena.remove( target )
  417.                 target = None
  418.                 changed = true
  419.             else:
  420.                 #print( "release:  "+str(id(event.pick))+" drops "+str(target)+" at "+str(newpos) )
  421.                 target.move( newpos )
  422.                 negative = target.charge > 0
  423.                 target = None
  424.                 drag = false
  425.                 changed = true
  426.         elif run:
  427.             #print( "release:  "+str(id(event.pick))+" stops stepping" )
  428.             step = false
  429.             run = false
  430.         else:
  431.             #print( "release:  "+str(id(event.pick))+" remains idle" )
  432.             changed = false
  433.     else:
  434.         print( "Unknown Event!", pprint.pprint( inspect.getmembers( event ) ) )
  435.         changed = false
Advertisement
Add Comment
Please, Sign In to add comment