"""coupling_components.py: Utilities for coupling using MUSCLE3."""
import logging
import numpy as np
import pandas as pd
from libmuscle import Grid, Message
from ymmsl import Component, Conduit, Model, Ports
[docs]
logger = logging.getLogger(__name__)
[docs]
def compact_market_info_to_msg(
price: np.ndarray | None, scheduled_demand: np.ndarray | None, timestamp: float
) -> Message:
"""Compact market information to a message.
Args:
price: A numpy array with market prices.
scheduled_demand: A numpy array with the demand scheduled for a satellite.
timestamp: Floating point value to serve as timestamp for model coordination.
Returns:
A muscle3 message where the data attribute is a dictionary with keys
"price" and "scheduled_demand".
"""
if price is None and scheduled_demand is None:
return Message(timestamp=timestamp, data={"price": None, "scheduled_demand": None})
price = Grid(price)
scheduled_demand = Grid(scheduled_demand)
data_dict = {
"price": price,
"scheduled_demand": scheduled_demand,
}
return Message(timestamp=timestamp, data=data_dict)
[docs]
def compact_bids_to_msg(demand_bids: pd.DataFrame, timestamp: float) -> Message:
"""Compact a DataFrame of (block) bids columns into a MUSCLE3 Message.
This takes bids in the following format:
+--------------------+------------------+-----------+----------+-------+
| exclusive_group_id | profile_block_id | timestamp | quantity | price |
+--------------------+------------------+-----------+----------+-------+
| ... | ... | ... | ... | ... |
+--------------------+------------------+-----------+----------+-------+
where:
- `exclusive_group_id`: ID of which exclusive group this bid belongs to
- `profile_block_id`: ID of which profile block this bid belongs to
- `timestamp`: timestamp for this bid
- `quantity`: quantity for this bid
- `price`: price for this bid
and (exclusive_group_id, profile_block_id, timestamp) are its Index.
Every bid must have a `profile_block_id` and `exclusive_group_id`. If a bid
does not make use of profile block or exclusive group functionality, the
`exclusive_group_id` must be unique, while `profile_block_id` can be any value.
The resulting message contains the dataframe converted to a dictionary where
- column and index names become dictionary keys
- column and index values become dictionary values.
Args:
demand_bids: Dataframe table of the bids from a satellite model.
timestamp: Floating point value to serve as timestamp for model coordination.
Returns:
MUSCLE3 message object containing the bids table information in a
dictionary.
"""
demand_bids_dict = {col: Grid(demand_bids[col].to_numpy(), None) for col in demand_bids.columns}
# add indices
for level in demand_bids.index.names:
level_values = demand_bids.index.get_level_values(level)
if level == "timestamp":
# `.astype(int) / 1e9` is equivalent to calling `.timestamp()`, but works on whole array
level_values = level_values.astype(np.int64) / 1e9
demand_bids_dict[level] = Grid(level_values.to_numpy(), None)
msg = Message(timestamp, data=demand_bids_dict)
return msg
[docs]
def get_coupling_setup(config_name: str, number_of_satellites: int) -> Model:
"""Create the MUSCLE3 coupling configuration for the energy system network.
Args:
config_name: name of this run to be used as model name
number_of_satellites: number of satellites to spin up
Returns:
MUSCLE3 Model object with the standard coupling configuration
"""
components = [
Component("central", "central", None, Ports(o_i=["market_info_out"], s=["bids_in"])),
Component("satellite", "satellite", [number_of_satellites], Ports(f_init=["market_info_in"], o_f=["bids_out"])),
]
conduits = [
Conduit("central.market_info_out", "satellite.market_info_in"),
Conduit("satellite.bids_out", "central.bids_in"),
]
model = Model(config_name, components, conduits)
return model