def merge_two_stars(bodies, particles_in_encounter): """ Merge two stars into one """ com_pos = particles_in_encounter.center_of_mass() com_vel = particles_in_encounter.center_of_mass_velocity() star_0 = particles_in_encounter[0] star_1 = particles_in_encounter[1] new_particle = Particles(1) new_particle.birth_age = particles_in_encounter.birth_age.min() new_particle.mass = particles_in_encounter.total_mass() new_particle.age = min(particles_in_encounter.age) \ * max(particles_in_encounter.mass)/new_particle.mass new_particle.position = com_pos new_particle.velocity = com_vel new_particle.name = "Star" new_particle.radius = particles_in_encounter.radius.max() print("# old radius:", particles_in_encounter.radius.in_(units.AU)) print("# new radius:", new_particle.radius.in_(units.AU)) bodies.add_particles(new_particle) print("# Two stars (M=", particles_in_encounter.mass.in_(units.MSun), ") collided at d=", (star_0.position - star_1.position).length().in_(units.AU)) bodies.remove_particles(particles_in_encounter)
def test5(self): print("Testing SinkParticles accrete, one particle within two sinks' radii") particles = Particles(10) particles.radius = 42.0 | units.RSun particles.mass = list(range(1,11)) | units.MSun particles.position = [[i, 2*i, 3*i] for i in range(10)] | units.parsec particles.velocity = [[i, 0, -i] for i in range(10)] | units.km/units.s particles.age = list(range(10)) | units.Myr copy = particles.copy() sinks = SinkParticles(particles[[3, 7]], sink_radius=[4,12]|units.parsec,looping_over=self.looping_over) self.assertEqual(sinks.sink_radius, [4.0, 12.0] | units.parsec) self.assertEqual(sinks.mass, [4.0, 8.0] | units.MSun) self.assertEqual(sinks.position, [[3, 6, 9], [7, 14, 21]] | units.parsec) sinks.accrete(particles) self.assertEqual(len(particles), 4) # 6 particles were accreted self.assertEqual(sinks.mass, [12.0, 40.0] | units.MSun) # mass of sinks increased self.assertEqual(sinks.get_intersecting_subset_in(particles).mass, [12.0, 40.0] | units.MSun) # original particles' masses match self.assertEqual(particles.total_mass(), copy.total_mass()) # total mass is conserved self.assertEqual(particles.center_of_mass(), copy.center_of_mass()) # center of mass is conserved self.assertEqual(particles.center_of_mass_velocity(), copy.center_of_mass_velocity()) # center of mass velocity is conserved self.assertEqual(particles.total_momentum(), copy.total_momentum()) # momentum is conserved self.assertEqual(particles.total_angular_momentum()+sinks.angular_momentum.sum(axis=0), copy.total_angular_momentum()) # angular_momentum is conserved
def test5(self): print "Testing SinkParticles accrete, one particle within two sinks' radii" particles = Particles(10) particles.radius = 42.0 | units.RSun particles.mass = range(1,11) | units.MSun particles.position = [[i, 2*i, 3*i] for i in range(10)] | units.parsec particles.velocity = [[i, 0, -i] for i in range(10)] | units.km/units.s particles.age = range(10) | units.Myr copy = particles.copy() sinks = SinkParticles(particles[[3, 7]], sink_radius=[4,12]|units.parsec,looping_over=self.looping_over) self.assertEqual(sinks.sink_radius, [4.0, 12.0] | units.parsec) self.assertEqual(sinks.mass, [4.0, 8.0] | units.MSun) self.assertEqual(sinks.position, [[3, 6, 9], [7, 14, 21]] | units.parsec) sinks.accrete(particles) self.assertEqual(len(particles), 4) # 6 particles were accreted self.assertEqual(sinks.mass, [12.0, 40.0] | units.MSun) # mass of sinks increased self.assertEqual(sinks.get_intersecting_subset_in(particles).mass, [12.0, 40.0] | units.MSun) # original particles' masses match self.assertEqual(particles.total_mass(), copy.total_mass()) # total mass is conserved self.assertEqual(particles.center_of_mass(), copy.center_of_mass()) # center of mass is conserved self.assertEqual(particles.center_of_mass_velocity(), copy.center_of_mass_velocity()) # center of mass velocity is conserved self.assertEqual(particles.total_momentum(), copy.total_momentum()) # momentum is conserved self.assertEqual(particles.total_angular_momentum()+sinks.angular_momentum.sum(axis=0), copy.total_angular_momentum()) # angular_momentum is conserved
def make_amuse_fresco_stars_only(x, mstar, age_yr, L, res=512, p=5e-4, mass_limits=[0, 0], mass_rescale=1., filename=None, vmax=None): number_of_stars = len(mstar) # print(np.max(mstar)) # ind = mstar>30 # x=x[ind] # mstar=mstar[ind] # age_yr=age_yr[ind] # number_of_stars = len(mstar) if (mass_limits[0] != 0.0) or (mass_limits[1] != 0.0): if (mass_limits[0] == 0.0): mass_limits[0] = np.min(mstar) if (mass_limits[1] == 0.0): mass_limits[1] = np.max(mstar) mstar_new = np.clip(mstar, mass_limits[0], mass_limits[1]) #small correction to make them stand apart (e.g. instea of clipping both 100 and 50 msun to 50, we get 50 and 60 so they don't look identical) mstar_new = mstar_new + (mstar - mstar_new) * 0.1 ##limits masses of star #logm = np.log10(mstar); logm0 = np.max(logm) + np.min(logm); #mstar_new = 10**( (logm - logm0)/mass_limits + logm0 ) mstar_new = mstar**mass_rescale new_stars = Particles(number_of_stars) new_stars.age = age_yr | units.yr new_stars.mass = mstar_new | units.MSun new_stars.position = x | units.pc stars = new_stars gas = Particles() #inspectvar(units) stars.luminosity = lum_MS(mstar_new) | units.LSun #stars.main_sequence_lifetime = [450.0, 420.0] | units.Myr stars.radius = rad_MS(mstar_new) | units.RSun #stars.spin = [4700, 4700] | units.yr**-1 # stars.stellar_type = [1, 1] | units.stellar_type # se = SSE() # se.particles.add_particles(stars) # from_se = se.particles.new_channel_to(stars) # from_se.copy() # inspectvar(stars) image, _ = make_fresco_image( stars, gas, return_vmax=True,\ image_width=[L | units.pc,L | units.pc], image_size=[res,res],percentile=1-p,vmax=vmax) #image: (2048,2048,3) RGB if not (filename is None): #Save image to file plt.imshow(image[::-1], extent=(-L / 2.0, L / 2.0, -L / 2.0, L / 2.0)) plt.xlim(-L / 2.0, L / 2.0) plt.ylim(-L / 2.0, L / 2.0) plt.imsave(filename, image[::-1]) return image[::-1]
from amuse.community.sse.interface import SSE #from amuse.ext.masc import make_a_star_cluster from amuse.ext import masc from amuse.ext.fresco import make_fresco_image import h5py filename = "/scratch/05917/tg852163/GMC_sim/Runs/Physics_ladder/M2e4_C_M_J_RTH_2e7/output/snapshot_060.hdf5" with h5py.File(filename, 'r') as F: mstar = np.array(F["PartType5"]["Masses"]) x = np.array( F["PartType5"]["Coordinates"]) - F["Header"].attrs["BoxSize"] / 2 number_of_stars = len(mstar) new_stars = Particles(number_of_stars) new_stars.age = 0 | units.yr new_stars.mass = mstar | units.MSun new_stars.position = x | units.pc stars = new_stars gas = Particles() se = SSE() se.particles.add_particles(stars) from_se = se.particles.new_channel_to(stars) from_se.copy() for p in (1.0 - np.logspace(-2, -7, 6)): image, vmax = make_fresco_image( stars, gas, return_vmax=True, image_width=[20. | units.pc, 20. | units.pc],
def get_orbit_ini(m0, m1, peri, ecc, incl, omega, rel_force=0.01, r_disk=50|units.AU): converter=nbody_system.nbody_to_si(1|units.MSun,1|units.AU) # semi-major axis if ecc!=1.0: semi = peri/(1.0-ecc) else: semi = 1.0e10 | units.AU # relative position and velocity vectors at the pericenter using kepler kepler = Kepler_twobody(converter) kepler.initialize_code() kepler.initialize_from_elements(mass=(m0+m1), semi=semi, ecc=ecc, periastron=peri) # at pericenter # moving particle backwards to radius r where: F_m1(r) = rel_force*F_m0(r_disk) #r_disk = peri r_ini = r_disk*(1.0 + numpy.sqrt(m1/m0)/rel_force) kepler.return_to_radius(radius=r_ini) rl = kepler.get_separation_vector() r = [rl[0].value_in(units.AU), rl[1].value_in(units.AU), rl[2].value_in(units.AU)] | units.AU vl = kepler.get_velocity_vector() v = [vl[0].value_in(units.kms), vl[1].value_in(units.kms), vl[2].value_in(units.kms)] | units.kms period_kepler = kepler.get_period() time_peri = kepler.get_time() kepler.stop() # rotation of the orbital plane by inclination and argument of periapsis a1 = ([1.0, 0.0, 0.0], [0.0, numpy.cos(incl), -numpy.sin(incl)], [0.0, numpy.sin(incl), numpy.cos(incl)]) a2 = ([numpy.cos(omega), -numpy.sin(omega), 0.0], [numpy.sin(omega), numpy.cos(omega), 0.0], [0.0, 0.0, 1.0]) rot = numpy.dot(a1,a2) r_au = numpy.reshape(r.value_in(units.AU), 3, 1) v_kms = numpy.reshape(v.value_in(units.kms), 3, 1) r_rot = numpy.dot(rot, r_au) | units.AU v_rot = numpy.dot(rot, v_kms) | units.kms bodies = Particles(2) bodies[0].mass = m0 bodies[0].radius = 1.0|units.RSun bodies[0].position = (0,0,0) | units.AU bodies[0].velocity = (0,0,0) | units.kms bodies[1].mass = m1 bodies[1].radius = 1.0|units.RSun bodies[1].x = r_rot[0] bodies[1].y = r_rot[1] bodies[1].z = r_rot[2] bodies[1].vx = v_rot[0] bodies[1].vy = v_rot[1] bodies[1].vz = v_rot[2] bodies.age = 0.0 | units.yr bodies.move_to_center() print "\t r_rel_ini = ", r_rot.in_(units.AU) print "\t v_rel_ini = ", v_rot.in_(units.kms) print "\t time since peri = ", time_peri.in_(units.yr) a_orbit, e_orbit, p_orbit = orbital_parameters(r_rot, v_rot, (m0+m1)) print "\t a = ", a_orbit.in_(units.AU), "\t e = ", e_orbit, "\t period = ", p_orbit.in_(units.yr) return bodies, time_peri
def get_orbit_ini(m0, m1, peri, ecc, incl, omega, rel_force=0.01, r_disk=50 | units.AU): converter = nbody_system.nbody_to_si(1 | units.MSun, 1 | units.AU) # semi-major axis if ecc != 1.0: semi = peri / (1.0 - ecc) else: semi = 1.0e10 | units.AU # relative position and velocity vectors at the pericenter using kepler kepler = Kepler_twobody(converter) kepler.initialize_code() kepler.initialize_from_elements(mass=(m0 + m1), semi=semi, ecc=ecc, periastron=peri) # at pericenter # moving particle backwards to radius r where: F_m1(r) = rel_force*F_m0(r_disk) #r_disk = peri r_ini = r_disk * (1.0 + numpy.sqrt(m1 / m0) / rel_force) kepler.return_to_radius(radius=r_ini) rl = kepler.get_separation_vector() r = [ rl[0].value_in(units.AU), rl[1].value_in(units.AU), rl[2].value_in( units.AU) ] | units.AU vl = kepler.get_velocity_vector() v = [ vl[0].value_in(units.kms), vl[1].value_in(units.kms), vl[2].value_in( units.kms) ] | units.kms period_kepler = kepler.get_period() time_peri = kepler.get_time() kepler.stop() # rotation of the orbital plane by inclination and argument of periapsis a1 = ([1.0, 0.0, 0.0], [0.0, numpy.cos(incl), -numpy.sin(incl)], [0.0, numpy.sin(incl), numpy.cos(incl)]) a2 = ([numpy.cos(omega), -numpy.sin(omega), 0.0], [numpy.sin(omega), numpy.cos(omega), 0.0], [0.0, 0.0, 1.0]) rot = numpy.dot(a1, a2) r_au = numpy.reshape(r.value_in(units.AU), 3, 1) v_kms = numpy.reshape(v.value_in(units.kms), 3, 1) r_rot = numpy.dot(rot, r_au) | units.AU v_rot = numpy.dot(rot, v_kms) | units.kms bodies = Particles(2) bodies[0].mass = m0 bodies[0].radius = 1.0 | units.RSun bodies[0].position = (0, 0, 0) | units.AU bodies[0].velocity = (0, 0, 0) | units.kms bodies[1].mass = m1 bodies[1].radius = 1.0 | units.RSun bodies[1].x = r_rot[0] bodies[1].y = r_rot[1] bodies[1].z = r_rot[2] bodies[1].vx = v_rot[0] bodies[1].vy = v_rot[1] bodies[1].vz = v_rot[2] bodies.age = 0.0 | units.yr bodies.move_to_center() print "\t r_rel_ini = ", r_rot.in_(units.AU) print "\t v_rel_ini = ", v_rot.in_(units.kms) print "\t time since peri = ", time_peri.in_(units.yr) a_orbit, e_orbit, p_orbit = orbital_parameters(r_rot, v_rot, (m0 + m1)) print "\t a = ", a_orbit.in_( units.AU), "\t e = ", e_orbit, "\t period = ", p_orbit.in_(units.yr) return bodies, time_peri
def form_stars(sink, initial_mass_function=settings.stars_initial_mass_function, lower_mass_limit=settings.stars_lower_mass_limit, upper_mass_limit=settings.stars_upper_mass_limit, local_sound_speed=0.2 | units.kms, logger=None, randomseed=None, **keyword_arguments): """ Let a sink form stars. """ logger = logger or logging.getLogger(__name__) if randomseed is not None: logger.info("setting random seed to %i", randomseed) numpy.random.seed(randomseed) # sink_initial_density = sink.mass / (4/3 * numpy.pi * sink.radius**3) initialised = sink.initialised or False if not initialised: logger.debug("Initialising sink %i for star formation", sink.key) next_mass = generate_next_mass( initial_mass_function=initial_mass_function, lower_mass_limit=lower_mass_limit, upper_mass_limit=upper_mass_limit, ) # sink.next_number_of_stars = len(next_mass) # sink.next_total_mass = next_mass.sum() sink.next_primary_mass = next_mass[0] # if sink.next_number_of_stars > 1: # sink.next_secondary_mass = next_mass[1] # if sink.next_number_of_stars > 2: # sink.next_tertiary_mass = next_mass[2] sink.initialised = True if sink.mass < sink.next_primary_mass: logger.debug("Sink %i is not massive enough for the next star", sink.key) return [sink, Particles()] # We now have the first star that will be formed. # Next, we generate a list of stellar masses, so that the last star in the # list is just one too many for the sink's mass. mass_left = sink.mass - sink.next_primary_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) new_stars = Particles(number_of_stars) new_stars.age = 0 | units.Myr new_stars[0].mass = sink.next_primary_mass new_stars[1:].mass = masses[:-1] sink.next_primary_mass = masses[-1] # if sink.next_number_of_stars > 1: # new_stars[1].mass = sink.next_secondary_mass # if sink.next_number_of_stars > 2: # new_stars[2].mass = sink.next_tertiary_mass new_stars.position = sink.position new_stars.velocity = sink.velocity # Random position within the sink radius radius = sink.radius rho = numpy.random.random(number_of_stars) * 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) y = rho * sin(phi) * sin(theta) z = rho * cos(phi) new_stars.x += x new_stars.y += y new_stars.z += z # Random velocity, sample magnitude from gaussian with local sound speed # like Wall et al (2019) # temperature = 10 | units.K try: local_sound_speed = sink.u.sqrt() except AttributeError: local_sound_speed = local_sound_speed # or (gamma * local_pressure / density).sqrt() velocity_magnitude = numpy.random.normal( # loc=0.0, # <- since we already added the velocity of the sink scale=local_sound_speed.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) vy = velocity_magnitude * sin(velocity_phi) * sin(velocity_theta) vz = velocity_magnitude * cos(velocity_phi) new_stars.vx += vx new_stars.vy += vy new_stars.vz += vz new_stars.origin_cloud = sink.key # For Pentacle, this is the PP radius new_stars.radius = 0.05 | units.parsec sink.mass -= new_stars.total_mass() # TODO: fix sink's momentum etc # EDIT: Do not shrink the sinks at this point, but rather when finished # forming stars. # # Shrink the sink's (accretion) radius to prevent it from accreting # # relatively far away gas and moving a lot # sink.radius = ( # (sink.mass / sink_initial_density) # / (4/3 * numpy.pi) # )**(1/3) # cleanup # sink.initialised = False new_stars.birth_mass = new_stars.mass return [sink, new_stars]
def form_stars_from_group_older_version( group_index, sink_particles, newly_removed_gas, 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): """ Form stars from specific group of sinks. NOTE: This is the older version where removed gas is considered as star-forming region. This is now being updated to the above latest version. """ 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) logger.info("%i sinks found in group #%i: %s", number_of_sinks, group_index, group.key) group_mass = group.total_mass() logger.info("Group mass: %s", 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", group_index) 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() logger.info("Group's next primary mass is %s", group.group_next_primary_mass[0]) # 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 # Find the newly removed gas in the group removed_gas = Particles() if not newly_removed_gas.is_empty(): for s in group: removed_gas_by_this_sink = ( newly_removed_gas[newly_removed_gas.accreted_by_sink == s.key]) removed_gas.add_particles(removed_gas_by_this_sink) logger.info("%i removed gas found in this group", len(removed_gas)) # Star forming regions that contain the removed gas and the group # of sinks if not removed_gas.is_empty(): removed_gas.radius = removed_gas.h_smooth star_forming_regions = group.copy() star_forming_regions.density = ( star_forming_regions.initial_density / 1000 ) # /1000 to reduce likelihood of forming stars in sinks star_forming_regions.accreted_by_sink = star_forming_regions.key try: star_forming_regions.u = star_forming_regions.u except AttributeError: star_forming_regions.u = local_sound_speed**2 star_forming_regions.add_particles(removed_gas.copy()) star_forming_regions.sorted_by_attribute("density").reversed() # Generate a probability list of star forming region indices the # stars should associate to probabilities = (star_forming_regions.density / star_forming_regions.density.sum()) probabilities /= probabilities.sum() # Ensure sum is exactly 1 logger.info("Max & min probabilities: %s, %s", probabilities.max(), probabilities.min()) logger.info("%i star forming regions", len(star_forming_regions)) def delta_positions_and_velocities(new_stars, star_forming_regions, probabilities): """ Assign positions and velocities of stars in the star forming regions according to the probability distribution """ number_of_stars = len(new_stars) # Create an index list of removed gas from probability list sample = numpy.random.choice(len(star_forming_regions), number_of_stars, p=probabilities) # Assign the stars to the removed gas according to the sample 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.accreted_by_sink 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) X = 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) V = list(zip(*[vx, vy, vz])) | units.kms return X, V dX, dV = delta_positions_and_velocities(new_stars, star_forming_regions, probabilities) 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 # mass_ratio = 1 - new_stars.total_mass()/group.total_mass() # group.mass *= mass_ratio 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: %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