run_simulated_annealing
module
Simulated annealing algorithm used to match the simulation of the Antennae to the observations by comparing binarized images.
Source code
"""Simulated annealing algorithm used to match the simulation of the
Antennae to the observations by comparing binarized images."""
import numpy as np
import scipy
import pickle
from scipy import ndimage
from fast_histogram import histogram2d
from scipy.signal import convolve2d
from numba import jit
from matplotlib.image import imread
import datetime
from simulation import Simulation, Galaxy
T = .25 # Initial tempreature
STEPS = 0
DECAY = .998 # Exponential decay factor
def simToBitmap(sim, theta, phi, view, scale, x, y, galaxy):
"""Obtain a bitmap of one galaxy as viewed from a given direction.
The binning has been chosen so that the scale and the offset (x,y)
are expected to be approximately 1 and (0, 0) respectively.
Parameters:
sim (Simulation): Simulation to project.
theta (float): polar angle of viewing direction.
phi (float): azimuthal angle of viewing direction.
view (float): rotation angle along viewing direction.
scale (float): scaling factor.
x (float): x offset
y (float): y offset
galaxy (int): galaxy to plot. Either 0, or 1. They are assumed
to have the same number of particles.
"""
# Obtain components in new x',y' plane
r_vec = (sim.project(theta, phi, view) - [[x+.12,y+1.3]]) * scale
# Select a single galaxy. We match them separately in the algorithm.
if galaxy==0: r_vec = r_vec[2:len(r_vec)//2-1] #omit central masses
if galaxy==1: r_vec = r_vec[len(r_vec)//2-1:]
# Use a fast histogram, much faster than numpy ()
H = histogram2d(r_vec[:,0], r_vec[:,1],
range=[[-5,5], [-5,5]], bins=(50, 50))
im = np.zeros((50, 50))
H = convolve2d(H, np.ones((2,2)), mode='same') # Smooth the image
im[H>=1] = 1 # Binary the image
return im
@jit(nopython=True) # Numba annotation
def bitmapScoreAlgo(im1, im2):
"""Computes the f1 score betwen two binarized images
Parameters
im1 (arr): nxm ground truth image
im2 (arr): nxm candidate image
Returns
f1 score
"""
TP = np.sum((im1==1.0) & (im2==1.0))
TN = np.sum((im1==0.0) & (im2==0.0))
FP = np.sum((im1==0.0) & (im2==1.0))
FN = np.sum((im1==1.0) & (im2==0.0))
if TP==0: return 0
precision = TP/(TP+FP)
recall = TP/(TP+FN)
return 2*precision*recall / (precision + recall)
# The matching algorithm attempts to improve the match by shifting the
# image by one pixel in each direction. If none improve the score the
# f1-score of said local maximum is retuned. To make this highly efficient
# (as this is run millions of time) we explicitely write functions to
# shift an image by 1 pixel in each direction, as these can then be compiled
# using Numba (jit annotation) to low-level code.
# Performance is crucial here and must sadly be prioritized over conciseness
@jit(nopython=True)
def shiftBottom(im, im2):
"""Shifts an image by one pixel downwards.
Parameters:
im (arr): the nxm image to shift by one pixel.
im2 (arr): an nxm array where the new image will be placed.
Returns:
A reference to im2
"""
im2[1:] = im[:-1]
im2[0] = 0
return im2
@jit(nopython=True)
def shiftTop(im, im2):
"""Shifts an image by one pixel upwards."""
im2[:-1] = im[1:]
im2[-1] = 0
return im2
@jit(nopython=True)
def shiftLeft(im, im2):
"""Shifts an image by one pixel to the left."""
im2[:,1:] = im[:,:-1]
im2[:,0] = 0
return im2
@jit(nopython=True)
def shiftRight(im, im2):
"""Shifts an image by one pixel to the right."""
im2[:,:-1] = im[:,1:]
im2[:,-1] = 0
return im2
@jit
def bitmapScore(im1, im2, _prev=None, _bestscore=None, _zeros=None):
"""Computes the bitmap score between two images. This is the f1-score
but we allow the algorithm to attempt to improve the score by
shifting the images. The algorithm is implemented recursively.
Parameters:
im1 (array): The ground truth nxm image.
im2 (array): The candidate nxm imgae.
_prev: Used internally for recursion.
_bestscore: Used internally for recursion.
_zeros: Used internally for recursion.
Returns:
f1-score for the two images.
"""
# When the function is called externally, initialize an array of zeros
# and compute the score for no shifting. The zeros array is used for
# performance to only create a new array once.
if _bestscore is None:
_bestscore = bitmapScoreAlgo(im1, im2)
_zeros = np.zeros_like(im2)
# Attempt to improve the score by shifting the image in a direction
# Keeping track of _prev allows to not 'go back' and undo and attempt
# to undo a shift needlessly.
if _prev is not 0: # try left
shifted = shiftLeft(im2, _zeros)
score = bitmapScoreAlgo(im1, shifted)
if score > _bestscore: return bitmapScore(im1, shifted,
_prev=1, _bestscore=score, _zeros=_zeros)
if _prev is not 1: # try right
shifted = shiftRight(im2, _zeros)
score = bitmapScoreAlgo(im1, shifted)
if score > _bestscore: return bitmapScore(im1, shifted,
_prev=0, _bestscore=score, _zeros=_zeros)
if _prev is not 2: # try top
shifted = shiftTop(im2, _zeros)
score = bitmapScoreAlgo(im1, shifted)
if score > _bestscore: return bitmapScore(im1, shifted,
_prev=3, _bestscore=score, _zeros=_zeros)
if _prev is not 3: # try bottom
shifted = shiftBottom(im2, _zeros)
score = bitmapScoreAlgo(im1, shifted)
if score > _bestscore: return bitmapScore(im1, shifted,
_prev=2, _bestscore=score, _zeros=_zeros)
# Return _bestscore if shifting did not improve (local maximum).
return _bestscore
def attemptSimulation(theta1, phi1, theta2, phi2, rmin=1, e=.5, R=2,
disk1=.75, disk2=.65, mu=1, plot=False, steps=2000):
"""Runs a simulation with the given parameters and compares it
to observations of the antennae to return a score out of 2.
Parameters:
theta1 (float): polar angle for the spin of the first galaxy.
phi1 (float): azimuthal angle for the spin of the first galaxy.
theta2 (float): polar angle for the spin of the second galaxy.
phi2 (float): azimuthal angle for the spin of the second galaxy.
rmin (float): closest distance of approach of orbit.
e (float): eccentricity of the orbit.
R (float): initial separation
disk1 (float): radius of the uniform disk of the first galaxy
disk2 (float): radius of the uniform disk of the first galaxy
mu (float): ratio of masses of the two galaxies
plot (float): if true the simulation will be saved to data/annealing/
steps (float): number of times the f1 score will be computed, along
random viewing directions per 100 timesteps.
Returns:
f1-score: score obtained by the simulation.
"""
# Initialize the simulation
sim = Simulation(dt=1E-3, soft=0.1, verbose=False, CONFIG=None, method='bruteForceNumbaOptimized')
galaxy1 = Galaxy(orientation = (theta1, phi1), centralMass=2/(1+mu),
sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0},
disk={'model':'uniform', 'particles':2000, 'l':disk1, 'totalMass':0},
halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0})
galaxy2 = Galaxy(orientation = (theta2, phi2), centralMass=2*mu/(1+mu),
sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0},
disk={'model':'uniform', 'particles':2000, 'l':disk2, 'totalMass':0},
halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0})
sim.setOrbit(galaxy1, galaxy2, e=e, R0=R, rmin=rmin) # define the orbit
# Run the simulation manually
# Initialize the parameters consistently with the velocities
sim.rprev_vec = sim.r_vec - sim.v_vec * sim.dt
# Keep track of the best score
bestScore = 0
# and its corresponding binarized image and parameters
bestImage, bestParams = 0, 0
hasReachedPericenter = False
# Run until tmax = 25
for i in range(25001):
sim.step()
if i%100==0: # Every $\Delta t = 0.1$
# Check if we are close to pericenter
centers = sim.r_vec[sim.type[:,0] == 'center']
if np.linalg.norm(centers[0] - centers[1]) < 1.3*rmin:
hasReachedPericenter = True
# Do not evaluate the f1-score until pericenter.
if not hasReachedPericenter: continue
# Check multiple (steps) viewing directions at random
localBestScore = 0
localBestImage, localBestParams = 0, 0
for j in range(steps):
# The viewing directions are isotropically distributed
theta = np.arccos(np.random.uniform(-1, 1))
phi = np.random.uniform(0, 2*np.pi)
# Rotation along line of sight
view = np.random.uniform(0, 2*np.pi)
scale = np.random.uniform(0.6, 1.0)
x = np.random.uniform(-1.0, 1.0) # Offsets
y = np.random.uniform(-1.0, 1.0)
# Get images for each galaxy and compute their score separately
im1 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=0)
im2 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=1)
score = bitmapScore(GT1, im1) + bitmapScore(GT2, im2)
if score > localBestScore:
localBestScore = score
localBestImage = [im1,im2]
localBestParams = [i, theta, phi, view, scale, x, y]
if bestScore < localBestScore:
bestScore = localBestScore
bestImage = localBestImage
bestParams = localBestParams
if plot:
sim.save('annealing', type='2D')
print('Best score for this attempt', bestScore)
print('using viewing parameters', bestParams)
return bestScore
################################################################################
################################################################################
# Generate a (50, 50) bitmap for each galaxy
# They are stored globally in GT1 and GT2 (Ground Truth)
im = imread('literature/figs/g1c.tif')
im = np.mean(im, axis=-1)
im = scipy.misc.imresize(im, (50,50))
GT1 = np.zeros((50, 50))
GT1[im > 50] = 1
im = imread('literature/figs/g2c.tif')
im = np.mean(im, axis=-1)
im = scipy.misc.imresize(im, (50,50))
GT2 = np.zeros((50, 50))
# Define the limits and relate scale of the variations in each parameter
# In the same order as attemptSimulation
# phi1, theta1, phi2, theta2, rmin (fixed), e, R (fixed), disk1, disk2
LIMITS = np.array([[np.pi, 2 * np.pi], [-np.pi, np.pi],
[0, np.pi], [-np.pi, np.pi],
[1,1], [.5,1.0], [2.2,2.2], [.5,.8], [.5,.8]])
VARIATIONS = np.array([.08, .15, .08, .15, 0, .01, 0, .01, .01])
# Choose a random starting point and evaluate it
bestparams = [np.random.uniform(*l) for l in LIMITS]
log = []
bestscore = attemptSimulation(*bestparams, steps=500)
print('Starting with score', bestscore, 'with parameters', bestparams)
log.append([bestscore, bestparams, True])
for i in range(STEPS):
T = T * DECAY #exponential decay
# Perturb the parameters
params = bestparams + np.random.normal(scale=VARIATIONS) * 2 * T / 0.04
# Allow the angles from $-\pi$ to $\pi$ to wrap around
for j in [1,3]:
params[j] = np.mod(params[j] - LIMITS[j][0], LIMITS[j][1] - LIMITS[j][0])
params[j] += LIMITS[j][0]
# Clip parameters outside their allowed range
params = np.clip(params, LIMITS[:, 0], LIMITS[:, 1])
# Evaluate the score for this attempt, use more steps as i progresses
# so as to reduce the noise in the evaluation of the score
score = attemptSimulation(*params, steps=500 + i)
# Perform simulated annealing with a typical exponential rule
if score > bestscore or np.exp(-(bestscore-score)/T) > np.random.rand():
# Flip to this new point
print('NEW BEST ____', i, T, score, params)
bestscore = score
bestparams = params
log.append([score, params, True])
else: # Remain in the old point
log.append([score, params, False])
# Save the progress for future plotting
pickle.dump(log, open('data/logs/logb.pickle', "wb" ) )
Functions
def attemptSimulation(theta1, phi1, theta2, phi2, rmin=1, e=0.5, R=2, disk1=0.75, disk2=0.65, mu=1, plot=False, steps=2000)
-
Runs a simulation with the given parameters and compares it to observations of the antennae to return a score out of 2.
Parameters
theta1
:float
- polar angle for the spin of the first galaxy.
phi1
:float
- azimuthal angle for the spin of the first galaxy.
theta2
:float
- polar angle for the spin of the second galaxy.
phi2
:float
- azimuthal angle for the spin of the second galaxy.
rmin
:float
- closest distance of approach of orbit.
e
:float
- eccentricity of the orbit.
R
:float
- initial separation
disk1
:float
- radius of the uniform disk of the first galaxy
disk2
:float
- radius of the uniform disk of the first galaxy
mu
:float
- ratio of masses of the two galaxies
plot
:float
- if true the simulation will be saved to data/annealing/
steps
:float
- number of times the f1 score will be computed, along random viewing directions per 100 timesteps.
Returns
f1-score: score obtained by the simulation.
Source code
def attemptSimulation(theta1, phi1, theta2, phi2, rmin=1, e=.5, R=2, disk1=.75, disk2=.65, mu=1, plot=False, steps=2000): """Runs a simulation with the given parameters and compares it to observations of the antennae to return a score out of 2. Parameters: theta1 (float): polar angle for the spin of the first galaxy. phi1 (float): azimuthal angle for the spin of the first galaxy. theta2 (float): polar angle for the spin of the second galaxy. phi2 (float): azimuthal angle for the spin of the second galaxy. rmin (float): closest distance of approach of orbit. e (float): eccentricity of the orbit. R (float): initial separation disk1 (float): radius of the uniform disk of the first galaxy disk2 (float): radius of the uniform disk of the first galaxy mu (float): ratio of masses of the two galaxies plot (float): if true the simulation will be saved to data/annealing/ steps (float): number of times the f1 score will be computed, along random viewing directions per 100 timesteps. Returns: f1-score: score obtained by the simulation. """ # Initialize the simulation sim = Simulation(dt=1E-3, soft=0.1, verbose=False, CONFIG=None, method='bruteForceNumbaOptimized') galaxy1 = Galaxy(orientation = (theta1, phi1), centralMass=2/(1+mu), sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0}, disk={'model':'uniform', 'particles':2000, 'l':disk1, 'totalMass':0}, halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0}) galaxy2 = Galaxy(orientation = (theta2, phi2), centralMass=2*mu/(1+mu), sim=sim, bulge={'model':'plummer', 'particles':0, 'totalMass':0, 'l':0}, disk={'model':'uniform', 'particles':2000, 'l':disk2, 'totalMass':0}, halo={'model':'NFW', 'particles':0, 'rs':1, 'totalMass':0}) sim.setOrbit(galaxy1, galaxy2, e=e, R0=R, rmin=rmin) # define the orbit # Run the simulation manually # Initialize the parameters consistently with the velocities sim.rprev_vec = sim.r_vec - sim.v_vec * sim.dt # Keep track of the best score bestScore = 0 # and its corresponding binarized image and parameters bestImage, bestParams = 0, 0 hasReachedPericenter = False # Run until tmax = 25 for i in range(25001): sim.step() if i%100==0: # Every $\Delta t = 0.1$ # Check if we are close to pericenter centers = sim.r_vec[sim.type[:,0] == 'center'] if np.linalg.norm(centers[0] - centers[1]) < 1.3*rmin: hasReachedPericenter = True # Do not evaluate the f1-score until pericenter. if not hasReachedPericenter: continue # Check multiple (steps) viewing directions at random localBestScore = 0 localBestImage, localBestParams = 0, 0 for j in range(steps): # The viewing directions are isotropically distributed theta = np.arccos(np.random.uniform(-1, 1)) phi = np.random.uniform(0, 2*np.pi) # Rotation along line of sight view = np.random.uniform(0, 2*np.pi) scale = np.random.uniform(0.6, 1.0) x = np.random.uniform(-1.0, 1.0) # Offsets y = np.random.uniform(-1.0, 1.0) # Get images for each galaxy and compute their score separately im1 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=0) im2 = simToBitmap(sim, theta, phi, view, scale, x, y, galaxy=1) score = bitmapScore(GT1, im1) + bitmapScore(GT2, im2) if score > localBestScore: localBestScore = score localBestImage = [im1,im2] localBestParams = [i, theta, phi, view, scale, x, y] if bestScore < localBestScore: bestScore = localBestScore bestImage = localBestImage bestParams = localBestParams if plot: sim.save('annealing', type='2D') print('Best score for this attempt', bestScore) print('using viewing parameters', bestParams) return bestScore
def bitmapScore(im1, im2)
-
Computes the bitmap score between two images. This is the f1-score but we allow the algorithm to attempt to improve the score by shifting the images. The algorithm is implemented recursively.
Parameters
im1
:array
- The ground truth nxm image.
im2
:array
- The candidate nxm imgae.
_prev
- Used internally for recursion.
_bestscore
- Used internally for recursion.
_zeros
- Used internally for recursion.
Returns
f1-score for the two images.
Source code
@jit def bitmapScore(im1, im2, _prev=None, _bestscore=None, _zeros=None): """Computes the bitmap score between two images. This is the f1-score but we allow the algorithm to attempt to improve the score by shifting the images. The algorithm is implemented recursively. Parameters: im1 (array): The ground truth nxm image. im2 (array): The candidate nxm imgae. _prev: Used internally for recursion. _bestscore: Used internally for recursion. _zeros: Used internally for recursion. Returns: f1-score for the two images. """ # When the function is called externally, initialize an array of zeros # and compute the score for no shifting. The zeros array is used for # performance to only create a new array once. if _bestscore is None: _bestscore = bitmapScoreAlgo(im1, im2) _zeros = np.zeros_like(im2) # Attempt to improve the score by shifting the image in a direction # Keeping track of _prev allows to not 'go back' and undo and attempt # to undo a shift needlessly. if _prev is not 0: # try left shifted = shiftLeft(im2, _zeros) score = bitmapScoreAlgo(im1, shifted) if score > _bestscore: return bitmapScore(im1, shifted, _prev=1, _bestscore=score, _zeros=_zeros) if _prev is not 1: # try right shifted = shiftRight(im2, _zeros) score = bitmapScoreAlgo(im1, shifted) if score > _bestscore: return bitmapScore(im1, shifted, _prev=0, _bestscore=score, _zeros=_zeros) if _prev is not 2: # try top shifted = shiftTop(im2, _zeros) score = bitmapScoreAlgo(im1, shifted) if score > _bestscore: return bitmapScore(im1, shifted, _prev=3, _bestscore=score, _zeros=_zeros) if _prev is not 3: # try bottom shifted = shiftBottom(im2, _zeros) score = bitmapScoreAlgo(im1, shifted) if score > _bestscore: return bitmapScore(im1, shifted, _prev=2, _bestscore=score, _zeros=_zeros) # Return _bestscore if shifting did not improve (local maximum). return _bestscore
def bitmapScoreAlgo(im1, im2)
-
Computes the f1 score betwen two binarized images
Parameters
- im1 (arr): nxm ground truth image im2 (arr): nxm candidate image
Returns
- f1 score
Source code
@jit(nopython=True) # Numba annotation def bitmapScoreAlgo(im1, im2): """Computes the f1 score betwen two binarized images Parameters im1 (arr): nxm ground truth image im2 (arr): nxm candidate image Returns f1 score """ TP = np.sum((im1==1.0) & (im2==1.0)) TN = np.sum((im1==0.0) & (im2==0.0)) FP = np.sum((im1==0.0) & (im2==1.0)) FN = np.sum((im1==1.0) & (im2==0.0)) if TP==0: return 0 precision = TP/(TP+FP) recall = TP/(TP+FN) return 2*precision*recall / (precision + recall)
def shiftBottom(im, im2)
-
Shifts an image by one pixel downwards.
Parameters
im
:arr
- the nxm image to shift by one pixel.
im2
:arr
- an nxm array where the new image will be placed.
Returns
A reference to im2
Source code
@jit(nopython=True) def shiftBottom(im, im2): """Shifts an image by one pixel downwards. Parameters: im (arr): the nxm image to shift by one pixel. im2 (arr): an nxm array where the new image will be placed. Returns: A reference to im2 """ im2[1:] = im[:-1] im2[0] = 0 return im2
def shiftLeft(im, im2)
-
Shifts an image by one pixel to the left.
Source code
@jit(nopython=True) def shiftLeft(im, im2): """Shifts an image by one pixel to the left.""" im2[:,1:] = im[:,:-1] im2[:,0] = 0 return im2
def shiftRight(im, im2)
-
Shifts an image by one pixel to the right.
Source code
@jit(nopython=True) def shiftRight(im, im2): """Shifts an image by one pixel to the right.""" im2[:,:-1] = im[:,1:] im2[:,-1] = 0 return im2
def shiftTop(im, im2)
-
Shifts an image by one pixel upwards.
Source code
@jit(nopython=True) def shiftTop(im, im2): """Shifts an image by one pixel upwards.""" im2[:-1] = im[1:] im2[-1] = 0 return im2
def simToBitmap(sim, theta, phi, view, scale, x, y, galaxy)
-
Obtain a bitmap of one galaxy as viewed from a given direction. The binning has been chosen so that the scale and the offset (x,y) are expected to be approximately 1 and (0, 0) respectively.
Parameters: sim (Simulation): Simulation to project. theta (float): polar angle of viewing direction. phi (float): azimuthal angle of viewing direction. view (float): rotation angle along viewing direction. scale (float): scaling factor. x (float): x offset y (float): y offset galaxy (int): galaxy to plot. Either 0, or 1. They are assumed
to have the same number of particles.Source code
def simToBitmap(sim, theta, phi, view, scale, x, y, galaxy): """Obtain a bitmap of one galaxy as viewed from a given direction. The binning has been chosen so that the scale and the offset (x,y) are expected to be approximately 1 and (0, 0) respectively. Parameters: sim (Simulation): Simulation to project. theta (float): polar angle of viewing direction. phi (float): azimuthal angle of viewing direction. view (float): rotation angle along viewing direction. scale (float): scaling factor. x (float): x offset y (float): y offset galaxy (int): galaxy to plot. Either 0, or 1. They are assumed to have the same number of particles. """ # Obtain components in new x',y' plane r_vec = (sim.project(theta, phi, view) - [[x+.12,y+1.3]]) * scale # Select a single galaxy. We match them separately in the algorithm. if galaxy==0: r_vec = r_vec[2:len(r_vec)//2-1] #omit central masses if galaxy==1: r_vec = r_vec[len(r_vec)//2-1:] # Use a fast histogram, much faster than numpy () H = histogram2d(r_vec[:,0], r_vec[:,1], range=[[-5,5], [-5,5]], bins=(50, 50)) im = np.zeros((50, 50)) H = convolve2d(H, np.ones((2,2)), mode='same') # Smooth the image im[H>=1] = 1 # Binary the image return im