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
10 changes: 5 additions & 5 deletions src/ispypsa/pypsa_build/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _add_links_to_network(
CQ-NQ_existing p_min_pu , -0.714 # fallback only

timeslice_snapshots:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
qld_peak_demand 2025 2025-01-13 12:00

network.snapshots:
Expand Down Expand Up @@ -115,7 +115,7 @@ def _build_link_pu_overrides(
CQ-NQ_existing p_min_pu , -0.714 # fallback only

timeslice_snapshots:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
qld_peak_demand 2025 2025-01-13 12:00

snapshots:
Expand Down Expand Up @@ -161,7 +161,7 @@ def _expand_limits_to_snapshots(
CQ-NQ_existing p_min_pu qld_peak_demand -0.9 # no fallback

timeslice_snapshots:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
qld_peak_demand 2025 2025-01-13 12:00

snapshots:
Expand Down Expand Up @@ -234,14 +234,14 @@ def _place_named_limits_at_snapshots(
CQ-NQ_existing p_max_pu qld_peak_demand 0.857

timeslice_snapshots:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
qld_peak_demand 2025 2025-01-13 12:00

returns:
name attribute investment_periods snapshots value
CQ-NQ_existing p_max_pu 2025 2025-01-13 12:00 0.857
"""
active_at = timeslice_snapshots.rename(columns={"timeslice_id": "timeslice"})
active_at = timeslice_snapshots.copy()
active_at["snapshots"] = pd.to_datetime(active_at["snapshots"])
placed = named.merge(active_at, on="timeslice")
return placed.loc[:, _LIMIT_PER_SNAPSHOT_COLUMNS]
Expand Down
52 changes: 26 additions & 26 deletions src/ispypsa/templater/timeslices.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ispypsa.templater.custom_constraints_from_plexos import _tag_to_timeslice

_TIMESLICE_COLUMNS = [
"timeslice_id",
"timeslice",
"reference_year",
"start_month_day",
"end_month_day",
Expand Down Expand Up @@ -90,7 +90,7 @@ def _template_timeslices(
2026 2015

returns:
timeslice_id reference_year start_month_day end_month_day
timeslice reference_year start_month_day end_month_day
nsw_peak_demand 2015 11-18 11-20 # end exclusive
"""
events = _parse_calendar_events(timeslice_calendar)
Expand All @@ -115,29 +115,29 @@ def _parse_calendar_events(calendar: pd.DataFrame) -> pd.DataFrame:

I/O Example:
DATETIME=18/11/2021, NAME="NSW Hot Day", TIMESLICE=-1
-> DATETIME=2021-11-18, timeslice_id="nsw_peak_demand", TIMESLICE=-1
-> DATETIME=2021-11-18, timeslice="nsw_peak_demand", TIMESLICE=-1
"""
events = calendar.copy()
events["DATETIME"] = pd.to_datetime(events["DATETIME"], dayfirst=True)
events["timeslice_id"] = events["NAME"].map(_tag_to_timeslice)
return events.sort_values(["timeslice_id", "DATETIME"])
events["timeslice"] = events["NAME"].map(_tag_to_timeslice)
return events.sort_values(["timeslice", "DATETIME"])


def _add_next_event_columns(events: pd.DataFrame) -> pd.DataFrame:
"""Annotates each event with the date and state of the next event for the
same timeslice (NaN/NaT on each timeslice's last event).

I/O Example:
timeslice_id DATETIME TIMESLICE
timeslice DATETIME TIMESLICE
nsw_peak_demand 2021-11-18 -1
nsw_peak_demand 2021-11-20 0

->
timeslice_id DATETIME TIMESLICE next_date next_state
timeslice DATETIME TIMESLICE next_date next_state
nsw_peak_demand 2021-11-18 -1 2021-11-20 0
nsw_peak_demand 2021-11-20 0 NaT NaN
"""
grouped = events.groupby("timeslice_id")
grouped = events.groupby("timeslice")
events["next_date"] = grouped["DATETIME"].shift(-1)
events["next_state"] = grouped["TIMESLICE"].shift(-1)
return events
Expand All @@ -163,19 +163,19 @@ def _extract_windows(events: pd.DataFrame) -> pd.DataFrame:
"""Turns each on event into a window row ending at the paired off event.

I/O Example:
timeslice_id DATETIME TIMESLICE next_date next_state
timeslice DATETIME TIMESLICE next_date next_state
nsw_peak_demand 2021-11-18 -1 2021-11-20 0
nsw_peak_demand 2021-11-20 0 NaT NaN

returns:
timeslice_id start_date end_date
timeslice start_date end_date
nsw_peak_demand 2021-11-18 2021-11-20
"""
windows = events[events["TIMESLICE"] == -1]
windows = windows.rename(
columns={"DATETIME": "start_date", "next_date": "end_date"}
)
return windows[["timeslice_id", "start_date", "end_date"]].reset_index(drop=True)
return windows[["timeslice", "start_date", "end_date"]].reset_index(drop=True)


def _drop_horizon_truncated_planning_years(windows: pd.DataFrame) -> pd.DataFrame:
Expand All @@ -187,7 +187,7 @@ def _drop_horizon_truncated_planning_years(windows: pd.DataFrame) -> pd.DataFram
nothing is lost by dropping the year entirely.

I/O Example:
timeslice_id start_date end_date planning_year
timeslice start_date end_date planning_year
nsw_peak_demand 2057-11-18 2057-11-20 2058 # dropped: shares
nsw_winter_reference 2058-04-01 NaT 2058 # the truncated year
nsw_peak_demand 2056-11-18 2056-11-20 2057 # kept
Expand Down Expand Up @@ -228,7 +228,7 @@ def _extend_sequence_to_horizon(
2027 2011

windows (only planning_year is read):
timeslice_id start_date end_date planning_year
timeslice start_date end_date planning_year
nsw_peak_demand 2028-11-18 2028-11-20 2029

returns:
Expand Down Expand Up @@ -256,17 +256,17 @@ def _convert_windows_to_month_days(windows: pd.DataFrame) -> pd.DataFrame:
next calendar year, and winter's 04-01 -> 10-01 extends past 30 June).

I/O Example:
timeslice_id start_date end_date planning_year reference_year
timeslice start_date end_date planning_year reference_year
nsw_peak_demand 2025-11-18 2025-11-20 2026 2015

->
timeslice_id reference_year planning_year start_month_day end_month_day
timeslice reference_year planning_year start_month_day end_month_day
nsw_peak_demand 2015 2026 11-18 11-20
"""
windows["start_month_day"] = windows["start_date"].dt.strftime("%m-%d")
windows["end_month_day"] = windows["end_date"].dt.strftime("%m-%d")
return windows[
["timeslice_id", "reference_year", "planning_year"]
["timeslice", "reference_year", "planning_year"]
+ ["start_month_day", "end_month_day"]
]

Expand All @@ -278,7 +278,7 @@ def _raise_on_inconsistent_reference_year_patterns(patterns: pd.DataFrame) -> No
decoding one pattern per reference year would silently lose windows."""
occurrences = patterns.groupby(["reference_year", "planning_year"]).apply(
lambda x: frozenset(
zip(x["timeslice_id"], x["start_month_day"], x["end_month_day"])
zip(x["timeslice"], x["start_month_day"], x["end_month_day"])
),
include_groups=False,
)
Expand All @@ -296,12 +296,12 @@ def _keep_first_occurrence_per_reference_year(patterns: pd.DataFrame) -> pd.Data
occurrences are identical — validated before this is called).

I/O Example:
timeslice_id reference_year planning_year start_month_day end_month_day
timeslice reference_year planning_year start_month_day end_month_day
nsw_peak_demand 2015 2026 11-18 11-20
nsw_peak_demand 2015 2031 11-18 11-20

returns:
timeslice_id reference_year start_month_day end_month_day
timeslice reference_year start_month_day end_month_day
nsw_peak_demand 2015 11-18 11-20
"""
first_occurrence = patterns.groupby("reference_year")["planning_year"].transform(
Expand All @@ -316,12 +316,12 @@ def _raise_unless_windows_tile_the_year(timeslices: pd.DataFrame) -> None:
"""Raise unless, within every region and reference year, the windows tile
the year exactly — no day left uncovered and none covered twice. A gap there
would let a snapshot fall in no timeslice (silently taking a base limit); an
overlap would let it fall in two. The region is the timeslice_id prefix
overlap would let it fall in two. The region is the timeslice prefix
before the first underscore.
"""
# add a region column from the timeslice_id prefix (nsw_peak_demand -> nsw)
# add a region column from the timeslice prefix (nsw_peak_demand -> nsw)
# to group by, so each region's windows are checked for tiling independently
tagged = timeslices.assign(region=timeslices["timeslice_id"].str.split("_").str[0])
tagged = timeslices.assign(region=timeslices["timeslice"].str.split("_").str[0])
not_tiling = sorted(
(region, int(year))
for (region, year), windows in tagged.groupby(["region", "reference_year"])
Expand Down Expand Up @@ -371,8 +371,8 @@ def _raise_unless_only_winter_crosses_financial_year(timeslices: pd.DataFrame) -
),
axis=1,
)
is_winter = timeslices["timeslice_id"].str.endswith("_winter_reference")
crossing = sorted(timeslices.loc[spans_july & ~is_winter, "timeslice_id"].unique())
is_winter = timeslices["timeslice"].str.endswith("_winter_reference")
crossing = sorted(timeslices.loc[spans_july & ~is_winter, "timeslice"].unique())
if crossing:
raise ValueError(
f"Only winter_reference windows may cross the 1 July financial-year "
Expand Down Expand Up @@ -403,8 +403,8 @@ def _raise_unless_winter_is_constant_per_region(timeslices: pd.DataFrame) -> Non
reference years, so which reference year owns its end never changes its
value. This is the assumption documented on _template_timeslices.
"""
winter = timeslices[timeslices["timeslice_id"].str.endswith("_winter_reference")]
winter = winter.assign(region=winter["timeslice_id"].str.split("_").str[0])
winter = timeslices[timeslices["timeslice"].str.endswith("_winter_reference")]
winter = winter.assign(region=winter["timeslice"].str.split("_").str[0])
varying = sorted(
region
for region, group in winter.groupby("region")
Expand Down
16 changes: 8 additions & 8 deletions src/ispypsa/translator/timeslices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

logger = logging.getLogger(__name__)

_TIMESLICE_SNAPSHOT_COLUMNS = ["timeslice_id", "investment_periods", "snapshots"]
_TIMESLICE_SNAPSHOT_COLUMNS = ["timeslice", "investment_periods", "snapshots"]


def _create_timeslice_snapshot_mapping(
Expand All @@ -20,14 +20,14 @@ def _create_timeslice_snapshot_mapping(
Mapping is done on a model year by model year basis:
- Each model year is mapped to a reference year.
- Then the timeslice windows for that reference year are used to map each
snapshot to a timeslice_id.
snapshot to a timeslice.

I/O Example:
timeslices (each year's windows tile (cover in full) its financial year:
summer opens in November and wraps past New Year, a peak day interrupts it,
summer resumes to April, then winter runs April to November, crossing 30
June into the next financial year):
timeslice_id reference_year start_month_day end_month_day
timeslice reference_year start_month_day end_month_day
nsw_summer_typical 2011 11-01 01-31
nsw_peak_demand 2011 01-31 02-02
nsw_summer_typical 2011 02-02 04-01
Expand All @@ -49,7 +49,7 @@ def _create_timeslice_snapshot_mapping(
-> FY2025 uses 2011's pattern, FY2026 uses 2018's

returns:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
nsw_winter_reference 2025 2024-08-15 12:00:00
nsw_summer_typical 2025 2025-01-20 12:00:00
nsw_peak_demand 2025 2025-01-31 12:00:00
Expand Down Expand Up @@ -111,14 +111,14 @@ def _tag_snapshots_with_pattern(
2026 2026-01-07 12:00:00 01-07

pattern (reference year 2018):
timeslice_id start_month_day end_month_day
timeslice start_month_day end_month_day
nsw_summer_typical 11-01 01-07
nsw_peak_demand 01-07 01-08
nsw_summer_typical 01-08 04-01
nsw_winter_reference 04-01 11-01

returns:
timeslice_id investment_periods snapshots
timeslice investment_periods snapshots
nsw_winter_reference 2026 2025-08-15 12:00:00
nsw_peak_demand 2026 2026-01-07 12:00:00
"""
Expand Down Expand Up @@ -154,7 +154,7 @@ def _concat_tagged_snapshots(mapped: list[pd.DataFrame]) -> pd.DataFrame:
"""Combines the per-model-year tagged snapshots into one mapping table,
in snapshot order."""
mapping = pd.concat(mapped, ignore_index=True)
mapping = mapping.sort_values(["snapshots", "timeslice_id"]).reset_index(drop=True)
mapping = mapping.sort_values(["snapshots", "timeslice"]).reset_index(drop=True)
return mapping.loc[:, _TIMESLICE_SNAPSHOT_COLUMNS]


Expand All @@ -174,7 +174,7 @@ def _log_referenced_timeslices_without_snapshots(
referenced = set(link_timeslice_limits["timeslice"]) | set(
custom_constraints_rhs["timeslice"].dropna()
)
without_snapshots = referenced - set(timeslice_snapshots["timeslice_id"])
without_snapshots = referenced - set(timeslice_snapshots["timeslice"])
if without_snapshots:
logger.warning(
f"Timeslices referenced by transmission limits or custom constraints "
Expand Down
2 changes: 1 addition & 1 deletion src/ispypsa/validation/schemas/custom_constraints_rhs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ columns:
type: string
required: false
allowed_values_from:
- timeslices: timeslice_id
- timeslices: timeslice
description: >
Demand condition the limit applies to: the constraint binds only at
snapshots inside the timeslice's active windows.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ custom_validation:
A path that names timeslices must name them all for its region, or add an empty-timeslice
fallback, in both directions; a single all-empty row covers the whole space.
Because the timeslices table's windows partition the year, covering every
region timeslice_id covers every snapshot downstream.
region timeslice covers every snapshot downstream.
columns:
path_id:
type: string
Expand All @@ -60,10 +60,10 @@ columns:
type: string
required: false
allowed_values_from:
- timeslices: timeslice_id
- timeslices: timeslice
description: >
Demand condition the limit applies to: the limit binds only at snapshots
inside the timeslice's active windows. A timeslice_id's region is the prefix
inside the timeslice's active windows. A timeslice's region is the prefix
before its first underscore (e.g. qld in qld_peak_demand).

If absent (or empty):
Expand Down
8 changes: 4 additions & 4 deletions src/ispypsa/validation/schemas/timeslices.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
table: timeslices
required: false
unique:
- [timeslice_id, reference_year, start_month_day]
- [timeslice, reference_year, start_month_day]
custom_validation:
- name: no_overlapping_windows_per_region_and_reference_year
description: >
Within a region (the timeslice_id prefix before the first underscore)
Within a region (the timeslice prefix before the first underscore)
and reference_year, no two windows may cover the same day. Windows are
compared as [start, end) month-day ranges where an end_month_day at or
before its start wraps past New Year (e.g. 11-20 to 03-20).
- name: windows_cover_full_year_per_region_and_reference_year
description: >
Within a region (the timeslice_id prefix before the first underscore)
Within a region (the timeslice prefix before the first underscore)
and reference_year, the windows must cover every day of the year, leaving
no day with no active timeslice. Together with
no_overlapping_windows_per_region_and_reference_year this makes the
Expand Down Expand Up @@ -62,7 +62,7 @@ description: >
No timeslice is ever active: timeslice-tagged transmission path limits and
custom-constraint RHS values never apply to any snapshot.
columns:
timeslice_id:
timeslice:
type: string
required: true
allowed_values: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def test_create_ispypsa_inputs_new_format(
# RefYear5000 calendar, identical at every granularity.
timeslices = pd.read_csv(output_dir / "timeslices.csv")
assert len(timeslices) == _NUM_TIMESLICE_PATTERN_ROWS_75
assert timeslices["timeslice_id"].nunique() == _NUM_TIMESLICE_IDS_75
assert timeslices["timeslice"].nunique() == _NUM_TIMESLICE_IDS_75
assert timeslices["reference_year"].nunique() == _NUM_REFERENCE_YEARS_75

# custom_constraints — populated at sub_regions, header-only elsewhere (see
Expand Down
Loading
Loading