Introduction to tidywater: Basic functions

library(tidywater)

Defining a water

The first step for any modelling project using tidywater is to define your starting water quality. This can be done using the define_water function. This function allows you to input everything you know about your water (pH, alkalinity, temperature, etc.). It also standardizes units for subsequent modelling steps, calculates the carbonate balance from the alkalinity and pH (if provided), and estimates the ionic strength of the water from your inputs. The function then takes all this information and creates a unique object of class “water” (for more details, see R’s S4 class).

The “water” class is the building block of tidywater and allows you to keep track of all the changes in your water through various water treatment processes in a tidy way. Think of the “water” class as a snapshot of everything that is known about a water at each step in a modelling process.

Why does tidywater depend on this special class instead of just using a data frame? The water class is a good way to standardize inputs and set up a data structure that would reduce user error. Funneling users through define_water reduces naming errors in downstream functions. It also ensures that all slots have the proper data class. For example, ph must always be a number, and treatment is always a character.

To start, let’s create a blank water class and call it empty_water.

empty_water <- define_water()
#> Warning in define_water(): Missing value for pH. Carbonate balance will not be
#> calculated.
#> Warning in define_water(): Missing value for alkalinity. Carbonate balance will
#> not be calculated.
#> Warning in define_water(): Major ions missing and neither TDS or conductivity
#> entered. Ideal conditions will be assumed. Ionic strength will be set to NA and
#> activity coefficients in future calculations will be set to 1.

Notice that several warnings are generated about the water class. define_water will return warnings noting the assumptions and limitations of the water class depending on how much information you provided. For example, since we did not provide any information about the water’s pH or alkalinity, the carbonate balance cannot be calculated. Similarly, since we did not provide any information about major ions in the water, information about the water’s hardness and ionic strength are missing.

Even so, we successfully created our first water class! You can view the first few slots that a water class can contain by printing empty_water. But there are slots for general parameters, carbon content, corrosivity indicators, major cations and anions, and disinfection byproducts (DBPs). Different functions within tidywater will allow you to model all these parameters and more.

print(empty_water)
#> pH (unitless):  NA 
#> Temperature (deg C):  25 
#> Alkalinity (mg/L CaCO3):  NA 
#> Use summary functions or slot names to view other parameters.

So let’s add some things to this water that we already know. Let’s say we know pH, alkalinity, temperature, and the sodium concentration. We can define each of these parameters as arguments in define_water, which accepts pH (unitless), alkalinity in units of mg/L CaCO3, temperature in degrees C, and ions in mg/L (with the exception of bromide and manganese, reported as ug/L). The function also refers to all chemical compounds by their chemical symbol or formula. Let’s call the new water class my_water.

Notice that the slots for ph, alk, temp,and na are now populated when we print out the resulting water. The sodium slot (na) is output in units of mol/L (M). We can also see that define_water automatically calculated the amount of carbonate (co3) and bicarbonate (hco3) in the water based on the alkalinity and pH.

my_water <- define_water(ph = 7.5, alk = 100, temp = 20, na = 5)
my_water
#> pH (unitless):  7.5 
#> Temperature (deg C):  20 
#> Alkalinity (mg/L CaCO3):  100 
#> Use summary functions or slot names to view other parameters.

You can easily summarize your water quality using the summarize_wq function, which takes a water class object and exports an easy-to-read table of the water quality into the console.

summarize_wq(my_water)
General water quality parameters Result Units
pH 7.5 -
Temp 20.0 deg C
Alkalinity 100.0 mg/L as CaCO3
Total_Hardness NA mg/L as CaCO3
TDS NA mg/L
Conductivity NA uS/cm
TOC NA mg/L

This is a great way to get started with tidywater for users who only need to model a single water quality. If you have a large data set with multiple water qualities that you want to model, though, this might sound tedious. It wouldn’t be possible to manually enter your pH, alkalinity, temperature, etc. for, let’s say, 5,000 rows. Instead, tidywater has helper functions to generate many water classes simultaneously from an imported data set. See the help_functions_blend vignette for large data processing (paste this in the console: vignette("help_functions_blend", package = "tidywater")).

Plotting and balancing ions

Back to our single use case, you’ll notice one of the warnings alerts the user to missing cations and/or anions, and that you should use balance_ions to correct this. This is based on the electroneutrality principle which states that the charges of all the cations and anions in a water should add up to 0. If your water is missing any ions, it is likely that it isn’t balanced. This could cause the results of your model to be off.

To check, you can use the plot_ions function on our water object to see what the current ion balance looks like. You can see that cation and anion bars are not equal, so this is not very realistic.

plot_ions(my_water)
#> Warning: Removed 5 rows containing missing values or values outside the scale range
#> (`geom_bar()`).
#> Warning: Removed 5 rows containing missing values or values outside the scale range
#> (`geom_text()`).
#> Warning: Removed 5 rows containing missing values or values outside the scale range
#> (`geom_text_repel()`).

To correct this, the balance_ions function automatically adds in sodium, potassium, chloride, or sulfate until anions + cations = 0. Let’s see what happens to the ion plot when we use balance_ions to estimate the missing ion concentrations.

balanced_water <- my_water %>% balance_ions()
plot_ions(balanced_water)

Great! balance_ions added potassium to my_water so it is now electrically neutral. To see how much potassium was added, you can use the “@” symbol with a water class object to access the slot you want, in this case, potassium.

my_water@k # We did not input any potassium in the original water
#> [1] NA

balanced_water@k # The balanced water now contains 0.00178 M of potassium
#> [1] 0.001780775

Converting units

The potassium units are in M. What if you want to know what that is in mg/L? There are two ways to do this in tidywater. The easiest way is to use the summarize_wq function shown above, which converts the ion concentrations back to mg/L.

Another option is to use the convert_units function, which allows you to specify the chemical and the starting and ending units. In this case, we want to convert the potassium concentration in balanced_water from M to mg/L.

convert_units(value = balanced_water@k, formula = "k", startunit = "M", endunit = "mg/L")
#> [1] 69.62529

Modelling assumptions

Ionic strength

Obviously, “adding” 69.6 mg/L of potassium to my_water is an assumption. There could be other cations in the water besides potassium that we don’t know about. But having a balanced water is important because it affects the ionic strength, which in turn affects how different species interact when we dose chemicals into the water. You can see more detail on ionic strength and chemical dosing in the chemdose_ph vignette, but for now let’s take a look at the ionic strength of my_water before and after balancing the ions.

# The ionic strength slot was NA in the original water because we did not
# provide enough information to calculate it
my_water@is
#> [1] NA
balanced_water@is # Now the ionic strength is 0.002 M
#> [1] NA

There are a couple of other assumptions that define_water makes to be able to set up the data for downstream tidywater functions. These include standard ratios for hardness and DOC.

Hardness

First, when only you only know the total hardness of the water (tot_hard), calcium hardness (ca_hard) is assumed to be 65% of the total, which is about average for U.S. drinking water. When only ca_hard is provided, tot_hard is assumed to be equal to calcium hardness/0.65. Magnesium hardness (mg_hard) is then calculated as total hardness - calcium hardness.

Let’s look at this using define_water. We can use convert_units to get the output in standard units of hardness. By defining only the total hardness, we see that the water vector also contains estimates for calcium and magnesium.

# Calculate hardness or calcium hardness
hard_water <- define_water(8, 20, 100, tot_hard = 150)

# total hardness in mg/L CaCO3
hard_water@tot_hard
#> [1] 150

# calcium hardness
convert_units(value = hard_water@ca, formula = "ca", startunit = "M", endunit = "mg/L CaCO3")
#> [1] 97.5

# magnesium hardness
convert_units(value = hard_water@mg, formula = "mg", startunit = "M", endunit = "mg/L CaCO3")
#> [1] 52.5

Total and dissolved organic carbon

TOC and DOC inputs operate similarly to hardness. TOC and DOC are important to know for modelling and predicting various water treatment processes. If you only know TOC, define_water applies a standard conversion to estimate DOC, and vice versa. If only toc is provided, doc is assumed to be 95% of the TOC. If only doc is provided, toc is assumed to be DOC/0.95. This ratio would generally be an acceptable estimate for most drinking water scenarios, but the amount of TOC that is dissolved in raw waters could vary widely. Westerhoff and Anning, 2000 reported a range of 10-100% dissolved TOC in 25 surface waters, so keep in mind that the standard conversion may not be right for your context.

# Calculate TOC and DOC
toc_water <- define_water(8, 20, 100, toc = 3)
toc_water@toc # mg/L
#> [1] 3
toc_water@doc # mg/L
#> [1] 2.85

doc_water <- define_water(8, 20, 100, doc = 1.3)
doc_water@toc # mg/L
#> [1] 1.368421
doc_water@doc # mg/L
#> [1] 1.3

Overall, tidywater helps you model scenarios with as little information as possible because we often don’t have all the data we would like, but the less you have to assume about your water, the better. It’s important to understand and list the assumptions you are making during any modelling effort.

Check out our other vignettes to learn more about tidywater’s functions for modelling softening, coagulation, DBP formation…. here ____.