Add query_forecast_end function and integrate into job processing

- Introduced the `query_forecast_end` function in `incubations.py` to determine the end date for query-synthesis weather based on incubation status and treatment intervals.
- Updated the `_run_job_inner` function in `job.py` to utilize `query_forecast_end`, enhancing the job's ability to handle future disease forecasting.
- Added unit tests in `test_incubations.py` to validate the behavior of `query_forecast_end` under various scenarios, ensuring accurate forecasting logic.
- Improved overall code clarity and maintainability with the new function integration.
This commit is contained in:
Arsham Mirehvandi 2026-10-22 09:35:16 +02:00
parent ebc8ad26b6
commit 47b4300391
3 changed files with 63 additions and 1 deletions

View File

@ -46,6 +46,7 @@ from pipeline.stages.incubations import (
forecast_has_risk, forecast_has_risk,
incubation_start_covered, incubation_start_covered,
parse_register, parse_register,
query_forecast_end,
reconcile_register, reconcile_register,
register_payload, register_payload,
) )
@ -251,7 +252,7 @@ def _run_job_inner(resources: Resources, job: Job, as_of: date, dry_run: bool) -
disease.disease_english, disease.disease_english,
weather, weather,
phenology, phenology,
future_disease, query_forecast_end(future_disease, incubation_register, as_of, interval_open),
) )
payload: dict[str, Any] = { payload: dict[str, Any] = {

View File

@ -214,6 +214,27 @@ def first_future_disease_date(
return None return None
def query_forecast_end(
future_disease: date | None,
register: dict[str, IncubationRecord],
as_of: date,
interval_open: bool,
) -> date | None:
"""End date for query-synthesis weather. None means today through as_of+5.
An uncovered incubation that has already started, with the treatment
interval closed, uses the full forward window. Otherwise the window ends
on the first future risk day.
"""
underway = any(
not record.infection_covered and record.start_date <= as_of
for record in register.values()
)
if underway and not interval_open:
return None
return future_disease
def covered_by_key(register: dict[str, IncubationRecord]) -> dict[str, bool]: def covered_by_key(register: dict[str, IncubationRecord]) -> dict[str, bool]:
return {key: record.infection_covered for key, record in register.items()} return {key: record.infection_covered for key, record in register.items()}

View File

@ -16,6 +16,7 @@ from pipeline.stages.incubations import (
forecast_has_risk, forecast_has_risk,
incubation_start_covered, incubation_start_covered,
parse_register, parse_register,
query_forecast_end,
reconcile_register, reconcile_register,
) )
from pipeline.stages.products import ProductIndex, ProductRow from pipeline.stages.products import ProductIndex, ProductRow
@ -247,6 +248,45 @@ class RiskAndPrefilterTests(unittest.TestCase):
register = {"incubazioneprimaria2": _record(_FUTURE, True)} register = {"incubazioneprimaria2": _record(_FUTURE, True)}
self.assertEqual(first_future_disease_date(forecasts, register, _AS_OF), _FAR) self.assertEqual(first_future_disease_date(forecasts, register, _AS_OF), _FAR)
def test_underway_uncovered_incubation_uses_the_full_window(self) -> None:
tomorrow = _AS_OF + timedelta(days=1)
forecasts = {
_PAST: ["INCUBAZPRIMARIA2_40"],
tomorrow: ["INCUBAZPRIMARIA2_60"],
_FAR: ["FASE5"],
}
register = {"incubazioneprimaria2": _record(_PAST, False)}
future = first_future_disease_date(forecasts, register, _AS_OF)
self.assertEqual(future, tomorrow)
self.assertIsNone(query_forecast_end(future, register, _AS_OF, False))
def test_open_interval_keeps_the_first_future_day(self) -> None:
tomorrow = _AS_OF + timedelta(days=1)
forecasts = {tomorrow: ["INCUBAZPRIMARIA2_60"]}
register = {"incubazioneprimaria2": _record(_PAST, False)}
future = first_future_disease_date(forecasts, register, _AS_OF)
self.assertEqual(query_forecast_end(future, register, _AS_OF, True), tomorrow)
def test_future_only_incubation_keeps_its_start_day(self) -> None:
forecasts = {_FUTURE: ["INCUBAZPRIMARIA2_40"]}
register = {"incubazioneprimaria2": _record(_FUTURE, False)}
future = first_future_disease_date(forecasts, register, _AS_OF)
self.assertEqual(query_forecast_end(future, register, _AS_OF, False), _FUTURE)
def test_covered_incubation_keeps_a_later_fase5(self) -> None:
forecasts = {_PAST: ["INCUBAZPRIMARIA2_80"], _FAR: ["FASE5"]}
register = {"incubazioneprimaria2": _record(_PAST, True)}
future = first_future_disease_date(forecasts, register, _AS_OF)
self.assertEqual(future, _FAR)
self.assertEqual(query_forecast_end(future, register, _AS_OF, False), _FAR)
def test_incubation_starting_today_uses_the_full_window(self) -> None:
tomorrow = _AS_OF + timedelta(days=1)
forecasts = {_TODAY: ["INCUBAZPRIMARIA2_10"], tomorrow: ["INCUBAZPRIMARIA2_20"]}
register = {"incubazioneprimaria2": _record(_TODAY, False)}
future = first_future_disease_date(forecasts, register, _AS_OF)
self.assertIsNone(query_forecast_end(future, register, _AS_OF, False))
def test_uncovered_past_incubation_selects_incubazprimaria(self) -> None: def test_uncovered_past_incubation_selects_incubazprimaria(self) -> None:
forecasts = {_PAST: ["INCUBAZPRIMARIA2_80.7", "FASE5"]} forecasts = {_PAST: ["INCUBAZPRIMARIA2_80.7", "FASE5"]}
rule = select_prefilter_rule(forecasts, {}, _AS_OF, {"incubazioneprimaria2": False}) rule = select_prefilter_rule(forecasts, {}, _AS_OF, {"incubazioneprimaria2": False})