Skip to content
Open
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
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,19 +1,19 @@
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 expanded [start, end) day ranges, not raw month-day strings:
an end_month_day at or before its start wraps into the following calendar
year (e.g. 11-20 to 03-20), and winter windows run past 30 June into the
next financial year (04-01 to 10-01).
- 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 @@ -52,7 +52,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
20 changes: 10 additions & 10 deletions tests/test_model/test_add_links_with_timeslice_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_named_timeslices_overlay_the_fallback(csv_str_to_df):
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
qld_peak_demand, 2025, 2025-01-01 02:00:00
""")
Expand Down Expand Up @@ -79,7 +79,7 @@ def test_fallback_only_attribute_gets_the_fallback_at_every_snapshot(csv_str_to_
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
""")

Expand Down Expand Up @@ -114,7 +114,7 @@ def test_fallback_only_limits_with_no_timeslices_apply_at_every_snapshot(csv_str
CQ-NQ_existing, p_min_pu, , 0.0
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
""")

_add_links_to_network(
Expand All @@ -141,7 +141,7 @@ def test_named_timeslices_that_tile_the_snapshots_need_no_fallback(csv_str_to_df
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_winter_reference, 2025, 2025-01-01 00:00:00
qld_peak_demand, 2025, 2025-01-01 01:00:00
qld_peak_demand, 2025, 2025-01-01 02:00:00
Expand Down Expand Up @@ -172,7 +172,7 @@ def test_named_timeslice_with_no_snapshots_leaves_the_fallback(csv_str_to_df):
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
""")

_add_links_to_network(
Expand All @@ -198,7 +198,7 @@ def test_snapshot_covered_by_neither_named_timeslice_nor_fallback_raises(csv_str
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
""")

Expand Down Expand Up @@ -226,7 +226,7 @@ def test_existing_link_with_no_timeslice_limits_raises(csv_str_to_df):
name, attribute, timeslice, value
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
""")

with pytest.raises(ValueError) as excinfo:
Expand All @@ -252,7 +252,7 @@ def test_existing_link_missing_one_attribute_raises(csv_str_to_df):
CQ-NQ_existing, p_max_pu, , 1.0
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
""")

Expand Down Expand Up @@ -286,7 +286,7 @@ def test_expansion_links_keep_their_static_values(csv_str_to_df):
CQ-NQ_existing, p_min_pu, , -0.714
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
""")

Expand Down Expand Up @@ -341,7 +341,7 @@ def test_expand_limits_to_snapshots(csv_str_to_df):
NQ-CQ_other, p_max_pu, , 0.8
""")
timeslice_snapshots = csv_str_to_df("""
timeslice_id, investment_periods, snapshots
timeslice, investment_periods, snapshots
qld_peak_demand, 2025, 2025-01-01 01:00:00
""")
snapshots = pd.MultiIndex.from_arrays(
Expand Down
Loading
Loading