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
3 changes: 3 additions & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ New features
* Let storage scheduling infer missing ``power-capacity`` from directional device capacities before falling back to site capacity, and default the missing opposite capacity to zero when only a non-zero ``consumption-capacity`` or ``production-capacity`` is configured [see `PR #2222 <https://www.github.com/FlexMeasures/flexmeasures/pull/2222>`_]
* Support multiple feeders to a shared storage [see `PR #2001 <https://www.github.com/FlexMeasures/flexmeasures/pull/2001>`_ ]
* The flex-context can now define multiple commodities, each specifying their own prices and grid capacities [see `PR #1946 <https://www.github.com/FlexMeasures/flexmeasures/pull/1946>`_, `PR #2172 <https://www.github.com/FlexMeasures/flexmeasures/pull/2172>`_, `PR #2235 <https://www.github.com/FlexMeasures/flexmeasures/pull/2235>`_ and `PR #2271 <https://www.github.com/FlexMeasures/flexmeasures/pull/2271>`_]
* In the UI, the flex-context editor supports editing commitments (name, commodity, baseline and deviation prices, each accepting a fixed value or a sensor), also within each commodity context [see `PR #2287 <https://www.github.com/FlexMeasures/flexmeasures/pull/2287>`_]
* Commitments in the flex-context support a ``commodity`` field, defaulting to electricity, to bind a commitment to that commodity's devices [see `PR #2287 <https://www.github.com/FlexMeasures/flexmeasures/pull/2287>`_]
* CLI support for adding/editing account attributes [see `PR #2242 <https://www.github.com/FlexMeasures/flexmeasures/pull/2242>`_]
* Extended ``GET /api/v3_0/jobs/<uuid>`` with a ``result`` field containing ``unresolved`` and ``resolved`` soft state-of-charge constraint analysis (``soc-minima``/``soc-maxima`` violations or satisfied constraints, keyed by asset ID) for scheduling jobs; both arrays are empty when no SoC constraints were defined [see `PR #2072 <https://www.github.com/FlexMeasures/flexmeasures/pull/2072>`_]

Expand All @@ -36,6 +38,7 @@ Infrastructure / Support

Bugfixes
-----------
* Namespace user-given commitment names with a ``custom:`` prefix, so they cannot shadow the commitments the scheduler sets up internally (whose costs are reported in the same name-keyed dict of the scheduling results) [see `PR #2285 <https://www.github.com/FlexMeasures/flexmeasures/pull/2285>`_]
* Let storage scheduling treat missing constant SoC bounds as unconstrained lower or upper bounds [see `PR #2221 <https://www.github.com/FlexMeasures/flexmeasures/pull/2221>`_]
* Allow root assets belonging to different accounts to share the same name, while keeping asset names unique among root assets within the same account and among children of the same parent [see `PR #2226 <https://www.github.com/FlexMeasures/flexmeasures/pull/2226>`_]
* Fix queued train-predict forecasting jobs losing their resolved forecast window or failing on detached database objects in workers [see `PR #2035 <https://www.github.com/FlexMeasures/flexmeasures/pull/2035>`_]
Expand Down
13 changes: 12 additions & 1 deletion flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,13 @@ def convert_to_commitments(
flex_model,
**timing_kwargs,
) -> list[FlowCommitment | StockCommitment]:
"""Convert list of commitment specifications (dicts) to a list of FlowCommitments."""
"""Convert list of commitment specifications (dicts) to a list of FlowCommitments.

User-given commitment names are namespaced with a "custom:" prefix, so users can
pick any name without colliding with the commitments the scheduler sets up
internally (e.g. "electricity net energy" or "any soc minima"). The prefixed
name is what shows up in the commitment costs of the scheduling results.
"""
commitment_specs = self.flex_context.get("commitments", [])
if len(commitment_specs) == 0:
return []
Expand All @@ -1324,6 +1330,11 @@ def convert_to_commitments(
price_unit = self.flex_context["shared_currency_unit"] + "/MW"
commitments = []
for commitment_spec in commitment_specs:
# Namespace the user-given name (guarding against double prefixing,
# in case the specs are converted more than once).
if not commitment_spec["name"].startswith("custom:"):
commitment_spec["name"] = f"custom:{commitment_spec['name']}"

# Convert baseline, up_price and down_price to pd.Series, then create FlowCommitment
if "up_price" in commitment_spec:
commitment_spec["upwards_deviation_price"] = (
Expand Down
42 changes: 42 additions & 0 deletions flexmeasures/data/models/planning/tests/test_commitments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import numpy as np

from flexmeasures.data.services.utils import get_or_create_model
from flexmeasures.utils.unit_utils import ur
from flexmeasures.data.models.planning import (
Commitment,
StockCommitment,
Expand Down Expand Up @@ -1782,3 +1783,44 @@ def test_electricity_device_indices_exclude_other_commodities():
assert mapping["electricity"] == [0, 2, 3, 4]
assert mapping["gas"] == [1, 5]
assert scheduler._electricity_device_indices() == [0, 2, 3, 4]


def test_user_commitment_names_are_namespaced(app):
"""User-given commitment names get a "custom:" prefix, so they cannot shadow
the commitments the scheduler sets up internally (e.g. "electricity net energy"),
whose costs are reported in the same name-keyed dict.
"""
scheduler = object.__new__(StorageScheduler)
start = pd.Timestamp("2024-01-01T00:00:00+01:00")
end = pd.Timestamp("2024-01-01T04:00:00+01:00")
resolution = pd.Timedelta("1h")
scheduler.flex_context = {
"shared_currency_unit": "EUR",
"commitments": [
{
# Deliberately shadowing an internal commitment name
"name": "electricity net energy",
"baseline": ur.Quantity("0 MW"),
"up_price": ur.Quantity("100 EUR/MWh"),
},
],
}
flex_model = [{"commodity": "electricity"}]

commitments = scheduler.convert_to_commitments(
flex_model,
query_window=(start, end),
resolution=resolution,
beliefs_before=start,
)
assert len(commitments) == 1
assert commitments[0].name == "custom:electricity net energy"

# Converting the same specs again must not double the prefix.
commitments = scheduler.convert_to_commitments(
flex_model,
query_window=(start, end),
resolution=resolution,
beliefs_before=start,
)
assert commitments[0].name == "custom:electricity net energy"
11 changes: 10 additions & 1 deletion flexmeasures/data/schemas/scheduling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@ def forbid_time_series_specs(self, data: dict, **kwargs):


class CommitmentSchema(Schema):
name = fields.Str(required=True, data_key="name")
name = fields.Str(required=True, data_key="name", validate=validate.Length(min=1))
commodity = fields.Str(
required=False,
load_default="electricity",
data_key="commodity",
metadata=dict(
description="Commodity whose devices this commitment binds. Defaults to electricity.",
example="electricity",
),
)
baseline = VariableQuantityField("MW", required=False, data_key="baseline")
up_price = VariableQuantityField("/MW", required=False, data_key="up-price")
down_price = VariableQuantityField(
Expand Down
12 changes: 10 additions & 2 deletions flexmeasures/data/schemas/scheduling/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,16 @@ def to_dict(self):
example={"sensor": 11},
)
COMMITMENTS = MetaData(
description="Prior commitments. Support for this field in the UI is still under further development, but you can find more information in :ref:`commitments`.",
example=[],
description="""Prior commitments. Each commitment needs a ``name`` and a ``baseline``, plus at least one deviation price (``up-price`` and/or ``down-price``); its ``commodity`` defaults to electricity.
You can find more information in :ref:`commitments`.
""",
example=[
{
"name": "capacity contract",
"baseline": "100 kW",
"up-price": {"sensor": 5},
}
],
)
CONSUMPTION_PRICE = MetaData(
description="The commodity price (e.g. electricity price) applied to the site's aggregate consumption. Can be (a sensor recording) market prices, but also CO₂ intensity—whatever fits your optimization problem. [#old_consumption_price_field]_",
Expand Down
21 changes: 18 additions & 3 deletions flexmeasures/ui/static/openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4573,7 +4573,14 @@
"type": "object",
"properties": {
"name": {
"type": "string"
"type": "string",
"minLength": 1
},
"commodity": {
"type": "string",
"default": "electricity",
"description": "Commodity whose devices this commitment binds. Defaults to electricity.",
"example": "electricity"
},
"baseline": {},
"up-price": {},
Expand Down Expand Up @@ -4718,8 +4725,16 @@
"example": true
},
"commitments": {
"description": "Prior commitments. Support for this field in the UI is still under further development, but you can find more information in <a href=\"/ui/static/documentation/html/search.html?q=commitments\" target=\"_blank\">the docs</a>.",
"example": [],
"description": "Prior commitments. Each commitment needs a <code>name</code> and a <code>baseline</code>, plus at least one deviation price (<code>up-price</code> and/or <code>down-price</code>); its <code>commodity</code> defaults to electricity.\nYou can find more information in <a href=\"/ui/static/documentation/html/search.html?q=commitments\" target=\"_blank\">the docs</a>.\n",
"example": [
{
"name": "capacity contract",
"baseline": "100 kW",
"up-price": {
"sensor": 5
}
}
],
"type": "array",
"items": {
"$ref": "#/components/schemas/Commitment"
Expand Down
Loading
Loading