Source code for dpest.eco

import yaml
from dpest.functions import *


[docs] def eco( ecotype=None, eco_file_path=None, output_path=None, new_template_file_extension=None, header_start=None, tpl_first_line=None, minima=None, maxima=None, mrk='~', **parameters_grouped ): """ Creates a ``PEST template file (.TPL)`` for CERES-Wheat ecotype parameters based on a ``DSSAT ecotype file (.ECO)``. This module is specific to the CERES-Wheat model and uses default values tailored for this model. **Required Arguments:** ======= * **ecotype** (*str*): Ecotype ID to modify. This should match the ``ECO#`` (ecotype ID) column in the ``DSSAT ecotype file (.ECO)`` * **eco_file_path** (*str*): Full path to the ``DSSAT ecotype (.ECO)`` file. Typically, this is the path to the ``WHCER048.ECO`` file, usually located at ``C:\DSSAT48\Genotype\WHCER048.ECO``. **Optional Arguments:** ======= * **output_path** (*str*, *default: current working directory*): Directory to save the generated ``PEST template file (.TPL)``. * **new_template_file_extension** (*str*, *default: ".TPL"*): Extension for the generated ``PEST template file (.TPL)``. This is the PEST default value and should not be changed without good reason. * **header_start** (*str*, *default: "@ECO#"*): Identifier for the header row in the ``DSSAT ecotype file (.ECO)``. * **tpl_first_line** (*str*, *default: "ptf"*): First line to include in the ``PEST template file (.TPL)``. This is the PEST default value and should not be changed without good reason. * **minima** (*str*, *default: "999991"*): Row identifier for the minima ecotype parameter values. * **maxima** (*str*, *default: "999992"*): Row identifier for the maxima ecotype parameter values. * **mrk** (*str*, *default: "~"*) Primary marker delimiter character for the template file. Must be a single character and cannot be A-Z, a-z, 0-9, !, [, ], (, ), :, space, tab, or &. * **parameters_grouped** (*dict*, *optional*): Parameters to calibrate, grouped and comma-separated. If not provided, all ecotype parameters are calibrated. For example: ``PHEN='P1, P2FR1', VERN='VEFF'``. 'PHEN' and 'VERN' are ecotype parameter group names, and the values are the specific ecotype parameters to calibrate, using the same names as in the ``DSSAT ecotype file (.ECO)``. Parameter group names should be less than 12 characters. **Returns:** ======= * *tuple*: A tuple containing: * *dict*: A dictionary containing: * ``'parameters'``: Current ecotype parameter values for the specified ecotype. * ``'minima_parameters'``: Minima values for all ecotype parameters. * ``'maxima_parameters'``: Maxima values for all ecotype parameters. * ``'parameters_grouped'``: The grouped ecotype parameters used for template generation. * *str*: The full path to the generated .TPL file. **Examples:** ======= 1. **Basic Usage (Required Arguments Only):** .. code-block:: python from dpest import eco # Call the module with only the required arguments ecotype_parameters, ecotype_tpl_path = eco( ecotype = 'CAWH01', eco_file_path = 'C:/DSSAT48/Genotype/WHCER048.ECO' ) # The returned tuple and path are saved in the variables, can be used with any name that the user prefer, to call them later This example creates a template file using only the required arguments. Note that the returned tuple ``(ecotype_parameters, ecotype_tpl_path)`` is captured. The ``ecotype_parameters`` dictionary will be used later to make the control file's parameter groups and parameters sections using the ``pst`` module. The ``ecotype_tpl_path`` path will be used in the ``input_output_file_pairs`` argument of the ``pst`` module to match the original ``DSSAT ecotype file (.ECO)`` to the template file. 2. **Specifying Parameter Groups (Tuple Not Saved):** .. code-block:: python from dpest import eco # Call the module specifying parameter groups, but not saving the returned tuple eco( ecotype = 'CAWH01', eco_file_path = 'C:/DSSAT48/Genotype/WHCER048.ECO', PHEN = 'P1, P2FR1', VERN = 'VEFF' ) This example demonstrates how to specify the ``parameters_grouped`` argument to calibrate only specific ecotype parameters. In this case, the returned tuple is not saved, but the ``PEST template file (.TPL)`` is still created at the specified location. If you want to use the ecotype parameters and path for the ``pst`` module, the returned tuple should be saved with the names that the user prefer. """ # Defaul variable values yml_ecotype_block = 'ECOTYPE_TPL_FILE' yaml_file_variables = 'FILE_VARIABLES' yaml_parameters = 'PARAMETERS' try: ## Get the yaml_data # Get the directory of the current script current_dir = os.path.dirname(os.path.abspath(__file__)) # Construct the path to arguments.yml arguments_file = os.path.join(current_dir, 'arguments.yml') # Ensure the YAML file exists if not os.path.isfile(arguments_file): raise FileNotFoundError(f"YAML file not found: {arguments_file}") # Load YAML configuration with open(arguments_file, 'r') as yml_file: yaml_data = yaml.safe_load(yml_file) # Validate inputs if ecotype is None: raise ValueError("The 'ecotype' argument is required and must be specified by the user.") # Validate eco_file_path validated_eco_file_path = validate_file(eco_file_path, '.ECO') # Get the ecotype template file variables function_arguments = yaml_data[yml_ecotype_block][yaml_file_variables] # Validate marker delimiters using the validate_marker() function mrk = validate_marker(mrk, "mrk") if new_template_file_extension is None: new_template_file_extension = function_arguments['new_template_file_extension'] if header_start is None: header_start = function_arguments['header_start'] if tpl_first_line is None: tpl_first_line = function_arguments['tpl_first_line'] if minima is None: minima = str(function_arguments['minima']) if maxima is None: maxima = str(function_arguments['maxima']) if parameters_grouped == {}: # 1) Get the 5-char genotype key from the ECO filename (e.g. WHCER) eco_basename = os.path.basename(validated_eco_file_path) # WHCER048.ECO genotype_key = eco_basename[:5] # WHCER # 2) Look up crop/model for this genotype in top-level arguments.yml try: genotype_cfg = yaml_data["GENOTYPE_FILES"][genotype_key] except KeyError as exc: raise ValueError( "Automatic detection of ecotype parameter groups is not available " f"for the ECO file '{eco_basename}' (genotype key '{genotype_key}').\n\n" "Please provide the 'parameters_grouped' argument explicitly when " "calling eco(), for example:\n" " eco(..., `PHEN='P1, P2FR1', VERN='VEFF')" ) from exc crop = genotype_cfg["crop"] # e.g. 'wheat' model = genotype_cfg["model"] # e.g. 'ceres' # 3) Build path to dpest/<crop>/<model>/arguments.yml crop_model_arguments_file = get_crop_model_arguments_file_path( crop=crop, model=model, ) if not os.path.isfile(crop_model_arguments_file): raise FileNotFoundError( f"YAML file not found for crop='{crop}' and model='{model}': " f"{crop_model_arguments_file}" ) # 4) Load ECOTYPE_TPL_FILE.PARAMETERS from that crop/model file with open(crop_model_arguments_file, "r") as cm_yml: crop_model_yaml_data = yaml.safe_load(cm_yml) try: raw_parameters_grouped = crop_model_yaml_data[yml_ecotype_block][yaml_parameters] except KeyError as exc: raise ValueError( "No default ecotype parameter groups are defined for this crop/model.\n\n" "To use eco() with this ECO file, you must provide the " "'parameters_grouped' argument explicitly, for example:\n" " ECO(\n" " ecotype = 'CAWH01',\n" " eco_file_path='C:/DSSAT48/Genotype/WHCER048.ECO',\n" " PHEN = 'P1, P2FR1',\n" " VERN = 'VEFF',\n" " )" ) from exc # Convert list-of-strings to comma-separated strings for internal use parameters_grouped = { key: ", ".join(value) for key, value in raw_parameters_grouped.items() } # Combine all the groups of parameters into a list parameters = [] for key, value in parameters_grouped.items(): group_parameters = [param.strip() for param in value.split(',')] # Strip spaces from each parameter parameters.extend(group_parameters) # Add the group parameters to the main list # Read the ECO file file_content = read_dssat_file(validated_eco_file_path) lines = file_content.split('\n') # Locate header and target lines header_line_number = next(idx for idx, line in enumerate(lines) if line.startswith(header_start)) header_line = lines[header_line_number] # Get the number of the line that contains the parameters of the specified ecotype ecotype_line_number = find_ecotype(file_content, header_start, ecotype, validated_eco_file_path) if isinstance(ecotype_line_number, str): # Error message returned raise ValueError(ecotype_line_number) minima_line_number = find_ecotype(file_content, header_start, minima, validated_eco_file_path) if isinstance(minima_line_number, str): # Error message returned raise ValueError(minima_line_number) maxima_line_number = find_ecotype(file_content, header_start, maxima, validated_eco_file_path) if isinstance(maxima_line_number, str): # Error message returned raise ValueError(maxima_line_number) # Extract parameter values for ecotype, minima, and maxima def extract_parameter_values(line_number): parameter_values = {} for parameter in parameters: try: par_position = find_parameter_position(header_line, parameter) parameter_value = lines[line_number][par_position[0]:par_position[1] + 1].strip() parameter_values[parameter] = parameter_value except Exception: raise ValueError( f"Parameter '{parameter}' does not exist in the header line of {validated_eco_file_path}.") return parameter_values # Delete extra spaces on the parameter values def clean_parameter_values(parameter_dict): cleaned_dict = {} for key, value in parameter_dict.items(): if isinstance(value, str) and ' ' in value: cleaned_dict[key] = value.split()[1] else: cleaned_dict[key] = value return cleaned_dict minima_parameter_values = clean_parameter_values(extract_parameter_values(minima_line_number)) maxima_parameter_values = clean_parameter_values(extract_parameter_values(maxima_line_number)) # Dictionary to store current parameter values current_parameter_values = {} # Iterate each parameter in the list to generate the template parameter_par_truncated = {} count = 0 for parameter in parameters: # Get the parameter position on the line par_position = find_parameter_position(header_line, parameter) # Validate and adjust the starting position if necessary if par_position[0] < len(ecotype): par_position[0] = len(ecotype) # Extract the current value of the parameter from the line parameter_value = lines[ecotype_line_number][par_position[0]:par_position[1] + 1].strip() # Extract the second element when two numbers where obtained instead of one if len(str(parameter_value).split()) > 1: parameter_value = str(parameter_value).split()[-1] # Store the current value in the dictionary current_parameter_values[parameter] = parameter_value # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Enforce 3‑character for parameter keys OLD VERSION # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # # Get the length of a parameter including empty spaces # char_compl = header_line[par_position[0] + 1:par_position[1] + 1] # # # Calculate the number of available characters for the parameter # available_space = len(char_compl) - 2 # # # Build a 3-character base key for the parameter, padded with '-' # base_key = parameter.strip() # if len(base_key) > 3: # base_key = base_key[:3] # if len(base_key) < 3: # base_key = base_key.ljust(3, '-') # # truncated_parameter = base_key # # # Ensure uniqueness: if this 3-char key already exists, add a numeric suffix # counter = 1 # while any(truncated_parameter.strip() == existing.strip() for existing in parameter_par_truncated.values()): # suffix = str(counter) # core = base_key # # Trim core if needed so core+suffix stays within 3 characters # if len(core) + len(suffix) > 3: # core = core[:3 - len(suffix)] # truncated_parameter = (core + suffix).ljust(3, '-') # counter += 1 # # # Save the parameter complete name and truncated name into a dictionary # parameter_par_truncated[parameter] = truncated_parameter.strip() # # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # / Enforce 3‑character for parameter keys OLD VERSION # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Enforce 3-character parameter keys for ecotype parameters # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Get the length of a parameter including empty spaces char_compl = header_line[par_position[0] + 1:par_position[1] + 1] # Calculate the number of available characters for the parameter available_space = len(char_compl) - 2 # Build a 3-character base key: # - first character identifies parameter source # - remaining two characters come from the parameter name # For ecotype parameters, reserve "2" as the first character. parameter_clean = parameter.strip() source_code = '2' if len(parameter_clean) >= 2: base_key = source_code + parameter_clean[:2] elif len(parameter_clean) == 1: base_key = (source_code + parameter_clean).ljust(3, '-') else: base_key = source_code.ljust(3, '-') truncated_parameter = base_key # Ensure uniqueness within this ECO file only # If the 3-char key already exists, replace the last character(s) # with a counter while keeping the source code prefix. counter = 1 while any(truncated_parameter.strip() == existing.strip() for existing in parameter_par_truncated.values()): suffix = str(counter) # Keep the source code in the first position and use as much # of the parameter-derived part as possible before appending # the numeric suffix. core = base_key[:3 - len(suffix)] truncated_parameter = (core + suffix).ljust(3, '-') counter += 1 # Save the parameter complete name and truncated name into a dictionary parameter_par_truncated[parameter] = truncated_parameter.strip() # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # / Enforce 3-character parameter keys for ecotype parameters # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Avoid overflowing 3‑char fields # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Determine how wide the numeric field is in the ECO line field_width = par_position[1] - par_position[0] + 1 # If the field is only 3 characters wide, we can only fit "~X~" if field_width <= 3: short_key = truncated_parameter.strip() if not short_key: short_key = '0' short_key = short_key[0] variable_template = f"{mrk}{short_key}{mrk}" elif field_width == 4: # " ~X~" (still 4 chars, but only 1-char key) short_key = truncated_parameter.strip() if not short_key: short_key = '0' short_key = short_key[0] variable_template = f" {mrk}{short_key}{mrk}" else: # field_width >= 5: " ~XXX~" with full 3-char key variable_template = f" {mrk}{truncated_parameter}{mrk}" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # / Avoid overflowing 3‑char fields # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Extract the ecotype line to modify parameters ecotype_line = lines[ecotype_line_number] if count == 0: # Replace the content at the specified position with adjusted_template modified_line = ( ecotype_line[:par_position[0]] # Part of the line before the parameter + variable_template # Insert the adjusted template + ecotype_line[par_position[1] + 1:] # Part of the line after the parameter ) else: # Replace the content at the specified position with adjusted_template modified_line = ( modified_line[:par_position[0]] # Part of the line before the parameter + variable_template # Insert the adjusted template + modified_line[par_position[1] + 1:] # Part of the line after the parameter ) count += 1 # # Save the parameter compleate name and truncated name into a dictionary # parameter_par_truncated[parameter] = truncated_parameter.strip() # Insert the modified line back into the text lines[ecotype_line_number] = modified_line # Insert 'ptf' and marker at the beginning of the file content lines.insert(0, f"{tpl_first_line} {mrk}") # Validate output_path output_path = validate_output_path(output_path) # Add the file name and extension to the path for the new file output_new_file_path = os.path.join(output_path, os.path.splitext(os.path.basename(validated_eco_file_path))[ 0] + '_ECO' + '.' + new_template_file_extension) # Save the updated text to a new .TPL file with open(output_new_file_path, 'w') as file: file.write("\n".join(lines)) # Replace keys in current_parameter_values current_parameter_values = {parameter_par_truncated[key]: value for key, value in current_parameter_values.items() if key in parameter_par_truncated} minima_parameter_values = {parameter_par_truncated[key]: value for key, value in minima_parameter_values.items() if key in parameter_par_truncated} maxima_parameter_values = {parameter_par_truncated[key]: value for key, value in maxima_parameter_values.items() if key in parameter_par_truncated} # Update the values in parameters_grouped parameters_grouped = { key: ', '.join(parameter_par_truncated.get(param.strip(), param.strip()) for param in value.split(',')) for key, value in parameters_grouped.items() } print(f"Template file successfully created at: {output_new_file_path}") return {'parameters': current_parameter_values, 'minima_parameters': minima_parameter_values, 'maxima_parameters': maxima_parameter_values, 'parameters_grouped': parameters_grouped}, output_new_file_path except ValueError as ve: print(f"ValueError: {ve}") except FileNotFoundError as fe: print(f"FileNotFoundError: {fe}") except Exception as e: print(f"An unexpected error occurred: {e}")