Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions calibration/models/verification_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@


class VerificationRun(BaseRun):
hindcast_run = models.ForeignKey(HindcastRun, on_delete=models.CASCADE, db_index=True, related_name="verification_runs")
hindcast_run = models.ForeignKey(HindcastRun, on_delete=models.CASCADE, related_name="verification_runs")

class Meta:
db_table = "verification_run"
indexes = [
models.Index(fields=["hindcast_run"], name="idx_verif_hindcast_run"),
models.Index(fields=["status"], name="idx_verif_status"),
]

Expand Down
8 changes: 5 additions & 3 deletions calibration/run_util/run_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ def get_run_owner(run: BaseRun) -> User:
Return the owner associated with a run.

- CalibrationRun: owner is stored directly on the model.
- ValidationRun, ForecastRun, HindcastRun: owner is resolved via calibration_run.
- VerificationRun: owner is resolved via parent_run → calibration_run.
- ValidationRun, ColdStartRun, ForecastRun, and HindcastRun:
owner is resolved through calibration_run.
- VerificationRun: owner is resolved through
hindcast_run.calibration_run.

:param run: A BaseRun instance.
:return: The owner of the associated CalibrationRun.
Expand All @@ -120,7 +122,7 @@ def get_run_owner(run: BaseRun) -> User:
return run.calibration_run.owner

if isinstance(run, VerificationRun):
return run.parent_run.calibration_run.owner
return run.hindcast_run.calibration_run.owner

raise AttributeError(f"Cannot determine owner for run of type {type(run).__name__}")

Expand Down
2 changes: 0 additions & 2 deletions calibration/views/calibration_gage_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,6 @@ def reset_gage_dependent_state_on_change(
if geopackage and os.path.exists(geopackage):
os.remove(geopackage)

run.forcing_eds_dir_path = None

# The catchment count is no longer valid when the gage changes.
run.num_catchments = None

Expand Down
2 changes: 1 addition & 1 deletion calibration/views/calibration_log_files_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def resolve_log_context(
return None, error_return
assert verification_run is not None

calibration_run = verification_run.parent_run.calibration_run
calibration_run = verification_run.hindcast_run.calibration_run

else:
# Must be a calibration
Expand Down
63 changes: 27 additions & 36 deletions calibration/views/calibration_run_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def get_status_for_verification(verification_run: VerificationRun, include_perfo
- Verification timing and status fields
- Failure messages (if any)
- Performance metrics (only if requested and job is DONE or FAILED)
- A summarized view of the associated ForecastRun or HindcastRun
- A summarized view of the associated HindcastRun

All database access is read-only and executed inside a readonly transaction.

Expand All @@ -601,12 +601,12 @@ def get_status_for_verification(verification_run: VerificationRun, include_perfo
when the run status allows it.
:return: A dict suitable for the verification status response serializer.
"""
parent_run = verification_run.parent_run
hindcast_run = verification_run.hindcast_run

verification_data = {
'message': f'{get_job_description(verification_run)}, status is {verification_run.status.name}',
'verification_run_id': verification_run.id,
'calibration_run_id': parent_run.calibration_run_id,
'calibration_run_id': hindcast_run.calibration_run_id,
'status': verification_run.status.name,
'submit_date': verification_run.submit_date,
'sent_date': verification_run.sent_date,
Expand All @@ -629,46 +629,37 @@ def get_status_for_verification(verification_run: VerificationRun, include_perfo
if verification_metrics:
verification_data['performance_metrics'] = verification_metrics

parent_data = {
'calibration_run_id': parent_run.calibration_run_id,
'status': parent_run.status.name,
'configuration': parent_run.configuration.name,
'cycle_date': parent_run.cycle_date,
'submit_date': parent_run.submit_date,
'sent_date': parent_run.sent_date,
'run_start': parent_run.run_start,
'run_end': parent_run.run_end,
hindcast_data = {
'calibration_run_id': hindcast_run.calibration_run_id,
'hindcast_run_id': hindcast_run.id,
'status': hindcast_run.status.name,
'configuration': hindcast_run.configuration.name,
'cycle_date': hindcast_run.cycle_date,
'interval_cycle': hindcast_run.interval_cycle,
'num_iterations': hindcast_run.num_iterations,
'created_new_cold_start': hindcast_run.created_new_cold_start,
'submit_date': hindcast_run.submit_date,
'sent_date': hindcast_run.sent_date,
'run_start': hindcast_run.run_start,
'run_end': hindcast_run.run_end,
}

if isinstance(parent_run, ForecastRun):
parent_data['forecast_run_id'] = parent_run.id
elif isinstance(parent_run, HindcastRun):
parent_data['hindcast_run_id'] = parent_run.id
parent_data['interval_cycle'] = parent_run.interval_cycle
parent_data['num_iterations'] = parent_run.num_iterations
parent_data['created_new_cold_start'] = parent_run.created_new_cold_start
else:
raise TypeError(f"Unexpected verification parent run type: {type(parent_run).__name__}")

parent_failure_message = normalize_failure_messages(parent_run.failure_messages)
if parent_failure_message:
parent_data['failure_messages'] = parent_failure_message
hindcast_failure_message = normalize_failure_messages(hindcast_run.failure_messages)
if hindcast_failure_message:
hindcast_data['failure_messages'] = hindcast_failure_message

if parent_run.run_end and parent_run.submit_date:
parent_data['elapsed_time'] = parent_run.run_end - parent_run.submit_date
if hindcast_run.run_end and hindcast_run.submit_date:
hindcast_data['elapsed_time'] = hindcast_run.run_end - hindcast_run.submit_date

parent_metrics = (
get_performance_metrics(parent_run.performance_metrics)
if should_include_metrics(parent_run.status, include_performance_metrics)
hindcast_metrics = (
get_performance_metrics(hindcast_run.performance_metrics)
if should_include_metrics(hindcast_run.status, include_performance_metrics)
else None
)
if parent_metrics:
parent_data['performance_metrics'] = parent_metrics
if hindcast_metrics:
hindcast_data['performance_metrics'] = hindcast_metrics

if verification_run.forecast_run_id is not None:
verification_data['forecast_run'] = parent_data
else:
verification_data['hindcast_run'] = parent_data
verification_data['hindcast_run'] = hindcast_data

return verification_data

Expand Down
57 changes: 17 additions & 40 deletions calibration/views/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ def validate_run_instance(

allowed_statuses = [s.db_instance for s in effective_run_status]

# Some run types do not reach the archive flag through a single fixed parent.
# For example, VerificationRun may need to follow either
# `forecast_run__calibration_run__is_archived` or
# `hindcast_run__calibration_run__is_archived`. Since these are nested
# attribute paths rather than direct attributes on `run`, we use
# `get_nested_attr()` to walk each path safely.
# Some run types reach the archive flag through a related run rather than
# through a direct attribute. Since these are nested attribute paths rather
# than direct attributes on `run`, use `get_nested_attr()` to walk each path
# safely.
is_archived = any(get_nested_attr(run, field, False) for field in is_archived_fields)
if is_archived and not include_archived:
return ResponseError(
Expand Down Expand Up @@ -444,24 +442,12 @@ def get_verification_run(
verification_run_id,
user,
run_status,
owner_fields=(
'forecast_run__calibration_run__owner',
'hindcast_run__calibration_run__owner',
),
is_archived_fields=(
'forecast_run__calibration_run__is_archived',
'hindcast_run__calibration_run__is_archived',
),
owner_fields=('hindcast_run__calibration_run__owner',),
is_archived_fields=('hindcast_run__calibration_run__is_archived',),
include_archived=include_archived,
select_related_fields=(
'status',
'performance_metrics',
'forecast_run',
'forecast_run__status',
'forecast_run__performance_metrics',
'forecast_run__configuration',
'forecast_run__calibration_run',
'forecast_run__calibration_run__owner',
'hindcast_run',
'hindcast_run__status',
'hindcast_run__performance_metrics',
Expand Down Expand Up @@ -706,25 +692,17 @@ def create_hindcast_run_internal(
return hindcast_run


def create_verification_run_internal(run: ForecastRun | HindcastRun) -> VerificationRun:
def create_verification_run_internal(hindcast_run: HindcastRun) -> VerificationRun:
"""
Create a new VerificationRun for the given ForecastRun or HindcastRun.

- Calls create_verification_input(verification_run) to generate the config
Create a new VerificationRun for the given HindcastRun.

:param run: Forecast or Hindcast job to associate with this verification run
:param hindcast_run: Hindcast job to associate with this verification run.
:return: New VerificationRun instance.
"""
if isinstance(run, ForecastRun):
verification_run = VerificationRun.objects.create(
status=StatusEnum.SAVED.db_instance,
forecast_run=run,
)
else:
verification_run = VerificationRun.objects.create(
status=StatusEnum.SAVED.db_instance,
hindcast_run=run,
)
verification_run = VerificationRun.objects.create(
status=StatusEnum.SAVED.db_instance,
hindcast_run=hindcast_run,
)

os.makedirs(get_verification_run_dir(verification_run))
logger.info(f"Creating {get_job_description(verification_run)}")
Expand Down Expand Up @@ -1072,12 +1050,11 @@ def get_job_description(run: BaseRun) -> str:
elif isinstance(run, ColdStartRun):
return f"Cold Start Job {run.id} for Calibration Job {run.calibration_run.id}, user: {run.calibration_run.owner.username}"
elif isinstance(run, VerificationRun):
parent_run = run.parent_run
parent_job_type = 'Forecast' if run.forecast_run_id is not None else 'Hindcast'
hindcast_run = run.hindcast_run
return (
f"Verification Job {run.id} for {parent_job_type} Job {parent_run.id} "
f"for Calibration Job {parent_run.calibration_run.id}, "
f"user: {parent_run.calibration_run.owner.username}"
f"Verification Job {run.id} for Hindcast Job {hindcast_run.id} "
f"for Calibration Job {hindcast_run.calibration_run.id}, "
f"user: {hindcast_run.calibration_run.owner.username}"
)

raise ValueError(f"Unknown job type: {type(run).__name__}")
Expand Down
104 changes: 40 additions & 64 deletions calibration/views/verification_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import yaml

from calibration.enums import HindcastConfigEnum, ForecastConfigEnum
from calibration.enums import HindcastConfigEnum
from calibration.models import VerificationRun
from calibration.util.caching import generate_forecast_config_yaml
from calibration.util.ngen_locations import get_verification_run_dir, VERF_CROSSWALK_NGEN_FILE, get_forecast_output_file_path, \
from calibration.util.ngen_locations import get_verification_run_dir, VERF_CROSSWALK_NGEN_FILE, \
get_verification_yaml_config_file, get_hindcast_output_file_name, get_hindcast_dir, get_observational_file_for_hindcast
from calibration.views.called_from import called_from
from calibration.views.common import format_datetime
Expand Down Expand Up @@ -81,15 +81,16 @@

def create_verification_input(run: VerificationRun) -> str:
"""
Create the nwm.eval configuration for a hindcast-based verification run.

:param run: The VerificationRun instance to validate and prepare.
:return: Path to the generated config file.
"""
logger.info(called_from())

is_hindcast = run.hindcast_run_id is not None
parent_run = run.parent_run
calibration_run = parent_run.calibration_run
configuration_internal_name = parent_run.configuration.internal_name
hindcast_run = run.hindcast_run
calibration_run = hindcast_run.calibration_run
configuration_internal_name = hindcast_run.configuration.internal_name

short_range = configuration_internal_name.startswith('short_range')
medium_range = configuration_internal_name.startswith('medium_range')
Expand All @@ -114,84 +115,59 @@ def create_verification_input(run: VerificationRun) -> str:
file_paths['base_dir'] = get_verification_run_dir(run)
file_paths['crosswalk_file'] = {'ngen': VERF_CROSSWALK_NGEN_FILE}
file_paths['fcst_config_file'] = generate_forecast_config_yaml(
enum_class=HindcastConfigEnum if is_hindcast else ForecastConfigEnum
enum_class=HindcastConfigEnum
)

if is_hindcast:
hindcast_run = run.hindcast_run
assert hindcast_run is not None

file_paths['fcst_data_dir'] = {
calibration_run.job_name: get_hindcast_dir(hindcast_run)
}
file_paths['fcst_data_file'] = get_hindcast_output_file_name(hindcast_run)
file_paths['obs_data_file'] = get_observational_file_for_hindcast(hindcast_run)
else:
forecast_run = run.forecast_run
assert forecast_run is not None

file_paths['fcst_data_file'] = {
calibration_run.job_name: get_forecast_output_file_path(forecast_run)
}

file_paths['fcst_data_dir'] = {
calibration_run.job_name: get_hindcast_dir(hindcast_run)
}
file_paths['fcst_data_file'] = get_hindcast_output_file_name(hindcast_run)
file_paths['obs_data_file'] = get_observational_file_for_hindcast(hindcast_run)
file_paths['output_dir'] = get_verification_run_dir(run)

general: dict[str, Any] = config['general']

# Override values in YAML with info from our parent run / calibration run
# Override values in YAML with info from the hindcast run / calibration run
general['location_set_name'] = 'usgs_' + calibration_run.gage.gage_id
general['location_list'] = [calibration_run.gage.gage_id]
general['nwm_configuration'] = configuration_internal_name
general['dataset_name'] = [calibration_run.job_name]
general['nwm_version'] = ['ngen']
general['forecast_start_date'] = [format_datetime(parent_run.cycle_date)]
general['forecast_start_date'] = [format_datetime(hindcast_run.cycle_date)]

# Forecast verification uses one cycle, so the end date is the same as the start date.
#
# Hindcast verification spans multiple cycles. For hindcast, forecast_end_date should be
# Hindcast verification spans multiple cycles. forecast_end_date should be
# the start time of the last hindcast cycle.
if is_hindcast:
hindcast_run = run.hindcast_run
assert hindcast_run is not None

last_cycle_date = hindcast_run.cycle_date + timedelta(
hours=hindcast_run.interval_cycle * (hindcast_run.num_iterations - 1)
)
general['forecast_end_date'] = [format_datetime(last_cycle_date)]
else:
general['forecast_end_date'] = [format_datetime(parent_run.cycle_date)]
last_cycle_date = hindcast_run.cycle_date + timedelta(
hours=hindcast_run.interval_cycle * (hindcast_run.num_iterations - 1)
)
general['forecast_end_date'] = [format_datetime(last_cycle_date)]

nwm_forecast: dict[str, Any] = config['nwm_forecast']
nwm_forecast['data_source'] = 'hindcast' if is_hindcast else 'ngenCERF'
nwm_forecast['data_source'] = 'hindcast'

metrics: dict[str, Any] = config['metrics']
metrics['lead_times'] = ['all', '1-5', '6-10', '11-18', 'all_aggregated'] if is_hindcast else ['all_aggregated']

plots: dict[str, Any] = config['plots']

if is_hindcast:
if short_range:
metrics_lead_times = metrics_lead_time_short_range
time_series_lead_times = time_series_lead_times_short_range
bar_chart_lead_times = bar_chart_lead_times_short_range
elif medium_range:
metrics_lead_times = metrics_lead_time_medium_range
time_series_lead_times = time_series_lead_times_medium_range
bar_chart_lead_times = bar_chart_lead_times_medium_range
elif long_range:
metrics_lead_times = metrics_lead_time_long_range
time_series_lead_times = time_series_lead_times_long_range
bar_chart_lead_times = bar_chart_lead_times_long_range
else:
raise ValueError(f"Unsupported hindcast configuration: {configuration_internal_name}")

metrics['lead_times'] = metrics_lead_times
plots['time_series']['lead_times'] = time_series_lead_times
plots['metric_table']['lead_times'] = bar_chart_lead_times # Use same as bar chart
plots['barchart']['lead_times'] = bar_chart_lead_times
plots['barchart']['metric_subset'] = ['KGE', 'NSE', 'CORR', 'NNSE', 'PBIAS']
if short_range:
metrics_lead_times = metrics_lead_time_short_range
time_series_lead_times = time_series_lead_times_short_range
bar_chart_lead_times = bar_chart_lead_times_short_range
elif medium_range:
metrics_lead_times = metrics_lead_time_medium_range
time_series_lead_times = time_series_lead_times_medium_range
bar_chart_lead_times = bar_chart_lead_times_medium_range
elif long_range:
metrics_lead_times = metrics_lead_time_long_range
time_series_lead_times = time_series_lead_times_long_range
bar_chart_lead_times = bar_chart_lead_times_long_range
else:
metrics['lead_times'] = ['all_aggregated']
raise ValueError(f"Unsupported hindcast configuration: {configuration_internal_name}")

metrics['lead_times'] = metrics_lead_times
plots['time_series']['lead_times'] = time_series_lead_times
plots['metric_table']['lead_times'] = bar_chart_lead_times # Use same as bar chart
plots['barchart']['lead_times'] = bar_chart_lead_times
plots['barchart']['metric_subset'] = ['KGE', 'NSE', 'CORR', 'NNSE', 'PBIAS']

# -----------------------------
# FILE WRITE PHASE
Expand Down
Loading