Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CropGym update for new PCSE version #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: ['3.11']
python-version: ['3.9']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest pytest-cov
pip uninstall -y --quiet pcse
git clone https://github.com/ajwdewit/pcse.git /tmp/pcse
cd /tmp/pcse && git switch develop_WOFOST_v8_1 && git apply --ignore-space-change --ignore-whitespace ${GITHUB_WORKSPACE}/notebooks/nitrogen-winterwheat/pcse-lintul3.patch && pip install --quiet -e . && cd ${GITHUB_WORKSPACE}
pip install stable_baselines3 sb3-contrib gymnasium lib_programname tensorboard scipy tqdm pandas numpy
cd /tmp/pcse && git switch develop_wofost81 && git reset --hard 67a3d0c3a46280bddcf7617de3b27c82e5cb0a40 && pip install --quiet -e . && cd ${GITHUB_WORKSPACE}
pip install dotmap
pip install stable_baselines3 sb3-contrib
pip install gymnasium==0.28.1 lib_programname tensorboard scipy tqdm pandas numpy
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
_notebooks_old/nitrogen-winterwheat/tensorboard_logs
_notebooks_old/nitrogen-winterwheat/.ipynb_checkpoints
.pyc
/comet/
347 changes: 347 additions & 0 deletions evaluate_agent.py

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions notebooks/nitrogen-winterwheat/wofost_results_lt.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"IDCROP_PARAMETRIZATION","IDSEASON","DECADE","IDGRID","POTENTIAL_YIELD_BIOMASS","POTENTIAL_YIELD_STORAGE","WATER_LIM_YIELD_BIOMASS","WATER_LIM_YIELD_STORAGE","POTENTIAL_LEAF_AREA_INDEX","WATER_LIM_LEAF_AREA_INDEX","DEVELOPMENT_STAGE","RELATIVE_SOIL_MOISTURE","TOTAL_WATER_CONSUMPTION","TOTAL_WATER_REQUIREMENT","FSM","FSMUR","LEAVES_DIED_BY_COLD","RUNOFF","SOIL_EVAPORATION","LOSS_TO_SUBSOIL"
90,2023,20220101,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220102,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220103,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220201,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220202,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220203,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220301,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220302,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220303,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220401,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220402,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220403,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220501,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220502,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220503,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220601,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220602,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220603,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220701,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220702,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220703,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220801,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220802,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220803,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220901,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220902,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20220903,1125143,0,0,0,0,0,0,0,0,0,0,0,0,,0,0,0
90,2023,20221001,1125143,44.025,0,44.025,0,0.072,0.072,0,86.06,0.019,0.019,0.3,0.25,,0,0.49,0.048
90,2023,20221002,1125143,74.128,0,74.128,0,0.122,0.122,0,79.2018,0.057,0.057,0.29,0.25,,0,0.705,0.052
90,2023,20221003,1125143,108.164,0,108.164,0,0.179,0.179,0,85.0645,0.099,0.099,0.29,0.25,,0,0.905,0.107
90,2023,20221101,1125143,133.438,0,133.438,0,0.22,0.22,1,89.594,0.133,0.133,0.3,0.26,,0,1.04,0.229
90,2023,20221102,1125143,143.656,0,143.656,0,0.237,0.237,1,88.3602,0.17,0.17,0.3,0.26,,0,1.222,0.241
90,2023,20221103,1125143,143.656,0,143.656,0,0.237,0.237,1,87.3306,0.191,0.191,0.29,0.26,,0,1.342,0.314
90,2023,20221201,1125143,143.656,0,143.656,0,0.237,0.237,1,88.6169,0.213,0.213,0.29,0.27,,0,1.489,0.537
90,2023,20221202,1125143,143.656,0,143.656,0,0.237,0.237,1,90.7424,0.222,0.222,0.3,0.22,,0,1.512,1.059
90,2023,20221203,1125143,143.656,0,143.656,0,0.237,0.237,1,94.3761,0.235,0.235,0.3,0.22,,0,1.605,2.229
90,2023,20230101,1125143,145.763,0,145.763,0,0.241,0.241,1,92.3939,0.264,0.264,0.3,0,,0,1.793,4.762
90,2023,20230102,1125143,147.422,0,147.422,0,0.243,0.243,1,92.9396,0.297,0.297,0.3,0,,0,1.981,6.755
90,2023,20230103,1125143,147.422,0,147.422,0,0.243,0.243,1,93.8225,0.333,0.333,0.31,0,,0,2.096,7.205
90,2023,20230201,1125143,147.422,0,147.422,0,0.243,0.243,1,94.4353,0.375,0.375,0.31,0,,0,2.193,7.477
90,2023,20230202,1125143,147.422,0,147.422,0,0.243,0.243,2,96.4911,0.426,0.426,0.32,0,,0,2.374,8.465
90,2023,20230203,1125143,147.422,0,147.422,0,0.243,0.243,2,93.4095,0.479,0.479,0.31,0,,0,2.641,8.853
90,2023,20230301,1125143,147.422,0,147.422,0,0.243,0.243,2,94.5014,0.564,0.564,0.31,0,,0,2.825,9.14
90,2023,20230302,1125143,151.195,0,151.195,0,0.25,0.25,5,93.5602,0.677,0.677,0.31,0,,0,3.117,9.979
90,2023,20230303,1125143,208.74,0,208.74,0,0.342,0.342,10,94.5868,0.849,0.849,0.31,0,,0,3.716,10.479
90,2023,20230401,1125143,220.361,0,220.361,0,0.362,0.362,14,93.3386,1.07,1.07,0.31,0,,0,4.211,10.493
90,2023,20230402,1125143,507.517,0,507.517,0,0.86,0.86,24,91.7317,1.584,1.584,0.31,0,,0,4.832,10.493
90,2023,20230403,1125143,1543.478,0,1543.478,0,2.59,2.59,36,84.7832,3.052,3.052,0.3,0,,0,5.288,10.493
90,2023,20230501,1125143,1983.533,0,1983.533,0,3.266,3.266,45,71.5264,5.391,5.39,0.27,0,,0,5.382,10.493
90,2023,20230502,1125143,3830.204,0,3821.027,0,5.183,5.172,63,57.7279,8.4858,8.498,0.25,0,,0,5.522,10.493
90,2023,20230503,1125143,7281.584,0,7121.601,0,6.691,6.59,85,38.662,12.5484,12.73,0.23,0,,0,5.559,10.493
90,2023,20230601,1125143,10515.345,855.815,9508.649,444.633,6.466,6.178,104,21.7454,15.773,16.98,0.2,0,,0,5.623,10.493
90,2023,20230602,1125143,13433.53,3774.001,11035.749,1971.733,6.123,5.522,126,21.4759,18.138,20.981,0.2,0,,0,5.803,10.493
90,2023,20230603,1125143,16210.72,6551.191,12204.994,3140.978,5.893,4.843,150,19.6569,20.3424,25.12,0.19,0,,0,6.103,10.493
90,2023,20230701,1125143,18268.085,8608.556,13063.877,3999.861,4.062,3.782,172,14.3353,22.2047,28.684,0.19,0,,0,6.523,10.493
90,2023,20230702,1125143,19032.086,9372.557,13176.751,4112.735,1.397,1.313,195,10.3989,23.3512,31.945,0.18,0,,0,7.256,10.493
90,2023,20230703,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,12.737,23.5833,32.315,0.18,0,,0,9.178,10.493
90,2023,20230801,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,24.6234,23.5833,32.315,0.2,0,,0,11.958,10.493
90,2023,20230802,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,21.4613,23.5833,32.315,0.2,0,,0,12.789,10.493
90,2023,20230803,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,38.8486,23.5834,32.315,0.22,0,,0,14.343,10.493
90,2023,20230901,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,37.4859,23.5834,32.315,0.22,0,,0,15.305,10.493
90,2023,20230902,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,38.4906,23.5834,32.315,0.22,0,,0,15.697,10.493
90,2023,20230903,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,39.3361,23.5834,32.315,0.22,0,,0,16.087,10.493
90,2023,20231001,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,45.8878,23.5834,32.315,0.23,0,,0,16.774,10.493
90,2023,20231002,1125143,19032.086,9372.557,13176.751,4112.735,0,0,200,59.6303,23.5834,32.315,0.26,0,,0,17.373,10.503
206 changes: 188 additions & 18 deletions pcse_gym/envs/common_env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import os
import copy

import numpy as np
import yaml
Expand All @@ -18,41 +19,198 @@
"""


def replace_years(agro_management, years):
class AgroManagementContainer:
def __init__(self, agro_management: list):
self.agro_structure = agro_management
self.campaign_date: datetime.date = list(agro_management[0].keys())[0]
self.crop_name: str = agro_management[0][self.campaign_date]['CropCalendar']['crop_name']
self.crop_variety: str = agro_management[0][self.campaign_date]['CropCalendar']['variety_name']
self.crop_start_date: datetime.date = agro_management[0][self.campaign_date]['CropCalendar']['crop_start_date']
self.crop_start_type: str = agro_management[0][self.campaign_date]['CropCalendar']['crop_start_type']
self.crop_end_date: datetime.date = agro_management[0][self.campaign_date]['CropCalendar']['crop_end_date']
self.crop_end_type: str = agro_management[0][self.campaign_date]['CropCalendar']['crop_end_type']
self.max_duration: int = agro_management[0][self.campaign_date]['CropCalendar']['max_duration']

self.structure = None
self.build_structure()

def build_structure(self):
self.structure = yaml.load(f'''
- {self.campaign_date}:
CropCalendar:
crop_name: {self.crop_name}
variety_name: {self.crop_variety}
crop_start_date: {self.crop_start_date}
crop_start_type: {self.crop_start_type}
crop_end_date: {self.crop_end_date}
crop_end_type: {self.crop_end_type}
max_duration: {self.max_duration}
TimedEvents: null
StateEvents: null
''', Loader=yaml.SafeLoader)

def replace_years(self, y):
"""
Years replaced are the harvest date. Campaign start and sow date starts a year before.
"""
if isinstance(y, list):
y = y[0]
if self.campaign_date.year == self.crop_end_date.year:
yprev = y
else:
yprev = y - 1
self.campaign_date = self.campaign_date.replace(year=yprev)
self.crop_start_date = self.crop_start_date.replace(year=yprev)
self.crop_end_date = self.crop_end_date.replace(year=y)

self.build_structure()
return self.structure

def replace_sow_date(self, year, month, day):
self.crop_start_date = self.crop_start_date.replace(year=year, month=month, day=day)

self.build_structure()
return self.structure

def replace_harvest_date(self, year, month, day):
self.crop_end_date = self.crop_end_date.replace(year=year, month=month, day=day)

self.build_structure()
return self.structure

def replace_start_type(self, start):
assert start == 'sowing' or start == 'emergence'
self.crop_start_type = start

self.build_structure()
return self.structure

@property
def get_structure(self):
return self.structure

@property
def get_start_date(self):
return self.crop_start_date

@property
def get_end_date(self):
return self.crop_end_date


def replace_years_(agro_management, years): # deprecated
if not isinstance(years, list):
years = [years]

updated_agro_management = [{k.replace(year=year): v for k, v in agro.items()} for agro, year in
zip(agro_management, years)]

def replace_year_value(d, year):
# TODO: refactor date_keys so it's lighter,
# direct reference is a bit tricky with the dictionary name (2006-10-01)
agro = agro_management[0]
date_keys = [[v2.year for v1 in v.values() if isinstance(v1, dict) for v2 in v1.values()
if isinstance(v2, datetime.date)] for v in agro.values()]
date_keys = date_keys[0]
if date_keys[0] < date_keys[1]:
updated_agro_management = [{k.replace(year=year - 1): v for k, v in agro.items()} for agro, year in
zip(agro_management, years)]
else:
updated_agro_management = [{k.replace(year=year): v for k, v in agro.items()} for agro, year in
zip(agro_management, years)]

def replace_year_value(d, year, y_sow=None):
for k, v in d.items():
if isinstance(v, dict):
replace_year_value(v, year)
replace_year_value(v, year, y_sow)
else:
if isinstance(v, datetime.date):
if isinstance(v, datetime.date) and y_sow and k == 'crop_start_date':
up_dict = {k: v.replace(year=y_sow)}
d.update(up_dict)
elif isinstance(v, datetime.date):
up_dict = {k: v.replace(year=year)}
d.update(up_dict)

for agro, year in zip(updated_agro_management, years):
replace_year_value(agro, year)
if date_keys[0] < date_keys[1]:
year_sow = year - 1
replace_year_value(agro, year, year_sow)
else:
replace_year_value(agro, year)
return updated_agro_management


def get_weather_data_provider(location) -> pcse.db.NASAPowerWeatherDataProvider:
wdp = pcse.db.NASAPowerWeatherDataProvider(*location)
def get_weather_data_provider(location, random_weather=False) -> pcse.db.NASAPowerWeatherDataProvider or pcse.fileinput.CSVWeatherDataProvider:
if random_weather:
wdp = get_random_weather_provider(location)
else:
wdp = pcse.db.NASAPowerWeatherDataProvider(*location)
return wdp


def get_random_weather_provider(location) -> pcse.fileinput.CSVWeatherDataProvider:
path_to_file = os.path.dirname(os.path.realpath(__file__))
lat, lon = location
csv_name = f'{lat}-{lon}_random_weather.csv'
filename = os.path.join(path_to_file[:-4], 'utils', 'weather_utils', 'random_weather_csv', csv_name)
wdp = pcse.fileinput.CSVWeatherDataProvider(filename)
return wdp


class Engine(pcse.engine.Engine):
"""
Wraps around the PCSE engine/crop model to set a flag when the simulation has terminated
Wraps around the PCSE engine/crop model for correct rate updates after fertilization action and
to set a flag when the simulation has terminated
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._flag_terminated = False

def _run(self, action):
"""Make one time step of the simulation.
"""

# Update timer
self.day, delt = self.timer()

# State integration
self.integrate(self.day, delt)

# Driving variables
self.drv = self._get_driving_variables(self.day)

# Agromanagement decisions
self.agromanager(self.day, self.drv)

if action > 0:
self._send_signal(signal=pcse.signals.apply_n,
amount=action,
application_depth=10.,
cnratio=0.,
f_orgmat=0.,
f_NH4N=0.5,
f_NO3N=0.5,
initial_age=0,
recovery=0.7
)

# Rate calculation
self.calc_rates(self.day, self.drv)

if self.flag_terminate is True:
self._terminate_simulation(self.day)

def run(self, days=1, action=0):
"""Advances the system state with given number of days"""

# do action at end of time step
days_counter = days
days_done = 0
while (days_done < days) and (self.flag_terminate is False):
days_done += 1
days_counter -= 1
if days_counter > 0:
self._run(0)
else:
self._run(action)

@property
def terminated(self):
return self._flag_terminated
Expand Down Expand Up @@ -129,27 +287,34 @@ def __init__(self,

# Set location
if location is None:
location = (52, 5.5)
location = (52.0, 5.5)
self._location = location
self._timestep = timestep

# Store the crop/soil/site parameters
self._crop_params = crop_parameters
self._site_params = site_parameters
self._site_params_ = site_parameters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah we can remove that. Remnants from the random initialization :)

self._soil_params = soil_parameters

# Agent will have no access to weather
self.no_weather = kwargs.get('no_weather', False)

# Store the agro-management config
with open(agro_config, 'r') as f:
self._agro_management = yaml.load(f, Loader=yaml.SafeLoader)

# Initialize Agromanagement Container Class
self.agmt = AgroManagementContainer(self._agro_management)

if years is not None:
self._agro_management = replace_years(self._agro_management, years)
self._agro_management = self.agmt.replace_years(years)

# Store the PCSE Engine config
self._model_config = model_config

# Get the weather data source
self._weather_data_provider = get_weather_data_provider(self._location)
self._weather_data_provider = get_weather_data_provider(self._location, kwargs.get('random_weather'))

# Create a PCSE engine / crop growth model
self._model = self._init_pcse_model()
Expand All @@ -165,7 +330,12 @@ def __init__(self,
# Define Gym action space
self.action_space = self._get_action_space()

def _init_pcse_model(self, *args, **kwargs) -> Engine:
def _init_pcse_model(self, options=None, *args, **kwargs) -> Engine:

# Inject different initial condition every episode if it specified in args
if options is not None:
self._site_params['NH4I'] = options['NH4I']
self._site_params['NO3I'] = options['NO3I']

# Combine the config files in a single PCSE ParameterProvider object
self._parameter_provider = pcse.base.ParameterProvider(cropdata=self._crop_params,
Expand Down Expand Up @@ -294,10 +464,10 @@ def step(self, action) -> tuple:
# Apply action
if isinstance(action, np.ndarray):
action = action[0]
self._apply_action(action)
action = self._apply_action(action) # is subclassed by sb3

# Run the crop growth model
self._model.run(days=self._timestep)
self._model.run(days=self._timestep, action=action)
# Get the model output
output = self._model.get_output()[-self._timestep:]
info['days'] = [day['day'] for day in output]
Expand Down Expand Up @@ -408,7 +578,7 @@ def reset(self,
info = dict()

# Create a PCSE engine / crop growth model
self._model = self._init_pcse_model()
self._model = self._init_pcse_model(options)
output = self._model.get_output()[-self._timestep:]
o = self._get_observation(output)
info['date'] = self.date
Expand Down
2 changes: 1 addition & 1 deletion pcse_gym/envs/configs/Wofost81_NWLP_FD.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ OUTPUT_VARS = ["DVS","LAI", "TAGP", "TWSO", "TWLV", "TWST",
"WRT", "TRA", "RD", "SM", "WWLOW", "RFTRA",
"NAVAIL", "Ndemand", "RNuptake", "NuptakeTotal",
"NamountSO", "NamountLV", "NamountST", "NamountRT",
"NfixTotal", "NlossesTotal"]
"NfixTotal", "NlossesTotal", "IDWST"]
# interval for OUTPUT signals, either "daily"|"dekadal"|"monthly"
# For daily output you change the number of days between successive
# outputs using OUTPUT_INTERVAL_DAYS. For dekadal and monthly
Expand Down
Loading
Loading