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
5 changes: 5 additions & 0 deletions src/ispypsa/templater/create_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from ispypsa.templater.existing_planned import (
_template_generators_existing_planned,
_template_storage_existing_planned,
)
from ispypsa.templater.filter_template import _filter_template
from ispypsa.templater.flow_paths import (
Expand Down Expand Up @@ -101,6 +102,7 @@
"costs_connection",
"generators_existing_planned",
"generators_new_entrant",
"storage_existing_planned",
"storage_new_entrant",
"custom_constraints",
"custom_constraints_lhs",
Expand Down Expand Up @@ -257,6 +259,9 @@ def create_ispypsa_inputs_template(
template["generators_existing_planned"] = _template_generators_existing_planned(
iasr_tables, regional_granularity, sub_regional_geography
)
template["storage_existing_planned"] = _template_storage_existing_planned(
iasr_tables, regional_granularity, sub_regional_geography
)

if regional_granularity == "sub_regions":
template.update(
Expand Down
288 changes: 203 additions & 85 deletions src/ispypsa/templater/existing_planned.py

Large diffs are not rendered by default.

105 changes: 84 additions & 21 deletions src/ispypsa/templater/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,51 +552,54 @@ def _assert_table_valid(
raise ValueError(f"'{table_name}' table is empty - cannot merge {merge_desc}")


def _apply_known_value_replacement(
iasr_tables: dict[str, pd.DataFrame], correction: dict
def _apply_iasr_table_replacements(
iasr_tables: dict[str, pd.DataFrame], corrections: list[dict]
) -> dict[str, pd.DataFrame]:
"""Returns ``iasr_tables`` with a known correction applied to one table's column.
"""Returns ``iasr_tables`` with 'corrections' applied to input IASR tables.

Shared shape for a small, explicitly declared fix (a documented typo or naming
mismatch) to a single column of a single source table. ``correction`` bundles the
fix's specifics (``table_name``, ``column``, ``replacements``). Returns a shallow
copy of ``iasr_tables`` with only that table replaced.
Shared shape for small, explicitly declared fixes (a documented typo or naming
mismatch) to specified locations. ``corrections`` can carry multiple fixes,
each with 'fix' specifics (``table_name``, ``column``, ``replacements``) bundled.
Returns a shallow copy of ``iasr_tables`` with only listed tables replaced.

Note: while fuzzy-matching is used to standardise names or other ID strings,
some typos/diffs are too 'big' to pass any safe fuzzy-match threshold (see
example below - fuzz.ratio("KiataWF1", "KIATAWF1") == 50). This function
explicitly handles those known instances where this is the case.

I/O Example (correction = existing_planned._MAXIMUM_CAPACITY_ID_TYPO_FIX):
I/O Example:
iasr_tables["maximum_capacity_..."]:
IASR ID Power Station Installed capacity (MW)
KiataWF1 Kiata Wind Farm 31.05
BW01 Bayswater 660.0

correction:
corrections (as a single dict element in list):
table_name: "maximum_capacity_..."
column: "IASR ID"
replacements: {"KiataWF1": "KIATAWF1"}

returns copy of iasr_tables with only that one table edited:
returns copy of iasr_tables with only listed tables edited:
iasr_tables["maximum_capacity_..."]:
IASR ID Power Station Installed capacity (MW)
KIATAWF1 Kiata Wind Farm 31.05
BW01 Bayswater 660.0
"""
table_name = correction["table_name"]
corrected = iasr_tables[table_name].replace(
{correction["column"]: correction["replacements"]}
)
return {**iasr_tables, table_name: corrected}
corrected_tables = iasr_tables
for correction in corrections:
table_name = correction["table_name"]
col_to_fix = correction["column"]
replacements = correction["replacements"]
corrected = iasr_tables[table_name].replace({col_to_fix: replacements})
corrected_tables = {**corrected_tables, table_name: corrected}
return corrected_tables


def _group_properties_by_source(
property_map: dict[str, dict],
) -> dict[tuple[str, str], dict]:
"""Groups a property map's entries by their source (table, key_col).

Shared by ``new_entrants._merge_properties`` and
Shared by ``_merge_category_keyed_properties`` and
``existing_planned._merge_unit_keyed_properties`` so a table contributing several
properties (e.g. ``battery_properties`` feeds six) is validated and key-resolved
once per source, not once per property.
Expand Down Expand Up @@ -673,10 +676,68 @@ def _get_property_value_map(
attrs.get("scale", 1.0)
)
# TODO: 'year' type cols become floats from this transform - leave for
# validator to type-correct or edit handling here?
# validator to type-correct or edit handling here? See Open-ISP/ISPyPSA#145
return value_map


def _merge_category_keyed_properties(
df: pd.DataFrame,
iasr_tables: dict[str, pd.DataFrame],
property_map: dict[str, dict],
df_key_col: str,
) -> pd.DataFrame:
"""Merges every non-unit-keyed property in ``property_map`` onto ``df``.

Groups properties by their source (table, key_col) — see
``_group_properties_by_source`` — so a table that contributes several properties
(e.g. ``battery_properties`` feeds six new entrant storage properties) is
validated and fuzzy-matched against ``df_key_col`` values once per property map.

I/O Example:
An abbreviated example merging the 'efficiency_charge' property into an
'existing_planned_storage' summary table.

df:
name technology
Liddell BESS Battery storage (4hrs storage)

df_key_col = "technology"

property_map:
efficiency_charge: table="battery_properties",
key_col="Technology",
value_col="Charge efficiency_%"

iasr_tables['battery_properties']:
Technology Charge efficiency_%
Battery storage (4hrs storage) 92.5

returns (adds one column per map key):
name technology efficiency_charge
Liddell BESS Battery storage (4hrs storage) 92.5
"""
df = df.copy()
for (table_name, key_col), props in _group_properties_by_source(
property_map
).items():
table = iasr_tables[table_name]
_assert_table_valid(
table,
table_name,
_required_property_columns(props),
f"{sorted(props.keys())}",
)
matched_key_col = _fuzzy_map_to_allowed_values(
df[df_key_col],
table[key_col],
task_desc=f"merging properties from '{table_name}'",
)
for new_col, attrs in props.items():
property_values = _get_property_value_map(table, attrs)
df[new_col] = matched_key_col.map(property_values)
return df


def _is_battery_row(
df: pd.DataFrame, col_to_check: str = "Technology Type"
) -> pd.Series:
Expand Down Expand Up @@ -714,22 +775,24 @@ def _derive_phes_symmetric_efficiency(phes: pd.DataFrame) -> pd.DataFrame:

The IASR PHES tables give only a single round-trip efficiency. Assuming symmetric
legs, each one-way efficiency is its square root, so e.g. a 76% round trip becomes
~87.2% charge and ~87.2% discharge (sqrt(0.76) ≈ 0.872).
~87.2% charge and ~87.2% discharge (sqrt(0.76) ≈ 0.872). The function returns
the input `phes` df with two new columns (efficiency_charge and efficiency_discharge),
dropping the intermediate round_trip_efficiency column.

I/O Example:
phes:
name round_trip_efficiency
NQ Pumped Hydro-10h 76.0

returns (adds the two efficiency columns):
name round_trip_efficiency efficiency_charge efficiency_discharge
NQ Pumped Hydro-10h 76.0 87.18 87.18
name efficiency_charge efficiency_discharge
NQ Pumped Hydro-10h 87.18 87.18
"""
phes = phes.copy()
one_way_efficiency = (phes["round_trip_efficiency"] / 100) ** 0.5 * 100
phes["efficiency_charge"] = one_way_efficiency
phes["efficiency_discharge"] = one_way_efficiency
return phes
return phes.drop(columns=["round_trip_efficiency"])


def _standardise_storage_capitalisation(series: pd.Series) -> pd.Series:
Expand Down
67 changes: 63 additions & 4 deletions src/ispypsa/templater/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@
"""
New entrant property columns (keys) mapped to the IASR table and columns that contain
property values and the technology for which the values apply. Consumed by
``ispypsa.templater.new_entrants`` via ``_merge_properties``.
``ispypsa.templater.new_entrants`` via ``_merge_category_keyed_properties``.

`table`: IASR table name holding the named property (key)
`key_col`: column in the IASR table that contains the 'technology' string.
Expand Down Expand Up @@ -766,12 +766,12 @@
}

"""
Existing/planned (ECAA) generator property columns (keys) mapped to the IASR table and
column that contains their values. Consumed by
Existing/planned (ECAA) generator and storage unit-level property columns (keys) mapped
to the IASR table and column that contains their values. Consumed by
``ispypsa.templater.existing_planned`` via ``_merge_unit_keyed_properties``.

Shaped like the new entrant maps above, but every entry here shares the same
``key_col`` (``IASR ID``) — each generator's ``name`` is resolved against it via
``key_col`` (``IASR ID``) — each unit's ``name`` is resolved against it via
fuzzy matching (small typos only; see ``existing_planned._resolve_unit_keys``)
rather than the technology-level fuzzy grouping the new entrant maps need.

Expand Down Expand Up @@ -815,3 +815,62 @@
value_col="Expected Closure Year (Calendar year)",
),
}

_STORAGE_EXISTING_PLANNED_UNIT_PROPERTY_MAP = {
"capacity": dict(
table="maximum_capacity_existing_committed_anticipated_additional_generators",
key_col="IASR ID",
value_col="Installed capacity (MW)",
),
"storage_capacity": dict(
table="maximum_capacity_existing_committed_anticipated_additional_generators",
key_col="IASR ID",
value_col="Storage Capacity (MWh)",
),
"commissioning_date": dict(
table="maximum_capacity_existing_committed_anticipated_additional_generators",
key_col="IASR ID",
value_col="Commissioning date",
numeric=False,
),
"closure_year": dict(
table="expected_closure_years",
key_col="IASR ID",
value_col="Expected Closure Year (Calendar year)",
),
}

"""
Existing/planned (ECAA) storage properties that aren't published per unit, mapped to
the IASR table and column that contains their values. Consumed by
``ispypsa.templater.existing_planned`` via ``_merge_storage_type_split_properties``,
which merges each map onto its own subset of storage rows with
``helpers._merge_category_keyed_properties``.

Each map is keyed on a different category shared by many units:
- batteries on ``technology`` (``battery_properties``' ``Technology``)
- PHES on ``power_station`` (the PHES properties table's ``Power Station``)

Entries use the same fields as the unit-level maps above.
"""

_BATTERY_EXISTING_PLANNED_TECH_PROPERTY_MAP = {
"efficiency_charge": dict(
table="battery_properties",
key_col="Technology",
value_col="Charge efficiency_%",
),
"efficiency_discharge": dict(
table="battery_properties",
key_col="Technology",
value_col="Discharge efficiency_%",
),
}

_PHES_EXISTING_PLANNED_STATION_PROPERTY_MAP = {
"round_trip_efficiency": dict(
table="pumped_hydro_existing_committed_anticipated_additional_properties",
key_col="Power Station",
value_col="Pumping efficiency (%)",
),
}
Loading