Intuitive and unastonishing physical quantities, units, constants, and uncertainties in Python
Project description
quanstants
A small package to make life easier for scientists who need physical quantities, units, and constants.
quanstants
strives to make doing scientific calculations in Python as intuitive as doing them on paper.
It is strongly inspired by the LaTeX package siunitx
, and of course also astropy
and pint
.
What makes quanstants
different?
An emphasis on the principle of least astonishment.
Importantly, applied from the perspective of a non-programmer in the natural sciences.
quanstants
is particularly suited to situations where it is desirable to have the result match exactly that which you would produce yourself, and less suited to huge numbers of complex calculations.
For example, the implicit sanity checking that comes with tracking units helps to ensure calculations are being done correctly, and it works very well for programmatically producing nicely formatted text or LaTeX code for presentation of calculation results in e.g. a scientific publication.
quanstants
makes life easy by providing the following:
- The
Quantity
object, which describes a physical quantity as a product of a number and a unit, per the SI definition, with an associated uncertainty - Arithmetic with quantities that is intelligent and unit-aware
- Tracking units is an extremely useful sanity check to avoid mistakes
- Impossible mathematical operations are flagged and prevented
- Addition/subtraction of quantities with the same dimensions but different units works, via conversion
- A large library of units including SI, metric, imperial, and non-standard units
- Easy creation of quantities through arithmetic between numbers and units
- Easy unit conversion/expression of quantities in alternative units
- Robust parsing of string input for quantity creation
- Only a small core selection is loaded by default, but further unit modules are very easy to load (see examples below)
- Support for temperatures on non-absolute scales such as Celsius and Fahrenheit, and sensible, unsurprising behaviour of those temperatures in calculations
- A large library of constants covering various fields, not just physics
- Easy definition of custom constants
- Default behaviours for numbers that align with non-programmers' expectations, rather than Python defaults
5.2 * m
gives exactly5.2 m
, not5.2000000000000001776356... m
0.1 m + 0.2 m
is exactly equal to0.3 m
(whereas in Python,0.1 + 0.2 == 0.3
returnsFalse
)- Significant figures are maintained:
1.30 m
does not become1.3 m
, and0.15 m - 0.05 m
⇒0.10 m
, not0.1 m
- Rounding gives the same result as most scientists would write if rounding manually:
1.75 m
⇒1.8 m
,1.65 m
⇒1.7 m
, and-1.65 m
⇒-1.7 m
("round half away from zero", whereas the Python default is "round half to even") - Rounding to a certain number of significant figures is easy
- Use of the
Decimal
type under the hood to avoid the quirks of binary floating point, which as well as enabling the above, avoids imprecision arising from rounding errors- Most mathematical operations and functions are delegated to Python's
Decimal
, which in turn follows the General Decimal Arithmetic Specification and thus indirectly IEEE 754.quanstants
simply tracks units and uncertainties on top, and aims to support all operations onQuantity
that are supported byDecimal
- Most mathematical operations and functions are delegated to Python's
- Customization of various options including the rounding behaviour, automatic cancellation of units, and many elements of printing formatting
- Configuration via a
.toml
file – autodetected in the current directory, but can also be manually loaded
- Configuration via a
Usage
Quantity creation
Create quantities easily by multiplying numbers and units:
>>> from quanstants import units as qu
>>> 4 * qu.metre
Quantity(4, m)
>>> print(4 * qu.metre)
4 m
Units are also available under their symbol:
>>> 4 * qu.m
Quantity(4, m)
>>> qu.m is qu.metre # Both refer to same object
True
And under alternative names too where appropriate:
>>> from quanstants import units as qu
>>> 4 * qu.meter
Quantity(4, m)
Use the usual exponent syntax with units:
>>> 4 * qu.metre**2
Quantity(4, m²)
>>> 4 * qu.joule * qu.kilogram**-1
Quantity(4, J kg⁻¹)
Alternatively, use division:
>>> 4 * qu.metre / qu.second
Quantity(4, m s⁻¹)
But watch out for Python's order of operations, which still applies as normal:
>>> 8 * qu.J * qu.mol**-1 * qu.kg**-1 # Exponention takes priority over multiplication
Quantity(8, J mol⁻¹ kg⁻¹)
>>> 8 * qu.J / qu.mol * qu.kg # Evaluated left to right as ((8 * J) / mol) * kg
Quantity(8, J kg mol⁻¹)
>>> 8 * qu.J / (qu.mol * qu.kg) # Terms in parentheses evaluated first
Quantity(8, J mol⁻¹ kg⁻¹)
Numbers can be given as int
, float
, str
, or Decimal
, and the quantity will show the expected result and also retain the precision provided:
>>> 4010 * qu.metre**3
Quantity(4010, m³)
>>> 4.01 * qu.volt
Quantity(4.01, V)
>>> "4010" * qu.coulomb # Python's Decimal assumes all figures are significant
Quantity(4010, C)
>>> "4.01e3" * qu.coulomb
Quantity(4.01E+3, C)
>>> from decimal import Decimal
>>> Decimal("0.401") * qu.newton * qu.metre
Quantity(0.401, N m)
>>> Decimal("40.1") * qu.newton * qu.metre
Quantity(40.1, N m)
Contrast the float
result with the default behaviour of Decimal
:
>>> Decimal(4.01)
Decimal('4.0099999999999997868371792719699442386627197265625')
Though note that, unavoidably, trailing zeroes in a float
will have been dropped before being passed to Quantity
:
>>> 741.60 * qu.g * qu.mol**-1
Quantity(741.6, g mol⁻¹)
>>> "741.60" * qu.g * qu.mol**-1
Quantity(741.60, g mol⁻¹)
If necessary, quantities can also be created using the Quantity
class:
>>> from quanstants import units as qu, Quantity
>>> density = Quantity(0.997, qu.kg/qu.L)
>>> print(density)
0.997 kg L⁻¹
Units and prefixes
Multiply a prefix with a unit to create a prefixed unit:
>>> from quanstants import units as qu, prefixes as qp
>>> 50 * (qp.micro * qu.metre)
Quantity(50, μm)
>>> 50 * (qp.μ * qu.m)
Quantity(50, μm)
Binary prefixes are also defined:
>>> 256 * (qp.gibi * qu.byte)
Quantity(256, GiB)
>>> (256 * (qp.giga * qu.byte)) < (256 * (qp.gibi * qu.byte))
True
Various common prefixed units are pre-generated and available within the quanstants.units
namespace, but not under their symbol to avoid clashes with unprefixed units:
>>> 50 * qu.micrometre
Quantity(50, μm)
>>> 50 * qu.micron
Quantity(50, μm)
>>> "99.7" * qu.megahertz # qu.MHz raises AttributeError
Quantity(99.7, MHz)
A huge selection of units is available, including metric units not approved by the SI:
>>> 45032.5 * qu.kilowatthour
Quantity(45032.5, kWh)
>>> 1.27 * qu.carat
Quantity(1.27, ct)
Other systems and categories of units are easily accessed by importing the appropriate submodule, which adds the units within to quanstants.units
:
>>> from quanstants.units import imperial
>>> 6 * qu.foot
Quantity(6, ft)
>>> from quanstants.units import us
>>> 20 * qu.us_fluid_ounce
Quantity(20, fl oz)
>>> 32 * qu.nautical_mile
Quantity(32, nmi)
>>> 32 * (qp.nano * qu.mile) # Nothing to stop you doing this
Quantity(32, nmi)
>>> 32 * qu.nautical_mile == 32 * (qp.nano * qu.mile)
False
Units are often made available in multiple submodules if shared between systems, so you only need to worry about importing the unit system of interest:
>>> from quanstants.units import imperial, us
>>> imperial.foot is us.foot
True
>>> imperial.foot is us.us_survey_foot
False
Currently, quanstants
comes by default with a set of >51 pre-defined units, accessible under >113 different names in the quanstants.units
namespace module.
An additional 87 prefixed units are also pre-defined.
With all unit submodules imported, >117 (unprefixed) units are defined under >212 different names.
This compares to ~950 defined names in a default pint
UnitRegistry()
.
astropy.units
contains >3900 defined names, but most are just prefixed units, so the total number of unique unprefixed units is more like 70 to 80.
Uncertainties
The numerical value of a quantity can be most conveniently given an associated uncertainty using the provided method:
>>> from quanstants import units as qu
>>> gravity = ("6.67430e-11" * qu.newton * qu.metre**2 * qu.kilogram**-2).with_uncertainty("0.00015e-11")
>>> gravity
Quantity(6.67430E-11, N m² kg⁻², uncertainty=1.5E-15)
The uncertainty can be given as a number with the same units as the quantity, or as a Quantity
itself:
>>> ("4.2" * qu.m).plus_minus("0.2") # Alias for with_uncertainty()
Quantity(4.2, m, uncertainty=0.2)
>>> ("4.2" * qu.metre).plus_minus("20" * qu.centimetre)
Quantity(4.2, m, uncertainty=0.20)
When printed, uncertainties are shown by parentheses if possible, or with ±
as a fallback:
>>> print(("6.67430e-11" * qu.newton * qu.metre**2 * qu.kilogram**-2).with_uncertainty("0.00015e-11"))
6.67430(15)E-11 N m² kg⁻²
>>> print(("4.2" * qu.metre).plus_minus("20" * qu.centimetre)) # Precisions don't match
4.2 ± 0.20 m
The use of ± at all times can be forced by setting quanstants.quanfig.UNCERTAINTY_STYLE
.
The uncertainty can also be passed to Quantity()
directly:
>>> from quanstants import units as qu, Quantity
>>> density = Quantity(0.99704702, qu.kg/qu.L, uncertainty=0.00000083)
>>> print(density)
0.99704702(83) kg L⁻¹
A request for the uncertainty of a quantity returns it as another Quantity
:
>>> density.uncertainty
Quantity(8.3E-7, kg L⁻¹)
Parsing strings
Though not generally the preferred option for quantity creation, quantities can also be created by passing a single string as an argument to the Quantity
class with the number and unit separated by a space:
>>> Quantity("4 m")
Quantity(4, m)
>>> Quantity("4 metre")
Quantity(4, m)
>>> Quantity("741.60 g mol-1")
Quantity(741.60, g mol⁻¹)
>>> Quantity("0.997e3 g/L")
Quantity(997, g L⁻¹)
Uncertainties can also be specified in the string, in either of two usual forms:
>>> Quantity("6.67430(15)E-11 N m² kg⁻²")
Quantity(6.67430E-11, N m² kg⁻², uncertainty=1.5E-15)
>>> Quantity("8.293 ± 0.010 V")
Quantity(8.293, V, uncertainty=0.010)
>>> Quantity("8.293 +/- 0.010 V") # Also acceptable
Quantity(8.293, V, uncertainty=0.010)
Arithmetic
Mathematical operations are in general performed with the quantity considered to be the product of its number and unit.
The implementation of arithmetic on the numerical part(s) is typically that of the Decimal
type.
Quantities can be multiplied and divided by other quantities or by numbers, and the results will possess the correct number to the correct precision, and the correct units:
>>> (4 * qu.metre * qu.second**-1) * (6 * qu.second)
Quantity(24, m)
>>> ("3.20" * qu.W) * "16.90"
Quantity(54.0800, W)
>>> (200 * qu.megajoule) / (70 * qu.kilogram)
Quantity(2.857142857142857142857142857, MJ kg⁻¹)
Raising to the power of a number is also supported for integer and fractional powers:
>>> (3 * qu.watt)**2
Quantity(9, W²)
>>> from fractions import Fraction
>>> (20 * qu.metre**2)**Fraction(1, 2)
Quantity(4.472135954999579392818347337, m)
Addition and subtraction is possible when it makes physical sense i.e. when the units of the two quantities are the same or have the same dimensions:
>>> (4 * qu.metre) + (0.5 * qu.metre)
Quantity(4.5, m)
>>> (4 * qu.metre) - (50 * qu.centimetre)
Quantity(3.50, m)
>>> from quanstants.units import us
>>> (4 * qu.metre) + (2 * qu.foot)
Quantity(4.6096, m)
If the units do not match, a MismatchedUnitsError
will be raised, which serves as a useful sanity check:
>>> (4 * qu.metre) + (3 * qu.kilogram)
MismatchedUnitsError: Can't add quantity in Unit(m) to quantity in Unit(kg).
Similarly, (in)equalities are implemented between quantities of the same dimensions:
>>> (0.3 * qu.litre) > (150 * qu.millilitre)
True
>>> (0.15 * qu.litre) >= (150 * qu.millilitre)
True
>>> (0.3 * qu.litre) > (150 * qu.centimetre**3)
True
Quantities can be used as exponents themselves, and have the same sqrt()
, exp()
, ln()
, and log10()
functions implemented as Decimal
does:
>>> a = 100 * qu.m
>>> b = 25 * qu.m
>>> a/b
Quantity(4, (unitless))
>>> 2**(a/b)
Quantity(16, (unitless))
>>> (a/b).sqrt()
Quantity(2, (unitless))
>>> (a/b).exp()
Quantity(54.59815003314423907811026120, (unitless))
>>> (a/b).ln()
Quantity(1.386294361119890618834464243, (unitless))
>>> (a/b).log10()
Quantity(0.6020599913279623904274777894, (unitless))
>>> a.log10()
NotDimensionlessError: Cannot take the logarithm of a non-dimensionless quantity!
You can easily check whether a quantity is dimensionless:
>>> (a/b).is_dimensionless()
True
Uncertainties are kept track of correctly across arithmetic operations using uncertainty propagation rules:
>>> a = (3 * qu.m).plus_minus(0.1)
>>> b = (2 * qu.m).plus_minus(0.2)
>>> a + b
Quantity(5, m, uncertainty=0.2236067977499789696409173669)
>>> a - b
Quantity(1, m, uncertainty=0.2236067977499789696409173669)
>>> a * b
Quantity(6, m², uncertainty=0.6324555320336758663997787090)
>>> a / b
Quantity(1.5, (unitless), uncertainty=0.1581138830084189665999446772)
>>> 2**(a/b)
Quantity(2.828427124746190097603377448, (unitless), uncertainty=0.3099848428288716908396318060)
By default, the correlation between two quantities is assumed to be zero. Uncertainties in the results of operations on two correlated quantities can also be obtained, however, by accessing the corresponding dunder method of the quantity directly and giving the correlation:
>>> a.__add__(b, correlation=0.7)
Quantity(5, m, uncertainty=0.2792848008753788233976784908)
>>> a.__sub__(b, correlation=1)
Quantity(1, m, uncertainty=0.1)
As a result of the high precision of Decimal
being kept and maintained across operations, the numbers involved can get unwieldy.
By default, the printed/string representation of a long fractional decimal is truncated, while the formal representation produced with repr
retains all precision:
>>> energy_density = (200 * qu.megajoule).plus_minus(13) / (70 * qu.kilogram).plus_minus(0.5)
>>> print(energy_density)
2.85714285… ± 0.18683224… MJ kg⁻¹
>>> repr(energy_density)
'Quantity(2.857142857142857142857142857, MJ kg⁻¹, uncertainty=0.1868322484107889153699226089)'
As can be seen above, this is not the same as rounding; the trailing digits are simply omitted, and the value of the final digit shown is unchanged.
The number of digits at which truncation begins can be customized by setting quanstants.quanfig.ELLIPSIS_LONG_DECIMAL
to an integer, or turned off completely by setting it to 0
.
Note that the numbers and uncertainties of constants are never truncated.
Conversion
Expressing a quantity in other units is done easily:
>>> (2 * qu.metre).to(qu.millimetre)
Quantity(2E+3, mm)
>>> (6 * qu.foot).to(qu.metre)
Quantity(1.8288, m)
>>> (6 * qu.hour).to(qu.s)
Quantity(21600, s)
>>> from quanstants import prefixes as qp
>>> ((3 * (qp.kilo * qu.watt)) * (1 * qu.day)).to(qu.joule)
Quantity(2.59200E+8, J)
Use the base()
function to express a quantity in terms of SI base units:
>>> (50 * qu.joule).base()
Quantity(50, m² kg s⁻²)
Use the cancel()
function to combine like terms in the unit after a calculation:
>>> speed = 3 * qu.m * qu.s**-1
>>> time = 15 * qu.s
>>> distance = 45 * (qu.m * qu.s * qu.s**-1)
>>> distance
Quantity(45, m s s⁻¹)
>>> distance.cancel()
Quantity(45, m)
Note that by default quanstants.quanfig.AUTO_CANCEL
is set to True
, in which case cancelling happens automatically after arithmetic operations:
>>> speed * time
Quantity(45, m)
Use fully_cancel()
to additionally combine units with the same dimensions:
>>> x = (30 * qu.kilowatt * qu.s) / (200 * qu.s * qu.watt**-1)
>>> x.cancel()
Quantity(0.15, kW W)
>>> x.fully_cancel()
Quantity(0.00015, kW²)
>>> length = (3000 * qu.metre**2) / (20 * qu.foot)
>>> length.cancel()
Quantity(150, m² ft⁻¹)
>>> length.fully_cancel()
Quantity(492.1259842519685039370078740, m)
Note that fully_cancel()
additionally drops any UnitlessUnit
s, which are dimensionless base units equal to 1; this includes radians and steradians:
</code></pre>
<p>The presentation of compound units is dependent on the order of mathematical operations.
While this doesn't affect (in)equalities, it may sometimes be useful to get a canonical, reproducible representation of a quantity and its units, for which the <code>canonical()</code> method is available:</p>
<pre lang="python"><code>>>> mass = 20 * qu.kilogram
>>> acceleration = 3 * qu.metre * qu.second**-2
>>> mass * acceleration
Quantity(60, kg m s⁻²)
>>> acceleration * mass
Quantity(60, m kg s⁻²)
>>> (mass * acceleration).canonical()
Quantity(60, m kg s⁻²)
>>> (acceleration * mass).canonical()
Quantity(60, m kg s⁻²)
Equalities
Both units and quantities are (to a large extent) immutable – once created, their properties are fixed.
A quantity cannot be changed in-place, and operations on one will return a new Quantity
object.
This means units, quantities, and their subclasses are hashable and can be used e.g. as keys in a dict
.
Quantities with equivalent values will compare equal:
>>> 3 * qu.kilometre == 3000 * qu.metre
True
This is the case even if the quantities have uncertainties, even if the uncertainties are different sizes:
>>> 3 * qu.kilometre == (3000 * qu.metre).plus_minus(20)
True
Units are compared based on their value, and are equal to quantities with the same value:
>>> qu.watt == qu.joule / qu.second
True
>>> qu.watt == 1 * qu.J * qu.s**-1
True
A quantity with number 0 is still a valid quantity (and it might have an uncertainty!), but quantities with numbers equal to zero are considered themselves equal to 0 (and therefore equal to each other):
>>> 0 * qu.m
Quantity(0, m)
>>> 0 * qu.m == 0
True
>>> 0 * qu.m == 0 * qu.s
True
Unitless quantities are considered to be equal to their numerical value, and UnitlessUnit
s are considered equal to 1:
>>> 2 * qu.unitless
Quantity(2, (unitless))
>>> 2 * qu.unitless == 2
True
>>> qu.unitless == 1
True
>>> qu.unitless == qu.radian
True
Constants
In a similar fashion to units and prefixes, physical constants are available in the constants
module under various names and symbols:
>>> from quanstants import units as qu, prefixes as qp, constants as qc
>>> qc.Planck_constant
Constant(Planck_constant = 6.62607015E-34 J s)
>>> qc.h
Constant(Planck_constant = 6.62607015E-34 J s)
>>> qc.Planck # Anything ending with "constant" is available under the truncated form too
Constant(Planck_constant = 6.62607015E-34 J s)
Only a small selection is imported initially, but further sets of constants can be added to the constants
module in a way analogous to units:
>>> from quanstants.constants import codata2018
>>> qc.vacuum_electric_permittivity
Constant(vacuum_electric_permittivity = 8.8541878128(13)E-12 F m⁻¹)
Note the case-sensitivity: constants named after a person or other proper noun are capitalized, whereas units named after people are not (as per SI style):
>>> qc.Bohr_radius
Constant(Bohr_radius = 5.29177210903(80)E-11 m)
>>> 16 * qu.tesla # Incidentally, the magnetic field required to levitate a frog
Quantity(16, T)
Mostly, constants behave like quantities:
>>> qc.proton_mass
Constant(proton_mass = 1.67262192369(51)E-27 kg)
>>> E = qc.proton_mass * qc.speed_of_light**2
>>> E
Quantity(1.503277615985125705245525892E-10, kg m² s⁻², uncertainty=4.583651411557769964000000001E-20)
Constants can be used as units by calling the as_unit()
method, which creates a unit with the same value:
>>> qc.proton_mass.to((qp.M * qu.eV)/qc.c.as_unit()**2)
Quantity(938.2720881604903652873556334, MeV c⁻², uncertainty=2.860890187940270488303725942E-7)
Rounding
In the sciences it is typically desirable to round to a number of significant figures rather than decimal places.
quanstants
provides both via appropriate methods:
>>> a = (324.9 * qu.J) * (1.674 * qu.mol**-1)
>>> a
Quantity(543.8826, J mol⁻¹)
>>> a.round_to_places(3)
Quantity(543.883, J mol⁻¹)
>>> a.round_to_figures(4)
Quantity(543.9, J mol⁻¹)
>>> a.round_to_figures(2)
Quantity(5.4E+2, J mol⁻¹)
>>> a.round_to_sigfigs(2) # An alias
Quantity(5.4E+2, J mol⁻¹)
If ndigits
is not provided, rounding is done to the number of places or figures specified by quanstants.quanfig.NDIGITS_PLACES
or quanstants.quanfig.NDIGITS_FIGURES
respectively, which are by default 2 decimal places and 3 significant figures:
>>> a.round_to_places()
Quantity(543.88, J mol⁻¹)
>>> a.round_to_figures()
Quantity(544, J mol⁻¹)
Like siunitx
, if rounding is requested to more decimal places or significant figures than a number has, the default behaviour is to "pad" the number with extra zeroes to reach the desired precision.
This can be turned off globally by setting quanstants.quanfig.ROUND_PAD
to False
, or can be set for a single rounding operation by passing pad=True
or pad=False
to the rounding function:
>>> a.round_to_places(6)
Quantity(543.882600, J mol⁻¹)
>>> a.round_to_places(6, pad=False)
Quantity(543.8826, J mol⁻¹)
If a quantity has a known uncertainty, it can be useful to round the number off to the same precision as the uncertainty using round_to_uncertainty()
.
For this, the uncertainty is first rounded to either a provided number of significant figures, or to the number of significant figures set by quanstants.quanfig.NDIGITS_UNCERTAINTY
, which is by default 1:
>>> b = a.plus_minus(0.03)
>>> b
Quantity(543.8826, J mol⁻¹, uncertainty=0.03)
>>> b.round_to_uncertainty()
Quantity(543.88, J mol⁻¹, uncertainty=0.03)
>>> (1.2345 * qu.m).plus_minus(0.071).round_to_uncertainty(1)
Quantity(1.23, m, uncertainty=0.07)
>>> (1.2345 * qu.m).plus_minus(0.071).round_to_uncertainty(2)
Quantity(1.235, m, uncertainty=0.071)
>>> a.round_to_uncertainty() # Exact quantities are returned unchanged and remain exact
Quantity(543.8826, J mol⁻¹)
Here, padding is only done for the number to make its precision match the uncertainty; no padding is done for the uncertainty rounding, as this would imply an increase in precision:
>>> (1.23 * qu.m).plus_minus(0.0071).round_to_uncertainty(1, pad=True) # Note that True is default
Quantity(1.230, m, uncertainty=0.007)
>>> (1.23 * qu.m).plus_minus(0.0071).round_to_uncertainty(5, pad=True)
Quantity(1.2300, m, uncertainty=0.0071)
A general round()
method is provided, which can take arguments for the rounding method to use with all quantities or for only those with or without uncertainties:
>>> a.round(method_if_uncertainty="PLACES", method_if_exact="FIGURES")
Quantity(544, J mol⁻¹)
>>> b.round(method_if_uncertainty="PLACES", method_if_exact="FIGURES")
Quantity(543.88, J mol⁻¹, uncertainty=0.03)
>>> a.round(method="UNCERTAINTY")
Quantity(543.8826, J mol⁻¹)
>>> b.round(method="UNCERTAINTY")
Quantity(543.88, J mol⁻¹, uncertainty=0.03)
ndigits
, pad
, and mode
can also be passed to round()
, in which case they are passed on to the respective rounding function.
The defaults are set such that calling round()
without any arguments is a quick and convenient way to get a sensibly rounded number – exact quantities are rounded to 3 s.f. and quantities with uncertainties have their uncertainty rounded to 1 s.f. and the quantity is then rounded to the same precision.
>>> a.round()
Quantity(544, J mol⁻¹)
>>> b.round()
Quantity(543.88, J mol⁻¹, uncertainty=0.03)
The methods used can be overruled by setting quanstants.quanfig.ROUND_TO_IF_UNCERTAINTY
and quanstants.quanfig.ROUND_TO_IF_EXACT
.
Passing a Quantity
to Python's built-in round()
simply calls Quantity.round()
:
>>> round(a, 5)
Quantity(543.88, J mol⁻¹)
Most of us are taught to round off numbers to the nearest round number, with 5s rounded away from zero ("round half away from zero"), so when rounding to two significant figures 1.23 becomes 1.2, 1.28 becomes 1.3, 1.25 also becomes 1.3, and -1.25 becomes -1.3.
Python's default behaviour does not match this however – "tie-breaks", or where the deciding digit is a 5, are rounded such that the final digit of the rounded number is even ("round half to even").
This means that even when using Decimal
, rounding results can be unexpected.
quanstants
overrides Python and does by default what most users would do themselves, by rounding half away from zero:
>>> from decimal import Decimal
>>> round(Decimal("1.25"), 1)
Decimal('1.2')
>>> ("1.25" * qu.m).round_to_places(1)
Quantity(1.3, m)
The rounding mode used for quantities can be chosen by setting quanstants.quanfig.ROUNDING_MODE
to any of the decimal
module's rounding modes:
>>> from quanstants import quanfig
>>> quanfig.ROUNDING_MODE = "ROUND_HALF_DOWN"
>>> ("1.25" * qu.m).round_to_places(1)
Quantity(1.2, m)
>>> quanfig.ROUNDING_MODE = "ROUND_HALF_UP"
>>> ("1.25" * qu.m).round_to_places(1)
Quantity(1.3, m)
Note the distinction in quanstants
between a rounding "method" (to decimal places/significant figures/uncertainty) and rounding "mode" (how to round the final digit i.e. up or down).
The rounding takes place in a decimal.localcontext()
, meaning that the user's choices of rounding mode for Decimal
and quanstants
are kept separate:
>>> from decimal import Decimal
>>> round(Decimal("1.25"), 1)
Decimal('1.2')
>>> ("1.25" * qu.m).round_to_places(1)
Quantity(1.3, m)
>>> import decimal
>>> decimal.getcontext().rounding = decimal.ROUND_HALF_UP
>>> quanfig.ROUNDING_MODE = "ROUND_HALF_DOWN"
>>> round(Decimal("1.25"), 1)
Decimal('1.3')
>>> ("1.25" * qu.m).round_to_places(1)
Quantity(1.2, m)
Finally, a method is provided to round just the uncertainty of a quantity without changing the number.
If ndigits
and method
are not specified, they default to quanstants.quanfig.NDIGITS_<rounding method>
and quanstants.quanfig.ROUND_TO_IF_EXACT
respectively (so by default 3 s.f.), and the uncertainty is never padded when rounded in this manner:
>>> quanfig.ROUNDING_MODE = "ROUND_HALF_UP"
>>> c = a.with_uncertainty("0.0323")
>>> c.round_uncertainty(1)
Quantity(543.8826, J mol⁻¹, uncertainty=0.03)
>>> c.round_uncertainty(3, method="PLACES")
Quantity(543.8826, J mol⁻¹, uncertainty=0.032)
Temperatures
quanstants
also has full support for temperatures on various scales.
As usual, a variety of temperature units are made available under a variety of names:
>>> qu.degreeCelsius is qu.celsius
True
>>> qu.degreeCelsius is qu.degreeCentigrade
True
>>> qu.degree_Fahrenheit is qu.fahrenheit
True
Absolute temperatures in kelvin are normal quantities like any other:
>>> T = 400 * qu.kelvin
>>> T
Quantity(400, K)
>>> 2 * T
Quantity(800, K)
>>> (1600 * qu.joule) / T
Quantity(4, J K⁻¹)
Note that multiplying a number by a temperature unit other than kelvin also creates a normal Quantity
, which thus also represents an absolute temperature:
>>> T = 25 * qu.celsius
>>> T
Quantity(25, °C)
>>> T.to(qu.kelvin)
Quantity(25, K)
To instead create a relative temperature on the scale of a TemperatureUnit
, use the @
operator, which can be thought of here as standing for "on":
>>> T = 25 @ qu.celsius
>>> T
Temperature(25, °C)
>>> T.to(qu.kelvin)
Quantity(298.15, K)
Arithmetic with Temperature
instances works correctly, as they are first converted to absolute temperatures in kelvin:
>>> T = 126.85 @ qu.celsius
>>> T
Temperature(126.85, °C)
>>> 2 * T
Quantity(800.00, K)
>>> (1600 * qu.joule) / T
Quantity(4, J K⁻¹)
Taking the difference between two temperatures will give the temperature difference as a Quantity
but in the same unit as the temperature:
>>> (25 @ qu.celsius) - (-5 @ qu.celsius)
Quantity(30, °C)
This distinction between absolute and relative temperatures proves useful e.g. to increase or decrease a temperature by adding or subtracting a temperature increment:
>>> (25 @ qu.celsius) + (25 * qu.celsius)
Temperature(50, °C)
>>> (25 @ qu.celsius) - (25 * qu.celsius)
Temperature(0, °C)
Temperatures can be converted between scales using the on_scale()
method, and this can also be used with quantities representing absolute temperatures:
>>> ((273.15 * qu.kelvin) + (25 * qu.kelvin)).on_scale(qu.celsius)
Temperature(25.00, °C)
>>> (0 @ qu.celsius).on_scale(qu.fahrenheit)
Temperature(32, °F)
Logarithmic scales
Logarithmic units are also a feature of quanstants
.
Similar to the creation of temperatures on a temperature scale above, a logarithmic quantity on a logarithmic scale is created using the @
operator:
>>> a = 30 @ qu.decibel
>>> a
LogarithmicQuantity(30, dB)
Logarithmic quantities cannot be created using the *
operator.
A logarithmic scale always expresses the ratio of two values; as such, a logarithmic unit without an associated reference value is considered to have a reference value of 1:
>>> a.base()
Quantity(1000, (unitless))
A referenced logarithmic unit can be created from an unreferenced one using the provided method:
>>> qu.decibel.with_reference(1 * qu.watt)
LogarithmicUnit(dB, reference=(1 W))
>>> b = 30 @ qu.decibel.with_reference(1 * qu.watt)
>>> b
LogarithmicQuantity(30, dB (1 W))
>>> b.to_absolute()
Quantity(1000, W)
>>> b.base()
Quantity(1000, m² kg s⁻³)
>>> b.to(qu.kilowatt)
Quantity(1.000, kW)
As with temperatures, an absolute quantity can be expressed on a logarithmic scale using the on_scale()
method:
>>> (30 * qu.watt).on_scale(qu.dB.with_reference(1 * qu.watt))
LogarithmicQuantity(14.77121254719662437295027903, dB (1 W))
If the units of the quantity and reference do not match, an error will be raised:
>>> (30 * qu.watt).on_scale(qu.dB.with_reference(1 * qu.hertz))
MismatchedUnitsError: Ratio of quantity to reference value is not dimensionless!
However, if the LogarithmicUnit
passed is unreferenced, the reference value will be assumed to be 1 of the unit of the absolute quantity:
>>> (30 * qu.watt).on_scale(qu.dB)
LogarithmicQuantity(14.77121254719662437295027903, dB (1 W))
Arithmetic is possible with logarithmic quantities on the same scale, but is otherwise undefined:
>>> c = 20 @ qu.decibel.with_reference(1 * qu.watt)
>>> b + c
LogarithmicQuantity(30.41392685158225040750199971, dB (1 W))
>>> b - c
LogarithmicQuantity(29.54242509439324874590055807, dB (1 W))
>>> b * c
LogarithmicQuantity(50, dB (1 W))
>>> b / c
LogarithmicQuantity(10, dB (1 W))
>>> b + (100 * qu.watt)
MismatchedUnitsError: Arithmetic is only possible between logarithmic quantities on the same scale!
Logarithmic quantities may have uncertainties, but these remain defined as absolute quantities as quanstants
does not support asymmetrical uncertainties:
>>> d = b.with_uncertainty(50 * qu.watt)
>>> d
LogarithmicQuantity(30, dB (1 W), uncertainty=(50 W))
>>> d.to_absolute()
Quantity(1000, W, uncertainty=50)
Some common logarithmic scales are already defined in quanstants.units.logarithmic
, generally under their frequently used "suffixed" symbols:
>>> from quanstants.units import logarithmic
>>> 30 @ qu.dBW
LogarithmicQuantity(30, dB (1 W))
>>> 30 @ qu.dBm
LogarithmicQuantity(30, dB (1 mW))
Note that the actual symbol of the unit remains the non-suffixed version.
Despite the common use of such suffixes to denote the use of a particular reference value, this is prohibited by the SI, and when printing logarithmic quantities quanstants
by default follows the recommended style:
>>> print(30 @ qu.dBm)
30 dB (1 mW)
If desired, however, this can be changed by setting the appropriate variable:
>>> from quanstants import quanfig
>>> quanfig.LOGARITHMIC_UNIT_STYLE = "SUFFIX"
>>> print(30 @ qu.dBm)
30 dBm
Configuration
The config object quanstants.quanfig
provides access to many variables that change the behaviour of quanstants
objects:
>>> from quanstants import quanfig
>>> quanfig.ROUNDING_MODE # The rounding mode used by internal Decimal objects
'ROUND_HALF_UP'
>>> quanfig.INVERSE_UNIT # The style to use to show unit division i.e. m s⁻¹ or m / s
'NEGATIVE_SUPERSCRIPT'
Simply set one of the variables to change behaviour:
>>> 3 * qu.m * qu.s**-1
Quantity(3, m s⁻¹)
>>> quanfig.INVERSE_UNIT = "SLASH"
>>> 3 * qu.m * qu.s**-1
Quantity(3, m/s)
Upon being imported for the first time, quanstants
will look for a configuration file called quanstants.toml
.
If one is found, key/value pairs for configuration options within will override the defaults:
[config.rounding]
ROUNDING_MODE = "ROUND_HALF_DOWN"
ROUND_PAD = false
[config.arithmetic]
AUTO_CANCEL = false
[config.printing]
INVERSE_UNIT = "SLASH"
With the result being:
>>> from quanstants import units as qu
>>> print(3 * qu.m * qu.s * qu.s**-1)
3 m s/s
Available configuration options are contained in quanfig.options
as a dictionary or can be printed to stdout
by calling the provided method on quanfig
:
>>> from quanstants import quanfig
>>> quanfig.options
>>> quanfig.print_options()
Alternatively, all options and their default values are contained within ./quanstants/config.toml
.
Currently, the following locations are checked in the given order:
- The current working directory, from
pathlib.Path.cwd()
- All parent directories of the above
- The user's config directory at
~/.config/quanstants/quanstants.toml
on macOS and Linux (or on Linux,$XDG_CONFIG_HOME/quanstants/quanstants.toml
), or%USERPROFILE%/AppData/Roaming/quanstants/quanstants.toml
on Windows.
Any options specified in the first file found will override the defaults. Once a file has been found, any other files will be completely ignored.
Custom units and constants can also be defined in quanstants.toml
to enable easy reuse:
[units.derived]
thaum.symbol = "thm"
thaum.value.number = "3.595e16"
thaum.value.unit = "J"
thaum.canon_symbol = true
[constants]
swallow_airspeed_velocity.symbol = "swv"
swallow_airspeed_velocity.value.number = "9"
swallow_airspeed_velocity.value.unit = "m s-1"
swallow_airspeed_velocity.alt_names = [ "european_swallow_airspeed_velocity", "unladen_swallow_airspeed_velocity" ]
>>> from quanstants import units as qu, constants as qc
>>> print(qu.thm, qu.thm.value)
Unit(thm) 3.595E+16 J
>>> print(qc.european_swallow_airspeed_velocity)
swallow_airspeed_velocity = 9 m s⁻¹
Extra .toml
files can be fed to quanfig
and read for config options, units, and constants:
>>> quanfig.load_toml(sections=["config", "units", "constants"], toml_path="my_toml.toml")
Standards and naming
The primary source of authority is the SI brochure (9th Edition v2.01 in English), followed by ISO/IEC 80000.
Following the SI/ISO example means the names of units are primarily those defined there e.g. metre, litre. As far as possible, however, units are also made available under alternative names, particular those common in American English.
For consistency, names of constants are written first and foremost in British/Irish/European English, but should also be available under the American English equivalents.
Comparison to other packages
Both astropy
(via its modules constants
and units
) and pint
are excellent packages, but both had limitations that inspired the creation of quanstants
.
While they and various other packages each do some things very well and have some advanced functionality, all met only a subset of my (fairly basic) needs, so quanstants
aims to take the best of each.
Moreover, I felt it was possible to have an API more user-friendly than the others available.
astropy and pint
- Crucially, neither package makes the same opinionated decisions about number handling and rounding as
quanstants
:- they do not store numbers as
Decimal
by default - they do not implement the more traditional rounding behaviour
- they do not allow easy rounding to significant figures or to match the uncertainty
- they do not store numbers as
- Both treat radians and steradians as base units rather than as dimensionless units, making working with angles more complicated
astropy
- The
quanstants
API is fairly similar to that ofastropy
- The unit and constant selection is much broader than simply those useful for astronomers/astrophysicists
astropy
does not support conversion to and from relative temperatures on scales other than kelvin, whereasquanstants
has full support for both relative and absolute temperaturesastropy
quantities do not have an associated uncertainty, whereasquanstants
has full uncertainty supportastropy
places a lot more restrictions on which units can be prefixedastropy
is 37 MB at the time of writing, and it depends onnumpy
, adding another 28 MB;quanstants
has no hard dependency onnumpy
or any other large package- As a result of the above, import time is significantly faster
- Also slightly faster in common operations
pint
- The
quanstants
API and codebase was written to be much simpler, more obvious, and more intuitive than that ofpint
- The immutability of units and quantities, while not total, is much stronger in
quanstants
-- any function or operation that would change aQuantity
returns a newQuantity
- Uncertainty support exists in
pint
but only via extension with a package, and it is often buggy;quanstants
offers out-of-the-box native support for uncertainties that is reliable pint
contains lots of units, but not many constants;quanstants
contains significantly more constant values- Significantly faster in common operations
What is not (yet) supported?
The following are aimed for but are not yet implemented:
- Complex numbers
- Adjustment of prefixes (either automatically or on request) so that e.g. 2000 kJ becomes 2 MJ
- Type hints to require quantities with a certain unit or dimension
numpy
ndarray
s of quantitiesArrow
arrays of quantities- Use in
polars
dataframes
Why Decimal?
For the general arguments, see the documentation for Decimal
, but to name a few:
-
It easily allows significance and precision to be preserved.
-
The lack of exactness of floats, i.e. that
0.1 + 0.1 + 0.1 - 0.3 != 0
and0.1 + 0.2 != 0.3
. -
Decimal numbers are what the user believes them to be. An inexperienced user thinks that the float
8.7
is 8.7, because that is what is printed by Python, when in reality it is stored as8.699999999999999289457264239899814128875732421875
. -
The ease of implementation of the desired rounding behaviours. Significant figures are a challenge without the numerical type having a concept of significance.
-
The rounding behaviour is correct. Even if rounding is configured so that half rounds up,
round(0.35, 1)
will still always give0.3
, because it is actually0.34999999999999997779553950749686919152736663818359375
, this is just not obvious to the user.
Meanwhile, the usual arguments against the use of Decimal
are that the precision of float
is high enough for most applications, and that using binary floats has a performance advantage.
Using Quantity
objects in complex or extensive calculations, as opposed to binary floats or even decimal floats, will of course have a not-unsubstantial associated overhead.
quanstants
is not intended for this usage; but after all, neither is Python – in many cases other advantages win out over pure speed and computational efficiency.
And while from a purely numerical perspective, the error in binary floats is of course technically very small and suffices for calculations, the simple fact that most decimals cannot be represented exactly by a binary float makes life difficult for many applications; for example, the preparation of documents, or for checking more trivial calculations.
By way of example, the original author was motivated originally by their experiences using Python to prepare data for publication in a scientific manuscript.
In one situation, quantities in two columns were all measured to a precision of 3 significant figures, but this precision would not be carried through.
Measured values such as 1.260
would be inserted as "1.26".
Moreover, a third column containing the difference between the first two columns needed to be rounded to two decimal places to reflect the lower precision – a trivial sum to do by hand, but time-consuming for thousands of data points, hence the use of Python in the first place.
Experienced programmers are likely aware of the pitfalls of working with binary mathematics and can identify when binary floating point suffices and when a switch to decimal is necessary for precision. For inexperienced programmers, the behaviour of binary floating point is often perplexing. One aim of this package is to alleviate this confusion.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file quanstants-0.1.0.tar.gz
.
File metadata
- Download URL: quanstants-0.1.0.tar.gz
- Upload date:
- Size: 180.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.12.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8c2f35d051aaf5f7b2dc743bdb487391695d4a42f4ed7676df5b34b8000c7523 |
|
MD5 | 8debbc3349e6d91d25240a9db3c5df2f |
|
BLAKE2b-256 | d44b66c734d3a6097ec23d770b5f7b816536d4e64a266741a58a57c9898223e0 |
File details
Details for the file quanstants-0.1.0-py3-none-any.whl
.
File metadata
- Download URL: quanstants-0.1.0-py3-none-any.whl
- Upload date:
- Size: 81.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.12.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 06112c8cb4d2dee3955d5881e130ed8f1480b097e3caadbd37a4e783d76a62e8 |
|
MD5 | a36a89c9fd90a9db8cc569369e653096 |
|
BLAKE2b-256 | 7ea9f079e6fac82fbfde8725baa46cdb23ead4a8101ca9628c6ea598be6f5c9a |