def _plot_observer_spectrum(self, label_lines=False): """Plot the spectrum components and observer spectra on a 1x2 panel plot. The left panel has the components, whilst the right panel has the observer spectrum. Parameters ---------- label_lines: bool Plot line IDs. """ name = self._get_spec_key("spec") if name not in self.available: raise IOError( "A .spec/.log_spec file was not read in, cannot use this function" ) fig, ax = plt.subplots(1, 2, figsize=(12, 5), sharey="row") # Plot the components of the observer spectrum, i.e Emitted, Created, # Disc, etc. for component in self.columns[:-self.n_inclinations]: if component in ["Lambda", "Freq."]: continue ax[0] = self._plot_thing(component, name, label_lines=label_lines, ax_update=ax[0]) for line in ax[0].get_lines( ): # Set the different spectra to have a transparency line.set_alpha(0.7) ax[0].legend(ncol=2, loc="upper right").set_zorder(0) # Now plot the observer spectra for inclination in self.inclinations: ax[1] = self._plot_thing(inclination, name, label_lines=label_lines, ax_update=ax[1]) for label, line in zip(self.inclinations, ax[1].get_lines()): line.set_alpha(0.7) line.set_label(str(label) + r"$^{\circ}$") ax[1].set_ylabel("") ax[1].legend(ncol=2, loc="upper right").set_zorder(0) # Final clean up to make a nice spectrum ax[0].set_title("Components") ax[1].set_title("Observer spectra") fig = finish_figure(fig, wspace=0) return fig, ax
def components(spectrum, xmin=None, xmax=None, scale="loglog", alpha=0.65, multiply_by_spatial_units=False, display=False): """Plot the different components of the spectrum. The components are the columns labelled with words, rather than inclination angles. These columns are not supposed to add together, i.e. all of them together shouldn't equal the Emitted spectrum, which is the angle averaged escaping flux/luminosity. Parameters ---------- spectrum: pypython.Spectrum The spectrum object. xmin: float [optional] The minimum x boundary to plot. xmax: float [optional] The maximum x boundary to plot. scale: str [optional] The scaling of the axes. alpha: float [optional] The line transparency on the plot. multiply_by_spatial_units: bool [optional] Plot in flux or nu Lnu instead of flux density or luminosity. display: bool [optional] Display the object once plotted. Returns ------- fig: plt.Figure matplotlib Figure object. ax: plt.Axes matplotlib Axes object. """ fig, ax = plt.subplots(2, 1, figsize=(12, 10)) ax[0] = _plot_subplot(ax[0], spectrum, ["CenSrc", "Disk", "WCreated"], xmin, xmax, alpha, scale, multiply_by_spatial_units) ax[1] = _plot_subplot(ax[1], spectrum, ["Created", "Emitted", "Wind", "HitSurf"], xmin, xmax, alpha, scale, multiply_by_spatial_units) fig = finish_figure(fig) fig.savefig(f"{spectrum.fp}/{spectrum.root}_{spectrum.current}_components.png") if display: plt.show() return fig, ax
def _wind1d(m_points, parameter_points, distance_units, scale="logx", fig=None, ax=None, i=0, j=0): """Plot a 1D wind. Parameters ---------- m_points: np.ndarray The 1st axis points, which are the r bins. parameter_points: np.ndarray The wind parameter to be plotted, in the same shape as the n_points and m_points arrays. distance_units: pypython.wind.WindDistanceUnits The units of the distance axis. scale: str [optional] The scaling of the axes: [logx, logy, loglog, linlin] fig: plt.Figure [optional] A Figure object to update, otherwise a new one will be created. ax: plt.Axes [optional] An axes array to update, otherwise a new one will be created. i: int [optional] The i index for the sub panel to plot onto. j: int [optional] The j index for the sub panel to plot onto. Returns ------- fig: plt.Figure The (updated) Figure object for the plot. ax: plt.Axes The (updated) axes array for the plot. """ if fig is None or ax is None: fig, ax = plt.subplots(figsize=(8, 6), squeeze=False) ax[i, j].plot(m_points, parameter_points) if distance_units == WindDistanceUnits.cm: ax[i, j].set_xlabel(r"$R$ [cm]") else: ax[i, j].set_xlabel(r"$r / R_{g}$") # ax[i, j].set_xlim(np.min(m_points[m_points > 0]), np.max(m_points)) ax[i, j] = set_axes_scales(ax[i, j], scale) fig = finish_figure(fig) return fig, ax
def _plot_thing(self, thing, spec_type, scale="loglog", label_lines=False, ax_update=None): """Plot a specific column in a spectrum file. Parameters ---------- thing: str The name of the thing to be plotted. scale: str The scale of the axes, i.e. loglog, logx, logy, linlin. label_lines: bool Plot line IDs. ax_update: plt.Axes An plt.Axes object to update, i.e. to plot on. """ if ax_update: ax = ax_update else: fig, ax = plt.subplots(figsize=(9, 5)) if spec_type: key = spec_type else: key = self.current units = self.spectra[key].units distance = self.spectra[key].distance ax = set_axes_scales(ax, scale) ax = plot.set_axes_labels(ax, units=units, distance=distance) # How things are plotted depends on the units of the spectrum if units == SpectrumUnits.f_lm or units == SpectrumUnits.l_lm: x_thing = "Lambda" else: x_thing = "Freq." label = thing if thing.isdigit(): label += r"$^{\circ}$" ax.plot(self.spectra[key][x_thing], self.spectra[key][thing], label=label, zorder=0) if label_lines: ax = plot.add_line_ids(ax, plot.common_lines(units), linestyle="none", fontsize=10) if ax_update: return ax else: fig = finish_figure(fig) return fig, ax
def _wind2d(m_points, n_points, parameter_points, distance_units, coordinate_system, inclinations_to_plot=None, scale="loglog", vmin=None, vmax=None, fig=None, ax=None, i=0, j=0): """Plot a 2D wind using a contour plot. Parameters ---------- m_points: np.ndarray The 1st axis points, either x or angular (in degrees) bins. n_points: np.ndarray The 2nd axis points, either z or radial bins. parameter_points: np.ndarray The wind parameter to be plotted, in the same shape as the n_points and m_points arrays. distance_units: pypython.wind.WindDistanceUnits The units of the distance axes. coordinate_system: pypython.wind.WindCoordSystem The coordinate system in use, either rectilinear or polar. inclinations_to_plot: List[str] [optional] A list of inclination angles to plot onto the ax[0, 0] sub panel. Must be strings and 0 < inclination < 90. scale: str [optional] The scaling of the axes: [logx, logy, loglog, linlin] vmin: float [optional] The minimum value to plot. vmax: float [optional] The maximum value to plot. fig: plt.Figure [optional] A Figure object to update, otherwise a new one will be created. ax: plt.Axes [optional] An axes array to update, otherwise a new one will be created. i: int [optional] The i index for the sub panel to plot onto. j: int [optional] The j index for the sub panel to plot onto. Returns ------- fig: plt.Figure The (updated) Figure object for the plot. ax: plt.Axes The (updated) axes array for the plot. """ if fig is None or ax is None: if coordinate_system == WindCoordSystem.cylindrical: fig, ax = plt.subplots(figsize=(8, 6), squeeze=False) elif coordinate_system == WindCoordSystem.polar: fig, ax = plt.subplots(figsize=(8, 6), squeeze=False, subplot_kw={"projection": "polar"}) else: raise ValueError(f"Unknown projection, expected {WindCoordSystem.cylindrical} or {WindCoordSystem.polar}") im = ax[i, j].pcolormesh(m_points, n_points, parameter_points, shading="auto", vmin=vmin, vmax=vmax, linewidth=0, rasterized=True) fig.colorbar(im, ax=ax[i, j]) # this plots lines representing sight lines for different observers of # different inclinations if inclinations_to_plot: n_coords = np.unique(m_points) for inclination in inclinations_to_plot: if coordinate_system == WindCoordSystem.cylindrical: m_coords = n_coords * np.tan(0.5 * PI - np.deg2rad(float(inclination))) else: x_coords = np.logspace(np.log10(0), np.max(n_points)) m_coords = x_coords * np.tan(0.5 * PI - np.deg2rad(90 - float(inclination))) m_coords = np.sqrt(x_coords**2 + m_coords**2) ax[0, 0].plot(n_coords, m_coords, label=inclination + r"$^{\circ}$") ax[0, 0].legend(loc="lower left") # Clean up the axes with labs and set up scales, limits etc if coordinate_system == WindCoordSystem.cylindrical: if distance_units == WindDistanceUnits.cm: ax[i, j].set_xlabel(r"$x$ [cm]") ax[i, j].set_ylabel(r"$z$ [cm]") else: ax[i, j].set_xlabel(r"$x / R_{g}$") ax[i, j].set_ylabel(r"$z / R_{g}$") ax[i, j].set_xlim(np.min(m_points[m_points > 0]), np.max(m_points)) ax[i, j] = set_axes_scales(ax[i, j], scale) else: ax[i, j].set_theta_zero_location("N") ax[i, j].set_theta_direction(-1) ax[i, j].set_thetamin(0) ax[i, j].set_thetamax(90) ax[i, j].set_rlabel_position(90) if distance_units == WindDistanceUnits.cm: ax[i, j].set_ylabel(r"$\log_{10}(r)$ [cm]") else: ax[i, j].set_ylabel(r"$\log_{10}(r / R_{g})$") ax[i, j].set_ylim(np.min(n_points[n_points > 0]), np.max(n_points)) fig = finish_figure(fig) return fig, ax
def reprocessing(spectrum, xmin=None, xmax=None, scale="loglog", label_edges=True, alpha=0.75, display=False): """Create a plot to show the amount of reprocessing in the model. Parameters ---------- Returns ------- fig: plt.Figure matplotlib Figure object. ax: plt.Axes matplotlib Axes object. """ if "spec_tau" not in spectrum.available: raise ValueError("There is no spec_tau spectrum so cannot create this plot") if "spec" not in spectrum.available and "log_spec" not in spectrum.available: raise ValueError("There is no observer spectrum so cannot create this plot") fig, ax = plt.subplots(figsize=(12, 7)) ax2 = ax.twinx() ax = set_axes_scales(ax, scale) ax2 = set_axes_scales(ax2, scale) # Plot the optical depth spectrum.set("spec_tau") for n, inclination in enumerate(spectrum.inclinations): y = spectrum[inclination] if np.count_nonzero == 0: continue ax.plot(spectrum["Freq."], y, label=str(inclination) + r"$^{\circ}$", alpha=alpha) ax.legend(loc="upper left") ax.set_xlim(xmin, xmax) ax.set_xlabel("Rest-frame Frequency [Hz]") ax.set_ylabel("Continuum Optical Depth") if label_edges: ax = add_line_ids(ax, photoionization_edges(spectrum=spectrum), "none") ax.set_zorder(ax2.get_zorder() + 1) ax.patch.set_visible(False) # Plot the emitted and created spectrum spectrum.set("spec") for thing in ["Created", "Emitted"]: x, y = get_xy_subset(spectrum["Freq."], spectrum[thing], xmin, xmax) if spectrum.units == SpectrumUnits.f_lm: y *= spectrum["Lambda"] else: y *= spectrum["Freq."] ax2.plot(x, y, label=thing, alpha=alpha) ax2.legend(loc="upper right") ax2 = set_axes_labels(ax2, spectrum) fig = finish_figure(fig) fig.savefig(f"{spectrum.fp}/{spectrum.root}_reprocessing.png") if display: plt.show() return fig, ax
def optical_depth(spectrum, inclinations="all", xmin=None, xmax=None, scale="loglog", label_edges=True, frequency_space=True, display=False): """Plot the continuum optical depth spectrum. Create a plot of the continuum optical depth against either frequency or wavelength. Frequency space is the default and preferred. This function returns the Figure and Axes object. Parameters ---------- spectrum: pypython.Spectrum The spectrum object. inclinations: str or list or tuple [optional] A list of inclination angles to plot, but all is also an acceptable choice if all inclinations are to be plotted. xmin: float [optional] The lower x boundary for the figure xmax: float [optional] The upper x boundary for the figure scale: str [optional] The scale of the axes for the plot. label_edges: bool [optional] Label common absorption edges of interest onto the figure frequency_space: bool [optional] Create the figure in frequency space instead of wavelength space display: bool [optional] Display the final plot if True. Returns ------- fig: plt.Figure matplotlib Figure object. ax: plt.Axes matplotlib Axes object. """ fig, ax = plt.subplots(1, 1, figsize=(12, 7)) current = spectrum.current spectrum.set("spec_tau") # Determine if we're plotting in frequency or wavelength space and then # determine the inclinations we want to plot if frequency_space: xlabel = "Freq." units = SpectrumUnits.f_nu else: xlabel = "Lambda" units = SpectrumUnits.f_lm if not xmin: xmin = np.min(spectrum[xlabel]) if not xmax: xmax = np.max(spectrum[xlabel]) inclinations = _get_inclinations(spectrum, inclinations) # Now loop over the inclinations and plot each one, skipping ones which are # completely empty for inclination in inclinations: if inclination != "all": if inclination not in spectrum.inclinations: # Skip inclinations which don't exist continue x, y = get_xy_subset(spectrum[xlabel], spectrum[inclination], xmin, xmax) if np.count_nonzero(y) == 0: # skip arrays which are all zeros continue ax.plot(x, y, label=f"{inclination}" + r"$^{\circ}$") ax = set_axes_scales(ax, scale) ax.set_ylabel(r"Continuum Optical Depth") if frequency_space: ax.set_xlabel(r"Rest-frame Frequency [Hz]") else: ax.set_xlabel(r"Rest-frame Wavelength [$\AA$]") ax.legend(loc="upper left") if label_edges: ax = add_line_ids(ax, photoionization_edges(units=units), linestyle="none", offset=0) spectrum.set(current) fig = finish_figure(fig) fig.savefig(f"{spectrum.fp}/{spectrum.root}_spec_optical_depth.png") if display: plt.show() return fig, ax
def observer(spectrum, inclinations, xmin=None, xmax=None, scale="logy", multiply_by_spatial_units=False, label_lines=True, display=False): """Plot the request observer spectrum. If all is passed to inclinations, then all the observer angles will be plotted on a single figure. This function will only be available if there is a .spec or .log_spec file available in the passed spectrum, it does not work for spec_tot, etc. For these spectra, plot.spectrum.spectrum_components should be used instead, or plot.plot for something finer tuned. Parameters ---------- spectrum: pypython.Spectrum The spectrum being plotted, "spec" or "log_spec" should be the active spectrum. inclinations: list or str The inclination angles to plot. xmin: float [optional] The lower x boundary of the plot. xmax: float [optional] The upper x boundary of the plot. scale: str [optional] The scale of the axes. multiply_by_spatial_units: bool [optional] Plot the flux instead of flux density. label_lines: bool [optional] Label common spectrum lines. display: bool [optional] Display the figure once plotted. Returns ------- fig: plt.Figure matplotlib Figure object. ax: plt.Axes matplotlib Axes object. """ if spectrum.current not in ["spec", "log_spec"]: spectrum.set("spec") fig, ax = plt.subplots(1, 1, figsize=(12, 5)) inclinations = _get_inclinations(spectrum, inclinations) wrong_input = [] for inclination in inclinations: if inclination not in spectrum.inclinations: wrong_input.append(inclination) if len(wrong_input) > 0: print(f"The following inclinations provided are not in the spectrum inclinations and will be skipped:" f" {', '.join(wrong_input)}") for to_remove in wrong_input: inclinations.remove(to_remove) if len(inclinations) == 0: print(f"Returning empty figure without creating plot as there is nothing to plot") return fig, ax ax = _plot_subplot(ax, spectrum, inclinations, xmin, xmax, 1.0, scale, multiply_by_spatial_units) if label_lines: ax = add_line_ids(ax, common_lines(spectrum=spectrum), "none") fig = finish_figure(fig) fig.savefig(f"{spectrum.fp}/{spectrum.root}_{spectrum.current}.png") if display: plt.show() return fig, ax
def multiple_models(output_name, spectra, spectrum_type, things_to_plot, xmin=None, xmax=None, multiply_by_spatial_units=False, alpha=0.7, scale="loglog", label_lines=True, log_spec=False, smooth=None, distance=None, display=False): """Plot multiple spectra, from multiple models, given in the list of spectra provided. Spectrum file paths are passed and then each spectrum is loaded in as a Spectrum object. Each spectrum must have the same units and are also assumed to be at the same distance. In this function, it is possible to compare Emitted or Created spectra. It is agnostic to the type of spectrum file being plotted, unlike spectrum_observer. todo: label absorption edges if spec_tau is selected Parameters ---------- output_name: str The name of the output .png file. spectra: str The file paths of the spectra to plot. spectrum_type: str The type of spectrum to plot, i.e. spec or spec_tot. things_to_plot: str or list of str or tuple of str The things which will be plotted, i.e. '45' or ['Created', '45', '60'] xmin: float [optional] The lower x boundary of the plot xmax: float [optional] The upper x boundary for the plot multiply_by_spatial_units: bool [optional] Plot in flux units, instead of flux density. alpha: float [optional] The transparency of the plotted spectra. scale: str [optional] The scaling of the axes. label_lines: bool [optional] Label common emission and absorption features, will not work with spec_tau. log_spec: bool [optional] Use either the linear or logarithmically spaced spectra. smooth: int [optional] The amount of smoothing to apply to the spectra. distance: float [optional] The distance to scale the spectra to in parsecs. display: bool [optional] Display the figure after plotting, or don't. Returns ------- fig: plt.Figure matplotlib Figure object. ax: plt.Axes matplotlib Axes object. """ if type(spectra) is str: spectra = list(spectra) spectra_to_plot = [] for spectrum in spectra: if type(spectrum) is not Spectrum: root, fp = get_root_name(spectrum) spectra_to_plot.append(Spectrum(root, fp, log_spec, smooth, distance, spectrum_type)) else: spectra_to_plot.append(spectrum) units = list(dict.fromkeys([spectrum.units for spectrum in spectra_to_plot])) if len(units) > 1: msg = "" for spectrum in spectra_to_plot: msg += f"{spectrum.units} : {spectrum.fp}{spectrum.root}.{spectrum.current}\n" raise ValueError(f"Some of the spectra have different units, unable to plot:\n{msg}") # Now, this is some convoluted code to get the inclination angles # get only the unique, sorted, values if inclination == "all" if things_to_plot == "all": things_to_plot = () for spectrum in spectra_to_plot: # have to do it like this, as spectrum.inclinations is a tuple things_to_plot += spectrum.inclinations if len(things_to_plot) == 0: raise ValueError("\"all\" does not seem to have worked, try specifying what to plot instead") things_to_plot = tuple(sorted(list(dict.fromkeys(things_to_plot)))) # Gets sorted, unique values from tuple else: things_to_plot = things_to_plot.split(",") things_to_plot = tuple(things_to_plot) n_to_plot = len(things_to_plot) n_rows, n_cols = subplot_dims(n_to_plot) if n_rows > 1: figsize = (12, 12) else: figsize = (12, 5) fig, ax = plt.subplots(n_rows, n_cols, figsize=figsize, squeeze=False) fig, ax = remove_extra_axes(fig, ax, n_to_plot, n_rows * n_cols) ax = ax.flatten() for n, thing in enumerate(things_to_plot): for spectrum in spectra_to_plot: try: y = spectrum[thing] except KeyError: continue # We will skip key errors, as models may have different inclinations if np.count_nonzero(y) == 0: # skip arrays which are all zeros continue if multiply_by_spatial_units: if spectrum.units == SpectrumUnits.f_lm: y *= spectrum["Lambda"] else: y *= spectrum["Freq."] if spectrum.units == SpectrumUnits.f_lm: x = spectrum["Lambda"] else: x = spectrum["Freq."] x, y = get_xy_subset(x, y, xmin, xmax) label = spectrum.fp.replace("_", r"\_") + spectrum.root.replace("_", r"\_") ax[n].plot(x, y, label=label, alpha=alpha) ax[n] = set_axes_scales(ax[n], scale) ax[n] = set_axes_labels(ax[n], spectra_to_plot[0], multiply_by_spatial_units=multiply_by_spatial_units) if thing.isdigit(): ax[n].set_title(f"{thing}" + r"$^{\circ}$") else: ax[n].set_title(f"{thing}") if label_lines: ax[n] = add_line_ids(ax[n], common_lines(spectrum=spectra_to_plot[0]), "none") ax[0].legend(fontsize=10).set_zorder(0) fig = finish_figure(fig) fig.savefig(f"{output_name}.png") if display: plt.show() return fig, ax