Reworked both from.SHE and as.SHE to resolve

the aforementioned bug and also some inconsistent behaviour when submitting a vector of potentials for certain electrode scales.
Both issues should be fixed now.
master
Taha Ahmed 7 years ago
parent 878dcdc701
commit bee42f9fd9

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

@ -89,7 +89,7 @@ RefCanonicalName <- function(refname) {
"Mg/Mg2+", "Mg/Mg2+",
"Magnesium") "Magnesium")
# if no argument or empty string supplied as arg, return the the entire list as df # if no argument or empty string supplied as arg, return the entire list as df
# to give the user a nice overview of all available options # to give the user a nice overview of all available options
if (missing(refname) || refname == "") { if (missing(refname) || refname == "") {
max.row.length <- 0 max.row.length <- 0
@ -185,7 +185,8 @@ potentials.as.SHE <- function() {
# all potentials vs SHE # all potentials vs SHE
potentials <- potentials <-
as.data.frame(matrix(data = as.data.frame(
matrix(data =
# electrode # electrolyte # conc/M # conc label # temp # pot vs SHE # set id # ref # electrode # electrolyte # conc/M # conc label # temp # pot vs SHE # set id # ref
c("AgCl/Ag", "NaCl(aq)", "5.9", "saturated", "25", "0.2630", "9", "CRC 97th ed., 97-05-22", c("AgCl/Ag", "NaCl(aq)", "5.9", "saturated", "25", "0.2630", "9", "CRC 97th ed., 97-05-22",
"AgCl/Ag", "KCl(aq)", "3.5", "3.5M at 25C", "10", "0.215", "1", "Sawyer1995", "AgCl/Ag", "KCl(aq)", "3.5", "3.5M at 25C", "10", "0.215", "1", "Sawyer1995",
@ -297,24 +298,23 @@ as.SHE <- function(potential,
# if the supplied temperature does not exist in the data, this function will attempt to interpolate # if the supplied temperature does not exist in the data, this function will attempt to interpolate
# note that concentration has to match, no interpolation is attempted for conc # note that concentration has to match, no interpolation is attempted for conc
# make this work for arbitrary-length vectors of potential and scale # potential and scale vectors supplied by user could have arbitrary length
# make sure potential and scale args have the same length # just make sure potential and scale args have the same length (or length(scale) == 1)
if (length(potential) == 0 | length(scale) == 0) { if (length(potential) == 0 | length(scale) == 0) {
stop("Arguments potential or scale cannot be empty!") stop("Potential or scale arguments cannot be empty!")
} else if (length(potential) != length(scale)) { } else if (length(potential) != length(scale)) {
# stop, unless length(scale) == 1 where we will assume it should be recycled # stop, unless length(scale) == 1 where we will assume it should be recycled
if (length(scale) == 1) { if (length(scale) == 1) {
message("Argument <scale> has unit length. Recycling it to match length of <potential>. ") message("Arg <scale> has unit length. We'll recycle it to match length of <potential>.")
scale <- rep(scale, length(potential)) scale <- rep(scale, length(potential))
} else { } else {
stop(paste0("Correspondence between the supplied potentials and scales could not be worked out.\n", stop("Length of <potential> and <scale> must be equal OR <scale> may be unit length.")
"Please make sure the number of elements in each match, or make <scale> unit length."))
} }
} }
arglength <- length(potential) arglength <- length(potential)
# make the args concentration, temperature and electrolyte this same length, # make the args concentration, temperature and electrolyte this same length,
# unless the user supplied them (only necessary for > 1) # unless the user supplied them (only necessary for length > 1)
if (arglength > 1) { if (arglength > 1) {
# handle two cases: # handle two cases:
# 1. user did not touch concentration, temperature and electrolyte args. # 1. user did not touch concentration, temperature and electrolyte args.
@ -327,7 +327,7 @@ as.SHE <- function(potential,
identical(electrolyte, formals(as.SHE)$electrolyte)) { identical(electrolyte, formals(as.SHE)$electrolyte)) {
# case 1 # case 1
# message("NOTE: default concentration and temperature values used for all potentials and scales.") # message("NOTE: default concentration and temperature values used for all potentials and scales.")
message(paste0("Default concentration (", formals(as.SHE)$concentration, "), temperature (", formals(as.SHE)$temperature, "C) used for all supplied potential and scale values.")) message(paste0("The default concentration (", formals(as.SHE)$concentration, ") and temperature (", formals(as.SHE)$temperature, "C) will be assumed for all your potential/scale values."))
concentration <- rep(concentration, arglength) concentration <- rep(concentration, arglength)
temperature <- rep(temperature, arglength) temperature <- rep(temperature, arglength)
electrolyte <- rep(electrolyte, arglength) electrolyte <- rep(electrolyte, arglength)
@ -340,7 +340,7 @@ as.SHE <- function(potential,
## we can now safely assume that length(<args>) == arglength ## we can now safely assume that length(<args>) == arglength
# place args into a single dataframe # place args into a single dataframe
# this way, we can correlate columns to each other by row # this way, we can correlate columns to each other by row
df <- dfargs <-
data.frame(potential = potential, data.frame(potential = potential,
scale = common::RefCanonicalName(scale), scale = common::RefCanonicalName(scale),
electrolyte = electrolyte, electrolyte = electrolyte,
@ -348,139 +348,155 @@ as.SHE <- function(potential,
temperature = temperature, temperature = temperature,
stringsAsFactors = FALSE) stringsAsFactors = FALSE)
# add column to keep track of vacuum scale # add column to keep track of vacuum scale
df$vacuum <- as.logical(FALSE) # dfargs$vacuum <- as.logical(FALSE)
# add column to hold calc potential vs SHE # add column to hold calc potential vs SHE
df$SHE <- as.numeric(NA) dfargs$SHE <- as.numeric(NA)
## From here on, ONLY access the arguments via this dataframe
## That is, use dfargs$electrolyte, NOT electrolyte
# SHE scale special considerations # SHE scale special considerations
# 1. concentration is constant # 1. concentration is constant for SHE
if (any(df$scale == common::RefCanonicalName("SHE"))) { if (any(dfargs$scale == common::RefCanonicalName("SHE"))) {
df$concentration[which(df$scale == common::RefCanonicalName("SHE"))] <- "" dfargs$concentration[which(dfargs$scale == common::RefCanonicalName("SHE"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("SHE"))] <- "" dfargs$electrolyte[which(dfargs$scale == common::RefCanonicalName("SHE"))] <- ""
} }
# AVS scale special considerations # AVS scale special considerations
# 1. concentration is meaningless # 1. concentration is meaningless for AVS
# 2. direction is opposite of electrochemical scales, requiring change of sign if (any(dfargs$scale == common::RefCanonicalName("AVS"))) {
if (any(df$scale == common::RefCanonicalName("AVS"))) { # concentration is meaningless for AVS (no electrolyte) so for those rows, we'll reset it
# concentration is meaningless for AVS (no electrolyte) dfargs$concentration[which(dfargs$scale == common::RefCanonicalName("AVS"))] <- ""
# so for those rows, we'll reset it dfargs$electrolyte[which(dfargs$scale == common::RefCanonicalName("AVS"))] <- ""
df$concentration[which(df$scale == common::RefCanonicalName("AVS"))] <- "" # dfargs$vacuum[which(dfargs$scale == common::RefCanonicalName("AVS"))] <- TRUE
df$electrolyte[which(df$scale == common::RefCanonicalName("AVS"))] <- "" }
df$vacuum[which(df$scale == common::RefCanonicalName("AVS"))] <- TRUE
} # now just work our way through dfargs, line-by-line to determine potential as SHE
# all necessary conditions should be recorded right here in dfargs
# now just work our way through df, line-by-line to determine potential as SHE for (p in 1:dim(dfargs)[1]) {
# all necessary conditions should be recorded right here in df ## WE ARE NOW WORKING ROW-BY-ROW THROUGH THE SUPPLIED ARGUMENTS IN dfargs
for (p in 1:dim(df)[1]) { # Step-wise matching:
# Fixed a bug 2018-03-04 # + first, we subset against electrode scale. If dataset only has one row, done. Else,
# Issue: if scale was {Li,Na,Mg} the default electrolyte string "saturated" caused # + we subset against either conc.string or conc.num. Stop if zero rows in dataset (error), otherwise proceed.
# zero rows to be returned in the subset.SHE.data match, with error returned to user. # Our "dataset" is the literature data supplied via the argument as.SHE.data
# Fixed by making the matching more step-wise: this.data.scale <- subset(as.SHE.data, electrode == dfargs$scale[p])
# + first, subset against electrode scale. If only one row, done. If more, # subset.scale <- subset(as.SHE.data, electrode == dfargs$scale[p])
# + subset against either conc.string or conc.num. Stop if zero rows (error), otherwise proceed. if (dim(this.data.scale)[1] > 1) {
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 # continue matching, now against conc.string or conc.num
if (is.character(df$concentration[p])) { if (is.character(dfargs$concentration[p])) {
subset.concentration <- this.data.concentration <-
subset(subset.scale, conc.string == df$concentration[p]) # subset.concentration <-
subset(this.data.scale, conc.string == dfargs$concentration[p])
} else { } else {
subset.concentration <- this.data.concentration <-
subset(subset.scale, conc.num == df$concentration[p]) # subset.concentration <-
subset(this.data.scale, conc.num == dfargs$concentration[p])
} }
# stop if the resulting dataframe after matching contains no rows # stop if the resulting dataframe after matching contains no rows
if (dim(subset.concentration)[1] == 0) { if (dim(this.data.concentration)[1] == 0) {
stop("Sorry, it seems we failed to find any matching entries in potentials.as.SHE().") stop(paste0("Failed to find any matching entries in dataset for ",
paste(dfargs[p, ], collapse = " ", sep = "")))
} }
# Note: it's ok at this point if the resulting df contains more than one row as # Note: it's ok at this point if the resulting dataset contains more than one row as
# more matching will be done below # more matching will be done below
# If we haven't had reason to stop(), we should be good # 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 # just housekeeping: rename the variable so we don't have to edit code below
subset.SHE.data <- subset.concentration this.SHE.data <- this.data.concentration
# subset.SHE.data <- subset.concentration
} else { } else {
# just housekeeping: rename the variable so we don't have to change the code that follows # just housekeeping again
subset.SHE.data <- subset.scale this.SHE.data <- this.data.scale
} # 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) ## Electrolyte
default.electrolyte <- "KCl(aq)" # == We would like to transparently handle the following scenario:
# If this subset contains more than one unique electrolyte (e.g., NaCl and KCl) # || if the user did not specify electrolyte solution (which we can check by using formals())
# the user MUST have made a choice (in the "electrolyte" argument) that results # || but the dataset (after subsetting against scale and concentration above) still contains
# in a single electrolyte remaining, or else we will warn and abort # || more than one electrolyte
if (length(unique(subset.SHE.data$electrolyte)) > 1) { # >> Approach: we'll specify a "fallback" electrolyte, KCl (usually that's what the user wants)
# data (in subset.SHE.data) contains more than one electrolyte # >> and inform/warn about it
# if user did not change electrolyte arg value, use default and issue warning
if (identical(electrolyte, formals(as.SHE)$electrolyte)) { # KCl is a good assumption, as we always have KCl
warning(paste0("You did not specify an electrolyte, but more than one ", # for the cases where an electrode system has more than one electrolyte
"is available for E = ", df$potential[p], " V vs ", df$scale[p], ".\n", fallback.electrolyte <- "KCl(aq)"
"Using electrolyte: ", default.electrolyte)) if (length(unique(this.SHE.data$electrolyte)) > 1) {
subset.SHE.data <- if (formals(as.SHE)$electrolyte == "") {
subset(subset.SHE.data, electrolyte == default.electrolyte) warning(paste0("More than one electrolyte ",
"available for E(", dfargs$scale[p], ") in dataset. ",
"I'll assume you want ", fallback.electrolyte, "."))
this.SHE.data <-
subset(this.SHE.data, electrolyte == fallback.electrolyte)
} else { } else {
# else the user did change the electrolyte arg, use the user's value # else the user did change the electrolyte arg, use the user's value
subset.SHE.data <- this.SHE.data <-
subset.SHE.data[which(subset.SHE.data$electrolyte == electrolyte), ] subset(this.SHE.data, electrolyte == dfargs$electrolyte[p])
# print only for debugging - disable before production! # but stop if the resulting dataframe contains no rows
print(subset.SHE.data) if (dim(this.SHE.data)[1] == 0) stop("Your choice of electrolyte does not match any 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 { } else {
# data only contains one electrolyte # dataset contains only one unique electrolyte
# just check that it matches whatever the user supplied, if not, # again, check if electrolyte in arg matches the one in dataset
# issue a warning (but don't abort, typically the user did not set it # if it does, great, if it does not, print a message and use it anyway
# because they don't care and want whatever is in the data) if (unique(this.SHE.data$electrolyte) == dfargs$electrolyte[p]) {
if (any(subset.SHE.data$electrolyte != electrolyte)) { this.SHE.data <-
warning(paste0("The requested electrolyte: ", subset(this.SHE.data, electrolyte == dfargs$electrolyte[p])
ifelse(any(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 { } else {
subset.SHE.data <- # whatever electrolyte the user supplied does not match what's left in the datasubset
subset(subset.SHE.data, electrolyte == electrolyte) # but at this point the user is probably better served by returning the electrolyte we have
# along with an informative message (that's the only reason for the if-else below)
electrolytes.in.subset <-
unique(subset(as.SHE.data, electrode == dfargs$scale[p])$electrolyte)
if (dfargs$electrolyte[p] == "") {
message(
paste0('Electrolyte "" (empty string) not in dataset for E(',
dfargs$scale[p], '). ',
'These electrolytes are: ',
paste(electrolytes.in.subset, collapse = ', or '), '.',
"I'll assume you want ", fallback.electrolyte, ".")
)
} else {
message(paste0("Electrolyte ", dfargs$electrolyte[p], " not in dataset for E(",
dfargs$scale[p], "). ",
"These electrolytes are: ",
paste(electrolytes.in.subset, collapse = ", or "), ".",
"I'll assume you want ", fallback.electrolyte, ".")
)
}
} }
} }
# temperature # temperature
# either happens to match a temperature in the dataset, or we interpolate # either happens to match a temperature in the dataset, or we interpolate
# (under the assumption that potential varies linearly with temperature) # (under the assumption that potential varies linearly with temperature)
if (!any(subset.SHE.data$temp == df$temperature[p])) { if (!any(this.SHE.data$temp == dfargs$temperature[p])) {
# sought temperature was not available in dataset, check that it falls inside # 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 # 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)) && if ((dfargs$temperature[p] <= max(this.SHE.data$temp)) && (dfargs$temperature[p] >= min(this.SHE.data$temp))) {
(df$temperature[p] >= min(subset.SHE.data$temp))) {
# within dataset range, do linear interpolation # within dataset range, do linear interpolation
lm.subset <- stats::lm(SHE ~ temp, data = subset.SHE.data) lm.subset <- stats::lm(SHE ~ temp, data = this.SHE.data)
# interpolated temperature, calculated based on linear regression # interpolated temperature, calculated based on linear regression
# (more accurate than simple linear interpolation with approx()) # (more accurate than simple linear interpolation with approx())
pot.interp <- pot.interp <-
lm.subset$coefficients[2] * df$temperature[p] + lm.subset$coefficients[1] lm.subset$coefficients[2] * dfargs$temperature[p] + lm.subset$coefficients[1]
### CALC POTENTIAL vs SHE ### CALC POTENTIAL vs SHE
df$SHE[p] <- dfargs$SHE[p] <-
ifelse(df$vacuum[p], ifelse(dfargs$scale[p] == "AVS",
pot.interp - df$potential[p], pot.interp - dfargs$potential[p],
pot.interp + df$potential[p]) pot.interp + dfargs$potential[p])
} }
} else { } else {
# requested temperature does exist in dataset # requested temperature does exist in dataset
### CALC POTENTIAL vs SHE ### CALC POTENTIAL vs SHE
df$SHE[p] <- dfargs$SHE[p] <-
ifelse(df$vacuum[p], ifelse(dfargs$scale[p] == "AVS",
subset(subset.SHE.data, temp == df$temperature[p])$SHE - df$potential[p], subset(this.SHE.data, temp == dfargs$temperature[p])$SHE - dfargs$potential[p],
subset(subset.SHE.data, temp == df$temperature[p])$SHE + df$potential[p]) subset(this.SHE.data, temp == dfargs$temperature[p])$SHE + dfargs$potential[p])
} }
} }
return(df$SHE) return(dfargs$SHE)
} }
@ -509,27 +525,29 @@ from.SHE <- function(potential,
temperature = 25, temperature = 25,
as.SHE.data = potentials.as.SHE()) { as.SHE.data = potentials.as.SHE()) {
# make this work for arbitrary-length vectors of potential and scale # if the supplied temperature does not exist in the data, this function will attempt to interpolate
# make sure potential and scale args have the same length # note that concentration has to match, no interpolation is attempted for conc
# potential and scale vectors supplied by user could have arbitrary length
# just make sure potential and scale args have the same length (or length(scale) == 1)
if (length(potential) == 0 | length(scale) == 0) { if (length(potential) == 0 | length(scale) == 0) {
stop("Arguments potential or scale cannot be empty!") stop("Potential or scale arguments cannot be empty!")
} else if (length(potential) != length(scale)) { } else if (length(potential) != length(scale)) {
# stop, unless length(scale) == 1 where we will assume it should be recycled # stop, unless length(scale) == 1 where we will assume it should be recycled
if (length(scale) == 1) { if (length(scale) == 1) {
message("Argument <scale> has unit length. Recycling it to match length of <potential>. ") message("Arg <scale> has unit length. We'll recycle it to match length of <potential>.")
scale <- rep(scale, length(potential)) scale <- rep(scale, length(potential))
} else { } else {
stop(paste0("Correspondence between the supplied potentials and scales could not be worked out.\n", stop("Length of <potential> and <scale> must be equal OR <scale> may be unit length.")
"Please make sure the number of elements in each match, or make <scale> unit length."))
} }
} }
arglength <- length(potential) arglength <- length(potential)
# make the args concentration, temperature and electrolyte this same length, # make the args concentration, temperature and electrolyte this same length,
# unless the user supplied them (only necessary for > 1) # unless the user supplied them (only necessary for length > 1)
if (arglength > 1) { if (arglength > 1) {
# handle two cases: # handle two cases:
# 1. user did not touch concentration, temperature or electrolyte args. # 1. user did not touch concentration, temperature and electrolyte args.
# Assume they forgot and reset their length and print a message # Assume they forgot and reset their length and print a message
# 2. user did change concentration or temperature or electrolyte, but still failed to # 2. user did change concentration or temperature or electrolyte, but still failed to
# ensure length equal to arglength. In this case, abort. # ensure length equal to arglength. In this case, abort.
@ -538,7 +556,7 @@ from.SHE <- function(potential,
identical(temperature, formals(from.SHE)$temperature) & identical(temperature, formals(from.SHE)$temperature) &
identical(electrolyte, formals(from.SHE)$electrolyte)) { identical(electrolyte, formals(from.SHE)$electrolyte)) {
# case 1 # case 1
message(paste0("Default concentration (", formals(from.SHE)$concentration, "), temperature (", formals(from.SHE)$temperature, "C) used for all supplied potential and scale values.")) message(paste0("The default concentration (", formals(from.SHE)$concentration, ") and temperature (", formals(from.SHE)$temperature, "C) will be assumed for all your potential/scale values."))
concentration <- rep(concentration, arglength) concentration <- rep(concentration, arglength)
temperature <- rep(temperature, arglength) temperature <- rep(temperature, arglength)
electrolyte <- rep(electrolyte, arglength) electrolyte <- rep(electrolyte, arglength)
@ -551,147 +569,154 @@ from.SHE <- function(potential,
## we can now safely assume that length(<args>) == arglength ## we can now safely assume that length(<args>) == arglength
# place args into a single dataframe # place args into a single dataframe
# this way, we can correlate columns to each other by row # this way, we can correlate columns to each other by row
df <- dfargs <-
data.frame(potential = potential, # vs SHE data.frame(potential = potential, # vs SHE
scale = common::RefCanonicalName(scale), # target scale scale = common::RefCanonicalName(scale), # target scale
electrolyte = electrolyte, electrolyte = electrolyte,
concentration = concentration, concentration = concentration,
temperature = temperature, temperature = temperature,
stringsAsFactors = FALSE) stringsAsFactors = FALSE)
# # add column to keep track of vacuum scale
# df$vacuum <- as.logical(FALSE) ## From here on, ONLY access the arguments via this dataframe
# # add column to hold calc potential vs target scale ## That is, use dfargs$electrolyte, NOT electrolyte (and so on)
# df$targetscale <- as.numeric(NA)
# SHE scale special considerations
## Special considerations # 1. concentration is constant for SHE
# SHE scale independent of concentration, per definition if (any(dfargs$scale == common::RefCanonicalName("SHE"))) {
if (any(df$scale == common::RefCanonicalName("SHE"))) { dfargs$concentration[which(dfargs$scale == common::RefCanonicalName("SHE"))] <- ""
df$concentration[which(df$scale == common::RefCanonicalName("SHE"))] <- "" dfargs$electrolyte[which(dfargs$scale == common::RefCanonicalName("SHE"))] <- ""
df$electrolyte[which(df$scale == common::RefCanonicalName("SHE"))] <- "" }
}
# AVS scale: concentration is meaningless (no electrolyte) # AVS scale special considerations
if (any(df$scale == common::RefCanonicalName("AVS"))) { # 1. concentration is meaningless for AVS
df$concentration[which(df$scale == common::RefCanonicalName("AVS"))] <- "" if (any(dfargs$scale == common::RefCanonicalName("AVS"))) {
df$electrolyte[which(df$scale == common::RefCanonicalName("AVS"))] <- "" # concentration is meaningless for AVS (no electrolyte) so for those rows, we'll reset it
} dfargs$concentration[which(dfargs$scale == common::RefCanonicalName("AVS"))] <- ""
dfargs$electrolyte[which(dfargs$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 # now just work our way through dfargs, line-by-line to determine potential as SHE
subset.scale <- subset(as.SHE.data, electrode == df$scale[p]) # all necessary conditions should be recorded right here in dfargs
if (dim(subset.scale)[1] > 1) { for (p in 1:dim(dfargs)[1]) {
## WE ARE NOW WORKING ROW-BY-ROW THROUGH THE SUPPLIED ARGUMENTS IN dfargs
# Step-wise matching:
# + first, we subset against electrode scale. If dataset only has one row, done. Else,
# + we subset against either conc.string or conc.num. Stop if zero rows in dataset (error), otherwise proceed.
# Our "dataset" is the literature data supplied via the argument as.SHE.data
this.data.scale <- subset(as.SHE.data, electrode == dfargs$scale[p])
# subset.scale <- subset(as.SHE.data, electrode == dfargs$scale[p])
if (dim(this.data.scale)[1] > 1) {
# continue matching, now against conc.string or conc.num # continue matching, now against conc.string or conc.num
if (is.character(df$concentration[p])) { if (is.character(dfargs$concentration[p])) {
subset.concentration <- this.data.concentration <-
subset(subset.scale, conc.string == df$concentration[p]) subset(this.data.scale, conc.string == dfargs$concentration[p])
} else { } else {
subset.concentration <- this.data.concentration <-
subset(subset.scale, conc.num == df$concentration[p]) subset(this.data.scale, conc.num == dfargs$concentration[p])
} }
# stop if the resulting dataframe after matching contains no rows # stop if the resulting dataframe after matching contains no rows
if (dim(subset.concentration)[1] == 0) { if (dim(this.data.concentration)[1] == 0) {
stop("Sorry, it seems we failed to find any matching entries in potentials.as.SHE().") stop(paste0("Failed to find any matching entries in dataset for ",
paste(dfargs[p, ], collapse = " ", sep = "")))
} }
# Note: it's ok at this point if the resulting df contains more than one row as # Note: it's ok at this point if the resulting dataset contains more than one row as
# more matching will be done below # more matching will be done below
# If we haven't had reason to stop(), we should be good # 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 # just housekeeping: rename the variable so we don't have to edit code below
subset.SHE.data <- subset.concentration this.SHE.data <- this.data.concentration
} else { } else {
# just housekeeping again # just housekeeping again
subset.SHE.data <- subset.scale this.SHE.data <- this.data.scale
} }
# use KCl(aq) as default to avoid aborting
# (good assumption at this point, as we always have KCl for the cases ## Electrolyte
# where an electrode system has more than one electrolyte) # == We would like to transparently handle the following scenario:
default.electrolyte <- "KCl(aq)" # || if the user did not specify electrolyte solution (which we can check by using formals())
# If this subset contains more than one unique electrolyte (e.g., NaCl and KCl) # || but the dataset (after subsetting against scale and concentration above) still contains
# the user MUST have made a choice (in the "electrolyte" argument) that results # || more than one electrolyte
# in a single electrolyte remaining, or else we will warn and abort # >> Approach: we'll specify a "fallback" electrolyte, KCl (usually that's what the user wants)
if (length(unique(subset.SHE.data$electrolyte)) > 1) { # >> and inform/warn about it
# data (in subset.SHE.data) contains more than one electrolyte
# if user did not change electrolyte arg value, use default and issue warning # KCl is a good assumption, as we always have KCl for the cases where
if (identical(electrolyte, formals(as.SHE)$electrolyte)) { # an electrode system has more than one electrolyte
warning(paste0("You did not specify an electrolyte, but more than one ", fallback.electrolyte <- "KCl(aq)"
"is available for E = ", df$potential[p], " V vs ", df$scale[p], ".\n", if (length(unique(this.SHE.data$electrolyte)) > 1) {
"We'll use the default electrolyte: ", default.electrolyte)) if (formals(as.SHE)$electrolyte == "") {
subset.SHE.data <- warning(paste0("More than one electrolyte ",
subset(subset.SHE.data, electrolyte == default.electrolyte) "available for E(", dfargs$scale[p], ") in dataset. ",
"I'll assume you want ", fallback.electrolyte, "."))
this.SHE.data <-
subset(this.SHE.data, electrolyte == fallback.electrolyte)
} else { } else {
# else the user did change the electrolyte arg, use the user's value # else the user did change the electrolyte arg, use the user's value
subset.SHE.data <- this.SHE.data <-
subset.SHE.data[which(subset.SHE.data$electrolyte == electrolyte), ] subset(this.SHE.data, electrolyte == dfargs$electrolyte[p])
# print only for debugging - disable before production! # but stop if the resulting dataframe contains no rows
print(subset.SHE.data) if (dim(this.SHE.data)[1] == 0) stop("Your choice of electrolyte does not match any 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 { } else {
# data only contains one electrolyte # dataset contains only one unique electrolyte
# just check that it matches whatever the user supplied, if not, # again, check if electrolyte in arg matches the one in dataset
# issue a warning (but don't abort, typically the user did not set it # if it does, great, if it does not, print a message and use it anyway
# because they don't care and want whatever is in the data) if (unique(this.SHE.data$electrolyte) == dfargs$electrolyte[p]) {
if (any(subset.SHE.data$electrolyte != electrolyte)) { this.SHE.data <-
warning(paste0("The requested electrolyte: ", subset(this.SHE.data, electrolyte == dfargs$electrolyte[p])
ifelse(any(electrolyte == ""), } else {
"<none specified>", # whatever electrolyte the user supplied does not match what's left in the datasubset
electrolyte), # but at this point the user is probably better served by returning the electrolyte we have
" was not found for E = ", df$potential[p], " V vs ", df$scale[p], ".\n", # along with an informative message (that's the only reason for the if-else below)
"My data only lists one electrolyte for that scale - return value calculated on that basis.")) electrolytes.in.subset <-
subset.SHE.data <- unique(subset(as.SHE.data, electrode == dfargs$scale[p])$electrolyte)
subset(subset.SHE.data, electrolyte == unique(subset.SHE.data$electrolyte)) if (dfargs$electrolyte[p] == "") {
message(
paste0('Electrolyte "" (empty string) not in dataset for E(',
dfargs$scale[p], '). ',
'These electrolytes are: ',
paste(electrolytes.in.subset, collapse = ', or '), '.',
"I'll assume you want ", fallback.electrolyte, ".")
)
} else { } else {
subset.SHE.data <- message(paste0("Electrolyte ", dfargs$electrolyte[p], " not in dataset for E(",
subset(subset.SHE.data, electrolyte == electrolyte) dfargs$scale[p], "). ",
"These electrolytes are: ",
paste(electrolytes.in.subset, collapse = ", or "), ".",
"I'll assume you want ", fallback.electrolyte, ".")
)
}
} }
} }
# temperature # temperature
# either happens to match a temperature in the dataset, or we interpolate # either happens to match a temperature in the dataset, or we interpolate
# (under the assumption that potential varies linearly with temperature) # (under the assumption that potential varies linearly with temperature)
if (!any(subset.SHE.data$temp == df$temperature[p])) { if (!any(this.SHE.data$temp == dfargs$temperature[p])) {
# sought temperature was not available in dataset, check that it falls inside # 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 # 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)) && if ((dfargs$temperature[p] <= max(this.SHE.data$temp)) && (dfargs$temperature[p] >= min(this.SHE.data$temp))) {
(df$temperature[p] >= min(subset.SHE.data$temp))) {
# within dataset range, do linear interpolation # within dataset range, do linear interpolation
lm.subset <- stats::lm(SHE ~ temp, data = subset.SHE.data) lm.subset <- stats::lm(SHE ~ temp, data = this.SHE.data)
# interpolated temperature, calculated based on linear regression # interpolated temperature, calculated based on linear regression
# (more accurate than simple linear interpolation with approx()) # (more accurate than simple linear interpolation with approx())
pot.interp <- pot.interp <-
lm.subset$coefficients[2] * df$temperature[p] + lm.subset$coefficients[1] lm.subset$coefficients[2] * dfargs$temperature[p] + lm.subset$coefficients[1]
# message("Calc potential using interp temperature")
### CALC POTENTIAL vs requested scale ### CALC POTENTIAL vs requested scale
if (df$scale[p] == common::RefCanonicalName("AVS")) { dfargs$potentialvsscale[p] <-
# message("Target scale is AVS") ifelse(dfargs$scale[p] == "AVS",
df$potentialvsscale[p] <- pot.interp - dfargs$potential[p],
pot.interp - df$potential[p] dfargs$potential[p] - pot.interp)
} else {
# message("Target scale is not AVS")
df$potentialvsscale[p] <-
df$potential[p] - pot.interp
}
} }
} else { } else {
# requested temperature does exist in dataset # requested temperature does exist in dataset
### CALC POTENTIAL vs requested scale ### CALC POTENTIAL vs requested scale
# message("Calc potential using exact temperature match") dfargs$potentialvsscale[p] <-
if (df$scale[p] == common::RefCanonicalName("AVS")) { ifelse(dfargs$scale[p] == "AVS",
# message("Target scale is AVS") subset(this.SHE.data, temp == dfargs$temperature[p])$SHE - dfargs$potential[p],
df$potentialvsscale[p] <- dfargs$potential[p] - subset(this.SHE.data, temp == dfargs$temperature[p])$SHE)
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) return(dfargs$potentialvsscale)
} }

@ -2,9 +2,3 @@
Includes common numerical functions, unit converters, Includes common numerical functions, unit converters,
some LaTeX-specific functions, as well as reference data. some LaTeX-specific functions, as well as reference data.
## Known bugs
`as.SHE()` and `from.SHE()` will fail to return any results if a vector of potentials is supplied along with a vector containing more than one reference scale (e.g., `as.SHE(potentials = c(0.24, 0.46, -0.15), scale = c("AgCl", "SCE", "AVS"))`).
This bug was first confirmed for vesion `0.0.0.9011`.

Loading…
Cancel
Save