diff --git a/src/quartz_api/internal/eclipse.py b/src/quartz_api/internal/eclipse.py deleted file mode 100644 index 7eda3e3b..00000000 --- a/src/quartz_api/internal/eclipse.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Manual eclipse adjustment for national solar forecasts, 12 August 2026. - -PVNet has never seen an eclipse in training and ignores the effect, so all models -over-forecast through the eclipse window. Same problem and same fix as 29 March 2025 -(openclimatefix/uk-pv-national-gsp-api#404). National only, GB and NL, forecasts only. - -TEMPORARY: once the stored values are corrected, set ECLIPSE_ENABLED = False and -delete this module and its call sites. -""" - -import dataclasses -import datetime as dt -import logging -import os - -from quartz_api.internal import models - -log = logging.getLogger(__name__) - -ECLIPSE_ENABLED = True - -# Override with ECLIPSE_DATE=2026-08-10 to test on an ordinary day. -ECLIPSE_DATE: dt.date = dt.date.fromisoformat( - os.environ.get("ECLIPSE_DATE", "2026-08-12"), -) - -# Fraction of the theoretical non-eclipse value by UTC time of day, from -# James' and Sukh's output tables in our shared doc. GB is half-hourly, NL quarter-hourly. -_FACTORS: dict[str, dict[dt.time, float]] = { - "GB": { - dt.time(17, 0): 1.000000, - dt.time(17, 30): 0.968392, - dt.time(18, 0): 0.566288, - dt.time(18, 30): 0.197790, - dt.time(19, 0): 0.719758, - dt.time(19, 30): 0.997780, - dt.time(20, 0): 1.000000, - }, - "NL": { - dt.time(17, 0): 1.000000, - dt.time(17, 15): 1.000000, - dt.time(17, 30): 0.943559, - dt.time(17, 45): 0.720819, - dt.time(18, 0): 0.420696, - dt.time(18, 15): 0.161150, - dt.time(18, 30): 0.299477, - dt.time(18, 45): 0.620353, - dt.time(19, 0): 0.892493, - dt.time(19, 15): 0.999197, - dt.time(19, 30): 1.000000, - dt.time(19, 45): 1.000000, - dt.time(20, 0): 1.000000, - }, -} - -_STEP_MINUTES: dict[str, int] = {"GB": 30, "NL": 15} - - -def factor_for(country: str, timestamp: dt.datetime) -> float: - """Return the eclipse multiplier for a target time, or 1.0 if unaffected.""" - if not ECLIPSE_ENABLED: - return 1.0 - - table = _FACTORS.get(country.upper()) - if not table: - return 1.0 - - if timestamp.tzinfo is None: - timestamp = timestamp.replace(tzinfo=dt.UTC) - else: - timestamp = timestamp.astimezone(dt.UTC) - - if timestamp.date() != ECLIPSE_DATE: - return 1.0 - - time_of_day = timestamp.time().replace(second=0, microsecond=0) - factor = table.get(time_of_day) - if factor is not None: - return factor - - # A miss inside the window means serving an unadjusted forecast — be loud. - if min(table) <= time_of_day <= max(table): - log.warning( - "Eclipse adjustment: %s timestamp %s falls inside the eclipse window " - "but not on the %d-minute table grid — serving it unadjusted", - country.upper(), - timestamp.isoformat(), - _STEP_MINUTES.get(country.upper(), 30), - ) - return 1.0 - - -def snapshot_factor_for(country: str, timestamp: dt.datetime) -> float: - """Return the multiplier for a snapshot time, snapped onto the table grid first.""" - if not ECLIPSE_ENABLED or country.upper() not in _FACTORS: - return 1.0 - - step = dt.timedelta(minutes=_STEP_MINUTES[country.upper()]) - if timestamp.tzinfo is None: - timestamp = timestamp.replace(tzinfo=dt.UTC) - epoch = dt.datetime(1970, 1, 1, tzinfo=dt.UTC) - return factor_for(country, epoch + round((timestamp - epoch) / step) * step) - - -def _scaled( - value: models.PredictedGenerationValue, - factor: float, -) -> models.PredictedGenerationValue: - """Return a copy with power and every p-level scaled.""" - if factor == 1.0: - return value - return dataclasses.replace( - value, - power_kilowatts=value.power_kilowatts * factor, - plevels_kilowatts={ - level: power * factor for level, power in value.plevels_kilowatts.items() - }, - ) - - -def adjust_predicted_generation( - values: list[models.PredictedGenerationValue], - country: str, -) -> list[models.PredictedGenerationValue]: - """Apply the eclipse adjustment to a national forecast time series.""" - return [_scaled(v, factor_for(country, v.valid_timestamp)) for v in values] - - -def adjust_snapshot( - values: list[models.PredictedGenerationValue], - country: str, -) -> list[models.PredictedGenerationValue]: - """Apply the eclipse adjustment to a national forecast snapshot.""" - return [_scaled(v, snapshot_factor_for(country, v.valid_timestamp)) for v in values] - - -def adjust_national_only( - values: list[models.PredictedGenerationValue], - country: str, - national_uuid: object, -) -> list[models.PredictedGenerationValue]: - """Adjust only the national location's values within a mixed-location list.""" - return [ - _scaled(v, factor_for(country, v.valid_timestamp)) - if v.location_uuid == national_uuid - else v - for v in values - ] diff --git a/src/quartz_api/internal/service/uk_national/gsp_router.py b/src/quartz_api/internal/service/uk_national/gsp_router.py index 9d17b1fb..3fe103a3 100644 --- a/src/quartz_api/internal/service/uk_national/gsp_router.py +++ b/src/quartz_api/internal/service/uk_national/gsp_router.py @@ -24,7 +24,7 @@ from pydantic import AfterValidator, TypeAdapter from starlette import status -from quartz_api.internal import eclipse, models +from quartz_api.internal import models from quartz_api.internal.middleware.auth import AuthDependency from quartz_api.internal.service.uk_national.metadata import format_metadata @@ -123,10 +123,6 @@ async def get_forecasts_for_a_specific_gsp( ) log.info(f"Fetched {len(pgvs)} predicted generation values for gsp_id {gsp_id}") - # gsp_id 0 is the national location; real GSPs are left alone. - if gsp_id == 0: - pgvs = eclipse.adjust_predicted_generation(pgvs, "GB") - out: list[ForecastValue] = [ ForecastValue( target_time=pp.valid_timestamp, @@ -383,14 +379,6 @@ async def get_all_available_forecasts( ] log.info(f"Fetched predicted generation values for {len(results)} GSPs") - # Both live paths can include gsp_id 0; the pre-warmed cache path excludes it. - if 0 in gsps_to_convert: - national_uuid = gsps_to_convert[0].uuid - results = [ - eclipse.adjust_national_only(snapshot, "GB", national_uuid) - for snapshot in results - ] - gsp_uuid_id_map = {v.uuid: k for k, v in gsps_to_convert.items()} if compact: return _build_compact_response(results=results, gsp_uuid_id_map=gsp_uuid_id_map) diff --git a/src/quartz_api/internal/service/uk_national/national_router.py b/src/quartz_api/internal/service/uk_national/national_router.py index 9ca7eeec..2b34810a 100644 --- a/src/quartz_api/internal/service/uk_national/national_router.py +++ b/src/quartz_api/internal/service/uk_national/national_router.py @@ -10,7 +10,7 @@ from pydantic import AfterValidator from starlette import status -from quartz_api.internal import eclipse, models +from quartz_api.internal import models from quartz_api.internal.middleware.auth import AuthDependency from quartz_api.internal.service.uk_national.metadata import format_metadata @@ -187,7 +187,6 @@ async def get_national_forecast( log.info(f"Fetched {len(pgvs)} predicted generation values") all_pgvs = sorted(all_pgvs, key=lambda x: x.valid_timestamp, reverse=False) - all_pgvs = eclipse.adjust_predicted_generation(all_pgvs, "GB") out: list[NationalForecastValue] = [ NationalForecastValue( target_time=v.valid_timestamp, diff --git a/src/quartz_api/internal/service/uk_national/test_national_router.py b/src/quartz_api/internal/service/uk_national/test_national_router.py index 5b69f367..7a6eb7f1 100644 --- a/src/quartz_api/internal/service/uk_national/test_national_router.py +++ b/src/quartz_api/internal/service/uk_national/test_national_router.py @@ -5,7 +5,7 @@ import pytest import time_machine -from quartz_api.internal import eclipse, models +from quartz_api.internal import models from .endpoint_types import gsp_id_map @@ -235,46 +235,3 @@ async def test_national_last_updated_default(api_client, mock_storage: AsyncMock assert kwargs["window_start"] == frozen_time - dt.timedelta(minutes=30) assert kwargs["window_end"] == frozen_time + dt.timedelta(minutes=30) - - -@pytest.mark.asyncio -async def test_national_forecast_eclipse_adjustment( - api_client, - mock_storage: AsyncMock, - monkeypatch, -): - monkeypatch.setattr(eclipse, "ECLIPSE_ENABLED", True) - monkeypatch.setattr(eclipse, "ECLIPSE_DATE", dt.date(2026, 8, 12)) - frozen_time = dt.datetime(2026, 8, 12, 17, 0, tzinfo=dt.UTC) - - def _value(hour: int, minute: int) -> models.PredictedGenerationValue: - return models.PredictedGenerationValue( - power_kilowatts=10000.0, - valid_timestamp=dt.datetime(2026, 8, 12, hour, minute, tzinfo=dt.UTC), - location_uuid=gsp_id_map[0].uuid, - capacity_kilowatts=20000.0, - forecaster_name="blend_adjust", - forecaster_version="1.3.0", - created_timestamp=frozen_time, - init_timestamp=frozen_time, - plevels_kilowatts={"p10": 8000.0, "p90": 12000.0}, - metadata={}, - ) - - mock_storage.get_locations.return_value = [gsp_id_map[0]] - # 17:00 is before the eclipse bites, 18:00 is mid-eclipse. - mock_storage.get_predicted_generation.return_value = [_value(17, 0), _value(18, 0)] - - - with time_machine.travel(frozen_time, tick=False): - response = await api_client.get("/v0/solar/GB/national/forecast") - - assert response.status_code == 200 - before, during = response.json() - - assert before["expectedPowerGenerationMegawatts"] == 10.0 - # v0 rounds MW to 2dp. - assert during["expectedPowerGenerationMegawatts"] == round(10.0 * 0.566288, 2) - assert during["plevels"]["plevel_10"] == pytest.approx(8.0 * 0.566288) - assert during["plevels"]["plevel_90"] == pytest.approx(12.0 * 0.566288) - assert before["plevels"]["plevel_10"] == 8.0 diff --git a/src/quartz_api/internal/service/v1/routes/forecasts.py b/src/quartz_api/internal/service/v1/routes/forecasts.py index d57e7aec..a4dee404 100644 --- a/src/quartz_api/internal/service/v1/routes/forecasts.py +++ b/src/quartz_api/internal/service/v1/routes/forecasts.py @@ -12,7 +12,7 @@ from fastapi_cache.decorator import cache from starlette import status -from quartz_api.internal import eclipse, models +from quartz_api.internal import models from quartz_api.internal.middleware.auth import AuthDependency from ..cache import ( @@ -136,9 +136,6 @@ async def get_forecast( ), ) - if location_type == models.LocationType.NATION: - pgvs = eclipse.adjust_predicted_generation(pgvs, country.code) - first = pgvs[0] if pgvs else None return ForecastResponse( region_name=location_display_name(region, country), @@ -286,9 +283,6 @@ async def get_forecasts_at_time( authdata={}, ) - if location_type == models.LocationType.NATION: - snapshot = eclipse.adjust_snapshot(snapshot, country.code) - region_names = {to_uuid(r.uuid): location_display_name(r, country) for r in regions} first = snapshot[0] if snapshot else None return ForecastSnapshot( diff --git a/src/quartz_api/internal/service/v1/test_router.py b/src/quartz_api/internal/service/v1/test_router.py index 648e6639..28464c02 100644 --- a/src/quartz_api/internal/service/v1/test_router.py +++ b/src/quartz_api/internal/service/v1/test_router.py @@ -15,7 +15,7 @@ from fastapi_cache.backends.inmemory import InMemoryBackend from httpx import ASGITransport, AsyncClient -from quartz_api.internal import eclipse, models +from quartz_api.internal import models from quartz_api.internal.backends.dummydb.client import StorageClient from quartz_api.internal.middleware.auth import AuthDependency @@ -1796,197 +1796,3 @@ async def test_nl_forecast_period_display_name_filter( ) assert resp2.status_code == 200 assert len(resp2.json()["regions"]) == 1 - -# --- Eclipse adjustment (12 Aug 2026) --- - -_ECLIPSE_DATE = dt.date(2026, 8, 12) -# 18:00 UTC on the eclipse date. -_ECLIPSE_FACTOR_GB = 0.566288 -_ECLIPSE_FACTOR_NL = 0.420696 -_ECLIPSE_POWER_KW = 10_000.0 -_ECLIPSE_WINDOW = "start_utc=2026-08-12T00:00:00Z&end_utc=2026-08-13T00:00:00Z" - - -def _eclipse_pgv( - location_uuid: UUID, - hour: int, - minute: int = 0, -) -> models.PredictedGenerationValue: - return models.PredictedGenerationValue( - power_kilowatts=_ECLIPSE_POWER_KW, - valid_timestamp=dt.datetime(2026, 8, 12, hour, minute, tzinfo=dt.UTC), - location_uuid=location_uuid, - capacity_kilowatts=20_000.0, - forecaster_name="blend_adjust", - forecaster_version="1.3.0", - plevels_kilowatts={"p10": 8_000.0, "p90": 12_000.0}, - ) - - -class _EclipseForecastMixin: - """Returns fixed forecast values spanning the eclipse window.""" - - async def get_predicted_generation( # type: ignore[override] - self, - location_uuid: UUID | str, - window_start: dt.datetime, - window_end: dt.datetime, - energy_type: models.EnergyType, - location_type: models.LocationType, - authdata: dict[str, str], - created_cutoff: dt.datetime | None = None, - forecast_horizon_minutes: int = 0, - forecaster_name: str | None = None, - forecaster_version: str | None = None, - ) -> list[models.PredictedGenerationValue]: - uuid = location_uuid if isinstance(location_uuid, UUID) else UUID(str(location_uuid)) - # 12:00 is clear of the eclipse, 18:00 is mid-eclipse in both countries. - return [_eclipse_pgv(uuid, 12), _eclipse_pgv(uuid, 18)] - - async def get_predicted_generation_snapshot( # type: ignore[override] - self, - location_uuids: list[UUID], - snapshot_timestamp_utc: dt.datetime, - energy_type: models.EnergyType, - authdata: dict[str, str], - forecaster_name: str | None = None, - forecaster_version: str | None = None, - ) -> list[models.PredictedGenerationValue]: - return [ - _eclipse_pgv(uuid, snapshot_timestamp_utc.hour, snapshot_timestamp_utc.minute) - for uuid in location_uuids - ] - - -class EclipseNationClient(_EclipseForecastMixin, NationResponseClient): - """GB national location with forecast values spanning the eclipse.""" - - -class EclipseGSPClient(_EclipseForecastMixin, FixedUUIDStorageClient): - """GB GSP location with the same values — must come back unadjusted.""" - - async def get_locations( # type: ignore[override] - self, - energy_type: models.EnergyType, - location_type: models.LocationType | None, - authdata: dict, - location_uuid: UUID | None = None, - enclosing_location_uuid: UUID | None = None, - ) -> list[models.Location]: - if location_type is None and location_uuid is not None: - return [ - models.Location( - uuid=location_uuid, - name="Fixed GSP", - latitude=51.0, - longitude=-1.0, - capacity_kilowatts=76_000, - location_type=models.LocationType.GSP, - ), - ] - return await super().get_locations( - energy_type=energy_type, - location_type=location_type, - authdata={}, - location_uuid=location_uuid, - enclosing_location_uuid=enclosing_location_uuid, - ) - - -class EclipseNLNationClient(_EclipseForecastMixin, NationNameStorageClient): - """NL national location with forecast values spanning the eclipse.""" - - def __init__(self) -> None: - super().__init__("nl_national") - - -@pytest.fixture(autouse=True) -def _pin_eclipse_date(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(eclipse, "ECLIPSE_ENABLED", True) - monkeypatch.setattr(eclipse, "ECLIPSE_DATE", _ECLIPSE_DATE) - - -async def _eclipse_client( - db: models.StorageInterface, - permissions: list[str], -) -> AsyncGenerator[AsyncClient, None]: - FastAPICache.init(InMemoryBackend(), prefix="test") - app = _make_app(db, permissions) - async with AsyncClient( - transport=ASGITransport(app=app), - base_url="http://test", - ) as ac: - yield ac - - -@pytest_asyncio.fixture -async def eclipse_national_client() -> AsyncGenerator[AsyncClient, None]: - async for c in _eclipse_client(EclipseNationClient(), ["read:gb"]): - yield c - - -@pytest_asyncio.fixture -async def eclipse_gsp_client() -> AsyncGenerator[AsyncClient, None]: - async for c in _eclipse_client(EclipseGSPClient(), ["read:gb"]): - yield c - - -@pytest_asyncio.fixture -async def eclipse_nl_client() -> AsyncGenerator[AsyncClient, None]: - async for c in _eclipse_client(EclipseNLNationClient(), ["read:nl"]): - yield c - - -@pytest.mark.anyio -async def test_gb_national_forecast_is_eclipse_adjusted( - eclipse_national_client: AsyncClient, -) -> None: - resp = await eclipse_national_client.get( - f"/v1/GB/solar/regions/{uuid4()}/forecast?{_ECLIPSE_WINDOW}", - ) - assert resp.status_code == 200 - midday, during = resp.json()["values"] - - assert midday["power_kW"] == _ECLIPSE_POWER_KW - assert during["power_kW"] == pytest.approx(_ECLIPSE_POWER_KW * _ECLIPSE_FACTOR_GB) - assert during["plevels_kW"]["p10"] == pytest.approx(8_000.0 * _ECLIPSE_FACTOR_GB) - assert during["plevels_kW"]["p90"] == pytest.approx(12_000.0 * _ECLIPSE_FACTOR_GB) - - -@pytest.mark.anyio -async def test_nl_national_forecast_uses_the_nl_curve( - eclipse_nl_client: AsyncClient, -) -> None: - resp = await eclipse_nl_client.get( - f"/v1/NL/solar/regions/national/forecast?{_ECLIPSE_WINDOW}", - ) - assert resp.status_code == 200 - _, during = resp.json()["values"] - - assert during["power_kW"] == pytest.approx(_ECLIPSE_POWER_KW * _ECLIPSE_FACTOR_NL) - - -@pytest.mark.anyio -async def test_gsp_forecast_is_not_eclipse_adjusted( - eclipse_gsp_client: AsyncClient, -) -> None: - resp = await eclipse_gsp_client.get( - f"/v1/GB/solar/regions/{_FIXED_GSP_UUID}/forecast?{_ECLIPSE_WINDOW}", - ) - assert resp.status_code == 200 - for value in resp.json()["values"]: - assert value["power_kW"] == _ECLIPSE_POWER_KW - - -@pytest.mark.anyio -async def test_national_snapshot_is_eclipse_adjusted( - eclipse_national_client: AsyncClient, -) -> None: - resp = await eclipse_national_client.get( - "/v1/GB/solar/forecasts/snapshot?region_type=national&time_utc=2026-08-12T18:00:00Z", - ) - assert resp.status_code == 200 - values = resp.json()["values"] - assert values - for value in values: - assert value["power_kW"] == pytest.approx(_ECLIPSE_POWER_KW * _ECLIPSE_FACTOR_GB) diff --git a/src/quartz_api/internal/test_eclipse.py b/src/quartz_api/internal/test_eclipse.py deleted file mode 100644 index ffff3779..00000000 --- a/src/quartz_api/internal/test_eclipse.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Tests for the manual eclipse adjustment applied to national forecasts.""" - -import datetime as dt -import logging -from uuid import UUID, uuid4 - -import pytest - -from . import eclipse, models - -NATIONAL_UUID = uuid4() -GSP_UUID = uuid4() - - -@pytest.fixture(autouse=True) -def _eclipse_on(monkeypatch): - monkeypatch.setattr(eclipse, "ECLIPSE_ENABLED", True) - monkeypatch.setattr(eclipse, "ECLIPSE_DATE", dt.date(2026, 8, 12)) - - -def _pgv( - hour: int, - minute: int, - power_kw: float = 1000.0, - location_uuid: UUID = NATIONAL_UUID, -) -> models.PredictedGenerationValue: - return models.PredictedGenerationValue( - power_kilowatts=power_kw, - valid_timestamp=dt.datetime(2026, 8, 12, hour, minute, tzinfo=dt.UTC), - location_uuid=location_uuid, - capacity_kilowatts=2000.0, - forecaster_name="blend_adjust", - forecaster_version="1.3.0", - plevels_kilowatts={"p10": 800.0, "p50": 1000.0, "p90": 1200.0}, - ) - - -def test_factor_at_tabulated_time(): - assert eclipse.factor_for("GB", dt.datetime(2026, 8, 12, 18, 30, tzinfo=dt.UTC)) == 0.197790 - - -def test_gb_and_nl_curves_are_independent(): - deepest_gb = dt.datetime(2026, 8, 12, 18, 30, tzinfo=dt.UTC) - deepest_nl = dt.datetime(2026, 8, 12, 18, 15, tzinfo=dt.UTC) - - assert eclipse.factor_for("GB", deepest_gb) == 0.197790 - assert eclipse.factor_for("NL", deepest_nl) == 0.161150 - # 18:15 is not on the GB half-hourly grid. - assert eclipse.factor_for("GB", deepest_nl) == 1.0 - assert eclipse.factor_for("NL", deepest_gb) == 0.299477 - - -def test_factor_outside_window_and_off_date(): - assert eclipse.factor_for("GB", dt.datetime(2026, 8, 12, 12, 0, tzinfo=dt.UTC)) == 1.0 - assert eclipse.factor_for("GB", dt.datetime(2026, 8, 11, 18, 30, tzinfo=dt.UTC)) == 1.0 - - -def test_factor_when_disabled_or_unknown_country(monkeypatch): - ts = dt.datetime(2026, 8, 12, 18, 30, tzinfo=dt.UTC) - assert eclipse.factor_for("IN", ts) == 1.0 - - monkeypatch.setattr(eclipse, "ECLIPSE_ENABLED", False) - assert eclipse.factor_for("GB", ts) == 1.0 - - -def test_naive_and_offset_timestamps_are_normalised_to_utc(): - assert eclipse.factor_for("GB", dt.datetime(2026, 8, 12, 18, 30)) == 0.197790 # noqa: DTZ001 - - # 19:30 BST is 18:30 UTC. - bst = dt.datetime(2026, 8, 12, 19, 30, tzinfo=dt.timezone(dt.timedelta(hours=1))) - assert eclipse.factor_for("GB", bst) == 0.197790 - - -def test_in_window_grid_miss_warns(caplog): - with caplog.at_level(logging.WARNING): - assert eclipse.factor_for("GB", dt.datetime(2026, 8, 12, 18, 20, tzinfo=dt.UTC)) == 1.0 - assert "not on the 30-minute table grid" in caplog.text - - -def test_snapshot_factor_rounds_onto_the_grid(): - assert eclipse.snapshot_factor_for( - "GB", dt.datetime(2026, 8, 12, 18, 20, tzinfo=dt.UTC), - ) == 0.197790 - assert eclipse.snapshot_factor_for( - "GB", dt.datetime(2026, 8, 12, 18, 10, tzinfo=dt.UTC), - ) == 0.566288 - # NL rounds on a 15-minute grid, so 18:20 lands on 18:15. - assert eclipse.snapshot_factor_for( - "NL", dt.datetime(2026, 8, 12, 18, 20, tzinfo=dt.UTC), - ) == 0.161150 - on_grid = dt.datetime(2026, 8, 12, 18, 30, tzinfo=dt.UTC) - assert eclipse.snapshot_factor_for("GB", on_grid) == eclipse.factor_for("GB", on_grid) - - -def test_adjust_scales_power_and_every_plevel(): - [adjusted] = eclipse.adjust_predicted_generation([_pgv(18, 30)], "GB") - - assert adjusted.power_kilowatts == pytest.approx(1000.0 * 0.197790) - assert adjusted.plevels_kilowatts["p10"] == pytest.approx(800.0 * 0.197790) - assert adjusted.plevels_kilowatts["p50"] == pytest.approx(1000.0 * 0.197790) - assert adjusted.plevels_kilowatts["p90"] == pytest.approx(1200.0 * 0.197790) - assert adjusted.capacity_kilowatts == 2000.0 - assert adjusted.forecaster_name == "blend_adjust" - - -def test_adjust_leaves_values_outside_the_window_alone(): - [adjusted] = eclipse.adjust_predicted_generation([_pgv(12, 0)], "GB") - - assert adjusted.power_kilowatts == 1000.0 - assert adjusted.plevels_kilowatts == {"p10": 800.0, "p50": 1000.0, "p90": 1200.0} - - -def test_adjust_does_not_mutate_the_input_values(): - original = _pgv(18, 30) - eclipse.adjust_predicted_generation([original], "GB") - - assert original.power_kilowatts == 1000.0 - assert original.plevels_kilowatts["p90"] == 1200.0 - - -def test_adjust_national_only_skips_other_locations(): - values = [ - _pgv(18, 30, location_uuid=NATIONAL_UUID), - _pgv(18, 30, location_uuid=GSP_UUID), - ] - national, gsp = eclipse.adjust_national_only(values, "GB", NATIONAL_UUID) - - assert national.power_kilowatts == pytest.approx(1000.0 * 0.197790) - assert gsp.power_kilowatts == 1000.0