1
0

run_simulated_annealing.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. """Simulated annealing algorithm used to match the simulation of the
  2. Antennae to the observations by comparing binarized images."""
  3. import numpy as np
  4. import scipy
  5. import pickle
  6. from scipy import ndimage
  7. from fast_histogram import histogram2d
  8. from scipy.signal import convolve2d
  9. from numba import jit
  10. from matplotlib.image import imread
  11. import datetime
  12. from simulation import Simulation, Galaxy
  13. T = .25 # Initial tempreature
  14. STEPS = 0
  15. DECAY = .998 # Exponential decay factor
  16. def simToBitmap(sim, theta, phi, view, scale, x, y, galaxy):
  17. """Obtain a bitmap of one galaxy as viewed from a given direction.
  18. The binning has been chosen so that the scale and the offset (x,y)
  19. are expected to be approximately 1 and (0, 0) respectively.
  20. Parameters:
  21. sim (Simulation): Simulation to project.
  22. theta (float): polar angle of viewing direction.
  23. phi (float): azimuthal angle of viewing direction.
  24. view (float): rotation angle along viewing direction.
  25. scale (float): scaling factor.
  26. x (float): x offset
  27. y (float): y offset
  28. galaxy (int): galaxy to plot. Either 0, or 1. They are assumed
  29. to have the same number of particles.
  30. """
  31. # Obtain components in new x',y' plane
  32. r_vec = (sim.project(theta, phi, view) - [[x+.12,y+1.3]]) * scale
  33. # Select a single galaxy. We match them separately in the algorithm.
  34. if galaxy==0: r_vec = r_vec[2:len(r_vec)//2-1] #omit central masses
  35. if galaxy==1: r_vec = r_vec[len(r_vec)//2-1:]
  36. # Use a fast histogram, much faster than numpy ()
  37. H = histogram2d(r_vec[:,0], r_vec[:,1],
  38. range=[[-5,5], [-5,5]], bins=(50, 50))
  39. im = np.zeros((50, 50))
  40. H = convolve2d(H, np.ones((2,2)), mode='same') # Smooth the image
  41. im[H>=1] = 1 # Binary the image
  42. return im
  43. @jit(nopython=True) # Numba annotation
  44. def bitmapScoreAlgo(im1, im2):
  45. """Computes the f1 score betwen two binarized images
  46. Parameters
  47. im1 (arr): nxm ground truth image
  48. im2 (arr): nxm candidate image
  49. Returns
  50. f1 score
  51. """
  52. TP = np.sum((im1==1.0) & (im2==1.0))
  53. TN = np.sum((im1==0.0) & (im2==0.0))
  54. FP = np.sum((im1==0.0) & (im2==1.0))
  55. FN = np.sum((im1==1.0) & (im2==0.0))
  56. if TP==0: return 0
  57. precision = TP/(TP+FP)
  58. recall = TP/(TP+FN)
  59. return 2*precision*recall / (precision + recall)
  60. # The matching algorithm attempts to improve the match by shifting the
  61. # image by one pixel in each direction. If none improve the score the
  62. # f1-score of said local maximum is retuned. To make this highly efficient
  63. # (as this is run millions of time) we explicitely write functions to
  64. # shift an image by 1 pixel in each direction, as these can then be compiled
  65. # using Numba (jit annotation) to low-level code.
  66. # Performance is crucial here and must sadly be prioritized over conciseness
  67. @jit(nopython=True)
  68. def shiftBottom(im, im2):
  69. """Shifts an image by one pixel downwards.
  70. Parameters:
  71. im (arr): the nxm image to shift by one pixel.
  72. im2 (arr): an nxm array where the new image will be placed.
  73. Returns:
  74. A reference to im2
  75. """
  76. im2[1:] = im[:-1]
  77. im2[0] = 0
  78. return im2
  79. @jit(nopython=True)
  80. def shiftTop(im, im2):
  81. """Shifts an image by one pixel upwards."""
  82. im2[:-1] = im[1:]
  83. im2[-1] = 0
  84. return im2
  85. @jit(nopython=True)
  86. def shiftLeft(im, im2):
  87. """Shifts an image by one pixel to the left."""
  88. im2[:,1:] = im[:,:-1]
  89. im2[:,0] = 0
  90. return im2
  91. @jit(nopython=True)
  92. def shiftRight(im, im2):
  93. """Shifts an image by one pixel to the right."""
  94. im2[:,:-1] = im[:,1:]
  95. im2[:,-1] = 0
  96. return im2
  97. @jit
  98. def bitmapScore(im1, im2, _prev=None, _bestscore=None, _zeros=None):
  99. """Computes the bitmap score between two images. This is the f1-score
  100. but we allow the algorithm to attempt to improve the score by
  101. shifting the images. The algorithm is implemented recursively.
  102. Parameters:
  103. im1 (array): The ground truth nxm image.
  104. im2 (array): The candidate nxm imgae.
  105. _prev: Used internally for recursion.
  106. _bestscore: Used internally for recursion.
  107. _zeros: Used internally for recursion.
  108. Returns:
  109. f1-score for the two images.
  110. """
  111. # When the function is called externally, initialize an array of zeros
  112. # and compute the score for no shifting. The zeros array is used for
  113. # performance to only create a new array once.
  114. if _bestscore is None:
  115. _bestscore = bitmapScoreAlgo(im1, im2)
  116. _zeros = np.zeros_like(im2)
  117. # Attempt to improve the score by shifting the image in a direction
  118. # Keeping track of _prev allows to not 'go back' and undo and attempt
  119. # to undo a shift needlessly.
  120. if _prev is not 0: # try left
  121. shifted = shiftLeft(im2, _zeros)
  122. score = bitmapScoreAlgo(im1, shifted)
  123. if score > _bestscore: return bitmapScore(im1, shifted,
  124. _prev=1, _bestscore=score, _zeros=_zeros)
  125. if _prev is not 1: # try right
  126. shifted = shiftRight(im2, _zeros)
  127. score = bitmapScoreAlgo(im1, shifted)
  128. if score > _bestscore: return bitmapScore(im1, shifted,
  129. _prev=0, _bestscore=score, _zeros=_zeros)
  130. if _prev is not 2: # try top
  131. shifted = shiftTop(im2, _zeros)
  132. score = bitmapScoreAlgo(im1, shifted)
  133. if score > _bestscore: return bitmapScore(im1, shifted,
  134. _prev=3, _bestscore=score, _zeros=_zeros)
  135. if _prev is not 3: # try bottom
  136. shifted = shiftBottom(im2, _zeros)
  137. score = bitmapScoreAlgo(im1, shifted)
  138. if score > _bestscore: return bitmapScore(im1, shifted,
  139. _prev=2, _bestscore=score, _zeros=_zeros)
  140. # Return _bestscore if shifting did not improve (local maximum).
  141. return _bestscore
  142. def attemptSimulation(theta1, phi1, theta2, phi2, rmin=1, e=.5, R=2,
  143. disk1=.75, disk2=.65, mu=1, plot=False, steps=2000):
  144. """Runs a simulation with the given parameters and compares it
  145. to observations of the antennae to return a score out of 2.
  146. Parameters:
  147. theta1 (float): polar angle for the spin of the first galaxy.
  148. phi1 (float): azimuthal angle for the spin of the first galaxy.
  149. theta2 (float): polar angle for the spin of the second galaxy.
  150. phi2 (float): azimuthal angle for the spin of the second galaxy.
  151. rmin (float): closest distance of approach of orbit.
  152. e (float): eccentricity of the orbit.
  153. R (float): initial separation
  154. disk1 (float): radius of the uniform disk of the first galaxy
  155. disk2 (float): radius of the uniform disk of the first galaxy
  156. mu (float): ratio of masses of the two galaxies
  157. plot (float): if true the simulation will be saved to data/annealing/
  158. steps (float): number of times the f1 score will be computed, along
  159. random viewing directions per 100 timesteps.
  160. Returns:
  161. f1-score: score obtained by the simulation.
  162. """
  163. # Initialize the simulation
  164. sim = Simulation(dt=1E-3, soft=0.1, verbose=False, CONFIG=None, method='bruteForceNumbaOptimized')
  165. galaxy1 = Galaxy(orientation = (theta1, phi1), centralMass=2/(1+mu),
  166. sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0},
  167. disk={'model':'uniform', 'particles':2000, 'l':disk1, 'totalMass':0},
  168. halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0})
  169. galaxy2 = Galaxy(orientation = (theta2, phi2), centralMass=2*mu/(1+mu),
  170. sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0},
  171. disk={'model':'uniform', 'particles':2000, 'l':disk2, 'totalMass':0},
  172. halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0})
  173. sim.setOrbit(galaxy1, galaxy2, e=e, R0=R, rmin=rmin) # define the orbit
  174. # Run the simulation manually
  175. # Initialize the parameters consistently with the velocities
  176. sim.rprev_vec = sim.r_vec - sim.v_vec * sim.dt
  177. # Keep track of the best score
  178. bestScore = 0
  179. # and its corresponding binarized image and parameters
  180. bestImage, bestParams = 0, 0
  181. hasReachedPericenter = False
  182. # Run until tmax = 25
  183. for i in range(25001):
  184. sim.step()
  185. if i%100==0: # Every $\Delta t = 0.1$
  186. # Check if we are close to pericenter
  187. centers = sim.r_vec[sim.type[:,0] == 'center']
  188. if np.linalg.norm(centers[0] - centers[1]) < 1.3*rmin:
  189. hasReachedPericenter = True
  190. # Do not evaluate the f1-score until pericenter.
  191. if not hasReachedPericenter: continue
  192. # Check multiple (steps) viewing directions at random
  193. localBestScore = 0
  194. localBestImage, localBestParams = 0, 0
  195. for j in range(steps):
  196. # The viewing directions are isotropically distributed
  197. theta = np.arccos(np.random.uniform(-1, 1))
  198. phi = np.random.uniform(0, 2*np.pi)
  199. # Rotation along line of sight
  200. view = np.random.uniform(0, 2*np.pi)
  201. scale = np.random.uniform(0.6, 1.0)
  202. x = np.random.uniform(-1.0, 1.0) # Offsets
  203. y = np.random.uniform(-1.0, 1.0)
  204. # Get images for each galaxy and compute their score separately
  205. im1 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=0)
  206. im2 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=1)
  207. score = bitmapScore(GT1, im1) + bitmapScore(GT2, im2)
  208. if score > localBestScore:
  209. localBestScore = score
  210. localBestImage = [im1,im2]
  211. localBestParams = [i, theta, phi, view, scale, x, y]
  212. if bestScore < localBestScore:
  213. bestScore = localBestScore
  214. bestImage = localBestImage
  215. bestParams = localBestParams
  216. if plot:
  217. sim.save('annealing', type='2D')
  218. print('Best score for this attempt', bestScore)
  219. print('using viewing parameters', bestParams)
  220. return bestScore
  221. ################################################################################
  222. ################################################################################
  223. # Generate a (50, 50) bitmap for each galaxy
  224. # They are stored globally in GT1 and GT2 (Ground Truth)
  225. im = imread('literature/figs/g1c.tif')
  226. im = np.mean(im, axis=-1)
  227. im = scipy.misc.imresize(im, (50,50))
  228. GT1 = np.zeros((50, 50))
  229. GT1[im > 50] = 1
  230. im = imread('literature/figs/g2c.tif')
  231. im = np.mean(im, axis=-1)
  232. im = scipy.misc.imresize(im, (50,50))
  233. GT2 = np.zeros((50, 50))
  234. # Define the limits and relate scale of the variations in each parameter
  235. # In the same order as attemptSimulation
  236. # phi1, theta1, phi2, theta2, rmin (fixed), e, R (fixed), disk1, disk2
  237. LIMITS = np.array([[np.pi, 2 * np.pi], [-np.pi, np.pi],
  238. [0, np.pi], [-np.pi, np.pi],
  239. [1,1], [.5,1.0], [2.2,2.2], [.5,.8], [.5,.8]])
  240. VARIATIONS = np.array([.08, .15, .08, .15, 0, .01, 0, .01, .01])
  241. # Choose a random starting point and evaluate it
  242. bestparams = [np.random.uniform(*l) for l in LIMITS]
  243. log = []
  244. bestscore = attemptSimulation(*bestparams, steps=500)
  245. print('Starting with score', bestscore, 'with parameters', bestparams)
  246. log.append([bestscore, bestparams, True])
  247. for i in range(STEPS):
  248. T = T * DECAY #exponential decay
  249. # Perturb the parameters
  250. params = bestparams + np.random.normal(scale=VARIATIONS) * 2 * T / 0.04
  251. # Allow the angles from $-\pi$ to $\pi$ to wrap around
  252. for j in [1,3]:
  253. params[j] = np.mod(params[j] - LIMITS[j][0], LIMITS[j][1] - LIMITS[j][0])
  254. params[j] += LIMITS[j][0]
  255. # Clip parameters outside their allowed range
  256. params = np.clip(params, LIMITS[:, 0], LIMITS[:, 1])
  257. # Evaluate the score for this attempt, use more steps as i progresses
  258. # so as to reduce the noise in the evaluation of the score
  259. score = attemptSimulation(*params, steps=500 + i)
  260. # Perform simulated annealing with a typical exponential rule
  261. if score > bestscore or np.exp(-(bestscore-score)/T) > np.random.rand():
  262. # Flip to this new point
  263. print('NEW BEST ____', i, T, score, params)
  264. bestscore = score
  265. bestparams = params
  266. log.append([score, params, True])
  267. else: # Remain in the old point
  268. log.append([score, params, False])
  269. # Save the progress for future plotting
  270. pickle.dump(log, open('data/logs/logb.pickle', "wb" ) )