From ce7b03a3502254265b0917495d4a24bf1692aab4 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Wed, 18 May 2016 09:02:13 +0300 Subject: [PATCH 01/16] Add `get_experment_data` method to 0MQ API Add `get_experment_data` method to 0MQ API, which returns a `pandas.DataFrame` containing two columns, `time_s` and `current_amps` which correspond to the *time in seconds* since the start of the DStat experiment, and the measured current in *amps*, respectively. --- dstat_interface/main.py | 66 +++++++++++++++++++++++++++++---------- dstat_interface/plugin.py | 17 ++++++++-- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index a7e05d1..d95fd0f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -54,6 +54,8 @@ from interface.db import DB_Window import dstat_comm as comm import interface.exp_window as exp_window import interface.adc_pot as adc_pot +import numpy as np +import pandas as pd import plot import params import parameter_test @@ -77,6 +79,34 @@ root_logger.addHandler(log_handler) logger = logging.getLogger("dstat.main") + +def dstat_data_to_frame(data): + ''' + Convert DStat experiment data to `pandas.DataFrame`. + + Args + ---- + + data (list) : List of numpy array pairs (i.e., tuples). For each array + pair, each item in the zipped array has two values; the first value + is the *time in seconds* since the start of the DStat experiment, + the second value is the measured current in *amps*. + + Returns + ------- + + (`pandas.DataFrame`) : Table containing two columns, `time_s` and + `current_amps`. + ''' + if not data: + return pd.DataFrame(None, columns=['time_s', 'current_amps']) + else: + return (pd.concat([pd.DataFrame(np.column_stack(d), + columns=['time_s', 'current_amps']) + for d in data]) + .reset_index(drop=True)) + + class Main(object): """Main program """ def __init__(self): @@ -105,7 +135,7 @@ class Main(object): self.exp_window = exp_window.Experiments(self.builder) self.analysis_opt_window = analysis.AnalysisOptions(self.builder) - + self.db_window = DB_Window() self.builder.get_object('menu_database_options').connect_object( 'activate', DB_Window.show, self.db_window @@ -179,9 +209,9 @@ class Main(object): 'menu_dropbot_disconnect') self.dropbot_enabled = False self.dropbot_triggered = False - + self.metadata = None # Should only be added to by plugin interface - + self.plot_notebook.get_nth_page( self.plot_notebook.page_num(self.ft_window)).hide() self.plot_notebook.get_nth_page( @@ -195,6 +225,8 @@ class Main(object): self.active_experiment_id = None # UUIDs for completed experiments. self.completed_experiment_ids = OrderedDict() + # Data collected during completed experiments. + self.completed_experiment_data = OrderedDict() def on_window1_destroy(self, object, data=None): """ Quit when main window closed.""" @@ -434,16 +466,16 @@ class Main(object): experiment_id = uuid.uuid4() self.active_experiment_id = experiment_id logger.info("Current measurement id: %s", experiment_id.hex) - + self.metadata = metadata - + if self.metadata is not None: logger.info("Loading external metadata") self.db_window.update_from_metadata(self.metadata) elif self.db_window.params['exp_id_entry'] is None: logger.info("DB exp_id field blank, autogenerating") self.db_window.on_exp_id_autogen_button_clicked() - + self.db_window.params = {'measure_id_entry':experiment_id.hex} def exceptions(): @@ -470,13 +502,13 @@ class Main(object): else: nb.get_nth_page(nb.page_num(self.ft_window)).hide() # nb.get_nth_page(nb.page_num(self.period_window)).hide() - + if parameters['db_enable_checkbutton']: if db.current_db is None: db.start_db() elif not db.current_db.connected: db.restart_db() - + comm.serial_instance.proc_pipe_p.send(self.current_exp) # Flush data pipe @@ -507,9 +539,9 @@ class Main(object): try: if param_override is not None: params.set_params(self, param_override) - + parameters.update(params.get_params(self)) - + self.line = 0 self.lastline = 0 self.lastdataline = 0 @@ -814,7 +846,7 @@ class Main(object): # Database output if self.current_exp.parameters['db_enable_checkbutton']: meta = {} - + if self.current_exp.parameters['metadata'] is not None: metadata = self.current_exp.parameters['metadata'] exp_metakeys = ['experiment_uuid', 'patient_id', 'name'] @@ -824,9 +856,9 @@ class Main(object): if k not in exp_metakeys } ) - + name = self.current_exp.parameters['measure_name_entry'] - + newname = db.current_db.add_results( measurement_uuid=self.active_experiment_id.hex, measurement_name=name, @@ -836,14 +868,13 @@ class Main(object): timestamp=None, data=self.current_exp.export() ) - + self.db_window.params = {'measure_name_entry':newname} - # uDrop # UI stuff finally: self.metadata = None # Reset metadata - + self.spinner.stop() self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) @@ -851,6 +882,9 @@ class Main(object): self.start_ocp() self.completed_experiment_ids[self.active_experiment_id] =\ datetime.utcnow() + # Save current measurements for experiment in `pandas.DataFrame`. + self.completed_experiment_data[self.active_experiment_id] =\ + dstat_data_to_frame(self.current_exp.data['data']) def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index 59ec93d..e073a1d 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -119,15 +119,26 @@ class DstatPlugin(ZmqPlugin): data = decode_content_data(request) self.parent.metadata = request - def on_execute__save_text(self, request): + def on_execute__get_experiment_data(self, request): ''' Args ---- - save_data_path (str) : Path to file to save text data. + experiment_id (str) : Path to file to save text data. + + Returns + ------- + + (pandas.DataFrame) : Experiment results with columns `time_s` and + `current_amps`. ''' data = decode_content_data(request) - save_text(self.parent.current_exp, data['save_data_path']) + if data['experiment_id'] in self.parent.completed_experiment_ids: + return self.parent.completed_experiment_data[data['experiment_id']] + elif data['experiment_id'] == self.parent.active_experiment_id: + return None + else: + raise KeyError('Unknown experiment ID: %s' % data['experiment_id']) def on_execute__save_plot(self, request): ''' -- GitLab From 3179ca74bfccdb5669d5dcd95d81d0fdfb45d43a Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 13:52:01 +0300 Subject: [PATCH 02/16] Port code to use `pandas.DataFrame` objects Add the following functions, which existing code to use `pandas.DataFrame` objects: - `integrate_fft`: Integrate a bandwidth around a target frequency in DStat FFT results. - `dstat_to_fft_frame`: Compute FFT for set of DStat measurements. - `dstat_to_frame`: Convert DStat text file results to `pandas.DataFrame`. - `reduce_dstat_data`: Reduce measurements for multiple DStat experiments to a single row per experiment with an aggregate signal value. See function docstrings for more details. --- dstat_interface/analysis.py | 259 ++++++++++++++++++++++++++++++------ 1 file changed, 221 insertions(+), 38 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 9526872..2286cb4 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -1,30 +1,31 @@ #!/usr/bin/env python # DStat Interface - An interface for the open hardware DStat potentiostat -# Copyright (C) 2014 Michael D. M. Dryden - +# Copyright (C) 2014 Michael D. M. Dryden - # Wheeler Microfluidics Laboratory -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Functions for analyzing data. -""" +""" import logging import pygtk import gtk -from numpy import mean, trapz +import numpy as np +import pandas as pd logger = logging.getLogger('dstat.analysis') @@ -34,61 +35,61 @@ class AnalysisOptions(object): self.builder = builder self.builder.add_from_file('interface/analysis_options.glade') self.builder.connect_signals(self) - + self.window = self.builder.get_object('analysis_dialog') self.stats_button = self.builder.get_object('stats_button') self.stats_start = self.builder.get_object('stats_start_spin') self.stats_start_button = self.builder.get_object('stats_start_button') - self.stats_stop = self.builder.get_object('stats_stop_spin') + self.stats_stop = self.builder.get_object('stats_stop_spin') self.stats_stop_button = self.builder.get_object('stats_stop_button') self.stats_button.connect('toggled', self.on_button_toggled_hide, - [self.stats_stop, + [self.stats_stop, self.stats_stop_button, self.stats_start, self.stats_start_button ] ) - + self._params = {'stats_true':False, 'stats_start_true':False, 'stats_stop':0, 'stats_stop_true':False, 'stats_start':0 } - + def show(self): """Show options window.""" self.window.run() self.window.hide() - + def on_button_toggled_hide(self, control, widgets): """Hide unchecked fields""" active = control.get_active() - + for widget in widgets: widget.set_sensitive(active) - + @property def params(self): """Getter for analysis params""" self._params['stats_true'] = self.stats_button.get_active() - + self._params['stats_start_true'] = self.stats_start_button.get_active() self._params['stats_start'] = self.stats_start.get_value() - + self._params['stats_stop_true'] = self.stats_stop_button.get_active() self._params['stats_stop'] = self.stats_stop.get_value() return self._params - + @params.setter def params(self, params): for key in self._params: if key in params: self._params[key] = params[key] - + self.stats_button.set_active(self._params['stats_true']) self.stats_start_button.set_active(self._params['stats_start_true']) self.stats_start.set_value(self._params['stats_start']) @@ -97,32 +98,32 @@ class AnalysisOptions(object): def do_analysis(experiment): """Takes an experiment class instance and runs selected analysis.""" - + experiment.analysis = {} - + if experiment.parameters['stats_true']: if (experiment.parameters['stats_start_true'] or experiment.parameters['stats_stop_true']): - + if experiment.parameters['stats_start_true']: start = experiment.parameters['stats_start'] else: start = min(experiment.data['data'][0][0]) - + if experiment.parameters['stats_stop_true']: stop = experiment.parameters['stats_stop'] else: stop = min(experiment.data['data'][0][0]) - + data = _data_slice(experiment.data['data'], start, stop ) else: data = experiment.data['data'] - + experiment.analysis.update(_summary_stats(data)) - + try: x, y = experiment.data['ft'][0] experiment.analysis['FT Integral'] = _integrateSpectrum( @@ -140,27 +141,27 @@ def _data_slice(data, start, stop): between start and stop (in whatever x-axis units for the experiment type). """ output = [] - + for scan in range(len(data)): t = [] for i in range(len(data[scan])): t.append([]) output.append(tuple(t)) - + for i in range(len(data[scan][0])): # x-axis column if data[scan][0][i] >= start or data[scan][0][i] <= stop: for d in range(len(output[scan])): - output[scan][d].append(data[scan][d][i]) - + output[scan][d].append(data[scan][d][i]) + return output - + def _summary_stats(data): - """Takes data and returns summary statistics of first y variable as dict of + """Takes data and returns summary statistics of first y variable as dict of name, (scan, values). """ - + stats = {'min':[],'max':[], 'mean':[]} - + for scan in range(len(data)): stats['min'].append( (scan, min(data[scan][1])) @@ -169,10 +170,10 @@ def _summary_stats(data): (scan, max(data[scan][1])) ) stats['mean'].append( - (scan, mean(data[scan][1])) + (scan, np.mean(data[scan][1])) ) return stats - + def _integrateSpectrum(x, y, target, bandwidth): """ Returns integral of range of bandwidth centered on target. @@ -184,10 +185,192 @@ def _integrateSpectrum(x, y, target, bandwidth): if x[i] >= target-bandwidth/2: j = i break - + for i in range(j,len(x)): if x[i] >= target+bandwidth/2: k = i break - return [(0, trapz(y=y[j:k], x=x[j:k]))] \ No newline at end of file + return [(0, np.trapz(y=y[j:k], x=x[j:k]))] + + +def integrate_fft(df_fft, target_hz, bandwidth): + ''' + Integrate a bandwidth around a target frequency in DStat FFT results. + + Args + ---- + + df_data (pandas.DataFrame) : DStat FFT results as returned by the + `dstat_to_fft_frame` function, including the columns `frequency` + and `amplitude`. + target_hz (float) : Target frequency. + bandwidth (float) : Bandwidth (centered at target frequency) to + integrate within. + + Returns + ------- + + (float) : Integrated amplitude within bandwidth around target + frequency (definite integral as approximated by trapezoidal + rule). See `numpy.trapz` for more information. + ''' + df_bandpass = df_fft.loc[(df_fft.frequency >= target_hz - .5 * bandwidth) & + (df_fft.frequency <= target_hz + .5 * bandwidth)] + + # Definite integral as approximated by trapezoidal rule. + return np.trapz(x=df_bandpass.frequency, y=df_bandpass.amplitude) + + +def dstat_to_fft_frame(df_data, sample_frequency_hz=60., settling_period_s=2.): + ''' + Compute FFT for set of DStat measurements. + + Args + ---- + + df_data (pandas.DataFrame) : DStat experiment results with at least the + columns `time_s`, and `current_amps`. + settling_period_s (float) : Signal settling time (in seconds). No + measurements before specified time will be considered when + computing the FFT. + + Returns + ------- + + df_data (pandas.DataFrame) : DStat FFT table with the columns + `frequency` and `amplitude`. + ''' + df_data_i = df_data.loc[(df_data.time_s > settling_period_s)].copy() + df_data_i.reset_index(drop=True, inplace=True) + + avg = np.mean(df_data_i.current_amps.values) + + # Find first and last rising edge in synchronous signal. + rising_edge_i = np.zeros(df_data_i.shape[0], dtype=bool) + rising_edge_i[:-1] = ((df_data_i.current_amps <= avg)[:-1].values & + (df_data_i.current_amps > avg)[1:].values) + df_data_i['rising_edge'] = rising_edge_i + first_edge, last_edge = df_data_i.loc[df_data_i.rising_edge].index[[0, -1]] + + # Set start and end of FFT measurements range to mean. + df_data_i.current_amps.values[[first_edge, last_edge]] = avg + + # Restrict FFT to measurements between first and last rising edge. + df_edges_i = df_data_i.loc[first_edge:last_edge - 1, ['time_s', + 'current_amps']] + + # Length of the signal. + N = len(df_edges_i) + + # Compute frequencies based on sampling frequency. + frequencies = np.fft.fftfreq(N, d=1. / sample_frequency_hz)[:N / 2] + + # FFT computing and normalization. + Y = np.fft.fft(df_edges_i.current_amps.values) / N + Y = abs(Y[:N / 2]) + + # Create data frame from frequency and amplitude arrays. + df_fft_i = pd.DataFrame(np.column_stack([frequencies, Y]), + columns=['frequency', 'amplitude']) + return df_fft_i + + +def dstat_to_frame(data_path_i): + ''' + Convert DStat text file results to `pandas.DataFrame`. + + Args + ---- + + data_path_i (str) : Path to DStat results text file. + + Returns + ------- + + (pandas.DataFrame) : DStat measurements in a table with the column + `name` and `current_amps`, indexed by `utc_timestamp` and `time_s`. + ''' + with data_path_i.open('r') as input_i: + diff = (dt.datetime.utcnow() - dt.datetime.now()) + utc_timestamp = arrow.get(input_i.readline().split(' ')[-1]) + diff + + str_data = StringIO.StringIO('\n'.join(l for l in data_path_i.lines() + if not l.startswith('#'))) + df_data = pd.read_csv(str_data, sep='\s+', header=None) + df_data.rename(columns={0: 'time_s', 1: 'current_amps'}, inplace=True) + df_data.insert(0, 'name', re.sub(r'-data$', '', data_path_i.namebase)) + df_data.insert(0, 'utc_timestamp', utc_timestamp.datetime + + df_data.index.map(lambda v: dt.timedelta(seconds=v))) + df_data.set_index(['utc_timestamp', 'time_s'], inplace=True) + return df_data + + +def reduce_dstat_data(df_dstat, groupby, settling_period_s=2., bandwidth=1., + summary_fields=[]): + ''' + Reduce measurements for each DStat experiment in `df_dstat` to a single + row with an aggregate signal value. + + For continuous detection, the aggregate signal column corresponds to the + mean `current_amps`. + + For synchronous detection experiments (i.e., where `target_hz` is greater + than 0), the aggregate signal corresponds to the integrated amplitude of + the `current_amps` FFT within the bandwidth around target frequency. + + Args + ---- + + df_md_dstat (pandas.DataFrame) : DStat measurements table at least + containing the columns `current_amps`, and `time_s`. For + synchronous detection experiments the table must also include the + columns `sample_frequency_hz`, `target_hz`. + groupby (object, list) : Column(s) that identify distinct DStat + experiments. Each row of the output summary table directly + corresponds to each distinct combination of the `groupby` + column(s), where the first column(s) in the output table correspond + to the respective `groupby` column(s). + settling_period_s (float) : Measurement settling period in seconds. + Measurements taken before start time will not be included in + calculations. + bandwidth (float) : Bandwidth (centered at synchronous detection + frequency) to integrate within. + summary_fields (list) : List of columns to extract first value in each + group to include in each row of the output summary table. + + Returns + ------- + + (pd.DataFrame) : Table containing the `groupby` column(s), the + `summary_fields` columns, and the column `signal` (i.e., the + aggregate signal value). + ''' + rows = [] + + for index_i, df_i in df_dstat.groupby(groupby): + # Get `pandas.Series` containing summary metadata fields (not including signal). + summary_i = df_i.iloc[0][summary_fields].copy() + summary_i.name = index_i + + if 'target_hz' in summary_i and summary_i['target_hz'] > 0: + # Synchronous detection (e.g., shuttered). + # + # Use FFT to integrate signal at bandwidth surrounding target synchonization + # frequency. + df_fft_i = dstat_to_fft_frame(df_i, sample_frequency_hz= + summary_i['sample_frequency_hz'], + settling_period_s=settling_period_s) + summary_i['signal'] = integrate_fft(df_fft_i, + summary_i['target_hz'], + bandwidth) + else: + # Continuous detection. + # + # Take mean measurement value (after settling period). + summary_i['signal'] = (df_i.loc[df_i.time_s > settling_period_s] + .current_amps.mean()) + + rows.append(list(index_i) + summary_i.tolist()) + + return pd.DataFrame(rows, columns=groupby + summary_i.keys().tolist()) -- GitLab From 172c085674ebc83f27ee944a8950bb033b3557ad Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 14:08:08 +0300 Subject: [PATCH 03/16] Add plot_dstat_data function --- dstat_interface/plot.py | 85 +++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 7c5c017..8fa5acb 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -1,27 +1,30 @@ #!/usr/bin/env python # DStat Interface - An interface for the open hardware DStat potentiostat -# Copyright (C) 2014 Michael D. M. Dryden - +# Copyright (C) 2014 Michael D. M. Dryden - # Wheeler Microfluidics Laboratory -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Creates data plot. """ -import gtk from matplotlib.figure import Figure +import gtk +import matplotlib as mpl +import matplotlib.pyplot as plt +import si_prefix as si #from matplotlib.backends.backend_gtkcairo\ # import FigureCanvasGTKCairo as FigureCanvas @@ -35,14 +38,21 @@ try: import seaborn as sns except ImportError: pass - + from numpy import sin, linspace, pi, mean, trapz from scipy import fft, arange +from .analysis import dstat_to_fft_frame + + +# Format float values as string w.r.t. amps, e.g., `A`, `mA`, `uA`, etc. +A_formatter = mpl.ticker.FuncFormatter(lambda x, pos: + '{}A'.format(si.si_format(x))) + def plotSpectrum(y,Fs): """ Plots a Single-Sided Amplitude Spectrum of y(t) - """ + """ y = y-mean(y) n = len(y) # length of the signal k = arange(n) @@ -51,7 +61,7 @@ def plotSpectrum(y,Fs): frq = frq[range(n/2)] # one side frequency range Y = fft(y)/n # fft computing and normalization Y = abs(Y[range(n/2)]) - + return (frq, Y) def integrateSpectrum(x, y, target, bandwidth): @@ -198,5 +208,58 @@ class FT_Box(PlotBox): Experiment.plots['ft'] = self self.figure.canvas.draw() - - + + +def plot_dstat_data(df_data, settling_period_s=2., axes=None, label=None): + ''' + Plot DStat experiment current measurement results. + + Args + ---- + + df_data (pandas.DataFrame) : DStat experiment results with at + least the columns `time_s`, and `current_amps`. For synchronous + detection experiments the table must also include the columns + `sample_frequency_hz`, `target_hz`. + axes (list) : List of at least two `matplotlib` axes to plot to. The + first axis is used to plot the `current_amp` values. The second + axis is used to plot the FFT for experiments using synchronous + detection. If `None`, axes are automatically created. + + Returns + ------- + + (list) : List of two `matplotlib` axes use for current amps and FFT + plots, respectively. + ''' + if axes is None: + fig, axes = plt.subplots(2, figsize=(12, 8)) + + # Get style properties to use for plot `i`. + plot_props = axes[0]._get_lines.prop_cycler.next() + # Plot measured DStat current at each time point. + df_data.set_index('time_s').current_amps.plot(ax=axes[0], label=label, + **plot_props) + + # Compute median measured current. The median helps to eliminate outliers. + median = df_data.current_amps.median() + # Plot median as a straight line. + axes[0].plot(axes[0].get_xlim(), 2 * [median], linewidth=2, **plot_props) + + # # Annotate the median with the name of the sample. + # axes[0].text(axes[0].get_xlim()[0], median, label, fontsize=14, + # color='black') + + # Format y-axes[0] tick labels to be like `1.0nA`, `3.7mA`, etc. + axes[0].yaxis.set_major_formatter(A_formatter) + + if 'sample_frequency_hz' in df_data and (df_data.iloc[0] + ['sample_frequency_hz'] > 0): + # Synchronous detection (e.g., shuttered). + # + # Compute FFT of signal measured at specified sampling frequency. + df_fft = dstat_to_fft_frame(df_data, sample_frequency_hz= + df_data.iloc[0]['sample_frequency_hz'], + settling_period_s=settling_period_s) + df_fft.plot(x='frequency', y='amplitude', ax=axes[1], label=label) + return axes -- GitLab From 14de7cb19a2e025757a8c17d09078f9bf017a375 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 14:08:48 +0300 Subject: [PATCH 04/16] Format plot y-axis labels using SI prefixes --- dstat_interface/plot.py | 44 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 8fa5acb..40a69ec 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -70,52 +70,52 @@ def integrateSpectrum(x, y, target, bandwidth): """ j = 0 k = len(x) - + for i in range(len(x)): if x[i] >= target-bandwidth/2: j = i break - + for i in range(j,len(x)): if x[i] >= target+bandwidth/2: k = i break - + return trapz(y=y[j:k], x=x[j:k]) - + def findBounds(y): start_index = 0; stop_index = len(y)-1; - + for i in range(len(y)): if (y[i] <= mean(y) and y[i+1] > mean(y)): start_index = i break - + for i in range(len(y)): if (y[-(i+1)] <= mean(y) and y[-i] > mean(y)): stop_index = len(y)-1-i # len(y) is last index + 1 break - + return (start_index, stop_index) - - + + class PlotBox(object): """Contains main data plot and associated methods.""" def __init__(self, plotwindow_instance): """Creates plot and moves it to a gtk container. - + Arguments: plotwindow_instance -- gtk container to hold plot. """ - + self.figure = Figure() self.figure.subplots_adjust(left=0.07, bottom=0.07, right=0.96, top=0.96) self.axe1 = self.figure.add_subplot(111) - + self.axe1.plot([0, 1], [0, 1]) - + self.axe1.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') @@ -127,22 +127,22 @@ class PlotBox(object): self.toolbar = NavigationToolbar(self.canvas, self.win) self.vbox.pack_start(self.toolbar, False, False) self.vbox.reparent(plotwindow_instance) - + def clearall(self): """Remove all lines on plot. """ for i in range(len(self.axe1.lines)): self.axe1.lines.pop(0) self.addline() - + def clearline(self, line_number): """Remove a line specified by line_number.""" self.lines[line_number].remove() self.lines.pop(line_number) - + def addline(self): """Add a new line to plot. (initialized with dummy data)))""" self.axe1.plot([0, 1], [0, 1]) - + def updateline(self, Experiment, line_number): """Update a line specified by line_number with data stored in the Experiment instance. @@ -154,6 +154,8 @@ class PlotBox(object): Experiment.data['data'][line_number][1][1::divisor]) self.axe1.lines[line_number].set_xdata( Experiment.data['data'][line_number][0][1::divisor]) + # Format y-axis tick labels to be like `1.0nA`, `3.7mA`, etc. + self.axe1.yaxis.set_major_formatter(A_formatter) def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored @@ -162,7 +164,7 @@ class PlotBox(object): self.axe1.set_xlabel(Experiment.xlabel) self.axe1.set_ylabel(Experiment.ylabel) self.axe1.set_xlim(Experiment.xmin, Experiment.xmax) - + Experiment.plots['data'] = self self.figure.canvas.draw() @@ -181,7 +183,7 @@ class FT_Box(PlotBox): for i in range(len(data)): if data[i] > target: return i - + y = Experiment.data['data'][line_number][1] x = Experiment.data['data'][line_number][0] freq = Experiment.parameters['adc_rate_hz'] @@ -196,7 +198,7 @@ class FT_Box(PlotBox): self.axe1.lines[line_number].set_ydata(Y) self.axe1.lines[line_number].set_xdata(f) Experiment.data['ft'] = [(f, Y)] - + def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored in the Experiment instance. Stores class instance in Experiment. @@ -204,7 +206,7 @@ class FT_Box(PlotBox): self.axe1.set_xlabel("Freq (Hz)") self.axe1.set_ylabel("|Y| (A/Hz)") self.axe1.set_xlim(0, Experiment.parameters['adc_rate_hz']/2) - + Experiment.plots['ft'] = self self.figure.canvas.draw() -- GitLab From 7aa2b936ff986643791cd9683d09bbf953321618 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 14:16:21 +0300 Subject: [PATCH 05/16] Require pandas and si-prefix --- pavement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pavement.py b/pavement.py index bf1f907..34a6550 100644 --- a/pavement.py +++ b/pavement.py @@ -16,10 +16,10 @@ setup(name='dstat_interface', author_email='mdryden@chemutoronto.ca', url='http://microfluidics.utoronto.ca/dstat', license='GPLv3', - packages=['dstat_interface', ], - install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', - 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2', 'zodb', - 'zeo', 'psutil'], + packages=['dstat_interface'], + install_requires=['matplotlib', 'numpy', 'pandas', 'psutil', 'pyserial', + 'pyyaml', 'pyzmq', 'seaborn', 'si-prefix', 'zeo', + 'zmq-plugin>=0.2.post2', 'zodb'], # Install data listed in `MANIFEST.in` include_package_data=True) -- GitLab From 5b6bc8ef4b8f2ebfab9e15b046ed5cf8e9e55470 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 16:56:46 +0300 Subject: [PATCH 06/16] Fix relative import exception --- dstat_interface/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 40a69ec..51e8b94 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -41,7 +41,7 @@ except ImportError: from numpy import sin, linspace, pi, mean, trapz from scipy import fft, arange -from .analysis import dstat_to_fft_frame +from dstat_interface.analysis import dstat_to_fft_frame # Format float values as string w.r.t. amps, e.g., `A`, `mA`, `uA`, etc. -- GitLab From bfbd186537e72981f6ed699ca2c05b4291213c18 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 19 May 2016 17:27:57 +0300 Subject: [PATCH 07/16] Do not SI format y-axis labels - freezes Microdrop 0MQ API --- dstat_interface/plot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 51e8b94..8946394 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -155,7 +155,6 @@ class PlotBox(object): self.axe1.lines[line_number].set_xdata( Experiment.data['data'][line_number][0][1::divisor]) # Format y-axis tick labels to be like `1.0nA`, `3.7mA`, etc. - self.axe1.yaxis.set_major_formatter(A_formatter) def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored -- GitLab From 3e205b6dcdfa7b7a168fe7a5bd824909cdb7cc25 Mon Sep 17 00:00:00 2001 From: Ryan Fobel Date: Thu, 19 May 2016 22:04:54 +0300 Subject: [PATCH 08/16] Fix missing imports --- .gitignore | 1 + dstat_interface/analysis.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 868139a..84addf7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ setup.py *.egg-info last_params last_params.yml +.idea diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 2286cb4..bdaccec 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -21,11 +21,15 @@ Functions for analyzing data. """ import logging +import datetime as dt +import cStringIO as StringIO +import re import pygtk import gtk import numpy as np import pandas as pd +import arrow logger = logging.getLogger('dstat.analysis') -- GitLab From edb21a21e37893af0a6dc6044ab17a3dd377dfe9 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Sat, 21 May 2016 08:26:21 +0300 Subject: [PATCH 09/16] Wait until data saved before marking complete --- dstat_interface/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d95fd0f..d648b2a 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -880,11 +880,11 @@ class Main(object): self.stopbutton.set_sensitive(False) self.start_ocp() - self.completed_experiment_ids[self.active_experiment_id] =\ - datetime.utcnow() # Save current measurements for experiment in `pandas.DataFrame`. self.completed_experiment_data[self.active_experiment_id] =\ dstat_data_to_frame(self.current_exp.data['data']) + self.completed_experiment_ids[self.active_experiment_id] =\ + datetime.utcnow() def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" -- GitLab From 7997ff42e5b748be9efd443224305793e77eaf4a Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Fri, 26 Aug 2016 17:00:41 -0400 Subject: [PATCH 10/16] [FIX] Support all exp types in dstat_data_to_frame Add support for all experiment types in `dstat_data_to_frame` function. --- dstat_interface/analysis.py | 20 ++++-- dstat_interface/main.py | 127 +++++++++++++++++++++++++++--------- dstat_interface/plot.py | 4 +- 3 files changed, 111 insertions(+), 40 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index bdaccec..3a94c81 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -282,18 +282,24 @@ def dstat_to_fft_frame(df_data, sample_frequency_hz=60., settling_period_s=2.): def dstat_to_frame(data_path_i): ''' - Convert DStat text file results to `pandas.DataFrame`. + Convert DStat text file results to ``pandas.DataFrame``. - Args - ---- - - data_path_i (str) : Path to DStat results text file. + Parameters + ---------- + data_path_i : str + Path to DStat results text file. Returns ------- + pandas.DataFrame + DStat measurements in a table with the column ``name`` and + ``current_amps``, indexed by ``utc_timestamp`` and ``time_s``. + + Notes + ----- - (pandas.DataFrame) : DStat measurements in a table with the column - `name` and `current_amps`, indexed by `utc_timestamp` and `time_s`. + **TODO** Currently only works for experiments with current as the only + measurement column (i.e., chronoamperometry, photodiode). ''' with data_path_i.open('r') as input_i: diff = (dt.datetime.utcnow() - dt.datetime.now()) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d648b2a..1ad8aac 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -17,8 +17,27 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +'''GUI Interface for Wheeler Lab DStat -""" GUI Interface for Wheeler Lab DStat """ +Attributes +---------- +EXPERIMENT_TYPES : pandas.Series + Enum-like type defining experiment type codes:: + + Index Experiment type Value + + CA Chronoamperometry 0 + LSV Linear Sweep Voltammetry 1 + CV Cyclic Voltammetry 2 + SWV Square Wave Voltammetry 3 + DPV Differential Pulse Voltammetry 4 + PD Photodiode 5 + POT Potentiometry 6 + +COLUMN_MAPPING : OrderedDict + Mapping of :data:`EXPERIMENT_TYPES` code values to list of corresponding + measurement column names. +''' import sys import os @@ -79,32 +98,75 @@ root_logger.addHandler(log_handler) logger = logging.getLogger("dstat.main") - -def dstat_data_to_frame(data): +EXPERIMENT_TYPES = pd.Series(0, index=['CA', 'LSV', 'CV', 'SWV', 'DPV', 'PD', + 'POT']) +EXPERIMENT_TYPES[:] = range(EXPERIMENT_TYPES.size) + +EXPERIMENT_TYPE_NAMES = EXPERIMENT_TYPES.copy() +EXPERIMENT_TYPE_NAMES[:] = ['Chronoamperometry', + 'Linear Sweep Voltammetry', + 'Cyclic Voltammetry', + 'Square Wave Voltammetry', + 'Differential Pulse Voltammetry', + 'Photodiode', + 'Potentiometry'] + +COLUMN_MAPPING = OrderedDict([(EXPERIMENT_TYPES.CA, ['current_amps']), + (EXPERIMENT_TYPES.LSV, ['voltage_volts', + 'current_amps']), + (EXPERIMENT_TYPES.CV, ['voltage_volts', + 'current_amps']), + (EXPERIMENT_TYPES.SWV, ['voltage_volts', + 'current_amps', + 'forward_current_amps', + 'reverse_current_amps']), + (EXPERIMENT_TYPES.DPV, ['voltage_volts', + 'current_amps', + 'forward_current_amps', + 'reverse_current_amps']), + (EXPERIMENT_TYPES.PD, ['current_amps']), + (EXPERIMENT_TYPES.POT, ['voltage_volts'])]) + + +def dstat_data_to_frame(experiment_type, data): ''' - Convert DStat experiment data to `pandas.DataFrame`. - - Args - ---- - - data (list) : List of numpy array pairs (i.e., tuples). For each array - pair, each item in the zipped array has two values; the first value - is the *time in seconds* since the start of the DStat experiment, - the second value is the measured current in *amps*. + Convert DStat experiment data to ``pandas.DataFrame``. + + Parameters + ========== + experiment_type : int + Experiment type code (one of the values in :data:`EXPERIMENT_TYPES`). + data : list + List of numpy array pairs (i.e., tuples). For each array pair, each + item in the zipped array has two values; the first value is the *time + in seconds* since the start of the DStat experiment, the second value + is an array where each row of the array contains measurements + corresponding to the respective time point in the time array. The + columns in the second array depends on the test being run: + + - Linear Sweep Voltammetry/Cyclic Voltammetry: voltage, current + - Square Wave Voltammetry/Differential Pulse Voltammetry: + - voltage, current, forward current, reverse current + - Photodiode: current + - Potentiometry: voltage Returns - ------- - - (`pandas.DataFrame`) : Table containing two columns, `time_s` and - `current_amps`. + ======= + pandas.DataFrame + Table containing columns ``time_s``, ``scan_i``, and additional columns + matching experiment type (see :data:`COLUMN_MAPPING`). ''' + columns = ['time_s'] + COLUMN_MAPPING[experiment_type] if not data: - return pd.DataFrame(None, columns=['time_s', 'current_amps']) + return pd.DataFrame(None, columns=columns) else: - return (pd.concat([pd.DataFrame(np.column_stack(d), - columns=['time_s', 'current_amps']) - for d in data]) - .reset_index(drop=True)) + frames = [] + for i, scan_arrays_i in enumerate(data): + frame_i = pd.DataFrame(np.column_stack(scan_arrays_i), + columns=columns) + frame_i.insert(1, 'scan_i', i) + frames.append(frame_i) + return pd.concat(frames).reset_index(drop=True) class Main(object): @@ -175,7 +237,7 @@ class Main(object): self.serial_combobox.set_active(0) - #initialize experiment selection combobox + # Initialize experiment selection combobox self.expcombobox = self.builder.get_object('expcombobox') self.expcombobox.pack_start(self.cell, True) self.expcombobox.add_attribute(self.cell, 'text', 2) @@ -227,6 +289,8 @@ class Main(object): self.completed_experiment_ids = OrderedDict() # Data collected during completed experiments. self.completed_experiment_data = OrderedDict() + # Active experiment type code. + self.active_experiment_type = None def on_window1_destroy(self, object, data=None): """ Quit when main window closed.""" @@ -465,6 +529,7 @@ class Main(object): # Assign current experiment a unique identifier. experiment_id = uuid.uuid4() self.active_experiment_id = experiment_id + self.active_experiment_type = self.expcombobox.get_active() logger.info("Current measurement id: %s", experiment_id.hex) self.metadata = metadata @@ -528,7 +593,6 @@ class Main(object): while comm.serial_instance.data_pipe_p.poll(): # Clear data pipe comm.serial_instance.data_pipe_p.recv() - selection = self.expcombobox.get_active() parameters = {} parameters['version'] = self.version parameters['metadata'] = self.metadata @@ -551,7 +615,7 @@ class Main(object): self.stopbutton.set_sensitive(True) self.statusbar.remove_all(self.error_context_id) - if selection == 0: # CA + if self.active_experiment_type == EXPERIMENT_TYPES.CA: # Add experiment parameters to existing parameters.update(self.exp_window.get_params('cae')) if not parameters['potential']: @@ -570,7 +634,7 @@ class Main(object): return experiment_id - elif selection == 1: # LSV + elif self.active_experiment_type == EXPERIMENT_TYPES.LSV: parameter_test.lsv_test(parameters) self.current_exp = comm.LSVExp(parameters) @@ -578,7 +642,7 @@ class Main(object): return experiment_id - elif selection == 2: # CV + elif self.active_experiment_type == EXPERIMENT_TYPES.CV: parameter_test.cv_test(parameters) self.current_exp = comm.CVExp(parameters) @@ -586,7 +650,7 @@ class Main(object): return experiment_id - elif selection == 3: # SWV + elif self.active_experiment_type == EXPERIMENT_TYPES.SWV: parameter_test.swv_test(parameters) self.current_exp = comm.SWVExp(parameters) @@ -594,7 +658,7 @@ class Main(object): return experiment_id - elif selection == 4: # DPV + elif self.active_experiment_type == EXPERIMENT_TYPES.DPV: parameter_test.dpv_test(parameters) self.current_exp = comm.DPVExp(parameters) @@ -602,7 +666,7 @@ class Main(object): return experiment_id - elif selection == 6: # PD + elif self.active_experiment_type == EXPERIMENT_TYPES.PD: parameter_test.pd_test(parameters) self.current_exp = comm.PDExp(parameters) @@ -610,7 +674,7 @@ class Main(object): return experiment_id - elif selection == 7: # POT + elif self.active_experiment_type == EXPERIMENT_TYPES.POT: if not (self.version[0] >= 1 and self.version[1] >= 2): self.statusbar.push(self.error_context_id, "v1.1 board does not support potentiometry.") @@ -882,7 +946,8 @@ class Main(object): self.start_ocp() # Save current measurements for experiment in `pandas.DataFrame`. self.completed_experiment_data[self.active_experiment_id] =\ - dstat_data_to_frame(self.current_exp.data['data']) + dstat_data_to_frame(self.active_experiment_type, + self.current_exp.data['data']) self.completed_experiment_ids[self.active_experiment_id] =\ datetime.utcnow() diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 8946394..ea66ae0 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -39,9 +39,9 @@ try: except ImportError: pass -from numpy import sin, linspace, pi, mean, trapz +from numpy import mean, trapz from scipy import fft, arange -from dstat_interface.analysis import dstat_to_fft_frame +from analysis import dstat_to_fft_frame # Format float values as string w.r.t. amps, e.g., `A`, `mA`, `uA`, etc. -- GitLab From 51cee98b3ad863e412857086691f8b2ce62e3792 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 14:43:14 -0500 Subject: [PATCH 11/16] [NB] Add Conda build recipe --- .conda-recipe/bld.bat | 15 +++++++++++++ .conda-recipe/meta.yaml | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .conda-recipe/bld.bat create mode 100644 .conda-recipe/meta.yaml diff --git a/.conda-recipe/bld.bat b/.conda-recipe/bld.bat new file mode 100644 index 0000000..ba2bd5c --- /dev/null +++ b/.conda-recipe/bld.bat @@ -0,0 +1,15 @@ +REM Generate `setup.py` from `pavement.py` definition. +"%PYTHON%" -m paver generate_setup + +REM **Workaround** `conda build` runs a copy of `setup.py` named +REM `conda-build-script.py` with the recipe directory as the only argument. +REM This causes paver to fail, since the recipe directory is not a valid paver +REM task name. +REM +REM We can work around this by wrapping the original contents of `setup.py` in +REM an `if` block to only execute during package installation. +"%PYTHON%" -c "input_ = open('setup.py', 'r'); data = input_.read(); input_.close(); output_ = open('setup.py', 'w'); output_.write('\n'.join(['import sys', 'import path_helpers as ph', '''if ph.path(sys.argv[0]).name == 'conda-build-script.py':''', ' sys.argv.pop()', 'else:', '\n'.join([(' ' + d) for d in data.splitlines()])])); output_.close(); print open('setup.py', 'r').read()" + +REM Install source directory as Python package. +"%PYTHON%" -m pip install --no-cache --find-links http://192.99.4.95/wheels --trusted-host 192.99.4.95 . +if errorlevel 1 exit 1 diff --git a/.conda-recipe/meta.yaml b/.conda-recipe/meta.yaml new file mode 100644 index 0000000..f3f4ad4 --- /dev/null +++ b/.conda-recipe/meta.yaml @@ -0,0 +1,50 @@ +# source will be downloaded prior to filling in jinja templates +# Example assumes that this folder has setup.py in it +source: + git_url: ../ + +package: + name: dstat-interface +{% if GIT_DESCRIBE_NUMBER > '0' %} + version: {{ GIT_DESCRIBE_TAG[1:] }}.post{{ GIT_DESCRIBE_NUMBER }} +{% else %} + version: {{ GIT_DESCRIBE_TAG[1:] }} +{% endif %} + +build: + entry_points: + - dstat-interface = dstat_interface.main + + # If this is a new build for the same version, increment the build + # number. If you do not include this key, it defaults to 0. + number: 0 + +requirements: + build: + - python + - matplotlib + - pandas + - psutil + - pycairo-gtk2 + - pyserial + - pyyaml + - pyzmq + - seaborn + - si-prefix + - zeo + - zmq-plugin + - zodb + + run: + - matplotlib + - pandas + - psutil + - pycairo-gtk2 + - pyserial + - pyyaml + - pyzmq + - seaborn + - si-prefix + - zeo + - zmq-plugin + - zodb -- GitLab From 50c4e23ef849ca4cb07b2ce806661ce813b1a6ff Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 14:46:06 -0500 Subject: [PATCH 12/16] [FIX] Add paver as build requirement --- .conda-recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.conda-recipe/meta.yaml b/.conda-recipe/meta.yaml index f3f4ad4..fc54474 100644 --- a/.conda-recipe/meta.yaml +++ b/.conda-recipe/meta.yaml @@ -22,6 +22,7 @@ build: requirements: build: - python + - paver - matplotlib - pandas - psutil -- GitLab From 9ae84d54b479e2b1105d3c03feb01e68da8ff4d9 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 14:48:25 -0500 Subject: [PATCH 13/16] [FIX] Add path_helpers as build requirement --- .conda-recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.conda-recipe/meta.yaml b/.conda-recipe/meta.yaml index fc54474..5ef0bd4 100644 --- a/.conda-recipe/meta.yaml +++ b/.conda-recipe/meta.yaml @@ -23,6 +23,7 @@ requirements: build: - python - paver + - path_helpers - matplotlib - pandas - psutil -- GitLab From e894757330880d45f05aab9c4dce52cf914e279f Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 14:51:08 -0500 Subject: [PATCH 14/16] [FIX] Fix entry point --- .conda-recipe/meta.yaml | 2 +- dstat_interface/main.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.conda-recipe/meta.yaml b/.conda-recipe/meta.yaml index 5ef0bd4..4bb00c6 100644 --- a/.conda-recipe/meta.yaml +++ b/.conda-recipe/meta.yaml @@ -13,7 +13,7 @@ package: build: entry_points: - - dstat-interface = dstat_interface.main + - dstat-interface = dstat_interface.main:main # If this is a new build for the same version, increment the build # number. If you do not include this key, it defaults to 0. diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 1ad8aac..14b6e97 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -1031,8 +1031,12 @@ class Main(object): self.plugin = None -if __name__ == "__main__": +def main(): multiprocessing.freeze_support() gobject.threads_init() MAIN = Main() gtk.main() + + +if __name__ == "__main__": + main() -- GitLab From 922ae1250ca51488ce8e4077cd9e4ea3f84820b6 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 14:57:03 -0500 Subject: [PATCH 15/16] Add package directory to path --- dstat_interface/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 14b6e97..21fb96f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -1032,6 +1032,11 @@ class Main(object): def main(): + import sys + + parent_dir = os.path.dirname(__file__) + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) multiprocessing.freeze_support() gobject.threads_init() MAIN = Main() -- GitLab From 4ee87a9d2d62cd9b00268611f9377235b50e5ae4 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Mon, 7 Nov 2016 15:04:32 -0500 Subject: [PATCH 16/16] Change directory into package parent directory Change directory into package parent directory to support loading glade UI files using relative paths. --- dstat_interface/main.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 21fb96f..62ceaf1 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -65,7 +65,16 @@ except ImportError: sys.exit(1) from serial import SerialException import logging -os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) +# Add package directory to Python path. +# +# This is required for relative imports, which are required for running under a +# `multiprocessing` process. +parent_dir = os.path.abspath(os.path.dirname(__file__)) +if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) +# Change directory into package parent directory to support loading glade UI +# files using relative paths. +os.chdir(parent_dir) from version import getVersion import interface.save as save @@ -1032,11 +1041,6 @@ class Main(object): def main(): - import sys - - parent_dir = os.path.dirname(__file__) - if parent_dir not in sys.path: - sys.path.insert(0, parent_dir) multiprocessing.freeze_support() gobject.threads_init() MAIN = Main() -- GitLab