Exemple #1
0
def noise_build(eco: np.ndarray, t: int, bb: Dict) -> np.ndarray:
    noise_sd = bb.get('noise_sd')
    res_forcing_amps = bb.get('res_forcing_amps')
    include_zoo = bb.get('include_zoo')

    rhs = np.zeros_like(eco)
    phy_indices = helpers.eco_indices('phy', bio=bb)
    res_indices = helpers.eco_indices('res', bio=bb)
    zoo_indices = helpers.eco_indices('zoo', bio=bb)
    rhs[phy_indices] = -noise_sd * eco[phy_indices]
    rhs[res_indices] = noise_sd * (res_forcing_amps - eco[res_indices])

    if include_zoo in (1, True, None):
        rhs[zoo_indices] = -noise_sd * eco[zoo_indices]

    return rhs
Exemple #2
0
# DEBUG

out_list = list()
out_keys = list()
xaxis = None

num_years_plot = 3

params = helpers.load(params_name, 'params', 'single')
res_phy_stoich_ratio = params.get('bio').get('res_phy_stoich_ratio')
num_phy = params.get('bio').get('num_phy')
num_zoo = params.get('bio').get('num_zoo')

debug = helpers.load(params_name, 'debug', 'single')
eco = helpers.load(params_name, 'data', 'single')
phy = eco[helpers.eco_indices('phy', params=params), :]
zoo = eco[helpers.eco_indices('zoo', params=params), :]
zoo_prey_pref = params.get('bio').get('zoo_prey_pref')

# we can compute res_zoo_makeup_ratio
prey = np.zeros((num_phy + 1, num_zoo))
t, ind = np.unique(debug['t'][0], return_index=True)

t_y = helpers.get_last_n_years(t, num_years_plot) / c.NUM_DAYS_PER_YEAR

index = 0  # 0 = nitrate (or nitrogen if single_nit = True), 2 = silicate, 3 = phosphate, 4 = iron

keys = list(debug.keys())
keys.remove('res_zoo_remin_frac')
keys.remove('t')
Exemple #3
0
    def time_series_plot(self, kind: str = 'indiv', ax: mat_ax.Axes = None,
                         plotting_threshold: float = -np.infty, labels: List = None, return_lines: bool = True,
                         num_years: int = None, num_days: int = None, color_kw: Dict = None,
                         plot_kw: Dict = None, legend_kw: Dict = None, legend_off: bool = False,
                         resize_box: tuple = (0, 0.2, 0, 0.7), compartments: Iterable[Dict] = ({'phy': 'all'},),
                         res_to_carbon: Union[bool, int, list] = True, phy_index: int = None):

        """Plot one or more time series from the stored ecosystem

        Parameters
        ----------
        kind
            'indiv' (default), 'shannon', 'total'
        ax
            Axis we'd like to plot on.
        num_years
            How many years to include
        num_days
            How many days?
        plotting_threshold
            Don't plot any items below this threshold
        labels
            Legend labels
        resize_box
            Compress figure box
        legend_off
            Leave off legend
        return_lines
            Returns lines associated with plot object
        res_to_carbon
            Convert resources to carbon units
        phy_index
            If we're plotting resource, which phyto compartment(s) to use to convert to carbon units
        plot_kw
            Keyword arguments to pass to matplotlib
        color_kw
            colors: list of colors to use for plotting
            cmap: used to create list of colors
        legend_kw
            dict of keyword arguments to be passed directly to `legend` method of `ax`
        compartments
            List of compartment dictionaries

        """

        ty_vec = self.t_in / c.NUM_DAYS_PER_YEAR

        if ax is None:
            ax = plt.subplot(111)

        if num_years is not None:
            ty_vec = helpers.get_last_n_years(self.t_in, num_years) / c.NUM_DAYS_PER_YEAR
        if num_days is not None:
            ty_vec = helpers.get_last_n_days(self.t_in, num_days) / c.NUM_DAYS_PER_YEAR
            num_years = num_days / c.NUM_DAYS_PER_YEAR

        box1 = ax.get_position()
        ax.set_position([box1.x0 + box1.width * resize_box[0], box1.y0 + box1.height * resize_box[1],
                         box1.width + box1.width * resize_box[2], box1.height * resize_box[3]])

        ax.set_xlim(left=self.num_years - num_years, right=self.num_years)

        plt_obj = list()

        # colors
        if kind in ('shannon', 'total'):
            eco = helpers.restrict_ts(self.eco_in, self.params, num_years=num_years, num_days=num_days, kind=kind,
                                      compartments=compartments)
            if color_kw is not None:
                if 'colors' in color_kw:
                    colors = iter(color_kw['colors'])
                else:
                    colors = iter(helpers.color_cycle(1, cmap=color_kw.get('cmap')))
            else:
                colors = iter(helpers.color_cycle(1))

            if np.mean(eco) > plotting_threshold:
                line, = ax.plot(ty_vec, eco, **plot_kw if plot_kw is not None else {})
                line.set_color(next(colors))
                plt_obj.append(line)
            if return_lines:
                return plt_obj
        else:
            eco = helpers.restrict_ts(self.eco_in, self.params, num_years=num_years, num_days=num_days)
            name_list = helpers.get_name_list(self.params, compartments=compartments, for_plot=True)

            new_name_list = list()

            for list_dict in compartments:
                key = list(list_dict.keys())[0]

                if list_dict[key] == 'all' or list_dict[key] is None:
                    indices = list(range(self.params['bio']['num_{}'.format(key)]))
                else:
                    indices = list_dict[key]

                conversion = None

                # get name_list for just this set of compartments
                curr_name_list = [x for x in name_list if x.startswith(helpers.short_name(key))]
                if key == 'res':
                    if res_to_carbon not in (False, 0):
                        conversion = al.res_to_carbon_conversions(eco, self.params, phy_index=phy_index)

                for ind, r in enumerate(indices):
                    start_index = list(helpers.eco_indices(key, self.params['bio']))[0]
                    name = curr_name_list[ind]
                    output = np.squeeze(eco[start_index + r, :])

                    # convert resource to phyto units
                    if key == 'res' and res_to_carbon is not False:
                        output = conversion[r, :]

                    if np.mean(output) >= plotting_threshold:
                        new_name_list.append(name)
                        line, = ax.plot(ty_vec, output, label=name, **plot_kw if plot_kw is not None else {})
                        plt_obj.append(line)

            # NOW set colors
            if color_kw is not None:
                if 'colors' in color_kw:
                    colors = iter(color_kw.get('colors'))
                else:
                    colors = iter(helpers.color_cycle(len(new_name_list), cmap=color_kw.get('cmap')))
            else:
                colors = iter(helpers.color_cycle(len(new_name_list)))

            for i in range(len(plt_obj)):
                plt_obj[i].set_color(next(colors))

            if labels:
                new_name_list = labels

            if len(new_name_list) > 0:
                if not legend_off:
                    if legend_kw is not None:
                        ax.legend(labels=new_name_list, **legend_kw)
                    else:
                        ax.legend(labels=new_name_list, loc='upper center', bbox_to_anchor=(0.5, -0.2),
                                  fancybox=True, shadow=True, ncol=5)

            if return_lines:
                return plt_obj
Exemple #4
0
    def phase_plot(self, ax: mat_ax.Axes = None, fig: List = None, compartments: Iterable[Dict] = None,
                   num_years: int = None, num_days: int = None, plot3d: bool = False, res_phy: bool = False):

        """Phase plot from stored ecosystem

        Parameters
        ----------
        fig
            Figure we're plotting on. Only used if plot3d is true
        ax
            Axis we'd like to plot on.
        num_years
            How many years to include
        num_days
            How many days to include?
        num_days
            How many days?
        plot3d
            Plot the three arguments with the highest average concentrations against each other in 3D
        res_phy
            Plot total resources vs. total phytoplankton. Only applies if plot3d=False
        compartments
            List of compartment dictionaries

        """

        eco = helpers.restrict_ts(self.eco_in, self.params, num_years=num_years, num_days=num_days,
                                  compartments=compartments)

        # find time average
        eco_time_avg = np.mean(eco, 1)

        # three largest quantities
        indices = eco_time_avg.argsort()[::-1][:3]

        # indices
        name_list = helpers.get_name_list(self.params, for_plot=True)

        # take the last end_pct of entries
        if plot3d:
            ax = Axes3D(fig)
            num_divisions = 5
            ax.locator_params(nbins=num_divisions, nticks=num_divisions)

            ax.plot(eco[indices[0], :],
                    eco[indices[1], :],
                    eco[indices[2], :])
            ax.set_xlabel(name_list[indices[0]])
            ax.set_ylabel(name_list[indices[1]])
            ax.set_zlabel(name_list[indices[2]])

        else:
            if ax is None:
                ax = fig.add_subplot(111)

            # plot P vs. R
            if res_phy:
                bio = self.params['bio']
                ax.plot(np.sum(eco[helpers.eco_indices('phy', bio), :], 0),
                        np.sum(eco[helpers.eco_indices('res', bio), :], 0),
                        linewidth=2)
                ax.set_xlabel('P')
                ax.set_ylabel('R')

            else:
                # plot two largest quantities against each other
                ax.plot(eco[indices[0], :],
                        eco[indices[1], :],
                        linewidth=2.0)
                ax.set_xlabel(name_list[indices[0]])
                ax.set_ylabel(name_list[indices[1]])

        return ax
Exemple #5
0
def rhs_build(eco: np.ndarray, t: int,
              num_phy: int,
              num_zoo: int,
              num_res: int,
              res_phy_stoich_ratio: np.ndarray,
              res_phy_remin_frac: np.ndarray,
              phy_growth_sat: np.ndarray,
              res_forcing_amps: np.ndarray,
              num_compartments: int,
              phy_mort_rate: Union[np.ndarray, float],
              phy_growth_rate_max: np.ndarray,
              turnover_rate: float = 0.04,
              zoo_hill_coeff: float = 1,
              phy_self_shade: float = 0,
              shade_background: float = 0,
              turnover_min: float = None,
              turnover_max: float = None,
              turnover_radius: float = None,
              turnover_period: Union[float, np.ndarray] = 360,
              turnover_series: np.ndarray = None,
              turnover_phase_shift: float = 0,
              light_series: np.ndarray = None,
              light_min: float = None,
              light_max: float = None,
              light_kind: str = 'ramp',
              turnover_kind: str = 'sine',
              mixed_layer_ramp_times: np.ndarray = None,
              mixed_layer_ramp_lengths: np.ndarray = None,
              phy_source: float = 0,
              res_source: float = 0,
              zoo_source: float = 0,
              dilute_zoo: bool = True,
              zoo_mort_rate: tuple = None,
              linear_zoo_mort_rate: tuple = None,
              zoo_grazing_rate_max: np.ndarray = None,
              zoo_slop_feed: np.ndarray = None,
              zoo_prey_pref: np.ndarray = None,
              zoo_grazing_sat: np.ndarray = None,
              noise: np.ndarray = None,
              noise_additive: bool = False,
              include_zoo: bool = False,
              res_uptake_type: str = 'perfect',
              zoo_model_type: str = 'Real',
              debug_dict: Dict = None,
              dummy_param: int = 0
              ) -> Union[np.ndarray, Tuple[np.ndarray, Dict]]:
    """Build the RHS of the bio equations

    Parameters
    ----------
    eco
        Current values of ecosystem variables
    t
        Current time
    debug_dict
        Return key-value pairs for RHS terms, to study relative magnitudes
    num_phy
        Number of phytoplankton
    num_zoo
        Number of zooplankton
    num_res
        Number of resources
    res_phy_stoich_ratio
        `num_res` by `num_phy` matrix of stoichiometric coefficients. Convert from carbon to nutrient units
    res_phy_remin_frac
        Converts carbon from phytoplankton into resource units
    turnover_series
        If we want to pass the full turnover series in as a function
    light_series
        If we want to pass the full light series in as a function
    mixed_layer_ramp_times
        If we want to represent mixed layer: at what times do ramps occur?
    mixed_layer_ramp_lengths
        If we want to represent mixed layer: how long are the ramps?
    turnover_rate
        (Constant) Dilution coefficient
    turnover_min
        Minimum dilution coefficient (for time-varying case)
    turnover_max
        Maximum dilution coefficient (for time-varying case)
    turnover_radius
        Fraction of mean for radius: between 0 and 1 (for time-varying case)
    turnover_period
        Period of nutrient delivery term. If a list, assume a range (or spectrum; TODO: decide on this)
    shade_background
        Background light attenuation
    phy_self_shade
        Self-shading coefficient for phyto
    phy_growth_sat
        `num_res` by `num_phy` matrix of Monod/Michaelis-Menten half-saturation coefficients.
    res_forcing_amps
        Deep nutrient supply in resource equations
    zoo_grazing_rate_max
        max growth rates of zooplankton
    zoo_prey_pref
        Prey preference matrix; built off zoo_high_pref and zoo_low_pref
    zoo_slop_feed
        fraction of phyto biomass that is utilized by zoo when grazing
    phy_source
        additional source to add to phytoplankton compartments
    res_source
        additional source to add to resource compartments
    zoo_source
        additional source to add to zooplankton compartments
    dilute_zoo
        Subject zooplankton to dilution?
    zoo_mort_rate
        quadratic mortality rate for zooplankton
    linear_zoo_mort_rate
        (optional) linear mortality rate for zooplankton
    zoo_grazing_sat
        saturation coefficients for zooplankton grazing
    num_compartments
        total number of stored compartments in ecosystem. should be `num_phy` + 7, even if zooplankton disabled
    zoo_hill_coeff
        Exponent for grazing.
        n=1: Holling II
        n=2: Holling III
    phy_mort_rate
        mortality rates for phytoplankton
    phy_growth_rate_max
        maximal growth rates for phytoplankton
    include_zoo
        are we including zooplankton?
    res_uptake_type
        how are we uptaking resources?
        - 'perfect' (law of the minimum) by default
        - 'interactive' (product of Monod terms, rather than the minimum) also an option
    no3_inhibit_decay
        controls how much nitrate intake is inhibited by ammonium
    zoo_model_type
        form of zooplankton grazing. 'Real' (i.e. Michaelis-Menten) by default. 'KTW' also an option
    noise
        optional vector of noise to add to turnover rate
    noise_additive
        is noise additive or multiplicative (default)?
    dummy_param
        Dummy parameter for running the same simulation multiple times for sweeps (e.g. for ensemble of noisy sims)

    Returns
    ---------
    np.ndarray
        RHS in a `num_compartments` x 1 vector
    """

    ########################################################################

    # Assign indices for phyto, resources, predator
    num_dict = {'num_phy': num_phy, 'num_res': num_res, 'num_zoo': num_zoo}
    phy_indices = helpers.eco_indices('phy', num_dict)
    res_indices = helpers.eco_indices('res', num_dict)
    zoo_indices = helpers.eco_indices('zoo', num_dict)

    res_zoo_remin_frac = None

    def compute_turnover(turnover_rate=turnover_rate):
        if turnover_series is not None:
            if turnover_min is None or turnover_max is None:
                return turnover_series[int(t)]
            else:
                # Scale by turnover min and turnover max
                series_max = np.max(turnover_series)
                series_min = np.min(turnover_series)
                return turnover_min + (turnover_series[int(t)] - series_min) / (series_max - series_min) * (turnover_max - turnover_min)
        if None not in (turnover_min, turnover_max):
            # Mixed layer representation
            if turnover_kind == 'ramp':
                turnover_rate = helpers.ml_profile(low_val=turnover_min, high_val=turnover_max,
                                                   phase_shift=turnover_phase_shift,
                                                   mixed_layer_ramp_lengths=mixed_layer_ramp_lengths,
                                                   mixed_layer_ramp_times=mixed_layer_ramp_times, t=t)
                return turnover_rate
            elif turnover_kind == 'sine':
                mean_value = 0.5 * (turnover_min + turnover_max)
                return mean_value + 0.5 * (turnover_max - turnover_min) * np.sin(2 * np.pi / turnover_period * t + np.pi/4)
        if turnover_radius is not None:
            tau_max = turnover_rate * (1 + turnover_radius)
            tau_min = turnover_rate * (1 - turnover_radius)
            return turnover_rate + 0.5 * (tau_max - tau_min) * np.sin(2 * np.pi / turnover_period * t)
        else:
            return turnover_rate

    turnover_rate = compute_turnover()

    if noise is not None:
        # Additive or multiplicative noise?
        if noise_additive:
            turnover_rate += turnover_rate * noise[int(t)]
        else:
            turnover_rate *= noise[int(t)]

    if debug_dict:
        # add current time (if integer, approximately)
        if len(debug_dict['t']) == 0 or int(t) != int(debug_dict['t'][-1]):
            for key in debug_dict:
                if len(np.unique(debug_dict[key][0])) == 1 and np.unique(debug_dict[key][0])[0] == c.NAN_VALUE:
                    debug_dict[key][0] = np.zeros(debug_dict[key][0].shape)
                else:
                    debug_dict[key].append(np.zeros(debug_dict[key][-1].shape))
            debug_dict['t'][-1] = np.array([t])

    rhs = np.zeros((num_compartments,))

    # res_forcing/phy_forcing are prescribed forcings
    # May be "Newtonian cooling" like term for Tilman-like model

    phy = eco[phy_indices]
    res = eco[res_indices]
    zoo = eco[zoo_indices]

    prey = np.zeros(num_phy + num_zoo,)
    prey[:num_phy] = phy
    prey[num_phy:] = zoo

    #  grazing terms
    food_total_const = np.zeros(num_zoo, )  # F_rho in Vallina
    prey_preference_var = np.zeros((num_phy + num_zoo, num_zoo))  # phi in Vallina
    food_total_var = np.zeros(num_zoo, )  # F_phi in Vallina
    zoo_graze_mat = np.zeros((num_phy + num_zoo, num_zoo))  # grazing matrix
    zoo_grazing_sat_var = np.zeros(num_zoo, )  # variable saturation coefficient

    if include_zoo:
        prey_preference_product = prey[:, None] * zoo_prey_pref  # [num_phy+num_zoo, num_zoo]
        food_total_const = np.sum(prey_preference_product, axis=0)  # [num_zoo,]
        prey_preference_var = prey_preference_product / (food_total_const[None, :] + 1e-6)
        food_total_var = np.sum(prey[:, None] * prey_preference_var, axis=0)

        # variable saturation coefficient
        zoo_grazing_sat_var = zoo_grazing_sat * food_total_var / (food_total_const + 1e-6)

        # compute res_zoo_remin_frac dynamically
        # add small factor to prey/food
        if zoo_model_type == 'Real':
            # num_res x num_zoo
            res_zoo_remin_frac = (res_phy_remin_frac @ prey_preference_product[:num_phy, :]) / \
                                 (food_total_const[None, :] + 1e-6)

            res_zoo_remin_frac += (res_zoo_remin_frac @ prey_preference_product[num_phy:, :]) / \
                                        (food_total_const[None, :] + 1e-6)

        elif zoo_model_type == 'KTW':
            res_zoo_remin_frac = (res_phy_remin_frac @ prey_preference_var[:num_phy, :]) / \
                                 (food_total_var[None, :] + 1e-6)

            res_zoo_remin_frac += (res_zoo_remin_frac @ prey_preference_var[num_phy:, :]) / \
                                  (food_total_var[None, :] + 1e-6)

        if debug_dict:
            debug_dict['res_zoo_remin_frac'][-1] = res_zoo_remin_frac

    # phy terms
    phy_mort_term = phy_mort_rate if isinstance(phy_mort_rate, (list, np.ndarray)) \
        else phy_mort_rate * np.ones(num_phy,)

    # used to build zoo_graze_mat
    def get_grazing_term():

        switching_term = feeding_probability = None

        if zoo_model_type == 'Real':
            switching_term = zoo_prey_pref / (food_total_const[None, :] + 1e-6)
            feeding_probability = (food_total_const ** zoo_hill_coeff
                                   / (zoo_grazing_sat ** zoo_hill_coeff
                                      + food_total_const ** zoo_hill_coeff))

        elif zoo_model_type == 'KTW':
            switching_term = prey_preference_var / (food_total_var[None, :] + 1e-6)
            feeding_probability = (food_total_var ** zoo_hill_coeff
                                   / (zoo_grazing_sat_var ** zoo_hill_coeff
                                      + food_total_var ** zoo_hill_coeff))

        return zoo_grazing_rate_max[None, :] * switching_term * feeding_probability[None, :]

    phy_growth_list = eco[res_indices, None] / (eco[res_indices, None] + phy_growth_sat)

    # TODO: Add factor for changes due to light
    # We could add a parameter that allows us to yield a function of time
    # We want to ramp down gradually during winter, and up quickly during winter-spring bloom
    # This will be a piecewise linear combo
    if light_series is not None:
        light_factor = light_series[int(t)]
    elif None not in (light_min, light_max):
        if light_kind == 'ramp':
            light_factor = helpers.light_profile(low_val=light_min, high_val=light_max,
                                                 mixed_layer_ramp_lengths=mixed_layer_ramp_lengths,
                                                 mixed_layer_ramp_times=mixed_layer_ramp_times, t=t)
        elif light_kind == 'sine':
            mean_value = (light_min + light_max)/2
            light_factor = mean_value + (light_max - light_min)/2 * np.sin(2 * np.pi / c.NUM_DAYS_PER_YEAR * t + 5 * np.pi/4)
    else:
        light_factor = 1

    phy_res_growth_term = None
    if res_uptake_type == 'interactive':
        phy_res_growth_term = np.prod(phy_growth_list, axis=0)
    elif res_uptake_type == 'perfect':
        phy_res_growth_term = np.min(phy_growth_list, axis=0)

    phy_growth_vec = phy_growth_rate_max * phy_res_growth_term
    phy_growth_vec *= np.exp(-(shade_background + phy_self_shade * np.sum(phy)))

    # Scale growth rate by light
    phy_growth_vec *= light_factor

    # Gain in phytoplankton biomass corresponds to loss in resource
    res_uptake = -(phy_growth_vec[None, :] * res_phy_stoich_ratio) @ phy

    # -cji * U(I(t), R)
    rhs[res_indices] += res_uptake

    if debug_dict:
        debug_dict['res_uptake'][-1] += res_uptake

    if include_zoo:
        zoo_graze_mat = get_grazing_term()
        phy_zoomort = (-zoo_graze_mat[:num_phy, :] @ zoo) * phy

        # -sum_n Gin * Z[n]
        rhs[phy_indices] += phy_zoomort

        if debug_dict:
            debug_dict['phy_zoomort'][-1] += phy_zoomort

    # Phytoplankton terms
    phy_netgrowth = (phy_growth_vec - phy_mort_term - turnover_rate) * phy

    # (1-fe) * rji * phy_mort_term
    rhs[phy_indices] += phy_netgrowth

    if debug_dict:
        debug_dict['phy_growth'][-1] += phy_growth_vec * phy
        debug_dict['phy_mort'][-1] += -phy_mort_term * phy
        debug_dict['phy_turnover'][-1] += -turnover_rate * phy

    if include_zoo:

        zoo_mort_term = np.array(zoo_mort_rate) * zoo

        if linear_zoo_mort_rate is not None:
            zoo_mort_term += np.array(linear_zoo_mort_rate)

        zoo_graze_sum = zoo_slop_feed * (zoo_graze_mat[:num_phy, :].T @ phy)

        # beta_n * sum (Gin * Pi) - zoo_mort_term - tau
        rhs[zoo_indices] += (zoo_graze_sum - zoo_mort_term) * zoo

        if dilute_zoo:
            rhs[zoo_indices] += -turnover_rate * zoo

        if debug_dict:
            debug_dict['zoo_growth'][-1] += zoo_graze_sum * zoo
            debug_dict['zoo_mort'][-1] += -zoo_mort_term * zoo - (turnover_rate if dilute_zoo else 0) * zoo

        zoo_zoo_graze = zoo_slop_feed * (zoo_graze_mat[num_phy:, :].T @ zoo)
        zoo_zoo_mort = -zoo_graze_mat[num_phy:, :] @ zoo

        rhs[zoo_indices] += (zoo_zoo_graze + zoo_zoo_mort) * zoo

        if debug_dict:
            debug_dict['zoo_growth'][-1] += zoo_zoo_graze * zoo
            debug_dict['zoo_mort'][-1] += zoo_zoo_mort * zoo

    res_forcing_rhs = turnover_rate * (res_forcing_amps - res)
    rhs[res_indices] += res_forcing_rhs  # add forcing

    if debug_dict:
        debug_dict['res_forcing'][-1] += res_forcing_rhs

    # If any value is below extinction threshold and rhs < 0, set rhs to 0 and eco to 0 there
    # this prevents negative blowup.
    # phy_condition = np.where(phy < c.EXTINCT_THRESH)[0]
    # rhs[phy_indices][phy_condition] = 0
    # eco[phy_indices][phy_condition] = 0

    # any additional sources
    rhs[phy_indices] += phy_source
    rhs[res_indices] += res_source

    if include_zoo:
        rhs[zoo_indices] += zoo_source

    return rhs
ax[1].set_xticks([])

#########################

# ZOOPLANKTON

zoo_indices = [1]  # Just plot Z^l (Z2)

zoo_indiv_alpha = 0.6
zoo_total_alpha = 0.8

zoo_indiv_lw = 1.5
zoo_total_lw = 1.0

zoo = helpers.get_last_n_years(
    plotter.eco_in[helpers.eco_indices('zoo', params=params), :], num_years_ts)

zoo_legend_kw = {
    'fontsize': 11,
    'loc': 'upper center',
    'bbox_to_anchor': (0.5, -0.3),
    'ncol': 3
}

zoo_colors = ['#0033cb', '#008800']

zoo_labels = ['Z$^l$', 'Total Z']

for i in range(len(zoo_indices)):
    ax[2].plot(t_y,
               zoo[zoo_indices[i], :],