AirSeaFluxCode.py 34.9 KB
Newer Older
sbiri's avatar
sbiri committed
1
import warnings
sbiri's avatar
sbiri committed
2
import numpy as np
3
import pandas as pd
sbiri's avatar
sbiri committed
4
import logging
5
from hum_subs import (get_hum, gamma)
sbiri's avatar
sbiri committed
6 7 8
from util_subs import *
from flux_subs import *
from cs_wl_subs import *
sbiri's avatar
sbiri committed
9 10


sbiri's avatar
sbiri committed
11 12
class S88:
    def _wind_iterate(self, ind):
13
        if self.gust[0] in [1, 2, 3]:
sbiri's avatar
sbiri committed
14
            self.wind[ind] = np.sqrt(np.power(np.copy(self.spd[ind]), 2) +
sbiri's avatar
sbiri committed
15 16 17
                                     np.power(get_gust(self.gust[1],
                                                       self.theta[ind],
                                                       self.usr[ind],
18 19
                                                       self.tsrv[ind],
                                                       self.gust[2],
20
                                                       self.grav[ind]), 2))
21
            # ratio of gusty to horizontal wind
sbiri's avatar
sbiri committed
22
            self.GustFact[ind] = self.wind[ind]/self.spd[ind]
sbiri's avatar
sbiri committed
23 24 25
            # if we update wind, also need to update u10n
            # remove effect of gustiness following Fairall et al. (2003)
            # usr is divided by (GustFact)^0.5
26
            self.u10n[ind] = self.spd[ind]-self.usr[ind]/kappa/np.sqrt(
27
                self.GustFact[ind])*(np.log(self.h_in[0, ind]/self.ref10) -
28
                                     self.psim[ind])
29 30 31
            if self.gust[0] == 2:
                self.GustFact[ind] = 1
                # option to not remove GustFact
32
                self.u10n[ind] = self.spd[ind]-self.usr[ind]/kappa*(
33
                    np.log(self.h_in[0, ind]/self.ref10)-self.psim[ind])
sbiri's avatar
sbiri committed
34 35
        else:
            self.wind[ind] = np.copy(self.spd[ind])
sbiri's avatar
sbiri committed
36
            self.u10n[ind] = self.wind[ind]-self.usr[ind]/kappa*(
37
                np.log(self.h_in[0, ind]/self.ref10)-self.psim[ind])
sbiri's avatar
sbiri committed
38

sbiri's avatar
sbiri committed
39 40 41 42
    def get_heights(self, hin, hout=10):
        self.hout = hout
        self.hin = hin
        self.h_in = get_heights(hin, len(self.spd))
sbiri's avatar
sbiri committed
43
        self.h_out = get_heights(self.hout, 1)
sbiri's avatar
sbiri committed
44

sbiri's avatar
sbiri committed
45 46 47
    def get_specHumidity(self, qmeth="Buck2"):
        self.qair, self.qsea = get_hum(self.hum, self.T, self.SST, self.P,
                                       qmeth)
sbiri's avatar
sbiri committed
48 49
        if (np.all(np.isnan(self.qsea)) or np.all(np.isnan(self.qair))):
            raise ValueError("qsea and qair cannot be nan")
sbiri's avatar
sbiri committed
50 51 52
        self.dq_in = self.qair-self.qsea
        self.dq_full = self.qair-self.qsea

sbiri's avatar
sbiri committed
53 54 55 56
        # Set lapse rate and Potential Temperature (now we have humdity)
        self.cp = 1004.67*(1+0.00084*self.qsea)
        self.tlapse = gamma("dry", self.SST, self.T, self.qair/1000, self.cp)
        self.theta = np.copy(self.T)+self.tlapse*self.h_in[1]
sbiri's avatar
sbiri committed
57 58
        self.dt_in = self.theta-self.SST
        self.dt_full = self.theta-self.SST
sbiri's avatar
sbiri committed
59 60

    def _fix_coolskin_warmlayer(self, wl, cskin, skin, Rl, Rs):
sbiri's avatar
sbiri committed
61
        skin = self.skin if skin is None else skin
sbiri's avatar
sbiri committed
62 63
        assert wl in [0, 1], "wl not valid"
        assert cskin in [0, 1], "cskin not valid"
sbiri's avatar
sbiri committed
64 65
        assert skin in ["C35", "ecmwf", "Beljaars"], "Skin value not valid"

sbiri's avatar
sbiri committed
66 67
        if ((cskin == 1 or wl == 1) and
            (np.all(Rl == None) or np.all(np.isnan(Rl))) and
68
                ((np.all(Rs == None) or np.all(np.isnan(Rs))))):
sbiri's avatar
sbiri committed
69 70 71 72 73
            print("Cool skin/warm layer is switched ON; "
                  "Radiation input should not be empty")
            raise

        self.wl = wl
sbiri's avatar
sbiri committed
74 75
        self.cskin = cskin
        self.skin = skin
sbiri's avatar
sbiri committed
76 77
        self.Rs = np.full(self.spd.shape, np.nan) if Rs is None else Rs
        self.Rl = np.full(self.spd.shape, np.nan) if Rl is None else Rl
sbiri's avatar
sbiri committed
78

sbiri's avatar
sbiri committed
79
    def set_coolskin_warmlayer(self, wl=0, cskin=0, skin=None, Rl=None,
sbiri's avatar
sbiri committed
80
                               Rs=None):
sbiri's avatar
sbiri committed
81
        wl = 0 if wl is None else wl
sbiri's avatar
sbiri committed
82 83
        if hasattr(self, "skin") == False:
            self.skin = "C35"
sbiri's avatar
sbiri committed
84 85
        self._fix_coolskin_warmlayer(wl, cskin, skin, Rl, Rs)

sbiri's avatar
sbiri committed
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    def _update_coolskin_warmlayer(self, ind):
        if self.cskin == 1:
            if self.skin == "C35":
                self.dter[ind], self.tkt[ind] = cs_C35(np.copy(
                    self.SST[ind]), self.rho[ind], self.Rs[ind], self.Rnl[ind],
                    self.cp[ind], self.lv[ind], np.copy(self.tkt[ind]),
                    self.usr[ind], self.tsr[ind], self.qsr[ind], self.grav[ind])
            elif self.skin == "ecmwf":
                self.dter[ind] = cs_ecmwf(
                    self.rho[ind], self.Rs[ind], self.Rnl[ind], self.cp[ind],
                    self.lv[ind], self.usr[ind], self.tsr[ind], self.qsr[ind],
                    np.copy(self.SST[ind]), self.grav[ind])
            elif self.skin == "Beljaars":
                self.Qs[ind], self.dter[ind] = cs_Beljaars(
                    self.rho[ind], self.Rs[ind], self.Rnl[ind], self.cp[ind],
                    self.lv[ind], self.usr[ind], self.tsr[ind], self.qsr[ind],
                    self.grav[ind], np.copy(self.Qs[ind]))
            self.dqer[ind] = get_dqer(self.dter[ind], self.SST[ind],
                                      self.qsea[ind], self.lv[ind])
sbiri's avatar
sbiri committed
105 106
            self.skt[ind] = np.copy(self.SST[ind])+self.dter[ind]
            self.skq[ind] = np.copy(self.qsea[ind])+self.dqer[ind]
sbiri's avatar
sbiri committed
107 108 109 110 111 112 113 114 115 116
            if self.wl == 1:
                self.dtwl[ind] = wl_ecmwf(
                    self.rho[ind], self.Rs[ind], self.Rnl[ind], self.cp[ind],
                    self.lv[ind], self.usr[ind], self.tsr[ind], self.qsr[ind],
                    np.copy(self.SST[ind]), np.copy(self.skt[ind]),
                    np.copy(self.dter[ind]), self.grav[ind])
                self.skt[ind] = (np.copy(self.SST[ind])+self.dter[ind] +
                                 self.dtwl[ind])
                self.dqer[ind] = get_dqer(self.dter[ind], self.skt[ind],
                                          self.qsea[ind], self.lv[ind])
sbiri's avatar
sbiri committed
117 118 119 120 121 122 123 124
                self.skq[ind] = np.copy(self.qsea[ind])+self.dqer[ind]
        else:
            self.dter[ind] = np.zeros(self.SST[ind].shape)
            self.dqer[ind] = np.zeros(self.SST[ind].shape)
            self.dtwl[ind] = np.zeros(self.SST[ind].shape)
            self.tkt[ind] = np.zeros(self.SST[ind].shape)

    def _first_guess(self):
125
        # reference height
sbiri's avatar
sbiri committed
126 127 128 129 130 131 132 133 134 135 136 137
        self.ref10 = 10

        #  first guesses
        self.t10n, self.q10n = np.copy(self.theta), np.copy(self.qair)
        self.rho = self.P*100/(287.1*self.t10n*(1+0.6077*self.q10n))
        self.lv = (2.501-0.00237*(self.SST-CtoK))*1e6  # J/kg

        #  Zeng et al. 1998
        self.tv = self.theta*(1+0.6077*self.qair)   # virtual potential T
        self.dtv = self.dt_in*(1+0.6077*self.qair)+0.6077*self.theta*self.dq_in

        # Set the wind array
sbiri's avatar
sbiri committed
138
        self.wind = np.sqrt(np.power(np.copy(self.spd), 2)+0.25)
sbiri's avatar
sbiri committed
139 140 141 142
        self.GustFact = self.wind*0+1

        # Rb eq. 11 Grachev & Fairall 1997, use air temp height
        # use self.tv??  adjust wind to T-height?
sbiri's avatar
sbiri committed
143
        Rb = self.grav*self.h_in[1]*self.dtv/(self.T*np.power(self.wind, 2))
144 145
        # eq. 12 Grachev & Fairall 1997   # DO.THIS
        self.monob = self.h_in[1]/12.0/Rb
sbiri's avatar
sbiri committed
146
        # where does 12.0 come from??
sbiri's avatar
sbiri committed
147 148 149

        # ------------

150 151
        dummy_array = lambda val : np.full(self.T.shape, val)*self.msk
        # def dummy_array(val): return np.full(self.T.shape, val)*self.msk
sbiri's avatar
sbiri committed
152
        if self.cskin + self.wl > 0:
sbiri's avatar
sbiri committed
153 154
            self.dter, self.tkt, self.dtwl = [
                dummy_array(x) for x in (-0.3, 0.001, 0.3)]
155 156
            self.dqer = get_dqer(self.dter, self.SST, self.qsea,
                                 self.lv)
sbiri's avatar
sbiri committed
157 158
            self.Rnl = 0.97*(self.Rl-5.67e-8*np.power(
                self.SST-0.3*self.cskin, 4))
sbiri's avatar
sbiri committed
159 160
            self.Qs = 0.945*self.Rs
        else:
sbiri's avatar
sbiri committed
161 162 163 164
            self.dter, self.dqer, self.dtwl = [
                dummy_array(x) for x in (0.0, 0.0, 0.0)]
            self.Rnl, self.Qs, self.tkt = [
                np.empty(self.arr_shp)*self.msk for _ in range(3)]
sbiri's avatar
sbiri committed
165 166 167 168 169
        self.skt = np.copy(self.SST)
        self.skq = np.copy(self.qsea)

        self.u10n = np.copy(self.wind)
        self.usr = 0.035*self.u10n
sbiri's avatar
sbiri committed
170 171
        self.cd10n, self.zo = cdn_calc(
            self.u10n, self.usr, self.theta, self.grav, self.meth)
sbiri's avatar
sbiri committed
172 173
        self.psim = psim_calc(self.h_in[0]/self.monob, self.meth)
        self.cd = cd_calc(self.cd10n, self.h_in[0], self.ref10, self.psim)
174
        self.usr =np.sqrt(self.cd*np.power(self.wind, 2))
sbiri's avatar
sbiri committed
175 176 177 178
        self.zot, self.zoq, self.tsr, self.qsr = [
            np.empty(self.arr_shp)*self.msk for _ in range(4)]
        self.ct10n, self.cq10n, self.ct, self.cq = [
            np.empty(self.arr_shp)*self.msk for _ in range(4)]
179
        self.tv10n = self.zot
sbiri's avatar
sbiri committed
180 181 182 183 184 185

    def iterate(self, maxiter=10, tol=None):
        if maxiter < 5:
            warnings.warn("Iteration number <5 - resetting to 5.")
            maxiter = 5

sbiri's avatar
sbiri committed
186 187 188 189
        # Decide which variables to use in tolerances based on tolerance
        # specification
        tol = ['all', 0.01, 0.01, 1e-05, 1e-3,
               0.1, 0.1] if tol is None else tol
sbiri's avatar
sbiri committed
190 191
        assert tol[0] in ['flux', 'ref', 'all'], "unknown tolerance input"

sbiri's avatar
sbiri committed
192 193
        old_vars = {"flux": ["tau", "sensible", "latent"],
                    "ref": ["u10n", "t10n", "q10n"]}
sbiri's avatar
sbiri committed
194 195 196
        old_vars["all"] = old_vars["ref"] + old_vars["flux"]
        old_vars = old_vars[tol[0]]

sbiri's avatar
sbiri committed
197 198
        new_vars = {"flux": ["tau", "sensible", "latent"],
                    "ref": ["u10n", "t10n", "q10n"]}
sbiri's avatar
sbiri committed
199 200 201 202 203 204 205 206 207 208 209
        new_vars["all"] = new_vars["ref"] + new_vars["flux"]
        new_vars = new_vars[tol[0]]
        # I'm sure there are better ways of doing this
        # extract tolerance values by deleting flag from tol
        tvals = np.delete(np.copy(tol), 0)
        tol_vals = list([float(tt) for tt in tvals])

        ind = np.where(self.spd > 0)
        it = 0

        # Setup empty arrays
sbiri's avatar
sbiri committed
210 211
        self.tsrv, self.psim, self.psit, self.psiq = [
            np.zeros(self.arr_shp)*self.msk for _ in range(4)]
sbiri's avatar
sbiri committed
212 213

        # extreme values for first comparison
214
        dummy_array = lambda val : np.full(self.T.shape, val)*self.msk
215
        # you can use def instead of lambda
sbiri's avatar
sbiri committed
216 217 218
        # def dummy_array(val): return np.full(self.arr_shp, val)*self.msk
        self.itera, self.tau, self.sensible, self.latent = [
            dummy_array(x) for x in (-1, 1e+99, 1e+99, 1e+99)]
sbiri's avatar
sbiri committed
219 220 221 222 223 224 225 226 227 228

        # Generate the first guess values
        self._first_guess()

        #  iteration loop
        ii = True
        while ii & (it < maxiter):
            it += 1

            # Set the old variables (for comparison against "new")
sbiri's avatar
sbiri committed
229
            old = np.array([np.copy(getattr(self, i)) for i in old_vars])
sbiri's avatar
sbiri committed
230 231

            # Calculate cdn
sbiri's avatar
sbiri committed
232 233 234 235 236
            self.cd10n[ind], self.zo[ind] = cdn_calc(
                self.u10n[ind], self.usr[ind], self.theta[ind], self.grav[ind],
                self.meth)

            if np.all(np.isnan(self.cd10n)):
sbiri's avatar
sbiri committed
237
                logging.info('break %s at iteration %s cd10n<0', meth, it)
sbiri's avatar
sbiri committed
238 239 240 241 242 243
                break

            self.psim[ind] = psim_calc(
                self.h_in[0, ind]/self.monob[ind], self.meth)
            self.cd[ind] = cd_calc(
                self.cd10n[ind], self.h_in[0, ind], self.ref10, self.psim[ind])
244 245
            # Update the wind values
            self._wind_iterate(ind)
sbiri's avatar
sbiri committed
246 247

            # temperature
sbiri's avatar
sbiri committed
248 249 250 251 252 253 254 255
            self.ct10n[ind], self.zot[ind] = ctqn_calc(
                "ct", self.h_in[1, ind]/self.monob[ind], self.cd10n[ind],
                self.usr[ind], self.zo[ind], self.theta[ind], self.meth)
            self.psit[ind] = psit_calc(
                self.h_in[1, ind]/self.monob[ind], self.meth)
            self.ct[ind] = ctq_calc(
                self.cd10n[ind], self.cd[ind], self.ct10n[ind],
                self.h_in[1, ind], self.ref10, self.psit[ind])
256
            # humidity
sbiri's avatar
sbiri committed
257 258 259 260 261 262 263 264
            self.cq10n[ind], self.zoq[ind] = ctqn_calc(
                "cq", self.h_in[2, ind]/self.monob[ind], self.cd10n[ind],
                self.usr[ind], self.zo[ind], self.theta[ind], self.meth)
            self.psiq[ind] = psit_calc(
                self.h_in[2, ind]/self.monob[ind], self.meth)
            self.cq[ind] = ctq_calc(
                self.cd10n[ind], self.cd[ind], self.cq10n[ind],
                self.h_in[2, ind], self.ref10, self.psiq[ind])
265

sbiri's avatar
sbiri committed
266 267 268 269 270
            # Some parameterizations set a minimum on parameters
            try:
                self._minimum_params()
            except AttributeError:
                pass
sbiri's avatar
sbiri committed
271 272 273

            self.dt_full[ind] = self.dt_in[ind] - \
                self.dter[ind]*self.cskin - self.dtwl[ind]*self.wl
sbiri's avatar
sbiri committed
274
            self.dq_full[ind] = self.dq_in[ind] - self.dqer[ind]*self.cskin
sbiri's avatar
sbiri committed
275 276 277 278 279
            self.usr[ind], self.tsr[ind], self.qsr[ind] = get_strs(
                self.h_in[:, ind], self.monob[ind], self.wind[ind],
                self.zo[ind], self.zot[ind], self.zoq[ind], self.dt_full[ind],
                self.dq_full[ind], self.cd[ind], self.ct[ind], self.cq[ind],
                self.meth)
sbiri's avatar
sbiri committed
280 281 282

            # Update CS/WL parameters
            self._update_coolskin_warmlayer(ind)
sbiri's avatar
sbiri committed
283

sbiri's avatar
sbiri committed
284
            # Logging output
sbiri's avatar
sbiri committed
285 286 287 288 289 290 291 292
            log_vars = {"dter": 2, "dqer": 7, "tkt": 2,
                        "Rnl": 2, "usr": 3, "tsr": 4, "qsr": 7}
            log_vars = [np.round(np.nanmedian(getattr(self, V)), R)
                        for V, R in log_vars.items()]
            log_vars.insert(0, self.meth)
            logging.info(
                'method {} | dter = {} | dqer = {} | tkt = {} | Rnl = {} |'
                ' usr = {} | tsr = {} | qsr = {}'.format(*log_vars))
sbiri's avatar
sbiri committed
293 294

            if self.cskin + self.wl > 0:
sbiri's avatar
sbiri committed
295 296 297
                self.Rnl[ind] = 0.97*(self.Rl[ind]-5.67e-8 *
                                      np.power(self.SST[ind] +
                                               self.dter[ind]*self.cskin, 4))
sbiri's avatar
sbiri committed
298 299
            # not sure how to handle lapse/potemp
            # well-mixed in potential temperature ...
300
            self.t10n[ind] = self.theta[ind]-self.tlapse[ind]*self.ref10 - \
sbiri's avatar
sbiri committed
301
                self.tsr[ind]/kappa * \
302
                (np.log(self.h_in[1, ind]/self.ref10)-self.psit[ind])
303
            self.q10n[ind] = self.qair[ind]-self.qsr[ind]/kappa * \
sbiri's avatar
sbiri committed
304
                (np.log(self.h_in[2, ind]/self.ref10)-self.psiq[ind])
sbiri's avatar
sbiri committed
305 306

            # update stability info
sbiri's avatar
sbiri committed
307 308 309 310 311 312
            self.tsrv[ind] = get_tsrv(
                self.tsr[ind], self.qsr[ind], self.theta[ind], self.qair[ind])
            self.Rb[ind] = get_Rb(
                self.grav[ind], self.usr[ind], self.h_in[0, ind],
                self.h_in[1, ind], self.tv[ind], self.dtv[ind], self.wind[ind],
                self.monob[ind], self.meth)
sbiri's avatar
sbiri committed
313
            if self.L == "tsrv":
sbiri's avatar
sbiri committed
314 315 316
                self.monob[ind] = get_Ltsrv(
                    self.tsrv[ind], self.grav[ind], self.tv[ind],
                    self.usr[ind])
sbiri's avatar
sbiri committed
317
            else:
sbiri's avatar
sbiri committed
318
                self.monob[ind] = get_LRb(
319
                    self.Rb[ind], self.h_in[1, ind], self.monob[ind],
sbiri's avatar
sbiri committed
320
                    self.zo[ind], self.zot[ind], self.meth)
sbiri's avatar
sbiri committed
321 322 323

            # Update the wind values
            self._wind_iterate(ind)
sbiri's avatar
sbiri committed
324

sbiri's avatar
sbiri committed
325
            # make sure you allow small negative values convergence
sbiri's avatar
sbiri committed
326 327
            if it == 1:
                self.u10n = np.where(self.u10n < 0, 0.5, self.u10n)
sbiri's avatar
sbiri committed
328

sbiri's avatar
sbiri committed
329
            self.itera[ind] = np.full(1, it)
sbiri's avatar
sbiri committed
330
            # remove effect of gustiness following Fairall et al. (2003)
sbiri's avatar
sbiri committed
331 332
            # usr is divided by (GustFact)^0.5 (here applied to sensible and
            # latent as well as tau)
333
            # GustFact should be 1 if gust is OFF or gust[0]=2
sbiri's avatar
sbiri committed
334
            self.tau = self.rho*np.power(self.usr, 2)/self.GustFact
335 336 337 338 339 340 341 342
            if self.gust[0] == 3:
                self.sensible = self.rho*self.cp*self.usr*self.tsr
                self.latent = self.rho*self.lv*self.usr*self.qsr
            else:
                self.sensible = self.rho*self.cp*self.usr / \
                    np.sqrt(self.GustFact)*self.tsr
                self.latent = self.rho*self.lv*self.usr / \
                    np.sqrt(self.GustFact)*self.qsr
sbiri's avatar
sbiri committed
343 344

            # Set the new variables (for comparison against "old")
sbiri's avatar
sbiri committed
345
            new = np.array([np.copy(getattr(self, i)) for i in new_vars])
sbiri's avatar
sbiri committed
346

sbiri's avatar
sbiri committed
347
            if it > 2:  # force at least three iterations
348
                d = np.abs(new-old)  # change over this iteration
sbiri's avatar
sbiri committed
349 350 351 352
                for ii in range(0, len(tol_vals)):
                    d[ii, ] = d[ii, ]/tol_vals[ii]  # ratio to tolerance
                # identifies non-convergence
                ind = np.where(d.max(axis=0) >= 1)
sbiri's avatar
sbiri committed
353 354 355 356 357 358 359 360

            self.ind = np.copy(ind)
            ii = False if (ind[0].size == 0) else True
            # End of iteration loop

        self.itera[ind] = -1
        self.itera = np.where(self.itera > maxiter, -1, self.itera)
        logging.info('method %s | # of iterations:%s', self.meth, it)
sbiri's avatar
sbiri committed
361 362
        logging.info('method %s | # of points that did not converge :%s \n',
                     self.meth, self.ind[0].size)
sbiri's avatar
sbiri committed
363 364

    def _get_humidity(self):
365
        """Calculate RH used for flagging purposes & output."""
sbiri's avatar
sbiri committed
366
        if self.hum[0] in ('rh', 'no'):
sbiri's avatar
sbiri committed
367
            self.rh = self.hum[1]
sbiri's avatar
sbiri committed
368
        elif self.hum[0] == 'Td':
sbiri's avatar
sbiri committed
369 370 371
            Td = self.hum[1]  # dew point temperature (K)
            Td = np.where(Td < 200, np.copy(Td)+CtoK, np.copy(Td))
            T = np.where(self.T < 200, np.copy(self.T)+CtoK, np.copy(self.T))
372
            # T = np.copy(self.T)
sbiri's avatar
sbiri committed
373 374 375
            esd = 611.21*np.exp(17.502*((Td-CtoK)/(Td-32.19)))
            es = 611.21*np.exp(17.502*((T-CtoK)/(T-32.19)))
            self.rh = 100*esd/es
376 377 378 379
        elif self.hum[0] == "q":
            es = 611.21*np.exp(17.502*((self.T-CtoK)/(self.T-32.19)))
            e = self.qair*self.P/(0.378*self.qair+0.622)
            self.rh = 100*e/es
sbiri's avatar
sbiri committed
380

381 382 383
    def _flag(self, out=0):
        """Set the general flags."""
        flag = np.full(self.arr_shp, "n", dtype="object")
sbiri's avatar
sbiri committed
384

sbiri's avatar
sbiri committed
385 386 387 388 389
        if self.hum[0] == 'no':
            if self.cskin == 1:
                flag = np.where(
                    np.isnan(self.spd+self.T+self.SST+self.P+self.Rs+self.Rl),
                    "m", flag)
sbiri's avatar
sbiri committed
390
            else:
sbiri's avatar
sbiri committed
391 392
                flag = np.where(
                    np.isnan(self.spd+self.T+self.SST+self.P), "m", flag)
sbiri's avatar
sbiri committed
393
        else:
sbiri's avatar
sbiri committed
394 395
            if self.cskin == 1:
                flag = np.where(np.isnan(
396 397
                    self.spd+self.T+self.SST+self.hum[1]+self.P +
                    self.Rs+self.Rl), "m", flag)
sbiri's avatar
sbiri committed
398
            else:
sbiri's avatar
sbiri committed
399 400 401
                flag = np.where(
                    np.isnan(self.spd+self.T+self.SST+self.hum[1]+self.P),
                    "m", flag)
sbiri's avatar
sbiri committed
402 403 404 405

            flag = np.where(self.rh > 100, "r", flag)

        # u10n flag
sbiri's avatar
sbiri committed
406 407 408 409 410
        flag = np.where(((self.u10n < 0) | (self.u10n > 200)) & (flag == "n"),
                        "u",
                        np.where(((self.u10n < 0) | (self.u10n > 200)) &
                                 (np.char.find(flag.astype(str), 'u') == -1),
                                 flag+[","]+["u"], flag))
sbiri's avatar
sbiri committed
411
        # q10n flag
sbiri's avatar
sbiri committed
412 413 414 415 416
        flag = np.where(
            ((self.q10n < 0) | (self.q10n > 40*0.001)) & (flag == "n"), "q",
            np.where(
                ((self.q10n < 0) | (self.q10n > 40*0.001)) & (flag != "n"),
                flag+[","]+["q"], flag))
417

sbiri's avatar
sbiri committed
418
        # t10n flag
sbiri's avatar
sbiri committed
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
        flag = np.where(
            ((self.t10n < 173) | (self.t10n > 373)) & (flag == "n"), "t",
            np.where(((self.t10n < 173) | (self.t10n > 373)) & (flag != "n"),
                     flag+[","]+["t"], flag))

        flag = np.where(
            ((self.Rb < -0.5) | (self.Rb > 0.2) |
             ((self.hin[0]/self.monob) > 1000)) & (flag == "n"), "l",
            np.where(((self.Rb < -0.5) | (self.Rb > 0.2) |
                      ((self.hin[0]/self.monob) > 1000)) &
                     (flag != "n"), flag+[","]+["l"], flag))

        if out == 1:
            flag = np.where((self.itera == -1) & (flag == "n"), "i", np.where(
                (self.itera == -1) &
                ((flag != "n") & (np.char.find(flag.astype(str), 'm') == -1)),
                flag+[","]+["i"], flag))
sbiri's avatar
sbiri committed
436
        else:
sbiri's avatar
sbiri committed
437 438 439 440 441
            flag = np.where((self.itera == -1) & (flag == "n"), "i", np.where(
                (self.itera == -1) &
                ((flag != "n") & (np.char.find(flag.astype(str), 'm') == -1) &
                 (np.char.find(flag.astype(str), 'u') == -1)),
                flag+[","]+["i"], flag))
sbiri's avatar
sbiri committed
442 443
        self.flag = flag

sbiri's avatar
sbiri committed
444 445 446
    def get_output(self, out=0):

        assert out in [0, 1], "out must be either 0 or 1"
sbiri's avatar
sbiri committed
447

448
        self._get_humidity()  # Get the Relative humidity
sbiri's avatar
sbiri committed
449 450 451 452
        self._flag(out=out)  # Get flags

        # remove effect of gustiness following Fairall et al. (2003)
        # usr is divided by (GustFact)^0.5
453
        self.uref = self.spd-self.usr/kappa/np.sqrt(self.GustFact) * \
sbiri's avatar
sbiri committed
454
            (np.log(self.h_in[0]/self.h_out[0])-self.psim +
455
             psim_calc(self.h_out[0]/self.monob, self.meth))
sbiri's avatar
sbiri committed
456
        # include lapse rate adjustment as theta is well-mixed
sbiri's avatar
sbiri committed
457 458
        self.tref = self.theta-self.tlapse*self.h_out[1]-self.tsr/kappa * \
            (np.log(self.h_in[1]/self.h_out[1])-self.psit +
459
             psit_calc(self.h_out[1]/self.monob, self.meth))
sbiri's avatar
sbiri committed
460 461
        self.qref = self.qair-self.qsr/kappa * \
            (np.log(self.h_in[2]/self.h_out[2])-self.psiq +
462
             psit_calc(self.h_out[2]/self.monob, self.meth))
sbiri's avatar
sbiri committed
463

sbiri's avatar
sbiri committed
464 465 466
        if self.wl == 0:
            self.dtwl = np.zeros(self.T.shape)*self.msk
            # reset to zero if not used
sbiri's avatar
sbiri committed
467 468

        # Do not calculate lhf if a measure of humidity is not input
sbiri's avatar
sbiri committed
469 470 471 472 473
        # This gets filled into a pd dataframe and so no need to specify y
        # dimension of array
        if self.hum[0] == 'no':
            self.latent, self.qsr, self.q10n = np.empty(3)
            self.qref, self.qair, self.rh = np.empty(3)
sbiri's avatar
sbiri committed
474 475 476 477 478

        # Set the final wind speed values
        # this seems to be gust (was wind_speed)
        self.ug = np.sqrt(np.power(self.wind, 2)-np.power(self.spd, 2))

sbiri's avatar
sbiri committed
479 480
        # Get class specific flags (will only work if self.u_hi and self.u_lo
        # have been set in the class)
sbiri's avatar
sbiri committed
481 482 483 484 485 486
        try:
            self._class_flag()
        except AttributeError:
            pass

        # Combine all output variables into a pandas array
sbiri's avatar
sbiri committed
487 488 489
        res_vars = ("tau", "sensible", "latent", "monob", "cd", "cd10n", "ct",
                    "ct10n", "cq", "cq10n", "tsrv", "tsr", "qsr", "usr",
                    "psim", "psit", "psiq", "u10n", "t10n", "q10n", "zo",
490
                    "zot", "zoq", "uref", "tref", "qref", "dter",
sbiri's avatar
sbiri committed
491
                    "dqer", "dtwl", "qair", "qsea", "Rl", "Rs", "Rnl", "ug",
492
                    "Rb", "rh", "rho", "cp", "tkt", "lv", "itera")
sbiri's avatar
sbiri committed
493 494

        res = np.zeros((len(res_vars), len(self.spd)))
sbiri's avatar
sbiri committed
495 496
        for i, value in enumerate(res_vars):
            res[i][:] = getattr(self, value)
sbiri's avatar
sbiri committed
497

sbiri's avatar
sbiri committed
498
        if out == 0:
sbiri's avatar
sbiri committed
499 500
            res[:, self.ind] = np.nan
            # set missing values where data have non acceptable values
sbiri's avatar
sbiri committed
501
            if self.hum[0] != 'no':
502 503 504
                res[:-1] = np.asarray([
                    np.where(self.q10n < 0, np.nan, res[i][:])
                    for i in range(len(res_vars)-1)])
505 506
                # len(res_vars)-1 instead of len(res_vars) in order to keep
                # itera= -1 for no convergence
507 508 509
            res[:-1] = np.asarray([
                np.where(self.u10n < 0, np.nan, res[i][:])
                for i in range(len(res_vars)-1)])
sbiri's avatar
sbiri committed
510
        else:
sbiri's avatar
sbiri committed
511 512 513 514
            warnings.warn("Warning: the output will contain values for points"
                          " that have not converged and negative values "
                          "(if any) for u10n/q10n")

515 516
        resAll = pd.DataFrame(data=res.T, index=range(self.nlen),
                              columns=res_vars)
sbiri's avatar
sbiri committed
517 518 519 520 521 522 523 524

        resAll["flag"] = self.flag

        return resAll

    def add_variables(self, spd, T, SST, lat=None, hum=None, P=None, L=None):

        # Add the mandatory variables
sbiri's avatar
sbiri committed
525 526 527 528
        assert type(spd) == type(T) == type(
            SST) == np.ndarray, "input type of spd, T and SST should be"
        " numpy.ndarray"
        self.L = "tsrv" if L is None else L
sbiri's avatar
sbiri committed
529 530 531 532
        self.arr_shp = spd.shape
        self.nlen = len(spd)
        self.spd = spd
        self.T = np.where(T < 200, np.copy(T)+CtoK, np.copy(T))
sbiri's avatar
sbiri committed
533
        self.hum = ['no', np.full(SST.shape, 80)] if hum is None else hum
sbiri's avatar
sbiri committed
534
        self.SST = np.where(SST < 200, np.copy(SST)+CtoK, np.copy(SST))
sbiri's avatar
sbiri committed
535
        self.lat = np.full(self.arr_shp, 45) if lat is None else lat
sbiri's avatar
sbiri committed
536
        self.grav = gc(self.lat)
537
        self.P = np.full(self.nlen, 1013) if P is None else P
sbiri's avatar
sbiri committed
538 539

        # mask to preserve missing values when initialising variables
sbiri's avatar
sbiri committed
540
        self.msk = np.empty(SST.shape)
sbiri's avatar
sbiri committed
541 542 543
        self.msk = np.where(np.isnan(spd+T+SST), np.nan, 1)
        self.Rb = np.empty(SST.shape)*self.msk

sbiri's avatar
sbiri committed
544 545
    def add_gust(self, gust=None):
        if np.all(gust is None):
sbiri's avatar
sbiri committed
546 547 548
            try:
                gust = self.default_gust
            except AttributeError:
sbiri's avatar
sbiri committed
549
                gust = [0, 0, 0]  # gustiness OFF
sbiri's avatar
sbiri committed
550 551
        elif ((np.size(gust) < 3) and (gust == 0)):
            gust = [0, 0, 0]
sbiri's avatar
sbiri committed
552

sbiri's avatar
sbiri committed
553
        assert np.size(gust) == 3, "gust input must be a 3x1 array"
554 555
        assert gust[0] in [
            0, 1, 2, 3], "gust at position 0 must be 0, 1, 2 or 3"
sbiri's avatar
sbiri committed
556 557 558
        self.gust = gust

    def _class_flag(self):
sbiri's avatar
sbiri committed
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
        """A flag specific to this class - only used for certain classes where
         u_lo and u_hi are defined"""
        # flag.tmp = np.where(
        #     ((self.u10n < self.u_lo[0]) | (self.u10n > self.u_hi[0])),"o", "")
        # self.flag = np.where(
        #     self.flag == "n" & flag.tmp == "o", "o", self.flag)
        # self.flag = np.where(
        #     flag.tmp == "o" & self.flag != "n" & self.flag != "o" &
        #     self.flag != "m", self.flag+[","]+["o"], self.flag)
        # flag.add = ["u", "q", "t"]
        # self.flag = np.where(
        #     flag.tmp == "o" & (np.char.find(self.flag.astype(str), 'u') == -1 |
        #                        np.char.find(self.flag.astype(str), 'q') == -1 |
        #                        np.char.find(self.flag.astype(str), 't') == -1),
        #     self.flag+[","]+["o"], self.flag))
        self.flag = np.where(((self.u10n < self.u_lo[0]) |
                              (self.u10n > self.u_hi[0])) &
                             (self.flag == "n"), "o",
                             np.where(((self.u10n < self.u_lo[1]) |
                                       (self.u10n > self.u_hi[1])) &
                                      ((self.flag != "n") & (np.char.find(
                                          self.flag.astype(str), 'u') == -1) &
                                       (np.char.find(
                                           self.flag.astype(str), 'q') == -1)),
sbiri's avatar
sbiri committed
583 584 585 586 587
                                      self.flag+[","]+["o"], self.flag))

    def __init__(self):
        self.meth = "S88"

588

sbiri's avatar
sbiri committed
589 590 591 592
class S80(S88):

    def __init__(self):
        self.meth = "S80"
sbiri's avatar
sbiri committed
593 594
        self.u_lo = [6, 6]
        self.u_hi = [22, 22]
sbiri's avatar
sbiri committed
595

sbiri's avatar
sbiri committed
596

sbiri's avatar
sbiri committed
597 598 599 600
class YT96(S88):

    def __init__(self):
        self.meth = "YT96"
601
        # no limits to u range as we use eq. 21 for cdn
sbiri's avatar
sbiri committed
602 603
        # self.u_lo = [0, 3]
        # self.u_hi = [26, 26]
sbiri's avatar
sbiri committed
604

605

sbiri's avatar
sbiri committed
606
class LP82(S88):
sbiri's avatar
sbiri committed
607

sbiri's avatar
sbiri committed
608 609
    def __init__(self):
        self.meth = "LP82"
sbiri's avatar
sbiri committed
610 611
        self.u_lo = [3, 3]
        self.u_hi = [25, 25]
sbiri's avatar
sbiri committed
612 613 614 615 616 617 618 619 620


class NCAR(S88):

    def _minimum_params(self):
        self.cd = np.maximum(np.copy(self.cd), 1e-4)
        self.ct = np.maximum(np.copy(self.ct), 1e-4)
        self.cq = np.maximum(np.copy(self.cq), 1e-4)
        self.zo = np.minimum(np.copy(self.zo), 0.0025)
621
        # self.u10n = np.maximum(np.copy(self.u10n), 0.25)
sbiri's avatar
sbiri committed
622

sbiri's avatar
sbiri committed
623 624
    def __init__(self):
        self.meth = "NCAR"
sbiri's avatar
sbiri committed
625 626
        self.u_lo = [0.5, 0.5]
        self.u_hi = [999, 999]
sbiri's avatar
sbiri committed
627

628

sbiri's avatar
sbiri committed
629
class UA(S88):
sbiri's avatar
sbiri committed
630

sbiri's avatar
sbiri committed
631 632
    def __init__(self):
        self.meth = "UA"
sbiri's avatar
sbiri committed
633 634 635
        self.default_gust = [1, 1, 1000]
        self.u_lo = [-999, -999]
        self.u_hi = [18, 18]
sbiri's avatar
sbiri committed
636 637 638


class C30(S88):
sbiri's avatar
sbiri committed
639 640
    # def set_coolskin_warmlayer(self, wl=0, cskin=1, skin="C35", Rl=None, Rs=None):
    #     self._fix_coolskin_warmlayer(wl, cskin, skin, Rl, Rs)
sbiri's avatar
sbiri committed
641 642 643

    def __init__(self):
        self.meth = "C30"
sbiri's avatar
sbiri committed
644
        self.default_gust = [1, 1.2, 600]
sbiri's avatar
sbiri committed
645
        self.skin = "C35"
sbiri's avatar
sbiri committed
646

647

sbiri's avatar
sbiri committed
648 649 650
class C35(C30):
    def __init__(self):
        self.meth = "C35"
sbiri's avatar
sbiri committed
651
        self.default_gust = [1, 1.2, 600]
sbiri's avatar
sbiri committed
652
        self.skin = "C35"
sbiri's avatar
sbiri committed
653

654

sbiri's avatar
sbiri committed
655
class ecmwf(C30):
sbiri's avatar
sbiri committed
656 657 658
    # def set_coolskin_warmlayer(self, wl=0, cskin=1, skin="ecmwf", Rl=None,
    #                            Rs=None):
    #     self._fix_coolskin_warmlayer(wl, cskin, skin, Rl, Rs)
659 660 661 662 663
    # def _minimum_params(self):
    #     self.cdn = np.maximum(np.copy(self.cdn), 0.1e-3)
    #     self.ctn = np.maximum(np.copy(self.ctn), 0.1e-3)
    #     self.cqn = np.maximum(np.copy(self.cqn), 0.1e-3)
    #     self.wind = np.maximum(np.copy(self.wind), 0.2)
sbiri's avatar
sbiri committed
664 665 666

    def __init__(self):
        self.meth = "ecmwf"
sbiri's avatar
sbiri committed
667
        self.default_gust = [1, 1, 1000]
sbiri's avatar
sbiri committed
668
        self.skin = "ecmwf"
sbiri's avatar
sbiri committed
669

670

sbiri's avatar
sbiri committed
671
class Beljaars(C30):
sbiri's avatar
sbiri committed
672 673 674
    # def set_coolskin_warmlayer(self, wl=0, cskin=1, skin="Beljaars", Rl=None,
    #                            Rs=None):
    #     self._fix_coolskin_warmlayer(wl, cskin, skin, Rl, Rs)
sbiri's avatar
sbiri committed
675

sbiri's avatar
sbiri committed
676 677
    def __init__(self):
        self.meth = "Beljaars"
sbiri's avatar
sbiri committed
678
        self.default_gust = [1, 1, 1000]
sbiri's avatar
sbiri committed
679
        self.skin = "Beljaars"
sbiri's avatar
sbiri committed
680 681


682
def AirSeaFluxCode(spd, T, SST, lat=None, hum=None, P=None, hin=18, hout=10,
sbiri's avatar
sbiri committed
683
                   Rl=None, Rs=None, cskin=0, skin=None, wl=0, gust=None,
sbiri's avatar
sbiri committed
684 685
                   meth="S88", qmeth="Buck2", tol=None, maxiter=10, out=0,
                   L=None):
686
    """
sbiri's avatar
sbiri committed
687 688 689
    Calculate turbulent surface fluxes using different parameterizations.

    Calculate height adjusted values for spd, T, q
sbiri's avatar
sbiri committed
690 691 692 693 694 695 696 697 698 699 700

    Parameters
    ----------
        spd : float
            relative wind speed in m/s (is assumed as magnitude difference
            between wind and surface current vectors)
        T : float
            air temperature in K (will convert if < 200)
        SST : float
            sea surface temperature in K (will convert if < 200)
        lat : float
701 702 703
            latitude (deg), default 45deg
        hum : float
            humidity input switch 2x1 [x, values] default is relative humidity
704 705 706
            x='rh' : relative humidity in %
            x='q' : specific humidity (g/kg)
            x='Td' : dew point temperature (K)
sbiri's avatar
sbiri committed
707
        P : float
708
            air pressure (hPa), default 1013hPa
sbiri's avatar
sbiri committed
709
        hin : float
710
            sensor heights in m (array 3x1 or 3xn), default 18m
sbiri's avatar
sbiri committed
711 712 713 714 715 716
        hout : float
            output height, default is 10m
        Rl : float
            downward longwave radiation (W/m^2)
        Rs : float
            downward shortwave radiation (W/m^2)
sbiri's avatar
sbiri committed
717
        cskin : int
718
            0 switch cool skin adjustment off, else 1
719
            default is 0
720 721 722 723
        skin : str
            cool skin method option "C35", "ecmwf" or "Beljaars"
        wl : int
            warm layer correction default is 0, to switch on set to 1
724
        gust : int
725
            3x1 [x, beta, zi] x=0 gustiness is OFF, x=1 gustiness is ON and
726 727
            use gustiness factor, x=2 gustiness is ON and gustiness factor=1,
            x=3 gustiness is ON and gustiness factor=1 for the heat fluxes;
728
            beta gustiness parameter, beta=1 for UA, beta=1.2 for COARE
729
            zi PBL height (m) 600 for COARE, 1000 for UA and ecmwf
730
            default is switched OFF
731
        meth : str
sbiri's avatar
sbiri committed
732
            "S80", "S88", "LP82", "YT96", "UA", "NCAR", "C30", "C35",
733
            "ecmwf", "Beljaars"
734 735
        qmeth : str
            is the saturation evaporation method to use amongst
736 737
            "HylandWexler","Hardy","Preining","Wexler","GoffGratch","WMO",
            "MagnusTetens","Buck","Buck2","WMO2018","Sonntag","Bolton",
738 739 740 741 742 743 744
            "IAPWS","MurphyKoop"]
            default is Buck2
        tol : float
           4x1 or 7x1 [option, lim1-3 or lim1-6]
           option : 'flux' to set tolerance limits for fluxes only lim1-3
           option : 'ref' to set tolerance limits for height adjustment lim-1-3
           option : 'all' to set tolerance limits for both fluxes and height
sbiri's avatar
sbiri committed
745 746
                    adjustment lim1-6
           default is tol=['all', 0.01, 0.01, 1e-05, 1e-3, 0.1, 0.1]
sbiri's avatar
sbiri committed
747 748
        maxiter : int
            number of iterations (default = 10)
749
        out : int
750 751
            set 0 to set points that have not converged, negative values of
                  u10n and q10n to missing (default)
752
            set 1 to keep points
753
        L : str
sbiri's avatar
sbiri committed
754
           Monin-Obukhov length definition options
755 756 757
           "tsrv"  : default
           "Rb" : following ecmwf (IFS Documentation cy46r1)

sbiri's avatar
sbiri committed
758 759 760
    Returns
    -------
        res : array that contains
sbiri's avatar
sbiri committed
761 762 763 764
                       1. momentum flux       (N/m^2)
                       2. sensible heat       (W/m^2)
                       3. latent heat         (W/m^2)
                       4. Monin-Obhukov length (m)
sbiri's avatar
sbiri committed
765
                       5. drag coefficient (cd)
sbiri's avatar
sbiri committed
766
                       6. neutral drag coefficient (cd10n)
sbiri's avatar
sbiri committed
767
                       7. heat exchange coefficient (ct)
sbiri's avatar
sbiri committed
768
                       8. neutral heat exchange coefficient (ct10n)
sbiri's avatar
sbiri committed
769
                       9. moisture exhange coefficient (cq)
sbiri's avatar
sbiri committed
770
                       10. neutral moisture exchange coefficient (cq10n)
sbiri's avatar
sbiri committed
771
                       11. star virtual temperatcure (tsrv)
sbiri's avatar
sbiri committed
772
                       12. star temperature (tsr)
773 774
                       13. star specific humidity (qsr)
                       14. star wind speed (usr)
sbiri's avatar
sbiri committed
775
                       15. momentum stability function (psim)
776 777
                       16. heat stability function (psit)
                       17. moisture stability function (psiq)
778
                       18. 10m neutral wind speed (u10n)
779
                       19. 10m neutral temperature (t10n)
sbiri's avatar
sbiri committed
780 781 782 783 784 785 786
                       20. 10m neutral specific humidity (q10n)
                       21. surface roughness length (zo)
                       22. heat roughness length (zot)
                       23. moisture roughness length (zoq)
                       24. wind speed at reference height (uref)
                       25. temperature at reference height (tref)
                       26. specific humidity at reference height (qref)
787 788 789 790 791 792 793 794 795 796 797
                       27. cool-skin temperature depression (dter)
                       28. cool-skin humidity depression (dqer)
                       29. warm layer correction (dtwl)
                       30. specific humidity of air (qair)
                       31. specific humidity at sea surface (qsea)
                       32. downward longwave radiation (Rl)
                       33. downward shortwave radiation (Rs)
                       34. downward net longwave radiation (Rnl)
                       35. gust wind speed (ug)
                       36. Bulk Richardson number (Rib)
                       37. relative humidity (rh)
798 799 800 801 802 803
                       38. air density (rho)
                       39  specific heat of moist air (cp)
                       40. thickness of the viscous layer (delta)
                       41. lv latent heat of vaporization (Jkg−1)
                       42. number of iterations until convergence
                       43. flag ("n": normal, "o": out of nominal range,
804
                                 "u": u10n<0, "q":q10n<0
805
                                 "m": missing,
806
                                 "l": Rib<-0.5 or Rib>0.2 or z/L>1000,
sbiri's avatar
sbiri committed
807
                                 "r" : rh>100%,
808
                                 "t" : t10n<173K or t10n>373K
809
                                 "i": convergence fail at n)
810

811
    2021 / Author S. Biri
812 813
    2021 / Restructured by R. Cornes
    2021 / Simplified by E. Kent
sbiri's avatar
sbiri committed
814
    """
sbiri's avatar
sbiri committed
815
    logging.basicConfig(filename='flux_calc.log', filemode="w",
816
                        format='%(asctime)s %(message)s', level=logging.INFO)
817
    logging.captureWarnings(True)
sbiri's avatar
sbiri committed
818 819 820 821 822 823

    iclass = globals()[meth]()
    iclass.add_gust(gust=gust)
    iclass.add_variables(spd, T, SST, lat=lat, hum=hum, P=P, L=L)
    iclass.get_heights(hin, hout)
    iclass.get_specHumidity(qmeth=qmeth)
sbiri's avatar
sbiri committed
824 825
    iclass.set_coolskin_warmlayer(wl=wl, cskin=cskin, skin=skin, Rl=Rl, Rs=Rs)
    iclass.iterate(tol=tol, maxiter=maxiter)
sbiri's avatar
sbiri committed
826 827
    resAll = iclass.get_output(out=out)

828
    return resAll