Skip to content

Commit c780d3a

Browse files
committed
simplify name of validators and utils functions
1 parent fcc13a0 commit c780d3a

3 files changed

Lines changed: 109 additions & 53 deletions

File tree

‎pySWATPlus/types.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ class ParameterBase(BaseModel):
88
units: typing.Optional[list[int]] = None
99
conditions: typing.Optional[dict[str, list[str]]] = None
1010

11-
@field_validator("units")
11+
@field_validator('units')
1212
@classmethod
1313
def validate_units(cls, v: typing.Optional[typing.Iterable[int]]) -> typing.Optional[typing.Iterable[int]]:
1414
if v is not None and any(num <= 0 for num in v):
15-
raise ValueError(f"All unit IDs must be > 0, got {list(v)}")
15+
raise ValueError(f'All unit IDs must be > 0, got {list(v)}')
1616
return list(v) if v is not None else None
1717

1818

‎pySWATPlus/utils.py‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pydantic import BaseModel
88
import hashlib
99
import json
10+
import datetime
1011

1112

1213
def _build_line_to_add(
@@ -35,6 +36,24 @@ def _build_line_to_add(
3536
return arg_to_add.rstrip() + '\n'
3637

3738

39+
def _date_str_to_object(
40+
date_str: str
41+
) -> datetime.date:
42+
'''
43+
Converts a date string in 'YYYY-MM-DD' format to a `datetime.date` object
44+
'''
45+
46+
date_fmt = '%d-%b-%Y'
47+
try:
48+
get_date = datetime.datetime.strptime(date_str, date_fmt).date()
49+
except ValueError:
50+
raise ValueError(
51+
f'Invalid date format: "{date_str}"; expected format is DD-Mon-YYYY (e.g., 15-Mar-2010)'
52+
)
53+
54+
return get_date
55+
56+
3857
def _clean(
3958
df: pandas.DataFrame
4059
) -> pandas.DataFrame:
@@ -85,34 +104,39 @@ def _ensure_path(p: str | pathlib.Path) -> pathlib.Path:
85104
'''
86105
Validate and convert a path-like argument to a resolved pathlib.Path object.
87106
'''
107+
88108
if not isinstance(p, (str, pathlib.Path)):
89109
raise TypeError(f"Argument must be a string or Path object, got {type(p).__name__}")
90110
return pathlib.Path(p).resolve()
91111

92112

93-
def _format_val_field(value: float) -> str:
94-
"""
113+
def _format_val_field(
114+
value: float
115+
) -> str:
116+
'''
95117
Format a number for the VAL column:
96118
- 16 characters total: 1 leading space + 15-character numeric field
97119
- Right-aligned
98120
- Fixed-point if integer part fits; scientific if too large
99-
"""
121+
'''
100122

101123
# Convert to string without formatting
102124
s = str(value)
103125

104126
if len(s) > 15:
105127
# Use scientific notation
106-
formatted = f"{value:.6e}"
128+
formatted = f'{value:.6e}'
107129
else:
108130
# If it fits, just use normal string
109131
formatted = s
110132

111133
# Right-align to 16 characters
112-
return f"{formatted:>16}"
134+
return f'{formatted:>16}'
113135

114136

115-
def _compact_units(unit_list: Iterable[int]) -> list[int]:
137+
def _compact_units(
138+
unit_list: Iterable[int]
139+
) -> list[int]:
116140
'''
117141
Compact a 1-based list of unit IDs into SWAT units syntax.
118142
@@ -122,6 +146,7 @@ def _compact_units(unit_list: Iterable[int]) -> list[int]:
122146
123147
All IDs must be 1-based (Fortran-style).
124148
'''
149+
125150
if not unit_list:
126151
return []
127152

@@ -158,23 +183,28 @@ def _parse_conditions(
158183
'''
159184
Parse the conditions that must be added to that parameter in calibration.cal file
160185
'''
186+
161187
conditions = parameters.conditions
162188
if not conditions:
163189
return []
164190

165191
conditions_parsed = []
166192
for parameter, condition_keys in conditions.items():
167193
for key in condition_keys:
168-
conditions_parsed.append(f"{parameter:<19}{'=':<15} {0:<16}{key}")
194+
conditions_parsed.append(f'{parameter:<19}{'=':<15} {0:<16}{key}')
169195

170196
return conditions_parsed
171197

172198

173-
def _make_unique_param_name(param_key: str, model: BaseModel) -> str:
174-
"""
199+
def _make_unique_param_name(
200+
param_key: str,
201+
model: BaseModel
202+
) -> str:
203+
'''
175204
Generate a unique, deterministic parameter identifier by combining the parameter key
176205
with a hash of the Pydantic model's serialized contents.
177-
"""
206+
'''
207+
178208
payload = json.dumps(model.model_dump(), sort_keys=True)
179209
uid = hashlib.md5(payload.encode()).hexdigest()
180-
return f"{param_key}|{uid}"
210+
return f'{param_key}|{uid}'

‎pySWATPlus/validators.py‎

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pathlib
33
import typing
44
import types
5-
from datetime import datetime
5+
import datetime
66
from .types import ParameterModel, ParameterBoundedModel
77

88

@@ -11,7 +11,7 @@ def _variable_origin_static_type(
1111
vars_values: dict[str, typing.Any]
1212
) -> None:
1313
'''
14-
Validates input variables against their expected types.
14+
Checks that input variables match their expected origin types.
1515
'''
1616

1717
# iterate name and type of method variables
@@ -55,7 +55,7 @@ def _path_directory(
5555
path: pathlib.Path
5656
) -> None:
5757
'''
58-
Validates path of the direcotry.
58+
Ensures the input path refers to a valid directory.
5959
'''
6060

6161
if not path.is_dir():
@@ -66,20 +66,40 @@ def _path_directory(
6666
return None
6767

6868

69+
def _date_begin_earlier_end(
70+
begin_date: datetime.date,
71+
end_date: datetime.date
72+
) -> None:
73+
'''
74+
Checks that begin date is earlier than end date.
75+
'''
76+
77+
date_fmt = '%d-%b-%Y'
78+
79+
if begin_date >= end_date:
80+
raise ValueError(
81+
f'begin_date {begin_date.strftime(date_fmt)} must be earlier than end_date {end_date.strftime(date_fmt)}'
82+
)
83+
84+
6985
def _validate_date_str(
7086
date_str: str
7187
) -> None:
7288
'''
7389
Validates a date string in 'YYYY-MM-DD' format.
7490
Raises ValueError if invalid.
7591
'''
92+
7693
try:
77-
datetime.strptime(date_str, '%Y-%m-%d').date()
94+
datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
7895
except ValueError:
7996
raise ValueError(f'Invalid date format: "{date_str}". Expected YYYY-MM-DD.')
8097

8198

82-
def _validate_units(param_change: ParameterModel, txtinout_path: pathlib.Path) -> None:
99+
def _calibration_units(
100+
txtinout_path: pathlib.Path,
101+
param_change: ParameterModel
102+
) -> None:
83103
'''
84104
Validate units for a given parameter change against calibration parameters.
85105
'''
@@ -98,26 +118,19 @@ def _validate_units(param_change: ParameterModel, txtinout_path: pathlib.Path) -
98118

99119
# Get the object type for the parameter
100120
row = cal_parms_df.loc[cal_parms_df['name'] == name, 'obj_typ']
101-
if row.empty:
102-
raise ValueError(f"Parameter '{name}' not found in file 'cal_parms.cal'.")
103-
104121
obj_type = row.iloc[0]
105-
if not obj_type:
106-
raise ValueError("Missing 'obj_typ' column in 'cal_parms.cal' file.")
107122

108-
# Supported mapping of obj_type → file
123+
# Supported mapping of obj_type
109124
obj_type_files = {
110125
'hru': 'hru-data.hru',
111126
'sol': 'hru-data.hru',
112127
'res': 'reservoir.res',
113-
'aqu': 'aquifer.aqu',
128+
'aqu': 'aquifer.aqu'
114129
}
115-
116130
if obj_type not in obj_type_files:
117-
supported = ", ".join(obj_type_files.keys())
118131
raise ValueError(
119-
f"Parameter '{name}' does not support units. "
120-
f"Only parameters of type [{supported}] support units."
132+
f'Parameter "{name}" with obj_type "{obj_type}" in "cal_parms.cal" does not support "units" key. '
133+
f'Supported obj_type: [{', '.join(obj_type_files.keys())}].'
121134
)
122135

123136
file = obj_type_files[obj_type]
@@ -135,13 +148,16 @@ def _validate_units(param_change: ParameterModel, txtinout_path: pathlib.Path) -
135148
max_unit = max(units)
136149
if len(df) < max_unit:
137150
raise ValueError(
138-
f"Invalid units for parameter '{name}'. "
139-
f"Some ids exceed the maximum available in {file} "
140-
f"(requested up to {max_unit}, available {len(df)})."
151+
f'Invalid units for parameter "{name}". '
152+
f'Some ids exceed the maximum available in {file} '
153+
f'(requested up to {max_unit}, available {len(df)}).'
141154
)
142155

143156

144-
def _validate_conditions(param_change: ParameterModel, txtinout_path: pathlib.Path) -> None:
157+
def _calibration_conditions(
158+
txtinout_path: pathlib.Path,
159+
param_change: ParameterModel
160+
) -> None:
145161
'''
146162
Validate conditions for a given parameter change against calibration parameters.
147163
'''
@@ -172,59 +188,69 @@ def _validate_conditions(param_change: ParameterModel, txtinout_path: pathlib.Pa
172188
for cond_name, cond_values in conditions.items():
173189
if cond_name not in supported_conditions:
174190
raise ValueError(
175-
f"Condition '{cond_name}' for parameter '{name}' is not supported. "
176-
f"Available conditions are: {', '.join(sorted(supported_conditions))}."
191+
f'Condition "{cond_name}" for parameter "{name}" is not supported. '
192+
f'Available conditions are: {', '.join(sorted(supported_conditions))}.'
177193
)
178194

179195
valid_values = validators.get(cond_name, set())
180196
for val in cond_values:
181197
if val not in valid_values:
182198
raise ValueError(
183-
f"Condition '{cond_name}' for parameter '{name}' "
184-
f"has invalid value '{val}'. "
185-
f"Valid values are: {sorted(valid_values)}."
199+
f'Condition "{cond_name}" for parameter "{name}" has invalid value "{val}". '
200+
f'Expected are: {sorted(valid_values)}.'
186201
)
187202

188203

189-
def _validate_conditions_and_units(parameters: list[ParameterModel], txtinout_path: pathlib.Path) -> None:
204+
def _calibration_conditions_and_units(
205+
txtinout_path: pathlib.Path,
206+
parameters: list[ParameterModel]
207+
) -> None:
190208
'''
191209
This function checks:
192210
- That the parameter exists in the calibration parameters.
193211
- That the parameter supports units/conditions (based on 'obj_typ').
194212
- That specified units correspond to valid IDs in the relevant SWAT+ input files.
195213
- That conditions (if applicable) exist and are valid.
196214
'''
215+
197216
for param_change in parameters:
198217
try:
199-
_validate_conditions(param_change, txtinout_path)
200-
_validate_units(param_change, txtinout_path)
218+
_calibration_conditions(
219+
txtinout_path=txtinout_path,
220+
param_change=param_change
221+
)
222+
_calibration_units(
223+
txtinout_path=txtinout_path,
224+
param_change=param_change
225+
)
201226
except ValueError as e:
202227
raise ValueError(
203-
f"{e}\n\n"
204-
f"If you want to ignore the validation, set "
205-
f"'skip_units_and_conditions_validation=True'"
228+
f'{e}\n\n'
229+
f'If you want to ignore the validation, set "skip_validation=True"'
206230
) from e
207231

208232

209-
def _validate_cal_parameters(
210-
txtinout_folder: pathlib.Path,
233+
def _calibration_parameters(
234+
txtinout_path: pathlib.Path,
211235
parameters: list[ParameterBoundedModel] | list[ParameterModel]
212236
) -> None:
213237
'''
214238
Check if parameters exists in cal_parms.cal
215239
'''
216240

217-
file_path = txtinout_folder / "cal_parms.cal"
218-
219-
if not file_path.exists():
220-
raise FileNotFoundError("cal_parms.cal file does not exist in the TxtInOut folder")
241+
# Path of cal_parms.cal
242+
file_path = txtinout_path / 'cal_parms.cal'
221243

222-
cal_parms_df = pandas.read_csv(
244+
# DataFrame from cal_parms.cal file
245+
parms_df = pandas.read_csv(
223246
filepath_or_buffer=file_path,
224247
skiprows=2,
225248
sep=r'\s+'
226249
)
227250

251+
# Check validity of input calibration parameter name
228252
for param in parameters:
229-
if param.name not in cal_parms_df['name'].values:
230-
raise ValueError(f"The parameter '{param.name}' is not in cal_parms.cal")
253+
if param.name not in parms_df['name'].values:
254+
raise ValueError(
255+
f'Calibration parameter "{param.name}" not found in cal_parms.cal file'
256+
)

0 commit comments

Comments
 (0)