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
146 changes: 120 additions & 26 deletions packages/r2x-reeds-to-sienna/src/r2x_reeds_to_sienna/getter_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,65 @@ def _get_source_max_active_power(component: Any) -> float | None:
return None


def normalize_max_active_power_time_series(context: PluginContext) -> None:
"""Normalize ReEDS MW profiles for PowerSystems scaling during serialization."""
def _get_target_time_series_profiles(
target_system: Any,
target_component: Any,
name: str,
metadata_records: list[Any],
) -> list[tuple[SingleTimeSeries, dict[str, Any]]]:
"""Return target time series and features matching source metadata records."""
return [
(
target_system.get_time_series(
target_component,
name=name,
time_series_type=SingleTimeSeries,
**metadata.features,
),
dict(metadata.features),
)
for metadata in metadata_records
]


def _normalize_target_time_series(
target_system: Any,
target_component: Any,
name: str,
profiles: list[tuple[SingleTimeSeries, dict[str, Any]]],
normalization_value: float,
scaling_function: str,
) -> int:
"""Normalize target profiles and associate their PowerSystems scaling function."""
from infrasys.normalization import NormalizationByValue
from r2x_sienna.exporter import set_time_series_scaling_factor_multiplier

for time_series, features in profiles:
normalized_time_series = SingleTimeSeries.from_array(
data=time_series.data_array,
name=time_series.name,
initial_timestamp=time_series.initial_timestamp,
resolution=time_series.resolution,
normalization=NormalizationByValue(value=normalization_value),
)
replace_single_time_series(
target_system,
target_component,
normalized_time_series,
**features,
)

set_time_series_scaling_factor_multiplier(
target_system,
target_component,
name,
scaling_function,
)
return len(profiles)


def normalize_max_active_power_time_series(context: PluginContext) -> None:
"""Normalize ReEDS MW profiles for PowerSystems scaling during serialization."""
source_system = cast(Any, context.source_system)
target_system = cast(Any, context.target_system)
target_by_uuid = {str(component.uuid): component for component in target_system.iter_all_components()}
Expand All @@ -66,39 +120,79 @@ def normalize_max_active_power_time_series(context: PluginContext) -> None:
)
continue

for metadata in metadata_records:
features = metadata.features
time_series = target_system.get_time_series(
target_component,
name="max_active_power",
time_series_type=SingleTimeSeries,
**features,
)
normalized_time_series = SingleTimeSeries.from_array(
data=time_series.data_array,
name=time_series.name,
initial_timestamp=time_series.initial_timestamp,
resolution=time_series.resolution,
normalization=NormalizationByValue(value=max_active_power),
)
replace_single_time_series(
target_system,
target_component,
normalized_time_series,
**features,
)
normalized += 1

set_time_series_scaling_factor_multiplier(
profiles = _get_target_time_series_profiles(
target_system,
target_component,
"max_active_power",
metadata_records,
)
normalized += _normalize_target_time_series(
target_system,
target_component,
"max_active_power",
profiles,
max_active_power,
"get_max_active_power",
)

logger.info("Normalized %s max_active_power time series for Sienna", normalized)


def normalize_reserve_requirement_time_series(context: PluginContext) -> None:
"""Normalize ReEDS reserve requirement profiles for PowerSystems scaling."""
from r2x_reeds.models import ReEDSReserve
from r2x_sienna.models import VariableReserve, VariableReserveNonSpinning

source_system = cast(Any, context.source_system)
target_system = cast(Any, context.target_system)
target_by_uuid = {
str(component.uuid): component
for component in target_system.iter_all_components()
if isinstance(component, VariableReserve | VariableReserveNonSpinning)
}
normalized = 0

for source_reserve in source_system.get_components(ReEDSReserve):
metadata_records = source_system.list_time_series_metadata(
source_reserve,
name="requirement",
time_series_type=SingleTimeSeries,
)
if not metadata_records:
continue

target_reserve = target_by_uuid.get(str(source_reserve.uuid))
if target_reserve is None:
continue

profiles = _get_target_time_series_profiles(
target_system,
target_reserve,
"requirement",
metadata_records,
)
peak_requirement = max(float(np.max(profile.data_array)) for profile, _ in profiles)

if peak_requirement <= 0.0:
logger.warning(
"Cannot normalize requirement time series for %s without a positive MW basis",
source_reserve.name,
)
continue

target_reserve.requirement = peak_requirement / float(target_system.base_power)
normalized += _normalize_target_time_series(
target_system,
target_reserve,
"requirement",
profiles,
peak_requirement,
"get_requirement",
)

logger.info("Normalized %s reserve requirement time series for Sienna", normalized)


def attach_pumped_hydro_inflow_time_series(context: PluginContext) -> None:
"""Attach zero inflow profiles to translated pumped-hydro reservoirs."""
from r2x_reeds.models import ReEDSDemand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
add_generator_emissions,
attach_pumped_hydro_inflow_time_series,
normalize_max_active_power_time_series,
normalize_reserve_requirement_time_series,
)
from r2x_reeds_to_sienna.plugin_config import ReEDSToSiennaConfig

Expand Down Expand Up @@ -57,6 +58,7 @@ def reeds_to_sienna(system: System, config: ReEDSToSiennaConfig) -> System:
apply_rules_to_context(context)

normalize_max_active_power_time_series(context)
normalize_reserve_requirement_time_series(context)
attach_pumped_hydro_inflow_time_series(context)
add_generator_emissions(context)

Expand Down
36 changes: 36 additions & 0 deletions packages/r2x-reeds-to-sienna/tests/test_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,42 @@ def record_scaling_call(system, owner, name, function_name):
assert scaling_calls == [(result, target_wind, "max_active_power", "get_max_active_power")]


def test_reeds_to_sienna_normalizes_reserve_requirement_time_series(monkeypatch):
source = _build_source_system()
source_reserve = source.get_component(ReEDSReserve, "spin_up")
scaling_calls = []

def record_scaling_call(system, owner, name, function_name):
scaling_calls.append((system, owner, name, function_name))

monkeypatch.setattr(
"r2x_sienna.exporter.set_time_series_scaling_factor_multiplier",
record_scaling_call,
)
source_profile = SingleTimeSeries.from_array(
data=[0.0, 25.0, 50.0],
name="requirement",
initial_timestamp=datetime(2012, 1, 1),
resolution=timedelta(hours=1),
)
source.add_time_series(source_profile, source_reserve)

result = reeds_to_sienna(source, config=ReEDSToSiennaConfig())

target_reserve = result.get_component(VariableReserve, "spin_up")
target_profile = result.get_time_series(target_reserve, name="requirement")
target_metadata = result.list_time_series_metadata(target_reserve, name="requirement")[0]
preserved_source = source.get_time_series(source_reserve, name="requirement")

assert target_reserve.requirement == pytest.approx(0.5)
assert isinstance(target_metadata.normalization, NormalizationByValue)
assert target_metadata.normalization.value == pytest.approx(50.0)
assert target_profile.data_array == pytest.approx([0.0, 0.5, 1.0])
assert preserved_source.data_array == pytest.approx([0.0, 25.0, 50.0])

assert scaling_calls == [(result, target_reserve, "requirement", "get_requirement")]


def test_reeds_to_sienna_translates_hydro():
source = _build_source_system()
result = reeds_to_sienna(source, config=ReEDSToSiennaConfig())
Expand Down