Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/save soc values as a sensor #1018

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
14 changes: 12 additions & 2 deletions flexmeasures/cli/data_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,9 +1216,16 @@ def create_schedule(ctx):
"--soc-at-start",
"soc_at_start",
type=QuantityField("%", validate=validate.Range(min=0, max=1)),
required=True,
required=False,
help="State of charge (e.g 32.8%, or 0.328) at the start of the schedule.",
)
@click.option(
Ahmad-Wahid marked this conversation as resolved.
Show resolved Hide resolved
"--soc",
"soc",
type=VariableQuantityField("MWh"),
required=False,
help="State of charge (e.g sensor:<id>, or 0.328) at the start of the schedule.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
help="State of charge (e.g sensor:<id>, or 0.328) at the start of the schedule.",
# help='State of charge (e.g sensor:<id>, or "328 kWh") at the start of the schedule. If a sensor is passed, the computed schedule will also be saved to it.',
help='If a sensor (e.g sensor:<id>) recording the state of charge is passed, the computed schedule will also be saved to it.',

)
@click.option(
"--soc-target",
"soc_target_strings",
Expand Down Expand Up @@ -1353,6 +1360,7 @@ def add_schedule_for_storage( # noqa C901
start: datetime,
duration: timedelta,
soc_at_start: ur.Quantity,
soc: ur.Quantity | Sensor | None,
charging_efficiency: ur.Quantity | Sensor | None,
discharging_efficiency: ur.Quantity | Sensor | None,
soc_gain: ur.Quantity | Sensor | None,
Expand Down Expand Up @@ -1402,7 +1410,8 @@ def add_schedule_for_storage( # noqa C901
)
raise click.Abort()
capacity_str = f"{power_sensor.get_attribute('max_soc_in_mwh')} MWh"
soc_at_start = convert_units(soc_at_start.magnitude, soc_at_start.units, "MWh", capacity=capacity_str) # type: ignore
if soc_at_start is not None:
soc_at_start = convert_units(soc_at_start.magnitude, soc_at_start.units, "MWh", capacity=capacity_str) # type: ignore
soc_targets = []
for soc_target_tuple in soc_target_strings:
soc_target_value_str, soc_target_datetime_str = soc_target_tuple
Expand All @@ -1429,6 +1438,7 @@ def add_schedule_for_storage( # noqa C901
belief_time=server_now(),
resolution=power_sensor.event_resolution,
flex_model={
"soc": soc,
"soc-at-start": soc_at_start,
"soc-targets": soc_targets,
"soc-min": soc_min,
Expand Down
12 changes: 12 additions & 0 deletions flexmeasures/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,18 @@ def create_test_battery_assets(
),
)
db.session.add(test_battery_sensor_kw)
test_battery_sensor_kwh = Sensor(
name="state of charge (Wh)",
generic_asset=test_battery,
event_resolution=timedelta(hours=0),
unit="Wh",
attributes=dict(
daily_seasonality=True,
weekly_seasonality=True,
yearly_seasonality=True,
),
)
db.session.add(test_battery_sensor_kwh)

test_battery_no_prices = GenericAsset(
name="Test battery with no known prices",
Expand Down
73 changes: 71 additions & 2 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
get_power_values,
fallback_charging_policy,
get_continuous_series_sensor_or_quantity,
# get_soc_sensor_value,
)
from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException
from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema
from flexmeasures.data.schemas.scheduling import FlexContextSchema
from flexmeasures.utils.time_utils import get_max_planning_horizon
from flexmeasures.utils.coding_utils import deprecated
from flexmeasures.utils.unit_utils import ur, convert_units
from flexmeasures.utils.calculations import integrate_time_series


class MetaStorageScheduler(Scheduler):
Expand Down Expand Up @@ -79,6 +81,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
sensor = self.sensor

soc_at_start = self.flex_model.get("soc_at_start")
soc = self.flex_model.get("soc")
soc_targets = self.flex_model.get("soc_targets")
soc_min = self.flex_model.get("soc_min")
soc_max = self.flex_model.get("soc_max")
Expand All @@ -101,6 +104,12 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
self.flex_context.get("inflexible_device_sensors")
or self.sensor.generic_asset.get_inflexible_device_sensors()
)
soc_sensor = None
if isinstance(soc, Sensor):
soc_sensor = soc
# soc_at_start = get_soc_sensor_value(soc, start)
# elif (isinstance(soc, float) or isinstance(soc, int)) and soc > 0:
# soc_at_start = soc

# Check for required Sensor attributes
power_capacity_in_mw = self.flex_model.get(
Expand Down Expand Up @@ -438,6 +447,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
end,
resolution,
soc_at_start,
soc_sensor,
device_constraints,
ems_constraints,
commitment_quantities,
Expand Down Expand Up @@ -471,6 +481,16 @@ def deserialize_flex_config(self):
# Otherwise, we try to retrieve the current state of charge from the asset (if that is the valid one at the start).
# If that doesn't work, we set the starting soc to 0 (some assets don't use the concept of a state of charge,
# and without soc targets and limits the starting soc doesn't matter).

# if "soc" in self.flex_model:
# if (
# self.flex_model["soc"] is not None
# and self.flex_model["soc-at-start"] is not None
# ):
# raise Exception(
# "Both 'soc-at-start' and 'soc' parameters are provided, however, only one of them is necessary."
# )

if (
"soc-at-start" not in self.flex_model
or self.flex_model["soc-at-start"] is None
Expand All @@ -485,6 +505,9 @@ def deserialize_flex_config(self):
else:
self.flex_model["soc-at-start"] = 0

if self.flex_model.get("soc") is None:
self.flex_model["soc"] = "0 MWh"

self.ensure_soc_min_max()

# Now it's time to check if our flex configuration holds up to schemas
Expand Down Expand Up @@ -600,6 +623,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
end,
resolution,
soc_at_start,
soc_sensor,
device_constraints,
ems_constraints,
commitment_quantities,
Expand All @@ -612,19 +636,41 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
sensor, device_constraints[0], start, end, resolution
)
storage_schedule = convert_units(storage_schedule, "MW", sensor.unit)
if soc_sensor is not None:
soc_schedule = integrate_time_series(
storage_schedule,
soc_at_start,
down_efficiency=device_constraints[0]["derivative down efficiency"],
up_efficiency=device_constraints[0]["derivative up efficiency"],
storage_efficiency=device_constraints[0]["efficiency"],
)
soc_schedule = convert_units(
soc_schedule, f"{sensor.unit}h", soc_sensor.unit
)

# Round schedule
if self.round_to_decimals:
storage_schedule = storage_schedule.round(self.round_to_decimals)
if soc_sensor is not None:
soc_schedule = soc_schedule.round(self.round_to_decimals)

if self.return_multiple:
return [
data_list = [
{
"name": "storage_schedule",
"sensor": sensor,
"data": storage_schedule,
}
]
if soc_sensor is not None:
data_list.append(
{
"name": "soc_schedule",
"sensor": soc_sensor,
"data": soc_schedule,
}
)
return data_list
else:
return storage_schedule

Expand All @@ -649,6 +695,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
end,
resolution,
soc_at_start,
soc_sensor,
device_constraints,
ems_constraints,
commitment_quantities,
Expand All @@ -670,19 +717,41 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
# Obtain the storage schedule from all device schedules within the EMS
storage_schedule = ems_schedule[0]
storage_schedule = convert_units(storage_schedule, "MW", sensor.unit)
if soc_sensor is not None:
soc_schedule = integrate_time_series(
storage_schedule,
soc_at_start,
down_efficiency=device_constraints[0]["derivative down efficiency"],
up_efficiency=device_constraints[0]["derivative up efficiency"],
storage_efficiency=device_constraints[0]["efficiency"],
)
soc_schedule = convert_units(
soc_schedule, f"{sensor.unit}h", soc_sensor.unit
)

# Round schedule
if self.round_to_decimals:
storage_schedule = storage_schedule.round(self.round_to_decimals)
if soc_sensor is not None:
soc_schedule = soc_schedule.round(self.round_to_decimals)

if self.return_multiple:
return [
data_list = [
{
"name": "storage_schedule",
"sensor": sensor,
"data": storage_schedule,
}
]
if soc_sensor is not None:
data_list.append(
{
"name": "soc_schedule",
"sensor": soc_sensor,
"data": soc_schedule,
}
)
return data_list
else:
return storage_schedule

Expand Down
Loading
Loading