Skip to content

agromanagement module

Module to setup agromanagement

WOFOSTAgroEventBuilder

Helper class to build PCSE agromanagement events using a YAML schema for validation.

Source code in cropengine/agromanagement.py
class WOFOSTAgroEventBuilder:
    """
    Helper class to build PCSE agromanagement events using a YAML schema for validation.
    """

    def __init__(self):
        try:
            with pkg_resources.files(configs).joinpath("agromanagement.yaml").open(
                "r"
            ) as f:
                self.schema = yaml.safe_load(f)["wofost"]
        except Exception as e:
            raise RuntimeError(f"Failed to load agromanagement.yaml: {e}")

    def get_timed_events_info(self):
        return self.schema["TimedEvents"]

    def get_state_events_info(self):
        return self.schema["StateEvents"]

    def _convert_date(
        self, date_val: Union[str, datetime.date, None]
    ) -> Optional[datetime.date]:
        """Helper to convert string dates to datetime.date objects."""
        if date_val is None:
            return None
        if isinstance(date_val, str):
            return datetime.datetime.strptime(date_val, "%Y-%m-%d").date()
        if isinstance(date_val, (datetime.date, datetime.datetime)):
            return date_val
        raise ValueError(
            f"Invalid date format: {date_val}. Expected YYYY-MM-DD string or datetime.date object."
        )

    def create_timed_events(self, signal_type: str, events_list: List[Dict]) -> dict:
        """
        Creates a single TimedEvent dictionary containing a LIST of dates.
        """
        if signal_type not in self.schema["TimedEvents"]:
            raise ValueError(f"Unknown TimedEvent signal: {signal_type}")

        schema_def = self.schema["TimedEvents"][signal_type]
        required_params = schema_def["events_table"].keys()

        populated_events_list = []

        for entry in events_list:
            current_date = self._convert_date(entry["event_date"])

            params = {}
            for param in required_params:
                params[param] = entry[param]

            populated_events_list.append({current_date: params})

        return {
            "event_signal": signal_type,
            "name": schema_def.get("name"),
            "comment": schema_def.get("comment"),
            "events_table": populated_events_list,
        }

    def create_state_events(
        self,
        signal_type: str,
        state_var: str,
        zero_condition: str,
        events_list: List[Dict],
    ) -> dict:
        """
        Creates a single StateEvent dictionary containing a LIST of thresholds.
        """
        if signal_type not in self.schema["StateEvents"]:
            raise ValueError(f"Unknown StateEvent signal: {signal_type}")

        schema_def = self.schema["StateEvents"][signal_type]
        required_params = schema_def["events_table"].keys()

        # Change: Use a LIST for the events table
        populated_events_list = []

        for entry in events_list:
            threshold = entry["threshold"]

            params = {}
            for param in required_params:
                params[param] = entry[param]

            # Append as a single-key dictionary to the list
            populated_events_list.append({threshold: params})

        return {
            "event_signal": signal_type,
            "event_state": state_var,
            "zero_condition": zero_condition,
            "name": schema_def.get("name"),
            "comment": schema_def.get("comment"),
            "events_table": populated_events_list,
        }

create_state_events(self, signal_type, state_var, zero_condition, events_list)

Creates a single StateEvent dictionary containing a LIST of thresholds.

Source code in cropengine/agromanagement.py
def create_state_events(
    self,
    signal_type: str,
    state_var: str,
    zero_condition: str,
    events_list: List[Dict],
) -> dict:
    """
    Creates a single StateEvent dictionary containing a LIST of thresholds.
    """
    if signal_type not in self.schema["StateEvents"]:
        raise ValueError(f"Unknown StateEvent signal: {signal_type}")

    schema_def = self.schema["StateEvents"][signal_type]
    required_params = schema_def["events_table"].keys()

    # Change: Use a LIST for the events table
    populated_events_list = []

    for entry in events_list:
        threshold = entry["threshold"]

        params = {}
        for param in required_params:
            params[param] = entry[param]

        # Append as a single-key dictionary to the list
        populated_events_list.append({threshold: params})

    return {
        "event_signal": signal_type,
        "event_state": state_var,
        "zero_condition": zero_condition,
        "name": schema_def.get("name"),
        "comment": schema_def.get("comment"),
        "events_table": populated_events_list,
    }

create_timed_events(self, signal_type, events_list)

Creates a single TimedEvent dictionary containing a LIST of dates.

Source code in cropengine/agromanagement.py
def create_timed_events(self, signal_type: str, events_list: List[Dict]) -> dict:
    """
    Creates a single TimedEvent dictionary containing a LIST of dates.
    """
    if signal_type not in self.schema["TimedEvents"]:
        raise ValueError(f"Unknown TimedEvent signal: {signal_type}")

    schema_def = self.schema["TimedEvents"][signal_type]
    required_params = schema_def["events_table"].keys()

    populated_events_list = []

    for entry in events_list:
        current_date = self._convert_date(entry["event_date"])

        params = {}
        for param in required_params:
            params[param] = entry[param]

        populated_events_list.append({current_date: params})

    return {
        "event_signal": signal_type,
        "name": schema_def.get("name"),
        "comment": schema_def.get("comment"),
        "events_table": populated_events_list,
    }

WOFOSTAgroManagementProvider (list)

A dynamic provider for WOFOST AgroManagement. Generates a rotation of crops based on start/end dates and handles YAML serialization.

Source code in cropengine/agromanagement.py
class WOFOSTAgroManagementProvider(list):
    """
    A dynamic provider for WOFOST AgroManagement.
    Generates a rotation of crops based on start/end dates and handles YAML serialization.
    """

    def __init__(self):
        super().__init__()

    def _convert_date(
        self, date_val: Union[str, datetime.date, None]
    ) -> Optional[datetime.date]:
        """Helper to convert string dates to datetime.date objects."""
        if date_val is None:
            return None
        if isinstance(date_val, str):
            return datetime.datetime.strptime(date_val, "%Y-%m-%d").date()
        if isinstance(date_val, (datetime.date, datetime.datetime)):
            return date_val
        raise ValueError(
            f"Invalid date format: {date_val}. Expected YYYY-MM-DD string or datetime.date object."
        )

    def add_campaign(
        self,
        campaign_start_date: Union[str, datetime.date],
        campaign_end_date: Union[str, datetime.date],
        crop_name: str,
        variety_name: str,
        crop_start_date: Union[str, datetime.date],
        crop_end_date: Optional[Union[str, datetime.date]] = None,
        crop_start_type: str = "sowing",
        crop_end_type: str = "maturity",
        max_duration: int = 300,
        timed_events: List[Dict] = None,
        state_events: List[Dict] = None,
    ):
        """
        Adds a single cropping campaign to the rotation.

        Args:
            campaign_start_date: Start date of the campaign (str 'YYYY-MM-DD' or date object).
            campaign_end_date: End date of the campaign (str 'YYYY-MM-DD' or date object).
            crop_name: Name of the crop (e.g., 'wheat').
            variety_name: Variety identifier (e.g., 'winter-wheat').
            crop_start_date: Date of sowing or emergence (str 'YYYY-MM-DD' or date object).
            crop_end_date: Optional harvest date (str 'YYYY-MM-DD' or date object).
            crop_start_type: 'sowing' or 'emergence'.
            crop_end_type: 'maturity', 'harvest', or 'earliest'.
            max_duration: Maximum duration of the crop cycle in days.
            timed_events: List of timed event dictionaries (from EventBuilder).
            state_events: List of state event dictionaries (from EventBuilder).
        """
        # Convert inputs to ensure they are date objects or valid strings
        c_start = self._convert_date(campaign_start_date)
        c_end = self._convert_date(campaign_end_date)
        crop_start = self._convert_date(crop_start_date)
        crop_end = self._convert_date(crop_end_date) if crop_end_date else None

        self._last_campaign_end = c_end

        # 1. Define the base CropCalendar
        crop_calendar = {
            "crop_name": crop_name,
            "variety_name": variety_name,
            "crop_start_date": crop_start,
            "crop_start_type": crop_start_type,
            "crop_end_type": crop_end_type,
            "max_duration": max_duration,
        }

        # 2. Conditionally add crop_end_date only if it exists
        if crop_end is not None:
            crop_calendar["crop_end_date"] = crop_end

        # 3. Build the full config
        campaign_config = {
            "CropCalendar": crop_calendar,
            "TimedEvents": timed_events if timed_events else None,
            "StateEvents": state_events if state_events else None,
        }

        # Append the campaign dictionary {start_date: config} to the list
        self.append({c_start: campaign_config})

    def add_trailing_empty_campaign(self):
        """
        Adds a final empty campaign to ensure the simulation runs until the very end
        of the requested period.

        Args:
            start_date: Start date of the empty period (str 'YYYY-MM-DD' or date object).
        """
        if self._last_campaign_end is None:
            raise RuntimeError(
                "Cannot add trailing empty campaign before adding at least one campaign."
            )

        self.append({self._last_campaign_end: None})

    def save_to_yaml(self, filename: str):
        """
        Exports the current agromanagement configuration to a YAML file.

        Structure matches the PCSE requirement:
        AgroManagement:
        - Date:
            CropCalendar: ...
            TimedEvents: ...
        """
        # Wrap the list in the root 'AgroManagement' key
        output_structure = {"AgroManagement": list(self)}

        with open(filename, "w") as f:
            yaml.dump(output_structure, f, sort_keys=False)

add_campaign(self, campaign_start_date, campaign_end_date, crop_name, variety_name, crop_start_date, crop_end_date=None, crop_start_type='sowing', crop_end_type='maturity', max_duration=300, timed_events=None, state_events=None)

Adds a single cropping campaign to the rotation.

Parameters:

Name Type Description Default
campaign_start_date Union[str, datetime.date]

Start date of the campaign (str 'YYYY-MM-DD' or date object).

required
campaign_end_date Union[str, datetime.date]

End date of the campaign (str 'YYYY-MM-DD' or date object).

required
crop_name str

Name of the crop (e.g., 'wheat').

required
variety_name str

Variety identifier (e.g., 'winter-wheat').

required
crop_start_date Union[str, datetime.date]

Date of sowing or emergence (str 'YYYY-MM-DD' or date object).

required
crop_end_date Union[str, datetime.date]

Optional harvest date (str 'YYYY-MM-DD' or date object).

None
crop_start_type str

'sowing' or 'emergence'.

'sowing'
crop_end_type str

'maturity', 'harvest', or 'earliest'.

'maturity'
max_duration int

Maximum duration of the crop cycle in days.

300
timed_events List[Dict]

List of timed event dictionaries (from EventBuilder).

None
state_events List[Dict]

List of state event dictionaries (from EventBuilder).

None
Source code in cropengine/agromanagement.py
def add_campaign(
    self,
    campaign_start_date: Union[str, datetime.date],
    campaign_end_date: Union[str, datetime.date],
    crop_name: str,
    variety_name: str,
    crop_start_date: Union[str, datetime.date],
    crop_end_date: Optional[Union[str, datetime.date]] = None,
    crop_start_type: str = "sowing",
    crop_end_type: str = "maturity",
    max_duration: int = 300,
    timed_events: List[Dict] = None,
    state_events: List[Dict] = None,
):
    """
    Adds a single cropping campaign to the rotation.

    Args:
        campaign_start_date: Start date of the campaign (str 'YYYY-MM-DD' or date object).
        campaign_end_date: End date of the campaign (str 'YYYY-MM-DD' or date object).
        crop_name: Name of the crop (e.g., 'wheat').
        variety_name: Variety identifier (e.g., 'winter-wheat').
        crop_start_date: Date of sowing or emergence (str 'YYYY-MM-DD' or date object).
        crop_end_date: Optional harvest date (str 'YYYY-MM-DD' or date object).
        crop_start_type: 'sowing' or 'emergence'.
        crop_end_type: 'maturity', 'harvest', or 'earliest'.
        max_duration: Maximum duration of the crop cycle in days.
        timed_events: List of timed event dictionaries (from EventBuilder).
        state_events: List of state event dictionaries (from EventBuilder).
    """
    # Convert inputs to ensure they are date objects or valid strings
    c_start = self._convert_date(campaign_start_date)
    c_end = self._convert_date(campaign_end_date)
    crop_start = self._convert_date(crop_start_date)
    crop_end = self._convert_date(crop_end_date) if crop_end_date else None

    self._last_campaign_end = c_end

    # 1. Define the base CropCalendar
    crop_calendar = {
        "crop_name": crop_name,
        "variety_name": variety_name,
        "crop_start_date": crop_start,
        "crop_start_type": crop_start_type,
        "crop_end_type": crop_end_type,
        "max_duration": max_duration,
    }

    # 2. Conditionally add crop_end_date only if it exists
    if crop_end is not None:
        crop_calendar["crop_end_date"] = crop_end

    # 3. Build the full config
    campaign_config = {
        "CropCalendar": crop_calendar,
        "TimedEvents": timed_events if timed_events else None,
        "StateEvents": state_events if state_events else None,
    }

    # Append the campaign dictionary {start_date: config} to the list
    self.append({c_start: campaign_config})

add_trailing_empty_campaign(self)

Adds a final empty campaign to ensure the simulation runs until the very end of the requested period.

Parameters:

Name Type Description Default
start_date

Start date of the empty period (str 'YYYY-MM-DD' or date object).

required
Source code in cropengine/agromanagement.py
def add_trailing_empty_campaign(self):
    """
    Adds a final empty campaign to ensure the simulation runs until the very end
    of the requested period.

    Args:
        start_date: Start date of the empty period (str 'YYYY-MM-DD' or date object).
    """
    if self._last_campaign_end is None:
        raise RuntimeError(
            "Cannot add trailing empty campaign before adding at least one campaign."
        )

    self.append({self._last_campaign_end: None})

save_to_yaml(self, filename)

Exports the current agromanagement configuration to a YAML file.

Structure matches the PCSE requirement: AgroManagement: - Date: CropCalendar: ... TimedEvents: ...

Source code in cropengine/agromanagement.py
def save_to_yaml(self, filename: str):
    """
    Exports the current agromanagement configuration to a YAML file.

    Structure matches the PCSE requirement:
    AgroManagement:
    - Date:
        CropCalendar: ...
        TimedEvents: ...
    """
    # Wrap the list in the root 'AgroManagement' key
    output_structure = {"AgroManagement": list(self)}

    with open(filename, "w") as f:
        yaml.dump(output_structure, f, sort_keys=False)