Package: python-pyunits
PyUnitsUse Units in your Python CodeWARNING: This code is still under development. Unless you are invigorated
by the thought of an extended correspondence with me, I would recommendagainstits use for any serious(read: paid)project.Here's something we've all done before:length1=42# Meters# ... 500 lines laterlength2=12# Inchesresult=length1+length2# OopsInstallationpipinstallpython-pyunitsUnits are a pain.And any time we're computing with some sort of physical quantities, we almost
certainly have to take them into account. Whenever I read (or write) this kind
of code, I am amazed at how many subtle bugs crop up due to missing or
incorrect unit conversions.WolframAlpha can do it, so why can't Python?Why indeed? We're used to scientific software and even search engines auto-magically
handling unit conversions and dimensional analysis for us. Python is used for a
lot of scientific applications, but sadly, this feature isn't built-in to the
language.Ladies and gentlemen, I give you PyUnits:fromexamplesimportexample_unitsaseulength1=eu.Meters(42)length2=eu.Inches(12)print(length1+length2)# Prints "42.3048 m"Okay. So maybe you're not turned on by unit conversions. I can understand that.
But guess what? PyUnits also understands dimensional analysis. In fact, it
understands it at least as well asRandall Munroedoes:fromexamplesimportexample_unitsaseufrompyunits.compound_unitsimportMul,DivCubicMeters=Mul(eu.Meters,Mul(eu.Meters,eu.Meters))CubicIn=Mul(eu.Inches,Mul(eu.Inches,eu.Inches))Pascals=Div(eu.Joules,CubicMeters)MilesPerIn3=Div(eu.Miles,CubicIn)plank_energy=eu.Joules(1.956E9)core_pressure=Pascals(3.6E11)# Technically, this is the Prius C gas mileage. It seems that Priuses# (Priui?) have improved somewhat since the comic was published...# Note also that we don't really have the ability to do mpg directly yet,# so we do mi / in^3 and divide by 231. (1 gal = 231 in^3)prius_mileage=MilesPerIn3(46/231)channel_width=eu.Kilometers(33.8)pi=plank_energy/core_pressure*prius_mileage/channel_widthprint(pi)# Prints 3.14"But wait", I hear you cry. "That's a totally contrived example!" And it may
well be. So let's try something a little more... practical:importmathfromexamplesimportexample_unitsaseufrompyunits.compound_unitsimportMulSquareMiles=Mul(eu.Miles,eu.Miles)SquareMeters=Mul(eu.Meters,eu.Meters)population_density=18600/SquareMiles(1.0)sex_frequency=80/eu.Years(1.0)sex_duration=eu.Minutes(30)denom_value=math.pi*population_density*sex_frequency*sex_durationfraction=2.0/denom_value# Ensure result is in square meters.fraction=SquareMeters(fraction)# Square root has to be done outside of PyUnits, since we don't support that# (yet).sex_radius=math.sqrt(fraction.raw)print(sex_radius)# Prints "139.33", the correct result for these parameters.As you can see, PyUnits is suitable for Serious Scientific Work.CookbookI have a colleague who likes nothing better than copying and pasting code from
README files. He refers to this type of file as a "cookbook". Therefore, I will
be providing a similar facility in this section.Building a Unit LibrarySo far, all the examples in this document have used the example unit library
provided in this repository underexamples/example_units.py. That's all well
and good, but if you're using PyUnits for real, the first thing you're probably
going to want to do is create your own unit library:frompyunits.unitimportStandardUnit,Unitfrompyunits.unit_typeimportUnitTypeclassLength(UnitType):"""Type for length units."""@Length.decorateclassMeters(StandardUnit):"""A meters unit."""@propertydefname(self)->str:"""See superclass for documentation."""return"m"@Length.decorateclassCentimeters(Unit):"""A centimeters unit."""def_from_standard(self,standard_value:StandardUnit)->None:"""See superclass for documentation."""# Convert from meters.self._set_raw(standard_value.raw*100)defto_standard(self)->Meters:"""See superclass for documentation."""# Convert to meters.returnMeters(self.raw/100)@propertydefname(self)->str:"""See superclass for documentation."""return"cm"Fundamentally, PyUnits has two types of objects that the user needs to be aware
of:UnitandUnitType. The former is easy: aUnitinstance simply
represents a value with a specific unit. The second is a little harder to
explain.Intuitively, some units can be trivially converted to each-other, eg. Meters
and Inches, and some can't be, eg. Meters and Seconds. TheUnitTypeclass
exists in order to allow PyUnits to understand these relationships. When using
PyUnits, everyUnitsubclass must be decorated with an appropriateUnitTypesubclass. Two units that are marked with the sameUnitTypecan be trivially
converted to each-other. Two units that aren't can't easily be converted, and
PyUnits with raise an error if you try to do so.Standard UnitsFor all the units of a particularUnitType, PyUnits expects the user to choose
a "standard unit". In practice, which unit you use as the standard one doesn't
really matter, as long as you can tell PyUnits how to convert from every other
unit to the standard one and vice-versa.In practice, this is done by overriding two methods:The_from_standard()method takes an instance of the standard unit and
initializes thisUnitinstance appropriately with its converted value.Theto_standard()method returns an instance of the standard unit with
an equivalent value to this one.Once we have that set up, PyUnits can convert implicitly between all the units
of thisUnitType:meters=Meters(10)centimeters=Centimeters(meters)print(centimeters)# prints "1000 cm"Which unit is considered to be the standard one is defined by which one
inherits from theStandardUnitclass. (This isMetersin the above example.)Pretty-PrintingPyUnits has (currently limited) support for pretty-printing unit values. This
is utilized by overriding thenameproperty, as seen in the example above.
This property should return a suffix that will be appended to the unit value
when printing.Numpy IntegrationPyUnits can essentially be thought of as a wrapper around Numpy. That is
becauseUnitsubclasses actually store and manipulate Numpy arrays internally:importnumpyasnpfromexamplesimportexample_unitsaseusecs=eu.Seconds(np.array([1,2,3]))print(secs)# prints "[1 2 3] s"You can access the raw Numpy value of a unit using therawproperty:print(secs.raw)# print "[1 2 3]"Dimensional AnalysisPyUnits is generally clever about unit operations:fromexamplesimportexample_unitsaseumeters=eu.Meters(2)seconds=eu.Seconds(4)print(meters/seconds)# prints 0.5 m\n-\nsAs can be seen in the earlier examples, it will even go so far as to
auto-simplify the results of multiplication and division operations.Also, compound unit types can be created manually:fromexamplesimportexample_unitsaseufrompyunits.compound_unitsimportMul,DivWatts=Div(eu.Joules,eu.Seconds)SquareMeters=Mul(eu.Meters,eu.Meters)# These can then be used like normal units.watts=Watts(10)square_meters=SquareMeters(50)Unitless ValuesPyUnits has the special concept of "Unitless" values, which, paradoxically,
can be used like aUnitin many cases. These crop up most often when doing
division.fromexamplesimportexample_unitsaseumeters1=eu.Meters(10)meters2=eu.Meters(5)print(meters1/meters2)# prints "2"The result of this division is an instance of the classUnitless. This class
can also be used directly in order to represent concepts such as "inverse meters".fromexamplesimportexample_unitsaseufrompyunits.compound_unitsimportDivfrompyunits.unitlessimportUnitlessinverse_meters=Div(Unitless,eu.Meters)PyUnits will refuse to implicitly convertUnitlessinstances, however. If
you want to use this as a value of a unit, you have to explicitly take the raw
value:fromexamplesimportexample_unitsaseufrompyunits.unitlessimportUnitlessunitless=Unitless(5)meters=eu.Meters(unitless)# Error!# The correct way...meters=eu.Meters(unitless.raw)This is an explicit design choice that was made to avoid cases where values
that did not have units could "magically" acquire them.