Source code for pkpd.viewers.tk_experiment

# **************************************************************************
# *
# * Authors:     J.M. De la Rosa Trevin (delarosatrevin@gmail.com)
# *              Carlos Oscar Sorzano (info@kinestat.com)
# *
# * Kinestat Pharma
# *
# * 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 2 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, write to the Free Software
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
# * 02111-1307  USA
# *
# *  All comments concerning this program package may be sent to the
# *  e-mail address 'info@kinestat.com'
# *
# **************************************************************************

import math
import numpy as np
try:
    from itertools import izip
except ImportError:
    izip = zip

import tkinter as tk
import tkinter.ttk as ttk

from scipy.interpolate import InterpolatedUnivariateSpline

import pyworkflow.object as pwobj
from pkpd.utils import uniqueFloatValues
import pyworkflow.gui as gui
from pyworkflow.gui.widgets import Button, HotButton, ComboBox
from pyworkflow.gui.text import TaggedText
from pyworkflow.gui.tree import TreeProvider, BoundTree
from pwem.viewers.plotter import EmPlotter


[docs]class VariablesTreeProvider(TreeProvider): def __init__(self, experiment): self.experiment = experiment
[docs] def getColumns(self): return [('Name', 60), ('Unit', 60), ('Format', 60), ('Type', 60), ('Role', 60), ('Comment', 100)]
[docs] def getObjects(self): sortedVars = [] for key in sorted(self.experiment.variables.keys()): sortedVars.append(self.experiment.variables[key]) return sortedVars
[docs] def getObjectInfo(self, obj): key = obj.varName return {'key': key, 'text': key, 'values': (obj.getUnitsString(), obj.displayString, obj.getTypeString(), obj.getRoleString(), obj.comment)}
[docs]class DosesTreeProvider(TreeProvider): def __init__(self, experiment): self.experiment = experiment
[docs] def getColumns(self): return [('Name', 60), ('Via',40), ('Dose', 100), ('Amount', 80)]
[docs] def getObjects(self): sortedDoses = [] for key in sorted(self.experiment.doses.keys()): sortedDoses.append(self.experiment.doses[key]) return sortedDoses
[docs] def getObjectInfo(self, obj): key = obj.doseName return {'key': key, 'text': key, 'values': (obj.via.viaName, obj.getDoseString(), obj.doseAmount+" "+obj.getDUnitsString() ) }
[docs]class GroupsTreeProvider(TreeProvider): def __init__(self, experiment): self.experiment = experiment
[docs] def getColumns(self): return [('Name', 60), ('Samples', 60)]
[docs] def getObjects(self): sortedGroups = [] for key in sorted(self.experiment.groups.keys()): sortedGroups.append(self.experiment.groups[key]) return sortedGroups
[docs] def getObjectInfo(self, obj): key = obj.groupName return {'key': key, 'text': key, 'values': (obj.getSamplesString() ) }
[docs]class ViasTreeProvider(TreeProvider): def __init__(self, experiment): self.experiment = experiment
[docs] def getColumns(self): return [('Name', 60), ('Tlag', 60), ('Bioavailability',60)]
[docs] def getObjects(self): sortedVias = [] for key in sorted(self.experiment.vias.keys()): sortedVias.append(self.experiment.vias[key]) return sortedVias
[docs] def getObjectInfo(self, obj): key = obj.viaName if 'tlag' in obj.paramsToOptimize: tlagStr="To be optimized" else: tlagStr=str(obj.tlag)+" "+obj.tunits._toString() if 'bioavailability' in obj.paramsToOptimize: bioavailabilityStr="To be optimized" else: bioavailabilityStr=str(obj.bioavailability) return {'key': key, 'text': key, 'values': (tlagStr, bioavailabilityStr ) }
[docs]class SamplesTreeProvider(TreeProvider): FIT_COLUMNS = ['R2', 'R2adj', 'AIC', 'AICc', 'BIC'] def __init__(self, experiment, fitting=None): self.experiment = experiment self.fitting = fitting numberOfSamples = len(experiment.samples) if numberOfSamples > 0: sample = list(self.experiment.samples.values())[0] if sample.descriptors: self.columns = [(key, 60) for key in sorted(sample.descriptors.keys())] else: self.columns = [] if self.fitting: self.columns.extend([(key, 60) for key in self.FIT_COLUMNS]) else: self.columns = []
[docs] def getColumns(self): return [('Name', 60), ('Dose', 60),('Groups',60)] + self.columns
[docs] def getObjects(self): sortedSamples = [] for key in sorted(self.experiment.samples.keys()): sample = self.experiment.samples[key] if self.fitting: sample.fit = self.fitting.getSampleFit(key) sortedSamples.append(sample) return sortedSamples
[docs] def getObjectInfo(self, obj): key = obj.sampleName values = [','.join(obj.doseList)] values +=[','.join(obj.groupList)] if obj.descriptors: values += [obj.descriptors[k] for k, _ in self.columns if k in obj.descriptors] if self.fitting: if obj.fit!=None: values.extend([getattr(obj.fit, attr) for attr in self.FIT_COLUMNS]) return {'key': key, 'text': key, 'values': tuple(values) }
def _sortEnabled(self): return None
[docs]class MeasurementTreeProvider(TreeProvider): def __init__(self, sample): self.sample = sample self.columns = [(key, 60) for key in self.sample.measurementPattern]
[docs] def getColumns(self): return [('Sample #',60)]+self.columns
[docs] def getObjects(self): return self.sample.getSampleMeasurements()
[docs] def getObjectInfo(self, obj): key = obj.n return {'key': key, 'text': key, 'values': tuple(obj.getValues()) }
[docs]class FitValuesTreeProvider(TreeProvider): def __init__(self, fitting, sampleFit): self.sampleFit = sampleFit if type(fitting.predicted)==list: self.columns = [('n',30), (fitting.predictor.getLabel(), 60)]+[(y.getLabel(),60) for y in fitting.predicted]+\ [("Fitted Value", 60)] else: self.columns = [('n',30), (fitting.predictor.getLabel(), 60), (fitting.predicted.getLabel(), 60), ("Fitted Value", 60)]
[docs] def getColumns(self): return self.columns
[docs] def getObjects(self): fit = self.sampleFit def _object(n, x, y, yp): obj = pwobj.Object() obj.n = n obj.x = x obj.y = y obj.yp = yp return obj if type(fit.y[0])==list: retval = [] Ncols=len(fit.y) n=0 i=0 for xi, yi, ypi in izip(fit.x,fit.y,fit.yp): for x,y,yp in izip(xi,yi,ypi): yList=[] for ii in range(Ncols): if ii==i: yList.append(y) else: yList.append("NA") retval.append(_object(n, x, yList, yp)) n+=1 i+=1 return retval else: nrange=[x for x in range(len(fit.x))] return [_object(n, x, y, yp) for n, x, y, yp in izip(nrange, fit.x, fit.y, fit.yp)]
[docs] def getObjectInfo(self, obj): key = obj.n if type(obj.y)==list: return {'key': key, 'text': key, 'values': tuple([obj.x]+obj.y+[obj.yp]) } else: return {'key': key, 'text': key, 'values': (obj.x, obj.y, obj.yp) }
[docs]class ExperimentWindow(gui.Window): """ This class creates a Window that will display some Point's contained in a Data object. It will allow to launch 1D, 2D and 3D plots by selecting any combination of the x1, x2...xn from the Point dimension. Points can be selected by either Click and Drag in the Scatter plot or.. by creating an Expression. Finally, there is a button 'Create Cluster' that will call a callback fuction to take care of it. """ def __init__(self, **kwargs): gui.Window.__init__(self, minsize=(420, 200), **kwargs) self.experiment = kwargs.get('experiment') self.fitting = kwargs.get('fitting', None) self.callback = kwargs.get('callback', None) content = tk.Frame(self.root) self._createContent(content) content.grid(row=0, column=0, sticky='news') content.columnconfigure(0, weight=1) content.rowconfigure(0, weight=1) self.plotter = None def _createContent(self, content): # Create and fill the frame containing the Experiment # info, variables and doses p = tk.PanedWindow(content, orient=tk.VERTICAL, bg='white') p.grid(row=0, column=0, sticky='news', padx=5, pady=5) self._createTopFrame(p) # Create the middle frame containing the Samples Box self._createSamplesFrame(p) # Create the last frame with the buttons self._createButtonsFrame(content) #self._updateSelectionLabel() def _createTopFrame(self, content): frame = tk.Frame(content) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) tab = ttk.Notebook(frame) tab.grid(row=0, column=0, sticky='news', padx=5, pady=5) def addTab(label): lf = tk.Frame(tab) tab.add(lf, text=label) return lf lfGeneral = addTab('General') self._addLabel(lfGeneral, 'Title', 0, 0) self._titleVar = tk.StringVar() if 'title' in self.experiment.general: self._titleVar.set(self.experiment.general['title']) else: self._titleVar.set("") titleEntry = tk.Entry(lfGeneral, width=26, textvariable=self._titleVar, bg='white') titleEntry.grid(row=0, column=1, sticky='nw', padx=5, pady=(5, 0)) self._addLabel(lfGeneral, 'Comment', 1, 0) commentText = gui.text.Text(lfGeneral, width=30, height=3, bg='white') if 'comment' in self.experiment.general: commentText.setText(self.experiment.general['comment']) else: commentText.setText("") commentText.grid(row=1, column=1, sticky='nw', padx=5, pady=(5, 0)) self._commentText = commentText lfVars = addTab('Variables') self.varsTree = self._addBoundTree(lfVars, VariablesTreeProvider, 5) lfVias = addTab('Vias') self.viasTree = self._addBoundTree(lfVias, ViasTreeProvider, 5) lfDoses = addTab('Doses') self.dosesTree = self._addBoundTree(lfDoses, DosesTreeProvider, 5) lfGroups = addTab('Groups') self.groupsTree = self._addBoundTree(lfGroups, GroupsTreeProvider, 5) # Fitting tab if self.fitting: tabFitting = addTab('Fitting') t = TaggedText(tabFitting, width=80, height=10, bg='white') t.grid(row=0, column=0, sticky='nw', padx=10, pady=10) t.setText('Select one of the samples to see more information.') t.setReadOnly(True) self.fittingText = t # Buttons for fitting buttonsFrame = self._createFittingButtonsFrame(tabFitting) buttonsFrame.grid(row=0, column=1, sticky='news', padx=5, pady=5) #frame.grid(row=0, column=0, sticky='news', padx=5, pady=(10, 5)) content.add(frame, sticky='news', padx=5, pady=5) def _createSamplesFrame(self, content): frame = tk.Frame(content) #frame = tk.LabelFrame(content, text='General') lfSamples = tk.LabelFrame(frame, text='Samples') gui.configureWeigths(frame) lfSamples.grid(row=0, column=0, sticky='news', padx=5, pady=5) self.samplesTree = self._addBoundTree(lfSamples, SamplesTreeProvider, 10, fitting=self.fitting) self.samplesTree.itemDoubleClick = self._onSampleDoubleClick self.samplesTree.itemClick = self._onSampleClick plotFrame = tk.Frame(lfSamples) plotFrame.grid(row=1, column=0, sticky='ws', padx=5, pady=5) # Add a combobox with the variable for time timeVars = [v.varName for v in self.experiment.variables.values() if v.role == v.ROLE_TIME] timeVars += [v.varName for v in self.experiment.variables.values() if v.role == v.ROLE_MEASUREMENT] measureVars = [v.varName for v in self.experiment.variables.values() if v.role == v.ROLE_MEASUREMENT] def addVar(text, col, choices): varFrame = tk.Frame(plotFrame) varFrame.grid(row=0, column=col, sticky='new') label = tk.Label(varFrame, text=text, font=self.fontBold) label.grid(row=0, column=0, padx=5, pady=2, sticky='nw') if len(choices)==0: choices=[''] combo = ComboBox(varFrame, choices, width=10) combo.grid(row=0, column=1, sticky='nw', padx=5, pady=5) radioVar = tk.IntVar() radio = tk.Checkbutton(varFrame, text='Log10', variable=radioVar) radio.grid(row=0, column=2, sticky='nw', padx=5, pady=5) return combo, radio, radioVar self.timeWidget = addVar('Time variable', 0, timeVars) self.measureWidget = addVar('Measure variable', 1, list(set(measureVars+timeVars))) self.measureWidget[2].set(True) self.plotButton = Button(plotFrame, ' Plot ', font=self.fontBold, command=self._onPlotClick, tooltip='Select one or more samples to plot ' 'their measures of the selected ' 'variables (optionally in log).') self.plotButton.grid(row=0, column=2, sticky='ne', padx=5) self.useCurrentPlotVar = tk.IntVar() self.useCurrentPlot = tk.Checkbutton(plotFrame, text='Use current plot', variable=self.useCurrentPlotVar) self.useCurrentPlot.grid(row=1, column=0, sticky='nw', padx=5, pady=5) self.plotSummaryButton = Button(plotFrame, ' Summary Plot ', font=self.fontBold, command=self._onPlotSummaryClick, tooltip='Select several samples to plot' ' their statistics' ' (min, max, avg and std).') self.plotSummaryButton.grid(row=1, column=2, sticky='ne', padx=5, pady=5) #frame.grid(row=1, column=0, sticky='news', padx=5, pady=5) content.add(frame, sticky='news', padx=5, pady=5) def _createButtonsFrame(self, content): frame = tk.Frame(content) gui.configureWeigths(frame) buttonsFrame = tk.Frame(frame) buttonsFrame.grid(row=0, column=0, sticky='ne') closeButton = Button(buttonsFrame, 'Close', command=self.close, imagePath='fa-times.png') closeButton.grid(row=0, column=0, sticky='ne', padx=5) self.newButton = HotButton(buttonsFrame, ' New Experiment ', command=self._onCreateClick, tooltip='Create a new experiment with the ' 'selected samples. You can also edit' 'title and comment.') self.newButton.grid(row=0, column=1, sticky='ne', padx=5) frame.grid(row=1, column=0, sticky='news', padx=5, pady=5) def _createFittingButtonsFrame(self, content): buttonsFrame = tk.Frame(content) buttonsFrame.grid(row=0, column=0, sticky='ne') plotValuesButton = Button(buttonsFrame, 'Plot Fit', command=self._onPlotFitClick) plotValuesButton.grid(row=0, column=0, sticky='sew', padx=5, pady=5) openValuesButton = Button(buttonsFrame, 'Open Fit', command=self._onOpenFitClick) openValuesButton.grid(row=1, column=0, sticky='sew', padx=5, pady=5) return buttonsFrame def _addLabel(self, parent, text, r, c): label = tk.Label(parent, text=text, font=self.fontBold) label.grid(row=r, column=c, padx=5, pady=5, sticky='ne') return label def _addBoundTree(self, parent, ProviderClass, height, **kwargs): bt = BoundTree(parent, ProviderClass(self.experiment, **kwargs), height=height) bt.grid(row=0, column=0, sticky='news', padx=5, pady=5) gui.configureWeigths(parent) return bt
[docs] def getUnits(self, varName): return self.experiment.variables[varName].getUnitsString()
[docs] def getLabel(self, varName, useLog): varLabel = '%s [%s]' % (varName, self.getUnits(varName)) if useLog: varLabel = "log10(%s)" % varLabel return varLabel
[docs] def getTimeVarName(self): return self.timeWidget[0].getText()
[docs] def useTimeLog(self): return self.timeWidget[2].get()
[docs] def getTimeLabel(self): return self.getLabel(self.getTimeVarName(), self.useTimeLog())
[docs] def getMeasureVarName(self): return self.measureWidget[0].getText()
[docs] def useMeasureLog(self): return self.measureWidget[2].get()
[docs] def getMeasureLabel(self): return self.getLabel(self.getMeasureVarName(), self.useMeasureLog())
[docs] def getPlotValues(self, sample): xValues, yValues = sample.getXYValues(self.getTimeVarName(), self.getMeasureVarName()) xValues=xValues[0] # From [array(...)] to array(...) yValues=yValues[0] useMeasureLog = self.useMeasureLog() useTimeLog = self.useTimeLog() if not (useMeasureLog or useTimeLog): return xValues, yValues # If log will be used either for time or measure var # we need to filter elements larger than 0 newXValues = [] newYValues = [] def _value(v, useLog): if useLog: return math.log10(v) if v > 0 else None return v for x, y in izip(xValues, yValues): x = _value(x, useTimeLog) y = _value(y, useMeasureLog) if x is not None and y is not None: newXValues.append(x) newYValues.append(y) return newXValues, newYValues
[docs] def reuseFigure(self): return None if not self.useCurrentPlotVar.get() else 'active'
def _onPlotClick(self, e=None): sampleKeys = self.samplesTree.selection() if sampleKeys: if self.plotter is None or self.plotter.isClosed(): self.plotter = EmPlotter(style='seaborn-whitegrid', figure=self.reuseFigure()) doShow = True ax = self.plotter.createSubPlot("Plot", self.getTimeLabel(), self.getMeasureLabel()) self.plotDict = {} else: doShow = False ax = self.plotter.getLastSubPlot() samples = [self.experiment.samples[k] for k in sampleKeys] for s in samples: if not s.sampleName in self.plotDict or not doShow: x, y = self.getPlotValues(s) label = s.sampleName if not doShow: label+=" "+self.getTimeVarName()+" vs "+self.getMeasureVarName() ax.plot(x, y, label=label) self.plotDict[s.sampleName] = True leg = ax.legend() if leg: leg.set_draggable(True) if doShow: self.plotter.show() else: self.plotter.draw() else: self.showInfo("Please select some sample(s) to plot.") def _onPlotSummaryClick(self, e=None): sampleKeys = self.samplesTree.selection() n = len(sampleKeys) if n == 1: self.showInfo("Please select several samples to plot.") else: if n > 1: samples = [self.experiment.samples[k] for k in sampleKeys] else: samples = list(self.experiment.samples.values()) xmin=1e38 xmax=-1e38 for s in samples: xValues, _ = self.getPlotValues(s) xmin = min(xmin, min(xValues)) xmax = max(xmax, max(xValues)) dataDict = {} # key will be time values xrange=np.arange(xmin,xmax,(xmax-xmin)/300.0) for s in samples: xValues, yValues = self.getPlotValues(s) xValuesUnique,yValuesUnique = uniqueFloatValues(xValues,yValues) B = InterpolatedUnivariateSpline(xValuesUnique,yValuesUnique, k=1) yrange = B(xrange) for x, y in izip(xrange, yrange): if x in dataDict: dataDict[x].append(y) else: dataDict[x] = [y] sortedTime = sorted(dataDict.keys()) # We will store five values (min, 25%, 50%, 75%, max) # for each of the time entries computed percentileList = [0, 25, 50, 75, 100] Y = np.zeros((len(sortedTime), 5)) for i, t in enumerate(sortedTime): Y[i,:] = np.percentile(dataDict[t], percentileList) plotter = EmPlotter(style='seaborn-whitegrid', figure=self.reuseFigure()) # *** Pending ax = plotter.createSubPlot("Summary Plot", self.getTimeLabel(), self.getMeasureLabel()) ax.plot(sortedTime, Y[:, 0], 'r--', label="Minimum") ax.plot(sortedTime, Y[:, 1], 'b--', label="25%") ax.plot(sortedTime, Y[:, 2], 'g', label="50% (Median)") ax.plot(sortedTime, Y[:, 3], 'b--', label="75%") ax.plot(sortedTime, Y[:, 4], 'r--', label="Maximum") ax.grid(True) ax.legend() plotter.show() def _onCreateClick(self, e=None): sampleKeys = self.samplesTree.selection() if sampleKeys and self.callback: self.callback() else: self.showInfo("Please select some sample(s) to create a new experiment.") def _onSampleClick(self, obj): # In fitting mode we need to display some information for # the given sample fit if self.fitting: selection = self.samplesTree.selection() if selection: sampleKey = selection[0] sampleFit = self.fitting.getSampleFit(sampleKey) textMsg = sampleFit.getBasicInfo() else: textMsg = 'Select one of the samples to see more information.' self.fittingText.setReadOnly(False) self.fittingText.setText(textMsg) self.fittingText.setReadOnly(True) if not (self.plotter is None or self.plotter.isClosed()): self._onPlotClick() def _onSampleDoubleClick(self, obj): sampleKeys = self.samplesTree.selection() if sampleKeys: MeasureWindow(masterWindow=self, sample=self.experiment.samples[sampleKeys[0]]).show() def _onPlotFitClick(self, e=None): sampleKeys = self.samplesTree.selection() if sampleKeys: def _value(v, useLog): if useLog: return math.log10(v) if v > 0 else None return v def _values(values, useLog=self.useMeasureLog()): return [_value(float(x), useLog) for x in values] def _plot(varLabelX, varLabelY, x, y, yp): plotter = EmPlotter(style='seaborn-whitegrid', figure=self.reuseFigure()) ax = plotter.createSubPlot("Plot", varLabelX, varLabelY) xValues = _values(x, useLog=self.useTimeLog()) ax.plot(xValues, _values(y), 'x', label="Observations") idx = np.argsort(xValues) ypValues = _values(yp) ax.plot(np.asarray(xValues)[idx], np.asarray(ypValues)[idx], 'g', label="Fit") leg = ax.legend() if leg: leg.set_draggable(True) plotter.show() # Get first selected element fit = self.fitting.getSampleFit(sampleKeys[0]) if fit==None: return varLabelX = self.getLabel(self.fitting.predictor.varName, self.useTimeLog()) if type(self.fitting.predicted)==list: i=0 for v in self.fitting.predicted: varLabelY = self.getLabel(v.varName, self.useMeasureLog()) _plot(varLabelX, varLabelY, fit.x[i], fit.y[i], fit.yp[i]) i+=1 else: varLabelY = self.getLabel(self.fitting.predicted.varName, self.useMeasureLog()) _plot(varLabelX, varLabelY, fit.x[0], fit.y[0], fit.yp[0]) else: self.showInfo("Please select some sample(s) to plot.") def _onOpenFitClick(self, e=None): sampleKeys = self.samplesTree.selection() if sampleKeys: # Get first selected element fit = self.fitting.getSampleFit(sampleKeys[0]) FitWindow(masterWindow=self, fitting=self.fitting, sampleFit=fit).show() else: self.showInfo("Please select some sample(s) to plot.") def _onClosing(self): if self.plotter: self.plotter.close() gui.Window._onClosing(self)
[docs]class MeasureWindow(gui.Window): def __init__(self, **kwargs): gui.Window.__init__(self, minsize=(420, 200), **kwargs) self.title = kwargs.get('title', 'Measurements') self.sample = kwargs.get('sample', None) self.provider = kwargs.get('provider', None) if not self.provider: self.provider = MeasurementTreeProvider(self.sample) content = tk.Frame(self.root) self._createContent(content) content.grid(row=0, column=0, sticky='news') content.columnconfigure(0, weight=1) def _createContent(self, content): self._createMeasurementFrame(content) self._createButtonsFrame(content) def _createMeasurementFrame(self, content): frame = tk.Frame(content) #frame = tk.LabelFrame(content, text='General') lfSamples = tk.LabelFrame(frame, text=self.title) gui.configureWeigths(frame) lfSamples.grid(row=0, column=0, sticky='news', padx=5, pady=5) self.measurementTree = self._addBoundTree(lfSamples, self.provider, 10) frame.grid(row=0, column=0, sticky='news', padx=5, pady=5) def _createButtonsFrame(self, content): frame = tk.Frame(content) gui.configureWeigths(frame) buttonsFrame = tk.Frame(frame) buttonsFrame.grid(row=0, column=0, sticky='ne') closeButton = Button(buttonsFrame, 'Close', command=self.close, imagePath='fa-times.png') frame.grid(row=2, column=0, sticky='news', padx=5, pady=5) def _addBoundTree(self, parent, provider, height): bt = BoundTree(parent, provider, height=height) bt.grid(row=0, column=0, sticky='news', padx=5, pady=5) gui.configureWeigths(parent) return bt
[docs]class FitWindow(MeasureWindow): def __init__(self, **kwargs): self.fitting = kwargs['fitting'] self.sampleFit = kwargs['sampleFit'] MeasureWindow.__init__(self, title="Fitted Values", provider=FitValuesTreeProvider(self.fitting, self.sampleFit), **kwargs)