Skip to content

Commit

Permalink
fix: Fixed parser helpers and typehinting
Browse files Browse the repository at this point in the history
  • Loading branch information
pesap committed Oct 24, 2024
1 parent c19e4fe commit f3c4a26
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 22 deletions.
11 changes: 9 additions & 2 deletions src/r2x/exporter/plexos.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
Transformer2W,
TransmissionInterface,
)
from r2x.models.branch import Line
from r2x.units import get_magnitude
from r2x.utils import custom_attrgetter, get_enum_from_string, read_json

Expand Down Expand Up @@ -211,6 +212,8 @@ def insert_component_properties(
match component_type.__name__:
case "GenericBattery":
custom_map = {"active_power": "Max Power", "storage_capacity": "Capacity"}
case "Line":
custom_map = {"rating": "Max Flow"}
case _:
custom_map = {}
property_map = self.property_map | custom_map
Expand Down Expand Up @@ -247,7 +250,9 @@ def add_component_category(
class_id = self._db_mgr.get_class_id(class_enum)
categories = [
(class_id, rank, category or "")
for rank, category in enumerate(sorted(component_categories), start=existing_rank + 1)
for rank, category in enumerate(
sorted(component_categories, key=lambda x: (x is None, x)), start=existing_rank + 1
)
]

# Maybe replace `t_category` with right schema.
Expand Down Expand Up @@ -415,6 +420,8 @@ def add_lines(self) -> None:
# NOTE: The default line on Plexos is a `MonitoredLine` without category. If we need to add a category
# in the future, we will uncomment the line below with the pertinent category name.
# self.add_component_category(MonitoredLine, class_enum=ClassEnum.Line)
self.bulk_insert_objects(Line, class_enum=ClassEnum.Line, collection_enum=CollectionEnum.Lines)
self.insert_component_properties(Line, parent_class=ClassEnum.System, collection=CollectionEnum.Lines)
self.bulk_insert_objects(
MonitoredLine, class_enum=ClassEnum.Line, collection_enum=CollectionEnum.Lines
)
Expand All @@ -426,7 +433,7 @@ def add_lines(self) -> None:
collection_properties = self._db_mgr.get_valid_properties(
collection=CollectionEnum.Lines, parent_class=ClassEnum.System, child_class=ClassEnum.Line
)
for line in self.system.get_components(MonitoredLine, filter_func=lambda x: getattr(x, "ext", False)):
for line in self.system.get_components(MonitoredLine, Line):
properties = get_export_properties(
line.ext,
partial(apply_property_map, property_map=self.property_map),
Expand Down
12 changes: 11 additions & 1 deletion src/r2x/models/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from typing import Annotated
from infrasys.models import InfraSysBaseModelWithIdentifers
from infrasys.value_curves import LinearCurve
from pydantic import Field, computed_field
from infrasys.cost_curves import ProductionVariableCostCurve
from infrasys.cost_curves import FuelCurve, ProductionVariableCostCurve
from r2x.units import Currency, FuelPrice
from operator import attrgetter

Expand Down Expand Up @@ -59,6 +60,15 @@ class ThermalGenerationCost(OperationalCost):
start_up: Annotated[Currency | None, Field(description="Cost to start the unit.")] = Currency(0, "usd")
variable: ProductionVariableCostCurve | None = None

@classmethod
def example(cls) -> "ThermalGenerationCost":
return ThermalGenerationCost(
fixed=FuelPrice(0, "usd/MWh"),
shut_down=Currency(100, "usd"),
start_up=Currency(100, "usd"),
variable=FuelCurve(value_curve=LinearCurve(10)), # type: ignore
)


class StorageCost(OperationalCost):
charge_variable_cost: ProductionVariableCostCurve | None = None
Expand Down
16 changes: 10 additions & 6 deletions src/r2x/parser/parser_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,24 +144,24 @@ def fill_missing_timestamps(data_file: pl.DataFrame, hourly_time_index: pl.DataF
... ).to_frame("datetime")
>>> fill_missing_timestamps(df, hourly_time_index)
"""
# Match case based on available columns
match data_file.columns:
column_set = set(data_file.columns)
match column_set:
# Case when "year", "month", "day", and "hour" are all present
case ["year", "month", "day", "hour", *_]:
case s if s.issuperset({"year", "month", "day", "hour"}):
data_file = data_file.with_columns(
pl.datetime(pl.col("year"), pl.col("month"), pl.col("day"), pl.col("hour"))
)
upsample_data = hourly_time_index.join(data_file, on="datetime", how="left")
return upsample_data.fill_null(strategy="forward")

# Case when "year", "month", and "day" are present but "hour" is missing
case ["year", "month", "day", *_]:
case s if s.issuperset({"year", "month", "day"}):
data_file = data_file.with_columns(pl.datetime(pl.col("year"), pl.col("month"), pl.col("day")))
upsample_data = hourly_time_index.join(data_file, on="datetime", how="left")
return upsample_data.fill_null(strategy="forward")

# Case when "day" is missing, but "year" and "month" are present
case ["year", "month", *_] if "day" not in data_file.columns:
case s if s.issuperset({"year", "month"}):
data_file = data_file.with_columns(pl.datetime(pl.col("year"), pl.col("month"), 1)) # First day
upsample_data = hourly_time_index.join(data_file, on="datetime", how="left")
return upsample_data.fill_null(strategy="forward")
Expand Down Expand Up @@ -200,16 +200,20 @@ def resample_data_to_hourly(data_file: pl.DataFrame) -> pl.DataFrame:
# Expecting two rows: one for hour 0 and one for hour 1
"""
# Create a timestamp from year, month, day, hour, and minute
if max(data_file["period"]) == 48.0: # sub-hourly data
data_file = data_file.with_columns(((pl.col("period") - 1) / 2).cast(pl.Int32).alias("hour"))
data_file = data_file.with_columns(
pl.datetime(
data_file["year"],
data_file["month"],
data_file["day"],
hour=data_file["hour"],
minute=data_file["minute"],
# minute=data_file["minute"],
).alias("timestamp")
)

data_file = data_file.drop_nulls().sort("timestamp")

# Group by the hour and aggregate the values
return (
data_file.group_by_dynamic("timestamp", every="1h")
Expand Down
17 changes: 11 additions & 6 deletions src/r2x/parser/plexos.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ def __init__(self, *args, xml_file: str | None = None, **kwargs) -> None:
# TODO(pesap): Rename exceptions to include R2X
# https://github.com/NREL/R2X/issues/5
# R2X needs at least one of this maps defined to correctly work.
if not self.fuel_map and not self.device_map and not self.device_match_string:
if (
not self.fuel_map
and not self.device_map
and not self.device_match_string
and not self.category_map
):
msg = (
"Neither `plexos_fuel_map` or `plexos_device_map` or `device_match_string` was provided. "
"To fix, provide any of the mappings."
Expand Down Expand Up @@ -858,23 +863,23 @@ def _construct_value_curves(self, record, generator_name): # noqa: C901
heat_rate_avg = (
Quantity(
np.median(heat_rate_avg.data),
units=heat_rate_avg.units,
units=heat_rate_avg.data.units,
)
if isinstance(heat_rate_avg, SingleTimeSeries)
else heat_rate_avg
)
heat_rate_base = (
Quantity(
np.median(heat_rate_base.data),
units=heat_rate_base.units,
units=heat_rate_base.data.units,
)
if isinstance(heat_rate_base, SingleTimeSeries)
else heat_rate_base
)
heat_rate_incr = (
Quantity(
np.median(heat_rate_incr.data),
units=heat_rate_incr.units,
units=heat_rate_incr.data.units,
)
if isinstance(heat_rate_incr, SingleTimeSeries)
else heat_rate_incr
Expand Down Expand Up @@ -1091,7 +1096,7 @@ def _set_unit_capacity(self, record):
if rating_factor := record.get("Rating Factor"):
if isinstance(rating_factor, SingleTimeSeries):
rating_factor_data = rating_factor.data
if rating_factor.units == "percent":
if rating_factor_data.units == "percent":
rating_factor_data = rating_factor_data.to("")
if not (max_active_power := record.get("max_active_power")):
# Order of the operation matters
Expand Down Expand Up @@ -1195,7 +1200,7 @@ def _construct_load_profiles(self):

if max_active_power := mapped_records.get("max_active_power"):
max_load = (
np.max(max_active_power.data)
np.nanmax(max_active_power.data)
if isinstance(max_active_power, SingleTimeSeries)
else max_active_power
)
Expand Down
2 changes: 1 addition & 1 deletion src/r2x/parser/plexos_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DATAFILE_COLUMNS(Enum): # noqa: N801
"""Enum of possible Data file columns in Plexos."""

NV = ("name", "value")
Y = "year"
Y = ("year",)
PV = ("pattern", "value")
TS_NPV = ("name", "pattern", "value")
TS_NYV = ("name", "year", "value")
Expand Down
13 changes: 8 additions & 5 deletions tests/models/pjm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def pjm_2area() -> System:
# Add topology elements
system.add_component(Area(name="init"))
system.add_component(Area(name="Area2"))
system.add_component(LoadZone(name="LoadZone1"))
system.add_component(LoadZone(name="init"))
system.add_component(LoadZone(name="Area2"))

for bus in pjm_2area_components["bus"]:
system.add_component(
Expand All @@ -40,6 +41,7 @@ def pjm_2area() -> System:
bus_type=ACBusTypes(bus["bustype"]),
base_voltage=Voltage(bus["base_voltage"], "kV"),
area=system.get_component(Area, bus["area"]),
load_zone=system.get_component(LoadZone, bus["area"]),
magnitude=bus["magnitude"],
)
)
Expand Down Expand Up @@ -68,7 +70,7 @@ def pjm_2area() -> System:
x=0.03,
to_bus=bust,
rating_up=1000 * ureg.MW,
rating=1000 * ureg.MW,
rating_down=-1000 * ureg.MW,
)
system.add_component(branch_monitored)

Expand Down Expand Up @@ -97,14 +99,15 @@ def pjm_2area() -> System:
startup_cost=gen["StartupCost"] * ureg.Unit("usd"),
shutdown_cost=gen["ShutDnCost"] * ureg.Unit("usd"),
vom_price=gen["VOM"] * ureg.Unit("usd/MWh"),
min_down_time=gen["MinTimeDn"],
min_up_time=gen["MinTimeUp"],
min_down_time=Time(gen["MinTimeDn"], "hour"),
min_up_time=Time(gen["MinTimeUp"], "hour"),
mean_time_to_repair=Time(10.0, "hour"),
forced_outage_rate=Percentage(0.0),
planned_outage_rate=Percentage(0.0),
ramp_up=gen["RampLimitsUp"],
ramp_down=gen["RampLimitsDn"],
bus=system.get_component(ACBus, gen["BusName"]),
category="thermal",
)
)

Expand Down Expand Up @@ -169,7 +172,7 @@ def pjm_2area() -> System:
reserve_map = ReserveMap(name="pjm_reserve_map")
reserve = Reserve(
name="SpinUp-pjm",
region=system.get_component(LoadZone, "LoadZone1"),
region=system.get_component(LoadZone, "init"),
reserve_type=ReserveType.SPINNING,
vors=0.05,
duration=3600.0,
Expand Down
3 changes: 3 additions & 0 deletions tests/test_parser_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ def test_fill_missing_timestamps():
# Assert that the result contains 24 rows (for each hour of the day)
assert len(result) == 24

data_file = pl.DataFrame({"name": ["testname"], "year": [2020], "month": [1], "value": [1]})
result = fill_missing_timestamps(data_file, hourly_time_index)

data_file = pl.DataFrame({"year": [2020], "value": [1]})
with pytest.raises(ValueError):
_ = fill_missing_timestamps(data_file, hourly_time_index)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_plexos_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

def test_get_column_enum():
"""Test multiple cases for get_column_enum function."""
# Case 1: Exact match for NV
columns = ["year", "random_column", "random_column_2"]
assert get_column_enum(columns) == DATAFILE_COLUMNS.Y

columns = ["name", "value"]
assert get_column_enum(columns) == DATAFILE_COLUMNS.NV

Expand Down

0 comments on commit f3c4a26

Please sign in to comment.