From bc989f5342742b920550d815f8227024a0afa56c Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Thu, 22 May 2025 15:49:31 +0200 Subject: [PATCH 1/9] market componentn added; market.py edited --- .../management/tillage_capital/__init__.py | 1 + .../management/tillage_capital/farmer.py | 193 ++++++++++++++++++ inseeds/components/market/__init__.py | 0 inseeds/components/market/component.py | 37 ++++ inseeds/components/market/market.py | 69 +++++++ 5 files changed, 300 insertions(+) create mode 100644 inseeds/components/farming/management/tillage_capital/__init__.py create mode 100755 inseeds/components/farming/management/tillage_capital/farmer.py create mode 100644 inseeds/components/market/__init__.py create mode 100644 inseeds/components/market/component.py create mode 100644 inseeds/components/market/market.py 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..1eafcd4 --- /dev/null +++ b/inseeds/components/farming/management/tillage_capital/farmer.py @@ -0,0 +1,193 @@ +"""Farmer entity type class of inseeds_farmer_management""" + +import numpy as np + +from inseeds.components import farming + + +class Farmer(farming.Farmer): + """Farmer (Individual) entity type mixin class.""" + + 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 + + @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 + ) + + @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_yield = self.cropyield_previous / self.cropyield - 1 + + return sigmoid( + self.weight_yield * attitude_own_yield + + 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_cropyields = self.split_neighbourhood_status("cropyield") + average_soilcs = self.split_neighbourhood_status("soilc") + + # select the average of the neighbours that are using a different + # strategy + yields_diff = average_cropyields[not self.tillage] + soils_diff = average_soilcs[not self.tillage] + + # calculate the difference between the own status and the average + # status of the neighbours + if np.isnan(yields_diff): + yield_comparison = 0 + else: + yield_comparison = yields_diff / self.cropyield - 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_yield * yield_comparison + + self.weight_soil * soil_comparison + ) + + @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) + + 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 + + def update(self, t): + # call the base class update method + super().update(t) + + """Update the behaviour of the farmer based on the TPB""" + + # If strategy switch time is down to 0 calculate TPB-based strategy + # switch probability value + if self.strategy_switch_time <= 0: + 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) + + # decrease pbc after strategy switch + self.pbc = max(self.pbc - 0.25, 0.5) + + # 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 + + # set the values of the farmers attributes to the LPJmL + # variables + self.set_lpjml(attribute="tillage") + + # increase pbc if tpb is near 0.5 to learn from own experience + elif self.tpb <= 0.5 and self.tpb > 0.4: + self.pbc = min( + self.pbc + 0.25 / self.strategy_switch_duration, 1 + ) + + else: + # decrease the counter for strategy switch time each year + self.strategy_switch_time -= 1 + + +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 From 9b14c9b2747ef9c3276ffd8ead50df1c5a907521 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 2 Jul 2025 10:30:01 +0200 Subject: [PATCH 2/9] update_pbc function created in tillage farmer --- .../farming/management/tillage/farmer.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/inseeds/components/farming/management/tillage/farmer.py b/inseeds/components/farming/management/tillage/farmer.py index 1eafcd4..5bac49c 100755 --- a/inseeds/components/farming/management/tillage/farmer.py +++ b/inseeds/components/farming/management/tillage/farmer.py @@ -92,7 +92,7 @@ def social_norm(self): if self.tillage == 1: return sigmoid(0.5 - social_norm) else: - return sigmoid(social_norm - 0.5) + return sigmoid(social_norm - 0.5) def split_neighbourhood(self, attribute): """split the neighbourhood of farmers after a defined boolean attribute @@ -139,6 +139,19 @@ def split_neighbourhood_status(self, variable): second_var = np.nan return first_var, second_var + + def update_pbc(self): + + if self.tpb > 0.5: + # decrease pbc after strategy switch + self._pbc = max(self._pbc - 0.25, 0.5) + + # increase pbc if tpb is near 0.5 to learn from own experience + elif self.tpb <= 0.5 and self.tpb > 0.4: + self._pbc = min( + self._pbc + 0.25 / self.strategy_switch_duration, 1 + ) + def update(self, t): # call the base class update method @@ -154,13 +167,12 @@ def update(self, t): + self.weight_norm * self.social_norm ) * self.pbc + self.update_pbc() + if self.tpb > 0.5: # switch strategy self.tillage = int(not self.tillage) - # decrease pbc after strategy switch - self.pbc = max(self.pbc - 0.25, 0.5) - # set back counter for strategy switch self.strategy_switch_time = np.random.normal( self.strategy_switch_duration, @@ -177,12 +189,6 @@ def update(self, t): # variables self.set_lpjml(attribute="tillage") - # increase pbc if tpb is near 0.5 to learn from own experience - elif self.tpb <= 0.5 and self.tpb > 0.4: - self.pbc = min( - self.pbc + 0.25 / self.strategy_switch_duration, 1 - ) - else: # decrease the counter for strategy switch time each year self.strategy_switch_time -= 1 From 178d5924894c662f06c7e1e570974c798cd44244 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 2 Jul 2025 10:30:41 +0200 Subject: [PATCH 3/9] farmer with capital created that inherits from the tillage farmer --- .../management/tillage_capital/farmer.py | 199 ++++-------------- 1 file changed, 36 insertions(+), 163 deletions(-) diff --git a/inseeds/components/farming/management/tillage_capital/farmer.py b/inseeds/components/farming/management/tillage_capital/farmer.py index 1eafcd4..9ac0126 100755 --- a/inseeds/components/farming/management/tillage_capital/farmer.py +++ b/inseeds/components/farming/management/tillage_capital/farmer.py @@ -1,193 +1,66 @@ """Farmer entity type class of inseeds_farmer_management""" import numpy as np +import math from inseeds.components import farming -class Farmer(farming.Farmer): - """Farmer (Individual) entity type mixin class.""" +class Farmer(farming.management.tillage.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: what is the initial C? @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 - ) - + def capital(self): + self._capital = self.cropyield + return self._capital + + # TODO: Do we need this? + # @capital.setter + # def capital(value): + # self._capital = value + + # TODO: Implement new equation @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_yield = self.cropyield_previous / self.cropyield - 1 - - return sigmoid( - self.weight_yield * attitude_own_yield - + self.weight_soil * attitude_own_soil - ) + super().attitude_own_land(self) + # TODO: Impelemtn new equation @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_cropyields = self.split_neighbourhood_status("cropyield") - average_soilcs = self.split_neighbourhood_status("soilc") - - # select the average of the neighbours that are using a different - # strategy - yields_diff = average_cropyields[not self.tillage] - soils_diff = average_soilcs[not self.tillage] - - # calculate the difference between the own status and the average - # status of the neighbours - if np.isnan(yields_diff): - yield_comparison = 0 - else: - yield_comparison = yields_diff / self.cropyield - 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_yield * yield_comparison - + self.weight_soil * soil_comparison - ) - - @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) - - 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") + super().attitude_social_learning(self) - # 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 + def update_pbc(self, C, TC): - return first_var, second_var - - def update(self, t): - # call the base class update method - super().update(t) - - """Update the behaviour of the farmer based on the TPB""" - - # If strategy switch time is down to 0 calculate TPB-based strategy - # switch probability value - if self.strategy_switch_time <= 0: - self.tpb = ( - self.weight_attitude * self.attitude - + self.weight_norm * self.social_norm - ) * self.pbc + if C >= TC: if self.tpb > 0.5: - # switch strategy - self.tillage = int(not self.tillage) - # decrease pbc after strategy switch - self.pbc = max(self.pbc - 0.25, 0.5) - - # 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 - - # set the values of the farmers attributes to the LPJmL - # variables - self.set_lpjml(attribute="tillage") + self._pbc = max(self._pbc - 0.25, 0.5) # increase pbc if tpb is near 0.5 to learn from own experience elif self.tpb <= 0.5 and self.tpb > 0.4: - self.pbc = min( - self.pbc + 0.25 / self.strategy_switch_duration, 1 + self._pbc = min( + self._pbc + self.diff_pbc_income_function(C, TC) + self.diff_pbc_ts_function(ts = self.strategy_switch_duration), 1 + ) + else: + self._pbc = min( + self._pbc + self.diff_pbc_income_function(C, TC), 1 ) - else: - # decrease the counter for strategy switch time each year - self.strategy_switch_time -= 1 - + self._pbc = 0.5 + + def diff_pbc_income_function(C, TC): + alpha = 3 #shift to the right + beta = 0.5 + gamma = 6 + return 0.25 / (1 + math.exp(-gamma*((C - (alpha*TC))/(TC/beta)))) + + # TODO: needs to be revised + def diff_pbc_ts_function(ts): + return 0.25 / ts -def sigmoid(x): - """The following part contains helping stuff""" - return 0.5 * (np.tanh(x) + 1) From e08c56d6be1676595bf3197c0ded0549265e0f8d Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 11:49:51 +0200 Subject: [PATCH 4/9] new model created to run farmers with capital --- inseeds/models/01_capital_farming/__init__.py | 1 + inseeds/models/01_capital_farming/config.yaml | 67 +++++++++ inseeds/models/01_capital_farming/main.py | 27 ++++ inseeds/models/01_capital_farming/model.py | 131 ++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 inseeds/models/01_capital_farming/__init__.py create mode 100644 inseeds/models/01_capital_farming/config.yaml create mode 100755 inseeds/models/01_capital_farming/main.py create mode 100644 inseeds/models/01_capital_farming/model.py diff --git a/inseeds/models/01_capital_farming/__init__.py b/inseeds/models/01_capital_farming/__init__.py new file mode 100644 index 0000000..0dd0c1c --- /dev/null +++ b/inseeds/models/01_capital_farming/__init__.py @@ -0,0 +1 @@ +from .model import Cell, Farmer, World, Model diff --git a/inseeds/models/01_capital_farming/config.yaml b/inseeds/models/01_capital_farming/config.yaml new file mode 100644 index 0000000..02c564c --- /dev/null +++ b/inseeds/models/01_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/01_capital_farming/main.py b/inseeds/models/01_capital_farming/main.py new file mode 100755 index 0000000..c0e112c --- /dev/null +++ b/inseeds/models/01_capital_farming/main.py @@ -0,0 +1,27 @@ +import os +import argparse + +from pycoupler.coupler import LPJmLCoupler +from inseeds.models.regenerative_tillage 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/01_capital_farming/model.py b/inseeds/models/01_capital_farming/model.py new file mode 100644 index 0000000..f569269 --- /dev/null +++ b/inseeds/models/01_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( + "xxxs", + "dsfdsdfsdfsdfsdfs", + # unit="euro", + ) + ) + + +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) From c519b2004a3afdf328e540d36e9967e77b3dfe20 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 11:50:43 +0200 Subject: [PATCH 5/9] small bug fix in tillage farmer --- inseeds/components/farming/management/tillage/farmer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inseeds/components/farming/management/tillage/farmer.py b/inseeds/components/farming/management/tillage/farmer.py index 5bac49c..88abc43 100755 --- a/inseeds/components/farming/management/tillage/farmer.py +++ b/inseeds/components/farming/management/tillage/farmer.py @@ -144,12 +144,12 @@ def update_pbc(self): if self.tpb > 0.5: # decrease pbc after strategy switch - self._pbc = max(self._pbc - 0.25, 0.5) + self.pbc = max(self.pbc - 0.25, 0.5) # increase pbc if tpb is near 0.5 to learn from own experience elif self.tpb <= 0.5 and self.tpb > 0.4: - self._pbc = min( - self._pbc + 0.25 / self.strategy_switch_duration, 1 + self.pbc = min( + self.pbc + 0.25 / self.strategy_switch_duration, 1 ) From 2fdf7137180c45483eb5ac558a29caf9eabf61b3 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 16:21:16 +0200 Subject: [PATCH 6/9] model renamed to capital_farming, small changes in the model --- .../{01_capital_farming => capital_farming}/__init__.py | 0 .../{01_capital_farming => capital_farming}/config.yaml | 0 .../models/{01_capital_farming => capital_farming}/main.py | 2 +- .../{01_capital_farming => capital_farming}/model.py | 7 ++++--- 4 files changed, 5 insertions(+), 4 deletions(-) rename inseeds/models/{01_capital_farming => capital_farming}/__init__.py (100%) rename inseeds/models/{01_capital_farming => capital_farming}/config.yaml (100%) rename inseeds/models/{01_capital_farming => capital_farming}/main.py (92%) rename inseeds/models/{01_capital_farming => capital_farming}/model.py (96%) diff --git a/inseeds/models/01_capital_farming/__init__.py b/inseeds/models/capital_farming/__init__.py similarity index 100% rename from inseeds/models/01_capital_farming/__init__.py rename to inseeds/models/capital_farming/__init__.py diff --git a/inseeds/models/01_capital_farming/config.yaml b/inseeds/models/capital_farming/config.yaml similarity index 100% rename from inseeds/models/01_capital_farming/config.yaml rename to inseeds/models/capital_farming/config.yaml diff --git a/inseeds/models/01_capital_farming/main.py b/inseeds/models/capital_farming/main.py similarity index 92% rename from inseeds/models/01_capital_farming/main.py rename to inseeds/models/capital_farming/main.py index c0e112c..cc30fd7 100755 --- a/inseeds/models/01_capital_farming/main.py +++ b/inseeds/models/capital_farming/main.py @@ -2,7 +2,7 @@ import argparse from pycoupler.coupler import LPJmLCoupler -from inseeds.models.regenerative_tillage import Model +from inseeds.models.capital_farming import Model def run_inseeds(config_file): diff --git a/inseeds/models/01_capital_farming/model.py b/inseeds/models/capital_farming/model.py similarity index 96% rename from inseeds/models/01_capital_farming/model.py rename to inseeds/models/capital_farming/model.py index f569269..5ae938c 100644 --- a/inseeds/models/01_capital_farming/model.py +++ b/inseeds/models/capital_farming/model.py @@ -65,9 +65,10 @@ class Farmer(tillage_capital.Farmer): ), # TODO: implement euro/dollar in copan:CORE capital=Variable( - "xxxs", - "dsfdsdfsdfsdfsdfs", - # unit="euro", + "farmers capital", + "farmers capital based on income and costs", + datatype=float, + unit="euro", ) ) From def2862b1fa75c3daaa206c830d881c22d3b56a1 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 17:43:11 +0200 Subject: [PATCH 7/9] unit euro deleted --- inseeds/models/capital_farming/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/inseeds/models/capital_farming/model.py b/inseeds/models/capital_farming/model.py index 5ae938c..27b8b41 100644 --- a/inseeds/models/capital_farming/model.py +++ b/inseeds/models/capital_farming/model.py @@ -68,7 +68,6 @@ class Farmer(tillage_capital.Farmer): "farmers capital", "farmers capital based on income and costs", datatype=float, - unit="euro", ) ) From 9c0b500ba5680b75b729a0258328a65f5a505c3d Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 17:43:37 +0200 Subject: [PATCH 8/9] changed tillage farmer back to normal implementation --- .../farming/management/tillage/farmer.py | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/inseeds/components/farming/management/tillage/farmer.py b/inseeds/components/farming/management/tillage/farmer.py index 88abc43..95d9811 100755 --- a/inseeds/components/farming/management/tillage/farmer.py +++ b/inseeds/components/farming/management/tillage/farmer.py @@ -92,7 +92,7 @@ def social_norm(self): if self.tillage == 1: return sigmoid(0.5 - social_norm) else: - return sigmoid(social_norm - 0.5) + return sigmoid(social_norm - 0.5) def split_neighbourhood(self, attribute): """split the neighbourhood of farmers after a defined boolean attribute @@ -139,19 +139,6 @@ def split_neighbourhood_status(self, variable): second_var = np.nan return first_var, second_var - - def update_pbc(self): - - if self.tpb > 0.5: - # decrease pbc after strategy switch - self.pbc = max(self.pbc - 0.25, 0.5) - - # increase pbc if tpb is near 0.5 to learn from own experience - elif self.tpb <= 0.5 and self.tpb > 0.4: - self.pbc = min( - self.pbc + 0.25 / self.strategy_switch_duration, 1 - ) - def update(self, t): # call the base class update method @@ -167,12 +154,13 @@ def update(self, t): + self.weight_norm * self.social_norm ) * self.pbc - self.update_pbc() - if self.tpb > 0.5: # switch strategy self.tillage = int(not self.tillage) + # decrease pbc after strategy switch + self.pbc = max(self.pbc - 0.25, 0.5) + # set back counter for strategy switch self.strategy_switch_time = np.random.normal( self.strategy_switch_duration, @@ -189,6 +177,12 @@ def update(self, t): # variables self.set_lpjml(attribute="tillage") + # increase pbc if tpb is near 0.5 to learn from own experience + elif self.tpb <= 0.5 and self.tpb > 0.4: + self.pbc = min( + self.pbc + 0.25 / self.strategy_switch_duration, 1 + ) + else: # decrease the counter for strategy switch time each year self.strategy_switch_time -= 1 @@ -196,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 From f84c3306c31b8d49c7959154ecac75c89f678c97 Mon Sep 17 00:00:00 2001 From: MaRimm31 Date: Wed, 6 Aug 2025 17:44:23 +0200 Subject: [PATCH 9/9] finalization farmer with money (model run was successful) --- .../management/tillage_capital/farmer.py | 253 +++++++++++++++--- 1 file changed, 218 insertions(+), 35 deletions(-) diff --git a/inseeds/components/farming/management/tillage_capital/farmer.py b/inseeds/components/farming/management/tillage_capital/farmer.py index 9ac0126..defe3ff 100755 --- a/inseeds/components/farming/management/tillage_capital/farmer.py +++ b/inseeds/components/farming/management/tillage_capital/farmer.py @@ -5,62 +5,245 @@ from inseeds.components import farming - -class Farmer(farming.management.tillage.Farmer): +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 - self.capital = 1000 # TODO: what is the initial C? + # initialize previous cropyield + self.cropyield_previous = self.cropyield - @property - def capital(self): - self._capital = self.cropyield - return self._capital + # 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 - # TODO: Do we need this? - # @capital.setter - # def capital(value): - # self._capital = value + # ### Capital ### - # TODO: Implement new equation + 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): - super().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 + ) - # TODO: Impelemtn new equation @property def attitude_social_learning(self): - super().attitude_social_learning(self) + """Calculate the attitude of the farmer through social learning based + on the comparison to neighbours using a different strategy""" - def update_pbc(self, C, TC): + # 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") - if C >= TC: + # 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] - if self.tpb > 0.5: - # decrease pbc after strategy switch - self._pbc = max(self._pbc - 0.25, 0.5) + # 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 - # increase pbc if tpb is near 0.5 to learn from own experience - elif self.tpb <= 0.5 and self.tpb > 0.4: - self._pbc = min( - self._pbc + self.diff_pbc_income_function(C, TC) + self.diff_pbc_ts_function(ts = self.strategy_switch_duration), 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: - self._pbc = min( - self._pbc + self.diff_pbc_income_function(C, TC), 1 - ) + 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: - self._pbc = 0.5 + return sigmoid(social_norm - 0.5) + + # Perceived Behaviour Control # - def diff_pbc_income_function(C, TC): + 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*((C - (alpha*TC))/(TC/beta)))) - - # TODO: needs to be revised - def diff_pbc_ts_function(ts): - return 0.25 / ts + 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)