pH Functions
Background
The Ocean Observatories Initiative deploys two families of pH instruments
across its moored platforms. The table below lists the OOI instrument classes
covered by this module.
| Class |
Hardware |
Platform |
Designator meaning |
| PHSEN-A/D/E/F |
Sunburst SAMI-pH |
Moored (fixed depth) |
pH Sensor |
| PHSEN-G/H |
Sea-Bird Scientific Deep SeapHOx V2 |
Moored (fixed depth) |
pH Sensor |
ph_functions.py processes data from the Sunburst Sensors SAMI-pH
(PHSEN-A/D/E/F) and computes the L2 pH of seawater data product
(PHWATER_L2). phsen_h_functions.py processes data from the Sea-Bird
Scientific Deep SeapHOx V2
(PHSEN-G and PHSEN-H) and computes the same PHWATER_L2 data product. Both
instruments measure pH on the total hydrogen ion scale (\(\text{pH}_T\)). All
calibration coefficients are from factory calibration sheets supplied with
individual instruments.
PHWATER_L2 — pH of Seawater
PHWATER_L2 is the pH of seawater on the total hydrogen ion scale
(\(\text{pH}_T\)). Both PHSEN-A/D/E/F and PHSEN-G/H produce PHWATER_L2 directly
from their raw L0 data; there is no intermediate L1 product.
Sunburst Sensors SAMI-pH
The SAMI-pH measures pH using a colorimetric reaction with the pH indicator
meta-Cresol Purple (mCP). A seawater sample is pumped through a flow cell and
injected with a pulse of indicator solution. Two LEDs illuminate the
indicator-sample mixture at 434 nm and 578 nm — the peak absorbance wavelengths
of the protonated (\(\text{HI}^-\)) and deprotonated (\(\text{I}^{2-}\)) forms of
mCP, respectively. The ratio of the absorbances at these two wavelengths is used
to compute pH on the total scale.
L0 Inputs
Each SAMI-II data record contains two types of raw light measurements:
- Blank measurements — 4 sets of 4 interleaved readings
\([\text{ref}_{434},\ \text{sig}_{434},\ \text{ref}_{578},\ \text{sig}_{578}]\)
collected while pumping pure seawater (16 values total).
- Sample measurements — 23 sets of 4 interleaved readings in the same
arrangement, collected while pumping the seawater–indicator mixture
(92 values total).
Raw signal intensities range from 0 to 4096 counts.
Thermistor Temperature
The raw thermistor count is converted to temperature in \(^\circ\)C. The
conversion depends on the SAMI hardware generation (12-bit or 14-bit ADC).
For 12-bit hardware (full-scale count \(= 4096\)):
\[r_t = \ln\left(\frac{\text{Therm}}{4096 - \text{Therm}} \times 17400\right)\]
For 14-bit hardware (full-scale count \(= 16384\)):
\[r_t = \ln\left(\frac{\text{Therm}}{16384 - \text{Therm}} \times 17400\right)\]
In both cases temperature is then:
\[T_C = \frac{1}{0.0010183 + 0.000241 \times r_t + 1.5 \times 10^{-7} \times {r_t}^3} - 273.15\]
The 14-bit ADC is a post-DPS hardware change not described in DPS 1341-00510.
Blank Normalization
The blank signal intensity ratio at each wavelength is computed from the 4
blank measurement sets and averaged:
\[B_{434} = \frac{1}{4}\sum_{k=1}^{4} \frac{\text{sig}_{434,k}}{\text{ref}_{434,k}}\]
\[B_{578} = \frac{1}{4}\sum_{k=1}^{4} \frac{\text{sig}_{578,k}}{\text{ref}_{578,k}}\]
Absorbance
The blank-corrected absorbances at each wavelength are computed using
Beer's Law:
\[A_{434} = -\log_{10}\left(\frac{\text{sig}_{434}}{\text{ref}_{434}}\right) - \left(-\log_{10} B_{434}\right)\]
\[A_{578} = -\log_{10}\left(\frac{\text{sig}_{578}}{\text{ref}_{578}}\right) - \left(-\log_{10} B_{578}\right)\]
The absorbance ratio is:
\[R = \frac{A_{578}}{A_{434}}\]
Temperature-Dependent Molar Absorptivities
The molar absorptivities of the two indicator forms are adjusted for
temperature using the factory-supplied reference values \(e_{a434}\),
\(e_{b434}\), \(e_{a578}\), \(e_{b578}\) at a reference temperature of
24.788 \(^\circ\)C:
\[E_{a434} = e_{a434} - 26 \times (T_C - 24.788)\]
\[E_{a578} = e_{a578} + (T_C - 24.788)\]
\[E_{b434} = e_{b434} + 12 \times (T_C - 24.788)\]
\[E_{b578} = e_{b578} - 71 \times (T_C - 24.788)\]
The absorptivity ratios used in the pH equation are:
\[e_1 = E_{a578} / E_{a434}, e_2 = E_{b578} / E_{a434}, e_3 = E_{b434} / E_{a434}\]
pKa
The apparent dissociation constant of mCP is computed from temperature and
salinity (Clayton and Byrne, 1993):
\[pK'_a = \frac{1245.69}{T_C + 273.15} + 3.8275 + 0.0021 \times (35 - S)\]
where \(S\) is the seawater practical salinity from a co-lcated CTD (default
35.0 if no CTD data are available).
PHWATER_L2 Calculation
Point-by-point pH values are computed for each of the 23 sample measurement
sets:
\[\text{pH}_\text{point} = pK'_a + \log_{10}\left(\frac{R - e_1}{e_2 - R \times e_3}\right)\]
Indicator concentrations for the protonated and deprotonated forms are:
\[[\text{HI}^-] = \frac{A_{434} \times E_{b578} - A_{578} \times E_{b434}}{E_{a434} \times E_{b578} - E_{b434} \times E_{a578}}\]
\[[\text{I}^{2-}] = \frac{A_{578} \times E_{a434} - A_{434} \times E_{a578}}{E_{a434} \times E_{b578} - E_{b434} \times E_{a578}}\]
\[C_\text{ind} = [\text{HI}^-] + [\text{I}^{2-}]\]
The first 5 of the 23 measurement sets are discarded. From the remaining 18,
the 8 consecutive points with the highest linear \(R^2\) between \(C_\text{ind}\)
and \(\text{pH}_\text{point}\) are selected. The final PHWATER_L2 value is
the y-intercept of a linear regression of \(\text{pH}_\text{point}\) on
\(C_\text{ind}\) through those 8 points — i.e., the pH extrapolated to zero
indicator concentration.
An impurity correction is applied when the extrapolated pH exceeds 8.2:
\[\text{pH}_\text{final} = \text{pH} \times \text{ind_slp} + \text{ind_off}\]
where ind_slp and ind_off are instrument-specific correction factors
not described in DPS 1341-00510.
Output accuracy: \(\pm 0.01\) pH units; precision \(\pm 0.005\) pH units
(DPS 1341-00510, §4.4). Algorithm results are valid between 0 and 35
\(^\circ\)C and at salinities of \(35 \pm 1\); salinity corrections
from a co-located CTD extend the valid salinity range (DPS 1341-00510,
§3.3).
Sea-Bird Scientific Deep SeapHOx V2
The Deep SeapHOx V2 combines the Deep SeaFET V2 ISFET pH sensor with the
Sea-Bird Electronics SBE 37-SMP-ODO MicroCAT CTD+DO sensor. PHSEN-G and
PHSEN-H differ only in the pressure rating of their strain-gauge pressure
sensor (Sea-Bird Scientific Deep SeapHOx V2 data sheet, DS53, May 2025).
The ISFET external electrochemical cell exhibits a Nernstian response to pH
and is sensitive to chloride activity. The raw ISFET voltage is digitized
by a 23-bit ADC with a 2.5 V reference and unity gain and converted to
volts before the pH calculation (Sea-Bird Scientific Application Note 99).
PHWATER_L2 Calculation
The Nernst factor is computed from temperature and fundamental constants
(Application Note 99):
\[S_\text{nernst} = \frac{R \times T \times \ln(10)}{F}\]
where \(R = 8.3144621\ \text{J} \times (\text{mol} \times \text{K})^{-1}\),
\(T\) is temperature in K, and \(F = 96485.365\ \text{C} \times \text{mol}^{-1}\).
The pressure response of the sensor is modeled by a 6th-order polynomial
in pressure \(P\) (dbar) using factory coefficients \(f_1\) through \(f_6\)
(Application Note 99):
\[f(P) = f_1 P + f_2 P^2 + f_3 P^3 + f_4 P^4 + f_5 P^5 + f_6 P^6\]
The coefficient \(f_0\) is captured in \(k_0\) and is not used separately.
Total pH on the total hydrogen ion scale is then (Application Note 99,
Johnson et al. 2016, Johnson et al. 2017):
\[\begin{align}
pH_T &= \frac{V_\text{FET/REF} - k_0 - k_2 \times t - f(P)}{S_\text{nernst}} \\
&\quad + \log_{10}(Cl_T) + 2 \times \log_{10}(\gamma_{\pm\text{HCl}})_{T\&P} \\
&\quad - \log_{10}\left(1 + \frac{S_T}{K_{S,T\&P}}\right) \\
&\quad - \log_{10}\left(\frac{1000 - 1.005 \times S}{1000}\right)
\end{align}\]
where \(t\) is temperature in \(^\circ\)C, \(S\) is practical salinity,
\(Cl_T\) is total chloride, \((\gamma_{\pm\text{HCl}})_{T\&P}\) is the HCl
activity coefficient corrected for temperature and pressure, \(S_T\) is
total sulfate, and \(K_{S,T\&P}\) is the acid dissociation constant of
\(\text{HSO}_4^-\) corrected for temperature and pressure.
The intermediate quantities are computed as follows (Application Note 99):
Total chloride (Dickson et al. 2007):
\[Cl_T = \frac{0.99889}{35.453} \times \frac{S}{1.80655} \times \frac{1000}{1000 - 1.005 \times S}\]
Sample ionic strength (Dickson et al. 2007):
\[I = \frac{19.924 \times S}{1000 - 1.005 \times S}\]
Debye-Hückel constant (Khoo et al. 1977):
\[A_{DH} = 3.4286 \times 10^{-6} \times t^2 + 6.7503 \times 10^{-4} \times t + 0.49172143\]
HCl activity coefficient — temperature only (Khoo et al. 1977):
\[\log(\gamma_{\pm\text{HCl}})_T = \frac{-A_{DH} \times \sqrt{I}}{1 + 1.394 \times \sqrt{I}} + (0.08885 - 0.000111 \times t) \times I\]
Partial molal volume of HCl (Millero 1983):
\[\bar{V}_\text{HCl} = 17.85 + 0.1044 \times t - 0.0001316 \times t^2\]
HCl activity coefficient — temperature and pressure (Johnson et al. 2017):
\[\log(\gamma_{\pm\text{HCl}})_{T\&P} = \log(\gamma_{\pm\text{HCl}})_T + \frac{\bar{V}_\text{HCl} \times p}{2 \times \ln(10) \times R \times T \times 10}\]
where \(p\) is pressure in bar.
Total sulfate (Dickson et al. 2007):
\[S_T = \frac{0.1400}{96.062} \times \frac{S}{1.80655}\]
Acid dissociation constant of HSO\(_4\)\(^-\) at \(T\) (Dickson et al. 2007):
\[K_S = (1 - 0.001005 \times S) \times \exp(adc)\]
where \(adc\) is:
\[\begin{align}
adc &= (\frac{-4276.1}{T} + 141.328 - 23.093 \times \ln(T) \\
&\quad + \left(\frac{-13856}{T} + 324.57 - 47.986\ln(T)\right) \\
&\quad \times \sqrt{I} + \left(\frac{35474}{T} - 771.54 + 114.723 \times \ln(T)\right) \\
&\quad \times I - \frac{2698}{T} \times I^{1.5} + \frac{1776}{T} \times I^2
\end{align}\]
Partial molal volume of \(\text{HSO}_4^-\) (Millero 1983):
\[\bar{V}_S = -18.03 + 0.0466 \times t + 0.000316 \times t^2\]
Compressibility of \(\text{HSO}_4^-\) (Millero 1983):
\[\bar{K}_S = \frac{-4.53 + 0.09 \times t}{1000}\]
Acid dissociation constant corrected for \(T\) and \(P\) (Millero 1982):
\[K_{S,T\&P} = K_S \times \exp\left(\frac{-\bar{V}_S \times p + 0.5 \times \bar{K}_S \times p^2}{R \times T \times 10}\right)\]
Calibration coefficients \(k_0\), \(k_2\), and \(f_1\)–\(f_6\) are from factory
calibration sheets supplied by Sea-Bird Scientific with each instrument.
Output accuracy: \(\pm 0.05\) pH units; resolution 0.004 pH units; typical
stability 0.003 pH units per month (Sea-Bird Scientific Deep SeapHOx V2
data sheet, DS53, May 2025).
Core Functions
ph_calc_phwater(ref, light, therm, ea434, eb434, ea578, eb578, ind_slp, ind_off, psal=35.0)
Compute the OOI L2 pH of seawater (PHWATER_L2) from the Sunburst
SAMI-II pH instrument (PHSEN).
| Parameters: |
-
ref
((array_like, shape(nRec, 16)))
–
Raw blank reference and signal measurements from the PHSEN blank
cycle. Contains 4 sets of 4 interleaved measurements:
[ref434, sig434, ref578, sig578][counts].
-
light
((array_like, shape(nRec, 92)))
–
Raw reference and signal measurements from the PHSEN measurement
cycle. Contains 23 sets of 4 interleaved measurements:
[ref434, sig434, ref578, sig578][counts].
-
therm
((array_like, shape(nRec)))
–
Thermistor temperature at the end of the measurement cycle,
converted to degrees C via ph_thermistor (ABSTHRM_L0) [deg_C].
-
ea434
((array_like, shape(nRec)))
–
Calibration coefficient 1. Molar absorptivity of the acidic
indicator form at 434 nm at the reference temperature [unitless].
-
eb434
((array_like, shape(nRer)))
–
Calibration coefficient 2. Molar absorptivity of the basic
indicator form at 434 nm at the reference temperature [unitless].
-
ea578
((array_like, shape(nRec)))
–
Calibration coefficient 3. Molar absorptivity of the acidic
indicator form at 578 nm at the reference temperature [unitless].
-
eb578
((array_like, shape(nRec)))
–
Calibration coefficient 4. Molar absorptivity of the basic
indicator form at 578 nm at the reference temperature [unitless].
-
ind_slp
((float or array_like, shape(nRec)))
–
Indicator impurity slope correction factor applied to pH values
greater than 8.2 [unitless].
-
ind_off
((float or array_like, shape(nRec)))
–
Indicator impurity offset correction factor applied to pH values
greater than 8.2 [unitless].
-
psal
((float or array_like, shape(nRec)), default:
35.0
)
–
Practical salinity from a co-located CTD. Default is 35.0 if
CTD data are unavailable [unitless].
|
| Returns: |
-
ph( (ndarray, shape(nRec))
) –
pH of seawater on the total hydrogen ion scale (PHWATER_L2)
[unitless].
|
Notes
The algorithm selects the 8 most linearly consistent measurement
points from 23 collected during each cycle (skipping the first 5)
by finding the window of 8 consecutive points with the highest
linear correlation coefficient (R^2) between indicator concentration
and point pH. The final pH is extrapolated to zero indicator
concentration from this best-fit region.
An impurity correction (ind_slp, ind_off) is applied when the
calculated pH exceeds 8.2. This correction is not described in DPS
1341-00510 and was added post-publication.
Source code in ion_functions/data/ph_functions.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353 | def ph_calc_phwater(ref, light, therm, ea434, eb434, ea578, eb578,
ind_slp, ind_off, psal=35.0):
"""
Compute the OOI L2 pH of seawater (PHWATER_L2) from the Sunburst
SAMI-II pH instrument (PHSEN).
Parameters
----------
ref : array_like, shape (nRec, 16)
Raw blank reference and signal measurements from the PHSEN blank
cycle. Contains 4 sets of 4 interleaved measurements:
[ref434, sig434, ref578, sig578] [counts].
light : array_like, shape (nRec, 92)
Raw reference and signal measurements from the PHSEN measurement
cycle. Contains 23 sets of 4 interleaved measurements:
[ref434, sig434, ref578, sig578] [counts].
therm : array_like, shape (nRec)
Thermistor temperature at the end of the measurement cycle,
converted to degrees C via ph_thermistor (ABSTHRM_L0) [deg_C].
ea434 : array_like, shape (nRec)
Calibration coefficient 1. Molar absorptivity of the acidic
indicator form at 434 nm at the reference temperature [unitless].
eb434 : array_like, shape (nRer)
Calibration coefficient 2. Molar absorptivity of the basic
indicator form at 434 nm at the reference temperature [unitless].
ea578 : array_like, shape (nRec)
Calibration coefficient 3. Molar absorptivity of the acidic
indicator form at 578 nm at the reference temperature [unitless].
eb578 : array_like, shape (nRec)
Calibration coefficient 4. Molar absorptivity of the basic
indicator form at 578 nm at the reference temperature [unitless].
ind_slp : float or array_like, shape (nRec)
Indicator impurity slope correction factor applied to pH values
greater than 8.2 [unitless].
ind_off : float or array_like, shape (nRec)
Indicator impurity offset correction factor applied to pH values
greater than 8.2 [unitless].
psal : float or array_like, shape (nRec), optional
Practical salinity from a co-located CTD. Default is 35.0 if
CTD data are unavailable [unitless].
Returns
-------
ph : ndarray, shape (nRec,)
pH of seawater on the total hydrogen ion scale (PHWATER_L2)
[unitless].
Notes
-----
The algorithm selects the 8 most linearly consistent measurement
points from 23 collected during each cycle (skipping the first 5)
by finding the window of 8 consecutive points with the highest
linear correlation coefficient (R^2) between indicator concentration
and point pH. The final pH is extrapolated to zero indicator
concentration from this best-fit region.
An impurity correction (ind_slp, ind_off) is applied when the
calculated pH exceeds 8.2. This correction is not described in DPS
1341-00510 and was added post-publication.
"""
ref = (np.atleast_2d(ref)).astype(float)
nRec = ref.shape[0]
light = np.atleast_3d(light).astype(float)
light = np.reshape(light, (nRec, 23, 4))
therm = np.reshape(therm, (nRec, 1)).astype(float)
ea434 = np.reshape(ea434, (nRec, 1)).astype(float)
eb434 = np.reshape(eb434, (nRec, 1)).astype(float)
ea578 = np.reshape(ea578, (nRec, 1)).astype(float)
eb578 = np.reshape(eb578, (nRec, 1)).astype(float)
if np.isscalar(ind_slp) is True:
ind_slp = np.tile(ind_slp, (nRec)).astype(float)
else:
ind_slp = np.reshape(ind_slp, (nRec)).astype(float)
if np.isscalar(ind_off) is True:
ind_off = np.tile(ind_off, (nRec)).astype(float)
else:
ind_off = np.reshape(ind_off, (nRec)).astype(float)
if np.isscalar(psal) is True:
psal = np.tile(psal, (nRec, 1)).astype(float)
else:
psal = np.reshape(psal, (nRec, 1)).astype(float)
# Calculate blanks from the 16 sets of reference light measurements
arr434 = np.array([
(ref[:, 1] / ref[:, 0]),
(ref[:, 5] / ref[:, 4]),
(ref[:, 9] / ref[:, 8]),
(ref[:, 13] / ref[:, 12]),
])
blank434 = np.reshape(np.mean(arr434, axis=0), (nRec, 1))
arr578 = np.array([
(ref[:, 3] / ref[:, 2]),
(ref[:, 7] / ref[:, 6]),
(ref[:, 11] / ref[:, 10]),
(ref[:, 15] / ref[:, 14]),
])
blank578 = np.reshape(np.mean(arr578, axis=0), (nRec, 1))
# Extract 23 sets of 4 light measurements into arrays corresponding
# to the raw reference and signal measurements at 434 and 578 nm.
ref434 = light[:, :, 0] # reference signal, 434 nm
int434 = light[:, :, 1] # signal intensity, 434 nm (PH434SI_L0)
ref578 = light[:, :, 2] # reference signal, 578 nm
int578 = light[:, :, 3] # signal intensity, 578 nm (PH578SI_L0)
# Absorbance
A434 = -sp.log10(int434 / ref434)
A434blank = -sp.log10(blank434)
abs434 = A434 - A434blank
A578 = -sp.log10(int578 / ref578)
A578blank = -sp.log10(blank578)
abs578 = A578 - A578blank
R = abs578 / abs434
# pKa from Clayton and Byrne, 1993
pKa = (1245.69 / (therm + 273.15)) + 3.8275 + (0.0021 * (35. - psal))
pKa = np.reshape(pKa, (-1, 1))
# Molar absorptivities
Ea434 = ea434 - (26. * (therm - 24.788))
Ea578 = ea578 + (therm - 24.788)
Eb434 = eb434 + (12. * (therm - 24.788))
Eb578 = eb578 - (71. * (therm - 24.788))
e1 = Ea578 / Ea434
e2 = Eb578 / Ea434
e3 = Eb434 / Ea434
V1 = R - e1
V2 = e2 - R * e3
# indicator concentration calculations
HI = (abs434 * Eb578 - abs578 * Eb434) / (Ea434 * Eb578 - Eb434 * Ea578)
I = (abs578 * Ea434 - abs434 * Ea578) / (Ea434 * Eb578 - Eb434 * Ea578)
IndConc = HI + I
pointph = np.real(pKa + sp.log10(V1 / V2))
# determine the most linear region of points for pH of seawater
# calculation, skipping the first 5 points.
IndConca = IndConc[:, 5:]
Y = pointph[:, 5:]
X = np.linspace(1, 18, 18)
# create arrays for vectorized computations used in sum of squares.
step = 7 # number of points to use
count = step + 1
nPts = np.size(X) - step
x = np.zeros((nPts, count))
y = np.zeros((nRec, nPts, count))
for i in range(nPts):
x[i, :] = X[i:i+count]
for j in range(nRec):
y[j, i, :] = Y[j, i:i+count]
# compute correlation coefficient for each window of 8 points
sumx = np.sum(x, axis=1)
sumy = np.sum(y, axis=2)
sumxy = np.sum(x * y, axis=2)
sumx2 = np.sum(x**2, axis=1)
sumy2 = np.sum(y**2, axis=2)
sumxx = sumx * sumx
sumyy = sumy * sumy
ssxy = sumxy - (sumx * sumy) / count
ssx = sumx2 - (sumxx / count)
ssy = sumy2 - (sumyy / count)
r2 = ssxy**2 / (ssx * ssy)
# Range of seawater points to use
cutoff1 = np.argmax(r2, axis=1) # Find the first, best R-squared value
cutoff2 = cutoff1 + count
# Indicator and pH range limited to best points
IndConcS = np.zeros((nRec, count))
pointphS = np.zeros((nRec, count))
for i in range(nRec):
IndConcS[i, :] = IndConca[i, cutoff1[i]:cutoff2[i]]
pointphS[i, :] = Y[i, cutoff1[i]:cutoff2[i]]
# Final pH calculation: extrapolate to zero indicator concentration
sumx = np.sum(IndConcS, axis=1)
sumy = np.sum(pointphS, axis=1)
sumxy = np.sum(pointphS * IndConcS, axis=1)
sumx2 = np.sum(IndConcS**2, axis=1)
sumy2 = np.sum(pointphS**2, axis=1)
xbar = np.mean(IndConcS, axis=1)
ybar = np.mean(pointphS, axis=1)
sumxx = sumx * sumx
sumyy = sumy * sumy
ssxy = sumxy - (sumx * sumy) / count
ssx = sumx2 - (sumxx / count)
ssy = sumy2 - (sumyy / count)
slope = ssxy / ssx
ph = ybar - slope * xbar
# pH corrections due to indicator impurity if the calculated pH is
# greater than 8.2.
phFlag = ph >= 8.2
ph[phFlag] = ph[phFlag] * ind_slp[phFlag] + ind_off[phFlag]
return ph
|
History
| Date |
Author |
Change |
| 2013-04-19 |
Christopher Wingard |
Initial code |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
ph_total(vrs_ext, degc, psu, dbar, k0, k2, f)
Compute the OOI L2 pH of seawater (PHWATER_L2) from the Sea-Bird
Scientific Deep SeapHOx V2 (PHSEN-G and PHSEN-H).
| Parameters: |
-
vrs_ext
(array_like)
–
External ISFET reference voltage (V_FET/REF) [Volts].
-
degc
(array_like)
–
In-situ temperature from the co-located CTD [deg_C].
-
psu
(array_like)
–
Practical salinity from the co-located CTD [unitless].
-
dbar
(array_like)
–
Pressure from the co-located CTD [dbar].
-
k0
(float or array_like)
–
Calibration coefficient 1. Cell standard potential offset
for the external reference [Volts].
-
k2
(float or array_like)
–
Calibration coefficient 2. Temperature slope coefficient for
the external reference [Volts deg_C^-1].
-
f
((array_like, shape(..., 6)))
–
Calibration coefficients 3-8. Coefficients f1 through f6 of
the 6th-order pressure response polynomial f(P). Coefficient
f0 is captured in k0 and is not used here [unitless].
|
| Returns: |
-
p_h( ndarray
) –
pH of seawater on the total hydrogen ion scale (PHWATER_L2)
[unitless].
|
Notes
The algorithm follows Sea-Bird Scientific Application Note 99
(Johnson et al. 2016; Johnson et al. 2017). The ISFET external
cell exhibits a Nernstian response to pH and is sensitive to
chloride activity. The total pH is computed as:
pH = (V_FET/REF - k0 - k2*t - f(P)) / S_nernst
+ log10(Cl_T) + 2*log10(gamma_HCl)_T&P
- log10(1 + S_T/K_S,T&P)
- log10((1000 - 1.005*S) / 1000)
where S_nernst = RTln(10)/F, Cl_T is total chloride, gamma_HCl
is the HCl activity coefficient corrected for temperature and
pressure, S_T is total sulfate, and K_S,T&P is the acid
dissociation constant of HSO4- corrected for temperature and
pressure. All intermediate quantities are computed from salinity,
temperature, and pressure following Dickson et al. (2007),
Khoo et al. (1977), Millero (1982, 1983), and Johnson et al.
(2017).
Calibration coefficients k0, k2, and f are from factory
calibration sheets supplied by Sea-Bird Scientific.
Source code in ion_functions/data/phsen_h_functions.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576 | def ph_total(vrs_ext, degc, psu, dbar, k0, k2, f):
"""
Compute the OOI L2 pH of seawater (PHWATER_L2) from the Sea-Bird
Scientific Deep SeapHOx V2 (PHSEN-G and PHSEN-H).
Parameters
----------
vrs_ext : array_like
External ISFET reference voltage (V_FET/REF) [Volts].
degc : array_like
In-situ temperature from the co-located CTD [deg_C].
psu : array_like
Practical salinity from the co-located CTD [unitless].
dbar : array_like
Pressure from the co-located CTD [dbar].
k0 : float or array_like
Calibration coefficient 1. Cell standard potential offset
for the external reference [Volts].
k2 : float or array_like
Calibration coefficient 2. Temperature slope coefficient for
the external reference [Volts deg_C^-1].
f : array_like, shape (..., 6)
Calibration coefficients 3-8. Coefficients f1 through f6 of
the 6th-order pressure response polynomial f(P). Coefficient
f0 is captured in k0 and is not used here [unitless].
Returns
-------
p_h : ndarray
pH of seawater on the total hydrogen ion scale (PHWATER_L2)
[unitless].
Notes
-----
The algorithm follows Sea-Bird Scientific Application Note 99
(Johnson et al. 2016; Johnson et al. 2017). The ISFET external
cell exhibits a Nernstian response to pH and is sensitive to
chloride activity. The total pH is computed as:
pH = (V_FET/REF - k0 - k2*t - f(P)) / S_nernst
+ log10(Cl_T) + 2*log10(gamma_HCl)_T&P
- log10(1 + S_T/K_S,T&P)
- log10((1000 - 1.005*S) / 1000)
where S_nernst = R*T*ln(10)/F, Cl_T is total chloride, gamma_HCl
is the HCl activity coefficient corrected for temperature and
pressure, S_T is total sulfate, and K_S,T&P is the acid
dissociation constant of HSO4- corrected for temperature and
pressure. All intermediate quantities are computed from salinity,
temperature, and pressure following Dickson et al. (2007),
Khoo et al. (1977), Millero (1982, 1983), and Johnson et al.
(2017).
Calibration coefficients k0, k2, and f are from factory
calibration sheets supplied by Sea-Bird Scientific.
"""
f = np.atleast_2d(f)
fp = (f[:, 0] * dbar + f[:, 1] * dbar ** 2 + f[:, 2] * dbar ** 3
+ f[:, 3] * dbar ** 4 + f[:, 4] * dbar ** 5 + f[:, 5] * dbar ** 6)
bar = dbar * 0.10 # convert pressure from dbar to bar
# Nernstian response of the pH electrode (slope of the response)
r = 8.3144621 # J/(mol K) universal gas constant
t = degc + 273.15 # temperature in Kelvin
f = 9.6485365e4 # C/mol Faraday constant
snerst = r * t * np.log(10) / f
# total chloride in seawater (Dickson et al. 2007)
cl_total = (0.99889 / 35.453) * (psu / 1.80655) * (1000 / (1000 - 1.005 * psu))
# partial Molal volume of HCl (Millero 1983)
vhcl = 17.85 + 0.1044 * degc - 0.0001316 * degc ** 2
# Sample ionic strength (Dickson et al. 2007)
i = (19.924 * psu) / (1000 - 1.005 * psu)
# Debye-Huckel constant for activity of HCl (Khoo et al. 1977)
adh = 0.0000034286 * degc ** 2 + 0.00067503 * degc + 0.49172143
# log of HCl activity coefficient as a function of temperature
# (Khoo et al. 1977)
loghclt = ((-adh * np.sqrt(i)) / (1 + 1.394 * np.sqrt(i))) + (0.08885 - 0.000111 * degc) * i
# log10 of HCl activity coefficient as a function of temperature
# and pressure (Johnson et al. 2017)
loghcltp = loghclt + (((vhcl * bar) / (np.log(10) * r * t * 10)) / 2)
# total sulfate in seawater (Dickson et al. 2007)
so4_total = (0.1400 / 96.062) * (psu / 1.80655)
# acid dissociation constant of HSO4- (Dickson et al. 2007)
ks = (1 - 0.001005 * psu) * np.exp(
(-4276.1 / t) + 141.328 - 23.093 * np.log(t)
+ ((-13856 / t) + 324.57 - 47.986 * np.log(t)) * np.sqrt(i)
+ ((35474 / t) - 771.54 + 114.723 * np.log(t)) * i
- (2698 / t) * i ** 1.5 + (1776 / t) * i ** 2)
# partial Molal volume of HSO4- (Millero 1983)
v_hso4 = -18.03 + 0.0466 * degc + 0.000316 * degc ** 2
# compressibility of HSO4- (Millero 1983)
kbar_s = (-4.53 + 0.09 * degc) / 1000
# acid dissociation constant of HSO4- corrected for T and P
# (Millero 1982)
kstp = ks * np.exp((-v_hso4 * bar + 0.5 * kbar_s * bar ** 2) / (r * t * 10))
# calculate total pH adjusted for pressure, temperature, and salinity
p_h = (((vrs_ext - k0 - k2 * degc - fp) / snerst)
+ np.log10(cl_total) + 2 * loghcltp
- np.log10(1 + (so4_total / kstp))
- np.log10((1000 - 1.005 * psu) / 1000))
return p_h
|
History
| Date |
Author |
Change |
| 2026-01-21 |
Samuel Dahlberg |
Initial code, adapted from Christopher Wingard's ph_total in cgsn-processing |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
Helper Functions
ph_434_intensity(light)
Extract the L0 signal intensity at 434 nm (PH434SI_L0) from the PHSEN
light measurement array.
| Parameters: |
-
light
(array_like)
–
Raw light measurement array from the PHSEN instrument. May be a
single record or an array of records. The array is reshaped
internally to (-1, 23, 4) where the second index selects the
measurement set and the third index selects the channel [counts].
|
| Returns: |
-
si434( (ndarray, shape(nRec, 23))
) –
Signal intensity at 434 nm (PH434SI_L0) [counts].
|
Notes
Each PHSEN data record contains 23 sets of 4 light measurements
interleaved as [ref434, sig434, ref578, sig578]. Column index 1
(0-based) of each set is the 434 nm signal intensity.
Source code in ion_functions/data/ph_functions.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | def ph_434_intensity(light):
"""
Extract the L0 signal intensity at 434 nm (PH434SI_L0) from the PHSEN
light measurement array.
Parameters
----------
light : array_like
Raw light measurement array from the PHSEN instrument. May be a
single record or an array of records. The array is reshaped
internally to (-1, 23, 4) where the second index selects the
measurement set and the third index selects the channel [counts].
Returns
-------
si434 : ndarray, shape (nRec, 23)
Signal intensity at 434 nm (PH434SI_L0) [counts].
Notes
-----
Each PHSEN data record contains 23 sets of 4 light measurements
interleaved as [ref434, sig434, ref578, sig578]. Column index 1
(0-based) of each set is the 434 nm signal intensity.
"""
light = np.atleast_3d(light).astype(float)
new = np.reshape(light, (-1, 23, 4))
si434 = new[:, :, 1]
return si434 # signal intensity, 434 nm (PH434SI_L0)
|
History
| Date |
Author |
Change |
| 2013-04-19 |
Christopher Wingard |
Initial code |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
ph_578_intensity(light)
Extract the L0 signal intensity at 578 nm (PH578SI_L0) from the PHSEN
light measurement array.
| Parameters: |
-
light
(array_like)
–
Raw light measurement array from the PHSEN instrument. May be a
single record or an array of records. The array is reshaped
internally to (-1, 23, 4) where the second index selects the
measurement set and the third index selects the channel [counts].
|
| Returns: |
-
si578( (ndarray, shape(nRec, 23))
) –
Signal intensity at 578 nm (PH578SI_L0) [counts].
|
Notes
Each PHSEN data record contains 23 sets of 4 light measurements
interleaved as [ref434, sig434, ref578, sig578]. Column index 3
(0-based) of each set is the 578 nm signal intensity.
Source code in ion_functions/data/ph_functions.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 | def ph_578_intensity(light):
"""
Extract the L0 signal intensity at 578 nm (PH578SI_L0) from the PHSEN
light measurement array.
Parameters
----------
light : array_like
Raw light measurement array from the PHSEN instrument. May be a
single record or an array of records. The array is reshaped
internally to (-1, 23, 4) where the second index selects the
measurement set and the third index selects the channel [counts].
Returns
-------
si578 : ndarray, shape (nRec, 23)
Signal intensity at 578 nm (PH578SI_L0) [counts].
Notes
-----
Each PHSEN data record contains 23 sets of 4 light measurements
interleaved as [ref434, sig434, ref578, sig578]. Column index 3
(0-based) of each set is the 578 nm signal intensity.
"""
light = np.atleast_3d(light).astype(float)
new = np.reshape(light, (-1, 23, 4))
si578 = new[:, :, 3]
return si578 # signal intensity, 578 nm (PH578SI_L0)
|
History
| Date |
Author |
Change |
| 2013-04-19 |
Christopher Wingard |
Initial code |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
ph_thermistor(traw, sami_bits=12)
Convert the PHSEN thermistor counts (ABSTHRM_L0) to temperature in
degrees C.
| Parameters: |
-
traw
(array_like)
–
Raw thermistor counts from the PHSEN instrument (ABSTHRM_L0)
[counts].
-
sami_bits
(int, default:
12
)
–
ADC resolution of the SAMI hardware generation. Use 12 for
original SAMI-II hardware (full-scale 4096 counts) and 14 for
newer hardware (full-scale 16384 counts). Default is 12.
|
| Returns: |
-
therm( ndarray
) –
Thermistor temperature [deg_C].
|
Notes
The conversion uses a three-term polynomial in the natural log of the
thermistor resistance. The full-scale count differs between SAMI
hardware generations: 4096 for 12-bit hardware and 16384 for 14-bit
hardware. The reference resistance is 17400 ohms in both cases.
Source code in ion_functions/data/ph_functions.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109 | def ph_thermistor(traw, sami_bits=12):
"""
Convert the PHSEN thermistor counts (ABSTHRM_L0) to temperature in
degrees C.
Parameters
----------
traw : array_like
Raw thermistor counts from the PHSEN instrument (ABSTHRM_L0)
[counts].
sami_bits : int, optional
ADC resolution of the SAMI hardware generation. Use 12 for
original SAMI-II hardware (full-scale 4096 counts) and 14 for
newer hardware (full-scale 16384 counts). Default is 12.
Returns
-------
therm : ndarray
Thermistor temperature [deg_C].
Notes
-----
The conversion uses a three-term polynomial in the natural log of the
thermistor resistance. The full-scale count differs between SAMI
hardware generations: 4096 for 12-bit hardware and 16384 for 14-bit
hardware. The reference resistance is 17400 ohms in both cases.
"""
traw = np.atleast_1d(traw)
sami_bits = np.atleast_1d(sami_bits)
if sami_bits[0] == 14:
rt = np.log((traw / (16384.0 - traw)) * 17400.0)
else:
rt = np.log((traw / (4096.0 - traw)) * 17400.0)
inv = 0.0010183 + 0.000241 * rt + 0.00000015 * rt**3
therm = (1.0 / inv) - 273.15
return therm
|
History
| Date |
Author |
Change |
| 2014-05-01 |
Christopher Wingard |
Initial code |
| 2023-08-15 |
Samuel Dahlberg |
Removed use of numexpr; added default for sami_bits |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
ph_battery(braw, sami_bits=12)
Convert the PHSEN battery counts to battery voltage in Volts.
| Parameters: |
-
braw
(array_like)
–
Raw battery counts from the PHSEN instrument [counts].
-
sami_bits
(int, default:
12
)
–
ADC resolution of the SAMI hardware generation. Use 12 for
original SAMI-II hardware (full-scale 4096 counts) and 14 for
newer hardware (full-scale 16384 counts). Default is 12.
|
Notes
The full-scale voltage and count differ between SAMI hardware
generations. For 12-bit hardware the full-scale is 15 V at 4096
counts; for 14-bit hardware the full-scale is 3 V at 4000 counts.
Source code in ion_functions/data/ph_functions.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 | def ph_battery(braw, sami_bits=12):
"""
Convert the PHSEN battery counts to battery voltage in Volts.
Parameters
----------
braw : array_like
Raw battery counts from the PHSEN instrument [counts].
sami_bits : int, optional
ADC resolution of the SAMI hardware generation. Use 12 for
original SAMI-II hardware (full-scale 4096 counts) and 14 for
newer hardware (full-scale 16384 counts). Default is 12.
Returns
-------
volts : ndarray
Battery voltage [Volts].
Notes
-----
The full-scale voltage and count differ between SAMI hardware
generations. For 12-bit hardware the full-scale is 15 V at 4096
counts; for 14-bit hardware the full-scale is 3 V at 4000 counts.
"""
braw = np.atleast_1d(braw)
sami_bits = np.atleast_1d(sami_bits)
if sami_bits[0] == 14:
volts = braw * 3. / 4000.
else:
volts = braw * 15. / 4096.
return volts
|
History
| Date |
Author |
Change |
| 2013-04-19 |
Christopher Wingard |
Initial code |
| 2023-08-15 |
Samuel Dahlberg |
Added 14-bit hardware support |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
convert_ph_voltage_counts(ph_counts)
Convert raw ISFET pH voltage counts to volts for the Deep SeapHOx V2.
| Parameters: |
-
ph_counts
(array_like)
–
Raw ISFET pH voltage counts from the Deep SeapHOx V2 instrument
[counts].
|
| Returns: |
-
ph_volts( ndarray
) –
ISFET external reference voltage (V_FET/REF) [Volts].
|
Notes
The conversion uses a 23-bit ADC with a 2.5 V reference and unity
gain. The full-scale count is 8388608 (2^23).
Source code in ion_functions/data/phsen_h_functions.py
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458 | def convert_ph_voltage_counts(ph_counts):
"""
Convert raw ISFET pH voltage counts to volts for the Deep SeapHOx V2.
Parameters
----------
ph_counts : array_like
Raw ISFET pH voltage counts from the Deep SeapHOx V2 instrument
[counts].
Returns
-------
ph_volts : ndarray
ISFET external reference voltage (V_FET/REF) [Volts].
Notes
-----
The conversion uses a 23-bit ADC with a 2.5 V reference and unity
gain. The full-scale count is 8388608 (2^23).
"""
adc_vref = 2.5
gain = 1
adc_23bit = 8388608.0
ph_volts = adc_vref / gain * (ph_counts / adc_23bit - 1)
return ph_volts
|
History
| Date |
Author |
Change |
| 2026-01-21 |
Samuel Dahlberg |
Initial code, adapted from Sea-Bird Scientific processing library |
| 2026-04-20 |
Christopher Wingard |
Converted to NumPy docstring format; updated documentation |
References
Byrne, R. H., Robertbaldo, G., Thompson, S. W. and Chen, C. T. A. (1988).
Seawater pH measurements — an at-sea comparison of spectrophotometric and
potentiometric methods. Deep-Sea Research Part A, 35(8): 1405–1410.
Clayton, T. D. and Byrne, R. H. (1993). Spectrophotometric seawater pH
measurements — total hydrogen-ion concentration scale calibration of
m-Cresol Purple and at-sea results. Deep-Sea Research Part I, 40(10):
2115–2129.
Dickson, A. G., Sabine, C. L., and Christian, J. R. (2007). Guide to Best
Practices for Ocean CO2 Measurements. PICES Special Publication 3, IOCCP
Report No. 8.
Johnson, K. S., Jannasch, H. W., Coletti, L. J., Elrod, V. A., Martz, T. R.,
Takeshita, Y., Carlson, R. J., and Connery, J. G. (2016). Deep-Sea DuraFET:
A pressure tolerant pH sensor designed for global sensor networks. Analytical
Chemistry, 88: 3249–3256.
Johnson, K. S., Plant, J. N., and Maurer, T. L. (2017). Processing BGC-Argo
pH data at the DAC level. BGC-Argo document.
Khoo, K. H., Ramette, R. W., Culberson, C. H., and Bates, R. G. (1977).
Determination of hydrogen ion concentrations in seawater from 5 C to 40 C:
standard potentials at salinities 20 to 45%. Analytical Chemistry, 49:
29–34.
Liu, X., Patsavas, M. C., and Byrne, R. H. (2011). Purification and
characterization of meta-cresol purple for spectrophotometric seawater pH
measurements. Environmental Science and Technology, 45: 4862–4868.
Martz, T. R., Connery, J. G., and Johnson, K. S. (2010). Testing the
Honeywell Durafet for seawater pH applications. Limnology and Oceanography:
Methods, 8: 172–184.
Martz, T. R., Carr, J. J., French, C. R., and DeGrandpre, M. D. (2003). A
submersible autonomous sensor for spectrophotometric pH measurements of
natural waters. Analytical Chemistry, 75: 1844–1850.
Millero, F. J. (1982). The effect of pressure on the solubility of minerals
in water and seawater. Geochimica et Cosmochimica Acta, 46: 11–22.
Millero, F. J. (1983). In Chemical Oceanography; Riley, J. P., Chester, R.,
Eds.; Academic Press: London, Vol. 8, pp 1–88.
OOI (2012). Data Product Specification for pH of Seawater. Document Control
Number 1341-00510.
Sea-Bird Scientific (2025). Deep SeapHOx V2 Data Sheet. Document DS53,
May 2025.
Sea-Bird Scientific. Application Note 99: Calculating pH from ISFET pH
Sensors. SeaFET V2, Shallow SeapHOx V2, Deep SeapHOx V2, Floats.
Seidel, M. P., DeGrandpre, M. D., and Dickson, A. G. (2008). A sensor for
in situ indicator-based measurements of seawater pH. Marine Chemistry,
109: 18–28.