Examples

Simple kinematics

When using the package, the first task is to select a set of base quantities.

For instance, the base quantities distance and duration (dimensions, LT) may be used for a straight-line kinematics problems. Other kinds of quantity are then declared in terms of this basis. For example, speed is the time required to cover a distance.

from QV import *

quantity = Context( ("Length","L"),("Time","T") )
quantity.declare('Speed','V','Length/Time')

Here, context maintains one-to-one relationships between the names, and short symbols, of kinds of quantities and the signatures associated with measurements of them. So, after the declaration of speed, context does not allow any other quantity to be declared with the same signature.

Units are defined in relation to kinds of quantity. In this case, we might write

SI =  UnitRegister("SI",quantity)

metre = SI.unit( RatioScale(quantity.Length,'metre','m') )
second = SI.unit( RatioScale(quantity.Time,'second','s') )
metre_per_second = SI.unit( RatioScale(quantity.Speed,'metre_per_second','m/s') )

Here, the SI object keeps a register of units, each associated with the measurement of a kind of quantity and hence to the signature of that quantity. The first unit declaration for a quantity creates a reference unit within the register; other units of the same kind of quantity can also be registered, but they must be related to the reference unit by a conversion factor (see below, where a related unit, L/(100 km), is created for fuel consumption.)

Quantity values may be defined with the function qvalue() and used in calculations. For instance,

d = qvalue(0.5,metre)
t = qvalue(1.0,second)
print( "average speed =", qresult(d/t) )

v0 = qvalue(5.2,metre_per_second)
x0 = qvalue(0.3,metre)
print( "displacement =", x0 + v0*t )

The output is

average speed = 0.5 m/s
displacement = 5.5 m

An interesting implementation detail is apparent here. The function qresult() is applied to d/t to resolve the units, but it is not used in the calculation of x0 + v0*t. The reason is that individual multiplications or divisions are often just intermediate steps in a calculation. So, QV will not try to resolve the kind of quantity of an operation until signalled to do so. However, addition and subtraction of different kinds of quantity is not allowed. So, the arguments in the sum x0 + v0*t must be checked, and this requires QV to resolve the units of v0*t.

Fuel consumption

This package facilitates the use of ad hoc units. For example, fuel consumption is typically stated in units of litres per 100 km. This can be handled as follows [1]

from fractions import Fraction

quantity = Context( ("Distance","L"), ("Volume","V") )
FuelConsumption = quantity.declare( 'FuelConsumption','FC','Volume/Distance' )

ureg =  UnitRegister("ureg",quantity)

# Reference units
kilometre = ureg.unit( RatioScale(quantity['Distance'],'kilometre','km') )
litre = ureg.unit( RatioScale(quantity['Volume'],'litre','L') )
litres_per_km = ureg.unit( RatioScale(quantity['FuelConsumption'],'litres_per_km','L/km' ) )

litres_per_100_km = ureg.unit(
    proportional_unit(
        litres_per_km,
        'litres_per_100_km','L/(100 km)',
        Fraction(1,100)
    )
)

Calculations proceed as might be expected

distance = qvalue(25.6,kilometre)
fuel = qvalue(2.2,litre)

consumes = fuel/distance
print( "average consumption =", qresult( consumes, litres_per_100_km ) )

distance = qvalue(155,kilometre)
print( 'fuel required =', qresult( consumes * distance ) )

which gives the following results [2].

average consumption = 8.59375 L/(100 km)
fuel required = 13.3203125 L

It is interesting that QV can treat distance and volume as quite distinct quantities, although they share the dimension of length in the SI [3].

Electrical quantities

Electrical measurements involve particular quantities, and associated units. We can use base quantities \(V\), \(I\) and \(T\), for potential difference, current and duration, respectively. Then additional quantities of interest include: resistance, capacitance, inductance, energy, power and angular frequency. The context can be configured, as follows

quantity = Context( ("Current","I"),("Voltage","V"),("Time","T") )

quantity.declare('Resistance','R','Voltage/Current')
quantity.declare('Capacitance','C','I*T/V')
quantity.declare('Inductance','L','V*T/I')
quantity.declare('Angular_frequency','F','1/T')
quantity.declare('Power','P','V*I')
quantity.declare('Energy','E','P*T')

Suitable units are:

ureg =  UnitRegister("Reg",quantity)

volt = ureg.unit( RatioScale(quantity.Voltage,'volt','V') )
second = ureg.unit( RatioScale(quantity.Time,'second','s') )
ampere = ureg.unit( RatioScale(quantity.Current,'ampere','A') )
ohm = ureg.unit( RatioScale(quantity.Resistance,'Ohm','Ohm') )
henry = ureg.unit( RatioScale(quantity.Inductance,'henry','H') )
rad_per_s = ureg.unit( RatioScale(quantity.Angular_frequency,'radian_per_second','rad/s') )
watt = ureg.unit( RatioScale(quantity.Power,'watt','W') )
joule = ureg.unit( RatioScale(quantity.Energy,'joule','J') )

Calculations are then straightforward. For example,

from math import pi

v1 = qvalue(0.5,volt)
i1 = qvalue(1.E-3,ampere)
l1 = qvalue(0.3E-3,henry)
w1 = qvalue(2*pi*2.3E3,rad_per_s)

r1 = v1/i1

print( "resistance =", qresult(r1) )
print( "reactance =", qresult(w1*l1) )
print( "energy =", qresult(0.5*l1*i1*i1) )
print( "power =", qresult(v1*i1) )

r2 = qvalue(2.48E3,ohm)
print(  "parallel resistance =",  qresult( (r1*r2)/(r1 + r2) ) )

Which produces

resistance = 500.0 Ohm
reactance = 4.33539786195 Ohm
energy = 1.5e-10 J
power = 0.0005 W
parallel resistance = 416.10738255 Ohm

Ratios

Ratios of the same quantities arise frequently in calculations. These ratios are often described as dimensionless, but they are not plain numbers and the quantities involved should not be ignored.

Dimensionless ratios can retain quantity information if defined using the function qratio.

For example, continuing the electrical case above (where r1 and r2 were evaluated), a resistor network may be used to scale down a voltage by some fraction (often called a potential, or resistive, divider). The resistance ratio can be defined as a dimensionless quantity in this way

quantity.declare( 'Resistance_ratio','R/R', 'Resistance//Resistance' )
ureg.unit( RatioScale(quantity.Resistance_ratio,'ohm_per_ohm','Ohm/Ohm') )

divider = qratio( r2,(r1+r2) )

v_in = qvalue( 5.12, volt)
v_out = qresult(divider * v_in)

if divider.unit.is_ratio_of(ohm.kind_of_quantity):
    print( "Resistive divider" )
    print( "  ratio =", divider )
    print( "  v_out =", v_out )

which produces the output

Resistive divider
  ratio = 0.832214765101 Ohm/Ohm
  v_out = 4.26093959732 V

Note, we use the operator // when declaring a dimensionless ratio as a kind of quantity. This is necessary to preserve information about the quantities in the ratio.

Another example is the voltage gain of an amplifying stage

from QV.prefix import micro

microvolt = ureg.unit( micro(volt) )

quantity.declare('Voltage_ratio','V/V','Voltage//Voltage')
volt_per_volt= ureg.unit( RatioScale(quantity.Voltage_ratio,'volt_per_volt','V/V') )

volt_per_millivolt = ureg.unit( proportional_unit(volt_per_volt,'volt_per_millivolt','V/mV',1E3) )
volt_per_microvolt = ureg.unit( proportional_unit(volt_per_volt,'volt_per_micovolt','V/uV',1E6) )

v1 = qvalue(0.5,volt)
v2 = qvalue(0.5,microvolt)
gain = qratio( v1, v2 )

print( "Gain =", qresult(gain) )
print( "Gain =", qresult(gain,volt_per_microvolt) )
print( "Gain =", qresult(gain,volt_per_millivolt) )
print( "Gain =", qresult(gain,volt_per_volt) )

The output is (Note, when no preferred unit is given (the first case), units are simplified to a dimensionless quantity.)

Gain = 1000000.0
Gain = 1.0 V/uV
Gain = 1000.0 V/mV
Gain = 1000000.0 V/V

Angles

It is well known that some SI quantities have the same dimensions and so cannot be distinguished by dimensional analysis [Brownstein]. In the case of angle, this ambiguity can be removed by introducing a new dimensional constant \(\eta\) but then some of the basic equations of physics also have to be changed [Quincey].

It is not as bad as it sounds. For example, the well-known equation

\[s = r \cdot \theta \;,\]

for the length of arc subtended by an angle \(\theta\) on a circle of radius \(r\), becomes

\[s = \eta \cdot r \cdot \theta \;.\]

In this equation, \(\theta\) has the dimension \(A\) and the constant \(\eta\) has the dimension \(A^{-1}\), so \(s\) has the dimension of length, as expected (references [Brownstein] and [Quincey] should be consulted for more detail).

No one is suggesting that a dimension for angle should be added to the SI, however, a number of authors have remarked that using an extra dimension in computer systems would obtain more reliable dimensional homogeneity checks. The quantity-value package is perfect for this. The following simple example shows how the arc length calculation can be coded. More particularly, it shows how to introduce the dimension for angle and define the dimensional constant \(\eta\).

quantity = Context( ("Length","L"), ("Time","T"), ("Angle","A") )
InverseAngle = quantity.declare('InverseAngle','1/A','1/A')

xi = UnitRegister("xi",quantity)

metre = xi.unit( RatioScale(quantity['Length'],'metre','m') )
second = xi.unit( RatioScale(quantity['Time'],'second','s')  )
radian = xi.unit( RatioScale(quantity['Angle'],'radian','rad')  )
inv_radian = xi.unit( RatioScale(quantity['InverseAngle'],'per radian','1/rad')  )

from math import pi

# Constants
PI = qvalue( pi, radian )
ETA = qresult( 1.0 / PI )

print( "pi =", PI)
print( "eta =", ETA )

radius = qvalue( 0.1, metre )
angle = qresult( PI/8 )
arc_length = qresult( ETA * angle * radius )

print( "arc length =", arc_length )

The output displays

pi = 3.14159265359 rad
eta = 0.318309886184 1/rad
arc length = 0.0125 m

Footnotes

[Brownstein] (1,2)
    1. Brownstein, Angles - lets treat them squarely, Am. J. Phys. 65(7), July 1997, pp 605-614 .

[Quincey] (1,2)
  1. Quincey and R. J. C. Brown, Implications of adopting plane angle as a base quantity in the SI, Metrologia 53, 2016, pp 998-1002.