Source code for upy.printable

# Copyright (c) 2010 Friedrich Romstedt <friedrichromstedt@gmail.com>
# See also <www.friedrichromstedt.org> (if e-mail has changed)
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

# Developed since: Feb 2010

import numpy
import upy.decimal2


class PrintableElement:
    """An element of an PrintableUndarray.  Modify the .width_* attrs of
    .dn_value, .dn_uncertainty and .dn_exponent in order to make the instance
    print nicely."""

[docs] def __init__(self, value, uncertainty, enforce_sign_value = None, enforce_sign_exponent = None, conversion_reporter = None, format = None, precision = None, infinite_precision = None): """VALUE is the nominal value, UNCERTAINTY a value proportional to the standard deviation. The proportionality factor depends on what the user intends to print. To enforce the printing of optional '+' signs in the value and the exponent, use ENFORCE_SIGN_VALUE and ENFORCE_SIGN_EXPONENT. The three FORMATS supported are 'float', e.g. 0.120 +- 0.034, 'exp', e.g. (1.20 +- 0.34) 10^-1, and 'int', e.g. 12300 +- 4500. By default, the format will be determined from the values and the uncertainties. The 'int' mode is choosen upon integer type of values and uncertainties. If the uncertainty of a value is zero, the printing mode will be determined from the value alone, else both must be integer to enable int printing mode. PRECISION influences the precision of the output. Generally, the precision is determined from the uncertainty. With PRECISION = 1, the output will look like (1.0 +- 0.3), with PRECISION = 2, like (1.00 +- 0.23). If the uncertainty is zero, INFINITE_PRECISION will be used instead, giving the number of digits beind the point, either in float or exp mode. In int mode all post-point digits are suppressed. If both the value and the uncertainty are zero, only (0 +- 0) is printed. The default PRECISION is 2.""" # Other than in upy.decimal2, positive precisions indice digits # /behind/ the point (not before the point). # Convert VALUE and UNCERTAINTY to numpy ... value = numpy.asarray(value) uncertainty = numpy.asarray(uncertainty) # Autodetermine FORMAT ... if format is None: if uncertainty != 0: # Consider VALUE, and also consider UNCERTAINTY. if value.dtype.kind in 'iu' and uncertainty.dtype.kind in 'iu': format = 'int' else: format = 'exp' else: # Do only consider VALUE. if value.dtype.kind in 'iu': format = 'int' else: format = 'exp' # Autodetermine precision ... if precision is None: precision = 2 # Autodetermine infinite precision ... if infinite_precision is not None: # Use in initialisation the convention of upy.decimal2, that # digits behind the point have negative indices. infinite_precision = -infinite_precision # Store variables ... self.value = value self.uncertainty = uncertainty self.conversion_reporter = conversion_reporter self.format = format # Find out intrinsic parameters ... self.dn_value = upy.decimal2.DecimalNumber(self.value, enforce_sign = enforce_sign_value, infinite_precision = infinite_precision) self.dn_uncertainty = upy.decimal2.DecimalNumber(self.uncertainty, ceil = True, infinite_precision = infinite_precision) self.strings_value = self.dn_value.get_strings() self.strings_uncertainty = self.dn_uncertainty.get_strings() self.leftmost_digit_value = self.dn_value.get_leftmost_digit() self.leftmost_digit_uncertainty = self.dn_uncertainty.\ get_leftmost_digit() if self.format == 'exp': # Determine exponent and precision ... if self.leftmost_digit_value.infinite: # Choose the exponent such that the uncertainty prints nicely. if self.leftmost_digit_uncertainty.infinite: # Exponent doesn't matter. self.exponent = 0 # Signal that it's exactly zero by printing (0 +- 0). self.precision = 0 else: self.exponent = self.leftmost_digit_uncertainty.z # Only print (0.0 +- 1.2) for PRECISION = 2. self.precision = -(precision - 1) else: # Choose the exponent such that the value prints nicely. self.exponent = self.leftmost_digit_value.z if self.leftmost_digit_uncertainty.infinite: # Use default value: self.precision = None else: # Print (1.234 +- 0.012) for PRECISION = 2. # # The position of the leftmost digit of .uncertainty with # the .exponent taken into account is self.\ # leftmost_digit_uncertainty.z - self.exponent. self.precision = -(precision - 1) + \ (self.leftmost_digit_uncertainty.z - \ self.exponent) elif self.format == 'float': # Simply skip the exponent ... self.exponent = 0 # Determine the precision ... # Choose the precision such that the uncertainty prints nicely. if self.leftmost_digit_uncertainty.infinite: # Precision not limited. if self.leftmost_digit_value.infinite: # Signal that value is completely zero, print simply # 0 +- 0: self.precision = 0 else: # Print 0.123456789012345 +- 0.000000000000000: self.precision = None else: # Print (0.000 +- 0.012) for PRECISION = 2: self.precision = -(precision - 1) + \ self.leftmost_digit_uncertainty.z elif self.format == 'int': # Simply skip the exponent ... self.exponent = 0 # Determine precision ... # Choose the precision such that the uncertainty prints nicely. if self.leftmost_digit_uncertainty.infinite: # Precision unlimited. # # self.leftmost_digit_value may be finite or infinite. In the # infinite case, we print up to precision 0. In the finite # case, we also do so (it's an integer we're printing). self.precision = 0 else: # Print (12300 +- 4500) for PRECISION = 2, but limit the # .precision to 0, i.e., print 0 +- 1 instead of 0.0 +- 1.0. # Attempt the float value: self.precision = -(precision - 1) + \ self.leftmost_digit_uncertainty.z # Correct it if it's negative: self.precision = max(0, self.precision) else: raise ValueError('Unknown format "%s".' % format) self.dn_exponent = upy.decimal2.DecimalNumber(self.exponent, precision = 0, enforce_sign = enforce_sign_exponent) # Fill in the intrinsic parameters ... self.dn_value.set_exponent(self.exponent) self.dn_uncertainty.set_exponent(self.exponent) self.dn_value.set_precision(self.precision) self.dn_uncertainty.set_precision(self.precision)
[docs] def __str__(self): """Note that the return value ends on a whitespace, to make the space between elements in array printing two spaces wide.""" strings_value = self.dn_value.get_strings() strings_uncertainty = self.dn_uncertainty.get_strings() strings_exponent = self.dn_exponent.get_strings() if self.conversion_reporter is not None: self.conversion_reporter(strings_value, strings_uncertainty, strings_exponent) # Add a space as additional spacer when printing arrays ... if self.format == 'exp': # Exponential float format. return '(' + str(strings_value) + ' +- ' + \ str(strings_uncertainty) + \ ') 10^' + str(strings_exponent) + ' ' else: # Format 'float' or 'int'. return str(strings_value) + ' +- ' + str(strings_uncertainty) + ' '
class Widths:
[docs] def __init__(self): """Initialise to zero.""" self.width_left = 0 self.width_point = 0 self.width_right = 0
[docs] def report(self, strings): """Report a upy.decimal2.DecimalStrings instance to be printed.""" self.width_left = max(self.width_left, len(strings.str_left)) self.width_point = max(self.width_point, len(strings.str_point)) self.width_right = max(self.width_right, len(strings.str_right))
[docs] def apply_to(self, decimal_number): """Apply this widths to a upy.decimal2.DecimalNumber DECIMAL_NUMBER.""" decimal_number.width_left = self.width_left decimal_number.width_point = self.width_point decimal_number.width_right = self.width_right
[docs]class PrintableUndarray: """Prints an undarray. To print subportions, use __getattr__()."""
[docs] def __init__(self, undarray = None, sigmas = None, enforce_sign_value = None, enforce_sign_exponent = None, format = None, precision = None, infinite_precision = None): """Print undarray UNDARRAY or printable array PRINTABLE_ARRAY by giving SIGMAS standard deviations as uncertainty. To enforce the printing of optional '+' signs in the value and the exponent, use ENFORCE_SIGN_VALUE and ENFORCE_SIGN_EXPONENT. The three FORMATS supported are 'float', e.g. 0.120 +- 0.034, 'exp', e.g. (1.20 +- 0.34) 10^-1, and 'int', e.g. 12300 +- 4500. By default, the format will be determined from the values and the uncertainties. The 'int' mode is choosen upon integer type of values and uncertainties. If the uncertainty is all-zero, the printing mode will be determined from the value alone, else both must be integer to enable int printing mode. PRECISION influences the precision of the output. Generally, the precision is determined from the uncertainty. With PRECISION = 1, the output will look like (1.0 +- 0.3), with PRECISION = 2, like (1.00 +- 0.23). If the uncertainty is zero, INFINITE_PRECISION will be used instead, giving the number of digits beind the point, either in float or exp mode. In int mode all post-point digits are suppressed. If both the value and the uncertainty are zero, only (0 +- 0) is printed. The default PRECISION is 2. Note that you can affect the way the array is printed also by calling numpy.set_printoptions().""" if sigmas is None: sigmas = 2 # Autodetermine FORMAT ... if format is None: sigma = undarray.sigma() uncertainty = sigma * sigmas # SIGMAS may be zero. if (uncertainty != 0).any(): # Consider value, and also consider uncertainty. if undarray.value.dtype.kind in 'iu' and \ sigma.dtype.kind in 'iu': format = 'int' else: format = 'exp' else: # Do only consider value. if undarray.value.dtype.kind in 'iu': format = 'int' else: format = 'exp' # Store attributes ... self.sigmas = sigmas self.enforce_sign_value = enforce_sign_value self.enforce_sign_exponent = enforce_sign_exponent self.format = format self.precision = precision self.infinite_precision = infinite_precision self.undarray = undarray # Load from UNDARRAY ... self.printable_array = numpy.zeros(shape = undarray.shape, dtype = numpy.object) self.shape = undarray.shape self.ndim = undarray.ndim # Put in the values ... self[()] = undarray
[docs] def report_strings(self, strings_value, strings_uncertainty, strings_exponent): self.widths_value.report(strings_value) self.widths_uncertainty.report(strings_uncertainty) self.widths_exponent.report(strings_exponent) # # Item access ... #
[docs] def __getitem__(self, key): """Return a portion of self.""" return PrintableUndarray( undarray = self.undarray[key], sigmas = self.sigmas, enforce_sign_value = self.enforce_sign_value, enforce_sign_exponent = self.enforce_sign_exponent)
[docs] def __setitem__(self, key, value): """Set a portion KEY of self to undarray VALUE.""" # Allow for scalar indices. if not isinstance(key, tuple): key = (key,) if len(key) == self.ndim: # Final level reached. self.printable_array[key] = PrintableElement( value.value, self.sigmas * value.sigma(), enforce_sign_value = self.enforce_sign_value, enforce_sign_exponent = self.enforce_sign_exponent, conversion_reporter = self.report_strings, format = self.format, precision = self.precision, infinite_precision = self.infinite_precision) else: # Iterate. for idx in xrange(0, self.shape[len(key)]): new_key = tuple(list(key) + [idx]) self[new_key] = value[idx]
[docs] def __len__(self): return len(self.printable_array) # # String conversion ... #
def _apply_widths(self, key, widths_value, widths_uncertainty, widths_exponent): """Apply the widths WIDTHS_* to all elements.""" if len(key) == self.ndim: # Final level reached. widths_value.apply_to(self.printable_array[key].\ dn_value) widths_uncertainty.apply_to(self.printable_array[key].\ dn_uncertainty) widths_exponent.apply_to(self.printable_array[key].\ dn_exponent) else: # Iterate. for idx in xrange(0, self.shape[len(key)]): new_key = tuple(list(key) + [idx]) self._apply_widths(new_key, widths_value, widths_uncertainty, widths_exponent)
[docs] def __str__(self): """First, simulate the conversion, thereby reporting all used elements to .widths_*. Not all elements may be printed because of ellipsis. Then, apply the found maximum widths to self, and print the array.""" # Set the minimum widths ... self.widths_value = Widths() self.widths_uncertainty = Widths() self.widths_exponent = Widths() # First, simulate and report the widths ... str(self.printable_array) # Now, apply the widths ... self._apply_widths(key = (), widths_value = self.widths_value, widths_uncertainty = self.widths_uncertainty, widths_exponent = self.widths_exponent) # Now, as the elements are synchronised, build the final string ... return str(self.printable_array)