Source code for pqu.PQU

# <<BEGIN-copyright>>
# <<END-copyright>>

# Notes.
# Need sphinx 1.1 or higher to use ":private-members:" parameter in PQU.rst. This is why :py:meth:`PQU._getOtherAsPQU` do not display properly.

"""
---------------------------
Introduction
---------------------------

This module contains classes and functions for representing and manipulating a value with units and uncertainty, 
herein called a "Physical Quantity with Uncertainty" or PQU. For example, if the distance between start and 
finish lines is measured to be '100.1 m' with an uncertainty of '0.4 m', it can be inputted to PQU as the string
'100.1(4) m' or '100.1 +/- 0.4 m' (both are allowed input forms as well as several others - see `PQU`_). 

From this module, most users should only need the PQU class which stores a PQU object and
supports common math operations (e.g., addition, subtraction, multiplication) including the operation on 
the units and uncertainty. 
For example, if a person races between the start and finish lines in a time of '16.3 +/- 0.1 s', the PQU class can 
be used to determine the person's speed as:

>>> from pqu import PQU
>>> distance = PQU.PQU( '100.1(4) m' )
>>> time = PQU.PQU( '16.3 +/- 0.1 s' )
>>> speed = distance / time
>>> print speed
6.14 +/- 0.04 m/s
>>> print speed.copyToUnit( 'mi/h' )
13.7 +/- 0.1 mi/h

In addition to calculating the speed and unit, the PQU class has also propagated the significant digits and the uncertainty. In this example
the uncertainty was propagated using Goodman's expression for uncorrelated values (see `Uncertainty propagation`_). The
propagation of significant digits explains why the printed speed has only 3 digits. The following Python code illustrates
significant digits for the speed calculation:

>>> print 100.1 / 16.3
6.14110429448
>>> print float( speed )
6.14110429448
>>> print speed.info( )
value = 6.14110429447852724e+00, significantDigits = 3, order = 0, isPercent = False, unit = "m/s"
uncertainty = value = 4.49629906071862956e-02, significantDigits = 1, order = -2, isPercent = False

As can be seen, the internal speed value is as expected. However, since the PQU class (with the help
of the pqu_float class) tracks a value's significant digits, when a PQU instance is printed (via the __str__
method), only at most 'significantDigits' are displayed. The allowed range for 'significantDigits' is
1 to sys.float_info.dig + 1 inclusive. For addition and subtraction, the member 'order' is also required and
is why the following output is the same for both print statements:

>>> print distance
100.1(4) m
>>> print distance + '100.1(4) mum'         # note 'mum' is micrometer
100.1 +/- 0.4 m

How is 'significantDigits' determined? That depends on how PQU is called. If a string without
uncertainty is entered as the only argument then 'significantDigits' is the number of digits in the string
(ignoring leading '0'). Some examples are:

>>> print PQU.PQU( '12.345' ).info( )
value = 1.23450000000000006e+01, significantDigits = 5, order = 1, isPercent = False, unit = ""
uncertainty = 
>>> print PQU.PQU( '12.34500' ).info( )
value = 1.23450000000000006e+01, significantDigits = 7, order = 1, isPercent = False, unit = ""
uncertainty = 
>>> print PQU.PQU( '12.34500e-12' ).info( )
value = 1.23450000000000004e-11, significantDigits = 7, order = -11, isPercent = False, unit = ""
uncertainty = 
>>> print PQU.PQU( '0012.34500e-12' ).info( )
value = 1.23450000000000004e-11, significantDigits = 7, order = -11, isPercent = False, unit = ""
uncertainty = 
>>> print PQU.PQU( '00.0012' ).info( )
value = 1.19999999999999989e-03, significantDigits = 2, order = -3, isPercent = False, unit = ""
uncertainty = 

If the string has an uncertainty, then it is also used in calculating 'significantDigits'.
Some examples are (note - these are the same as the last examples, with uncertainties added):

>>> print PQU.PQU( '12.345 +/- 1e-8' ).info( )
value = 1.23450000000000006e+01, significantDigits = 10, order = 1, isPercent = False, unit = ""
uncertainty = value = 1.00000000000000002e-08, significantDigits = 1, order = -8, isPercent = False
>>> print PQU.PQU( '12.345 +/- 1e-8' )
12.34500000 +/- 1.e-8
>>> print PQU.PQU( '12.34500 +/- 0.12' ).info( )
value = 1.23450000000000006e+01, significantDigits = 4, order = 1, isPercent = False, unit = ""
uncertainty = value = 1.19999999999999996e-01, significantDigits = 2, order = -1, isPercent = False
>>> print PQU.PQU( '12.34500 +/- 0.12' )
12.35 +/- 0.12
>>> print PQU.PQU( '12.34500e-12(32)' ).info( )
value = 1.23450000000000004e-11, significantDigits = 7, order = -11, isPercent = False, unit = ""
uncertainty = value = 3.20000000000000023e-16, significantDigits = 2, order = -16, isPercent = False
>>> print PQU.PQU( '12.34500e-12(32)' )
1.234500e-11(32)
>>> print PQU.PQU( '0012.34500e-12 +/- 32e-15' ).info( )
value = 1.23450000000000004e-11, significantDigits = 5, order = -11, isPercent = False, unit = ""
uncertainty = value = 3.20000000000000025e-14, significantDigits = 2, order = -14, isPercent = False
>>> print PQU.PQU( '0012.34500e-12 +/- 32e-15' )
1.2345e-11 +/- 3.2e-14
>>> print PQU.PQU( '00.0012 +/- 0.000002' ).info( )
value = 1.19999999999999989e-03, significantDigits = 4, order = -3, isPercent = False, unit = ""
uncertainty = value = 1.99999999999999991e-06, significantDigits = 1, order = -6, isPercent = False
>>> print PQU.PQU( '00.0012 +/- 0.000002' )        
1.200e-3 +/- 2.e-6

The PQU constructor (i.e., its __init__ method) allows various input options for creating an instance (see `PQU`_).

Each PQU has three main members. They are a value stored as a :py:class:`pqu_float` instance, an uncertainty stored
as a :py:class:`pqu_uncertainty` instance and a unit stored as a :py:class:`PhysicalUnit` instance.

------------------------------------------------------------------------------------------
Units and conversions
------------------------------------------------------------------------------------------

This module uses the SI units along with units for angle and solid angle as its base units. The base units are:

    +-----------+-------+---------------------------+
    | Unit      | symbol| Measure                   |
    +===========+=======+===========================+
    | meter     | 'm'   | length                    |
    +-----------+-------+---------------------------+
    | kilogram  | 'kg'  | mass                      |
    +-----------+-------+---------------------------+
    | second    | 's'   | time                      |
    +-----------+-------+---------------------------+
    | Ampere    | 'A'   | electrical current        |
    +-----------+-------+---------------------------+
    | Kelvin    | 'K'   | thermodynamic temperature |
    +-----------+-------+---------------------------+
    | mole      | 'mol' | amount of substance       |
    +-----------+-------+---------------------------+
    | candela   | 'cd'  | luminous intensity        |
    +-----------+-------+---------------------------+
    | radian    | 'rad' | angle                     | 
    +-----------+-------+---------------------------+
    | steradian | 'sr'  | solid angle               |
    +-----------+-------+---------------------------+

Any PQU can be represented as a combination of each of these units with an associated power for each unit. As example, a force
has the base units 'kg' and 'm' to power 1 as well as 's' to power -2 (represented as the string 'kg * m / s**2').
All other units are stored with these units as a base and a conversion factor. For example, the unit for foot ('ft') is
stored as a meter with the conversion factor of 0.3048. Two units are said to be compatible if they have the same power for each
base unit. Hence, 'ft' is compatible with 'm' but not 'kg' or 's'. Furthermore, the electron-Volt ('eV') is compatible
with Joule ('J') and Watt-second ('W * s') but not Watt ('W').

The PQU package has many defined units with prefixes (see `Defined prefixes and units`_). In general, the
PQU methods that operate on PQU objects do not attempt to simplify the units, even if the result is dimensionless.
For example, consider the division of '3.2 m' by 11.2 km:

>>> from pqu import PQU
>>> x = PQU.PQU( '3.2 m' )
>>> y = PQU.PQU( '11.2 km' )
>>> slope = x / y
>>> print slope
0.29 m/km
>>> dl = slope.copyToUnit( "" )
>>> print dl
2.9e-4

Here the method :py:meth:`PQU.copyToUnit` is used to convert the units into a dimensionless form. Here is another
example showing the use of the :py:meth:`PQU.copyToUnit` method:

>>> mass = PQU.PQU( "4.321 g" )
>>> speed = PQU.PQU( "1.234 inch / mus"  )  
>>> energy = mass * speed**2
>>> print energy
6.580 inch**2*g/mus**2
>>> energy_joules = energy.copyToUnit( "J" )
>>> print energy_joules
4.245e6 J

The method :py:meth:`PQU.inUnitsOf` is useful for returning a physical quantity in units of descending compatible units. 
For example, :py:meth:`PQU.inUnitsOf` will convert '3123452.12 s' into days, hours, minutes and seconds, or just hours and seconds as:

>>> t = PQU.PQU( '3123452.12 s' )
>>> t.inUnitsOf( 'd', 'h', 'min', 's' )
(PQU( "36.0000000 d" ), PQU( "3.000000 h" ), PQU( "37.0000 min" ), PQU( "32.12 s" ))
>>> t.inUnitsOf( 'h',  's' )
(PQU( "867.000000 h" ), PQU( "2252.12 s" ))

Also see the methods :py:meth:`PQU.convertToUnit` and :py:meth:`PQU.getValueAs`.

------------------------------------------------------------------------------------------
Changing non-hardwired constants
------------------------------------------------------------------------------------------

PQU has a set of defined constants that are hardwired and a set that are not hardwired. The non-hardwired
constants reside in the module pqu_constants.py and are import by PQU when it is first loaded. It is 
possible to change a non-hardwired constant by loading the pqu_constants.py module before PQU is imported, redefining
the constant and then importing PQU. For example, as of this writing the elementary charge is defined as '1.60217653e-19 * C':

>>> from pqu import PQU
>>> eV = PQU.PQU( 1, 'eV' )
>>> print eV
1. eV
>>> print eV.copyToUnit( 'J' )
1.60217653e-19 J

The following lines change the elementary charge to '1.6 * C':

>>> from pqu import pqu_constants
>>> pqu_constants.e = '1.6 * C'
>>> from pqu import PQU                 # This import does not work with doctest as PQU was previously imported.
>>> my_eV = PQU.PQU( 1, 'eV' )
>>> print my_eV
1. eV
>>> print my_eV.copyToUnit( 'J' )
1.6 J

The python 'reload' function can also be used as:

>>> from pqu import PQU
>>> eV = PQU.PQU( 1, 'eV' )
>>> print eV
1. eV
>>> print eV.copyToUnit( 'J' )
1.60217653e-19 J
>>> from pqu import pqu_constants
>>> pqu_constants.e = '1.6 * C'
>>> reload( PQU )
<module 'pqu.PQU' from 'pqu/PQU.pyc'>
>>> my_eV = PQU.PQU( 1, 'eV' )
>>> print my_eV
1. eV
>>> print my_eV.copyToUnit( 'J' )
1.6 J
>>> pqu_constants.e = '1.60217653e-19 * C'      # Needs to be the same as in the module pqu_constants.py for stuff below to pass doctest.
>>> reload( PQU )
<module 'pqu.PQU' from 'pqu/PQU.pyc'>

------------------------------------------------------------------------------------------
Uncertainty propagation
------------------------------------------------------------------------------------------

This section describes the propagation of uncertainties for the binary operators '+', '-', '*' and '/' as well as the power operator
(i.e., '**'). Firstly, a comment on the limits of uncertainty propagation. Uncertainties were added mainly to support the inputting
and outputting (i.e., converting to and from a string) of a physical quantity with an associated uncertainty. As can be seen from the documentation below,
the supported propagations of uncertainties are limited to simple propagation rules.

In the following discussion, Q1, Q2 and Q3 will be PQU instances with values
V1, V2 and V3 respectively and uncertainties dQ1, dQ2 and dQ3 respectively. In addition, n1 will be a number (i.e., 1.23).

For the binary operators, all numbers are converted to a PQU instance with uncertainty of 0. For example, the following
are all equivalent:

>>> from pqu import PQU
>>> Q1 = PQU.PQU( '12.3 +/- .2 m' )
>>> print Q1 * 0.6
7.4 +/- 0.1 m
>>> print Q1 * PQU.PQU( '0.6' )
7.4 +/- 0.1 m
>>> print PQU.PQU( '0.6' ) * Q1
7.4 +/- 0.1 m

All binary operators assume that the two operands are uncorrelated except when they are the same instance. When
the two operands are the same instance, 100% correlation is assumed. For example,

>>> print Q1 - Q1                       # Operands are the same instance
0. m
>>> print Q1 - PQU.PQU( '12.3 +/- .2 m' )   # Operands are not the same instance
0.0 +/- 0.3 m
>>> print Q1 + Q1                       # Operands are the same instance
24.6 +/- 0.4 m
>>> print Q1 + PQU.PQU( '12.3 +/- .2 m' )   # Operands are not the same instance
24.6 +/- 0.3 m
>>> print Q1 * Q1                       # Operands are the same instance
151. +/- 5. m**2
>>> print Q1 * PQU.PQU( '12.3 +/- .2 m' )   # Operands are not the same instance
151. +/- 3. m**2
>>> print Q1 / Q1                       # Operands are the same instance
1.
>>> print Q1 / PQU.PQU( '12.3 +/- .2 m' )   # Operands are not the same instance
1.00 +/- 0.02

There is one exception (depending on your divide-by-zero belief) to this rule of same instance and that happens when the 
value of the operand is 0. Divide-by-zero always executes a raise of 'ZeroDivisionError', even for 'a / a'.

>>> Q2 = PQU.PQU( "0.00 +/- 0.02" )
>>> print Q2 / "2 +/- 0.5"
0.00 +/- 0.01
>>> print Q2 / Q2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "PQU.py", line 1189, in __div__
    extraFactor = uncertaintySelf * uncertaintyOther
ZeroDivisionError: float division by zero

For the rest of this discussion, the two binary operands will be assumed to not be the same instance.
For addition and subtraction with PQU instances Q1 and Q2 as operands the uncertainty is propagated as dQ3 = sqrt( dQ1**2 + dQ2**2 ).
For multiplication with PQU instances Q1 and Q2 as operands the uncertainty is propagated using Goodman's expression with
uncorrelated values (i.e., as dQ3 = sqrt( ( V2 * dQ1 )**2 + ( V1 * dQ2) **2 + ( dQ1 * dQ2 )**2 )). For division, the expression 'Q1 / Q2'
is converted to 'Q1 * Q3' with 'Q3 = PQU( 1 / V2, unit = 1 / U2, uncertainty = dQ2 / ( V2 * V2 ) )' where U2 is the unit of Q2.

For the power method, simple propagation is perform which is only valid if the uncertainty is small relative to the value. 
This is, for 'Q2 = pow( Q1, n1 )' the uncertainty for Q2 is 'dQ2 = n1 * V1**(n1-1) * dQ1'.
Note, the 'sqrt' method is equivalent to the 'pow' method with power of 0.5.

For the trigonometry methods, no propagation of uncertainty is performed.

------------------------------------------------------------------------------------------
PQU methods by functionality
------------------------------------------------------------------------------------------

This section classifies some of the methods in the PQU class by their function.

For PQU methods that require a second operand (e.g., '+', '/'), the second operand will be passed to the
staticmethod :py:meth:`PQU._getOtherAsPQU` to determine if it is a suitable object. A suitable object includes any string that
is a valid PQU. For example: :py:meth:`PQU.toString`

>>> print PQU.PQU( '12.345' ) + "3.23"
15.58
>>> print PQU.PQU( '12.345 mm' ) + "3.23m"
3242. mm

**isCompatible**

    Returns True if self's unit is compatible with the units of the argument and False otherwise.

**isPercent, isDimensionless, isLength, isMass, isTime, isCurrent, isTemperature, isMoles, isLuminousIntensity,
isAngle, isSolidAngle, isEnergy, isSpin, isCharge**

    These methods return True if self is compatible with the unit implied by the method name.

**__float__**

    This method is called when the a PQU is the argument to the 'float' function (e.g., float( pqu )).
    This method returns a python float for the value of the PQU instance. As example:

>>> pqu = PQU.PQU( '13.7 +/- 0.1 mi/h' )
>>> value = float( pqu )
>>> print type( value ), value
<type 'float'> 13.7

The PQU class has methods for the following arithmetic operations. Unless stated, a new PQU instance is returned.

**__abs__, __neg__**

    These are the standard unitary operators that only operate on the PQU's value.

**__add__, __radd__, __iadd__, __sub__, __rsub__, __isub__**

    These are the standard binary operators for addition and subtraction. For these operators, the other
    operand is passed to :py:meth:`PQU._getOtherAsPQU`. The other operand must have units compatible with self.
    As expected, the __iadd__ and __isub__ methods modify self and return it.

**__mul__, __rmul__, __imul__, __div__, __rdiv__, __idiv__**

    These are the standard binary operators for multiplication and division. For these operators, the other
    operand is passed to :py:meth:`PQU._getOtherAsPQU`. As expected, the __imul__ and __idiv__ methods modify self and
    return it. All these methods call the __mul__ method; except, when a division happens, and 'self is other'
    and the denominator is not 0.
    That is, the expression 'a / a' is handled as a special case when float( a ) != 0. In this case, a 
    dimensionless PQU is returned with value 1 and no uncertainty.

    If self is not other, the uncertainty is propagated using Goodman's expression for uncorrelated values.
    Otherwise, 100% correlated uncertainty is assumed.

**__pow__**

    This method raises - not in the python sense but in the mathematical sense (i.e., x**y) -
    self to a power. Standard uncertainty propagation is performed which is only valid when
    the uncertainty is small compared to the value.  A quantity can be raised to a non-integer power 
    only if the result can be represented by integer powers of the base units.

**__eq__, __ne__, __lt__, __le__, __gt__, __ge__, compare, equivalent**

    These methods compare self to another object. For these operators, the other
    object is passed to :py:meth:`PQU._getOtherAsPQU`. The first 6 methods all call the :py:func:`compare` method
    with epsilonFactor = 5. All methods except 'equivalent' compare 
    self's and other's values only. The 'equivalent' method also considers self's and other's uncertainty.

**__deepcopy__, convertToUnit, copyToUnit, inUnitsOf, getUnitAsString, getValueAs, getUncertaintyValueAs**

    These methods change a PQU's unit or return a new instance.

**truncate**

    This method changes a PQU's value and uncertainty to agree with its printed value as defined
    by its significant digits.

**changeUncertaintyStyle, changeUncertaintyPercent**

    These methods allow for the changing of a PQU instance uncertainty.

# BRB: need more detail here.

------------------------------------------------------------------------------------------
Supporting classes
------------------------------------------------------------------------------------------
In addition to the :py:class:`PQU` class, the following classes are a part the the PQU module:
:py:class:`parsers`, :py:class:`pqu_float`, :py:class:`pqu_uncertainty`, :py:class:`PhysicalUnit` and NumberDict.
These classes are only intended as helper classes for the PQU class; hence, they have limited functionality and
are only intended for internal use.

------------------------------------------------------------------------------------------
History
------------------------------------------------------------------------------------------

This module is based on the Scientific/Physics/PhysicalQuantities.py module (Last revision: 2007-5-25)
written by Konrad Hinsen <hinsen@cnrs-orleans.fr> with contributions from Greg Ward and Berthold Hoellmann.

Most of the additions to Hinsen's version were done by Nidhi R. Patel and Bret R. Beck <beck6@llnl.gov>
with feedback from Caleb M. Mattoon, Neil Summers and David Brown.

The values of physical constants are taken from the 2010 recommended values from
CODATA. Other conversion factors (e.g. for British units) come from various 
sources. I can't guarantee for the correctness of all entries in the unit table,
so use this at your own risk.

-----------------------------------
Issues or features to be aware of
-----------------------------------

This section describes several oddities about PQU.

    - Units 'oz', 'lb' and 'ton' (i.e., ounce, pound and ton respectively) are units of mass and not force.
      As example, the unit 'psi' (i.e., pounds per square inch) is equivalent to about '32.174 ft / s**2 * lb / inch**2'.

    - In the following, the first line products a significantDigits that is small but correct per PQU 
      rules as the number of significant digits is determined by the '1'.

>>> from pqu import PQU
>>> b1 = PQU.PQU( '1 Bohr' )
>>> b2 = b1.inBaseUnits( )
>>> print b2
5.e-11 m
>>> print b2.value.info( )
value = 5.29177208114537818e-11, significantDigits = 1, order = -11, isPercent = False
>>> 
>>> b3 = PQU.PQU( 1, 'Bohr' )        # this yields significantDigits = 16.
>>> b4 = b3.inBaseUnits( )
>>> print b4
5.291772081145378e-11 m
>>> print b4.value.info( )
value = 5.29177208114537818e-11, significantDigits = 16, order = -11, isPercent = False

    - The temperature units are Kelvin (i.e., 'K'), Celsius ('degC'), Rankine ('degR') and Fahrenheit ('degF').
      Two of these units (i.e., 'degC' and 'degF') cannot be used with any other unit including itself. That is,
      PQU( value, 'degC' ) and PQU( value, 'degF' ) are the only allowed forms when 'degF' or 'degC' are present.
      The two absolute temperature units (i.e., 'K' and 'degR') have no such restriction. As an example, heat 
      capacity can be expressed in units of 'W/(m * K)' or 'BTU/(hr * ft * degR)' but cannot be expressed in units
      of 'W/(m * degC)' or 'BTU/(hr * ft * degF)'.
      Any temperature unit can be converted to another temperature unit. For example,

>>> from pqu import PQU
>>> t1 = PQU.PQU( '40 degC' )
>>> print t1.convertToUnit( 'degF' )
104. degF
>>> print t1.getValueAs( 'K' )
313.15
>>> print t1.convertToUnit( 'degR' )
564. degR

    - PQU, actually PhysicalUnit, allows the unit to have values. For example 'm/s/25**3' is currently a valid
      unit and stored that way.  This should probably not be allowed and will probably be 
      deprecated (i.e., do not rely on it).
      The following illustrates the issue:

>>> from pqu import PQU
>>> c1 = PQU.PQU( '2.23 c' )
>>> print c1
2.23 c
>>> print c1.getValueAs( 'm/s' )
668537181.34
>>> print c1.convertToUnit( 'km / s' )
6.69e5 km/s
>>> print c1.getValue( )
668537.18134
>>> 
>>> c2 = PQU.PQU( '2.23 c' ) / 3
>>> print c2
0.743 c
>>> print c2.getValueAs( 'm/s' )
222845727.113
>>> print c2.convertToUnit( 'km / s' )
2.23e5 km/s
>>> print c2.getValue( )
222845.727113
>>> 
>>> c3 = PQU.PQU( '2.23 c / 3' )
>>> print c3
2.23 c/3
>>> print c3.getValueAs( 'm/s' )
222845727.113
>>> print c3.convertToUnit( 'km / s' )
2.23e5 km/s
>>> print c3.getValue( )
222845.727113
>>> 
>>> print c1 == c2, c1 == c3, c2 == c3
False False True
>>> 
>>> print PQU.PQU( '2.23 2. * c / 3**2 / pi' )
2.23 c*2.0/9/3.14159265359

----------------------------
Future plans
----------------------------

    - Use fractions.Fraction in units.
    - Added uncertainty to data in pqu_constants.py and supporting logic.
"""

import sys, math, re, string
from functools import reduce
from NumberDict import NumberDict

MAJORVERSION = 1
MINORVERSION = 1
PATCHVERSION = 0
__version__ = '%s.%s.%s' % ( MAJORVERSION, MINORVERSION, PATCHVERSION )

maxSignificantDigits = sys.float_info.dig

__metaclass__ = type

[docs]def compare( value1, value2, epsilonFactor = 0 ) : """ This function compares two floats (or objects that can be converted to floats) in a fuzzy way where the fuzz factor is epsilonFactor * sys.float_info.epsilon. This function returns 0 if the floats are comparable as given by epsilonFactor (see below), otherwise, it returns 1 (-1) if value1 is greater (less) than value2. Two floats are comparable if the magnitude of the 'relative difference' between them is less than or equal to epsilonFactor * sys.float_info.epsilon. The relative difference is defined as ( value1 - value2 ) / max( abs( value1 ), abs( value2 ) ) Hence, two floats are comparable if ( value1 - value2 ) <= epsilonFactor * sys.float_info.epsilon * max( abs( value1 ), abs( value2 ) ) For the default epsilonFactor = 0, a 0 is return (i.e., the two floats are 'equal') only if they have the same value :param value1: value to compare to value2 :type value1: any object which float() accepts :param value2: value to compare to value1 :type value2: any object which float() accepts :param epsilonFactor: The factor to scale sys.float_info.epsilon to determine the fuzz factor. :type epsilonFactor: any object which float() accepts :returns: 1 if value1 is deemed greater than value2, 0 if equal and -1 otherwise :rtype: `int` """ valueOfSelf, valueOfOther = float( value1 ), float( value2 ) delta = valueOfSelf - valueOfOther if( delta == 0 ) : return( 0 ) _max = max( abs( valueOfSelf ), abs( valueOfOther ) ) if( abs( delta ) <= ( float( epsilonFactor ) * sys.float_info.epsilon * _max ) ) : return( 0 ) if( delta < 0. ) : return( -1 ) return( 1 )
[docs]def valueOrPQ( value, unitFrom = None, unitTo = None, asPQU = False, checkOrder = True ) : """ This function is designed as a convenience function for working with PQU and float instances. That is, instead of checking the type of value, let valueOrPQ handle some of the details. The first argument can be either a PQU or something that is a valid argument for the python 'float' function (e.g., 1.23, "1.23"). The returned instance is a PQU if asPQU is True and a float otherwise. The unitFrom and unitTo arguments can be either None, a PhysicalUnit or a string that represents a PhysicalUnit (e.g., "m", "", "MeV * b"). If unitTo is not None, the returned instance will be in units of unitTo. If unitFrom is not None and the first argument (i.e., value) is a PQU instance, they must have the same units. Note, this function was originally written when PQU did not support dimensionless units. As it now supports dimensionless units, this function may not be of much value but should be kept anyway. Options for input are: +-----+--------+------+-----------------+-----------------------------------+ | | | | from _getUnit | returned value asPQU | +-----+--------+------+--------+--------+--------+--------------------------+ |value|unitFrom|unitTo|unitFrom|unitTo |False |True | +=====+========+======+========+========+========+==========================+ |float|None |None |None |None |float |float | +-----+--------+------+--------+--------+--------+--------------------------+ |float|None |string|None |PU |float in|PQ in unitTo | | | |or PU | | |unitTo | | +-----+--------+------+--------+--------+--------+--------------------------+ |float|string |None |PU |None |float in|PQ in unitFrom | | |or PU | | | |unitFrom| | +-----+--------+------+--------+--------+--------+--------------------------+ |float|string |string|PU |PU |float in|PQ in unitTo | | |or PU |or PU | | |unitTo | | +-----+--------+------+--------+--------+--------+--------------------------+ |PQU |None |None |PU |None |float |PQ | +-----+--------+------+--------+--------+--------+--------------------------+ |PQU |string |None |PU |None |float in|PQU (need to check that PQ| | |or PU | | | |unitFrom|and unitFrom are the same)| +-----+--------+------+--------+--------+--------+--------------------------+ |PQU |string |string|PU |PU |float in|PQU in unitTo (ditto) | | |or PU |or PU | | |unitTo | | +-----+--------+------+--------+--------+--------+--------------------------+ |PQU |None |string|PU |PU |float in|PQU in unitTo | | | |or PU | | |unitTo | | +-----+--------+------+--------+--------+--------+--------------------------+ :param value: The number object convert if requested :type value: PQU instance or any object which float() accepts :param unitFrom: If not None, the unit for value :type unitFrom: None or instance from which units can be determined :param unitTo: If not None, the unit the returned value is in :type unitTo: None or instance from which units can be determined :param asPQU: If True returned instance is a PQU; otherwise, it is a float :type asPQU: `bool` :returns: value - in units of unitTo if unitTo is not None :rtype: `float` or PQU instance """ def _getUnit( unit, default = None ) : """ Returns an instance of PhysicalUnit based first on the argument 'unit' and then 'default'. 'unit' can only be a PhysicalUnit, string (e.g., '', 'MeV', 'b * eV') or None. Default must be either a PQU instance or None. """ if( isinstance( default, PQU ) ) : default = default.unit else : default = None if( unit is None ) : if( default is None ) : return( None ) return _findUnit( default ) return( _findUnit( unit ) ) unitFrom = _getUnit( unitFrom, value ) unitTo = _getUnit( unitTo ) if( unitFrom is None ) : # Handles first 2 cases in table above. if( asPQU ) : return( PQU( value, unitTo, checkOrder = checkOrder ) ) return( float( value ) ) if( isinstance( value, PQU ) ) : if( unitFrom != value.unit ) : raise Exception( "Unit '%s' not compatible with unit '%s'" % ( unitFrom, value.unit ) ) value = value.__deepcopy__( ) else : value = PQU( value, unitFrom, checkOrder = checkOrder ) # At this point unitFrom is a PhysicalUnit instance. if( unitTo is not None ) : value.convertToUnit( unitTo ) if( asPQU ) : return( value ) return( float( value ) )
[docs]def floatToShortestString( value, significantDigits = 15, trimZeros = True, keepPeriod = False, favorEFormBy = 0, includeSign = False ) : """ This function returns the shortest string representation of the float 'value' that is accurate to 'significantDigits' digits when converted back to a float. The float is converted to both the E-form (i.e., '%e') and F-form (i.e., '%f') with significantDigits. Then, after 'trimZeros' and 'keepPeriod' options are implemented, if the length of the E-form minus favorEFormBy is less than or equal to the length of the F-form, the E-form is returned. Otherwise the F-form is returned. For example, for significantDigits = 12 the returned string for the float 1.234 will be "1.234", and not "1.234000000000" or "1.23400000000e+00" while the string for the float 1.234e-9 will be "1.234-9", and not "0.00000000123400000000" or "1.23400000000e-09". :param value: The float to convert. :type value: float :param significantDigits: The number of significant digits desired. Restricted to the range 0 to 24 inclusive. :type significantDigits: integer :param trimZeros: If True, unneeded zeros to the right of '.' are removed. :type trimZeros: bool :param keepPeriod: If False, '.' is removed if there is no digit to its right. :type keepPeriod: bool :param favorEFormBy: The integer subtracted from the length of the E-form before the form with the shortest representation is determined. :type favorEFormBy: integer :param includeSign: If True, the returned string will always start with a sign character (i.e., '+' or '-'). Otherwise, only negative values will have a sign. :type includeSign: bool :rtype: `str` """ if type(value)==str: return value def isfinite( value ) : """Returns True if value is neither infinite nor not-a-number (NaN) and False otherwise.""" if( math.isinf( value ) or math.isnan( value ) ) : return( False ) return( True ) signNeeded = '' if( includeSign ) : signNeeded = '+' if( not( isfinite( value ) ) ) : return( ( "%" + signNeeded + "e" ) % value ) significantDigitsMinus1 = min( 24, max( 0, significantDigits - 1 ) ) # 24 is arbitrary. EForm = ( '%%%s.%de' % ( signNeeded, significantDigitsMinus1 ) ) % value mantissa, exponent = EForm.split( 'e' ) if( significantDigitsMinus1 == 0 ) : if( mantissa[-1] != '.' ) : mantissa += '.' # This is required, else the trimZeros will remove all characters for value = 0. if( trimZeros ) : mantissa = mantissa.rstrip( '0' ) if( not( keepPeriod ) and ( mantissa[-1] == '.' ) ) : mantissa = mantissa[:-1] exponent = int( exponent ) if( exponent == 0 ) : return( mantissa ) EForm = mantissa + 'e' + "%d" % exponent # Reconstruct "%e" string. digitsRightOfPeriod = significantDigitsMinus1 - exponent # For "%f" string, determine number of digit to the right of the '.'. if( ( digitsRightOfPeriod > 25 ) or ( exponent > 50 ) ) : return( EForm ) digitsRightOfPeriod = max( digitsRightOfPeriod, 0 ) FForm = ( '%%%s.%df' % ( signNeeded, digitsRightOfPeriod ) ) % value if( 'e' in FForm ) : return( EForm ) # For very large numbers, the '%.?f' format returns 'e' format (e.g., .1234e300 --> "1e+299"). if( '.' in FForm ) : if( trimZeros ) : FForm = FForm.rstrip( '0' ) if( not( keepPeriod ) ) : if( FForm[-1] == '.' ) : FForm = FForm[:-1] else : if( keepPeriod ) : FForm += '.' if( ( len( FForm ) + favorEFormBy ) < len( EForm ) ) : return( FForm ) return( EForm )
toShortestString = floatToShortestString # For legacy use. Deprecated: last date 1-April-2014 (no joke).
[docs]class pqu_float : """ This class is used by PQU and pqu_uncertainty to store more information about a float than is provided by the python float class as well as methods to operator on this information. The main members are: - value --- The python value of the float. - significantDigits --- The number of significant digits the value possess as defined by the creator of self. - order --- An integer that represents the order of the most significant digit of self. Calculated internally equivalent to int( log10( value ) ) and stored for convenience. - _isPercent --- True if self represents a percent and False otherwise. Note, the value is always stored as the non-percent value (e.g., 1% and 12% are stored as 0.01 and 0.12 respectively). """ def __init__( self, value, significantDigits, isPercent = False, checkOrder = True ) : """ Returns a new pqu_float instance with value, significantDigits and isPercent. Order is calculated from value. If argument checkOrder is True, staticmethod self.fixFloatsOrder is called and its returned value and order are used. """ value = float( value ) if( isPercent ) : value /= 100. self.order = self.calculateOrder( value ) if( checkOrder ) : value, self.order = self.fixFloatsOrder( value ) self.value = value self._isPercent = bool( isPercent ) self.setSignificantDigits( significantDigits ) def __repr__( self ) : return( '%s( "%s" )' % ( self.__class__.__name__, self ) ) def __str__( self ) : return( self.toString( ) ) def __hash__( self ) : return( hash( self.value ) + hash( self._isPercent ) + hash( self.significantDigits ) ) def __eq__( self, other ) : return( self.value == float( other ) ) def __ne__( self, other ) : return( self.value != float( other ) ) def __le__( self, other ) : return( self.value <= float( other ) ) def __lt__( self, other ) : return( self.value < float( other ) ) def __ge__( self, other ) : return( self.value >= float( other ) ) def __gt__( self, other ) : return( self.value > float( other ) ) def __abs__( self ) : return( pqu_float( abs( self.value ), self.significantDigits, self._isPercent, checkOrder = False ) ) def __neg__( self ) : return( pqu_float( -self.value, self.significantDigits, self._isPercent, checkOrder = False ) ) def __add__( self, other ) : if( isinstance( other, PQU ) ) : return( other.__radd__( self ) ) if( self._isPercent ) : raise Exception( 'Addition of per cent is not allowed (defined)' ) significantOrder = self.order - self.significantDigits if( isinstance( other, pqu_float ) ) : if( other._isPercent ) : raise Exception( 'Addition of per cent is not allowed (defined)' ) value = other.value significantOrder = max( significantOrder, other.order - other.significantDigits ) else : value = float( other ) pqu_f = pqu_float( value + self.value, significantOrder, False ) significantOrder = pqu_f.order - significantOrder pqu_f.setSignificantDigits( significantOrder ) return( pqu_f ) __radd__ = __add__ def __iadd__( self, other ) : self._setFrom( self + other ) return( self ) def __sub__( self, other ) : if( isinstance( other, PQU ) ) : return( other.__rsub__( self ) ) if( isinstance( other, pqu_float ) ) : other_ = -other else : other_ = -float( other ) return( self.__add__( other_ ) ) def __rsub__( self, other ) : return( -self + other ) def __isub__( self, other ) : self._setFrom( self - other ) return( self ) def __mul__( self, other ) : if( isinstance( other, PQU ) ) : return( other.__rmul__( self ) ) significantDigits = self.significantDigits if( isinstance( other, pqu_float ) ) : value, significantDigits = other.value, min( self.significantDigits, other.significantDigits ) else : value = float( other ) return( pqu_float( value * self.value, significantDigits, False ) ) __rmul__ = __mul__ def __imul__( self, other ) : self._setFrom( self * other ) return( self ) def __div__( self, other ) : if( isinstance( other, PQU ) ) : return( other.__rdiv__( self ) ) significantDigits = self.significantDigits if( isinstance( other, pqu_float ) ) : value, significantDigits = other.value, min( self.significantDigits, other.significantDigits ) else : value = float( other ) return( pqu_float( self.value / value, significantDigits, False ) ) def __rdiv__( self, other ) : value = float( other ) return( pqu_float( value / self.value, self.significantDigits ) ) def __idiv__( self, other ) : self._setFrom( self / other ) return( self ) def __pow__( self, other ) : power = float( other ) return( pqu_float( self.value**power, self.significantDigits ) ) def __float__( self ) : return( self.value ) def __bool__( self ) : return( self.value != 0. ) __nonzero__ = __bool__ # for python 2.x def __deepcopy__( self ) : other = self.__new__( pqu_float ) other.value, other.order, other.significantDigits, other._isPercent = self.value, self.order, self.significantDigits, self._isPercent return( other ) __copy__ = __deepcopy__ def _setFrom( self, other ) : """Sets self's members from other. Other must be a pqu_float instance.""" if( not( isinstance( other, pqu_float ) ) ) : raise TypeError( "other type %s not supported" % type( other ) ) self.value = other.value self._isPercent = other._isPercent self.order = other.order self.significantDigits = other.significantDigits
[docs] def getValue( self ) : # Deprecated - replaced with __float__ "Returns self's value. Equivalent to float( self )." return( self.value )
[docs] def getOrder( self ) : """Returns self's order. :rtype: `int` """ return( self.order )
[docs] def getSignificantDigits( self ) : """Returns self's significant digits. :rtype: `int` """ return( self.significantDigits )
[docs] def info( self, significantDigits = 17 ) : """ Returns a detailed string of self. Mainly for debugging. :rtype: `str` """ value = "%%.%de" % significantDigits % self.value return( "value = %s, significantDigits = %d, order = %d, isPercent = %s" % ( value, self.significantDigits, self.order, self._isPercent ) )
[docs] def isPercent( self ) : """Returns True if self is a percent and False otherwise. :rtype: `bool` """ return( self._isPercent )
[docs] def setPercentFlag( self, isPercent, scaleValue = False ) : """ If argument isPercent is True and self is not a percent, self is converted to a percent. If argument isPercent is False and self is a percent, self is converted to non-percent. Otherwise, a raise is executed. If scaleValue is False, the value is not changed; otherwise, value is scale by 0.01 (100) if isPercent is True (False). :param isPercent: :type isPercent: `bool` param scaleValue: :type scaleValue: `bool` """ if( bool( isPercent ) == self._isPercent ) : raise Exception( "Percent flag is already %s" % self._isPercent ) if( isPercent ) : self._isPercent, scale, orderDelta = True, 0.01, -2 else : self._isPercent, scale, orderDelta = False, 100., 2 if( scaleValue ) : self.value *= scale self.order += orderDelta
[docs] def setSignificantDigits( self, significantDigits ) : """ Sets self's significant digits to significantDigits. :param significantDigits: :type significantDigits: `int` """ self.significantDigits = max( 1, min( maxSignificantDigits + 1, int( significantDigits ) ) )
[docs] def toString( self, trimZeros = True, keepPeriod = True, includePercent = True, favorEFormBy = 0, significantDigits = None ) : """ Returns a string representation of self (i.e., '1.234e6' or '12%') using function :py:func:`floatToShortestString`. :param bool trimZeros: See function :py:func:`floatToShortestString` :param bool keepPeriod: See function :py:func:`floatToShortestString` :param int favorEFormBy: See function :py:func:`floatToShortestString` :param significantDigits: (*int* or *None*) Used to override self's default significantDigits :rtype: `str` """ value = self.value if( self._isPercent ) : value *= 100. if( significantDigits is None ) : significantDigits = self.significantDigits str1 = floatToShortestString( value, significantDigits = significantDigits, trimZeros = trimZeros, keepPeriod = keepPeriod, favorEFormBy = favorEFormBy ) if( includePercent and self._isPercent ) : str1 += '%' return( str1 )
[docs] def truncate( self, significantDigits = None ) : """ Adjusts self's value so that only significantDigits are non-zero. For example, if value is 3.453983207834109 and significantDigits = 4 then the value is set to 3.454. If significantDigits is None, self's significantDigits is used. :param significantDigits: :type significantDigits: None or `int` """ if( significantDigits is None ) : significantDigits = self.significantDigits significantDigits = min( maxSignificantDigits + 1, max( 1, significantDigits ) ) self.value = float( "%%.%de" % ( significantDigits - 1 ) % self.value ) self.setSignificantDigits( significantDigits )
[docs] @staticmethod def calculateOrder( value ) : """ Returns the order of value. Value must be convertible to a float. A float's order is the exponent in the representation 'm * 10**o' where 'm' is the mantissa and 'o' is the exponent. For example, 1.234e43 has order 43. :param value: any object convertible to a float. :rtype: integer """ value = abs( float( value ) ) if( value == 0. ) : return( 0 ) s1 = "%.17e" % value return( int( s1.split( 'e' )[1] ) )
[docs] @staticmethod def fixFloatsOrder( value ) : """ Many numbers are not represented in IEEE floating point by there true value. For example, '1e-12' is stored as the float 9.9999999999999998e-13. The order of the true value is -12 while the order of the floating point representation is -13. This function is implemented to help with this issue. For example, for PQU the string "2.300000000003 (1)" was originally being reprinted as "2.300000000003 (10)" because of this issue. To check for this, the value is multiplied by a small (of order sys.float_info.epsilon) factor to see if order changes. If it does, value is set to the smallest multiplier greater than 1 for which order is changed. The fixed value and its order are returned. :param value: any object convertible to a float. :rtype: tuple of float and integer """ value = float( value ) order1 = pqu_float.calculateOrder( value ) order2 = pqu_float.calculateOrder( value * ( 1 + 4 * sys.float_info.epsilon ) ) if( order1 != order2 ) : for i1 in xrange( 5 ) : value_ = value * ( 1 + i1 * sys.float_info.epsilon ) order2 = pqu_float.calculateOrder( value_ ) if( order1 != order2 ) : break value = value_ order1 = order2 return( value, order1 )
[docs] @staticmethod def surmiseSignificantDigits( value ) : """ This factory function returns a pqu_float instance given a float value and set its significantDigits to something "reasonable". The significantDigits are determined by using PQU.floatToShortestString with trimZeros = True. """ value_ = PQU( floatToShortestString( float( value ), trimZeros = True ) ) return( pqu_float( value, value_.getSignificantDigits( ) ) )
[docs]class pqu_uncertainty : """ This class is used by PQU to store the style of uncertainty and information about its value. The members are: * style --- One of the three following uncertainty styles supported by PQU: - pqu_uncertaintyStyleNone - No uncertainty was given for the PQU instance. - pqu_uncertaintyStylePlusMinus - The uncertainty was given as '+/- value' (e.g., '123 +/- 32'). - pqu_uncertaintyStyleParenthesis - The uncertainty was given as '(value)' (e.g., '123(32)'. * value --- The value stored as a pqu_float. """ pqu_uncertaintyStyleNone = None pqu_uncertaintyStylePlusMinus = 'plusMinus' pqu_uncertaintyStyleParenthesis = 'parenthesis' def __init__( self, uncertaintyStyle, value = None, significantDigits = 2, isPercent = False, checkOrder = True ) : """ Constructor method for the pqu_uncertainty class. The arguments value, significantDigits, isPrecent and checkOrder are passed onto pqu_float. significantDigits will be forced to be 1 or 2. """ if( isPercent and ( uncertaintyStyle != pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) ) : raise Exception( 'percent only supported for style "%s"' % pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) if( value == 0. ) : uncertaintyStyle = pqu_uncertainty.pqu_uncertaintyStyleNone significantDigits = max( 1, min( 2, int( significantDigits ) ) ) self.style = uncertaintyStyle if( uncertaintyStyle == pqu_uncertainty.pqu_uncertaintyStyleNone ) : self.value = pqu_float( 0., 2, checkOrder = False ) elif( uncertaintyStyle == pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : self.value = pqu_float( abs( value ), significantDigits, isPercent, checkOrder = checkOrder ) elif( uncertaintyStyle == pqu_uncertainty.pqu_uncertaintyStyleParenthesis ) : self.value = pqu_float( abs( value ), significantDigits, checkOrder = checkOrder ) else : raise TypeError( 'Invalid uncertainty style "%s"' % uncertaintyStyle ) def __repr__( self ) : return( '%s( "%s" )' % ( self.__class__.__name__, self ) ) def __str__( self ) : return( self.toString( ) ) def __hash__( self ) : return( hash( self.style ) + hash( self.value ) ) def __mul__( self, other ) : "For internal use only. Only works if other is a float." if( not( isinstance( other, float ) ) ) : TypeError( 'Other type = "%s" not supported' % type( other ) ) return( pqu_uncertainty( self.style, self.value * other, significantDigits = self.value.significantDigits, isPercent = self.isPercent( ) ) ) __rmul__ = __mul__ def __float__( self ) : return( float( self.value ) ) def __deepcopy__( self ) : other = self.__new__( pqu_uncertainty ) other.style, other.value = self.style, self.value.__deepcopy__( ) return( other ) __copy__ = __deepcopy__ def _changeUncertaintyStyle( self, style ) : """For internal use.""" if( not( self._okayToChangeUncertaintyStyleTo( style ) ) ) : raise Exception( 'Cannot change uncertainty style from "%s" to "%s"' % ( self.style, style ) ) self.style = style def _okayToChangeUncertaintyStyleTo( self, style, checkPercent = True ) : """For internal use.""" if( self.style == style ) : return( True ) if( self.style == pqu_uncertainty.pqu_uncertaintyStyleNone ) : return( False ) if( style == pqu_uncertainty.pqu_uncertaintyStyleNone ) : return( False ) if( ( style != pqu_uncertainty.pqu_uncertaintyStyleParenthesis ) and ( style != pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) ) : raise TypeError( 'Invalid style "%s"' % style ) if( checkPercent and self.value.isPercent( ) ) : raise Exception( "Cannot change pqu_uncertaintyStylePlusMinus that is a percent" ) return( True )
[docs] def getStyle( self ) : """ Returns self's style. :rtype: One of the three uncertainty styles """ return( self.style )
[docs] def getProperValue( self, v ) : """ Returns self's value. If value is a percent, returned result is multiplied by argument v first, where v is expected to be the number associated the the percent value. :rtype: `float` """ value = float( self.value ) if( self.value.isPercent( ) ) : value *= float( v ) return( value )
[docs] def info( self, significantDigits = 17 ) : """Returns a detailed string of self. Mainly for debugging. :rtype: `str` """ if( self.style == pqu_uncertainty.pqu_uncertaintyStyleNone ) : return( '' ) return( self.value.info( significantDigits = significantDigits ) )
[docs] def isUncertainty( self ) : """Returns False is style is pqu_uncertaintyStyleNone and True otherwise. :rtype: `bool` """ return( self.style != pqu_uncertainty.pqu_uncertaintyStyleNone )
[docs] def isPercent( self ) : """Returns True if self is a percent and False otherwise. :rtype: `bool` """ if( self.style == pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : return( self.value.isPercent( ) ) return( False )
[docs] def toString( self, prefix = ' ', significantDigits = None ) : """ Returns a string representation of self (i.e., '+/- 1.2%'). :param str prefix: A prefix for the '+/-' style :param significantDigits: Passed onto method :py:meth:`pqu_float.toString` :rtype: `str` """ if( self.style == pqu_uncertainty.pqu_uncertaintyStyleNone ) : return( '' ) if( self.style == pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : favorEFormBy = 0 if( self.value.order - self.value.getSignificantDigits( ) == 0 ) : favorEFormBy = 1 return( '%s+/- %s' % ( prefix, self.value.toString( trimZeros = False, favorEFormBy = favorEFormBy, significantDigits = significantDigits ) ) ) return( '(%d)' % int( pow( 10., self.value.getSignificantDigits( ) - self.value.getOrder( ) - 1 ) * float( self.value ) + 1e-12 ) )
[docs] def truncate( self ) : """ Adjusts self's value so that only the significant digits are non-zero. This is most useful when a PQU instance is calculated from other PQU instance which can create an uncertainty with many non-zero digits. As example, adding two PQUs with uncertainties 1.3 and 3.2 each with two significant digits, produces the uncertainty sqrt( 1.3**2 + 3.2**2 ) = 3.453983207834109. This uncertainty also only has two significant digits. After calling truncate the uncertainty become 3.5 which is the value that the method toString would return. """ self.value.truncate( )
[docs]class PQU : """ This class supports a float value with an optional uncertainty and unit. Many basic math operations are supported (e.g., +, -, * /). A PQU is anything that the staticmethod parsers.parsePQUString can parse. In this section the following notations are used: +---------------------------+-----------------------------------------------------------------------------------+ | Notation | Denote | +===========================+===================================================================================+ | pqu | The argument is a pqu instance. | +---------------------------+-----------------------------------------------------------------------------------+ | [], | The argument is optional. | +---------------------------+-----------------------------------------------------------------------------------+ | int | A python int (e.g., 23). | +---------------------------+-----------------------------------------------------------------------------------+ | float | A python float (e.g., 1.23e-12). | +---------------------------+-----------------------------------------------------------------------------------+ | number | Any of the following, int, float, pqu_float, a string representing a float (i.e., | | | the python float function must be able to convert the string - e.g., '12.3e3' but | | | not '12.3 eV') or any object having a __float__ method (hence, a numpy float | | | will work). | +---------------------------+-----------------------------------------------------------------------------------+ | uncertainty = number_unc | The uncertainty argument must be a valid number or a pqu_uncertainty object. | | | If number, uncertainty is of style '+/-'. | +---------------------------+-----------------------------------------------------------------------------------+ | unit = string_pu | The unit argument must be a valid unit string (e.g., "MeV", "MeV/m" but not | | | "1 MeV") or a PhysicalUnit object. | +---------------------------+-----------------------------------------------------------------------------------+ | string | String can be anything that the method parsers.parsePQUString is | | | capable of parsing (e.g., '1.23', '1.23 m', '1.23%', '1.23(12) m', | | | '1.23 +/- 0.12m'). If it contains a unit (uncertainty) then the unit | | | (uncertainty) argument must be None. | +---------------------------+-----------------------------------------------------------------------------------+ Calling options are: - PQU( pqu ) # The new PQU gets all data from pqu. Unit and uncertainty must be None. - PQU( number, [ unit = string_pu ], [ uncertainty = number_unc ] ) - PQU( string ) - PQU( string, unit = string_pu ) # If string does not contain a unit. - PQU( string, uncertainty = uncertainty ) # If string does not contain an uncertainty. - PQU( string, unit = string_pu, uncertainty = number ) # If string does not contain a unit or an uncertainty. For example, the following are allowed and are the same: - PQU( "1.23", "m", "12%" ) - PQU( "1.23 m", uncertainty = "12%" ) - PQU( "1.23 +/- 12%", "m" ) - PQU( "1.23 +/- 12%", "m", None ) # Same as above as None is the default. - PQU( "1.23+/-12% m" ) While the following are not allowed: - PQU( "1.23+/-12% m", "m" ) # Unit already given in value. - PQU( "1.23+/-12% m", uncertainty = "12%" ) # Uncertainty already given in value. - PQU( "1.23+/-12% m", "m", "12%" ) # Unit and uncertainty already given in value. """ def __init__( self, value, unit = None, uncertainty = None, checkOrder = True ) : if( isinstance( value, PQU ) ) : if( unit is not None ) : raise TypeError( 'When value is a PQU instance, unit must be None not type "%s"' % type( unit ) ) if( uncertainty is not None ) : raise TypeError( 'When value is a PQU instance, uncertainty must be None not type "%s"' % type( uncertainty ) ) self.value = value.value.__deepcopy__( ) self.unit = value.unit.__deepcopy__( ) self.uncertainty = value.uncertainty.__deepcopy__( ) return if( isinstance( uncertainty, pqu_uncertainty ) ) : uncertainty = uncertainty.__deepcopy__( ) if( isinstance( unit, PhysicalUnit ) ) : unit = unit.__deepcopy__( ) if( isinstance( value, unicode ) ) : value = str( value ) if( isinstance( value, str ) ) : value, unit_, uncertainty_ = parsers.parsePQUString( value ) if( uncertainty_.style != pqu_uncertainty.pqu_uncertaintyStyleNone ) : if( uncertainty is not None ) : raise Exception( 'uncertainty argument = "%s" must be None when value = "%s" is string with uncertainty.' % ( uncertainty, value ) ) uncertainty = uncertainty_ if( not( unit_.isDimensionless( ) ) ) : if( unit is not None ) : raise Exception( 'unit argument = "%s" must be None when value = "%s" is string with unit.' % ( unit, value ) ) if( len( unit_.symbols ) > 0 ) : unit = unit_ else : if( isinstance( value, pqu_float ) ) : value = value.__deepcopy__( ) else : try : value = pqu_float( value, maxSignificantDigits + 1, checkOrder = checkOrder ) except : raise TypeError( 'Cannot convert value = "%s" to a float.' % ( value, ) ) if( unit is None ) : unit = '' unit = _findUnit( unit ) # unit can be a PhysicalUnit instance or a string convertible to PhysicalUnit object. if( uncertainty is None ) : uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleNone ) else : if( not( isinstance( uncertainty, pqu_uncertainty ) ) ) : significantDigits, isPercent = 2, False uncertainty_ = uncertainty if( isinstance( uncertainty, str ) ) : uncertainty_, significantDigitsForZero, str2 = parsers.parseFloat( uncertainty, uncertainty ) if( str2[:1] == '%' ) : isPercent, str2 = True, str2[1:] if( len( str2.strip( ) ) > 0 ) : raise Exception( 'Extra characters for uncertainty "%s"' % uncertainty ) if( isinstance( uncertainty, pqu_float ) ) : significantDigits, isPercent = uncertainty_.getSignificantDigits( ), uncertainty_.isPercent( ) try : uncertainty_ = float( uncertainty_ ) except : raise TypeError( 'Invalid uncertainty = "%s"' % uncertainty ) if( uncertainty_ == 0. ) : uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleNone ) else : uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStylePlusMinus, uncertainty_, significantDigits = significantDigits, isPercent = isPercent ) if( uncertainty.getStyle( ) == pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : uncertaintyLeastOrder = uncertainty.value.getOrder( ) - uncertainty.value.getSignificantDigits( ) if( uncertainty.value.isPercent( ) ) : uncertainty_ = uncertainty.value * float( value ) uncertaintyLeastOrder = uncertainty_.getOrder( ) - uncertainty_.getSignificantDigits( ) # As uncertainty_ is still %. significantDigits = value.getOrder( ) - uncertaintyLeastOrder value.setSignificantDigits( significantDigits ) elif( uncertainty.getStyle( ) == pqu_uncertainty.pqu_uncertaintyStyleParenthesis ) : parenthesisPower = pqu_float.fixFloatsOrder( pow( 10., value.order - value.significantDigits + 1 ) )[0] uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleParenthesis, uncertainty.value * parenthesisPower, significantDigits = uncertainty.value.getSignificantDigits( ) ) if( value.isPercent( ) ) : if( uncertainty.isPercent( ) ) : raise Exception( 'Percent for both value and uncertainty is not allowed' ) if( not( unit.isDimensionless( ) ) ) : raise Exception( 'Percent value and unit is not allowed' ) self.value = value # Private instance guarantied by logic above. self.unit = unit # Private instance guarantied by logic above. self.uncertainty = uncertainty # Private instance guarantied by logic above. def __str__( self ) : return( self.toString( ) ) def __repr__( self ) : return( '%s( "%s" )' % ( self.__class__.__name__, self ) ) def __hash__( self ) : return( hash( self.value ) + hash( self.uncertainty ) + hash( self.unit ) ) def __abs__( self ) : return( self.__class__( abs( self.value ), self.unit, self.uncertainty ) ) def __neg__( self ) : return( self.__class__( -self.value, self.unit, self.uncertainty ) ) def __add__( self, other ) : if( self is other ) : return( 2 * self ) other = self._getOtherAsPQU( other ) factor, offset = other.unit.conversionTupleTo( self ) value = self.value + ( other.value + offset ) * factor uncertaintySelf, uncertaintyOther = self.getUncertaintyValueAs( ), other.getUncertaintyValueAs( ) * factor uncertainty = math.sqrt( uncertaintySelf * uncertaintySelf + uncertaintyOther * uncertaintyOther ) # Guess at style. If it should be pqu_uncertaintyStyleNone then uncertainty will be 0, which pqu_uncertainty will correct. uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStylePlusMinus, uncertainty ) leastSignificantOrder = value.order - value.significantDigits uncertainty.value.setSignificantDigits( uncertainty.value.order - leastSignificantOrder ) return( PQU( value, self.unit, uncertainty ) ) __radd__ = __add__ def __iadd__( self, other ) : self._setFrom( self + other ) return( self ) def __sub__( self, other ) : if( self is other ) : return( PQU( 0., self.unit, 0. ) ) if( isinstance( other, str ) ) : other = PQU( other ) return( self + -other ) def __rsub__( self, other ) : if( self is other ) : return( PQU( 0., self.unit, 0. ) ) return( other + -self ) def __isub__( self, other ) : self._setFrom( self - other ) return( self ) def __mul__( self, other ) : other = self._getOtherAsPQU( other ) selfValue, otherValue = float( self ), float( other ) uncertaintySelf, uncertaintyOther = self.getUncertaintyValueAs( ), other.getUncertaintyValueAs( ) uncertaintySelf2, uncertaintyOther2 = uncertaintySelf * otherValue, uncertaintyOther * selfValue extraFactor = uncertaintySelf * uncertaintyOther if( ( self is other ) and ( selfValue != 0 ) ) : extraFactor = 2 * selfValue * otherValue uncertainty = math.sqrt( uncertaintySelf2 * uncertaintySelf2 + uncertaintyOther2 * uncertaintyOther2 + extraFactor * uncertaintySelf * uncertaintyOther ) significantDigits = min( self.uncertainty.value.getSignificantDigits( ), other.uncertainty.value.getSignificantDigits( ) ) uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStylePlusMinus, uncertainty, significantDigits ) return( PQU( self.value * other.value, unit = self.unit * other.unit, uncertainty = uncertainty ) ) __rmul__ = __mul__ def __imul__( self, other ) : self._setFrom( self * other ) return( self ) def __div__( self, other ) : if( ( self is other ) and ( float( self ) != 0 ) ) : return( PQU( 1. ) ) other = self._getOtherAsPQU( other ) inv_other = 1. / float( other ) return( self * PQU( inv_other, 1 / other.unit, inv_other * inv_other * other.getUncertaintyValueAs( ) ) ) def __rdiv__( self, other ) : other = self._getOtherAsPQU( other ) return( other / self ) def __idiv__( self, other ) : self._setFrom( self / other ) return( self ) def __pow__( self, other ) : """Does not include uncertainty of other in calculation. Other must be an object convertible to a float. If it is a PQU instance, it must be dimensionless.""" other = self._getOtherAsPQU( other ) if( not( other.isDimensionless( ) ) ) : raise TypeError( 'Power must be dimensionless. It has dimension "%s".' % other.unit ) power = float( other ) valueToPower = pow( self.value, power ) value, uncertainty = float( self ), None if( value != 0 ) : # This is not the correct answer when value is small compared to uncertainty. Needs work? uncertainty = ( power * float( valueToPower ) / value ) * self.uncertainty if( uncertainty.getStyle( ) == pqu_uncertainty.pqu_uncertaintyStyleParenthesis ) : uncertainty.style = pqu_uncertainty.pqu_uncertaintyStylePlusMinus valueToPower = pqu_float( valueToPower, self.value.getSignificantDigits( ) ) return( PQU( valueToPower, unit = pow( self.unit, power ), uncertainty = uncertainty ) ) def __float__( self ) : return( float( self.value ) ) def __bool__( self ) : return( bool( self.value ) ) __nonzero__ = __bool__ # for python 2.x def __eq__( self, other ) : return( self.compare( other, 5 ) == 0 ) def __ne__( self, other ) : return( self.compare( other, 5 ) != 0 ) def __lt__( self, other ) : return( self.compare( other, 5 ) < 0 ) def __le__( self, other ) : return( self.compare( other, 5 ) <= 0 ) def __gt__( self, other ) : return( self.compare( other, 5 ) > 0 ) def __ge__( self, other ) : return( self.compare( other, 5 ) >= 0 ) def __deepcopy__( self ) : other = self.__new__( PQU ) other.value, other.unit, other.uncertainty = self.value.__deepcopy__( ), self.unit.__deepcopy__( ), self.uncertainty.__deepcopy__( ) return( other ) __copy__ = __deepcopy__ def _setFrom( self, other ) : """ Sets self's members from other. Other must be an instance of PQU. :param other: The PQU instance whose members are copied to self :type other: a PQU instance """ if( not( isinstance( other, PQU ) ) ) : raise TypeError( "other type %s not supported" % type( other ) ) self.value = other.value.__deepcopy__( ) self.unit = other.unit.__deepcopy__( ) self.uncertainty = other.uncertainty.__deepcopy__( )
[docs] def changeUncertaintyStyle( self, style ) : """ Changes self.uncertainty's style. If style is the same as self.uncertainty's style nothing is done. If style or self.uncertainty's style is pqu_uncertainty.pqu_uncertaintyStyleNone as raise is executed. :param style: One of the three pqu_uncertainty styles """ self.uncertainty._okayToChangeUncertaintyStyleTo( style, checkPercent = False ) self.uncertainty._changeUncertaintyStyle( style )
[docs] def changeUncertaintyPercent( self, toPercent ) : """ If self's style is not pqu_uncertaintyStylePlusMinus a raise is executed. Otherwise, if toPercent is True (False) self's uncertainty will be converted to a percent (non-percent) if not already a percent (non-percent). :param toPercent: Specifies whether self's uncertainty should be percent or not :type toPercent: `bool` """ style = self.uncertainty.getStyle( ) if( style != pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : raise TypeError( 'Can only change pqu_uncertaintyStylePlusMinus style to percent and not %s' % style ) toPercent = bool( toPercent ) if( toPercent != self.uncertainty.isPercent( ) ) : if( self.isPercent( ) ) : raise Exception( 'Uncertainty cannot be percent when value is a percent' ) if( toPercent ) : value = 100 * float( self.uncertainty ) / float( self ) self.uncertainty = pqu_uncertainty( style, value, self.uncertainty.value.getSignificantDigits( ), isPercent = True ) else : self.uncertainty = pqu_uncertainty( style, self.getUncertaintyValueAs( ), self.uncertainty.value.getSignificantDigits( ) )
[docs] def compare( self, other, epsilonFactor = 0 ) : """ Compares self's value to other's value (other's value is first converted to unit of self) using the :py:func:`compare` function. Also see method :py:meth:`PQU.equivalent`. :param other: The PQU instance to compare to self :type other: A PQU equivalent instance :param epsilonFactor: See the :py:func:`compare` function :returns: See the :py:func:`compare` function """ other = self._getOtherAsPQU( other ) valueOfOther = other.getValueAs( self.unit ) return( compare( self, valueOfOther, epsilonFactor = epsilonFactor ) )
[docs] def convertToUnit( self, unit ) : """ Changes self's unit to unit and adjusts the value such that the new value/unit is equivalent to the original value/unit. The new unit must be compatible with the original unit of self. :param str unit: a unit equivalent :raises TypeError: if the unit string is not a known unit or is a unit incompatible with self's unit """ if( isinstance( unit, PQU ) ) : unit = unit.unit factor, offset = self.unit.conversionTupleTo( unit ) unit = _findUnit( unit ) self.value = ( self.value + offset ) * factor if( not( self.uncertainty.isPercent( ) ) ) : self.uncertainty = self.uncertainty * factor self.unit = unit return( self )
[docs] def copyToUnit( self, unit ) : """ Like convertToUnit except a copy is made and returned. The copy unit is changed to unit. Self is left unchanged. :param str unit: a unit equivalent :rtype: PQU """ return( self.__deepcopy__( ).convertToUnit( unit ) )
[docs] def equivalent( self, other, factor = 1. ) : """ This method compares self to other and returns True if they are equivalent and False otherwise. The two are considered equivalent if their bands overlap. The band for self (or other) is all values within 'factor times uncertainty' of its value. That is, the band for self is the ranges from 'value - factor * uncertainty' to 'value + factor * uncertainty'. Note, this is different than the :py:meth:`compare` method which compares the values using sys.float_info.epsilon to calculate a 'band' and not their uncertainties. :param other: The object to compare to self :type other: a PQU equivalent :param factor: the 1/2 width in standard deviations of the band :type factor: `float` :returns: 1 if self is deemed greater than other, 0 if equal and -1 otherwise """ # BRB. Should this be named equivalent or consistent. factor = abs( factor ) other = self._getOtherAsPQU( other ) other2 = other.copyToUnit( self.unit ) selfValue, selfUncFactor = float( self ), factor * self.getUncertaintyValueAs( ) otherValue, otherUncFactor = float( other2 ), factor * other2.getUncertaintyValueAs( ) if( ( selfValue + selfUncFactor ) < ( otherValue - otherUncFactor ) ) : return( False ) if( ( selfValue - selfUncFactor ) > ( otherValue + otherUncFactor ) ) : return( False ) return( True )
[docs] def inUnitsOf( self, *units ) : """ Express the quantity in different units. If one unit is specified, a new PQU object is returned that expresses the quantity in that unit. If several units are specified, the return value is a tuple of PhysicalObject instances with one element per unit such that the sum of all quantities in the tuple equals the the original quantity and all the values except for the last one are integers. This is used to convert to irregular unit systems like hour/minute/second. :param units: one or several units :type units: `str` or sequence of `str` :returns: one or more physical quantities :rtype: `PQU` or `tuple` of `PQU` :raises TypeError: if any of the specified units are not compatible with the original unit """ units = list( map( _findUnit, units ) ) if( len( units ) == 1 ) : elf = self.__class__( self.value, self.unit, self.uncertainty ) elf.convertToUnit( units[0] ) return( elf ) else : units.sort( reverse = True ) result = [] value = self.value baseUnit = units[-1] base = value * self.unit.conversionFactorTo( baseUnit ) for subunit in units[:-1] : value = base * baseUnit.conversionFactorTo( subunit ) rounded = _round( value ) result.append( self.__class__( rounded, subunit ) ) base = base - rounded * subunit.conversionFactorTo( baseUnit ) result.append( self.__class__( base, units[-1] ) ) return( tuple( result ) )
[docs] def getSignificantDigits( self ) : """ Returns the number of significant digits for self's value. :rtype: `int` """ return( self.value.getSignificantDigits( ) )
[docs] def getUnitAsString( self ) : """ Returns a string representation of self's unit. :rtype: `str` """ return( self.unit.symbol( ) )
getUnitSymbol = getUnitAsString # To be deprecated.
[docs] def getValueAs( self, unit, asPQU = False ) : """ Returns self's value in units of unit. Unit must be compatible with self's unit. :param unit: The unit to return self's value in :rtype: `float` """ unit = _findUnit( unit ) factor, offset = self.unit.conversionTupleTo( unit ) value = ( float( self.value ) + offset ) * factor if( asPQU ) : return( PQU( value, unit ) ) return( value )
[docs] def getUncertaintyValueAs( self, unit = None, asPQU = False ) : """ Returns a python float representing self's uncertainty value. If the uncertainty is a percent, the returned float is self's value times self's uncertainty value. :rtype: `float` """ value = PQU( self.uncertainty.getProperValue( self.value ), self.unit ) if( unit is None ) : unit = self.unit return( value.getValueAs( unit, asPQU = asPQU ) )
[docs] def getValue( self ) : # To be deprecated - replaced by __float__ """Returns a python float representing self's value. This is equivalent to 'float( self )'.""" return( float( self ) )
[docs] def inBaseUnits( self ) : """ :returns: A copy of self converted to base units, i.e. SI units in most cases :rtype: PQU """ value = ( self.value + self.unit.offset ) * self.unit.factor uncertainty = self.uncertainty * self.unit.factor num = '' denom = '' for i1 in range( len( _base_symbols ) ) : unit = _base_symbols[i1] power = self.unit.powers[i1] if( power < 0 ) : denom = denom + '/' + unit if( power < -1 ) : denom = denom + '**' + str( -power ) elif( power > 0 ) : num = num + '*' + unit if( power > 1 ) : num = num + '**' + str( power ) if( len( num ) == 0 ) : num = '1' if( len( denom ) == 0 ) : num = '' else: num = num[1:] return( self.__class__( value, num + denom, uncertainty ) )
[docs] def info( self, significantDigits = 17 ) : """Returns a detailed string of self. Mainly for debugging. :rtype: `str` """ return( '%s, unit = "%s"\nuncertainty = %s' % ( self.value.info( significantDigits = significantDigits ), \ self.unit, self.uncertainty.info( significantDigits = significantDigits ) ) )
[docs] def isCompatible( self, unit ) : """ Returns True if self's unit is compatible with unit. :param unit: a unit :type unit: `str` :rtype: `bool` """ unit = _findUnit( unit ) return( self.unit.isCompatible( unit ) )
[docs] def isPercent( self ) : """Returns True if value is a percent and False otherwise. :rtype: `bool` """ return( self.value.isPercent( ) )
[docs] def isDimensionless( self ) : """Returns True if self is dimensionless and False otherwise. :rtype: `bool` """ return( self.unit.isDimensionless( ) )
[docs] def isLength( self ) : """Returns True if self has unit of length and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'm' ) )
[docs] def isMass( self ) : """Returns True if self has unit of mass and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'g' ) )
[docs] def isTime( self ) : """Returns True if self has unit of time and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 's' ) )
[docs] def isCurrent( self ) : """Returns True if self has unit of current and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'A' ) )
[docs] def isTemperature( self ) : """Returns True if self has unit of temperature and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'K' ) )
[docs] def isMoles( self ) : """Returns True if self has unit of mol and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'mol' ) )
[docs] def isLuminousIntensity( self ) : """Returns True if self has unit of luminous intensity and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'cd' ) )
[docs] def isAngle( self ) : """Returns True if self has unit of angle and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'rad' ) )
[docs] def isSolidAngle( self ) : """Returns True if self has unit of solid angle and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'sr' ) )
[docs] def isEnergy( self ) : """Returns True if self has unit of energy and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'J' ) )
[docs] def isSpin( self ) : """Returns True if self has unit of spin (i.e., same as 'hbar') and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'hbar' ) )
[docs] def isCharge( self ) : """ Returns True if self has unit of charge and False otherwise. :rtype: `bool` """ return( self.isPhysicalUnitOf( self, 'e' ) )
[docs] def setPercentFlag( self, isPercent, scaleValue = False ) : """ Changes the percent flag only when self is dimensionless and isPercent != self.isPercent( ). Otherwise, a raise is executed. For scaleValue argument see :py:meth:`pqu_float.setPercentFlag`. :param isPercent: See :py:meth:`pqu_float.setPercentFlag` :param scaleValue: See :py:meth:`pqu_float.setPercentFlag` """ if( not( self.isDimensionless( ) ) ) : raise Exception( 'Can only set/unset percent on a dimensionless PQU: unit = "%s"' % self.unit ) self.value.setPercentFlag( isPercent, scaleValue ) if( scaleValue and not( self.uncertainty.isPercent( ) ) ) : self.uncertainty.value.setPercentFlag( True, isPercent ) self.uncertainty.value.setPercentFlag( False, not( isPercent ) )
[docs] def simplify( self ) : """ This method returns a PQU instance that is the same as self but with its units simplified. This is, units that have the same base units are reduce to one of those units. For example, if self is equivalent to '1.2 km/m**3' then the returned instance will be equivalent to '1.2e3 / m**2' or '1.2e9 / km**2' (the actual units returned are not guaranteed). As another example, if self is equivalent to '1.2 ft**3 / lb / s / ( m**2 / kg )' then one returned outcome is '0.2457793723470261 ft/s'. That is, the 'ft' and 'm' units have the same base units and are all converted to 'ft' (in this case) and the 'lb' and 'kg' are all converted to 'lb' (or 'kg'). """ return( self * self.unit.simplifyUnitsFactor( ) )
[docs] def toString( self, significantDigits = None, keepPeriod = True ) : """ Returns a string representation of self (e.g., '35 +/- 1.2% J'). :rtype: `str` """ trimZeros = self.value.getSignificantDigits( ) > maxSignificantDigits includePercent, str1, str2 = True, '', '' uncertainty = self.uncertainty if( self.isPercent( ) ) : if( self.uncertainty.style == pqu_uncertainty.pqu_uncertaintyStylePlusMinus ) : str1, str2, includePercent = '(', ')%', False uncertainty = uncertainty * 100 elif( self.uncertainty.style == pqu_uncertainty.pqu_uncertaintyStyleParenthesis ) : str2, includePercent = '%', False str1 += self.value.toString( trimZeros = trimZeros, includePercent = includePercent, significantDigits = significantDigits, keepPeriod = keepPeriod ) \ + uncertainty.toString( significantDigits = significantDigits ) str3 = str( self.unit ) if( str3 != '' ) : str1 += ' ' return( str1 + str2 + str3 )
[docs] def truncate( self, value = False, uncertainty = True ) : """ If value is True, calls self.value's truncate method. If uncertainty is True, calls self.uncertainty's truncate method. See :py:meth:`pqu_float.truncate` :param value: If True self's value is truncated :type value: `bool` :param uncertainty: If True self's uncertainty is truncated :type uncertainty: `bool` """ if( value ) : self.value.truncate( ) if( uncertainty ) : self.uncertainty.truncate( )
[docs] def sqrt( self ) : """Returns a PQU of the square root of self.""" return( pow( self, 0.5 ) )
[docs] def sin( self ) : """Returns a PQU of the sin of self. Self must have unit of angle.""" if( self.unit.isAngle( ) ) : return( math.sin( self.value * self.unit.conversionFactorTo( _unit_table['rad'] ) ) ) raise TypeError( 'Argument of sin must be an angle.' )
[docs] def cos( self ) : """Returns a PQU of the cos of self. Self must have unit of angle.""" if( self.unit.isAngle( ) ) : return( math.cos( self.value * self.unit.conversionFactorTo( _unit_table['rad'] ) ) ) raise TypeError( 'Argument of cos must be an angle.' )
[docs] def tan( self ) : """Returns a PQU of the tan of self. Self must have unit of angle.""" if( self.unit.isAngle( ) ) : return( math.tan( self.value * self.unit.conversionFactorTo( _unit_table['rad'] ) ) ) raise TypeError( 'Argument of tan must be an angle.' )
@staticmethod def _getOtherAsPQU( other ) : """ Returns a PQU representation of other. If other is a PQU instance, it is returned (i.e., no copy is made). Otherwise, the PQU.__init__ method is called with only other as an argument and its return value returned. :param other: a PQU equivalent to convert, if needed, to a PQU instance :returns: a PQU instance equivalent to other """ if( isinstance( other, PQU ) ) : return( other ) return( PQU( other ) )
[docs] @staticmethod def isPhysicalUnitOf( elf, unit ) : """ Returns True if elf has the same unit as the argument unit and False otherwise. Elf and unit can be an instance of string, PhysicalUnit or PQU. :param elf: The instance to compare to unit :type elf: a unit equivalent :param unit: The desired unit :type unit: a unit equivalent :rtype: `bool` """ elfUnit = _getPhysicalUnit( elf ) unitUnit = _getPhysicalUnit( unit ) return( elfUnit.isCompatible( unitUnit ) )
PhysicalQuantityWithUncertainty = PQU # This is deprecated.
[docs]class parsers : """ .. rubric:: Regular Expression Matching """ _spaces = r'[ \t]*' _anything_RE = '(.*)' _sign = '[+-]?' # Optional sign regular expression. _unsignedInteger = '\d+' _signedInteger = _sign + _unsignedInteger _mantissa = '(%s)((\d+)\.?(\d*)|\.(\d+))' % _sign # Regular expression matching a decimal or an integer string. _exponent = '([eE](%s\d+))?' % _sign # Optional exponent regular expression. _floatingPoint_RE = '(%s%s)' % ( _mantissa, _exponent ) # Floating point regular expression. _floatingPoint_PO = re.compile( '^' + _spaces + _floatingPoint_RE + _spaces + '$' ) _floatingPoint_andAnythingElse_RE = '^' + _floatingPoint_RE + _anything_RE _floatingPoint_andAnythingElse_PO = re.compile( _floatingPoint_andAnythingElse_RE ) _uncertaintyPlusMinus_andAnythingElse_RE = '^' + _spaces + r'\+/-' + _spaces + _floatingPoint_RE + _anything_RE _uncertaintyPlusMinus_andAnythingElse_PO = re.compile( _uncertaintyPlusMinus_andAnythingElse_RE ) _uncertaintyParenthesis_andAnythingElse_RE = r'^[(](\d\d?)[)]' + _anything_RE _uncertaintyParenthesis_andAnythingElse_PO = re.compile( _uncertaintyParenthesis_andAnythingElse_RE ) _unitCharacters_RE = '([ a-zA-Z*/()]*)' # This does not match a valid unit, it only match the part of a # string that contains characters that can be present in a unit string.
[docs] @staticmethod def parseFloat( str1, fullStr ) : """ This method parses the beginning of str1 for a valid float. An arbitrary number of white spaces can exists before the float characters. This method returns a tuple of length 3. The first item of the tuple is the float value returned as a pqu_float instance with significantDigits determined from the string. The second item is significantDigitsForZero which, if the string represents a 0 value, gives the number of significant digits for the zero; otherwise None is returned. As example, the string '000.000' has 4 significant digits (i.e., it is considered equivalent to the representation '0.000'). The last object is the string of all characters after the last one used for converting the float. For example, with str1 = ' 12.345e3ABCD XYS' the returned tuple is: ( pqu_float( 1.2345e4, 5 ), None, 'ABCD XYS' ). :param str1: String to parse. :param fullStr: String which str1 is a part of. :rtype: ( pqu_float, integer | None, string ) """ """ The following table list the components of the groups returned by the regular expression match. Group | index | contents ------+------------------------------------------------------------------------------------- 0 | Float (e.g., for '12.34e-12 +/- 6.7e-13 m / s**2' this would be '12.34e-12') 1 | Float's sign 2 | Mantissa part of float 3 | Float's digits to the left of the period (e.g., for '12.34e-12' this would be '12') 4 | Float's digits to the right of the period if there are digits to left of period (e.g., for '12.34e-12' this would be '34') 5 | Float's digits to the right of the period if there are no digits to left of period (e.g., for '.56e-12' this would be '56') 6 | Float's exponent (e.g., for '12.34e-12' this would be 'e-12') 7 | Float's exponent value (e.g., for '12.34e-12' this would be '-12') 8 | Everything after the float """ if( not( isinstance( str1, str ) ) ) : raise TypeError( 'Argument must be a string: type = %s' % type( str1 ) ) match = parsers._floatingPoint_andAnythingElse_PO.match( str1 ) if( match is None ) : raise TypeError( 'String does not start with a float: "%s" of "%s"' % ( str1, fullStr ) ) groups = match.groups( ) value, order, significantDigits, significantDigitsForZero = parsers._parseFloatGroups( groups ) return( pqu_float( value, significantDigits, False, checkOrder = False ), significantDigitsForZero, groups[8] )
[docs] @staticmethod def parsePlusMinusUncertainty( str1, fullStr ) : """ This method parses the beginning of str1 for the sub-string '+/-' followed by a float string. An arbitrary number of white spaces can exists before the '+/-' sub-string and between it and the float string. This method returns a tuple of length 2. The first item of the tuple is float value returned as a pqu_uncertainty of style pqu_uncertaintyStylePlusMinus. Characters after the last one used for converting the float are returned as the second item. For example, with str1 = ' +/- 12. ABCD XYS' the returned tuple is: ( pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStylePlusMinus, 12. ), ' ABCD XYS' ) :param str1: String to parse. :param fullStr: String which str1 is a part of. :rtype: ( pqu_uncertainty, string ) """ if( not( isinstance( str1, str ) ) ) : raise TypeError( 'Argument must be a string: type = %s' % type( str1 ) ) match = parsers._uncertaintyPlusMinus_andAnythingElse_PO.match( str1 ) if( match is None ) : raise TypeError( 'String does not match "+/-" with a float: "%s" of "%s"' % ( str1, fullStr ) ) groups = match.groups( ) value, order, significantDigits, significantDigitsForZero = parsers._parseFloatGroups( groups ) uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStylePlusMinus, value, significantDigits, checkOrder = False ) return( uncertainty, groups[8] )
[docs] @staticmethod def parseParenthesisUncertainty( str1, fullStr ) : """ This method parses the beginning of str1 for the sub-string '(#)' where '#' is a 1 or 2 digit number. This method returns a tuple of length 2. The first item of the tuple is the number returned as a pqu_uncertainty of style pqu_uncertaintyStyleParenthesis. Characters after the ')' are returned as the second item. For example, with str1 = '(34) ABCD XYS' the returned tuple is: ( pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleParenthesis, 34, significantDigits = 2 ), ' ABCD XYS' ). :param str1: String to parse. :param fullStr: String which str1 is a part of. :rtype: ( pqu_uncertainty, string ) """ if( not( isinstance( str1, str ) ) ) : raise TypeError( 'Argument must be a string: type = %s' % type( str1 ) ) match = parsers._uncertaintyParenthesis_andAnythingElse_PO.match( str1 ) if( match is None ) : raise TypeError( 'String does not match "(#)" or "(##)" where "#" is a digit: "%s" or "%s"' % ( str1, fullStr ) ) groups = match.groups( ) uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleParenthesis, float( groups[0] ), len( groups[0] ), checkOrder = False ) return( uncertainty, groups[1] )
@staticmethod def _parseFloatGroups( groups ) : """ For internal use. This method analyzes the groups to determine the properties of a float and returns its value, order, significantDigits and significantDigitsForZero. """ significantDigitsForZero = None if( groups[0] == '' ) : return( None, None, None, None ) if( float( groups[2] ) == 0. ) : # Mantissa is zero. significantDigits, index = 1, 4 if( groups[3] is None ) : index = 5 significantDigitsForZero = len( groups[index] ) # Needed for zero with () style uncertainty (e.g., "0.0000(4)"). else : mantissa = groups[5] if( mantissa is None ) : # Mantissa has digits to the left and maybe right of the period (e.g., "123.45"). mantissa = groups[3] if( groups[4] is not None ) : mantissa += groups[4] significantDigits = len( mantissa.lstrip( '0' ) ) value, order = pqu_float.fixFloatsOrder( float( groups[0] ) ) return( value, order, significantDigits, significantDigitsForZero )
[docs] @staticmethod def parsePQUString( str1 ) : """ Parses the string str1 and returns the tuple ( value, unit, uncertainty ) where value is an instance of pqu_float, unit is an instance of PhysicalUnit and uncertainty is an instance of pqu_uncertainty. The string can be one of the following PQU forms. Here, F represents a valid float string, () represents the string '(#)' where '#' is a 1 or 2 digit number, +/- represents the string '+/- F', % is itself and u is a unit. +----+----------+----------------+---------+---------------------+---------------------+------+ | # | Form | Example | Value | Value - uncertainty | Value + uncertainty | Unit | +====+==========+================+=========+=====================+=====================+======+ | 0 | F | '234' | 234. | N/A | N/A | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 1 | F% | '234%' | 2.34 | N/A | N/A | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 2 | F u | '234 m' | 234. | N/A | N/A | 'm' | +----+----------+----------------+---------+---------------------+---------------------+------+ | 3 | F() | '234(5)' | 234. | 229. | 239. | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 4 | F()% | '234(5)%' | 2.34 | 2.29 | 2.39 | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 5 | F() u | '234(5) m' | 234. | 229. | 239. | 'm' | +----+----------+----------------+---------+---------------------+---------------------+------+ | 6 | F +/- | '234 +/- 5' | 234. | 229. | 239. | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 7 | F +/-% | '234 +/- 5%' | 234. | 222. | 246. | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ | 8 | F +/- u | '234 +/- 5 m' | 234. | 229. | 239. | 'm' | +----+----------+----------------+---------+---------------------+---------------------+------+ | 9 | F +/-% u | '234 +/- 5% m' | 234. | 222. | 246. | 'm' | +----+----------+----------------+---------+---------------------+---------------------+------+ | 10 | (F +/-)% | '(234 +/- 5)%' | 2.34 | 2.29 | 2.39 | N/A | +----+----------+----------------+---------+---------------------+---------------------+------+ Note 1) 5% of 234 is 11.7 rounded to 12. :param str1: String to parse. :rtype: ( pqu_float, PhysicalUnit, pqu_uncertainty ) """ str2, form10, isPercent = str1.strip( ), False, False if( str2[:1] == "(" ) : form10, str2 = True, str2[1:] value, significantDigitsForZero, str2 = parsers.parseFloat( str2, str1 ) if( form10 ) : uncertainty, str2 = parsers.parsePlusMinusUncertainty( str2, str1 ) uncertainty.value *= 0.01 str2 = str2.strip( ) if( str2 != ')%' ) : raise TypeError( 'input not of the form "(F +/- F)%' ) isPercent, str2 = True, '' else : uncertainty = pqu_uncertainty( pqu_uncertainty.pqu_uncertaintyStyleNone ) if( str2[:1] == '%' ) : # The '%' must immediately follow the float string. isPercent, str2 = True, str2[1:] else : str2 = str2.strip( ) if( str2[:1] == '(' ) : # Must be '()' style uncertainty. uncertainty, str2 = parsers.parseParenthesisUncertainty( str2, str1 ) if( str2[:1] == '%' ) : isPercent, str2 = True, str2[1:] if( significantDigitsForZero is not None ) : value.setSignificantDigits( significantDigitsForZero + 1 ) elif( str2[:1] == '+' ) : # Must be '+/-' style uncertainty. uncertainty, str2 = parsers.parsePlusMinusUncertainty( str2, str1 ) if( str2[:1] == '%' ) : # The '%' must immediately follow the float string. str2 = str2[1:] uncertainty.value.setPercentFlag( True, scaleValue = True ) str2 = str2.strip( ) if( isPercent ) : # str2 must now be empty as units are not allowed with "%". if( len( str2 ) > 0 ) : raise TypeError( 'value with percent cannot have units: "%s"' % str1 ) value.setPercentFlag( True, scaleValue = True ) try : unit = _findUnit( str2 ) except : raise TypeError( 'Invalid unit "%s", in PQU string "%s"' % ( str2, str1 ) ) return( value, unit, uncertainty )
[docs]class PhysicalUnit : """ .. rubric:: PHYSICAL UNIT A physical unit is defined by a symbol (possibly composite), a scaling factor, and the powers of each of the SI base units that enter into it. Units can be multiplied, divided, and raised to integer powers. :param symbols: a dictionary mapping each symbol component to its associated integer power (e.g., ``{'m': 1, 's': -1}``) for `m/s`). As a shorthand, a string may be passed which is assigned an implicit power 1. :type symbols: `dict` or `str` :param factor: a scaling factor :type factor: `float` :param offset: an additive offset to the base unit (used only for temperatures) :type offset: `float` :param powers: the integer powers for each of the nine base units :type powers: `list` of `int` """ def __init__( self, symbols, factor, powers, offset = 0 ) : if( symbols is not None ) : if( '1' in symbols ) : rmOne = False for key in symbols: if( symbols[key] > 0 ) : rmOne = True if( rmOne ) : del symbols['1'] if( isinstance( symbols, str ) ) : self.symbols = NumberDict( ) self.symbols[symbols] = 1 else : if( symbols is None ) : symbols = {} self.symbols = symbols if( '' in self.symbols ) : del self.symbols[''] self.factor = factor self.offset = offset self.powers = powers def __repr__( self ) : return( self.__class__.__name__ + "('" + self.symbol( ) + "')" ) def __hash__( self ) : return( hash( self.factor ) + hash( self.offset ) + hash( tuple( self.powers ) ) ) def __lt__( self, other ) : if( self.powers != other.powers ) : raise TypeError( 'Incompatible units' ) return( self.factor.__lt__( other.factor ) ) def __eq__( self, other ) : if( self.powers != other.powers ) : raise TypeError( 'Incompatible units' ) return( self.factor.__eq__( other.factor ) ) def __ne__( self, other ) : if( self.powers != other.powers ) : raise TypeError( 'Incompatible units' ) return( self.factor.__ne__( other.factor ) ) def __mul__( self, other ) : if( ( self.offset != 0 ) or ( isinstance( other, PhysicalUnit ) and ( other.offset != 0 ) ) ) : raise TypeError( "cannot multiply units with non-zero offset" ) if( isinstance( other, PhysicalUnit ) ) : return( PhysicalUnit( self.symbols + other.symbols, self.factor * other.factor, list( map( lambda a, b: a + b, self.powers, other.powers ) ) ) ) else: return( PhysicalUnit( self.symbols + { str( other ) : 1 }, self.factor * other, self.powers ) ) __rmul__ = __mul__ def __truediv__( self, other ) : if( ( self.offset != 0 ) or ( isinstance( other, PhysicalUnit ) and ( other.offset != 0 ) ) ) : raise TypeError( " cannot divide units with non-zero offset" ) if( isinstance( other, PhysicalUnit ) ) : return( PhysicalUnit( self.symbols - other.symbols, self.factor / other.factor, list( map( lambda a, b: a - b, self.powers, other.powers ) ) ) ) else: return( PhysicalUnit( self.symbols + { str( other ) : -1 }, self.factor / other, self.powers ) ) def __rtruediv__( self, other ) : if( ( self.offset != 0 ) or ( isinstance( other, PhysicalUnit ) and ( other.offset != 0 ) ) ) : raise TypeError( "cannot divide units with non-zero offset" ) if( isinstance( other, PhysicalUnit ) ) : return( PhysicalUnit( other.symbols - self.symbols, other.factor / self.factor, list( map( lambda a, b: a - b, other.powers, self.powers ) ) ) ) else: return( PhysicalUnit( NumberDict( { str( other ) : 1 } ) - self.symbols, other / self.factor, [ -x for x in self.powers ] ) ) __div__ = __truediv__ # for python 2.x __rdiv__ = __rtruediv__ def __pow__( self, other ) : if( self.offset != 0 ) : raise TypeError( "cannot exponentiate units with non-zero offset" ) if( not( isinstance( other, int ) ) ) : # See if very close to an integer. power = float( other ) rounded = int( math.floor( power + 0.5 ) ) if( abs( power - rounded ) < 1.e-14 ) : other = rounded # If not close, revert back to non-int so handled as inverse later. if( isinstance( other, int ) ) : return( PhysicalUnit( other * self.symbols, pow( self.factor, other ), list( map( lambda x, p = other: x * p, self.powers ) ) ) ) if( isinstance( other, float ) ) : # See if inverse (e.g., 1. / 3.) inv_exp = 1. / other rounded = int( math.floor( inv_exp + 0.5 ) ) if( abs( inv_exp-rounded ) < 1.e-10 ) : if reduce( lambda a, b: a and b, list( map( lambda x, e = rounded: x % e == 0, self.powers ) ) ) : f = pow( self.factor, other ) p = list( map( lambda x, p = rounded: x / p, self.powers ) ) if reduce( lambda a, b: a and b, list( map( lambda x, e = rounded: x % e == 0, list( self.symbols.values( ) ) ) ) ) : symbols = self.symbols / rounded else: symbols = NumberDict( ) if f != 1.: symbols[str( f )] = 1 for i in range( len( p ) ) : symbols[_base_symbols[i]] = p[i] return PhysicalUnit( symbols, f, p ) else: raise TypeError( 'Illegal exponent' ) raise TypeError( 'Only integer and inverse integer exponents are allowed' ) def __deepcopy__( self ) : other = self.__new__( PhysicalUnit ) other.symbols, other.factor, other.offset, other.powers = self.symbols.__deepcopy__( ), self.factor, self.offset, self.powers[:] return( other ) __copy__ = __deepcopy__
[docs] def conversionFactorTo( self, other ) : """ :param other: another unit :type other: `PhysicalUnit` :returns: the conversion factor from this unit to another unit :rtype: `float` :raises TypeError: if the units are not compatible """ other = _getPhysicalUnit( other ) if( self.powers != other.powers ) : raise TypeError( 'Incompatible units: cannot convert "%s" to "%s"' % ( str( self ), str( other ) ) ) if( ( self.offset != other.offset ) and ( self.factor != other.factor ) ) : raise TypeError( 'Unit conversion (%s to %s) cannot be expressed as a simple multiplicative factor' % ( self.symbol( ), other.symbol( ) ) ) return( self.factor / other.factor )
[docs] def conversionTupleTo( self, other ) : """ :param other: another unit :type other: `PhysicalUnit` :returns: the conversion factor and offset from this unit to another unit :rtype: (`float`, `float`) :raises TypeError: if the units are not compatible """ other_ = _getPhysicalUnit( other ) if( self.powers != other_.powers ) : raise TypeError( 'Unit "%s" not convertible with "%s"' % ( self, other ) ) # Let (s1,d1) be the conversion tuple from 'self' to base units (i.e. (x+d1)*s1 converts a value x from 'self' to base units, # and (x/s1)-d1 converts x from base to 'self' units) and (s2,d2) be the conversion tuple from 'other' to base units then # we want to compute the conversion tuple (S,D) from 'self' to 'other' such that (x+D)*S converts x from 'self' units to # 'other' units the formula to convert x from 'self' to 'other' units via the base units is (by definition of the conversion tuples): # ( ((x+d1)*s1) / s2 ) - d2 # = ( (x+d1) * s1/s2) - d2 # = ( (x+d1) * s1/s2 ) - (d2*s2/s1) * s1/s2 # = ( (x+d1) - (d1*s2/s1) ) * s1/s2 # = (x + d1 - d2*s2/s1) * s1/s2 # thus, D = d1 - d2*s2/s1 and S = s1/s2 factor = self.factor / other_.factor offset = self.offset - ( other_.offset * other_.factor / self.factor ) return( factor, offset )
[docs] def isCompatible( self, other ) : """ Returns True if self is compatible with other and False otherwise. :param other: another unit :type other: `PhysicalUnit` :returns: `True` if the units are compatible, i.e. if the powers of the base units are the same :rtype: `bool` """ return( self.powers == other.powers )
[docs] def isDimensionless( self ) : """Returns True if self is dimensionless and False otherwise. :rtype: `bool` """ return( not reduce( lambda a, b: a or b, self.powers ) )
[docs] def isAngle( self ) : """Returns True if self is an angle and False otherwise. :rtype: `bool` """ return self.powers[7] == 1 and reduce( lambda a, b: a + b, self.powers ) == 1
[docs] def setSymbol( self, symbol ) : """ Sets self's symbols to symbol with power 1. :param symbol: :type symbol: `str` """ self.symbols = NumberDict( ) self.symbols[symbol] = 1
[docs] def simplifyUnitsFactor( self ) : """ This method returns a PQU instance that can be used to simplify self's units. That is, all units with the same base units are reduced to a single unit. For example, if self has units of 'km/m' then the returned PQU is equivalent to '1e3 m/km'. """ ignores = [] result = PQU( 1 ) keys = self.symbols.keys( ) for i1, key in enumerate( keys ) : if( i1 in ignores ) : continue ignores.append( i1 ) u1 = PQU( 1, key ) for i2 in range( i1 + 1, len( keys ) ) : if( i2 in ignores ) : continue if( u1.isCompatible( keys[i2] ) ) : ignores.append( i2 ) power2 = self.symbols[keys[i2]] factor = PQU( 1, keys[i2] ).getValueAs( key )**power2 result *= factor * PQU( 1, key )**power2 / PQU( 1, keys[i2] )**power2 return( result )
[docs] def symbol( self ) : """ Returns a string representation of self. :returns: a string representation of self. :rtype: `str` """ num = '' denom = '' for unit in self.symbols : power = self.symbols[unit] if( power < 0 ) : denom = denom + '/' + unit if( power < -1 ) : denom = denom + '**' + str( -power ) elif( power > 0 ) : num = num + '*' + unit if( power > 1 ) : num = num + '**' + str( power ) if( len( num ) == 0 ) : if( len( denom ) > 0 ) : num = '1' else: num = num[1:] return num + denom
def __str__( self ) : return( self.symbol( ) )
# # Helper functions # def _findUnit( unit ) : if( isinstance( unit, str ) ) : symbol = unit.strip( ) if( symbol == '' ) : unit = _unit_table[symbol] else: unit = eval( symbol, _unit_table ) for cruft in ['__builtins__', '__args__'] : try: del _unit_table[cruft] except: pass if( not( isinstance( unit, PhysicalUnit ) ) ) : raise TypeError( '%s is not a unit' % type( unit ) ) return( unit ) def _round( x ) : if( x > 0. ) : x_ = math.floor( x ) else: x_ = math.ceil( x ) if( isinstance( x, pqu_float ) ) : x_ = pqu_float( x_, x.getSignificantDigits( ) ) return( x_ ) def _getPhysicalUnit( elf ) : if( isinstance( elf, PQU ) ) : return( elf.unit ) try : return( _findUnit( elf ) ) except : return( PQU( elf ).unit ) # SI unit definitions _base_symbols = [ 'm', 'kg', 's', 'A', 'K', 'mol', 'cd', 'rad', 'sr' ] _base_units = [ ( 'm', PhysicalUnit( 'm', 1., [ 1, 0, 0, 0, 0, 0, 0, 0, 0 ] ) ), ( 'g', PhysicalUnit( 'g', 0.001, [ 0, 1, 0, 0, 0, 0, 0, 0, 0 ] ) ), ( 's', PhysicalUnit( 's', 1., [ 0, 0, 1, 0, 0, 0, 0, 0, 0 ] ) ), ( 'A', PhysicalUnit( 'A', 1., [ 0, 0, 0, 1, 0, 0, 0, 0, 0 ] ) ), ( 'K', PhysicalUnit( 'K', 1., [ 0, 0, 0, 0, 1, 0, 0, 0, 0 ] ) ), ( 'mol', PhysicalUnit( 'mol', 1., [ 0, 0, 0, 0, 0, 1, 0, 0, 0 ] ) ), ( 'cd', PhysicalUnit( 'cd', 1., [ 0, 0, 0, 0, 0, 0, 1, 0, 0 ] ) ), ( 'rad', PhysicalUnit( 'rad', 1., [ 0, 0, 0, 0, 0, 0, 0, 1, 0 ] ) ), ( 'sr', PhysicalUnit( 'sr', 1., [ 0, 0, 0, 0, 0, 0, 0, 0, 1 ] ) ), ] _prefixes = [( 'Y', 1.e24, 'yotta' ), ( 'Z', 1.e21, 'zetta' ), ( 'E', 1.e18, 'exa' ), ( 'P', 1.e15, 'peta' ), ( 'T', 1.e12, 'tera' ), ( 'G', 1.e9, 'giga' ), ( 'M', 1.e6, 'mega' ), ( 'k', 1.e3, 'kilo' ), ( 'h', 1.e2, 'hecto' ), ( 'da', 1.e1, 'deka' ), ( 'd', 1.e-1, 'deci' ), ( 'c', 1.e-2, 'centi' ), ( 'm', 1.e-3, 'milli' ), ( 'mu', 1.e-6, 'micro' ), ( 'n', 1.e-9, 'nano' ), ( 'p', 1.e-12, 'pico' ), ( 'f', 1.e-15, 'femto' ), ( 'a', 1.e-18, 'atto' ), ( 'z', 1.e-21, 'zepto' ), ( 'y', 1.e-24, 'yocto' ), ] _help = [] _unit_table = {} for unit in _base_units: _unit_table[unit[0]] = unit[1] def _addUnit( symbol, unit, name = '' ) : if( symbol in _unit_table ) : raise KeyError( 'Unit ' + symbol + ' already defined' ) if( name ) : _help.append( ( name, unit, symbol ) ) if( isinstance( unit, str ) ) : unit = eval( unit, _unit_table ) for cruft in ['__builtins__', '__args__'] : try : del _unit_table[cruft] except : pass unit.setSymbol( symbol ) _unit_table[symbol] = unit def _addPrefixedUnit( unit ) : _prefixed_symbols = [] for prefix in _prefixes: symbol = prefix[0] + unit _addUnit( symbol, prefix[1] * _unit_table[unit] ) _prefixed_symbols.append( symbol ) for entry in _help: if isinstance( entry, tuple ) : if len( entry ) == 3: name0, unit0, symbol0 = entry i = _help.index( entry ) if unit == symbol0: _help.__setitem__( i, ( name0, unit0, symbol0, _prefixed_symbols ) ) # SI derived units; these automatically get prefixes _help.append( '----------------------------------------------------\nDefined prefixes and units\n----------------------------------------------------' ) _help.append( 'This section outlines the prefixes and unit defined by the PQU module as well as the values used for the physical constant.' ) _help.append( '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nPrefixes\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^' ) _help.append( '.. highlights::\n\n::' ) _help.append( ' ---- ------ ------\n' + ' name factor symbol\n' + ' ---- ------ ------' ) _help_prefix = [] for _prefix in _prefixes : _help.append( ( '%-5s %-6.0e %-s' % ( _prefix[2], _prefix[1], _prefix[0] ), '', '' ) ) _help.append( '\n\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nDerived SI units\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^' ) _help.append( 'These automatically get prefixes.' ) _help.append( '.. highlights::\n\n::' ) _help.append( ' %-26s %-32s %-s\n %-26s %-32s %-s\n %-26s %-32s %-s' % ( '--------------------------', '--------------------------------', '------', 'name', 'value', 'symbol', '--------------------------', '--------------------------------', '------' ) ) _unit_table['kg'] = PhysicalUnit( 'kg', 1., [ 0, 1, 0, 0, 0, 0, 0, 0, 0 ] ) _addUnit( 'Hz', '1/s', 'Hertz' ) _addUnit( 'N', 'm*kg/s**2', 'Newton' ) _addUnit( 'Pa', 'N/m**2', 'Pascal' ) _addUnit( 'J', 'N*m', 'Joule' ) _addUnit( 'W', 'J/s', 'Watt' ) _addUnit( 'C', 's*A', 'Coulomb' ) _addUnit( 'V', 'W/A', 'Volt' ) _addUnit( 'F', 'C/V', 'Farad' ) _addUnit( 'ohm', 'V/A', 'Ohm' ) _addUnit( 'S', 'A/V', 'Siemens' ) _addUnit( 'Wb', 'V*s', 'Weber' ) _addUnit( 'T', 'Wb/m**2', 'Tesla' ) _addUnit( 'H', 'Wb/A', 'Henry' ) _addUnit( 'lm', 'cd*sr', 'Lumen' ) _addUnit( 'lx', 'lm/m**2', 'Lux' ) _addUnit( 'Bq', '1/s', 'Becquerel' ) _addUnit( 'Gy', 'J/kg', 'Gray' ) _addUnit( 'Sv', 'J/kg', 'Sievert' ) del _unit_table['kg'] for unit in list( _unit_table.keys( ) ) : _addPrefixedUnit( unit ) # NOTE everything below does not have prefixes added to units # Dimensionless quantity _unit_table[''] = PhysicalUnit( '', 1., [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ) # Fundamental constants _help.append( '\n\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nFundamental constants\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^' ) _help.append( '.. highlights::\n\n::' ) _help.append( ' Fundamental constants ............................................ ' ) from . import pqu_constants _unit_table['pi'] = math.pi _addUnit( 'c', pqu_constants.c, 'speed of light' ) _addUnit( 'mu0', pqu_constants.mu0, 'permeability of vacuum' ) _addUnit( 'eps0', '1 / mu0 / c**2', 'permittivity of vacuum' ) _addUnit( 'Grav', pqu_constants.Grav, 'gravitational constant' ) _addUnit( 'hplanck', pqu_constants.hplanck, 'Planck constant' ) _addUnit( 'hbar', 'hplanck / ( 2 * pi )', 'Planck constant / 2pi' ) _addUnit( 'e', pqu_constants.e, 'elementary charge' ) _addUnit( 'me', pqu_constants.me, 'electron mass' ) _addUnit( 'mp', pqu_constants.mp, 'proton mass' ) _addUnit( 'Nav', pqu_constants.Nav, 'Avogadro number' ) _addUnit( 'k', pqu_constants.k, 'Boltzmann constant' ) # Time units _help.append( '\n\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nMore units\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^' ) _help.append( '.. highlights::\n\n::' ) _help.append( ' Time units ....................................................... ' ) _addUnit( 'min', '60.*s', 'minute' ) _addUnit( 'h', '60.*min', 'hour' ) _addUnit( 'd', '24.*h', 'day' ) _addUnit( 'wk', '7.*d', 'week' ) _addUnit( 'yr', '365.25*d', 'year' ) # Length units _help.append( ' Length units ..................................................... ' ) _addUnit( 'inch', '2.54 * cm', 'inch' ) _addUnit( 'ft', '12. * inch', 'foot' ) _addUnit( 'yd', '3. * ft', 'yard' ) _addUnit( 'mi', '5280. * ft', '(British) mile' ) _addUnit( 'nmi', '1852. * m', 'Nautical mile' ) _addUnit( 'Ang', '1.e-10 * m', 'Angstrom' ) _addUnit( 'lyr', 'c * yr', 'light year' ) _addUnit( 'au', '149597870700. * m', 'Astronomical unit' ) _addUnit( 'Bohr', '4. * pi * eps0 * hbar**2/me/e**2', 'Bohr radius' ) # Area units _help.append( ' Area units ....................................................... ' ) _addUnit( 'ha', '10000*m**2', 'hectare' ) _addUnit( 'acres', 'mi**2/640', 'acre' ) _addUnit( 'b', '1.e-28*m**2', 'barn' ) _addPrefixedUnit( 'b' ) # Volume units _help.append( ' Volume units ..................................................... ' ) _addUnit( 'l', 'dm**3', 'liter' ) _addUnit( 'dl', '0.1 * l', 'deci liter' ) _addUnit( 'cl', '0.01 * l', 'centi liter' ) _addUnit( 'ml', '0.001 * l', 'milli liter' ) _addUnit( 'tsp', pqu_constants.tsp, 'teaspoon' ) _addUnit( 'tbsp', '3. * tsp', 'tablespoon' ) _addUnit( 'floz', '2. * tbsp', 'fluid ounce' ) _addUnit( 'cup', '8. * floz', 'cup' ) _addUnit( 'pt', '16. * floz', 'pint' ) _addUnit( 'qt', '2. * pt', 'quart' ) _addUnit( 'galUS', '4. * qt', 'US gallon' ) _addUnit( 'galUK', pqu_constants.galUK, 'British gallon' ) # Mass units _help.append( ' Mass units ....................................................... ' ) _addUnit( 'amu', pqu_constants.amu, 'atomic mass units' ) _addUnit( 'oz', pqu_constants.oz, 'ounce' ) _addUnit( 'lb', '16. * oz', 'pound' ) _addUnit( 'ton', '2000. * lb', 'ton' ) # Force units _help.append( ' Force units ...................................................... ' ) _addUnit( 'dyn', '1.e-5 * N', 'dyne (cgs unit)' ) # Energy units _help.append( ' Energy units ..................................................... ' ) _addUnit( 'erg', '1.e-7*J', 'erg (cgs unit)' ) _addUnit( 'eV', 'e*V', 'electron volt' ) _addUnit( 'Hartree', 'me*e**4/16/pi**2/eps0**2/hbar**2', 'Wavenumbers/inverse cm' ) _addUnit( 'Ken', 'k*K', 'Kelvin as energy unit' ) _addUnit( 'cal', pqu_constants.cal, 'thermochemical calorie' ) _addUnit( 'kcal', '1000.*cal', 'thermochemical kilocalorie' ) _addUnit( 'cali', pqu_constants.cali, 'international calorie' ) _addUnit( 'kcali', '1000.*cali', 'international kilocalorie' ) _addUnit( 'Btu', pqu_constants.Btu, 'British thermal unit' ) _addPrefixedUnit( 'eV' ) # Power units _help.append( ' Power units ...................................................... ' ) _addUnit( 'hp', pqu_constants.hp, 'horsepower' ) # Pressure units _help.append( ' Pressure units ................................................... ' ) _addUnit( 'bar', '1.e5*Pa', 'bar (cgs unit)' ) _addUnit( 'atm', pqu_constants.atm, 'standard atmosphere' ) _addUnit( 'torr', 'atm/760', 'torr = mm of mercury' ) _addUnit( 'psi', pqu_constants.psi, 'pounds per square inch' ) # Angle units _help.append( ' Angle units ...................................................... ' ) _addUnit( 'deg', 'pi*rad/180.', 'degrees' ) _help.append( ' Temperature units ................................................ ' ) # Temperature units -- can't use the 'eval' trick that _addUnit provides # for degC and degF because you can't add units Kelvin = _findUnit( 'K' ) _addUnit( 'degR', '(5./9.)*K', 'degrees Rankine' ) _addUnit( 'degC', PhysicalUnit( None, 1.0, Kelvin.powers, 273.15 ), 'degrees Celsius' ) _addUnit( 'degF', PhysicalUnit( None, 5./9., Kelvin.powers, 459.67 ), 'degrees Fahrenheit' ) _help.append( '\n' ) _help.append( '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nPrefixed units\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^' ) _help.append( '.. highlights::\n\n::' ) del Kelvin
[docs]def description( ) : """ Return a string describing all available units. :rtype: `str` """ s = [] # collector for description text for entry in _help : if isinstance( entry, str ) : if entry != None: s.append( '\n' + entry + '\n' ) else: pass elif isinstance( entry, tuple ) : if len( entry ) == 4: name, unit, symbol, prefixes = entry elif len( entry ) == 3: name, unit, symbol = entry s.append( ' %-26s %-32s %-s\n' % ( name, unit, symbol ) ) else: # impossible raise TypeError( 'wrong construction of _help list' ) entry = ' ------------- ------ --------------\n %-13s %-6s prefixed units\n ------------- ------ --------------' % ( 'name', 'symbol' ) s.append( '\n' + entry + '\n' ) for entry in _help: if isinstance( entry, str ) : pass elif isinstance( entry, tuple ) : if len( entry ) == 4: name, unit, symbol, prefixes = entry s.append( ' %-13s %-6s %-s\n %-13s %-6s %-s\n\n' % ( name, symbol, ', '.join( prefixes[0:10] ), ' ', ' ', ', '.join( prefixes[10:20] ) ) ) s.append( '\n' ) return ''.join( s )
# add the description of the units to the module's doc string: __doc__ += '\n' + description( )
[docs]def printPredefinedUnits( ) : units = [] for symbol, unit in _unit_table.iteritems( ) : if( symbol in [ 'pi' ] ) : continue s = " %-10s %20.12e %20.12e" % ( symbol, unit.factor, unit.offset ) for power in unit.powers : s += " %3d" % power units.append( s ) units.sort( ) for unit in units : print unit
[docs]def convertUnits( unit, unitMap ) : PU = _findUnit( unit ) unitString = '' operator = '' for _unit in PU.symbols : try : __unit = unitMap[_unit] except : __unit = _unit unitString += "%s%s**%d" % ( operator, __unit, PU.symbols[_unit] ) operator = ' * ' newUnit = _findUnit( unitString ) factor = PU.conversionFactorTo( newUnit ) return( newUnit.symbol( ), factor )