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
1 change: 1 addition & 0 deletions src/r2x/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ReserveDirection(StrEnum):

UP = "UP"
DOWN = "DOWN"
SYMMETRIC = "SYMMETRIC"


class ACBusTypes(StrEnum):
Expand Down
2 changes: 1 addition & 1 deletion src/r2x/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@
StartTimeLimits,
UpDown,
)
from .services import Reserve, TransmissionInterface, VariableReserve
from .services import ConstantReserve, Reserve, TransmissionInterface, VariableReserve
from .topology import ACBus, Arc, Area, Bus, DCBus, LoadZone
168 changes: 101 additions & 67 deletions src/r2x/models/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,137 @@

from typing import Annotated

from pydantic import Field, NonNegativeFloat, PositiveFloat
from pydantic import Field

from r2x.enums import ReserveDirection, ReserveType
from r2x.enums import ReserveDirection
from r2x.models.core import Service
from r2x.models.named_tuples import MinMax
from r2x.models.topology import LoadZone
from r2x.units import Percentage


class Reserve(Service):
"""Class representing a reserve contribution."""
"""Base class representing a reserve contribution.

This is a base class for reserve products. For specific reserve types,
use ConstantReserve or VariableReserve classes.
"""

time_frame: Annotated[
PositiveFloat,
Field(description="Timeframe in which the reserve is required in seconds"),
] = 1e30
region: (
Annotated[
LoadZone,
Field(description="LoadZone where reserve requirement is required."),
]
| None
) = None
vors: Annotated[
float,
Field(description="Value of reserve shortage in $/MW. Any positive value as as soft constraint."),
] = -1
duration: (
Annotated[
PositiveFloat,
Field(description="Time over which the required response must be maintained in seconds."),
]
| None
) = None
reserve_type: ReserveType
load_risk: (
Annotated[
Percentage,
Field(
description="Proportion of Load that contributes to the requirement.",
),
]
| None
) = None
# ramp_rate: float | None = None # NOTE: Maybe we do not need this.
max_requirement: float = 0 # Should we specify which variable is the time series for?
direction: ReserveDirection

@classmethod
def example(cls) -> "Reserve":
return Reserve(
name="ExampleReserve",
region=LoadZone.example(),
direction=ReserveDirection.UP,
reserve_type=ReserveType.REGULATION,
)


class VariableReserve(Reserve):
time_frame: Annotated[
NonNegativeFloat,
Field(description="Timeframe in which the reserve is required in seconds"),
] = 0.0
requirement: Annotated[
NonNegativeFloat | None,
Field(
description="the value of required reserves in p.u (SYSTEM_BASE), validation range: (0, nothing)"
ge=0.0,
description=(
"Saturation time_frame in minutes to provide reserve contribution, "
"validation range: (0, nothing)"
)
),
]
] = 0.0
sustained_time: Annotated[
NonNegativeFloat,
Field(description="the time in seconds reserve contribution must sustained at a specified level"),
float,
Field(
ge=0.0,
description=(
"The time in seconds reserve contribution must be sustained at a specified level, "
"validation range: (0, nothing)"
)
),
] = 3600.0
max_output_fraction: Annotated[
NonNegativeFloat,
float,
Field(
ge=0.0,
le=1.0,
description="the time in seconds reserve contribution must sustained at a specified level",
description=(
"The maximum fraction of each device's output that can be assigned to the service, "
"validation range: (0, 1)"
)
),
] = 1.0
max_participation_factor: Annotated[
NonNegativeFloat,
float,
Field(
ge=0.0,
le=1.0,
description="the maximum portion [0, 1.0] of the reserve that can be contributed per device",
description=(
"The maximum portion [0, 1.0] of the reserve that can be contributed per device, "
"validation range: (0, 1)"
)
),
] = 1.0
deployed_fraction: Annotated[
NonNegativeFloat,
float,
Field(
ge=0.0,
le=1.0,
description="Fraction of service procurement that is assumed to be actually deployed.",
description=(
"Fraction of service procurement that is assumed to be actually deployed. "
"Most commonly, this is assumed to be either 0.0 or 1.0, validation range: (0, 1)"
)
),
] = 3600.0
] = 0.0
direction: ReserveDirection

@classmethod
def example(cls) -> "Reserve":
return Reserve(
name="ExampleReserve",
direction=ReserveDirection.UP,
)


class ConstantReserve(Reserve):
"""A reserve product with a constant procurement requirement.

Such as 3% of the system base power at all times. This reserve product includes online generators that can
respond right away after an unexpected contingency, such as a transmission line or generator outage. When
defining the reserve, the ReserveDirection must be specified to define this as a ReserveUp, ReserveDown,
or ReserveSymmetric.
"""

requirement: Annotated[
float,
Field(
ge=0.0,
description=(
"The value of required reserves in p.u. (SYSTEM_BASE), "
"validation range: (0, nothing)"
)
),
]

@classmethod
def example(cls) -> "ConstantReserve":
return ConstantReserve(
name="ExampleConstantReserve",
direction=ReserveDirection.UP,
requirement=0.03, # 3% of system base
)


class VariableReserve(Reserve):
"""A reserve product with a time-varying procurement requirement.

Such as a higher requirement during hours with an expected high load or high ramp. This reserve product
includes online generators that can respond right away after an unexpected contingency, such as a
transmission line or generator outage. When defining the reserve, the ReserveDirection must be specified
to define this as a ReserveUp, ReserveDown, or ReserveSymmetric. To model the time varying requirement,
a "requirement" time series should be added to this reserve.
"""

requirement: Annotated[
float,
Field(
ge=0.0,
description="The required quantity of the product should be scaled by a TimeSeriesData"
),
]

@classmethod
def example(cls) -> "VariableReserve":
return VariableReserve(
name="ExampleVariableReserve",
direction=ReserveDirection.UP,
requirement=0.05, # 5% of system base
)


class TransmissionInterface(Service):
Expand Down