Skip to content

site module

Module to prepare site data

SiteParameterError (Exception)

Custom exception for site parameter validation errors.

Source code in cropengine/site.py
class SiteParameterError(Exception):
    """Custom exception for site parameter validation errors."""

    pass

WOFOSTSiteParametersProvider

A unified data provider for WOFOST site-specific parameters.

Parameters:

Name Type Description Default
model str

The name of the WOFOST model version to use.

required
site_overrides dict

Dictionary of parameters to override defaults.

None
Source code in cropengine/site.py
class WOFOSTSiteParametersProvider:
    """
    A unified data provider for WOFOST site-specific parameters.

    Args:
        model (str): The name of the WOFOST model version to use.
        site_overrides (dict, optional): Dictionary of parameters to override defaults.
    """

    EMERGENCY_DEFAULTS = {
        "WAV": 10.0,
        "CO2": 360.0,
        "NAVAILI": 0.0,
        "NH4I": [0.05],
        "NO3I": [0.05],
    }

    def __init__(self, model, site_overrides=None):
        self.model = model
        self.raw_kwargs = site_overrides if site_overrides else {}
        self.param_metadata = []
        self.required_params = set()
        self.valid_param_names = set()
        self.all_param_defs = {}

        # 1. Load configuration
        try:
            with pkg_resources.files(configs).joinpath("site_params.yaml").open(
                "r"
            ) as f:
                self.full_config = yaml.safe_load(f)
        except Exception as e:
            raise RuntimeError(f"Failed to load site_params.yaml: {e}")

        config = self.full_config["wofost"]

        # 2. Validate Model and Prepare Metadata
        if self.model in config["model_mapping"]:

            profile_name = config["model_mapping"][self.model]
            profile_def = config["profiles"][profile_name]

            self.valid_param_names = set(profile_def["parameters"])
            self.required_params = set(profile_def.get("required", []))
            self.all_param_defs = config["site_params"]

        else:
            valid_models = list(config["model_mapping"].keys())
            raise SiteParameterError(
                f"Unknown model '{self.model}'. Available models: {valid_models}"
            )

    def get_params(self):
        """
        Validates inputs against the prepared metadata, applies defaults,
        and returns the final parameter dictionary.
        """
        validated_params = {}
        self.param_metadata = []

        # 1. Process Valid Parameters defined for this model
        for par_name in self.valid_param_names:
            if par_name not in self.all_param_defs:
                continue

            meta_def = self.all_param_defs[par_name].copy()
            is_required = par_name in self.required_params
            default_val = meta_def["default"]

            if par_name in self.raw_kwargs:
                value = self.raw_kwargs[par_name]
            else:
                value = default_val

                if is_required:
                    if value is not None:
                        print(
                            f"🚨 [WARN] Required site parameter '{par_name}' missing for model '{self.model}'. "
                            f"Using default value: {value}"
                        )
                    else:
                        if par_name in self.EMERGENCY_DEFAULTS:
                            value = self.EMERGENCY_DEFAULTS[par_name]
                            print(
                                f"🚨 [WARN] Required site parameter '{par_name}' missing (no YAML default). "
                                f"Using emergency fallback: {value}"
                            )
                        else:
                            raise SiteParameterError(
                                f"Required parameter '{par_name}' is missing and has no default value."
                            )

            # Convert types and check valid ranges
            if value is not None:
                value = self._convert_and_validate(par_name, value, meta_def)

            # Store Result
            validated_params[par_name] = value

            # Update Metadata List
            meta_record = {
                "parameter": par_name,
                "required": is_required,
                "value": value,
            }
            meta_record.update(meta_def)
            self.param_metadata.append(meta_record)

        # 2. Check for Unknown Parameters provided by user
        unknown_keys = [
            k for k in self.raw_kwargs.keys() if k not in self.valid_param_names
        ]
        if unknown_keys:
            raise SiteParameterError(
                f"🚨 [WARN] Ignoring unknown site parameters provided for '{self.model}': {unknown_keys}"
            )

        return validated_params

    def _convert_and_validate(self, name, value, definition):
        """
        Internal helper to cast types and validate ranges.
        """
        target_type_str = definition["type"]

        # Type Casting
        try:
            if target_type_str == "int":
                value = int(value)
            elif target_type_str == "float":
                value = float(value)
            elif target_type_str == "list":
                if not isinstance(value, list):
                    if isinstance(value, str) and "," in value:
                        value = [float(x.strip()) for x in value.split(",")]
                    else:
                        raise ValueError
        except (ValueError, TypeError):
            raise SiteParameterError(
                f"Parameter '{name}' must be of type {target_type_str}, got {type(value)}"
            )

        # Range Checking
        valid_range = definition["range"]

        if target_type_str == "list":
            min_val, max_val = valid_range
            if not all(min_val <= x <= max_val for x in value):
                raise SiteParameterError(
                    f"Elements in list '{name}' must be between {min_val} and {max_val}"
                )

        elif target_type_str == "int" and valid_range == [0, 1]:
            if value not in [0, 1]:
                raise SiteParameterError(f"Parameter '{name}' must be 0 or 1.")

        else:
            min_val, max_val = valid_range
            if not (min_val <= value <= max_val):
                raise SiteParameterError(
                    f"Value {value} for parameter '{name}' out of range [{min_val}, {max_val}]"
                )

        return value

get_params(self)

Validates inputs against the prepared metadata, applies defaults, and returns the final parameter dictionary.

Source code in cropengine/site.py
def get_params(self):
    """
    Validates inputs against the prepared metadata, applies defaults,
    and returns the final parameter dictionary.
    """
    validated_params = {}
    self.param_metadata = []

    # 1. Process Valid Parameters defined for this model
    for par_name in self.valid_param_names:
        if par_name not in self.all_param_defs:
            continue

        meta_def = self.all_param_defs[par_name].copy()
        is_required = par_name in self.required_params
        default_val = meta_def["default"]

        if par_name in self.raw_kwargs:
            value = self.raw_kwargs[par_name]
        else:
            value = default_val

            if is_required:
                if value is not None:
                    print(
                        f"🚨 [WARN] Required site parameter '{par_name}' missing for model '{self.model}'. "
                        f"Using default value: {value}"
                    )
                else:
                    if par_name in self.EMERGENCY_DEFAULTS:
                        value = self.EMERGENCY_DEFAULTS[par_name]
                        print(
                            f"🚨 [WARN] Required site parameter '{par_name}' missing (no YAML default). "
                            f"Using emergency fallback: {value}"
                        )
                    else:
                        raise SiteParameterError(
                            f"Required parameter '{par_name}' is missing and has no default value."
                        )

        # Convert types and check valid ranges
        if value is not None:
            value = self._convert_and_validate(par_name, value, meta_def)

        # Store Result
        validated_params[par_name] = value

        # Update Metadata List
        meta_record = {
            "parameter": par_name,
            "required": is_required,
            "value": value,
        }
        meta_record.update(meta_def)
        self.param_metadata.append(meta_record)

    # 2. Check for Unknown Parameters provided by user
    unknown_keys = [
        k for k in self.raw_kwargs.keys() if k not in self.valid_param_names
    ]
    if unknown_keys:
        raise SiteParameterError(
            f"🚨 [WARN] Ignoring unknown site parameters provided for '{self.model}': {unknown_keys}"
        )

    return validated_params