def plot_decomp(ds, sheath_label='ion_a', mode=1, fit_fl=True, ax=None, kwargs_for_plot=None, kwargs_for_fitplot=None, colour='r', plot_label=True, length=DEFAULT_L, gap=DEFAULT_G): sheath_error_label = sheath_label.replace('_a', '_d_a') if ax is None: fig, ax = plt.subplots() else: fig = ax.figure if kwargs_for_plot is None: kwargs_for_plot = {} if kwargs_for_fitplot is None: kwargs_for_fitplot = {} x, y = perform_decomp(ds, sheath_label=sheath_label, mode=mode, length=length, gap=gap) non_nan_y = ~np.isnan(y) x = x[non_nan_y] y = y[non_nan_y] yerr = ds[sheath_error_label].values[non_nan_y] * y if plot_label is True: plot_label = sheath_label ax.errorbar(x, y, yerr=yerr, color=colour, label=plot_label, **kwargs_for_plot) sl_fitter = fts.StraightLineFitter() fit_data = sl_fitter.fit(x, y) if fit_fl: c1, c2 = MODE_CONSTANTS[mode] fit_label = r'$c_1$ = {:.2g}, $c_2$ = {:.2g}'.format( fit_data.get_param(c1), fit_data.get_param(c2)) ax.plot(*fit_data.get_fit_plottables(), color=colour, label=fit_label, **kwargs_for_fitplot) ax.set_xlabel(MODE_LABELS[mode][0]) ax.set_ylabel(MODE_LABELS[mode][1]) ax.legend() return ax, fit_data
def homogenise(self, frequency=None, filter_arcs_fl=False, plot_fl=True): """ Chooses the region of interest and sections the time trace into individual sweeps, populating the member variable 'iv_arrs' with an IVData object for each sweep and combining these into a numpy array for each coax :param frequency: (float) The frequency of sweeps used in the shot If not specified it will be calculated from the raw voltage trace using FFT (which may be slow). :param filter_arcs_fl: (bool) Boolean flag, if true will attempt to automatically filter out arcs by excluding sweeps which have abnormally high max/min voltages :param plot_fl: (bool) Boolean flag, controls whether the method plots various useful figures """ triangle = f.TriangleWaveFitter() if frequency is None: # Use fourier decomposition to get frequency if none given frequency = triangle.get_frequency( self.raw_time, self.raw_voltage, accepted_freqs=self._ACCEPTED_FREQS) # Take the first 5% of data to run the sweep partitioning algorithm on slc_oi = slice(0, int(0.05 * len(self.raw_time))) # Smooth the voltage to get a first read of the peaks on the triangle wave smoothed_voltage = sig.savgol_filter(self.raw_voltage, 21, 2) top = sig.argrelmax(smoothed_voltage[slc_oi], order=100)[0] bottom = sig.argrelmin(smoothed_voltage[slc_oi], order=100)[0] _peaks = self.raw_time[np.concatenate([top, bottom])] _peaks.sort() # Get distances between the peaks and filter based on the found frequency _peak_distances = np.diff(_peaks) threshold = (1 / (2 * frequency)) - 0.001 _peaks_ind = np.where(_peak_distances > threshold)[0] # Starting from the first filtered peak, arrange a period-spaced array peaks_refined = np.arange(_peaks[_peaks_ind[0]], self.raw_time[-1], 1 / (2 * frequency)) self.peaks = peaks_refined if plot_fl: plt.figure() plt.plot(self.raw_time, self.raw_voltage) plt.plot(self.raw_time, triangle.fit(self.raw_time, self.raw_voltage).fit_y) for peak in self.peaks: plt.axvline(x=peak, linestyle='dashed', linewidth=1, color='r') if self.combine_sweeps_fl: skip = 2 sweep_fitter = triangle else: skip = 1 sweep_fitter = f.StraightLineFitter() for i in range(self.coaxes): for j in range(len(self.peaks) - skip): sweep_start = np.abs(self.raw_time - self.peaks[j]).argmin() sweep_stop = np.abs(self.raw_time - self.peaks[j + skip]).argmin() sweep_voltage = self.voltage[i][sweep_start:sweep_stop] sweep_time = self.raw_time[sweep_start:sweep_stop] if filter_arcs_fl: # TODO: Fix this sweep_fit = sweep_fitter.fit(sweep_time, sweep_voltage) self.max_voltage.append( (np.max(np.abs(sweep_voltage - sweep_fit.fit_y)))) if i == 0 and plot_fl: sweep_fit.plot() if np.max( np.abs(sweep_voltage - sweep_fit.fit_y)) > self._ARCING_THRESHOLD: self.arcs.append(np.mean(sweep_time)) continue # sweep_current = [current[sweep_start:sweep_stop] for current in self.current] sweep_current = self.current[i][sweep_start:sweep_stop] # Reverse alternate sweeps if not operating in combined sweeps mode, so if not self.combine_sweeps_fl and sweep_voltage[ 0] > sweep_voltage[-1]: # sweep_voltage = np.array(list(reversed(sweep_voltage))) # sweep_time = np.array(list(reversed(sweep_time))) # sweep_current = np.array(list(reversed(sweep_current))) sweep_time = np.flip(sweep_time) sweep_voltage = np.flip(sweep_voltage) sweep_current = np.flip(sweep_current) # Create IVData objects for each sweep (or sweep pair) # TODO: What's the std_err_scaler doing here? Look through commits self.iv_arrs[i].append( iv.IVData(sweep_voltage, sweep_current, sweep_time, std_err_scaler=0.95))
def gunn_fit(self, sat_region=_DEFAULT_STRAIGHT_CUTOFF, plot_fl=False): """ A version of Jamie Gunn's 4-parameter fit by subtracting the straight line of the ion saturation region from the whole IV curve. This is was adapted from a jupyter notebook made for Magnum data, and generates four plots: averaged IV, straight line overlay, corrected IV, 3-param fit to corrected IV. :return: A tuple of (corrected_iv_data, corrected_iv_fit_data) """ import matplotlib.pyplot as plt import flopter.core.fitters as fts iv_data = self # define a straight section and trim the iv data to it str_sec = np.where(iv_data['V'] <= sat_region) iv_data_ss = IVData.non_contiguous_trim(iv_data, str_sec) # needed to define the area of the straight section on a graph with a vertical line str_sec_end = np.argmax(iv_data['V'][str_sec]) # fit & plot a straight line to the 'straight section' sl_fitter = fts.StraightLineFitter() fit_data_ss = sl_fitter.fit(iv_data_ss['V'], iv_data_ss['I'], sigma=iv_data_ss['sigma']) # Extrapolate the straight line over a wider voltage range for illustrative purposes sl_range = np.linspace(-120, 100, 100) sl_function = fit_data_ss.fit_function(sl_range) # Subtract the gradient of the straight section from the whole IV curve. iv_data_corrected = iv_data.copy() iv_data_corrected['I'] = iv_data_corrected['I'] - ( fit_data_ss.get_param('m') * iv_data_corrected['V']) simple_iv_fitter = fts.SimpleIVFitter() fit_data_corrected = iv_data_corrected.multi_fit( sat_region=sat_region, iv_fitter=simple_iv_fitter, plot_fl=plot_fl) if plot_fl: plt.figure() plt.errorbar(iv_data['V'], iv_data['I'], yerr=iv_data['sigma'], label='Full IV', color='darkgrey', ecolor='lightgray') plt.legend() plt.xlabel(r'$V_p$ / V') plt.ylabel(r'$I$ / A') plt.ylim(-0.5, 1.3) plt.xlim(-102, 5) plt.figure() plt.errorbar(iv_data['V'], iv_data['I'], yerr=iv_data['sigma'], label='Full IV', color='darkgrey', ecolor='lightgray') plt.plot(sl_range, sl_function, label='SE Line', color='blue', linewidth=0.5, zorder=10) plt.legend() plt.xlabel(r'$V_p$ / V') plt.ylabel(r'$I$ / A') plt.ylim(-0.5, 1.3) plt.xlim(-102, 5) plt.figure() plt.plot(sl_range, sl_function, label='SE Line', color='blue', linewidth=0.5, zorder=10) plt.errorbar(iv_data_corrected['V'], iv_data_corrected['I'], label='Corrected IV', yerr=iv_data_corrected[c.SIGMA], color='darkgrey', ecolor='lightgray') plt.legend() plt.xlabel(r'$V_p$ / V') plt.ylabel(r'$I$ / A') plt.ylim(-0.5, 1.3) plt.xlim(-102, 5) plt.figure() plt.plot(sl_range, sl_function, label='SE Line', color='blue', linewidth=0.5) plt.errorbar(iv_data_corrected['V'], iv_data_corrected['I'], label='Corrected IV', yerr=iv_data_corrected[c.SIGMA], color='darkgrey', ecolor='lightgray') plt.plot(*fit_data_corrected.get_fit_plottables(), label='3 Param-Fit', zorder=10, color='r') plt.legend() plt.xlabel(r'$V_p$ / V') plt.ylabel(r'$I$ / A') plt.ylim(-0.5, 1.3) plt.xlim(-102, 5) plt.show() return iv_data_corrected, fit_data_corrected
def prepare(self, down_sampling_rate=5, plot_fl=False, filter_arcs_fl=False, roi_b_plasma=False, crit_freq=640, crit_ampl=1.1e-3): """ Preparation consists of downsampling (if necessary), choosing the region of interest and putting each sweep into a numpy array of iv_datas """ # This whole function should probably be put into a homogeniser implementation # Downsample by factor given arr_size = len(self.m_data.data[self.m_data.channels[0]]) downsample = np.arange(0, arr_size, down_sampling_rate, dtype=np.int64) for ch, data in self.m_data.data.items(): self.m_data.data[ch] = data[downsample] self.m_data.time = self.m_data.time[downsample] + self._ADC_TIMER_OFFSET self.m_data.data[self._VOLTAGE_CHANNEL] = self.m_data.data[ self._VOLTAGE_CHANNEL] * 100 # Find region of interest if roi_b_plasma and not self.offline and np.shape( self.magnum_data[mag.PLASMA_STATE])[1] == 2: start = np.abs(self.m_data.time - self.magnum_data[mag.PLASMA_STATE][0][0]).argmin() end = np.abs(self.m_data.time - self.magnum_data[mag.PLASMA_STATE][0][1]).argmin() else: start = 0 end = len(self.m_data.time) # Read in raw values from adc file - these are the time and the voltages measured on each channel self.raw_time = np.array(self.m_data.time[start:end]) self.raw_voltage = np.array( self.m_data.data[self._VOLTAGE_CHANNEL][start:end]) for i, probe_index in enumerate( [self._PROBE_CHANNEL_3, self._PROBE_CHANNEL_4]): self.raw_current.append( np.array(self.m_data.data[probe_index][start:end])) # Convert the adc voltages into the measured values for i in range(self.coaxes): # Current is ohmicly calculated from the voltage across a shunt resistor self.current.append(self.raw_current[i] / self.shunt_resistance) # Separate volages are applied to each probe depending on the current they draw self.voltage.append(self.raw_voltage - self.raw_current[i] - (self.cabling_resistance * self.current[i])) # self.current = np.array(self.current) self.filter(crit_ampl=crit_ampl, crit_freq=crit_freq, plot_fl=plot_fl) for i in range(self.coaxes): # Use fourier decomposition from get_frequency method in triangle fitter to get frequency triangle = f.TriangleWaveFitter() frequency = triangle.get_frequency( self.raw_time, self.voltage[i], accepted_freqs=self._ACCEPTED_FREQS) # Smooth the voltage to get a first read of the peaks on the triangle wave smoothed_voltage = sig.savgol_filter(self.voltage[i], 21, 2) top = sig.argrelmax(smoothed_voltage, order=100)[0] bottom = sig.argrelmin(smoothed_voltage, order=100)[0] _peaks = self.raw_time[np.concatenate([top, bottom])] _peaks.sort() # Get distances between the peaks and filter based on the found frequency _peak_distances = np.diff(_peaks) threshold = (1 / (2 * frequency)) - 0.001 _peaks_ind = np.where(_peak_distances > threshold)[0] # Starting from the first filtered peak, arrange a period-spaced array peaks_refined = np.arange(_peaks[_peaks_ind[0]], self.raw_time[-1], 1 / (2 * frequency)) self.peaks = peaks_refined if plot_fl: plt.figure() plt.plot(self.raw_time, self.voltage[i]) plt.plot(self.raw_time, triangle.fit(self.raw_time, self.voltage[i]).fit_y) for peak in self.peaks: plt.axvline(x=peak, linestyle='dashed', linewidth=1, color='r') if self.combine_sweeps_fl: skip = 2 sweep_fitter = triangle else: skip = 1 sweep_fitter = f.StraightLineFitter() # print('peaks_len = {}'.format(len(self.peaks) - skip)) for j in range(len(self.peaks) - skip): sweep_start = np.abs(self.raw_time - self.peaks[j]).argmin() sweep_stop = np.abs(self.raw_time - self.peaks[j + skip]).argmin() sweep_voltage = self.voltage[i][sweep_start:sweep_stop] sweep_time = self.raw_time[sweep_start:sweep_stop] if filter_arcs_fl: sweep_fit = sweep_fitter.fit(sweep_time, sweep_voltage) self.max_voltage.append( (np.max(np.abs(sweep_voltage - sweep_fit.fit_y)))) if i == 0 and plot_fl: sweep_fit.plot() if np.max( np.abs(sweep_voltage - sweep_fit.fit_y)) > self._ARCING_THRESHOLD: self.arcs.append(np.mean(sweep_time)) continue # sweep_current = [current[sweep_start:sweep_stop] for current in self.current] sweep_current = self.current[i][sweep_start:sweep_stop] # Reverse alternate sweeps if not operating in combined sweeps mode, so if not self.combine_sweeps_fl and sweep_voltage[ 0] > sweep_voltage[-1]: # sweep_voltage = np.array(list(reversed(sweep_voltage))) # sweep_time = np.array(list(reversed(sweep_time))) # sweep_current = np.array(list(reversed(sweep_current))) sweep_time = np.flip(sweep_time) sweep_voltage = np.flip(sweep_voltage) sweep_current = np.flip(sweep_current) # Create IVData objects for each sweep (or sweep pair) # TODO: What's the std_err_scaler doing here? Look through commits self.iv_arrs[i].append( iv.IVData(sweep_voltage, sweep_current, sweep_time, std_err_scaler=0.95))
def multi_fit(self, sat_region=_DEFAULT_STRAIGHT_CUTOFF, stage_2_guess=None, iv_fitter=None, sat_fitter=None, fix_vf_fl=False, plot_fl=False, print_fl=False, minimise_temp_fl=True, trim_to_floating_fl=True, **kwargs): """ Multi-stage fitting method using an initial straight line fit to the saturation region of the IV curve (decided by the sat_region kwarg). The fitted I_sat is then left fixed while T_e and a are found with a 2-parameter fit, which gives the guess parameters for an unconstrained full IV fit. :param sat_region: (Integer) Threshold voltage value below which the 'Straight section' is defined. The straight section is fitted to get an initial value of saturation current for subsequent fits. :param sat_fitter: Fitter object to be used for the initial saturation region fit :param stage_2_guess: Tuple-like containing starting values for all parameters in iv_fitter, to be used as initial parameters in the second stage fit. Note that only the temperature (and optionally sheath expansion parameter) will be used, as the isat will already have a value (by design) and the floating potential is set at the interpolated value. These will be overwritten if present. Default behaviour (stage_2_guess=None) is to use the defaults on the iv_fitter object. :param iv_fitter: Fitter object to be used for the fixed-I_sat fit and the final, full, free fit. :param plot_fl: (Boolean) If true, plots the output of all 3 stages of fitting. Default is False. :param fix_vf_fl: (Boolean) If true, fixes the floating potential for all 3 stages of fitting. The value used is the interpolated value of Voltage where Current = 0. Default is False. :param print_fl: (Boolean) If true, prints fitter information to console. :param minimise_temp_fl: (Boolean) Flag to control whether multiple final fits are performed at a range of upper indices past the floating potential, with the fit producing the lowest temperature returned. :param trim_to_floating_fl: (Boolean) Flag to control whether to truncate the IV characteristic to values strictly below the floating potential before fitting. This is ignored by the minimisation routine, so the full IV will be used in that case. :return: (IVFitData) Full 4-parameter IVFitData object """ import flopter.core.fitters as f if iv_fitter is None or not isinstance(iv_fitter, f.IVFitter): iv_fitter = f.FullIVFitter() if not isinstance(sat_fitter, f.GenericCurveFitter): if sat_fitter is not None and print_fl: print( 'Provided sat_fitter is not a valid child of GenericCurveFitter, continuing with the default \n' 'straight line fitter.') sat_fitter = f.StraightLineFitter() if print_fl: print(f'Running saturation region fit with {sat_fitter.name}, \n' f'running subsequent IV fits with {iv_fitter.name}') # find floating potential and max potential v_f = f.IVFitter.find_floating_pot_iv_data(self) if trim_to_floating_fl: iv_data_trim = self.get_below_floating(v_f=v_f, print_fl=print_fl) else: iv_data_trim = self.copy() # Find and fit straight section str_sec = np.where(iv_data_trim[c.POTENTIAL] <= sat_region) iv_data_ss = IVData.non_contiguous_trim(iv_data_trim, str_sec) if fix_vf_fl and c.FLOAT_POT in sat_fitter: sat_fitter.set_fixed_values({c.FLOAT_POT: v_f}) # Attempt first stage fit try: stage1_f_data = sat_fitter.fit(iv_data_ss[c.POTENTIAL], iv_data_ss[c.CURRENT], sigma=iv_data_ss[c.SIGMA]) except RuntimeError as e: raise MultiFitError(f'RuntimeError occured in stage 1. \n' f'Original error: {e}') # Use I_sat value to fit a fixed_value 4-parameter IV fit if c.ION_SAT in sat_fitter: isat_guess = stage1_f_data.get_isat() else: isat_guess = stage1_f_data.fit_function(sat_region) if fix_vf_fl: iv_fitter.set_fixed_values({ c.FLOAT_POT: v_f, c.ION_SAT: isat_guess }) else: iv_fitter.set_fixed_values({c.ION_SAT: isat_guess}) if stage_2_guess is None: stage_2_guess = list(iv_fitter.default_values) elif len(stage_2_guess) != len(iv_fitter.default_values): raise ValueError( f'stage_2_guess is not of the appropriate length ({len(stage_2_guess)}) for use as initial ' f'parameters in the given iv_fitter (should be length {len(iv_fitter.default_values)}).' ) stage_2_guess[iv_fitter.get_isat_index()] = isat_guess stage_2_guess[iv_fitter.get_vf_index()] = v_f # Attempt second stage fit try: stage2_f_data = iv_fitter.fit_iv_data(iv_data_trim, sigma=iv_data_trim[c.SIGMA], initial_vals=stage_2_guess) except RuntimeError as e: raise MultiFitError(f'RuntimeError occured in stage 2. \n' f'Original error: {e}') # Do a full 4 parameter fit with initial guess params taken from previous fit params = stage2_f_data.fit_params.get_values() iv_fitter.unset_fixed_values() if fix_vf_fl: iv_fitter.set_fixed_values({c.FLOAT_POT: v_f}) # Attempt third stage fit. Option to fit to multiple values past the floating potential to minimise the # temperature of the fit or just to the floating potential. try: if minimise_temp_fl: stage3_f_data = self.fit_to_minimum(initial_vals=params, fitter=iv_fitter, plot_fl=plot_fl, **kwargs) else: stage3_f_data = iv_fitter.fit_iv_data( iv_data_trim, initial_vals=params, sigma=iv_data_trim[c.SIGMA]) except RuntimeError as e: raise MultiFitError(f'RuntimeError occured in stage 3. \n' f'Original error: {e}') if plot_fl: fig, ax = plt.subplots(3, sharex=True, sharey=True) stage1_f_data.plot(ax=ax[0]) ax[0].set_xlabel('') ax[0].set_ylabel('Current (A)') stage2_f_data.plot(ax=ax[1]) ax[1].set_xlabel('') ax[1].set_ylabel('Current (A)') stage3_f_data.plot(ax=ax[2]) ax[2].set_xlabel('Voltage (V)') ax[2].set_ylabel('Current (A)') fig.suptitle('lower_offset = {}, upper_offset = {}'.format( self.trim_beg, self.trim_end)) return stage3_f_data