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.

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.

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.