import yaml
from dpest.functions import *
[docs]
def cul(
cultivar=None,
cul_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
):
"""
.. contents:: Table of Contents
:local:
:depth: 2
Creates a ``PEST template file (.TPL)`` for CERES-Wheat cultivar parameters based on the ``DSSAT cultivar file (.CUL)``. This module is specific to the CERES-Wheat model and uses default values tailored for this model.
**Required Arguments:**
=======
* **cultivar** (*str*): Name or ID of the cultivar to modify. This should match either the ``VAR#`` (cultivar ID) or ``VAR-NAM`` (cultivar name) column in the ``DSSAT cultivar file (.CUL)``.
* **cul_file_path** (*str*): Full path to the ``DSSAT cultivar file (.CUL)``. Typically, this is the path to the ``WHCER048.CUL`` file, usually located at ``C:\DSSAT48\Genotype\WHCER048.CUL``.
**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: "@VAR#"*): Identifier for the header row in the ``DSSAT cultivar file (.CUL)``.
* **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 parameter values.
* **maxima** (*str*, *default: "999992"*): Row identifier for the maxima 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*): Cultivar parameters to calibrate, grouped and comma-separated. If not provided, all cultivar parameters are calibrated. For example: ``P='P1V, P1D, P5', G='G1, G2, G3', PHINT='PHINT'``. Where, `P`, `G`, and `PHINT` are cultivar parameter group names, and the values are the specific cultivar parameters to calibrate, using the same names as in the ``DSSAT cultivar file (.CUL)``. Parameter group names should be less than 12 characters.
**Returns:**
=======
* *tuple*: A tuple containing:
* *dict*: A dictionary containing:
* ``'parameters'``: Current cultivar parameter values for the specified cultivar.
* ``'minima_parameters'``: Minima values for all cultivar parameters.
* ``'maxima_parameters'``: Maxima values for all cultivar parameters.
* ``'parameters_grouped'``: The grouped cultivar 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 cul
# Call the module with only the required arguments
my_cultivar_parameters, my_cultivar_tpl_path = cul(
cultivar = 'MANITOU',
cul_file_path = 'C:/DSSAT48/Genotype/WHCER048.CUL'
)
# The returned tuple and path are saved in the variables, can be used in any names that the user prefer, to call them later
This example creates a ``PEST template file (.TPL)`` using only the required arguments. Note that the returned tuple ``(cultivar_parameters, cultivar_tpl_path)`` is captured. The ``my_cultivar_parameters`` dictionary will be used later to make the control file's parameter groups and parameters sections using the ``pst`` module. The ``cultivar_tpl_path`` path will be used in the ``input_output_file_pairs`` argument of the ``pst`` module to match the original cultivar file to the ``PEST template file (.TPL)``.
2. **Specifying Parameter Groups (Tuple Not Saved):**
.. code-block:: python
from dpest import cul
# Call the module specifying parameter groups, but not saving the returned tuple
cul(
cultivar = 'MANITOU',
cul_file_path = 'C:/DSSAT48/Genotype/WHCER048.CUL',
P = 'P1V, P1D',
G = 'G1'
)
This example demonstrates how to specify the ``parameters_grouped`` argument to calibrate only specific cultivar 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 cultivar parameters and path for the ``pst`` module, the returned tuple should be saved in two variables.
"""
# Defaul variable values
yml_cultivar_block = 'CULTIVAR_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 package-level defaults from dpest/arguments.yml
arguments_file = os.path.join(current_dir, 'arguments.yml')
# Ensure the package-level defaults from dpest/arguments.yml file exists
if not os.path.isfile(arguments_file):
raise FileNotFoundError(f"YAML file not found: {arguments_file}")
# Load package-level defaults from dpest/arguments.yml configuration
with open(arguments_file, 'r') as yml_file:
yaml_data = yaml.safe_load(yml_file)
# Validate cultivar
if cultivar is None:
raise ValueError("The 'cultivar' argument is required and must be specified by the user.")
# Validate cul_file_path
validated_cul_file_path = validate_file(cul_file_path, '.CUL')
# Validate marker delimiters using the validate_marker() function
mrk = validate_marker(mrk, "mrk")
# Get the cultivar template file variables
function_arguments = yaml_data[yml_cultivar_block][yaml_file_variables]
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 CUL filename (e.g. WHCER)
cul_basename = os.path.basename(validated_cul_file_path) # WHCER048.CUL
genotype_key = cul_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 cultivar parameter groups is not available "
f"for the CUL file '{cul_basename}' (genotype key '{genotype_key}').\n\n"
"Please provide the 'parameters_grouped' argument explicitly when "
"calling cul(), for example:\n"
" cul(..., P='P1V, P1D', G='G1', PHINT='PHINT')"
) 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 CULTIVAR_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_cultivar_block][yaml_parameters]
except KeyError as exc:
raise ValueError(
"No default cultivar parameter groups are defined for this crop/model.\n\n"
"To use cul() with this CUL file, you must provide the "
"'parameters_grouped' argument explicitly, for example:\n"
" cul(\n"
" cultivar='MANITOU',\n"
" cul_file_path='C:/DSSAT48/Genotype/WHCER048.CUL',\n"
" P='P1V, P1D',\n"
" G='G1',\n"
" PHINT='PHINT',\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 CUL file
file_content = read_dssat_file(validated_cul_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 cultivar
cultivar_line_number = find_cultivar(file_content, header_start, cultivar, validated_cul_file_path)
if isinstance(cultivar_line_number, str): # Error message returned
raise ValueError(cultivar_line_number)
minima_line_number = find_cultivar(file_content, header_start, minima, validated_cul_file_path)
if isinstance(minima_line_number, str): # Error message returned
raise ValueError(minima_line_number)
maxima_line_number = find_cultivar(file_content, header_start, maxima, validated_cul_file_path)
if isinstance(maxima_line_number, str): # Error message returned
raise ValueError(maxima_line_number)
# Extract parameter values for cultivar, 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_cul_file_path}.")
return parameter_values
minima_parameter_values = extract_parameter_values(minima_line_number)
maxima_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)
# Extract the current value of the parameter from the line
parameter_value = lines[cultivar_line_number][par_position[0]:par_position[1] + 1].strip()
# Store the current value in the dictionary
current_parameter_values[parameter] = parameter_value
# 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
# # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# # Enforce 3-character parameter keys for cultivar parameters OLD VERSION
# # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# # 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
#
# # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# # / Enforce 3-character parameter keys for cultivar parameters OLD VERSION
# # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Enforce 3-character parameter keys for cultivar parameters
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Build a 3-character base key:
# - first character identifies parameter source
# - remaining two characters come from the parameter name
# For cultivar parameters, reserve "3" as the first character.
parameter_clean = parameter.strip()
source_code = '3'
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 CUL 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
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# / Enforce 3-character parameter keys for cultivar parameters
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Save the parameter complete name and truncated name into a dictionary
parameter_par_truncated[parameter] = truncated_parameter.strip()
# 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 cultivar line to modify parameters
cultivar_line = lines[cultivar_line_number]
if count == 0:
# Replace the content at the specified position with adjusted_template
modified_line = (
cultivar_line[:par_position[0]] # Part of the line before the parameter
+ variable_template # Insert the adjusted template
+ cultivar_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
# Insert the modified line back into the text
lines[cultivar_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_cul_file_path))[
0] + '_CUL' + '.' + 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}")