def get_Ks(ps): """ Helper function to calculate Ks. If ps.Ks is a dict, those Ks are used transparrently, with no pressure modification. """ if isinstance(ps.Ks, dict): Ks = Bunch(ps.Ks) else: if maxL(ps.Mg, ps.Ca) == 1: if ps.Mg is None: ps.Mg = 0.0528171 if ps.Ca is None: ps.Ca = 0.0102821 Ks = MyAMI_K_calc(TempC=ps.T, Sal=ps.S, Mg=ps.Mg, Ca=ps.Ca, P=ps.P) else: # if only Ca or Mg provided, fill in other with modern if ps.Mg is None: ps.Mg = 0.0528171 if ps.Ca is None: ps.Ca = 0.0102821 # calculate Ca and Mg specific Ks Ks = MyAMI_K_calc_multi(TempC=ps.T, Sal=ps.S, Ca=ps.Ca, Mg=ps.Mg, P=ps.P) # non-MyAMI Constants Ks.update(calc_KPs(ps.T, ps.S, ps.P)) Ks.update(calc_KF(ps.T, ps.S, ps.P)) Ks.update(calc_KSi(ps.T, ps.S, ps.P)) # pH conversions to total scale. # - KPn are all on SWS # - KSi is on SWS # - MyAMI KW is on SWS... DOES THIS MATTER? SWStoTOT = (1 + ps.TS / Ks.KSO4) / (1 + ps.TS / Ks.KSO4 + ps.TF / Ks.KF) # FREEtoTOT = 1 + ps.TS / Ks.KSO4 conv = ['KP1', 'KP2', 'KP3', 'KSi', 'KW'] for c in conv: Ks[c] *= SWStoTOT return Ks
def TA_DIC(TA, DIC, BT, TP, TSi, TS, TF, Ks): """ Returns pH Taken directly from MATLAB CO2SYS. """ L = maxL(TA, DIC, BT, TP, TSi, TS, TF, Ks.K1) pHguess = 7.0 pHtol = 0.00000001 pHx = np.full(L, pHguess) deltapH = np.array(pHtol + 1, ndmin=1) ln10 = np.log(10) while any(abs(deltapH) > pHtol): H = 10**-pHx # negative Denom = H**2 + Ks.K1 * H + Ks.K1 * Ks.K2 CAlk = DIC * Ks.K1 * (H + 2 * Ks.K2) / Denom BAlk = BT * Ks.KB / (Ks.KB + H) OH = Ks.KW / H PhosTop = Ks.KP1 * Ks.KP2 * H + 2 * Ks.KP1 * Ks.KP2 * Ks.KP3 - H**3 PhosBot = (H**3 + Ks.KP1 * H**2 + Ks.KP1 * Ks.KP2 * H + Ks.KP1 * Ks.KP2 * Ks.KP3) PAlk = TP * PhosTop / PhosBot SiAlk = TSi * Ks.KSi / (Ks.KSi + H) # positive Hfree = H / (1 + TS / Ks.KSO4) HSO4 = TS / (1 + Ks.KSO4 / Hfree) HF = TF / (1 + Ks.KF / Hfree) Residual = TA - CAlk - BAlk - OH - PAlk - SiAlk + Hfree + HSO4 + HF Slope = ln10 * ( DIC * Ks.K1 * H * (H**2 + Ks.K1 * Ks.K2 + 4 * H * Ks.K2) / Denom / Denom + BAlk * H / (Ks.KB + H) + OH + H) deltapH = Residual / Slope while any(abs(deltapH) > 1): FF = abs(deltapH) > 1 deltapH[FF] = deltapH[FF] / 2 pHx += deltapH return pHx
def calc_Ks(T, S, P, Mg, Ca, TS, TF, Ks=None): """ Helper function to calculate Ks. If Ks is a dict, those Ks are used transparrently (i.e. no pressure modification). """ if isinstance(Ks, dict): Ks = Bunch(Ks) else: if maxL(Mg, Ca) == 1: if Mg is None: Mg = 0.0528171 if Ca is None: Ca = 0.0102821 Ks = MyAMI_K_calc(TempC=T, Sal=S, P=P, Mg=Mg, Ca=Ca) else: # if only Ca or Mg provided, fill in other with modern if Mg is None: Mg = 0.0528171 if Ca is None: Ca = 0.0102821 # calculate Ca and Mg specific Ks Ks = MyAMI_K_calc_multi(TempC=T, Sal=S, P=P, Ca=Ca, Mg=Mg) # non-MyAMI Constants Ks.update(calc_KPs(T, S, P)) Ks.update(calc_KF(T, S, P)) Ks.update(calc_KSi(T, S, P)) # pH conversions to total scale. # - KP1, KP2, KP3 are all on SWS # - KSi is on SWS # - MyAMI KW is on SWS... DOES THIS MATTER? SWStoTOT = (1 + TS / Ks.KSO4) / (1 + TS / Ks.KSO4 + TF / Ks.KF) # FREEtoTOT = 1 + 'T_' + mode]S / Ks.KSO4 conv = ["KP1", "KP2", "KP3", "KSi", "KW"] for c in conv: Ks[c] *= SWStoTOT return Ks
def CO2_TA(CO2, TA, BT, TP, TSi, TS, TF, Ks): """ Returns pH Taken from matlab CO2SYS """ fCO2 = CO2 / Ks.K0 L = maxL(TA, CO2, BT, TP, TSi, TS, TF, Ks.K1) pHguess = 8.0 pHtol = 0.0000001 pHx = np.full(L, pHguess) deltapH = np.array(pHtol + 1, ndmin=1) ln10 = np.log(10) while any(abs(deltapH) > pHtol): H = 10**-pHx HCO3 = Ks.K0 * Ks.K1 * fCO2 / H CO3 = Ks.K0 * Ks.K1 * Ks.K2 * fCO2 / H**2 CAlk = HCO3 + 2 * CO3 BAlk = BT * Ks.KB / (Ks.KB + H) OH = Ks.KW / H PhosTop = Ks.KP1 * Ks.KP2 * H + 2 * Ks.KP1 * Ks.KP2 * Ks.KP3 - H**3 PhosBot = (H**3 + Ks.KP1 * H**2 + Ks.KP1 * Ks.KP2 * H + Ks.KP1 * Ks.KP2 * Ks.KP3) PAlk = TP * PhosTop / PhosBot SiAlk = TSi * Ks.KSi / (Ks.KSi + H) # positive Hfree = H / (1 + TS / Ks.KSO4) HSO4 = TS / (1 + Ks.KSO4 / Hfree) HF = TF / (1 + Ks.KF / Hfree) Residual = TA - CAlk - BAlk - OH - PAlk - SiAlk + Hfree + HSO4 + HF Slope = ln10 * (HCO3 + 4.0 * CO3 + BAlk * H / (Ks.KB + H) + OH + H) deltapH = Residual / Slope while any(abs(deltapH) > 1): FF = abs(deltapH) > 1 deltapH[FF] = deltapH[FF] / 2 pHx += deltapH return pHx
def CBsys(pHtot=None, DIC=None, CO2=None, HCO3=None, CO3=None, TA=None, fCO2=None, pCO2=None, BT=None, BO3=None, BO4=None, ABT=None, ABO3=None, ABO4=None, dBT=None, dBO3=None, dBO4=None, alphaB=None, T=25., S=35., P=None, Ca=None, Mg=None, TP=0., TSi=0., pHsws=None, pHfree=None, Ks=None, pdict=None, unit='umol'): """ Calculate carbon, boron and boron isotope chemistry of seawater from a minimal parameter set. Constants calculated by MyAMI model (Hain et al, 2015; doi:10.1002/2014GB004986). Speciation calculations from Zeebe & Wolf-Gladrow (2001; ISBN:9780444509468) Appendix B pH is Total scale. Inputs must either be single values, arrays of equal length or a mixture of both. If you use arrays of unequal length, it won't work. Note: Special Case! If pH is not known, you must provide either: - Two of [DIC, CO2, HCO3, CO3], and one of [BT, BO3, BO4] - One of [DIC, CO2, HCO3, CO3], and TA and BT - Two of [BT, BO3, BO4] and one of [DIC, CO2, HCO3, CO3] Isotopes will only be calculated if one of [ABT, ABO3, ABO4, dBT, dBO3, dBO4] is provided. Error propagation: If inputs are ufloat or uarray (from uncertainties package) errors will be propagated through all calculations, but: **WARNING** Error propagation NOT IMPLEMENTED for carbon system calculations with zero-finders (i.e. when pH is not given; cases 2-5 and 10-15). Concentration Units +++++++++++++++++++ * Ca and Mg must be in molar units. * All other units must be the same, and can be specified in the 'unit' variable. Defaults to umolar. * Isotopes can be in A (11B / BT) or d (delta). Either specified, both returned. Parameters ---------- pH, DIC, CO2, HCO3, CO3, TA : array-like Carbon system parameters. Two of these must be provided. If TA is specified, a B species must also be specified. pH, BT, BO3, BO4 : array-like Boron system parameters. Two of these must be provided. pH, ABT, ABO3, ABO4, dBT, dBO3, dBO4 : array-like Boron isotope system parameters. pH and one other parameter must be provided. alphaB : array-like Alpha value describing B fractionation (1.0XXX). If missing, it's calculated using the temperature sensitive formulation of Honisch et al (2008) T, S : array-like Temperature in Celcius and Salinity in PSU. Used in calculating MyAMI constants. P : array-like Pressure in Bar. Used in calculating MyAMI constants. unit : str Concentration units of C and B parameters (all must be in the same units). Can be 'mol', 'mmol', 'umol', 'nmol', 'pmol' or 'fmol'. Used in calculating Alkalinity. Default is 'umol'. Ca, Mg : arra-like The [Ca] and [Mg] of the seawater, * in mol / kg *. Used in calculating MyAMI constants. Ks : dict A dictionary of constants. Must contain keys 'K1', 'K2', 'KB' and 'KW'. If None, Ks are calculated using MyAMI model. pdict : dict Optionally, you can provide some or all parameters as a dict, with keys the same as the parameter names above. Any parameters included in the dict will overwrite manually specified parameters. This is particularly useful if you're including this in other code. Returns ------- dict(/Bunch) containing all calculated parameters. """ # Bunch inputs ps = Bunch(locals()) if isinstance(pdict, dict): ps.update(pdict) # convert unit to multiplier udict = { 'mol': 1., 'mmol': 1.e3, 'umol': 1.e6, 'nmol': 1.e9, 'pmol': 1.e12, 'fmol': 1.e15 } if isinstance(ps.unit, str): ps.unit = udict[ps.unit] elif isinstance(ps.unit, (int, float)): ps.unit = unit upar = [ 'DIC', 'CO2', 'HCO3', 'CO3', 'TA', 'fCO2', 'pCO2', 'BT', 'BO3', 'BO4', 'TP', 'TSi' ] for p in upar: if ps[p] is not None: ps[p] = np.divide(ps[p], ps.unit) # convert to molar # reassign unit, so conversions aren't repeated by Csys orig_unit = ps.unit ps.unit = 1. # determin max lengths kexcl = ['Ks', 'pdict', 'unit'] ks = [k for k in ps.keys() if k not in kexcl] L = maxL(*[ps[k] for k in ks]) # make inputs same length for k in ks: if ps[k] is not None: if isinstance(ps[k], (int, float)): ps[k] = np.full(L, ps[k]) # Conserved seawater chemistry if 'TS' not in ps: ps.TS = calc_TS(ps.S) if 'TF' not in ps: ps.TF = calc_TF(ps.S) # Calculate Ks ps.Ks = get_Ks(ps) # Calculate pH scales (does nothing if none pH given) ps.update( calc_pH_scales(ps.pHtot, ps.pHfree, ps.pHsws, ps.TS, ps.TF, ps.Ks)) # if fCO2 is given but CO2 is not, calculate CO2 if ps.CO2 is None: if ps.fCO2 is not None: ps.CO2 = fCO2_to_CO2(ps.fCO2, ps.Ks) elif ps.pCO2 is not None: ps.CO2 = fCO2_to_CO2(pCO2_to_fCO2(ps.pCO2, ps.T), ps.Ks) # if no B info provided, assume modern conc. nBspec = NnotNone(ps.BT, ps.BO3, ps.BO4) if nBspec == 0: ps.BT = calc_TB(ps.S) elif isinstance(BT, (int, float)): ps.BT = ps.BT * ps.S / 35. # This section works out the order that things should be calculated in. # Special case: if pH is missing, must have: # a) two C # b) two B # c) one pH-dependent B, one pH-dependent C... But that's cray... # (c not implemented!) if ps.pHtot is None: nCspec = NnotNone(ps.DIC, ps.CO2, ps.HCO3, ps.CO3) # a) if there are 2 C species, or one C species and TA and BT if ((nCspec == 2) | ((nCspec == 1) & (NnotNone(ps.TA, ps.BT) == 2))): ps.update(Csys(pdict=ps)) # calculate C first ps.update(Bsys(pdict=ps)) # then B # Note on the peculiar syntax here: # ps is a dict of parameters, where # everything that needs to be calculated # is None. # We give this to the [N]sys function # as pdict, which passes the paramters from THIS # function to [N]sys. # As the output of Csys is also a dict (Bunch) # with exactly the same form as ps, we can then # use the .update attribute of ps to update # all the paramters that were calculated by # Csyst. # Thus, all calculation is incremental, working # with the same parameter set. As dicts are # mutable, this has the added benefit of the # parameters only being stored in memory once. if ps.TA is None: (ps.TA, ps.CAlk, ps.BAlk, ps.PAlk, ps.SiAlk, ps.OH, ps.Hfree, ps.HSO4, ps.HF) = cTA(H=ps.H, DIC=ps.DIC, BT=ps.BT, TP=ps.TP, TSi=ps.TSi, TS=ps.TS, TF=ps.TF, Ks=ps.Ks, mode='multi') # necessary becayse TA in Csys fails if there's no BT # b) if there are 2 B species elif nBspec == 2: ps.update(Bsys(pdict=ps)) # calculate B first ps.update(Csys(pdict=ps)) # then C else: # if neither condition is met, throw an error raise ValueError(( "Impossible! You haven't provided enough parameters.\n" + "If you don't know pH, you must provide either:\n" + " - Two of [DIC, CO2, HCO3, CO3], and one of [BT, BO3, BO4]\n" + " - One of [DIC, CO2, HCO3, CO3], and TA and BT\n" + " - Two of [BT, BO3, BO4] and one of [DIC, CO2, HCO3, CO3]")) else: # if we DO have pH, it's dead easy! ps.update(Bsys(pdict=ps)) # calculate B first ps.update(Csys(pdict=ps)) # then C for p in upar + [ 'CAlk', 'BAlk', 'PAlk', 'SiAlk', 'OH', 'HSO4', 'HF', 'Hfree' ]: ps[p] *= orig_unit # convert back to input units # remove some superfluous outputs rem = ['pdict', 'unit'] for r in rem: if r in ps: del ps[r] return ps