def analyze_region(spectrogram: Spectrogram, roi: dict): """ """ time, velocity, intensity = spectrogram.slice(roi['time'], roi['velocity']) # Do we want to convert to power? power = spectrogram.power(intensity) # let's look at the peak power at each time peaks = np.max(power, axis=0) speaks = sorted(peaks) threshold = speaks[int(len(peaks) * 0.04)] amax = np.argmax(power, axis=0) power[power < threshold] = 0.0 span = 30 nmax = len(velocity) - 1 times, centers, widths, amps = [], [], [], [] def peg(x): if x < 0: return 0 if x > nmax: return nmax return x for t in range(len(time) - 1): if power[amax[t], t] > threshold: acenter = amax[t] vfrom, vto = peg(acenter - span), peg(acenter + span) gus = Gaussian(velocity[vfrom:vto], power[vfrom:vto, t]) if gus.valid: times.append(time[t]) centers.append(gus.center) widths.append(gus.width) amps.append(gus.amplitude) else: print(f"[{t}] {gus.error}") #power[power>=threshold] = 1 plt.pcolormesh(time * 1e6, velocity, power) plt.plot(np.array(times) * 1e6, centers, 'r.', alpha=0.5) plt.show()
def seamExtraction(SpectrogramObject:Spectrogram, startTime:int, stopTime:int, width:int, bottomIndex:int=0, topIndex:int=None, order:int = None, verbose:bool = False, theta = None): """ Input: intensityMatrix: a 2d array [velocity][time] and each cell corresponds to the intensity at that point. startTime: index corresponding to the start time in the intensity matrix of the signal being considered. stopTime: index corresponding to the stop time in the intensity matrix of the signal being considered. width: How wide of a window that is being considered at each time step. The window will be split evenly up and down bottomIndex: Upper bound on the velocity. Assumed to be a valid index for sgram's intensity array. Defaults to zero order: the order of the minkowski distance function that will be minimized to determine the most well connected signal. Defaults to 2: minimizing the Euclidean distance between neighboring pixels. verbose: Boolean that indicates if you want there to be debugging print statements. Defaults to False: theta: The relative importance of the phase array to the amplitude array if applicable. It should be between 0 and 1. Output: list of indices that should be plotted as the signal as an array of indices indicating the intensity values that correspond to the most connected signal as defined by the metric. """ if stopTime <= startTime: raise ValueError("Stop Time is assumed to be greater than start time.") else: try: theta = float(theta) except TypeError: raise TypeError("Theta needs to be able to be cast to a float.") if 0 > theta: theta = np.exp(theta) # This will get between 0 and 1. if theta > 1: theta = np.exp(-1*theta) # To get between 0 and 1. if width % 2 == 1: width += 1 tableHeight = stopTime - startTime + 1 velocities = np.zeros(tableHeight, dtype = np.int64) # This will hold the answers that we are looking for to return. It is a list of indices. halfFan = width//2 bottomDP = bottomIndex + 1 if topIndex == None: topIndex = SpectrogramObject.velocity.shape[0] # I just want the maximum velocity index elif topIndex <= bottomIndex: topIndex = SpectrogramObject.velocity.shape[0] t, vel, real_raw_vals, ovals = SpectrogramObject.slice((SpectrogramObject.time[startTime], SpectrogramObject.time[stopTime]), (SpectrogramObject.velocity[bottomDP], SpectrogramObject.velocity[topIndex])) amplitudeArray = [] # This is will be the magnitude of the complex data. phaseArray = [] # This will be the phase of the complex data. if SpectrogramObject.computeMode == "complex": # We have the complex data. amplitudeArray = real_raw_vals # It will be the appropriate item in this case. phaseArray = np.arctan2(np.real(ovals)/np.imag(ovals)) elif SpectrogramObject.computeMode == "psd" or SpectrogramObject.computeMode == "magnitude": amplitudeArray = real_raw_vals # This is all that we will have as we cannot get the phase info. elif SpectrogramObject.computeMode == "angle" or SpectrogramObject.computeMode == "phase": phaseArray = ovals if verbose: print("new time shape", t.shape, "vel shape:", vel.shape) print("Before the transpose: real_raw_vals.shape", real_raw_vals.shape) print("real_raw_vals.shape = ",real_raw_vals.shape) print("bottomDP", bottomDP) print() print("t2-t1", stopTime-startTime) print("(t2-t1)*halfan", (stopTime-startTime)*halfFan) print() print(topIndex) print(bottomIndex+1) print() amplitudeArray = np.transpose(amplitudeArray) phaseArray = np.transpose(phaseArray) tableHeight, tableWidth = np.transpose(real_raw_vals).shape DPTable = np.zeros((tableHeight, tableWidth)) parentTable = np.zeros(DPTable.shape, dtype = np.int64) for timeIndex in range(2, tableHeight + 1): dpTime = tableHeight - timeIndex for velocityIndex in range(tableWidth): bestSoFar = np.Infinity bestPointer = None for testIndex in range(-halfFan, halfFan+1): # The + 1 is so that you get a balanced window. if velocityIndex + testIndex >= 0 and velocityIndex + testIndex < tableWidth: # then we have a valid index to test from. ampAddition = 0 phaseAddition = 0 if len(amplitudeArray) != 0: # Then it has been initialized. ampAddition = np.power(np.abs(amplitudeArray[dpTime+1][testIndex+velocityIndex]-amplitudeArray[dpTime][velocityIndex]), order) if len(phaseArray) != 0: # Then it has been initialized. phaseAddition = np.power(np.abs(phaseArray[dpTime+1][testIndex+velocityIndex]-phaseArray[dpTime][velocityIndex]), order) current = (1-theta)*ampAddition + theta*phaseAddition + DPTable[dpTime+1][velocityIndex+testIndex] if current < bestSoFar: bestSoFar = current bestPointer = velocityIndex + testIndex if verbose: print("The bestValue has been updated for time", t[dpTime], "and velocity", vel[velocityIndex], \ "to", bestSoFar, "with a bestPointer of ", vel[velocityIndex+testIndex], "at the next timeslice.") DPTable[dpTime][velocityIndex] = bestSoFar parentTable[dpTime][velocityIndex] = bestPointer # Now for the reconstruction. currentPointer = np.argmin(DPTable[0]) if verbose: print("The value of the current pointer is", currentPointer) print("The minimum cost is", DPTable[0][currentPointer]) velocities = reconstruction(parentTable, currentPointer, bottomDP) return velocities, parentTable, DPTable, bottomDP, topIndex
class SpectrogramWidget: """ A Jupyter notebook widget to represent a spectrogram, along with numerous controls to adjust the appearance of the spectrogram. For the widget to behave in a Jupyter notebook, place:: %matplotlib widget at the top of the notebook. This requires that the package ipympl is installed, which can be done either with pip3 or conda install ipympl. I also recommend editing ~/.jupyter/custom/custom.css to modify the definition of .container:: .container { width: 100% !important; margin-right: 40px; margin-left: 40px; } **Inputs** - digfile: either a string or DigFile - kwargs: optional keyword arguments. These are passed to the Spectrogram constructor and to the routine that creates the control widgets. **Data members** - digfile: the source data DigFile - title: the title displayed above the spectrogram - baselines: a list of baseline velocities - spectrogram: a Spectrogram object deriving from digfile - fig: - axSpectrum: - axSpectrogram: - image: - colorbar: - individual_controls: dictionary of widgets - controls: """ _gspec = { 'width_ratios': [6, 1.25], 'height_ratios': [1], 'wspace': 0.05, 'left': 0.075, 'right': 0.975, } def __init__(self, *args, **kwargs): """ If one passes in a single unnamed arg, it can either be a digfile, a string pointing to a digfile, or a two-dimensional ndarray. If we are founded on a dig file, it is possible to recompute things. Only a subset of operations are possible when we're based on a two-dimensional array, but perhaps that is sometimes desirable. """ self.digfile = None if len(args) == 1: arg = args[0] if isinstance(arg, str): self.digfile = DigFile(arg) elif isinstance(arg, DigFile): self.digfile = arg if self.digfile == None and len(args): # Let's see if we have enough information to display a spectrogram # That means we have a two-dimensional ndarray, and possibly corresponding # time and velocity arrays. The signature would be (intensity, [times, velocities]). arg = args[0] if isinstance(arg, np.ndarray) and len(arg.shape) == 2: self._static = { 'intensity': arg, } if len(args) == 3: self._static['time'] = np.asarray(args[1]) self._static['velocity'] = np.asarray(args[2]) else: self._static['time'] = np.arange(0, -1 + arg.shape[1]) self._static['velocity'] = np.arange(0, -1 + arg.shape[0]) assert hasattr( self, '_static' ), "Inappropriate arguments passed to the SpectrogramWidget constructor" # If LaTeX is enabled in matplotlib, underscores in the title # cause problems in displaying the histogram. However, we can # solve this by not using latex in displaying the title, so there # is no need to alter characters here. self.title = "" if self.static else self.digfile.filename.split( '/')[-1] self.baselines = [] # Compute the base spectrogram (do we really need this?) self.spectrogram = None if self.dig: self.spectrogram = Spectrogram(self.digfile, None, None, **kwargs) self.spectrogram_fresh = True # flag for the first pass self.spectrogram.overlap = 0.875 self.fig, axes = plt.subplots(nrows=1, ncols=2, sharey=True, squeeze=True, gridspec_kw=self._gspec) self.axSpectrogram, self.axSpectrum = axes self.subfig = None self.axTrack = None self.axSpare = None # At the moment, clicking on the image updates the spectrum # shown on the left axes. It would be nice to be more sophisticated and # allow for more sophisticated interactions, including the ability # to display more than one spectrum. self.fig.canvas.mpl_connect('button_press_event', lambda x: self.handle_click(x)) self.fig.canvas.mpl_connect('key_press_event', lambda x: self.handle_key(x)) self.spectrum(None, "") self.image = None # we will set in update_spectrogram self.colorbar = None # we will set this on updating, based on the self.peak_followers = [] # will hold any PeakFollowers self.spectra = [] # will hold spectra displayed at right self.spectra_in_db = True # should spectra be displayed in db? self.controls = dict() # widgets stored by name self.layout = None # how the controls get laid out self.selecting = False # we are not currently selecting a ROI self.roi = [] # and we have no regions of interest self.threshold = None self.make_controls(**kwargs) display(self.layout) self.update_spectrogram() @property def static(self): "If we are not associated with a dig file, return True" return hasattr(self, '_static') @property def dig(self): "True if we are associated with a dig file and can recompute the spectrogram" return isinstance(self.digfile, DigFile) @property def intensity(self): "Return the two-dimensional array of intensities" if self.dig: return self.spectrogram.intensity else: return self._static['intensity'] def make_controls(self, **kwargs): """ Create the controls for this widget and store them in self.controls. """ cd = self.controls # the dictionary of controls t_range = kwargs.get('t_range', (0, 100)) # If we are associated with a dig file, include controls that # allow recomputation of the spectrogram if self.dig: df = self.digfile # FFT size ########################################### # Set the size of each spectrum pps = self.spectrogram.points_per_spectrum # pps = kwargs.get('points_per_spectrum', 8192) val = int(np.log2(pps)) cd['spectrum_size'] = slide = widgets.IntSlider( value=val, min=8, max=18, step=1, description="FFT 2^n") slide.continuous_update = False slide.observe( lambda x: self.overhaul(points_per_spectrum=2**x['new']), names="value") # Set the overlap percentage of successive time intervals cd['overlap'] = slide = widgets.FloatSlider( description='Overlap %', value=100.0 * self.spectrogram.overlap, min=0, max=100) slide.continuous_update = False slide.observe(lambda x: self.overhaul(overlap=x['new'] * 0.01), names="value") # Thumbnail ########################################### # Display a thumbnail of the raw signal cd['raw_signal'] = widgets.ToggleButton(value=False, description="Show V(t)", button_style='', icon='check') cd['raw_signal'].observe(lambda b: self.show_raw_signal(b), names="value") # Time range ########################################### cd['t_range'] = slide = ValueSlider("Time (µs)", t_range, (df.t0, df.t_final), 1e6, readout_format=".1f") slide.continuous_update = False # Velocity range ########################################### cd['velocity_range'] = slide = ValueSlider( "Velocity (km/s)", (0, 50), (0.0, self.spectrogram.v_max), 1e-3, readout_format=".1f", continuous_update=False) else: d = self._static cd['t_range'] = ValueSlider("Time", t_range, (d['time'][0], d['time'][-1]), 1e-3, readout_format=".1f", continuous_update=False) cd['velocity_range'] = ValueSlider("Velocity (km/s)", (0, 100), (0.0, d['velocity'][-1]), 1e-3, readout_format=".1f", continuous_update=False) cd['t_range'].observe(lambda x: self.do_update(x), names="value") cd['velocity_range'].observe(lambda x: self.update_velocity_range(x), names="value") # Color range ########################################### imax = self.intensity.max() if self.dig: hl = self.spectrogram.histogram_levels imin = hl['tens'][3] # Let's figure out the range likely to produce a clear # image. Put the 50% point at the bottom end and the 95% # at the top? def scale(x): return int(100.0 * (x - imin) / (imax - imin)) start_range = (scale(hl['tens'][8]), scale(hl['tenths'][9])) else: imin = imax - 150 # ?? this is bad and assumes db start_range = (40, 70) cd['intensity_range'] = slide = ValueSlider("Color", start_range, (imin, imax), multiplier=1, readout_format=".0f", continuous_update=False) slide.observe(lambda x: self.update_color_range(), names="value") # Threshold percentage ##################################### cd['threshold'] = slide = widgets.FloatSlider( description='Noise floor %', value=0, min=0, max=100.0, continuous_update=False) slide.observe(lambda x: self.update_threshold(x['new']), names="value") # Color map selector ########################################### the_maps = sorted(COLORMAPS.keys()) the_maps.append('Computed') cd['color_map'] = widgets.Dropdown( options=the_maps, value=DEFMAP, description='Color Map', disabled=False, ) cd['color_map'].observe(lambda x: self.update_cmap(), names="value") # Click selector ########################################### # What to do when registering a click in the spectrogram cd['clicker'] = widgets.Select(options=("Spectrum (dB)", "Spectrum", "Peak", "Gauss", "Template_Matching"), value='Spectrum (dB)', description="Click:", disabled=False) cd['marquee'] = mwidgets.RectangleSelector( self.axSpectrogram, lambda eclick, erelease: self.RSelect(eclick, erelease), interactive=True, useblit=True, rectprops=dict(facecolor='yellow', edgecolor='red', alpha=0.2, fill=True), drawtype='box') cd['marquee'].set_active(False) # Clear spectra ########################################### cd['clear_spectra'] = widgets.Button(description="Clear Spectra") cd['clear_spectra'].on_click(lambda b: self.clear_spectra()) # Clear peak_followers ########################################### cd['clear_followers'] = widgets.Button( description="Clear Peak Followers") cd['clear_followers'].on_click(lambda b: self.clear_followers()) # Computing baselines ########################################### cd['baselines'] = widgets.Dropdown(options=('_None_', 'Squash', 'FFT'), value='_None_', description='Baselines', disabled=False) cd['baselines'].observe(lambda x: self.update_baselines(x["new"]), names="value") cd['squash'] = widgets.Button(description="Squash in Vertical") cd['squash'].on_click(lambda b: self.squash_vertical()) columns = [ 'color_map;t_range;velocity_range;intensity_range;threshold', 'clicker;clear_spectra;clear_followers', ] if self.dig: columns.append('spectrum_size;overlap;raw_signal;baselines;squash') vboxes = [] for col in columns: vboxes.append(widgets.VBox([cd[x] for x in col.split(';')])) self.layout = widgets.HBox(vboxes) def range(self, var): "Return the range of the named control, or None if not found." if var in self.controls: return self.controls[var].range return None def RSelect(self, eclick, erelease): "Called when self.selecting is True and the marquee is active" if self.selecting: t0, t1 = eclick.xdata, erelease.xdata v0, v1 = eclick.ydata, erelease.ydata # make sure they are in the right order if t1 < t0: t0, t1 = t1, t0 if v1 < v0: v0, v1 = v1, v0 self.roi.append(dict(time=(t0, t1), velocity=(v0, v1))) def do_update(self, what): self.update_spectrogram() def show_raw_signal(self, box): """ Display or remove the thumbnail of the time series data at the top of the spectrogram window. """ if box.new: # display the thumbnail t_range = self.range('t_range') thumb = self.digfile.thumbnail(*t_range) # we have to superpose the thumbnail on the # existing velocity axis, so we need to rescale # the vertical. tvals = thumb['times'] * 1e6 # convert to µs yvals = thumb['peak_to_peak'] ylims = self.axSpectrum.get_ylim() # Map the thumbnail to the top 20% ymax = yvals.max() yrange = ymax - yvals.min() yscale = 0.2 * (ylims[1] - ylims[0]) / yrange vvals = ylims[1] - yscale * (ymax - yvals) self.raw = self.axSpectrogram.plot(tvals, vvals, 'r-', alpha=0.5)[0] else: try: self.axSpectrogram.lines.remove(self.raw) self.raw = None self.fig.canvas.draw() self.fig.canvas.flush_events() except: pass def overhaul(self, **kwargs): """ A parameter affecting the base spectrogram has been changed, so we need to recompute everything. """ if self.dig: if self.spectrogram_fresh: self.spectrogram_fresh = False else: self.spectrogram.set(**kwargs) self.update_spectrogram() def update_spectrogram(self): """ Recompute and display everything """ intense = self.spectrogram.intensity if self.dig else self._static[ 'intensity'] # Having recomputed the spectrum, we need to set the yrange # of the color map slider cmin = intense.min() cmax = intense.max() self.controls['intensity_range'].range = (cmin, cmax) self.display_spectrogram() def display_spectrogram(self): """ """ trange = self.range('t_range') vrange = self.range('velocity_range') if self.dig: # extract the requisite portions times, velocities, intensities = self.spectrogram.slice( trange, vrange) else: d = self._static times, velocities, intensities = d['time'], d['velocity'], d[ 'intensity'] # if we have already displayed an image, remove it if self.colorbar: self.colorbar.remove() self.colorbar = None if self.image: self.image.remove() self.image = None if self.threshold: intensities[intensities < self.threshold] = self.threshold self.image = self.axSpectrogram.pcolormesh(times * 1e6, velocities, intensities) self.colorbar = self.fig.colorbar(self.image, ax=self.axSpectrogram, fraction=0.08) self.axSpectrogram.set_title(self.title, usetex=False) self.axSpectrogram.set_xlabel('Time ($\mu$s)') self.axSpectrogram.set_xlim(*(np.array(trange) * 1e6)) self.axSpectrogram.set_ylabel('Velocity (m/s)') self.update_velocity_range() self.update_color_range() self.update_cmap() def update_threshold(self, x): sg = self.spectrogram if int(x) == 0: self.threshold = None else: if x < 90: threshold = sg.histogram_levels['tens'][x // 10] elif x < 99: threshold = sg.histogram_levels['ones'][int(x - 90)] else: threshold = sg.histogram_levels['tenths'][int(10 * (x - 99))] self.threshold = self.spectrogram.transform(threshold) self.display_spectrogram() def update_cmap(self): """ Update the color map used to display the spectrogram """ mapname = self.controls['color_map'].value if mapname == 'Computed': from UI_Elements.generate_color_map import make_spectrogram_color_map mapinfo = make_spectrogram_color_map(self.spectrogram, 4, mapname) maprange = (mapinfo['centroids'][1], mapinfo['centroids'][-2]) self.controls['intensity_range'].value = maprange self.image.set_cmap(COLORMAPS[mapname]) def update_velocity_range(self, info=None): """ Update the displayed velocity range using values obtained from the 'velocity_range' slider. """ if info: old_vmin, old_vmax = info['old'] vmin, vmax = info['new'] if vmax > old_vmax or vmin < old_vmin: return self.update_spectrogram() vmin, vmax = self.range('velocity_range') self.axSpectrogram.set_ylim(vmin, vmax) self.axSpectrum.set_ylim(vmin, vmax) def update_color_range(self): self.image.set_clim(self.range('intensity_range')) def handle_click(self, event): try: # convert time to seconds t, v = event.xdata * 1e-6, event.ydata except: return 0 if self.selecting: return 0 # Look up what we should do with the click action = self.controls['clicker'].value # print(f"The action I am attempting to do is {action}") try: if 'Spectrum' in action: self.spectrum(t, action) elif 'Template_Matching' in action: ti = self.spectrogram._time_to_index(t) vi = self.spectrogram._velocity_to_index(v) self.match_templates(ti, vi) else: # print("I am handling a click that should be a peak follower") self.follow(t, v, action) except Exception as eeps: pass def handle_key(self, event): try: # convert time to seconds _, v = event.xdata * 1e-6, event.ydata except: pass char = event.key if char == 'x': # remove the all spectra self.clear_spectra() if char in ('f', 'b', 'F', 'B'): # We'd like to go exploring if not hasattr(self, 'explorer_mark'): self.explorer_mark = 2 else: shifts = dict(f=4, F=20, b=-4, B=-40) self.explorer_mark += shifts[char] self.gaussian_explorer(self.explorer_mark) if char in ('m', 'M'): self.selecting = not self.selecting self.controls['marquee'].set_active(self.selecting) if char in "0123456789": n = int(char) # self.fan_out(int(char)) self.gauss_out(n) if char in ('a', 'A') and self.roi: self.analyze_roi() def clear_spectra(self): """Remove all spectra from axSpectrum and the corresponding markers from axSpectrogram """ for x in self.spectra: self.axSpectrogram.lines.remove(x['marker']) self.axSpectrum.lines.remove(x['line']) self.spectra = [] self.fig.canvas.draw() self.fig.canvas.flush_events() def clear_followers(self): """Remove all followers""" for x in self.peak_followers: self.axSpectrogram.lines.remove(x.line) self.peak_followers = [] self.fig.canvas.draw() self.fig.canvas.flush_events() def follow(self, t, v, action): """Attempt to follow the path starting with the clicked point.""" print( f"Let's follow something starting at {t, v} using action {action}." ) if action == "Gauss": fitter = GaussianFitter(self.spectrogram, (t, v)) self.gauss = fitter elif action == "Peak": follower = PeakFollower(self.spectrogram, (t, v)) # self.peak = follower self.peak_followers.append(follower) follower.run() tsec, v = follower.v_of_t follower.line = self.axSpectrogram.plot(tsec * 1e6, v, 'r.', alpha=0.4, markersize=2)[0] # print("Create a figure and axes, then call self.gauss.show_fit(axes)") def gauss_out(self, n: int): """ Show center velocity, width, and amplitude for gaussian fits to the data in follower n. """ if n >= len(self.peak_followers): return 0 WRITEOUT, fnum = False, 0 pf = self.peak_followers[n] times, centers, widths, amps = [], [], [], [] vind = pf.frame['vi_span'].to_numpy() tind = pf.frame['t_index'].to_numpy() sp = self.spectrogram for j in range(len(tind)): t = sp.time[tind[j]] * 1e6 vfrom, vto = vind[j] powers = sp.power(sp.intensity[vfrom:vto, tind[j]]) speeds = sp.velocity[vfrom:vto] gus = Gaussian(speeds, powers) if gus.valid: times.append(t) centers.append(gus.center) widths.append(gus.width) amps.append(gus.amplitude) if WRITEOUT: fname = f"{os.path.splitext(self.digfile.filename)[0]}_{fnum:04d}.csv" gus.write_csv(fname) fnum += 1 if WRITEOUT: # write out the times, too fname = f"{os.path.splitext(self.digfile.filename)[0]}_t.csv" v = np.asarray(times) np.savetxt(v, fname) fig, axes = plt.subplots(nrows=1, ncols=3, squeeze=True) ax1, ax2, ax3 = axes ax1.errorbar(times, centers, fmt='b-', yerr=widths) ax1.set_xlabel(r'$t$ ($\mu$s)') ax1.set_ylabel(r'$v$ (m/s)') ax2.plot(times, widths, 'r-') ax2.set_xlabel(r'$t$ ($\mu$s)') ax2.set_ylabel(r'$\delta v$ (m/s)') ax3.plot(times, amps, 'g-') ax3.set_xlabel(r'$t$ ($\mu$s)') ax3.set_ylabel('Amplitude') # Store the values for later access if not hasattr(self, "gauss_outs"): self.gauss_outs = [None for x in range(len(self.peak_followers))] else: while len(self.gauss_outs) < len(self.peak_followers): self.gauss_outs.append(None) self.gauss_outs[n] = dict(time=np.array(times), center=np.array(centers), width=np.array(widths), amplitude=np.array(amps)) def gaussian_explorer(self, follower_pt: int): """ Show center velocity, width, and amplitude for gaussian fits to the data in follower n. """ if len(self.peak_followers) == 0: return 0 pf = self.peak_followers[0] res = pf.results points = len(res['t_index']) def bound(x): return x % points follower_pt = bound(follower_pt) # We'd like to show data for this index, the previous one, # and the next one, along with the gaussian fit hoods = [pf.hood(n=bound(x + follower_pt)) for x in (-2, -1, 0, 1, 2)] # Check that we have the requisite figure, and make it if we don't if not hasattr(self, 'explorer_fig'): self.explorer_fig, self.explorer_axes = plt.subplots(1, 5, sharey=True) # also add a marker to the follower's representation on the # spectrogram to make it easier to see where we are self.explorer_marker = self.axSpectrogram.plot([], [], 'k*')[0] # show where we are tsec, v = pf.v_of_t self.explorer_marker.set_data([ tsec[follower_pt] * 1e6, ], [ v[follower_pt], ]) min_v, max_v, max_peak = 1e10, 0, 0 for ax, hood in zip(self.explorer_axes, hoods): ax.clear() # plot the data ax.plot(hood.velocity, hood.intensity, 'ko', alpha=0.5) # plot the background level used for the moment calculation bg = hood.moment['background'] ax.plot([hood.velocity[0], hood.velocity[-1]], [bg, bg], 'r-') # show the center and widths from the moment calculation tallest = np.max(hood.intensity) max_peak = max(tallest, max_peak) ax.plot([ hood.moment['center'] + x * hood.moment['std_err'] for x in (-1, 0, 1) ], 0.5 * tallest * np.ones(3), 'r.') # show the gaussian hood.plot_gaussian(ax) vcenter, width = hood.peak_v, hood.moment['std_dev'] min_v = min(min_v, vcenter - 12 * width) max_v = max(max_v, vcenter + 12 * width) # ax.set_xlim(vcenter - 12 * width, vcenter + 12 * width) ax.set_xlabel(f"$v$ (m/s)") ax.set_title(f"{hood.time*1e6:.2f}" + " $\\mu$s") txt = f"m = {hood.moment['center']:.1f} ± {hood.moment['std_err']:.1f}" txt = f"{txt}\ng = {hood.gaussian.center:.1f} ± {hood.gaussian.width:.1f}" ax.annotate(txt, xy=(0.05, 0.95), xycoords='axes fraction', horizontalalignment='left', verticalalignment='top') for ax in self.explorer_axes: ax.set_xlim(min_v, max_v) # label the common velocity axis ax = self.explorer_axes[0] ax.set_ylim(-0.05 * max_peak, 1.2 * max_peak) ax.set_ylabel("Intensity") def analyze_roi(self): """ Extract the region(s) of interest and process them """ for roi in self.roi: analyze_region(self.spectrogram, roi['time']) def fan_out(self, n: int): """Produce a zoomed in version of this trace, showing the neighborhood around the determined peak. """ if n >= len(self.peak_followers): return 0 pf = self.peak_followers[n] vind = pf.frame['vi_span'].to_numpy() tind = pf.frame['t_index'].to_numpy() self.subfig, axes = plt.subplots(nrows=1, ncols=2, sharey=True, squeeze=True, gridspec_kw=self._gspec) self.axSpare, self.axTrack = axes # We will create a "waterfall" of curves surrounding # the peaks, each offset by a bit. The x axis will # represent intensity, with subsequent time traces offset # by an amount I need to determine. The y axis # is velocity. spans = [] vvec = self.spectrogram.velocity # shortcut to velocity vector tvec = self.spectrogram.time ivec = self.spectrogram.intensity # pre-extract a bunch of one-dimensional curves # and be sure to convert to power for n in range(len(tind)): vfrom, vto = vind[n] spans.append({ 'v': vvec[vfrom:vto], 'power': self.spectrogram.power(ivec[vfrom:vto, tind[n]]), 't': tvec[tind[n]] * 1e6, }) maxima = np.array([np.max(x['power']) for x in spans]) maxpower = maxima.max() # Let's set the offset between times to be one tenth of # the maxpower offset = 0.025 * maxpower for n in reversed(list(range(len(spans)))): span = spans[n] self.axTrack.plot(span['power'] + n * offset, span['v'], 'b-', alpha=0.33) self.axTrack.set_ylabel('$v$') def squash_vertical(self): normy = self.spectrogram.squash(dB=False) * 2000 self.axSpectrogram.plot(self.spectrogram.time * 1e6, normy, 'b-', alpha=0.75) def update_baselines(self, method): """ Handle the baselines popup menu """ from ProcessingAlgorithms.SignalExtraction.baselines import baselines_by_squash blines = [] self.baselines = [] # remove any existing baselines if method == "Squash": peaks, sigs, heights = baselines_by_squash(self.spectrogram) blines.extend(peaks) # for n in range(len(heights)): # if heights[n] > 0.1: # blines.append(peaks[n]) # Now show the baselines in blines or remove any # if blines is empty if not blines: for b in self.baselines: self.axSpectrum.lines.remove(b['line']) self.baselines = [] # remove them else: edges = (self.spectrogram.intensity.min(), self.spectrogram.intensity.max()) for v in blines: bline = self.axSpectrum.plot([edges[0], edges[1]], [v, v], 'k-', alpha=0.4) self.baselines.append(dict(v=v, line=bline)) def baseline_intensity(self): self.update_baselines("Squash") figgy = plt.figure() ax = figgy.add_subplot(1, 1, 1) sg = self.spectrogram for bline in self.baselines: index = sg._velocity_to_index(bline['v']) if index > 2: ax.semilogy(sg.time * 1e6, sg.power(sg.intensity[index, :])) ax.set_xlabel(r"$t$ ($\mu$ s)") ax.set_ylabel(r"Power") def Spectrum(self, the_time: float): """ Return the column from the spectrogram in power form """ sg = self.spectrogram t_index = sg._time_to_index(the_time) vals = sg.intensity[:, t_index] return sg.power(vals) def spectrum(self, the_time: float, form: str): """ Display a spectrum in the left axes corresponding to the passed value of the_time (which is in seconds). """ _colors = ["r", "g", "b", "y"] if the_time is None: # Initialize the axes # self.axSpectrum.plot([0, 1], [0, 1], 'r-') self.axSpectrum.grid(axis='x', which='both', color='b', alpha=0.4) else: if True: delta_t = self.spectrogram.points_per_spectrum / 2 * \ self.digfile.dt the_spectrum = Spectrum(self.digfile.values( the_time - delta_t, the_time + delta_t), self.digfile.dt, remove_dc=True) # compute the level of the 90th percentile spec = dict(spectrum=the_spectrum) vals = the_spectrum.db ordering = np.argsort(vals) if self.baselines: blines = [x['v'] for x in self.baselines] n = -1 while the_spectrum.velocities[ordering[n]] in blines: n -= 1 else: n = -1 spec['max'] = vals[ordering[n]] noise_floor = int(n - 0.1 * len(vals)) spec['90'] = vals[ordering[noise_floor]] else: t_index = self.spectrogram._time_to_index(the_time) vals = self.spectrogram.intensity[:, t_index] # We need to worry about the format of the spectrum db = ('dB' in form) field = 'db' if db else 'power' the_line = self.axSpectrum.plot(getattr(the_spectrum, field), the_spectrum.velocities, _colors[len(self.spectra)], alpha=0.33) spec['line'] = the_line[0] tval = the_time * 1e6 # convert to microseconds marker = self.axSpectrogram.plot([tval, tval], [0, self.spectrogram.v_max], _colors[len(self.spectra)], alpha=0.33) spec['marker'] = marker[0] self.spectra.append(spec) if db != self.spectra_in_db: self.spectra_in_db = db # switch our mode # and replot all the spectra for spec in self.spectra: li = spec['line'] sp = spec['spectrum'] li.set(xdata=getattr(sp, field), ydata=sp.velocities) self.axSpectrum.set_xlabel("Power (dB)" if db else "Power") if db: # we should order the values and set a limit at something # like the strongest decile ninety = max([x['90'] for x in self.spectra]) peak = max([x['max'] for x in self.spectra]) self.axSpectrum.set_xlim(ninety, peak) return 0 line = self.axSpectrum.lines[0] intensities = the_spectrum.db line.set(xdata=intensities, ydata=the_spectrum.velocities) # We should also add a line to the spectrogram showing where # the spectrum came from. if not self.axSpectrogram.lines: self.axSpectrogram.plot([0, 0], [0, 1], 'r-', alpha=0.33) # this won't scale when we add baselines line = self.axSpectrogram.lines[0] line.set(xdata=[tval, tval], ydata=[0, self.spectrogram.v_max]) def match_templates(self, time, velocity): methodsToUse = [ 'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED' ] matcher = TemplateMatcher( self.spectrogram, template=Templates.opencv_long_start_pattern5.value, span=200, k=20, methods=methodsToUse) times, velos, scores, methodsUsed = matcher.match() time_offset = 2.0 + (2 * abs( matcher.spectrogram.time[matcher.template_time_offset_index] * 1e6) ) times = np.array(times) + time_offset matcher.add_to_plot(self.axSpectrogram, times, velos, scores, methodsUsed, show_points=True, show_medoids=True, verbose=False, visualize_opacity=False, show_bounds=True)
def seamExtraction(SpectrogramObject:Spectrogram, startTime:int, stopTime:int, width:int, signalJump:int=50, bottomIndex:int=0, topIndex:int=None, order:int = None): """ Input: intensityMatrix: a 2d array [velocity][time] and each cell corresponds to the intensity at that point. startTime: index corresponding to the start time in the intensity matrix of the signal being considered. stopTime: index corresponding to the stop time in the intensity matrix of the signal being considered. width: How wide of a window that is being considered at each time step. The window will be split evenly up and down signalJump: How far up you think the signal will be above the bottomIndex defaults to 50 bottomIndex: Upper bound on the velocity. Assumed to be a valid index for sgram's intensity array. Defaults to zero order: the order of the minkowski distance function that will be minimized to determine the most well connected signal. Defaults to 2: minimizing the Euclidean distance between neighboring pixels. Output: list of indices that should be plotted as the signal as an array of indices indicating the intensity values that correspond to the most connected signal as defined by the metric. """ if stopTime <= startTime: raise ValueError("Stop Time is assumed to be greater than start time.") else: if width % 2 == 1: width += 1 tableHeight = stopTime - startTime + 1 velocities = np.zeros(tableHeight, dtype = np.int64) # This will hold the answers that we are looking for to return. It is a list of indices. halfFan = width//2 if topIndex == None: topIndex = SpectrogramObject.velocity.shape[0] # I just want the maximum velocity index if topIndex <= bottomIndex + signalJump: topIndex = min((stopTime - startTime + 1)*width + bottomIndex, SpectrogramObject.velocity.shape[0]) transposedIntensities = np.transpose(SpectrogramObject.intensity) startIndex = np.argmax(transposedIntensities[startTime][bottomIndex+signalJump:topIndex+1])+bottomIndex+signalJump bottomDP = max(startIndex - (stopTime - startTime)*halfFan, bottomIndex + 1) print("BottomDP:", bottomDP, "TopIndex:", topIndex) t, vel, raw = SpectrogramObject.slice((SpectrogramObject.time[startTime], SpectrogramObject.time[stopTime]), (SpectrogramObject.velocity[bottomDP], SpectrogramObject.velocity[topIndex])) print("new time shape", t.shape, "vel shape:", vel.shape) print("Before the transpose: raw.shape", raw.shape) raw = np.transpose(raw) tableHeight, tableWidth = raw.shape print("raw.shape = ",raw.shape) print("bottomDP", bottomDP) print("The starting of index is ", startIndex) print() print("t2-t1", stopTime-startTime) print("(t2-t1)*halfan", (stopTime-startTime)*halfFan) print() print(topIndex) print(bottomIndex+1) print() print(tableWidth) print(tableHeight) DPTable = np.zeros((tableHeight, tableWidth)) parentTable = np.zeros(DPTable.shape, dtype = np.int64) for timeIndex in range(2, tableHeight + 1): dpTime = tableHeight - timeIndex for velocityIndex in range(tableWidth): bestSoFar = np.Infinity bestPointer = None for testIndex in range(-halfFan, halfFan+1): # The + 1 is so that you get a balanced window. if velocityIndex + testIndex >= 0 and velocityIndex + testIndex < tableWidth: # then we have a valid index to test from. current = np.power(np.abs(raw[dpTime+1][testIndex+velocityIndex]-raw[dpTime][velocityIndex]), order) \ + DPTable[dpTime+1][velocityIndex+testIndex] if current < bestSoFar: bestSoFar = current bestPointer = velocityIndex + testIndex DPTable[dpTime][velocityIndex] = bestSoFar parentTable[dpTime][velocityIndex] = bestPointer # Now for the reconstruction. currentPointer = np.argmin(DPTable[0]) # Get the values of the DP table to a more reasonable values by taking the orderth root. # for timeIndex in range(tableHeight+1): # for velocityIndex in range(tableWidth): # DPTable[timeIndex][velocityIndex] = np.power(DPTable[timeIndex][velocityIndex], 1/order) print("The value of the current pointer is", currentPointer) print("The minimum cost is", DPTable[0][currentPointer]) velocities = reconstruction(parentTable, currentPointer, bottomDP) return velocities, parentTable, DPTable, bottomDP, topIndex