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
41 changes: 30 additions & 11 deletions docs/translation_mappings/translation-from-plexos-to-pypsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ It gives the source of each field.
| [`Market`](#market--generator) | An import `Generator` |
| [`Reserve`](#reserve--extensions-sidecar) | No component. The translator carries it to the reserves sidecar, but nothing applies it. |
| [Region `VoLL`](#load-shedding) | No component in the two faithful pipelines. In `plexos-to-pypsa-monte-carlo-reliability`, a load shedding `Generator` at each bus. |
| `Zone`, `Interface`, `Transformer`, `Constraint`, `Waterway`, `Decision Variable` | [Not translated](#not-translated) |
| [`Constraint`](#constraint--extensions-sidecar) | No component. The translator carries it to the sidecar, but nothing applies it. |
| `Zone`, `Interface`, `Transformer`, `Waterway`, `Decision Variable` | [Not translated](#not-translated) |
| `Transmission`, `ST`/`MT Schedule`, `PASA`, `Production`, `Performance`, `Stochastic`, `Report`, `Diagnostic`, `System`, `List` | Not translated. These are solver settings, not model data. |

## Reading the tables
Expand Down Expand Up @@ -1004,7 +1005,7 @@ reservoir and no head reservoir.
| `Interface` | Nothing applies the group flow limits. Thus the dispatch can be more than a transfer limit that your PLEXOS model obeys. |
| `Transformer` | The translator does not carry it. |
| `Reserve` requirements | Nothing applies them. The generators that contribute can operate at full output. The translator does carry the reserves. Refer to [`Reserve`](#reserve--extensions-sidecar). |
| `Constraint` | Nothing applies the custom constraints. This includes the energy budgets, the running hour limits, the RPS targets and the emission targets. The translator reports every one. Refer to [`Constraint`](#constraint). |
| `Constraint` | Nothing applies the custom constraints. This includes the energy budgets, the running hour limits, the RPS targets and the emission targets. The translator reports every one, and carries each one it can read to the extensions sidecar. Refer to [`Constraint`](#constraint--extensions-sidecar). |
| `Waterway` | The cascade route between reservoirs is lost. Each reservoir is independent. |
| `Decision Variable` | The translator does not carry it. |
| Emission caps | Nothing applies them. Only the carbon price goes into the cost. |
Expand All @@ -1013,7 +1014,7 @@ reservoir and no head reservoir.
| Ancillary service and demand response pseudo-generators | The translator skips nothing. Refer to [`Generator`](#generator--generator). |
| Gas, heat and water networks | The translator accepts electricity only. |

## `Constraint`
## `Constraint` → extensions sidecar

A PLEXOS `Constraint` holds a weighted sum over the objects it names to a right-hand side.
It weights each object by a coefficient on the membership — `Generation Coefficient`,
Expand All @@ -1023,14 +1024,32 @@ group of hydro units and an annual running hour cap on a group of peakers are bo
this way.

A PyPSA `GlobalConstraint` limits one **carrier** over the whole horizon, and it has no way
to name a set of components. Thus no shape of `Constraint` fits it, and the translator
carries none of them.

It does report all of them. Every right-hand side a `Constraint` states becomes a not
mapped entry against the object that states it, giving the value, the sense, and each term
of the weighted sum: the object, the class it belongs to, and the coefficient weighting it.
A `Constraint` stating no right-hand side is reported against the object itself. One
warning names a few of them and counts the rest.
to name a set of components. Thus no shape of `Constraint` fits it, and the network file
holds none of them.

Each one the translator can read travels in the `extensions.json` file adjacent to the
network instead, as a `constraint` record in framework-neutral terms. The contract is in
`interop/core/extensions.py`. Nothing applies the limit, in the network or in the solve;
the sidecar carries it for a program that decides to use it, and for a later hop into a
framework that can express it.

| Sidecar field | From |
| --- | --- |
| `name` | `Constraint.name` |
| `sense` | `Sense`, as the inequality it holds in: `<=`, `==` or `>=` |
| `limits` | One entry per right-hand side the `Constraint` states, each giving the value, the span it applies over (`horizon`, `hour`, `day`, `week`, `month` or `year`) and the unit the model stated it in |
| `members` | One entry per object the sum names, each giving the name, the PLEXOS class it belongs to, the coefficient and the property that states the coefficient |
| `applies_to_expansion_plan` | `Include in LT Plan` |

A `Constraint` that states no sense, a `Sense` other than `-1`, `0` or `1`, or no
right-hand side at all, states no inequality to carry. The translator leaves that one out
and reports it, naming the `Sense` it could not read where that is the reason.

The report carries all of them either way. Every right-hand side a `Constraint` states
becomes a not mapped entry against the object that states it, giving the value, the sense,
and each term of the weighted sum: the object, the class it belongs to, and the coefficient
weighting it. A `Constraint` stating no right-hand side is reported against the object
itself. One warning names a few of them and counts the rest.

**Read that section of the report before you trust the dispatch.** A model that caps hydro
energy or peaker running hours with a `Constraint` gives a translated network in which
Expand Down
69 changes: 69 additions & 0 deletions interop/core/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ExtensionKind(StrEnum):
# PLEXOS Constraint. PyPSA's GlobalConstraint limits one carrier over the whole horizon
# and cannot name a set of components, which is why the concept needs the sidecar to
# survive a hop through it. Sienna has no equivalent either.
CONSTRAINT = "constraint"
NETWORK = "network" # PyPSA network-level attributes. No Sienna or PLEXOS equivalent.


Expand Down Expand Up @@ -84,6 +85,25 @@ class ReserveKind(StrEnum):
UNKNOWN = "unknown"


class ConstraintSense(StrEnum):
"""Which way a constraint holds its weighted sum against the right-hand side."""

AT_MOST = "<=" # PLEXOS Sense -1
EXACTLY = "==" # PLEXOS Sense 0
AT_LEAST = ">=" # PLEXOS Sense 1


class ConstraintPeriod(StrEnum):
"""The span one right-hand side applies over."""

HORIZON = "horizon" # PLEXOS RHS
HOUR = "hour" # PLEXOS RHS Hour
DAY = "day" # PLEXOS RHS Day
WEEK = "week" # PLEXOS RHS Week
MONTH = "month" # PLEXOS RHS Month
YEAR = "year" # PLEXOS RHS Year


class ExtensionRecord(BaseModel):
"""One component's record. ``name`` is the identifier in every framework."""

Expand Down Expand Up @@ -200,6 +220,45 @@ class ReserveExtension(ExtensionRecord):
is_mutually_exclusive: bool | None = None


class ConstraintMember(BaseModel):
"""One object a constraint weights, and the coefficient it is weighted by.

Two classes can hold an object of the same name and a constraint can weight a generator
and an emission alike, so the member carries the class its name belongs to. The
coefficient is absent where the constraint names the object without weighting it.
"""

model_config = ConfigDict(extra="forbid")

name: str
member_class: str # PLEXOS Generator, Line, Emission and the rest
coefficient: float | None = None
# PLEXOS names the coefficient per class: Generation Coefficient, Flow Coefficient, and
# so on. The name says which quantity of the member the coefficient weights.
coefficient_property: str | None = None


class ConstraintLimit(BaseModel):
"""One right-hand side the weighted sum is held to, and the span it applies over."""

model_config = ConfigDict(extra="forbid")

period: ConstraintPeriod
value: float
unit: str | None = None # the unit the source stated the limit in


class ConstraintExtension(ExtensionRecord):
"""A weighted sum over named objects, held to one or more right-hand sides."""

sense: ConstraintSense | None = None
limits: list[ConstraintLimit] = []
members: list[ConstraintMember] = []
# PLEXOS Include in LT Plan: whether the expansion plan has to meet the constraint as
# well as the dispatch.
applies_to_expansion_plan: bool | None = None


class NetworkExtension(ExtensionRecord):
"""The model file's own attributes. PyPSA only: neither Sienna nor PLEXOS has these."""

Expand All @@ -220,6 +279,7 @@ class Extensions(BaseModel):
controllable_line: list[ControllableLineExtension] = []
storage: list[StorageExtension] = []
reserve: list[ReserveExtension] = []
constraint: list[ConstraintExtension] = []
network: list[NetworkExtension] = []


Expand All @@ -231,6 +291,7 @@ class Extensions(BaseModel):
ExtensionKind.CONTROLLABLE_LINE: ControllableLineExtension,
ExtensionKind.STORAGE: StorageExtension,
ExtensionKind.RESERVE: ReserveExtension,
ExtensionKind.CONSTRAINT: ConstraintExtension,
ExtensionKind.NETWORK: NetworkExtension,
}

Expand Down Expand Up @@ -411,6 +472,10 @@ def record_for(
staged: StagedExtensions, kind: Literal[ExtensionKind.RESERVE], name: str
) -> ReserveExtension | None: ...
@overload
def record_for(
staged: StagedExtensions, kind: Literal[ExtensionKind.CONSTRAINT], name: str
) -> ConstraintExtension | None: ...
@overload
def record_for(
staged: StagedExtensions, kind: Literal[ExtensionKind.NETWORK], name: str
) -> NetworkExtension | None: ...
Expand Down Expand Up @@ -502,6 +567,10 @@ def read(self, kind: Literal[ExtensionKind.STORAGE]) -> ExtensionLookup[StorageE
@overload
def read(self, kind: Literal[ExtensionKind.RESERVE]) -> ExtensionLookup[ReserveExtension]: ...
@overload
def read(
self, kind: Literal[ExtensionKind.CONSTRAINT]
) -> ExtensionLookup[ConstraintExtension]: ...
@overload
def read(self, kind: Literal[ExtensionKind.NETWORK]) -> ExtensionLookup[NetworkExtension]: ...

def read(self, kind: ExtensionKind) -> ExtensionLookup[Any]:
Expand Down
Loading
Loading