Source code for beamprofiler.utils.plot

# -*- coding: utf-8 -*-
"""
This module handles the generation of the auxiliary plots.
"""

import os

import matplotlib.cm as cm
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.art3d as art3d
import numpy as np
from matplotlib.ticker import FormatStrFormatter, MultipleLocator
from mpl_toolkits.axes_grid1 import make_axes_locatable
from mpl_toolkits.axes_grid1.inset_locator import mark_inset, zoomed_inset_axes

from beamprofiler.utils import data_processing as dp


def general_plot(proj=None):
    """
    `general_plot` returns a general-purpose, blank pyplot graph.

    Parameters
    ----------
    proj : str, optional
        axes projection. The default is None.

    Returns
    -------
    fig : figure object of matplotlib.figure module
        blank matplotlib figure object.
    ax : AxesSubplot object of matplotlib.axes_subplots module
        blank matplotlib axes object.

    """

    golden_ratio = 1.618033988749895

    # Set global font style and size
    plt.rc('font', family='serif')
    plt.rcParams.update({'font.size': 6})

    fig = plt.figure(figsize=(5, 5 / golden_ratio), dpi=100)
    ax = fig.add_subplot(1, 1, 1, projection=proj)

    return fig, ax


def save(path, fileName, suffix, fmt):
    """
    `save` saves a pyplot graph in the designated path with the designated
    name and format.

    Parameters
    ----------
    path : std
        path to where the graph will be saved.
    fileName : str
        name of the power density distribution file.
    suffix : str
        suffix added to the graph's name.
    fmt : str
        image file format.

    Returns
    -------
    None.

    """

    fileName = os.path.splitext(fileName)[0]
    plt.savefig(os.path.join(path, fileName + suffix + fmt),
                bbox_inches='tight', dpi=300)


[docs]def histogram(path, fileName, beam, **kwargs): """ `histogram` plots the histogram of the power density distribution and the normal mixture fit of the top 40% of the range. Parameters ---------- path : std path to where the graph will be saved. fileName : str name of the power density distribution file. beam : Beam instance of type Beam. Other Parameters ---------------- n_bins : int number of bins used in the histogram. The default is 256. zoom : float zoom of the inset image. The default is 2. x1 : float lower bound of the inset image on the x-axis. The default is 1600. x2 : float upper bound of the inset image on the y-axis. The default is 2000. y1 : float lower bound of the inset image on the y-axis. The default is 0. y2 : float upper bound of the inset image on the x-axis. The default is 5000. fmt : str image file format. Returns ------- None. """ # Check if any default value has been redefined in kwargs n_bins = kwargs.pop('n_bins', 256) zoom = kwargs.pop('zoom', 2) x1 = kwargs.pop('x1', 1600) x2 = kwargs.pop('x2', 2000) y1 = kwargs.pop('y1', 0) y2 = kwargs.pop('y2', 5000) fmt = kwargs.pop('fmt', '.png') # Get the figure and axes objects fig, ax = general_plot() # Add title and axis titles ax.set_title('Histogram Analysis', loc='center', pad=None) ax.set_xlabel('Power Density (ADC/px)') ax.set_ylabel('Number of counts') # Histogram array = beam.raw_data_null.to_numpy().flatten() hist, bins, bars = ax.hist(array, bins=n_bins, alpha=1, align='right', histtype='stepfilled', label='Histogram') # Add an inset image inset = zoomed_inset_axes(ax, zoom=zoom, loc=1) inset.hist(array, bins=n_bins, alpha=1, histtype='stepfilled', align='right', label='Histogram') inset.set_xlim(x1, x2) inset.set_ylim(y1, y2) inset.tick_params(labelleft=False, labelbottom=False) mark_inset(ax, inset, loc1=3, loc2=3, fc="none", ec="0.5") # Get the highest histogram peak around the high-power density area (upper # 40% of the number of bins) low_bound = int(n_bins * 0.6) high_bound = n_bins max_count = max(hist[low_bound:high_bound]) pdf_x, pdf_y = dp.normal_mixture(beam.raw_data_null, beam.mix) pdf_y = max_count * (pdf_y / pdf_y.max()) inset.plot(pdf_x, pdf_y, 'k--', linewidth=0.7) # Now that the fit has been generated, it can be added to the main graph ax.plot(pdf_x, pdf_y, 'k--', linewidth=0.35) # Save and show save(path, fileName, ' - histogram', fmt) plt.show()
[docs]def heat_map_2d(path, fileName, beam, **kwargs): """ `heat_map_2d` plots the 2D heat map of the power density distribution. Parameters ---------- path : str path to where the graph will be saved. fileName : str name of the power density distribution file. beam : Beam instance of type `Beam`. Other Parameters ---------------- z_lim : float upper intensity limit of the cross-section graph of the power density distribution. The default is -1. cross_x : float x-coordinate of the cross-section graph of the power density distribution. The default is the calculated beam center about the x-axis. cross_y : float y-coordinate of the cross-section graph of the power density distribution. The default is the calculated beam center about the y-axis. rect : tuple size and position of the reference rectangle. The first two elements of the tuple define the width and length of the reference rectangle, repectively. The third and fourth elements of the tuple define the offset of the reference rectangle relative to the center of the beam. (width, length, x_offset, y_offset). Default is (0, 0, 0, 0). fmt : str image file format. The default is `.png`. Returns ------- None. """ # Check if any default value has been redefined in kwargs z_lim = kwargs.pop('z_lim', -1) cross_x = kwargs.pop('cross_x', beam.centerX * beam.xResolution) cross_y = kwargs.pop('cross_y', beam.centerY * beam.yResolution) rect = kwargs.pop('rect', (0, 0, 0, 0)) fmt = kwargs.pop('fmt', '.png') # Check if the length of rect matches the required value req_len = 4 if len(rect) != req_len: print("The kwarg 'rect' is missing %d argument(s), therefore the " "referece rectangle will not be ploted. Please add %d more " "argument(s) and try again." % ( (req_len-len(rect)), (req_len-len(rect)) ) ) rect=(0, 0, 0, 0) # Get the figure and axes objects fig, main_ax = general_plot() # Create and configure the axes divider = make_axes_locatable(main_ax) # Right ax right_ax = divider.append_axes("right", 0.6, pad=0.2, sharey=main_ax) right_ax.yaxis.set_tick_params(labelleft=False) right_ax.set_xlabel('Intensity') if z_lim != -1: right_ax.set_xlim(right=z_lim) # Top ax top_ax = divider.append_axes("top", 0.6, pad=0.2, sharex=main_ax) top_ax.xaxis.set_tick_params(labelbottom=False) top_ax.set_ylabel('Intensity') if z_lim != -1: top_ax.set_ylim(top=z_lim) # Configure and plot main graph z = beam.raw_data.to_numpy() main_ax.imshow(z, extent=(0, dp.get_xWindow(beam.raw_header), 0, dp.get_yWindow(beam.raw_header)), interpolation='nearest', cmap=cm.gist_rainbow_r, origin='lower') main_ax.axvline(cross_x, color='k', linestyle="--", lw=0.8) main_ax.axhline(cross_y, color='k', linestyle="-", lw=0.8) main_ax.set_xlabel('x-axis (mm)') main_ax.set_ylabel('y-axis (mm)') # If `rect` was not defined via the kwargs, do not do extra drawings if rect != (0, 0, 0, 0): # Add a black rectangle to the 2D heat map. The rectangle is defined by # an ancor point (botton-left corner) and a width and length. The ancor # point is set so the center of the rectangles matches the center of # the laser beam, however it can be moved about the x-axis by setting # `rect[2] != 0` and about the y-axis by setting `rect[3] != 0` ancor_x = (beam.centerX * beam.xResolution - rect[0]/2) + rect[2] ancor_y = (beam.centerY * beam.yResolution - rect[1]/2) + rect[3] p = mpatches.Rectangle( (ancor_x, ancor_y), rect[0], rect[1], linewidth=0.25, edgecolor='k', facecolor='none') main_ax.add_patch(p) # Add a black line to the top ax top_ax.axvline(ancor_x, color='k', linestyle='-', lw=0.25) top_ax.axvline(ancor_x+rect[0], color='k', linestyle='-', lw=0.25) # Add a black line to the right ax right_ax.axhline(ancor_y, color='k', linestyle='-', lw=0.25) right_ax.axhline(ancor_y+rect[1], color='k', linestyle='-', lw=0.25) # Create and configure the right sub ax y = np.mgrid[0:dp.get_yWindow(beam.raw_header):beam.yResolution] slice_y = 0 try: # Data along the y-axis at the x-position defined by cross_x slice_y = z[:, int(np.around(cross_x/beam.xResolution))] except IndexError: print("Slice position is outside of the range. Please, check the " "slice position.") right_ax.plot(slice_y, y, color='k', linestyle="--", lw=0.5) # Create and configure the top sub ax x = np.mgrid[0:dp.get_xWindow(beam.raw_header):beam.xResolution] slice_x = 0 try: # Data along the x-axis at the y-position defined by cross_y slice_x = z[int(np.around(cross_y/beam.yResolution)), :] except IndexError: print("Slice position is outside of the range. Please, check the " "slice position.") top_ax.plot(x, slice_x, color='k', linestyle="-", lw=0.5) # Save and show save(path, fileName, ' - 2d heat map', fmt) plt.show()
[docs]def heat_map_3d(path, fileName, beam, **kwargs): """ `heat_map_3d` plots the 3D heat map of the power density distribution. Parameters ---------- path : str path to where the graph will be saved. fileName : str name of the power density distribution file. beam : Beam instance of type `Beam`. Other Parameters ---------------- elev : float elevation viewing angle. The default is 50. azim : float azimuthal viewing angle. The default is 315. dist : float distance from the plot. The default is 11. rect : tuple size and position of the reference rectangle. The first two elements of the tuple define the width and length of the reference rectangle, repectively. The third and fourth elements of the tuple define the offset of the reference rectangle relative to the center of the beam. The fifth element of the tuple defines the offset of the reference rectangle relative to the z-axis. (width, length, x_offset, y_offset, z_offset). Default is (0, 0, 0, 0, 0). fmt : str image file format. Returns ------- None. """ # Check if any default value has been redefined in kwargs elev = kwargs.pop('elev', 50) azim = kwargs.pop('azim', 315) dist = kwargs.pop('dist', 11) rect = kwargs.pop('rect', (0, 0, 0, 0, 0)) fmt = kwargs.pop('fmt', '.png') # Check if the length of rect matches the required value req_len = 5 if len(rect) != req_len: print("The kwarg 'rect' is missing %d argument(s), therefore the " "referece rectangle will not be ploted. Please add %d more " "argument(s) and try again." % ( (req_len-len(rect)), (req_len-len(rect)) ) ) rect=(0, 0, 0, 0, 0) fig, ax = general_plot(proj='3d') # Configure view ax.view_init(elev=elev, azim=azim) ax.dist = dist # Plot data x = np.mgrid[0:dp.get_xWindow(beam.raw_header):beam.xResolution] y = np.mgrid[0:dp.get_yWindow(beam.raw_header):beam.yResolution] x_3d, y_3d = np.meshgrid(x, y) ax.plot_surface(x_3d, y_3d, beam.raw_data, cmap=cm.gist_rainbow_r, rstride=2, cstride=2, linewidth=2, antialiased=False) # If `rect` was not defined via the kwargs, do not do extra drawings if rect != (0, 0, 0, 0, 0): # Add a black rectangle to the 3D heat map. The rectangle is defined by # an ancor point (botton-left corner) and a width and length. The ancor # point is set so the center of the rectangles matches the center of # the laser beam, however it can be moved about the x-axis by setting # `rect[2] != 0` and about the y-axis by setting `rect[3] != 0` ancor_x = (beam.centerX * beam.xResolution - rect[0]/2) + rect[2] ancor_y = (beam.centerY * beam.yResolution - rect[1]/2) + rect[3] p = mpatches.Rectangle( (ancor_x, ancor_y), rect[0], rect[1], linewidth=0.25, edgecolor='k', facecolor='none') ax.add_patch(p) art3d.pathpatch_2d_to_3d(p, z=rect[4], zdir="z") # Set axis labels ax.set_xlabel("x-axis (mm)", labelpad=5) ax.set_ylabel("y-axis (mm)", labelpad=5) ax.set_zlabel("Intensity", labelpad=5) # Save and show save(path, fileName, ' - 3d heat map', fmt) plt.show()
[docs]def norm_energy_curve(path, fileName, beam, **kwargs): """ `norm_energy_curve` plots the normalized energy curve of the power density distribution. Parameters ---------- path : str path to where the graph will be saved. fileName : str name of the power density distribution file. beam : Beam instance of type `Beam`. Other Parameters ---------------- fmt : str image file format. Returns ------- None. """ # Check if any default value has been redefined in kwargs fmt = kwargs.pop('fmt', '.png') # Get the figure and axes objects fig, ax = general_plot() df = dp.pre_top_hat(beam.raw_data) # Plot x = df['Normalized Intensity'] y = df['Normalized Cumulative Energy'] ax.plot(x, y, ls='-', color='blue', linewidth=0.5) # Fill area under the curve ax.fill_between(x, y, 0, color='blue', alpha=0.5) ax.fill_between(x, y, 100, color='gray', alpha=0.5) # x-axis ax.set_xlabel('Normalized Intensity (%)') plt.xlim(0, 100) ax.xaxis.set_major_locator(MultipleLocator(10)) ax.xaxis.set_major_formatter(FormatStrFormatter('%d')) ax.xaxis.set_minor_locator(MultipleLocator(5)) ax.xaxis.set_tick_params(width=0.5) # y-axis ax.set_ylabel('Normalized Cumulative Energy (%)') plt.ylim(0, 100) ax.yaxis.set_major_locator(MultipleLocator(10)) ax.yaxis.set_major_formatter(FormatStrFormatter('%d')) ax.yaxis.set_minor_locator(MultipleLocator(5)) ax.yaxis.set_tick_params(width=0.5) ax.spines['top'].set_linewidth(0.5) ax.spines['bottom'].set_linewidth(0.5) ax.spines['right'].set_linewidth(0.5) ax.spines['left'].set_linewidth(0.5) # Title plt.title('Energy curve', fontdict=None, loc='center', pad=None) # Legend blue_patch = ( mpatches.Patch(color='blue', alpha=0.5, label=str(round(beam.topHatFactor*100, 2))+'%') ) gray_patch = ( mpatches.Patch(color='gray', alpha=0.5, label=str(round(100-beam.topHatFactor*100, 2))+'%') ) ax.legend(handles=[blue_patch, gray_patch], loc='lower left') # Save and show save(path, fileName, ' - energy curve', fmt) plt.show()