hemingclamp = boolean(default = False)\r
jpk = boolean(default = False)\r
mcs = boolean(default = False)\r
+mfp1d = boolean(default = True)\r
mfp1dexport = boolean(default = True)\r
+mfp3d = boolean(default = True)\r
picoforce = boolean(default = True)\r
picoforcealt = boolean(default = False)\r
tutorialdriver = boolean(default = False)\r
\r
[plugins]\r
autopeak = boolean(default = True)\r
-curvetools = boolean(default = True)\r
export = boolean(default = True)\r
fit = boolean(default = True)\r
flatfilts = boolean(default = True)\r
hemingclamp = False\r
jpk = False\r
mcs = False\r
+mfp1d = True\r
+mfp3d = True\r
mfp1dexport = True\r
picoforce = True\r
picoforcealt = False\r
#this section defines which plugins have to be loaded by Hooke\r
[plugins]\r
autopeak = True\r
-curvetools = True\r
export = True\r
fit = True\r
flatfilts = True\r
--- /dev/null
+#!/usr/bin/env python
+
+'''
+mfp1d.py
+
+Driver for MFP-1D files.
+
+Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada)
+This driver is based on the work of R. Naud and A. Seeholzer (see below)
+to read Igor binary waves. Code used with permission.
+
+This program is released under the GNU General Public License version 2.
+'''
+
+# DEFINITION:
+# Reads Igor's (Wavemetric) binary wave format, .ibw, files.
+#
+# ALGORITHM:
+# Parsing proper to version 2, 3, or version 5 (see Technical notes TN003.ifn:
+# http://mirror.optus.net.au/pub/wavemetrics/IgorPro/Technical_Notes/) and data
+# type 2 or 4 (non complex, single or double precision vector, real values).
+#
+# AUTHORS:
+# Matlab version: R. Naud August 2008 (http://lcn.epfl.ch/~naud/Home.html)
+# Python port: A. Seeholzer October 2008
+#
+# VERSION: 0.1
+#
+# COMMENTS:
+# Only tested for version 2 Igor files for now, testing for 3 and 5 remains to be done.
+# More header data could be passed back if wished. For significance of ignored bytes see
+# the technical notes linked above.
+
+import numpy
+import os.path
+import struct
+
+import lib.driver
+import lib.curve
+import lib.plot
+
+__version__='0.0.0.20100225'
+
+class mfp1dDriver(lib.driver.Driver):
+
+ def __init__(self, filename):
+ '''
+ This is a driver to import Asylum Research MFP-1D data.
+ Status: experimental
+ '''
+ self.data = []
+ self.note = []
+ self.retract_velocity = None
+ self.spring_constant = None
+ self.filename = filename
+
+ self.filedata = open(filename,'rU')
+ self.lines = list(self.filedata.readlines())
+ self.filedata.close()
+
+ self.filetype = 'mfp1d'
+ self.experiment = 'smfs'
+
+ def _load_from_file(self, filename, extract_note=False):
+ data = None
+ f = open(filename, 'rb')
+ ####################### ORDERING
+ # machine format for IEEE floating point with big-endian
+ # byte ordering
+ # MacIgor use the Motorola big-endian 'b'
+ # WinIgor use Intel little-endian 'l'
+ # If the first byte in the file is non-zero, then the file is a WinIgor
+ firstbyte = struct.unpack('b', f.read(1))[0]
+ if firstbyte == 0:
+ format = '>'
+ else:
+ format = '<'
+ ####################### CHECK VERSION
+ f.seek(0)
+ version = struct.unpack(format+'h', f.read(2))[0]
+ ####################### READ DATA AND ACCOMPANYING INFO
+ if version == 2 or version == 3:
+ # pre header
+ wfmSize = struct.unpack(format+'i', f.read(4))[0] # The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.
+ noteSize = struct.unpack(format+'i', f.read(4))[0] # The size of the note text.
+ if version==3:
+ formulaSize = struct.unpack(format+'i', f.read(4))[0]
+ pictSize = struct.unpack(format+'i', f.read(4))[0] # Reserved. Write zero. Ignore on read.
+ checksum = struct.unpack(format+'H', f.read(2))[0] # Checksum over this header and the wave header.
+ # wave header
+ dtype = struct.unpack(format+'h', f.read(2))[0]
+ if dtype == 2:
+ dtype = numpy.float32(.0).dtype
+ elif dtype == 4:
+ dtype = numpy.double(.0).dtype
+ else:
+ assert False, "Wave is of type '%i', not supported" % dtype
+ dtype = dtype.newbyteorder(format)
+
+ ignore = f.read(4) # 1 uint32
+ bname = self._flatten(struct.unpack(format+'20c', f.read(20)))
+ ignore = f.read(4) # 2 int16
+ ignore = f.read(4) # 1 uint32
+ dUnits = self._flatten(struct.unpack(format+'4c', f.read(4)))
+ xUnits = self._flatten(struct.unpack(format+'4c', f.read(4)))
+ npnts = struct.unpack(format+'i', f.read(4))[0]
+ amod = struct.unpack(format+'h', f.read(2))[0]
+ dx = struct.unpack(format+'d', f.read(8))[0]
+ x0 = struct.unpack(format+'d', f.read(8))[0]
+ ignore = f.read(4) # 2 int16
+ fsValid = struct.unpack(format+'h', f.read(2))[0]
+ topFullScale = struct.unpack(format+'d', f.read(8))[0]
+ botFullScale = struct.unpack(format+'d', f.read(8))[0]
+ ignore = f.read(16) # 16 int8
+ modDate = struct.unpack(format+'I', f.read(4))[0]
+ ignore = f.read(4) # 1 uint32
+ # Numpy algorithm works a lot faster than struct.unpack
+ data = numpy.fromfile(f, dtype, npnts)
+
+ elif version == 5:
+ # pre header
+ checksum = struct.unpack(format+'H', f.read(2))[0] # Checksum over this header and the wave header.
+ wfmSize = struct.unpack(format+'i', f.read(4))[0] # The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.
+ formulaSize = struct.unpack(format+'i', f.read(4))[0]
+ noteSize = struct.unpack(format+'i', f.read(4))[0] # The size of the note text.
+ dataEUnitsSize = struct.unpack(format+'i', f.read(4))[0]
+ dimEUnitsSize = struct.unpack(format+'4i', f.read(16))
+ dimLabelsSize = struct.unpack(format+'4i', f.read(16))
+ sIndicesSize = struct.unpack(format+'i', f.read(4))[0]
+ optionSize1 = struct.unpack(format+'i', f.read(4))[0]
+ optionSize2 = struct.unpack(format+'i', f.read(4))[0]
+
+ # header
+ ignore = f.read(4)
+ CreationDate = struct.unpack(format+'I',f.read(4))[0]
+ modData = struct.unpack(format+'I',f.read(4))[0]
+ npnts = struct.unpack(format+'i',f.read(4))[0]
+ # wave header
+ dtype = struct.unpack(format+'h',f.read(2))[0]
+ if dtype == 2:
+ dtype = numpy.float32(.0).dtype
+ elif dtype == 4:
+ dtype = numpy.double(.0).dtype
+ else:
+ assert False, "Wave is of type '%i', not supported" % dtype
+ dtype = dtype.newbyteorder(format)
+
+ ignore = f.read(2) # 1 int16
+ ignore = f.read(6) # 6 schar, SCHAR = SIGNED CHAR? ignore = fread(fid,6,'schar'); #
+ ignore = f.read(2) # 1 int16
+ bname = self._flatten(struct.unpack(format+'32c',f.read(32)))
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(4) # 1 int32
+ ndims = struct.unpack(format+'4i',f.read(16)) # Number of of items in a dimension -- 0 means no data.
+ sfA = struct.unpack(format+'4d',f.read(32))
+ sfB = struct.unpack(format+'4d',f.read(32))
+ dUnits = self._flatten(struct.unpack(format+'4c',f.read(4)))
+ xUnits = self._flatten(struct.unpack(format+'16c',f.read(16)))
+ fsValid = struct.unpack(format+'h',f.read(2))
+ whpad3 = struct.unpack(format+'h',f.read(2))
+ ignore = f.read(16) # 2 double
+ ignore = f.read(40) # 10 int32
+ ignore = f.read(64) # 16 int32
+ ignore = f.read(6) # 3 int16
+ ignore = f.read(2) # 2 char
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(4) # 2 int16
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(8) # 2 int32
+
+ data = numpy.fromfile(f, dtype, npnts)
+ note_str = f.read(noteSize)
+ if extract_note:
+ note_lines = note_str.split('\r')
+ self.note = {}
+ for line in note_lines:
+ if ':' in line:
+ key, value = line.split(':', 1)
+ self.note[key] = value
+ self.retract_velocity = float(self.note['RetractVelocity'])
+ self.spring_constant = float(self.note['SpringC'])
+ else:
+ assert False, "Fileversion is of type '%i', not supported" % dtype
+ data = []
+
+ f.close()
+ if len(data) > 0:
+ data_list = data.tolist()
+ count = len(data_list) / 2
+ return data_list[:count - 1], data_list[count:]
+ else:
+ return None
+
+ def _flatten(self, tup):
+ out = ''
+ for ch in tup:
+ out += ch
+ return out
+
+ def _read_columns(self):
+ extension = lib.curve.Data()
+ retraction = lib.curve.Data()
+
+ extension.y, retraction.y = self._load_from_file(self.filename, extract_note=True)
+ filename = self.filename.replace('deflection', 'LVDT', 1)
+ path, name = os.path.split(filename)
+ filename = os.path.join(path, 'lvdt', name)
+ extension.x, retraction.x = self._load_from_file(filename, extract_note=False)
+ return [[extension.x, extension.y], [retraction.x, retraction.y]]
+
+ def close_all(self):
+ self.filedata.close()
+
+ def is_me(self):
+ if len(self.lines) < 34:
+ return False
+
+ name, extension = os.path.splitext(self.filename)
+ #PullDist, PullDistSign, FastSamplingFrequency, SlowSamplingFrequency, FastDecimationFactor
+ #SlowDecimationFactor, IsDualPull, InitRetDist, RelaxDist, SlowTrigger, RelativeTrigger,
+ #EndOfNote
+ if extension == '.ibw' and 'deflection' in name:
+ if 'EndOfNote' in self.lines:
+ return True
+ else:
+ return False
+ else:
+ return False
+
+ def default_plots(self):
+ '''
+ loads the curve data
+ '''
+ defl_ext, defl_ret = self.deflection()
+
+ extension = lib.curve.Curve()
+ retraction = lib.curve.Curve()
+
+ extension.color = 'red'
+ extension.label = 'extension'
+ extension.style = 'plot'
+ extension.title = 'Force curve'
+ extension.units.x = 'm'
+ extension.units.y = 'N'
+ extension.x = self.data[0][0]
+ extension.y = [i * self.spring_constant for i in defl_ext]
+ retraction.color = 'blue'
+ retraction.label = 'retraction'
+ retraction.style = 'plot'
+ retraction.title = 'Force curve'
+ retraction.units.x = 'm'
+ retraction.units.y = 'N'
+ retraction.x = self.data[1][0]
+ retraction.y = [i * self.spring_constant for i in defl_ret]
+
+ plot = lib.plot.Plot()
+ plot.title = os.path.basename(self.filename)
+ plot.curves.append(extension)
+ plot.curves.append(retraction)
+
+ plot.normalize()
+ return plot
+
+ def deflection(self):
+ if not self.data:
+ self.data = self._read_columns()
+ return self.data[0][1], self.data[1][1]
'''
mfp1dexport.py
-Driver for text-exported MFP 1D files.
+Driver for text-exported MFP-1D files.
Copyright 2009 by Massimo Sandal
with modifications by Dr. Rolf Schmidt (Concordia University, Canada)
def __init__(self, filename):
'''
- This is a driver to import Asylum Research MFP 1D data.
+ This is a driver to import Asylum Research MFP-1D data.
Status: experimental
'''
self.filename = filename
--- /dev/null
+#!/usr/bin/env python
+
+'''
+mfp3d.py
+
+Driver for MFP-3D files.
+
+Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada)
+This driver is based on the work of R. Naud and A. Seeholzer (see below)
+to read Igor binary waves. Code used with permission.
+
+This program is released under the GNU General Public License version 2.
+'''
+
+# DEFINITION:
+# Reads Igor's (Wavemetric) binary wave format, .ibw, files.
+#
+# ALGORITHM:
+# Parsing proper to version 2, 3, or version 5 (see Technical notes TN003.ifn:
+# http://mirror.optus.net.au/pub/wavemetrics/IgorPro/Technical_Notes/) and data
+# type 2 or 4 (non complex, single or double precision vector, real values).
+#
+# AUTHORS:
+# Matlab version: R. Naud August 2008 (http://lcn.epfl.ch/~naud/Home.html)
+# Python port: A. Seeholzer October 2008
+#
+# VERSION: 0.1
+#
+# COMMENTS:
+# Only tested for version 2 Igor files for now, testing for 3 and 5 remains to be done.
+# More header data could be passed back if wished. For significance of ignored bytes see
+# the technical notes linked above.
+
+import numpy
+import os.path
+import struct
+
+import lib.driver
+import lib.curve
+import lib.plot
+
+__version__='0.0.0.20100225'
+
+class mfp3dDriver(lib.driver.Driver):
+
+ def __init__(self, filename):
+ '''
+ This is a driver to import Asylum Research MFP-3D data.
+ Status: experimental
+ '''
+ self.data = []
+ self.note = []
+ self.retract_velocity = None
+ self.spring_constant = None
+ self.filename = filename
+
+ self.filedata = open(filename,'rU')
+ self.lines = list(self.filedata.readlines())
+ self.filedata.close()
+
+ self.filetype = 'mfp3d'
+ self.experiment = 'smfs'
+
+ def _load_from_file(self, filename, extract_note=False):
+ data = None
+ f = open(filename, 'rb')
+ ####################### ORDERING
+ # machine format for IEEE floating point with big-endian
+ # byte ordering
+ # MacIgor use the Motorola big-endian 'b'
+ # WinIgor use Intel little-endian 'l'
+ # If the first byte in the file is non-zero, then the file is a WinIgor
+ firstbyte = struct.unpack('b', f.read(1))[0]
+ if firstbyte == 0:
+ format = '>'
+ else:
+ format = '<'
+ ####################### CHECK VERSION
+ f.seek(0)
+ version = struct.unpack(format+'h', f.read(2))[0]
+ ####################### READ DATA AND ACCOMPANYING INFO
+ if version == 2 or version == 3:
+ # pre header
+ wfmSize = struct.unpack(format+'i', f.read(4))[0] # The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.
+ noteSize = struct.unpack(format+'i', f.read(4))[0] # The size of the note text.
+ if version==3:
+ formulaSize = struct.unpack(format+'i', f.read(4))[0]
+ pictSize = struct.unpack(format+'i', f.read(4))[0] # Reserved. Write zero. Ignore on read.
+ checksum = struct.unpack(format+'H', f.read(2))[0] # Checksum over this header and the wave header.
+ # wave header
+ dtype = struct.unpack(format+'h', f.read(2))[0]
+ if dtype == 2:
+ dtype = numpy.float32(.0).dtype
+ elif dtype == 4:
+ dtype = numpy.double(.0).dtype
+ else:
+ assert False, "Wave is of type '%i', not supported" % dtype
+ dtype = dtype.newbyteorder(format)
+
+ ignore = f.read(4) # 1 uint32
+ bname = self._flatten(struct.unpack(format+'20c', f.read(20)))
+ ignore = f.read(4) # 2 int16
+ ignore = f.read(4) # 1 uint32
+ dUnits = self._flatten(struct.unpack(format+'4c', f.read(4)))
+ xUnits = self._flatten(struct.unpack(format+'4c', f.read(4)))
+ npnts = struct.unpack(format+'i', f.read(4))[0]
+ amod = struct.unpack(format+'h', f.read(2))[0]
+ dx = struct.unpack(format+'d', f.read(8))[0]
+ x0 = struct.unpack(format+'d', f.read(8))[0]
+ ignore = f.read(4) # 2 int16
+ fsValid = struct.unpack(format+'h', f.read(2))[0]
+ topFullScale = struct.unpack(format+'d', f.read(8))[0]
+ botFullScale = struct.unpack(format+'d', f.read(8))[0]
+ ignore = f.read(16) # 16 int8
+ modDate = struct.unpack(format+'I', f.read(4))[0]
+ ignore = f.read(4) # 1 uint32
+ # Numpy algorithm works a lot faster than struct.unpack
+ data = numpy.fromfile(f, dtype, npnts)
+
+ elif version == 5:
+ # pre header
+ checksum = struct.unpack(format+'H', f.read(2))[0] # Checksum over this header and the wave header.
+ wfmSize = struct.unpack(format+'i', f.read(4))[0] # The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.
+ formulaSize = struct.unpack(format+'i', f.read(4))[0]
+ noteSize = struct.unpack(format+'i', f.read(4))[0] # The size of the note text.
+ dataEUnitsSize = struct.unpack(format+'i', f.read(4))[0]
+ dimEUnitsSize = struct.unpack(format+'4i', f.read(16))
+ dimLabelsSize = struct.unpack(format+'4i', f.read(16))
+ sIndicesSize = struct.unpack(format+'i', f.read(4))[0]
+ optionSize1 = struct.unpack(format+'i', f.read(4))[0]
+ optionSize2 = struct.unpack(format+'i', f.read(4))[0]
+
+ # header
+ ignore = f.read(4)
+ CreationDate = struct.unpack(format+'I',f.read(4))[0]
+ modData = struct.unpack(format+'I',f.read(4))[0]
+ npnts = struct.unpack(format+'i',f.read(4))[0]
+ # wave header
+ dtype = struct.unpack(format+'h',f.read(2))[0]
+ if dtype == 2:
+ dtype = numpy.float32(.0).dtype
+ elif dtype == 4:
+ dtype = numpy.double(.0).dtype
+ else:
+ assert False, "Wave is of type '%i', not supported" % dtype
+ dtype = dtype.newbyteorder(format)
+
+ ignore = f.read(2) # 1 int16
+ ignore = f.read(6) # 6 schar, SCHAR = SIGNED CHAR? ignore = fread(fid,6,'schar'); #
+ ignore = f.read(2) # 1 int16
+ bname = self._flatten(struct.unpack(format+'32c',f.read(32)))
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(4) # 1 int32
+ ndims = struct.unpack(format+'4i',f.read(16)) # Number of of items in a dimension -- 0 means no data.
+ sfA = struct.unpack(format+'4d',f.read(32))
+ sfB = struct.unpack(format+'4d',f.read(32))
+ dUnits = self._flatten(struct.unpack(format+'4c',f.read(4)))
+ xUnits = self._flatten(struct.unpack(format+'16c',f.read(16)))
+ fsValid = struct.unpack(format+'h',f.read(2))
+ whpad3 = struct.unpack(format+'h',f.read(2))
+ ignore = f.read(16) # 2 double
+ ignore = f.read(40) # 10 int32
+ ignore = f.read(64) # 16 int32
+ ignore = f.read(6) # 3 int16
+ ignore = f.read(2) # 2 char
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(4) # 2 int16
+ ignore = f.read(4) # 1 int32
+ ignore = f.read(8) # 2 int32
+
+ data = numpy.fromfile(f, dtype, npnts)
+ note_str = f.read(noteSize)
+ if extract_note:
+ note_lines = note_str.split('\r')
+ self.note = {}
+ for line in note_lines:
+ if ':' in line:
+ key, value = line.split(':', 1)
+ self.note[key] = value
+ self.retract_velocity = float(self.note['RetractVelocity'])
+ self.spring_constant = float(self.note['SpringConstant'])
+ else:
+ assert False, "Fileversion is of type '%i', not supported" % dtype
+ data = []
+
+ f.close()
+ if len(data) > 0:
+ #we have 3 columns: deflection, LVDT, raw
+ count = npnts / 3
+ deflection = data[:count].tolist()
+ lvdt = data[count:2 * count].tolist()
+ #every column contains data for extension and retraction
+ #we assume the same number of points for each
+ #we could possibly extract this info from the note
+ count = npnts / 6
+ extension = lib.curve.Data()
+ retraction = lib.curve.Data()
+ extension.x = deflection[:count]
+ extension.y = lvdt[:count]
+ retraction.x = deflection[count:]
+ retraction.y = lvdt[count:]
+
+ return extension, retraction
+ else:
+ return None
+
+ def _flatten(self, tup):
+ out = ''
+ for ch in tup:
+ out += ch
+ return out
+
+ def _read_columns(self):
+ extension, retraction = self._load_from_file(self.filename, extract_note=True)
+ return [[extension.x, extension.y], [retraction.x, retraction.y]]
+
+ def close_all(self):
+ self.filedata.close()
+
+ def is_me(self):
+ if len(self.lines) < 34:
+ return False
+
+ name, extension = os.path.splitext(self.filename)
+ if extension == '.ibw':
+ return True
+ else:
+ return False
+
+ def default_plots(self):
+ '''
+ loads the curve data
+ '''
+ defl_ext, defl_ret = self.deflection()
+
+ extension = lib.curve.Curve()
+ retraction = lib.curve.Curve()
+
+ extension.color = 'red'
+ extension.label = 'extension'
+ extension.style = 'plot'
+ extension.title = 'Force curve'
+ extension.units.x = 'm'
+ extension.units.y = 'N'
+ extension.x = self.data[0][0]
+ extension.y = [i * self.spring_constant for i in defl_ext]
+ retraction.color = 'blue'
+ retraction.label = 'retraction'
+ retraction.style = 'plot'
+ retraction.title = 'Force curve'
+ retraction.units.x = 'm'
+ retraction.units.y = 'N'
+ retraction.x = self.data[1][0]
+ retraction.y = [i * self.spring_constant for i in defl_ret]
+
+ plot = lib.plot.Plot()
+ plot.title = os.path.basename(self.filename)
+ plot.curves.append(extension)
+ plot.curves.append(retraction)
+
+ plot.normalize()
+ return plot
+
+ def deflection(self):
+ if not self.data:
+ self.data = self._read_columns()
+ return self.data[0][1], self.data[1][1]
'''
raise "Not implemented yet."
- def detriggerize(self, forcext):
- '''
- Cuts away the trigger-induced s**t on the extension curve.
- DEPRECATED
- cutindex=2
- startvalue=forcext[0]
-
- for index in range(len(forcext)-1,2,-2):
- if forcext[index]>startvalue:
- cutindex=index
- else:
- break
-
- return cutindex
- '''
- return 0
-
def is_me(self):
'''
self-identification of file type magic
import copy\r
import os.path\r
import platform\r
+import shutil\r
import time\r
-#import wx\r
+\r
import wx.html\r
import wx.lib.agw.aui as aui\r
import wx.lib.evtmgr as evtmgr\r
lh.hookeDir = os.path.abspath(os.path.dirname(__file__))\r
from config.config import config\r
import drivers\r
+import lib.clickedpoint\r
+import lib.curve\r
import lib.delta\r
import lib.playlist\r
import lib.plotmanipulator\r
import lib.prettyformat\r
import panels.commands\r
+import panels.note\r
import panels.perspectives\r
import panels.playlist\r
import panels.plot\r
__releasedate__ = lh.HOOKE_VERSION[2]\r
__release_name__ = lh.HOOKE_VERSION[1]\r
\r
-#TODO: add general preferences to Hooke\r
-#this might be useful\r
-#ID_Config = wx.NewId()\r
ID_About = wx.NewId()\r
ID_Next = wx.NewId()\r
ID_Previous = wx.NewId()\r
ID_ViewAssistant = wx.NewId()\r
ID_ViewCommands = wx.NewId()\r
ID_ViewFolders = wx.NewId()\r
+ID_ViewNote = wx.NewId()\r
ID_ViewOutput = wx.NewId()\r
ID_ViewPlaylists = wx.NewId()\r
ID_ViewProperties = wx.NewId()\r
self.SetAppName('Hooke')\r
self.SetVendorName('')\r
\r
- windowPosition = (config['main']['left'], config['main']['top'])\r
- windowSize = (config['main']['width'], config['main']['height'])\r
+ window_height = config['main']['height']\r
+ window_left= config['main']['left']\r
+ window_top = config['main']['top']\r
+ window_width = config['main']['width']\r
+\r
+ #sometimes, the ini file gets confused and sets 'left'\r
+ #and 'top' to large negative numbers\r
+ #let's catch and fix this\r
+ #keep small negative numbers, the user might want those\r
+ if window_left < -window_width:\r
+ window_left = 0\r
+ if window_top < -window_height:\r
+ window_top = 0\r
+ window_position = (window_left, window_top)\r
+ window_size = (window_width, window_height)\r
\r
#setup the splashscreen\r
if config['splashscreen']['show']:\r
def make_command_class(*bases):\r
#create metaclass with plugins and plotmanipulators\r
return type(HookeFrame)("HookeFramePlugged", bases + (HookeFrame,), {})\r
- frame = make_command_class(*plugin_objects)(parent=None, id=wx.ID_ANY, title='Hooke', pos=windowPosition, size=windowSize)\r
+ frame = make_command_class(*plugin_objects)(parent=None, id=wx.ID_ANY, title='Hooke', pos=window_position, size=window_size)\r
frame.Show(True)\r
self.SetTopWindow(frame)\r
\r
self.panelFolders = self.CreatePanelFolders()\r
self.panelPlaylists = self.CreatePanelPlaylists()\r
self.panelProperties = self.CreatePanelProperties()\r
+ self.panelNote = self.CreatePanelNote()\r
self.panelOutput = self.CreatePanelOutput()\r
self.panelResults = self.CreatePanelResults()\r
self.plotNotebook = self.CreateNotebook()\r
- #self.textCtrlCommandLine=self.CreateCommandLine()\r
\r
# add panes\r
self._mgr.AddPane(self.panelFolders, aui.AuiPaneInfo().Name('Folders').Caption('Folders').Left().CloseButton(True).MaximizeButton(False))\r
self._mgr.AddPane(self.panelPlaylists, aui.AuiPaneInfo().Name('Playlists').Caption('Playlists').Left().CloseButton(True).MaximizeButton(False))\r
+ self._mgr.AddPane(self.panelNote, aui.AuiPaneInfo().Name('Note').Caption('Note').Left().CloseButton(True).MaximizeButton(False))\r
self._mgr.AddPane(self.plotNotebook, aui.AuiPaneInfo().Name('Plots').CenterPane().PaneBorder(False))\r
self._mgr.AddPane(self.panelCommands, aui.AuiPaneInfo().Name('Commands').Caption('Settings and commands').Right().CloseButton(True).MaximizeButton(False))\r
self._mgr.AddPane(self.panelProperties, aui.AuiPaneInfo().Name('Properties').Caption('Properties').Right().CloseButton(True).MaximizeButton(False))\r
plugin_config = ConfigObj(ini_path)\r
#self.config.merge(plugin_config)\r
self.configs['core'] = plugin_config\r
+ #existing_commands contains: {command: plugin}\r
+ existing_commands = {}\r
#make sure we execute _plug_init() for every command line plugin we import\r
for plugin in self.config['plugins']:\r
if self.config['plugins'][plugin]:\r
#add to plugins\r
commands = eval('dir(module.' + plugin+ '.' + plugin + 'Commands)')\r
#keep only commands (ie names that start with 'do_')\r
- #TODO: check for existing commands and warn the user!\r
commands = [command for command in commands if command.startswith('do_')]\r
if commands:\r
+ for command in commands:\r
+ if existing_commands.has_key(command):\r
+ message_str = 'Adding "' + command + '" in plugin "' + plugin + '".\n\n'\r
+ message_str += '"' + command + '" already exists in "' + str(existing_commands[command]) + '".\n\n'\r
+ message_str += 'Only "' + command + '" in "' + str(existing_commands[command]) + '" will work.\n\n'\r
+ message_str += 'Please rename one of the commands in the source code and restart Hooke or disable one of the plugins.'\r
+ dialog = wx.MessageDialog(self, message_str, 'Warning', wx.OK|wx.ICON_WARNING|wx.CENTER)\r
+ dialog.ShowModal()\r
+ dialog.Destroy()\r
+ existing_commands[command] = plugin\r
self.plugins[plugin] = commands\r
try:\r
#initialize the plugin\r
pass\r
except ImportError:\r
pass\r
- #initialize the commands tree\r
+ #add commands from hooke.py i.e. 'core' commands\r
commands = dir(HookeFrame)\r
commands = [command for command in commands if command.startswith('do_')]\r
if commands:\r
self.plugins['core'] = commands\r
+ #initialize the commands tree\r
self.panelCommands.Initialize(self.plugins)\r
for command in dir(self):\r
if command.startswith('plotmanip_'):\r
\r
#load default list, if possible\r
self.do_loadlist(self.config['core']['list'])\r
- #self.do_loadlist()\r
\r
def _BindEvents(self):\r
#TODO: figure out if we can use the eventManager for menu ranges\r
evtmgr.eventManager.Register(self.OnExecute, wx.EVT_BUTTON, self.panelCommands.ExecuteButton)\r
evtmgr.eventManager.Register(self.OnTreeCtrlCommandsSelectionChanged, wx.EVT_TREE_SEL_CHANGED, self.panelCommands.CommandsTree)\r
evtmgr.eventManager.Register(self.OnTreeCtrlItemActivated, wx.EVT_TREE_ITEM_ACTIVATED, self.panelCommands.CommandsTree)\r
+ evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
#property editor\r
self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
#results panel\r
self.MenuBar.FindItemById(ID_ViewPlaylists).Check(pane.window.IsShown())\r
if pane.name == 'Commands':\r
self.MenuBar.FindItemById(ID_ViewCommands).Check(pane.window.IsShown())\r
+ if pane.name == 'Note':\r
+ self.MenuBar.FindItemById(ID_ViewNote).Check(pane.window.IsShown())\r
if pane.name == 'Properties':\r
self.MenuBar.FindItemById(ID_ViewProperties).Check(pane.window.IsShown())\r
if pane.name == 'Output':\r
#commands tree\r
evtmgr.eventManager.DeregisterListener(self.OnExecute)\r
evtmgr.eventManager.DeregisterListener(self.OnTreeCtrlCommandsSelectionChanged)\r
+ evtmgr.eventManager.DeregisterListener(self.OnTreeCtrlItemActivated)\r
+ evtmgr.eventManager.DeregisterListener(self.OnUpdateNote)\r
\r
def AddPlaylist(self, playlist=None, name='Untitled'):\r
if playlist and playlist.count > 0:\r
playlist_root = self.panelPlaylists.PlaylistsTree.AppendItem(tree_root, playlist.name, 0)\r
#add all files to the Playlist tree\r
# files = {}\r
+ hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
for index, file_to_add in enumerate(playlist.files):\r
- #TODO: optionally remove the extension from the name of the curve\r
- #item_text, extension = os.path.splitext(curve.name)\r
- #curve_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, item_text, 1)\r
+ #optionally remove the extension from the name of the curve\r
+ if hide_curve_extension:\r
+ file_to_add.name = lh.remove_extension(file_to_add.name)\r
file_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, file_to_add.name, 1)\r
if index == playlist.index:\r
self.panelPlaylists.PlaylistsTree.SelectItem(file_ID)\r
#self.playlists[playlist.name] = [playlist, figure]\r
self.panelPlaylists.PlaylistsTree.Expand(playlist_root)\r
self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
+ self.UpdateNote()\r
self.UpdatePlot()\r
\r
def AppendToOutput(self, text):\r
folder = self.config['core']['workdir']\r
return wx.GenericDirCtrl(self, -1, dir=folder, size=(200, 250), style=wx.DIRCTRL_SHOW_FILTERS, filter=filters, defaultFilter=index)\r
\r
+ def CreatePanelNote(self):\r
+ return panels.note.Note(self)\r
+\r
def CreatePanelOutput(self):\r
return wx.TextCtrl(self, -1, '', wx.Point(0, 0), wx.Size(150, 90), wx.NO_BORDER|wx.TE_MULTILINE)\r
\r
view_menu.AppendCheckItem(ID_ViewAssistant, 'Assistant\tF9')\r
view_menu.AppendCheckItem(ID_ViewResults, 'Results\tF10')\r
view_menu.AppendCheckItem(ID_ViewOutput, 'Output\tF11')\r
+ view_menu.AppendCheckItem(ID_ViewNote, 'Note\tF12')\r
#perspectives\r
-# perspectives_menu = self.CreatePerspectivesMenu()\r
perspectives_menu = wx.Menu()\r
\r
#help\r
help_menu.Append(wx.ID_ABOUT, 'About Hooke')\r
#put it all together\r
menu_bar.Append(file_menu, 'File')\r
-# menu_bar.Append(edit_menu, 'Edit')\r
menu_bar.Append(view_menu, 'View')\r
menu_bar.Append(perspectives_menu, "Perspectives")\r
self.UpdatePerspectivesMenu()\r
if playlist.count > 1:\r
playlist.next()\r
self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
+ self.UpdateNote()\r
self.UpdatePlot()\r
\r
def OnNotebookPageClose(self, event):\r
playlist = self.GetActivePlaylist()\r
playlist.index = index\r
self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
+ self.UpdateNote()\r
self.UpdatePlot()\r
#if you uncomment the following line, the tree will collapse/expand as well\r
#event.Skip()\r
if playlist.count > 1:\r
playlist.previous()\r
self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
+ self.UpdateNote()\r
self.UpdatePlot()\r
\r
def OnPropGridChanged (self, event):\r
def OnRestorePerspective(self, event):\r
name = self.MenuBar.FindItemById(event.GetId()).GetLabel()\r
self._RestorePerspective(name)\r
-# self._mgr.LoadPerspective(self._perspectives[name])\r
-# self.config['perspectives']['active'] = name\r
-# self._mgr.Update()\r
-# all_panes = self._mgr.GetAllPanes()\r
-# for pane in all_panes:\r
-# if not pane.name.startswith('toolbar'):\r
-# if pane.name == 'Assistant':\r
-# self.MenuBar.FindItemById(ID_ViewAssistant).Check(pane.window.IsShown())\r
-# if pane.name == 'Folders':\r
-# self.MenuBar.FindItemById(ID_ViewFolders).Check(pane.window.IsShown())\r
-# if pane.name == 'Playlists':\r
-# self.MenuBar.FindItemById(ID_ViewPlaylists).Check(pane.window.IsShown())\r
-# if pane.name == 'Commands':\r
-# self.MenuBar.FindItemById(ID_ViewCommands).Check(pane.window.IsShown())\r
-# if pane.name == 'Properties':\r
-# self.MenuBar.FindItemById(ID_ViewProperties).Check(pane.window.IsShown())\r
-# if pane.name == 'Output':\r
-# self.MenuBar.FindItemById(ID_ViewOutput).Check(pane.window.IsShown())\r
-# if pane.name == 'Results':\r
-# self.MenuBar.FindItemById(ID_ViewResults).Check(pane.window.IsShown())\r
\r
def OnResultsCheck(self, index, flag):\r
- #TODO: fix for multiple results\r
results = self.GetActivePlot().results\r
if results.has_key(self.results_str):\r
results[self.results_str].results[index].visible = flag\r
def OnTreeCtrlItemActivated(self, event):\r
self.OnExecute(event)\r
\r
+ def OnUpdateNote(self, event):\r
+ '''\r
+ Saves the note to the active file.\r
+ '''\r
+ active_file = self.GetActiveFile()\r
+ active_file.note = self.panelNote.Editor.GetValue()\r
+\r
def OnView(self, event):\r
menu_id = event.GetId()\r
menu_item = self.MenuBar.FindItemById(menu_id)\r
\r
def _clickize(self, xvector, yvector, index):\r
'''\r
- returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
+ Returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
'''\r
- point = lh.ClickedPoint()\r
+ point = lib.clickedpoint.ClickedPoint()\r
point.index = index\r
point.absolute_coords = xvector[index], yvector[index]\r
point.find_graph_coords(xvector, yvector)\r
\r
def _delta(self, message='Click 2 points', whatset=lh.RETRACTION):\r
'''\r
- calculates the difference between two clicked points\r
+ Calculates the difference between two clicked points\r
'''\r
clicked_points = self._measure_N_points(N=2, message=message, whatset=whatset)\r
\r
General helper function for N-points measurements\r
By default, measurements are done on the retraction\r
'''\r
- if message != '':\r
+ if message:\r
dialog = wx.MessageDialog(None, message, 'Info', wx.OK)\r
dialog.ShowModal()\r
\r
\r
points = []\r
for clicked_point in clicked_points:\r
- point = lh.ClickedPoint()\r
+ point = lib.clickedpoint.ClickedPoint()\r
point.absolute_coords = clicked_point[0], clicked_point[1]\r
point.dest = 0\r
#TODO: make this optional?\r
points.append(point)\r
return points\r
\r
+ def do_copylog(self):\r
+ '''\r
+ Copies all files in the current playlist that have a note to the destination folder.\r
+ destination: select folder where you want the files to be copied\r
+ use_LVDT_folder: when checked, the files will be copied to a folder called 'LVDT' in the destination folder (for MFP-1D files only)\r
+ '''\r
+ playlist = self.GetActivePlaylist()\r
+ if playlist is not None:\r
+ destination = self.GetStringFromConfig('core', 'copylog', 'destination')\r
+ if not os.path.isdir(destination):\r
+ os.makedirs(destination)\r
+ for current_file in playlist.files:\r
+ if current_file.note:\r
+ shutil.copy(current_file.filename, destination)\r
+ if current_file.driver.filetype == 'mfp1d':\r
+ filename = current_file.filename.replace('deflection', 'LVDT', 1)\r
+ path, name = os.path.split(filename)\r
+ filename = os.path.join(path, 'lvdt', name)\r
+ use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')\r
+ if use_LVDT_folder:\r
+ destination = os.path.join(destination, 'LVDT')\r
+ shutil.copy(filename, destination)\r
+\r
def do_plotmanipulators(self):\r
'''\r
Please select the plotmanipulators you would like to use\r
'''\r
self.UpdatePlot()\r
\r
+ def do_preferences(self):\r
+ '''\r
+ Please set general preferences for Hooke here.\r
+ hide_curve_extension: hides the extension of the force curve files.\r
+ not recommended for 'picoforce' files\r
+ '''\r
+ pass\r
+\r
def do_test(self):\r
'''\r
Use this command for testing purposes. You find do_test in hooke.py.\r
self.AppendToOutput('NumPy version: ' + numpy_version)\r
self.AppendToOutput('---')\r
self.AppendToOutput('Platform: ' + str(platform.uname()))\r
- #TODO: adapt to 'new' config\r
- #self.AppendToOutput('---')\r
- #self.AppendToOutput('Loaded plugins:', self.config['loaded_plugins'])\r
+ self.AppendToOutput('******************************')\r
+ self.AppendToOutput('Loaded plugins')\r
+ self.AppendToOutput('---')\r
+\r
+ #sort the plugins into alphabetical order\r
+ plugins_list = [key for key, value in self.plugins.iteritems()]\r
+ plugins_list.sort()\r
+ for plugin in plugins_list:\r
+ self.AppendToOutput(plugin)\r
+\r
+ def UpdateNote(self):\r
+ #update the note for the active file\r
+ active_file = self.GetActiveFile()\r
+ if active_file is not None:\r
+ self.panelNote.Editor.SetValue(active_file.note)\r
\r
def UpdatePerspectivesMenu(self):\r
#add perspectives to menubar and _perspectives\r
perspectiveFile = open(filename, 'rU')\r
perspective = perspectiveFile.readline()\r
perspectiveFile.close()\r
- if perspective != '':\r
+ if perspective:\r
name, extension = os.path.splitext(perspectiveFilename)\r
if extension == '.txt':\r
self._perspectives[name] = perspective\r
if playlist is not None:\r
if playlist.index >= 0:\r
self.statusbar.SetStatusText(playlist.get_status_string(), 0)\r
+ self.UpdateNote()\r
self.UpdatePlot()\r
\r
def UpdatePlot(self, plot=None):\r
\r
def add_to_plot(curve):\r
if curve.visible and curve.x and curve.y:\r
+ #get the index of the subplot to use as destination\r
destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
+ #set all parameters for the plot\r
axes_list[destination].set_title(curve.title)\r
- axes_list[destination].set_xlabel(multiplier_x + curve.units.x)\r
- axes_list[destination].set_ylabel(multiplier_y + curve.units.y)\r
+ axes_list[destination].set_xlabel(curve.multiplier.x + curve.units.x)\r
+ axes_list[destination].set_ylabel(curve.multiplier.y + curve.units.y)\r
+ #set the formatting details for the scale\r
+ formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.multiplier.x, use_zero)\r
+ formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.multiplier.y, use_zero)\r
+ axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
+ axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
if curve.style == 'plot':\r
axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
if curve.style == 'scatter':\r
axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
-\r
- def get_format_x(x, pos):\r
- 'The two args are the value and tick position'\r
- multiplier = lib.prettyformat.get_exponent(multiplier_x)\r
- decimals_str = '%.' + str(decimals_x) + 'f'\r
- return decimals_str % (x/(10 ** multiplier))\r
-\r
- def get_format_y(x, pos):\r
- 'The two args are the value and tick position'\r
- multiplier = lib.prettyformat.get_exponent(multiplier_y)\r
- decimals_str = '%.' + str(decimals_y) + 'f'\r
- return decimals_str % (x/(10 ** multiplier))\r
-\r
- decimals_x = self.GetIntFromConfig('plot', 'x_decimals')\r
- decimals_y = self.GetIntFromConfig('plot', 'y_decimals')\r
- multiplier_x = self.GetStringFromConfig('plot', 'x_multiplier')\r
- multiplier_y = self.GetStringFromConfig('plot', 'y_multiplier')\r
+ #add the legend if necessary\r
+ if curve.legend:\r
+ axes_list[destination].legend()\r
\r
if plot is None:\r
active_file = self.GetActiveFile()\r
if not active_file.driver:\r
+ #the first time we identify a file, the following need to be set\r
active_file.identify(self.drivers)\r
+ for curve in active_file.plot.curves:\r
+ curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
+ curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
+ curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
+ curve.multiplier.x = self.GetStringFromConfig('core', 'preferences', 'x_multiplier')\r
+ curve.multiplier.y = self.GetStringFromConfig('core', 'preferences', 'y_multiplier')\r
+ if active_file.driver is None:\r
+ self.AppendToOutput('Invalid file: ' + active_file.filename)\r
+ return\r
self.displayed_plot = copy.deepcopy(active_file.plot)\r
#add raw curves to plot\r
self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
self.displayed_plot = copy.deepcopy(plot)\r
\r
figure = self.GetActiveFigure()\r
-\r
figure.clear()\r
- figure.suptitle(self.displayed_plot.title, fontsize=14)\r
-\r
+ #use '0' instead of e.g. '0.00' for scales\r
+ use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
+ #optionally remove the extension from the title of the plot\r
+ hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
+ if hide_curve_extension:\r
+ title = lh.remove_extension(self.displayed_plot.title)\r
+ else:\r
+ title = self.displayed_plot.title\r
+ figure.suptitle(title, fontsize=14)\r
+ #create the list of all axes necessary (rows and columns)\r
axes_list =[]\r
-\r
number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
-\r
for index in range(number_of_rows * number_of_columns):\r
axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
-\r
- for axes in axes_list:\r
- formatter_x = FuncFormatter(get_format_x)\r
- formatter_y = FuncFormatter(get_format_y)\r
- axes.xaxis.set_major_formatter(formatter_x)\r
- axes.yaxis.set_major_formatter(formatter_y)\r
-\r
+ #add all curves to the corresponding plots\r
for curve in self.displayed_plot.curves:\r
add_to_plot(curve)\r
\r
#make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
figure.subplots_adjust(hspace=0.3)\r
\r
- #TODO: add multiple results support to fit in curve.results:\r
#display results\r
self.panelResults.ClearResults()\r
if self.displayed_plot.results.has_key(self.results_str):\r
self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
else:\r
self.panelResults.ClearResults()\r
-\r
- legend = self.GetBoolFromConfig('plot', 'legend')\r
- for axes in axes_list:\r
- if legend:\r
- axes.legend()\r
+ #refresh the plot\r
figure.canvas.draw()\r
\r
if __name__ == '__main__':\r
app = Hooke(redirect=redirect)\r
\r
app.MainLoop()\r
-\r
-\r
--- /dev/null
+#!/usr/bin/env python\r
+\r
+'''\r
+clickedpoint.py\r
+\r
+ClickedPoint class for Hooke.\r
+\r
+Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada)\r
+\r
+This program is released under the GNU General Public License version 2.\r
+'''\r
+\r
+from scipy import arange\r
+\r
+class ClickedPoint(object):\r
+ '''\r
+ This class defines what a clicked point on the curve plot is.\r
+ '''\r
+ def __init__(self):\r
+\r
+ self.is_marker = None #boolean ; decides if it is a marker\r
+ self.is_line_edge = None #boolean ; decides if it is the edge of a line (unused)\r
+ self.absolute_coords = (None, None) #(float,float) ; the absolute coordinates of the clicked point on the graph\r
+ self.graph_coords = (None, None) #(float,float) ; the coordinates of the plot that are nearest in X to the clicked point\r
+ self.index = None #integer ; the index of the clicked point with respect to the vector selected\r
+ self.dest = None #0 or 1 ; 0=top plot 1=bottom plot\r
+\r
+ def find_graph_coords(self, xvector, yvector):\r
+ '''\r
+ Given a clicked point on the plot, finds the nearest point in the dataset (in X) that\r
+ corresponds to the clicked point.\r
+ '''\r
+ dists = []\r
+ for index in arange(1, len(xvector), 1):\r
+ dists.append(((self.absolute_coords[0] - xvector[index]) ** 2)+((self.absolute_coords[1] - yvector[index]) ** 2))\r
+\r
+ self.index=dists.index(min(dists))\r
+ self.graph_coords=(xvector[self.index], yvector[self.index])\r
This program is released under the GNU General Public License version 2.\r
'''\r
\r
+from matplotlib.ticker import Formatter\r
+import lib.prettyformat\r
+\r
class Curve(object):\r
\r
def __init__(self):\r
self.color = 'blue'\r
+ self.decimals = Decimals()\r
self.destination = Destination()\r
self.label = ''\r
+ self.legend = False\r
self.linewidth = 1\r
+ self.multiplier = Multiplier()\r
self.size = 0.5\r
self.style = 'plot'\r
self.title = ''\r
self.y = []\r
\r
\r
+class Data(object):\r
+\r
+ def __init__(self):\r
+ self.x = []\r
+ self.y = []\r
+\r
+\r
+class Decimals(object):\r
+\r
+ def __init__(self):\r
+ self.x = 2\r
+ self.y = 2\r
+\r
+\r
class Destination(object):\r
\r
def __init__(self):\r
self.row = 1\r
\r
\r
+class Multiplier(object):\r
+\r
+ def __init__(self):\r
+ self.x = 'n'\r
+ self.y = 'p'\r
+\r
+\r
+class PrefixFormatter(Formatter):\r
+ '''\r
+ Formatter (matplotlib) class that uses power prefixes.\r
+ '''\r
+ def __init__(self, decimals=2, multiplier='n', use_zero=True):\r
+ self.decimals = decimals\r
+ self.multiplier = multiplier\r
+ self.use_zero = use_zero\r
+\r
+ def __call__(self, x, pos=None):\r
+ 'Return the format for tick val *x* at position *pos*'\r
+ if self.use_zero:\r
+ if x == 0:\r
+ return '0'\r
+ multiplier = lib.prettyformat.get_exponent(self.multiplier)\r
+ decimals_str = '%.' + str(self.decimals) + 'f'\r
+ return decimals_str % (x / (10 ** multiplier))\r
+\r
+\r
class Units(object):\r
\r
def __init__(self):\r
--- /dev/null
+#!/usr/bin/env python
+
+'''
+delta.py
+
+Delta class for Hooke to describe differences between 2 points.
+
+Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada)
+
+This program is released under the GNU General Public License version 2.
+'''
+
+from lib.curve import Units
+
+class Point(object):
+
+ def __init__(self):
+ self.x = 0
+ self.y = 0
+
+class Delta(object):
+
+ def __init__(self):
+ self.point1 = Point()
+ self.point2 = Point()
+ self.units = Units()\r
+\r
+ def get_delta_x(self):\r
+ return self.point1.x - self.point2.x\r
+\r
+ def get_delta_y(self):\r
+ return self.point1.y - self.point2.y\r
+\r
+
\r
def __init__(self, filename=None, drivers=None):\r
self.driver = None\r
- self.notes = ''\r
+ self.note = ''\r
self.plot = lib.plot.Plot()\r
if filename is None:\r
self.filename = None\r
EXTENSION = 0
RETRACTION = 1
+def coth(z):
+ '''
+ Hyperbolic cotangent.
+ '''
+ return (numpy.exp(2 * z) + 1) / (numpy.exp(2 * z) - 1)
+
def delete_empty_lines_from_xmlfile(filename):
#the following 3 lines are needed to strip newlines.
#Otherwise, since newlines are XML elements too, the parser would read them
aFile=''.join(aFile)
return aFile
+def fit_interval_nm(start_index, x_vect, nm, backwards):
+ '''
+ Calculates the number of points to fit, given a fit interval in nm
+ start_index: index of point
+ plot: plot to use
+ backwards: if true, finds a point backwards.
+ '''
+ c = 0
+ i = start_index
+ maxlen=len(x_vect)
+ while abs(x_vect[i] - x_vect[start_index]) * (10**9) < nm:
+ if i == 0 or i == maxlen-1: #we reached boundaries of vector!
+ return c
+ if backwards:
+ i -= 1
+ else:
+ i += 1
+ c += 1
+ return c
+
def get_file_path(filename, folders = []):
if os.path.dirname(filename) == '' or os.path.isabs(filename) == False:
path = ''
for folder in folders:
path = os.path.join(path, folder)
filename = os.path.join(hookeDir, path, filename)
-
return filename
-def coth(z):
+def pickup_contact_point(filename=''):
'''
- hyperbolic cotangent
+ Picks up the contact point by left-clicking.
'''
- return (numpy.exp(2 * z) + 1) / (numpy.exp(2 * z) - 1)
-
-class ClickedPoint(object):
+ contact_point = self._measure_N_points(N=1, message='Please click on the contact point.')[0]
+ contact_point_index = contact_point.index
+ self.wlccontact_point = contact_point
+ self.wlccontact_index = contact_point.index
+ self.wlccurrent = filename
+ return contact_point, contact_point_index
+
+def remove_extension(filename):
'''
- this class defines what a clicked point on the curve plot is
+ Removes the extension from a filename.
'''
- def __init__(self):
-
- self.is_marker = None #boolean ; decides if it is a marker
- self.is_line_edge = None #boolean ; decides if it is the edge of a line (unused)
- self.absolute_coords = (None, None) #(float,float) ; the absolute coordinates of the clicked point on the graph
- self.graph_coords = (None, None) #(float,float) ; the coordinates of the plot that are nearest in X to the clicked point
- self.index = None #integer ; the index of the clicked point with respect to the vector selected
- self.dest = None #0 or 1 ; 0=top plot 1=bottom plot
-
- def find_graph_coords(self, xvector, yvector):
- '''
- Given a clicked point on the plot, finds the nearest point in the dataset (in X) that
- corresponds to the clicked point.
- '''
- dists = []
- for index in scipy.arange(1, len(xvector), 1):
- dists.append(((self.absolute_coords[0] - xvector[index]) ** 2)+((self.absolute_coords[1] - yvector[index]) ** 2))
-
- self.index=dists.index(min(dists))
- self.graph_coords=(xvector[self.index], yvector[self.index])
+ name, extension = os.path.splitext(filename)
+ return name
#CSV-HELPING FUNCTIONS
-def transposed2(lists, defval=0):
- '''
- transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
- elements
- (by Zoran Isailovski on the Python Cookbook online)
- '''
- if not lists: return []
- return map(lambda *row: [elem or defval for elem in row], *lists)
-
def csv_write_dictionary(f, data, sorting='COLUMNS'):
'''
Writes a CSV file from a dictionary, with keys as first column or row
if sorting=='ROWS':
print 'Not implemented!' #FIXME: implement it.
+
+def transposed2(lists, defval=0):
+ '''
+ transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
+ elements
+ (by Zoran Isailovski on the Python Cookbook online)
+ '''
+ if not lists: return []
+ return map(lambda *row: [elem or defval for elem in row], *lists)
+
#rebuild a data structure from the xml attributes\r
#the next two lines are here for backwards compatibility, newer playlist files use 'filename' instead of 'path'\r
if element.hasAttribute('path'):\r
+ #path, name = os.path.split(element.getAttribute('path'))\r
+ #path = path.split(os.sep)\r
+ #filename = lib.libhooke.get_file_path(name, path)\r
filename = element.getAttribute('path')\r
if element.hasAttribute('filename'):\r
+ #path, name = os.path.split(element.getAttribute('filename'))\r
+ #path = path.split(os.sep)\r
+ #filename = lib.libhooke.get_file_path(name, path)\r
filename = element.getAttribute('filename')\r
if os.path.isfile(filename):\r
data_file = lib.file.File(filename)\r
+ if element.hasAttribute('note'):\r
+ data_file.note = element.getAttribute('note')\r
self.files.append(data_file)\r
self.count = len(self.files)\r
if self.count > 0:\r
if multiplier == 0:\r
multiplier=get_multiplier(value)\r
unit_str = ''\r
- if unit != '':\r
+ if unit:\r
unit_str = ' ' + get_prefix(multiplier) + unit\r
if decimals >= 0:\r
format_str = '% ' + repr(leading_spaces_int + decimals) + '.' + repr(decimals) + 'f'\r
--- /dev/null
+#!/usr/bin/env python\r
+\r
+'''\r
+note.py\r
+\r
+Note panel for Hooke.\r
+\r
+Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada)\r
+\r
+This program is released under the GNU General Public License version 2.\r
+'''\r
+import wx\r
+\r
+class Note(wx.Panel):\r
+\r
+ def __init__(self, parent):\r
+ wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200))\r
+\r
+ self.Editor = wx.TextCtrl(self, style=wx.TE_MULTILINE)\r
+\r
+ self.UpdateButton = wx.Button(self, -1, 'Update note')\r
+\r
+ sizer = wx.BoxSizer(wx.VERTICAL)\r
+ sizer.Add(self.Editor, 1, wx.EXPAND)\r
+ sizer.Add(self.UpdateButton, 0, wx.EXPAND)\r
+\r
+ self.SetSizer(sizer)\r
+ self.SetAutoLayout(True)\r
\r
#line.figure.canvas.draw()\r
if self.display_coordinates:\r
- coordinateString = ''.join([str(event.xdata), ' ', str(event.ydata)])\r
+ coordinateString = ''.join(['x: ', str(event.xdata), ' y: ', str(event.ydata)])\r
#TODO: pretty format\r
self.SetStatusText(coordinateString)\r
\r
#Returns the width of a string in pixels\r
#Unfortunately, it does not work terribly well (although it should).\r
#Thus, we have to add a bit afterwards.\r
- #Annoys the heck out of me (me being Rolf).\r
+ #Annoys the heck out of me (illysam).\r
font = self.results_list.GetFont()\r
dc = wx.WindowDC(self.results_list)\r
dc.SetFont(font)\r
type = color\r
value = "(255,0,128)"\r
\r
+ [[delta_force]]\r
+ default = 10\r
+ minimum = 0\r
+ type = integer\r
+ value = 10\r
+ \r
[[fit_function]]\r
default = wlc\r
elements = wlc, fjc, fjcPEG\r
default = 0.35e-9\r
minimum = 0\r
type = float\r
- value = 0.175\r
+ value = 0.35\r
+ \r
+ [[plot_linewidth]]\r
+ default = 1\r
+ maximum = 10000\r
+ minimum = 1\r
+ type = integer\r
+ value = 2\r
+ \r
+ [[plot_size]]\r
+ default = 20\r
+ maximum = 10000\r
+ minimum = 1\r
+ type = integer\r
+ value = 4\r
+ \r
+ [[plot_style]]\r
+ default = scatter\r
+ elements = plot, scatter\r
+ type = enum\r
+ value = plot\r
\r
[[rebase]]\r
default = False\r
outside of which the peak is automatically discarded (in nm)
auto_min_p: Minimum persistence length (if using WLC) or Kuhn length (if using FJC)
outside of which the peak is automatically discarded (in nm)
+ delta_force: defines the force window in points to locate the peak minimum (default: 10)
+ do not change unless you know what you are doing
'''
#default variables
auto_right_baseline = self.GetFloatFromConfig('autopeak', 'auto_right_baseline')
baseline_clicks = self.GetStringFromConfig('autopeak', 'baseline_clicks')
color = self.GetColorFromConfig('autopeak', 'color')
+ delta_force = self.GetIntFromConfig('autopeak', 'delta_force')
fit_function = self.GetStringFromConfig('autopeak', 'fit_function')
fit_points = self.GetIntFromConfig('autopeak', 'auto_fit_points')
noauto = self.GetBoolFromConfig('autopeak', 'noauto')
+ #persistence_length has to be given in nm
persistence_length = self.GetFloatFromConfig('autopeak', 'persistence_length')
#rebase: redefine the baseline
+ plot_linewidth = self.GetIntFromConfig('autopeak', 'plot_linewidth')
+ plot_size = self.GetIntFromConfig('autopeak', 'plot_size')
+ plot_style = self.GetStringFromConfig('autopeak', 'plot_style')
rebase = self.GetBoolFromConfig('autopeak', 'rebase')
reclick = self.GetBoolFromConfig('autopeak', 'reclick')
slope_span = self.GetIntFromConfig('autopeak', 'auto_slope_span')
if not usepl:
pl_value = None
else:
- pl_value = persistence_length / 10 ** 9
+ pl_value = persistence_length
usepoints = self.GetBoolFromConfig('autopeak', 'usepoints')
whatset_str = self.GetStringFromConfig('autopeak', 'whatset')
if whatset_str == 'extension':
if whatset_str == 'retraction':
whatset = lh.RETRACTION
- #TODO: should this be variable?
- delta_force = 10
-
#setup header column labels for results
if fit_function == 'wlc':
fit_results = lib.results.ResultsWLC()
#--Contact point arguments
if reclick:
- contact_point, contact_point_index = self.pickup_contact_point(filename=filename)
+ contact_point, contact_point_index = lh.pickup_contact_point(filename=filename)
elif noauto:
if self.wlccontact_index is None or self.wlccurrent != filename:
- contact_point, contact_point_index = self.pickup_contact_point(filename=filename)
+ contact_point, contact_point_index = lh.pickup_contact_point(filename=filename)
else:
contact_point = self.wlccontact_point
contact_point_index = self.wlccontact_index
if rebase or (self.basecurrent != filename) or self.basepoints is None:
if baseline_clicks == 'automatic':
self.basepoints = []
- base_index_0 = peak_location[-1] + self.fit_interval_nm(peak_location[-1], retraction.x, auto_right_baseline, False)
+ base_index_0 = peak_location[-1] + lh.fit_interval_nm(peak_location[-1], retraction.x, auto_right_baseline, False)
self.basepoints.append(self._clickize(retraction.x, retraction.y, base_index_0))
- base_index_1 = self.basepoints[0].index + self.fit_interval_nm(self.basepoints[0].index, retraction.x, auto_left_baseline, False)
+ base_index_1 = self.basepoints[0].index + lh.fit_interval_nm(self.basepoints[0].index, retraction.x, auto_left_baseline, False)
self.basepoints.append(self._clickize(retraction.x, retraction.y, base_index_1))
if baseline_clicks == '1 point':
self.basepoints=self._measure_N_points(N=1, message='Click on 1 point to select the baseline.', whatset=whatset)
- base_index_1 = self.basepoints[0].index + self.fit_interval_nm(self.basepoints[0].index, retraction.x, auto_left_baseline, False)
+ base_index_1 = self.basepoints[0].index + lh.fit_interval_nm(self.basepoints[0].index, retraction.x, auto_left_baseline, False)
self.basepoints.append(self._clickize(retraction.x, retraction.y, base_index_1))
if baseline_clicks == '2 points':
self.basepoints=self._measure_N_points(N=2, message='Click on 2 points to select the baseline.', whatset=whatset)
#WLC FITTING
#define fit interval
if not usepoints:
- fit_points = self.fit_interval_nm(peak, retraction.x, auto_fit_nm, True)
+ fit_points = lh.fit_interval_nm(peak, retraction.x, auto_fit_nm, True)
peak_point = self._clickize(retraction.x, retraction.y, peak)
other_fit_point=self._clickize(retraction.x, retraction.y, peak - fit_points)
if len(fit_result.result) > 0:
fit_result.color = color
fit_result.label = fit_function + '_' + str(index)
+ fit_result.linewidth = plot_linewidth
+ fit_result.size = plot_size
+ fit_result.style = plot_style
fit_result.title = retraction.title
fit_result.units.x = retraction.units.x
fit_result.units.y = retraction.units.y
default = False\r
type = boolean\r
value = False\r
+\r
+[preferences]\r
+ [[hide_curve_extension]]\r
+ default = False\r
+ type = boolean\r
+ value = False\r
+ \r
+ [[legend]]\r
+ default = False\r
+ type = boolean\r
+ value = False\r
+ \r
+ [[x_decimals]]\r
+ default = 2\r
+ maximum = 10\r
+ minimum = 0\r
+ type = integer\r
+ value = 2\r
+ \r
+ [[x_multiplier]]\r
+ default = n\r
+ elements = n, p\r
+ type = enum\r
+ value = n\r
+ \r
+ [[y_decimals]]\r
+ default = 2\r
+ maximum = 10\r
+ minimum = 0\r
+ type = integer\r
+ value = 2\r
+ \r
+ [[y_multiplier]]\r
+ default = n\r
+ elements = n, p\r
+ type = enum\r
+ value = n\r
+ \r
+ [[use_zero]]\r
+ default = True\r
+ type = boolean\r
+ value = True\r
+\r
+[copylog]\r
+ [[destination]]\r
+ default = ""\r
+ type = folder\r
+ value = \r
+ \r
+ [[use_LVDT_folder]]\r
+ default = False\r
+ type = boolean\r
+ value = False\r
type = string\r
value = _\r
\r
+[notes]\r
+ [[filename]]\r
+ default = notes\r
+ type = string\r
+ value = notes\r
+ \r
+ [[folder]]\r
+ default = ""\r
+ type = folder\r
+ value = \r
+ \r
+ [[prefix]]\r
+ default = notes_\r
+ type = string\r
+ value = notes_\r
+ \r
+ [[use_playlist_filename]]\r
+ default = False\r
+ type = boolean\r
+ value = False\r
+\r
[results]\r
[[append]]\r
default = True\r
'''
Exports all fitting results (if available) in a columnar ASCII format.
'''
-
ext = self.GetStringFromConfig('export', 'fits', 'ext')
folder = self.GetStringFromConfig('export', 'fits', 'folder')
prefix = self.GetStringFromConfig('export', 'fits', 'prefix')
output_file.write('\n'.join(output))
output_file.close
+ def do_notes(self):
+ '''
+ Exports the note for all the files in a playlist.
+ '''
+ filename = self.GetStringFromConfig('export', 'notes', 'filename')
+ folder = self.GetStringFromConfig('export', 'notes', 'folder')
+ prefix = self.GetStringFromConfig('export', 'notes', 'prefix')
+ use_playlist_filename = self.GetBoolFromConfig('export', 'notes', 'use_playlist_filename')
+
+ playlist = self.GetActivePlaylist()
+ output_str = ''
+ for current_file in playlist.files:
+ output_str = ''.join([output_str, current_file.filename, ' | ', current_file.note, '\n'])
+ if output_str:
+ output_str = ''.join(['Notes taken at ', time.asctime(), '\n', playlist.filename, '\n', output_str])
+ if use_playlist_filename:
+ path, filename = os.path.split(playlist.filename)
+ filename = lh.remove_extension(filename)
+ filename = ''.join([prefix, filename, '.txt'])
+ filename = os.path.join(folder, filename)
+ output_file = open(filename, 'w')
+ output_file.write(output_str)
+ output_file.close
+ else:
+ dialog = wx.MessageDialog(None, 'No notes found, file not saved.', 'Info', wx.OK)
+ dialog.ShowModal()
+
def do_overlay(self):
'''
Exports all retraction files in a playlist with the same scale.
for row_index, row in enumerate(new_x):
output_str += ''.join([str(new_x[row_index]), ', ', str(new_y[row_index]), '\n'])
- if output_str != '':
+ if output_str:
filename = ''.join([filename_prefix, current_file.name])
filename = current_file.filename.replace(current_file.name, filename)
output_file = open(filename, 'w')
line_str = current_file.plot.results[key].get_result_as_string(index)
line_str = ''.join([line_str, separator, current_file.filename])
output_str = ''.join([output_str, line_str, '\n'])
- if output_str != '':
+ if output_str:
output_str = ''.join(['Analysis started ', time.asctime(), '\n', output_str])
if append and os.path.isfile(filename):
- output_file = open(filename,'a')
- else:
- output_file = open(filename, 'w')
- output_file.write(output_str)
- output_file.close
+ output_file = open(filename,'a')
+ else:
+ output_file = open(filename, 'w')
+ output_file.write(output_str)
+ output_file.close
else:
dialog = wx.MessageDialog(None, 'No results found, file not saved.', 'Info', wx.OK)
dialog.ShowModal()
import scipy.stats
import scipy.odr
+import lib.clickedpoint
+
class fitCommands(object):
'''
Do not use any of the following commands directly:
contact_point_index=self.wlccontact_index
else:
cindex=self.find_contact_point()
- contact_point=lh.ClickedPoint()
+ contact_point = lib.clickedpoint.ClickedPoint()
contact_point.absolute_coords=displayed_plot.vectors[1][0][cindex], displayed_plot.vectors[1][1][cindex]
contact_point.find_graph_coords(displayed_plot.vectors[1][0], displayed_plot.vectors[1][1])
contact_point.is_marker=True
return xext,ysub,contact
#now, exploit a ClickedPoint instance to calculate index...
- dummy=lh.ClickedPoint()
+ dummy=lib.clickedpoint.ClickedPoint()
dummy.absolute_coords=(x_intercept,y_intercept)
dummy.find_graph_coords(xret2,yret)
[[convolution]]\r
default = "[6.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0]"\r
type = string\r
- #value = '[6.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0]'\r
value = "[6.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]"\r
\r
[[maxcut]]\r
maximum = 100\r
minimum = 0\r
type = integer\r
- value = 2\r
+ value = 7\r
\r
[[mindeviation]]\r
default = 5\r
maximum = 10000\r
minimum = 1\r
type = integer\r
- value = 7\r
+ value = 4\r
\r
[[min_deviation]]\r
default = 9\r
maximum = 100\r
minimum = 1\r
type = float\r
- value = 10\r
+ value = 9\r
\r
[peaks]\r
[[color]]\r
default = black\r
type = color\r
- value = "(128,128,128)"\r
+ value = "(0,0,0)"\r
\r
[[size]]\r
default = 20\r
maximum = 10000\r
minimum = 1\r
type = integer\r
- value = 50\r
+ value = 20\r
def has_peaks(self, plot=None, plugin=None):
'''
Finds peak position in a force curve.
- FIXME: should be moved to peakspot.py
- #TODO: should this really be moved? this is obviously tied into flatfilts/convfilt
- #flatfilts.py is where 'has_peaks' belongs
'''
if plugin is None:
current_file.peak_size = peak_size
features.append(file_index - 1)
- #TODO: warn when flatten is not applied?
+ #Warn that no flattening had been done.
+ if not self.HasPlotmanipulator('plotmanip_flatten'):
+ self.AppendToOutput('Flatten manipulator was not found. Processing was done without flattening.')
+ else:
+ if not self.AppliesPlotmanipulator('flatten'):
+ self.AppendToOutput('Flatten manipulator was not applied.')
+ self.AppendToOutput('Try to enable the flatten plotmanipulator for better results.')
+
if not features:
self.AppendToOutput('Found nothing interesting. Check the playlist, could be a bug or criteria could be too stringent.')
else:
Plugin regarding general velocity clamp measurements
-Copyright 2008 by Massimo Sandal (University of Bologna, Italy)
+Copyright 2008 by Massimo Sandal, Fabrizio Benedetti, Marco Brucale, Bruno Samori (University of Bologna, Italy),
+and Alberto Gomez-Casado (University of Twente)
with modifications by Dr. Rolf Schmidt (Concordia University, Canada)
This program is released under the GNU General Public License version 2.
maximum = 10\r
minimum = 0\r
type = integer\r
- value = 1\r
+ value = 2\r
\r
[[x_multiplier]]\r
default = n\r
class plotCommands:
def do_plot(self):
+ active_file = self.GetActiveFile()
+ for curve in active_file.plot.curves:
+ curve.decimals.x = self.GetIntFromConfig('plot', 'x_decimals')
+ curve.decimals.y = self.GetIntFromConfig('plot', 'y_decimals')
+ curve.legend = self.GetBoolFromConfig('plot', 'legend')
+ curve.multiplier.x = self.GetStringFromConfig('plot', 'x_multiplier')
+ curve.multiplier.y = self.GetStringFromConfig('plot', 'y_multiplier')
+
self.UpdatePlot();
default = retraction\r
elements = extension, retraction, both\r
type = enum\r
- value = both\r
+ value = retraction\r
\r
[procplots]\r
[[centerzero]]\r
from scipy.signal import medfilt
from lib.peakspot import conv_dx
+import lib.prettyformat
class procplotsCommands:
self.UpdatePlot(plot)
-
def do_derivplot(self):
'''
Plots the discrete differentiation of the currently displayed force curve.
-------
Syntax: subtplot
'''
- #TODO: what is sub_filter supposed to do?
#TODO: add option to keep previous subtplot
plot = self.GetDisplayedPlotCorrected()
for index in whatset:
fft_curve = self.fft_plot(copy.deepcopy(plot.curves[index]), boundaries)
+ fft_curve.decimals.x = 3
+ fft_curve.decimals.y = 0
fft_curve.destination.column = column
fft_curve.destination.row = row
+ fft_curve.label = plot.curves[index].label
+ fft_curve.legend = True
+ fft_curve.multiplier.x = lib.prettyformat.get_prefix(max(fft_curve.x))
+ fft_curve.multiplier.y = lib.prettyformat.get_prefix(max(fft_curve.y))
+ #fft_curve.multiplier.y = ''
fft_curve.title = 'FFT'
- fft_curve.units.x = 'frequency'
+ fft_curve.units.x = 'Hz'
fft_curve.units.y = 'power'
plot.curves.append(fft_curve)