def pos_shift( shell_radii, particles_per_shell, res_increase_factor=85, ): """ Return the relative positions shift for this shell as a numpy array. """ total_particles = sum(particles_per_shell) relative_positions = numpy.zeros( total_particles * 3, dtype=float, ).reshape(total_particles, 3) ipos = 1 for shell in range(1, len(shell_radii)): num_pts = particles_per_shell[shell] indices = numpy.arange(0, num_pts, dtype=float) + 0.5 phi = arccos(1 - 2 * indices / num_pts) theta = pi * (1 + 5**0.5) * indices ipos2 = ipos + num_pts x = shell_radii[shell] * cos(theta) * sin(phi) y = shell_radii[shell] * sin(theta) * sin(phi) z = shell_radii[shell] * cos(phi) relative_positions[ipos:ipos2, 0:3] = numpy.dstack((x, y, z)) ipos = ipos2 return relative_positions
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
def new_rotation_matrix(phi, theta, psi): """ Return the rotation matrix, to rotate positions, around the x-axis (phi), y-axis (theta) and z-axis (psi). See wikipedia for reference """ return numpy.array( ( (cos(theta)*cos(psi), -cos(phi)*sin(psi) + sin(phi)*sin(theta)*cos(psi), sin(phi)*sin(psi) + cos(phi)*sin(theta)*cos(psi)) , (cos(theta)*sin(psi), cos(phi)*cos(psi) + sin(phi)*sin(theta)*sin(psi), -sin(phi)*cos(psi) + cos(phi)*sin(theta)*sin(psi)) , (-sin(theta) , sin(phi)*cos(theta) , cos(phi)*cos(theta)) ) )
def test31(self): """ test trigonometric unit stuff """ self.assertEqual(units.pi, numpy.pi) a = units.pi self.assertEqual(trigo.to_rad(a), numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 180. | units.deg) self.assertEqual(trigo.to_rev(a), 0.5 | units.rev) a = 90 | units.deg self.assertEqual(trigo.to_rad(a), numpy.pi / 2 | units.rad) self.assertEqual(trigo.to_deg(a), 90. | units.deg) self.assertEqual(trigo.to_rev(a), 0.25 | units.rev) a = 0.75 | units.rev self.assertEqual(trigo.to_rad(a), 3 / 2. * numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 270. | units.deg) self.assertEqual(trigo.to_rev(a), 0.75 | units.rev) a = 2 * numpy.pi self.assertEqual(trigo.to_rad(a), 2 * numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 360. | units.deg) self.assertEqual(trigo.to_rev(a), 1. | units.rev) a = 45. | units.deg self.assertEqual(trigo.sin(a), numpy.sin(45. / 180 * numpy.pi)) self.assertEqual(trigo.cos(a), numpy.cos(45. / 180 * numpy.pi)) self.assertEqual(trigo.tan(a), numpy.tan(45. / 180 * numpy.pi)) a = 1. | units.rad self.assertEqual(trigo.sin(a), numpy.sin(1.)) self.assertEqual(trigo.cos(a), numpy.cos(1.)) self.assertEqual(trigo.tan(a), numpy.tan(1.)) a = 0.125 | units.rev self.assertEqual(trigo.sin(a), numpy.sin(45. / 180 * numpy.pi)) self.assertEqual(trigo.cos(a), numpy.cos(45. / 180 * numpy.pi)) self.assertEqual(trigo.tan(a), numpy.tan(45. / 180 * numpy.pi)) a = 45. | units.deg self.assertAlmostEqual(trigo.arcsin(trigo.sin(a)), 45. | units.deg, 13) self.assertAlmostEqual(trigo.arccos(trigo.cos(a)), 45. | units.deg, 13) self.assertAlmostEqual(trigo.arctan(trigo.tan(a)), 45. | units.deg, 13)
def test31(self): """ test trigonometric unit stuff """ self.assertEqual(units.pi,numpy.pi) a=units.pi self.assertEqual(trigo.to_rad(a), numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 180. | units.deg) self.assertEqual(trigo.to_rev(a), 0.5 | units.rev) a=90 | units.deg self.assertEqual(trigo.to_rad(a), numpy.pi/2 | units.rad) self.assertEqual(trigo.to_deg(a), 90. | units.deg) self.assertEqual(trigo.to_rev(a), 0.25 | units.rev) a=0.75 | units.rev self.assertEqual(trigo.to_rad(a), 3/2.*numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 270. | units.deg) self.assertEqual(trigo.to_rev(a), 0.75 | units.rev) a=2*numpy.pi self.assertEqual(trigo.to_rad(a), 2*numpy.pi | units.rad) self.assertEqual(trigo.to_deg(a), 360. | units.deg) self.assertEqual(trigo.to_rev(a), 1. | units.rev) a=45. | units.deg self.assertEqual(trigo.sin(a),numpy.sin(45./180*numpy.pi)) self.assertEqual(trigo.cos(a),numpy.cos(45./180*numpy.pi)) self.assertEqual(trigo.tan(a),numpy.tan(45./180*numpy.pi)) a=1. | units.rad self.assertEqual(trigo.sin(a),numpy.sin(1.)) self.assertEqual(trigo.cos(a),numpy.cos(1.)) self.assertEqual(trigo.tan(a),numpy.tan(1.)) a=0.125 | units.rev self.assertEqual(trigo.sin(a),numpy.sin(45./180*numpy.pi)) self.assertEqual(trigo.cos(a),numpy.cos(45./180*numpy.pi)) self.assertEqual(trigo.tan(a),numpy.tan(45./180*numpy.pi)) a=45. | units.deg self.assertAlmostEqual(trigo.arcsin(trigo.sin(a)),45. | units.deg,13) self.assertAlmostEqual(trigo.arccos(trigo.cos(a)),45. | units.deg,13) self.assertAlmostEqual(trigo.arctan(trigo.tan(a)),45. | units.deg,13)
def get_potential_at_point(self, eps, x, y, z): """ Calculate the spiral arm potential at specified point from Cox & Gomez 2002 Input: eps, x, y, z Returns: potential """ phi = arctan(y / x) # if x < (0 | units.parsec): # phi = pi + phi # d2 r = (x**2 + y**2)**0.5 gamma = (self.number_of_arms * (phi + self.phir * (self.time_initial + self.model_time) - log(r / self.fiducial_radius) / tan(self.pitch_angle))) result = 0 | units.parsec for n in range(3): Kn = (n + 1) * self.number_of_arms / (r * sin(self.pitch_angle)) Bn = Kn * self.scale_height * (1 + 0.4 * Kn * self.scale_height) Dn = (1 + Kn * self.scale_height + 0.3 * (Kn * self.scale_height)**2) / (1 + 0.3 * Kn * self.scale_height) result = (result + (self.Cz[n] / (Dn * Kn)) * cos( (n + 1) * gamma) * (1 / cosh((Kn * z) / Bn))**Bn) spiral_value = ( -4 * pi * G * self.scale_height * self.density_at_fiducial_radius * exp(-(r - self.fiducial_radius) / self.radial_scale_length) * result) return spiral_value.in_(units.parsec**2 * units.Myr**-2)
def true_anomaly_from_eccentric_anomaly(E, e): return 2 * arctan2((1 + e)**0.5 * sin(E / 2), (1 - e)**0.5 * cos(E / 2))
def rel_posvel_arrays_from_orbital_elements( primary_mass, secondary_mass, semi_major_axis, eccentricity=0 | units.rad, true_anomaly=0 | units.rad, inclination=0 | units.rad, longitude_of_the_ascending_node=0 | units.rad, argument_of_periapsis=0 | units.rad, G=nbody_system.G): """ Returns relative positions/velocities for secondaries orbiting primaries. If primary_mass is a scalar, assumes the same primary for all secondaries. """ try: number_of_secondaries = len(secondary_mass) except: number_of_secondaries = 1 # arrays need to be equal to number of secondaries, or have just one value primary_mass = equal_length_array_or_scalar(primary_mass, length=number_of_secondaries) semi_major_axis = equal_length_array_or_scalar( semi_major_axis, length=number_of_secondaries) eccentricity = equal_length_array_or_scalar(eccentricity, length=number_of_secondaries) true_anomaly = equal_length_array_or_scalar(true_anomaly, length=number_of_secondaries) inclination = equal_length_array_or_scalar(inclination, length=number_of_secondaries) longitude_of_the_ascending_node = equal_length_array_or_scalar( longitude_of_the_ascending_node, length=number_of_secondaries) argument_of_periapsis = equal_length_array_or_scalar( argument_of_periapsis, length=number_of_secondaries) cos_true_anomaly = cos(true_anomaly) sin_true_anomaly = sin(true_anomaly) cos_inclination = cos(inclination) sin_inclination = sin(inclination) cos_arg_per = cos(argument_of_periapsis) sin_arg_per = sin(argument_of_periapsis) cos_long_asc_nodes = cos(longitude_of_the_ascending_node) sin_long_asc_nodes = sin(longitude_of_the_ascending_node) # alpha is a unit vector directed along the line of node alphax = (cos_long_asc_nodes * cos_arg_per - sin_long_asc_nodes * sin_arg_per * cos_inclination) alphay = (sin_long_asc_nodes * cos_arg_per + cos_long_asc_nodes * sin_arg_per * cos_inclination) alphaz = sin_arg_per * sin_inclination alpha = numpy.array([alphax, alphay, alphaz]) # beta is a unit vector perpendicular to alpha and the orbital angular # momentum vector betax = (-cos_long_asc_nodes * sin_arg_per - sin_long_asc_nodes * cos_arg_per * cos_inclination) betay = (-sin_long_asc_nodes * sin_arg_per + cos_long_asc_nodes * cos_arg_per * cos_inclination) betaz = cos_arg_per * sin_inclination beta = numpy.array([betax, betay, betaz]) # Relative position and velocity separation = ( # Compute the relative separation semi_major_axis * (1.0 - eccentricity**2) / (1.0 + eccentricity * cos_true_anomaly)) position_vector = (separation * cos_true_anomaly * alpha + separation * sin_true_anomaly * beta).T velocity_tilde = ( (G * (primary_mass + secondary_mass) / (semi_major_axis * (1.0 - eccentricity**2)))**0.5) # Common factor velocity_vector = (-1.0 * velocity_tilde * sin_true_anomaly * alpha + velocity_tilde * (eccentricity + cos_true_anomaly) * beta).T return position_vector, velocity_vector
def swan_eq(**kwargs): grav = kwargs["grav"] rho_air = kwargs["rho_air"] rho_water = kwargs["rho_water"] flow = kwargs["flow"] fhigh = kwargs["fhigh"] u10 = kwargs["u10"] udir = kwargs["udir"] gridname = kwargs["gridname"] coord = kwargs["coord"] depth = kwargs["depth"] visc = kwargs["viscosity"] planetary_radius = kwargs["planetary_radius"] pixel_scale = kwargs["pixel_scale"] under_relaxation_factor = kwargs["under_relaxation_factor"] msc = kwargs["msc"] mdc = kwargs["mdc"] print(kwargs) nodes = read_set_from_file("kraken_nodes", "amuse", close_file=True) elements = read_set_from_file("kraken_elements", "amuse", close_file=True) s = Swan(grid_type="unstructured", input_grid_type="unstructured", redirection="none", coordinates=coord) s.parameters.gravitational_acceleration = grav s.parameters.air_density = rho_air s.parameters.water_density = rho_water s.parameters.planetary_radius = planetary_radius s.parameters.maximum_error_level = 2 s.parameters.under_relaxation_factor = under_relaxation_factor ncells = len(elements) nverts = len(nodes) s.parameters.number_of_cells = ncells s.parameters.number_of_vertices = nverts s.parameters.number_of_directions = mdc s.parameters.number_of_frequencies = msc s.parameters.lowest_frequency = flow s.parameters.highest_frequency = fhigh s.parameters.minimum_wind_speed = 0.1 | units.m / units.s s.parameters.max_iterations_stationary = 50 s.parameters.use_gen3_parameters = True s.parameters.use_breaking_parameters = True s.parameters.use_triads_parameters = True s.parameters.use_friction_parameters = True s.parameters.use_input_wind = True if visc.number > 0: s.parameters.use_input_turbulent_visc = True s.parameters.turbulent_viscosity_factor = 1. #~ s.parameters.uniform_wind_velocity=u10 #~ s.parameters.uniform_wind_direction=udir print(s.parameters) #~ raise channel = nodes.new_channel_to(s.nodes) if coord == "spherical": channel.copy_attributes(["lon", "lat", "vmark"]) else: channel.transform(["x", "y", "vmark"], lambda x, y, z: (x * pixel_scale, y * pixel_scale, z), ["x", "y", "vmark"]) #~ channel.copy_attributes(["x","y","vmark"]) channel = elements.new_channel_to(s.elements) channel.copy_attributes(["n1", "n2", "n3"]) #~ print abs(s.nodes.lon-nodes.lon).max() #~ print abs(s.nodes.lat-nodes.lat).max() forcings = s.forcings.empty_copy() if depth == "default": forcings.depth = nodes.depth elif depth == "x2": forcings.depth = nodes.depth * 2 elif depth == "x10": forcings.depth = nodes.depth * 10 elif depth == "sqrt": maxdepth = nodes.depth.max() forcings.depth = maxdepth * (nodes.depth / maxdepth)**0.5 else: raise Exception("unknown depth option") wind_vx = u10 * cos(udir) wind_vy = u10 * sin(udir) if coord == "spherical": forcings.wind_vx = wind_vx forcings.wind_vy = wind_vy else: forcings.wind_vx = wind_vx * sin(nodes.lon) + wind_vy * cos(nodes.lon) forcings.wind_vy = -wind_vx * cos(nodes.lon) + wind_vy * sin(nodes.lon) #~ pyplot.quiver(s.forcings.x.number,s.forcings.y.number, #~ forcings.wind_vx.number,forcings.wind_vy.number, scale=50) #~ pyplot.show() #~ raise if visc.number > 0: forcings.visc = visc forcings.new_channel_to(s.forcings).copy_attributes( ["depth", "wind_vx", "wind_vy", "visc"]) else: forcings.new_channel_to(s.forcings).copy_attributes( ["depth", "wind_vx", "wind_vy"]) s.evolve_model(0. | units.s) label = runlabel(**kwargs) print("label:", label) with open("args_" + label + ".pkl", "w") as f: pickle.dump(kwargs, f) write_set_to_file(s.nodes, gridname + "_nodes_eq_" + label + ".amuse", "amuse", append_to_file=False)
def new_rotation_matrix(phi, theta, psi): """ Return the rotation matrix, to rotate positions, around the x-axis (phi), y-axis (theta) and z-axis (psi). See wikipedia for reference """ return numpy.array( ((cos(theta) * cos(psi), -cos(phi) * sin(psi) + sin(phi) * sin(theta) * cos(psi), sin(phi) * sin(psi) + cos(phi) * sin(theta) * cos(psi)), (cos(theta) * sin(psi), cos(phi) * cos(psi) + sin(phi) * sin(theta) * sin(psi), -sin(phi) * cos(psi) + cos(phi) * sin(theta) * sin(psi)), (-sin(theta), sin(phi) * cos(theta), cos(phi) * cos(theta))))
def new_stars_from_sink(origin, upper_mass_limit=125 | units.MSun, lower_mass_limit=0.1 | units.MSun, default_radius=0.25 | units.pc, velocity_dispersion=1 | units.kms, logger=None, initial_mass_function="kroupa", distribution="random", randomseed=None, **keyword_arguments): """ Form stars from an origin particle that keeps track of the properties of this region. """ logger = logger or logging.getLogger(__name__) if randomseed is not None: logger.info("setting random seed to %i", randomseed) numpy.random.seed(randomseed) try: initialised = origin.initialised except AttributeError: initialised = False if not initialised: logger.debug("Initialising origin particle %i for star formation", origin.key) next_mass = new_star_cluster( initial_mass_function=initial_mass_function, upper_mass_limit=upper_mass_limit, lower_mass_limit=lower_mass_limit, number_of_stars=1, **keyword_arguments) origin.next_primary_mass = next_mass[0].mass origin.initialised = True if origin.mass < origin.next_primary_mass: logger.debug( "Not enough in star forming region %i to form the next star", origin.key) return Particles() mass_reservoir = origin.mass - origin.next_primary_mass stellar_masses = new_star_cluster( stellar_mass=mass_reservoir, upper_mass_limit=upper_mass_limit, lower_mass_limit=lower_mass_limit, imf=initial_mass_function, ).mass number_of_stars = len(stellar_masses) new_stars = Particles(number_of_stars) new_stars.age = 0 | units.yr new_stars[0].mass = origin.next_primary_mass new_stars[1:].mass = stellar_masses[:-1] origin.next_primary_mass = stellar_masses[-1] new_stars.position = origin.position new_stars.velocity = origin.velocity try: radius = origin.radius except AttributeError: radius = default_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 velocity_magnitude = numpy.random.normal( scale=velocity_dispersion.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 = origin.key origin.mass -= new_stars.total_mass() return new_stars
def true_anomaly_from_eccentric_anomaly(E, e): return 2*arctan2((1+e)**0.5*sin(E/2), (1-e)**0.5*cos(E/2))
def rel_posvel_arrays_from_orbital_elements( primary_mass, secondary_mass, semi_major_axis, eccentricity=0 | units.rad, true_anomaly=0 | units.rad, inclination=0 | units.rad, longitude_of_the_ascending_node=0 | units.rad, argument_of_periapsis=0 | units.rad, G=nbody_system.G ): """ Returns relative positions/velocities for secondaries orbiting primaries. If primary_mass is a scalar, assumes the same primary for all secondaries. """ try: number_of_secondaries = len(secondary_mass) except: number_of_secondaries = 1 # arrays need to be equal to number of secondaries, or have just one value primary_mass = equal_length_array_or_scalar( primary_mass, length=number_of_secondaries) semi_major_axis = equal_length_array_or_scalar( semi_major_axis, length=number_of_secondaries) eccentricity = equal_length_array_or_scalar( eccentricity, length=number_of_secondaries) true_anomaly = equal_length_array_or_scalar( true_anomaly, length=number_of_secondaries) inclination = equal_length_array_or_scalar( inclination, length=number_of_secondaries) longitude_of_the_ascending_node = equal_length_array_or_scalar( longitude_of_the_ascending_node, length=number_of_secondaries) argument_of_periapsis = equal_length_array_or_scalar( argument_of_periapsis, length=number_of_secondaries) cos_true_anomaly = cos(true_anomaly) sin_true_anomaly = sin(true_anomaly) cos_inclination = cos(inclination) sin_inclination = sin(inclination) cos_arg_per = cos(argument_of_periapsis) sin_arg_per = sin(argument_of_periapsis) cos_long_asc_nodes = cos(longitude_of_the_ascending_node) sin_long_asc_nodes = sin(longitude_of_the_ascending_node) # alpha is a unit vector directed along the line of node alphax = ( cos_long_asc_nodes*cos_arg_per - sin_long_asc_nodes*sin_arg_per*cos_inclination ) alphay = ( sin_long_asc_nodes*cos_arg_per + cos_long_asc_nodes*sin_arg_per*cos_inclination ) alphaz = sin_arg_per*sin_inclination alpha = numpy.array([alphax, alphay, alphaz]) # beta is a unit vector perpendicular to alpha and the orbital angular # momentum vector betax = ( - cos_long_asc_nodes*sin_arg_per - sin_long_asc_nodes*cos_arg_per*cos_inclination ) betay = ( - sin_long_asc_nodes*sin_arg_per + cos_long_asc_nodes*cos_arg_per*cos_inclination ) betaz = cos_arg_per*sin_inclination beta = numpy.array([betax, betay, betaz]) # Relative position and velocity separation = ( # Compute the relative separation semi_major_axis*(1.0 - eccentricity**2) / (1.0 + eccentricity*cos_true_anomaly) ) position_vector = ( separation*cos_true_anomaly*alpha + separation*sin_true_anomaly*beta ).T velocity_tilde = ( ( G*(primary_mass + secondary_mass) / (semi_major_axis*(1.0 - eccentricity**2)) )**0.5 ) # Common factor velocity_vector = ( -1.0 * velocity_tilde * sin_true_anomaly * alpha + velocity_tilde*(eccentricity + cos_true_anomaly)*beta ).T return position_vector, velocity_vector
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(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
def coriolis_beta(lat, omega=(1. | units.rev) / (sidereal_day)): return 2 * omega * cos(lat) / Rearth