Skip to content

DIMensional ANAlysis library for python - Extend Decimal arithmetic with arbitrary named units with unit error detection.

Notifications You must be signed in to change notification settings

nejucomo/dimana

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

_DIM_ensional _ANA_lysis

A python library for tracking and verifying dimensional units of measure.

For background see the dimensional analysis wikipedia entry.

dimana values can be parsed with the Value constructor:

>>> from dimana import Value
>>> reward = Value('12.5 [BTC]')
>>> reward
<Value '12.5 [BTC]'>

The grammar for units can handle powers expressed with the ^ symbol, and a single division with the / symbol:

>>> Value('9.807 [meter/sec^2]')
<Value '9.807 [meter / sec^2]'>

Values track their units through arithmetic operations:

>>> time = Value('10 [min]')
>>> rate = reward / time
>>> rate
<Value '1.25 [BTC / min]'>

Incoherent operations raise exceptions:

>>> reward + time # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
  ...
UnitsMismatch: 'BTC' does not match 'min'

A value associates a scalar amount with dimensional units. These are available on the instance as amount and units:

>>> rate.amount
Decimal('1.25')
>>> rate.units
<Units 'BTC / min'>

The scalar amount of a value is represented with decimal.Decimal instance on the amount attribute:

>>> reward.amount
Decimal('12.5')

Arithmetic operations rely on the decimal library for arithmetic logic, including precision tracking:

>>> reward * Value('713.078000 [USD / BTC]')
<Value '8913.4750000 [USD]'>

Units are available in the units attribute of Value instances. They are instances of dimana.Units. You can parse Units instances directly:

>>> from dimana import Units
>>> meter = Units('meter')
>>> meter
<Units 'meter'>
>>> sec = Units('sec')
>>> sec
<Units 'sec'>

There are four ways to create values:

  • parsing a 'value text' with the constructor: Value,
  • as the result of arithmetic operations on other values,
  • with the explicit constructor,
  • by calling a Units instance.

The first two are described above, the last two next:

Values can be constructed explicitly directly given Decimal and Units instances:

>>> from dimana import Value
>>> from decimal import Decimal
>>> Value(Decimal('23.50'), meter)
<Value '23.50 [meter]'>

Note that this constructor is strict about types and the first argument must be a decimal.Decimal:

>>> Value(7, meter)
Traceback (most recent call last):
  ...
TypeError: Expected 'Decimal', found 'int'

Many applications require a finite statically known set of Units instances, and then need to create Value instances from specific explicit Units instances. This is more specific (thus safer) when the units are already known than calling the Value constructor which returns a value with arbitrary units.

For example:

>>> from decimal import Decimal
>>> from dimana import Value, Units
>>> METER = Units('METER')
>>> userinput = '163' # In an application this might be from arbitrary input.
>>> height = Value(Decimal(userinput), METER)
>>> height
<Value '163 [METER]'>

Because this pattern is so common, Units instances support parsing an amount directly by calling Units instances:

>>> height2 = METER(userinput)
>>> height == height2
True

The str()-ification of Value and Units instances matches the 'canonical parsing format':

>>> trolls = Value('3 [troll]')
>>> print(trolls)
3 [troll]
>>> trolls == Value(str(trolls))
True

The repr() of these class instances contains the class name and the str()-ification:

>>> print(repr(trolls))
<Value '3 [troll]'>
>>> print(repr(trolls.units))
<Units 'troll'>

This section explores the Units class more closely.

Because the 0 and 1 amounts are very common, they are available as attributes of a Units instance:

>>> meter.zero
<Value '0 [meter]'>
>>> sec.one
<Value '1 [sec]'>

The base case of units with 'no dimension' is available as Scalar. This instance of Units represents, for example, ratios:

>>> from dimana import Scalar
>>> total = Value('125 [meter]')
>>> current = Value('15 [meter]')
>>> completion = current / total
>>> completion
<Value '0.12'>
>>> completion.units is Scalar
True

Parsing a value which does not specify units produces a scalar value:

>>> completion == Value('0.12')
True

By design, dimana does not do implicit coercion of float instances into Value instances to help avoid numeric bugs:

>>> experience = Value('42 [XP]')
>>> experience * 1.25
Traceback (most recent call last):
  ...
TypeError: Expected 'Value', found 'float'

Using Scalar is necessary in these cases. Parsing a value with no units specification gives a 'scalar value':

>>> experience * Value('1.25')
<Value '52.50 [XP]'>

There is a single instance of Units for each combination of unit:

>>> (meter + meter) is meter
True
>>> (meter / sec) is Units('meter / sec')
True

Thus, to test if two Units instances represent the same units, just use the is operator:

>>> if meter is (Units('meter / sec') * sec):
...     print('Yes, it is meters.')
...
Yes, it is meters.

The Units.match method does such a check and raises UnitsMismatch if the units do not match:

>>> meter.match(Units('meter / sec') * sec)
>>> meter.match(Units('meter / sec^2') * sec) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
  ...
UnitsMismatch: 'meter' does not match 'meter / sec'

This uniqueness depends globally on the unit string names, so if a large application depended on two completely separate libraries, each of which rely on dimana, and both libraries define <Units 's'> they will be using the same instance. This could be a problem if, for example, one library uses the S to represent seconds while the other uses it to represent Siemens.

Each instance of Units persists to the end of the process, so instantiating Units dynamically could present a resource management problem, especially if a malicious entity can instantiate arbitrary unit types.

(The plan is to wait for real life applications that encounter these problems before adding complexity to this package.)

There is no definite roadmap other than to adapt to existing users' needs. However, some potential new features would be:

  • Python 3 support with an identical API.
  • Support for more numeric operations.
  • More streamlined interaction with decimal, such as for rounding a Value to a given precision.
  • Add an 'expression evaluator' for quick-and-easy interactive interpreter calculations, eg: dimana.eval
  • Add a commandline wrapper around eval.
  • Removed old class-scoped APIs, such as parse methods, in favor of using constructors directly.
  • Added tox support for python 2.7 and 3.5.
  • Extended the README.rst to have a more complete overview, a future roadmap, and this changelog.
  • Made several breaking API changes:
    • Now toplevel dimana only publicly exposes Units and Value.
    • Introduced Units.from_string parser.
    • Introduced zero and one properties of Units instances.
    • Renamed the old Value.decimal attribute to Value.amount.
  • Added code examples in README.rst and hooked doctests of that documentation into the unittest suite.
  • Pivoted the API to the separation between Value and Units with the two parse methods.
  • Strict requirement of Decimal instances without implicit coercion.

The 0.1 line of dimana had a very different interface based on a single Dimana class, and a more rudimentary parser, and was generally a messier proof-of-concept.

  • There was no representation of the modern Units instances, rather only the equivalent of Value instances.
  • It used dynamic type generation for what is now each instance of Units.
  • It had less obvious error messages and less complete unit testing.
  • It had no documentation and no doctests.

There appears to be no way to accurately test exception details with doctest for both python 2 and 3. The best option seems to be to ignore exception details. :-<

About

DIMensional ANAlysis library for python - Extend Decimal arithmetic with arbitrary named units with unit error detection.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages