def total_angle(self): """ The total angle in radians from start_point to end_point """ if self._total_angle is None: self._total_angle = Vector.angle_between(self(0), self(1)) return self._total_angle
def __init__( self, initial_coordinate: ArrayLike, final_coordinate: ArrayLike, atmosphere_params: ArrayLike, operating_frequency: float, point_number: Optional[int] = None, use_high_ray: Optional[bool] = True, ): """ supply initial and final points, atmosphere parameters (f_0, r_m, y_m) and operating_frequency (f) :param initial_coordinate : array_like, shape (3,) the cartesian coordinates of the path start :param final_coordinate : array_like, shape (3,) the cartesian coordinates of the path end :param atmosphere_params : Tuple, shape (3,) the atmosphere parameters as a tuple of (max_plasma_frequency (f_0), height_of_max_plasma_frequency(r_m), layer_semi_width(y_m)) :param operating_frequency: float The operating frequency of the ray :param point_number : int The number of points to interpolate between in the final path :param use_high_ray : bool Whether or not to use high ray """ super().__init__() # Initial and final points are normalized, but poly_fit(0) and poly_fit(1) will return the radial component # for the initial and final point initial_rad, final_rad = linalg.norm(initial_coordinate), linalg.norm(final_coordinate) self.initial_point = initial_coordinate / linalg.norm(initial_coordinate) self.final_point = final_coordinate / linalg.norm(final_coordinate) self.normal_vec = np.cross(self.initial_point, self.final_point) self.normal_vec = self.normal_vec / linalg.norm(self.normal_vec) angle_between = Vector.angle_between(initial_coordinate, final_coordinate) # Parameters for quasi-parabolic path only depend on atmospheric model and ignore magnetic field self._parameters = self.calculate_parameters(atmosphere_params, operating_frequency) # Point number is the number of points to calculate. All points in between are interpolated from PC spline if point_number is not None: self.point_number = point_number else: self.point_number = int(angle_between * EARTH_RADIUS) # Each point is evenly spaced along the great circle path connecting initial and final coordinates # Each point is a 2-vector holding its angular value in radians (along great circle path) # and its radial value at each point (in km) self.points = np.zeros((self.point_number, 2)) self.points[:, 0] = np.linspace(0, angle_between, self.point_number) self.points[:, 1] = np.linspace(initial_rad, final_rad, self.point_number) # Real representation of the line will be a poly fit of radius vs angle along great circle self._poly_fit = None self._total_angle = None self.using_high_ray = use_high_ray
def interpolate_params(self, radial=False, degree=3): self._total_angle = Vector.angle_between(self.initial_point, self.final_point) self._poly_fit_angular = UnivariateSpline( self._angular_parameters[:, 0], self._angular_parameters[:, 1], k=min(degree, len(self._angular_parameters) - 1), s=0, ext=0 ) if radial: self._poly_fit_radial = UnivariateSpline( self._radial_parameters[:, 0], self._radial_parameters[:, 1], k=degree, s=0, ext=0 ) else: cartesian_points = np.zeros((len(self._radial_parameters), 3)) for index in range(len(self._radial_parameters)): alpha = self._radial_parameters[index, 0] r_1 = Rotation.from_rotvec(self.normal_vec * alpha * self.total_angle) v_1 = r_1.apply(Vector.unit_vector(self.initial_point)) rotation_vec_2 = Vector.unit_vector(np.cross(self.normal_vec, v_1)) rotation_vec_2 *= self._poly_fit_angular(alpha) r_2 = Rotation.from_rotvec(rotation_vec_2) v_2 = r_2.apply(v_1) v_2 *= self._radial_parameters[index, 1] cartesian_points[index] = v_2 self._poly_fit_cartesian = [] for index in range(3): self._poly_fit_cartesian.append( UnivariateSpline( self._radial_parameters[:, 0], cartesian_points[:, index], k=degree, s=0 ) )
def compile_points(self): fc, rm, rb, ym, f = self._parameters total_angle = Vector.angle_between(self.initial_point, self.final_point) points_unprocessed = quasi_parabolic_core.get_quasi_parabolic_path( total_angle * EARTH_RADIUS, f, rm + EARTH_RADIUS, ym, fc ** 2 ) if self.using_high_ray or len(points_unprocessed) == 1: points_unprocessed = points_unprocessed[0] else: points_unprocessed = points_unprocessed[1] points_unprocessed[:, 1] = points_unprocessed[:, 1] points_unprocessed[:, 0] = points_unprocessed[:, 0] / EARTH_RADIUS self.points = points_unprocessed self._total_angle = total_angle self._poly_fit = UnivariateSpline( points_unprocessed[:, 0], points_unprocessed[:, 1], k=3, s=0, ext=0 )
def visualize(self, initial_point: np.ndarray, final_point: np.ndarray, fig: plt.Figure = None, ax: plt.Axes = None, show: bool = False, **kwargs) -> Optional[Tuple[plt.Figure, plt.Axes]]: """ Given an initial and final point (and some formatting parameters), visualize the atmosphere on a matplotlib graph :param initial_point : np.ndarray, shape (3, ) 3-vec corresponding to the starting coordinate of the path in cartesian coordinates. Used to determine the interval in which to calculate the atmosphere :param final_point : np.ndarray, shape (3, ) 3-vec corresponding to the starting coordinate of the path in cartesian coordinates. Used to determine the interval in which to calculate the atmosphere :param fig : Figure, optional If fig and ax are both provided, display the atmosphere colormap on top of the old axes otherwise, create a new figure and axes to work with :param ax : Axes, optional If fig and ax are both provided, display the atmosphere colormap on top of the old axes otherwise, create a new figure and axes to work with :param show : boolean, optional If show is true, display the plotted atmosphere immediately and return nothing. Otherwise, don't display and instead return the computed figure and axes :param kwargs : dict, optional Any additional kwargs are passed to the imshow function :returns : (Figure, Axes), optional If show is False, return the computed figure and axes, otherwise return nothing. """ total_angle = Vector.angle_between(initial_point, final_point) normal_vec = Vector.unit_vector(np.cross(initial_point, final_point)) point_number = 500 if ax is None or fig is None: fig, ax = plt.subplots(1, 1, figsize=(6, 4.5)) radii = np.linspace(Constants.EARTH_RADIUS, Constants.EARTH_RADIUS + 400E3, point_number) alpha = np.linspace(0, total_angle, point_number) r_1 = Rotation.from_rotvec(normal_vec * alpha.reshape(-1, 1)) v_1 = r_1.apply(initial_point) v_1 = Vector.cartesian_to_spherical(v_1) frequency_grid = np.zeros((point_number, point_number)) for i in range(point_number): plotted_vecs = np.repeat(v_1[i].reshape(-1, 1), point_number, axis=1).T plotted_vecs[:, 0] = radii frequency_grid[:, i] = self.plasma_frequency( Vector.spherical_to_cartesian(plotted_vecs)) / 1E6 image = ax.imshow( frequency_grid, cmap='gist_rainbow', interpolation='bilinear', origin='lower', alpha=1, aspect='auto', extent=[0, total_angle * Constants.EARTH_RADIUS / 1000, 0, 400], **kwargs) ax.yaxis.set_ticks_position('both') color_bar = fig.colorbar(image, ax=ax) color_bar.set_label("Plasma Frequency (MHz)") if show: ax.set_title("Chapman Layers Atmosphere") plt.show() else: return fig, ax