Exemplo n.º 1
0
def get_component_binary_elements(comp1, comp2):
    kep = Kepler(redirection = "none")
    kep.initialize_code()

    mass = comp1.mass + comp2.mass
    pos = comp2.position - comp1.position
    vel = comp2.velocity - comp1.velocity
    kep.initialize_from_dyn(mass, pos[0], pos[1], pos[2],
                            vel[0], vel[1], vel[2])
    a,e = kep.get_elements()
    r = kep.get_separation()
    E,J = kep.get_integrals()	# per unit reduced mass, note
    kep.stop()

    return mass,a,e,r,E
Exemplo n.º 2
0
def get_binary_elements(p):
    comp1 = p.child1
    comp2 = p.child2
    kep = Kepler(redirection="none")
    kep.initialize_code()

    mass = comp1.mass + comp2.mass
    pos = [comp2.x - comp1.x, comp2.y - comp1.y, comp2.z - comp1.z]
    vel = [comp2.vx - comp1.vx, comp2.vy - comp1.vy, comp2.vz - comp1.vz]
    kep.initialize_from_dyn(mass, pos[0], pos[1], pos[2], vel[0], vel[1],
                            vel[2])
    a, e = kep.get_elements()
    kep.stop()

    return mass, a, e
Exemplo n.º 3
0
def get_binary_elements(p):
    comp1 = p.child1
    comp2 = p.child2
    kep = Kepler(redirection = "none")
    kep.initialize_code()

    mass = comp1.mass + comp2.mass
    pos = [comp2.x-comp1.x, comp2.y-comp1.y, comp2.z-comp1.z]
    vel = [comp2.vx-comp1.vx, comp2.vy-comp1.vy, comp2.vz-comp1.vz]
    kep.initialize_from_dyn(mass, pos[0], pos[1], pos[2],
                            vel[0], vel[1], vel[2])
    a,e = kep.get_elements()
    kep.stop()

    return mass,a,e
Exemplo n.º 4
0
def get_component_binary_elements(comp1, comp2):
    kep = Kepler(redirection="none")
    kep.initialize_code()

    mass = comp1.mass + comp2.mass
    pos = comp2.position - comp1.position
    vel = comp2.velocity - comp1.velocity
    kep.initialize_from_dyn(mass, pos[0], pos[1], pos[2], vel[0], vel[1],
                            vel[2])
    a, e = kep.get_elements()
    r = kep.get_separation()
    E, J = kep.get_integrals()  # per unit reduced mass, note
    kep.stop()

    return mass, a, e, r, E
Exemplo n.º 5
0
def run_kepler(mass, semi, ecc, time):

    kep = Kepler(redirection='none')
    kep.initialize_code()

    kep.set_longitudinal_unit_vector(1.0, 1.0,
                                     0.0)
    kep.initialize_from_elements(mass, semi, ecc)
    a,e = kep.get_elements()
    p = kep.get_periastron()
    print "elements:", a, e, p
    kep.transform_to_time(time)
    x,y,z = kep.get_separation_vector()
    print "separation:", x,y,z
    x,y,z = kep.get_longitudinal_unit_vector()
    print "longitudinal:", x,y,z

    pos = [1, 0, 0] | nbody_system.length
    vel = [0, 0.5, 0] | nbody_system.speed
    kep.initialize_from_dyn(mass, pos[0], pos[1], pos[2],
                            vel[0], vel[1], vel[2])
    a,e = kep.get_elements()
    p = kep.get_periastron()
    print "elements:", a, e, p
    kep.transform_to_time(time)
    x,y,z = kep.get_separation_vector()
    print "separation:", x,y,z
    x,y,z = kep.get_velocity_vector()
    print "velocity:", x,y,z
    x,y,z = kep.get_longitudinal_unit_vector()
    print "longitudinal:", x,y,z

    kep.set_random(42)
    kep.make_binary_scattering(0.5 | nbody_system.mass,
                               0.5,
                               0.5 | nbody_system.mass,
                               0.0 | nbody_system.speed,
                               0.0 | nbody_system.length,
                               1.e-6,
                               0)
    kep.stop()
Exemplo n.º 6
0
def update_orb_elem(host_star, planets, converter=None, kepler_worker=None):
    if kepler_worker == None:
        if converter == None:
            tot_sys = Particles(particles=(host_star, planets))
            converter = nbody_system.nbody_to_si(tot_sys.mass.sum(),
                                                 2 * host_star.radius)
        kep_p = Kepler(unit_converter=converter, redirection='none')
        kep_p.initialize_code()
    else:
        kep_p = kepler_worker
    for planet in planets:
        total_mass = host_star.mass + planet.mass
        kep_pos = host_star.position - planet.position
        kep_vel = host_star.velocity - planet.velocity
        kep_p.initialize_from_dyn(total_mass, kep_pos[0], kep_pos[1],
                                  kep_pos[2], kep_vel[0], kep_vel[1],
                                  kep_vel[2])
        planet.semimajor_axis, planet.eccentricity = kep_p.get_elements()
        planet.period = kep_p.get_period()
        planet.true_anomaly, planet.mean_anomaly = kep_p.get_angles()
    if kepler_worker == None:
        kep_p.stop()
Exemplo n.º 7
0
def get_heirarchical_systems_from_set(bodies,
                                      kepler_workers=None,
                                      converter=None,
                                      RelativePosition=False):
    # Initialize Kepler
    if kepler_workers == None:
        if converter == None:
            converter = nbody_system.nbody_to_si(
                bodies.mass.sum(),
                2 * np.max(bodies.radius.number) | bodies.radius.unit)
        kep_p = Kepler(unit_converter=converter, redirection='none')
        kep_p.initialize_code()
        kep_s = Kepler(unit_converter=converter, redirection='none')
        kep_s.initialize_code()
    else:
        kep_p = kepler_workers[0]
        kep_s = kepler_workers[1]
    # Seperate Out Planets and Stars from Bodies
    stars, planets = util.get_stars(bodies), util.get_planets(bodies)
    num_stars, num_planets = len(stars), len(planets)
    # Initialize the Dictionary that Contains all Planetary Systems
    systems = {}
    # Initialize the List Used to Check Star IDs Against Already Classified Binaries
    binary_ids = []
    # Find Nearest Neighbors of the Set
    closest_neighbours = stars.nearest_neighbour()
    # Start Looping Through Stars to Find Bound Planets
    for index, star in enumerate(stars):
        # If the star is already in Binary_IDs, just go to the next star.
        if star.id in binary_ids:
            continue
        # If not, Set the System ID and Set-up Data Structure.
        system_id = star.id
        current_system = systems.setdefault(system_id, Particles())
        current_system.add_particle(star)
        noStellarHierarchy = False
        # If there is only one stars, there is obviously no stellar heirarchy in
        # the encounter that is occuring.
        if len(stars) == 1:
            noStellarHierarchy = True
        if not noStellarHierarchy:
            # Check to see if the Nearest Neighbor is Mutual
            star_neighbour_id = closest_neighbours[index].id
            neighbour_neighbour_id = closest_neighbours[
                stars.id == star_neighbour_id].id[0]
            for other_star in (stars - star):
                # Check to see if the two stars are bound.
                kep_s.initialize_from_dyn(
                    star.mass + other_star.mass, star.x - other_star.x,
                    star.y - other_star.y, star.z - other_star.z,
                    star.vx - other_star.vx, star.vy - other_star.vy,
                    star.vz - other_star.vz)
                a_s, e_s = kep_s.get_elements()
                print(star.id, other_star.id, e_s)
                # If they ARE NOT bound ...
                if e_s >= 1.0:
                    noStellarHierarchy = True
                # If they ARE bound ...
                else:
                    # If the star is the star's neighbour's neighbour and visa-versa, then proceed.
                    print(star.id, other_star.id, star_neighbour_id,
                          neighbour_neighbour_id)
                    if star.id == neighbour_neighbour_id and other_star.id == star_neighbour_id:
                        noStellarHierarchy = False
                        print("Binary composed of Star", star.id, "and Star",
                              other_star.id, "has been detected!")
                        current_system.add_particle(other_star)
                        binary_ids.append(star.id)
                        binary_ids.append(other_star.id)
                    else:
                        print(
                            "!!! Alert: Bound Stars are not closest neighbours ..."
                        )
                        print("!!! Current Star:", star.id, "| Other Star:",
                              other_star.id)
                        print("!!! CS's Neighbour:", star_neighbour_id, \
                              "| CS's Neighbour's Neighbour:", neighbour_neighbour_id)
    checked_planet_ids = []
    for KeyID in systems.keys():
        current_system = systems[KeyID]
        sys_stars = util.get_stars(current_system)
        noStellarHierarchy = False
        # If there is only one stars, there is obviously no stellar heirarchy in
        # the encounter that is occuring.
        if len(sys_stars) == 1:
            noStellarHierarchy = True
        for planet in planets:
            if planet.id in checked_planet_ids:
                continue
            star = sys_stars[sys_stars.id == KeyID][0]
            total_mass = star.mass + planet.mass
            kep_pos = star.position - planet.position
            kep_vel = star.velocity - planet.velocity
            kep_p.initialize_from_dyn(total_mass, kep_pos[0], kep_pos[1],
                                      kep_pos[2], kep_vel[0], kep_vel[1],
                                      kep_vel[2])
            a_p, e_p = kep_p.get_elements()
            P_p = kep_p.get_period()
            Ta_p, Ma_p = kep_p.get_angles()
            host_star_id = star.id
            if e_p < 1.0:
                # Check to See if The Planetary System is tied to a Stellar Binary
                # Note: Things get complicated if it is ...
                if noStellarHierarchy:
                    # Get Additional Information on Orbit
                    planet.semimajor_axis = a_p
                    planet.eccentricity = e_p
                    planet.period = P_p
                    planet.true_anomaly = Ta_p
                    planet.mean_anomaly = Ma_p
                    planet.host_star = star.id
                    # Add the Planet to the System Set
                    current_system.add_particle(planet)
                else:
                    # Handling for Planetary Systems in Stellar Heirarchical Structures
                    # Note: We check to see which other star in the current systems
                    #       have a better boundness with the planet and choose that
                    #       as the new host star.
                    for other_star in sys_stars - star:
                        total_mass = other_star.mass + planet.mass
                        kep_pos = other_star.position - planet.position
                        kep_vel = other_star.velocity - planet.velocity
                        kep_p.initialize_from_dyn(total_mass, kep_pos[0],
                                                  kep_pos[1], kep_pos[2],
                                                  kep_vel[0], kep_vel[1],
                                                  kep_vel[2])
                        a_p2, e_p2 = kep_p.get_elements()
                        # Check to see if the planet is more bound to 'star' or
                        # 'other_star'. If its more bound to 'other_star',
                        # set the attributes to the more bound object. This will
                        # replace *_p with the better values with each loop.
                        if e_p2 < e_p:
                            a_p = a_p2
                            e_p = e_p2
                            P_p = kep_p.get_period()
                            Ta_p, Ma_p = kep_p.get_angles()
                            host_star_id = other_star.id
                    planet.semimajor_axis = a_p
                    planet.eccentricity = e_p
                    planet.period = P_p
                    planet.true_anomaly = Ta_p
                    planet.mean_anomaly = Ma_p
                    planet.host_star = host_star_id
                    # Add the Planet to the System Set
                    current_system.add_particle(planet)
                checked_planet_ids.append(planet.id)
            elif not noStellarHierarchy:
                # Handling for Planetary Systems in Stellar Heirarchical Structures
                # Note: We check to see which other star in the current systems
                #       have a better boundness with the planet and choose that
                #       as the new host star.
                for other_star in sys_stars - star:
                    total_mass = other_star.mass + planet.mass
                    kep_pos = other_star.position - planet.position
                    kep_vel = other_star.velocity - planet.velocity
                    kep_p.initialize_from_dyn(total_mass, kep_pos[0],
                                              kep_pos[1], kep_pos[2],
                                              kep_vel[0], kep_vel[1],
                                              kep_vel[2])
                    a_p2, e_p2 = kep_p.get_elements()
                    # Check to see if the planet is more bound to 'star' or
                    # 'other_star'. If its more bound to 'other_star',
                    # set the attributes to the more bound object. This will
                    # replace *_p with the better values with each loop.
                    if e_p2 < e_p:
                        a_p = a_p2
                        e_p = e_p2
                        P_p = kep_p.get_period()
                        Ta_p, Ma_p = kep_p.get_angles()
                        host_star_id = other_star.id
                planet.semimajor_axis = a_p
                planet.eccentricity = e_p
                planet.period = P_p
                planet.true_anomaly = Ta_p
                planet.mean_anomaly = Ma_p
                planet.host_star = host_star_id
                # Add the Planet to the System Set
                current_system.add_particle(planet)
            else:
                print(
                    "!!! Alert: Planet is not bound nor is it bound to any other star."
                )
    if kepler_workers == None:
        kep_p.stop()
        kep_s.stop()
    return systems
Exemplo n.º 8
0
def get_planetary_systems_from_set(bodies,
                                   converter=None,
                                   RelativePosition=False):
    # Initialize Kepler
    if converter == None:
        converter = nbody_system.nbody_to_si(
            bodies.mass.sum(),
            2 * np.max(bodies.radius.number) | bodies.radius.unit)
    kep_p = Kepler(unit_converter=converter, redirection='none')
    kep_p.initialize_code()
    kep_s = Kepler(unit_converter=converter, redirection='none')
    kep_s.initialize_code()
    # Seperate Out Planets and Stars from Bodies
    stars, planets = util.get_stars(bodies), util.get_planets(bodies)
    num_stars, num_planets = len(stars), len(planets)
    # Initialize the Dictionary that Contains all Planetary Systems
    systems = {}
    # Start Looping Through Stars to Find Bound Planets
    for star in stars:
        system_id = star.id
        #star.semimajor_axis, star.eccentricity, star.period, star.true_anomaly, star.mean_anomaly, star.kep_energy, star.angular_momentum = \
        #    None, None, None, None, None, None, None
        current_system = systems.setdefault(system_id, Particles())
        current_system.add_particle(star)
        for planet in planets:
            total_mass = star.mass + planet.mass
            kep_pos = star.position - planet.position
            kep_vel = star.velocity - planet.velocity
            kep_p.initialize_from_dyn(total_mass, kep_pos[0], kep_pos[1],
                                      kep_pos[2], kep_vel[0], kep_vel[1],
                                      kep_vel[2])
            a_p, e_p = kep_p.get_elements()
            if e_p < 1.0:
                # Check to See if The Stellar System is a Binary
                # Note: Things get complicated if it is ...
                noStellarHeirarchy = False
                for other_star in (stars - star):
                    kep_s.initialize_from_dyn(
                        star.mass + other_star.mass, star.x - other_star.x,
                        star.y - other_star.y, star.z - other_star.z,
                        star.vx - other_star.vx, star.vy - other_star.vy,
                        star.vz - other_star.vz)
                    a_s, e_s = kep_s.get_elements()
                    r_apo = kep_s.get_apastron()
                    HillR = util.calc_HillRadius(a_s, e_s, other_star.mass,
                                                 star.mass)
                    if e_s >= 1.0 or HillR < r_apo:
                        noStellarHeirarchy = True
                    else:
                        noStellarHeirarchy = False
                if noStellarHeirarchy:
                    # Get Additional Information on Orbit
                    planet.semimajor_axis = a_p
                    planet.eccentricity = e_p
                    planet.period = kep_p.get_period()
                    planet.true_anomaly, planet.mean_anomaly = kep_p.get_angles(
                    )
                    #planet.kep_energy, planet.angular_momentum = kep_p.get_integrals()
                    # Add the Planet to the System Set
                    current_system.add_particle(planet)
                else:
                    # Handling for Planetary Systems in Stellar Heirarchical Structures
                    # Note: This is empty for now, maybe consider doing it by the heaviest bound stellar object as the primary.
                    pass
            else:
                continue
    kep_p.stop()
    kep_s.stop()
    return systems
Exemplo n.º 9
0
def compress_binary_components(comp1, comp2, scale):

    # Compress the two-body system consisting of comp1 and comp2 to
    # lie within distance scale of one another.

    pos1 = comp1.position
    pos2 = comp2.position
    sep12 = ((pos2-pos1)**2).sum()

    if sep12 > scale*scale:
        print '\ncompressing components', int(comp1.id.number), \
              'and', int(comp2.id.number), 'to separation', scale.number
        sys.stdout.flush()
        mass1 = comp1.mass
        mass2 = comp2.mass
        total_mass = mass1 + mass2
        vel1 = comp1.velocity
        vel2 = comp2.velocity
        cmpos = (mass1*pos1+mass2*pos2)/total_mass
        cmvel = (mass1*vel1+mass2*vel2)/total_mass

        # For now, create and delete a temporary kepler
        # process to handle the transformation.  Obviously
        # more efficient to define a single kepler at the
        # start of the calculation and reuse it.

        kep = Kepler(redirection = "none")
        kep.initialize_code()
        mass = comp1.mass + comp2.mass
        rel_pos = pos2 - pos1
        rel_vel = vel2 - vel1
        kep.initialize_from_dyn(mass,
                                rel_pos[0], rel_pos[1], rel_pos[2],
                                rel_vel[0], rel_vel[1], rel_vel[2])
        M,th = kep.get_angles()
        a,e = kep.get_elements()
        if e < 1:
            peri = a*(1-e)
            apo = a*(1+e)
        else:
            peri = a*(e-1)
            apo = 2*a		# OK - used ony to reset scale
        limit = peri + 0.01*(apo-peri)
        if scale < limit: scale = limit

        if M < 0:
            # print 'approaching'
            kep.advance_to_periastron()
            kep.advance_to_radius(limit)
        else:
            # print 'receding'
            if kep.get_separation() < scale:
                kep.advance_to_radius(limit)
            else:
                kep.return_to_radius(scale)

        # a,e = kep.get_elements()
        # r = kep.get_separation()
        # E,J = kep.get_integrals()
        # print 'kepler: a,e,r =', a.number, e.number, r.number
        # print 'E, J =', E, J

        # Note: if periastron > scale, we are now just past periastron.

        new_rel_pos = kep.get_separation_vector()
        new_rel_vel = kep.get_velocity_vector()
        kep.stop()

        # Enew = 0
        # r2 = 0
        # for k in range(3):
        #     Enew += 0.5*(new_rel_vel[k].number)**2
        #     r2 += (new_rel_pos[k].number)**2
        # rnew = math.sqrt(r2)
        # Enew -= mass.number/r1
        # print 'E, Enew, rnew =', E.number, E1, r1

        # Problem: the vectors returned by kepler are lists,
        # not numpy arrays, and it looks as though we can say
        # comp1.position = pos, but not comp1.position[k] =
        # xxx, as we'd like...  Also, we don't know how to
        # copy a numpy array with units...  TODO

        newpos1 = pos1 - pos1	# stupid trick to create zero vectors
        newpos2 = pos2 - pos2	# with the proper form and units...
        newvel1 = vel1 - vel1
        newvel2 = vel2 - vel2

        frac2 = mass2/total_mass
        for k in range(3):
            dxk = new_rel_pos[k]
            dvk = new_rel_vel[k]
            newpos1[k] = cmpos[k] - frac2*dxk
            newpos2[k] = cmpos[k] + (1-frac2)*dxk
            newvel1[k] = cmvel[k] - frac2*dvk
            newvel2[k] = cmvel[k] + (1-frac2)*dvk

        # Perform the changes to comp1 and comp2, and recursively
        # transmit them to the (currently absolute) coordinates of
        # all lower components.

        offset_particle_tree(comp1, newpos1-pos1, newvel1-vel1)
        offset_particle_tree(comp2, newpos2-pos2, newvel2-vel2)
Exemplo n.º 10
0
def CutOrAdvance(enc_bodies, primary_sysID, converter=None):
    bodies = enc_bodies.copy()
    if converter==None:
        converter = nbody_system.nbody_to_si(bodies.mass.sum(), 2 * np.max(bodies.radius.number) | bodies.radius.unit)
    systems = stellar_systems.get_heirarchical_systems_from_set(bodies, converter=converter, RelativePosition=False)
    # Deal with Possible Key Issues with Encounters with 3+ Star Particles Being Run More than Other Systems ...
    if int(primary_sysID) not in systems.keys():
        print "...: Error: Previously run binary system has been found! Not running this system ..."
        print primary_sysID
        print systems.keys()
        print "---------------------------------"
        return None
    # As this function is pulling from Multiples, there should never be more or less than 2 "Root" Particles ...
    if len(systems) != 2:
        print "...: Error: Encounter has more roots than expected! Total Root Particles:", len(systems)
        print bodies
        print "---------------------------------"
        return None
    # Assign the Primary System to #1 and Perturbing System to #2
    sys_1 = systems[int(primary_sysID)]
    secondary_sysID = [key for key in systems.keys() if key!=int(primary_sysID)][0]
    sys_2 = systems[secondary_sysID]
    print 'All System Keys:', systems.keys()
    print 'Primary System Key:', primary_sysID
    print 'System 1 IDs:', sys_1.id
    print 'System 2 IDs:', sys_2.id
    # Calculate Useful Quantities
    mass_ratio = sys_2.mass.sum()/sys_1.mass.sum()
    total_mass = sys_1.mass.sum() + sys_2.mass.sum()
    rel_pos = sys_1.center_of_mass() - sys_2.center_of_mass()
    rel_vel = sys_1.center_of_mass_velocity() - sys_2.center_of_mass_velocity()
    # Initialize Kepler Worker
    kep = Kepler(unit_converter = converter, redirection = 'none')
    kep.initialize_code()
    kep.initialize_from_dyn(total_mass, rel_pos[0], rel_pos[1], rel_pos[2], rel_vel[0], rel_vel[1], rel_vel[2])
    # Check to See if the Periastron is within the Ignore Distance for 10^3 Perturbation
    p = kep.get_periastron()
    ignore_distance = mass_ratio**(1./3.) * 600 | units.AU
    if p > ignore_distance:
        print "Encounter Ignored due to Periastron of", p.in_(units.AU), "and an IgnoreDistance of",ignore_distance
        kep.stop()
        print "---------------------------------"
        return None
    # Move the Particles to be Relative to their Respective Center of Mass
    cm_sys_1, cm_sys_2 = sys_1.center_of_mass(), sys_2.center_of_mass()
    cmv_sys_1, cmv_sys_2 = sys_1.center_of_mass_velocity(), sys_2.center_of_mass_velocity()
    for particle in sys_1:
        particle.position -= cm_sys_1
        particle.velocity -= cmv_sys_1
    for particle in sys_2:
        particle.position -= cm_sys_2
        particle.velocity -= cmv_sys_2
    # Check to See if the Planets are Closer than the Ignore Distance
    # Note: This shouldn't happen in the main code, but this prevents overshooting the periastron in debug mode.
    if kep.get_separation() > ignore_distance:
        kep.advance_to_radius(ignore_distance)
    # Advance the Center of Masses to the Desired Distance in Reduced Mass Coordinates
    x, y, z = kep.get_separation_vector()
    rel_pos_f = rel_pos.copy()
    rel_pos_f[0], rel_pos_f[1], rel_pos_f[2] = x, y, z
    vx, vy, vz = kep.get_velocity_vector()
    rel_vel_f = rel_vel.copy()
    rel_vel_f[0], rel_vel_f[1], rel_vel_f[2] = vx, vy, vz
    # Transform to Absolute Coordinates from Kepler Reduced Mass Coordinates
    cm_pos_1, cm_pos_2 = sys_2.mass.sum() * rel_pos_f / total_mass, -sys_1.mass.sum() * rel_pos_f / total_mass
    cm_vel_1, cm_vel_2 = sys_2.mass.sum() * rel_vel_f / total_mass, -sys_1.mass.sum() * rel_vel_f / total_mass
    # Move the Particles to the New Postions of their Respective Center of Mass
    for particle in sys_1:
        particle.position += cm_pos_1
        particle.velocity += cm_vel_1
    for particle in sys_2:
        particle.position += cm_pos_2
        particle.velocity += cm_vel_2
    # Stop Kepler and Return the Systems as a Particle Set
    kep.stop()
    # Collect the Collective Particle Set to be Returned Back
    final_set = Particles()
    final_set.add_particles(sys_1)
    final_set.add_particles(sys_2)
    print "---------------------------------"
    return final_set
Exemplo n.º 11
0
class StellarEncounterInHydrodynamics(object):
    """
    Resolves collisions between stars by converting them to SPH models, let them 
    collide in an SPH code, and converting the resulting SPH particle distribution 
    back to a 1D stellar evolution model.
    
    Requires a stellar evolution code to supply the internal structure of the 
    stars for the convert_stellar_model_to_SPH routine.
    Requires a gravity code to set up the initial configuration. The stars in the 
    gravity code have typically already collided, so they are first "evolved" back 
    in time up to a certain separation, assuming Keplerian motion.
    
    :argument number_of_particles: Total number of gas particles in the SPH simulation
    :argument hydrodynamics: SPH code class for the simulation
    :argument initial_separation: a factor relative to the sum of the radii (1 means in contact, default: 5)
    """
    
    stellar_evolution_code_required = True
    gravity_code_required = True
    
    def __init__(
            self, 
            number_of_particles,
            hydrodynamics, 
            initial_separation = 5, 
            relax_sph_models = True,
            verbose = False, 
            debug = False, 
            hydrodynamics_arguments = dict(),
            hydrodynamics_parameters = dict(),
            star_to_sph_arguments = dict(),
            sph_to_star_arguments = dict(),
        ):
        
        self.number_of_particles = number_of_particles
        self.hydrodynamics = hydrodynamics
        self.initial_separation = initial_separation
        if not relax_sph_models:
            self.relax = self.no_relax
        self.verbose = verbose
        self.debug = debug
        self.hydrodynamics_arguments = hydrodynamics_arguments
        self.hydrodynamics_parameters = hydrodynamics_parameters
        self.star_to_sph_arguments = star_to_sph_arguments
        self.sph_to_star_arguments = sph_to_star_arguments
        
        self.dynamical_timescales_per_step = 1.0 # encounter_is_over check is performed at this interval
        self.extra_steps_when_encounter_is_over = 3
        
        self.continue_with_kepler = False
    
    def handle_collision(self, primary, secondary, stellar_evolution_code=None, gravity_code=None):
        particles = self.local_copy_of_particles(primary, secondary)
        self.collect_required_attributes(particles, gravity_code, stellar_evolution_code)
        self.backtrack_particles(particles)
        gas_particles = self.convert_stars(particles, stellar_evolution_code)
        self.simulate_collision(gas_particles)
        self.models = [convert_SPH_to_stellar_model(group, **self.sph_to_star_arguments) for group in self.groups_after_encounter]
        return self.new_particles_with_internal_structure_from_models()
    
    def new_particles_with_internal_structure_from_models(self):
        def get_internal_structure(set, particle=None):
            return self.models[(set.key == particle.key).nonzero()[0]]
        
        result = Particles(len(self.models))
        result.add_function_attribute("get_internal_structure", None, get_internal_structure)
        result.mass = [model.dmass.sum().as_quantity_in(self.mass_unit) for model in self.models]
        result.radius = [model.radius[-1].as_quantity_in(self.radius_unit) for model in self.models]
        result.position = (self.original_center_of_mass + self.stars_after_encounter.position).as_quantity_in(self.position_unit)
        result.velocity = (self.original_center_of_mass_velocity + self.stars_after_encounter.velocity).as_quantity_in(self.velocity_unit)
        return result
    
    def local_copy_of_particles(self, primary, secondary):
        particles = Particles(0)
        particles.add_particle(primary)
        particles.add_particle(secondary)
        return particles
    
    def collect_required_attributes(self, particles, gravity_code, stellar_evolution_code):
        # Collect the required attributes and copy to the particles in memory
        required_attributes = set(["mass", "x","y","z", "vx","vy","vz", "radius"])
        required_attributes -= set(particles.get_attribute_names_defined_in_store())
        for code in [stellar_evolution_code, gravity_code]:
            attrs_in_code = required_attributes & set(code.particles.get_attribute_names_defined_in_store())
            if len(attrs_in_code) > 0:
                code.particles.copy_values_of_attributes_to(list(attrs_in_code), particles)
                required_attributes -= attrs_in_code
        
        self.mass_unit = particles.mass.unit
        self.radius_unit = particles.radius.unit
        self.position_unit = particles.position.unit
        self.velocity_unit = particles.velocity.unit
        self.dynamical_timescale = numpy.pi * (particles.radius.sum()**3 / (8 * constants.G * particles.total_mass())).sqrt()
    
    def start_kepler(self, mass_unit, length_unit):
        unit_converter = nbody_system.nbody_to_si(mass_unit, length_unit)
        self.kepler = Kepler(unit_converter, redirection = "none" if self.debug else "null")
        self.kepler.initialize_code()
    
    def initialize_binary_in_kepler(self, star_a, star_b):
        self.kepler.initialize_from_dyn(
            star_a.mass + star_b.mass, 
            star_a.x - star_b.x, star_a.y - star_b.y, star_a.z - star_b.z,
            star_a.vx-star_b.vx, star_a.vy-star_b.vy, star_a.vz-star_b.vz
        )
        return self.kepler
    
    def backtrack_particles(self, particles):
        self.original_center_of_mass = particles.center_of_mass()
        self.original_center_of_mass_velocity = particles.center_of_mass_velocity()
        
        initial_separation = self.initial_separation * particles.radius.sum()
        if self.verbose:
            print "Particles at collision:"
            print particles
            print "Backtrack particles to initial separation", initial_separation.as_string_in(units.RSun)
        
        self.start_kepler(particles.total_mass(), initial_separation)
        kepler = self.initialize_binary_in_kepler(particles[0], particles[1])
        kepler.return_to_radius(initial_separation)
        self.begin_time = kepler.get_time()
        
        particles[1].position = kepler.get_separation_vector()
        particles[1].velocity = kepler.get_velocity_vector()
        kepler.advance_to_periastron()
        self.begin_time -= kepler.get_time()
        particles[0].position = [0, 0, 0] | units.m
        particles[0].velocity = [0, 0, 0] | units.m / units.s
        particles.move_to_center()
        if self.verbose:
            print "Backtracking particles done. Initial conditions:"
            print particles
    
    def convert_stars(self, particles, stellar_evolution_code):
        n_particles = self.divide_number_of_particles(particles)
        se_colliders = particles.get_intersecting_subset_in(stellar_evolution_code.particles)
        if self.verbose:
            print "Converting stars of {0} to SPH models of {1} particles, respectively.".format(particles.mass, n_particles)
        sph_models = (
            self.relax(convert_stellar_model_to_SPH(se_colliders[0], n_particles[0], **self.star_to_sph_arguments)),
            self.relax(convert_stellar_model_to_SPH(se_colliders[1], n_particles[1], **self.star_to_sph_arguments))
        )
        gas_particles = Particles()
        for particle, sph_model in zip(particles, sph_models):
            sph_model.position += particle.position
            sph_model.velocity += particle.velocity
            gas_particles.add_particles(sph_model)
        if self.verbose:
            print "Converting stars to SPH particles done"
        if self.debug:
            print gas_particles
        return gas_particles
    
    def divide_number_of_particles(self, particles):
        n1 = int(0.5 + self.number_of_particles * particles[0].mass / particles.total_mass())
        return (n1, self.number_of_particles - n1)
    
    def relax(self, sph_model):
        if self.debug:
            monitor = dict(time=[]|units.day, kinetic=[]|units.J, potential=[]|units.J, thermal=[]|units.J)
        gas_particles = sph_model.gas_particles
        hydro = self.new_hydrodynamics(gas_particles)
        hydro.parameters.artificial_viscosity_alpha = 0.0 # Viscous damping doesn't seem to be very important, but turned off just in case...
        channel_from_hydro = hydro.gas_particles.new_channel_to(gas_particles)
        channel_to_hydro = gas_particles.new_channel_to(hydro.gas_particles)
        
        dynamical_timescale = numpy.pi * (gas_particles.total_radius()**3 / (8 * constants.G * gas_particles.total_mass())).sqrt()
        t_end_in_t_dyn = 2.5 # Relax for this many dynamical timescales
        n_steps = 100
        velocity_damp_factor = 1.0 - (2.0*numpy.pi*t_end_in_t_dyn)/n_steps # Critical damping
        if self.verbose:
            print "Relaxing SPH model with {0} for {1} ({2} dynamical timescales).".format(
                self.hydrodynamics.__name__, 
                (t_end_in_t_dyn*dynamical_timescale).as_string_in(units.day),
                t_end_in_t_dyn)
        for i_step, time in enumerate(t_end_in_t_dyn*dynamical_timescale * numpy.linspace(1.0/n_steps, 1.0, n_steps)):
            hydro.evolve_model(time)
            channel_from_hydro.copy_attributes(["mass","x","y","z","vx","vy","vz","u"])
            gas_particles.position -= gas_particles.center_of_mass()
            gas_particles.velocity = velocity_damp_factor * (gas_particles.velocity - gas_particles.center_of_mass_velocity())
            channel_to_hydro.copy_attributes(["x","y","z","vx","vy","vz"])
            if self.debug:
                K, U, Q = hydro.kinetic_energy, hydro.potential_energy, hydro.thermal_energy
                print "t, K, U, Q:", time, K, U, Q
                monitor["time"].append(time)
                monitor["kinetic"].append(K)
                monitor["potential"].append(U)
                monitor["thermal"].append(Q)
                
        hydro.stop()
        if self.debug:
            energy_evolution_plot(monitor["time"], monitor["kinetic"], monitor["potential"], monitor["thermal"])
        return gas_particles
    
    def no_relax(self, sph_model):
        return sph_model.gas_particles
    
    def new_hop(self, particles):
        converter = nbody_system.nbody_to_si(particles.total_mass(), 1.0 | units.RSun)
        if self.debug:
            print "Output of Hop is redirected to hop_out.log"
            options = dict(redirection="file", redirect_file="hop_out.log")
        else:
            options = dict()
        hop = Hop(unit_converter=converter, **options)
        hop.parameters.number_of_neighbors_for_hop = 100
        hop.parameters.saddle_density_threshold_factor = 0.8
        hop.parameters.relative_saddle_density_threshold = True
        return hop
    
    def new_hydrodynamics(self, gas_particles):
        unit_converter = nbody_system.nbody_to_si(gas_particles.total_mass(), self.dynamical_timescale)
        hydro = self.hydrodynamics(unit_converter, **self.hydrodynamics_arguments)
        hydro.initialize_code()
        for par, value in self.hydrodynamics_parameters.iteritems():
            setattr(hydro.parameters, par, value)
        hydro.commit_parameters()
        hydro.gas_particles.add_particles(gas_particles)
        hydro.commit_particles()
        return hydro
    
    def simulate_collision(self, gas_particles):
        self.hop = self.new_hop(gas_particles)
        hydro = self.new_hydrodynamics(gas_particles)
        channel = hydro.gas_particles.new_channel_to(gas_particles)
        
        if self.verbose:
            print "Simulating collision with {0} from {1} to {2}.".format(
                self.hydrodynamics.__name__, 
                self.begin_time.as_string_in(units.day), 
                (self.dynamical_timescales_per_step * self.dynamical_timescale).as_string_in(units.day))
        
        hydro.evolve_model(self.dynamical_timescales_per_step * self.dynamical_timescale - self.begin_time)
        channel.copy_attributes(["x","y","z","vx","vy","vz","pressure","density","u"])
        extra_steps_counter = 0
        while True:
            if self.encounter_is_over(gas_particles):
                extra_steps_counter += 1
                if extra_steps_counter > self.extra_steps_when_encounter_is_over:
                    print "Encounter is over and finished extra steps."
                    break
                else:
                    print "Encounter is over. Now performing step {0} out of {1} extra steps".format(
                        extra_steps_counter, self.extra_steps_when_encounter_is_over)
            else:
                extra_steps_counter = 0
            print "Continuing to {0}.".format((hydro.model_time + self.next_dt + self.begin_time).as_string_in(units.day))
            if self.continue_with_kepler:
                self.evolve_with_kepler(hydro)
            hydro.evolve_model(hydro.model_time + self.next_dt)
            channel.copy_attributes(["x","y","z","vx","vy","vz","pressure","density","u"])
        
        hydro.stop()
        self.hop.stop()
        self.kepler.stop()
    
    def encounter_is_over(self, gas_particles):
        self.next_dt = self.dynamical_timescales_per_step * self.dynamical_timescale
        groups = self.group_bound_particles(gas_particles)
        stars = self.convert_groups_to_stars(groups)
        self.groups_after_encounter = groups
        self.stars_after_encounter = stars
        if len(stars) > 1:
            # Should do full check for stable binaries, triple, multiples, two escapers,
            # escaping star + binary, etc.
            # For now we only check whether the two most massive groups will (re)collide
            a, b = stars.sorted_by_attribute("mass")[-2:]
            if self.debug: print "System consists of {0} groups. The two most massive are: {1} and {2}.".format(len(stars), a.mass.as_string_in(units.MSun), b.mass.as_string_in(units.MSun))
            if self.binary_will_collide(a, b):
                return False
        
        if self.verbose:
            print "Encounter is over, {0} stars after encounter.".format(len(groups))
        return True
    
    def group_bound_particles(self, gas_particles):
        groups, lost = self.analyze_particle_distribution(gas_particles)
        while len(lost) > 0:
            if self.debug:
                group_plot(groups, lost)
            previous_number_of_lost_particles = len(lost)
            groups, lost = self.select_bound_particles(groups, lost)
            if len(lost) == previous_number_of_lost_particles:
                break
        return groups
    
    def convert_groups_to_stars(self, groups):
        stars = Particles(len(groups))
        for star, group in zip(stars, groups):
            star.mass = group.total_mass()
            star.position = group.center_of_mass()
            star.velocity = group.center_of_mass_velocity()
            star.radius = group.LagrangianRadii(mf=[0.9], cm=star.position)[0][0]
        return stars
    
    def analyze_particle_distribution(self, gas_particles):
        if self.verbose:
            print "Analyzing particle distribution using Hop"
        if "density" in gas_particles.get_attribute_names_defined_in_store():
            if self.debug: print "Using the original particles' density"
            self.hop.parameters.outer_density_threshold = 0.5 * gas_particles.density.mean()
            self.hop.particles.add_particles(gas_particles)
            gas_particles.copy_values_of_attribute_to("density", self.hop.particles)
        else:
            if self.debug: print "Using Hop to calculate the density"
            self.hop.particles.add_particles(gas_particles)
            self.hop.calculate_densities()
            self.hop.parameters.outer_density_threshold = 0.5 * self.hop.particles.density.mean()
        self.hop.do_hop()
        result = []
        for group in self.hop.groups():
            result.append(group.get_intersecting_subset_in(gas_particles))
        lost = self.hop.no_group().get_intersecting_subset_in(gas_particles)
        self.hop.particles.remove_particles(self.hop.particles)
        return result, lost
    
    def select_bound_particles(self, groups, lost):
        specific_total_energy_relative_to_group = [] | (units.m / units.s)**2
        for group in groups:
            group_mass = group.total_mass()
            group_com = group.center_of_mass()
            group_com_velocity = group.center_of_mass_velocity()
            specific_total_energy_relative_to_group.append(
                (lost.velocity - group_com_velocity).lengths_squared() + lost.u - 
                constants.G * group_mass / (lost.position - group_com).lengths())
        index_minimum = specific_total_energy_relative_to_group.argmin(axis=0)
        bound=lost[:0]
        for i, group in enumerate(groups):
            bound_to_group = lost[numpy.logical_and(
                index_minimum == i, 
                specific_total_energy_relative_to_group[i] < 0 | (units.m / units.s)**2
            )]
            bound += bound_to_group
            groups[i] = group + bound_to_group
        return groups, lost - bound
    
    def binary_will_collide(self, a, b):
        self.continue_with_kepler = False
        if self.verbose:
            print "Using Kepler to check whether the two stars will (re)collide."
        kepler = self.initialize_binary_in_kepler(a, b)
        
        true_anomaly = kepler.get_angles()[1]
        eccentricity = kepler.get_elements()[1]
        if true_anomaly > 0.0 and eccentricity >= 1.0:
            if self.verbose:
                print "Stars are on hyperbolic/parabolic orbits and moving away from each other, interaction is over."
            return False
        
        periastron = kepler.get_periastron()
        will_collide = periastron < a.radius + b.radius
        if self.verbose:
            print "Stars {0} collide. Distance at periastron: {1}, sum of radii: {2}".format(
                "will" if will_collide else "won't",
                periastron.as_string_in(units.RSun), (a.radius + b.radius).as_string_in(units.RSun))
        
        if will_collide:
            # 1) check whether the stars are still relaxing: less than ~3 t_dyn passed since last moment of contact --> relax
            # 2) check whether the stars are already within 'initial_separation', else skip (dtmax?)
            
            kepler.advance_to_periastron()
            self.next_dt = kepler.get_time() + self.dynamical_timescales_per_step * self.dynamical_timescale
            if self.debug:
                print "Time to collision: {0}, next_dt: {1}".format(
                    kepler.get_time().as_string_in(units.day), self.next_dt.as_string_in(units.day))
            if kepler.get_time() > 3 * self.dynamical_timescale and kepler.get_apastron() > 2.0 * self.initial_separation * (a.radius + b.radius):
                # evolve for 3 * self.dynamical_timescale and skip the rest until ~initial_separation
                kepler.return_to_apastron()
                kepler.return_to_radius(a.radius + b.radius)
                if -kepler.get_time() > 2.9 * self.dynamical_timescale: # If ~3 t_dyn have passed since the end of the collision
                    if self.verbose: print "~3 t_dyn have passed since the end of the collision -> skip to next collision"
                    self.continue_with_kepler = True
                    kepler.advance_to_apastron()
                    kepler.advance_to_radius(2.0 * self.initial_separation * (a.radius + b.radius))
                    self.skip_to_relative_position_velocity = (kepler.get_separation_vector(), kepler.get_velocity_vector())
                    self.begin_time = kepler.get_time()
                    kepler.advance_to_periastron()
                    self.next_dt = self.dynamical_timescales_per_step * self.dynamical_timescale + kepler.get_time() - self.begin_time
                else:
                    self.next_dt = 3 * self.dynamical_timescale + kepler.get_time()
        return will_collide
    
    def evolve_with_kepler(self, hydro):
        if self.verbose: print "evolve_with_kepler"
        indices_two_most_massive = self.stars_after_encounter.mass.argsort()[-2:]
        groups = [self.groups_after_encounter[i] for i in indices_two_most_massive]
        old_particles = self.stars_after_encounter[indices_two_most_massive]
        new_particles = Particles(2)
        new_particles.mass = old_particles.mass
        new_particles[0].position, new_particles[0].velocity = self.skip_to_relative_position_velocity
        new_particles.move_to_center()
        for group, old_particle, new_particle in zip(groups, old_particles, new_particles):
            in_hydro = group.get_intersecting_subset_in(hydro.gas_particles)
            if self.verbose: print in_hydro.center_of_mass().as_quantity_in(units.RSun), old_particle.position.as_quantity_in(units.RSun), new_particle.position.as_quantity_in(units.RSun)
            in_hydro.position += new_particle.position - old_particle.position
            in_hydro.velocity += new_particle.velocity - old_particle.velocity
Exemplo n.º 12
0
def compress_binary_components(comp1, comp2, scale):

    # Compress the two-body system consisting of comp1 and comp2 to
    # lie within distance scale of one another.

    pos1 = comp1.position
    pos2 = comp2.position
    sep12 = ((pos2 - pos1)**2).sum()

    if sep12 > scale * scale:
        print('\ncompressing components', int(comp1.id.number), \
              'and', int(comp2.id.number), 'to separation', scale.number)
        sys.stdout.flush()
        mass1 = comp1.mass
        mass2 = comp2.mass
        total_mass = mass1 + mass2
        vel1 = comp1.velocity
        vel2 = comp2.velocity
        cmpos = (mass1 * pos1 + mass2 * pos2) / total_mass
        cmvel = (mass1 * vel1 + mass2 * vel2) / total_mass

        # For now, create and delete a temporary kepler
        # process to handle the transformation.  Obviously
        # more efficient to define a single kepler at the
        # start of the calculation and reuse it.

        kep = Kepler(redirection="none")
        kep.initialize_code()
        mass = comp1.mass + comp2.mass
        rel_pos = pos2 - pos1
        rel_vel = vel2 - vel1
        kep.initialize_from_dyn(mass, rel_pos[0], rel_pos[1], rel_pos[2],
                                rel_vel[0], rel_vel[1], rel_vel[2])
        M, th = kep.get_angles()
        a, e = kep.get_elements()
        if e < 1:
            peri = a * (1 - e)
            apo = a * (1 + e)
        else:
            peri = a * (e - 1)
            apo = 2 * a  # OK - used ony to reset scale
        limit = peri + 0.01 * (apo - peri)
        if scale < limit: scale = limit

        if M < 0:
            # print 'approaching'
            kep.advance_to_periastron()
            kep.advance_to_radius(limit)
        else:
            # print 'receding'
            if kep.get_separation() < scale:
                kep.advance_to_radius(limit)
            else:
                kep.return_to_radius(scale)

        # a,e = kep.get_elements()
        # r = kep.get_separation()
        # E,J = kep.get_integrals()
        # print 'kepler: a,e,r =', a.number, e.number, r.number
        # print 'E, J =', E, J

        # Note: if periastron > scale, we are now just past periastron.

        new_rel_pos = kep.get_separation_vector()
        new_rel_vel = kep.get_velocity_vector()
        kep.stop()

        # Enew = 0
        # r2 = 0
        # for k in range(3):
        #     Enew += 0.5*(new_rel_vel[k].number)**2
        #     r2 += (new_rel_pos[k].number)**2
        # rnew = math.sqrt(r2)
        # Enew -= mass.number/r1
        # print 'E, Enew, rnew =', E.number, E1, r1

        # Problem: the vectors returned by kepler are lists,
        # not numpy arrays, and it looks as though we can say
        # comp1.position = pos, but not comp1.position[k] =
        # xxx, as we'd like...  Also, we don't know how to
        # copy a numpy array with units...  TODO

        newpos1 = pos1 - pos1  # stupid trick to create zero vectors
        newpos2 = pos2 - pos2  # with the proper form and units...
        newvel1 = vel1 - vel1
        newvel2 = vel2 - vel2

        frac2 = mass2 / total_mass
        for k in range(3):
            dxk = new_rel_pos[k]
            dvk = new_rel_vel[k]
            newpos1[k] = cmpos[k] - frac2 * dxk
            newpos2[k] = cmpos[k] + (1 - frac2) * dxk
            newvel1[k] = cmvel[k] - frac2 * dvk
            newvel2[k] = cmvel[k] + (1 - frac2) * dvk

        # Perform the changes to comp1 and comp2, and recursively
        # transmit them to the (currently absolute) coordinates of
        # all lower components.

        offset_particle_tree(comp1, newpos1 - pos1, newvel1 - vel1)
        offset_particle_tree(comp2, newpos2 - pos2, newvel2 - vel2)
Exemplo n.º 13
0
class StellarEncounterInHydrodynamics(object):
    """
    Resolves collisions between stars by converting them to SPH models, let them 
    collide in an SPH code, and converting the resulting SPH particle distribution 
    back to a 1D stellar evolution model.
    
    Requires a stellar evolution code to supply the internal structure of the 
    stars for the convert_stellar_model_to_SPH routine.
    Requires a gravity code to set up the initial configuration. The stars in the 
    gravity code have typically already collided, so they are first "evolved" back 
    in time up to a certain separation, assuming Keplerian motion.
    
    :argument number_of_particles: Total number of gas particles in the SPH simulation
    :argument hydrodynamics: SPH code class for the simulation
    :argument initial_separation: a factor relative to the sum of the radii (1 means in contact, default: 5)
    """

    stellar_evolution_code_required = True
    gravity_code_required = True

    def __init__(
            self,
            number_of_particles,
            hydrodynamics,
            initial_separation=5,
            relax_sph_models=True,
            verbose=False,
            debug=False,
            hydrodynamics_arguments=dict(),
            hydrodynamics_parameters=dict(),
            star_to_sph_arguments=dict(),
            sph_to_star_arguments=dict(),
    ):

        self.number_of_particles = number_of_particles
        self.hydrodynamics = hydrodynamics
        self.initial_separation = initial_separation
        if not relax_sph_models:
            self.relax = self.no_relax
        self.verbose = verbose
        self.debug = debug
        self.hydrodynamics_arguments = hydrodynamics_arguments
        self.hydrodynamics_parameters = hydrodynamics_parameters
        self.star_to_sph_arguments = star_to_sph_arguments
        self.sph_to_star_arguments = sph_to_star_arguments

        self.dynamical_timescales_per_step = 1.0  # encounter_is_over check is performed at this interval
        self.extra_steps_when_encounter_is_over = 3

        self.continue_with_kepler = False

    def handle_collision(self,
                         primary,
                         secondary,
                         stellar_evolution_code=None,
                         gravity_code=None):
        particles = self.local_copy_of_particles(primary, secondary)
        self.collect_required_attributes(particles, gravity_code,
                                         stellar_evolution_code)
        self.backtrack_particles(particles)
        gas_particles = self.convert_stars(particles, stellar_evolution_code)
        self.simulate_collision(gas_particles)
        self.models = [
            convert_SPH_to_stellar_model(group, **self.sph_to_star_arguments)
            for group in self.groups_after_encounter
        ]
        return self.new_particles_with_internal_structure_from_models()

    def new_particles_with_internal_structure_from_models(self):
        def get_internal_structure(set, particle=None):
            return self.models[(set.key == particle.key).nonzero()[0]]

        result = Particles(len(self.models))
        result.add_function_attribute("get_internal_structure", None,
                                      get_internal_structure)
        result.mass = [
            model.dmass.sum().as_quantity_in(self.mass_unit)
            for model in self.models
        ]
        result.radius = [
            model.radius[-1].as_quantity_in(self.radius_unit)
            for model in self.models
        ]
        result.position = (self.original_center_of_mass +
                           self.stars_after_encounter.position).as_quantity_in(
                               self.position_unit)
        result.velocity = (self.original_center_of_mass_velocity +
                           self.stars_after_encounter.velocity).as_quantity_in(
                               self.velocity_unit)
        return result

    def local_copy_of_particles(self, primary, secondary):
        particles = Particles(0)
        particles.add_particle(primary)
        particles.add_particle(secondary)
        return particles

    def collect_required_attributes(self, particles, gravity_code,
                                    stellar_evolution_code):
        # Collect the required attributes and copy to the particles in memory
        required_attributes = set(
            ["mass", "x", "y", "z", "vx", "vy", "vz", "radius"])
        required_attributes -= set(
            particles.get_attribute_names_defined_in_store())
        for code in [stellar_evolution_code, gravity_code]:
            attrs_in_code = required_attributes & set(
                code.particles.get_attribute_names_defined_in_store())
            if len(attrs_in_code) > 0:
                code.particles.copy_values_of_attributes_to(
                    list(attrs_in_code), particles)
                required_attributes -= attrs_in_code

        self.mass_unit = particles.mass.unit
        self.radius_unit = particles.radius.unit
        self.position_unit = particles.position.unit
        self.velocity_unit = particles.velocity.unit
        self.dynamical_timescale = numpy.pi * (
            particles.radius.sum()**3 /
            (8 * constants.G * particles.total_mass())).sqrt()

    def start_kepler(self, mass_unit, length_unit):
        unit_converter = nbody_system.nbody_to_si(mass_unit, length_unit)
        self.kepler = Kepler(unit_converter,
                             redirection="none" if self.debug else "null")
        self.kepler.initialize_code()

    def initialize_binary_in_kepler(self, star_a, star_b):
        self.kepler.initialize_from_dyn(
            star_a.mass + star_b.mass, star_a.x - star_b.x,
            star_a.y - star_b.y, star_a.z - star_b.z, star_a.vx - star_b.vx,
            star_a.vy - star_b.vy, star_a.vz - star_b.vz)
        return self.kepler

    def backtrack_particles(self, particles):
        self.original_center_of_mass = particles.center_of_mass()
        self.original_center_of_mass_velocity = particles.center_of_mass_velocity(
        )

        initial_separation = self.initial_separation * particles.radius.sum()
        if self.verbose:
            print "Particles at collision:"
            print particles
            print "Backtrack particles to initial separation", initial_separation.as_string_in(
                units.RSun)

        self.start_kepler(particles.total_mass(), initial_separation)
        kepler = self.initialize_binary_in_kepler(particles[0], particles[1])
        kepler.return_to_radius(initial_separation)
        self.begin_time = kepler.get_time()

        particles[1].position = kepler.get_separation_vector()
        particles[1].velocity = kepler.get_velocity_vector()
        kepler.advance_to_periastron()
        self.begin_time -= kepler.get_time()
        particles[0].position = [0, 0, 0] | units.m
        particles[0].velocity = [0, 0, 0] | units.m / units.s
        particles.move_to_center()
        if self.verbose:
            print "Backtracking particles done. Initial conditions:"
            print particles

    def convert_stars(self, particles, stellar_evolution_code):
        n_particles = self.divide_number_of_particles(particles)
        se_colliders = particles.get_intersecting_subset_in(
            stellar_evolution_code.particles)
        if self.verbose:
            print "Converting stars of {0} to SPH models of {1} particles, respectively.".format(
                particles.mass, n_particles)
        sph_models = (self.relax(
            convert_stellar_model_to_SPH(se_colliders[0], n_particles[0],
                                         **self.star_to_sph_arguments)),
                      self.relax(
                          convert_stellar_model_to_SPH(
                              se_colliders[1], n_particles[1],
                              **self.star_to_sph_arguments)))
        gas_particles = Particles()
        for particle, sph_model in zip(particles, sph_models):
            sph_model.position += particle.position
            sph_model.velocity += particle.velocity
            gas_particles.add_particles(sph_model)
        if self.verbose:
            print "Converting stars to SPH particles done"
        if self.debug:
            print gas_particles
        return gas_particles

    def divide_number_of_particles(self, particles):
        n1 = int(0.5 + self.number_of_particles * particles[0].mass /
                 particles.total_mass())
        return (n1, self.number_of_particles - n1)

    def relax(self, sph_model):
        if self.debug:
            monitor = dict(time=[] | units.day,
                           kinetic=[] | units.J,
                           potential=[] | units.J,
                           thermal=[] | units.J)
        gas_particles = sph_model.gas_particles
        hydro = self.new_hydrodynamics(gas_particles)
        hydro.parameters.artificial_viscosity_alpha = 0.0  # Viscous damping doesn't seem to be very important, but turned off just in case...
        channel_from_hydro = hydro.gas_particles.new_channel_to(gas_particles)
        channel_to_hydro = gas_particles.new_channel_to(hydro.gas_particles)

        dynamical_timescale = numpy.pi * (
            gas_particles.total_radius()**3 /
            (8 * constants.G * gas_particles.total_mass())).sqrt()
        t_end_in_t_dyn = 2.5  # Relax for this many dynamical timescales
        n_steps = 100
        velocity_damp_factor = 1.0 - (
            2.0 * numpy.pi * t_end_in_t_dyn) / n_steps  # Critical damping
        if self.verbose:
            print "Relaxing SPH model with {0} for {1} ({2} dynamical timescales).".format(
                self.hydrodynamics.__name__,
                (t_end_in_t_dyn * dynamical_timescale).as_string_in(units.day),
                t_end_in_t_dyn)
        for i_step, time in enumerate(
                t_end_in_t_dyn * dynamical_timescale *
                numpy.linspace(1.0 / n_steps, 1.0, n_steps)):
            hydro.evolve_model(time)
            channel_from_hydro.copy_attributes(
                ["mass", "x", "y", "z", "vx", "vy", "vz", "u"])
            gas_particles.position -= gas_particles.center_of_mass()
            gas_particles.velocity = velocity_damp_factor * (
                gas_particles.velocity -
                gas_particles.center_of_mass_velocity())
            channel_to_hydro.copy_attributes(["x", "y", "z", "vx", "vy", "vz"])
            if self.debug:
                K, U, Q = hydro.kinetic_energy, hydro.potential_energy, hydro.thermal_energy
                print "t, K, U, Q:", time, K, U, Q
                monitor["time"].append(time)
                monitor["kinetic"].append(K)
                monitor["potential"].append(U)
                monitor["thermal"].append(Q)

        hydro.stop()
        if self.debug:
            energy_evolution_plot(monitor["time"], monitor["kinetic"],
                                  monitor["potential"], monitor["thermal"])
        return gas_particles

    def no_relax(self, sph_model):
        return sph_model.gas_particles

    def new_hop(self, particles):
        converter = nbody_system.nbody_to_si(particles.total_mass(),
                                             1.0 | units.RSun)
        if self.debug:
            print "Output of Hop is redirected to hop_out.log"
            options = dict(redirection="file", redirect_file="hop_out.log")
        else:
            options = dict()
        hop = Hop(unit_converter=converter, **options)
        hop.parameters.number_of_neighbors_for_hop = 100
        hop.parameters.saddle_density_threshold_factor = 0.8
        hop.parameters.relative_saddle_density_threshold = True
        return hop

    def new_hydrodynamics(self, gas_particles):
        unit_converter = nbody_system.nbody_to_si(gas_particles.total_mass(),
                                                  self.dynamical_timescale)
        hydro = self.hydrodynamics(unit_converter,
                                   **self.hydrodynamics_arguments)
        hydro.initialize_code()
        for par, value in self.hydrodynamics_parameters.iteritems():
            setattr(hydro.parameters, par, value)
        hydro.commit_parameters()
        hydro.gas_particles.add_particles(gas_particles)
        hydro.commit_particles()
        return hydro

    def simulate_collision(self, gas_particles):
        self.hop = self.new_hop(gas_particles)
        hydro = self.new_hydrodynamics(gas_particles)
        channel = hydro.gas_particles.new_channel_to(gas_particles)

        if self.verbose:
            print "Simulating collision with {0} from {1} to {2}.".format(
                self.hydrodynamics.__name__,
                self.begin_time.as_string_in(units.day),
                (self.dynamical_timescales_per_step *
                 self.dynamical_timescale).as_string_in(units.day))

        hydro.evolve_model(self.dynamical_timescales_per_step *
                           self.dynamical_timescale - self.begin_time)
        channel.copy_attributes(
            ["x", "y", "z", "vx", "vy", "vz", "pressure", "density", "u"])
        extra_steps_counter = 0
        while True:
            if self.encounter_is_over(gas_particles):
                extra_steps_counter += 1
                if extra_steps_counter > self.extra_steps_when_encounter_is_over:
                    print "Encounter is over and finished extra steps."
                    break
                else:
                    print "Encounter is over. Now performing step {0} out of {1} extra steps".format(
                        extra_steps_counter,
                        self.extra_steps_when_encounter_is_over)
            else:
                extra_steps_counter = 0
            print "Continuing to {0}.".format(
                (hydro.model_time + self.next_dt +
                 self.begin_time).as_string_in(units.day))
            if self.continue_with_kepler:
                self.evolve_with_kepler(hydro)
            hydro.evolve_model(hydro.model_time + self.next_dt)
            channel.copy_attributes(
                ["x", "y", "z", "vx", "vy", "vz", "pressure", "density", "u"])

        hydro.stop()
        self.hop.stop()
        self.kepler.stop()

    def encounter_is_over(self, gas_particles):
        self.next_dt = self.dynamical_timescales_per_step * self.dynamical_timescale
        groups = self.group_bound_particles(gas_particles)
        stars = self.convert_groups_to_stars(groups)
        self.groups_after_encounter = groups
        self.stars_after_encounter = stars
        if len(stars) > 1:
            # Should do full check for stable binaries, triple, multiples, two escapers,
            # escaping star + binary, etc.
            # For now we only check whether the two most massive groups will (re)collide
            a, b = stars.sorted_by_attribute("mass")[-2:]
            if self.debug:
                print "System consists of {0} groups. The two most massive are: {1} and {2}.".format(
                    len(stars), a.mass.as_string_in(units.MSun),
                    b.mass.as_string_in(units.MSun))
            if self.binary_will_collide(a, b):
                return False

        if self.verbose:
            print "Encounter is over, {0} stars after encounter.".format(
                len(groups))
        return True

    def group_bound_particles(self, gas_particles):
        groups, lost = self.analyze_particle_distribution(gas_particles)
        while len(lost) > 0:
            if self.debug:
                group_plot(groups, lost)
            previous_number_of_lost_particles = len(lost)
            groups, lost = self.select_bound_particles(groups, lost)
            if len(lost) == previous_number_of_lost_particles:
                break
        return groups

    def convert_groups_to_stars(self, groups):
        stars = Particles(len(groups))
        for star, group in zip(stars, groups):
            star.mass = group.total_mass()
            star.position = group.center_of_mass()
            star.velocity = group.center_of_mass_velocity()
            star.radius = group.LagrangianRadii(mf=[0.9],
                                                cm=star.position)[0][0]
        return stars

    def analyze_particle_distribution(self, gas_particles):
        if self.verbose:
            print "Analyzing particle distribution using Hop"
        if "density" in gas_particles.get_attribute_names_defined_in_store():
            if self.debug: print "Using the original particles' density"
            self.hop.parameters.outer_density_threshold = 0.5 * gas_particles.density.mean(
            )
            self.hop.particles.add_particles(gas_particles)
            gas_particles.copy_values_of_attribute_to("density",
                                                      self.hop.particles)
        else:
            if self.debug: print "Using Hop to calculate the density"
            self.hop.particles.add_particles(gas_particles)
            self.hop.calculate_densities()
            self.hop.parameters.outer_density_threshold = 0.5 * self.hop.particles.density.mean(
            )
        self.hop.do_hop()
        result = []
        for group in self.hop.groups():
            result.append(group.get_intersecting_subset_in(gas_particles))
        lost = self.hop.no_group().get_intersecting_subset_in(gas_particles)
        self.hop.particles.remove_particles(self.hop.particles)
        return result, lost

    def select_bound_particles(self, groups, lost):
        specific_total_energy_relative_to_group = [] | (units.m / units.s)**2
        for group in groups:
            group_mass = group.total_mass()
            group_com = group.center_of_mass()
            group_com_velocity = group.center_of_mass_velocity()
            specific_total_energy_relative_to_group.append(
                (lost.velocity - group_com_velocity).lengths_squared() +
                lost.u - constants.G * group_mass /
                (lost.position - group_com).lengths())
        index_minimum = specific_total_energy_relative_to_group.argmin(axis=0)
        bound = lost[:0]
        for i, group in enumerate(groups):
            bound_to_group = lost[numpy.logical_and(
                index_minimum == i,
                specific_total_energy_relative_to_group[i] < 0 |
                (units.m / units.s)**2)]
            bound += bound_to_group
            groups[i] = group + bound_to_group
        return groups, lost - bound

    def binary_will_collide(self, a, b):
        self.continue_with_kepler = False
        if self.verbose:
            print "Using Kepler to check whether the two stars will (re)collide."
        kepler = self.initialize_binary_in_kepler(a, b)

        true_anomaly = kepler.get_angles()[1]
        eccentricity = kepler.get_elements()[1]
        if true_anomaly > 0.0 and eccentricity >= 1.0:
            if self.verbose:
                print "Stars are on hyperbolic/parabolic orbits and moving away from each other, interaction is over."
            return False

        periastron = kepler.get_periastron()
        will_collide = periastron < a.radius + b.radius
        if self.verbose:
            print "Stars {0} collide. Distance at periastron: {1}, sum of radii: {2}".format(
                "will" if will_collide else "won't",
                periastron.as_string_in(units.RSun),
                (a.radius + b.radius).as_string_in(units.RSun))

        if will_collide:
            # 1) check whether the stars are still relaxing: less than ~3 t_dyn passed since last moment of contact --> relax
            # 2) check whether the stars are already within 'initial_separation', else skip (dtmax?)

            kepler.advance_to_periastron()
            self.next_dt = kepler.get_time(
            ) + self.dynamical_timescales_per_step * self.dynamical_timescale
            if self.debug:
                print "Time to collision: {0}, next_dt: {1}".format(
                    kepler.get_time().as_string_in(units.day),
                    self.next_dt.as_string_in(units.day))
            if kepler.get_time(
            ) > 3 * self.dynamical_timescale and kepler.get_apastron(
            ) > 2.0 * self.initial_separation * (a.radius + b.radius):
                # evolve for 3 * self.dynamical_timescale and skip the rest until ~initial_separation
                kepler.return_to_apastron()
                kepler.return_to_radius(a.radius + b.radius)
                if -kepler.get_time(
                ) > 2.9 * self.dynamical_timescale:  # If ~3 t_dyn have passed since the end of the collision
                    if self.verbose:
                        print "~3 t_dyn have passed since the end of the collision -> skip to next collision"
                    self.continue_with_kepler = True
                    kepler.advance_to_apastron()
                    kepler.advance_to_radius(2.0 * self.initial_separation *
                                             (a.radius + b.radius))
                    self.skip_to_relative_position_velocity = (
                        kepler.get_separation_vector(),
                        kepler.get_velocity_vector())
                    self.begin_time = kepler.get_time()
                    kepler.advance_to_periastron()
                    self.next_dt = self.dynamical_timescales_per_step * self.dynamical_timescale + kepler.get_time(
                    ) - self.begin_time
                else:
                    self.next_dt = 3 * self.dynamical_timescale + kepler.get_time(
                    )
        return will_collide

    def evolve_with_kepler(self, hydro):
        if self.verbose: print "evolve_with_kepler"
        indices_two_most_massive = self.stars_after_encounter.mass.argsort(
        )[-2:]
        groups = [
            self.groups_after_encounter[i] for i in indices_two_most_massive
        ]
        old_particles = self.stars_after_encounter[indices_two_most_massive]
        new_particles = Particles(2)
        new_particles.mass = old_particles.mass
        new_particles[0].position, new_particles[
            0].velocity = self.skip_to_relative_position_velocity
        new_particles.move_to_center()
        for group, old_particle, new_particle in zip(groups, old_particles,
                                                     new_particles):
            in_hydro = group.get_intersecting_subset_in(hydro.gas_particles)
            if self.verbose:
                print in_hydro.center_of_mass().as_quantity_in(
                    units.RSun), old_particle.position.as_quantity_in(
                        units.RSun), new_particle.position.as_quantity_in(
                            units.RSun)
            in_hydro.position += new_particle.position - old_particle.position
            in_hydro.velocity += new_particle.velocity - old_particle.velocity