def main(): # Fixed settings stellar_evolution = True se_code = "SeBa" length_unit = units.parsec dpi = 600 percentile = 0.9995 # for determining vmax # Parse arguments args = new_argument_parser() starsfilename = args.starsfilename gasfilename = args.gasfilename followfilename = args.followfilename imagefilename = args.imagefilename imagetype = args.imagetype vmax = args.vmax if args.vmax > 0 else None n_fieldstars = args.n_fieldstars filetype = args.filetype contours = args.contours np.random.seed(args.seed) plot_axes = args.plot_axes angle_x = args.angle_x | units.deg angle_y = args.angle_y | units.deg angle_z = args.angle_z | units.deg sourcebands = args.sourcebands psf_type = args.psf_type.lower() psf_sigma = args.psf_sigma age = args.age | units.Myr image_width = args.width | units.parsec pixels = args.pixels frames = args.frames if followfilename is not None: use_com = True else: use_com = args.use_com x_offset = args.x_offset | units.parsec y_offset = args.y_offset | units.parsec z_offset = args.z_offset | units.parsec extinction = args.calculate_extinction # Derived settings if psf_type not in ["hubble", "gaussian"]: print(("Invalid PSF type: %s" % psf_type)) exit() image_size = [pixels, pixels] # If the nr of pixels is changed, zoom the PSF accordingly. zoom_factor = pixels / 2048. if starsfilename: stars = read_set_from_file( starsfilename, filetype, close_file=True, ) if stellar_evolution and (age > 0 | units.Myr): print(( "Calculating luminosity/temperature for %s old stars..." % (age) )) evolve_to_age(stars, age, stellar_evolution=se_code) if use_com: if followfilename is not None: followstars = read_set_from_file( followfilename, filetype, close_file=True, ) center_on_these_stars = followstars.get_intersecting_subset_in( stars, ) else: center_on_these_stars = stars com = center_on_these_stars.center_of_mass() x_offset, y_offset, z_offset = com stars.x -= x_offset stars.y -= y_offset stars.z -= z_offset else: stars = Particles() if n_fieldstars: minage = 400 | units.Myr maxage = 12 | units.Gyr fieldstars = new_field_stars( n_fieldstars, width=image_width, height=image_width, ) fieldstars.age = ( minage + ( np.random.sample(n_fieldstars) * (maxage - minage) ) ) evolve_to_age(fieldstars, 0 | units.yr, stellar_evolution=se_code) stars.add_particles(fieldstars) if gasfilename: gas = read_set_from_file( gasfilename, filetype, close_file=True, ) if use_com: if stars.is_empty(): com = gas.center_of_mass() x_offset, y_offset, z_offset = com gas.x -= x_offset gas.y -= y_offset gas.z -= z_offset # Gadget and Fi disagree on the definition of h_smooth. # For gadget, need to divide by 2 to get the Fi value (??) gas.h_smooth *= 0.5 gas.radius = gas.h_smooth # Select only the relevant gas particles (plus a margin) minx = (1.1 * -image_width/2) maxx = (1.1 * image_width/2) miny = (1.1 * -image_width/2) maxy = (1.1 * image_width/2) gas_ = gas.select( lambda x, y: x > minx and x < maxx and y > miny and y < maxy, ["x", "y"] ) gas = gas_ else: gas = Particles() # gas.h_smooth = 0.05 | units.parsec converter = nbody_system.nbody_to_si( stars.total_mass() if not stars.is_empty() else gas.total_mass(), image_width, ) # Initialise figure and axes fig = initialise_image( dpi=dpi, image_size=image_size, length_unit=length_unit, image_width=image_width, plot_axes=plot_axes, x_offset=x_offset, y_offset=y_offset, z_offset=z_offset, ) ax = fig.get_axes()[0] xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() for frame in range(frames): fig = initialise_image(fig) if (frame != 0) or (frames == 1): if not stars.is_empty(): rotate(stars, angle_x, angle_y, angle_z) if not gas.is_empty(): rotate(gas, angle_x, angle_y, angle_z) image, vmax = make_image( stars=stars if not stars.is_empty() else None, gas=gas if not gas.is_empty() else None, converter=converter, image_width=image_width, image_size=image_size, percentile=percentile, calc_temperature=True, age=age, vmax=vmax, sourcebands=sourcebands, zoom_factor=zoom_factor, psf_type=psf_type, psf_sigma=psf_sigma, return_vmax=True, extinction=extinction, ) if not stars.is_empty(): ax.imshow( image, origin='lower', extent=[ xmin, xmax, ymin, ymax, ], ) if contours and not gas.is_empty(): gascontours = column_density_map( gas, zoom_factor=zoom_factor, image_width=image_width, image_size=image_size, ) gascontours[np.isnan(gascontours)] = 0.0 vmax = np.max(gascontours) / 2 # vmin = np.min(image[np.where(image > 0.0)]) vmin = vmax / 100 levels = 10**( np.linspace( np.log10(vmin), np.log10(vmax), num=5, ) )[1:] # print(vmin, vmax) # print(levels) ax.contour( origin='lower', levels=levels, colors="white", linewidths=0.1, extent=[ xmin, xmax, ymin, ymax, ], ) else: image = column_density_map( gas, image_width=image_width, image_size=image_size, ) ax.imshow( image, origin='lower', extent=[ xmin, xmax, ymin, ymax, ], cmap="gray", ) if frames > 1: savefilename = "%s-%06i.%s" % ( imagefilename if imagefilename is not None else "test", frame, imagetype, ) else: savefilename = "%s.%s" % ( imagefilename if imagefilename is not None else "test", imagetype, ) plt.savefig( savefilename, dpi=dpi, )
def main(): mode = [] # Fixed settings stellar_evolution = True se_code = "SeBa" length_unit = units.parsec dpi = 600 percentile = 0.9995 # for determining vmax # Parse arguments args = new_argument_parser() starsfilename = args.starsfilename gasfilename = args.gasfilename imagefilename = args.imagefilename imagetype = args.imagetype vmax = args.vmax if args.vmax > 0 else None n_fieldstars = args.n_fieldstars filetype = args.filetype contours = args.contours np.random.seed(args.seed) plot_axes = args.plot_axes angle_x = args.angle_x | units.deg angle_y = args.angle_y | units.deg angle_z = args.angle_z | units.deg sourcebands = args.sourcebands psf_type = args.psf_type.lower() psf_sigma = args.psf_sigma age = args.age | units.Myr image_width = args.width | units.parsec pixels = args.pixels frames = args.frames # Derived settings if args.calculate_extinction: mode.append("extinction") if psf_type not in ["hubble", "gaussian"]: print(("Invalid PSF type: %s" % psf_type)) exit() image_size = [pixels, pixels] # If the nr of pixels is changed, zoom the PSF accordingly. zoom_factor = pixels / 2048. if starsfilename: stars = read_set_from_file( starsfilename, filetype, close_file=True, ) if stellar_evolution and (age > 0 | units.Myr): print(("Calculating luminosity/temperature for %s old stars..." % (age))) evolve_to_age(stars, age, stellar_evolution=se_code) com = stars.center_of_mass() stars.position -= com else: stars = Particles() if n_fieldstars: minage = 400 | units.Myr maxage = 12 | units.Gyr fieldstars = new_field_stars( n_fieldstars, width=image_width, height=image_width, ) fieldstars.age = (minage + (np.random.sample(n_fieldstars) * (maxage - minage))) evolve_to_age(fieldstars, 0 | units.yr, stellar_evolution=se_code) stars.add_particles(fieldstars) if not stars.is_empty(): mode.append("stars") if gasfilename: gas = read_set_from_file( gasfilename, filetype, close_file=True, ) if "stars" not in mode: com = gas.center_of_mass() gas.position -= com # Gadget and Fi disagree on the definition of h_smooth. # For gadget, need to divide by 2 to get the Fi value (??) gas.h_smooth *= 0.5 gas.radius = gas.h_smooth else: gas = Particles() if not gas.is_empty(): mode.append("gas") if contours: mode.append("contours") # gas.h_smooth = 0.05 | units.parsec converter = nbody_system.nbody_to_si( stars.total_mass() if "stars" in mode else gas.total_mass(), image_width, ) # Initialise figure and axes fig = initialise_image( dpi=dpi, image_size=image_size, length_unit=length_unit, image_width=image_width, plot_axes=plot_axes, ) ax = fig.get_axes()[0] xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() for frame in range(frames): fig = initialise_image(fig) if (frame != 0) or (frames == 1): if not stars.is_empty(): rotate(stars, angle_x, angle_y, angle_z) if not gas.is_empty(): rotate(gas, angle_x, angle_y, angle_z) image, vmax = make_image( stars, gas, mode=mode, converter=converter, image_width=image_width, image_size=image_size, percentile=percentile, calc_temperature=True, age=age, vmax=vmax, sourcebands=sourcebands, zoom_factor=zoom_factor, psf_type=psf_type, psf_sigma=psf_sigma, return_vmax=True, ) if "stars" in mode: ax.imshow( image, origin='lower', extent=[ xmin, xmax, ymin, ymax, ], ) if ("contours" in mode) and ("gas" in mode): gascontours = column_density_map( gas, zoom_factor=zoom_factor, image_width=image_width, image_size=image_size, ) gascontours[np.isnan(gascontours)] = 0.0 vmax = np.max(gascontours) / 2 # vmin = np.min(image[np.where(image > 0.0)]) vmin = vmax / 100 levels = 10**(np.linspace( np.log10(vmin), np.log10(vmax), num=5, ))[1:] # print(vmin, vmax) # print(levels) ax.contour( gascontours, origin='lower', levels=levels, colors="white", linewidths=0.1, extent=[ xmin, xmax, ymin, ymax, ], ) else: image = column_density_map( gas, image_width=image_width, image_size=image_size, ) ax.imshow( image, origin='lower', extent=[ xmin, xmax, ymin, ymax, ], cmap="gray", ) plt.savefig( "%s-%06i.%s" % ( imagefilename, frame, imagetype, ), dpi=dpi, )
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