Pleaf_pd = p.Ps_pd - p.height * cst.rho * cst.g0 * conv.MEGA
    P, E = hydraulics(p, res=res, kmax=p.kmaxS1)

    # iter on the solution until it is stable enough
    iter = 0

    while True:

        if case == 1:  # assuming colimitation
            Cicol = calc_colim_Ci(p, Cs, Tleaf, photo)
            dCi = Cs - Cicol  # Pa

            # calculate dA, μmol m-2 s-1
            As, __, __ = calc_photosynthesis(p,
                                             0.,
                                             Cs,
                                             photo,
                                             Tleaf=Tleaf,
                                             gsc=0.)
            Acol, __, __ = calc_photosynthesis(p,
                                               0.,
                                               Cicol,
                                               photo,
                                               Tleaf=Tleaf,
                                               gsc=0.)
            dA = As - Acol  # ambient - colimitation

            # dAdCi (in mol H2O) is needed to calculate gs, mmol m-2 s-1
            dAdCi = dA * conv.GwvGc * p.Patm / dCi

            # kcost, unitless
            cost_pd = kcost(p, Pleaf_pd, Pleaf_pd)
def profit_psi(p, photo='Farquhar', res='low', inf_gb=False, deriv=False):
    """
    Finds the instateneous profit maximization, following the
    optmization criterion for which, at each instant in time, the
    stomata regulate canopy gas exchange and pressure to achieve the
    maximum profit, which is the maximum difference between the
    normalized photosynthetic gain (gain) and the hydraulic cost
    function (cost). That is when d(gain)/dP = d(cost)/dP.

    Arguments:
    ----------
    p: recarray object or pandas series or class containing the data
        time step's met data & params

    photo: string
        either the Farquhar model for photosynthesis, or the Collatz
        model

    res: string
        either 'low' (default), 'med', or 'high' to run the optimising
        solver

    onopt: boolean
        if True, the optimisation is performed. If Fall, fall back on
        previously performed optimisation for the value of the maximum
        profit.

    inf_gb: bool
        if True, gb is prescrived and very large

    Returns:
    --------
    E_can: float
        transpiration [mmol m-2 s-1] at maximum profit across leaves

    gs_can: float
        stomatal conductance [mol m-2 s-1] at maximum profit across
        leaves

    An_can: float
        net photosynthetic assimilation rate [umol m-2 s-1] at maximum
        profit across leaves

    Ci_can: float
        intercellular CO2 concentration [Pa] at maximum profit across
        leaves

    rublim_can: string
        'True' if the C assimilation is rubisco limited, 'False'
        otherwise

    """

    # hydraulics
    P, trans = hydraulics(p, res=res)
    cost, __ = hydraulic_cost(p, P)

    # look for the most net profit
    gain, Ci, mask = photo_gain(p, trans, photo, res, inf_gb=inf_gb)
    expr = gain - cost[mask]

    if deriv:
        expr = np.abs(np.gradient(expr, P[mask]))

    # deal with edge cases by rebounding the solution
    gc, gs, gb, __ = leaf_energy_balance(p, trans[mask], inf_gb=inf_gb)

    try:
        if inf_gb:  # check on valid range
            check = expr[gc > cst.zero]

        else:  # further constrain the realm of possible gs
            check = expr[np.logical_and(gc > cst.zero, gs < 1.5 * gb)]

        idx = np.isclose(expr, max(check))

        if deriv:
            idx = np.isclose(expr, min(check))

        idx = [list(idx).index(e) for e in idx if e]

        if inf_gb:  # check for algo. "overshooting" due to inf. gb
            while Ci[idx[0]] < 2. * p.gamstar25:

                idx[0] -= 1

                if idx[0] < 3:
                    break

        # optimized where Ci for both photo models are close
        Ci = Ci[idx[0]]
        trans = trans[mask][idx[0]]  # mol.m-2.s-1
        gs = gs[idx[0]]
        Pleaf = P[mask][idx[0]]

        # rubisco- or electron transport-limitation?
        An, Aj, Ac = calc_photosynthesis(p, trans, Ci, photo, inf_gb=inf_gb)
        rublim = rubisco_limit(Aj, Ac)

        # leaf temperature?
        Tleaf, __ = leaf_temperature(p, trans, inf_gb=inf_gb)

        if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and
            (An > 0.)) or (idx[0] == len(P) - 1) or any(
                np.isnan([An, Ci, trans, gs, Tleaf, Pleaf])):
            An, Ci, trans, gs, gb, Tleaf, Pleaf = (9999., ) * 7

        elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero):
            trans *= conv.MILI  # mmol.m-2.s-1

        return An, Ci, rublim, trans, gs, gb, Tleaf, Pleaf

    except ValueError:  # no opt

        return (9999., ) * 8
예제 #3
0
def gas_exchange(p, fw, photo='Farquhar', res='low', dynamic=True, inf_gb=False,
                 iter_max=40, threshold_conv=0.1):

    # initial state
    Cs = p.CO2  # Pa
    Tleaf = p.Tair  # deg C

    # hydraulics
    P, E = hydraulics(p, res=res, kmax=p.kmaxT)

    # initialise gs over A
    g0 = 1.e-9  # g0 ~ 0, removing it entirely introduces errors
    Cs_umol_mol = Cs * conv.MILI / p.Patm  # umol mol-1
    gsoA = g0 + p.g1T * fw / Cs_umol_mol

    # iter on the solution until it is stable enough
    iter = 0

    while True:

        An, Aj, Ac, Ci = calc_photosynthesis(p, 0., Cs, photo, Tleaf=Tleaf,
                                             gs_over_A=gsoA)

        # stomatal conductance, with fwsoil effect
        gs = np.maximum(cst.zero, conv.GwvGc * gsoA * An)

        # calculate new trans, gw, gb, etc.
        trans, real_zero, gw, gb, __ = calc_trans(p, Tleaf, gs, inf_gb=inf_gb)
        new_Tleaf, __ = leaf_temperature(p, trans, Tleaf=Tleaf, inf_gb=inf_gb)
        Pleaf = P[bn.nanargmin(np.abs(E - trans))]

        # update Cs (Pa)
        boundary_CO2 = p.Patm * conv.FROM_MILI * An / (gb * conv.GbcvGb)
        Cs = np.maximum(cst.zero, np.minimum(p.CO2, p.CO2 - boundary_CO2))
        Cs_umol_mol = Cs * conv.MILI / p.Patm

        # update gs over A
        gsoA = g0 + p.g1T * fw / Cs_umol_mol

        # force stop when atm. conditions yield E < 0. (non-physical)
        if (iter < 1) and (not real_zero):
            real_zero = None

        # check for convergence
        if ((real_zero is None) or (iter >= iter_max) or ((iter >= 2) and
            real_zero and (abs(Tleaf - new_Tleaf) <= threshold_conv) and not
            np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero))):
            break

        # no convergence, iterate on leaf temperature
        Tleaf = new_Tleaf
        iter += 1

    if ((np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and
        (An > 0.)) or np.isclose(Ci, 0., rtol=cst.zero, atol=cst.zero) or
        (Ci < 0.) or np.isclose(Ci, p.CO2, rtol=cst.zero, atol=cst.zero) or
        (Ci > p.CO2) or (real_zero is None) or (not real_zero) or
       any(np.isnan([An, Ci, trans, gs, Tleaf, Pleaf]))):
        An, Ci, trans, gs, gb, Tleaf, Pleaf = (9999.,) * 7

    return An, Aj, Ac, Ci, trans, gs, gb, new_Tleaf, Pleaf
예제 #4
0
def Cmax_gs(p, photo='Farquhar', res='low', inf_gb=False):
    """
    Finds the instantaneous optimal C gain for a given C cost.
    First, the C gain equation is derived for gs, beta, Ci unknown.
    Then, the derived form of the equation is solved for Ci over a range of
    possible betas, gs, all of which are directly or indirectly leaf
    water potential P dependent.
    A check (check_solve) is performed to verify that the optimization satisfies
    the zero equality criteria and, finally, results are bound via a range of
    physically possible Ci values.
    N.B.: there can be several possible optimizations

    Arguments:
    ----------
    p: recarray object or pandas series or class containing the data
        time step's met data & params

    photo: string
        either the Farquhar model for photosynthesis, or the Collatz model

    inf_gb: bool
        if True, gb is prescrived and very large

    Returns:
    --------
    gsOPT: float
        stomatal conductance [mol.m-2.s-1] for which the A(gs) is maximized

    AnOPT: float
        maximum C assimilation rate [μmol.m-2.s-1] given by the diffusive supply
        of CO2

    transOPT: float
        transpiration rate [mmol.m-2.s-1] for which the A(gs) is maximized

    CiOPT: float
        intercellular CO2 concentration [Pa] for which the A(gs) is maximized

    """

    # energy balance
    P, trans = hydraulics(p, res=res, kmax=p.kmaxCM)

    # expression of optimization
    Ci, mask = Ci_sup_dem(p, trans, photo=photo, res=res, inf_gb=inf_gb)
    gc, gs, gb, __ = leaf_energy_balance(p, trans[mask], inf_gb=inf_gb)
    expr = np.abs(
        np.gradient(A_trans(p, trans[mask], Ci, inf_gb=inf_gb), P[mask]) -
        dcost_dpsi(p, P[mask], gs))

    try:
        if inf_gb:  # check on valid range
            check = expr[gc > cst.zero]

        else:  # further constrain the realm of possible gs
            check = expr[np.logical_and(gc > cst.zero, gs < 1.5 * gb)]

        idx = np.isclose(expr, min(check))
        idx = [list(idx).index(e) for e in idx if e]

        if inf_gb:  # check for algo. "overshooting" due to inf. gb
            while Ci[idx[0]] < 2. * p.gamstar25:

                idx[0] -= 1

                if idx[0] < 3:
                    break

        # optimized where Ci for both photo models are close
        Ci = Ci[idx[0]]
        trans = trans[mask][idx[0]]  # mol.m-2.s-1
        gs = gs[idx[0]]
        Pleaf = P[mask][idx[0]]

        # rubisco limitation or electron transport-limitation?
        An, Aj, Ac = calc_photosynthesis(p,
                                         trans,
                                         Ci,
                                         photo=photo,
                                         inf_gb=inf_gb)
        rublim = rubisco_limit(Aj, Ac)

        # leaf temperature?
        Tleaf, __ = leaf_temperature(p, trans, inf_gb=inf_gb)

        if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and
            (An > 0.)) or (idx[0] == len(P) - 1) or any(
                np.isnan([An, Ci, trans, gs, Tleaf, Pleaf])):
            An, Ci, trans, gs, gb, Tleaf, Pleaf = (9999., ) * 7

        elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero):
            trans *= conv.MILI  # mmol.m-2.s-1

        return An, Ci, rublim, trans, gs, gb, Tleaf, Pleaf

    except ValueError:  # no opt
        return (9999., ) * 8
def floop(p, model, photo='Farquhar', inf_gb=True):

    # initialize the system
    Dleaf = p.VPD  # kPa
    Cs = p.CO2  # Pa

    if model == 'Tuzet':
        iter_min = 2  # needed to update the fw with the LWP

    else:
        iter_min = 1

    # hydraulics
    P, trans = hydraulics(p, kmax=p.kmaxT)

    try:  # is Tleaf one of the input fields?
        Tleaf = p.Tleaf
        iter_max = 0

        if model == 'Tuzet':
            iter_max = 2  # needed to update the fw with the LWP

    except (IndexError, AttributeError, ValueError):  # calc. Tleaf
        Tleaf = p.Tair  # deg C
        iter_max = 40

    if model != 'Tuzet':  # energy balance requirements
        Pleaf_pd = p.Ps_pd - p.height * cst.rho * cst.g0 * conv.MEGA

        if model == 'Medlyn':
            Dleaf = np.maximum(0.05, Dleaf)  # gs model not valid < 0.05

            if p.height > 0 and sw >= p.fc:
                fw = 1.  # no moisture stress

            else:
                fw = fwWP(p, p.Ps)  # moisture stress function

    else:  # Tuzet model
        fw = fLWP(p, p.LWP_ini)  # stress factor

    if (model == 'Medlyn') or (model == 'Tuzet'):  # init. gs over A
        g0 = 1.e-9  # g0 ~ 0, removing it entirely introduces errors
        Cs_umol_mol = Cs * conv.MILI / p.Patm  # umol mol-1

        if model == 'Medlyn':
            gsoA = g0 + (1. + p.g1 * fw / (Dleaf**0.5)) / Cs_umol_mol

        else:  # Tuzet
            gsoA = g0 + p.g1T * fw * Cs_umol_mol

    # iter on the solution until it is stable enough
    iter = 0

    while True:

        if model == 'Eller':
            Cicol = calc_colim_Ci(p, Cs, Tleaf, photo)
            dCi = Cs - Cicol  # Pa

            # calculate dA, μmol m-2 s-1
            As, __, __ = calc_photosynthesis(p,
                                             0.,
                                             Cs,
                                             photo,
                                             Tleaf=Tleaf,
                                             gsc=0.)
            Acol, __, __ = calc_photosynthesis(p,
                                               0.,
                                               Cicol,
                                               photo,
                                               Tleaf=Tleaf,
                                               gsc=0.)
            dA = As - Acol  # ambient - colimitation

            # dAdCi (in mol H2O) is needed to calculate gs, mmol m-2 s-1
            dAdCi = dA * conv.GwvGc * p.Patm / dCi

            # kcost, unitless
            cost_pd = kcost(p, Pleaf_pd, Pleaf_pd)
            cost_mid = kcost(p, -p.P50, Pleaf_pd)
            dkcost = cost_pd - cost_mid

            # dP is needed to calculate gs
            dP = 0.5 * (Pleaf_pd + p.P50)  # MPa,  /!\ sign of P50

            # xi, the loss of xylem cost of stomatal opening, mmol m-2 s-1
            dq = Dleaf / p.Patm  # mol mol-1, equivalent to D / Patm
            Xi = 2. * p.kmaxS1 * (cost_pd**2.) * dP / (dq * dkcost)

            # calculate gs at the co-limitation point, mmol m-2 s-1
            gscol = Acol * conv.GwvGc * p.Patm / dCi

            # calculate gs, mol m-2 s-1
            if dAdCi <= 0.:  # cp from SOX code, ??? it should never happen!
                gs = gscol * conv.FROM_MILI

            else:
                gs = 0.5 * dAdCi * conv.FROM_MILI * ((
                    (1. + 4. * Xi / dAdCi)**0.5) - 1.)

        elif model == 'SOX-OPT':  # retrieve all potential Ci values
            Cis = Ci_stream(p, Cs, Tleaf, 'low')

            # rate of photosynthesis, μmol m-2 s-1
            A, __, __ = calc_photosynthesis(p, 0., Cis, photo, Tleaf=Tleaf)

            # gb?
            __, gb = leaf_temperature(p, 0., Tleaf=Tleaf, inf_gb=inf_gb)

            if inf_gb or (iter < 1):  # gas-exchange trans, mmol m-2 s-1
                E = A * conv.GwvGc * Dleaf / (p.CO2 - Cis)

            else:
                E = (A * (gb * conv.GwvGc + gs * conv.GbvGbc) / (gs + gb) *
                     Dleaf / (p.CO2 - Cis))

            # cost, Pleaf
            mask = np.logical_and(Pleaf_pd - E / p.ksc_prev <= Pleaf_pd,
                                  Pleaf_pd - E / p.ksc_prev >= P[-1])
            P = (Pleaf_pd - E / p.ksc_prev)[mask]
            cost = kcost(p, P, Pleaf_pd)

            try:  # optimal point
                iopt = np.argmax(cost * A[mask])
                Ci = Cis[mask][iopt]

            except Exception:

                return 9999. * 1000., p.ksc_prev

            # get net rate of photosynthesis at optimum, μmol m-2 s-1
            An, __, __ = calc_photosynthesis(p, 0., Ci, photo, Tleaf=Tleaf)

            # get associated gc and gs
            gc = p.Patm * conv.FROM_MILI * An / (p.CO2 - Ci)

            if inf_gb:
                gs = gc * conv.GwvGc

            else:
                gs = np.maximum(cst.zero,
                                gc * gb * conv.GwvGc / (gb - conv.GbvGbc * gc))

        else:
            An, __, __, __ = calc_photosynthesis(p,
                                                 0.,
                                                 Cs,
                                                 photo,
                                                 Tleaf=Tleaf,
                                                 gs_over_A=gsoA)
            gs = np.maximum(cst.zero, conv.GwvGc * gsoA * An)

        # calculate new trans, gw, gb, Tleaf
        E, real_zero, gw, gb, Dleaf = calc_trans(p, Tleaf, gs, inf_gb=inf_gb)
        new_Tleaf, __ = leaf_temperature(p, E, Tleaf=Tleaf, inf_gb=inf_gb)

        if model == 'Eller':  # calculate An
            An, __, __ = calc_photosynthesis(p,
                                             0.,
                                             Cs,
                                             photo,
                                             Tleaf=Tleaf,
                                             gsc=conv.U * conv.GcvGw * gs)

        # update Cs (Pa)
        boundary_CO2 = p.Patm * conv.FROM_MILI * An / (gb * conv.GbcvGb)
        Cs = np.maximum(cst.zero, np.minimum(p.CO2, p.CO2 - boundary_CO2))
        Cs_umol_mol = Cs * conv.MILI / p.Patm

        if model == 'Tuzet':  # update Pleaf and fw
            Pleaf = P[np.nanargmin(np.abs(trans - E))]  # Tuzet model

            if np.abs(fw - fLWP(p, Pleaf)) < 0.5:  # is fw stable?
                fw = fLWP(p, Pleaf)  # update

            # update gsoA
            gsoA = g0 + p.g1T * fw / Cs_umol_mol

        else:  # update the leaf-to-air VPD
            if (np.isclose(E, cst.zero, rtol=cst.zero, atol=cst.zero)
                    or np.isclose(gw, cst.zero, rtol=cst.zero, atol=cst.zero)
                    or np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero)):
                Dleaf = p.VPD  # kPa

            if model == 'Medlyn':
                Dleaf = np.maximum(0.05, Dleaf)  # gs model not valid < 0.05

                # update gs over A
                gsoA = g0 + (1. + p.g1 * fw / (Dleaf**0.5)) / Cs_umol_mol

        # force stop when atm. conditions yield E < 0. (non-physical)
        if (iter < 1) and (not real_zero):
            real_zero = None

        # check for convergence
        if ((real_zero is None) or (iter >= iter_max) or
            ((iter > iter_min) and real_zero and
             (abs(Tleaf - new_Tleaf) <= 0.1)
             and not np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero))):
            break

        # no convergence, iterate
        Tleaf = new_Tleaf
        iter += 1

        if iter_max < 5:  # no "real" iteration if Tleaf is prescribed
            Cs = p.CO2
            Tleaf = p.Tleaf

    if model == 'Tuzet':

        return gs * 1000., Pleaf

    elif model == 'SOX-OPT':

        return gs * 1000., p.kmaxS2 * cost[iopt]

    else:

        return gs * 1000.  # mmol/m2/s
def fmtx(p, model, photo='Farquhar', inf_gb=True):

    # hydraulics
    if (model == 'CAP') or (model == 'MES'):
        if model == 'CAP':
            P, trans = hydraulics(p, kmax=p.krlC, Pcrit=p.PcritC)

        else:
            P, trans = hydraulics(p, kmax=p.krlM, Pcrit=p.PcritM)

    elif model == 'LeastCost':
        P, trans = hydraulics(p, kmax=p.kmaxLC)

    else:
        P, trans = hydraulics(p)

    # expressions of optimisation
    if model == 'ProfitMax':  # look for the most net profit
        cost, __ = hydraulic_cost(p, P)
        gain, Ci, mask = photo_gain(p, trans, photo, 'low', inf_gb=inf_gb)
        expr = gain - cost[mask]

    elif (model != 'CAP') and (model != 'MES'):
        Ci, mask = Ci_sup_dem(p, trans, photo=photo, inf_gb=inf_gb)

    if model == 'ProfitMax2':
        expr = A_trans(p, trans[mask], Ci,
                       inf_gb=inf_gb) * (1. - trans[mask] / trans[-1])

    if model == 'CGain':
        expr = (A_trans(p, trans[mask], Ci, inf_gb=inf_gb) -
                p.Kappa * fPLC(p, P[mask]))

    if model == 'LeastCost':
        expr = ((p.Eta * conv.MILI * trans[mask] +
                 dVmaxoAdXi(p, trans[mask], Ci, inf_gb=inf_gb)) /
                A_trans(p, trans[mask], Ci, inf_gb=inf_gb))

    # leaf energy balance
    gc, gs, gb, ww = leaf_energy_balance(p, trans, inf_gb=inf_gb)

    if model == 'CMax':

        try:
            expr = np.abs(
                np.gradient(A_trans(p, trans[mask], Ci, inf_gb=inf_gb),
                            P[mask]) - dcost_dpsi(p, P[mask], gs))

        except Exception:

            return 9999. * 1000.  # returning an actual NaN causes trouble

    if model == 'WUE-LWP':
        expr = (A_trans(p, trans[mask], Ci, inf_gb=inf_gb) -
                p.Lambda * conv.MILI * trans[mask])

    if model == 'CAP':
        cost = phiLWP(P, p.PcritC)
        Ci, mask = Ci_sup_dem(p,
                              trans,
                              photo=photo,
                              Vmax25=p.Vmax25 * cost,
                              inf_gb=inf_gb)
        An, __, __ = calc_photosynthesis(p,
                                         trans[mask],
                                         Ci,
                                         photo,
                                         Vmax25=p.Vmax25 * cost[mask],
                                         inf_gb=inf_gb)

        try:
            expr = An

        except Exception:

            return 9999. * 1000.  # returning an actual NaN causes trouble

    if model == 'MES':  # Ci is Cc
        cost = phiLWP(P, p.PcritM)
        Cc, mask = Ci_sup_dem(p, trans, photo=photo, phi=cost, inf_gb=inf_gb)
        An, __, __ = calc_photosynthesis(p,
                                         trans[mask],
                                         Cc,
                                         photo,
                                         inf_gb=inf_gb)

        try:
            expr = An

        except Exception:

            return 9999. * 1000.  # returning an actual NaN causes trouble

    if inf_gb:  # deal with edge cases by rebounding the solution
        check = expr[gc[mask] > cst.zero]

    else:  # accounting for gb
        check = expr[np.logical_and(gc[mask] > cst.zero, gs[mask] < 1.5 * gb)]

    try:
        if (model == 'LeastCost') or (model == 'CMax'):
            idx = np.isclose(expr, min(check))

        else:
            idx = np.isclose(expr, max(check))

        idx = [list(idx).index(e) for e in idx if e]

        if len(idx) >= 1:  # opt values

            return gs[mask][idx[0]] * 1000.  # mmol/m2/s

        else:

            return 9999. * 1000.  # returning an actual NaN causes trouble

    except ValueError:  # expr function is empty

        return 9999. * 1000.
예제 #7
0
def solve_std(p, sw, photo='Farquhar', res='low', iter_max=40,
              threshold_conv=0.1, inf_gb=False):

    """
    Checks the energy balance by looking for convergence of the new leaf
    temperature with the leaf temperature predicted by the previous
    iteration. Then returns the corresponding An, E, Ci, etc.

    Arguments:
    ----------
    p: recarray object or pandas series or class containing the data
        time step's met data & params

    sw: float
        mean volumetric soil moisture content [m3 m-3]

    photo: string
        either the Farquhar model for photosynthesis, or the Collatz
        model

    threshold_conv: float
        convergence threshold for the new leaf temperature to be in
        energy balance

    iter_max: int
        maximum number of iterations allowed on the leaf temperature
        before reaching the conclusion that the system is not energy
        balanced

    inf_gb: bool
        if True, gb is prescrived and very large

    Returns:
    --------
    trans_can: float
        transpiration rate of canopy [mmol m-2 s-1] across leaves

    gs_can: float
        stomatal conductance of canopy [mol m-2 s-1] across leaves

    An_can: float
        C assimilation rate of canopy [umol m-2 s-1] across leaves

    Ci_can: float
        average intercellular CO2 concentration of canopy [Pa] across
        leaves

    rublim_can: string
        'True' if the C assimilation is rubisco limited, 'False'
        otherwise.

    """

    # initial state
    Cs = p.CO2  # Pa
    Tleaf = p.Tair  # deg C
    Dleaf = np.maximum(0.05, p.VPD)  # gs model not valid < 0.05

    # hydraulics
    P, E = hydraulics(p, res=res)

    if sw >= p.fc:
        g1 = p.g1

    else:
        g1 = p.g1 * fwWP(p, p.Ps)

    # initialise gs over A
    g0 = 1.e-9  # g0 ~ 0, removing it entirely introduces errors
    Cs_umol_mol = Cs * conv.MILI / p.Patm  # umol mol-1
    gsoA = g0 + (1. + g1 / (Dleaf ** 0.5)) / Cs_umol_mol

    # iter on the solution until it is stable enough
    iter = 0

    while True:

        An, Aj, Ac, Ci = calc_photosynthesis(p, 0., Cs, photo, Tleaf=Tleaf,
                                             gs_over_A=gsoA)

        # stomatal conductance, with moisture stress effect
        gs = np.maximum(cst.zero, conv.GwvGc * gsoA * An)

        # calculate new trans, gw, gb, mol.m-2.s-1
        trans, real_zero, gw, gb, Dleaf = calc_trans(p, Tleaf, gs,
                                                     inf_gb=inf_gb)
        new_Tleaf, __ = leaf_temperature(p, trans, Tleaf=Tleaf, inf_gb=inf_gb)

        # update Cs (Pa)
        boundary_CO2 = p.Patm * conv.FROM_MILI * An / (gb * conv.GbcvGb)
        Cs = np.maximum(cst.zero, np.minimum(p.CO2, p.CO2 - boundary_CO2))
        Cs_umol_mol = Cs * conv.MILI / p.Patm

        # new leaf-air vpd, kPa
        if (np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) or
            np.isclose(gw, cst.zero, rtol=cst.zero, atol=cst.zero) or
            np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero)):
            Dleaf = np.maximum(0.05, p.VPD)  # kPa

        # update gs over A
        gsoA = g0 + (1. + g1 / (Dleaf ** 0.5)) / Cs_umol_mol

        # force stop when atm. conditions yield E < 0. (non-physical)
        if (iter < 1) and (not real_zero):
            real_zero = None

        # check for convergence
        if ((real_zero is None) or (iter >= iter_max) or ((iter >= 1) and
            real_zero and (abs(Tleaf - new_Tleaf) <= threshold_conv) and not
            np.isclose(gs, cst.zero, rtol=cst.zero, atol=cst.zero))):
            break

        # no convergence, iterate on leaf temperature
        Tleaf = new_Tleaf
        iter += 1

    Pleaf = P[bn.nanargmin(np.abs(trans - E))]
    rublim = rubisco_limit(Aj, Ac)  # lim?

    if ((np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero) and
        (An > 0.)) or np.isclose(Ci, 0., rtol=cst.zero, atol=cst.zero) or
        (Ci < 0.) or np.isclose(Ci, p.CO2, rtol=cst.zero, atol=cst.zero) or
        (Ci > p.CO2) or (real_zero is None) or (not real_zero) or
       any(np.isnan([An, Ci, trans, gs, new_Tleaf, Pleaf]))):
        An, Ci, trans, gs, gb, new_Tleaf, Pleaf = (9999.,) * 7

    elif not np.isclose(trans, cst.zero, rtol=cst.zero, atol=cst.zero):
        trans *= conv.MILI  # mmol.m-2.s-1

    return An, Ci, rublim, trans, gs, gb, new_Tleaf, Pleaf
def Ci_sup_dem(p,
               trans,
               photo='Farquhar',
               res='low',
               Vmax25=None,
               phi=None,
               inf_gb=False):

    # ref. photosynthesis
    A_ref, __, __ = calc_photosynthesis(p,
                                        trans,
                                        p.CO2,
                                        photo,
                                        Vmax25=Vmax25,
                                        inf_gb=inf_gb)

    # Cs < Ca, used to ensure physical solutions
    __, __, gb, __ = leaf_energy_balance(p, trans, inf_gb=inf_gb)
    boundary_CO2 = p.Patm * conv.FROM_MILI * A_ref / (gb * conv.GbcvGb)
    Cs = np.maximum(cst.zero, np.minimum(p.CO2, p.CO2 - boundary_CO2))  # Pa

    # potential Ci values over the full range of transpirations
    if res == 'low':
        NCis = 500

    if res == 'med':
        NCis = 2000

    if res == 'high':
        NCis = 8000

    # retrieve the appropriate Cis from the supply-demand
    Cis = np.asarray(
        [np.linspace(0.1, Cs[e], NCis) for e in range(len(trans))])

    if (Vmax25 is None) and (phi is not None):  # account for gm
        Tref = p.Tref + conv.C_2_K  # degk, Tref set to 25 degC

        try:  # is Tleaf one of the input fields?
            Tleaf = p.Tleaf

        except (IndexError, AttributeError, ValueError):  # calc. Tleaf
            Tleaf, __ = leaf_temperature(p, trans, inf_gb=inf_gb)

        # CO2 compensation point
        gamstar = arrhen(p.gamstar25, p.Egamstar, Tref, Tleaf)

        try:  # now getting the Cc
            Ccs = np.asarray([
                phi[e] * (Cis[e] - gamstar[e]) + gamstar[e]
                for e in range(len(trans))
            ])

        except IndexError:  # only one Tleaf
            Ccs = np.asarray([
                phi[e] * (Cis[e] - gamstar) + gamstar
                for e in range(len(trans))
            ])

    if phi is None:
        Ccs = None

    Ci = mtx_minimise(p,
                      trans,
                      Cis,
                      photo,
                      Vmax25=Vmax25,
                      all_Ccs=Ccs,
                      inf_gb=inf_gb)
    mask = ~Ci.mask

    try:
        if len(mask) > 0:
            pass

    except TypeError:
        mask = ([False] + [
            True,
        ] * len(Ci))[:-1]

    return Ci[mask], mask
def mtx_minimise(p,
                 trans,
                 all_Cis,
                 photo,
                 Vmax25=None,
                 all_Ccs=None,
                 inf_gb=False):
    """
    Uses matrices to find each value of Ci for which An(supply) ~
    An(demand) on the transpiration stream.

    Arguments:
    ----------
    p: recarray object or pandas series or class containing the data
        time step's met data & params

    trans: array
        transpiration [mol m-2 s-1], values depending on the possible
        leaf water potentials (P) and the Weibull parameters b, c

    all_Cis: array
        all potential Ci values over the transpiration stream (for each
        water potential, Ci values can be anywhere between a lower bound
        and Cs)

    photo: string
        either the Farquhar model for photosynthesis, or the Collatz
        model

    inf_gb: bool
        if True, gb is prescrived and very large

    Returns:
    --------
    The value of Ci for which An(supply) is the closest to An(demand)
    (e.g. An(supply) - An(demand) closest to zero).

    """

    if Vmax25 is not None:
        demand, __, __ = calc_photosynthesis(p,
                                             np.expand_dims(trans, axis=1),
                                             all_Cis,
                                             photo,
                                             Vmax25=np.expand_dims(Vmax25,
                                                                   axis=1),
                                             inf_gb=inf_gb)

    elif all_Ccs is not None:
        demand, __, __ = calc_photosynthesis(p,
                                             np.expand_dims(trans, axis=1),
                                             all_Ccs,
                                             photo,
                                             inf_gb=inf_gb)

    else:
        demand, __, __ = calc_photosynthesis(p,
                                             np.expand_dims(trans, axis=1),
                                             all_Cis,
                                             photo,
                                             inf_gb=inf_gb)

    supply = A_trans(p, np.expand_dims(trans, axis=1), all_Cis, inf_gb=inf_gb)

    # find the meeting point between demand and supply
    idx = bn.nanargmin(np.abs(supply - demand), axis=1)  # closest ~0

    if all_Ccs is not None:
        all_Cis = all_Ccs

    # each Ci on the transpiration stream
    Ci = np.asarray([all_Cis[e, idx[e]] for e in range(len(trans))])
    Ci = np.ma.masked_where(idx == 0, Ci)
    Ci = np.ma.masked_where(idx == all_Cis.shape[1] - 1, Ci)

    return Ci