Ejemplo n.º 1
0
def form_stars_from_group(group_index,
                          sink_particles,
                          lower_mass_limit=settings.stars_lower_mass_limit,
                          upper_mass_limit=settings.stars_upper_mass_limit,
                          local_sound_speed=0.2 | units.kms,
                          minimum_sink_mass=0.01 | units.MSun,
                          logger=None,
                          randomseed=None,
                          shrink_sinks=True,
                          **keyword_arguments):
    """
    Last reviwed on 27 Nov 2020.

    Form stars from specific group of sinks.
    """
    logger = logger or logging.getLogger(__name__)
    #logger.info(
    #    "Using form_stars_from_group on group %i",
    #    group_index
    #)
    if randomseed is not None:
        logger.info("Setting random seed to %i", randomseed)
        numpy.random.seed(randomseed)

    # Sanity check: each sink particle must be in a group.
    ungrouped_sinks = sink_particles.select_array(lambda x: x <= 0,
                                                  ['in_group'])
    if not ungrouped_sinks.is_empty():
        logger.info(
            "WARNING: There exist ungrouped sinks. Something is wrong!")
        return None

    # Consider only group with input group index from here onwards.
    group = sink_particles[sink_particles.in_group == group_index]

    # Sanity check: group must have at least a sink
    if group.is_empty():
        logger.info(
            "WARNING: There is no sink in the group: Something is wrong!")
        return None

    number_of_sinks = len(group)
    group_mass = group.total_mass()
    logger.info("%i sinks found in group #%i with total mass %s",
                number_of_sinks, group_index, group_mass.in_(units.MSun))

    next_mass = generate_next_mass(
        initial_mass_function=initial_mass_function,
        lower_mass_limit=lower_mass_limit,
        upper_mass_limit=upper_mass_limit,
    )[0][0]
    try:
        # Within a group, group_next_primary_mass values are either
        # a mass, or 0 MSun. If all values are 0 MSun, this is a
        # new group. Else, only interested on the non-zero value. The
        # non-zero values are the same.

        #logger.info(
        #    'SANITY CHECK: group_next_primary_mass %s',
        #    group.group_next_primary_mass
        #)
        if group.group_next_primary_mass.max() == 0 | units.MSun:
            logger.info('Initiate group #%i for star formation', group_index)
            group.group_next_primary_mass = next_mass
        else:
            next_mass = group.group_next_primary_mass.max()
    # This happens for the first ever assignment of this attribute
    except AttributeError:
        logger.info(
            'AttributeError exception: Initiate group #%i for star formation',
            group_index)
        group.group_next_primary_mass = next_mass

    #logger.info("Next mass is %s", next_mass)

    if group_mass < next_mass:
        logger.info("Group #%i is not massive enough for the next star %s",
                    group_index, next_mass.in_(units.MSun))
        return None

    # Form stars from the leftover group sink mass
    mass_left = group_mass - next_mass
    masses = new_masses(
        stellar_mass=mass_left,
        lower_mass_limit=lower_mass_limit,
        upper_mass_limit=upper_mass_limit,
        initial_mass_function=settings.stars_initial_mass_function)
    number_of_stars = len(masses)

    #logger.info(
    #    "%i stars created in group #%i with %i sinks",
    #    number_of_stars, group_index, number_of_sinks
    #)

    new_stars = Particles(number_of_stars)
    new_stars.age = 0 | units.Myr
    new_stars[0].mass = next_mass
    new_stars[1:].mass = masses[:-1]
    group.group_next_primary_mass = masses[-1]
    new_stars = new_stars.sorted_by_attribute("mass").reversed()
    new_stars.in_group = group_index

    # Create placeholders for attributes of new_stars
    new_stars.position = [0, 0, 0] | units.pc
    new_stars.velocity = [0, 0, 0] | units.kms
    new_stars.origin_cloud = group[0].key
    new_stars.star_forming_radius = 0 | units.pc
    new_stars.star_forming_u = local_sound_speed**2

    #logger.info(
    #    "Group's next primary mass is %s",
    #    group.group_next_primary_mass[0]
    #)

    # Don't mess with the actual group sink particle set.
    star_forming_regions = group.copy()
    star_forming_regions.sorted_by_attribute("mass").reversed()

    # Generate a probability list of star forming region indices the
    # stars should associate to
    probabilities = (star_forming_regions.mass /
                     star_forming_regions.mass.sum())
    probabilities /= probabilities.sum()  # Ensure sum is exactly 1
    logger.info("Max & min probabilities: %s, %s", probabilities.max(),
                probabilities.min())

    #logger.info("All probabilities: %s", probabilities)

    # Create index list of star forming regions from probability list
    sample = numpy.random.choice(len(star_forming_regions),
                                 number_of_stars,
                                 p=probabilities)

    # Assign the stars to the sampled star forming regions
    star_forming_regions_sampled = star_forming_regions[sample]
    new_stars.position = star_forming_regions_sampled.position
    new_stars.velocity = star_forming_regions_sampled.velocity
    new_stars.origin_cloud = star_forming_regions_sampled.key
    new_stars.star_forming_radius = star_forming_regions_sampled.radius
    try:
        new_stars.star_forming_u = star_forming_regions_sampled.u
    except AttributeError:
        new_stars.star_forming_u = local_sound_speed**2

    # Random position of stars within the sink radius they assigned to
    rho = (numpy.random.random(number_of_stars) *
           new_stars.star_forming_radius)
    theta = (numpy.random.random(number_of_stars) * (2 * numpy.pi | units.rad))
    phi = (numpy.random.random(number_of_stars) * numpy.pi | units.rad)
    x = (rho * sin(phi) * cos(theta)).value_in(units.pc)
    y = (rho * sin(phi) * sin(theta)).value_in(units.pc)
    z = (rho * cos(phi)).value_in(units.pc)

    dX = list(zip(*[x, y, z])) | units.pc

    # Random velocity, sample magnitude from gaussian with local sound speed
    # like Wall et al (2019)
    # temperature = 10 | units.K

    # or (gamma * local_pressure / density).sqrt()
    velocity_magnitude = numpy.random.normal(
        # loc=0.0,  # <- since we already added the velocity of the sink
        scale=new_stars.star_forming_u.sqrt().value_in(units.kms),
        size=number_of_stars,
    ) | units.kms
    velocity_theta = (numpy.random.random(number_of_stars) *
                      (2 * numpy.pi | units.rad))
    velocity_phi = (numpy.random.random(number_of_stars) *
                    (numpy.pi | units.rad))
    vx = (velocity_magnitude * sin(velocity_phi) *
          cos(velocity_theta)).value_in(units.kms)
    vy = (velocity_magnitude * sin(velocity_phi) *
          sin(velocity_theta)).value_in(units.kms)
    vz = (velocity_magnitude * cos(velocity_phi)).value_in(units.kms)

    dV = list(zip(*[vx, vy, vz])) | units.kms

    #logger.info("Updating new stars...")
    new_stars.position += dX
    new_stars.velocity += dV

    # For Pentacle, this is the PP radius
    new_stars.radius = 0.05 | units.parsec

    # Remove sink mass according to the position of stars
    excess_star_mass = 0 | units.MSun
    for s in group:
        #logger.info('Sink mass before reduction: %s', s.mass.in_(units.MSun))
        total_star_mass_nearby = (
            new_stars[new_stars.origin_cloud == s.key]).total_mass()

        # To prevent sink mass becomes negative
        if s.mass > minimum_sink_mass:
            if (s.mass - total_star_mass_nearby) <= minimum_sink_mass:
                excess_star_mass += (total_star_mass_nearby - s.mass +
                                     minimum_sink_mass)
                #logger.info(
                #    'Sink mass goes below %s; excess mass is now %s',
                #    minimum_sink_mass.in_(units.MSun),
                #    excess_star_mass.in_(units.MSun)
                #)
                s.mass = minimum_sink_mass
            else:
                s.mass -= total_star_mass_nearby
        else:
            excess_star_mass += total_star_mass_nearby
            #logger.info(
            #    'Sink mass is already <= minimum mass allowed; '
            #    'excess mass is now %s',
            #    excess_star_mass.in_(units.MSun)
            #)

        #logger.info('Sink mass after reduction: %s', s.mass.in_(units.MSun))

    # Reduce all sinks in group equally with the excess star mass
    #logger.info('Reducing all sink mass equally with excess star mass...')
    mass_ratio = 1 - excess_star_mass / group.total_mass()
    group.mass *= mass_ratio

    logger.info("Total sink mass in group after sink mass reduction: %s",
                group.total_mass().in_(units.MSun))

    if shrink_sinks:
        group.radius = ((group.mass / group.initial_density) /
                        (4 / 3 * numpy.pi))**(1 / 3)
        #logger.info(
        #    "New radii: %s",
        #    group.radius.in_(units.pc)
        #)

    return new_stars