Added function to convert *from* SHE.

Minor updates to RefCanonicalName(), including printing all options in a formatted table if no refname is specified.
master
Taha Ahmed 7 years ago
parent 3da30f2920
commit cae4c711b3

@ -1,7 +1,7 @@
Package: common
Type: Package
Title: chepec common
Version: 0.0.0.9009
Version: 0.0.0.9010
Description: Commonly used functions and scripts.
Authors@R: person("Taha", "Ahmed", email = "taha@chepec.se", role = c("aut", "cre"))
License: GPL-3
@ -9,4 +9,5 @@ LazyData: TRUE
RoxygenNote: 6.0.1
Imports:
xtable,
utils
utils,
knitr

@ -19,6 +19,7 @@ export(VapourPressureWater)
export(as.SHE)
export(as.degrees)
export(as.radians)
export(from.SHE)
export(int2padstr)
export(is.wholenumber)
export(molarity2mass)

@ -64,6 +64,7 @@ RefCanonicalName <- function(refname) {
"Hg2Cl2",
"Calomel-Mercury",
"Mercury-Calomel",
"Calomel",
"SCE")
electrode.system[["AVS"]] <-
c("AVS",
@ -88,6 +89,39 @@ RefCanonicalName <- function(refname) {
"Mg/Mg2+",
"Magnesium")
# if no argument or empty string supplied as arg, return the the entire list as df
# to give the user a nice overview of all available options
if (missing(refname) || refname == "") {
max.row.length <- 0
for (i in 1:length(electrode.system)) {
# find the longest row and save its length
this.row.length <- length(electrode.system[[i]])
if (this.row.length > max.row.length) max.row.length <- this.row.length
}
# initialise an empty df with dimensions that fit electrode.system
overview.names <-
data.frame(
structure(dimnames =
list(
# rownames
seq(1, length(electrode.system)),
# colnames
c("canonical", paste0("option", seq(1, max.row.length - 1)))),
matrix("",
nrow = length(electrode.system),
ncol = max.row.length,
byrow = TRUE)),
stringsAsFactors = FALSE)
# now populate the df
for (i in 1:length(electrode.system)) {
this.row.length <- length(electrode.system[[i]])
overview.names[i,1:this.row.length] <- electrode.system[[i]]
}
message(paste0("You did not specify any reference electrode name.\n",
"Here are the options supported by this function (case-insensitive):"))
print(knitr::kable(overview.names))
}
# defining refname in this manner makes sure to get all possible combinations
# but there might be a number of duplicates, but those we can
# get rid of in the next step
@ -239,7 +273,7 @@ potentials.as.SHE <- function() {
#' Convert from electrochemical or electronic scale to SHE
#' Convert from electrochemical or physical scale to SHE
#'
#' Convert an arbitrary number of potentials against any known electrochemical
#' scale (or the electronic vacuum scale) to potential vs SHE.
@ -301,7 +335,7 @@ as.SHE <- function(potential,
# this way, we can correlate columns to each other by row
df <-
data.frame(potential = potential,
scale = RefCanonicalName(scale),
scale = common::RefCanonicalName(scale),
electrolyte = electrolyte,
concentration = concentration,
temperature = temperature,
@ -313,20 +347,20 @@ as.SHE <- function(potential,
# SHE scale special considerations
# 1. concentration is constant
if (any(df$scale == RefCanonicalName("SHE"))) {
df$concentration[which(df$scale == RefCanonicalName("SHE"))] <- ""
df$electrolyte[which(df$scale == RefCanonicalName("SHE"))] <- ""
if (any(df$scale == common::RefCanonicalName("SHE"))) {
df$concentration[which(df$scale == common::RefCanonicalName("SHE"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("SHE"))] <- ""
}
# AVS scale special considerations
# 1. concentration is meaningless
# 2. direction is opposite of electrochemical scales, requiring change of sign
if (any(df$scale == RefCanonicalName("AVS"))) {
if (any(df$scale == common::RefCanonicalName("AVS"))) {
# concentration is meaningless for AVS (no electrolyte)
# so for those rows, we'll reset it
df$concentration[which(df$scale == RefCanonicalName("AVS"))] <- ""
df$electrolyte[which(df$scale == RefCanonicalName("AVS"))] <- ""
df$vacuum[which(df$scale == RefCanonicalName("AVS"))] <- TRUE
df$concentration[which(df$scale == common::RefCanonicalName("AVS"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("AVS"))] <- ""
df$vacuum[which(df$scale == common::RefCanonicalName("AVS"))] <- TRUE
}
# now just work our way through df, line-by-line to determine potential as SHE
@ -444,6 +478,208 @@ as.SHE <- function(potential,
#' Convert from SHE scale to another electrochemical or physical scale
#'
#' Convert an arbitrary number of potentials vs SHE to another electrochemical
#' scale (or the vacuum scale).
#' The available target scales are those listed by \code{\link{potentials.as.SHE}}.
#'
#' @param potential potential in volt
#' @param scale name of the target scale
#' @param electrolyte optional, specify electrolyte solution, e.g., "KCl(aq)". Must match one of the values in \code{\link{potentials.as.SHE}$electrolyte}
#' @param concentration of electrolyte in mol/L, or as the string "saturated"
#' @param temperature of system in degrees Celsius
#' @param as.SHE.data by default this parameter reads the full dataset \code{\link{potentials.as.SHE}}
#'
#' @return potential in the specified target scale
#' @export
from.SHE <- function(potential,
scale,
electrolyte = "",
concentration = "saturated",
temperature = 25,
as.SHE.data = potentials.as.SHE()) {
# make this work for arbitrary-length vectors of potential and scale
# make sure potential and scale args have the same length
if (length(potential) == 0 | length(scale) == 0) {
stop("Arguments potential or scale cannot be empty!")
} else if (length(potential) != length(scale)) {
stop("Arguments potential and scale must have the same number of elements")
}
arglength <- length(potential)
# make the args concentration, temperature and electrolyte this same length,
# unless the user supplied them (only necessary for > 1)
if (arglength > 1) {
# handle two cases:
# 1. user did not touch concentration, temperature and electrolyte args.
# Assume they forgot and reset their length and print a message
# 2. user did change concentration or temperature or electrolyte, but still failed to
# ensure length equal to arglength. In this case, abort.
# note: we can get the default value set in the function call using formals()
if (identical(concentration, formals(from.SHE)$concentration) &
identical(temperature, formals(from.SHE)$temperature) &
identical(electrolyte, formals(from.SHE)$electrolyte)) {
# case 1
message(paste0("Default concentration (", formals(from.SHE)$concentration, "), temperature (", formals(from.SHE)$temperature, "C) used for all supplied potential and scale values."))
concentration <- rep(concentration, arglength)
temperature <- rep(temperature, arglength)
electrolyte <- rep(electrolyte, arglength)
} else {
# case 2
stop("Concentration, temperature and electrolyte arguments must have the same number of elements as potential and scale!")
}
}
## we can now safely assume that length(<args>) == arglength
# place args into a single dataframe
# this way, we can correlate columns to each other by row
df <-
data.frame(potential = potential, # vs SHE
scale = common::RefCanonicalName(scale), # target scale
electrolyte = electrolyte,
concentration = concentration,
temperature = temperature,
stringsAsFactors = FALSE)
# # add column to keep track of vacuum scale
# df$vacuum <- as.logical(FALSE)
# # add column to hold calc potential vs target scale
# df$targetscale <- as.numeric(NA)
## Special considerations
# SHE scale independent of concentration, per definition
if (any(df$scale == common::RefCanonicalName("SHE"))) {
df$concentration[which(df$scale == common::RefCanonicalName("SHE"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("SHE"))] <- ""
}
# AVS scale: concentration is meaningless (no electrolyte)
if (any(df$scale == common::RefCanonicalName("AVS"))) {
df$concentration[which(df$scale == common::RefCanonicalName("AVS"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("AVS"))] <- ""
}
for (p in 1:dim(df)[1]) {
# First, subset against electrode scale. If as.SHE.data only contains one row
# for this electrode scale we are DONE. If not, proceed to subset against concentration
subset.scale <- subset(as.SHE.data, electrode == df$scale[p])
if (dim(subset.scale)[1] > 1) {
# continue matching, now against conc.string or conc.num
if (is.character(df$concentration[p])) {
subset.concentration <-
subset(subset.scale, conc.string == df$concentration[p])
} else {
subset.concentration <-
subset(subset.scale, conc.num == df$concentration[p])
}
# stop if the resulting dataframe after matching contains no rows
if (dim(subset.concentration)[1] == 0) {
stop("Sorry, it seems we failed to find any matching entries in potentials.as.SHE().")
}
# Note: it's ok at this point if the resulting df contains more than one row as
# more matching will be done below
# If we haven't had reason to stop(), we should be good
# just housekeeping: rename the variable so we don't have to edit code below
subset.SHE.data <- subset.concentration
} else {
# just housekeeping again
subset.SHE.data <- subset.scale
}
# use KCl(aq) as default to avoid aborting
# (good assumption at this point, as we always have KCl for the cases
# where an electrode system has more than one electrolyte)
default.electrolyte <- "KCl(aq)"
# If this subset contains more than one unique electrolyte (e.g., NaCl and KCl)
# the user MUST have made a choice (in the "electrolyte" argument) that results
# in a single electrolyte remaining, or else we will warn and abort
if (length(unique(subset.SHE.data$electrolyte)) > 1) {
# data (in subset.SHE.data) contains more than one electrolyte
# if user did not change electrolyte arg value, use default and issue warning
if (identical(electrolyte, formals(as.SHE)$electrolyte)) {
warning(paste0("You did not specify an electrolyte, but more than one ",
"is available for E = ", df$potential[p], " V vs ", df$scale[p], ".\n",
"We'll use the default electrolyte: ", default.electrolyte))
subset.SHE.data <-
subset(subset.SHE.data, electrolyte == default.electrolyte)
} else {
# else the user did change the electrolyte arg, use the user's value
subset.SHE.data <-
subset.SHE.data[which(subset.SHE.data$electrolyte == electrolyte), ]
# print only for debugging - disable before production!
print(subset.SHE.data)
# stop if the resulting dataframe contains no rows
if (dim(subset.SHE.data)[1] == 0) {
stop("Your choice of electrolyte does not match any data!")
}
}
} else {
# data only contains one electrolyte
# just check that it matches whatever the user supplied, if not,
# issue a warning (but don't abort, typically the user did not set it
# because they don't care and want whatever is in the data)
if (unique(subset.SHE.data$electrolyte) != electrolyte) {
warning(paste0("The requested electrolyte: ",
ifelse(electrolyte == "", "<none specified>", electrolyte),
" was not found for E = ", df$potential[p], " V vs ", df$scale[p], ".\n",
"My data only lists one electrolyte for that scale - return value calculated on that basis."))
subset.SHE.data <-
subset(subset.SHE.data, electrolyte == unique(subset.SHE.data$electrolyte))
} else {
subset.SHE.data <-
subset(subset.SHE.data, electrolyte == electrolyte)
}
}
# temperature
# either happens to match a temperature in the dataset, or we interpolate
# (under the assumption that potential varies linearly with temperature)
if (!any(subset.SHE.data$temp == df$temperature[p])) {
# sought temperature was not available in dataset, check that it falls inside
# note: important to use less/more-than-or-equal in case data only contains one value
if ((df$temperature[p] <= max(subset.SHE.data$temp)) &&
(df$temperature[p] >= min(subset.SHE.data$temp))) {
# within dataset range, do linear interpolation
lm.subset <- stats::lm(SHE ~ temp, data = subset.SHE.data)
# interpolated temperature, calculated based on linear regression
# (more accurate than simple linear interpolation with approx())
pot.interp <-
lm.subset$coefficients[2] * df$temperature[p] + lm.subset$coefficients[1]
message("Calc potential using interp temperature")
### CALC POTENTIAL vs requested scale
if (df$scale[p] == common::RefCanonicalName("AVS")) {
# message("Target scale is AVS")
df$potentialvsscale[p] <-
pot.interp - df$potential[p]
} else {
# message("Target scale is not AVS")
df$potentialvsscale[p] <-
df$potential[p] - pot.interp
}
}
} else {
# requested temperature does exist in dataset
### CALC POTENTIAL vs requested scale
message("Calc potential using exact temperature match")
if (df$scale[p] == common::RefCanonicalName("AVS")) {
# message("Target scale is AVS")
df$potentialvsscale[p] <-
subset(subset.SHE.data, temp == df$temperature[p])$SHE - df$potential[p]
} else {
# message("Target scale is not AVS")
df$potentialvsscale[p] <-
df$potential[p] - subset(subset.SHE.data, temp == df$temperature[p])$SHE
}
}
}
return(df$potentialvsscale)
}
#' ConvertRefPotEC
#'
#' This function does the heavy lifting.

@ -2,7 +2,7 @@
% Please edit documentation in R/unit-converters-electrochemical.R
\name{as.SHE}
\alias{as.SHE}
\title{Convert from electrochemical or electronic scale to SHE}
\title{Convert from electrochemical or physical scale to SHE}
\usage{
as.SHE(potential, scale, electrolyte = "", concentration = "saturated",
temperature = 25, as.SHE.data = potentials.as.SHE())

@ -0,0 +1,30 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/unit-converters-electrochemical.R
\name{from.SHE}
\alias{from.SHE}
\title{Convert from SHE scale to another electrochemical or physical scale}
\usage{
from.SHE(potential, scale, electrolyte = "", concentration = "saturated",
temperature = 25, as.SHE.data = potentials.as.SHE())
}
\arguments{
\item{potential}{potential in volt}
\item{scale}{name of the target scale}
\item{electrolyte}{optional, specify electrolyte solution, e.g., "KCl(aq)". Must match one of the values in \code{\link{potentials.as.SHE}$electrolyte}}
\item{concentration}{of electrolyte in mol/L, or as the string "saturated"}
\item{temperature}{of system in degrees Celsius}
\item{as.SHE.data}{by default this parameter reads the full dataset \code{\link{potentials.as.SHE}}}
}
\value{
potential in the specified target scale
}
\description{
Convert an arbitrary number of potentials vs SHE to another electrochemical
scale (or the vacuum scale).
The available target scales are those listed by \code{\link{potentials.as.SHE}}.
}
Loading…
Cancel
Save