diff --git a/inseeds/components/farming/management/tillage/farmer.py b/inseeds/components/farming/management/tillage/farmer.py index 1eafcd4..95d9811 100755 --- a/inseeds/components/farming/management/tillage/farmer.py +++ b/inseeds/components/farming/management/tillage/farmer.py @@ -190,4 +190,4 @@ def update(self, t): def sigmoid(x): """The following part contains helping stuff""" - return 0.5 * (np.tanh(x) + 1) + return 0.5 * (np.tanh(x) + 1) \ No newline at end of file diff --git a/inseeds/components/farming/management/tillage_capital/__init__.py b/inseeds/components/farming/management/tillage_capital/__init__.py new file mode 100644 index 0000000..16372ce --- /dev/null +++ b/inseeds/components/farming/management/tillage_capital/__init__.py @@ -0,0 +1 @@ +from .farmer import Farmer diff --git a/inseeds/components/farming/management/tillage_capital/farmer.py b/inseeds/components/farming/management/tillage_capital/farmer.py new file mode 100755 index 0000000..defe3ff --- /dev/null +++ b/inseeds/components/farming/management/tillage_capital/farmer.py @@ -0,0 +1,249 @@ +"""Farmer entity type class of inseeds_farmer_management""" + +import numpy as np +import math + +from inseeds.components import farming + +class Farmer(farming.Farmer): + + def __init__(self, **kwargs): + """Initialize an instance of Farmer.""" + super().__init__(**kwargs) # must be the first line + + # initialize previous soilc + self.soilc_previous = self.soilc + + # initialize previous cropyield + self.cropyield_previous = self.cropyield + + # Randomize switch time at beginning of simulation to avoid + # synchronization of agents + self.strategy_switch_time = np.random.randint( + 0, self.strategy_switch_duration + ) + + # initialize tbp for meaningful output + self.tpb = 0 + + self.capital = 1000 # TODO: normalverteilung um mean wert der in config.yml festgelgt wird, abhängig vom cropyield? Ernte aus 23 in Geld übersetzen + + self.transition_costs = 1000 # TODO: normalverteilung um mean wert der in config.yml festgelt wird + + # initialize previous capital + self.capital_previous = self.capital + + # ### Capital ### + + def update_capital(self): + self.capital = self.cropyield + + + # ### Theory of Planned Behavior ### + + # Attitude # + @property + def attitude(self): + """Calculate the attitude of the farmer following the TPB""" + return ( + self.weight_social_learning * self.attitude_social_learning + + self.weight_own_land * self.attitude_own_land + ) + # TODO: be mor strict in using properties and setters? + # TODO: Check equations again and think about it + + + @property + def attitude_own_land(self): + """Calculate the attitude of the farmer based on their own land""" + # compare own soil and yield to previous values + attitude_own_soil = self.soilc_previous / self.soilc - 1 + attitude_own_capital= self.capital_previous / self.capital - 1 + + return sigmoid( + self.weight_capital * attitude_own_capital + + self.weight_soil * attitude_own_soil + ) + + @property + def attitude_social_learning(self): + """Calculate the attitude of the farmer through social learning based + on the comparison to neighbours using a different strategy""" + + # split variables (crop yield, soilc) status of neighbours into groups + # of different strategies applied and average them + average_capitals = self.split_neighbourhood_status("capital") + average_soilcs = self.split_neighbourhood_status("soilc") + + # select the average of the neighbours that are using a different + # strategy and have a switch time <= 0 (only farmers that have + # switched a while ago) + capitals_diff = average_capitals[(not self.tillage) & (self.strategy_switch_time <= 0)] + soils_diff = average_soilcs[not self.tillage] + + # calculate the difference between the own status and the average + # status of the neighbours + if np.isnan(capitals_diff): + capital_comparison = 0 + else: + capital_comparison = capitals_diff / self.capital - 1 + + if np.isnan(soils_diff): + soil_comparison = 0 + else: + soil_comparison = soils_diff / self.soilc - 1 + + # calculate the attitude of social learning based on the comparison + return sigmoid( + self.weight_capital * capital_comparison + + self.weight_soil * soil_comparison + ) + + def split_neighbourhood(self, attribute): + """split the neighbourhood of farmers after a defined boolean attribute + (e.g. tillage) + """ + # init split into two neighbourhood lists + first_nb = [] + second_nb = [] + + # split the neighbourhood into two groups based on the attribute + # of the neighbours + for neighbour in self.neighbourhood: + if getattr(neighbour, attribute) == 0: + first_nb.append(neighbour) + else: + second_nb.append(neighbour) + return first_nb, second_nb + + def split_neighbourhood_status(self, variable): + """split the neighbourhood of farmers after a defined attribute + (tillage) and calculate the average of each group + """ + # split the neighbourhood into two groups based on the behaviour + first_nb, second_nb = self.split_neighbourhood("tillage") + + # calculate the average of the variable for first group + if first_nb: + first_var = sum(getattr(n, variable) for n in first_nb) / len( + first_nb + ) + # if there are no neighbours of the same strategy, set the average + # to 0 + else: + first_var = np.nan + + # calculate the average of the variable for second group + if second_nb: + second_var = sum(getattr(n, variable) for n in second_nb) / len( + second_nb + ) + # if there are no neighbours of the same strategy, set the average + # to 0 + else: + second_var = np.nan + + return first_var, second_var + + # Social Norm # + + @property + def social_norm(self): + """Calculate the social norm of the farmer based on the majority + behaviour of the neighbours""" + social_norm = 0 + if self.neighbourhood: + social_norm = sum(n.tillage for n in self.neighbourhood) / len( + self.neighbourhood + ) + if self.tillage == 1: + return sigmoid(0.5 - social_norm) + else: + return sigmoid(social_norm - 0.5) + + # Perceived Behaviour Control # + + def update_pbc_capital(self): + """Calculate the perceived behaviour control based on the available capital""" + # if capital is enough to cover the transition costs, pbc is increased by a sigmoid function + # otherwise pbc is set to 0.5 + if self.capital >= self.transition_costs: + self.pbc = min( + self.pbc + self.diff_pbc_capital_function(), 1 + ) + else: + return 0.5 + + def diff_pbc_capital_function(self): + """Calculate PBC boost based on capital vs transition costs""" + alpha = 3 #shift to the right + beta = 0.5 + gamma = 6 + return 0.25 / (1 + math.exp(-gamma*((self.capital - (alpha*self.transition_costs))/(self.transition_costs/beta)))) + + def diff_pbc_ts_function(self): + """Calculate PBC boost based on time since last switch""" + if self.strategy_switch_time <= 0: + return 0.25 + return 0.25 / self.strategy_switch_time + + # ### Update ### + + def update(self, t): + # call the base class update method + super().update(t) + + # update capital in every year based on the income and costs + self.update_capital() + + # If strategy switch time is down to 0 calculate TPB-based strategy + # switch probability value + if self.strategy_switch_time <= 0: + + self.update_pbc_capital() + + self.tpb = ( + self.weight_attitude * self.attitude + + self.weight_norm * self.social_norm + ) * self.pbc + + + if self.tpb > 0.5: + # switch strategy + self.tillage = int(not self.tillage) + + self.pbc = max(self.pbc - 0.25, 0.5) + + self.capital = self.capital - self.transition_costs + + # set back counter for strategy switch + self.strategy_switch_time = np.random.normal( + self.strategy_switch_duration, + round(self.strategy_switch_duration / 2), + ) + + # freeze the current soilc and cropyield values that were used + # for the decision making in the next evaluation after + # self.strategy_switch_duration + self.cropyield_previous = self.cropyield + self.soilc_previous = self.soilc + self.capital_previous = self.capital # not sure if this is necessary + + # set the values of the farmers attributes to the LPJmL + # variables + self.set_lpjml(attribute="tillage") + + elif self.tpb <= 0.5 and self.tpb > 0.4: + self.pbc = min( + self.pbc + self.diff_pbc_ts_function(), 1 + ) + + else: + # decrease the counter for strategy switch time each year + self.strategy_switch_time -= 1 + +# ### Helper Functions ### + +def sigmoid(x): + """The following part contains helping stuff""" + return 0.5 * (np.tanh(x) + 1) diff --git a/inseeds/components/market/__init__.py b/inseeds/components/market/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inseeds/components/market/component.py b/inseeds/components/market/component.py new file mode 100644 index 0000000..e477a62 --- /dev/null +++ b/inseeds/components/market/component.py @@ -0,0 +1,37 @@ +from inseeds.components import base + + +class Component(base.Component): + """Model mixing class for farmer_management. + This component initializes farmers in the model to make decisions + on which management practices to apply to their fields. + Two practices are available: conventional and conservation tillage. + The theory of planned behaviour is used to model farmer decision-making. + Two farmer AFTs are implemented, the traditionalist and the pioneer. + """ + # TODO: muss geändert werden + def init_market(self, market_class, **kwargs): + """Initialize farmers.""" + farmers = [] + + for cell in self.world.cells: + if cell.output.cftfrac.sum("band") == 0: + continue + + farmer = farmer_class(cell=cell, model=self) + farmers.append(farmer) + + farmers_sorted = sorted(farmers, key=lambda farmer: farmer.avg_hdate) + for farmer in farmers_sorted: + farmer.init_neighbourhood() + + # self.world.farmers = set(farmers_sorted + + def update(self, t): + super().update(t) + + farmers_sorted = sorted( + self.world.farmers, key=lambda farmer: farmer.avg_hdate + ) + for farmer in farmers_sorted: + farmer.update(t) \ No newline at end of file diff --git a/inseeds/components/market/market.py b/inseeds/components/market/market.py new file mode 100644 index 0000000..1a55f9b --- /dev/null +++ b/inseeds/components/market/market.py @@ -0,0 +1,69 @@ +"""Farmer entity type class of inseeds_farmer_management""" + +# This file is part of pycopancore. +# +# Copyright (C) 2016-2017 by COPAN team at Potsdam Institute for Climate +# Impact Research +# +# URL: +# Contact: core@pik-potsdam.de +# License: BSD 2-clause license +import numpy as np + +import pycopancore.model_components.base as core +import inseeds.components.base as base + +# TODO +class Market(core.Individual, base.Individual): + """Farmer (Individual) entity type mixin class.""" + + # standard methods: + def __init__(self, **kwargs): + """Initialize an instance of Farmer.""" + super().__init__(**kwargs) # must be the first line + + # initialize the Market specific attributes # TO-DO + + + # average harvest date of the cell is used as a proxy for the order + # of the agents making decisions in time through the year + # self.avg_hdate = self.cell_avg_hdate # Zeitpunkt pro Jahr um Harvest zu verkaufen + + # soilc is the last "measured" soilc value of the farmer whereas the + # cell_soilc value is the actual status of soilc of the cell + # self.soilc = self.cell_soilc + + #def init_neighbourhood(self): + # """Initialize the neighbourhood of the agent.""" + # self.neighbourhood = [ + # neighbour + # for cell_neighbours in self.cell.neighbourhood + # if len(cell_neighbours.individuals) > 0 + # for neighbour in cell_neighbours.individuals + # ] + + @property + # TODO: Abfrage ob Selbst auf Zelle oder Selbst auf Country + def farmers(self): + """Return the set of all farmers.""" + farmers = { + farmer + for farmer in self.cell.individuals + if farmer.__class__.__name__ == "Farmer" # noqa + } + return farmers + + @property + def farmer(self): + """Return the first farmer.""" + farmers = self.farmers + if len(farmers) == 0: + return None + return list(farmers)[0] + + # TODO: Füll update function + def update(self, t): + super().update(t) + + if self.control_run: + return diff --git a/inseeds/models/capital_farming/__init__.py b/inseeds/models/capital_farming/__init__.py new file mode 100644 index 0000000..0dd0c1c --- /dev/null +++ b/inseeds/models/capital_farming/__init__.py @@ -0,0 +1 @@ +from .model import Cell, Farmer, World, Model diff --git a/inseeds/models/capital_farming/config.yaml b/inseeds/models/capital_farming/config.yaml new file mode 100644 index 0000000..02c564c --- /dev/null +++ b/inseeds/models/capital_farming/config.yaml @@ -0,0 +1,67 @@ +# Settings for LPJmL specifically for coupling (pycoupler) +# (not covered by LPJmL's direct config) +lpjml_settings: + country_code_to_name: true + iso_country_code: true + +# Variables to be written to copan_core_data table file +output: + farmer: + - "aft_id" + - "tillage" + - "tpb" + - "pbc" + - "social_norm" + - "attitude" + - "attitude_own_land" + - "attitude_social_learning" + - "soilc" + - "cropyield" + +# Define how copan_core_data table file should be written +output_settings: + write_lon_lat: true + file_format: "csv" # "parquet" "csv" + +# Define which farmer variables map with coupled LPJmL input variables +coupling_map: + tillage: ["with_tillage"] + # residues: ["residue_on_field"] + +control_run: False +pioneer_share: 0.25 + +# Analogous to LPJmL pftpar, define the AFT parameters for the two different +# farmer types +aftpar: + # AFT for conservative/traditional values following farmer tending to stay + # with conventional agriculture + traditionalist: + pbc: 0.75 + weight_attitude: 0.6 + weight_yield: 0.8 + weight_capital: 0.8 + weight_soil: 0.4 + weight_norm: 0.4 + weight_social_learning: 0.4 + weight_own_land: 0.6 + # duration of waiting time before switching to another strategy + strategy_switch_duration: 10 # years + capital: 1000 + transition_cost: 700 + + # AFT for pioneer farmer who more likely tends to switch to new (promising) + # regenerative agriculture practices + pioneer: + pbc: 0.95 + weight_attitude: 0.8 + weight_yield: 0.4 + weight_capital: 0.4 + weight_soil: 0.8 + weight_norm: 0.2 + weight_social_learning: 0.6 + weight_own_land: 0.4 + # duration of waiting time before switching to another strategy + strategy_switch_duration: 10 # years + capital: 1000 + transition_cost: 1500 diff --git a/inseeds/models/capital_farming/main.py b/inseeds/models/capital_farming/main.py new file mode 100755 index 0000000..cc30fd7 --- /dev/null +++ b/inseeds/models/capital_farming/main.py @@ -0,0 +1,27 @@ +import os +import argparse + +from pycoupler.coupler import LPJmLCoupler +from inseeds.models.capital_farming import Model + + +def run_inseeds(config_file): + """Run the INSEEDS model with the given configuration file""" + if not os.path.exists(config_file): + raise FileNotFoundError(f"{config_file} does not exist") + + model = Model(config_file=config_file) + + for year in model.lpjml.get_sim_years(): + model.update(year) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("config_file", help="Path to the configuration file") + args = parser.parse_args() + + run_inseeds(args.config_file) + +# execute program via +# python inseeds.py /path/to/config_coupled_fn.json diff --git a/inseeds/models/capital_farming/model.py b/inseeds/models/capital_farming/model.py new file mode 100644 index 0000000..27b8b41 --- /dev/null +++ b/inseeds/models/capital_farming/model.py @@ -0,0 +1,131 @@ +import pycopancore.model_components.base as core +from pycopancore.data_model.variable import Variable +from pycopancore.data_model.master_data_model.dimensions_and_units import ( + DimensionsAndUnits as DAU, +) + +from inseeds.components import base +from inseeds.components import farming +from inseeds.components.farming.management import tillage_capital +from inseeds.components import lpjml + + +class Farmer(tillage_capital.Farmer): + """Farmer entity type.""" + + output_variables = base.Output( + aft_id=Variable("AFT ID", "unique identifier for agent"), + avg_hdate=Variable( + "average harvest date", + "weighted average harvest date of grown crops (by crop area)", + unit=DAU.doy, + ), + soilc=Variable( + "soil organic carbon", + "soil organic carbon content of agent land", + unit=DAU.gC_per_m2, + ), + cropyield=Variable( + "average crop yield", + "average crop yield of agent land weighted by crop area", + unit=DAU.gC_per_m2, + ), + tillage=Variable( + "agent tillage behaviour", + "conventional=1, conservation=0", + datatype=bool, + ), + pbc=Variable( + "perceived behavioural control", + "own appraisal of how much efficacy agent posesses", + ), + tpb=Variable( + "theory of planned behaviour", + "attitude, subjective norm, perceived behavioural control", + ), + social_norm=Variable( + "social norm", + "social norm based on observation of own and\ + neighboring land", + ), + attitude=Variable( + "attitude", + "farmer attitude based on observation of yield and soilC of\ + own land and neighboring land", + ), + attitude_own_land=Variable( + "attitude towards own land", + "attitude based on observation of yield and soilC\ + of own land", + ), + attitude_social_learning=Variable( + "attitude based on social learning", + "attitude based on observation of yield and\ + soilC of neighboring land", + ), + # TODO: implement euro/dollar in copan:CORE + capital=Variable( + "farmers capital", + "farmers capital based on income and costs", + datatype=float, + ) + ) + + +class Cell(lpjml.Cell, farming.Cell): + """Cell entity type.""" + + pass + + +class World(lpjml.World, farming.World): + """World entity type.""" + + pass + + +class Model(lpjml.Component, farming.Component): + """Model class for the InSEEDS Social model integrating the LPJmL model and + coupling component as well as the farmer management component. + """ + + name = "InSEEDS farmer management" + description = "InSEEDS farmer management model representing only social \ + dynamics and decision-making on the basis of the TPB" + + def __init__(self, **kwargs): + """Initialize an instance of World.""" + # Initialize the parent classes first + super().__init__(**kwargs) + + # Ensure self.lpjml is initialized before accessing it + if not hasattr(self, "lpjml") or self.lpjml is None: + raise ValueError("lpjml must be initialized in the parent class.") + + # initialize LPJmL world + self.world = World( + model=self, + input=self.lpjml.read_input(), + output=self.lpjml.read_historic_output().isel(time=[-1]), + grid=self.lpjml.grid, + country=self.lpjml.country, + area=self.lpjml.terr_area, + ) + + # initialize cells + self.init_cells(cell_class=Cell) + + # initialize farmers + self.init_farmers(farmer_class=Farmer) + + self.write_output_table( + init=True, + file_format=self.config.coupled_config.output_settings.file_format, + ) + + def update(self, t): + super().update(t) + self.write_output_table( + file_format=self.config.coupled_config.output_settings.file_format + ) + self.update_lpjml(t)