Skip to main content

physical quantities (numbers with units)

Project description

Version: 1.0.0
Released: 2016-11-26

https://img.shields.io/travis/KenKundert/quantiphy/master.svg https://img.shields.io/coveralls/KenKundert/quantiphy.svg https://img.shields.io/pypi/v/quantiphy.svg https://img.shields.io/pypi/pyversions/quantiphy.svg https://img.shields.io/pypi/dd/quantiphy.svg

Use ‘pip3 install quantiphy’ to install. Requires Python3.3 or better. Python2.7 is also supported, however support for unicode units is weak.

Synopsis

The QuantiPhy package provides the Quantity class that:

  1. accepts real values with units in a variety of common forms, including those that include SI scale factors,

  2. converts them into an object that is treated as a floating point number in expressions,

  3. generally includes the units when printing and by default employs the SI scale factors.

  4. Unit conversion is supported when converting to or from quantities.

Introduction

QuantiPhy is a light-weight package that allows numbers to be combined with units into physical quantities. Physical quantities are very commonly encountered when working with real-world systems when numbers are involved. And when encountered, the numbers often use SI scale factors to make them easier to read and write. Surprisingly, most computer languages do not support numbers in these forms, meaning that when working with physical quantities, one often has to choose what form to use, one that is easy for computers to read or one that is easy for humans to read. For example, consider this table of critical frequencies needed in jitter tolerance measurements in optical communication:

>>> table1 = """
...     SDH     | Rate          | f1      | f2       | f3      | f4
...     --------+---------------|---------|----------|---------|--------
...     STM-1   | 155.52 Mb/s   | 500 Hz  | 6.5 kHz  | 65 kHz  | 1.3 MHz
...     STM-4   | 622.08 Mb/s   | 1 kHz   | 25 kHz   | 250 kHz | 5 MHz
...     STM-16  | 2.48832 Gb/s  | 5 kHz   | 100 kHz  | 1 MHz   | 20 MHz
...     STM-64  | 9.95328 Gb/s  | 20 kHz  | 400 kHz  | 4 MHz   | 80 MHz
...     STM-256 | 39.81312 Gb/s | 80 kHz  | 1.92 MHz | 16 MHz  | 320 MHz
... """

This table was formatted to be easily read by humans. If it were formatted for computers, the numbers would be given without units and in exponential notation because they have dramatically different sizes. For example, it might look like this:

>>> table2 = """
...     SDH     | Rate (b/s)    | f1 (Hz) | f2 (Hz)  | f3 (Hz) | f4 (Hz)
...     --------+---------------|---------|----------|---------|--------
...     STM-1   | 1.5552e8      | 5e2     | 6.5e3    | 6.5e3   | 1.3e6
...     STM-4   | 6.2208e8      | 1e3     | 2.5e3    | 2.5e5   | 5e6
...     STM-16  | 2.48832e9     | 5e3     | 1e5      | 1e6     | 2e7
...     STM-64  | 9.95328e9     | 2e4     | 4e5      | 4e6     | 8e7
...     STM-256 | 3.981312e10   | 8e4     | 1.92e6   | 1.6e7   | 3.20e8
... """

This contains the same information, but it is much harder for humans to read and interpret. Often the compromise of partially scaling the numbers can be used to make the table easier to interpret:

>>> table3 = """
...     SDH     | Rate (Mb/s)   | f1 (kHz)| f2 (kHz) | f3 (kHz)| f4 (MHz)
...     --------+---------------|---------|----------|---------|--------
...     STM-1   | 155.52        | 0.5     | 6.5      | 65      | 1.3
...     STM-4   | 622.08        | 1       | 2.5      | 250     | 5
...     STM-16  | 2488.32       | 5       | 100      | 1000    | 20
...     STM-64  | 9953.28       | 20      | 400      | 4000    | 80
...     STM-256 | 39813.120     | 80      | 1920     | 16000   | 320
... """

This looks cleaner, but it is still involves some effort to interpret because the values are distant from their corresponding scaling and units, because the large and small values are oddly scaled (0.5 kHz is more naturally given as 500Hz and 39813 MHz is more naturally given as 39.8 GHz), and because each column may have a different scaling factor.

All these tables contain the same information, but in the second two tables the readability has been traded off in order to make the data easier to read into a computer.

QuanitiPhy makes it easy to read and generate numbers with units and scale factors so you do not have to choose between human and computer readability. For example, the above tables could be read with the following (it must be tweaked somewhat to handle tables 2 and 3):

>>> from quantiphy import Quantity

>>> sdh = []
>>> lines = table1.strip().split('\n')
>>> for line in lines[2:]:
...     fields = line.split('|')
...     name = fields[0].strip()
...     critical_freqs = [Quantity(f) for f in fields[1:]]
...     sdh.append((name, critical_freqs))
>>> for name, freqs in sdh:
...     print('{:8s}: {:12s} {:9s} {:9s} {}'.format(name, *freqs))
STM-1   : 155.52 Mb/s  500 Hz    6.5 kHz   65 kHz
STM-4   : 622.08 Mb/s  1 kHz     25 kHz    250 kHz
STM-16  : 2.4883 Gb/s  5 kHz     100 kHz   1 MHz
STM-64  : 9.9533 Gb/s  20 kHz    400 kHz   4 MHz
STM-256 : 39.813 Gb/s  80 kHz    1.92 MHz  16 MHz

Quantity is used to convert a number string, such as ‘155.52 Mb/s’ into an internal representation that includes the value and the units: 155.52e6 and ‘b/s’. The scaling factor is properly included. Once a value is converted to a Quantity, it can be treated just like a normal float. The main difference occurs when it is time to convert it back to a string. When doing so, the scale factor and units are included by default.

Quantities

The Quantity class is used to create a quantity (an object with both a value and units). Normally, creating a quantity takes one or two arguments. The first is taken to be the value, and the second, if given, is taken to be the model, which is a source of default values. More on this in a bit, but for the time being you can assume the model is a string that contains the units for the quantity. The value may be given as a float or as a string. The string may be in floating point notation, in scientific notation, or use SI scale factors and may include the units. For example, any of the following ways can be used to specify 1ns:

>>> period = Quantity(1e-9, 's')
>>> print(period)
1 ns

>>> period = Quantity('0.000000001 s')
>>> print(period)
1 ns

>>> period = Quantity('1e-9s')
>>> print(period)
1 ns

>>> period = Quantity('1ns')
>>> print(period)
1 ns

When given as a string, the number may use any of the following scale factors:

Y (1024)
Z (1021)
E (1018)
P (1015)
T (1012)
G (109)
M (106)
k (103)
_ (1)
c (10-2)
% (10-2)
m (10-3)
u (10-6)
μ (10-6)
n (10-9)
p (10-12)
f (10-15)
a (10-18)
z (10-21)
y (10-24)

So far our 1ns is just a value. However, you may also give a name and description. For example:

>>> period = Quantity('Tclk = 10ns -- clock period')
>>> print(period.name, '=', period, ' #', period.desc)
Tclk = 10 ns  # clock period

If you only specify a real number for the value, then the units, name, and description do not get values. This is where the second argument, the model, helps. It may be another quantity or it may be a string. Any attributes that are not provided by the first argument are taken from the second if available. If the second argument is a string, it is split. If it contains one value, that value is taken to be the units, if it contains two, those values are taken to be the name and units, and it it contains more than two, the remaining values are taken to be the description. For example:

>>> out_period = Quantity(10*period, period)
>>> print(out_period.name, '=', out_period, ' #', out_period.desc)
Tclk = 100 ns  # clock period

>>> freq = Quantity(100e6, 'Hz')
>>> print(freq)
100 MHz

>>> freq = Quantity(100e6, 'Fin Hz')
>>> print(freq.name, '=', freq, ' #', freq.desc)
Fin = 100 MHz  #

>>> freq = Quantity(100e6, 'Fin Hz Input frequency')
>>> print(freq.name, '=', freq, ' #', freq.desc)
Fin = 100 MHz  # Input frequency

In addition, you can explicitly specify the units, the name, and the description using named arguments. These values override anything specified in the value or the model.

>>> out_period = Quantity(
...     10*period, period, name='output period',
...     desc='period at output of frequency divider'
... )
>>> print(out_period.name, '=', out_period, ' #', out_period.desc)
output period = 100 ns  # period at output of frequency divider

Finally, you can overwrite the quantities attributes to override the units, name, or description.

>>> out_period = Quantity(10*period)
>>> out_period.units = 's'
>>> out_period.name = 'output period'
>>> out_period.desc = 'period at output of frequency divider'
>>> print(out_period.name, '=', out_period, ' #', out_period.desc)
output period = 100 ns  # period at output of frequency divider

From a quantity object, you access its value in various ways:

>>> h_line = Quantity('1420.405751786 MHz')

>>> h_line.as_tuple()
(1420405751.786, 'Hz')

>>> str(h_line)
'1.4204 GHz'

>>> h_line.render()
'1.4204 GHz'

>>> h_line.render(si=False)
'1.4204e9 Hz'

You can also access the value without the units:

>>> float(h_line)
1420405751.786

>>> h_line.render(False)
'1.4204G'

>>> h_line.render(False, si=False)
'1.4204e9'

Or you can access just the units:

>>> h_line.units
'Hz'

You can also access the full precision of the quantity:

>>> h_line.render(prec='full')
'1.420405751786 GHz'

>>> h_line.render(si=False, prec='full')
'1.420405751786e9 Hz'

Full precision implies whatever precision was used when specifying the quantity if it was specified as a string. If it was specified as a real number, then a fixed, user controllable number of digits are used (default=12). Generally one uses ‘full’ when generating output that will be read by a machine.

If you specify fmt to render, it will generally include the name and perhaps the description if they are available. The formatting is controlled by ‘assign_fmt’, which is described later. With the default formatting, the description is not printed.

>>> h_line.render(fmt=True)
'1.4204 GHz'

>>> out_period.render(fmt=True)
'output period = 100 ns'

Quantities As Reals

You can use a quantity in the same way that you can use a real number, meaning that you can use it in expressions and it will evaluate to its real value:

>>> period = Quantity('1us')
>>> print(period)
1 us

>>> frequency = 1/period
>>> print(frequency)
1000000.0

>>> type(period)
<class 'quantiphy.Quantity'>

>>> type(frequency)
<class 'float'>

Notice that when performing arithmetic operations on quantities the units are completely ignored and do not propagate in any way to the newly computed result.

Rescaling When Creating a Quantity

It may be that a value as given uses inconvenient units. For example, you are given temperature in Fahrenheit, but you would prefer it in Kelvin. Or perhaps you are given mass data in a string that contains kilograms as a simple number (without units or scale factor). In this case you need to convert to grams so that if the SI scale factors you don’t end up with milli-kilograms. To address these issues, use the scale argument to the Quantify class.

For example:

>>> m = Quantity('2.529', scale=1000, units='g')
>>> print(m)
2.529 kg

In this case the value is given in kilograms, and is converted to grams by multiplying the given value by 1000. Finally the units are specified as ‘g’.

When specifying the scale you can also specify the units. For example:

>>> m = Quantity('2.529', scale=(1000, 'g'))
>>> print(m)
2.529 kg

This indicates that the units should be set to ‘g’ after the scale operation.

So far the scale operation has been a simple multiplication, but it is possible to pass a function in for scale to perform more complicated scale operations. for example:

>>> def f2k(f, units):
...     return (f - 32)/1.8 + 273.15, 'K'

>>> t = Quantity(212, scale=f2k)
>>> print(t)
373.15 K

The function is expected to take two arguments: the value and the given units, and it is expected to return two values: the scaled value and the new units. In this example f2k ignores the given units and just assumes degrees Fahrenheit. But you can write a more sophisticated function as follows:

>>> def to_kelvin(t, units):
...     if units in ['F', '°F']:
...         return (t - 32)/1.8 + 273.15, 'K'
...     if units in ['C', '°C']:
...         return t + 273.15, 'K'
...     if units in ['K']:
...         return t, 'K'
...     raise NotImplementedError

>>> t = Quantity(212, units='°F', scale=to_kelvin)
>>> print(t)
373.15 K

In this case, you initially specify the quantity to be 212 °F, but before the value of the quantity is fixed it is rescaled to Kelvin. It was necessary to specify the units to initially be ‘°F’ so that the scaling function knows what to convert from.

QuantiPhy also has a built-in unit conversion feature that is accessed by passing the units to convert to as the value of scale. For example:

>>> t = Quantity('212 °F', scale='K')
>>> print(t)
373.15 K

>>> d = Quantity('d = 93 Mmiles  -- average distance from Sun to Earth', scale='m')
>>> print(d)
149.67 Gm

You can add your own unit conversions to QuantiPhy by using UnitConversion:

>>> from quantiphy import Quantity, UnitConversion

>>> UnitConversion('g', 'lb lbs', 453.59237)
<...>

>>> m = Quantity('10 lbs', scale='g')
>>> print(m)
4.5359 kg

UnitConversion accepts a scale factor and an offset, so can support temperature conversions. Also, the conversion can occur in either direction:

>>> m = Quantity('1 kg', scale='lbs')
>>> print(m)
2.2046 lbs

Unit conversions between the following units are built-in:

K

K, F, °F, R, °R

C, °C

K, C, °C, F, °F, R, °R

m

km, m, cm, mm, um, μm, micron, nm, Å, angstrom, mi, mile, miles

g

oz, lb, lbs

s

s, sec, min, hour, hr , day

When using unit conversions it is important to only convert to units without scale factors (such as those in the first column above) when creating a quantity. If the units used in a quantity includes a scale factor, then it is easy to end up with two scale factors when converting the number to a string (ex: 1 mkm or one milli-kilo-meter).

Here is an example that uses quantity rescaling. Imagine that a table is being read that gives temperature versus time, but the temperature is given in °F and the time is given in minutes, but for the purpose of later analysis it is desired that the values be converted to the more natural units of Kelvin and seconds:

>>> rawdata = '''
...     0 450
...     10 400
...     20 360
... '''
>>> data = []
>>> for line in rawdata.split('\n'):
...     if line:
...         time, temp = line.split()
...         time = Quantity(time, 'min', scale='s')
...         temp = Quantity(temp, '°F', scale='K')
...         data += [(time, temp)]
>>> for time, temp in data:
...     print('{:7s} {}'.format(time, temp))
0 s     505.37 K
600 s   477.59 K
1.2 ks  455.37 K

Rescaling When Rendering a Quantity

It is also possible rescale the value of a quantity when rendering it. In this case the value of the quantity is not affected by the scaling, only the rendered value is affected. As before, scale can be a float, a tuple, a function, or a string:

>>> m = Quantity('2529 g')
>>> print('mass in kg: %s' % m.render(False, scale=0.001))
mass in kg: 2.529

>>> print(m.render(scale=(0.0022046, 'lb')))
5.5754 lb

>>> import math
>>> def to_dB(value, units):
...     return 20*math.log10(value), 'dB'+units

>>> T = Quantity('100mV')
>>> print(T.render(scale=to_dB))
-20 dBV

>>> print(m.render(scale='lb'))
5.5755 lb

When converting to units that have scale factors, it is important to disable SI scale factors to avoid producing units that have two scale factors (ex: 1 mkm or one milli-kilo-meter). For example:

>>> d = Quantity('1 mm')
>>> print(d.render(scale='cm'))
100 mcm

>>> print(d.render(scale='cm', si=False))
100e-3 cm

In an earlier example the units of time and temperature data were converted to normal SI units. Presumably this make processing easier. Now, when producing output, the units can be converted back if desired:

>>> for time, temp in data:
...     print('%-7s %s' % (time.render(scale='min'), temp.render(scale='°F')))
0 min   450 °F
10 min  400 °F
20 min  360 °F

Preferences

You can adjust some of the behavior of these functions on a global basis using set_preferences:

>>> Quantity.set_preferences(prec=2, spacer='')
>>> h_line.render()
'1.42GHz'

>>> h_line.render(prec=4)
'1.4204GHz'

Specifying prec (precision) as 4 gives 5 digits of precision (you get one more digit than the number you specify for precision). Thus, the common range for prec is from 0 to around 12 to 14 for double precision numbers.

Passing None as a value in set_preferences returns that preference to its default value:

>>> Quantity.set_preferences(prec=None, spacer=None)
>>> h_line.render()
'1.4204 GHz'

The available preferences are:

si (bool):

Use SI scale factors by default. Default is True.

units (bool):

Output units by default. Default is True.

prec (int):

Default precision in digits where 0 corresponds to 1 digit, must be nonnegative. This precision is used when full precision is not requested. Default is 4 digits.

full_prec (int):

Default full precision in digits where 0 corresponds to 1 digit. Must be nonnegative. This precision is used when full precision is requested if the precision is not otherwise known. Default is 12 digits.

spacer (str):

May be ‘’ or ‘ ‘, use the latter if you prefer a space between the number and the units. Generally using ‘ ‘ makes numbers easier to read, particularly with complex units, and using ‘’ is easier to parse. Default is ‘ ‘.

unity_sf (str):

The output scale factor for unity, generally ‘’ or ‘_’. Default is ‘’. Generally ‘’ is used if only humans are expected to read the result and ‘_’ is used if you expect to parse the numbers again. Using ‘_’ eliminates the ambiguity between units and scale factors.

output_sf (str):

Which scale factors to output, generally one would only use familiar scale factors. Default is ‘TGMkmunpfa’. This setting does not affect the scale factors that are recognized when reading number.

render_sf (dict, func):

Use this to change the way individual scale factors are rendered. May be a dictionary or a function. For example, to replace u with μ, use render_sf = {‘u’: ‘μ’}.

>>> period = Quantity('1μs')
>>> print(period)
1 us

>>> Quantity.set_preferences(render_sf={'u': 'μ'})
>>> print(period)
1 μs

To render exponential notation as traditional scientific notation, use:

>>> sf_mapper = str.maketrans({
...     'e': '×10',
...     '-': '⁻',
...     '0': '⁰',
...     '1': '¹',
...     '2': '²',
...     '3': '³',
...     '4': '⁴',
...     '5': '⁵',
...     '6': '⁶',
...     '7': '⁷',
...     '8': '⁸',
...     '9': '⁹',
... })

>>> def map_sf(sf):
...     return sf.translate(sf_mapper)

>>> Quantity.set_preferences(render_sf=map_sf)
>>> h_line.render(si=False)
'1.4204×10⁹ Hz'

Both of these are common enough so that QuantiPhy provides these rendering methods for you.

>>> Quantity.set_preferences(render_sf=Quantity.render_sf_in_greek)
>>> print(period)
1 μs

>>> Quantity.set_preferences(render_sf=Quantity.render_sf_in_sci_notation)
>>> h_line.render(si=False)
'1.4204×10⁹ Hz'

>>> Quantity.set_preferences(render_sf=None)
ignore_sf (bool):

Whether scale factors should be ignored by default when converting strings into numbers. Default is False.

reltol (real):

Relative tolerance, used by is_close() when determining equivalence. Default is 10-6.

abstol (real):

Absolute tolerance, used by is_close() when determining equivalence. Default is 10-12.

keep_components (bool):

Whether components of number should be kept if the quantities’ value was given as string. Doing so takes a bit of space, but allows the original precision of the number to be recreated when full precision is requested.

assign_fmt (str or tuple):

Format string for an assignment. Will be passed through string format method to generate a string that includes the quantity name. Format string takes three possible arguments named n, q, and d for the name, value and description. The default is '{n} = {v}'.

If two strings are given as a tuple, then the first is used if the description is present and the second used otherwise. For example, an alternate specification that prints the description in the form of a Python comment if it is available is: ({n} = {v} # {d}', '{n} = {v}').

assign_rec (str):

Regular expression used to recognize an assignment. Used in Quantity and add_to_namespace() to convert a string to a quantity when a name is present. Default recognizes the form:

“Temp = 300_K – Temperature”.

Ambiguity of Scale Factors and Units

By default, QuantiPhy treats both the scale factor and the units as being optional. With the scale factor being optional, the meaning of some specifications can be ambiguous. For example, ‘1m’ may represent 1 milli or it may represent 1 meter. Similarly, ‘1meter’ my represent 1 meter or 1 milli-eter. In this case QuantiPhy gives preference to the scale factor, so ‘1m’ normally converts to 1e-3. To allow you to avoid this ambiguity, QuantiPhy accepts ‘_’ as the unity scale factor. In this way ‘1_m’ is unambiguously 1 meter. You can instruct QuantiPhy to output ‘_’ as the unity scale factor by specifying the unity_sf argument to set_preferences:

>>> Quantity.set_preferences(unity_sf='_', spacer='')
>>> l = Quantity(1, 'm')
>>> print(l)
1_m

If you need to interpret numbers that have units and are known not to have scale factors, you can specify the ignore_sf preference:

>>> Quantity.set_preferences(ignore_sf=True, unity_sf='', spacer=' ')
>>> l = Quantity('1000m')
>>> l.as_tuple()
(1000.0, 'm')

>>> print(l)
1 km

>>> Quantity.set_preferences(ignore_sf=False)

Exceptional Values

You can test whether the value of the quantity is infinite or is not-a-number.

>>> h_line.is_infinite()
False

>>> h_line.is_nan()
False

Equivalence

You can determine whether the value of a quantity or real number is equivalent to that of a quantity. The two values need not be identical, they just need to be close to be deemed equivalent. The reltol and abstol preferences are used to determine if they are close.

>>> h_line.is_close(h_line)
True

>>> h_line.is_close(h_line + 1)
True

>>> h_line.is_close(h_line + 1e4)
False

By default, is_close() looks at the both the value and the units if the argument has units. In this way if you compare two quantities with different units, the is_close test will always fail if their units differ.

>>> Quantity('10ns').is_close(Quantity('10nm'))
False

Physical Constants

The Quantity class also supports a small number of predefined physical constants.

Plank’s constant:

>>> Quantity.set_preferences(
...     fmt=True, spacer=' ', assign_fmt=('{n} = {v} -- {d}', '{n} = {v}')
... )

>>> plank = Quantity('h')
>>> print(plank)
h = 662.61e-36 J-s -- Plank's constant

>>> rplank = Quantity('hbar')
>>> print(rplank)
ħ = 105.46e-36 J-s -- reduced Plank's constant

Boltzmann’s constant:

>>> boltz = Quantity('k')
>>> print(boltz)
k = 13.806e-24 J/K -- Boltzmann's constant

Elementary charge:

>>> q = Quantity('q')
>>> print(q)
q = 160.22e-21 C -- elementary charge

Speed of light:

>>> c = Quantity('c')
>>> print(c)
c = 299.79 Mm/s -- speed of light

Zero degrees Celsius in Kelvin:

>>> zeroC = Quantity('0C')
>>> print(zeroC)
0°C = 273.15 K -- zero degrees Celsius in Kelvin

QuantiPhy uses k rather than K to represent kilo so that you can distinguish between kilo and Kelvin.

Permittivity of free space:

>>> eps0 = Quantity('eps0')
>>> print(eps0)
ε = 8.8542 pF/m -- permittivity of free space

Permeability of free space:

>>> mu0 = Quantity('mu0')
>>> print(mu0)
μ = 1.2566 uH/m -- permeability of free space

Characteristic impedance of free space:

>>> Z0 = Quantity('Z0')
>>> print(Z0)
Z = 376.73 Ohms -- characteristic impedance of free space

You can add additional constants by adding them to the CONSTANTS dictionary:

>>> from quantiphy import Quantity, CONSTANTS
>>> CONSTANTS['h_line'] = (1.420405751786e9, 'Hz')
>>> h_line = Quantity('h_line')
>>> print(h_line)
1.4204 GHz

The value of the constant may be a tuple or a string. If it is a string, it will be interpreted as if it were passed as the primary argument to Quantity. If it is a tuple, it may contain up to 4 values, the value, the units, the name, and the description. This value may also be a string, and if so it must contain a simple number. The benefit of using a string in this case is that QuantiPhy will recognize the significant figures and use them as the full precision for the quantity.

>>> CONSTANTS['lambda'] = 'λ = 211.0611405389mm -- wavelength of hydrogen line'
>>> print(Quantity('lambda'))
λ = 211.06 mm -- wavelength of hydrogen line

>>> CONSTANTS['lambda'] = (Quantity('c')/h_line,)
>>> print(Quantity('lambda'))
211.06m

>>> CONSTANTS['lambda'] = (Quantity('c')/h_line, 'm')
>>> print(Quantity('lambda'))
211.06 mm

>>> CONSTANTS['lambda'] = (Quantity('c')/h_line, 'm', 'λ')
>>> print(Quantity('lambda'))
λ = 211.06 mm

>>> CONSTANTS['lambda'] = (Quantity('c')/h_line, 'm', 'λ', 'wavelength of hydrogen line')
>>> print(Quantity('lambda'))
λ = 211.06 mm -- wavelength of hydrogen line

String Formatting

Quantities can be passed into the string format method:

>>> print('{}'.format(h_line))
1.4204 GHz

>>> print('{:s}'.format(h_line))
1.4204 GHz

In these cases the preferences for SI scale factors, units, and precision are honored.

You can override the precision as part of the format specification

>>> print('{:.6}'.format(h_line))
1.420406 GHz

You can also specify the width and alignment.

>>> print('|{:15.6}|'.format(h_line))
|1.420406 GHz   |

>>> print('|{:<15.6}|'.format(h_line))
|1.420406 GHz   |

>>> print('|{:>15.6}|'.format(h_line))
|   1.420406 GHz|

The ‘q’ type specifier can be used to explicitly indicate that both the number and the units are desired and that SI scale factors should be used, regardless of the current preferences.

>>> print('{:.6q}'.format(h_line))
1.420406 GHz

Alternately, ‘r’ can be used to indicate just the number represented using SI scale factors is desired, and the units should not be included.

>>> print('{:r}'.format(h_line))
1.4204G

You can also use the floating point format type specifiers:

>>> print('{:f}'.format(h_line))
1420405751.7860

>>> print('{:e}'.format(h_line))
1.4204e+09

>>> print('{:g}'.format(h_line))
1.4204e+09

Use ‘u’ to indicate that only the units are desired:

>>> print('{:u}'.format(h_line))
Hz

Access the name or description of the quantity using ‘n’ and ‘d’.

>>> wavelength = Quantity('lambda')
>>> print('{:n}'.format(wavelength))
λ

>>> print('{:d}'.format(wavelength))
wavelength of hydrogen line

Using the upper case versions of the format codes that print the numerical value of the quantity (SQRFEG) to indicate that the name and perhaps description should be included as well. They are under the control of the assign_fmt preference.

>>> trise = Quantity('10ns', name='trise')

>>> print('{:S}'.format(trise))
trise = 10 ns

>>> print('{:Q}'.format(trise))
trise = 10 ns

>>> print('{:R}'.format(trise))
trise = 10n

>>> print('{:F}'.format(trise))
trise = 0.0000

>>> print('{:E}'.format(trise))
trise = 1.0000e-08

>>> print('{:G}'.format(trise))
trise = 1e-08

>>> print('{0:n} = {0:q} ({0:d})'.format(wavelength))
λ = 211.06 mm (wavelength of hydrogen line)

>>> print('{:S}'.format(wavelength))
λ = 211.06 mm -- wavelength of hydrogen line

You can also specify two values to assign_fmt, in which case the first is used if there is a description and the second used otherwise.

>>> Quantity.set_preferences(assign_fmt=('{n} = {v} -- {d}', '{n} = {v}'))

>>> print('{:S}'.format(trise))
trise = 10 ns

>>> print('{:S}'.format(wavelength))
λ = 211.06 mm -- wavelength of hydrogen line

Finally, you can add units after the format code, which will cause the number to be scaled to those units if the transformation represents a known unit conversion.

>>> Tboil = Quantity('Boiling point = 100 °C')
>>> print('{:S°F}'.format(Tboil))
Boiling point = 212 °F

>>> eff_channel_length = Quantity('leff = 14nm')
>>> print('{:SÅ}'.format(eff_channel_length))
leff = 140 Å

This feature can be used to simplify the conversion of the time and temperature information back into the original units:

>>> for time, temp in data:
...     print('{:<7smin} {:s°F}'.format(time, temp))
0 min   450 °F
10 min  400 °F
20 min  360 °F

Exceptions

A ValueError is raised if Quantity is passed a string it cannot convert into a number:

>>> try:
...     q = Quantity('xxx')
... except ValueError as err:
...     print(err)
xxx: not a valid number.

Add to Namespace

It is possible to put a collection of quantities in a text string and then use the add_to_namespace function to parse the quantities and add them to the Python namespace. For example:

>>> design_parameters = '''
...     Fref = 156 MHz  -- Reference frequency
...     Kdet = 88.3 uA  -- Gain of phase detector (Imax)
...     Kvco = 9.07 GHz/V  -- Gain of VCO
... '''
>>> Quantity.add_to_namespace(design_parameters)

>>> print(Fref, Kdet, Kvco, sep='\n')
Fref = 156 MHz -- Reference frequency
Kdet = 88.3 uA -- Gain of phase detector (Imax)
Kvco = 9.07 GHz/V -- Gain of VCO

Any number of quantities may be given, with each quantity given on its own line. The identifier given to the left ‘=’ is the name of the variable in the local namespace that is used to hold the quantity. The text after the ‘–’ is used as a description of the quantity.

Subclassing Quantity

By subclassing Quantity you can create different sets of default behaviors that are active simultaneously. For example:

>>> class ConventionalQuantity(Quantity):
...     pass

>>> ConventionalQuantity.set_preferences(si=False, units=False)

>>> period1 = Quantity(1e-9, 's')
>>> period2 = ConventionalQuantity(1e-9, 's')
>>> print(period1, period2)
1 ns 1e-9

Example

Here is a simple example that uses QuantiPhy. It runs the du command, which prints out the disk usage of files and directories. The results from du are gathered and then sorted by size and then the size and name of each item is printed.

Quantity is used to interpret the ‘human’ size output from du and convert it to a float, which is easily sorted, then is is converted back to a string with SI scale factors and units when rendered in the print statement.

#!/bin/env python3
# runs du and sorts the output while suppressing any error messages from du

from quantiphy import Quantity
from inform import os_error
from shlib import Run
import sys

try:
   du = Run(['du', '-h'] + sys.argv[1:], modes='WEO1')

   files = []
   for line in du.stdout.split('\n'):
      if line:
          size, filename = line.split('\t', 1)
          files += [(Quantity(size, 'B'), filename)]

   files.sort(key=lambda x: x[0])

   for each in files:
      print(*each, sep='\t')

except OSError as err:
   sys.exit(os_error(err))
except KeyboardInterrupt:
   sys.exit('dus: killed by user')

MatPlotLib Example

In this example QuantiPhy is used to create easy to read axis labels in MatPlotLib. It uses NumPy to do a spectral analysis of a signal and then produces an SVG version of the results using MatPlotLib.

#!/usr/bin/env python3

import numpy as np
from numpy.fft import fft, fftfreq, fftshift
import matplotlib as mpl
mpl.use('SVG')
from matplotlib.ticker import FuncFormatter
import matplotlib.pyplot as pl
from quantiphy import Quantity

# define some utility functions
def mag(spectrum):
    return np.absolute(spectrum)

def freq_fmt(val, pos):
    return Quantity(val, 'Hz').render()
freq_formatter = FuncFormatter(freq_fmt)

def volt_fmt(val, pos):
    return Quantity(val, 'V').render()
volt_formatter = FuncFormatter(volt_fmt)

# read the data from delta-sigma.smpl
data = np.fromfile('delta-sigma.smpl', sep=' ')
time, wave = data.reshape((2, len(data)//2), order='F')

# print out basic information about the data
timestep = Quantity(time[1] - time[0], 's')
nonperiodicity = Quantity(wave[-1] - wave[0], 'V')
period = Quantity(timestep * len(time), 's')
freq_res = Quantity(1/period, 'Hz')
print('timestep = {}'.format(timestep))
print('nonperiodicity = {}'.format(nonperiodicity))
print('timepoints = {}'.format(len(time)))
print('period = {}'.format(period))
print('freq resolution = {}'.format(freq_res))

# create the window
window = np.kaiser(len(time), 11)/0.37
    # beta=11 corresponds to alpha=3.5 (beta = pi*alpha)
    # the processing gain with alpha=3.5 is 0.37
windowed = window*wave

# transform the data into the frequency domain
spectrum = 2*fftshift(fft(windowed))/len(time)
freq = fftshift(fftfreq(len(wave), timestep))

# generate graphs of the resulting spectrum
fig = pl.figure()
ax = fig.add_subplot(111)
ax.plot(freq, mag(spectrum))
ax.set_yscale('log')
ax.xaxis.set_major_formatter(freq_formatter)
ax.yaxis.set_major_formatter(volt_formatter)
pl.savefig('spectrum.svg')
ax.set_xlim((0, 1e6))
pl.savefig('spectrum-zoomed.svg')

Notice the axis labels in the generated graph. Use of QuantiPhy makes the widely scaled units compact and easy to read.

spectrum-zoomed.png

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quantiphy-1.0.0.tar.gz (46.0 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page