From 9b4d44a1a148b3ce8fdc6a200dd1a1cd3a81ac4c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 31 Jul 2010 15:13:49 -0400 Subject: [PATCH] prettyformat -> hooke.util.si, and 'plot SI format' option to GUI --- hooke/ui/gui/__init__.py | 16 ++-- hooke/ui/gui/curve.py | 86 ------------------ hooke/ui/gui/panel/plot.py | 50 +++++++---- hooke/ui/gui/prettyformat.py | 142 ------------------------------ hooke/util/si.py | 163 +++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+), 254 deletions(-) delete mode 100644 hooke/ui/gui/curve.py delete mode 100644 hooke/ui/gui/prettyformat.py create mode 100644 hooke/util/si.py diff --git a/hooke/ui/gui/__init__.py b/hooke/ui/gui/__init__.py index 5dac90f..38d00bd 100644 --- a/hooke/ui/gui/__init__.py +++ b/hooke/ui/gui/__init__.py @@ -34,7 +34,6 @@ from . import menu as menu from . import navbar as navbar from . import panel as panel from .panel.propertyeditor import prop_from_argument, prop_from_setting -from . import prettyformat as prettyformat from . import statusbar as statusbar @@ -960,15 +959,12 @@ class GUI (UserInterface): Setting(section=self.setting_section, option='plot legend', value=True, help='Enable/disable the plot legend.'), - Setting(section=self.setting_section, option='plot x format', - value='None', - help='Display format for plot x values.'), - Setting(section=self.setting_section, option='plot y format', - value='None', - help='Display format for plot y values.'), - Setting(section=self.setting_section, option='plot zero', - value=0, - help='Select "0" vs. e.g. "0.00" for plot axes?'), + Setting(section=self.setting_section, option='plot SI format', + value='True', + help='Enable/disable SI plot axes numbering.'), + Setting(section=self.setting_section, option='plot decimals', + value=2, + help='Number of decimal places to show if "plot SI format" is enabled.'), Setting(section=self.setting_section, option='folders-workdir', value='.', help='This should probably go...'), diff --git a/hooke/ui/gui/curve.py b/hooke/ui/gui/curve.py deleted file mode 100644 index 1a8c688..0000000 --- a/hooke/ui/gui/curve.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python - -''' -curve.py - -Curve and related classes for Hooke. - -Copyright 2010 by Dr. Rolf Schmidt (Concordia University, Canada) - -This program is released under the GNU General Public License version 2. -''' - -from matplotlib.ticker import Formatter -import lib.prettyformat - -class Curve(object): - - def __init__(self): - self.color = 'blue' - self.decimals = Decimals() - self.destination = Destination() - self.label = '' - self.legend = False - self.linewidth = 1 - self.prefix = Prefix() - self.size = 0.5 - self.style = 'plot' - self.title = '' - self.units = Units() - self.visible = True - self.x = [] - self.y = [] - - -class Data(object): - - def __init__(self): - self.x = [] - self.y = [] - - -class Decimals(object): - - def __init__(self): - self.x = 2 - self.y = 2 - - -class Destination(object): - - def __init__(self): - self.column = 1 - self.row = 1 - - -class Prefix(object): - - def __init__(self): - self.x = 'n' - self.y = 'p' - - -class PrefixFormatter(Formatter): - ''' - Formatter (matplotlib) class that uses power prefixes. - ''' - def __init__(self, decimals=2, prefix='n', use_zero=True): - self.decimals = decimals - self.prefix = prefix - self.use_zero = use_zero - - def __call__(self, x, pos=None): - 'Return the format for tick val *x* at position *pos*' - if self.use_zero: - if x == 0: - return '0' - multiplier = lib.prettyformat.get_exponent(self.prefix) - decimals_str = '%.' + str(self.decimals) + 'f' - return decimals_str % (x / (10 ** multiplier)) - - -class Units(object): - - def __init__(self): - self.x = '' - self.y = '' diff --git a/hooke/ui/gui/panel/plot.py b/hooke/ui/gui/panel/plot.py index 2281812..c8e0dc2 100644 --- a/hooke/ui/gui/panel/plot.py +++ b/hooke/ui/gui/panel/plot.py @@ -15,12 +15,29 @@ matplotlib.use('WXAgg') # use wxpython with antigrain (agg) rendering from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar from matplotlib.figure import Figure +from matplotlib.ticker import Formatter import wx from ....util.callback import callback, in_callback +from ....util.si import ppSI, split_data_label from . import Panel +class HookeFormatter (Formatter): + """:class:`matplotlib.ticker.Formatter` using SI prefixes. + """ + def __init__(self, unit='', decimals=2): + self.decimals = decimals + self.unit = unit + + def __call__(self, x, pos=None): + """Return the format for tick val `x` at position `pos`. + """ + if x == 0: + return '0' + return ppSI(value=x, unit=self.unit, decimals=self.decimals) + + class PlotPanel (Panel, wx.Panel): """UI for graphical curve display. """ @@ -115,7 +132,6 @@ class PlotPanel (Panel, wx.Panel): #self.SetStatusText(coordinateString) def _resize_canvas(self): - print 'resizing' w,h = self.GetClientSize() tw,th = self._c['toolbar'].GetSizeTuple() dpi = float(self._c['figure'].get_dpi()) @@ -134,10 +150,8 @@ class PlotPanel (Panel, wx.Panel): self.update(config=config) def update(self, config={}): - print 'updating' - x_format = config['plot x format'] - y_format = config['plot y format'] - zero = config['plot zero'] + x_name = 'z piezo (m)' + y_name = 'deflection (m)' self._c['figure'].clear() self._c['figure'].suptitle( @@ -145,17 +159,19 @@ class PlotPanel (Panel, wx.Panel): fontsize=12) axes = self._c['figure'].add_subplot(1, 1, 1) -# if x_format != 'None': -# f = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero) -# axes.xaxis.set_major_formatter(f) -# if y_format != 'None': -# f = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero) -# axes.yaxis.set_major_formatter(f) - - x_name = 'z piezo (m)' - y_name = 'deflection (m)' - #axes.set_xlabel(x_name) - #axes.set_ylabel(y_name) + if config['plot si format'] == 'True': # TODO: config should convert + d = int(config['plot decimals']) # TODO: config should convert + x_n, x_unit = split_data_label(x_name) + y_n, y_unit = split_data_label(y_name) + fx = HookeFormatter(decimals=d, unit=x_unit) + axes.xaxis.set_major_formatter(fx) + fy = HookeFormatter(decimals=d, unit=y_unit) + axes.yaxis.set_major_formatter(fy) + axes.set_xlabel(x_n) + axes.set_ylabel(y_n) + else: + axes.set_xlabel(x_name) + axes.set_ylabel(y_name) self._c['figure'].hold(True) for i,data in enumerate(self._curve.data): @@ -166,3 +182,5 @@ class PlotPanel (Panel, wx.Panel): if config['plot legend'] == 'True': # HACK: config should convert axes.legend(loc='best') self._c['canvas'].draw() + +# LocalWords: matplotlib diff --git a/hooke/ui/gui/prettyformat.py b/hooke/ui/gui/prettyformat.py deleted file mode 100644 index 0c0a662..0000000 --- a/hooke/ui/gui/prettyformat.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright - -"""Simple Python function to format values with nice prefixes. -""" - -import math -from numpy import isnan - - -def pretty_format(value, unit='', decimals=-1, multiplier=0, leading_spaces=False): - if value == 0: - return '0' - if isnan(value): - return 'NaN' - - output_str = '' - leading_spaces_int = 0 - if leading_spaces: - leading_spaces_int = 5 - #automatic setting of multiplier - if multiplier == 0: - multiplier=get_multiplier(value) - unit_str = '' - if unit: - unit_str = ' ' + get_prefix(multiplier) + unit - if decimals >= 0: - format_str = '% ' + repr(leading_spaces_int + decimals) + '.' + repr(decimals) + 'f' - output_str = format_str % (value / multiplier) + unit_str - else: - output_str = str(value / multiplier) + unit_str - - if decimals < 0: - output_str = str(value / multiplier) + ' ' + get_prefix(value / multiplier) + unit - - if leading_spaces_int == 0: - output_str = output_str.lstrip() - - return output_str - -def get_multiplier(value): - return pow(10, get_power(value)) - -def get_power(value): - if value != 0 and not isnan(value): - #get the log10 from value (make sure the value is not negative) - value_temp = math.floor(math.log10(math.fabs(value))) - #reduce the log10 to a multiple of 3 and return it - return value_temp - (value_temp % 3) - else: - return 0 - -def get_exponent(prefix): - #set up a dictionary to find the exponent - exponent = { - 'Y': 24, - 'Z': 21, - 'E': 18, - 'P': 15, - 'T': 12, - 'G': 9, - 'M': 6, - 'k': 3, - '': 0, - 'm': -3, - u'\u00B5': -6, - 'n': -9, - 'p': -12, - 'f': -15, - 'a': -18, - 'z': -21, - 'y': -24, - } - if exponent.has_key(prefix): - return exponent[prefix] - else: - return 1 - -def get_prefix(value): - #set up a dictionary to find the prefix - prefix = { - 24: lambda: 'Y', - 21: lambda: 'Z', - 18: lambda: 'E', - 15: lambda: 'P', - 12: lambda: 'T', - 9: lambda: 'G', - 6: lambda: 'M', - 3: lambda: 'k', - 0: lambda: '', - -3: lambda: 'm', - -6: lambda: u'\u00B5', - -9: lambda: 'n', - -12: lambda: 'p', - -15: lambda: 'f', - -18: lambda: 'a', - -21: lambda: 'z', - -24: lambda: 'y', - } - if value != 0 and not isnan(value): - #get the log10 from value - value_temp = math.floor(math.log10(math.fabs(value))) - else: - value_temp = 0 - #reduce the log10 to a multiple of 3 and create the return string - return prefix.get(value_temp - (value_temp % 3))() - -''' -test_value=-2.4115665714484597e-008 -print 'Value: '+str(test_value)+')' -print 'pretty_format example (value, unit)' -print pretty_format(test_value, 'N') -print'-----------------------' -print 'pretty_format example (value, unit, decimals)' -print pretty_format(test_value, 'N', 3) -print'-----------------------' -print 'pretty_format example (value, unit, decimals, multiplier)' -print pretty_format(test_value, 'N', 5, 0.000001) -print'-----------------------' -print 'pretty_format example (value, unit, decimals, multiplier, leading spaces)' -print pretty_format(0.0166276297705, 'N', 3, 0.001, True) -print pretty_format(0.00750520813323, 'N', 3, 0.001, True) -print pretty_format(0.0136453282825, 'N', 3, 0.001, True) -''' -''' -#to output a scale: -#choose any value on the axis and find the multiplier and prefix for it -#use those to format the rest of the scale -#as values can span several orders of magnitude, you have to decide what units to use - -#tuple of values: -scale_values=0.000000000985, 0.000000001000, 0.000000001015 -#use this element (change to 1 or 2 to see the effect on the scale and label) -index=0 -#get the multiplier from the value at index -multiplier=get_multiplier(scale_values[index]) -print '\nScale example' -decimals=3 -#print the scale -for aValue in scale_values: print decimalFormat(aValue/multiplier, decimals), -#print the scale label using the value at index -print '\n'+get_prefix(scale_values[index])+'N' -''' diff --git a/hooke/util/si.py b/hooke/util/si.py new file mode 100644 index 0000000..ebb3c19 --- /dev/null +++ b/hooke/util/si.py @@ -0,0 +1,163 @@ +# Copyright + +"""Define functions for handling numbers in SI notation. + +Notes +----- +To output a scale, choose any value on the axis and find the +multiplier and prefix for it. Use those to format the rest of the +scale. As values can span several orders of magnitude, you have +to decide what units to use. + +>>> xs = (985e-12, 1e-9, 112358e-12) + +Get the power from the first (or last, or middle, ...) value + +>>> p = get_power(xs[0]) +>>> for x in xs: +... print ppSI(x, decimals=2, power=p) +985.00 p +1000.00 p +112358.00 p +>>> print prefix_from_value(xs[0]) + 'N' +pN +""" + +import math +from numpy import isnan +import re + + +PREFIX = { + 24: 'Y', + 21: 'Z', + 18: 'E', + 15: 'P', + 12: 'T', + 9: 'G', + 6: 'M', + 3: 'k', + 0: '', + -3: 'm', + -6: u'\u00B5', + -9: 'n', + -12: 'p', + -15: 'f', + -18: 'a', + -21: 'z', + -24: 'y', + } +"""A dictionary of SI prefixes from 10**24 to 10**-24. + +Examples +-------- +>>> PREFIX[0] +'' +>>> PREFIX[6] +'M' +>>> PREFIX[-9] +'n' +""" + +_DATA_LABEL_REGEXP = re.compile('^([^(]*[^ ]) ?\(([^)]*)\)$') +"""Used by :func:`data_label_unit`. +""" + + +def ppSI(value, unit='', decimals=None, power=None, pad=False): + """Pretty-print `value` in SI notation. + + The current implementation ignores `pad` if `decimals` is `None`. + + Examples + -------- + >>> x = math.pi * 1e-8 + >>> print ppSI(x, 'N') + 31.415927 nN + >>> print ppSI(x, 'N', 3) + 31.416 nN + >>> print ppSI(x, 'N', 4, power=-12) + 31415.9265 pN + >>> print ppSI(x, 'N', 5, pad=True) + 31.41593 nN + + If you want the decimal indented by six spaces with `decimal=2`, + `pad` should be the sum of + + * 6 (places before the decimal point) + * 1 (length of the decimal point) + * 2 (places after the decimal point) + + >>> print ppSI(-x, 'N', 2, pad=(6+1+2)) + -31.42 nN + """ + if value == 0: + return '0' + if isnan(value): + return 'NaN' + + if power == None: # auto-detect power + power = get_power(value) + + if decimals == None: + format = lambda n: '%f' % n + else: + if pad == False: # no padding + format = lambda n: '%.*f' % (decimals, n) + else: + if pad == True: # auto-generate pad + # 1 for ' ', 1 for '-', 3 for number, 1 for '.', and decimals. + pad = 6 + decimals + format = lambda n: '%*.*f' % (pad, decimals, n) + return '%s %s%s' % (format(value / pow(10,power)), PREFIX[power], unit) + + +def get_power(value): + """Return the SI power for which `0 <= |value|/10**pow < 1000`. + + Exampes + ------- + >>> get_power(0) + 0 + >>> get_power(123) + 0 + >>> get_power(-123) + 0 + >>> get_power(1e8) + 6 + >>> get_power(1e-16) + -18 + """ + if value != 0 and not isnan(value): + # get log10(|value|) + value_temp = math.floor(math.log10(math.fabs(value))) + # reduce the log10 to a multiple of 3 + return int(value_temp - (value_temp % 3)) + else: + return 0 + +def prefix_from_value(value): + """Determine the SI power of `value` and return its prefix. + + Examples + -------- + >>> prefix_from_value(0) + '' + >>> prefix_from_value(1e10) + 'G' + """ + return PREFIX[get_power(value)] + +def split_data_label(label): + """Split `curve.data[i].info['name']` labels into `(name, unit)`. + + Examples + -------- + >>> split_data_label('z piezo (m)') + ('z piezo', 'm') + >>> split_data_label('deflection (N)') + ('deflection', 'N') + """ + m = _DATA_LABEL_REGEXP.match(label) + assert m != None, label + return m.groups() -- 2.26.2