Skip to contents
library(chemunits) # load the library

What are chemunits?

FIXME: point to [units::set_units()] documentation and [units::valid_udunits()]

Conversions

# standard conversions
set_cu(1, "N") |> set_cu("kN")
## 0.001 [kN]
set_cu(1, "V*A*s") |> set_cu("N*cm")
## 100 [cm*N]
# more complex (and fun but not always useful) 
set_cu(1, "horsepower") |> set_cu("W")
## 745.6999 [W]
set_cu(1, "dram") |> set_cu("g")
## 1.771845 [g]
set_cu(1, "firkin") |> set_cu("L")
## 39.74683 [L]
set_cu(1, "fortnight") |> set_cu("days")
## 14 [d]
set_cu(1, "fathom") |> set_cu("pica")
## 433.6209 [pica]
# more complex and useful for chemistry
set_cu(0, "degC") |> set_cu("degF")
## 32 [degF]
set_cu(32, "degF") |> set_cu("K")
## 273.15 [K]
set_cu(1, "bar") |> set_cu("atm")
## 0.9869233 [atm]
set_cu(1, "bar") |> set_cu("psi")
## 14.50377 [psi]
set_cu(1, "mbar") |> set_cu("mTorr")
## 750.0616 [mTorr]

Preferred units

chemunits_options(preferred_units = c("g", "1"))
set_cu(1, "lb", convert_to_preferred = TRUE)
## 453.5924 [g]
set_units(15, "psi", convert_to_preferred = TRUE) # no preferred unit defined for pressure
## 15 [psi]
chemunits_options(preferred_units = c("g", "bar", "1"))
set_units(15, "psi", convert_to_preferred = TRUE) # FIXME: why does this not work?
## 15 [psi]

Preferred units are particularly convenient when converting units through multiplication and division of different units as described in more details in the Arithmetic section.

Auto-scaling

FIXME

All chemical quantities in this package keep track of the units they represent with a base unit as detailed in the quantities vignette and a metric prefix. By default, the metric prefix is adjusted automatically to keep numeric values in a range close to 1. However, scaling to a specific prefix is easily achieved.

and point to [units::valid_udunits_prefixes()]

# automatic scaling
chemunits_options(auto_scale_units = c("g", "L", "M"))
set_cu(5000, "g") # automatically scaled to kg
## 5 [kg]
set_cu(100, "nL") * 1e5 # automatically scaled to mL
## 10 [mL]
set_cu(1e-4, "M") # automatically scaled to microM
## 100 [µM]
# no automatic scaling
set_cu(0.5, "g", auto_scale = FALSE) # explicly stays g
## 500 [mg]
set_cu(5000, "J") # stays J because it wasn't defined to be included
## 5000 [J]
# enable it for J too
chemunits_options(auto_scale_units = c("g", "L", "M", "J"))
set_cu(5000, "J") # now it becomes kJ
## 5 [kJ]

Formatting

To retrieve numeric values as text that includes their units, simply use [base::format()].

# get quantity with unit
set_cu(100, "mg") |> format()
## [1] "100 [mg]"
set_cu(0:10, "C") |> format()
##  [1] " 0 [C]" " 1 [C]" " 2 [C]" " 3 [C]" " 4 [C]" " 5 [C]" " 6 [C]" " 7 [C]"
##  [9] " 8 [C]" " 9 [C]" "10 [C]"
set_cu(760, "Torr") |> format()
## [1] "760 [Torr]"
# by default autosales each value individually if the unit is scalalbe
set_cu(c(0.1, 1, 1000), "g") |> format() # auto-scalable unit
## [1] "100 [mg]" "1 [g]"    "1 [kg]"
set_cu(c(0.1, 1, 1000), "N") |> format()
## [1] "1e-01 [N]" "1e+00 [N]" "1e+03 [N]"
# but the autoscaling can be turned off too
set_cu(c(0.1, 1, 1000), "g") |> format(auto_scale = FALSE, scientific = FALSE)
## [1] "   0.1 [g]" "   1.0 [g]" "1000.0 [g]"
# or you can specify the desired unit
set_cu(c(0.1, 1, 1000), "g") |> format(units = "kg", scientific = FALSE)
## [1] "0.0001 [kg]" "0.0010 [kg]" "1.0000 [kg]"
# of course impossible conversion don't work
try(set_cu(1, "kg") |> format(units = "J"))
## Error in `units<-`(`*tmp*`, value = units) : 
##   cannot convert kg into J

The numeric value of a quantity can be retrieved via the get_chemvalue()/get_cv() function which converts to the requested units (if possible) and returns the numeric value in those units.

# FIXME: move this to the end
set_cu(100, "mg") |> get_cv("mg") # returns 100
## [1] 100
set_cu(1, "kg") |> get_cv("lb") # can use any valid unit
## [1] 2.204623
set_cu(0, "degC") |> get_cv("degF") # or use it for unit conversion
## [1] 32
set_cu(760, "Torr") |> get_cv("atm") # or use it for unit conversion
## [1] 1
try(set_cu(1, "kg") |> get_cv("M")) # impossible conversions fail
## Error in `units<-`(`*tmp*`, value = units) : 
##   cannot convert kg into M

The units of a number can also be retrieved directly with the get_units() function for vectors, lists, as well as entire data frames (see details on the latter in the data frames section).

set_cu(5000, "g") |> get_units()
## [1] "kg"
x <- list(
  a = set_cu(5000, "g"),
  b = 42,
  c = set_cu(100, "mbar")
) 
x |> get_units()
##      a      b      c 
##   "kg"     NA "mbar"

Arithmetic

Several common arithmetic operations are implemented for easy inter-conversion between quantities. All arithmetic operations also automatically keep track of the units and metric prefixes for correct calculations.

Addition and Subtraction

Quantities of the same type (e.g. masses, volumes, etc.) can be added or subtracted with proper interpration of the metric prefixes. The resulting quantity will be scaled to the best metric prefix as described above. Attempts to add or subtract non-matching quantities (e.g. mass + volume) or a quantity and a number without units will fail with an error to avoid unexpect behaviour and ambiguous calculations.

set_cu(0.003, "g") - set_cu(2, "mg") + set_cu(5, "µg") # 1.005 mg
## 1.005 [mg]
try(set_cu(1, "g") + set_cu(1, "L")) # not allowed
## Error in `units<-`(`*tmp*`, value = units(e1)) : 
##   cannot convert L into g
try(set_cu(1, "g") + 1) # not allowed
## Error in Ops.units(set_cu(1, "g"), 1) : 
##   both operands of the expression should be "units" objects

Multiplication / Division

Quantities can be multipled/divided by a number. The resulting quantity will be scaled to the best metric prefix. This is most commonly used with multiplication/division by 1000.

set_cu(1, "mg") * 1000 # convert mg into g
## 1 [g]
set_cu(1, "mg") / 1e6 # convert mg into ng
## 1 [ng]

Quantities can also be divided by another quantity of the same type (e.g. a mass by another mass) effectively canceling out the units resulting in a unitless ratio (symbolized by [1]) with the metric prefixes properly taken into consideration.

# how many mg in a kg?
set_cu(1, "kg") / set_cu(1, "mg")
## 1e+06 [1]

Thanks to the underlying units package and UDUNITS-2 API, almost any unit calculations are possible. In cases where the result can be converted to one of your preferred units, that conversion and proper prefix scaling also happens automatically. A few examples below

Concentration (Molarity) = Amount / Volume

# calculate molarity --> uh oh, no automatic converstion to M
set_cu(5, "nmol") / set_cu(50, "mL") 
## 0.1 [nmol/mL]
# set preferred units
chemunits_options(preferred_units = c("mol", "g", "g/mol", "M/bar", "M", "L", "1"))
set_cu(5, "nmol") / set_cu(50, "mL") # calculation molarity
## 100 [nM]
set_cu(5, "nmol") / set_cu(100, "nM") # calculate volume
## 50 [mL]
set_cu(100, "nM") * set_cu(50, "mL") # calculate amount
## 5e-09 [mol]

Concentration (Density) = Mass / Volume

set_cu(5, "ng") / set_cu(50, "mL") # calculate density
## 100 [pg/mL]
set_cu(5, "ng") / set_cu(100, "ng/L") # calculate volume
## 50 [mL]
set_cu(100, "ng/L") * set_cu(50, "mL") # calculate mass
## 5 [ng]

Amount = Mass / Molecular Mass

set_cu(10, "g") / set_cu(50, "g/mol") # calculate amount
## 0.2 [mol]
set_cu(10, "g") / set_cu(200, "mmol") # calculate molecular weight
## 50 [g/mol]
set_cu(200, "mmol") * set_cu(50, "g/mol") # calculate mass
## 10 [g]

Solubility = Molarity / Pressure

set_cu(10, "mM") / set_cu(200, "mbar") # calculate solubility
## 50 [mM/bar]
set_cu(10, "mM") / set_cu(50, "mM/bar") # calculate pressure
## 0.2 [bar]
set_cu(50, "mM/bar") * set_cu(200, "mbar") # calculate molarity
## 10 [mM]

Comparisons

Quantities can be compared with all common logic operators (>, >=, <, <=, ==, !=) taking the metric scaling properly into consideration. Attempts to compare non-matching quantities (e.g. mass & volume) will fail with an error to avoid unexpect behaviour. Comparisons of quantities with numerics are allowed but it is important to be cautious about these since the metric scaling in quantities affects the numeric value.

set_cu(5, "mg") < set_cu(1, "g")
## [1] TRUE
set_cu(5, "mg") > set_cu(10, "ng")
## [1] TRUE
set_cu(5, "mg") == set_cu(0.005, "g")
## [1] TRUE
set_cu(5, "mg") != set_cu(5, "g")
## [1] TRUE
try(set_cu(1, "mg") > set_cu(1, "L")) # not allowed (different quantities)
## Error in Ops.units(set_cu(1, "mg"), set_cu(1, "L")) : 
##   cannot compare non-convertible units

It is important to note that due to machine errors, the == is best avoided in favor of more reliable comparisons such as tests that check whether the difference between quantities is smaller than a tiny quantity:

x <- set_cu(1, "mg")

# whether it's *3 and then /3 or the other way around should be identical
# but is not because of machine precision
x*3/3 == x/3*3
## [1] FALSE
dplyr::near(x*3/3, x/3*3, tol = set_cu(1, "fg")) # the same within 1 femtogram
## [1] TRUE

Data Frames

Units (and thus chemunits as well) are fully supported in dplyr type data frames (tibbles) and the units are displayed underneath the column headers, e.g. [mL]:

library(dplyr)
df <- tibble(
  weight = set_cu(1:5, "mg"),
  volume = set_cu(20, "mL")
)
df
## # A tibble: 5 × 2
##   weight volume
##     [mg]   [mL]
## 1      1     20
## 2      2     20
## 3      3     20
## 4      4     20
## 5      5     20
# or for formatted output in notebooks
df |> knitr::kable()
weight volume
1 [mg] 20 [mL]
2 [mg] 20 [mL]
3 [mg] 20 [mL]
4 [mg] 20 [mL]
5 [mg] 20 [mL]
# or with explicit units in the header
df |> make_units_explicit() |> knitr::kable()
weight [mg] volume [mL]
1 20
2 20
3 20
4 20
5 20

This also means that all arithmetic operations are allowed within data frames.

df <- tibble(
  id = 1:5,
  weight = set_cu(1:5, "mg"),
  vol = set_cu(20, "mL"),
  mw = set_cu(500, "g/mol"),
  amount = weight / mw,
  conc = amount / vol
)
df
## # A tibble: 5 × 6
##      id weight  vol      mw   amount conc
##   <int>   [mg] [mL] [g/mol]    [mol] [µM]
## 1     1      1   20     500 0.000002  100
## 2     2      2   20     500 0.000004  200
## 3     3      3   20     500 0.000006  300
## 4     4      4   20     500 0.000008  400
## 5     5      5   20     500 0.00001   500

To get the columns’ units use get_units()

df |> get_units()
##      id  weight     vol      mw  amount    conc 
##      NA    "mg"    "mL" "g/mol"   "mol"    "µM"

To make units of specific columns explicit, use make_units_explicit()

# make all units columns explicit
df |> make_units_explicit()
## # A tibble: 5 × 6
##      id `weight [mg]` `vol [mL]` `mw [g/mol]` `amount [mol]` `conc [µM]`
##   <int>         <dbl>      <dbl>        <dbl>          <dbl>       <dbl>
## 1     1             1         20          500       0.000002         100
## 2     2             2         20          500       0.000004         200
## 3     3             3         20          500       0.000006         300
## 4     4             4         20          500       0.000008         400
## 5     5             5         20          500       0.00001          500
# change the prefix and suffix
df |> make_units_explicit(prefix = ".", suffix = "")
## # A tibble: 5 × 6
##      id weight.mg vol.mL `mw.g/mol` amount.mol conc.µM
##   <int>     <dbl>  <dbl>      <dbl>      <dbl>   <dbl>
## 1     1         1     20        500   0.000002     100
## 2     2         2     20        500   0.000004     200
## 3     3         3     20        500   0.000006     300
## 4     4         4     20        500   0.000008     400
## 5     5         5     20        500   0.00001      500
# make a specific subset of columns explicit (using tidy select syntax)
df |> make_units_explicit(weight:mw) # from weight to mw
## # A tibble: 5 × 6
##      id `weight [mg]` `vol [mL]` `mw [g/mol]`   amount conc
##   <int>         <dbl>      <dbl>        <dbl>    [mol] [µM]
## 1     1             1         20          500 0.000002  100
## 2     2             2         20          500 0.000004  200
## 3     3             3         20          500 0.000006  300
## 4     4             4         20          500 0.000008  400
## 5     5             5         20          500 0.00001   500
df |> make_units_explicit(-mw) # all but mw
## # A tibble: 5 × 6
##      id `weight [mg]` `vol [mL]`      mw `amount [mol]` `conc [µM]`
##   <int>         <dbl>      <dbl> [g/mol]          <dbl>       <dbl>
## 1     1             1         20     500       0.000002         100
## 2     2             2         20     500       0.000004         200
## 3     3             3         20     500       0.000006         300
## 4     4             4         20     500       0.000008         400
## 5     5             5         20     500       0.00001          500

To just straight-up remove the units from a tibble, use drop_units():

df |> drop_units()
## # A tibble: 5 × 6
##      id weight   vol    mw   amount  conc
##   <int>  <dbl> <dbl> <dbl>    <dbl> <dbl>
## 1     1      1    20   500 0.000002   100
## 2     2      2    20   500 0.000004   200
## 3     3      3    20   500 0.000006   300
## 4     4      4    20   500 0.000008   400
## 5     5      5    20   500 0.00001    500

Plotting

Quantities are automatically plotted with proper axis labels if used in ggplot.

# example data set
library(tidyr)
df <- 
  crossing(
    conc = set_cu(c(0.5, 25), "mM"),
    volume = set_cu(c(0.2, 90, 400), "mL")
  ) |> 
  mutate(
    amount = conc * volume,
    amount_text = format(amount)
  )
df
## # A tibble: 6 × 4
##   conc volume    amount amount_text   
##   [mM]   [mL]     [mol] <chr>         
## 1  0.5    0.2 0.0000001 1.00e-07 [mol]
## 2  0.5   90   0.000045  4.50e-05 [mol]
## 3  0.5  400   0.0002    2.00e-04 [mol]
## 4 25      0.2 0.000005  5.00e-06 [mol]
## 5 25     90   0.00225   2.25e-03 [mol]
## 6 25    400   0.01      1.00e-02 [mol]

By default, units are included in the x and y axes titles if a quantity is being plotted. Discrete quantities used for other aesthetics can be converted to a factor using [forcats::fct_in_order()] which automatically includes the units and keeps the levels in order of appearance (using [factor()] rearranges them alphabetically).

library(forcats)
library(ggplot2)
df |> 
  mutate(conc = fct_inorder(format(conc))) |> 
  ggplot() +
  aes(x = volume, y = amount, color = conc) +
  geom_line() +
  facet_wrap(~conc, scales = "free")
## Warning: The `scale_name` argument of `continuous_scale()` is deprecated as of ggplot2
## 3.5.0.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

Alternatively, the units can be specified for the axes using the [units::scale_x_units()] and [units::scale_y_units()] functions and the units parameter to the format() function. Units must be valid for the quantities that are plotted.

df |> 
  mutate(conc = factor(format(conc, units = "mM"))) |> 
  ggplot() +
  aes(x = volume, y = amount, color = conc) +
  geom_line() +
  facet_wrap(~conc, scales = "free") +
  scale_x_units(unit = "L") +
  scale_y_units(unit = "mmol")

FIXME

–> this does not work yet, have to implement scale_x_chemunits it seems :(

Lastly, the scale_x_set_cu() and scale_y_set_cu() support all axis transformations supported by scale_x_continuous() and scale_y_continuous(). For example the log10 transform can be used like this:

# df |>
#   mutate(conc = fct_inorder(format(conc))) |> 
#   ggplot() +
#   aes(x = volume, y = amount, color = conc) +
#   geom_line() +
#   facet_wrap(~conc, scales = "free") +
#   scale_x_units(transform = "log10", auto_scale = TRUE) +
#   scale_y_units(transform = "log10", auto_scale = TRUE)

Concatenation

Numbers with units can be concatenated using the regular c() function as long as they are compatible. Concatenation make sure that the metric prefix is taken into consideration and scales the new vector to the best metric of the median.

c(
  set_cu(1, "g"), 
  set_cu(1:3, "mg"),
  set_cu(2, "g")
)
## ChemUnits: [mg]
## [1] 1000    1    2    3 2000

Missing data

Missing data (NA), empty vector (numeric(0)) and infinity placeholders (Inf, -Inf) are fully supported and work the same as in any other R vectors.

set_cu(c(10, NA, -Inf, Inf, numeric(0)), "mg")
## ChemUnits: [mg]
## [1]   10   NA -Inf  Inf

Defining new units

You can add your own units thanks to [units::install_unit()] and then include it in your preferred_units and/or auto_scale_units to make full use of the chemunits functionality. For example, let’s define the SI-accepted unit Dalton as an alternative for molecular weight (sometimes also called molar mass or molecular mass) with 1 Da = 1 g/mol:

# define new unit
install_unit("Da", "g/mol")

# test conversion
set_cu(10, "g/mol") |> set_cu("Da")
## 10 [Da]
# include in preferred and auto-scale units
chemunits_options(preferred_units = c("mol", "g", "Da", "M/bar", "M", "L", "1"))
chemunits_options(auto_scale_units = c("mol", "g", "Da", "M/bar", "M", "L"))
set_cu(10000, "g") / set_cu(1, "mol")
## 10 [kDa]