def get_light_path_branch(kin_vec, grating_list, path_list, crystal_list, g_orders): """ Get the light path for one of the branch. :param kin_vec: The incident wave vector with a shape of (3,) :param grating_list: :param path_list: :param crystal_list: :param g_orders: The diffraction orders of the gratings :return: """ # Get kout from the first grating kout_g1 = kin_vec + g_orders[0] * grating_list[0].base_wave_vector # Get the intersection point on the Bragg crystal intersect_1 = path_list[0] * kout_g1 / util.l2_norm(kout_g1) # Get the intersection point on the rest of the crystals and the second grating. intersect_list, kout_vec_list = get_point_with_definite_path( kin_vec=kout_g1, path_sections=path_list[1:-1], crystal_list=crystal_list, init_point=intersect_1) # Get the final output momentum kout_g2 = kout_vec_list[-1] + g_orders[1] * grating_list[1].base_wave_vector # Calculate the observation point intersect_final = intersect_list[ -1] + path_list[-1] * kout_g2 / util.l2_norm(kout_g2) # Get branch 1 info num = len(path_list) + 1 intersect_branch = np.zeros((num, 3), dtype=np.float64) intersect_branch[1, :] = intersect_1[:] intersect_branch[2:-1, :] = intersect_list[:, :] intersect_branch[-1, :] = intersect_final[:] kout_branch = np.zeros((num, 3), dtype=np.float64) kout_branch[0, :] = kin_vec[:] kout_branch[1, :] = kout_g1[:] kout_branch[2:-1, :] = kout_vec_list[:, :] kout_branch[-1, :] = kout_g2[:] return intersect_branch, kout_branch
def set_pulse_properties(self, central_energy, polar, sigma_x, sigma_y, sigma_z, x0): """ Set the pulse properties assuming that the pulse is propagating along the positive z direction. :param central_energy: :param polar: :param sigma_x: The unit is fs. However, in the function, it's converted into um. :param sigma_y: The unit is fs. However, in the function, it's converted into um. :param sigma_z: The unit is fs. However, in the function, it's converted into um. :param x0: :return: """ # Get the corresponding wave vector self.klen0 = util.kev_to_wave_number(energy=central_energy) self.polar = np.array(np.reshape(polar, (3, )), dtype=np.complex128) self.k0 = np.array([0., 0., self.klen0]) self.n = self.k0 / util.l2_norm(self.k0) self.omega0 = self.klen0 * util.c self.x0 = x0 # Initialize the sigma matrix self.set_sigma_mat(sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z)
def set_pulse_properties(self, central_energy, polar, sigma_x, sigma_y, sigma_z, x0): """ Set the pulse properties assuming that the pulse is propagating along the positive z direction. :param central_energy: :param polar: :param sigma_x: The unit is fs. However, in the function, it's converted into um. :param sigma_y: The unit is fs. However, in the function, it's converted into um. :param sigma_z: The unit is fs. However, in the function, it's converted into um. :param x0: :return: """ # Get the corresponding wave vector self.klen0 = util.kev_to_wave_number(energy=central_energy) self.polar = np.array(np.reshape(polar, (3, )), dtype=np.complex128) self.k0 = np.array([0., 0., self.klen0]) self.n = self.k0 / util.l2_norm(self.k0) self.omega0 = self.klen0 * util.c self.x0 = x0 # Initialize the sigma matrix self.set_sigma_mat(sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z) # Normalize the pulse such that the incident total energy is 1 au # Then in this case, if one instead calculate the square L2 norm of the spectrum, then # the value is 8 * pi ** 3 self.scaling = 2. * np.sqrt(2) * np.power(np.pi, 0.75) * np.sqrt( sigma_x * sigma_y * sigma_z * (util.c**3))
def get_point_with_definite_path(kin_vec, path_sections, crystal_list, init_point): """ Provide the crystals, calculate teh corresponding intersection points. :param kin_vec: :param path_sections: :param crystal_list: :param init_point: :return: """ # Get the number of crystals num = len(crystal_list) # Prepare holders for the calculation intersect_list = np.zeros((num, 3), dtype=np.float64) kout_list = np.zeros((num, 3), dtype=np.float64) # Copy the initial point init = np.copy(init_point) kin = np.copy(kin_vec) for idx in range(num): # Get the reflected wavevector kout = util.get_bragg_kout(kin=kin, h=crystal_list[idx].h, normal=crystal_list[idx].normal, compare_length=False) # Get the next intersection point intersect = init + kout * path_sections[idx] / util.l2_norm(kout) # Update the intersection list and the kout list kout_list[idx, :] = kout[:] intersect_list[idx, :] = intersect[:] # Update the kin and init init = np.copy(intersect) kin = np.copy(kout) return intersect_list, kout_list
def get_lightpath(device_list, kin, initial_point, final_plane_point, final_plane_normal): """ This function is used to generate the light path of the incident wave vector in the series of devices. This function correctly handles the light path through the telescopes. :param device_list: :param kin: :param initial_point: :param final_plane_normal: :param final_plane_point: :return: """ # Create a holder for kout vectors kout_list = [np.copy(kin), ] # Create a list for the intersection points intersection_list = [np.copy(initial_point)] # Path length path_length = 0. # Loop through all the devices. for idx in range(len(device_list)): ############################################################### # Step 1: Get the device device = device_list[idx] ############################################################### # Step 2: Find the intersection and kout if device.type == "Crystal: Bragg Reflection": intersection_list.append(util.get_intersection(s=intersection_list[-1], k=kout_list[-1], n=device.normal, x0=device.surface_point)) # Find the path length displacement = intersection_list[-1] - intersection_list[-2] path_length += np.dot(displacement, kout_list[-1]) / util.l2_norm(kout_list[-1]) # Find the output k vector kout_list.append(util.get_bragg_kout(kin=kout_list[-1], h=device.h, normal=device.normal)) if device.type == "Transmissive Grating": intersection_list.append(util.get_intersection(s=intersection_list[-1], k=kout_list[-1], n=device.normal, x0=device.surface_point)) # Find the path length displacement = intersection_list[-1] - intersection_list[-2] path_length += np.dot(displacement, kout_list[-1]) / util.l2_norm(kout_list[-1]) # Find the wave vecotr kout_list.append(kout_list[-1] + device.momentum_transfer) ################################################################ # Step 3: Find the output position on the observation plane intersection_list.append(util.get_intersection(s=intersection_list[-1], k=kout_list[-1], x0=final_plane_point, n=final_plane_normal)) # Update the path length displacement = intersection_list[-1] - intersection_list[-2] path_length += np.dot(displacement, kout_list[-1]) / util.l2_norm(kout_list[-1]) return intersection_list, kout_list, path_length
def set_normal(self, normal): self.normal = normal / util.l2_norm(normal) self.__update_h()
def adjust_path_length(fix_branch_path, fix_branch_crystal, var_branch_path, var_branch_crystal, grating_pair, kin, delay_time=None): """ This function automatically change the configurations of the variable branch to match the delay time. :param delay_time: :param fix_branch_path: :param var_branch_path: :param fix_branch_crystal: :param var_branch_crystal: :param grating_pair: :param kin: :return: """ # --------------------------------------------------------- # Step 1 : Get the original path # --------------------------------------------------------- (intersect_fixed, kout_fixed) = get_light_path_branch( kin_vec=kin, grating_list=grating_pair, path_list=fix_branch_path, crystal_list=fix_branch_crystal, g_orders=[-1, 1]) # By default, -1 corresponds to the fixed branch. (intersect_var, kout_var) = get_light_path_branch(kin_vec=kin, grating_list=grating_pair, path_list=var_branch_path, crystal_list=var_branch_crystal, g_orders=[1, -1]) # ---------------------------------------------------------- # Step 2 : Tune the final section of the fixed branch # to make sure the intersection point is close to the z axis # ---------------------------------------------------------- # Make sure the fixed delay branch intersect with the y axis sine = np.abs(kout_fixed[-2, 1] / np.linalg.norm(kout_fixed[-2])) fix_branch_path[-2] = np.abs(intersect_fixed[-3][1]) / sine # Update our understanding of the path of the branch with fixed delay (intersect_fixed, kout_fixed) = get_light_path_branch( kin_vec=kin, grating_list=grating_pair, path_list=fix_branch_path, crystal_list=fix_branch_crystal, g_orders=[-1, 1]) # By default, -1 corresponds to the fixed branch. # ---------------------------------------------------------- # Step 3 : Tune the path sections of the variable branch # so that the light path intersect with the fixed branch on the second grating # ---------------------------------------------------------- term_1 = np.linalg.norm( np.cross(intersect_fixed[-2] - intersect_var[-4], kout_var[-3])) term_2 = np.linalg.norm(np.cross( kout_var[-2], kout_var[-3])) / np.linalg.norm(kout_var[-2]) var_branch_path[-2] = term_1 / term_2 term_1 = np.linalg.norm( np.cross(intersect_fixed[-2] - intersect_var[-4], kout_var[-2])) term_2 = np.linalg.norm(np.cross( kout_var[-3], kout_var[-2])) / np.linalg.norm(kout_var[-3]) var_branch_path[-3] = term_1 / term_2 # Tune channel-cut pair together to find the delay zero position path_diff = np.sum(fix_branch_path[:-1]) - np.sum(var_branch_path[:-1]) klen = util.l2_norm(kout_var[-3]) cos_theta = np.dot(kout_var[-2], kout_var[-3]) / klen**2 delta = path_diff / 2. / (1 - cos_theta) # Change the variable path sections with the calculated length change var_branch_path[-5] += delta var_branch_path[-3] += delta var_branch_path[-4] -= 2 * delta * cos_theta # ---------------------------------------------------------- # Step 3 : Adjust the path sections to match the delay time. # ---------------------------------------------------------- if delay_time is not None: # Find the momentum information (intersect_var, kout_var) = get_light_path_branch( kin_vec=kin, grating_list=grating_pair, path_list=var_branch_path, crystal_list=var_branch_crystal, g_orders=[1, -1]) # By default, -1 corresponds to the fixed branch. delay_length = delay_time * util.c cos_theta = np.dot(kout_var[2], kout_var[3]) / util.l2_norm( kout_var[3]) / util.l2_norm(kout_var[2]) delta = delay_length / 2. / (1 - cos_theta) # Change the variable path sections with the calculated length change var_branch_path[-5] += delta var_branch_path[-3] += delta var_branch_path[-4] -= 2 * delta * cos_theta # ---------------------------------------------------------- # Step 4 : Get the corresponding intersection position # ---------------------------------------------------------- (intersect_fixed, kout_fixed) = get_light_path_branch(kin_vec=kin, grating_list=grating_pair, path_list=fix_branch_path, crystal_list=fix_branch_crystal, g_orders=[-1, 1]) (intersect_var, kout_var) = get_light_path_branch(kin_vec=kin, grating_list=grating_pair, path_list=var_branch_path, crystal_list=var_branch_crystal, g_orders=[1, -1]) return (fix_branch_path, kout_fixed, intersect_fixed, var_branch_path, kout_var, intersect_var)
def get_light_trajectory_with_total_path(kin_vec, init_point, total_path, crystal_list, g_orders): """ Get the light path for one of the branch. :param kin_vec: The incident wave vector with a shape of (3,) :param init_point: :param total_path: :param crystal_list: :param g_orders: The diffraction orders of the gratings :return: """ # Create holder for the calculation num = len(crystal_list) + 1 intersect_array = np.zeros((num, 3), dtype=np.float64) kout_array = np.zeros((num, 3), dtype=np.float64) intersect_array[0, :] = np.copy(init_point) kout_array[0, :] = np.copy(kin_vec) # Create auxiliary variables for the calculation g_idx = 0 # The index for grating remain_path = np.copy(total_path) # Loop through all the crystals to get the path for idx in range(1, num): crystal_obj = crystal_list[idx - 1] # Get the output wave vector if crystal_obj.type == "Crystal: Bragg Reflection": kout_array[idx, :] = util.get_bragg_kout(kin=kout_array[idx - 1], h=crystal_obj.h, normal=crystal_obj.normal, compare_length=False) if crystal_obj.type == "Transmissive Grating": kout_array[idx, :] = ( kout_array[idx - 1] + g_orders[g_idx] * crystal_obj.base_wave_vector) # Update the index of the grating g_idx += 1 # The last intersection point need to be calculated individually if idx == num - 1: break # Get the new intersection point intersect_array[idx] = util.get_intersection( s=intersect_array[idx - 1], k=kout_array[idx], n=crystal_list[idx].normal, x0=crystal_list[idx].surface_point) # Get the remaining path path_tmp = util.l2_norm(intersect_array[idx] - intersect_array[idx - 1]) # If the path length is not long enough to cover the whole device, then stop early. if path_tmp > remain_path: intersect_array[idx] = ( intersect_array[idx - 1] + kout_array[idx] * remain_path / util.l2_norm(kout_array[idx])) return intersect_array, kout_array else: remain_path -= path_tmp # If the path length is long though to cover the whole path, then calculate the final point intersect_array[-1] = ( intersect_array[-2] + kout_array[-1] * remain_path / util.l2_norm(kout_array[-1])) return intersect_array, kout_array