def projected_area(mesh, normal): """ Get the mesh's projected area along a certain vector It was initially unseen in trimesh's code It could be replaced by trimesh.path.polygons.projected(m, normal=[1,1,0]).area now See : https://github.com/mikedh/trimesh/issues/898 :param mesh: Mesh :type mesh: trimesh.Trimesh :param normal: Normal vector to the plane on which the mesh should be projected :type normal: loas.Vec """ normal = normal/np.linalg.norm(normal) m = copy.deepcopy(mesh) dir_rot = normal + loas.Vec(1,0,0) if np.linalg.norm(dir_rot) < 1e-6: dir_rot = loas.Vec(0,1,0) m.apply_transform(trimesh.transformations.rotation_matrix(math.pi, dir_rot.line())) m.apply_transform(trimesh.transformations.projection_matrix((0,0,0),(1,0,0))) polygons = [ shapely.geometry.Polygon(triangle[:,1:]) for index_triangle, triangle in enumerate(m.triangles) if np.linalg.norm(m.face_normals[index_triangle] - np.array([1,0,0])) < 1e-6 ] poly_merged = shapely.ops.unary_union(polygons) return poly_merged.area
def axis(self): """ Return the normalized rotation axis of the quaternion """ res = loas.Vec(self.b, self.c, self.d) if np.linalg.norm(res) == 0: return loas.Vec(1, 0, 0) return res / np.linalg.norm(res)
def get_Q_sfc(normal): """ Returns the quaternion to pass from the reference frame to the local surface frame where the x axis is normal to the surface """ dir_rot = normal + loas.Vec(1, 0, 0) if np.linalg.norm(dir_rot) < 1e-6: dir_rot = loas.Vec(0, 1, 0) return loas.Quat(0, *dir_rot)
def _getRandomOrigin(): r = sat_bs_radius*math.sqrt(random.random()) theta = 2*math.pi*random.random() rel_pos = loas.Vec( r*math.cos(theta), r*math.sin(theta), 2*sat_bs_radius ) return rel_pos + sat_bs_center
def euler(self): """ Returns euler angle of the rotation defined by the quaternion It is bad to use euler angles. """ q0, q1, q2, q3 = self.a, self.b, self.c, self.d return loas.Vec( math.atan2(2 * (q0 * q1 + q2 * q3), 1 - 2 * (q1**2 + q2**2)), math.asin(2 * (q0 * q2 - q3 * q1)), math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2**2 + q3**2)))
def _runSingleSim(self, sat_W, sat_Q, sat_speed, sat_temp, part_density, part_mol_mass, part_temp, with_drag_coef): """ Utilitary function that runs a single simultation runSim is a user-friendly wrapper of this simulation """ part_mass = part_mol_mass/scipy.constants.N_A nb_part = round(self.part_per_iteration/self.nb_workers) args = ( True, sat_speed, sat_Q, sat_W, sat_temp, nb_part, part_mass, part_temp, part_density ) for i in range(self.nb_workers): self.workers_input_queue.put(args) torque = loas.Vec(0,0,0) drag = 0 particle_data = [] for _ in range(self.nb_workers): torque_add, drag_add = self.workers_output_queue.get() torque += torque_add drag += drag_add if with_drag_coef: S_p = loas.utils.projected_area( self.sat_mesh, sat_Q.R2V(loas.Vec(0,0,1)) ) drag_coef = drag*2/part_density/sat_speed**2/S_p return drag, torque, drag_coef return drag, torque
def model(part_speed_i, normal, sat_temp, part_mass): # Schamberg model Q_sfc = get_Q_sfc(normal) part_speed_r_norm = scipy.stats.maxwell.rvs( scale=math.sqrt(scipy.constants.k * sat_temp / part_mass)) theta = 2 * theta_0 / math.pi * math.asin( 2 * random.random() - 1 ) + theta_i #angle par rapport à la normale à la surface (donc le vecteur (1,0,0)) dans le repère de la sfc phi = 2 * math.pi * random.random() #angle dans le plan (yOz) part_speed_r = Q_sfc.V2R(part_speed_r_norm * loas.Vec(math.cos(theta), math.sin(theta) * math.cos(phi), math.sin(theta) * math.sin(phi))) return part_speed_r
def plot_all_traj(): plt.figure() ax = plt.subplot(111, projection='aitoff') change_labels(ax) for p in np.linspace(0, 2 * math.pi, 4)[:-1]: for t in np.linspace(0, math.pi, 4)[:-1]: Q0 = v2q(c2v(t, p)) traj = np.array([ v2c(q2v(Q)) for Q in sim_traj(Q0, loas.Vec(0, 0, 0), 50, 1000) ]) plt.scatter(*v2m(t, p), color='blue') plt.plot(*v2m(traj[:, 0], traj[:, 1]), color='red') print(p, t) plt.grid() plt.show()
def plot_all_traj_normals(): dt = 50 ax = plt.figure().gca(projection='3d') for p in np.linspace(0, 2 * math.pi, 10)[:-1]: for t in np.linspace(0, math.pi, 10)[:-1]: Q0 = v2q(c2v(t, p)) traj = [q2v(Q) for Q in sim_traj(Q0, loas.Vec(0, 0, 0), dt, 1000)] normals = [] for i in range(1, len(traj) - 1): normal = (traj[i + 1] - traj[i]).cross(traj[i - 1] - traj[i]) / dt**2 normals.append(normal.line()) normals = np.array(normals) ax.plot(*np.transpose(np.array(normals)), color='red') #plt.plot(*np.transpose(np.array(traj))) print(p, t) ax.auto_scale_xyz(*[[np.min(normals), np.max(normals)]] * 3) plt.legend() plt.show()
def model(part_speed_i, normal, sat_temp, part_mass): Q_sfc = get_Q_sfc(normal) #quaternion de passage sur la surface if random.random() < epsilon: # specular reflexion normal_rel_speed = ( np.transpose(normal) @ part_speed_i)[0, 0] * normal part_speed_r = part_speed_i - 2 * normal_rel_speed else: part_speed_r_norm = scipy.stats.maxwell.rvs( scale=math.sqrt(scipy.constants.k * sat_temp / part_mass)) theta = math.asin( 2 * random.random() - 1 ) #angle par rapport à la normale à la surface (donc le vecteur (1,0,0)) dans le repère de la sfc phi = 2 * math.pi * random.random() #angle dans le plan (yOz) part_speed_r = Q_sfc.V2R(part_speed_r_norm * loas.Vec(math.cos(theta), math.sin(theta) * math.cos(phi), math.sin(theta) * math.sin(phi))) return part_speed_r
def run(w): mesh = trimesh.load_mesh("../models/crocus/45.stl") mesh.apply_scale(.01) sat_Q = [ loas.Quat(math.cos(angle / 2), math.sin(angle / 2), 0, 0) for angle in np.linspace(0, 2 * math.pi, 100) ] drag = loas.RAD(sat_mesh=mesh, model=loas.models.maxwell(0.10), part_per_iteration=1e5, nb_workers=8) drag.start() res = drag.runSim(sat_W=loas.Vec(float(w), 0, 0), sat_Q=sat_Q, sat_speed=7000, sat_temp=300, part_density=1e-11, part_mol_mass=0.016, part_temp=1800, with_drag_coef=True) drag.stop() with open('res_temp/' + str(w), 'w') as f: f.write(str(res))
import loas import math # load mesh object and resize it mesh = trimesh.load_mesh("./models/slab.stl") bounds = np.array(mesh.bounds) mesh.apply_translation( -(bounds[0] + bounds[1]) / 2) # center the satellite (the mass center should be on 0,0) mesh.apply_scale(.08) # rescale the model drag = loas.RAD(sat_mesh=mesh, model=loas.models.maxwell(0.10), part_per_iteration=1e4, nb_workers=6) drag.start() sat_Q = [ loas.Quat(math.cos(angle / 2), math.sin(angle / 2), 0, 0) for angle in np.linspace(0, math.pi / 2, 10) ] print( drag.runSim(sat_W=loas.Vec(0, 0, 0), sat_Q=sat_Q, sat_speed=7000, sat_temp=300, part_density=1e-11, part_mol_mass=0.016, part_temp=1800, with_drag_coef=True)) drag.stop()
phis = (math.pi * (1 + 5**0.5) * indices)%(2*math.pi) return thetas, phis Ts, Ps = get_grid(500) Qs = [v2q(c2v(t,p)) for t,p in np.transpose([Ts,Ps])] mesh = trimesh.load_mesh("../models/satellite.stl") drag = loas.RAD( sat_mesh = mesh, model = loas.models.maxwell(0.10), part_per_iteration = 1e5, nb_workers = 8 ) drag.start() res = drag.runSim sat_W = loas.Vec(0,0,0), sat_Q = Qs, sat_speed = 7000, sat_temp = 300, part_density = 1e-11, part_mol_mass = 0.016, part_temp = 1800, with_drag_coef = False ) drag.stop() # save the toque in the satellite frame bc it dont depends on the rotation around (0,0,1) in this frame Cs = [Qs[index].R2V(torque) for index, (drag, torque) in enumerate(res)] for name, obj in (('T.npy', Ts), ('P.npy', Ps), ('C.npy', Cs)): with open('res_temp/'+name, 'wb') as f:
def v2q(vec): #Q.V2R(loas.Vec(x,z,y)) = (0,0,1) assert np.linalg.norm(vec) - 1 < 1e-10 axis = vec + loas.Vec(0, 0, -1) if np.linalg.norm(axis) < 1e-6: axis = loas.Vec(1, 0, 0) return loas.Quat(0, *axis)
def c2v(theta, phi): return loas.Vec( math.sin(theta) * math.cos(phi), math.sin(theta) * math.sin(phi), math.cos(theta))
def q2v(Q): return Q.R2V(loas.Vec(0, 0, -1))
def vec(self): """ Returns a numpy array representation of the quaternion """ return loas.Vec(self.a, self.b, self.c, self.d)
def _sparse_drag_worker( workers_input_queue, workers_output_queue, sat_mesh, max_part_batch, part_per_iteration, model ): """ Unitary worker for Sparse Atmospheric drag computation. Computes the collision of a certain amount of random particles on the mesh, adn sends back the torque Every worker is launched only once. The trick is that, once started, communicate with the rest of the program through Queues. There are two queues, one for input, one for output. The worker waits for any input and once it gets it, runs the simulation, and outputs its parameters. :param workers_input_queue: Queue which is used to pass parameters to the worker :type workers_input_queue: multiprocessing.Queue :param workers_output_queue: Queue which is used to sends the workers simulation result :type workers_input_queue: multiprocessing.Queue :param create_batch_data_save: if set to true, the worker will output at each iteration a list containing the result of every particle simultation so that it can be worked into a batch and printed on the pyglet's window. :type create_batch_data_save: bool :param sat_mesh: Satellite's mesh :type sat_mesh: trimesh.Trimesh :param sat_bs_radius: Radius of the bounding sphere of the saellite, i.e. the sphere that entirely includes the satellite. It defines the radius of thje circle in witch it is needed to generate particles :type sat_bs_radius: float :param max_part_batch: Maximum number of particles given at once to the ray tester. If set to 0, disables, the limit. :param max_part_batch: int """ sat_bs_radius = sat_mesh.bounding_sphere.extents[0]/2 sat_bs_center = loas.Vec(*sat_mesh.bounding_sphere.center_mass) workers_running = True def _getRandomOrigin(): r = sat_bs_radius*math.sqrt(random.random()) theta = 2*math.pi*random.random() rel_pos = loas.Vec( r*math.cos(theta), r*math.sin(theta), 2*sat_bs_radius ) return rel_pos + sat_bs_center while workers_running: args = ( workers_running, sat_speed, sat_Q, sat_W, sat_temp, part_pending, part_mass, part_temp, part_density ) = workers_input_queue.get() if not workers_running: return part_speed = loas.Vec(0,0,-sat_speed) dir_sat = sat_Q.R2V(part_speed)[:,0] torque_dt = loas.Vec(0,0,0) drag_dt = 0 while part_pending > 0: # disable batching if max_part_batch to 0 if max_part_batch > 0: part_batch = min(part_pending, max_part_batch) else: part_batch = part_pending part_pending -= part_batch origins_sat = np.array([sat_Q.R2V(_getRandomOrigin())[:,0] for _ in range(part_batch)]) locations, indexes_ray, indexes_tri = sat_mesh.ray.intersects_location( ray_origins=origins_sat, ray_directions=[dir_sat]*part_batch ) #filter only for closest point locations_filtered = {} for index, location_sat in enumerate(locations): index_tri = indexes_tri[index] index_ray = indexes_ray[index] origin_sat = origins_sat[index_ray] dist = (origin_sat[0] - location_sat[0])**2 + (origin_sat[1] - location_sat[1])**2 + (origin_sat[2] - location_sat[2])**2 if not index_ray in locations_filtered: locations_filtered[index_ray] = (location_sat, index_tri, dist) elif locations_filtered[index_ray][2] > dist: locations_filtered[index_ray] = (location_sat, index_tri, dist) # process torque given by actual hit point for location_sat, index_tri, _ in locations_filtered.values(): location = sat_Q.V2R(loas.Vec(*location_sat)) normal = sat_Q.V2R(loas.Vec(*sat_mesh.face_normals[index_tri])) normal /= np.linalg.norm(normal) part_speed_i = part_speed - sat_W.cross(location) part_speed_r = model(part_speed_i, normal, sat_temp, part_mass) momentum = part_mass*(part_speed_i-part_speed_r) drag_dt += ((np.transpose(part_speed)/np.linalg.norm(part_speed)) @ momentum)[0,0] torque_dt += location.cross(momentum) scale_factor = part_density / part_mass * sat_speed * math.pi*sat_bs_radius**2 / part_per_iteration torque = torque_dt * scale_factor drag = drag_dt * scale_factor workers_output_queue.put((torque, drag))