@ -35,10 +35,14 @@ SHE2AVS <- function(she) {
#' (as defined by this package).
#' This function tries to match against as many variations as possible for each
#' reference electrode.
#' The entire point of this function is to decrease the mental load on the user
#' by not requiring them to remember a particular label or name for each reference
#' electrode, instead almost any sufficiently distinct label or string will still
#' be correctly identified.
#'
#' @param refname string
#' @param refname string or a vector of strings
#'
#' @return the canonical name or empty string
#' @return vector wi th corr esponding " canonical" name or empty string (if none found)
#' @export
RefCanonicalName <- function ( refname ) {
# scale names
@ -73,33 +77,35 @@ RefCanonicalName <- function(refname) {
" Li/Li+" ,
" Lithium" )
# to match the lowercase version, use tolower()
# perhaps also replace hyphens and slashes with space?
matches <-
data.frame ( electrode = names ( electrode.system ) ,
m = rep ( 0 , length ( electrode.system ) ) ,
# 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
electrode <-
data.frame ( refname =
# here we create lower-case version of electrode.system,
# and version with symbols (-/) subbed with spaces
c ( unname ( unlist ( electrode.system ) ) ,
tolower ( unname ( unlist ( electrode.system ) ) ) ,
gsub ( " [-/]" , " " , unname ( unlist ( electrode.system ) ) ) ) ,
refcanon =
rep ( sub ( " [0-9]$" , " " , names ( unlist ( electrode.system ) ) ) , 3 ) ,
stringsAsFactors = FALSE )
# loop over electrode systems
for ( i in 1 : length ( electrode.system ) ) {
# check for a match in any cell of this row,
# also trying all lower-case and substituting symbols with spaces
if ( any ( electrode.system [ [i ] ] == refname ) ||
any ( tolower ( electrode.system [ [i ] ] ) == refname ) ||
any ( gsub ( " [-/]" , " " , electrode.system [ [i ] ] ) == refname ) ) {
matches $ m [i ] <- 1
}
# detect and remove duplicates
electrode <-
electrode [ ! duplicated ( electrode $ refname ) , ]
# reset row numbering in dataframe just for good measure
row.names ( electrode ) <- 1 : dim ( electrode ) [1 ]
# pre-allocate the return vector
refcanon <- rep ( " " , length ( refname ) )
# now all we have to do is check each user-submitted refname against
# electrode$refname and return the value on the same row but next column
for ( i in 1 : length ( refname ) ) {
refcanon [i ] <-
electrode $ refcanon [which ( electrode $ refname == refname [i ] ) ]
}
# if everything went as expected we should have just one match
if ( sum ( matches $ m ) != 1 ) {
# something wrong (should probably add warn/error here)
# for now, just return empty string
return ( " " )
} else {
return ( matches $ electrode [which ( matches $ m == 1 ) ] )
}
return ( refcanon )
}
@ -212,7 +218,10 @@ potentials.as.SHE <- function() {
#' Convert from electrochemical or electronic scale to SHE
#'
#' @param potential in the original scale, V or eV
#' Convert an arbitrary number of potentials against any known electrochemical
#' scale (or the electronic vacuum scale) to potential vs SHE.
#'
#' @param potential potential in volt
#' @param scale name of the original scale
#' @param concentration of electrolyte in mol/L, or as the string "saturated"
#' @param temperature of system in degrees Celsius
@ -229,109 +238,104 @@ as.SHE <- function(potential,
# 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
if ( RefCanonicalName ( scale ) == " " ) {
warning ( " as.SHE(): Sorry, you have supplied an unrecognised electrode scale." )
return ( NA )
# 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 equal number of elements" )
}
# there is the simple case of
if ( RefCanonicalName ( scale ) == " SHE" ) {
warning ( " This function can only convert from scales other than SHE!" )
return ( NA )
arglength <- length ( potential )
# make the concentration and temperature args to this same length,
# unless the user supplied them (only necessary for > 1)
if ( arglength > 1 ) {
# handle two cases:
# 1. user did not touch concentration or temperature args.
# Assume they forgot and reset their length and print a message
# 2. user did change concentration or temperature, 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 ( as.SHE ) $ concentration ) &
identical ( temperature , formals ( as.SHE ) $ temperature ) ) {
# case 1
# message("NOTE: default concentration and temperature values used for all potentials and scales.")
message ( paste0 ( " Default concentration (" , formals ( as.SHE ) $ concentration , " ) and default temperature (" , formals ( as.SHE ) $ temperature , " C) used for all supplied potential and scale values." ) )
concentration <- rep ( concentration , arglength )
temperature <- rep ( temperature , arglength )
} else {
# case 2
stop ( " Arguments concentration and temperature must have same number of elements as potential and scale!" )
}
}
# AVS needs special consideration
if ( RefCanonicalName ( scale ) == " AVS" ) {
# reset arg concentration
concentration <- " "
# second, since AVS scale goes in the opposite direction to the electrochemical scales
# we will define our own function
negifavs <- function ( a , b ) {
a - b
}
} else {
# we will define the same function differently for
# the case we're not dealing with AVS
negifavs <- function ( a , b ) {
a + b
}
## 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 ,
scale = RefCanonicalName ( scale ) ,
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 SHE
df $ SHE <- as.numeric ( NA )
# 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" ) ) ) {
# concentration is meaningless for AVS (no electrolyte)
# so for those rows, we'll reset it
df $ concentration [which ( df $ scale == RefCanonicalName ( " AVS" ) ) ] <- " "
df $ vacuum [which ( df $ scale == RefCanonicalName ( " AVS" ) ) ] <- TRUE
}
if ( is.character ( concentration ) ) {
# supplied concentration is character string
subspot <-
subset ( subset ( as.SHE.data ,
electrode == RefCanonicalName ( scale ) ) ,
conc.string == concentration )
# if either "scale" or "concentration" are not found in the data, subspot will contain zero rows
if ( dim ( subspot ) [1 ] == 0 ) {
warning ( " as.SHE(): Supplied scale or concentration does not exist in data. Returning NA." )
return ( NA )
}
# so far, we have
# scale: checked!
# concentration: checked!
# only temperature remains to be handled
# temperature value could happen to match a value in the data, or lie somewhere in between
# note: we will not allow extrapolation
if ( ! any ( subspot $ temp == temperature ) ) {
# if sought temperature is not available in dataset, check that it falls inside
if ( ( temperature < max ( subspot $ temp ) ) && ( temperature > min ( subspot $ temp ) ) ) {
# within dataset range, do linear interpolation
lm.subspot <- stats :: lm ( SHE ~ temp , data = subspot )
# interpolated temperature, calculated based on linear regression
# (more accurate than simple linear interpolation with approx())
potinterp <-
lm.subspot $ coefficients [2 ] * temperature + lm.subspot $ coefficients [1 ]
### CALC RETURN POTENTIAL
return ( negifavs ( potinterp , potential ) )
} else {
# outside dataset range, warning and return NA (we don't extrapolate)
warning ( " as.SHE(): the temperature you requested falls outside data range." )
return ( NA )
}
# now just work our way through df, line-by-line to determine potential as SHE
# all necessary conditions should be recorded right here in df
for ( p in 1 : dim ( df ) [1 ] ) {
if ( is.character ( df $ concentration [p ] ) ) {
subset.SHE.data <-
subset ( subset ( as.SHE.data , conc.string == df $ concentration [p ] ) ,
electrode == df $ scale [p ] )
} else {
# requested temperature does exist in dataset
### CALC RETURN POTENTIAL
return ( negifavs ( subset ( subspot , temp == temperature ) $ SHE , potential ) )
subset.SHE.data <-
subset ( subset ( as.SHE.data , conc.num == df $ concentration [p ] ) ,
electrode == df $ scale [p ] )
}
# outer-most if-else
} else {
# supplied concentration is numeric
# note: all code inside this else is the same as inside the if,
# just for the case of numeric concentration
subspot <-
subset ( subset ( as.SHE.data ,
electrode == RefCanonicalName ( scale ) ) ,
conc.num == concentration )
# if either "scale" or "concentration" are not found in the data, subspot will contain zero rows
if ( dim ( subspot ) [1 ] == 0 ) {
warning ( " as.SHE(): Supplied scale or concentration does not exist in data. Returning NA." )
return ( NA )
}
if ( ! any ( subspot $ temp == temperature ) ) {
# if sought temperature is not available in dataset, check that it falls inside
if ( ( temperature < max ( subspot $ temp ) ) && ( temperature > min ( subspot $ temp ) ) ) {
# 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
if ( ( df $ temperature [p ] < max ( subset.SHE.data $ temp ) ) &&
( df $ temperature [p ] > min ( subset.SHE.data $ temp ) ) ) {
# within dataset range, do linear interpolation
lm.subs po t <- stats :: lm ( SHE ~ temp , data = subs pot )
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())
potinterp <-
lm.subspot $ coefficients [2 ] * temperature + lm.subspot $ coefficients [1 ]
### CALC RETURN POTENTIAL
return ( negifavs ( potinterp , potential ) )
} else {
# outside dataset range, warning and return NA (we don't extrapolate)
warning ( " as.SHE(): the temperature you requested falls outside data range." )
return ( NA )
pot.interp <-
lm.subset $ coefficients [2 ] * df $ temperature [p ] + lm.subset $ coefficients [1 ]
### CALC POTENTIAL vs SHE
df $ SHE [p ] <-
ifelse ( df $ vacuum [p ] ,
pot.interp - df $ potential [p ] ,
pot.interp + df $ potential [p ] )
}
} else {
# requested temperature does exist in dataset
### CALC RETURN POTENTIAL
return ( negifavs ( subset ( subspot , temp == temperature ) $ SHE , potential ) )
### CALC POTENTIAL vs SHE
df $ SHE [p ] <-
ifelse ( df $ vacuum [p ] ,
subset ( subset.SHE.data , temp == df $ temperature [p ] ) $ SHE - df $ potential [p ] ,
subset ( subset.SHE.data , temp == df $ temperature [p ] ) $ SHE + df $ potential [p ] )
}
}
return ( df $ SHE )
}