Source code for farmingpy.apsim.ensemble

from . import apsim
import numpy as np
import pandas as pd
import joblib
from typing import Union
import Models
from Models.TwinYields import ModelEnsemble, SimulationEnsemble
from Models.Core import IModel
from dataclasses import dataclass

@dataclass
class Fertilizer:
    """
    A class representing a fertilizer with its nitrogen content.

    Attributes:
        no3 (float): The nitrate content of the fertilizer in units of kilograms per hectare.
        nh4 (float): The ammonia content of the fertilizer in units of kilograms per hectare.
    """
    no3: float = 0.0
    nh4: float = 0.0


class Report(object):
    """Reporting class for APSIM model ensembles"""

    def __init__(self, ensemble):
        """
        Parameters
        ----------

        ensemble
            Model ensemble used for reporting
        """
        self.en = ensemble
        self.report_PMF = False
        self.report_RG = False

        if self.en.Plants is not None:
            self.Plants = self.en.Plants
            self.Grains = self.en.Grains
            self.Leaves = self.en.Leaves
            self.Stems = self.en.Stems
            self.Spikes = self.en.Spikes
            self.report_PMF = True

        if self.en.AGPRyegrass is not None:
            self.AGPRyegrass = self.en.AGPRyegrass
            self.report_RG = True

        self.data = []

    @property
    def today(self):
        """
        Returns
        -------
        np.datetime64
            Current simulation date
        """
        return self.en.today

    @property
    def dataframe(self):
        """
        Returns
        -------
        DataFrame
            Simulated outputs
        """
        df = pd.concat([pd.DataFrame.from_dict(d) for d in self.data])
        return df.reset_index(drop=True)

    def grain_protein(self, Grain):
        """Calculate grain protein %, equation from APSIM"""

        gn = Grain.Live.N + Grain.Dead.N
        gwt = Grain.Live.Wt + Grain.Dead.Wt
        if gwt > 0:
            return (gn/gwt) * 100 * 5.71
        else:
            return 0

    def report(self):
        """
        Called on each timestep in the ensemble, used to store
        simulated values.
        """
        if self.report_PMF:
            self.data.append({
                "idx" : [idx for idx in range(self.en.N)],
                "date" : [self.today for _ in range(self.en.N)],
                "LAI" : [p.LAI for p in self.Plants],
                "Yield" : [g.Wt*10 for g in self.Grains],
                "Biomass" : [p.AboveGround.Wt for p in self.Plants],
                "Grain_protein" : [self.grain_protein(g) for g in self.Grains],
                "Crop_AboveGroundNKgha" : [p.AboveGround.N*10 for p in self.Plants],
                "N_stress" : [l.Fn for l in self.Leaves],
                "W_stress" : [l.Fw for l in self.Leaves],
                "N_TotalPlantSupply" : [p.Arbitrator.N.TotalPlantSupply for p in self.Plants],
                "N_TotalPlantDemand" : [p.Arbitrator.N.TotalPlantDemand for p in self.Plants],
                "DM_NutrientLimitation" : [p.Arbitrator.DM.NutrientLimitation for p in self.Plants]
            })
        if self.report_RG:
            self.data.append({
                "idx" : [idx for idx in range(self.en.N)],
                "date" : [self.today for _ in range(self.en.N)],
                "LAI" : [p.LAI for p in self.AGPRyegrass],
                "Biomass" : [p.AboveGround.Wt for p in self.AGPRyegrass],
            })

[docs] class APSIMXEnsemble(object): """Ensemble of APSIM simulation models. Requires custom build of APSIM from: https://github.com/mpastell/ApsimX/tree/clock_management """
[docs] def __init__(self, model : Union[str, IModel], N = 50, Ncores = -1): """ Parameters ---------- model Union[str, IModel] Base model for the ensemble. Path to .apsimx file or C# Models.Core.Simulations object Should contain single simulation with single field and needs to use TwinClock. N Number of ensemble members Ncores Number of cores to use, if -1 all physical cores are used. """ if type(model) == str: model = apsim.APSIMX(model).Model if Ncores == -1: Ncores = joblib.cpu_count(only_physical_cores=False) self.en = ModelEnsemble(model, N, Ncores) self.en.Prepare() # Find objects self.Models = self.en.Models self.Simulations = self.en.Simulations self.N = self.en.N self.Fertilisers = [sim.FindDescendant[Models.Fertiliser]() for sim in self.en.Simulations] self.WaterBalances = [sim.FindDescendant[Models.WaterModel.WaterBalance]() for sim in self.en.Simulations] self.Plants = [sim.FindDescendant[Models.PMF.Plant]() for sim in self.en.Simulations] if self.Plants[0] is None: self.Plants = None else: self.Leaves = [plant.FindChild[Models.PMF.Organs.Leaf]() for plant in self.Plants] self.Grains = [plant.FindChild[Models.PMF.Organs.ReproductiveOrgan]("Grain") for plant in self.Plants] self.Stems = [plant.FindDescendant[Models.PMF.Organs.GenericOrgan]("Stem") for plant in self.Plants] self.Spikes = [plant.FindDescendant[Models.PMF.Organs.GenericOrgan]("Spike") for plant in self.Plants] ps = [s.FindDescendant[Models.AgPasture.PastureSpecies]() for s in self.en.Simulations] if ps[0] is not None: self.AGPRyegrass = [rg for rg in ps if rg.Name=='AGPRyegrass'] if len(self.AGPRyegrass) == 0: self.AGPRyegrass = None else: self.AGPRyegrass = None self.models = [apsim.APSIMX(m) for m in self.Models] self.report = None self.fertilize_events = {} self.irrigate_events = {}
def commence(self): """Commence the simulation""" self.en.Commence() def step(self): """Proceed with one day""" self.en.Step() def done(self): """Call at the end of simulation""" self.en.Done() @property def today(self): """ Returns ------- np.datetime64 Current simulation date """ return np.datetime64(self.en.Today.ToString("yyy-MM-dd")) @property def enddate(self): """ Returns ------- np.datetime64 Simulation end date """ return np.datetime64(self.en.EndDate.ToString("yyy-MM-dd")) def fertilize_on(self, date: str, fertilizer: Fertilizer) -> None: """ Schedule a fertilization event at the specified date with the given fertilizer. Parameters ---------- date : str Date of the fertilization event in the format "yyy-MM-dd". fertilizer : Fertilizer Type and amount of fertilizer to be applied. """ self.fertilize_events[str(date)] = fertilizer def apply_fertilizer(self, fertilizer): if fertilizer.no3 > 0: [f.Apply(fertilizer.no3, Models.Fertiliser.Types.NO3N) for f in self.Fertilisers] if fertilizer.nh4 > 0: [f.Apply(fertilizer.no3, Models.Fertiliser.Types.NH4N) for f in self.Fertilisers] def irrigate_on(self, date: str, amount: float): """ Schedule an irrigation event at the specified date with the given amount. Parameters ---------- date : str Date of the irrigation event in the format "yyyy-MM-dd". amount : float Amount of water to be applied. """ self.irrigate_events[str(date)] = amount def irrigate(self, amount): for wb in self.WaterBalances: wb.Water[0] = wb.Water[0] + amount def run(self, reportclass = Report): """ Run the simulation, custom class can be used to control reporting. Parameters ---------- reportclass Class to handle reporting, see `farmingpy.apsim.Report` for the default implementation Returns ------- DataFrame Simulated outputs """ reporter = reportclass(self) self.commence() while self.today <= self.enddate: today_str = str(self.today) if today_str in self.fertilize_events: self.apply_fertilizer(self.fertilize_events[today_str]) if today_str in self.irrigate_events: self.irrigate(self.irrigate_events[today_str]) self.step() reporter.report() self.done() self.report = reporter return self.report.dataframe