physipy: make python unit-aware

Author:Murphy  |  View: 25352  |  Time: 2025-03-22 21:57:03

Have you ever done engineering/scientific computation with Python, and ended up lost or confused about which unit your variable was expressed in, like "is that the value in meters or millimeters"? Or you realized that at some point you added an electrical current with a resistance – which is impossible? As every physics teacher has said at some point: you cannot add carrots and tomatoes.

Well, physipy is here exactly to solve those kinds of problems.

Photo by Artturi Jalli on Unsplash

Table of content:

· What is physipy? · Understanding physipy, an example at a timeComputing body-mass-index BMI with physipyNewton's law of motion with numpy arrayOhm's Law with NumPy functions Einstein's Mass-Energy Equivalence for common particles, with favunitFree-fall with built-in favunitPlotting an object position and speed with Matplotlib · Wrapup

All images by author.

What is physipy?

physipy is a lightweight package that allows you to define and declare physical units very easily and keep track of the units of all your variables. Put another way, you'll never have to suffix your variable with the corresponding unit (like my_height_cm will become just my_height) and if you ever try to add carrots and tomatoes, an exception will be raised.

physipy offers nice features for a scientific/engineering work: it integrates perfectly with Numpy, provides a Pandas extension, a fully-fledged doc, works out of the box with Matplotlib, and provides (ipy)widgets for jupyter.

Import the meter and the second, and start working with unit-aware variables.

For full disclosure, I am the creator of physipy. Also note that physipy is under MIT Licence.

Understanding physipy, an example at a time

In this first post, we'll see how physipy can be used with pretty basic examples, especially how it integrates well with NumPy and Matplotlib.

The examples are meant to be read in order because concepts and tools are added along the way. On the other hand, they are all meant to be pretty simple and well-commented, so you should still understand by jumping straight to the last one.

You can also head directly to the documentation of Physipy or the project repository on GitHub.

physipy : make python unit-aware

GitHub – mocquin/physipy: A python package that transparently handles physical quantities like 2…

To install physipy, you can use pypi with a simple command line:

pip install physipy

Or if you prefer the latest version from github, you can download and un-zip the package locally, or clone the git repository with :

git clone https://github.com/mocquin/physipy

Computing body-mass-index BMI with physipy

As you probably know, a common health metric is the body-mass index. As wikipedia states:

Body mass index (BMI) is a value derived from the mass (weight) and height of a person. The BMI is defined as the body mass divided by the square of the body height, and is expressed in units of kg/m2, resulting from mass in kilograms (kg) and height in metres (m).

Let's see what the code without physipy would look like:

#### BMI example, without physipy

# no physipy → we need to keep track of the unit everywhere
my_weight_kg = 75
my_height_m = 1.89

def bmi_calculator(weight_kg, height_m):
    "Compute Body-Mass-Index from weight and height."
    return weight_kg / height_m**2

print(bmi_calculator(my_weight_kg, my_height_m)) # notice that the output is pure, unit-less number, while it is actually kg-per-meter-squared
# 20.99605274208449

Now let's see what the code with physipy looks like:

from physipy import kg, m

# define physical quantities that are unit aware
my_weight = 75 * kg
my_height = 1.89 * m

def bmi_calculator(weight, height):
    "Compute Body-Mass-Index from weight and height."
    return weight / height**2

print(bmi_calculator(my_weight, my_height))
# 20.99605274208449 kg/m**2

Notice how little the code changed: we can get rid of any unit suffix in our variable names without losing track of them.

On the other hand, we can see that the variables' values are defined explicitly, using named units. Another nice feature is that the returned output still has the appropriate unit attached, in this case kg/m**2.

The nice thing is that we can use any unit and don't have to worry about any conversion: all the conversions are done in the backend by Physipy.

For example, let's say you recorded the heights in centimeters and the weights in grams.

# retrieve the units
from physipy import units

# units is just a dict
cm = units['cm']
g = units['g']

print(bmi_calculator(75000*g, 189*cm))
# 20.99605274208449 kg/m**2

So we get the same output value while we were able to specify the variable in other units.

Newton's law of motion with numpy array

We demonstrate here how to attach unit to regular numpy arrays, just using the regular mutliplication operator (like with floats and ints):

import numpy as np
from physipy import m, kg, s

m = np.array([10, 20, 30]) * kg # mass
a = 2 * m / s**2 # acceleration

# Calculate force using Newton's Second Law : force = mass * acceleration
F = m * a

print(F) # [ 200. 800. 1800.] kg**2/s**2 
# 1 kg**s/s**2 is equivalent to 1 Newton

Ohm's Law with NumPy functions

Using NumPy and Physipy works with almost all NumPy's functions. Here we can call a NumPy function with unit-aware values, and the returned arrays still have the right unit.

import numpy as np
from physipy import units

V = units['V']
ohm = units['ohm']

V = 12 * V # voltage
R = np.arange(1*ohm, 5*ohm) # array with unit ohm

# Calculate current using Ohm's Law
I = V / R
print(I) # [12. 6. 4. 3.] A
# If for some reason you forgot the unit of I, physipy knows and attaches
# it to I for you - you get an output in Amps for free !

Einstein's Mass-Energy Equivalence for common particles, with favunit

Einstein's Mass-Energy Equivalence for common particles, with Favunit In this example, we introduce the .favunit attribute of any unit-aware object, meaning the "favorite unit" for this variable. It is this unit that will be used to print the variable. In the Système International, the standard unit for energy is the Joule, which is equal to 1 kg*m2/s2.

import numpy as np
from physipy import constants, kg, units

# constants is just a dict of physical constants
c = constants['c']
J = units['J']

masses = np.array([
    9.1093837E-31, # mass of electron
    1.673E-27, # mass of proton
    1.675E-27, # mass of neutron
]) * kg

# Calculate energy using Einstein's equation
E = masses * c**2

# indexing works just as usual, with the added output energy unit
print(E[0]) # enery for proton : 8.187105775475753e-14 kg*m**2/s**2
print(E) # [8.18710578e-14 1.50361741e-10 1.50541492e-10] kg*m**2/s**2

# changing the display unit to Joule
E.favunit = J
print(E[0]) # 8.187105775475753e-14 J
print(E) # [8.18710578e-14 1.50361741e-10 1.50541492e-10] J

As you can see, the same variable favunit can be modified at will in order to change its string representation. The cool thing about Physipy is that this .favunit attribute is only used for printing purposes; the actual "true" value and physical unit (kg*m2/s2) are stored in the backend.

Free-fall with built-in favunit

In this example, we're simulating the free fall of an object from an initial height of 100 meters. We use the equation of motion for free fall, h=h0−1/2gt^2, where h is the height of the object at time t, h0​ is the initial height, g is the acceleration due to gravity, and t is time.

We use this example to demonstrate how favunit – the unit used by variable for printing – can be set either "manually" in a function or using the decorator set_favunit.

Let's first see how to set the favorite unit manually:

import numpy as np
from physipy import m, s, units
cm = units['cm'] # we are using cm as the favunit

# Constants
initial_height = 100 * m # Initial height of the object
gravity = 9.81 * m / s**2 # Acceleration due to gravity

# Time samples
time_samples = np.linspace(0, 3, 50) * s # Time samples from 0 to 5 seconds

def calculate_height(time_samples):
    heigth = initial_height - 0.5 * gravity * time_samples**2
    heigth.favunit = cm
    return heigth

# Calculate heights for each time sample
heights = calculate_height(time_samples)

# Print time samples and corresponding heights
for t, h in zip(time_samples, heights):
    print(f"Time: {t}, Height: {h}")
# Time: 0.0 s, Height: 10000.0 cm
# ...
# Time: 3.0 s, Height: 5585.5 cm

While pretty simple, setting the height's favunit manually has nothing to do with the actual math computation. Another way to do this is by using the set_favunit decorator. So the two following snippets are equivalent:

Setting the favunit manually:

def calculate_height(time_samples):
    heigth = initial_height - 0.5 * gravity * time_samples**2
    heigth.favunit = cm
    return heigth

Here using the set_favunit decorator

from physipy import set_favunit

@set_favunit(cm)
def calculate_height(time_samples):
    heigth = initial_height - 0.5 * gravity * time_samples**2
    return heigth

Using the set_favunit decorator might be preferred because it keeps the computation logic and the unit-printing separated.

Plotting an object position and speed with Matplotlib

In this example, we're visualizing the motion of an object under the influence of gravity. Specifically, we're plotting two important aspects of its motion: displacement and velocity, against time.

The important statement is calling setup_matplotlib() so that Matplotlib becomes aware of Physipy, and by default will set the axis label with the corresponding units, anytime you plot a unit-aware object.

In other words, you can get rid of the set_xlabel('s') or set_ylabel('km') to specify the units; physipy does that automatically.

import numpy as np
import matplotlib.pyplot as plt
from physipy import units, s, setup_matplotlib, constants

cm = units['cm'] # centimeter
ms = units["ms"] # millisecond
g = constants['g']

# this makes matplotlib aware of physipy
setup_matplotlib()

time = np.linspace(0, 10, 100) * s # Time values
time.favunit = ms

velocity = g * time # Velocity as a function of time (assuming constant acceleration)

displacement = 0.5 * g * time**2 # displace from origin position
displacement.favunit = cm

# Plotting
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(time, displacement, label="Displacement", color="blue")
ax2 = ax.twinx()
ax2.plot(time, velocity, label="Velocity", linestyle=" - ", color="red")
Notice that some units were added to the axis' labels, with no explicit call to set_xlabel/set_ylabel.

Wrapup

Hopefully, those examples allowed you to understand the basics on how to use use physipy. You should remember the following things:

  • The simplest way to use physipy is to import some units/constants, that are stored in classic python dict with from physipy import units, constants. Note the Système International units are available directly from physipy import m, s, kg.
  • To define unit aware variables just multiply the value with the unit, like heights = np.linspace(1, 50)*km.
  • Almost all numpy functions work on unit-aware arrays, for example you can simply find the maximum of an array of heights using np.max(heights), the returned value will be 50000 m.
  • By default, unit-aware values will be printed using SI-units, like a Joule will be printed as kg*m**2/s**2. If you want a particular unit to be used, you just set the favunit attribute: heights.favunit = mm
  • You can make matplotlib aware of physipy and handle the units using from physipy import setup_matplotlib and then setup_matplotlib(). From then, any plot will add the units to the axis labels.

The advantages of physipy are that it is lightweight (simple examples, the source code is simple), it highly integrates with numpy and matplotlib.

We'll see later that it is also a pretty fast package to handle units in python, thanks to its lightweight structure (hence light overhead). It also provide a Pandas extension and ipywidgets for jupyter/notebooks.

In the next post, we'll dive a bit in physipy, introducing the 2 (and only 2) important classes of physipy, namely Dimension and Quantity.

You can find more examples and a lot of information by checking the documentation of the project at :

physipy : make python unit-aware


You might like some of my previous posts, I write about Python, machine learning/datascience, signal processing, numerical techniques:

Sklearn tutorial

Scientific/numerical python

Data science and Machine Learning

Fourier-transforms for time-series

Tags: Data Science Numpy Pandas Physics Python

Comment