|  | @@ -0,0 +1,420 @@
 | 
	
		
			
				|  |  | +"""Definition of the Simulation class and the Galaxy constructor."""
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import os
 | 
	
		
			
				|  |  | +import pickle
 | 
	
		
			
				|  |  | +import numpy as np
 | 
	
		
			
				|  |  | +import matplotlib.pyplot as plt
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +from utils import random_unit_vectors, cascade_round
 | 
	
		
			
				|  |  | +from distributions import PLUMMER, HERNQUIST, UNIFORM, EXP, NFW
 | 
	
		
			
				|  |  | +import acceleration
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +##############################################################################
 | 
	
		
			
				|  |  | +##############################################################################
 | 
	
		
			
				|  |  | +class Simulation:
 | 
	
		
			
				|  |  | +    """"Main class for the gravitational simulation.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Attributes:
 | 
	
		
			
				|  |  | +        r_vec (array): position of the particles in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        rprev_vec (array): position of the particles in the previous timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        v_vec (array): velocity in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        a_vec (array): acceleration in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        mass (array): mass of each particle in the simulation.
 | 
	
		
			
				|  |  | +            Shape: (number of particles,)
 | 
	
		
			
				|  |  | +        type (array): non-unique identifier for each particle.
 | 
	
		
			
				|  |  | +            Shape: (number of particles,)
 | 
	
		
			
				|  |  | +        tracks (array): list of positions through the simulation for central
 | 
	
		
			
				|  |  | +            masses. Shape: (tracked particles, n+1, 3).
 | 
	
		
			
				|  |  | +        CONFIG (array): configuration used to create the simulation.
 | 
	
		
			
				|  |  | +            It will be saved along the state of the simulation.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        dt (float): timestep of the simulation
 | 
	
		
			
				|  |  | +        n (int): current timestep. Initialized as n=0.
 | 
	
		
			
				|  |  | +        soft (float): softening length used by the simulation.
 | 
	
		
			
				|  |  | +        verbose (boolean): When True progress statements will be printed.
 | 
	
		
			
				|  |  | +    """
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, dt, soft, verbose, CONFIG, method, **kwargs):
 | 
	
		
			
				|  |  | +        """Constructor for the Simulation class.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Arguments:
 | 
	
		
			
				|  |  | +            dt (float): timestep of the simulation
 | 
	
		
			
				|  |  | +            n (int): current timestep. Initialized as n=0.
 | 
	
		
			
				|  |  | +            soft (float): softening length used by the simulation.
 | 
	
		
			
				|  |  | +            verbose (bool): When True progress statements will be printed.
 | 
	
		
			
				|  |  | +            CONFIG (dict): configuration file used to create the simulation.
 | 
	
		
			
				|  |  | +            method (string): Optional. Algorithm to use when computing the 
 | 
	
		
			
				|  |  | +                gravitational forces. One of 'bruteForce', 'bruteForce_numba',
 | 
	
		
			
				|  |  | +                'bruteForce_numbaopt', 'bruteForce_CPP', 'barnesHut_CPP'.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        self.n = 0
 | 
	
		
			
				|  |  | +        self.t = 0
 | 
	
		
			
				|  |  | +        self.dt = dt
 | 
	
		
			
				|  |  | +        self.soft = soft
 | 
	
		
			
				|  |  | +        self.verbose = verbose
 | 
	
		
			
				|  |  | +        self.CONFIG = CONFIG
 | 
	
		
			
				|  |  | +        # Initialize empty arrays for all necessary properties
 | 
	
		
			
				|  |  | +        self.r_vec = np.empty((0, 3))
 | 
	
		
			
				|  |  | +        self.v_vec = np.empty((0, 3))
 | 
	
		
			
				|  |  | +        self.a_vec = np.empty((0, 3))
 | 
	
		
			
				|  |  | +        self.mass = np.empty((0,))
 | 
	
		
			
				|  |  | +        self.type = np.empty((0, 2))
 | 
	
		
			
				|  |  | +        algorithms = {
 | 
	
		
			
				|  |  | +            'bruteForce': acceleration.bruteForce,
 | 
	
		
			
				|  |  | +            'bruteForceNumba': acceleration.bruteForceNumba,
 | 
	
		
			
				|  |  | +            'bruteForceNumbaOptimized': acceleration.bruteForceNumbaOptimized,
 | 
	
		
			
				|  |  | +            'bruteForceCPP': acceleration.bruteForceCPP,
 | 
	
		
			
				|  |  | +            'barnesHutCPP': acceleration.barnesHutCPP
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            self.acceleration = algorithms[method]
 | 
	
		
			
				|  |  | +        except: raise Exception("Method '{}' unknown".format(method))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def add(self, body):
 | 
	
		
			
				|  |  | +        """Add a body to the simulation. It must expose the public attributes
 | 
	
		
			
				|  |  | +           body.r_vec, body.v_vec, body.a_vec, body.type, body.mass.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Arguments:
 | 
	
		
			
				|  |  | +            body: Object to be added to the simulation (e.g. a Galaxy object)
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # Extend all relevant attributes by concatenating the body
 | 
	
		
			
				|  |  | +        for name in ['r_vec', 'v_vec', 'a_vec', 'type', 'mass']:
 | 
	
		
			
				|  |  | +            simattr, bodyattr = getattr(self, name), getattr(body, name)
 | 
	
		
			
				|  |  | +            setattr(self, name, np.concatenate([simattr, bodyattr], axis=0))
 | 
	
		
			
				|  |  | +        # Order based on mass
 | 
	
		
			
				|  |  | +        order = np.argsort(-self.mass)
 | 
	
		
			
				|  |  | +        for name in ['r_vec', 'v_vec', 'a_vec', 'type', 'mass']: 
 | 
	
		
			
				|  |  | +            setattr(self, name, getattr(self, name)[order])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Update the list of objects to keep track of
 | 
	
		
			
				|  |  | +        self.tracks = np.empty((np.sum(self.type[:,0]=='center'), 0, 3))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def step(self):
 | 
	
		
			
				|  |  | +        """Perform a single step of the simulation.
 | 
	
		
			
				|  |  | +           Makes use of a 4th order Verlet integrator.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # Calculate the acceleration
 | 
	
		
			
				|  |  | +        self.a_vec = self.acceleration(self.r_vec, self.mass, soft=self.soft)
 | 
	
		
			
				|  |  | +        # Update the state using the Verlet algorithm
 | 
	
		
			
				|  |  | +        # (A custom algorithm is written mainly for learning purposes)
 | 
	
		
			
				|  |  | +        self.r_vec, self.rprev_vec = (2*self.r_vec - self.rprev_vec
 | 
	
		
			
				|  |  | +            + self.a_vec * self.dt**2, self.r_vec)
 | 
	
		
			
				|  |  | +        self.n += 1
 | 
	
		
			
				|  |  | +        # Update tracks
 | 
	
		
			
				|  |  | +        self.tracks = np.concatenate([self.tracks,
 | 
	
		
			
				|  |  | +            self.r_vec[self.type[:,0]=='center'][:,np.newaxis]], axis=1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def run(self, tmax, saveEvery, outputFolder, **kwargs):
 | 
	
		
			
				|  |  | +        """Run the galactic simulation.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Attributes:
 | 
	
		
			
				|  |  | +            tmax (float): Time to which the simulation will run to.
 | 
	
		
			
				|  |  | +                This is measured here since the start of the simulation,
 | 
	
		
			
				|  |  | +                not since pericenter.
 | 
	
		
			
				|  |  | +            saveEvery (int): The state is saved every saveEvery steps.
 | 
	
		
			
				|  |  | +            outputFolder (string): It will be saved to /data/outputFolder/
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # When the simulation starts, intialize self.rprev_vec
 | 
	
		
			
				|  |  | +        self.rprev_vec = self.r_vec - self.v_vec * self.dt
 | 
	
		
			
				|  |  | +        if self.verbose: print('Simulation starting. Bon voyage!')
 | 
	
		
			
				|  |  | +        while(self.t < tmax):
 | 
	
		
			
				|  |  | +            self.step()
 | 
	
		
			
				|  |  | +            if(self.n % saveEvery == 0):
 | 
	
		
			
				|  |  | +                self.save('data/{}'.format(outputFolder))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        print('Simulation complete.')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def save(self, outputFolder):
 | 
	
		
			
				|  |  | +        """Save the state of the simulation to the outputFolder.
 | 
	
		
			
				|  |  | +           Two files are saved:
 | 
	
		
			
				|  |  | +                sim{self.n}.pickle: serializing the state.
 | 
	
		
			
				|  |  | +                sim{self.n}.png: a simplified 2D plot of x, y.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # Create the output folder if it doesn't exist
 | 
	
		
			
				|  |  | +        if not os.path.exists(outputFolder): os.makedirs(outputFolder)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Compute some useful quantities
 | 
	
		
			
				|  |  | +        # v_vec is not required by the integrator, but useful
 | 
	
		
			
				|  |  | +        self.v_vec = (self.r_vec - self.rprev_vec)/self.dt
 | 
	
		
			
				|  |  | +        self.t = self.n * self.dt # prevents numerical rounding errors
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Serialize state
 | 
	
		
			
				|  |  | +        file = open(outputFolder+'/data{}.pickle'.format(self.n), "wb")
 | 
	
		
			
				|  |  | +        pickle.dump({'r_vec': self.r_vec, 'v_vec': self.v_vec,
 | 
	
		
			
				|  |  | +                     'type': self.type, 'mass': self.mass,
 | 
	
		
			
				|  |  | +                     'CONFIG': self.CONFIG, 't': self.t,
 | 
	
		
			
				|  |  | +                     'tracks': self.tracks}, file)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Save simplified plot of the current state.
 | 
	
		
			
				|  |  | +        # Its main use is to detect ill-behaved situations early on.
 | 
	
		
			
				|  |  | +        fig = plt.figure()
 | 
	
		
			
				|  |  | +        plt.xlim(-5, 5); plt.ylim(-5, 5); plt.axis('equal')
 | 
	
		
			
				|  |  | +        # Dark halo is plotted in red, disk in blue, bulge in green
 | 
	
		
			
				|  |  | +        PLTCON = [('dark', 'r', 0.3), ('disk', 'b', 1.0), ('bulge', 'g', 0.5)]
 | 
	
		
			
				|  |  | +        for type_, c, a in PLTCON: 
 | 
	
		
			
				|  |  | +            plt.scatter(self.r_vec[self.type[:,0]==type_][:,0],
 | 
	
		
			
				|  |  | +                self.r_vec[self.type[:,0]==type_][:,1], s=0.1, c=c, alpha=a)
 | 
	
		
			
				|  |  | +        # Central mass as a magenta star 
 | 
	
		
			
				|  |  | +        plt.scatter(self.r_vec[self.type[:,0]=='center'][:,0],
 | 
	
		
			
				|  |  | +            self.r_vec[self.type[:,0]=='center'][:,1], s=100, marker="*", c='m')
 | 
	
		
			
				|  |  | +        # Save to png file
 | 
	
		
			
				|  |  | +        fig.savefig(outputFolder+'/sim{}.png'.format(self.n), dpi=150)
 | 
	
		
			
				|  |  | +        plt.close(fig)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def project(self, theta, phi, view=0):
 | 
	
		
			
				|  |  | +        """Projects the 3D simulation onto a plane as viewed from the
 | 
	
		
			
				|  |  | +           direction described by the (theta, phi, view). Angles in radians.
 | 
	
		
			
				|  |  | +           (This is used by the simulated annealing algorithm)
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +        Parameters:
 | 
	
		
			
				|  |  | +            theta (float): polar angle.
 | 
	
		
			
				|  |  | +            phi (float): azimuthal angle.
 | 
	
		
			
				|  |  | +            view (float): rotation along line of sight.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        M1 = np.array([[np.cos(phi), np.sin(phi), 0],
 | 
	
		
			
				|  |  | +                       [-np.sin(phi), np.cos(phi), 0],
 | 
	
		
			
				|  |  | +                       [0, 0, 1]])
 | 
	
		
			
				|  |  | +        M2 = np.array([[1, 0, 0],
 | 
	
		
			
				|  |  | +                       [0, np.cos(theta), np.sin(theta)],
 | 
	
		
			
				|  |  | +                       [0, -np.sin(theta), np.cos(theta)]])
 | 
	
		
			
				|  |  | +        M3 = np.array([[np.cos(view), np.sin(view), 0],
 | 
	
		
			
				|  |  | +                       [-np.sin(view), np.cos(view), 0],
 | 
	
		
			
				|  |  | +                       [0, 0, 1]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        M = np.matmul(M1, np.matmul(M2, M3)) # combine rotations
 | 
	
		
			
				|  |  | +        r = np.tensordot(self.r_vec, M, axes=[1, 0])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return r[:,0:2]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def setOrbit(self, galaxy1, galaxy2, e, rmin, R0):
 | 
	
		
			
				|  |  | +        """Sets the two galaxies galaxy1, galaxy2 in an orbit.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Parameters:
 | 
	
		
			
				|  |  | +            galaxy1 (Galaxy): 1st galaxy (main)
 | 
	
		
			
				|  |  | +            galaxy2 (Galaxy): 2nd galaxy (companion)
 | 
	
		
			
				|  |  | +            e: eccentricity of the orbit
 | 
	
		
			
				|  |  | +            rmin: distance of closest approach
 | 
	
		
			
				|  |  | +            R0: initial separation
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        m1, m2 = np.sum(galaxy1.mass), np.sum(galaxy2.mass)
 | 
	
		
			
				|  |  | +        # Relevant formulae:
 | 
	
		
			
				|  |  | +        # $r_0 = r (1 + e) \cos(\phi)$, where $r_0$ ($\neq R_0$) is the semi-latus rectum
 | 
	
		
			
				|  |  | +        # $r_0 = r_\textup{min} (1 + e)$
 | 
	
		
			
				|  |  | +        # $v^2 = G M (2/r - 1/a)$, where a is the semimajor axis
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Solve the reduced two-body problem with reduced mass $\mu$ (mu)
 | 
	
		
			
				|  |  | +        M = m1 + m2
 | 
	
		
			
				|  |  | +        r0 = rmin * (1 + e)
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            phi = np.arccos((r0/R0 - 1) / e) # inverting the orbit equation
 | 
	
		
			
				|  |  | +            phi = -np.abs(phi) # Choose negative (incoming) angle
 | 
	
		
			
				|  |  | +            ainv = (1 - e) / rmin # ainv = $1/a$, as a may be infinite
 | 
	
		
			
				|  |  | +            v = np.sqrt(M * (2/R0 - ainv))
 | 
	
		
			
				|  |  | +            # Finally, calculate the angle of motion. angle = tan(dy/dx)
 | 
	
		
			
				|  |  | +            # $dy/dx = ((dr/d\phi) sin(\phi) + r \cos(\phi))/((dr/d\phi) cos(\phi) - r \sin(\phi))$
 | 
	
		
			
				|  |  | +            dy = R0/r0 * e * np.sin(phi)**2 + np.cos(phi)
 | 
	
		
			
				|  |  | +            dx = R0/r0 * e * np.sin(phi) * np.cos(phi) - np.sin(phi)
 | 
	
		
			
				|  |  | +            vangle = np.arctan2(dy, dx)
 | 
	
		
			
				|  |  | +        except: raise Exception('''The orbital parameters cannot be satisfied.
 | 
	
		
			
				|  |  | +            For elliptical orbits check that R0 is posible (<rmax).''')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # We now need the actual motion of each body
 | 
	
		
			
				|  |  | +        R_vec = np.array([[R0*np.cos(phi), R0*np.sin(phi), 0.]])
 | 
	
		
			
				|  |  | +        V_vec = np.array([[v*np.cos(vangle), v*np.sin(vangle), 0.]])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        galaxy1.r_vec += m2/M * R_vec
 | 
	
		
			
				|  |  | +        galaxy1.v_vec += m2/M * V_vec
 | 
	
		
			
				|  |  | +        galaxy2.r_vec += -m1/M * R_vec
 | 
	
		
			
				|  |  | +        galaxy2.v_vec += -m1/M * V_vec
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # Explicitely add the galaxies to the simulation
 | 
	
		
			
				|  |  | +        self.add(galaxy1)
 | 
	
		
			
				|  |  | +        self.add(galaxy2)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if self.verbose: print('Galaxies set in orbit.')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +##############################################################################
 | 
	
		
			
				|  |  | +##############################################################################
 | 
	
		
			
				|  |  | +class Galaxy():
 | 
	
		
			
				|  |  | +    """"Helper class for creating galaxies.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Attributes:
 | 
	
		
			
				|  |  | +        r_vec (array): position of the particles in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        v_vec (array): velocity in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        a_vec (array): acceleration in the current timestep.
 | 
	
		
			
				|  |  | +            Shape: (number of particles, 3)
 | 
	
		
			
				|  |  | +        mass (array): mass of each particle in the simulation.
 | 
	
		
			
				|  |  | +            Shape: (number of particles,)
 | 
	
		
			
				|  |  | +        type (array): non-unique identifier for each particle.
 | 
	
		
			
				|  |  | +            Shape: (number of particles,)    """
 | 
	
		
			
				|  |  | +    def __init__(self, orientation, centralMass, bulge, disk, halo, sim):
 | 
	
		
			
				|  |  | +        """Constructor for the Galaxy class.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +           Parameters:
 | 
	
		
			
				|  |  | +                orientation (tupple): (inclination i, argument of pericenter w)
 | 
	
		
			
				|  |  | +                centralMass (float): mass at the center of the galaxy
 | 
	
		
			
				|  |  | +                bulge (dict): passed to the addBulge method.
 | 
	
		
			
				|  |  | +                disk (dict): passed to the addDisk method.
 | 
	
		
			
				|  |  | +                halo (dict): passed to the addHalo method.
 | 
	
		
			
				|  |  | +                sim (Simulation): simulation object
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        if sim.verbose: print('Initializing galaxy')
 | 
	
		
			
				|  |  | +        # Build the central mass
 | 
	
		
			
				|  |  | +        self.r_vec = np.zeros((1, 3))
 | 
	
		
			
				|  |  | +        self.v_vec = np.zeros((1, 3))
 | 
	
		
			
				|  |  | +        self.a_vec = np.zeros((1, 3))
 | 
	
		
			
				|  |  | +        self.mass = np.array([centralMass])
 | 
	
		
			
				|  |  | +        self.type = np.array([['center', 0]])
 | 
	
		
			
				|  |  | +        # Build the other components
 | 
	
		
			
				|  |  | +        self.addBulge(**bulge)
 | 
	
		
			
				|  |  | +        if sim.verbose: print('Bulge created.')
 | 
	
		
			
				|  |  | +        self.addDisk(**disk)
 | 
	
		
			
				|  |  | +        if sim.verbose: print('Disk created.')
 | 
	
		
			
				|  |  | +        self.addHalo(**halo)
 | 
	
		
			
				|  |  | +        if sim.verbose: print('Halo created.')
 | 
	
		
			
				|  |  | +        # Correct particles velocities to generate circular orbits
 | 
	
		
			
				|  |  | +        # $a_\textup{centripetal} = v^2/r$
 | 
	
		
			
				|  |  | +        a_vec = sim.acceleration(self.r_vec, self.mass, soft=sim.soft)
 | 
	
		
			
				|  |  | +        a = np.linalg.norm(a_vec, axis=-1, keepdims=True)
 | 
	
		
			
				|  |  | +        r = np.linalg.norm(self.r_vec, axis=-1, keepdims=True)
 | 
	
		
			
				|  |  | +        v = np.linalg.norm(self.v_vec[1:], axis=-1, keepdims=True)
 | 
	
		
			
				|  |  | +        direction_unit = self.v_vec[1:]/v
 | 
	
		
			
				|  |  | +        # Set orbital velocities (except for central mass)
 | 
	
		
			
				|  |  | +        self.v_vec[1:] = np.sqrt(a[1:] * r[1:]) * direction_unit
 | 
	
		
			
				|  |  | +        self.a_vec = np.zeros_like(self.r_vec)
 | 
	
		
			
				|  |  | +        # Rotate the galaxy into its correct orientation
 | 
	
		
			
				|  |  | +        self.rotate(*(np.array(orientation)/360 * 2*np.pi))
 | 
	
		
			
				|  |  | +        if sim.verbose: print('Galaxy set in rotation and oriented.')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def addBulge(self, model, totalMass, particles, l):
 | 
	
		
			
				|  |  | +        """Adds a bulge to the galaxy.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Parameters:
 | 
	
		
			
				|  |  | +                model (string): parametrization of the bulge.
 | 
	
		
			
				|  |  | +                    'plummer' and 'hernquist' are supported.
 | 
	
		
			
				|  |  | +                totalMass (float): total mass of the bulge
 | 
	
		
			
				|  |  | +                particles (int): number of particles in the bulge
 | 
	
		
			
				|  |  | +                l (float): characteristic length scale of the model.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        if particles == 0: return None
 | 
	
		
			
				|  |  | +        # Divide the mass equally among all particles
 | 
	
		
			
				|  |  | +        mass = np.ones(particles) * totalMass/particles
 | 
	
		
			
				|  |  | +        self.mass = np.concatenate([self.mass, mass], axis=0)
 | 
	
		
			
				|  |  | +        # Create particles according to the radial distribution from model
 | 
	
		
			
				|  |  | +        if model == 'plummer':
 | 
	
		
			
				|  |  | +            r = PLUMMER.ppf(np.random.rand(particles), scale=l)
 | 
	
		
			
				|  |  | +        elif model == 'hernquist':
 | 
	
		
			
				|  |  | +            r = HERNQUIST.ppf(np.random.rand(particles), scale=l)
 | 
	
		
			
				|  |  | +        else: raise Exception("""Bulge distribution not allowed.
 | 
	
		
			
				|  |  | +                    'plummer' and 'hernquist' are the supported values""")
 | 
	
		
			
				|  |  | +        r_vec = r[:,np.newaxis] * random_unit_vectors(size=particles)
 | 
	
		
			
				|  |  | +        self.r_vec = np.concatenate([self.r_vec, r_vec], axis=0)
 | 
	
		
			
				|  |  | +        # Set them orbitting along random directions normal to r_vec
 | 
	
		
			
				|  |  | +        v_vec = np.cross(r_vec, random_unit_vectors(size=particles))
 | 
	
		
			
				|  |  | +        self.v_vec = np.concatenate([self.v_vec, v_vec], axis=0)
 | 
	
		
			
				|  |  | +        # Label the particles
 | 
	
		
			
				|  |  | +        type_ = [['bulge', 0]]*particles
 | 
	
		
			
				|  |  | +        self.type = np.concatenate([self.type, type_], axis=0)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def addDisk(self, model, totalMass, particles, l):
 | 
	
		
			
				|  |  | +        """Adds a disk to the galaxy.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Parameters:
 | 
	
		
			
				|  |  | +                model (string): parametrization of the disk.
 | 
	
		
			
				|  |  | +                    'rings', 'uniform' and 'exp' are supported.
 | 
	
		
			
				|  |  | +                totalMass (float): total mass of the bulge
 | 
	
		
			
				|  |  | +                particles (int): number of particles in the bulge
 | 
	
		
			
				|  |  | +                l: fot 'exp' and 'uniform' characteristic length of the
 | 
	
		
			
				|  |  | +                    model. For 'rings' tupple of the form (inner radius,
 | 
	
		
			
				|  |  | +                    outer radius, number of rings)
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        if particles == 0: return None
 | 
	
		
			
				|  |  | +        # Create particles according to the radial distribution from model
 | 
	
		
			
				|  |  | +        if model == 'uniform':
 | 
	
		
			
				|  |  | +            r = UNIFORM.ppf(np.random.rand(particles), scale=l)
 | 
	
		
			
				|  |  | +            type_ = [['disk', 0]]*particles
 | 
	
		
			
				|  |  | +            r_vec = r[:,np.newaxis] * random_unit_vectors(particles, '2D')
 | 
	
		
			
				|  |  | +            self.type = np.concatenate([self.type, type_], axis=0)
 | 
	
		
			
				|  |  | +        elif model == 'rings':
 | 
	
		
			
				|  |  | +            # l = [inner radius, outter radius, number of rings]
 | 
	
		
			
				|  |  | +            distances = np.linspace(*l)
 | 
	
		
			
				|  |  | +            # Aim for roughly constant areal density
 | 
	
		
			
				|  |  | +            # Cascade rounding preserves the total number of particles
 | 
	
		
			
				|  |  | +            perRing = cascade_round(particles * distances / np.sum(distances))
 | 
	
		
			
				|  |  | +            particles = int(np.sum(perRing)) # prevents numerical errors
 | 
	
		
			
				|  |  | +            r_vec = np.empty((0, 3))
 | 
	
		
			
				|  |  | +            for d, n, i in zip(distances, perRing, range(l[2])):
 | 
	
		
			
				|  |  | +                type_ = [['disk', i+1]]*int(n)
 | 
	
		
			
				|  |  | +                self.type = np.concatenate([self.type, type_], axis=0)
 | 
	
		
			
				|  |  | +                phi = np.linspace(0, 2 * np.pi, n, endpoint=False)
 | 
	
		
			
				|  |  | +                ringr = d * np.array([[np.cos(i), np.sin(i), 0] for i in phi])
 | 
	
		
			
				|  |  | +                r_vec = np.concatenate([r_vec, ringr], axis=0)
 | 
	
		
			
				|  |  | +        elif model == 'exp':
 | 
	
		
			
				|  |  | +            r = EXP.ppf(np.random.rand(particles), scale=l)
 | 
	
		
			
				|  |  | +            r_vec = r[:,np.newaxis] * random_unit_vectors(particles, '2D')
 | 
	
		
			
				|  |  | +            type_ = [['disk', 0]]*particles
 | 
	
		
			
				|  |  | +            self.type = np.concatenate([self.type, type_], axis=0)
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            raise Exception("""Disk distribution not allowed.
 | 
	
		
			
				|  |  | +                    'uniform', 'rings' and 'exp' are the supported values""")
 | 
	
		
			
				|  |  | +        self.r_vec = np.concatenate([self.r_vec, r_vec], axis=0)
 | 
	
		
			
				|  |  | +        # Divide the mass equally among all particles
 | 
	
		
			
				|  |  | +        mass = np.ones(particles) * totalMass/particles
 | 
	
		
			
				|  |  | +        self.mass = np.concatenate([self.mass, mass], axis=0)
 | 
	
		
			
				|  |  | +        # Set them orbitting along the spin plane
 | 
	
		
			
				|  |  | +        v_vec = np.cross(r_vec, [0, 0, 1])
 | 
	
		
			
				|  |  | +        self.v_vec = np.concatenate([self.v_vec, v_vec], axis=0)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def addHalo(self, model, totalMass, particles, rs):
 | 
	
		
			
				|  |  | +        """Adds a halo to the galaxy.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            Parameters:
 | 
	
		
			
				|  |  | +                model (string): parametrization of the halo.
 | 
	
		
			
				|  |  | +                    Only 'NFW' is supported.
 | 
	
		
			
				|  |  | +                totalMass (float): total mass of the halo
 | 
	
		
			
				|  |  | +                particles (int): number of particles in the halo
 | 
	
		
			
				|  |  | +                rs (float): characteristic length scale of the NFW profile.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        if particles == 0: return None
 | 
	
		
			
				|  |  | +        # Divide the mass equally among all particles
 | 
	
		
			
				|  |  | +        mass = np.ones(particles)*totalMass/particles
 | 
	
		
			
				|  |  | +        self.mass = np.concatenate([self.mass, mass], axis=0)
 | 
	
		
			
				|  |  | +        # Create particles according to the radial distribution from model
 | 
	
		
			
				|  |  | +        if model == 'NFW':
 | 
	
		
			
				|  |  | +            r = NFW.ppf(np.random.rand(particles), scale=rs)
 | 
	
		
			
				|  |  | +        else: raise Exception("""Bulge distribution not allowed.
 | 
	
		
			
				|  |  | +                    'plummer' and 'hernquist' are the supported values""")
 | 
	
		
			
				|  |  | +        r_vec = r[:,np.newaxis] * random_unit_vectors(size=particles)
 | 
	
		
			
				|  |  | +        self.r_vec = np.concatenate([self.r_vec, r_vec], axis=0)
 | 
	
		
			
				|  |  | +        # Orbit along random directions normal to the radial vector
 | 
	
		
			
				|  |  | +        v_vec = np.cross(r_vec, random_unit_vectors(size=particles))
 | 
	
		
			
				|  |  | +        self.v_vec = np.concatenate([self.v_vec, v_vec], axis=0)
 | 
	
		
			
				|  |  | +        # Label the particles
 | 
	
		
			
				|  |  | +        type_ = [['dark'], 0]*particles
 | 
	
		
			
				|  |  | +        self.type = np.concatenate([self.type, type_], axis=0)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def rotate(self, theta, phi):
 | 
	
		
			
				|  |  | +        """Rotates the galaxy so that its spin is along the (theta, phi)
 | 
	
		
			
				|  |  | +           direction.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Parameters:
 | 
	
		
			
				|  |  | +            theta (float): polar angle.
 | 
	
		
			
				|  |  | +            phi (float): azimuthal angle.
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        M1 = np.array([[1, 0, 0],
 | 
	
		
			
				|  |  | +                       [0, np.cos(theta), np.sin(theta)],
 | 
	
		
			
				|  |  | +                       [0, -np.sin(theta), np.cos(theta)]])
 | 
	
		
			
				|  |  | +        M2 = np.array([[np.cos(phi), np.sin(phi), 0],
 | 
	
		
			
				|  |  | +                       [-np.sin(phi), np.cos(phi), 0],
 | 
	
		
			
				|  |  | +                       [0, 0, 1]])
 | 
	
		
			
				|  |  | +        M = np.matmul(M1, M2) # combine rotations
 | 
	
		
			
				|  |  | +        self.r_vec = np.tensordot(self.r_vec, M, axes=[1, 0])
 | 
	
		
			
				|  |  | +        self.v_vec = np.tensordot(self.v_vec, M, axes=[1, 0])
 |