diff --git a/.github/workflows/main-tests.yml b/.github/workflows/main-tests.yml
index 0fdaa6aa15..b34d2876f3 100644
--- a/.github/workflows/main-tests.yml
+++ b/.github/workflows/main-tests.yml
@@ -31,7 +31,7 @@ jobs:
env:
PYTHON: ""
- uses: julia-actions/julia-processcoverage@v1
- - uses: codecov/codecov-action@v1
+ - uses: codecov/codecov-action@v4
with:
file: ./lcov.info
flags: unittests
diff --git a/.github/workflows/performance_comparison.yml b/.github/workflows/performance_comparison.yml
index a946c87002..f51c97a444 100644
--- a/.github/workflows/performance_comparison.yml
+++ b/.github/workflows/performance_comparison.yml
@@ -33,6 +33,14 @@ jobs:
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo "::set-output name=body::$body"
+ - name: Read solve results
+ id: solve_results
+ run: |
+ body="$(cat solve_time.txt)"
+ body="${body//'%'/'%25'}"
+ body="${body//$'\n'/'%0A'}"
+ body="${body//$'\r'/'%0D'}"
+ echo "::set-output name=body::$body"
- name: Find Comment
uses: peter-evans/find-comment@v1
id: fc
@@ -54,6 +62,10 @@ jobs:
| Version | Build Time |
| :--- | :----: |
${{ steps.build_results.outputs.body }}
+
+ | Version | Solve Time |
+ | :--- | :----: |
+ ${{ steps.solve_results.outputs.body }}
- name: Update comment
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v1
@@ -69,4 +81,8 @@ jobs:
| :--- | :----: |
${{ steps.build_results.outputs.body }}
+ | Version | Build Time |
+ | :--- | :----: |
+ ${{ steps.solve_results.outputs.body }}
+
edit-mode: replace
diff --git a/.github/workflows/pr_testing.yml b/.github/workflows/pr_testing.yml
index 2debe86fc0..ee99371f1d 100644
--- a/.github/workflows/pr_testing.yml
+++ b/.github/workflows/pr_testing.yml
@@ -27,7 +27,7 @@ jobs:
env:
PYTHON: ""
- uses: julia-actions/julia-processcoverage@v1
- - uses: codecov/codecov-action@v1
+ - uses: codecov/codecov-action@v4
with:
file: ./lcov.info
flags: unittests
diff --git a/Project.toml b/Project.toml
index 27ece061e4..485e92f041 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
name = "PowerSimulations"
uuid = "e690365d-45e2-57bb-ac84-44ba829e73c4"
authors = ["Jose Daniel Lara", "Clayton Barrows", "Daniel Thom", "Dheepak Krishnamurthy", "Sourabh Dalvi"]
-version = "0.27.4"
+version = "0.28.3"
[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
@@ -25,7 +25,6 @@ PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
-SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
@@ -38,7 +37,7 @@ Dates = "1"
Distributed = "1"
DocStringExtensions = "~v0.9"
HDF5 = "~0.17"
-InfrastructureSystems = "^1.21"
+InfrastructureSystems = "2"
InteractiveUtils = "1"
JSON = "0.21"
JSON3 = "1"
@@ -47,12 +46,11 @@ LinearAlgebra = "1"
Logging = "1"
MathOptInterface = "1"
PowerModels = "^0.21"
-PowerNetworkMatrices = "^0.10"
+PowerNetworkMatrices = "^0.11"
PowerFlows = "0.7"
-PowerSystems = "^3"
+PowerSystems = "4"
PrettyTables = "2"
ProgressMeter = "^1.5"
-SHA = "0.7"
Serialization = "1"
TimeSeries = "~0.23, ~0.24"
TimerOutputs = "~0.5"
diff --git a/README.md b/README.md
index 4572e85b45..5d53a36cb5 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[![Documentation](https://github.com/NREL-Sienna/PowerSimulations.jl/workflows/Documentation/badge.svg)](https://nrel-sienna.github.io/PowerSimulations.jl/latest)
[![DOI](https://zenodo.org/badge/109443246.svg)](https://zenodo.org/badge/latestdoi/109443246)
[](https://join.slack.com/t/nrel-sienna/shared_invite/zt-glam9vdu-o8A9TwZTZqqNTKHa7q3BpQ)
-[![PowerSimulations Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/PowerSimulations)](https://pkgs.genieframework.com?packages=PowerSimulations)
+[![PowerSimulations.jl Downloads](https://img.shields.io/badge/dynamic/json?url=http%3A%2F%2Fjuliapkgstats.com%2Fapi%2Fv1%2Ftotal_downloads%2FPowerSimulations&query=total_requests&label=Downloads)](http://juliapkgstats.com/pkg/PowerSimulations)
`PowerSimulations.jl` is a Julia package for power system modeling and simulation of Power Systems operations. The objectives of the package are:
diff --git a/docs/Project.toml b/docs/Project.toml
index fab2364752..920e4c969c 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -17,5 +17,6 @@ TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
[compat]
Documenter = "0.27"
-InfrastructureSystems = "1"
+InfrastructureSystems = "2"
julia = "^1.6"
+Latexify = "=0.16.3"
diff --git a/docs/make.jl b/docs/make.jl
index dda574be59..ce9640a385 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -15,9 +15,9 @@ pages = OrderedDict(
"modeler_guide/psi_structure.md",
"modeler_guide/problem_templates.md",
"modeler_guide/running_a_simulation.md",
+ "modeler_guide/read_results.md",
"modeler_guide/simulation_recorder.md",
"modeler_guide/logging.md",
- "modeler_guide/tips_and_tricks.md",
"modeler_guide/debugging_infeasible_models.md",
"modeler_guide/parallel_simulations.md",
"modeler_guide/modeling_faq.md",
@@ -31,12 +31,16 @@ pages = OrderedDict(
"Troubleshooting" => "code_base_developer_guide/troubleshooting.md",
],
"Formulation Library" => Any[
+ "Introduction" => "formulation_library/Introduction.md",
"General" => "formulation_library/General.md",
+ "Network" => "formulation_library/Network.md",
"Thermal Generation" => "formulation_library/ThermalGen.md",
"Renewable Generation" => "formulation_library/RenewableGen.md",
"Load" => "formulation_library/Load.md",
- "Network" => "formulation_library/Network.md",
"Branch" => "formulation_library/Branch.md",
+ "Services" => "formulation_library/Service.md",
+ "Feedforwards" => "formulation_library/Feedforward.md",
+ "Piecewise Linear Cost" => "formulation_library/Piecewise.md",
],
"API Reference" => "api/PowerSimulations.md",
)
diff --git a/docs/src/api/PowerSimulations.md b/docs/src/api/PowerSimulations.md
index c038a7d6c1..be88337f83 100644
--- a/docs/src/api/PowerSimulations.md
+++ b/docs/src/api/PowerSimulations.md
@@ -7,18 +7,42 @@ end
# API Reference
-### Table of Contents
+## Table of Contents
+
+* [Device Models](#Device-Models)
+ * [Formulations](#Formulations)
+ * [Problem Templates](#Problem-Templates)
+* [Decision Models](#Decision-Models)
+* [Emulation Models](#Emulation-Models)
+* [Service Models](#Service-Models)
+* [Simulation Models](#Simulation-Models)
+* [Variables](#Variables)
+ * [Common Variables](#Common-Variables)
+ * [Thermal Unit Variables](#Thermal-Unit-Variables)
+ * [Storage Unit Variables](#Storage-Unit-Variables)
+ * [Branches and Network Variables](#Branches-and-Network-Variables)
+ * [Services Variables](#Services-Variables)
+ * [Feedforward Variables](#Feedforward-Variables)
+* [Constraints](#Constraints)
+ * [Common Constraints](#Common-Constraints)
+ * [Network Constraints](#Network-Constraints)
+ * [Power Variable Limit Constraints](#Power-Variable-Limit-Constraints)
+ * [Services Constraints](#Services-Constraints)
+ * [Thermal Unit Constraints](#Thermal-Unit-Constraints)
+ * [Renewable Unit Constraints](#Renewable-Unit-Constraints)
+ * [Branches Constraints](#Branches-Constraints)
+ * [Feedforward Constraints](#Feedforward-Constraints)
+* [Parameters](#Parameters)
+ * [Time Series Parameters](#Time-Series-Parameters)
+ * [Variable Value Parameters](#Variable-Value-Parameters)
+ * [Objective Function Parameters](#Objective-Function-Parameters)
-1. [Device Models](#device-models)
-2. [Decision Models](#decision-models)
-3. [Emulation Models](#emulation-models)
-4. [Service Models](#service-models)
-5. [Simulation Models](#simulation-models)
-6. [Variables](#variables)
-7. [Constraints](#constraints)
-8. [Parameters](#parameters)
+```@raw html
+
+
+```
-# Device Models
+## Device Models
List of structures and methods for Device models
@@ -34,24 +58,15 @@ Refer to the [Formulations Page](@ref formulation_library) for each Abstract Dev
Refer to the [Problem Templates Page](@ref op_problem_template) for available `ProblemTemplate`s.
-### Problem Templates
-
-Refer to the [Problem Templates Page](https://nrel-siip.github.io/PowerSimulations.jl/latest/modeler_guide/problem_templates/) for available `ProblemTemplate`s.
```@raw html
```
-# Service Models
-
-List of structures and methods for Service models
-
-```@docs
-ServiceModel
-```
+---
-# Decision Models
+## Decision Models
```@docs
DecisionModel
@@ -66,7 +81,9 @@ solve!(::DecisionModel)
```
-# Emulation Models
+---
+
+## Emulation Models
```@docs
EmulationModel
@@ -81,7 +98,24 @@ run!(::EmulationModel)
```
-# Simulation Models
+---
+
+## Service Models
+
+List of structures and methods for Service models
+
+```@docs
+ServiceModel
+```
+
+```@raw html
+
+
+```
+
+---
+
+## Simulation Models
Refer to the [Simulations Page](@ref running_a_simulation) to explanations on how to setup a Simulation, with Sequencing and Feedforwards.
@@ -99,6 +133,8 @@ execute!(::Simulation)
```
+---
+
# Variables
For a list of variables for each device refer to its Formulations page.
@@ -122,6 +158,7 @@ HotStartVariable
WarmStartVariable
ColdStartVariable
PowerAboveMinimumVariable
+PowerOutput
```
### Storage Unit Variables
@@ -134,6 +171,8 @@ ReservationVariable
```@docs
FlowActivePowerVariable
+FlowActivePowerSlackUpperBound
+FlowActivePowerSlackLowerBound
FlowActivePowerFromToVariable
FlowActivePowerToFromVariable
FlowReactivePowerFromToVariable
@@ -145,21 +184,23 @@ VoltageMagnitude
VoltageAngle
```
-### Regulation and Services Variables
+### Services Variables
```@docs
ActivePowerReserveVariable
ServiceRequirementVariable
-DeltaActivePowerUpVariable
-DeltaActivePowerDownVariable
-AdditionalDeltaActivePowerUpVariable
-AdditionalDeltaActivePowerDownVariable
-AreaMismatchVariable
-SteadyStateFrequencyDeviation
-SmoothACE
SystemBalanceSlackUp
SystemBalanceSlackDown
ReserveRequirementSlack
+InterfaceFlowSlackUp
+InterfaceFlowSlackDown
+```
+
+### Feedforward Variables
+
+```@docs
+UpperBoundFeedForwardSlack
+LowerBoundFeedForwardSlack
```
```@raw html
@@ -167,7 +208,9 @@ ReserveRequirementSlack
```
-# Constraints
+---
+
+## Constraints
### Common Constraints
@@ -179,11 +222,7 @@ PieceWiseLinearCostConstraint
### Network Constraints
```@docs
-AreaDispatchBalanceConstraint
-AreaParticipationAssignmentConstraint
-BalanceAuxConstraint
CopperPlateBalanceConstraint
-FrequencyResponseConstraint
NodalBalanceActiveConstraint
NodalBalanceReactiveConstraint
```
@@ -198,13 +237,11 @@ InputActivePowerVariableLimitsConstraint
OutputActivePowerVariableLimitsConstraint
```
-### Regulation and Services Constraints
+### Services Constraints
```@docs
-ParticipationAssignmentConstraint
-RegulationLimitsConstraint
RequirementConstraint
-ReserveEnergyCoverageConstraint
+ParticipationFractionConstraint
ReservePowerConstraint
```
@@ -215,7 +252,6 @@ ActiveRangeICConstraint
CommitmentConstraint
DurationConstraint
RampConstraint
-RampLimitConstraint
StartupInitialConditionConstraint
StartupTimeLimitTemperatureConstraint
```
@@ -224,41 +260,40 @@ StartupTimeLimitTemperatureConstraint
```@docs
EqualityConstraint
-
```
### Branches Constraints
```@docs
-AbsoluteValueConstraint
-FlowLimitFromToConstraint
-FlowLimitToFromConstraint
+FlowLimitConstraint
FlowRateConstraint
FlowRateConstraintFromTo
FlowRateConstraintToFrom
-HVDCDirection
HVDCLossesAbsoluteValue
HVDCPowerBalance
NetworkFlowConstraint
RateLimitConstraint
-RateLimitConstraintFromTo
-RateLimitConstraintToFrom
PhaseAngleControlLimit
```
### Feedforward Constraints
```@docs
-FeedforwardSemiContinousConstraint
-FeedforwardIntegralLimitConstraint
+FeedforwardSemiContinuousConstraint
FeedforwardUpperBoundConstraint
FeedforwardLowerBoundConstraint
-FeedforwardEnergyTargetConstraint
```
-# Parameters
+```@raw html
+
+
+```
+
+---
+
+## Parameters
-## Time Series Parameters
+### Time Series Parameters
```@docs
ActivePowerTimeSeriesParameter
@@ -266,15 +301,13 @@ ReactivePowerTimeSeriesParameter
RequirementTimeSeriesParameter
```
-## Variable Value Parameters
+### Variable Value Parameters
```@docs
UpperBoundValueParameter
LowerBoundValueParameter
OnStatusParameter
-EnergyLimitParameter
FixValueParameter
-EnergyTargetParameter
```
### Objective Function Parameters
diff --git a/docs/src/formulation_library/Branch.md b/docs/src/formulation_library/Branch.md
index f55d6c37e7..81c1dd6b89 100644
--- a/docs/src/formulation_library/Branch.md
+++ b/docs/src/formulation_library/Branch.md
@@ -1,67 +1,356 @@
# `PowerSystems.Branch` Formulations
+!!! note
+ The use of reactive power variables and constraints will depend on the network model used, i.e., whether it uses (or does not use) reactive power. If the network model is purely active power-based, reactive power variables and related constraints are not created.
-Valid `DeviceModel`s for subtypes of `Branch` include the following:
-
-```@eval
-using PowerSimulations
-using PowerSystems
-using DataFrames
-using Latexify
-combos = PowerSimulations.generate_device_formulation_combinations()
-filter!(x -> x["device_type"] <: Branch, combos)
-combo_table = DataFrame(
- "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
- "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
- "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
- )
-mdtable(combo_table, latex = false)
-```
+### Table of contents
----
+1. [`StaticBranch`](#StaticBranch)
+2. [`StaticBranchBounds`](#StaticBranchBounds)
+3. [`StaticBranchUnbounded`](#StaticBranchUnbounded)
+4. [`HVDCTwoTerminalUnbounded`](#HVDCTwoTerminalUnbounded)
+5. [`HVDCTwoTerminalLossless`](#HVDCTwoTerminalLossless)
+6. [`HVDCTwoTerminalDispatch`](#HVDCTwoTerminalDispatch)
+7. [`PhaseAngleControl`](#PhaseAngleControl)
+8. [Valid configurations](#Valid-configurations)
## `StaticBranch`
+Formulation valid for `PTDFPowerModel` Network model
+
```@docs
StaticBranch
```
+**Variables:**
+
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``(-\infty,\infty)``
+ - Symbol: ``f``
+If Slack variables are enabled:
+- [`FlowActivePowerSlackUpperBound`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: 2e5
+ - Symbol: ``f^\text{sl,up}``
+- [`FlowActivePowerSlackLowerBound`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: 2e5
+ - Symbol: ``f^\text{sl,lo}``
+
+**Static Parameters**
+
+- ``R^\text{max}`` = `PowerSystems.get_rating(branch)`
+
+**Objective:**
+
+Add a large proportional cost to the objective function if rate constraint slack variables are used ``+ (f^\text{sl,up} + f^\text{sl,lo}) \cdot 2 \cdot 10^5``
+
+**Expressions:**
+
+No expressions are used.
+
+**Constraints:**
+
+For each branch ``b \in \{1,\dots, B\}`` (in a system with ``N`` buses) the constraints are given by:
+
+```math
+\begin{aligned}
+& f_t = \sum_{i=1}^N \text{PTDF}_{i,b} \cdot \text{Bal}_{i,t}, \quad \forall t \in \{1,\dots, T\}\\
+& f_t - f_t^\text{sl,up} \le R^\text{max},\quad \forall t \in \{1,\dots, T\} \\
+& f_t + f_t^\text{sl,lo} \ge -R^\text{max},\quad \forall t \in \{1,\dots, T\}
+\end{aligned}
+```
+on which ``\text{PTDF}`` is the ``N \times B`` system Power Transfer Distribution Factors (PTDF) matrix, and ``\text{Bal}_{i,t}`` is the active power bus balance expression (i.e. ``\text{Generation}_{i,t} - \text{Demand}_{i,t}``) at bus ``i`` at time-step ``t``.
+
---
## `StaticBranchBounds`
+Formulation valid for `PTDFPowerModel` Network model
+
```@docs
StaticBranchBounds
```
+**Variables:**
+
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``\left[-R^\text{max},R^\text{max}\right]``
+ - Symbol: ``f``
+
+**Static Parameters**
+
+- ``R^\text{max}`` = `PowerSystems.get_rating(branch)`
+
+**Objective:**
+
+No cost is added to the objective function.
+
+**Expressions:**
+
+No expressions are used.
+
+**Constraints:**
+
+For each branch ``b \in \{1,\dots, B\}`` (in a system with ``N`` buses) the constraints are given by:
+
+```math
+\begin{aligned}
+& f_t = \sum_{i=1}^N \text{PTDF}_{i,b} \cdot \text{Bal}_{i,t}, \quad \forall t \in \{1,\dots, T\}
+\end{aligned}
+```
+on which ``\text{PTDF}`` is the ``N \times B`` system Power Transfer Distribution Factors (PTDF) matrix, and ``\text{Bal}_{i,t}`` is the active power bus balance expression (i.e. ``\text{Generation}_{i,t} - \text{Demand}_{i,t}``) at bus ``i`` at time-step ``t``.
+
---
## `StaticBranchUnbounded`
+Formulation valid for `PTDFPowerModel` Network model
+
```@docs
StaticBranchUnbounded
```
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``(-\infty,\infty)``
+ - Symbol: ``f``
+
+
+**Objective:**
+
+No cost is added to the objective function.
+
+**Expressions:**
+
+No expressions are used.
+
+**Constraints:**
+
+For each branch ``b \in \{1,\dots, B\}`` (in a system with ``N`` buses) the constraints are given by:
+
+```math
+\begin{aligned}
+& f_t = \sum_{i=1}^N \text{PTDF}_{i,b} \cdot \text{Bal}_{i,t}, \quad \forall t \in \{1,\dots, T\}
+\end{aligned}
+```
+on which ``\text{PTDF}`` is the ``N \times B`` system Power Transfer Distribution Factors (PTDF) matrix, and ``\text{Bal}_{i,t}`` is the active power bus balance expression (i.e. ``\text{Generation}_{i,t} - \text{Demand}_{i,t}``) at bus ``i`` at time-step ``t``.
+
+---
+
+## `HVDCTwoTerminalUnbounded`
+
+Formulation valid for `PTDFPowerModel` Network model
+
+```@docs
+HVDCTwoTerminalUnbounded
+```
+
+This model assumes that it can transfer power from two AC buses without losses and no limits.
+
+**Variables:**
+
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``\left(-\infty,\infty\right)``
+ - Symbol: ``f``
+
+
+**Objective:**
+
+No cost is added to the objective function.
+
+**Expressions:**
+
+The variable `FlowActivePowerVariable` ``f`` is added to the nodal balance expression `ActivePowerBalance`, by adding the flow ``f`` in the receiving bus and subtracting it from the sending bus. This is used then to compute the AC flows using the PTDF equation.
+
+**Constraints:**
+
+No constraints are added.
+
+
---
## `HVDCTwoTerminalLossless`
+Formulation valid for `PTDFPowerModel` Network model
+
```@docs
HVDCTwoTerminalLossless
```
+This model assumes that it can transfer power from two AC buses without losses.
+
+**Variables:**
+
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``\left(-\infty,\infty\right)``
+ - Symbol: ``f``
+
+
+**Static Parameters**
+
+- ``R^\text{from,min}`` = `PowerSystems.get_active_power_limits_from(branch).min`
+- ``R^\text{from,max}`` = `PowerSystems.get_active_power_limits_from(branch).max`
+- ``R^\text{to,min}`` = `PowerSystems.get_active_power_limits_to(branch).min`
+- ``R^\text{to,max}`` = `PowerSystems.get_active_power_limits_to(branch).max`
+
+**Objective:**
+
+No cost is added to the objective function.
+
+**Expressions:**
+
+The variable `FlowActivePowerVariable` ``f`` is added to the nodal balance expression `ActivePowerBalance`, by adding the flow ``f`` in the receiving bus and subtracting it from the sending bus. This is used then to compute the AC flows using the PTDF equation.
+
+**Constraints:**
+
+```math
+\begin{align*}
+& R^\text{min} \le f_t \le R^\text{max},\quad \forall t \in \{1,\dots, T\} \\
+\end{align*}
+```
+where:
+```math
+\begin{align*}
+& R^\text{min} = \begin{cases}
+ \min\left(R^\text{from,min}, R^\text{to,min}\right), & \text{if } R^\text{from,min} \ge 0 \text{ and } R^\text{to,min} \ge 0 \\
+ \max\left(R^\text{from,min}, R^\text{to,min}\right), & \text{if } R^\text{from,min} \le 0 \text{ and } R^\text{to,min} \le 0 \\
+ R^\text{from,min},& \text{if } R^\text{from,min} \le 0 \text{ and } R^\text{to,min} \ge 0 \\
+ R^\text{to,min},& \text{if } R^\text{from,min} \ge 0 \text{ and } R^\text{to,min} \le 0
+ \end{cases}
+\end{align*}
+```
+and
+```math
+\begin{align*}
+& R^\text{max} = \begin{cases}
+ \min\left(R^\text{from,max}, R^\text{to,max}\right), & \text{if } R^\text{from,max} \ge 0 \text{ and } R^\text{to,max} \ge 0 \\
+ \max\left(R^\text{from,max}, R^\text{to,max}\right), & \text{if } R^\text{from,max} \le 0 \text{ and } R^\text{to,max} \le 0 \\
+ R^\text{from,max},& \text{if } R^\text{from,max} \le 0 \text{ and } R^\text{to,max} \ge 0 \\
+ R^\text{to,max},& \text{if } R^\text{from,max} \ge 0 \text{ and } R^\text{to,max} \le 0
+ \end{cases}
+\end{align*}
+```
+
---
-## `HVDCTwoTerminalDispatch`
+
+## `HVDCTwoTerminalDispatch`
+
+Formulation valid for `PTDFPowerModel` Network model
```@docs
HVDCTwoTerminalDispatch
```
+**Variables**
+
+- [`FlowActivePowerToFromVariable`](@ref):
+ - Symbol: ``f^\text{to-from}``
+- [`FlowActivePowerFromToVariable`](@ref):
+ - Symbol: ``f^\text{from-to}``
+- [`HVDCLosses`](@ref):
+ - Symbol: ``\ell``
+- [`HVDCFlowDirectionVariable`](@ref)
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u^\text{dir}``
+
+**Static Parameters**
+
+- ``R^\text{from,min}`` = `PowerSystems.get_active_power_limits_from(branch).min`
+- ``R^\text{from,max}`` = `PowerSystems.get_active_power_limits_from(branch).max`
+- ``R^\text{to,min}`` = `PowerSystems.get_active_power_limits_to(branch).min`
+- ``R^\text{to,max}`` = `PowerSystems.get_active_power_limits_to(branch).max`
+- ``L_0`` = `PowerSystems.get_loss(branch).l0`
+- ``L_1`` = `PowerSystems.get_loss(branch).l1`
+
+**Objective:**
+
+No cost is added to the objective function.
+
+**Expressions:**
+
+Each `FlowActivePowerToFromVariable` ``f^\text{to-from}`` and `FlowActivePowerFromToVariable` ``f^\text{from-to}`` is added to the nodal balance expression `ActivePowerBalance`, by adding the respective flow in the receiving bus and subtracting it from the sending bus. That is, ``f^\text{to-from}`` adds the flow to the `from` bus, and subtracts the flow from the `to` bus, while ``f^\text{from-to}`` adds the flow to the `to` bus, and subtracts the flow from the `from` bus This is used then to compute the AC flows using the PTDF equation.
+
+In addition, the `HVDCLosses` are subtracted to the `from` bus in the `ActivePowerBalance` expression.
+
+**Constraints:**
+
+```math
+\begin{align*}
+& R^\text{from,min} \le f_t^\text{from-to} \le R^\text{from,max}, \forall t \in \{1,\dots, T\} \\
+& R^\text{to,min} \le f_t^\text{to-from} \le R^\text{to,max},\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{to-from} - f_t^\text{from-to} \le L_1 \cdot f_t^\text{to-from} - L_0,\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{from-to} - f_t^\text{to-from} \ge L_1 \cdot f_t^\text{from-to} + L_0,\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{from-to} - f_t^\text{to-from} \ge - M^\text{big} (1 - u^\text{dir}_t),\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{to-from} - f_t^\text{from-to} \ge - M^\text{big} u^\text{dir}_t,\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{to-from} - f_t^\text{from-to} \le \ell_t,\quad \forall t \in \{1,\dots, T\} \\
+& f_t^\text{from-to} - f_t^\text{to-from} \le \ell_t,\quad \forall t \in \{1,\dots, T\}
+\end{align*}
+```
+
---
-## `HVDCTwoTerminalUnbounded`
+## `PhaseAngleControl`
+
+Formulation valid for `PTDFPowerModel` Network model
```@docs
-HVDCTwoTerminalUnbounded
+PhaseAngleControl
```
+
+**Variables:**
+
+- [`FlowActivePowerVariable`](@ref):
+ - Bounds: ``(-\infty,\infty)``
+ - Symbol: ``f``
+- [`PhaseShifterAngle`](@ref):
+ - Symbol: ``\theta^\text{shift}``
+
+**Static Parameters**
+
+- ``R^\text{max}`` = `PowerSystems.get_rating(branch)`
+- ``\Theta^\text{min}`` = `PowerSystems.get_phase_angle_limits(branch).min`
+- ``\Theta^\text{max}`` = `PowerSystems.get_phase_angle_limits(branch).max`
+- ``X`` = `PowerSystems.get_x(branch)` (series reactance)
+
+**Objective:**
+
+No changes to objective function
+
+**Expressions:**
+
+Adds to the `ActivePowerBalance` expression the term ``-\theta^\text{shift} /X`` to the `from` bus and ``+\theta^\text{shift} /X`` to the `to` bus, that the `PhaseShiftingTransformer` is connected.
+
+**Constraints:**
+
+For each branch ``b \in \{1,\dots, B\}`` (in a system with ``N`` buses) the constraints are given by:
+
+```math
+\begin{aligned}
+& f_t = \sum_{i=1}^N \text{PTDF}_{i,b} \cdot \text{Bal}_{i,t} + \frac{\theta^\text{shift}_t}{X}, \quad \forall t \in \{1,\dots, T\}\\
+& -R^\text{max} \le f_t \le R^\text{max},\quad \forall t \in \{1,\dots, T\}
+\end{aligned}
+```
+on which ``\text{PTDF}`` is the ``N \times B`` system Power Transfer Distribution Factors (PTDF) matrix, and ``\text{Bal}_{i,t}`` is the active power bus balance expression (i.e. ``\text{Generation}_{i,t} - \text{Demand}_{i,t}``) at bus ``i`` at time-step ``t``.
+
+
+---
+
+## Valid configurations
+
+Valid `DeviceModel`s for subtypes of `Branch` include the following:
+
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.generate_device_formulation_combinations()
+filter!(x -> (x["device_type"] <: Branch) && (x["device_type"] != TModelHVDCLine), combos)
+combo_table = DataFrame(
+ "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
+ "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
+ "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
+ )
+mdtable(combo_table, latex = false)
+```
\ No newline at end of file
diff --git a/docs/src/formulation_library/Feedforward.md b/docs/src/formulation_library/Feedforward.md
new file mode 100644
index 0000000000..bdda721f36
--- /dev/null
+++ b/docs/src/formulation_library/Feedforward.md
@@ -0,0 +1,161 @@
+# [FeedForward Formulations](@id ff_formulations)
+
+**FeedForwards** are the mechanism to define how information is shared between models. Specifically, a FeedForward defines what to do with information passed with an inter-stage chronology in a Simulation. The most common FeedForward is the `SemiContinuousFeedForward` that affects the semi-continuous range constraints of thermal generators in the economic dispatch problems based on the value of the (already solved) unit-commitment variables.
+
+The creation of a FeedForward requires at least specifying the `component_type` on which the FeedForward will be applied. The `source` variable specifies which variable will be taken from the problem solved, for example, the commitment variable of the thermal unit in the unit commitment problem. Finally, the `affected_values` specify which variables will be affected in the problem to be solved, for example, the next economic dispatch problem.
+
+### Table of contents
+
+1. [`SemiContinuousFeedforward`](#SemiContinuousFeedForward)
+2. [`FixValueFeedforward`](#FixValueFeedforward)
+3. [`UpperBoundFeedforward`](#UpperBoundFeedforward)
+4. [`LowerBoundFeedforward`](#LowerBoundFeedforward)
+
+---
+
+## `SemiContinuousFeedforward`
+
+```@docs
+SemiContinuousFeedforward
+```
+
+**Variables:**
+
+No variables are created
+
+**Parameters:**
+
+- ``\text{on}^\text{th}`` = `OnStatusParameter` obtained from the source variable, typically the commitment variable of the unit commitment problem ``u^\text{th}``.
+
+**Objective:**
+
+No changes to the objective function.
+
+**Expressions:**
+
+Adds ``-\text{on}^\text{th}P^\text{th,max}`` to the `ActivePowerRangeExpressionUB` expression and ``-\text{on}^\text{th}P^\text{th,min}`` to the `ActivePowerRangeExpressionLB` expression.
+
+**Constraints:**
+
+Limits the `ActivePowerRangeExpressionUB` and `ActivePowerRangeExpressionLB` by zero as:
+
+```math
+\begin{align*}
+& \text{ActivePowerRangeExpressionUB}_t := p_t^\text{th} - \text{on}_t^\text{th}P^\text{th,max} \le 0, \quad \forall t\in \{1, \dots, T\} \\
+& \text{ActivePowerRangeExpressionLB}_t := p_t^\text{th} - \text{on}_t^\text{th}P^\text{th,min} \ge 0, \quad \forall t\in \{1, \dots, T\}
+\end{align*}
+```
+
+Thus, if the commitment parameter is zero, the dispatch is limited to zero, forcing to turn off the generator without introducing binary variables in the economic dispatch problem.
+
+---
+
+## `FixValueFeedforward`
+
+```@docs
+FixValueFeedforward
+```
+
+**Variables:**
+
+No variables are created
+
+**Parameters:**
+
+The parameter `FixValueParameter` is used to match the result obtained from the source variable (from the simulation state).
+
+**Objective:**
+
+No changes to the objective function.
+
+**Expressions:**
+
+No changes on expressions.
+
+**Constraints:**
+
+Set the `VariableType` from the `affected_values` to be equal to the source parameter store in `FixValueParameter`
+
+```math
+\begin{align*}
+& \text{AffectedVariable}_t = \text{SourceVariableParameter}_t, \quad \forall t \in \{1,\dots, T\}
+\end{align*}
+```
+
+---
+
+## `UpperBoundFeedforward`
+
+```@docs
+UpperBoundFeedforward
+```
+
+**Variables:**
+
+If slack variables are enabled:
+- [`UpperBoundFeedForwardSlack`](@ref)
+ - Bounds: [0.0, ]
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{ff,ubsl}``
+
+
+**Parameters:**
+
+The parameter `UpperBoundValueParameter` stores the result obtained from the source variable (from the simulation state) that will be used as an upper bound to the affected variable.
+
+**Objective:**
+
+The slack variable is added to the objective function using its large default cost ``+ p^\text{ff,ubsl} \cdot 10^6``
+
+**Expressions:**
+
+No changes on expressions.
+
+**Constraints:**
+
+Set the `VariableType` from the `affected_values` to be lower than the source parameter store in `UpperBoundValueParameter`.
+
+```math
+\begin{align*}
+& \text{AffectedVariable}_t - p_t^\text{ff,ubsl} \le \text{SourceVariableParameter}_t, \quad \forall t \in \{1,\dots, T\}
+\end{align*}
+```
+
+---
+
+## `LowerBoundFeedforward`
+
+```@docs
+LowerBoundFeedforward
+```
+
+**Variables:**
+
+If slack variables are enabled:
+- [`LowerBoundFeedForwardSlack`](@ref)
+ - Bounds: [0.0, ]
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{ff,lbsl}``
+
+
+**Parameters:**
+
+The parameter `LowerBoundValueParameter` stores the result obtained from the source variable (from the simulation state) that will be used as a lower bound to the affected variable.
+
+**Objective:**
+
+The slack variable is added to the objective function using its large default cost ``+ p^\text{ff,lbsl} \cdot 10^6``
+
+**Expressions:**
+
+No changes on expressions.
+
+**Constraints:**
+
+Set the `VariableType` from the `affected_values` to be greater than the source parameter store in `LowerBoundValueParameter`.
+
+```math
+\begin{align*}
+& \text{AffectedVariable}_t + p_t^\text{ff,lbsl} \ge \text{SourceVariableParameter}_t, \quad \forall t \in \{1,\dots, T\}
+\end{align*}
+```
\ No newline at end of file
diff --git a/docs/src/formulation_library/General.md b/docs/src/formulation_library/General.md
index 1f7f20891f..ceb5e1b9da 100644
--- a/docs/src/formulation_library/General.md
+++ b/docs/src/formulation_library/General.md
@@ -15,11 +15,11 @@ No variables are created for `DeviceModel(<:DeviceType, FixedOutput)`
**Static Parameters:**
- ThermalGen:
- - ``Pg^\text{max}`` = `PowerSystems.get_max_active_power(device)`
- - ``Qg^\text{max}`` = `PowerSystems.get_max_reactive_power(device)`
+ - ``P^\text{th,max}`` = `PowerSystems.get_max_active_power(device)`
+ - ``Q^\text{th,max}`` = `PowerSystems.get_max_reactive_power(device)`
- Storage:
- - ``Pg^\text{max}`` = `PowerSystems.get_max_active_power(device)`
- - ``Qg^\text{max}`` = `PowerSystems.get_max_reactive_power(device)`
+ - ``P^\text{st,max}`` = `PowerSystems.get_max_active_power(device)`
+ - ``Q^\text{st,max}`` = `PowerSystems.get_max_reactive_power(device)`
**Time Series Parameters:**
@@ -48,7 +48,7 @@ No objective terms are created for `DeviceModel(<:DeviceType, FixedOutput)`
**Expressions:**
-Adds the active and reactive parameters listed for specific device types above to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+Adds the active and reactive parameters listed for specific device types above to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations).
**Constraints:**
@@ -89,9 +89,9 @@ where
- For `PolynomialFunctionData`:
- ``C_n`` = `get_coefficients(variable_cost)[n]`
-### `PiecewiseLinearPointData` and `PiecewiseLinearSlopeData`
+### `` and `PiecewiseLinearSlopeData`
-`variable_cost::PiecewiseLinearPointData` and `variable_cost::PiecewiseLinearSlopeData`: create a piecewise linear cost term in the objective function
+`variable_cost::PiecewiseLinearData` and `variable_cost::PiecewiseLinearSlopeData`: create a piecewise linear cost term in the objective function
```math
\begin{aligned}
@@ -101,12 +101,12 @@ where
where
-- For `variable_cost::PiecewiseLinearPointData`, ``f(x)`` is the piecewise linear function obtained by connecting the `(x, y)` points `get_points(variable_cost)` in order.
+- For `variable_cost::PiecewiseLinearData`, ``f(x)`` is the piecewise linear function obtained by connecting the `(x, y)` points `get_points(variable_cost)` in order.
- For `variable_cost = PiecewiseLinearSlopeData([x0, x1, x2, ...], y0, [s0, s1, s2, ...])`, ``f(x)`` is the piecewise linear function obtained by starting at `(x0, y0)`, drawing a segment at slope `s0` to `x=x1`, drawing a segment at slope `s1` to `x=x2`, etc.
-___
+---
-### `StorageManagementCost`
+## `StorageCost`
Adds an objective function cost term according to:
@@ -118,7 +118,7 @@ Adds an objective function cost term according to:
**Impact of different cost configurations:**
-The following table describes all possible configuration of the `StorageManagementCost` with the target constraint in hydro or storage device models. Cases 1(a) & 2(a) will have no impact of the models operations and the target constraint will be rendered useless. In most cases that have no energy target and a non-zero value for ``C^{value}``, if this cost is too high (``C^{value} >> 0``) or too low (``C^{value} <<0``) can result in either the model holding on to stored energy till the end or the model not storing any energy in the device. This is caused by the fact that when energy target is zero, we have ``E_t = - E^{shortage}_t``, and ``- E^{shortage}_t * C^{value}`` in the objective function is replaced by ``E_t * C^{value}``, thus resulting in ``C^{value}`` to be seen as the cost of stored energy.
+The following table describes all possible configurations of the `StorageCost` with the target constraint in hydro or storage device models. Cases 1(a) & 2(a) will not impact the model's operations, and the target constraint will be rendered useless. In most cases that have no energy target and a non-zero value for ``C^{value}``, if this cost is too high (``C^{value} >> 0``) or too low (``C^{value} <<0``) can result in either the model holding on to stored energy till the end of the model not storing any energy in the device. This is caused by the fact that when the energy target is zero, we have ``E_t = - E^{shortage}_t``, and ``- E^{shortage}_t * C^{value}`` in the objective function is replaced by ``E_t * C^{value}``, thus resulting in ``C^{value}`` to be seen as the cost of stored energy.
| Case | Energy Target | Energy Shortage Cost | Energy Value / Energy Surplus cost | Effect |
diff --git a/docs/src/formulation_library/Introduction.md b/docs/src/formulation_library/Introduction.md
new file mode 100644
index 0000000000..47a8425d4e
--- /dev/null
+++ b/docs/src/formulation_library/Introduction.md
@@ -0,0 +1,67 @@
+# [Formulations Introduction](@id formulation_intro)
+
+PowerSimulations.jl enables modularity in its formulations by assigning a `DeviceModel` to each `PowerSystems.jl` component type existing in a defined system.
+
+`PowerSimulations.jl` has a multiple `AbstractDeviceFormulation` subtypes that can be applied to different `PowerSystems.jl` device types, each dispatching to different methods for populating the optimization problem **variables**, **objective function**, **expressions** and **constraints**.
+
+## Example Formulation
+
+For example a typical optimization problem in a `DecisionModel` in `PowerSimulations.jl` with three `DeviceModel` has the abstract form of:
+
+```math
+\begin{align*}
+ &\min_{\boldsymbol{x}}~ \text{Objective\_DeviceModelA} + \text{Objective\_DeviceModelB} + \text{Objective\_DeviceModelC} \\
+ & ~~\text{s.t.} \\
+ & \hspace{0.9cm} \text{Constraints\_NetworkModel} \\
+ & \hspace{0.9cm} \text{Constraints\_DeviceModelA} \\
+ & \hspace{0.9cm} \text{Constraints\_DeviceModelB} \\
+ & \hspace{0.9cm} \text{Constraints\_DeviceModelC}
+\end{align*}
+```
+
+Suppose this is a system with the following characteristics:
+- Horizon: 48 hours
+- Interval: 24 hours
+- Resolution: 1 hour
+- Three Buses: 1, 2 and 3
+- One `ThermalStandard` (device A) unit at bus 1
+- One `RenewableDispatch` (device B) unit at bus 2
+- One `PowerLoad` (device C) at bus 3
+- Three `Line` that connects all the buses
+
+Now, we assign the following `DeviceModel` to each `PowerSystems.jl` with:
+
+| Type | Formulation |
+| ----------- | ----------- |
+| Network | `CopperPlatePowerModel` |
+| `ThermalStandard` | `ThermalDispatchNoMin` |
+| `RenewableDispatch` | `RenewableFullDispatch` |
+| `PowerLoad` | `StaticPowerLoad` |
+
+Note that we did not assign any `DeviceModel` to `Line` since the `CopperPlatePowerModel` used for the network assumes that everything is lumped in the same node (like a copper plate with infinite capacity), and hence there are no flows between buses that branches can limit.
+
+Each `DeviceModel` formulation is described in specific in their respective page, but the overall optimization problem will end-up as:
+
+```math
+\begin{align*}
+ &\min_{\boldsymbol{p}^\text{th}, \boldsymbol{p}^\text{re}}~ \sum_{t=1}^{48} C^\text{th} p_t^\text{th} - C^\text{re} p_t^\text{re} \\
+ & ~~\text{s.t.} \\
+ & \hspace{0.9cm} p_t^\text{th} + p_t^\text{re} = P_t^\text{load}, \quad \forall t \in {1,\dots, 48} \\
+ & \hspace{0.9cm} 0 \le p_t^\text{th} \le P^\text{th,max} \\
+ & \hspace{0.9cm} 0 \le p_t^\text{re} \le \text{ActivePowerTimeSeriesParameter}_t
+\end{align*}
+```
+
+Note that the `StaticPowerLoad` does not impose any cost to the objective function or constraint but adds its power demand to the supply-balance demand of the `CopperPlatePowerModel` used. Since we are using the `ThermalDispatchNoMin` formulation for the thermal generation, the lower bound for the power is 0, instead of ``P^\text{th,min}``. In addition, we are assuming a linear cost ``C^\text{th}``. Finally, the `RenewableFullDispatch` formulation allows the dispatch of the renewable unit between 0 and its maximum injection time series ``p_t^\text{re,param}``.
+
+# Nomenclature
+
+In the formulations described in the other pages, the nomenclature is as follows:
+- Lowercase letters are used for variables, e.g., ``p`` for power.
+- Uppercase letters are used for parameters, e.g., ``C`` for costs.
+- Subscripts are used for indexing, e.g., ``(\cdot)_t`` for indexing at time ``t``.
+- Superscripts are used for descriptions, e.g., ``(\cdot)^\text{th}`` to describe a thermal (th) variable/parameter.
+- Bold letters are used for vectors, e.g., ``\boldsymbol{p} = \{p\}_{1,\dots,24}``.
+
+
+
diff --git a/docs/src/formulation_library/Load.md b/docs/src/formulation_library/Load.md
index c3bcbabb3c..dcb7b9b8d0 100644
--- a/docs/src/formulation_library/Load.md
+++ b/docs/src/formulation_library/Load.md
@@ -1,21 +1,16 @@
# `PowerSystems.ElectricLoad` Formulations
-Valid `DeviceModel`s for subtypes of `ElectricLoad` include the following:
+Electric load formulations define the optimization models that describe load units (demand) mathematical model in different operational settings, such as economic dispatch and unit commitment.
-```@eval
-using PowerSimulations
-using PowerSystems
-using DataFrames
-using Latexify
-combos = PowerSimulations.generate_device_formulation_combinations()
-filter!(x -> x["device_type"] <: ElectricLoad, combos)
-combo_table = DataFrame(
- "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
- "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
- "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
- )
-mdtable(combo_table, latex = false)
-```
+!!! note
+ The use of reactive power variables and constraints will depend on the network model used, i.e., whether it uses (or does not use) reactive power. If the network model is purely active power-based, reactive power variables and related constraints are not created.
+
+### Table of contents
+
+1. [`StaticPowerLoad`](#StaticPowerLoad)
+2. [`PowerLoadInterruption`](#PowerLoadInterruption)
+3. [`PowerLoadDispatch`](#PowerLoadDispatch)
+4. [Valid configurations](#Valid-configurations)
---
@@ -31,6 +26,8 @@ No variables are created
**Time Series Parameters:**
+Uses the `max_active_power` timeseries parameter to determine the demand value at each time-step
+
```@eval
using PowerSimulations
using PowerSystems
@@ -46,7 +43,7 @@ mdtable(combo_table, latex = false)
**Expressions:**
-Subtracts the parameters listed above from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+Subtracts the parameters listed above from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations).
**Constraints:**
@@ -65,12 +62,19 @@ PowerLoadInterruption
- [`ActivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: 0.0
+ - Symbol: ``p^\text{ld}``
- [`ReactivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: 0.0
+ - Symbol: ``q^\text{ld}``
- [`OnVariable`](@ref):
- - Bounds: {0,1}
+ - Bounds: ``\{0,1\}``
- Default initial value: 1
+ - Symbol: ``u^\text{ld}``
+
+**Static Parameters:**
+- ``P^\text{ld,max}`` = `PowerSystems.get_max_active_power(device)`
+- ``Q^\text{ld,max}`` = `PowerSystems.get_max_reactive_power(device)`
**Time Series Parameters:**
@@ -89,25 +93,22 @@ mdtable(combo_table, latex = false)
**Objective:**
-Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``Pg``.
+Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\text{ld}``.
+
**Expressions:**
-- Adds ``Pg`` and ``Qg`` terms and to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
-- Subtracts the time series parameters listed above terms from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+- Subtract``p^\text{ld}`` and ``q^\text{ld}`` terms and to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
**Constraints:**
-``Pg`` and ``Qg`` represent the "unserved" active and reactive power loads
-
```math
\begin{aligned}
-& Pg_t \le ActivePowerTimeSeriesParameter_t\\
-& Pg_t - u_t ActivePowerTimeSeriesParameter_t \le 0 \\
-& Qg_t \le ReactivePowerTimeSeriesParameter_t\\
-& Qg_t - u_t ReactivePowerTimeSeriesParameter_t\le 0
+& p_t^\text{ld} \le u_t^\text{ld} \cdot \text{ActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots, T\} \\
+& q_t^\text{re} = \text{pf} \cdot p_t^\text{re}, \quad \forall t \in \{1,\dots, T\}
\end{aligned}
```
+on which ``\text{pf} = \sin(\arctan(Q^\text{ld,max}/P^\text{ld,max}))``.
---
@@ -122,9 +123,15 @@ PowerLoadDispatch
- [`ActivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: `PowerSystems.get_active_power(device)`
+ - Symbol: ``p^\text{ld}``
- [`ReactivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: `PowerSystems.get_reactive_power(device)`
+ - Symbol: ``q^\text{ld}``
+
+**Static Parameters:**
+- ``P^\text{ld,max}`` = `PowerSystems.get_max_active_power(device)`
+- ``Q^\text{ld,max}`` = `PowerSystems.get_max_reactive_power(device)`
**Time Series Parameters:**
@@ -143,20 +150,38 @@ mdtable(combo_table, latex = false)
**Objective:**
-Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``Pg``.
+Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``p^\text{ld}``.
+
**Expressions:**
-- Adds ``Pg`` and ``Qg`` terms and to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
-- Subtracts the time series parameters listed above terms from the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+- Subtract``p^\text{ld}`` and ``q^\text{ld}`` terms and to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
**Constraints:**
-``Pg`` and ``Qg`` represent the "unserved" active and reactive power loads
-
```math
\begin{aligned}
-& Pg_t \le ActivePowerTimeSeriesParameter_t\\
-& Qg_t \le ReactivePowerTimeSeriesParameter_t\\
+& p_t^\text{ld} \le \text{ActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots, T\}\\
+& q_t^\text{ld} = \text{pf} \cdot p_t^\text{ld}, \quad \forall t \in \{1,\dots, T\}\\
\end{aligned}
```
+on which ``\text{pf} = \sin(\arctan(Q^\text{ld,max}/P^\text{ld,max}))``.
+
+## Valid configurations
+
+Valid `DeviceModel`s for subtypes of `ElectricLoad` include the following:
+
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.generate_device_formulation_combinations()
+filter!(x -> x["device_type"] <: ElectricLoad, combos)
+combo_table = DataFrame(
+ "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
+ "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
+ "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
+ )
+mdtable(combo_table, latex = false)
+```
diff --git a/docs/src/formulation_library/Network.md b/docs/src/formulation_library/Network.md
index e5f5e742e4..9fad1c2d27 100644
--- a/docs/src/formulation_library/Network.md
+++ b/docs/src/formulation_library/Network.md
@@ -1,3 +1,144 @@
# [Network Formulations](@id network_formulations)
-TODO
+Network formulations are used to describe how the network and buses are handled when constructing constraints. The most common constraint decided by the network formulation is the supply-demand balance constraint. Available Network Models are:
+
+| Formulation | Description |
+| ----- | ---- |
+| `CopperPlatePowerModel` | Copper plate connection between all components, i.e. infinite transmission capacity |
+| `AreaBalancePowerModel` | Network model approximation to represent inter-area flow with each area represented as a single node |
+| `PTDFPowerModel` | Uses the PTDF factor matrix to compute the fraction of power transferred in the network across the branches |
+| `AreaPTDFPowerModel` | Uses the PTDF factor matrix to compute the fraction of power transferred in the network across the branches and balances power by Area instead of system-wide |
+
+[`PowerModels.jl`](https://github.com/lanl-ansi/PowerModels.jl) available formulations:
+
+- Exact non-convex models: `ACPPowerModel`, `ACRPowerModel`, `ACTPowerModel`.
+- Linear approximations: `DCPPowerModel`, `NFAPowerModel`.
+- Quadratic approximations: `DCPLLPowerModel`, `LPACCPowerModel`
+- Quadratic relaxations: `SOCWRPowerModel`, `SOCWRConicPowerModel`, `SOCBFPowerModel`, `SOCBFConicPowerModel`, `QCRMPowerModel`, `QCLSPowerModel`.
+- SDP relaxations: `SDPWRMPowerModel`, `SparseSDPWRMPowerModel`.
+
+All of these formulations are described in the [PowerModels.jl documentation](https://lanl-ansi.github.io/PowerModels.jl/stable/formulation-details/) and will not be described here.
+
+---
+
+## `CopperPlatePowerModel`
+
+```@docs
+CopperPlatePowerModel
+```
+
+**Variables:**
+
+If Slack variables are enabled:
+
+- [`SystemBalanceSlackUp`](@ref):
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,up}``
+- [`SystemBalanceSlackDown`](@ref):
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,dn}``
+
+**Objective:**
+
+Add a large proportional cost to the objective function if slack variables are used ``+ (p^\text{sl,up} + p^\text{sl,dn}) \cdot 10^6``
+
+**Expressions:**
+
+Adds ``p^\text{sl,up}`` and ``p^\text{sl,dn}`` terms to the respective active power balance expressions `ActivePowerBalance` created by this `CopperPlatePowerModel` network formulation.
+
+**Constraints:**
+
+Adds the `CopperPlateBalanceConstraint` to balance the active power of all components available in the system
+
+```math
+\begin{align}
+& \sum_{c \in \text{components}} p_t^c = 0, \quad \forall t \in \{1, \dots, T\}
+\end{align}
+```
+
+---
+
+## `AreaBalancePowerModel`
+
+```@docs
+AreaBalancePowerModel
+```
+
+**Variables:**
+If Slack variables are enabled:
+
+- [`SystemBalanceSlackUp`](@ref) by area:
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,up}``
+- [`SystemBalanceSlackDown`](@ref) by area:
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,dn}``
+
+**Objective:**
+
+Adds ``p^\text{sl,up}`` and ``p^\text{sl,dn}`` terms to the respective active power balance expressions `ActivePowerBalance` per area.
+
+**Expressions:**
+
+Creates `ActivePowerBalance` expressions for each area that then are used to balance active power for all buses within a single area.
+
+**Constraints:**
+
+Adds the `CopperPlateBalanceConstraint` to balance the active power of all components available in an area.
+
+```math
+\begin{align}
+& \sum_{c \in \text{components}_a} p_t^c = 0, \quad \forall a\in \{1,\dots, A\}, t \in \{1, \dots, T\}
+\end{align}
+```
+
+---
+
+## `PTDFPowerModel`
+
+```@docs
+PTDFPowerModel
+```
+
+**Variables:**
+
+If Slack variables are enabled:
+
+- [`SystemBalanceSlackUp`](@ref):
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,up}``
+- [`SystemBalanceSlackDown`](@ref):
+ - Bounds: [0.0, ]
+ - Default initial value: 0.0
+ - Default proportional cost: 1e6
+ - Symbol: ``p^\text{sl,dn}``
+
+**Objective:**
+
+Add a large proportional cost to the objective function if slack variables are used ``+ (p^\text{sl,up} + p^\text{sl,dn}) \cdot 10^6``
+
+**Expressions:**
+
+Adds ``p^\text{sl,up}`` and ``p^\text{sl,dn}`` terms to the respective system-wide active power balance expressions `ActivePowerBalance` created by this `CopperPlatePowerModel` network formulation. In addition, it creates `ActivePowerBalance` expressions for each bus to be used in the calculation of branch flows.
+
+**Constraints:**
+
+Adds the `CopperPlateBalanceConstraint` to balance the active power of all components available in the system
+
+```math
+\begin{align}
+& \sum_{c \in \text{components}} p_t^c = 0, \quad \forall t \in \{1, \dots, T\}
+\end{align}
+```
+
+In addition creates `NodalBalanceActiveConstraint` for HVDC buses balance, if DC components are connected to an HVDC network.
diff --git a/docs/src/formulation_library/Piecewise.md b/docs/src/formulation_library/Piecewise.md
new file mode 100644
index 0000000000..2167769162
--- /dev/null
+++ b/docs/src/formulation_library/Piecewise.md
@@ -0,0 +1,77 @@
+# [Piecewise linear cost functions](@id pwl_cost)
+
+The choice for piecewise-linear (PWL) cost representation in `PowerSimulations.jl` is equivalent to the so-called λ-model from the paper [_The Impacts of Convex Piecewise Linear Cost Formulations on AC Optimal Power Flow_](https://www.sciencedirect.com/science/article/pii/S0378779621001723). The SOS constraints in each model are only implemented if the data for PWL is not convex.
+
+## Special Ordered Set (SOS) Constraints
+
+A special ordered set (SOS) is an ordered set of variables used as an additional way to specify integrality conditions in an optimization model.
+
+- Special Ordered Sets of type 1 (SOS1) are a set of variables, at most one of which can take a non-zero value, all others being at 0. They most frequently applications is in a a set of variables that are actually binary variables: in other words, we have to choose at most one from a set of possibilities.
+- Special Ordered Sets of type 2 (SOS2) are an ordered set of non-negative variables, of which at most two can be non-zero, and if two are non-zero these must be consecutive in their ordering. Special Ordered Sets of type 2 are typically used to model non-linear functions of a variable in a linear model, such as non-convex quadratic functions using PWL functions.
+
+## Standard representation of PWL costs
+
+Piecewise-linear costs are defined by a sequence of points representing the line segments for each generator: ``(P_k^\text{max}, C_k)`` on which we assume ``C_k`` is the cost of generating ``P_k^\text{max}`` power, and ``k \in \{1,\dots, K\}`` are the number of segments each generator cost function has.
+
+!!! note
+ `PowerSystems` has more options to specify cost functions for each thermal unit. Independent of which form of the cost data is provided, `PowerSimulations.jl` will internally transform the data to use the λ-model formulation. See **TODO: ADD PSY COST DOCS** for more information.
+
+### Commitment formulation
+
+ With this the standard representation of PWL costs for a thermal unit commitment is given by:
+
+```math
+\begin{align*}
+ \min_{\substack{p_{t}, \delta_{k,t}}}
+ & \sum_{t \in \mathcal{T}} \left(\sum_{k \in \mathcal{K}} C_{k,t} \delta_{k,t} \right) \Delta t\\
+ & \sum_{k \in \mathcal{K}} P_{k}^{\text{max}} \delta_{k,t} = p_{t} & \forall t \in \mathcal{T}\\
+ & \sum_{k \in \mathcal{K}} \delta_{k,t} = u_{t} & \forall t \in \mathcal{T}\\
+ & P^{\text{min}} u_{t} \leq p_{t} \leq P^{\text{max}} u_{t} & \forall t \in \mathcal{T}\\
+ &\left \{\delta_{1,t}, \dots, \delta_{K,t} \right \} \in \text{SOS}_{2} & \forall t \in \mathcal{T}
+\end{align*}
+```
+on which ``\delta_{k,t} \in [0,1]`` is the interpolation variable, ``p`` is the active power of the generator and ``u \in \{0,1\}`` is the commitment variable of the generator. In the case of a PWL convex costs, i.e. increasing slopes, the SOS constraint is omitted.
+
+### Dispatch formulation
+
+```math
+\begin{align*}
+ \min_{\substack{p_{t}, \delta_{k,t}}}
+ & \sum_{t \in \mathcal{T}} \left(\sum_{k \in \mathcal{K}} C_{k,t} \delta_{k,t} \right) \Delta t\\
+ & \sum_{k \in \mathcal{K}} P_{k}^{\text{max}} \delta_{k,t} = p_{t} & \forall t \in \mathcal{T}\\
+ & \sum_{k \in \mathcal{K}} \delta_{k,t} = \text{on}_{t} & \forall t \in \mathcal{T}\\
+ & P^{\text{min}} \text{on}_{t} \leq p_{t} \leq P^{\text{max}} \text{on}_{t} & \forall t \in \mathcal{T}\\
+ &\left \{\delta_{i,t}, \dots, \delta_{k,t} \right \} \in \text{SOS}_{2} & \forall t \in \mathcal{T}
+\end{align*}
+```
+on which ``\delta_{k,t} \in [0,1]`` is the interpolation variable, ``p`` is the active power of the generator and ``\text{on} \in \{0,1\}`` is the parameter that decides if the generator is available or not. In the case of a PWL convex costs, i.e. increasing slopes, the SOS constraint is omitted.
+
+## Compact representation of PWL costs
+
+### Commitment Formulation
+
+```math
+\begin{align*}
+ \min_{\substack{p_{t}, \delta_{k,t}}}
+ & \sum_{t \in \mathcal{T}} \left(\sum_{k \in \mathcal{K}} C_{k,t} \delta_{k,t} \right) \Delta t\\
+ & \sum_{k \in \mathcal{K}} P_{k}^{\text{max}} \delta_{k,t} = P^{\text{min}} u_{t} + \Delta p_{t} & \forall t \in \mathcal{T}\\
+ & \sum_{k \in \mathcal{K}} \delta_{k,t} = u_{t} & \forall t \in \mathcal{T}\\
+ & 0 \leq \Delta p_{t} \leq \left( P^{\text{max}} - P^{\text{min}} \right)u_{t} & \forall t \in \mathcal{T}\\
+ &\left \{\delta_{i,t} \dots \delta_{k,t} \right \} \in \text{SOS}_{2} & \forall t \in \mathcal{T}
+\end{align*}
+```
+on which ``\delta_{k,t} \in [0,1]`` is the interpolation variable, ``\Delta p`` is the active power of the generator above the minimum power and ``u \in \{0,1\}`` is the commitment variable of the generator. In the case of a PWL convex costs, i.e. increasing slopes, the SOS constraint is omitted.
+
+### Dispatch formulation
+
+```math
+\begin{align*}
+ \min_{\substack{p_{t}, \delta_{k,t}}}
+ & \sum_{t \in \mathcal{T}} \left(\sum_{k \in \mathcal{K}} C_{k,t} \delta_{k,t} \right) \Delta t\\
+ & \sum_{k \in \mathcal{K}} P_{k}^{\text{max}} \delta_{k,t} = P^{\text{min}} \text{on}_{t} + \Delta p_{t} & \forall t \in \mathcal{T}\\
+ & \sum_{k \in \mathcal{K}} \delta_{k,t} = \text{on}_{t} & \forall t \in \mathcal{T}\\
+ & 0 \leq \Delta p_{t} \leq \left( P^{\text{max}} - P^{\text{min}} \right)\text{on}_{t} & \forall t \in \mathcal{T}\\
+ &\left \{\delta_{i,t} \dots \delta_{k,t} \right \} \in \text{SOS}_{2} & \forall t \in \mathcal{T}
+\end{align*}
+```
+on which ``\delta_{k,t} \in [0,1]`` is the interpolation variable, ``\Delta p`` is the active power of the generator above the minimum power and ``u \in \{0,1\}`` is the commitment variable of the generator. In the case of a PWL convex costs, i.e. increasing slopes, the SOS constraint is omitted.
\ No newline at end of file
diff --git a/docs/src/formulation_library/RenewableGen.md b/docs/src/formulation_library/RenewableGen.md
index dd2de3122d..5dfca92c3e 100644
--- a/docs/src/formulation_library/RenewableGen.md
+++ b/docs/src/formulation_library/RenewableGen.md
@@ -1,21 +1,18 @@
# `PowerSystems.RenewableGen` Formulations
-Valid `DeviceModel`s for subtypes of `RenewableGen` include the following:
+Renewable generation formulations define the optimization models that describe renewable units mathematical model in different operational settings, such as economic dispatch and unit commitment.
-```@eval
-using PowerSimulations
-using PowerSystems
-using DataFrames
-using Latexify
-combos = PowerSimulations.generate_device_formulation_combinations()
-filter!(x -> x["device_type"] <: RenewableGen, combos)
-combo_table = DataFrame(
- "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
- "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
- "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
- )
-mdtable(combo_table, latex = false)
-```
+!!! note
+ The use of reactive power variables and constraints will depend on the network model used, i.e., whether it uses (or does not use) reactive power. If the network model is purely active power-based, reactive power variables and related constraints are not created.
+
+!!! note
+ Reserve variables for services are not included in the formulation, albeit their inclusion change the variables, expressions, constraints and objective functions created. A detailed description of the implications in the optimization models is described in the [Service formulation](@ref service_formulations) section.
+
+### Table of contents
+
+1. [`RenewableFullDispatch`](#RenewableFullDispatch)
+2. [`RenewableConstantPowerFactor`](#RenewableConstantPowerFactor)
+3. [Valid configurations](#Valid-configurations)
---
@@ -29,19 +26,21 @@ RenewableFullDispatch
- [`ActivePowerVariable`](@ref):
- Bounds: [0.0, ]
- - Default initial value: `PowerSystems.get_active_power(device)`
+ - Symbol: ``p^\text{re}``
- [`ReactivePowerVariable`](@ref):
- Bounds: [0.0, ]
- - Default initial value: `PowerSystems.get_reactive_power(device)`
+ - Symbol: ``q^\text{re}``
**Static Parameters:**
-- ``Pg^\text{min}`` = `PowerSystems.get_active_power_limits(device).min`
-- ``Qg^\text{min}`` = `PowerSystems.get_reactive_power_limits(device).min`
-- ``Qg^\text{max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``P^\text{re,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``Q^\text{re,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{re,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
**Time Series Parameters:**
+Uses the `max_active_power` timeseries parameter to limit the available active power at each time-step.
+
```@eval
using PowerSimulations
using PowerSystems
@@ -57,18 +56,19 @@ mdtable(combo_table, latex = false)
**Objective:**
-Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices.
+Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- p^\text{re}`` to incentivize generation from `RenewableGen` devices.
+
**Expressions:**
-Adds ``Pg`` and ``Qg`` terms to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+Adds ``p^\text{re}`` and ``q^\text{re}`` terms to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations).
**Constraints:**
```math
\begin{aligned}
-& Pg^\text{min} \le Pg_t \le ActivePowerTimeSeriesParameter_t \\
-& Qg^\text{min} \le Qg_t \le Qg^\text{max}
+& P^\text{re,min} \le p_t^\text{re} \le \text{ActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots, T\} \\
+& Q^\text{re,min} \le q_t^\text{re} \le Q^\text{re,max}, \quad \forall t \in \{1,\dots, T\}
\end{aligned}
```
@@ -85,16 +85,18 @@ RenewableConstantPowerFactor
- [`ActivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: `PowerSystems.get_active_power(device)`
+ - Symbol: ``p^\text{re}``
- [`ReactivePowerVariable`](@ref):
- Bounds: [0.0, ]
- Default initial value: `PowerSystems.get_reactive_power(device)`
+ - Symbol: ``q^\text{re}``
**Static Parameters:**
-- ``Pg^\text{min}`` = `PowerSystems.get_active_power_limits(device).min`
-- ``Qg^\text{min}`` = `PowerSystems.get_reactive_power_limits(device).min`
-- ``Qg^\text{max}`` = `PowerSystems.get_reactive_power_limits(device).max`
-- ``pf`` = `PowerSystems.get_power_factor(device)`
+- ``P^\text{re,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``Q^\text{re,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{re,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``\text{pf}`` = `PowerSystems.get_power_factor(device)`
**Time Series Parameters:**
@@ -113,18 +115,39 @@ mdtable(combo_table, latex = false)
**Objective:**
-Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices.
+Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- p_t^\text{re}`` to incentivize generation from `RenewableGen` devices.
**Expressions:**
-Adds ``Pg`` and ``Qg`` terms to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
+Adds ``p^\text{re}`` and ``q^\text{re}`` terms to the respective active and reactive power balance expressions created by the selected [Network Formulations](@ref network_formulations)
**Constraints:**
```math
\begin{aligned}
-& Pg^\text{min} \le Pg_t \le ActivePowerTimeSeriesParameter_t \\
-& Qg^\text{min} \le Qg_t \le Qg^\text{max} \\
-& Qg_t = pf * Pg_t
+& P^\text{re,min} \le p_t^\text{re} \le \text{ActivePowerTimeSeriesParameter}_t, \quad \forall t \in \{1,\dots, T\} \\
+& q_t^\text{re} = \text{pf} \cdot p_t^\text{re}, \quad \forall t \in \{1,\dots, T\}
\end{aligned}
```
+
+---
+
+## Valid configurations
+
+Valid `DeviceModel`s for subtypes of `RenewableGen` include the following:
+
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.generate_device_formulation_combinations()
+filter!(x -> x["device_type"] <: RenewableGen, combos)
+combo_table = DataFrame(
+ "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
+ "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
+ "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
+ )
+mdtable(combo_table, latex = false)
+```
+
diff --git a/docs/src/formulation_library/Service.md b/docs/src/formulation_library/Service.md
index f4331eba63..d43cd334d6 100644
--- a/docs/src/formulation_library/Service.md
+++ b/docs/src/formulation_library/Service.md
@@ -1,3 +1,495 @@
-# `PowerSystems.Service` Formulations
+# [`PowerSystems.Service` Formulations](@id service_formulations)
-TODO
+`Services` (or ancillary services) are models used to ensure that there is necessary support to the power grid from generators to consumers, in order to ensure reliable operation of the system.
+
+The most common application for ancillary services are reserves, i.e., generation (or load) that is not currently being used, but can be quickly made available in case of unexpected changes of grid conditions, for example a sudden loss of load or generation.
+
+A key challenge of adding services to a system, from a mathematical perspective, is specifying which units contribute to the specified requirement of a service, that implies the creation of new variables (such as reserve variables) and modification of constraints.
+
+In this documentation, we first specify the available `Services` in the grid, and what requirements impose in the system, and later we discuss the implication on device formulations for specific units.
+
+### Table of contents
+
+1. [`RangeReserve`](#RangeReserve)
+2. [`StepwiseCostReserve`](#StepwiseCostReserve)
+3. [`GroupReserve`](#GroupReserve)
+4. [`RampReserve`](#RampReserve)
+5. [`NonSpinningReserve`](#NonSpinningReserve)
+6. [`ConstantMaxInterfaceFlow`](#ConstantMaxInterfaceFlow)
+7. [Changes on Expressions](#Changes-on-Expressions-due-to-Service-models)
+
+---
+
+## `RangeReserve`
+
+```@docs
+RangeReserve
+```
+
+For each service ``s`` of the model type `RangeReserve` the following variables are created:
+
+**Variables**:
+
+- [`ActivePowerReserveVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: ``1.0 / \text{SystemBasePower}``
+ - Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``
+If slacks are enabled:
+- [`ReserveRequirementSlack`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: 1e5
+ - Symbol: ``r^\text{sl}``
+
+Depending on the `PowerSystems.jl` type associated to the `RangeReserve` formulation model, the parameters are:
+
+**Static Parameters**
+
+- ``\text{PF}`` = `PowerSystems.get_max_participation_factor(service)`
+
+For a `ConstantReserve` `PowerSystems` type:
+- ``\text{Req}`` = `PowerSystems.get_requirement(service)`
+
+**Time Series Parameters**
+
+For a `VariableReserve` `PowerSystems` type:
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.get_default_time_series_names(VariableReserve, RangeReserve)
+combo_table = DataFrame(
+ "Parameter" => map(x -> "[`$x`](@ref)", collect(keys(combos))),
+ "Default Time Series Name" => map(x -> "`$x`", collect(values(combos))),
+ )
+mdtable(combo_table, latex = false)
+```
+
+**Relevant Methods:**
+
+- ``\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.
+
+**Objective:**
+
+Add a large proportional cost to the objective function if slack variables are used ``+ r^\text{sl} \cdot 10^5``. In addition adds the default cost for `ActivePowerReserveVariables` as a proportional cost.
+
+**Expressions:**
+
+Adds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.
+
+For `ReserveUp` types, the variable is added to `ActivePowerRangeExpressionUB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable. Similarly, For `ReserveDown` types, the variable is added to `ActivePowerRangeExpressionLB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable
+
+
+*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):
+```math
+\text{ActivePowerRangeExpressionUB}_{t} = p_t^\text{th} + r_{s_1,t} + r_{s_2, t} \le P^\text{th,max}
+```
+similarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):
+```math
+\text{ActivePowerRangeExpressionLB}_{t} = p_t^\text{th} - r_{s_3,t} \ge P^\text{th,min}
+```
+
+**Constraints:**
+
+A RangeReserve implements two fundamental constraints. The first is that the sum of all reserves of contributing devices must be larger than the `RangeReserve` requirement. Thus, for a service ``s``:
+
+```math
+\sum_{d\in\mathcal{D}_s} r_{d,t} + r_t^\text{sl} \ge \text{Req},\quad \forall t\in \{1,\dots, T\} \quad \text{(for a ConstantReserve)} \\
+\sum_{d\in\mathcal{D}_s} r_{d,t} + r_t^\text{sl} \ge \text{RequirementTimeSeriesParameter}_{t},\quad \forall t\in \{1,\dots, T\} \quad \text{(for a VariableReserve)}
+```
+
+In addition, there is a restriction on how much each contributing device ``d`` can contribute to the requirement, based on the max participation factor allowed.
+
+```math
+r_{d,t} \le \text{Req} \cdot \text{PF} ,\quad \forall d\in \mathcal{D}_s, \forall t\in \{1,\dots, T\} \quad \text{(for a ConstantReserve)} \\
+r_{d,t} \le \text{RequirementTimeSeriesParameter}_{t} \cdot \text{PF}\quad \forall d\in \mathcal{D}_s, \forall t\in \{1,\dots, T\}, \quad \text{(for a VariableReserve)}
+```
+
+---
+
+## `StepwiseCostReserve`
+
+Service must be used with `ReserveDemandCurve` `PowerSystems.jl` type. This service model is used to model ORDC (Operating Reserve Demand Curve) in ERCOT.
+
+```@docs
+StepwiseCostReserve
+```
+
+For each service ``s`` of the model type `ReserveDemandCurve` the following variables are created:
+
+**Variables**:
+
+- [`ActivePowerReserveVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``
+- [`ServiceRequirementVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``\text{req}``
+
+**Time Series Parameters**
+
+For a `ReserveDemandCurve` `PowerSystems` type:
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.get_default_time_series_names(ReserveDemandCurve, StepwiseCostReserve)
+combo_table = DataFrame(
+ "Parameter" => map(x -> "[`$x`](@ref)", collect(keys(combos))),
+ "Default Time Series Name" => map(x -> "`$x`", collect(values(combos))),
+ )
+mdtable(combo_table, latex = false)
+```
+
+**Relevant Methods:**
+
+- ``\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.
+
+**Objective:**
+
+The `ServiceRequirementVariable` is added as a piecewise linear cost based on the decreasing offers listed in the `variable_cost` time series. These decreasing cost represent the scarcity prices of not having sufficient reserves. For example, if the variable ``\text{req} = 0``, then a really high cost is paid for not having enough reserves, and if ``\text{req}`` is larger, then a lower cost (or even zero) is paid.
+
+**Expressions:**
+
+Adds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.
+
+For `ReserveUp` types, the variable is added to `ActivePowerRangeExpressionUB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable. Similarly, For `ReserveDown` types, the variable is added to `ActivePowerRangeExpressionLB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable
+
+
+*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):
+```math
+\text{ActivePowerRangeExpressionUB}_{t} = p_t^\text{th} + r_{s_1,t} + r_{s_2, t} \le P^\text{th,max}
+```
+similarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):
+```math
+\text{ActivePowerRangeExpressionLB}_{t} = p_t^\text{th} - r_{s_3,t} \ge P^\text{th,min}
+```
+
+**Constraints:**
+
+A `StepwiseCostReserve` implements a single constraint, such that the sum of all reserves of contributing devices must be larger than the `ServiceRequirementVariable` variable. Thus, for a service ``s``:
+
+```math
+\sum_{d\in\mathcal{D}_s} r_{d,t} \ge \text{req}_t,\quad \forall t\in \{1,\dots, T\}
+```
+
+## `GroupReserve`
+
+Service must be used with `ConstantReserveGroup` `PowerSystems.jl` type. This service model is used to model an aggregation of services.
+
+```@docs
+GroupReserve
+```
+
+For each service ``s`` of the model type `GroupReserve` the following variables are created:
+
+**Variables**:
+
+No variables are created, but the services associated with the `GroupReserve` must have created variables.
+
+**Static Parameters**
+
+- ``\text{Req}`` = `PowerSystems.get_requirement(service)`
+
+**Relevant Methods:**
+
+- ``\mathcal{S}_s`` = `PowerSystems.get_contributing_services(system, service)`: Set (vector) of all contributing services to the group service ``s`` in the system.
+- ``\mathcal{D}_{s_i}`` = `PowerSystems.get_contributing_devices(system, service_aux)`: Set (vector) of all contributing devices to the service ``s_i`` in the system.
+
+**Objective:**
+
+Does not modify the objective function, besides the changes to the objective function due to the other services associated to the group service.
+
+**Expressions:**
+
+No changes, besides the changes to the expressions due to the other services associated to the group service.
+
+**Constraints:**
+
+A GroupReserve implements that the sum of all reserves of contributing devices, of all contributing services, must be larger than the `GroupReserve` requirement. Thus, for a `GroupReserve` service ``s``:
+
+```math
+\sum_{d\in\mathcal{D}_{s_i}} \sum_{i \in \mathcal{S}_s} r_{d,t} \ge \text{Req},\quad \forall t\in \{1,\dots, T\}
+```
+
+---
+
+## `RampReserve`
+
+```@docs
+RampReserve
+```
+
+For each service ``s`` of the model type `RampReserve` the following variables are created:
+
+**Variables**:
+
+- [`ActivePowerReserveVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: ``1.0 / \text{SystemBasePower}``
+ - Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``
+If slacks are enabled:
+- [`ReserveRequirementSlack`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: 1e5
+ - Symbol: ``r^\text{sl}``
+
+`RampReserve` only accepts `VariableReserve` `PowerSystems.jl` type. With that, the parameters are:
+
+**Static Parameters**
+
+- ``\text{TF}`` = `PowerSystems.get_time_frame(service)`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up` for thermal contributing devices
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down` for thermal contributing devices
+
+
+**Time Series Parameters**
+
+For a `VariableReserve` `PowerSystems` type:
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.get_default_time_series_names(VariableReserve, RampReserve)
+combo_table = DataFrame(
+ "Parameter" => map(x -> "[`$x`](@ref)", collect(keys(combos))),
+ "Default Time Series Name" => map(x -> "`$x`", collect(values(combos))),
+ )
+mdtable(combo_table, latex = false)
+```
+
+**Relevant Methods:**
+
+- ``\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.
+
+**Objective:**
+
+Add a large proportional cost to the objective function if slack variables are used ``+ r^\text{sl} \cdot 10^5``. In addition adds the default cost for `ActivePowerReserveVariables` as a proportional cost.
+
+**Expressions:**
+
+Adds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.
+
+For `ReserveUp` types, the variable is added to `ActivePowerRangeExpressionUB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable. Similarly, For `ReserveDown` types, the variable is added to `ActivePowerRangeExpressionLB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable
+
+
+*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):
+```math
+\text{ActivePowerRangeExpressionUB}_{t} = p_t^\text{th} + r_{s_1,t} + r_{s_2, t} \le P^\text{th,max}
+```
+similarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):
+```math
+\text{ActivePowerRangeExpressionLB}_{t} = p_t^\text{th} - r_{s_3,t} \ge P^\text{th,min}
+```
+
+**Constraints:**
+
+A RampReserve implements three fundamental constraints. The first is that the sum of all reserves of contributing devices must be larger than the `RampReserve` requirement. Thus, for a service ``s``:
+
+```math
+\sum_{d\in\mathcal{D}_s} r_{d,t} + r_t^\text{sl} \ge \text{RequirementTimeSeriesParameter}_{t},\quad \forall t\in \{1,\dots, T\}
+```
+
+Finally, there is a restriction based on the ramp limits of the contributing devices:
+
+```math
+r_{d,t} \le R^\text{th,up} \cdot \text{TF}\quad \forall d\in \mathcal{D}_s, \forall t\in \{1,\dots, T\}, \quad \text{(for ReserveUp)} \\
+r_{d,t} \le R^\text{th,dn} \cdot \text{TF}\quad \forall d\in \mathcal{D}_s, \forall t\in \{1,\dots, T\}, \quad \text{(for ReserveDown)}
+```
+
+---
+
+## `NonSpinningReserve`
+
+```@docs
+NonSpinningReserve
+```
+
+For each service ``s`` of the model type `NonSpinningReserve`, the following variables are created:
+
+**Variables**:
+
+- [`ActivePowerReserveVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: ``1.0 / \text{SystemBasePower}``
+ - Symbol: ``r_{d}`` for ``d`` in contributing devices to the service ``s``
+If slacks are enabled:
+- [`ReserveRequirementSlack`](@ref):
+ - Bounds: [0.0, ]
+ - Default proportional cost: 1e5
+ - Symbol: ``r^\text{sl}``
+
+`NonSpinningReserve` only accepts `VariableReserve` `PowerSystems.jl` type. With that, the parameters are:
+
+**Static Parameters**
+
+- ``\text{PF}`` = `PowerSystems.get_max_participation_factor(service)`
+- ``\text{TF}`` = `PowerSystems.get_time_frame(service)`
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min` for thermal contributing devices
+- ``T^\text{st,up}`` = `PowerSystems.get_time_limits(d).up` for thermal contributing devices
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).down` for thermal contributing devices
+
+Other parameters:
+
+- ``\Delta T``: Resolution of the problem in minutes.
+
+**Time Series Parameters**
+
+For a `VariableReserve` `PowerSystems` type:
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.get_default_time_series_names(VariableReserve, NonSpinningReserve)
+combo_table = DataFrame(
+ "Parameter" => map(x -> "[`$x`](@ref)", collect(keys(combos))),
+ "Default Time Series Name" => map(x -> "`$x`", collect(values(combos))),
+ )
+mdtable(combo_table, latex = false)
+```
+
+**Relevant Methods:**
+
+- ``\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing devices to the service ``s`` in the system.
+
+**Objective:**
+
+Add a large proportional cost to the objective function if slack variables are used ``+ r^\text{sl} \cdot 10^5``. In addition adds the default cost for `ActivePowerReserveVariables` as a proportional cost.
+
+**Expressions:**
+
+Adds the `ActivePowerReserveVariable` for upper/lower bound expressions of contributing devices.
+
+For `ReserveUp` types, the variable is added to `ActivePowerRangeExpressionUB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable. Similarly, For `ReserveDown` types, the variable is added to `ActivePowerRangeExpressionLB`, such that this expression considers both the `ActivePowerVariable` and its reserve variable
+
+
+*Example*: for a thermal unit ``d`` contributing to two different `ReserveUp` ``s_1, s_2`` services (e.g. Reg-Up and Spin):
+```math
+\text{ActivePowerRangeExpressionUB}_{t} = p_t^\text{th} + r_{s_1,t} + r_{s_2, t} \le P^\text{th,max}
+```
+similarly if ``s_3`` is a `ReserveDown` service (e.g. Reg-Down):
+```math
+\text{ActivePowerRangeExpressionLB}_{t} = p_t^\text{th} - r_{s_3,t} \ge P^\text{th,min}
+```
+
+**Constraints:**
+
+A NonSpinningReserve implements three fundamental constraints. The first is that the sum of all reserves of contributing devices must be larger than the `NonSpinningReserve` requirement. Thus, for a service ``s``:
+
+```math
+\sum_{d\in\mathcal{D}_s} r_{d,t} + r_t^\text{sl} \ge \text{RequirementTimeSeriesParameter}_{t},\quad \forall t\in \{1,\dots, T\}
+```
+
+In addition, there is a restriction on how much each contributing device ``d`` can contribute to the requirement, based on the max participation factor allowed.
+
+```math
+r_{d,t} \le \text{RequirementTimeSeriesParameter}_{t} \cdot \text{PF}\quad \forall d\in \mathcal{D}_s, \forall t\in \{1,\dots, T\},
+```
+
+Finally, there is a restriction based on the reserve response time for the non-spinning reserve if the unit is off. To do so, compute ``R^\text{limit}_d`` as the reserve response limit as:
+```math
+R^\text{limit}_d = \begin{cases}
+0 & \text{ if TF } \le T^\text{st,up}_d \\
+P^\text{th,min}_d + (\text{TF}_s - T^\text{st,up}_d) \cdot R^\text{th,up}_d \Delta T \cdot R^\text{th,up}_d & \text{ if TF } > T^\text{st,up}_d
+\end{cases}, \quad \forall d\in \mathcal{D}_s
+```
+
+Then, the constraint depends on the commitment variable ``u_t^\text{th}`` as:
+
+```math
+r_{d,t} \le (1 - u_{d,t}^\text{th}) \cdot R^\text{limit}_d, \quad \forall d \in \mathcal{D}_s, \forall t \in \{1,\dots, T\}
+```
+
+---
+
+## `ConstantMaxInterfaceFlow`
+
+This Service model only accepts the `PowerSystems.jl` `TransmissionInterface` type to properly function. It is used to model a collection of branches that make up an interface or corridor with a maximum transfer of power.
+
+```@docs
+ConstantMaxInterfaceFlow
+```
+
+**Variables**
+
+If slacks are used:
+- [`InterfaceFlowSlackUp`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``f^\text{sl,up}``
+- [`InterfaceFlowSlackDown`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``f^\text{sl,dn}``
+
+**Static Parameters**
+
+- ``F^\text{max}`` = `PowerSystems.get_active_power_flow_limits(service).max`
+- ``F^\text{min}`` = `PowerSystems.get_active_power_flow_limits(service).min`
+- ``C^\text{flow}`` = `PowerSystems.get_violation_penalty(service)`
+- ``\mathcal{M}_s`` = `PowerSystems.get_direction_mapping(service)`. Dictionary of contributing branches with its specified direction (``\text{Dir}_d = 1`` or ``\text{Dir}_d = -1``) with respect to the interface.
+
+**Relevant Methods**
+
+- ``\mathcal{D}_s`` = `PowerSystems.get_contributing_devices(system, service)`: Set (vector) of all contributing branches to the service ``s`` in the system.
+
+**Objective:**
+
+Add the violation penalty proportional cost to the objective function if slack variables are used ``+ (f^\text{sl,up} + f^\text{sl,dn}) \cdot C^\text{flow}``.
+
+**Expressions:**
+
+Creates the expression `InterfaceTotalFlow` to keep track of all `FlowActivePowerVariable` of contributing branches to the transmission interface.
+
+**Constraints:**
+
+It adds the constraint to limit the `InterfaceTotalFlow` by the specified bounds of the service ``s``:
+
+```math
+F^\text{min} \le f^\text{sl,up}_t - f^\text{sl,dn}_t + \sum_{d\in\mathcal{D}_s} \text{Dir}_d f_{d,t} \le F^\text{max}, \quad \forall t \in \{1,\dots,T\}
+```
+
+## Changes on Expressions due to Service models
+
+It is important to note that by adding a service to a Optimization Problem, variables for each contributing device must be created. For example, for every contributing generator ``d \in \mathcal{D}`` that is participating in services ``s_1,s_2,s_3``, it is required to create three set of `ActivePowerReserveVariable` variables:
+
+```math
+r_{s_1,d,t},~ r_{s_2,d,t},~ r_{s_3,d,t},\quad \forall d \in \mathcal{D}, \forall t \in \{1,\dots, T\}
+```
+
+### Changes on UpperBound (UB) and LowerBound (LB) limits
+
+Each contributing generator ``d`` has active power limits that the reserve variables affect. In simple terms, the limits are implemented using expressions `ActivePowerRangeExpressionUB` and `ActivePowerRangeExpressionLB` as:
+
+```math
+\text{ActivePowerRangeExpressionUB}_t \le P^\text{max} \\
+\text{ActivePowerRangeExpressionLB}_t \ge P^\text{min}
+```
+`ReserveUp` type variables contribute to the upper bound expression, while `ReserveDown` variables contribute to the lower bound expressions. So if ``s_1,s_2`` are `ReserveUp` services, and ``s_3`` is a `ReserveDown` service, then for a thermal generator ``d`` using a `ThermalStandardDispatch`:
+
+```math
+\begin{align*}
+& p_{d,t}^\text{th} + r_{s_1,d,t} + r_{s_2,d,t} \le P^\text{th,max},\quad \forall d\in \mathcal{D}^\text{th}, \forall t \in \{1,\dots,T\} \\
+& p_{d,t}^\text{th} - r_{s_3,d,t} \ge P^\text{th,min},\quad \forall d\in \mathcal{D}^\text{th}, \forall t \in \{1,\dots,T\}
+\end{align*}
+```
+
+while for a renewable generator ``d`` using a `RenewableFullDispatch`:
+
+```math
+\begin{align*}
+& p_{d,t}^\text{re} + r_{s_1,d,t} + r_{s_2,d,t} \le \text{ActivePowerTimeSeriesParameter}_t,\quad \forall d\in \mathcal{D}^\text{re}, \forall t \in \{1,\dots,T\}\\
+& p_{d,t}^\text{re} - r_{s_3,d,t} \ge 0,\quad \forall d\in \mathcal{D}^\text{re}, \forall t \in \{1,\dots,T\}
+\end{align*}
+```
+
+### Changes in Ramp limits
+
+For the case of Ramp Limits (of formulation that model these limits), the reserve variables only affect the current time, and not the previous time. Then, for the same example as before:
+```math
+\begin{align*}
+& p_{d,t}^\text{th} + r_{s_1,d,t} + r_{s_2,d,t} - p_{d,t-1}^\text{th}\le R^\text{th,up},\quad \forall d\in \mathcal{D}^\text{th}, \forall t \in \{1,\dots,T\}\\
+& p_{d,t}^\text{th} - r_{s_3,d,t} - p_{d,t-1}^\text{th} \ge -R^\text{th,dn},\quad \forall d\in \mathcal{D}^\text{th}, \forall t \in \{1,\dots,T\}
+\end{align*}
+```
diff --git a/docs/src/formulation_library/ThermalGen.md b/docs/src/formulation_library/ThermalGen.md
index d80072ff2b..e179c8c8e1 100644
--- a/docs/src/formulation_library/ThermalGen.md
+++ b/docs/src/formulation_library/ThermalGen.md
@@ -1,21 +1,29 @@
# `ThermalGen` Formulations
-Valid `DeviceModel`s for subtypes of `ThermalGen` include the following:
+Thermal generation formulations define the optimization models that describe thermal units mathematical model in different operational settings, such as economic dispatch and unit commitment.
-```@eval
-using PowerSimulations
-using PowerSystems
-using DataFrames
-using Latexify
-combos = PowerSimulations.generate_device_formulation_combinations()
-filter!(x -> x["device_type"] <: ThermalGen, combos)
-combo_table = DataFrame(
- "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
- "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
- "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
- )
-mdtable(combo_table, latex = false)
-```
+
+!!! note
+ Thermal units can include multiple terms added to the objective function, such as no-load cost, turn-on/off cost, fixed cost and variable cost. In addition, variable costs can be linear, quadratic or piecewise-linear formulations. These methods are properly described in the [cost function page](@ref pwl_cost).
+
+
+!!! note
+ The use of reactive power variables and constraints will depend on the network model used, i.e., whether it uses (or does not use) reactive power. If the network model is purely active power-based, reactive power variables and related constraints are not created.
+
+!!! note
+ Reserve variables for services are not included in the formulation, albeit their inclusion change the variables, expressions, constraints and objective functions created. A detailed description of the implications in the optimization models is described in the [Service formulation](@ref service_formulations) section.
+
+### Table of Contents
+
+1. [`ThermalBasicDispatch`](#ThermalBasicDispatch)
+2. [`ThermalDispatchNoMin`](#ThermalDispatchNoMin)
+3. [`ThermalCompactDispatch`](#ThermalCompactDispatch)
+4. [`ThermalStandardDispatch`](#ThermalStandardDispatch)
+5. [`ThermalBasicUnitCommitment`](#ThermalBasicUnitCommitment)
+6. [`ThermalBasicCompactUnitCommitment`](#ThermalBasicCompactUnitCommitment)
+7. [`ThermalStandardUnitCommitment`](#ThermalStandardUnitCommitment)
+8. [`ThermalMultiStartUnitCommitment`](#ThermalMultiStartUnitCommitment)
+9. [Valid configurations](#Valid-configurations)
---
@@ -24,38 +32,244 @@ mdtable(combo_table, latex = false)
```@docs
ThermalBasicDispatch
```
+**Variables:**
-TODO
+- [`ActivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.
+
+```math
+\begin{align*}
+& P^\text{th,min} \le p^\text{th}_t \le P^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& Q^\text{th,min} \le q^\text{th}_t \le Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\}
+\end{align*}
+```
---
-## `ThermalCompactDispatch`
+
+## `ThermalDispatchNoMin`
```@docs
-ThermalCompactDispatch
+ThermalDispatchNoMin
```
-TODO
+**Variables:**
+
+- [`ActivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.
+
+```math
+\begin{align}
+& 0 \le p^\text{th}_t \le P^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& Q^\text{th,min} \le q^\text{th}_t \le Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\}
+\end{align}
+```
---
-## `ThermalDispatchNoMin`
+## `ThermalCompactDispatch`
```@docs
-ThermalDispatchNoMin
+ThermalCompactDispatch
```
-TODO
+**Variables:**
+
+- [`PowerAboveMinimumVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``\Delta p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+
+**Auxiliary Variables:**
+- [`PowerOutput`](@ref):
+ - Symbol: ``P^\text{th}``
+ - Definition: ``P^\text{th} = \text{on}^\text{th}P^\text{min} + \Delta p^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`
+
+**Variable Value Parameters:**
+
+- ``\text{on}^\text{th}``: Used in feedforwards to define if the unit is on/off at each time-step from another problem. If no feedforward is used, the parameter takes a {0,1} value if the unit is available or not.
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``\text{on}^\text{th}P^\text{th,min} + \Delta p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. It also implements ramp constraints for the active power variable.
+
+```math
+\begin{align*}
+& 0 \le \Delta p^\text{th}_t \le \text{on}^\text{th}_t\left(P^\text{th,max} - P^\text{th,min}\right), \quad \forall t\in \{1, \dots, T\} \\
+& \text{on}^\text{th}_t Q^\text{th,min} \le q^\text{th}_t \le \text{on}^\text{th}_t Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& -R^\text{th,dn} \le \Delta p_1^\text{th} - \Delta p^\text{th, init} \le R^\text{th,up} \\
+& -R^\text{th,dn} \le \Delta p_t^\text{th} - \Delta p_{t-1}^\text{th} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\}
+\end{align*}
+```
---
+
## `ThermalStandardDispatch`
```@docs
ThermalStandardDispatch
```
-TODO
+**Variables:**
+
+- [`ActivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters.
+
+```math
+\begin{align*}
+& P^\text{th,min} \le p^\text{th}_t \le P^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& Q^\text{th,min} \le q^\text{th}_t \le Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& -R^\text{th,dn} \le p_1^\text{th} - p^\text{th, init} \le R^\text{th,up} \\
+& -R^\text{th,dn} \le p_t^\text{th} - p_{t-1}^\text{th} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\}
+\end{align*}
+```
+
+---
+
+## `ThermalBasicUnitCommitment`
+
+```@docs
+ThermalBasicUnitCommitment
+```
+
+**Variables:**
+
+- [`ActivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+- [`OnVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u_t^\text{th}``
+- [`StartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``v_t^\text{th}``
+- [`StopVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``w_t^\text{th}``
+
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. In addition, it creates the commitment constraint to turn on/off the device.
+
+```math
+\begin{align*}
+& u_t^\text{th} P^\text{th,min} \le p^\text{th}_t \le u_t^\text{th} P^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& u_t^\text{th} Q^\text{th,min} \le q^\text{th}_t \le u_t^\text{th} Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& u_1^\text{th} = u^\text{th,init} + v_1^\text{th} - w_1^\text{th} \\
+& u_t^\text{th} = u_{t-1}^\text{th} + v_t^\text{th} - w_t^\text{th}, \quad \forall t \in \{2,\dots,T\} \\
+& v_t^\text{th} + w_t^\text{th} \le 1, \quad \forall t \in \{1,\dots,T\}
+\end{align*}
+```
---
@@ -65,7 +279,60 @@ TODO
ThermalBasicCompactUnitCommitment
```
-TODO
+
+**Variables:**
+
+- [`PowerAboveMinimumVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``\Delta p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+- [`OnVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u_t^\text{th}``
+- [`StartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``v_t^\text{th}``
+- [`StopVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``w_t^\text{th}``
+
+**Auxiliary Variables:**
+- [`PowerOutput`](@ref):
+ - Symbol: ``P^\text{th}``
+ - Definition: ``P^\text{th} = u^\text{th}P^\text{min} + \Delta p^\text{th}``
+
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``u^\text{th}P^\text{th,min} + \Delta p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. In addition, it creates the commitment constraint to turn on/off the device.
+
+```math
+\begin{align*}
+& 0 \le \Delta p^\text{th}_t \le u^\text{th}_t\left(P^\text{th,max} - P^\text{th,min}\right), \quad \forall t\in \{1, \dots, T\} \\
+& u_t^\text{th} Q^\text{th,min} \le q^\text{th}_t \le u_t^\text{th} Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& u_1^\text{th} = u^\text{th,init} + v_1^\text{th} - w_1^\text{th} \\
+& u_t^\text{th} = u_{t-1}^\text{th} + v_t^\text{th} - w_t^\text{th}, \quad \forall t \in \{2,\dots,T\} \\
+& v_t^\text{th} + w_t^\text{th} \le 1, \quad \forall t \in \{1,\dots,T\}
+\end{align*}
+```
---
@@ -75,36 +342,335 @@ TODO
ThermalCompactUnitCommitment
```
-TODO
+**Variables:**
+
+- [`PowerAboveMinimumVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``\Delta p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+- [`OnVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u_t^\text{th}``
+- [`StartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``v_t^\text{th}``
+- [`StopVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``w_t^\text{th}``
+
+**Auxiliary Variables:**
+- [`PowerOutput`](@ref):
+ - Symbol: ``P^\text{th}``
+ - Definition: ``P^\text{th} = u^\text{th}P^\text{min} + \Delta p^\text{th}``
+- [`TimeDurationOn`](@ref):
+ - Symbol: ``V_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\text{th}``
+- [`TimeDurationOff`](@ref):
+ - Symbol: ``W_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`
+- ``D^\text{min,up}`` = `PowerSystems.get_time_limits(device).up`
+- ``D^\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`
+
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``u^\text{th}P^\text{th,min} + \Delta p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. It also creates the commitment constraint to turn on/off the device.
+
+```math
+\begin{align*}
+& 0 \le \Delta p^\text{th}_t \le u^\text{th}_t\left(P^\text{th,max} - P^\text{th,min}\right), \quad \forall t\in \{1, \dots, T\} \\
+& u_t^\text{th} Q^\text{th,min} \le q^\text{th}_t \le u_t^\text{th} Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& -R^\text{th,dn} \le \Delta p_1^\text{th} - \Delta p^\text{th, init} \le R^\text{th,up} \\
+& -R^\text{th,dn} \le \Delta p_t^\text{th} - \Delta p_{t-1}^\text{th} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\} \\
+& u_1^\text{th} = u^\text{th,init} + v_1^\text{th} - w_1^\text{th} \\
+& u_t^\text{th} = u_{t-1}^\text{th} + v_t^\text{th} - w_t^\text{th}, \quad \forall t \in \{2,\dots,T\} \\
+& v_t^\text{th} + w_t^\text{th} \le 1, \quad \forall t \in \{1,\dots,T\}
+\end{align*}
+```
----
+In addition, this formulation adds duration constraints, i.e. minimum-up time and minimum-down time constraints. The duration constraints are added over the start times looking backwards.
-## `ThermalMultiStartUnitCommitment`
+The duration times ``D^\text{min,up}`` and ``D^\text{min,dn}`` are processed to be used in multiple of the time-steps, given the resolution of the specific problem. In addition, parameters ``D^\text{init,up}`` and ``D^\text{init,dn}`` are used to identify how long the unit was on or off, respectively, before the simulation started.
-```@docs
-ThermalMultiStartUnitCommitment
+Minimum up-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,up} - D^\text{init,up} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th}
+\end{align*}
```
-TODO
+Minimum down-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,dn} - D^\text{init,dn} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th}
+\end{align*}
+```
---
-## `ThermalBasicUnitCommitment`
+## `ThermalStandardUnitCommitment`
```@docs
-ThermalBasicUnitCommitment
+ThermalStandardUnitCommitment
+```
+
+**Variables:**
+
+- [`ActivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+- [`OnVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u_t^\text{th}``
+- [`StartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``v_t^\text{th}``
+- [`StopVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``w_t^\text{th}``
+
+**Auxiliary Variables:**
+- [`TimeDurationOn`](@ref):
+ - Symbol: ``V_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\text{th}``
+- [`TimeDurationOff`](@ref):
+ - Symbol: ``W_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`
+- ``D^\text{min,up}`` = `PowerSystems.get_time_limits(device).up`
+- ``D^\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`
+
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. It also creates the commitment constraint to turn on/off the device.
+
+```math
+\begin{align*}
+& u^\text{th}_t P^\text{th,min} \le p^\text{th}_t \le u^\text{th}_t P^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& u_t^\text{th} Q^\text{th,min} \le q^\text{th}_t \le u_t^\text{th} Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& -R^\text{th,dn} \le p_1^\text{th} - p^\text{th, init} \le R^\text{th,up} \\
+& -R^\text{th,dn} \le p_t^\text{th} - p_{t-1}^\text{th} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\} \\
+& u_1^\text{th} = u^\text{th,init} + v_1^\text{th} - w_1^\text{th} \\
+& u_t^\text{th} = u_{t-1}^\text{th} + v_t^\text{th} - w_t^\text{th}, \quad \forall t \in \{2,\dots,T\} \\
+& v_t^\text{th} + w_t^\text{th} \le 1, \quad \forall t \in \{1,\dots,T\}
+\end{align*}
+```
+
+In addition, this formulation adds duration constraints, i.e. minimum-up time and minimum-down time constraints. The duration constraints are added over the start times looking backwards.
+
+The duration times ``D^\text{min,up}`` and ``D^\text{min,dn}`` are processed to be used in multiple of the time-steps, given the resolution of the specific problem. In addition, parameters ``D^\text{init,up}`` and ``D^\text{init,dn}`` are used to identify how long the unit was on or off, respectively, before the simulation started.
+
+Minimum up-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,up} - D^\text{init,up} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th}
+\end{align*}
+```
+
+Minimum down-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,dn} - D^\text{init,dn} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th}
+\end{align*}
```
-TODO
---
-## `ThermalStandardUnitCommitment`
+## `ThermalMultiStartUnitCommitment`
```@docs
-ThermalStandardUnitCommitment
+ThermalMultiStartUnitCommitment
+```
+
+
+**Variables:**
+
+- [`PowerAboveMinimumVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``\Delta p^\text{th}``
+- [`ReactivePowerVariable`](@ref):
+ - Bounds: [0.0, ]
+ - Symbol: ``q^\text{th}``
+- [`OnVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``u_t^\text{th}``
+- [`StartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``v_t^\text{th}``
+- [`StopVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``w_t^\text{th}``
+- [`ColdStartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``x_t^\text{th}``
+- [`WarmStartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``y_t^\text{th}``
+- [`HotStartVariable`](@ref):
+ - Bounds: ``\{0,1\}``
+ - Symbol: ``z_t^\text{th}``
+
+**Auxiliary Variables:**
+- [`PowerOutput`](@ref):
+ - Symbol: ``P^\text{th}``
+ - Definition: ``P^\text{th} = u^\text{th}P^\text{min} + \Delta p^\text{th}``
+- [`TimeDurationOn`](@ref):
+ - Symbol: ``V_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned on variable ``u_t^\text{th}``
+- [`TimeDurationOff`](@ref):
+ - Symbol: ``W_t^\text{th}``
+ - Definition: Computed post optimization by adding consecutive turned off variable ``1 - u_t^\text{th}``
+
+**Static Parameters:**
+
+- ``P^\text{th,min}`` = `PowerSystems.get_active_power_limits(device).min`
+- ``P^\text{th,max}`` = `PowerSystems.get_active_power_limits(device).max`
+- ``Q^\text{th,min}`` = `PowerSystems.get_reactive_power_limits(device).min`
+- ``Q^\text{th,max}`` = `PowerSystems.get_reactive_power_limits(device).max`
+- ``R^\text{th,up}`` = `PowerSystems.get_ramp_limits(device).up`
+- ``R^\text{th,dn}`` = `PowerSystems.get_ramp_limits(device).down`
+- ``D^\text{min,up}`` = `PowerSystems.get_time_limits(device).up`
+- ``D^\text{min,dn}`` = `PowerSystems.get_time_limits(device).down`
+- ``D^\text{cold}`` = `PowerSystems.get_start_time_limits(device).cold`
+- ``D^\text{warm}`` = `PowerSystems.get_start_time_limits(device).warm`
+- ``D^\text{hot}`` = `PowerSystems.get_start_time_limits(device).hot`
+- ``P^\text{th,startup}`` = `PowerSystems.get_power_trajectory(device).startup`
+- ``P^\text{th, shdown}`` = `PowerSystems.get_power_trajectory(device).shutdown`
+
+
+**Objective:**
+
+Add a cost to the objective function depending on the defined cost structure of the thermal unit by adding it to its `ProductionCostExpression`.
+
+**Expressions:**
+
+Adds ``u^\text{th}P^\text{th,min} + \Delta p^\text{th}`` to the `ActivePowerBalance` expression and ``q^\text{th}`` to the `ReactivePowerBalance`, to be used in the supply-balance constraint depending on the network model used.
+
+**Constraints:**
+
+For each thermal unit creates the range constraints for its active and reactive power depending on its static parameters. It also creates the commitment constraint to turn on/off the device.
+
+```math
+\begin{align*}
+& 0 \le \Delta p^\text{th}_t \le u^\text{th}_t\left(P^\text{th,max} - P^\text{th,min}\right), \quad \forall t\in \{1, \dots, T\} \\
+& u_t^\text{th} Q^\text{th,min} \le q^\text{th}_t \le u_t^\text{th} Q^\text{th,max}, \quad \forall t\in \{1, \dots, T\} \\
+& -R^\text{th,dn} \le \Delta p_1^\text{th} - \Delta p^\text{th, init} \le R^\text{th,up} \\
+& -R^\text{th,dn} \le \Delta p_t^\text{th} - \Delta p_{t-1}^\text{th} \le R^\text{th,up}, \quad \forall t\in \{2, \dots, T\} \\
+& u_1^\text{th} = u^\text{th,init} + v_1^\text{th} - w_1^\text{th} \\
+& u_t^\text{th} = u_{t-1}^\text{th} + v_t^\text{th} - w_t^\text{th}, \quad \forall t \in \{2,\dots,T\} \\
+& v_t^\text{th} + w_t^\text{th} \le 1, \quad \forall t \in \{1,\dots,T\} \\
+& \max\{P^\text{th,max} - P^\text{th,shdown}, 0\} \cdot w_1^\text{th} \le u^\text{th,init} (P^\text{th,max} - P^\text{th,min}) - P^\text{th,init}
+\end{align*}
+```
+
+In addition, this formulation adds duration constraints, i.e. minimum-up time and minimum-down time constraints. The duration constraints are added over the start times looking backwards.
+
+The duration times ``D^\text{min,up}`` and ``D^\text{min,dn}`` are processed to be used in multiple of the time-steps, given the resolution of the specific problem. In addition, parameters ``D^\text{init,up}`` and ``D^\text{init,dn}`` are used to identify how long the unit was on or off, respectively, before the simulation started.
+
+Minimum up-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,up} - D^\text{init,up} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,up} + 1}^t v_i^\text{th} \leq u_t^\text{th}
+\end{align*}
+```
+
+Minimum down-time constraint for ``t \in \{1,\dots T\}``:
+```math
+\begin{align*}
+& \text{If } t \leq D^\text{min,dn} - D^\text{init,dn} \text{ and } D^\text{init,up} > 0: \\
+& 1 + \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th} \quad \text{(for } i \text{ in the set of time steps).} \\
+& \text{Otherwise:} \\
+& \sum_{i=t-D^\text{min,dn} + 1}^t w_i^\text{th} \leq 1 - u_t^\text{th}
+\end{align*}
+```
+
+Finally, multi temperature start/stop constraints are implemented using the following constraints:
+
+```math
+\begin{align*}
+& v_t^\text{th} = x_t^\text{th} + y_t^\text{th} + z_t^\text{th}, \quad \forall t \in \{1, \dots, T\} \\
+& z_t^\text{th} \le \sum_{i \in [D^\text{hot}, D^\text{warm})}w_{t-i}^\text{th}, \quad \forall t \in \{D^\text{warm}, \dots, T\} \\
+& y_t^\text{th} \le \sum_{i \in [D^\text{warm}, D^\text{cold})}w_{t-i}^\text{th}, \quad \forall t \in \{D^\text{cold}, \dots, T\} \\
+& (D^\text{warm} - 1) z_t^\text{th} + (1 - z_t^\text{th}) M^\text{big} \ge \sum_{i=1}^t (1 - u_i^\text{th}) + D^\text{init,hot}, \quad \forall t \in \{1, \dots, T\} \\
+& D^\text{hot} z_t^\text{th} \le \sum_{i=1}^t (1 - u_i^\text{th}) + D^\text{init,hot}, \quad \forall t \in \{1, \dots, T\} \\
+& (D^\text{cold} - 1) y_t^\text{th} + (1 - y_t^\text{th}) M^\text{big} \ge \sum_{i=1}^t (1 - u_i^\text{th}) + D^\text{init,warm}, \quad \forall t \in \{1, \dots, T\} \\
+& D^\text{warm} y_t^\text{th} \le \sum_{i=1}^t (1 - u_i^\text{th}) + D^\text{init,warm}, \quad \forall t \in \{1, \dots, T\} \\
+\end{align*}
```
-TODO
---
+
+## Valid configurations
+
+Valid `DeviceModel`s for subtypes of `ThermalGen` include the following:
+
+```@eval
+using PowerSimulations
+using PowerSystems
+using DataFrames
+using Latexify
+combos = PowerSimulations.generate_device_formulation_combinations()
+filter!(x -> x["device_type"] <: ThermalGen, combos)
+combo_table = DataFrame(
+ "Valid DeviceModel" => ["`DeviceModel($(c["device_type"]), $(c["formulation"]))`" for c in combos],
+ "Device Type" => ["[$(c["device_type"])](https://nrel-Sienna.github.io/PowerSystems.jl/stable/model_library/generated_$(c["device_type"])/)" for c in combos],
+ "Formulation" => ["[$(c["formulation"])](@ref)" for c in combos],
+ )
+mdtable(combo_table, latex = false)
+```
diff --git a/docs/src/modeler_guide/debugging_infeasible_models.md b/docs/src/modeler_guide/debugging_infeasible_models.md
index c96c7ff78b..cc52f7a2ec 100644
--- a/docs/src/modeler_guide/debugging_infeasible_models.md
+++ b/docs/src/modeler_guide/debugging_infeasible_models.md
@@ -5,4 +5,168 @@ Getting infeasible solutions to models is a common occurrence in operations simu
## Adding slacks to the model
+One of the most common infeasibility issues observed is due to not enough generation to supply demand, or conversely, excessive fixed (non-curtailable) generation in a low demand scenario.
+
+The recommended solution for any of these cases is adding slack variables to the network model, for example:
+
+```julia
+template_uc = ProblemTemplate(
+ NetworkModel(
+ CopperPlatePowerModel,
+ use_slacks=true,
+ ),
+ )
+```
+will add slack variables to the `ActivePowerBalance` expression.
+
+In this case, if the problem is now feasible, the user can check the solution of the variables `SystemBalanceSlackUp` and `SystemBalanceSlackDown`, and if one value is greater than zero, it represents that not enough generation (for Slack Up) or not enough demand (for Slack Down) in the optimization problem.
+
+### Services cases
+
+In many scenarios, certain units are also required to provide reserve requirements, e.g. thermal units mandated to provide up-regulation. In such scenarios, it is also possible to add slack variables, by specifying the service model (`RangeReserve`) for the specific service type (`VariableReserve{ReserveUp}`) as:
+```julia
+set_service_model!(
+ template_uc,
+ ServiceModel(
+ VariableReserve{ReserveUp},
+ RangeReserve;
+ use_slacks=true
+ ),
+)
+```
+Again, if the problem is now feasible, check the solution of `ReserveRequirementSlack` variable, and if it is larger than zero in a specific time-step, then it is evidence that there is not enough reserve available to satisfy the requirement.
+
## Getting the infeasibility conflict
+
+Some solvers allows to identify which constraints and variables are producing the infeasibility, by finding the irreducible infeasible set (IIS), that is the subset of constraints and variable bounds that will become feasible if any single constraint or variable bound is removed.
+
+To enable this feature in `PowerSimulations` the keyword argument `calculate_conflict` must be set to `true`, when creating the `DecisionModel`. Note that not all solvers allow the computation of the IIS, but most commercial solvers have this capability. It is also recommended to enable the keyword argument `store_variable_names=true` to help understanding which variables are with infeasibility issues.
+
+The following code creates a decision model with the `Xpress` optimizer, and enabling the `calculate_conflict=true` keyword argument.
+
+```julia
+DecisionModel(
+ template_ed,
+ sys_rts_rt;
+ name="ED",
+ optimizer=optimizer_with_attributes(Xpress.Optimizer, "MIPRELSTOP" => 1e-2),
+ optimizer_solve_log_print=true,
+ calculate_conflict=true,
+ store_variable_names=true,
+)
+```
+
+Here is an example on how the IIS will be displayed as:
+
+```raw
+Error: Constraints participating in conflict basis (IIS)
+│
+│ ┌──────────────────────────────────────┐
+│ │ CopperPlateBalanceConstraint__System │
+│ ├──────────────────────────────────────┤
+│ │ (113, 26) │
+│ └──────────────────────────────────────┘
+│ ┌──────────────────────────────────┐
+│ │ EnergyAssetBalance__HybridSystem │
+│ ├──────────────────────────────────┤
+│ │ ("317_Hybrid", 26) │
+│ └──────────────────────────────────┘
+│ ┌─────────────────────────────────────────────┐
+│ │ PieceWiseLinearCostConstraint__HybridSystem │
+│ ├─────────────────────────────────────────────┤
+│ │ ("317_Hybrid", 26) │
+│ └─────────────────────────────────────────────┘
+│ ┌────────────────────────────────────────────────┐
+│ │ PieceWiseLinearCostConstraint__ThermalStandard │
+│ ├────────────────────────────────────────────────┤
+│ │ ("202_STEAM_3", 26) │
+│ │ ("101_STEAM_3", 26) │
+│ │ ("118_CC_1", 26) │
+│ │ ("202_STEAM_4", 26) │
+│ │ ("315_CT_6", 26) │
+│ │ ("201_STEAM_3", 26) │
+│ │ ("102_STEAM_4", 26) │
+│ └────────────────────────────────────────────────┘
+│ ┌──────────────────────────────────────────────────────────────────────┐
+│ │ ActivePowerVariableTimeSeriesLimitsConstraint__RenewableDispatch__ub │
+│ ├──────────────────────────────────────────────────────────────────────┤
+│ │ ("122_WIND_1", 26) │
+│ │ ("324_PV_3", 26) │
+│ │ ("312_PV_1", 26) │
+│ │ ("102_PV_1", 26) │
+│ │ ("101_PV_1", 26) │
+│ │ ("324_PV_2", 26) │
+│ │ ("313_PV_2", 26) │
+│ │ ("104_PV_1", 26) │
+│ │ ("101_PV_2", 26) │
+│ │ ("309_WIND_1", 26) │
+│ │ ("310_PV_2", 26) │
+│ │ ("113_PV_1", 26) │
+│ │ ("314_PV_1", 26) │
+│ │ ("324_PV_1", 26) │
+│ │ ("103_PV_1", 26) │
+│ │ ("303_WIND_1", 26) │
+│ │ ("314_PV_2", 26) │
+│ │ ("102_PV_2", 26) │
+│ │ ("314_PV_3", 26) │
+│ │ ("320_PV_1", 26) │
+│ │ ("101_PV_3", 26) │
+│ │ ("319_PV_1", 26) │
+│ │ ("314_PV_4", 26) │
+│ │ ("310_PV_1", 26) │
+│ │ ("215_PV_1", 26) │
+│ │ ("313_PV_1", 26) │
+│ │ ("101_PV_4", 26) │
+│ │ ("119_PV_1", 26) │
+│ └──────────────────────────────────────────────────────────────────────┘
+│ ┌─────────────────────────────────────────────────────────────────────────────┐
+│ │ FeedforwardSemiContinuousConstraint__ThermalStandard__ActivePowerVariable_ub │
+│ ├─────────────────────────────────────────────────────────────────────────────┤
+│ │ ("322_CT_6", 26) │
+│ │ ("321_CC_1", 26) │
+│ │ ("223_CT_4", 26) │
+│ │ ("213_CT_1", 26) │
+│ │ ("223_CT_6", 26) │
+│ │ ("123_CT_1", 26) │
+│ │ ("113_CT_3", 26) │
+│ │ ("302_CT_3", 26) │
+│ │ ("215_CT_4", 26) │
+│ │ ("301_CT_4", 26) │
+│ │ ("113_CT_2", 26) │
+│ │ ("221_CC_1", 26) │
+│ │ ("223_CT_5", 26) │
+│ │ ("315_CT_7", 26) │
+│ │ ("215_CT_5", 26) │
+│ │ ("113_CT_1", 26) │
+│ │ ("307_CT_2", 26) │
+│ │ ("213_CT_2", 26) │
+│ │ ("113_CT_4", 26) │
+│ │ ("218_CC_1", 26) │
+│ │ ("213_CC_3", 26) │
+│ │ ("323_CC_2", 26) │
+│ │ ("322_CT_5", 26) │
+│ │ ("207_CT_2", 26) │
+│ │ ("123_CT_5", 26) │
+│ │ ("123_CT_4", 26) │
+│ │ ("207_CT_1", 26) │
+│ │ ("301_CT_3", 26) │
+│ │ ("302_CT_4", 26) │
+│ │ ("307_CT_1", 26) │
+│ └─────────────────────────────────────────────────────────────────────────────┘
+│ ┌───────────────────────────────────────────────────────┐
+│ │ RenewableActivePowerLimitConstraint__HybridSystem__ub │
+│ ├───────────────────────────────────────────────────────┤
+│ │ ("317_Hybrid", 26) │
+│ └───────────────────────────────────────────────────────┘
+│ ┌───────────────────────────────────────┐
+│ │ ThermalOnVariableUb__HybridSystem__ub │
+│ ├───────────────────────────────────────┤
+│ │ ("317_Hybrid", 26) │
+│ └───────────────────────────────────────┘
+
+ Error: Serializing Infeasible Problem at /var/folders/1v/t69qyl0n5059n6c1nn7sp8zm7g8s6z/T/jl_jNSREb/compact_sim/problems/ED/infeasible_ED_2020-10-06T15:00:00.json
+```
+
+Note that the IIS clearly identify that the issue is happening at time step 26, and constraints are related with the `CopperPlateBalanceConstraint__System`, with multiple upper bound constraints, for the hybrid system, renewable units and thermal units. This highlights that there may not be enough generation in the system. Indeed, by enabling system slacks, the problem become feasible.
+
+Finally, the infeasible model is exported in a `json` file that can be loaded directly in `JuMP` to be explored. More information about this is [available here](https://jump.dev/JuMP.jl/stable/moi/submodules/FileFormats/overview/#Read-from-file).
\ No newline at end of file
diff --git a/docs/src/modeler_guide/definitions.md b/docs/src/modeler_guide/definitions.md
index 8a4946f85f..a8effa9ab5 100644
--- a/docs/src/modeler_guide/definitions.md
+++ b/docs/src/modeler_guide/definitions.md
@@ -1,15 +1,44 @@
# Definitions
+## A
+
+* *Attributes*: Certain device formulations can be customized by specifying attributes that will include/remove certain variables, expressions and/or constraints. For example, in `StorageSystemsSimulations.jl`, the device formulation of `StorageDispatchWithReserves` can be specified with the following dictionary of attributes:
+```julia
+set_device_model!(
+ template,
+ DeviceModel(
+ GenericBattery,
+ StorageDispatchWithReserves;
+ attributes=Dict{String, Any}(
+ "reservation" => false,
+ "cycling_limits" => false,
+ "energy_target" => false,
+ "complete_coverage" => false,
+ "regularization" => false,
+ ),
+ ),
+)
+```
+Changing the attributes between `true` or `false` can enable/disable multiple aspects of the formulation.
+
+## C
+
+* *Chronologies:* In `PowerSimulations.jl`, chronologies define where information is flowing. There are two types of chronologies. 1) **inter-stage chronologies** (`InterProblemChronology`) that define how information flows between stages. e.g. day-ahead solutions are used to inform economic dispatch problems; and 2) **intra-stage chronologies** (`IntraProblemChronology`) that define how information flows between multiple executions of a single stage. e.g. the dispatch setpoints of the first period of an economic dispatch problem are constrained by the ramping limits from setpoints in the final period of the previous problem.
+
## D
-* *Decision Problem*: A decision problem calculates the desired system operation based on forecasts of uncertain inputs and information about the state of the system. The output of a decision problem represents the policies used to drive the set-points of the system's devices, like generators or switches, and depends on the purpose of the problem. See the [Decision Model Tutorial](op_problem_tutorial) to learn more about solving individual problems.
+* *Decision Problem*: A decision problem calculates the desired system operation based on forecasts of uncertain inputs and information about the state of the system. The output of a decision problem represents the policies used to drive the set-points of the system's devices, like generators or switches, and depends on the purpose of the problem. See the [Decision Model Tutorial](@ref op_problem_tutorial) to learn more about solving individual problems.
-* *Device Formulation*: The model of a device that is incorporated into a large system optimization models. For instance, the storage device model used inside of a Unit Commitment (UC) problem. A device model needs to follow some requirements to be integrated into operation problems.
+* *Device Formulation*: The model of a device that is incorporated into a large system optimization models. For instance, the storage device model used inside of a Unit Commitment (UC) problem. A device model needs to follow some requirements to be integrated into operation problems. For more information about valid `DeviceModel`s and their mathematical representations, check out the [Formulation Library](@ref formulation_intro).
## E
* *Emulation Problem*: An emulation problem is used to mimic the system's behavior subject to an incoming decision and the realization of a forecasted inputs. The solution of the emulator produces outputs representative of the system performance when operating subject the policies resulting from the decision models.
+## F
+
+* *FeedForward*: The definition of exactly what information is passed using the defined chronologies is accomplished using FeedForwards. Specifically, a FeedForward is used to define what to do with information being passed with an inter-stage chronology in a Simulation. The most common FeedForward is the `SemiContinuousFeedForward` that affects the semi-continuous range constraints of thermal generators in the economic dispatch problems based on the value of the (already solved) unit-commitment variables.
+
## H
* *Horizon*: The number of steps in the look-ahead of a decision problem. For instance, a Day-Ahead problem usually has a 48 step horizon. Check the time [Time Series Data Section in PowerSystems.jl](https://nrel-sienna.github.io/PowerSystems.jl/stable/modeler_guide/time_series/)
@@ -20,4 +49,18 @@
## R
-* *Resolution*: The amount of time between timesteps in a simulation. For instance 1-hour or 5-minutes. In Julia these are defined using the syntax `Hour(1)` and `Minute(5)`. Check the time [Time Series Data Section in PowerSystems.jl](https://nrel-sienna.github.io/PowerSystems.jl/stable/modeler_guide/time_series/)
+* *Resolution*: The amount of time between time steps in a simulation. For instance 1-hour or 5-minutes. In Julia these are defined using the syntax `Hour(1)` and `Minute(5)`. Check the time [Time Series Data Section in PowerSystems.jl](https://nrel-sienna.github.io/PowerSystems.jl/stable/modeler_guide/time_series/)
+
+* *Results vs Realized Results*: In `PowerSimulations.jl` the term *results* is used to refer to the solution of all optimization problems in a *Simulation*. When using `read_variable(results, Variable)` in a `DecisionModel` of a simulation, the output is a dictionary with the values of such variable for every optimization problem solved, while `read_realized_variable(results, Variable)` will return the values of the specified interval and number of steps in the simulation. See the [Read Results page](@ref read_results) for more details.
+
+## S
+
+* *Service Formulation*: The model of a service that is incorporated into a large system optimization models. `Services` (or ancillary services) are models used to ensure that there is necessary support to the power grid from generators to consumers, in order to ensure reliable operation of the system. The most common application for ancillary services are reserves, i.e., generation (or load) that is not currently being used, but can be quickly made available in case of unexpected changes of grid conditions, for example a sudden loss of load or generation. A service model needs to follow some requirements to be integrated into operation problems. For more information about valid `ServiceModel`s and their mathematical representations, check out the [Formulation Library](@ref service_formulations).
+
+* *Simulation*: A simulation is a pre-determined sequence of decision problems in a way that solving it, resembles the solution procedures commonly used by operators. The most common simulation model is the solution of a Unit Commitment and Economic Dispatch sequence of problems.
+
+* *Solver*: A solver is a software package that incorporates algorithms for finding solutions to one or more classes of optimization problem. For example, FICO Xpress is a commercial optimization solver for linear programming (LP), convex quadratic programming (QP) problems, convex quadratically constrained quadratic programming (QCQP), second-order cone programming (SOCP) and their mixed integer counterparts. **A solver is required to be specified** in order to solve any computer optimization problem.
+
+## T
+
+* *Template*: A `ProblemTemplate` is just a collection of `DeviceModel`s that allows the user to specify the formulations of each set of devices (by device type) independently so that the modeler can adjust the level of detail according to the question of interest and the available data. For more information about valid `DeviceModel`s and their mathematical representations, check out the [Formulation Library](@ref formulation_intro).
\ No newline at end of file
diff --git a/docs/src/modeler_guide/parallel_simulations.md b/docs/src/modeler_guide/parallel_simulations.md
index 3f9ae2d6ea..ebd4d8f9c8 100644
--- a/docs/src/modeler_guide/parallel_simulations.md
+++ b/docs/src/modeler_guide/parallel_simulations.md
@@ -41,7 +41,7 @@ Here is example code to construct the `Simulation` with these parameters:
simulation_folder=output_dir,
)
status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index))
- if status != PSI.BuildStatus.BUILT
+ if status != PSI.SimulationBuildStatus.BUILT
error("Failed to build simulation: status=$status")
end
```
@@ -52,7 +52,7 @@ Here is example code to construct the `Simulation` with these parameters:
```
function execute_simulation(sim, args...; kwargs...)
status = execute!(sim)
- if status != PSI.RunStatus.SUCCESSFUL
+ if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED
error("Simulation failed to execute: status=$status")
end
end
@@ -183,7 +183,7 @@ Here is example code to construct the `Simulation` with these parameters:
simulation_folder=output_dir,
)
status = build!(sim; partitions=partitions, index=index, serialize=isnothing(index))
- if status != PSI.BuildStatus.BUILT
+ if status != PSI.SimulationBuildStatus.BUILT
error("Failed to build simulation: status=$status")
end
```
@@ -194,7 +194,7 @@ Here is example code to construct the `Simulation` with these parameters:
```
function execute_simulation(sim, args...; kwargs...)
status = execute!(sim)
- if status != PSI.RunStatus.SUCCESSFUL
+ if status != PSI.RunStatus.SUCCESSFULLY_FINALIZED
error("Simulation failed to execute: status=$status")
end
end
@@ -282,4 +282,3 @@ julia> results = SimulationResults("/job-outputs/")
Note the log files and results for each partition are located in
`/job-outputs//simulation_partitions`
-
diff --git a/docs/src/modeler_guide/problem_templates.md b/docs/src/modeler_guide/problem_templates.md
index 52eff0a56a..09addbde94 100644
--- a/docs/src/modeler_guide/problem_templates.md
+++ b/docs/src/modeler_guide/problem_templates.md
@@ -3,7 +3,7 @@
Templates are used to specify the modeling properties of the devices and network that are going to he used to specify a problem.
A `ProblemTemplate` is just a collection of `DeviceModel`s that allows the user to specify the formulations
of each set of devices (by device type) independently so that the modeler can adjust the level of detail according to the question of interest and the available data.
-For more information about valid `DeviceModel`s and their mathematical representations, check out the [Formulation Library](@ref formulation_library).
+For more information about valid `DeviceModel`s and their mathematical representations, check out the [Formulation Library](@ref formulation_intro).
## Building a `ProblemTemplate`
@@ -39,13 +39,3 @@ template_unit_commitment
using PowerSimulations #hide
template_unit_commitment()
```
-
-```@docs
-template_agc_reserve_deployment
-```
-
-```@example
-using PowerSimulations #hide
-using HydroPowerSimulations #hide
-template_agc_reserve_deployment()
-```
diff --git a/docs/src/modeler_guide/read_results.md b/docs/src/modeler_guide/read_results.md
new file mode 100644
index 0000000000..d1292f7b85
--- /dev/null
+++ b/docs/src/modeler_guide/read_results.md
@@ -0,0 +1,201 @@
+# [Read results](@id read_results)
+
+Once a `DecisionModel` is solved via `solve!(model)` or a Simulation is executed (and solved) via `execute!(simulation)`, the results are stored and can be accessed directly in the REPL for result exploration and plotting.
+
+## Read results of a Decision Problem
+
+Once a `DecisionModel` is solved, results are accessed using `OptimizationProblemResults(model)` as follows:
+
+```julia
+# The DecisionModel is already constructed
+build!(model, output_dir = mktempdir())
+solve!(model)
+
+results = OptimizationProblemResults(model)
+```
+
+The output will showcase the available expressions, parameters and variables to read. For example it will look like:
+
+```raw
+Start: 2020-01-01T00:00:00
+End: 2020-01-03T23:00:00
+Resolution: 60 minutes
+
+PowerSimulations Problem Auxiliary variables Results
+┌──────────────────────────────────────────┐
+│ CumulativeCyclingCharge__HybridSystem │
+│ CumulativeCyclingDischarge__HybridSystem │
+└──────────────────────────────────────────┘
+
+PowerSimulations Problem Expressions Results
+┌─────────────────────────────────────────────┐
+│ ProductionCostExpression__RenewableDispatch │
+│ ProductionCostExpression__ThermalStandard │
+└─────────────────────────────────────────────┘
+
+PowerSimulations Problem Duals Results
+┌──────────────────────────────────────┐
+│ CopperPlateBalanceConstraint__System │
+└──────────────────────────────────────┘
+
+PowerSimulations Problem Parameters Results
+┌────────────────────────────────────────────────────────────────────────┐
+│ ActivePowerTimeSeriesParameter__RenewableNonDispatch │
+│ RenewablePowerTimeSeries__HybridSystem │
+│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R3 │
+│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Reg_Up │
+│ ActivePowerTimeSeriesParameter__PowerLoad │
+│ ActivePowerTimeSeriesParameter__RenewableDispatch │
+│ RequirementTimeSeriesParameter__VariableReserve__ReserveDown__Reg_Down │
+│ ActivePowerTimeSeriesParameter__HydroDispatch │
+│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R1 │
+│ RequirementTimeSeriesParameter__VariableReserve__ReserveUp__Spin_Up_R2 │
+└────────────────────────────────────────────────────────────────────────┘
+
+PowerSimulations Problem Variables Results
+┌────────────────────────────────────────────────────────────────────┐
+│ ActivePowerOutVariable__HybridSystem │
+│ ReservationVariable__HybridSystem │
+│ RenewablePower__HybridSystem │
+│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R1 │
+│ SystemBalanceSlackUp__System │
+│ BatteryEnergyShortageVariable__HybridSystem │
+│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Reg_Up │
+│ StopVariable__ThermalStandard │
+│ BatteryStatus__HybridSystem │
+│ BatteryDischarge__HybridSystem │
+│ ActivePowerInVariable__HybridSystem │
+│ DischargeRegularizationVariable__HybridSystem │
+│ BatteryCharge__HybridSystem │
+│ ActivePowerVariable__RenewableDispatch │
+│ ActivePowerReserveVariable__VariableReserve__ReserveDown__Reg_Down │
+│ EnergyVariable__HybridSystem │
+│ OnVariable__HybridSystem │
+│ BatteryEnergySurplusVariable__HybridSystem │
+│ SystemBalanceSlackDown__System │
+│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R2 │
+│ ThermalPower__HybridSystem │
+│ ActivePowerVariable__ThermalStandard │
+│ StartVariable__ThermalStandard │
+│ ActivePowerReserveVariable__VariableReserve__ReserveUp__Spin_Up_R3 │
+│ OnVariable__ThermalStandard │
+│ ChargeRegularizationVariable__HybridSystem │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+Then the following code can be used to read results:
+
+```julia
+# Read active power of Thermal Standard
+thermal_active_power = read_variable(results, "ActivePowerVariable__ThermalStandard")
+
+# Read max active power parameter of RenewableDispatch
+renewable_param = read_parameter(results, "ActivePowerTimeSeriesParameter__RenewableDispatch")
+
+# Read cost expressions of ThermalStandard units
+cost_thermal = read_expression(results, "ProductionCostExpression__ThermalStandard")
+
+# Read dual variables
+dual_balance_constraint = read_dual(results, "CopperPlateBalanceConstraint__System")
+
+# Read auxiliary variables
+aux_var_result = read_aux_variable(results, "CumulativeCyclingCharge__HybridSystem")
+```
+
+Results will be in the form of DataFrames that can be easily explored.
+
+## Read results of a Simulation
+
+```julia
+# The Simulation is already constructed
+build!(sim)
+execute!(sim; enable_progress_bar=true)
+
+results_sim = SimulationResults(sim)
+```
+
+As an example, the `SimulationResults` printing will look like:
+
+```raw
+Decision Problem Results
+┌──────────────┬─────────────────────┬──────────────┬─────────────────────────┐
+│ Problem Name │ Initial Time │ Resolution │ Last Solution Timestamp │
+├──────────────┼─────────────────────┼──────────────┼─────────────────────────┤
+│ ED │ 2020-10-02T00:00:00 │ 60 minutes │ 2020-10-09T23:00:00 │
+│ UC │ 2020-10-02T00:00:00 │ 1440 minutes │ 2020-10-09T00:00:00 │
+└──────────────┴─────────────────────┴──────────────┴─────────────────────────┘
+
+Emulator Results
+┌─────────────────┬───────────┐
+│ Name │ Emulator │
+│ Resolution │ 5 minutes │
+│ Number of steps │ 2304 │
+└─────────────────┴───────────┘
+```
+
+With this, it is possible to obtain results of each `DecisionModel` and `EmulationModel` as follows:
+
+```julia
+# Use the Problem Name for Decision Problems
+results_uc = get_decision_problem_results(results_sim, "UC")
+results_ed = get_decision_problem_results(results_sim, "ED")
+results_emulator = get_emulation_problem_results(results_sim)
+```
+
+Once we have each decision (or emulation) problem results, we can explore directly using the approach for Decision Models, mentioned in the previous section.
+
+### Reading solutions for all simulation steps
+
+In this case, using `read_variable` (or read expression, parameter or dual), will return a dictionary of all steps (of that Decision Problem). For example, the following code:
+
+```julia
+thermal_active_power = read_variable(results_uc, "ActivePowerVariable__ThermalStandard")
+```
+will return:
+```
+DataStructures.SortedDict{Any, Any, Base.Order.ForwardOrdering} with 8 entries:
+ DateTime("2020-10-02T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-03T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-04T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-05T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-06T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-07T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-08T00:00:00") => 72×54 DataFrame…
+ DateTime("2020-10-09T00:00:00") => 72×54 DataFrame…
+```
+That is, a sorted dictionary for each simulation step, using as a key the initial timestamp for that specific simulation step.
+
+Note that in this case, each DataFrame, has a dimension of ``72 \times 54``, since the horizon is 72 hours (number of rows), but the interval is only 24 hours. Indeed, note the initial timestamp of each simulation step is the beginning of each day, i.e. 24 hours. Finally, there 54 columns, since this example system has 53 `ThermalStandard` units (plus 1 column for the timestamps). The user is free to explore the solution of any simulation step as needed.
+
+### Reading the "realized" solution (i.e. the interval)
+
+Using `read_realized_variable` (or read realized expression, parameter or dual), will return the DataFrame of the realized solution of any specific variable. That is, it will concatenate the corresponding simulation step with the specified interval of that step, to construct a single DataFrame with the "realized solution" of the entire simulation.
+
+For example, the code:
+```julia
+th_realized_power = read_realized_variable(results_uc, "ActivePowerVariable__ThermalStandard")
+```
+will return:
+```raw
+92×54 DataFrame
+ Row │ DateTime 322_CT_6 321_CC_1 202_STEAM_3 223_CT_4 123_STEAM_2 213_CT_1 223_CT_6 313_CC_1 101_STEAM_3 123_C ⋯
+ │ DateTime Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float ⋯
+─────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ 1 │ 2020-10-02T00:00:00 0.0 293.333 0.0 0.0 0.0 0.0 0.0 231.667 76.0 0.0 ⋯
+ 2 │ 2020-10-02T01:00:00 0.0 267.552 0.0 0.0 0.0 0.0 0.0 231.667 76.0 0.0
+ 3 │ 2020-10-02T02:00:00 0.0 234.255 0.0 0.0 -4.97544e-11 0.0 0.0 231.667 76.0 0.0
+ 4 │ 2020-10-02T03:00:00 0.0 249.099 0.0 0.0 -4.97544e-11 0.0 0.0 231.667 76.0 0.0
+ 5 │ 2020-10-02T04:00:00 0.0 293.333 0.0 0.0 -4.97544e-11 0.0 0.0 231.667 76.0 0.0 ⋯
+ 6 │ 2020-10-02T05:00:00 0.0 293.333 1.27578e-11 0.0 -4.97544e-11 0.0 0.0 293.333 76.0 0.0
+ ⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
+ 187 │ 2020-10-09T18:00:00 0.0 293.333 76.0 0.0 155.0 0.0 0.0 318.843 76.0 0.0
+ 188 │ 2020-10-09T19:00:00 0.0 293.333 76.0 0.0 124.0 0.0 0.0 293.333 76.0 0.0
+ 189 │ 2020-10-09T20:00:00 0.0 293.333 60.6667 0.0 124.0 0.0 0.0 0.0 76.0 0.0 ⋯
+ 190 │ 2020-10-09T21:00:00 -7.65965e-12 293.333 60.6667 0.0 124.0 0.0 0.0 0.0 76.0 0.0
+ 191 │ 2020-10-09T22:00:00 0.0 0.0 60.6667 0.0 124.0 0.0 0.0 0.0 76.0 7.156
+ 192 │ 2020-10-09T23:00:00 0.0 0.0 60.6667 0.0 117.81 0.0 0.0 0.0 76.0 0.0
+ 44 columns and 180 rows omitted
+```
+In this case, the 8 simulation steps of 24 hours (192 hours), in a single DataFrame, to enable easy exploration of the realized results for the user.
+
+
diff --git a/docs/src/modeler_guide/running_a_simulation.md b/docs/src/modeler_guide/running_a_simulation.md
index 906bff25a2..80df408917 100644
--- a/docs/src/modeler_guide/running_a_simulation.md
+++ b/docs/src/modeler_guide/running_a_simulation.md
@@ -7,10 +7,112 @@ Check out the [Operations Problem Tutorial](@ref op_problem_tutorial)
## Feedforward
-TODO
+The definition of exactly what information is passed using the defined chronologies is accomplished using FeedForwards.
+
+Specifically, a FeedForward is used to define what to do with information being passed with an inter-stage chronology in a Simulation. The most common FeedForward is the `SemiContinuousFeedForward` that affects the semi-continuous range constraints of thermal generators in the economic dispatch problems based on the value of the (already solved) unit-commitment variables.
+
+The creation of a FeedForward requires at least to specify the `component_type` on which the FeedForward will be applied. The `source` variable specify which variable will be taken from the problem solved, for example the commitment variable of the thermal unit in the unit commitment problem. Finally, the `affected_values` specify which variables will be affected in the problem to be solved, for example the next economic dispatch problem.
+
+The following code specify the creation of semi-continuous range constraints on the `ActivePowerVariable` based on the solution of the commitment variable `OnVariable` for all `ThermalStandard` units.
+
+```julia
+SemiContinuousFeedforward(
+ component_type=ThermalStandard,
+ source=OnVariable,
+ affected_values=[ActivePowerVariable],
+)
+```
+
+## Chronologies
+
+In PowerSimulations, chronologies define where information is flowing. There are two types
+of chronologies.
+
+- inter-stage chronologies: Define how information flows between stages. e.g. day-ahead solutions are used to inform economic dispatch problems
+- intra-stage chronologies: Define how information flows between multiple executions of a single stage. e.g. the dispatch setpoints of the first period of an economic dispatch problem are constrained by the ramping limits from setpoints in the final period of the previous problem.
## Sequencing
In a typical simulation pipeline, we want to connect daily (24-hours) day-ahead unit commitment problems, with multiple economic dispatch problems. Usually, our day-ahead unit commitment problem will have an hourly (1-hour) resolution, while the economic dispatch will have a 5-minute resolution.
-Depending on your problem, it is common to use a 2-day look-ahead for unit commitment problems, so in this case, the Day-Ahead problem will have: resolution = Hour(1) with interval = Hour(24) and horizon = 48. In the case of the economic dispatch problem, it is common to use a look-ahead of two hours. Thus, the Real-Time problem will have: resolution = Minute(5), with interval = Minute(5) (we only store the first operating point) and horizon = 24 (24 time steps of 5 minutes are 120 minutes, that is 2 hours).
+
+Depending on your problem, it is common to use a 2-day look-ahead for unit commitment problems, so in this case, the Day-Ahead problem will have: resolution = Hour(1) with interval = Hour(24) and horizon = Hour(48). In the case of the economic dispatch problem, it is common to use a look-ahead of two hours. Thus, the Real-Time problem will have: resolution = Minute(5), with interval = Minute(5) (we only store the first operating point) and horizon = 24 (24 time steps of 5 minutes are 120 minutes, that is 2 hours).
+
+## Simulation Setup
+
+The following code creates the entire simulation pipeline:
+
+```julia
+# We assume that the templates for UC and ED are ready
+# sys_da has the resolution of 1 hour:
+# with the 24 hours interval and horizon of 48 hours.
+# sys_rt has the resolution of 5 minutes:
+# with a 5-minute interval and horizon of 2 hours (24 time steps)
+
+# Create the UC Decision Model
+decision_model_uc = DecisionModel(
+ template_uc,
+ sys_da;
+ name="UC",
+ optimizer=optimizer_with_attributes(
+ Xpress.Optimizer,
+ "MIPRELSTOP" => 1e-1,
+ ),
+)
+
+# Create the ED Decision Model
+decision_model_ed = DecisionModel(
+ template_ed,
+ sys_rt;
+ name="ED",
+ optimizer=optimizer_with_attributes(Xpress.Optimizer),
+)
+
+# Specify the SimulationModels using a Vector of decision_models: UC, ED
+sim_models = SimulationModels(
+ decision_models=[
+ decision_model_uc,
+ decision_model_ed,
+ ],
+)
+
+# Create the FeedForwards:
+semi_ff = SemiContinuousFeedforward(
+ component_type=ThermalStandard,
+ source=OnVariable,
+ affected_values=[ActivePowerVariable],
+)
+
+# Specify the sequencing:
+sim_sequence = SimulationSequence(
+ # Specify the vector of decision models: sim_models
+ models=sim_models,
+ # Specify a Dict of feedforwards on which the FF applies
+ # based on the DecisionModel name, in this case "ED"
+ feedforwards=Dict(
+ "ED" => [semi_ff],
+ ),
+ # Specify the chronology, in this case inter-stage
+ ini_cond_chronology=InterProblemChronology(),
+)
+
+# Construct the simulation:
+sim = Simulation(
+ name="compact_sim",
+ steps=10, # 10 days
+ models=sim_models,
+ sequence=sim_sequence,
+ # Specify the start_time as a DateTime: e.g. DateTime("2020-10-01T00:00:00")
+ initial_time=start_time,
+ # Specify a temporary folder to avoid storing logs if not needed
+ simulation_folder=mktempdir(cleanup=true),
+)
+
+# Build the decision models and simulation setup
+build!(sim)
+
+# Execute the simulation using the Optimizer specified in each DecisionModel
+execute!(sim, enable_progress_bar=true)
+```
+
+Check the [PCM tutorial](@ref pcm_tutorial) for a more detailed tutorial on executing a simulation in a production cost modeling (PCM) environment.
diff --git a/docs/src/tutorials/adding_new_problem_model.md b/docs/src/tutorials/adding_new_problem_model.md
index bc41792a29..6f420817de 100644
--- a/docs/src/tutorials/adding_new_problem_model.md
+++ b/docs/src/tutorials/adding_new_problem_model.md
@@ -62,7 +62,7 @@ my_model = DecisionModel{MyCustomDecisionProblem}(
These methods can be defined optionally for your problem. By default for problems subtyped from `DecisionProblem` these checks are not executed. If the problems are subtyped from `DefaultDecisionProblem` these checks are always conducted with PowerSimulations defaults and require compliance with those defaults to pass. In any case, these can be overloaded when necessary depending on the problem requirements.
1. `validate_template`
-2. `validate_time_series`
+2. `validate_time_series!`
3. `reset!`
4. `solve_impl!`
diff --git a/docs/src/tutorials/basics_of_developing_models.md b/docs/src/tutorials/basics_of_developing_models.md
index a027918d6a..654ec25da0 100644
--- a/docs/src/tutorials/basics_of_developing_models.md
+++ b/docs/src/tutorials/basics_of_developing_models.md
@@ -1,3 +1,3 @@
# Basics of Developing Operation Models
-Check the page [PowerSimulations Structure](@ref) for more background on PowerSimulations.jl
+Check the page PowerSimulations Structure for more background on PowerSimulations.jl
diff --git a/docs/src/tutorials/decision_problem.md b/docs/src/tutorials/decision_problem.md
index b6d90126ef..be6325b600 100644
--- a/docs/src/tutorials/decision_problem.md
+++ b/docs/src/tutorials/decision_problem.md
@@ -16,6 +16,7 @@ using PowerSimulations
using HydroPowerSimulations
using PowerSystemCaseBuilder
using HiGHS # solver
+using Dates
```
## Data
@@ -59,14 +60,14 @@ Here we define template entries for all devices that inject or withdraw power on
network. For each device type, we can define a distinct `AbstractDeviceFormulation`. In
this case, we're defining a basic unit commitment model for thermal generators,
curtailable renewable generators, and fixed dispatch (net-load reduction) formulations
-for `HydroDispatch` and `RenewableFix` devices.
+for `HydroDispatch` and `RenewableNonDispatch` devices.
```@example op_problem
set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver)
-set_device_model!(template_uc, RenewableFix, FixedOutput)
+set_device_model!(template_uc, RenewableNonDispatch, FixedOutput)
```
### Service Formulations
@@ -113,7 +114,7 @@ The construction of an `DecisionModel` essentially applies an `ProblemTemplate`
to `System` data to create a JuMP model.
```@example op_problem
-problem = DecisionModel(template_uc, sys; optimizer = solver, horizon = 24)
+problem = DecisionModel(template_uc, sys; optimizer = solver, horizon = Hour(24))
build!(problem, output_dir = mktempdir())
```
@@ -132,10 +133,10 @@ solve!(problem)
## Results Inspection
-PowerSimulations collects the `DecisionModel` results into a `ProblemResults` struct:
+PowerSimulations collects the `DecisionModel` results into a `OptimizationProblemResults` struct:
```@example op_problem
-res = ProblemResults(problem)
+res = OptimizationProblemResults(problem)
```
### Optimizer Stats
diff --git a/docs/src/tutorials/pcm_simulation.md b/docs/src/tutorials/pcm_simulation.md
index bcf7f294fd..6f0ae2e92e 100644
--- a/docs/src/tutorials/pcm_simulation.md
+++ b/docs/src/tutorials/pcm_simulation.md
@@ -38,7 +38,7 @@ First, we'll create a `System` with hourly data to represent day-ahead forecaste
solar, and load profiles:
```@example pcm
-sys_DA = build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
+sys_DA = build_system(PSISystems, "modified_RTS_GMLC_DA_sys"; skip_serialization = true)
```
### 5-Minute system
@@ -47,7 +47,7 @@ The RTS data also includes 5-minute resolution time series data. So, we can crea
`System` to represent 15 minute ahead forecasted data for a "real-time" market:
```@example pcm
-sys_RT = build_system(PSISystems, "modified_RTS_GMLC_RT_sys")
+sys_RT = build_system(PSISystems, "modified_RTS_GMLC_RT_sys"; skip_serialization = true)
```
## `ProblemTemplate`s define stages
@@ -165,12 +165,13 @@ Now, we can build and execute a simulation using the `SimulationSequence` and `S
that we've defined.
```@example pcm
+path = mkdir(joinpath(".", "rts-store")) #hide
sim = Simulation(
name = "rts-test",
steps = 2,
models = models,
sequence = DA_RT_sequence,
- simulation_folder = mktempdir(".", cleanup = true),
+ simulation_folder = joinpath(".", "rts-store"),
)
```
@@ -244,13 +245,14 @@ problem definition), we can use:
```@example pcm
read_parameter(
ed_results,
- "ActivePowerTimeSeriesParameter__RenewableFix",
+ "ActivePowerTimeSeriesParameter__RenewableNonDispatch",
initial_time = DateTime("2020-01-01T06:00:00"),
count = 5,
)
```
-* note that this returns the results of each execution step in a separate dataframe *
+!!! info
+note that this returns the results of each execution step in a separate dataframe
If you want the realized results (without lookahead periods), you can call `read_realized_*`:
```@example pcm
@@ -258,8 +260,10 @@ read_realized_variables(
uc_results,
["ActivePowerVariable__ThermalStandard", "ActivePowerVariable__RenewableDispatch"],
)
+rm(path, force = true, recursive = true) #hide
```
+
## Plotting
Take a look at the plotting capabilities in [PowerGraphics.jl](https://github.com/nrel-siip/powergraphics.jl)
diff --git a/src/PowerSimulations.jl b/src/PowerSimulations.jl
index 456dbc691e..06dd24dee5 100644
--- a/src/PowerSimulations.jl
+++ b/src/PowerSimulations.jl
@@ -8,7 +8,6 @@ module PowerSimulations
export Simulation
export DecisionModel
export EmulationModel
-export ProblemResults
export ProblemTemplate
export InitialCondition
export SimulationModels
@@ -22,6 +21,7 @@ export NetworkModel
export PTDFPowerModel
export CopperPlatePowerModel
export AreaBalancePowerModel
+export AreaPTDFPowerModel
######## Device Models ########
export DeviceModel
@@ -35,6 +35,7 @@ export NonSpinningReserve
export PIDSmoothACE
export GroupReserve
export ConstantMaxInterfaceFlow
+export VariableMaxInterfaceFlow
######## Branch Models ########
export StaticBranch
@@ -113,7 +114,6 @@ export run_parallel_simulation
## Template Exports
export template_economic_dispatch
export template_unit_commitment
-export template_agc_reserve_deployment
export EconomicDispatchProblem
export UnitCommitmentProblem
export AGCReserveDeployment
@@ -123,7 +123,6 @@ export set_network_model!
export get_network_formulation
## Results interfaces
export SimulationResultsExport
-export ProblemResultsExport
export export_results
export export_realized_results
export export_optimizer_stats
@@ -179,6 +178,9 @@ export read_optimizer_stats
export serialize_optimization_model
## Utils Exports
+export OptimizationProblemResults
+export OptimizationProblemResultsExport
+export OptimizerStats
export get_all_constraint_index
export get_all_variable_index
export get_constraint_index
@@ -187,13 +189,8 @@ export list_recorder_events
export show_recorder_events
export list_simulation_events
export show_simulation_events
-export export_realized_results
export get_num_partitions
-## Enums
-export BuildStatus
-export RunStatus
-
# Variables
export ActivePowerVariable
export ActivePowerInVariable
@@ -223,6 +220,8 @@ export ReserveRequirementSlack
export VoltageMagnitude
export VoltageAngle
export FlowActivePowerVariable
+export FlowActivePowerSlackUpperBound
+export FlowActivePowerSlackLowerBound
export FlowActivePowerFromToVariable
export FlowActivePowerToFromVariable
export FlowReactivePowerFromToVariable
@@ -231,6 +230,8 @@ export PowerAboveMinimumVariable
export PhaseShifterAngle
export UpperBoundFeedForwardSlack
export LowerBoundFeedForwardSlack
+export InterfaceFlowSlackUp
+export InterfaceFlowSlackDown
# Auxiliary variables
export TimeDurationOn
@@ -243,10 +244,10 @@ export PowerFlowLineActivePower
# Constraints
export AbsoluteValueConstraint
+export LineFlowBoundConstraint
export ActivePowerVariableLimitsConstraint
export ActivePowerVariableTimeSeriesLimitsConstraint
export ActiveRangeICConstraint
-export AreaDispatchBalanceConstraint
export AreaParticipationAssignmentConstraint
export BalanceAuxConstraint
export CommitmentConstraint
@@ -254,7 +255,7 @@ export CopperPlateBalanceConstraint
export DurationConstraint
export EnergyBalanceConstraint
export EqualityConstraint
-export FeedforwardSemiContinousConstraint
+export FeedforwardSemiContinuousConstraint
export FeedforwardUpperBoundConstraint
export FeedforwardLowerBoundConstraint
export FeedforwardIntegralLimitConstraint
@@ -273,6 +274,7 @@ export FlowReactivePowerToFromConstraint
export FrequencyResponseConstraint
export HVDCPowerBalance
export HVDCLosses
+export HVDCFlowDirectionVariable
export InputActivePowerVariableLimitsConstraint
export NetworkFlowConstraint
export NodalBalanceActiveConstraint
@@ -321,9 +323,7 @@ export EmergencyDown
export RawACE
export ProductionCostExpression
export ActivePowerRangeExpressionLB
-export ReserveRangeExpressionLB
export ActivePowerRangeExpressionUB
-export ReserveRangeExpressionUB
#################################################################################
# Imports
@@ -347,11 +347,58 @@ import PowerNetworkMatrices: PTDF, VirtualPTDF
export PTDF
export VirtualPTDF
import InfrastructureSystems: @assert_op, list_recorder_events, get_name
+
+# IS.Optimization imports: functions that have PSY methods that IS needs to access (therefore necessary)
+import InfrastructureSystems.Optimization: get_data_field
+
+# IS.Optimization imports that get reexported: no additional methods in PowerSimulations (therefore necessary)
+import InfrastructureSystems.Optimization:
+ OptimizationProblemResults, OptimizationProblemResultsExport, OptimizerStats
+import InfrastructureSystems.Optimization:
+ read_variables, read_duals, read_parameters, read_aux_variables, read_expressions
+import InfrastructureSystems.Optimization: get_variable_values, get_dual_values,
+ get_parameter_values, get_aux_variable_values, get_expression_values, get_value
+import InfrastructureSystems.Optimization:
+ get_objective_value, export_realized_results, export_optimizer_stats
+
+# IS.Optimization imports that get reexported: yes additional methods in PowerSimulations (therefore may or may not be desired)
+import InfrastructureSystems.Optimization:
+ read_variable, read_dual, read_parameter, read_aux_variable, read_expression
+import InfrastructureSystems.Optimization: list_variable_keys, list_dual_keys,
+ list_parameter_keys, list_aux_variable_keys, list_expression_keys
+import InfrastructureSystems.Optimization: list_variable_names, list_dual_names,
+ list_parameter_names, list_aux_variable_names, list_expression_names
+import InfrastructureSystems.Optimization: read_optimizer_stats, get_optimizer_stats,
+ export_results, serialize_results, get_timestamps, get_model_base_power
+import InfrastructureSystems.Optimization: get_resolution, get_forecast_horizon
+
+# IS.Optimization imports that stay private, may or may not be additional methods in PowerSimulations
+import InfrastructureSystems.Optimization: ArgumentConstructStage, ModelConstructStage
+import InfrastructureSystems.Optimization: STORE_CONTAINERS, STORE_CONTAINER_DUALS,
+ STORE_CONTAINER_EXPRESSIONS, STORE_CONTAINER_PARAMETERS, STORE_CONTAINER_VARIABLES,
+ STORE_CONTAINER_AUX_VARIABLES
+import InfrastructureSystems.Optimization: OptimizationContainerKey, VariableKey,
+ ConstraintKey, ExpressionKey, AuxVarKey, InitialConditionKey, ParameterKey
+import InfrastructureSystems.Optimization:
+ RightHandSideParameter, ObjectiveFunctionParameter, TimeSeriesParameter
+import InfrastructureSystems.Optimization: VariableType, ConstraintType, AuxVariableType,
+ ParameterType, InitialConditionType, ExpressionType
+import InfrastructureSystems.Optimization: should_export_variable, should_export_dual,
+ should_export_parameter, should_export_aux_variable, should_export_expression
+import InfrastructureSystems.Optimization:
+ get_entry_type, get_component_type, get_output_dir
+import InfrastructureSystems.Optimization: read_results_with_keys, deserialize_key,
+ encode_key_as_string, encode_keys_as_strings, should_write_resulting_value,
+ convert_result_to_natural_units, to_matrix, get_store_container_type
+
+# IS.Optimization imports that stay private, may or may not be additional methods in PowerSimulations
+
export get_name
export get_model_base_power
export get_optimizer_stats
export get_timestamps
export get_resolution
+
import PowerModels
import TimerOutputs
import ProgressMeter
@@ -372,7 +419,6 @@ import TimeSeries
import DataFrames
import JSON
import CSV
-import SHA
import HDF5
import PrettyTables
@@ -427,9 +473,7 @@ include("core/definitions.jl")
include("core/formulations.jl")
include("core/abstract_simulation_store.jl")
include("core/operation_model_abstract_types.jl")
-include("core/optimization_container_types.jl")
include("core/abstract_feedforward.jl")
-include("core/optimization_container_keys.jl")
include("core/network_model.jl")
include("core/parameters.jl")
include("core/service_model.jl")
@@ -442,7 +486,6 @@ include("core/expressions.jl")
include("core/initial_conditions.jl")
include("core/settings.jl")
include("core/cache_utils.jl")
-include("core/optimizer_stats.jl")
include("core/dataset.jl")
include("core/dataset_container.jl")
include("core/results_by_time.jl")
@@ -454,15 +497,14 @@ include("core/optimization_container.jl")
include("core/store_common.jl")
include("initial_conditions/initial_condition_chronologies.jl")
include("operation/operation_model_interface.jl")
-include("operation/model_store_params.jl")
-include("operation/abstract_model_store.jl")
+include("core/model_store_params.jl")
+include("simulation/simulation_store_requirements.jl")
include("operation/decision_model_store.jl")
include("operation/emulation_model_store.jl")
include("operation/initial_conditions_update_in_memory_store.jl")
-include("operation/model_internal.jl")
+include("simulation/simulation_info.jl")
include("operation/decision_model.jl")
include("operation/emulation_model.jl")
-include("operation/problem_results_export.jl")
include("operation/problem_results.jl")
include("operation/operation_model_serialization.jl")
include("operation/time_series_interface.jl")
@@ -500,7 +542,11 @@ include("simulation/simulation.jl")
include("simulation/simulation_results_export.jl")
include("simulation/simulation_results.jl")
-include("devices_models/devices/common/objective_functions.jl")
+include("devices_models/devices/common/objective_function/common.jl")
+include("devices_models/devices/common/objective_function/linear_curve.jl")
+include("devices_models/devices/common/objective_function/quadratic_curve.jl")
+include("devices_models/devices/common/objective_function/market_bid.jl")
+include("devices_models/devices/common/objective_function/piecewise_linear.jl")
include("devices_models/devices/common/range_constraint.jl")
include("devices_models/devices/common/add_variable.jl")
include("devices_models/devices/common/add_auxiliary_variable.jl")
@@ -517,12 +563,13 @@ include("devices_models/devices/renewable_generation.jl")
include("devices_models/devices/thermal_generation.jl")
include("devices_models/devices/electric_loads.jl")
include("devices_models/devices/AC_branches.jl")
+include("devices_models/devices/area_interchange.jl")
include("devices_models/devices/TwoTerminalDC_branches.jl")
include("devices_models/devices/HVDCsystems.jl")
-include("devices_models/devices/regulation_device.jl")
+#include("devices_models/devices/regulation_device.jl")
# Services Models
-include("services_models/agc.jl")
+#include("services_models/agc.jl")
include("services_models/reserves.jl")
include("services_models/reserve_group.jl")
include("services_models/transmission_interface.jl")
@@ -547,7 +594,7 @@ include("devices_models/device_constructors/hvdcsystems_constructor.jl")
include("devices_models/device_constructors/branch_constructor.jl")
include("devices_models/device_constructors/renewablegeneration_constructor.jl")
include("devices_models/device_constructors/load_constructor.jl")
-include("devices_models/device_constructors/regulationdevice_constructor.jl")
+#include("devices_models/device_constructors/regulationdevice_constructor.jl")
# Network constructors
include("network_models/network_constructor.jl")
diff --git a/src/core/auxiliary_variables.jl b/src/core/auxiliary_variables.jl
index c8c37345dc..a595311368 100644
--- a/src/core/auxiliary_variables.jl
+++ b/src/core/auxiliary_variables.jl
@@ -1,21 +1,3 @@
-struct AuxVarKey{T <: AuxVariableType, U <: PSY.Component} <: OptimizationContainerKey
- meta::String
-end
-
-function AuxVarKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: AuxVariableType, U <: PSY.Component}
- if isabstracttype(U)
- error("Type $U can't be abstract")
- end
- return AuxVarKey{T, U}(meta)
-end
-
-get_entry_type(::AuxVarKey{T, U}) where {T <: AuxVariableType, U <: PSY.Component} = T
-get_component_type(::AuxVarKey{T, U}) where {T <: AuxVariableType, U <: PSY.Component} = U
-
"""
Auxiliary Variable for Thermal Generation Models to keep track of time elapsed on
"""
@@ -54,6 +36,7 @@ struct PowerFlowLineActivePower <: AuxVariableType end
should_write_resulting_value(::Type{<:AuxVariableType}) = true
convert_result_to_natural_units(::Type{<:AuxVariableType}) = false
+
convert_result_to_natural_units(::Type{PowerOutput}) = true
convert_result_to_natural_units(::Type{PowerFlowLineReactivePower}) = true
convert_result_to_natural_units(::Type{PowerFlowLineActivePower}) = true
diff --git a/src/core/constraints.jl b/src/core/constraints.jl
index f63b73fc79..a36e8257cd 100644
--- a/src/core/constraints.jl
+++ b/src/core/constraints.jl
@@ -1,94 +1,418 @@
-struct ConstraintKey{T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} <:
- OptimizationContainerKey
- meta::String
-end
-
-function ConstraintKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
- check_meta_chars(meta)
- return ConstraintKey{T, U}(meta)
-end
-
-get_entry_type(
- ::ConstraintKey{T, U},
-) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} = T
-get_component_type(
- ::ConstraintKey{T, U},
-) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}} = U
-
-function encode_key(key::ConstraintKey)
- return encode_symbol(get_component_type(key), get_entry_type(key), key.meta)
-end
-
-Base.convert(::Type{ConstraintKey}, name::Symbol) = ConstraintKey(decode_symbol(name)...)
-
struct AbsoluteValueConstraint <: ConstraintType end
+"""
+Struct to create the constraint for starting up ThermalMultiStart units.
+For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.
+
+The specified constraint is formulated as:
+
+```math
+\\max\\{P^\\text{th,max} - P^\\text{th,shdown}, 0\\} \\cdot w_1^\\text{th} \\le u^\\text{th,init} (P^\\text{th,max} - P^\\text{th,min}) - P^\\text{th,init}
+```
+"""
struct ActiveRangeICConstraint <: ConstraintType end
-struct AreaDispatchBalanceConstraint <: ConstraintType end
+"""
+Struct to create the constraint to balance power across specified areas.
+For more information check [Network Formulations](@ref network_formulations).
+
+The specified constraint is generally formulated as:
+
+```math
+\\sum_{c \\in \\text{components}_a} p_t^c = 0, \\quad \\forall a\\in \\{1,\\dots, A\\}, t \\in \\{1, \\dots, T\\}
+```
+"""
struct AreaParticipationAssignmentConstraint <: ConstraintType end
struct BalanceAuxConstraint <: ConstraintType end
+"""
+Struct to create the commitment constraint between the on, start, and stop variables.
+For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations).
+
+The specified constraints are formulated as:
+
+```math
+u_1^\\text{th} = u^\\text{th,init} + v_1^\\text{th} - w_1^\\text{th} \\\\
+u_t^\\text{th} = u_{t-1}^\\text{th} + v_t^\\text{th} - w_t^\\text{th}, \\quad \\forall t \\in \\{2,\\dots,T\\} \\\\
+v_t^\\text{th} + w_t^\\text{th} \\le 1, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct CommitmentConstraint <: ConstraintType end
+"""
+Struct to create the constraint to balance power in the copperplate model.
+For more information check [Network Formulations](@ref network_formulations).
+
+The specified constraint is generally formulated as:
+
+```math
+\\sum_{c \\in \\text{components}} p_t^c = 0, \\quad \\forall t \\in \\{1, \\dots, T\\}
+```
+"""
struct CopperPlateBalanceConstraint <: ConstraintType end
+"""
+Struct to create the duration constraint for commitment formulations, i.e. min-up and min-down.
+
+For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations).
+"""
struct DurationConstraint <: ConstraintType end
struct EnergyBalanceConstraint <: ConstraintType end
+
+"""
+Struct to create the constraint that sets the reactive power to the power factor
+in the RenewableConstantPowerFactor formulation for renewable units.
+
+For more information check [RenewableGen Formulations](@ref PowerSystems.RenewableGen-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+q_t^\\text{re} = \\text{pf} \\cdot p_t^\\text{re}, \\quad \\forall t \\in \\{1,\\dots, T\\}
+```
+"""
struct EqualityConstraint <: ConstraintType end
-struct FeedforwardSemiContinousConstraint <: ConstraintType end
+"""
+Struct to create the constraint for semicontinuous feedforward limits.
+
+For more information check [Feedforward Formulations](@ref ff_formulations).
+
+The specified constraint is formulated as:
+
+```math
+\\begin{align*}
+& \\text{ActivePowerRangeExpressionUB}_t := p_t^\\text{th} - \\text{on}_t^\\text{th}P^\\text{th,max} \\le 0, \\quad \\forall t\\in \\{1, \\dots, T\\} \\\\
+& \\text{ActivePowerRangeExpressionLB}_t := p_t^\\text{th} - \\text{on}_t^\\text{th}P^\\text{th,min} \\ge 0, \\quad \\forall t\\in \\{1, \\dots, T\\}
+\\end{align*}
+```
+"""
+struct FeedforwardSemiContinuousConstraint <: ConstraintType end
struct FeedforwardIntegralLimitConstraint <: ConstraintType end
+"""
+Struct to create the constraint for upper bound feedforward limits.
+
+For more information check [Feedforward Formulations](@ref ff_formulations).
+
+The specified constraint is formulated as:
+
+```math
+\\begin{align*}
+& \\text{AffectedVariable}_t - p_t^\\text{ff,ubsl} \\le \\text{SourceVariableParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}
+\\end{align*}
+```
+"""
struct FeedforwardUpperBoundConstraint <: ConstraintType end
+"""
+Struct to create the constraint for lower bound feedforward limits.
+
+For more information check [Feedforward Formulations](@ref ff_formulations).
+
+The specified constraint is formulated as:
+
+```math
+\\begin{align*}
+& \\text{AffectedVariable}_t + p_t^\\text{ff,lbsl} \\ge \\text{SourceVariableParameter}_t, \\quad \\forall t \\in \\{1,\\dots, T\\}
+\\end{align*}
+```
+"""
struct FeedforwardLowerBoundConstraint <: ConstraintType end
struct FeedforwardEnergyTargetConstraint <: ConstraintType end
struct FlowActivePowerConstraint <: ConstraintType end #not being used
struct FlowActivePowerFromToConstraint <: ConstraintType end #not being used
struct FlowActivePowerToFromConstraint <: ConstraintType end #not being used
-struct FlowLimitConstraint <: ConstraintType end #not being used
+"""
+Struct to create the constraint that set the flow limits through a PhaseShiftingTransformer.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+-R^\\text{max} \\le f_t \\le R^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
+struct FlowLimitConstraint <: ConstraintType end
struct FlowLimitFromToConstraint <: ConstraintType end
struct FlowLimitToFromConstraint <: ConstraintType end
+"""
+Struct to create the constraint that set the flow limits through an HVDC two-terminal branch.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+R^\\text{min} \\le f_t \\le R^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct FlowRateConstraint <: ConstraintType end
+"""
+Struct to create the constraint that set the flow from-to limits through an HVDC two-terminal branch.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+R^\\text{from,min} \\le f_t^\\text{from-to} \\le R^\\text{from,max}, \\forall t \\in \\{1,\\dots, T\\}
+```
+"""
struct FlowRateConstraintFromTo <: ConstraintType end
+"""
+Struct to create the constraint that set the flow to-from limits through an HVDC two-terminal branch.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+R^\\text{to,min} \\le f_t^\\text{to-from} \\le R^\\text{to,max},\\quad \\forall t \\in \\{1,\\dots, T\\}
+```
+"""
struct FlowRateConstraintToFrom <: ConstraintType end
struct FlowReactivePowerConstraint <: ConstraintType end #not being used
struct FlowReactivePowerFromToConstraint <: ConstraintType end #not being used
struct FlowReactivePowerToFromConstraint <: ConstraintType end #not being used
+"""
+Struct to create the constraints that set the power balance across a lossy HVDC two-terminal line.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraints are formulated as:
+
+```math
+\\begin{align*}
+& f_t^\\text{to-from} - f_t^\\text{from-to} \\le L_1 \\cdot f_t^\\text{to-from} - L_0,\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+& f_t^\\text{from-to} - f_t^\\text{to-from} \\ge L_1 \\cdot f_t^\\text{from-to} + L_0,\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+& f_t^\\text{from-to} - f_t^\\text{to-from} \\ge - M^\\text{big} (1 - u^\\text{dir}_t),\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+& f_t^\\text{to-from} - f_t^\\text{from-to} \\ge - M^\\text{big} u^\\text{dir}_t,\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+\\end{align*}
+```
+"""
struct HVDCPowerBalance <: ConstraintType end
struct FrequencyResponseConstraint <: ConstraintType end
+"""
+Struct to create the constraint the AC branch flows depending on the network model.
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint depends on the network model chosen. The most common application is the StaticBranch in a PTDF Network Model:
+
+```math
+f_t = \\sum_{i=1}^N \\text{PTDF}_{i,b} \\cdot \\text{Bal}_{i,t}, \\quad \\forall t \\in \\{1,\\dots, T\\}
+```
+"""
struct NetworkFlowConstraint <: ConstraintType end
+"""
+Struct to create the constraint to balance active power in nodal formulation.
+For more information check [Network Formulations](@ref network_formulations).
+
+The specified constraint depends on the network model chosen.
+"""
struct NodalBalanceActiveConstraint <: ConstraintType end
+"""
+Struct to create the constraint to balance reactive power in nodal formulation.
+For more information check [Network Formulations](@ref network_formulations).
+
+The specified constraint depends on the network model chosen.
+"""
struct NodalBalanceReactiveConstraint <: ConstraintType end
struct ParticipationAssignmentConstraint <: ConstraintType end
+"""
+Struct to create the constraint to participation assignments limits in the active power reserves.
+For more information check [Service Formulations](@ref service_formulations).
+
+The constraint is as follows:
+
+```math
+r_{d,t} \\le \\text{Req} \\cdot \\text{PF} ,\\quad \\forall d\\in \\mathcal{D}_s, \\forall t\\in \\{1,\\dots, T\\} \\quad \\text{(for a ConstantReserve)} \\\\
+r_{d,t} \\le \\text{RequirementTimeSeriesParameter}_{t} \\cdot \\text{PF}\\quad \\forall d\\in \\mathcal{D}_s, \\forall t\\in \\{1,\\dots, T\\}, \\quad \\text{(for a VariableReserve)}
+```
+"""
struct ParticipationFractionConstraint <: ConstraintType end
+"""
+Struct to create the PieceWiseLinearCostConstraint associated with a specified variable.
+
+See [Piecewise linear cost functions](@ref pwl_cost) for more information.
+"""
struct PieceWiseLinearCostConstraint <: ConstraintType end
+
+"""
+Struct to create the PieceWiseLinearBlockOfferConstraint associated with a specified variable.
+
+See [Piecewise linear cost functions](@ref pwl_cost) for more information.
+"""
+struct PieceWiseLinearBlockOfferConstraint <: ConstraintType end
+
+"""
+Struct to create the PieceWiseLinearUpperBoundConstraint associated with a specified variable.
+
+See [Piecewise linear cost functions](@ref pwl_cost) for more information.
+"""
+struct PieceWiseLinearUpperBoundConstraint <: ConstraintType end
+
+"""
+Struct to create the RampConstraint associated with a specified thermal device or reserve service.
+
+For thermal units, see more information in [Thermal Formulations](@ref ThermalGen-Formulations). The constraint is as follows:
+```math
+-R^\\text{th,dn} \\le p_t^\\text{th} - p_{t-1}^\\text{th} \\le R^\\text{th,up}, \\quad \\forall t\\in \\{1, \\dots, T\\}
+```
+
+For Ramp Reserve, see more information in [Service Formulations](@ref service_formulations). The constraint is as follows:
+
+```math
+r_{d,t} \\le R^\\text{th,up} \\cdot \\text{TF}\\quad \\forall d\\in \\mathcal{D}_s, \\forall t\\in \\{1,\\dots, T\\}, \\quad \\text{(for ReserveUp)} \\\\
+r_{d,t} \\le R^\\text{th,dn} \\cdot \\text{TF}\\quad \\forall d\\in \\mathcal{D}_s, \\forall t\\in \\{1,\\dots, T\\}, \\quad \\text{(for ReserveDown)}
+```
+"""
struct RampConstraint <: ConstraintType end
struct RampLimitConstraint <: ConstraintType end
struct RangeLimitConstraint <: ConstraintType end
+"""
+Struct to create the constraint that set the AC flow limits through branches.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+\\begin{align*}
+& f_t - f_t^\\text{sl,up} \\le R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+& f_t + f_t^\\text{sl,lo} \\ge -R^\\text{max},\\quad \\forall t \\in \\{1,\\dots, T\\}
+\\end{align*}
+```
+"""
struct RateLimitConstraint <: ConstraintType end
struct RateLimitConstraintFromTo <: ConstraintType end
struct RateLimitConstraintToFrom <: ConstraintType end
struct RegulationLimitsConstraint <: ConstraintType end
+"""
+Struct to create the constraint for satisfying active power reserve requirements.
+For more information check [Service Formulations](@ref service_formulations).
+
+The constraint is as follows:
+
+```math
+\\sum_{d\\in\\mathcal{D}_s} r_{d,t} + r_t^\\text{sl} \\ge \\text{Req},\\quad \\forall t\\in \\{1,\\dots, T\\} \\quad \\text{(for a ConstantReserve)} \\\\
+\\sum_{d\\in\\mathcal{D}_s} r_{d,t} + r_t^\\text{sl} \\ge \\text{RequirementTimeSeriesParameter}_{t},\\quad \\forall t\\in \\{1,\\dots, T\\} \\quad \\text{(for a VariableReserve)}
+```
+"""
struct RequirementConstraint <: ConstraintType end
struct ReserveEnergyCoverageConstraint <: ConstraintType end
+"""
+Struct to create the constraint for ensuring that NonSpinning Reserve can be delivered from turn-off thermal units.
+
+For more information check [Service Formulations](@ref service_formulations) for NonSpinningReserve.
+
+The constraint is as follows:
+
+```math
+r_{d,t} \\le (1 - u_{d,t}^\\text{th}) \\cdot R^\\text{limit}_d, \\quad \\forall d \\in \\mathcal{D}_s, \\forall t \\in \\{1,\\dots, T\\}
+```
+"""
struct ReservePowerConstraint <: ConstraintType end
struct SACEPIDAreaConstraint <: ConstraintType end
struct StartTypeConstraint <: ConstraintType end
+"""
+Struct to create the start-up initial condition constraints for ThermalMultiStart.
+
+For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.
+"""
struct StartupInitialConditionConstraint <: ConstraintType end
+"""
+Struct to create the start-up time limit constraints for ThermalMultiStart.
+
+For more information check [ThermalGen Formulations](@ref ThermalGen-Formulations) for ThermalMultiStartUnitCommitment.
+"""
struct StartupTimeLimitTemperatureConstraint <: ConstraintType end
+"""
+Struct to create the constraint that set the angle limits through a PhaseShiftingTransformer.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraint is formulated as:
+
+```math
+\\Theta^\\text{min} \\le \\theta^\\text{shift}_t \\le \\Theta^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct PhaseAngleControlLimit <: ConstraintType end
+"""
+Struct to create the constraints that set the losses through a lossy HVDC two-terminal line.
+
+For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
+
+The specified constraints are formulated as:
+
+```math
+\\begin{align*}
+& f_t^\\text{to-from} - f_t^\\text{from-to} \\le \\ell_t,\\quad \\forall t \\in \\{1,\\dots, T\\} \\\\
+& f_t^\\text{from-to} - f_t^\\text{to-from} \\le \\ell_t,\\quad \\forall t \\in \\{1,\\dots, T\\}
+\\end{align*}
+```
+"""
struct HVDCLossesAbsoluteValue <: ConstraintType end
struct HVDCDirection <: ConstraintType end
struct InterfaceFlowLimit <: ConstraintType end
abstract type PowerVariableLimitsConstraint <: ConstraintType end
+"""
+Struct to create the constraint to limit active power input expressions.
+For more information check [Device Formulations](@ref formulation_intro).
+
+The specified constraint depends on the UpperBound and LowerBound expressions, but
+in its most basic formulation is of the form:
+
+```math
+P^\\text{min} \\le p_t^\\text{in} \\le P^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct InputActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end
+"""
+Struct to create the constraint to limit active power output expressions.
+For more information check [Device Formulations](@ref formulation_intro).
+
+The specified constraint depends on the UpperBound and LowerBound expressions, but
+in its most basic formulation is of the form:
+
+```math
+P^\\text{min} \\le p_t^\\text{out} \\le P^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct OutputActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end
+"""
+Struct to create the constraint to limit active power expressions.
+For more information check [Device Formulations](@ref formulation_intro).
+
+The specified constraint depends on the UpperBound and LowerBound expressions, but
+in its most basic formulation is of the form:
+
+```math
+P^\\text{min} \\le p_t \\le P^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct ActivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end
+"""
+Struct to create the constraint to limit reactive power expressions.
+For more information check [Device Formulations](@ref formulation_intro).
+
+The specified constraint depends on the UpperBound and LowerBound expressions, but
+in its most basic formulation is of the form:
+
+```math
+Q^\\text{min} \\le q_t \\le Q^\\text{max}, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct ReactivePowerVariableLimitsConstraint <: PowerVariableLimitsConstraint end
+"""
+Struct to create the constraint to limit active power expressions by a time series parameter.
+For more information check [Device Formulations](@ref formulation_intro).
+
+The specified constraint depends on the UpperBound expressions, but
+in its most basic formulation is of the form:
+
+```math
+p_t \\le \\text{ActivePowerTimeSeriesParameter}_t, \\quad \\forall t \\in \\{1,\\dots,T\\}
+```
+"""
struct ActivePowerVariableTimeSeriesLimitsConstraint <: PowerVariableLimitsConstraint end
+struct LineFlowBoundConstraint <: ConstraintType end
+
abstract type EventConstraint <: ConstraintType end
struct OutageConstraint <: EventConstraint end
-
-# These apply to the processing of constraint duals
-should_write_resulting_value(::Type{<:ConstraintType}) = true
-convert_result_to_natural_units(::Type{<:ConstraintType}) = false
diff --git a/src/core/dataset.jl b/src/core/dataset.jl
index f263dfb711..a087455870 100644
--- a/src/core/dataset.jl
+++ b/src/core/dataset.jl
@@ -313,6 +313,6 @@ end
function set_value!(s::HDF5Dataset, vals, index::Int)
# Temporary while there is no implementation of caching of em_data
- _write_dataset!(s.values, vals, index:index)
+ _write_dataset!(s.values, vals, index)
return
end
diff --git a/src/core/dataset_container.jl b/src/core/dataset_container.jl
index 35f1c15a7b..8976aa79d3 100644
--- a/src/core/dataset_container.jl
+++ b/src/core/dataset_container.jl
@@ -177,7 +177,10 @@ function get_dataset_values(
return get_dataset_value(get_dataset(container, key), date)
end
-function get_last_recorded_row(container::DatasetContainer, key::OptimizationContainerKey)
+function get_last_recorded_row(
+ container::DatasetContainer,
+ key::OptimizationContainerKey,
+)
return get_last_recorded_row(get_dataset(container, key))
end
@@ -198,7 +201,10 @@ function get_last_updated_timestamp(
return get_last_updated_timestamp(get_dataset(container, key))
end
-function get_last_update_value(container::DatasetContainer, key::OptimizationContainerKey)
+function get_last_update_value(
+ container::DatasetContainer,
+ key::OptimizationContainerKey,
+)
return get_last_recorded_value(get_dataset(container, key))
end
diff --git a/src/core/definitions.jl b/src/core/definitions.jl
index 52af4c8103..74de4d65f6 100644
--- a/src/core/definitions.jl
+++ b/src/core/definitions.jl
@@ -15,6 +15,15 @@ const GAE = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}
const JuMPAffineExpressionArray = Matrix{GAE}
const JuMPAffineExpressionVector = Vector{GAE}
const JuMPConstraintArray = DenseAxisArray{JuMP.ConstraintRef}
+const JuMPAffineExpressionDArray = JuMP.Containers.DenseAxisArray{
+ JuMP.AffExpr,
+ 2,
+ Tuple{Vector{Int64}, UnitRange{Int64}},
+ Tuple{
+ JuMP.Containers._AxisLookup{Dict{Int64, Int64}},
+ JuMP.Containers._AxisLookup{Tuple{Int64, Int64}},
+ },
+}
const JuMPVariableMatrix = DenseAxisArray{
JuMP.VariableRef,
2,
@@ -30,23 +39,26 @@ const JuMPVariableArray = DenseAxisArray{JuMP.VariableRef}
const TwoTerminalHVDCTypes = Union{PSY.TwoTerminalHVDCLine, PSY.TwoTerminalVSCDCLine}
# Settings constants
-const UNSET_HORIZON = 0
+const UNSET_HORIZON = Dates.Millisecond(0)
+const UNSET_RESOLUTION = Dates.Millisecond(0)
const UNSET_INI_TIME = Dates.DateTime(0)
# Tolerance of comparisons
# MIP gap tolerances in most solvers are set to 1e-4
const ABSOLUTE_TOLERANCE = 1.0e-3
const BALANCE_SLACK_COST = 1e6
+const CONSTRAINT_VIOLATION_SLACK_COST = 2e5
const SERVICES_SLACK_COST = 1e5
const COST_EPSILON = 1e-3
const MISSING_INITIAL_CONDITIONS_TIME_COUNT = 999.0
const SECONDS_IN_MINUTE = 60.0
const MINUTES_IN_HOUR = 60.0
const SECONDS_IN_HOUR = 3600.0
+const MILLISECONDS_IN_HOUR = 3600000.0
const MAX_START_STAGES = 3
const OBJECTIVE_FUNCTION_POSITIVE = 1.0
const OBJECTIVE_FUNCTION_NEGATIVE = -1.0
-const INITIALIZATION_PROBLEM_HORIZON = 3
+const INITIALIZATION_PROBLEM_HORIZON_COUNT = 3
# The DEFAULT_RESERVE_COST value is used to avoid degeneracy of the solutions, reserve cost isn't provided.
const DEFAULT_RESERVE_COST = 1.0
const KiB = 1024
@@ -58,7 +70,6 @@ const PSI_NAME_DELIMITER = "__"
const M_VALUE = 1e6
const NO_SERVICE_NAME_PROVIDED = ""
-const CONTAINER_KEY_EMPTY_META = ""
const UPPER_BOUND = "ub"
const LOWER_BOUND = "lb"
const MAX_OPTIMIZE_TRIES = 2
@@ -66,7 +77,6 @@ const MAX_OPTIMIZE_TRIES = 2
# File Names definitions
const PROBLEM_SERIALIZATION_FILENAME = "operation_problem.bin"
const PROBLEM_LOG_FILENAME = "operation_problem.log"
-const HASH_FILENAME = "check.sha256"
const SIMULATION_SERIALIZATION_FILENAME = "simulation.bin"
const SIMULATION_LOG_FILENAME = "simulation.log"
const REQUIRED_RECORDERS = (:simulation_status, :execution)
@@ -80,23 +90,27 @@ const KNOWN_SIMULATION_PATHS = [
"simulation_files",
"simulation_partitions",
]
+"If the name of an extraneous file that appears in simulation results matches one of these regexes, it is safe to ignore"
+const IGNORABLE_FILES = [
+ r"^\.DS_Store$",
+ r"^\.Trashes$",
+ r"^\.Trash-.*$",
+ r"^\.nfs.*$",
+ r"^[Dd]esktop.ini$",
+]
const RESULTS_DIR = "results"
# Enums
-IS.@scoped_enum(BuildStatus, IN_PROGRESS = -1, BUILT = 0, FAILED = 1, EMPTY = 2,)
-IS.@scoped_enum(
- RunStatus,
- NOT_READY = -2,
- READY = -1,
- SUCCESSFUL = 0,
- RUNNING = 1,
- FAILED = 2,
-)
+ModelBuildStatus = IS.Optimization.ModelBuildStatus
+SimulationBuildStatus = IS.Simulation.SimulationBuildStatus
+
+RunStatus = IS.Simulation.RunStatus
+
IS.@scoped_enum(SOSStatusVariable, NO_VARIABLE = 1, PARAMETER = 2, VARIABLE = 3,)
IS.@scoped_enum(COMPACT_PWL_STATUS, VALID = 1, INVALID = 2, UNDETERMINED = 3)
-const ENUMS = (BuildStatus, RunStatus, SOSStatusVariable)
+const ENUMS = (ModelBuildStatus, SimulationBuildStatus, RunStatus, SOSStatusVariable)
const ENUM_MAPPINGS = Dict()
@@ -107,6 +121,10 @@ for enum in ENUMS
end
end
+# Special cases for backwards compatibility
+ENUM_MAPPINGS[RunStatus]["ready"] = RunStatus.INITIALIZED
+ENUM_MAPPINGS[RunStatus]["successful"] = RunStatus.SUCCESSFULLY_FINALIZED
+
"""
Get the enum value for the string. Case insensitive.
"""
@@ -123,21 +141,8 @@ function get_enum_value(enum, value::String)
return ENUM_MAPPINGS[enum][val]
end
-Base.convert(::Type{BuildStatus}, val::String) = get_enum_value(BuildStatus, val)
+Base.convert(::Type{SimulationBuildStatus}, val::String) =
+ get_enum_value(SimulationBuildStatus, val)
+Base.convert(::Type{ModelBuildStatus}, val::String) = get_enum_value(ModelBuildStatus, val)
Base.convert(::Type{RunStatus}, val::String) = get_enum_value(RunStatus, val)
Base.convert(::Type{SOSStatusVariable}, x::String) = get_enum_value(SOSStatusVariable, x)
-
-# Store const definitions
-# Update src/simulation/simulation_store_common.jl with any changes.
-const STORE_CONTAINER_DUALS = :duals
-const STORE_CONTAINER_PARAMETERS = :parameters
-const STORE_CONTAINER_VARIABLES = :variables
-const STORE_CONTAINER_AUX_VARIABLES = :aux_variables
-const STORE_CONTAINER_EXPRESSIONS = :expressions
-const STORE_CONTAINERS = (
- STORE_CONTAINER_DUALS,
- STORE_CONTAINER_PARAMETERS,
- STORE_CONTAINER_VARIABLES,
- STORE_CONTAINER_AUX_VARIABLES,
- STORE_CONTAINER_EXPRESSIONS,
-)
diff --git a/src/core/expressions.jl b/src/core/expressions.jl
index ab981f4f45..f5ad354a7d 100644
--- a/src/core/expressions.jl
+++ b/src/core/expressions.jl
@@ -1,34 +1,3 @@
-struct ExpressionKey{T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} <:
- OptimizationContainerKey
- meta::String
-end
-
-function ExpressionKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}
- if isabstracttype(U)
- error("Type $U can't be abstract")
- end
- check_meta_chars(meta)
- return ExpressionKey{T, U}(meta)
-end
-
-get_entry_type(
- ::ExpressionKey{T, U},
-) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} = T
-
-get_component_type(
- ::ExpressionKey{T, U},
-) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}} = U
-
-function encode_key(key::ExpressionKey)
- return encode_symbol(get_component_type(key), get_entry_type(key), key.meta)
-end
-
-Base.convert(::Type{ExpressionKey}, name::Symbol) = ExpressionKey(decode_symbol(name)...)
-
abstract type SystemBalanceExpressions <: ExpressionType end
abstract type RangeConstraintLBExpressions <: ExpressionType end
abstract type RangeConstraintUBExpressions <: ExpressionType end
@@ -40,19 +9,16 @@ struct EmergencyDown <: ExpressionType end
struct RawACE <: ExpressionType end
struct ProductionCostExpression <: CostExpressions end
struct ActivePowerRangeExpressionLB <: RangeConstraintLBExpressions end
-struct ComponentActivePowerRangeExpressionLB <: RangeConstraintLBExpressions end
-struct ReserveRangeExpressionLB <: RangeConstraintLBExpressions end
struct ActivePowerRangeExpressionUB <: RangeConstraintUBExpressions end
-struct ReserveRangeExpressionUB <: RangeConstraintUBExpressions end
-struct ComponentActivePowerRangeExpressionUB <: RangeConstraintUBExpressions end
struct ComponentReserveUpBalanceExpression <: ExpressionType end
struct ComponentReserveDownBalanceExpression <: ExpressionType end
struct InterfaceTotalFlow <: ExpressionType end
+struct PTDFBranchFlow <: ExpressionType end
-should_write_resulting_value(::Type{<:ExpressionType}) = false
should_write_resulting_value(::Type{<:CostExpressions}) = true
should_write_resulting_value(::Type{InterfaceTotalFlow}) = true
should_write_resulting_value(::Type{RawACE}) = true
+should_write_resulting_value(::Type{ActivePowerBalance}) = true
+should_write_resulting_value(::Type{ReactivePowerBalance}) = true
-convert_result_to_natural_units(::Type{<:ExpressionType}) = false
convert_result_to_natural_units(::Type{InterfaceTotalFlow}) = true
diff --git a/src/core/formulations.jl b/src/core/formulations.jl
index b9ea8a7748..1b6ddb109a 100644
--- a/src/core/formulations.jl
+++ b/src/core/formulations.jl
@@ -33,7 +33,7 @@ Formulation type to enable standard dispatch with a range and enforce intertempo
"""
struct ThermalStandardDispatch <: AbstractThermalDispatchFormulation end
"""
-Formulation type to enable basic dispatch without any intertemporal constraints and relaxed minimum generation. *may not work with PWL cost definitions*
+Formulation type to enable basic dispatch without any intertemporal constraints and relaxed minimum generation. *May not work with non-convex PWL cost definitions*
"""
struct ThermalDispatchNoMin <: AbstractThermalDispatchFormulation end
"""
@@ -58,7 +58,7 @@ abstract type AbstractLoadFormulation <: AbstractDeviceFormulation end
abstract type AbstractControllablePowerLoadFormulation <: AbstractLoadFormulation end
"""
-Formulation type to add a time series parameter for non-dispatchable `ElectricLoad` withdrawls to power balance constraints
+Formulation type to add a time series parameter for non-dispatchable `ElectricLoad` withdrawals to power balance constraints
"""
struct StaticPowerLoad <: AbstractLoadFormulation end
@@ -145,17 +145,31 @@ LossLess InterconnectingConverter Model
"""
struct LossLessConverter <: AbstractConverterFormulation end
-# TODO: Think if this an ok abstraction for future use cases
+"""
+LossLess Line Abstract Model
+"""
struct LossLessLine <: AbstractBranchFormulation end
############################## Network Model Formulations ##################################
# These formulations are taken directly from PowerModels
abstract type AbstractPTDFModel <: PM.AbstractDCPModel end
+"""
+Linear active power approximation using the power transfer distribution factor [PTDF](https://nrel-sienna.github.io/PowerNetworkMatrices.jl/stable/tutorials/tutorial_PTDF_matrix/) matrix.
+"""
struct PTDFPowerModel <: AbstractPTDFModel end
-
+"""
+Infinite capacity approximation of network flow to represent entire system with a single node.
+"""
struct CopperPlatePowerModel <: PM.AbstractActivePowerModel end
+"""
+Approximation to represent inter-area flow with each area represented as a single node.
+"""
struct AreaBalancePowerModel <: PM.AbstractActivePowerModel end
+"""
+Linear active power approximation using the power transfer distribution factor [PTDF](https://nrel-sienna.github.io/PowerNetworkMatrices.jl/stable/tutorials/tutorial_PTDF_matrix/) matrix. Balacing areas independently.
+"""
+struct AreaPTDFPowerModel <: AbstractPTDFModel end
#================================================
# exact non-convex models
@@ -219,10 +233,32 @@ abstract type AbstractAGCFormulation <: AbstractServiceFormulation end
struct PIDSmoothACE <: AbstractAGCFormulation end
+"""
+Struct to add reserves to be larger than a specified requirement for an aggregated collection of services
+"""
struct GroupReserve <: AbstractReservesFormulation end
+
+"""
+Struct for to add reserves to be larger than a specified requirement
+"""
struct RangeReserve <: AbstractReservesFormulation end
+"""
+Struct for to add reserves to be larger than a variable requirement depending of costs
+"""
struct StepwiseCostReserve <: AbstractReservesFormulation end
+"""
+Struct to add reserves to be larger than a specified requirement, with ramp constraints
+"""
struct RampReserve <: AbstractReservesFormulation end
+"""
+Struct to add non spinning reserve requirements larger than specified requirement
+"""
struct NonSpinningReserve <: AbstractReservesFormulation end
-
+"""
+Struct to add a constant maximum transmission flow for specified interface
+"""
struct ConstantMaxInterfaceFlow <: AbstractServiceFormulation end
+"""
+Struct to add a variable maximum transmission flow for specified interface
+"""
+struct VariableMaxInterfaceFlow <: AbstractServiceFormulation end
diff --git a/src/core/initial_conditions.jl b/src/core/initial_conditions.jl
index 59a2fdc68a..329cb47c94 100644
--- a/src/core/initial_conditions.jl
+++ b/src/core/initial_conditions.jl
@@ -1,21 +1,3 @@
-struct ICKey{T <: InitialConditionType, U <: PSY.Component} <: OptimizationContainerKey
- meta::String
-end
-
-function ICKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: InitialConditionType, U <: PSY.Component}
- if isabstracttype(U)
- error("Type $U can't be abstract")
- end
- return ICKey{T, U}(meta)
-end
-
-get_entry_type(::ICKey{T, U}) where {T <: InitialConditionType, U <: PSY.Component} = T
-get_component_type(::ICKey{T, U}) where {T <: InitialConditionType, U <: PSY.Component} = U
-
"""
Container for the initial condition data
"""
@@ -36,7 +18,7 @@ function InitialCondition(
end
function InitialCondition(
- ::ICKey{T, U},
+ ::InitialConditionKey{T, U},
component::U,
value::V,
) where {
@@ -159,3 +141,14 @@ struct InitialTimeDurationOn <: InitialConditionType end
struct InitialTimeDurationOff <: InitialConditionType end
struct InitialEnergyLevel <: InitialConditionType end
struct AreaControlError <: InitialConditionType end
+
+# Decide whether to run the initial conditions reconciliation algorithm based on the presence of any of these
+requires_reconciliation(::Type{<:InitialConditionType}) = false
+
+requires_reconciliation(::Type{InitialTimeDurationOn}) = true
+requires_reconciliation(::Type{InitialTimeDurationOff}) = true
+requires_reconciliation(::Type{DeviceStatus}) = true
+requires_reconciliation(::Type{DevicePower}) = true # to capture a case when device is off in HA but producing power in ED
+requires_reconciliation(::Type{DeviceAboveMinPower}) = true # ramping limits may make power differences in thermal compact devices between models infeasible
+requires_reconciliation(::Type{InitialEnergyLevel}) = true # large differences in initial storage levels could lead to infeasibilities
+# Not requiring reconciliation for AreaControlError
diff --git a/src/core/model_store_params.jl b/src/core/model_store_params.jl
new file mode 100644
index 0000000000..f6932c47c0
--- /dev/null
+++ b/src/core/model_store_params.jl
@@ -0,0 +1,58 @@
+struct ModelStoreParams <: IS.Optimization.AbstractModelStoreParams
+ num_executions::Int
+ horizon_count::Int
+ interval::Dates.Millisecond
+ resolution::Dates.Millisecond
+ base_power::Float64
+ system_uuid::Base.UUID
+ container_metadata::IS.Optimization.OptimizationContainerMetadata
+
+ function ModelStoreParams(
+ num_executions::Int,
+ horizon_count::Int,
+ interval::Dates.Millisecond,
+ resolution::Dates.Millisecond,
+ base_power::Float64,
+ system_uuid::Base.UUID,
+ container_metadata = IS.Optimization.OptimizationContainerMetadata(),
+ )
+ new(
+ num_executions,
+ horizon_count,
+ Dates.Millisecond(interval),
+ Dates.Millisecond(resolution),
+ base_power,
+ system_uuid,
+ container_metadata,
+ )
+ end
+end
+
+function ModelStoreParams(
+ num_executions::Int,
+ horizon::Dates.Millisecond,
+ interval::Dates.Millisecond,
+ resolution::Dates.Millisecond,
+ base_power::Float64,
+ system_uuid::Base.UUID,
+ container_metadata = IS.Optimization.OptimizationContainerMetadata(),
+)
+ return ModelStoreParams(
+ num_executions,
+ horizon ÷ resolution,
+ Dates.Millisecond(interval),
+ Dates.Millisecond(resolution),
+ base_power,
+ system_uuid,
+ container_metadata,
+ )
+end
+
+get_num_executions(params::ModelStoreParams) = params.num_executions
+get_horizon_count(params::ModelStoreParams) = params.horizon_count
+get_interval(params::ModelStoreParams) = params.interval
+get_resolution(params::ModelStoreParams) = params.resolution
+get_base_power(params::ModelStoreParams) = params.base_power
+get_system_uuid(params::ModelStoreParams) = params.system_uuid
+deserialize_key(params::ModelStoreParams, name) =
+ deserialize_key(params.container_metadata, name)
diff --git a/src/core/network_model.jl b/src/core/network_model.jl
index e533f052cc..3172547d62 100644
--- a/src/core/network_model.jl
+++ b/src/core/network_model.jl
@@ -2,14 +2,14 @@ function _check_pm_formulation(::Type{T}) where {T <: PM.AbstractPowerModel}
if !isconcretetype(T)
throw(
ArgumentError(
- "The device model must contain only concrete types, $(T) is an Abstract Type",
+ "The network model must contain only concrete types, $(T) is an Abstract Type",
),
)
end
end
"""
-Establishes the model for a particular device specified by type.
+Establishes the model for the network specified by type.
# Arguments
@@ -17,14 +17,14 @@ Establishes the model for a particular device specified by type.
# Accepted Key Words
- - `use_slacks::Bool`: Adds slacks to the network modelings
+ - `use_slacks::Bool`: Adds slacks to the network modeling
- `PTDF::PTDF`: PTDF Array calculated using PowerNetworkMatrices
- `duals::Vector{DataType}`: Constraint types to calculate the duals
- `reduce_radial_branches::Bool`: Skips modeling radial branches in the system to reduce problem size
# Example
ptdf_array = PTDF(system)
-thermal_gens = NetworkModel(PTDFPowerModel, ptdf = ptdf_array),
+nw = NetworkModel(PTDFPowerModel, ptdf = ptdf_array),
"""
mutable struct NetworkModel{T <: PM.AbstractPowerModel}
use_slacks::Bool
@@ -36,6 +36,7 @@ mutable struct NetworkModel{T <: PM.AbstractPowerModel}
reduce_radial_branches::Bool
power_flow_evaluation::Union{Nothing, PFS.PowerFlowEvaluationModel}
subsystem::Union{Nothing, String}
+ modeled_branch_types::Vector{DataType}
function NetworkModel(
::Type{T};
@@ -57,6 +58,7 @@ mutable struct NetworkModel{T <: PM.AbstractPowerModel}
reduce_radial_branches,
power_flow_evaluation,
nothing,
+ Vector{DataType}(),
)
end
end
@@ -122,7 +124,10 @@ function instantiate_network_model(
return
end
-function instantiate_network_model(model::NetworkModel{PTDFPowerModel}, sys::PSY.System)
+function instantiate_network_model(
+ model::NetworkModel{<:AbstractPTDFModel},
+ sys::PSY.System,
+)
if get_PTDF_matrix(model) === nothing
@info "PTDF Matrix not provided. Calculating using PowerNetworkMatrices.PTDF"
model.PTDF_matrix =
@@ -144,7 +149,7 @@ end
function _assign_subnetworks_to_buses(
model::NetworkModel{T},
sys::PSY.System,
-) where {T <: Union{CopperPlatePowerModel, PTDFPowerModel}}
+) where {T <: Union{CopperPlatePowerModel, AbstractPTDFModel}}
subnetworks = model.subnetworks
temp_bus_map = Dict{Int, Int}()
radial_network_reduction = PSI.get_radial_network_reduction(model)
diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl
index 29606dce01..692a69a3cc 100644
--- a/src/core/optimization_container.jl
+++ b/src/core/optimization_container.jl
@@ -1,39 +1,3 @@
-"""
-Optimization Container construction stage
-"""
-abstract type ConstructStage end
-
-struct ArgumentConstructStage <: ConstructStage end
-struct ModelConstructStage <: ConstructStage end
-
-struct OptimizationContainerMetadata
- container_key_lookup::Dict{String, <:OptimizationContainerKey}
-end
-
-function OptimizationContainerMetadata()
- return OptimizationContainerMetadata(Dict{String, OptimizationContainerKey}())
-end
-
-function deserialize_metadata(
- ::Type{OptimizationContainerMetadata},
- output_dir::String,
- model_name,
-)
- filename = _make_metadata_filename(model_name, output_dir)
- return Serialization.deserialize(filename)
-end
-
-function deserialize_key(metadata::OptimizationContainerMetadata, name::AbstractString)
- !haskey(metadata.container_key_lookup, name) && error("$name is not stored")
- return metadata.container_key_lookup[name]
-end
-
-add_container_key!(x::OptimizationContainerMetadata, key, val) =
- x.container_key_lookup[key] = val
-get_container_key(x::OptimizationContainerMetadata, key) = x.container_key_lookup[key]
-has_container_key(x::OptimizationContainerMetadata, key) =
- haskey(x.container_key_lookup, key)
-
struct PrimalValuesCache
variables_cache::Dict{VariableKey, AbstractArray}
expressions_cache::Dict{ExpressionKey, AbstractArray}
@@ -74,7 +38,7 @@ function get_objective_expression(v::ObjectiveFunction)
end
get_sense(v::ObjectiveFunction) = v.sense
is_synchronized(v::ObjectiveFunction) = v.synchronized
-set_synchronized_status(v::ObjectiveFunction, value) = v.synchronized = value
+set_synchronized_status!(v::ObjectiveFunction, value) = v.synchronized = value
reset_variant_terms(v::ObjectiveFunction) = v.variant_terms = zero(JuMP.AffExpr)
has_variant_terms(v::ObjectiveFunction) = !iszero(v.variant_terms)
set_sense!(v::ObjectiveFunction, sense::MOI.OptimizationSense) = v.sense = sense
@@ -87,10 +51,9 @@ function ObjectiveFunction()
)
end
-mutable struct OptimizationContainer <: AbstractModelContainer
+mutable struct OptimizationContainer <: IS.Optimization.AbstractOptimizationContainer
JuMPmodel::JuMP.Model
time_steps::UnitRange{Int}
- resolution::Dates.TimePeriod
settings::Settings
settings_copy::Settings
variables::Dict{VariableKey, AbstractArray}
@@ -101,14 +64,14 @@ mutable struct OptimizationContainer <: AbstractModelContainer
expressions::Dict{ExpressionKey, AbstractArray}
parameters::Dict{ParameterKey, ParameterContainer}
primal_values_cache::PrimalValuesCache
- initial_conditions::Dict{ICKey, Vector{<:InitialCondition}}
+ initial_conditions::Dict{InitialConditionKey, Vector{<:InitialCondition}}
initial_conditions_data::InitialConditionsData
infeasibility_conflict::Dict{Symbol, Array}
pm::Union{Nothing, PM.AbstractPowerModel}
base_power::Float64
optimizer_stats::OptimizerStats
built_for_recurrent_solves::Bool
- metadata::OptimizationContainerMetadata
+ metadata::IS.Optimization.OptimizationContainerMetadata
default_time_series_type::Type{<:PSY.TimeSeriesData}
power_flow_evaluation_data::Union{<:PowerFlowEvaluationData, Nothing}
end
@@ -119,7 +82,6 @@ function OptimizationContainer(
jump_model::Union{Nothing, JuMP.Model},
::Type{T},
) where {T <: PSY.TimeSeriesData}
- resolution = PSY.get_time_series_resolution(sys)
if isabstracttype(T)
error("Default Time Series Type $V can't be abstract")
end
@@ -135,7 +97,6 @@ function OptimizationContainer(
return OptimizationContainer(
jump_model === nothing ? JuMP.Model() : jump_model,
1:1,
- IS.time_period_conversion(resolution),
settings,
copy_for_serialization(settings),
Dict{VariableKey, AbstractArray}(),
@@ -146,14 +107,14 @@ function OptimizationContainer(
Dict{ExpressionKey, AbstractArray}(),
Dict{ParameterKey, ParameterContainer}(),
PrimalValuesCache(),
- Dict{ICKey, Vector{InitialCondition}}(),
+ Dict{InitialConditionKey, Vector{InitialCondition}}(),
InitialConditionsData(),
Dict{Symbol, Array}(),
nothing,
PSY.get_base_power(sys),
OptimizerStats(),
false,
- OptimizationContainerMetadata(),
+ IS.Optimization.OptimizationContainerMetadata(),
T,
nothing,
)
@@ -167,10 +128,10 @@ get_base_power(container::OptimizationContainer) = container.base_power
get_constraints(container::OptimizationContainer) = container.constraints
function cost_function_unsynch(container::OptimizationContainer)
- obj_func = PSI.get_objective_expression(container)
- if has_variant_terms(obj_func) && PSI.is_synchronized(container)
- PSI.set_synchronized_status(obj_func, false)
- PSI.reset_variant_terms(obj_func)
+ obj_func = get_objective_expression(container)
+ if has_variant_terms(obj_func) && is_synchronized(container)
+ set_synchronized_status!(obj_func, false)
+ reset_variant_terms(obj_func)
end
return
end
@@ -195,7 +156,7 @@ get_optimizer_stats(container::OptimizationContainer) = container.optimizer_stat
get_parameters(container::OptimizationContainer) = container.parameters
get_power_flow_evaluation_data(container::OptimizationContainer) =
container.power_flow_evaluation_data
-get_resolution(container::OptimizationContainer) = container.resolution
+get_resolution(container::OptimizationContainer) = get_resolution(container.settings)
get_settings(container::OptimizationContainer) = container.settings
get_time_steps(container::OptimizationContainer) = container.time_steps
get_variables(container::OptimizationContainer) = container.variables
@@ -212,7 +173,7 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}
key = ExpressionKey(T, U, meta)
return haskey(container.expressions, key)
@@ -222,7 +183,7 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}
key = VariableKey(T, U, meta)
return haskey(container.variables, key)
@@ -232,7 +193,7 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}
key = AuxVarKey(T, U, meta)
return haskey(container.aux_variables, key)
@@ -242,7 +203,7 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
key = ConstraintKey(T, U, meta)
return haskey(container.constraints, key)
@@ -252,7 +213,7 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
key = ParameterKey(T, U, meta)
return haskey(container.parameters, key)
@@ -262,9 +223,9 @@ function has_container_key(
container::OptimizationContainer,
::Type{T},
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}
- key = ICKey(T, U, meta)
+ key = InitialConditionKey(T, U, meta)
return haskey(container.initial_conditions, key)
end
@@ -356,10 +317,13 @@ function init_optimization_container!(
end
end
- if get_horizon(settings) == UNSET_HORIZON
- set_horizon!(settings, PSY.get_forecast_horizon(sys))
+ if get_resolution(settings) == UNSET_RESOLUTION
+ error("Resolution not set in the model. Can't continue with the build.")
end
- container.time_steps = 1:get_horizon(settings)
+
+ horizon_count = (get_horizon(settings) ÷ get_resolution(settings))
+ @assert horizon_count > 0
+ container.time_steps = 1:horizon_count
if T <: CopperPlatePowerModel || T <: AreaBalancePowerModel
total_number_of_devices =
@@ -416,6 +380,7 @@ function check_optimization_container(container::OptimizationContainer)
error("The model container has invalid values in $(encode_key_as_string(k))")
end
end
+ container.settings_copy = copy_for_serialization(container.settings)
return
end
@@ -449,11 +414,14 @@ function _make_system_expressions!(
container.expressions = Dict(
ExpressionKey(ActivePowerBalance, PSY.ACBus) =>
_make_container_array(ac_bus_numbers, time_steps),
- ExpressionKey(ActivePowerBalance, PSY.DCBus) =>
- _make_container_array(dc_bus_numbers, time_steps),
ExpressionKey(ReactivePowerBalance, PSY.ACBus) =>
_make_container_array(ac_bus_numbers, time_steps),
)
+
+ if !isempty(dc_bus_numbers)
+ container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] =
+ _make_container_array(dc_bus_numbers, time_steps)
+ end
return
end
@@ -473,9 +441,11 @@ function _make_system_expressions!(
container.expressions = Dict(
ExpressionKey(ActivePowerBalance, PSY.ACBus) =>
_make_container_array(ac_bus_numbers, time_steps),
- ExpressionKey(ActivePowerBalance, PSY.DCBus) =>
- _make_container_array(dc_bus_numbers, time_steps),
)
+ if !isempty(dc_bus_numbers)
+ container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] =
+ _make_container_array(dc_bus_numbers, time_steps)
+ end
return
end
@@ -499,9 +469,9 @@ function _make_system_expressions!(
container::OptimizationContainer,
subnetworks::Dict{Int, Set{Int}},
dc_bus_numbers::Vector{Int},
- ::Type{T},
+ ::Type{PTDFPowerModel},
bus_reduction_map::Dict{Int64, Set{Int64}},
-) where {T <: PTDFPowerModel}
+)
time_steps = get_time_steps(container)
if isempty(bus_reduction_map)
ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))
@@ -512,28 +482,147 @@ function _make_system_expressions!(
container.expressions = Dict(
ExpressionKey(ActivePowerBalance, PSY.System) =>
_make_container_array(subnetworks, time_steps),
- ExpressionKey(ActivePowerBalance, PSY.DCBus) =>
- _make_container_array(dc_bus_numbers, time_steps),
ExpressionKey(ActivePowerBalance, PSY.ACBus) =>
# Bus numbers are sorted to guarantee consistency in the order between the
# containers
_make_container_array(sort!(ac_bus_numbers), time_steps),
)
+
+ if !isempty(dc_bus_numbers)
+ container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] =
+ _make_container_array(dc_bus_numbers, time_steps)
+ end
+ return
+end
+
+function _make_system_expressions!(
+ container::OptimizationContainer,
+ subnetworks::Dict{Int, Set{Int}},
+ ::Type{AreaBalancePowerModel},
+ areas::IS.FlattenIteratorWrapper{PSY.Area},
+)
+ if length(subnetworks) > 1
+ throw(
+ IS.ConflictingInputsError(
+ "AreaBalancePowerModel doesn't support systems with multiple asynchronous areas",
+ ),
+ )
+ end
+ time_steps = get_time_steps(container)
+ container.expressions = Dict(
+ ExpressionKey(ActivePowerBalance, PSY.Area) =>
+ _make_container_array(PSY.get_name.(areas), time_steps),
+ )
+ return
+end
+
+function _make_system_expressions!(
+ container::OptimizationContainer,
+ subnetworks::Dict{Int, Set{Int}},
+ dc_bus_numbers::Vector{Int},
+ ::Type{AreaPTDFPowerModel},
+ areas::IS.FlattenIteratorWrapper{PSY.Area},
+ bus_reduction_map::Dict{Int64, Set{Int64}},
+)
+ time_steps = get_time_steps(container)
+ if isempty(bus_reduction_map)
+ ac_bus_numbers = collect(Iterators.flatten(values(subnetworks)))
+ else
+ ac_bus_numbers = collect(keys(bus_reduction_map))
+ end
+ container.expressions = Dict(
+ # Enforces the balance by Area
+ ExpressionKey(ActivePowerBalance, PSY.Area) =>
+ _make_container_array(PSY.get_name.(areas), time_steps),
+ # Keeps track of the Injections by bus.
+ ExpressionKey(ActivePowerBalance, PSY.ACBus) =>
+ # Bus numbers are sorted to guarantee consistency in the order between the
+ # containers
+ _make_container_array(sort!(ac_bus_numbers), time_steps),
+ )
+
+ if length(subnetworks) > 1
+ @warn "The system contains $(length(subnetworks)) synchronous regions. \
+ When combined with AreaPTDFPowerModel, the model can be infeasible if the data doesn't \
+ have a well defined topology"
+ subnetworks_ref_buses = collect(keys(subnetworks))
+ container.expressions[ExpressionKey(ActivePowerBalance, PSY.System)] =
+ _make_container_array(subnetworks_ref_buses, time_steps)
+ end
+
+ if !isempty(dc_bus_numbers)
+ container.expressions[ExpressionKey(ActivePowerBalance, PSY.DCBus)] =
+ _make_container_array(dc_bus_numbers, time_steps)
+ end
+
return
end
function initialize_system_expressions!(
container::OptimizationContainer,
- ::Type{T},
+ network_model::NetworkModel{T},
subnetworks::Dict{Int, Set{Int}},
system::PSY.System,
bus_reduction_map::Dict{Int64, Set{Int64}},
) where {T <: PM.AbstractPowerModel}
- dc_bus_numbers = [PSY.get_number(b) for b in PSY.get_components(PSY.DCBus, system)]
+ dc_bus_numbers = [
+ PSY.get_number(b) for
+ b in get_available_components(network_model, PSY.DCBus, system)
+ ]
_make_system_expressions!(container, subnetworks, dc_bus_numbers, T, bus_reduction_map)
return
end
+function initialize_system_expressions!(
+ container::OptimizationContainer,
+ network_model::NetworkModel{AreaBalancePowerModel},
+ subnetworks::Dict{Int, Set{Int}},
+ system::PSY.System,
+ ::Dict{Int64, Set{Int64}},
+)
+ areas = get_available_components(network_model, PSY.Area, system)
+ if isempty(areas)
+ throw(
+ IS.ConflictingInputsError(
+ "AreaBalancePowerModel doesn't support systems with no defined Areas",
+ ),
+ )
+ end
+ @assert !isempty(areas)
+ _make_system_expressions!(container, subnetworks, AreaBalancePowerModel, areas)
+ return
+end
+
+function initialize_system_expressions!(
+ container::OptimizationContainer,
+ network_model::NetworkModel{AreaPTDFPowerModel},
+ subnetworks::Dict{Int, Set{Int}},
+ system::PSY.System,
+ bus_reduction_map::Dict{Int64, Set{Int64}},
+)
+ areas = get_available_components(network_model, PSY.Area, system)
+ if isempty(areas)
+ throw(
+ IS.ConflictingInputsError(
+ "AreaPTDFPowerModel doesn't support systems with no Areas",
+ ),
+ )
+ end
+ dc_bus_numbers = [
+ PSY.get_number(b) for
+ b in get_available_components(network_model, PSY.DCBus, system)
+ ]
+ _make_system_expressions!(
+ container,
+ subnetworks,
+ dc_bus_numbers,
+ AreaPTDFPowerModel,
+ areas,
+ bus_reduction_map,
+ )
+ return
+end
+
function build_impl!(
container::OptimizationContainer,
template::ProblemTemplate,
@@ -543,7 +632,7 @@ function build_impl!(
transmission_model = get_network_model(template)
initialize_system_expressions!(
container,
- transmission,
+ get_network_model(template),
transmission_model.subnetworks,
sys,
transmission_model.radial_network_reduction.bus_reduction_map)
@@ -574,6 +663,7 @@ function build_impl!(
ArgumentConstructStage(),
get_service_models(template),
get_device_models(template),
+ transmission_model,
)
end
@@ -595,16 +685,6 @@ function build_impl!(
end
end
- TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Services" begin
- construct_services!(
- container,
- sys,
- ModelConstructStage(),
- get_service_models(template),
- get_device_models(template),
- )
- end
-
for device_model in values(template.devices)
@debug "Building Model for $(get_component_type(device_model)) with $(get_formulation(device_model)) formulation" _group =
LOG_GROUP_OPTIMIZATION_CONTAINER
@@ -650,6 +730,17 @@ function build_impl!(
end
end
+ TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Services" begin
+ construct_services!(
+ container,
+ sys,
+ ModelConstructStage(),
+ get_service_models(template),
+ get_device_models(template),
+ transmission_model,
+ )
+ end
+
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Objective" begin
@debug "Building Objective" _group = LOG_GROUP_OPTIMIZATION_CONTAINER
update_objective_function!(container)
@@ -720,7 +811,7 @@ function solve_impl!(container::OptimizationContainer, system::PSY.System)
_, optimizer_stats.timed_calculate_dual_variables =
@timed calculate_dual_variables!(container, system, is_milp(container))
- return RunStatus.SUCCESSFUL
+ return RunStatus.SUCCESSFULLY_FINALIZED
end
function compute_conflict!(container::OptimizationContainer)
@@ -746,7 +837,7 @@ function compute_conflict!(container::OptimizationContainer)
@info "Conflict Index returned empty for $key"
continue
else
- conflict[encode_key(key)] = conflict_indices
+ conflict[IS.Optimization.encode_key(key)] = conflict_indices
end
end
@@ -783,12 +874,6 @@ function serialize_optimization_model(container::OptimizationContainer, save_pat
return
end
-const _CONTAINER_METADATA_FILE = "optimization_container_metadata.bin"
-
-_make_metadata_filename(model_name::Symbol, output_dir) =
- joinpath(output_dir, string(model_name), _CONTAINER_METADATA_FILE)
-_make_metadata_filename(output_dir) = joinpath(output_dir, _CONTAINER_METADATA_FILE)
-
function serialize_metadata!(container::OptimizationContainer, output_dir::String)
for key in Iterators.flatten((
keys(container.constraints),
@@ -799,14 +884,15 @@ function serialize_metadata!(container::OptimizationContainer, output_dir::Strin
keys(container.expressions),
))
encoded_key = encode_key_as_string(key)
- if has_container_key(container.metadata, encoded_key)
+ if IS.Optimization.has_container_key(container.metadata, encoded_key)
# Constraints and Duals can store the same key.
- IS.@assert_op key == get_container_key(container.metadata, encoded_key)
+ IS.@assert_op key ==
+ IS.Optimization.get_container_key(container.metadata, encoded_key)
end
- add_container_key!(container.metadata, encoded_key, key)
+ IS.Optimization.add_container_key!(container.metadata, encoded_key, key)
end
- filename = _make_metadata_filename(output_dir)
+ filename = IS.Optimization._make_metadata_filename(output_dir)
Serialization.serialize(filename, container.metadata)
@debug "Serialized container keys to $filename" _group = IS.LOG_GROUP_SERIALIZATION
end
@@ -818,18 +904,24 @@ function deserialize_metadata!(
)
merge!(
container.metadata.container_key_lookup,
- deserialize_metadata(OptimizationContainerMetadata, output_dir, model_name),
+ deserialize_metadata(
+ IS.Optimization.OptimizationContainerMetadata,
+ output_dir,
+ model_name,
+ ),
)
return
end
function _assign_container!(container::Dict, key::OptimizationContainerKey, value)
if haskey(container, key)
- @error "$(encode_key(key)) is already stored" sort!(encode_key.(keys(container)))
+ @error "$(IS.Optimization.encode_key(key)) is already stored" sort!(
+ IS.Optimization.encode_key.(keys(container)),
+ )
throw(IS.InvalidValue("$key is already stored"))
end
container[key] = value
- @debug "Added container entry $(typeof(key)) $(encode_key(key))" _group =
+ @debug "Added container entry $(typeof(key)) $(IS.Optimization.encode_key(key))" _group =
LOG_GROUP_OPTIMZATION_CONTAINER
return
end
@@ -856,7 +948,7 @@ function add_variable_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}
var_key = VariableKey(T, U, meta)
return _add_variable_container!(container, var_key, sparse, axs...)
@@ -883,8 +975,8 @@ function add_variable_container!(
container::OptimizationContainer,
::T,
::Type{U};
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: PieceWiseLinearCostVariable, U <: Union{PSY.Component, PSY.System}}
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
+) where {T <: SparseVariableType, U <: Union{PSY.Component, PSY.System}}
var_key = VariableKey(T, U, meta)
_assign_container!(container.variables, var_key, _get_pwl_variables_container())
return container.variables[var_key]
@@ -897,8 +989,8 @@ end
function get_variable(container::OptimizationContainer, key::VariableKey)
var = get(container.variables, key, nothing)
if var === nothing
- name = encode_key(key)
- keys = encode_key.(get_variable_keys(container))
+ name = IS.Optimization.encode_key(key)
+ keys = IS.Optimization.encode_key.(get_variable_keys(container))
throw(IS.InvalidValue("variable $name is not stored. $keys"))
end
return var
@@ -908,7 +1000,7 @@ function get_variable(
container::OptimizationContainer,
::T,
::Type{U},
- meta::String = CONTAINER_KEY_EMPTY_META,
+ meta::String = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}
return get_variable(container, VariableKey(T, U, meta))
end
@@ -920,7 +1012,7 @@ function add_aux_variable_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: AuxVariableType, U <: PSY.Component}
var_key = AuxVarKey(T, U, meta)
if sparse
@@ -939,8 +1031,8 @@ end
function get_aux_variable(container::OptimizationContainer, key::AuxVarKey)
aux = get(container.aux_variables, key, nothing)
if aux === nothing
- name = encode_key(key)
- keys = encode_key.(get_aux_variable_keys(container))
+ name = IS.Optimization.encode_key(key)
+ keys = IS.Optimization.encode_key.(get_aux_variable_keys(container))
throw(IS.InvalidValue("Auxiliary variable $name is not stored. $keys"))
end
return aux
@@ -950,7 +1042,7 @@ function get_aux_variable(
container::OptimizationContainer,
::T,
::Type{U},
- meta::String = CONTAINER_KEY_EMPTY_META,
+ meta::String = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: AuxVariableType, U <: PSY.Component}
return get_aux_variable(container, AuxVarKey(T, U, meta))
end
@@ -962,7 +1054,7 @@ function add_dual_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
if is_milp(container)
@warn("The model has resulted in a MILP, \\
@@ -1005,7 +1097,7 @@ function add_constraints_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
cons_key = ConstraintKey(T, U, meta)
return _add_constraints_container!(container, cons_key, axs...; sparse = sparse)
@@ -1018,8 +1110,8 @@ end
function get_constraint(container::OptimizationContainer, key::ConstraintKey)
var = get(container.constraints, key, nothing)
if var === nothing
- name = encode_key(key)
- keys = encode_key.(get_constraint_keys(container))
+ name = IS.Optimization.encode_key(key)
+ keys = IS.Optimization.encode_key.(get_constraint_keys(container))
throw(IS.InvalidValue("constraint $name is not stored. $keys"))
end
@@ -1030,7 +1122,7 @@ function get_constraint(
container::OptimizationContainer,
::T,
::Type{U},
- meta::String = CONTAINER_KEY_EMPTY_META,
+ meta::String = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
return get_constraint(container, ConstraintKey(T, U, meta))
end
@@ -1139,7 +1231,7 @@ function add_param_container!(
multiplier_axs,
time_steps;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: TimeSeriesParameter, U <: PSY.Component, V <: PSY.TimeSeriesData}
param_key = ParameterKey(T, U, meta)
if isabstracttype(V)
@@ -1167,7 +1259,7 @@ function add_param_container!(
data_type::DataType = Float64,
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ObjectiveFunctionParameter, U <: PSY.Component, W <: VariableType}
param_key = ParameterKey(T, U, meta)
attributes =
@@ -1182,7 +1274,7 @@ function add_param_container!(
source_key::V,
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: VariableValueParameter, U <: PSY.Component, V <: OptimizationContainerKey}
param_key = ParameterKey(T, U, meta)
attributes = VariableValueAttributes(source_key)
@@ -1198,9 +1290,9 @@ function add_param_container!(
source_key::V,
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: FixValueParameter, U <: PSY.Component, V <: OptimizationContainerKey}
- if meta == CONTAINER_KEY_EMPTY_META
+ if meta == IS.Optimization.CONTAINER_KEY_EMPTY_META
error("$T parameters require passing the VariableType to the meta field")
end
param_key = ParameterKey(T, U, meta)
@@ -1222,7 +1314,7 @@ end
function get_parameter(container::OptimizationContainer, key::ParameterKey)
param_container = get(container.parameters, key, nothing)
if param_container === nothing
- name = encode_key(key)
+ name = IS.Optimization.encode_key(key)
throw(
IS.InvalidValue(
"parameter $name is not stored. $(collect(keys(container.parameters)))",
@@ -1236,7 +1328,7 @@ function get_parameter(
container::OptimizationContainer,
::T,
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
return get_parameter(container, ParameterKey(T, U, meta))
end
@@ -1270,7 +1362,7 @@ function get_parameter_array(
container::OptimizationContainer,
::T,
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
return get_parameter_array(container, ParameterKey(T, U, meta))
end
@@ -1278,7 +1370,7 @@ function get_parameter_multiplier_array(
container::OptimizationContainer,
::T,
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
return get_multiplier_array(get_parameter(container, ParameterKey(T, U, meta)))
end
@@ -1287,7 +1379,7 @@ function get_parameter_attributes(
container::OptimizationContainer,
::T,
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
return get_attributes(get_parameter(container, ParameterKey(T, U, meta)))
end
@@ -1347,7 +1439,7 @@ function add_expression_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}
expr_key = ExpressionKey(T, U, meta)
return _add_expression_container!(container, expr_key, GAE, axs...; sparse = sparse)
@@ -1359,7 +1451,7 @@ function add_expression_container!(
::Type{U},
axs...;
sparse = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ProductionCostExpression, U <: Union{PSY.Component, PSY.System}}
expr_key = ExpressionKey(T, U, meta)
expr_type = JuMP.QuadExpr
@@ -1393,7 +1485,7 @@ function get_expression(
container::OptimizationContainer,
::T,
::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: ExpressionType, U <: Union{PSY.Component, PSY.System}}
return get_expression(container, ExpressionKey(T, U, meta))
end
@@ -1408,7 +1500,7 @@ end
###################################Initial Conditions Containers############################
function _add_initial_condition_container!(
container::OptimizationContainer,
- ic_key::ICKey{T, U},
+ ic_key::InitialConditionKey{T, U},
length_devices::Int,
) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}
if built_for_recurrent_solves(container) && !get_rebuild_model(get_settings(container))
@@ -1426,9 +1518,9 @@ function add_initial_condition_container!(
::T,
::Type{U},
axs;
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: InitialConditionType, U <: Union{PSY.Component, PSY.System}}
- ic_key = ICKey(T, U, meta)
+ ic_key = InitialConditionKey(T, U, meta)
@debug "add_initial_condition_container" ic_key _group = LOG_GROUP_SERVICE_CONSTUCTORS
return _add_initial_condition_container!(container, ic_key, length(axs))
end
@@ -1438,10 +1530,10 @@ function get_initial_condition(
::T,
::Type{D},
) where {T <: InitialConditionType, D <: PSY.Component}
- return get_initial_condition(container, ICKey(T, D))
+ return get_initial_condition(container, InitialConditionKey(T, D))
end
-function get_initial_condition(container::OptimizationContainer, key::ICKey)
+function get_initial_condition(container::OptimizationContainer, key::InitialConditionKey)
initial_conditions = get(container.initial_conditions, key, nothing)
if initial_conditions === nothing
throw(IS.InvalidValue("initial conditions are not stored for $(key)"))
@@ -1551,7 +1643,7 @@ function calculate_aux_variables!(container::OptimizationContainer, system::PSY.
for key in keys(aux_vars)
calculate_aux_variable_value!(container, key, system)
end
- return RunStatus.SUCCESSFUL
+ return RunStatus.SUCCESSFULLY_FINALIZED
end
function _calculate_dual_variable_value!(
@@ -1593,7 +1685,7 @@ function _calculate_dual_variables_continous_model!(
for key in keys(duals_vars)
_calculate_dual_variable_value!(container, key, system)
end
- return RunStatus.SUCCESSFUL
+ return RunStatus.SUCCESSFULLY_FINALIZED
end
function _process_duals(container::OptimizationContainer, lp_optimizer)
@@ -1690,7 +1782,7 @@ function _process_duals(container::OptimizationContainer, lp_optimizer)
=#
end
end
- return RunStatus.SUCCESSFUL
+ return RunStatus.SUCCESSFULLY_FINALIZED
end
function _calculate_dual_variables_discrete_model!(
@@ -1705,7 +1797,7 @@ function calculate_dual_variables!(
sys::PSY.System,
is_milp::Bool,
)
- isempty(get_duals(container)) && return RunStatus.SUCCESSFUL
+ isempty(get_duals(container)) && return RunStatus.SUCCESSFULLY_FINALIZED
if is_milp
status = _calculate_dual_variables_discrete_model!(container, sys)
else
@@ -1769,7 +1861,7 @@ function lazy_container_addition!(
axs...;
kwargs...,
) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
- meta = get(kwargs, :meta, CONTAINER_KEY_EMPTY_META)
+ meta = get(kwargs, :meta, IS.Optimization.CONTAINER_KEY_EMPTY_META)
if !has_container_key(container, T, U, meta)
cons_container =
add_constraints_container!(container, constraint, U, axs...; kwargs...)
diff --git a/src/core/optimization_container_keys.jl b/src/core/optimization_container_keys.jl
deleted file mode 100644
index a6df17c554..0000000000
--- a/src/core/optimization_container_keys.jl
+++ /dev/null
@@ -1,40 +0,0 @@
-abstract type OptimizationContainerKey end
-
-const _DELIMITER = "__"
-
-function make_key(::Type{T}, args...) where {T <: OptimizationContainerKey}
- return T(args...)
-end
-
-function encode_key(key::OptimizationContainerKey)
- return encode_symbol(get_component_type(key), get_entry_type(key), key.meta)
-end
-
-encode_key_as_string(key::OptimizationContainerKey) = string(encode_key(key))
-encode_keys_as_strings(container_keys) = [encode_key_as_string(k) for k in container_keys]
-
-function encode_symbol(
- ::Type{T},
- ::Type{U},
- meta::String = CONTAINER_KEY_EMPTY_META,
-) where {T <: Union{PSY.Component, PSY.System}, U}
- meta_ = isempty(meta) ? meta : _DELIMITER * meta
- T_ = replace(replace(IS.strip_module_name(T), "{" => _DELIMITER), "}" => "")
- return Symbol("$(IS.strip_module_name(string(U)))$(_DELIMITER)$(T_)" * meta_)
-end
-
-function check_meta_chars(meta)
- # Underscores in this field will prevent us from being able to decode keys.
- if occursin(_DELIMITER, meta)
- throw(IS.InvalidValue("'$_DELIMITER' is not allowed in meta"))
- end
-end
-
-function should_write_resulting_value(key_val::OptimizationContainerKey)
- value_type = get_entry_type(key_val)
- return should_write_resulting_value(value_type)
-end
-
-function convert_result_to_natural_units(key::OptimizationContainerKey)
- return convert_result_to_natural_units(get_entry_type(key))
-end
diff --git a/src/core/optimization_container_types.jl b/src/core/optimization_container_types.jl
deleted file mode 100644
index 988135603f..0000000000
--- a/src/core/optimization_container_types.jl
+++ /dev/null
@@ -1,8 +0,0 @@
-abstract type AbstractModelContainer end
-
-abstract type VariableType end
-abstract type ConstraintType end
-abstract type AuxVariableType end
-abstract type ParameterType end
-abstract type InitialConditionType end
-abstract type ExpressionType end
diff --git a/src/core/optimizer_stats.jl b/src/core/optimizer_stats.jl
deleted file mode 100644
index dfac5c3cce..0000000000
--- a/src/core/optimizer_stats.jl
+++ /dev/null
@@ -1,106 +0,0 @@
-mutable struct OptimizerStats
- detailed_stats::Bool
- objective_value::Float64
- termination_status::Int
- primal_status::Int
- dual_status::Int
- solver_solve_time::Float64
- result_count::Int
- has_values::Bool
- has_duals::Bool
- # Candidate solution
- objective_bound::Union{Missing, Float64}
- relative_gap::Union{Missing, Float64}
- # Use missing instead of nothing so that CSV writting doesn't fail
- dual_objective_value::Union{Missing, Float64}
- # Work counters
- solve_time::Float64
- barrier_iterations::Union{Missing, Int}
- simplex_iterations::Union{Missing, Int}
- node_count::Union{Missing, Int}
- timed_solve_time::Float64
- timed_calculate_aux_variables::Float64
- timed_calculate_dual_variables::Float64
- solve_bytes_alloc::Union{Missing, Float64}
- sec_in_gc::Union{Missing, Float64}
-end
-
-function OptimizerStats()
- return OptimizerStats(
- false,
- NaN,
- -1,
- -1,
- -1,
- NaN,
- -1,
- false,
- false,
- missing,
- missing,
- missing,
- NaN,
- missing,
- missing,
- missing,
- NaN,
- 0,
- 0,
- missing,
- missing,
- )
-end
-
-"""
-Construct OptimizerStats from a vector that was serialized to HDF5.
-"""
-function OptimizerStats(data::Vector{Float64})
- vals = Vector(undef, length(data))
- to_missing = Set((
- :objective_bound,
- :dual_objective_value,
- :barrier_iterations,
- :simplex_iterations,
- :node_count,
- :solve_bytes_alloc,
- :sec_in_gc,
- ))
- for (i, name) in enumerate(fieldnames(OptimizerStats))
- if name in to_missing && isnan(data[i])
- vals[i] = missing
- else
- vals[i] = data[i]
- end
- end
- return OptimizerStats(vals...)
-end
-
-"""
-Convert OptimizerStats to a matrix of floats that can be serialized to HDF5.
-"""
-function to_matrix(stats::T) where {T <: OptimizerStats}
- field_values = Matrix{Float64}(undef, fieldcount(T), 1)
- for (ix, field) in enumerate(fieldnames(T))
- value = getfield(stats, field)
- field_values[ix] = ismissing(value) ? NaN : value
- end
- return field_values
-end
-
-function to_dataframe(stats::OptimizerStats)
- df = DataFrames.DataFrame([to_namedtuple(stats)])
- return df
-end
-
-function to_dict(stats::OptimizerStats)
- data = Dict()
- for field in fieldnames(typeof(stats))
- data[String(field)] = getfield(stats, field)
- end
-
- return data
-end
-
-function get_column_names(::Type{OptimizerStats})
- return (collect(string.(fieldnames(OptimizerStats))),)
-end
diff --git a/src/core/parameters.jl b/src/core/parameters.jl
index 79c1ad4b26..f4c48f0294 100644
--- a/src/core/parameters.jl
+++ b/src/core/parameters.jl
@@ -1,29 +1,3 @@
-struct ParameterKey{T <: ParameterType, U <: PSY.Component} <: OptimizationContainerKey
- meta::String
-end
-
-function ParameterKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: ParameterType, U <: PSY.Component}
- if isabstracttype(U)
- error("Type $U can't be abstract")
- end
- check_meta_chars(meta)
- return ParameterKey{T, U}(meta)
-end
-
-function ParameterKey(
- ::Type{T},
- meta::String = CONTAINER_KEY_EMPTY_META,
-) where {T <: ParameterType}
- return ParameterKey(T, PSY.Component, meta)
-end
-
-get_entry_type(::ParameterKey{T, U}) where {T <: ParameterType, U <: PSY.Component} = T
-get_component_type(::ParameterKey{T, U}) where {T <: ParameterType, U <: PSY.Component} = U
-
abstract type ParameterAttributes end
struct NoAttributes end
@@ -203,11 +177,11 @@ function _set_parameter!(
end
function _set_parameter!(
- array::AbstractArray{Vector{NTuple{2, Float64}}},
+ array::AbstractArray{T},
::JuMP.Model,
- value::Vector{NTuple{2, Float64}},
+ value::T,
ixs::Tuple,
-)
+) where {T <: IS.FunctionData}
array[ixs...] = value
return
end
@@ -251,7 +225,7 @@ end
function set_parameter!(
container::ParameterContainer,
jump_model::JuMP.Model,
- parameter::Vector{NTuple{2, Float64}},
+ parameter::IS.FunctionData,
ixs...,
)
param_array = get_parameter_array(container)
@@ -259,14 +233,6 @@ function set_parameter!(
return
end
-"""
-Parameters implemented through VariableRef
-"""
-abstract type RightHandSideParameter <: ParameterType end
-abstract type ObjectiveFunctionParameter <: ParameterType end
-
-abstract type TimeSeriesParameter <: RightHandSideParameter end
-
"""
Parameter to define active power time series
"""
@@ -278,10 +244,30 @@ Parameter to define reactive power time series
struct ReactivePowerTimeSeriesParameter <: TimeSeriesParameter end
"""
-Paramter to define requirement time series
+Parameter to define requirement time series
"""
struct RequirementTimeSeriesParameter <: TimeSeriesParameter end
+"""
+Parameter to define Flow From_To limit time series
+"""
+struct FromToFlowLimitParameter <: TimeSeriesParameter end
+
+"""
+Parameter to define Flow To_From limit time series
+"""
+struct ToFromFlowLimitParameter <: TimeSeriesParameter end
+
+"""
+Parameter to define Max Flow limit for interface time series
+"""
+struct MaxInterfaceFlowLimitParameter <: TimeSeriesParameter end
+
+"""
+Parameter to define Min Flow limit for interface time series
+"""
+struct MinInterfaceFlowLimitParameter <: TimeSeriesParameter end
+
abstract type VariableValueParameter <: RightHandSideParameter end
"""
@@ -313,11 +299,8 @@ abstract type AuxVariableValueParameter <: RightHandSideParameter end
struct EventParameter <: ParameterType end
-should_write_resulting_value(::Type{<:ParameterType}) = false
should_write_resulting_value(::Type{<:RightHandSideParameter}) = true
-convert_result_to_natural_units(::Type{<:ParameterType}) = false
-
convert_result_to_natural_units(::Type{ActivePowerTimeSeriesParameter}) = true
convert_result_to_natural_units(::Type{ReactivePowerTimeSeriesParameter}) = true
convert_result_to_natural_units(::Type{RequirementTimeSeriesParameter}) = true
diff --git a/src/core/settings.jl b/src/core/settings.jl
index 990c2fb03f..d9dd095cbc 100644
--- a/src/core/settings.jl
+++ b/src/core/settings.jl
@@ -1,5 +1,6 @@
struct Settings
- horizon::Base.RefValue{Int}
+ horizon::Base.RefValue{Dates.Millisecond}
+ resolution::Base.RefValue{Dates.Millisecond}
time_series_cache_size::Int
warm_start::Base.RefValue{Bool}
initial_time::Base.RefValue{Dates.DateTime}
@@ -25,7 +26,8 @@ function Settings(
initial_time::Dates.DateTime = UNSET_INI_TIME,
time_series_cache_size::Int = IS.TIME_SERIES_CACHE_SIZE_BYTES,
warm_start::Bool = true,
- horizon::Int = UNSET_HORIZON,
+ horizon::Dates.Period = UNSET_HORIZON,
+ resolution::Dates.Period = UNSET_RESOLUTION,
optimizer = nothing,
direct_mode_optimizer::Bool = false,
optimizer_solve_log_print::Bool = false,
@@ -42,8 +44,7 @@ function Settings(
store_variable_names = false,
ext = Dict{String, Any}(),
)
- if time_series_cache_size > 0 &&
- sys.data.time_series_storage isa IS.InMemoryTimeSeriesStorage
+ if time_series_cache_size > 0 && PSY.stores_time_series_in_memory(sys)
@info "Overriding time_series_cache_size because time series is stored in memory"
time_series_cache_size = 0
end
@@ -59,7 +60,8 @@ function Settings(
end
return Settings(
- Ref(horizon),
+ Ref(IS.time_period_conversion(horizon)),
+ Ref(IS.time_period_conversion(resolution)),
time_series_cache_size,
Ref(warm_start),
Ref(initial_time),
@@ -130,6 +132,7 @@ function restore_from_copy(
end
get_horizon(settings::Settings) = settings.horizon[]
+get_resolution(settings::Settings) = settings.resolution[]
get_initial_time(settings::Settings)::Dates.DateTime = settings.initial_time[]
get_optimizer(settings::Settings) = settings.optimizer
get_ext(settings::Settings) = settings.ext
@@ -150,8 +153,13 @@ get_store_variable_names(settings::Settings) = settings.store_variable_names
get_rebuild_model(settings::Settings) = settings.rebuild_model
use_time_series_cache(settings::Settings) = settings.time_series_cache_size > 0
-function set_horizon!(settings::Settings, horizon::Int)
- settings.horizon[] = horizon
+function set_horizon!(settings::Settings, horizon::Dates.TimePeriod)
+ settings.horizon[] = IS.time_period_conversion(horizon)
+ return
+end
+
+function set_resolution!(settings::Settings, resolution::Dates.TimePeriod)
+ settings.resolution[] = IS.time_period_conversion(resolution)
return
end
diff --git a/src/core/store_common.jl b/src/core/store_common.jl
index cde24a94d5..0be564377b 100644
--- a/src/core/store_common.jl
+++ b/src/core/store_common.jl
@@ -1,10 +1,3 @@
-# Keep these in sync with the Symbols in src/core/definitions.
-get_store_container_type(::AuxVarKey) = STORE_CONTAINER_AUX_VARIABLES
-get_store_container_type(::ConstraintKey) = STORE_CONTAINER_DUALS
-get_store_container_type(::ExpressionKey) = STORE_CONTAINER_EXPRESSIONS
-get_store_container_type(::ParameterKey) = STORE_CONTAINER_PARAMETERS
-get_store_container_type(::VariableKey) = STORE_CONTAINER_VARIABLES
-
# Aliases used for clarity in the method dispatches so it is possible to know if writing to
# DecisionModel data or EmulationModel data
const DecisionModelIndexType = Dates.DateTime
@@ -23,7 +16,7 @@ function write_results!(
:exports_path => joinpath(exports.path, string(get_name(model))),
:file_type => get_export_file_type(exports),
:resolution => get_resolution(model),
- :horizon => get_horizon(get_settings(model)),
+ :horizon_count => get_horizon(get_settings(model)) ÷ get_resolution(model),
)
else
export_params = nothing
@@ -58,13 +51,13 @@ function write_model_dual_results!(
if export_params !== nothing &&
should_export_dual(export_params[:exports], index, model_name, key)
- horizon = export_params[:horizon]
+ horizon_count = export_params[:horizon_count]
resolution = export_params[:resolution]
file_type = export_params[:file_type]
df = to_dataframe(jump_value.(constraint), key)
- time_col = range(index; length = horizon, step = resolution)
+ time_col = range(index; length = horizon_count, step = resolution)
DataFrames.insertcols!(df, 1, :DateTime => time_col)
- export_result(file_type, exports_path, key, index, df)
+ IS.Optimization.export_result(file_type, exports_path, key, index, df)
end
end
return
@@ -85,6 +78,8 @@ function write_model_parameter_results!(
end
horizon = get_horizon(get_settings(model))
+ resolution = get_resolution(get_settings(model))
+ horizon_count = horizon ÷ resolution
parameters = get_parameters(container)
for (key, container) in parameters
@@ -97,9 +92,9 @@ function write_model_parameter_results!(
resolution = export_params[:resolution]
file_type = export_params[:file_type]
df = to_dataframe(data, key)
- time_col = range(index; length = horizon, step = resolution)
+ time_col = range(index; length = horizon_count, step = resolution)
DataFrames.insertcols!(df, 1, :DateTime => time_col)
- export_result(file_type, exports_path, key, index, df)
+ IS.Optimization.export_result(file_type, exports_path, key, index, df)
end
end
return
@@ -132,13 +127,13 @@ function write_model_variable_results!(
if export_params !== nothing &&
should_export_variable(export_params[:exports], index, model_name, key)
- horizon = export_params[:horizon]
+ horizon_count = export_params[:horizon_count]
resolution = export_params[:resolution]
file_type = export_params[:file_type]
df = to_dataframe(data, key)
- time_col = range(index; length = horizon, step = resolution)
+ time_col = range(index; length = horizon_count, step = resolution)
DataFrames.insertcols!(df, 1, :DateTime => time_col)
- export_result(file_type, exports_path, key, index, df)
+ IS.Optimization.export_result(file_type, exports_path, key, index, df)
end
end
return
@@ -165,13 +160,13 @@ function write_model_aux_variable_results!(
if export_params !== nothing &&
should_export_aux_variable(export_params[:exports], index, model_name, key)
- horizon = export_params[:horizon]
+ horizon_count = export_params[:horizon_count]
resolution = export_params[:resolution]
file_type = export_params[:file_type]
df = to_dataframe(data, key)
- time_col = range(index; length = horizon, step = resolution)
+ time_col = range(index; length = horizon_count, step = resolution)
DataFrames.insertcols!(df, 1, :DateTime => time_col)
- export_result(file_type, exports_path, key, index, df)
+ IS.Optimization.export_result(file_type, exports_path, key, index, df)
end
end
return
@@ -204,13 +199,13 @@ function write_model_expression_results!(
if export_params !== nothing &&
should_export_expression(export_params[:exports], index, model_name, key)
- horizon = export_params[:horizon]
+ horizon_count = export_params[:horizon_count]
resolution = export_params[:resolution]
file_type = export_params[:file_type]
df = to_dataframe(data, key)
- time_col = range(index; length = horizon, step = resolution)
+ time_col = range(index; length = horizon_count, step = resolution)
DataFrames.insertcols!(df, 1, :DateTime => time_col)
- export_result(file_type, exports_path, key, index, df)
+ IS.Optimization.export_result(file_type, exports_path, key, index, df)
end
end
return
diff --git a/src/core/variables.jl b/src/core/variables.jl
index 51e7846400..42d5bc0560 100644
--- a/src/core/variables.jl
+++ b/src/core/variables.jl
@@ -1,89 +1,56 @@
-abstract type SubComponentVariableType <: VariableType end
-
-struct VariableKey{T <: VariableType, U <: Union{PSY.Component, PSY.System}} <:
- OptimizationContainerKey
- meta::String
-end
-
-function VariableKey(
- ::Type{T},
- ::Type{U},
- meta = CONTAINER_KEY_EMPTY_META,
-) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}
- if isabstracttype(U)
- error("Type $U can't be abstract")
- end
- check_meta_chars(meta)
- return VariableKey{T, U}(meta)
-end
-
-function VariableKey(
- ::Type{T},
- meta::String = CONTAINER_KEY_EMPTY_META,
-) where {T <: VariableType}
- return VariableKey(T, PSY.Component, meta)
-end
-
-get_entry_type(
- ::VariableKey{T, U},
-) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} = T
-get_component_type(
- ::VariableKey{T, U},
-) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}} = U
-
"""
Struct to dispatch the creation of Active Power Variables
-Docs abbreviation: ``Pg``
+Docs abbreviation: ``p``
"""
struct ActivePowerVariable <: VariableType end
"""
Struct to dispatch the creation of Active Power Variables above minimum power for Thermal Compact formulations
-Docs abbreviation: ``\\hat{Pg}``
+Docs abbreviation: ``\\Delta p``
"""
struct PowerAboveMinimumVariable <: VariableType end
"""
Struct to dispatch the creation of Active Power Input Variables for 2-directional devices. For instance storage or pump-hydro
-Docs abbreviation: ``Pg^{in}``
+Docs abbreviation: ``p^\\text{in}``
"""
struct ActivePowerInVariable <: VariableType end
"""
Struct to dispatch the creation of Active Power Output Variables for 2-directional devices. For instance storage or pump-hydro
-Docs abbreviation: ``Pg^{out}``
+Docs abbreviation: ``p^\\text{out}``
"""
struct ActivePowerOutVariable <: VariableType end
"""
Struct to dispatch the creation of Hot Start Variable for Thermal units with temperature considerations
-Docs abbreviation: TODO
+Docs abbreviation: ``z^\\text{th}``
"""
struct HotStartVariable <: VariableType end
"""
Struct to dispatch the creation of Warm Start Variable for Thermal units with temperature considerations
-Docs abbreviation: TODO
+Docs abbreviation: ``y^\\text{th}``
"""
struct WarmStartVariable <: VariableType end
"""
Struct to dispatch the creation of Cold Start Variable for Thermal units with temperature considerations
-Docs abbreviation: TODO
+Docs abbreviation: ``x^\\text{th}``
"""
struct ColdStartVariable <: VariableType end
"""
Struct to dispatch the creation of a variable for energy storage level (state of charge)
-Docs abbreviation: ``E``
+Docs abbreviation: ``e``
"""
struct EnergyVariable <: VariableType end
@@ -99,37 +66,42 @@ struct OnVariable <: VariableType end
"""
Struct to dispatch the creation of Reactive Power Variables
-Docs abbreviation: ``Qg``
+Docs abbreviation: ``q``
"""
struct ReactivePowerVariable <: VariableType end
"""
Struct to dispatch the creation of binary storage charge reservation variable
-Docs abbreviation: ``r``
+Docs abbreviation: ``u^\\text{st}``
"""
struct ReservationVariable <: VariableType end
"""
Struct to dispatch the creation of Active Power Reserve Variables
-Docs abbreviation: ``Pr``
+Docs abbreviation: ``r``
"""
struct ActivePowerReserveVariable <: VariableType end
+"""
+Struct to dispatch the creation of Service Requirement Variables
+
+Docs abbreviation: ``\\text{req}``
+"""
struct ServiceRequirementVariable <: VariableType end
"""
Struct to dispatch the creation of Binary Start Variables
-Docs abbreviation: TODO
+Docs abbreviation: ``v``
"""
struct StartVariable <: VariableType end
"""
Struct to dispatch the creation of Binary Stop Variables
-Docs abbreviation: TODO
+Docs abbreviation: ``w``
"""
struct StopVariable <: VariableType end
@@ -147,30 +119,59 @@ struct AdditionalDeltaActivePowerDownVariable <: VariableType end
struct SmoothACE <: VariableType end
+"""
+Struct to dispatch the creation of System-wide slack up variables. Used when there is not enough generation.
+
+Docs abbreviation: ``p^\\text{sl,up}``
+"""
struct SystemBalanceSlackUp <: VariableType end
+"""
+Struct to dispatch the creation of System-wide slack down variables. Used when there is not enough load curtailment.
+
+Docs abbreviation: ``p^\\text{sl,dn}``
+"""
struct SystemBalanceSlackDown <: VariableType end
+"""
+Struct to dispatch the creation of Reserve requirement slack variables. Used when there is not reserves in the system to satisfy the requirement.
+
+Docs abbreviation: ``r^\\text{sl}``
+"""
struct ReserveRequirementSlack <: VariableType end
+"""
+Struct to dispatch the creation of active power flow upper bound slack variables. Used when there is not enough flow through the branch in the forward direction.
+
+Docs abbreviation: ``f^\\text{sl,up}``
+"""
+struct FlowActivePowerSlackUpperBound <: VariableType end
+
+"""
+Struct to dispatch the creation of active power flow lower bound slack variables. Used when there is not enough flow through the branch in the reverse direction.
+
+Docs abbreviation: ``f^\\text{sl,lo}``
+"""
+struct FlowActivePowerSlackLowerBound <: VariableType end
+
"""
Struct to dispatch the creation of Voltage Magnitude Variables for AC formulations
-Docs abbreviation: TODO
+Docs abbreviation: ``v``
"""
struct VoltageMagnitude <: VariableType end
"""
Struct to dispatch the creation of Voltage Angle Variables for AC/DC formulations
-Docs abbreviation: TODO
+Docs abbreviation: ``\\theta``
"""
struct VoltageAngle <: VariableType end
"""
Struct to dispatch the creation of bidirectional Active Power Flow Variables
-Docs abbreviation: ``P``
+Docs abbreviation: ``f``
"""
struct FlowActivePowerVariable <: VariableType end
@@ -180,35 +181,35 @@ struct FlowActivePowerVariable <: VariableType end
"""
Struct to dispatch the creation of unidirectional Active Power Flow Variables
-Docs abbreviation: ``\\overrightarrow{P}``
+Docs abbreviation: ``f^\\text{from-to}``
"""
struct FlowActivePowerFromToVariable <: VariableType end
"""
Struct to dispatch the creation of unidirectional Active Power Flow Variables
-Docs abbreviation: ``\\overleftarrow{P}``
+Docs abbreviation: ``f^\\text{to-from}``
"""
struct FlowActivePowerToFromVariable <: VariableType end
"""
Struct to dispatch the creation of unidirectional Reactive Power Flow Variables
-Docs abbreviation: ``\\overrightarrow{Q}``
+Docs abbreviation: ``f^\\text{q,from-to}``
"""
struct FlowReactivePowerFromToVariable <: VariableType end
"""
Struct to dispatch the creation of unidirectional Reactive Power Flow Variables
-Docs abbreviation: ``\\overleftarrow{Q}``
+Docs abbreviation: ``f^\\text{q,to-from}``
"""
struct FlowReactivePowerToFromVariable <: VariableType end
"""
Struct to dispatch the creation of Phase Shifters Variables
-Docs abbreviation: TODO
+Docs abbreviation: ``\\theta^\\text{shift}``
"""
struct PhaseShifterAngle <: VariableType end
@@ -216,39 +217,63 @@ struct PhaseShifterAngle <: VariableType end
"""
Struct to dispatch the creation of HVDC Losses Auxiliary Variables
-Docs abbreviation: TODO
+Docs abbreviation: ``\\ell``
"""
struct HVDCLosses <: VariableType end
"""
Struct to dispatch the creation of HVDC Flow Direction Auxiliary Variables
-Docs abbreviation: TODO
+Docs abbreviation: ``u^\\text{dir}``
"""
struct HVDCFlowDirectionVariable <: VariableType end
+abstract type SparseVariableType <: VariableType end
+
"""
Struct to dispatch the creation of piecewise linear cost variables for objective function
-Docs abbreviation: TODO
+Docs abbreviation: ``\\delta``
+"""
+struct PieceWiseLinearCostVariable <: SparseVariableType end
+
+"""
+Struct to dispatch the creation of piecewise linear block offer variables for objective function
+
+Docs abbreviation: ``\\delta``
+"""
+struct PieceWiseLinearBlockOffer <: SparseVariableType end
+
"""
-struct PieceWiseLinearCostVariable <: VariableType end
+Struct to dispatch the creation of Interface Flow Slack Up variables
+Docs abbreviation: ``f^\\text{sl,up}``
+"""
struct InterfaceFlowSlackUp <: VariableType end
+"""
+Struct to dispatch the creation of Interface Flow Slack Down variables
+Docs abbreviation: ``f^\\text{sl,dn}``
+"""
struct InterfaceFlowSlackDown <: VariableType end
+"""
+Struct to dispatch the creation of Slack variables for UpperBoundFeedforward
+
+Docs abbreviation: ``p^\\text{ff,ubsl}``
+"""
struct UpperBoundFeedForwardSlack <: VariableType end
+"""
+Struct to dispatch the creation of Slack variables for LowerBoundFeedforward
+Docs abbreviation: ``p^\\text{ff,lbsl}``
+"""
struct LowerBoundFeedForwardSlack <: VariableType end
const START_VARIABLES = (HotStartVariable, WarmStartVariable, ColdStartVariable)
-should_write_resulting_value(::Type{<:VariableType}) = true
should_write_resulting_value(::Type{PieceWiseLinearCostVariable}) = false
-
-convert_result_to_natural_units(::Type{<:VariableType}) = false
-
+should_write_resulting_value(::Type{PieceWiseLinearBlockOffer}) = false
convert_result_to_natural_units(::Type{ActivePowerVariable}) = true
convert_result_to_natural_units(::Type{PowerAboveMinimumVariable}) = true
convert_result_to_natural_units(::Type{ActivePowerInVariable}) = true
diff --git a/src/devices_models/device_constructors/branch_constructor.jl b/src/devices_models/device_constructors/branch_constructor.jl
index 7fbe61d540..baa4768200 100644
--- a/src/devices_models/device_constructors/branch_constructor.jl
+++ b/src/devices_models/device_constructors/branch_constructor.jl
@@ -11,8 +11,25 @@ function construct_device!(
},
) where {T <: PSY.ACBranch}
if has_subnetworks(network_model)
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(model, sys)
+
+ if get_use_slacks(model)
+ add_variables!(
+ container,
+ FlowActivePowerSlackUpperBound,
+ network_model,
+ devices,
+ StaticBranch(),
+ )
+ add_variables!(
+ container,
+ FlowActivePowerSlackLowerBound,
+ network_model,
+ devices,
+ StaticBranch(),
+ )
+ end
+
add_variables!(
container,
FlowActivePowerVariable,
@@ -67,6 +84,9 @@ function construct_device!(
NetworkModel{AreaBalancePowerModel},
},
) where {T <: PSY.ACBranch}
+ if get_use_slacks(model)
+ throw(ArgumentError("StaticBranchBounds is not compatible with the use of slacks"))
+ end
if has_subnetworks(network_model)
devices =
get_available_components(model, sys)
@@ -178,13 +198,27 @@ function construct_device!(
container::OptimizationContainer,
sys::PSY.System,
::ArgumentConstructStage,
- model::DeviceModel{T, StaticBranch},
- ::NetworkModel{<:PM.AbstractActivePowerModel},
+ device_model::DeviceModel{T, StaticBranch},
+ network_model::NetworkModel{<:PM.AbstractActivePowerModel},
) where {T <: PSY.ACBranch}
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(device_model, sys)
+ if get_use_slacks(device_model)
+ add_variables!(
+ container,
+ FlowActivePowerSlackUpperBound,
+ devices,
+ StaticBranch(),
+ )
+ add_variables!(
+ container,
+ FlowActivePowerSlackLowerBound,
+ devices,
+ StaticBranch(),
+ )
+ end
+ add_feedforward_arguments!(container, device_model, devices)
- add_feedforward_arguments!(container, model, devices)
+ add_feedforward_arguments!(container, device_model, devices)
end
# For DC Power only. Implements constraints
@@ -192,16 +226,16 @@ function construct_device!(
container::OptimizationContainer,
sys::PSY.System,
::ModelConstructStage,
- model::DeviceModel{T, StaticBranch},
- network_model::NetworkModel{<:PM.AbstractActivePowerModel},
-) where {T <: PSY.ACBranch}
+ device_model::DeviceModel{T, StaticBranch},
+ network_model::NetworkModel{U},
+) where {T <: PSY.ACBranch, U <: PM.AbstractActivePowerModel}
@debug "construct_device" _group = LOG_GROUP_BRANCH_CONSTRUCTIONS
- devices =
- get_available_components(model, sys)
- add_constraints!(container, RateLimitConstraint, devices, model, network_model)
- add_feedforward_constraints!(container, model, devices)
- add_constraint_dual!(container, sys, model)
+ devices = get_available_components(device_model, sys)
+ add_constraints!(container, RateLimitConstraint, devices, device_model, network_model)
+ add_feedforward_constraints!(container, device_model, devices)
+ objective_function!(container, devices, device_model, U)
+ add_constraint_dual!(container, sys, device_model)
return
end
@@ -211,10 +245,26 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, StaticBranch},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(model, sys)
+ if get_use_slacks(model)
+ add_variables!(
+ container,
+ FlowActivePowerSlackUpperBound,
+ network_model,
+ devices,
+ StaticBranch(),
+ )
+ add_variables!(
+ container,
+ FlowActivePowerSlackLowerBound,
+ network_model,
+ devices,
+ StaticBranch(),
+ )
+ end
+
add_variables!(
container,
FlowActivePowerVariable,
@@ -230,12 +280,12 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{T, StaticBranch},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(model, sys)
add_constraints!(container, NetworkFlowConstraint, devices, model, network_model)
add_constraints!(container, RateLimitConstraint, devices, model, network_model)
+ objective_function!(container, devices, model, PTDFPowerModel)
add_constraint_dual!(container, sys, model)
return
end
@@ -245,10 +295,14 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, StaticBranchBounds},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(model, sys)
+
+ if get_use_slacks(model)
+ throw(ArgumentError("StaticBranchBounds is not compatible with the use of slacks"))
+ end
+
add_variables!(
container,
FlowActivePowerVariable,
@@ -264,7 +318,7 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{T, StaticBranchBounds},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
devices =
get_available_components(model, sys)
@@ -284,7 +338,7 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, StaticBranchUnbounded},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
devices =
get_available_components(model, sys)
@@ -303,7 +357,7 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{T, StaticBranchUnbounded},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.ACBranch}
devices =
get_available_components(model, sys)
@@ -315,12 +369,26 @@ end
# For AC Power only. Implements Bounds on the active power and rating constraints on the aparent power
function construct_device!(
- ::OptimizationContainer,
- ::PSY.System,
+ container::OptimizationContainer,
+ sys::PSY.System,
::ArgumentConstructStage,
- ::DeviceModel{T, StaticBranch},
- ::NetworkModel{<:PM.AbstractPowerModel},
-) where {T <: PSY.ACBranch} end
+ device_model::DeviceModel{T, StaticBranch},
+ network_model::NetworkModel{<:PM.AbstractPowerModel},
+) where {T <: PSY.ACBranch}
+ devices = get_available_components(device_model, sys)
+
+ if get_use_slacks(device_model)
+ # Only one slack is needed for this formulations in AC
+ add_variables!(
+ container,
+ FlowActivePowerSlackUpperBound,
+ devices,
+ StaticBranch(),
+ )
+ end
+
+ return
+end
function construct_device!(
container::OptimizationContainer,
@@ -343,21 +411,56 @@ function construct_device!(
container::OptimizationContainer,
sys::PSY.System,
::ArgumentConstructStage,
- ::DeviceModel{T, StaticBranchBounds},
+ device_model::DeviceModel{T, StaticBranchBounds},
::NetworkModel{<:PM.AbstractPowerModel},
-) where {T <: PSY.ACBranch} end
+) where {T <: PSY.ACBranch}
+ if get_use_slacks(device_model)
+ throw(
+ ArgumentError(
+ "StaticBranchBounds is not compatible with the use of slacks",
+ ),
+ )
+ end
+ return
+end
function construct_device!(
container::OptimizationContainer,
sys::PSY.System,
::ModelConstructStage,
- model::DeviceModel{T, StaticBranchBounds},
+ device_model::DeviceModel{T, StaticBranchBounds},
network_model::NetworkModel{<:PM.AbstractPowerModel},
) where {T <: PSY.ACBranch}
- devices =
- get_available_components(model, sys)
- branch_rate_bounds!(container, devices, model, network_model)
- add_constraint_dual!(container, sys, model)
+ devices = get_available_components(device_model, sys)
+ branch_rate_bounds!(container, devices, device_model, network_model)
+ add_constraints!(
+ container,
+ RateLimitConstraintFromTo,
+ devices,
+ device_model,
+ network_model,
+ )
+ add_constraints!(
+ container,
+ RateLimitConstraintToFrom,
+ devices,
+ device_model,
+ network_model,
+ )
+ add_constraint_dual!(container, sys, device_model)
+ return
+end
+
+function construct_device!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ModelConstructStage,
+ device_model::DeviceModel{T, StaticBranchBounds},
+ network_model::NetworkModel{<:PM.AbstractActivePowerModel},
+) where {T <: PSY.ACBranch}
+ devices = get_available_components(device_model, sys)
+ branch_rate_bounds!(container, devices, device_model, network_model)
+ add_constraint_dual!(container, sys, device_model)
return
end
@@ -370,8 +473,7 @@ function construct_device!(
network_model::NetworkModel{CopperPlatePowerModel},
) where {T <: TwoTerminalHVDCTypes}
if has_subnetworks(network_model)
- devices =
- get_available_components(model, sys)
+ devices = get_available_components(model, sys)
add_variables!(
container,
FlowActivePowerVariable,
@@ -427,10 +529,11 @@ function construct_device!(
container::OptimizationContainer,
sys::PSY.System,
::ModelConstructStage,
- model::DeviceModel{<:TwoTerminalHVDCTypes, HVDCTwoTerminalUnbounded},
+ device_model::DeviceModel{<:TwoTerminalHVDCTypes, HVDCTwoTerminalUnbounded},
::NetworkModel{<:PM.AbstractPowerModel},
)
- add_constraint_dual!(container, sys, model)
+ add_constraint_dual!(container, sys, device_model)
+ return
end
# Repeated method to avoid ambiguity between HVDCTwoTerminalUnbounded and HVDCTwoTerminalLossless
@@ -439,7 +542,7 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, HVDCTwoTerminalUnbounded},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: TwoTerminalHVDCTypes}
devices =
get_available_components(model, sys)
@@ -461,7 +564,7 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{<:TwoTerminalHVDCTypes, HVDCTwoTerminalUnbounded},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
)
add_constraint_dual!(container, sys, model)
return
@@ -497,7 +600,7 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, HVDCTwoTerminalLossless},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: TwoTerminalHVDCTypes}
devices =
get_available_components(model, sys)
@@ -519,10 +622,9 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{T, HVDCTwoTerminalLossless},
- network_model::NetworkModel{U},
+ network_model::NetworkModel{PTDFPowerModel},
) where {
T <: TwoTerminalHVDCTypes,
- U <: PTDFPowerModel,
}
devices =
get_available_components(model, sys)
@@ -536,7 +638,7 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{T, HVDCTwoTerminalDispatch},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: TwoTerminalHVDCTypes}
devices =
get_available_components(model, sys)
@@ -586,7 +688,7 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{T, HVDCTwoTerminalDispatch},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: TwoTerminalHVDCTypes}
devices =
get_available_components(model, sys)
@@ -704,7 +806,7 @@ function construct_device!(
sys::PSY.System,
::ArgumentConstructStage,
model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
)
devices = get_available_components(
model,
@@ -746,7 +848,7 @@ function construct_device!(
sys::PSY.System,
::ModelConstructStage,
model::DeviceModel{PSY.PhaseShiftingTransformer, PhaseAngleControl},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
)
devices = get_available_components(
model,
@@ -758,3 +860,187 @@ function construct_device!(
add_constraint_dual!(container, sys, model)
return
end
+
+################################# AreaInterchange Models ################################
+function construct_device!(
+ ::OptimizationContainer,
+ ::PSY.System,
+ ::ArgumentConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, U},
+ network_model::NetworkModel{T},
+) where {T <: PM.AbstractPowerModel, U <: Union{StaticBranchUnbounded, StaticBranch}}
+ error("AreaInterchange is not yet implemented for $T")
+ return
+end
+
+function construct_device!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ArgumentConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, T},
+ network_model::NetworkModel{U},
+) where {
+ T <: Union{StaticBranchUnbounded, StaticBranch},
+ U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel},
+}
+ if get_use_slacks(model)
+ add_variables!(
+ container,
+ FlowActivePowerSlackUpperBound,
+ network_model,
+ devices,
+ T(),
+ )
+ add_variables!(
+ container,
+ FlowActivePowerSlackLowerBound,
+ network_model,
+ devices,
+ T(),
+ )
+ end
+ devices = get_available_components(model, sys)
+ has_ts = PSY.has_time_series.(devices)
+ if any(has_ts) && !all(has_ts)
+ error(
+ "Not all AreaInterchange devices have time series. Check data to complete (or remove) time series.",
+ )
+ end
+ add_variables!(
+ container,
+ FlowActivePowerVariable,
+ network_model,
+ devices,
+ T(),
+ )
+ add_to_expression!(
+ container,
+ ActivePowerBalance,
+ FlowActivePowerVariable,
+ devices,
+ model,
+ network_model,
+ )
+ if all(has_ts)
+ for device in devices
+ name = PSY.get_name(device)
+ num_ts = length(unique(PSY.get_name.(PSY.get_time_series_keys(device))))
+ if num_ts < 2
+ error(
+ "AreaInterchange $name has less than two time series. It is required to add both from_to and to_from time series.",
+ )
+ end
+ end
+ add_parameters!(container, FromToFlowLimitParameter, devices, model)
+ add_parameters!(container, ToFromFlowLimitParameter, devices, model)
+ end
+ return
+end
+
+function construct_device!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ModelConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, StaticBranch},
+ network_model::NetworkModel{T},
+) where {T <: AreaBalancePowerModel}
+ devices = get_available_components(model, sys)
+ add_constraints!(container, FlowLimitConstraint, devices, model, network_model)
+ return
+end
+
+function _get_branch_map(
+ container::OptimizationContainer,
+ network_model::NetworkModel{AreaPTDFPowerModel},
+ sys::PSY.System,
+)
+ @assert !isempty(network_model.modeled_branch_types)
+
+ inter_area_branch_map =
+ Dict{Tuple{PSY.Area, PSY.Area}, Dict{DataType, Vector{<:PSY.ACBranch}}}()
+ for branch_type in network_model.modeled_branch_types
+ if branch_type == PSY.AreaInterchange
+ continue
+ end
+ if !has_container_key(container, FlowActivePowerVariable, branch_type)
+ continue
+ end
+ flow_vars = get_variable(container, FlowActivePowerVariable(), branch_type)
+ branch_names = axes(flow_vars)[1]
+ for bname in branch_names
+ d = PSY.get_component(branch_type, sys, bname)
+ area_from = PSY.get_area(PSY.get_arc(d).from)
+ area_to = PSY.get_area(PSY.get_arc(d).to)
+ if area_from != area_to
+ branch_typed_dict = get!(
+ inter_area_branch_map,
+ (area_from, area_to),
+ Dict{DataType, Vector{<:PSY.ACBranch}}(),
+ )
+ if !haskey(branch_typed_dict, branch_type)
+ branch_typed_dict[branch_type] = [d]
+ else
+ push!(branch_typed_dict[branch_type], d)
+ end
+ end
+ end
+ end
+ return inter_area_branch_map
+end
+
+function construct_device!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ModelConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, StaticBranch},
+ network_model::NetworkModel{T},
+) where {T <: AreaPTDFPowerModel}
+ devices = get_available_components(model, sys)
+ add_constraints!(container, FlowLimitConstraint, devices, model, network_model)
+ # Not ideal to do this here, but it is a not terrible workaround
+ # The area interchanges are like a services/device mix.
+ # Doesn't include the possibility of Multi-terminal HVDC
+ inter_area_branch_map = _get_branch_map(container, network_model, sys)
+
+ add_constraints!(
+ container,
+ LineFlowBoundConstraint,
+ devices,
+ model,
+ network_model,
+ inter_area_branch_map,
+ )
+ return
+end
+
+function construct_device!(
+ ::OptimizationContainer,
+ ::PSY.System,
+ ::ModelConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded},
+ network_model::NetworkModel{AreaBalancePowerModel},
+)
+ return
+end
+
+function construct_device!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ModelConstructStage,
+ model::DeviceModel{PSY.AreaInterchange, StaticBranchUnbounded},
+ network_model::NetworkModel{AreaPTDFPowerModel},
+)
+ inter_area_branch_map = _get_branch_map(container, network_model, sys)
+ # Not ideal to do this here, but it is a not terrible workaround
+ # The area interchanges are like a services/device mix.
+ # Doesn't include the possibility of Multi-terminal HVDC
+ add_constraints!(
+ container,
+ LineFlowBoundConstraint,
+ devices,
+ model,
+ network_model,
+ inter_area_branch_map,
+ )
+ return
+end
diff --git a/src/devices_models/devices/AC_branches.jl b/src/devices_models/devices/AC_branches.jl
index 035305affe..fc7b1e1e51 100644
--- a/src/devices_models/devices/AC_branches.jl
+++ b/src/devices_models/devices/AC_branches.jl
@@ -26,6 +26,17 @@ get_variable_multiplier(::PhaseShifterAngle, d::PSY.PhaseShiftingTransformer, ::
get_initial_conditions_device_model(::OperationModel, ::DeviceModel{T, U}) where {T <: PSY.ACBranch, U <: AbstractBranchFormulation} = DeviceModel(T, U)
+#### Properties of slack variables
+get_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.ACBranch}, ::AbstractBranchFormulation,) = false
+get_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.ACBranch}, ::AbstractBranchFormulation,) = false
+# These two methods are defined to avoid ambiguities
+get_variable_binary(::FlowActivePowerSlackUpperBound, ::Type{<:PSY.TwoTerminalHVDCLine}, ::AbstractTwoTerminalDCLineFormulation,) = false
+get_variable_binary(::FlowActivePowerSlackLowerBound, ::Type{<:PSY.TwoTerminalHVDCLine}, ::AbstractTwoTerminalDCLineFormulation,) = false
+get_variable_upper_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACBranch, ::AbstractBranchFormulation) = nothing
+get_variable_lower_bound(::FlowActivePowerSlackUpperBound, ::PSY.ACBranch, ::AbstractBranchFormulation) = 0.0
+get_variable_upper_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACBranch, ::AbstractBranchFormulation) = nothing
+get_variable_lower_bound(::FlowActivePowerSlackLowerBound, ::PSY.ACBranch, ::AbstractBranchFormulation) = 0.0
+
#! format: on
function get_default_time_series_names(
::Type{U},
@@ -44,19 +55,25 @@ end
# Additional Method to be able to filter the branches that are not in the PTDF matrix
function add_variables!(
container::OptimizationContainer,
- ::Type{FlowActivePowerVariable},
- network_model::NetworkModel{PTDFPowerModel},
- devices::IS.FlattenIteratorWrapper{T},
+ ::Type{T},
+ network_model::NetworkModel{<:AbstractPTDFModel},
+ devices::IS.FlattenIteratorWrapper{U},
formulation::AbstractBranchFormulation,
-) where {T <: PSY.ACBranch}
+) where {
+ T <: Union{
+ FlowActivePowerVariable,
+ FlowActivePowerSlackUpperBound,
+ FlowActivePowerSlackLowerBound,
+ },
+ U <: PSY.ACBranch}
time_steps = get_time_steps(container)
ptdf = get_PTDF_matrix(network_model)
branches_in_ptdf =
[b for b in devices if PSY.get_name(b) ∈ Set(PNM.get_branch_ax(ptdf))]
variable = add_variable_container!(
container,
- FlowActivePowerVariable(),
- T,
+ T(),
+ U,
PSY.get_name.(branches_in_ptdf),
time_steps,
)
@@ -67,15 +84,16 @@ function add_variables!(
for t in time_steps
variable[name, t] = JuMP.@variable(
get_jump_model(container),
- base_name = "FlowActivePowerVariable_$(T)_{$(name), $(t)}",
+ base_name = "$(T)_$(U)_{$(name), $(t)}",
)
- ub = get_variable_upper_bound(FlowActivePowerVariable(), d, formulation)
+ ub = get_variable_upper_bound(T(), d, formulation)
ub !== nothing && JuMP.set_upper_bound(variable[name, t], ub)
- lb = get_variable_lower_bound(FlowActivePowerVariable(), d, formulation)
+ lb = get_variable_lower_bound(T(), d, formulation)
lb !== nothing && JuMP.set_lower_bound(variable[name, t], lb)
end
end
+ return
end
function add_variables!(
@@ -116,8 +134,8 @@ function branch_rate_bounds!(
continue
end
for t in get_time_steps(container)
- JuMP.set_upper_bound(var[name, t], PSY.get_rate(d))
- JuMP.set_lower_bound(var[name, t], -1.0 * PSY.get_rate(d))
+ JuMP.set_upper_bound(var[name, t], PSY.get_rating(d))
+ JuMP.set_lower_bound(var[name, t], -1.0 * PSY.get_rating(d))
end
end
return
@@ -144,8 +162,8 @@ function branch_rate_bounds!(
continue
end
for t in time_steps, var in vars
- JuMP.set_upper_bound(var[name, t], PSY.get_rate(d))
- JuMP.set_lower_bound(var[name, t], -1.0 * PSY.get_rate(d))
+ JuMP.set_upper_bound(var[name, t], PSY.get_rating(d))
+ JuMP.set_lower_bound(var[name, t], -1.0 * PSY.get_rating(d))
end
end
return
@@ -161,7 +179,7 @@ function get_min_max_limits(
::Type{<:ConstraintType},
::Type{<:AbstractBranchFormulation},
) # -> Union{Nothing, NamedTuple{(:min, :max), Tuple{Float64, Float64}}}
- return (min = -1 * PSY.get_rate(device), max = PSY.get_rate(device))
+ return (min = -1 * PSY.get_rating(device), max = PSY.get_rating(device))
end
"""
@@ -182,7 +200,7 @@ function add_constraints!(
container::OptimizationContainer,
cons_type::Type{RateLimitConstraint},
devices::IS.FlattenIteratorWrapper{T},
- ::DeviceModel{T, U},
+ device_model::DeviceModel{T, U},
network_model::NetworkModel{V},
) where {
T <: PSY.ACBranch,
@@ -218,6 +236,12 @@ function add_constraints!(
array = get_variable(container, FlowActivePowerVariable(), T)
+ use_slacks = get_use_slacks(device_model)
+ if use_slacks
+ slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), T)
+ slack_lb = get_variable(container, FlowActivePowerSlackLowerBound(), T)
+ end
+
for device in devices
ci_name = PSY.get_name(device)
if ci_name ∈ PNM.get_radial_branches(radial_network_reduction)
@@ -226,9 +250,13 @@ function add_constraints!(
limits = get_min_max_limits(device, RateLimitConstraint, U) # depends on constraint type and formulation type
for t in time_steps
con_ub[ci_name, t] =
- JuMP.@constraint(container.JuMPmodel, array[ci_name, t] <= limits.max)
+ JuMP.@constraint(get_jump_model(container),
+ array[ci_name, t] - (use_slacks ? slack_ub[ci_name, t] : 0.0) <=
+ limits.max)
con_lb[ci_name, t] =
- JuMP.@constraint(container.JuMPmodel, array[ci_name, t] >= limits.min)
+ JuMP.@constraint(get_jump_model(container),
+ array[ci_name, t] + (use_slacks ? slack_lb[ci_name, t] : 0.0) >=
+ limits.min)
end
end
return
@@ -262,6 +290,54 @@ function add_constraints!(
return
end
+function _constraint_without_slacks!(
+ container::OptimizationContainer,
+ constraint::JuMPConstraintArray,
+ rating_data::Vector{Tuple{String, Float64}},
+ time_steps::UnitRange{Int64},
+ radial_branches_names::Set{String},
+ var1::JuMPVariableArray,
+ var2::JuMPVariableArray,
+)
+ for (branch_name, branch_rate) in rating_data
+ if branch_name ∈ radial_branches_names
+ continue
+ end
+ for t in time_steps
+ constraint[branch_name, t] = JuMP.@constraint(
+ get_jump_model(container),
+ var1[branch_name, t]^2 + var2[branch_name, t]^2 <= branch_rate^2
+ )
+ end
+ end
+ return
+end
+
+function _constraint_with_slacks!(
+ container::OptimizationContainer,
+ constraint::JuMPConstraintArray,
+ rating_data::Vector{Tuple{String, Float64}},
+ time_steps::UnitRange{Int64},
+ radial_branches_names::Set{String},
+ var1::JuMPVariableArray,
+ var2::JuMPVariableArray,
+ slack_ub::JuMPVariableArray,
+)
+ for (branch_name, branch_rate) in rating_data
+ if branch_name ∈ radial_branches_names
+ continue
+ end
+ for t in time_steps
+ constraint[branch_name, t] = JuMP.@constraint(
+ get_jump_model(container),
+ var1[branch_name, t]^2 + var2[branch_name, t]^2 -
+ slack_ub[branch_name, t] <= branch_rate^2
+ )
+ end
+ end
+ return
+end
+
"""
Add rate limit from to constraints for ACBranch with AbstractPowerModel
"""
@@ -269,10 +345,10 @@ function add_constraints!(
container::OptimizationContainer,
cons_type::Type{RateLimitConstraintFromTo},
devices::IS.FlattenIteratorWrapper{B},
- ::DeviceModel{B, <:AbstractBranchFormulation},
+ device_model::DeviceModel{B, <:AbstractBranchFormulation},
network_model::NetworkModel{T},
) where {B <: PSY.ACBranch, T <: PM.AbstractPowerModel}
- rating_data = [(PSY.get_name(h), PSY.get_rate(h)) for h in devices]
+ rating_data = [(PSY.get_name(h), PSY.get_rating(h)) for h in devices]
time_steps = get_time_steps(container)
var1 = get_variable(container, FlowActivePowerFromToVariable(), B)
@@ -289,17 +365,31 @@ function add_constraints!(
radial_network_reduction = get_radial_network_reduction(network_model)
radial_branches_names = PNM.get_radial_branches(radial_network_reduction)
- for r in rating_data
- if r[1] ∈ radial_branches_names
- continue
- end
- for t in time_steps
- constraint[r[1], t] = JuMP.@constraint(
- get_jump_model(container),
- var1[r[1], t]^2 + var2[r[1], t]^2 <= r[2]^2
- )
- end
+ use_slacks = get_use_slacks(device_model)
+ if use_slacks
+ slack_ub = get_variable(container, FlowActivePowerSlackUpperBound(), B)
+ _constraint_with_slacks!(
+ container,
+ constraint,
+ rating_data,
+ time_steps,
+ radial_branches_names,
+ var1,
+ var2,
+ slack_ub,
+ )
end
+
+ _constraint_without_slacks!(
+ container,
+ constraint,
+ rating_data,
+ time_steps,
+ radial_branches_names,
+ var1,
+ var2,
+ )
+
return
end
@@ -313,7 +403,7 @@ function add_constraints!(
::DeviceModel{B, <:AbstractBranchFormulation},
network_model::NetworkModel{T},
) where {B <: PSY.ACBranch, T <: PM.AbstractPowerModel}
- rating_data = [(PSY.get_name(h), PSY.get_rate(h)) for h in devices]
+ rating_data = [(PSY.get_name(h), PSY.get_rating(h)) for h in devices]
time_steps = get_time_steps(container)
var1 = get_variable(container, FlowActivePowerToFromVariable(), B)
@@ -341,17 +431,100 @@ function add_constraints!(
)
end
end
+ return
+end
+
+const ValidPTDFS = Union{
+ PNM.PTDF{
+ Tuple{Vector{Int}, Vector{String}},
+ Tuple{Dict{Int64, Int64}, Dict{String, Int64}},
+ Matrix{Float64},
+ },
+ PNM.VirtualPTDF{
+ Tuple{Vector{String}, Vector{Int64}},
+ Tuple{Dict{String, Int64}, Dict{Int64, Int64}},
+ },
+}
+
+function _make_flow_expressions!(
+ jump_model::JuMP.Model,
+ name::String,
+ time_steps::UnitRange{Int},
+ ptdf_col::AbstractVector{Float64},
+ nodal_balance_expressions::Matrix{JuMP.AffExpr},
+)
+ @debug Threads.threadid() name
+ expressions = Vector{JuMP.AffExpr}(undef, length(time_steps))
+ for t in time_steps
+ expressions[t] = JuMP.@expression(
+ jump_model,
+ sum(
+ ptdf_col[i] * nodal_balance_expressions[i, t] for
+ i in 1:length(ptdf_col)
+ )
+ )
+ end
+ return name, expressions
+ # change when using the not concurrent version
+ #return expressions
end
+function _make_flow_expressions!(
+ container::OptimizationContainer,
+ branches::Vector{String},
+ time_steps::UnitRange{Int},
+ ptdf::ValidPTDFS,
+ nodal_balance_expressions::JuMPAffineExpressionDArray,
+ branch_Type::DataType,
+)
+ branch_flow_expr = add_expression_container!(container,
+ PTDFBranchFlow(),
+ branch_Type,
+ branches,
+ time_steps,
+ )
+
+ jump_model = get_jump_model(container)
+
+ tasks = map(branches) do name
+ ptdf_col = ptdf[name, :]
+ Threads.@spawn _make_flow_expressions!(
+ jump_model,
+ name,
+ time_steps,
+ ptdf_col,
+ nodal_balance_expressions.data,
+ )
+ end
+ for task in tasks
+ name, expressions = fetch(task)
+ branch_flow_expr[name, :] .= expressions
+ end
+
+ #= Leaving serial code commented out for debugging purposes in the future
+ for name in branches
+ ptdf_col = ptdf[name, :]
+ branch_flow_expr[name, :] .= _make_flow_expressions!(
+ jump_model,
+ name,
+ time_steps,
+ ptdf_col,
+ nodal_balance_expressions.data,
+ )
+ end
+ =#
+
+ return branch_flow_expr
+end
"""
-Add network flow constraints for ACBranch and NetworkModel with PTDFPowerModel
+Add network flow constraints for ACBranch and NetworkModel with <: AbstractPTDFModel
"""
function add_constraints!(
container::OptimizationContainer,
::Type{NetworkFlowConstraint},
devices::IS.FlattenIteratorWrapper{B},
model::DeviceModel{B, <:AbstractBranchFormulation},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {B <: PSY.ACBranch}
ptdf = get_PTDF_matrix(network_model)
# This is a workaround to not call the same list comprehension to find
@@ -368,33 +541,36 @@ function add_constraints!(
)
nodal_balance_expressions =
get_expression(container, ActivePowerBalance(), PSY.ACBus)
-
flow_variables = get_variable(container, FlowActivePowerVariable(), B)
+ branch_flow_expr = _make_flow_expressions!(
+ container,
+ branches,
+ time_steps,
+ ptdf,
+ nodal_balance_expressions,
+ B,
+ )
jump_model = get_jump_model(container)
for name in branches
- ptdf_col = ptdf[name, :]
- flow_variables_ = flow_variables[name, :]
for t in time_steps
branch_flow[name, t] = JuMP.@constraint(
jump_model,
- sum(
- ptdf_col[i] * nodal_balance_expressions.data[i, t] for
- i in 1:length(ptdf_col)
- ) - flow_variables_[t] == 0.0
+ branch_flow_expr[name, t] - flow_variables[name, t] == 0.0
)
end
end
+ return
end
"""
-Add network flow constraints for PhaseShiftingTransformer and NetworkModel with PTDFPowerModel
+Add network flow constraints for PhaseShiftingTransformer and NetworkModel with <: AbstractPTDFModel
"""
function add_constraints!(
container::OptimizationContainer,
::Type{NetworkFlowConstraint},
devices::IS.FlattenIteratorWrapper{T},
model::DeviceModel{T, PhaseAngleControl},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: PSY.PhaseShiftingTransformer}
ptdf = get_PTDF_matrix(network_model)
branches = PSY.get_name.(devices)
@@ -440,7 +616,7 @@ function get_min_max_limits(
)
end
limit = min(
- PSY.get_rate(device),
+ PSY.get_rating(device),
PSY.get_flow_limits(device).to_from,
PSY.get_flow_limits(device).from_to,
)
@@ -529,39 +705,6 @@ function get_min_max_limits(
)
end
-"""
-Add branch flow constraints for monitored lines
-"""
-function add_constraints!(
- container::OptimizationContainer,
- ::Type{FlowLimitFromToConstraint},
- devices::IS.FlattenIteratorWrapper{T},
- model::DeviceModel{T, U},
- ::NetworkModel{V},
-) where {
- T <: PSY.MonitoredLine,
- U <: AbstractBranchFormulation,
- V <: PM.AbstractActivePowerModel,
-}
- add_range_constraints!(
- container,
- FlowLimitFromToConstraint,
- FlowActivePowerFromToVariable,
- devices,
- model,
- X,
- )
- add_range_constraints!(
- container,
- FlowLimitToFromConstraint,
- FlowActivePowerToFromVariable,
- devices,
- model,
- X,
- )
- return
-end
-
"""
Don't add branch flow constraints for monitored lines if formulation is StaticBranchUnbounded
"""
@@ -643,3 +786,47 @@ function add_constraints!(
end
return
end
+
+function objective_function!(
+ container::OptimizationContainer,
+ ::IS.FlattenIteratorWrapper{T},
+ device_model::DeviceModel{T, <:AbstractBranchFormulation},
+ ::Type{<:PM.AbstractPowerModel},
+) where {T <: PSY.ACBranch}
+ if get_use_slacks(device_model)
+ variable_up = get_variable(container, FlowActivePowerSlackUpperBound(), T)
+ # Use device names because there might be a radial network reduction
+ for name in axes(variable_up, 1)
+ for t in get_time_steps(container)
+ add_to_objective_invariant_expression!(
+ container,
+ variable_up[name, t] * CONSTRAINT_VIOLATION_SLACK_COST,
+ )
+ end
+ end
+ end
+ return
+end
+
+function objective_function!(
+ container::OptimizationContainer,
+ ::IS.FlattenIteratorWrapper{T},
+ device_model::DeviceModel{T, <:AbstractBranchFormulation},
+ ::Type{<:PM.AbstractActivePowerModel},
+) where {T <: PSY.ACBranch}
+ if get_use_slacks(device_model)
+ variable_up = get_variable(container, FlowActivePowerSlackUpperBound(), T)
+ variable_dn = get_variable(container, FlowActivePowerSlackLowerBound(), T)
+ # Use device names because there might be a radial network reduction
+ for name in axes(variable_up, 1)
+ for t in get_time_steps(container)
+ add_to_objective_invariant_expression!(
+ container,
+ (variable_dn[name, t] + variable_up[name, t]) *
+ CONSTRAINT_VIOLATION_SLACK_COST,
+ )
+ end
+ end
+ end
+ return
+end
diff --git a/src/devices_models/devices/HVDCsystems.jl b/src/devices_models/devices/HVDCsystems.jl
index ad36f26954..ecf47f4d89 100644
--- a/src/devices_models/devices/HVDCsystems.jl
+++ b/src/devices_models/devices/HVDCsystems.jl
@@ -165,6 +165,74 @@ function add_to_expression!(
return
end
+
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{AreaPTDFPowerModel},
+) where {
+ T <: ActivePowerBalance,
+ U <: ActivePowerVariable,
+ V <: PSY.InterconnectingConverter,
+ W <: AbstractConverterFormulation,
+}
+ error("AreaPTDFPowerModel doesn't support InterconnectingConverter")
+ return
+end
+
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{PTDFPowerModel},
+) where {
+ T <: ActivePowerBalance,
+ U <: ActivePowerVariable,
+ V <: PSY.InterconnectingConverter,
+ W <: AbstractConverterFormulation,
+}
+ variable = get_variable(container, U(), V)
+ expression_dc = get_expression(container, T(), PSY.DCBus)
+ expression_ac = get_expression(container, T(), PSY.ACBus)
+ for d in devices, t in get_time_steps(container)
+ name = PSY.get_name(d)
+ bus_number_dc = PSY.get_number(PSY.get_dc_bus(d))
+ bus_number_ac = PSY.get_number(PSY.get_bus(d))
+ _add_to_jump_expression!(
+ expression_ac[bus_number_ac, t],
+ variable[name, t],
+ 1.0,
+ )
+ _add_to_jump_expression!(
+ expression_dc[bus_number_dc, t],
+ variable[name, t],
+ -1.0,
+ )
+ end
+ return
+end
+
+function add_to_expression!(
+ ::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{AreaBalancePowerModel},
+) where {
+ T <: ActivePowerBalance,
+ U <: ActivePowerVariable,
+ V <: PSY.InterconnectingConverter,
+ W <: AbstractConverterFormulation,
+}
+ return
+end
+
function add_to_expression!(
::OptimizationContainer,
::Type{T},
diff --git a/src/devices_models/devices/area_interchange.jl b/src/devices_models/devices/area_interchange.jl
new file mode 100644
index 0000000000..4233f9c7c0
--- /dev/null
+++ b/src/devices_models/devices/area_interchange.jl
@@ -0,0 +1,206 @@
+#! format: off
+get_multiplier_value(::FromToFlowLimitParameter, d::PSY.AreaInterchange, ::AbstractBranchFormulation) = -1.0 * PSY.get_from_to_flow_limit(d)
+get_multiplier_value(::ToFromFlowLimitParameter, d::PSY.AreaInterchange, ::AbstractBranchFormulation) = PSY.get_to_from_flow_limit(d)
+#! format: on
+
+function get_default_time_series_names(
+ ::Type{PSY.AreaInterchange},
+ ::Type{V},
+) where {V <: AbstractBranchFormulation}
+ return Dict{Type{<:TimeSeriesParameter}, String}(
+ FromToFlowLimitParameter => "from_to_flow_limit",
+ ToFromFlowLimitParameter => "to_from_flow_limit",
+ )
+end
+
+function get_default_attributes(
+ ::Type{PSY.AreaInterchange},
+ ::Type{V},
+) where {V <: AbstractBranchFormulation}
+ return Dict{String, Any}()
+end
+
+function add_variables!(
+ container::OptimizationContainer,
+ ::Type{FlowActivePowerVariable},
+ model::NetworkModel{T},
+ devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},
+ formulation::AbstractBranchFormulation,
+) where {T <: Union{AreaBalancePowerModel, AreaPTDFPowerModel}}
+ time_steps = get_time_steps(container)
+
+ variable = add_variable_container!(
+ container,
+ FlowActivePowerVariable(),
+ PSY.AreaInterchange,
+ PSY.get_name.(devices),
+ time_steps,
+ )
+
+ for device in devices, t in time_steps
+ device_name = get_name(device)
+ variable[device_name, t] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "FlowActivePowerVariable_AreaInterchange_{$(device_name), $(t)}",
+ )
+ end
+ return
+end
+
+"""
+Add flow constraints for area interchanges
+"""
+function add_constraints!(
+ container::OptimizationContainer,
+ ::Type{FlowLimitConstraint},
+ devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},
+ model::DeviceModel{PSY.AreaInterchange, StaticBranch},
+ ::NetworkModel{T},
+) where {T <: Union{AreaBalancePowerModel, AreaPTDFPowerModel}}
+ time_steps = get_time_steps(container)
+ device_names = [PSY.get_name(d) for d in devices]
+
+ con_ub = add_constraints_container!(
+ container,
+ FlowLimitConstraint(),
+ PSY.AreaInterchange,
+ device_names,
+ time_steps;
+ meta = "ub",
+ )
+
+ con_lb = add_constraints_container!(
+ container,
+ FlowLimitConstraint(),
+ PSY.AreaInterchange,
+ device_names,
+ time_steps;
+ meta = "lb",
+ )
+
+ var_array = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)
+ if !all(PSY.has_time_series.(devices))
+ for device in devices
+ ci_name = PSY.get_name(device)
+ to_from_limit = PSY.get_flow_limits(device).to_from
+ from_to_limit = PSY.get_flow_limits(device).from_to
+ for t in time_steps
+ con_lb[ci_name, t] =
+ JuMP.@constraint(
+ get_jump_model(container),
+ var_array[ci_name, t] >= -1.0 * from_to_limit
+ )
+ con_ub[ci_name, t] =
+ JuMP.@constraint(
+ get_jump_model(container),
+ var_array[ci_name, t] <= to_from_limit
+ )
+ end
+ end
+ else
+ param_container_from_to =
+ get_parameter(container, FromToFlowLimitParameter(), PSY.AreaInterchange)
+ param_multiplier_from_to = get_parameter_multiplier_array(
+ container,
+ FromToFlowLimitParameter(),
+ PSY.AreaInterchange,
+ )
+ param_container_to_from =
+ get_parameter(container, ToFromFlowLimitParameter(), PSY.AreaInterchange)
+ param_multiplier_to_from = get_parameter_multiplier_array(
+ container,
+ ToFromFlowLimitParameter(),
+ PSY.AreaInterchange,
+ )
+ jump_model = get_jump_model(container)
+ for device in devices
+ name = PSY.get_name(device)
+ param_from_to = get_parameter_column_refs(param_container_from_to, name)
+ param_to_from = get_parameter_column_refs(param_container_to_from, name)
+ for t in time_steps
+ con_lb[name, t] = JuMP.@constraint(
+ jump_model,
+ var_array[name, t] >=
+ param_multiplier_from_to[name, t] * param_from_to[t]
+ )
+ con_ub[name, t] = JuMP.@constraint(
+ jump_model,
+ var_array[name, t] <=
+ param_multiplier_to_from[name, t] * param_to_from[t]
+ )
+ end
+ end
+ end
+ return
+end
+
+function add_constraints!(
+ container::OptimizationContainer,
+ ::Type{LineFlowBoundConstraint},
+ devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},
+ model::DeviceModel{PSY.AreaInterchange, <:AbstractBranchFormulation},
+ network_model::NetworkModel{AreaPTDFPowerModel},
+ inter_area_branch_map::Dict{
+ Tuple{PSY.Area, PSY.Area},
+ Dict{DataType, Vector{<:PSY.ACBranch}},
+ },
+)
+ @assert !isempty(inter_area_branch_map)
+ time_steps = get_time_steps(container)
+ device_names = [PSY.get_name(d) for d in devices]
+
+ con_ub = add_constraints_container!(
+ container,
+ LineFlowBoundConstraint(),
+ PSY.AreaInterchange,
+ device_names,
+ time_steps;
+ meta = "ub",
+ )
+
+ con_lb = add_constraints_container!(
+ container,
+ LineFlowBoundConstraint(),
+ PSY.AreaInterchange,
+ device_names,
+ time_steps;
+ meta = "lb",
+ )
+
+ area_ex_var = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)
+ jm = get_jump_model(container)
+ for area_interchange in devices
+ inter_change_name = PSY.get_name(area_interchange)
+ area_from = PSY.get_from_area(area_interchange)
+ area_to = PSY.get_to_area(area_interchange)
+ if haskey(inter_area_branch_map, (area_from, area_to))
+ inter_area_branches = inter_area_branch_map[(area_from, area_to)]
+ mult = 1.0
+ elseif haskey(inter_area_branch_map, (area_to, area_from))
+ inter_area_branches = inter_area_branch_map[(area_to, area_from)]
+ mult = -1.0
+ else
+ @warn(
+ "There are no branches modeled in Area InterChange $(summary(area_interchange)) \
+ LineFlowBoundConstraint not created"
+ )
+ continue
+ end
+
+ for t in time_steps
+ sum_of_flows = JuMP.AffExpr()
+ for (type, branches) in inter_area_branches
+ flow_vars = get_variable(container, FlowActivePowerVariable(), type)
+ for b in branches
+ b_name = PSY.get_name(b)
+ _add_to_jump_expression!(sum_of_flows, flow_vars[b_name, t], mult)
+ end
+ end
+ con_ub[inter_change_name, t] =
+ JuMP.@constraint(jm, sum_of_flows <= area_ex_var[inter_change_name, t])
+ con_lb[inter_change_name, t] =
+ JuMP.@constraint(jm, sum_of_flows >= area_ex_var[inter_change_name, t])
+ end
+ end
+ return
+end
diff --git a/src/devices_models/devices/common/add_constraint_dual.jl b/src/devices_models/devices/common/add_constraint_dual.jl
index cd8fc4e625..f416d886e5 100644
--- a/src/devices_models/devices/common/add_constraint_dual.jl
+++ b/src/devices_models/devices/common/add_constraint_dual.jl
@@ -30,7 +30,7 @@ function add_constraint_dual!(
container::OptimizationContainer,
sys::PSY.System,
model::NetworkModel{T},
-) where {T <: Union{CopperPlatePowerModel, PTDFPowerModel}}
+) where {T <: Union{CopperPlatePowerModel, AbstractPTDFModel}}
if !isempty(get_duals(model))
for constraint_type in get_duals(model)
assign_dual_variable!(container, constraint_type, sys, model)
diff --git a/src/devices_models/devices/common/add_to_expression.jl b/src/devices_models/devices/common/add_to_expression.jl
index 29bd067dd9..eb7702ed08 100644
--- a/src/devices_models/devices/common/add_to_expression.jl
+++ b/src/devices_models/devices/common/add_to_expression.jl
@@ -1,3 +1,15 @@
+_system_expression_type(::Type{PTDFPowerModel}) = PSY.System
+_system_expression_type(::Type{CopperPlatePowerModel}) = PSY.System
+_system_expression_type(::Type{AreaPTDFPowerModel}) = PSY.Area
+
+function _ref_index(network_model::NetworkModel{<:PM.AbstractPowerModel}, bus::PSY.ACBus)
+ return get_reference_bus(network_model, bus)
+end
+
+function _ref_index(::NetworkModel{AreaPTDFPowerModel}, device_bus::PSY.ACBus)
+ return PSY.get_name(PSY.get_area(device_bus))
+end
+
function add_expressions!(
container::OptimizationContainer,
::Type{T},
@@ -26,8 +38,15 @@ function add_expressions!(
W <: AbstractReservesFormulation,
} where {D <: PSY.Component}
time_steps = get_time_steps(container)
- names = [PSY.get_name(d) for d in devices]
- add_expression_container!(container, T(), D, names, time_steps)
+ @assert length(devices) == 1
+ add_expression_container!(
+ container,
+ T(),
+ D,
+ PSY.get_name.(devices),
+ time_steps;
+ meta = PSY.get_name(first(devices)),
+ )
return
end
@@ -102,6 +121,34 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ model::DeviceModel{V, W},
+ network_model::NetworkModel{AreaBalancePowerModel},
+) where {
+ T <: SystemBalanceExpressions,
+ U <: TimeSeriesParameter,
+ V <: PSY.Device,
+ W <: AbstractDeviceFormulation,
+}
+ param_container = get_parameter(container, U(), V)
+ multiplier = get_multiplier_array(param_container)
+ for d in devices, t in get_time_steps(container)
+ bus = PSY.get_bus(d)
+ area_name = PSY.get_name(PSY.get_area(bus))
+ name = PSY.get_name(d)
+ _add_to_jump_expression!(
+ get_expression(container, T(), PSY.Area)[area_name, t],
+ get_parameter_column_refs(param_container, name)[t],
+ multiplier[name, t],
+ )
+ end
+ return
+end
+
function add_to_expression!(
container::OptimizationContainer,
::Type{T},
@@ -163,6 +210,34 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{AreaBalancePowerModel},
+) where {
+ T <: SystemBalanceExpressions,
+ U <: VariableType,
+ V <: PSY.StaticInjection,
+ W <: AbstractDeviceFormulation,
+}
+ variable = get_variable(container, U(), V)
+ expression = get_expression(container, T(), PSY.Area)
+ for d in devices, t in get_time_steps(container)
+ name = PSY.get_name(d)
+ bus = PSY.get_bus(d)
+ area_name = PSY.get_name(PSY.get_area(bus))
+ _add_to_jump_expression!(
+ expression[area_name, t],
+ variable[name, t],
+ get_variable_multiplier(U(), V, W()),
+ )
+ end
+ return
+end
+
"""
Default implementation to add branch variables to SystemBalanceExpressions
"""
@@ -197,6 +272,37 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{X},
+) where {
+ T <: ActivePowerBalance,
+ U <: HVDCLosses,
+ V <: TwoTerminalHVDCTypes,
+ W <: HVDCTwoTerminalDispatch,
+ X <: Union{AreaPTDFPowerModel, AreaBalancePowerModel},
+}
+ variable = get_variable(container, U(), V)
+ expression = get_expression(container, T(), PSY.Area)
+ for d in devices
+ name = PSY.get_name(d)
+ device_bus_from = PSY.get_arc(d).from
+ area_name = PSY.get_name(PSY.get_area(device_bus_from))
+ for t in get_time_steps(container)
+ _add_to_jump_expression!(
+ expression[area_name, t],
+ variable[name, t],
+ get_variable_multiplier(U(), d, W()),
+ )
+ end
+ end
+ return
+end
+
"""
Default implementation to add branch variables to SystemBalanceExpressions
"""
@@ -206,13 +312,12 @@ function add_to_expression!(
::Type{U},
devices::IS.FlattenIteratorWrapper{V},
::DeviceModel{V, W},
- network_model::NetworkModel{X},
+ network_model::NetworkModel{PTDFPowerModel},
) where {
T <: ActivePowerBalance,
U <: FlowActivePowerToFromVariable,
V <: TwoTerminalHVDCTypes,
- W <: AbstractDeviceFormulation,
- X <: PTDFPowerModel,
+ W <: AbstractTwoTerminalDCLineFormulation,
}
var = get_variable(container, U(), V)
nodal_expr = get_expression(container, T(), PSY.ACBus)
@@ -248,11 +353,11 @@ function add_to_expression!(
U <: FlowActivePowerFromToVariable,
V <: TwoTerminalHVDCTypes,
W <: AbstractTwoTerminalDCLineFormulation,
- X <: PTDFPowerModel,
+ X <: AbstractPTDFModel,
}
var = get_variable(container, U(), V)
nodal_expr = get_expression(container, T(), PSY.ACBus)
- sys_expr = get_expression(container, T(), PSY.System)
+ sys_expr = get_expression(container, T(), _system_expression_type(X))
radial_network_reduction = get_radial_network_reduction(network_model)
for d in devices
bus_no_from =
@@ -438,6 +543,34 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ ::DeviceModel{V, W},
+ network_model::NetworkModel{AreaBalancePowerModel},
+) where {
+ T <: SystemBalanceExpressions,
+ U <: OnVariable,
+ V <: PSY.ThermalGen,
+ W <: Union{AbstractCompactUnitCommitment, ThermalCompactDispatch},
+}
+ variable = get_variable(container, U(), V)
+ expression = get_expression(container, T(), PSY.ACBus)
+ for d in devices, t in get_time_steps(container)
+ bus = PSY.get_bus(d)
+ area_name = PSY.get_name(PSY.get_area(bus))
+ name = PSY.get_name(d)
+ _add_to_jump_expression!(
+ expression[area_name, t],
+ variable[name, t],
+ get_variable_multiplier(U(), d, W()),
+ )
+ end
+ return
+end
+
"""
Default implementation to add parameters to SystemBalanceExpressions
"""
@@ -579,11 +712,11 @@ function add_to_expression!(
U <: TimeSeriesParameter,
V <: PSY.StaticInjection,
W <: AbstractDeviceFormulation,
- X <: PTDFPowerModel,
+ X <: AbstractPTDFModel,
}
param_container = get_parameter(container, U(), V)
multiplier = get_multiplier_array(param_container)
- sys_expr = get_expression(container, T(), PSY.System)
+ sys_expr = get_expression(container, T(), _system_expression_type(X))
nodal_expr = get_expression(container, T(), PSY.ACBus)
radial_network_reduction = get_radial_network_reduction(network_model)
for d in devices
@@ -591,10 +724,10 @@ function add_to_expression!(
device_bus = PSY.get_bus(d)
bus_no_ = PSY.get_number(device_bus)
bus_no = PNM.get_mapped_bus_number(radial_network_reduction, bus_no_)
- ref_bus = get_reference_bus(network_model, device_bus)
+ ref_index = _ref_index(network_model, device_bus)
param = get_parameter_column_refs(param_container, name)
for t in get_time_steps(container)
- _add_to_jump_expression!(sys_expr[ref_bus, t], param[t], multiplier[name, t])
+ _add_to_jump_expression!(sys_expr[ref_index, t], param[t], multiplier[name, t])
_add_to_jump_expression!(nodal_expr[bus_no, t], param[t], multiplier[name, t])
end
end
@@ -613,21 +746,23 @@ function add_to_expression!(
U <: OnStatusParameter,
V <: PSY.ThermalGen,
W <: AbstractDeviceFormulation,
- X <: PTDFPowerModel,
+ X <: AbstractPTDFModel,
}
parameter = get_parameter_array(container, U(), V)
sys_expr = get_expression(container, T(), PSY.System)
nodal_expr = get_expression(container, T(), PSY.ACBus)
radial_network_reduction = get_radial_network_reduction(network_model)
- for d in devices, t in get_time_steps(container)
+ for d in devices
name = PSY.get_name(d)
bus_no_ = PSY.get_number(PSY.get_bus(d))
bus_no = PNM.get_mapped_bus_number(radial_network_reduction, bus_no_)
mult = get_expression_multiplier(U(), T(), d, W())
device_bus = PSY.get_bus(d)
- ref_bus = get_reference_bus(network_model, device_bus)
- _add_to_jump_expression!(sys_expr[ref_bus, t], parameter[name, t], mult)
- _add_to_jump_expression!(nodal_expr[bus_no, t], parameter[name, t], mult)
+ ref_index = _ref_index(network_model, device_bus)
+ for t in get_time_steps(container)
+ _add_to_jump_expression!(sys_expr[ref_index, t], parameter[name, t], mult)
+ _add_to_jump_expression!(nodal_expr[bus_no, t], parameter[name, t], mult)
+ end
end
return
end
@@ -641,13 +776,12 @@ function add_to_expression!(
::Type{U},
devices::IS.FlattenIteratorWrapper{V},
device_model::DeviceModel{V, W},
- network_model::NetworkModel{X},
+ network_model::NetworkModel{PTDFPowerModel},
) where {
T <: ActivePowerBalance,
U <: VariableType,
V <: PSY.StaticInjection,
W <: AbstractDeviceFormulation,
- X <: PTDFPowerModel,
}
variable = get_variable(container, U(), V)
sys_expr = get_expression(container, T(), PSY.System)
@@ -657,10 +791,10 @@ function add_to_expression!(
name = PSY.get_name(d)
device_bus = PSY.get_bus(d)
bus_no = PNM.get_mapped_bus_number(radial_network_reduction, device_bus)
- ref_bus = get_reference_bus(network_model, device_bus)
+ ref_index = _ref_index(network_model, device_bus)
for t in get_time_steps(container)
_add_to_jump_expression!(
- sys_expr[ref_bus, t],
+ sys_expr[ref_index, t],
variable[name, t],
get_variable_multiplier(U(), V, W()),
)
@@ -680,25 +814,62 @@ function add_to_expression!(
::Type{U},
devices::IS.FlattenIteratorWrapper{V},
device_model::DeviceModel{V, W},
- network_model::NetworkModel{X},
+ network_model::NetworkModel{AreaPTDFPowerModel},
+) where {
+ T <: ActivePowerBalance,
+ U <: ActivePowerVariable,
+ V <: PSY.StaticInjection,
+ W <: AbstractDeviceFormulation,
+}
+ variable = get_variable(container, U(), V)
+ area_expr = get_expression(container, T(), PSY.Area)
+ nodal_expr = get_expression(container, T(), PSY.ACBus)
+ radial_network_reduction = get_radial_network_reduction(network_model)
+ for d in devices
+ name = PSY.get_name(d)
+ device_bus = PSY.get_bus(d)
+ area_name = PSY.get_name(PSY.get_area(device_bus))
+ bus_no = PNM.get_mapped_bus_number(radial_network_reduction, device_bus)
+ for t in get_time_steps(container)
+ _add_to_jump_expression!(
+ area_expr[area_name, t],
+ variable[name, t],
+ get_variable_multiplier(U(), V, W()),
+ )
+ _add_to_jump_expression!(
+ nodal_expr[bus_no, t],
+ variable[name, t],
+ get_variable_multiplier(U(), V, W()),
+ )
+ end
+ end
+ return
+end
+
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ devices::IS.FlattenIteratorWrapper{V},
+ device_model::DeviceModel{V, W},
+ network_model::NetworkModel{PTDFPowerModel},
) where {
T <: ActivePowerBalance,
U <: OnVariable,
V <: PSY.ThermalGen,
W <: Union{AbstractCompactUnitCommitment, ThermalCompactDispatch},
- X <: PTDFPowerModel,
}
variable = get_variable(container, U(), V)
- sys_expr = get_expression(container, T(), PSY.System)
+ sys_expr = get_expression(container, T(), _system_expression_type(PTDFPowerModel))
nodal_expr = get_expression(container, T(), PSY.ACBus)
radial_network_reduction = get_radial_network_reduction(network_model)
for d in devices
name = PSY.get_name(d)
bus_no = PNM.get_mapped_bus_number(radial_network_reduction, PSY.get_bus(d))
- ref_bus = get_reference_bus(network_model, PSY.get_bus(d))
+ ref_index = _ref_index(network_model, PSY.get_bus(d))
for t in get_time_steps(container)
_add_to_jump_expression!(
- sys_expr[ref_bus, t],
+ sys_expr[ref_index, t],
variable[name, t],
get_variable_multiplier(U(), d, W()),
)
@@ -753,6 +924,39 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{FlowActivePowerVariable},
+ devices::IS.FlattenIteratorWrapper{PSY.AreaInterchange},
+ ::DeviceModel{PSY.AreaInterchange, W},
+ network_model::NetworkModel{U},
+) where {
+ T <: ActivePowerBalance,
+ U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel},
+ W <: AbstractBranchFormulation,
+}
+ flow_variable = get_variable(container, FlowActivePowerVariable(), PSY.AreaInterchange)
+ expression = get_expression(container, T(), PSY.Area)
+ for d in devices
+ area_from_name = PSY.get_name(PSY.get_from_area(d))
+ area_to_name = PSY.get_name(PSY.get_to_area(d))
+ for t in get_time_steps(container)
+ _add_to_jump_expression!(
+ expression[area_from_name, t],
+ flow_variable[PSY.get_name(d), t],
+ -1.0,
+ )
+ _add_to_jump_expression!(
+ expression[area_to_name, t],
+ flow_variable[PSY.get_name(d), t],
+ 1.0,
+ )
+ end
+ end
+ return
+end
+
"""
Implementation of add_to_expression! for lossless branch/network models
"""
@@ -844,7 +1048,7 @@ function add_to_expression!(
::Type{U},
devices::IS.FlattenIteratorWrapper{PSY.PhaseShiftingTransformer},
::DeviceModel{PSY.PhaseShiftingTransformer, V},
- network_model::NetworkModel{PTDFPowerModel},
+ network_model::NetworkModel{<:AbstractPTDFModel},
) where {T <: ActivePowerBalance, U <: PhaseShifterAngle, V <: PhaseAngleControl}
var = get_variable(container, U(), PSY.PhaseShiftingTransformer)
expression = get_expression(container, T(), PSY.ACBus)
@@ -903,7 +1107,7 @@ function add_to_expression!(
devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},
model::ServiceModel{X, W},
) where {
- T <: Union{ActivePowerRangeExpressionUB, ReserveRangeExpressionUB},
+ T <: ActivePowerRangeExpressionUB,
U <: VariableType,
V <: PSY.Component,
X <: PSY.Reserve{PSY.ReserveUp},
@@ -927,8 +1131,11 @@ function add_to_expression!(
::Type{InterfaceTotalFlow},
::Type{T},
service::PSY.TransmissionInterface,
- model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},
-) where {T <: Union{InterfaceFlowSlackUp, InterfaceFlowSlackDown}}
+ model::ServiceModel{PSY.TransmissionInterface, U},
+) where {
+ T <: Union{InterfaceFlowSlackUp, InterfaceFlowSlackDown},
+ U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow},
+}
expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)
service_name = PSY.get_name(service)
variable = get_variable(container, T(), PSY.TransmissionInterface, service_name)
@@ -936,7 +1143,7 @@ function add_to_expression!(
_add_to_jump_expression!(
expression[service_name, t],
variable[t],
- get_variable_multiplier(T(), service, ConstantMaxInterfaceFlow()),
+ get_variable_multiplier(T(), service, U()),
)
end
return
@@ -947,8 +1154,8 @@ function add_to_expression!(
::Type{InterfaceTotalFlow},
::Type{FlowActivePowerVariable},
service::PSY.TransmissionInterface,
- model::ServiceModel{PSY.TransmissionInterface, ConstantMaxInterfaceFlow},
-)
+ model::ServiceModel{PSY.TransmissionInterface, V},
+) where {V <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}
expression = get_expression(container, InterfaceTotalFlow(), PSY.TransmissionInterface)
service_name = get_service_name(model)
for (device_type, devices) in get_contributing_devices_map(model)
@@ -975,7 +1182,7 @@ function add_to_expression!(
devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}},
model::ServiceModel{X, W},
) where {
- T <: Union{ActivePowerRangeExpressionLB, ReserveRangeExpressionLB},
+ T <: ActivePowerRangeExpressionLB,
U <: VariableType,
V <: PSY.Component,
X <: PSY.Reserve{PSY.ReserveDown},
@@ -1011,7 +1218,8 @@ function add_to_expression!(
add_expressions!(container, T, devices, model)
end
expression = get_expression(container, T(), V)
- for d in devices, mult in get_expression_multiplier(U(), T(), d, W())
+ for d in devices
+ mult = get_expression_multiplier(U(), T(), d, W())
for t in get_time_steps(container)
name = PSY.get_name(d)
_add_to_jump_expression!(
@@ -1042,7 +1250,8 @@ function add_to_expression!(
add_expressions!(container, T, devices, model)
end
expression = get_expression(container, T(), V)
- for d in devices, mult in get_expression_multiplier(U(), T(), d, W())
+ for d in devices
+ mult = get_expression_multiplier(U(), T(), d, W())
for t in get_time_steps(container)
name = PSY.get_name(d)
_add_to_jump_expression!(expression[name, t], parameter_array[name, t], -mult)
@@ -1079,7 +1288,7 @@ function add_to_expression!(
W <: Union{CopperPlatePowerModel, PTDFPowerModel},
}
variable = get_variable(container, U(), PSY.System)
- expression = get_expression(container, T(), PSY.System)
+ expression = get_expression(container, T(), _system_expression_type(W))
reference_buses = get_reference_buses(network_model)
for t in get_time_steps(container), n in reference_buses
_add_to_jump_expression!(
@@ -1091,6 +1300,31 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{T},
+ ::Type{U},
+ sys::PSY.System,
+ network_model::NetworkModel{AreaPTDFPowerModel},
+) where {
+ T <: ActivePowerBalance,
+ U <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},
+}
+ variable =
+ get_variable(container, U(), _system_expression_type(AreaPTDFPowerModel))
+ expression =
+ get_expression(container, T(), _system_expression_type(AreaPTDFPowerModel))
+ areas = get_available_components(network_model, PSY.Area, sys)
+ for t in get_time_steps(container), n in PSY.get_name.(areas)
+ _add_to_jump_expression!(
+ expression[n, t],
+ variable[n, t],
+ get_variable_multiplier(U(), PSY.Area, AreaPTDFPowerModel()),
+ )
+ end
+ return
+end
+
function add_to_expression!(
container::OptimizationContainer,
::Type{T},
@@ -1182,6 +1416,25 @@ function add_to_expression!(
return
end
+function add_to_expression!(
+ container::OptimizationContainer,
+ ::Type{S},
+ cost_expression::JuMP.AbstractJuMPScalar,
+ component::T,
+ time_period::Int,
+) where {S <: CostExpressions, T <: PSY.ReserveDemandCurve}
+ if has_container_key(container, S, T, PSY.get_name(component))
+ device_cost_expression = get_expression(container, S(), T, PSY.get_name(component))
+ component_name = PSY.get_name(component)
+ JuMP.add_to_expression!(
+ device_cost_expression[component_name, time_period],
+ cost_expression,
+ )
+ end
+ return
+end
+
+#=
function add_to_expression!(
container::OptimizationContainer,
::Type{T},
@@ -1237,3 +1490,4 @@ function add_to_expression!(
end
return
end
+=#
diff --git a/src/devices_models/devices/common/get_time_series.jl b/src/devices_models/devices/common/get_time_series.jl
index e94121e6e0..6aa8a8476a 100644
--- a/src/devices_models/devices/common/get_time_series.jl
+++ b/src/devices_models/devices/common/get_time_series.jl
@@ -15,7 +15,7 @@ function get_time_series(
container::OptimizationContainer,
component::T,
parameter::TimeSeriesParameter,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T <: PSY.Component}
parameter_container = get_parameter(container, parameter, T, meta)
return _get_time_series(container, component, parameter_container.attributes)
diff --git a/src/devices_models/devices/common/objective_function/common.jl b/src/devices_models/devices/common/objective_function/common.jl
new file mode 100644
index 0000000000..082cb79932
--- /dev/null
+++ b/src/devices_models/devices/common/objective_function/common.jl
@@ -0,0 +1,236 @@
+##################################
+#### ActivePowerVariable Cost ####
+##################################
+
+function add_variable_cost!(
+ container::OptimizationContainer,
+ ::U,
+ devices::IS.FlattenIteratorWrapper{T},
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ for d in devices
+ op_cost_data = PSY.get_operation_cost(d)
+ _add_variable_cost_to_objective!(container, U(), d, op_cost_data, V())
+ end
+ return
+end
+
+##################################
+#### Start/Stop Variable Cost ####
+##################################
+
+function add_shut_down_cost!(
+ container::OptimizationContainer,
+ ::U,
+ devices::IS.FlattenIteratorWrapper{T},
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ multiplier = objective_function_multiplier(U(), V())
+ for d in devices
+ op_cost_data = PSY.get_operation_cost(d)
+ cost_term = shut_down_cost(op_cost_data, d, V())
+ iszero(cost_term) && continue
+ for t in get_time_steps(container)
+ _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
+ end
+ end
+ return
+end
+
+##################################
+####### Proportional Cost ########
+##################################
+
+function add_proportional_cost!(
+ container::OptimizationContainer,
+ ::U,
+ devices::IS.FlattenIteratorWrapper{T},
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ multiplier = objective_function_multiplier(U(), V())
+ for d in devices
+ op_cost_data = PSY.get_operation_cost(d)
+ cost_term = proportional_cost(op_cost_data, U(), d, V())
+ iszero(cost_term) && continue
+ for t in get_time_steps(container)
+ _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
+ end
+ end
+ return
+end
+
+##################################
+######## OnVariable Cost #########
+##################################
+
+function add_proportional_cost!(
+ container::OptimizationContainer,
+ ::U,
+ devices::IS.FlattenIteratorWrapper{T},
+ ::V,
+) where {T <: PSY.ThermalGen, U <: OnVariable, V <: AbstractCompactUnitCommitment}
+ multiplier = objective_function_multiplier(U(), V())
+ for d in devices
+ op_cost_data = PSY.get_operation_cost(d)
+ cost_term = proportional_cost(op_cost_data, U(), d, V())
+ iszero(cost_term) && continue
+ for t in get_time_steps(container)
+ exp = _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
+ add_to_expression!(container, ProductionCostExpression, exp, d, t)
+ end
+ end
+ return
+end
+
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ op_cost::PSY.OperationalCost,
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ variable_cost_data = variable_cost(op_cost, T(), component, U())
+ _add_variable_cost_to_objective!(container, T(), component, variable_cost_data, U())
+ return
+end
+
+function add_start_up_cost!(
+ container::OptimizationContainer,
+ ::U,
+ devices::IS.FlattenIteratorWrapper{T},
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ for d in devices
+ op_cost_data = PSY.get_operation_cost(d)
+ _add_start_up_cost_to_objective!(container, U(), d, op_cost_data, V())
+ end
+ return
+end
+
+function _add_start_up_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.ThermalGen,
+ op_cost::Union{PSY.ThermalGenerationCost, PSY.MarketBidCost},
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ cost_term = start_up_cost(op_cost, component, U())
+ iszero(cost_term) && return
+ multiplier = objective_function_multiplier(T(), U())
+ for t in get_time_steps(container)
+ _add_proportional_term!(container, T(), component, cost_term * multiplier, t)
+ end
+ return
+end
+
+const MULTI_START_COST_MAP = Dict{DataType, Int}(
+ HotStartVariable => 1,
+ WarmStartVariable => 2,
+ ColdStartVariable => 3,
+)
+
+function _add_start_up_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.ThermalMultiStart,
+ op_cost::PSY.ThermalGenerationCost,
+ ::U,
+) where {T <: VariableType, U <: ThermalMultiStartUnitCommitment}
+ cost_terms = start_up_cost(op_cost, component, U())
+ cost_term = cost_terms[MULTI_START_COST_MAP[T]]
+ iszero(cost_term) && return
+ multiplier = objective_function_multiplier(T(), U())
+ for t in get_time_steps(container)
+ _add_proportional_term!(container, T(), component, cost_term * multiplier, t)
+ end
+ return
+end
+
+function _get_cost_function_parameter_container(
+ container::OptimizationContainer,
+ ::S,
+ component::T,
+ ::U,
+ ::V,
+ cost_type::Type{W},
+) where {
+ S <: ObjectiveFunctionParameter,
+ T <: PSY.Component,
+ U <: VariableType,
+ V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},
+ W,
+}
+ if has_container_key(container, S, T)
+ return get_parameter(container, S(), T)
+ else
+ container_axes = axes(get_variable(container, U(), T))
+ if has_container_key(container, OnStatusParameter, T)
+ sos_val = SOSStatusVariable.PARAMETER
+ else
+ sos_val = sos_status(component, V())
+ end
+ return add_param_container!(
+ container,
+ S(),
+ T,
+ U,
+ sos_val,
+ uses_compact_power(component, V()),
+ W,
+ container_axes...,
+ )
+ end
+end
+
+function _add_proportional_term!(
+ container::OptimizationContainer,
+ ::T,
+ component::U,
+ linear_term::Float64,
+ time_period::Int,
+) where {T <: VariableType, U <: PSY.Component}
+ component_name = PSY.get_name(component)
+ @debug "Linear Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
+ variable = get_variable(container, T(), U)[component_name, time_period]
+ lin_cost = variable * linear_term
+ add_to_objective_invariant_expression!(container, lin_cost)
+ return lin_cost
+end
+
+function _add_quadratic_term!(
+ container::OptimizationContainer,
+ ::T,
+ component::U,
+ q_terms::NTuple{2, Float64},
+ expression_multiplier::Float64,
+ time_period::Int,
+) where {T <: VariableType, U <: PSY.Component}
+ component_name = PSY.get_name(component)
+ @debug "$component_name Quadratic Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
+ var = get_variable(container, T(), U)[component_name, time_period]
+ q_cost_ = var .^ 2 * q_terms[1] + var * q_terms[2]
+ q_cost = q_cost_ * expression_multiplier
+ add_to_objective_invariant_expression!(container, q_cost)
+ return q_cost
+end
+
+##################################################
+################## Fuel Cost #####################
+##################################################
+
+function _get_fuel_cost_value(
+ ::OptimizationContainer,
+ fuel_cost::Float64,
+ ::Int,
+)
+ return fuel_cost
+end
+
+function _get_fuel_cost_value(
+ container::OptimizationContainer,
+ fuel_cost::IS.TimeSeriesKey,
+ time_period::Int,
+)
+ error("Not implemented yet fuel cost")
+ return fuel_cost
+end
diff --git a/src/devices_models/devices/common/objective_function/linear_curve.jl b/src/devices_models/devices/common/objective_function/linear_curve.jl
new file mode 100644
index 0000000000..06d47c9b29
--- /dev/null
+++ b/src/devices_models/devices/common/objective_function/linear_curve.jl
@@ -0,0 +1,165 @@
+# Add proportional terms to objective function and expression
+function _add_linearcurve_variable_term_to_model!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_term_per_unit::Float64,
+ time_period::Int,
+) where {T <: VariableType}
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ linear_cost = _add_proportional_term!(
+ container,
+ T(),
+ component,
+ proportional_term_per_unit * dt,
+ time_period,
+ )
+ add_to_expression!(
+ container,
+ ProductionCostExpression,
+ linear_cost,
+ component,
+ time_period,
+ )
+ return
+end
+
+# Dispatch for vector of proportional terms
+function _add_linearcurve_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_terms_per_unit::Vector{Float64},
+) where {T <: VariableType}
+ for t in get_time_steps(container)
+ _add_linearcurve_variable_term_to_model!(
+ container,
+ T(),
+ component,
+ proportional_terms_per_unit[t],
+ t,
+ )
+ end
+ return
+end
+
+# Dispatch for scalar proportional terms
+function _add_linearcurve_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_term_per_unit::Float64,
+) where {T <: VariableType}
+ for t in get_time_steps(container)
+ _add_linearcurve_variable_term_to_model!(
+ container,
+ T(),
+ component,
+ proportional_term_per_unit,
+ t,
+ )
+ end
+ return
+end
+
+"""
+Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_component::PSY.CostCurve{PSY.LinearCurve} : container for cost to be associated with variable
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.CostCurve{PSY.LinearCurve},
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ proportional_term_per_unit = get_proportional_cost_per_system_unit(
+ proportional_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ multiplier = objective_function_multiplier(T(), U())
+ _add_linearcurve_variable_cost!(
+ container,
+ T(),
+ component,
+ multiplier * proportional_term_per_unit,
+ )
+ return
+end
+
+function _add_fuel_linear_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ fuel_curve::Float64,
+ fuel_cost::Float64,
+) where {T <: VariableType}
+ _add_linearcurve_variable_cost!(container, T(), component, fuel_curve * fuel_cost)
+end
+
+function _add_fuel_linear_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ fuel_curve::Float64,
+ fuel_cost::IS.TimeSeriesKey,
+) where {T <: VariableType}
+ error("Not implemented yet")
+ _add_linearcurve_variable_cost!(container, T(), component, fuel_curve)
+end
+
+"""
+Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_component::PSY.FuelCurve{PSY.LinearCurve} : container for cost to be associated with variable
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.FuelCurve{PSY.LinearCurve},
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ fuel_curve_per_unit = get_proportional_cost_per_system_unit(
+ proportional_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ fuel_cost = PSY.get_fuel_cost(cost_function)
+ # Multiplier is not necessary here. There is no negative cost for fuel curves.
+ _add_fuel_linear_variable_cost!(
+ container,
+ T(),
+ component,
+ fuel_curve_per_unit,
+ fuel_cost,
+ )
+ return
+end
diff --git a/src/devices_models/devices/common/objective_function/market_bid.jl b/src/devices_models/devices/common/objective_function/market_bid.jl
new file mode 100644
index 0000000000..d46025e62f
--- /dev/null
+++ b/src/devices_models/devices/common/objective_function/market_bid.jl
@@ -0,0 +1,600 @@
+##################################################
+################# PWL Variables ##################
+##################################################
+
+# For Market Bid
+function _add_pwl_variables!(
+ container::OptimizationContainer,
+ ::Type{T},
+ component_name::String,
+ time_period::Int,
+ cost_data::PSY.PiecewiseStepData,
+) where {T <: PSY.Component}
+ var_container = lazy_container_addition!(container, PieceWiseLinearBlockOffer(), T)
+ # length(PiecewiseStepData) gets number of segments, here we want number of points
+ break_points = PSY.get_x_coords(cost_data)
+ pwlvars = Array{JuMP.VariableRef}(undef, length(break_points))
+ for i in 1:(length(break_points) - 1)
+ pwlvars[i] =
+ var_container[(component_name, i, time_period)] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "PieceWiseLinearBlockOffer_$(component_name)_{pwl_$(i), $time_period}",
+ lower_bound = 0.0,
+ )
+ end
+ return pwlvars
+end
+
+##################################################
+################# PWL Constraints ################
+##################################################
+
+"""
+Implement the constraints for PWL Block Offer variables. That is:
+
+```math
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} = p_t \\\\
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} <= P_{k+1,t}^{max} - P_{k,t}^{max}
+```
+"""
+function _add_pwl_constraint!(
+ container::OptimizationContainer,
+ component::T,
+ ::U,
+ break_points::Vector{Float64},
+ period::Int,
+) where {T <: PSY.Component, U <: VariableType}
+ variables = get_variable(container, U(), T)
+ const_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearBlockOfferConstraint(),
+ T,
+ axes(variables)...,
+ )
+ len_cost_data = length(break_points) - 1
+ jump_model = get_jump_model(container)
+ pwl_vars = get_variable(container, PieceWiseLinearBlockOffer(), T)
+ name = PSY.get_name(component)
+ const_container[name, period] = JuMP.@constraint(
+ jump_model,
+ variables[name, period] ==
+ sum(pwl_vars[name, ix, period] for ix in 1:len_cost_data)
+ )
+
+ #=
+ const_upperbound_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearUpperBoundConstraint(),
+ T,
+ axes(pwl_vars)...;
+ )
+ =#
+
+ # TODO: Parameter for this
+ for ix in 1:len_cost_data
+ JuMP.@constraint(
+ jump_model,
+ pwl_vars[name, ix, period] <= break_points[ix + 1] - break_points[ix]
+ )
+ end
+ return
+end
+
+"""
+Implement the constraints for PWL Block Offer variables for ORDC. That is:
+
+```math
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} = p_t \\\\
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} <= P_{k+1,t}^{max} - P_{k,t}^{max}
+```
+"""
+function _add_pwl_constraint!(
+ container::OptimizationContainer,
+ component::T,
+ ::U,
+ break_points::Vector{Float64},
+ sos_status::SOSStatusVariable,
+ period::Int,
+) where {T <: PSY.ReserveDemandCurve, U <: ServiceRequirementVariable}
+ name = PSY.get_name(component)
+ variables = get_variable(container, U(), T, name)
+ const_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearBlockOfferConstraint(),
+ T,
+ axes(variables)...;
+ meta = name,
+ )
+ len_cost_data = length(break_points) - 1
+ jump_model = get_jump_model(container)
+ pwl_vars = get_variable(container, PieceWiseLinearBlockOffer(), T)
+ const_container[name, period] = JuMP.@constraint(
+ jump_model,
+ variables[name, period] ==
+ sum(pwl_vars[name, ix, period] for ix in 1:len_cost_data)
+ )
+
+ for ix in 1:len_cost_data
+ JuMP.@constraint(
+ jump_model,
+ pwl_vars[name, ix, period] <= break_points[ix + 1] - break_points[ix]
+ )
+ end
+ return
+end
+
+##################################################
+################ PWL Expressions #################
+##################################################
+
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_data::PSY.PiecewiseStepData,
+ multiplier::Float64,
+) where {T <: PSY.Component}
+ name = PSY.get_name(component)
+ pwl_var_container = get_variable(container, PieceWiseLinearBlockOffer(), T)
+ gen_cost = JuMP.AffExpr(0.0)
+ cost_data = PSY.get_y_coords(cost_data)
+ for (i, cost) in enumerate(cost_data)
+ JuMP.add_to_expression!(
+ gen_cost,
+ cost * multiplier * pwl_var_container[(name, i, time_period)],
+ )
+ end
+ return gen_cost
+end
+
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_function::PSY.MarketBidCost,
+ ::PSY.PiecewiseStepData,
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ incremental_curve = PSY.get_incremental_offer_curves(cost_function)
+ value_curve = PSY.get_value_curve(incremental_curve)
+ power_units = PSY.get_power_units(incremental_curve)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ cost_data_normalized = get_piecewise_incrementalcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ return _get_pwl_cost_expression(
+ container,
+ component,
+ time_period,
+ cost_data_normalized,
+ dt,
+ )
+end
+
+"""
+Get cost expression for StepwiseCostReserve
+"""
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_data::PSY.PiecewiseStepData,
+ multiplier::Float64,
+) where {T <: PSY.ReserveDemandCurve}
+ name = PSY.get_name(component)
+ pwl_var_container = get_variable(container, PieceWiseLinearBlockOffer(), T)
+ slopes = PSY.get_y_coords(cost_data)
+ ordc_cost = JuMP.AffExpr(0.0)
+ for i in 1:length(slopes)
+ JuMP.add_to_expression!(
+ ordc_cost,
+ slopes[i] * multiplier * pwl_var_container[(name, i, time_period)],
+ )
+ end
+ return ordc_cost
+end
+
+#=
+# For Market Bid
+function _add_pwl_variables!(
+ container::OptimizationContainer,
+ ::Type{T},
+ component_name::String,
+ time_period::Int,
+ cost_data::PSY.PiecewiseStepData,
+) where {T <: PSY.Component}
+ var_container = lazy_container_addition!(container, PieceWiseLinearCostVariable(), T)
+ # length(PiecewiseStepData) gets number of segments, here we want number of points
+ pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data) + 1)
+ for i in 1:(length(cost_data) + 1)
+ pwlvars[i] =
+ var_container[(component_name, i, time_period)] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "PieceWiseLinearCostVariable_$(component_name)_{pwl_$(i), $time_period}",
+ )
+ end
+ return pwlvars
+end
+
+# For Market Bid #
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_data::PSY.PiecewiseStepData,
+ multiplier::Float64,
+) where {T <: PSY.Component}
+ # TODO: This functions needs to be reimplemented for the new model. The code is repeated
+ # because the internals will be different
+ name = PSY.get_name(component)
+ pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T)
+ gen_cost = JuMP.AffExpr(0.0)
+ cost_data = PSY.get_y_coords(cost_data)
+ for (i, cost) in enumerate(cost_data)
+ JuMP.add_to_expression!(
+ gen_cost,
+ cost * multiplier * pwl_var_container[(name, i, time_period)],
+ )
+ end
+ return gen_cost
+end
+
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_data::AbstractVector{PSY.LinearFunctionData},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ multiplier = objective_function_multiplier(U(), V())
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ base_power = get_base_power(container)
+ # Re-scale breakpoints by Basepower
+ time_steps = get_time_steps(container)
+ cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ for t in time_steps
+ proportional_value =
+ PSY.get_proportional_term(cost_data[t]) * multiplier * base_power * dt
+ cost_expressions[t] =
+ _add_proportional_term!(container, U(), component, proportional_value, t)
+ end
+ return cost_expressions
+end
+=#
+
+###############################################
+######## MarketBidCost: Fixed Curves ##########
+###############################################
+
+"""
+Add PWL cost terms for data coming from the MarketBidCost
+with a fixed incremental offer curve
+"""
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_function::PSY.MarketBidCost,
+ ::PSY.CostCurve{PSY.PiecewiseIncrementalCurve},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ name = PSY.get_name(component)
+ incremental_offer_curve = PSY.get_incremental_offer_curves(cost_function)
+ value_curve = PSY.get_value_curve(incremental_offer_curve)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ power_units = PSY.get_power_units(incremental_offer_curve)
+
+ data = get_piecewise_incrementalcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+
+ compact_status = validate_compact_pwl_data(component, data, base_power)
+ if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID
+ error(
+ "The data provided is not compatible with formulation $V. Use a formulation compatible with Compact Cost Functions",
+ )
+ # data = _convert_to_full_variable_cost(data, component)
+ elseif uses_compact_power(component, V()) && compact_status != COMPACT_PWL_STATUS.VALID
+ @warn(
+ "The cost data provided is not in compact form. Will attempt to convert. Errors may occur."
+ )
+ data = convert_to_compact_variable_cost(data)
+ else
+ @debug uses_compact_power(component, V()) compact_status name T V
+ end
+
+ cost_is_convex = PSY.is_convex(data)
+ if !cost_is_convex
+ error("MarketBidCost for component $(name) is non-convex")
+ end
+
+ break_points = PSY.get_x_coords(data)
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ for t in time_steps
+ _add_pwl_variables!(container, T, name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, t)
+ pwl_cost =
+ _get_pwl_cost_expression(container, component, t, cost_function, data, U(), V())
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+
+##################################################
+########## PWL for StepwiseCostReserve ##########
+##################################################
+
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractServiceFormulation}
+ multiplier = objective_function_multiplier(U(), V())
+ resolution = get_resolution(container)
+ dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
+ base_power = get_base_power(container)
+ value_curve = PSY.get_value_curve(cost_data)
+ power_units = PSY.get_power_units(cost_data)
+ cost_component = PSY.get_function_data(value_curve)
+ device_base_power = PSY.get_base_power(component)
+ data = get_piecewise_incrementalcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ name = PSY.get_name(component)
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ sos_val = _get_sos_value(container, V, component)
+ for t in time_steps
+ break_points = PSY.get_x_coords(data)
+ _add_pwl_variables!(container, T, name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
+ pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+
+#=
+"""
+Add PWL cost terms for data coming from the MarketBidCost
+with a timeseries incremental offer curve
+"""
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_function::PSY.MarketBidCost,
+ ::PSY.TimeSeriesKey,
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ name = PSY.get_name(component)
+ value_curve = PSY.get_value_curve(incremental_offer_curve)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ power_units = PSY.get_power_units(cost_function)
+
+ data = get_piecewise_incrementalcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ sos_val = _get_sos_value(container, V, component)
+ for t in time_steps
+ # Run checks in every time step because each time step has a PWL cost function
+ data = cost_data[t]
+ compact_status = validate_compact_pwl_data(component, data, base_power)
+ if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID
+ error(
+ "The data provided is not compatible with formulation $V. Use a formulation compatible with Compact Cost Functions",
+ )
+ # data = _convert_to_full_variable_cost(data, component)
+ elseif uses_compact_power(component, V()) &&
+ compact_status != COMPACT_PWL_STATUS.VALID
+ @warn(
+ "The cost data provided is not in compact form. Will attempt to convert. Errors may occur."
+ )
+ data = convert_to_compact_variable_cost(data)
+ else
+ @debug uses_compact_power(component, V()) compact_status name T V
+ end
+ cost_is_convex = PSY.is_convex(data)
+ break_points = PSY.get_x_coords(data) ./ base_power # TODO should this be get_x_lengths/get_breakpoint_upper_bounds?
+ _add_pwl_variables!(container, T, name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
+ if !cost_is_convex
+ _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
+ end
+ pwl_cost =
+ _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_data::AbstractVector{PSY.PiecewiseStepData},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractServiceFormulation}
+ multiplier = objective_function_multiplier(U(), V())
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ base_power = get_base_power(container)
+ # Re-scale breakpoints by Basepower
+ name = PSY.get_name(component)
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ sos_val = _get_sos_value(container, V, component)
+ for t in time_steps
+ data = cost_data[t]
+ break_points = PSY.get_x_coords(data) ./ base_power
+ _add_pwl_variables!(container, T, name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
+ _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
+ pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+=#
+############################################################
+######## MarketBidCost: PiecewiseIncrementalCurve ##########
+############################################################
+
+"""
+Creates piecewise linear market bid function using a sum of variables and expression for market participants.
+Decremental offers are not accepted for most components, except Storage systems and loads.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_function::MarketBidCost : container for market bid cost
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.MarketBidCost,
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ component_name = PSY.get_name(component)
+ @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name
+ time_steps = get_time_steps(container)
+ initial_time = get_initial_time(container)
+ incremental_cost_curves = PSY.get_incremental_offer_curves(cost_function)
+ decremental_cost_curves = PSY.get_decremental_offer_curves(cost_function)
+ if isnothing(decremental_cost_curves)
+ error("Component $(component_name) is not allowed to participate as a demand.")
+ end
+ #=
+ variable_cost_forecast = PSY.get_variable_cost(
+ component,
+ op_cost;
+ start_time = initial_time,
+ len = length(time_steps),
+ )
+ variable_cost_forecast_values = TimeSeries.values(variable_cost_forecast)
+ parameter_container = _get_cost_function_parameter_container(
+ container,
+ CostFunctionParameter(),
+ component,
+ T(),
+ U(),
+ eltype(variable_cost_forecast_values),
+ )
+ =#
+ pwl_cost_expressions =
+ _add_pwl_term!(
+ container,
+ component,
+ cost_function,
+ incremental_cost_curves,
+ T(),
+ U(),
+ )
+ jump_model = get_jump_model(container)
+ for t in time_steps
+ #=
+ set_multiplier!(
+ parameter_container,
+ # Using 1.0 here since we want to reuse the existing code that adds the mulitpler
+ # of base power times the time delta.
+ 1.0,
+ component_name,
+ t,
+ )
+ set_parameter!(
+ parameter_container,
+ jump_model,
+ variable_cost_forecast_values[t],
+ component_name,
+ t,
+ )
+ =#
+ add_to_expression!(
+ container,
+ ProductionCostExpression,
+ pwl_cost_expressions[t],
+ component,
+ t,
+ )
+ add_to_objective_variant_expression!(container, pwl_cost_expressions[t])
+ end
+
+ # Service Cost Bid
+ #=
+ ancillary_services = PSY.get_ancillary_service_offers(op_cost)
+ for service in ancillary_services
+ _add_service_bid_cost!(container, component, service)
+ end
+ =#
+ return
+end
+
+function _add_service_bid_cost!(
+ container::OptimizationContainer,
+ component::PSY.Component,
+ service::T,
+) where {T <: PSY.Reserve{<:PSY.ReserveDirection}}
+ time_steps = get_time_steps(container)
+ initial_time = get_initial_time(container)
+ base_power = get_base_power(container)
+ forecast_data = PSY.get_services_bid(
+ component,
+ PSY.get_operation_cost(component),
+ service;
+ start_time = initial_time,
+ len = length(time_steps),
+ )
+ forecast_data_values = PSY.get_cost.(TimeSeries.values(forecast_data))
+ # Single Price Bid
+ if eltype(forecast_data_values) == Float64
+ data_values = forecast_data_values
+ # Single Price/Quantity Bid
+ elseif eltype(forecast_data_values) == Vector{NTuple{2, Float64}}
+ data_values = [v[1][1] for v in forecast_data_values]
+ else
+ error("$(eltype(forecast_data_values)) not supported for MarketBidCost")
+ end
+
+ reserve_variable =
+ get_variable(container, ActivePowerReserveVariable(), T, PSY.get_name(service))
+ component_name = PSY.get_name(component)
+ for t in time_steps
+ add_to_objective_invariant_expression!(
+ container,
+ data_values[t] * base_power * reserve_variable[component_name, t],
+ )
+ end
+ return
+end
+
+function _add_service_bid_cost!(::OptimizationContainer, ::PSY.Component, ::PSY.Service) end
diff --git a/src/devices_models/devices/common/objective_function/piecewise_linear.jl b/src/devices_models/devices/common/objective_function/piecewise_linear.jl
new file mode 100644
index 0000000000..37ae04b62d
--- /dev/null
+++ b/src/devices_models/devices/common/objective_function/piecewise_linear.jl
@@ -0,0 +1,574 @@
+##################################################
+################# SOS Methods ####################
+##################################################
+
+function _get_sos_value(
+ container::OptimizationContainer,
+ ::Type{V},
+ component::T,
+) where {T <: PSY.Component, V <: AbstractDeviceFormulation}
+ if has_container_key(container, OnStatusParameter, T)
+ sos_val = SOSStatusVariable.PARAMETER
+ else
+ sos_val = sos_status(component, V())
+ end
+ return sos_val
+end
+
+function _get_sos_value(
+ container::OptimizationContainer,
+ ::Type{V},
+ component::T,
+) where {T <: PSY.Component, V <: AbstractServiceFormulation}
+ return SOSStatusVariable.NO_VARIABLE
+end
+
+##################################################
+################# PWL Variables ##################
+##################################################
+
+# This cases bounds the data by 1 - 0
+function _add_pwl_variables!(
+ container::OptimizationContainer,
+ ::Type{T},
+ component_name::String,
+ time_period::Int,
+ cost_data::PSY.PiecewiseLinearData,
+) where {T <: PSY.Component}
+ var_container = lazy_container_addition!(container, PieceWiseLinearCostVariable(), T)
+ # length(PiecewiseStepData) gets number of segments, here we want number of points
+ pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data) + 1)
+ for i in 1:(length(cost_data) + 1)
+ pwlvars[i] =
+ var_container[(component_name, i, time_period)] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "PieceWiseLinearCostVariable_$(component_name)_{pwl_$(i), $time_period}",
+ lower_bound = 0.0,
+ upper_bound = 1.0
+ )
+ end
+ return pwlvars
+end
+
+##################################################
+################# PWL Constraints ################
+##################################################
+
+"""
+Implement the constraints for PWL variables. That is:
+
+```math
+\\sum_{k\\in\\mathcal{K}} P_k^{max} \\delta_{k,t} = p_t \\\\
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} = on_t
+```
+"""
+function _add_pwl_constraint!(
+ container::OptimizationContainer,
+ component::T,
+ ::U,
+ break_points::Vector{Float64},
+ sos_status::SOSStatusVariable,
+ period::Int,
+) where {T <: PSY.Component, U <: VariableType}
+ variables = get_variable(container, U(), T)
+ const_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearCostConstraint(),
+ T,
+ axes(variables)...,
+ )
+ len_cost_data = length(break_points)
+ jump_model = get_jump_model(container)
+ pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T)
+ name = PSY.get_name(component)
+ const_container[name, period] = JuMP.@constraint(
+ jump_model,
+ variables[name, period] ==
+ sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data)
+ )
+
+ if sos_status == SOSStatusVariable.NO_VARIABLE
+ bin = 1.0
+ @debug "Using Piecewise Linear cost function but no variable/parameter ref for ON status is passed. Default status will be set to online (1.0)" _group =
+ LOG_GROUP_COST_FUNCTIONS
+
+ elseif sos_status == SOSStatusVariable.PARAMETER
+ param = get_default_on_parameter(component)
+ bin = get_parameter(container, param, T).parameter_array[name, period]
+ @debug "Using Piecewise Linear cost function with parameter OnStatusParameter, $T" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ elseif sos_status == SOSStatusVariable.VARIABLE
+ var = get_default_on_variable(component)
+ bin = get_variable(container, var, T)[name, period]
+ @debug "Using Piecewise Linear cost function with variable OnVariable $T" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ else
+ @assert false
+ end
+
+ const_normalization_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearCostConstraint(),
+ T,
+ axes(variables)...;
+ meta = "normalization",
+ )
+
+ const_normalization_container[name, period] = JuMP.@constraint(
+ jump_model,
+ sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin
+ )
+ return
+end
+
+"""
+Implement the constraints for PWL variables for Compact form. That is:
+
+```math
+\\sum_{k\\in\\mathcal{K}} P_k^{max} \\delta_{k,t} = p_t + P_min * u_t \\\\
+\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} = on_t
+```
+"""
+function _add_pwl_constraint!(
+ container::OptimizationContainer,
+ component::T,
+ ::U,
+ break_points::Vector{Float64},
+ sos_status::SOSStatusVariable,
+ period::Int,
+) where {T <: PSY.Component, U <: PowerAboveMinimumVariable}
+ variables = get_variable(container, U(), T)
+ const_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearCostConstraint(),
+ T,
+ axes(variables)...,
+ )
+ len_cost_data = length(break_points)
+ jump_model = get_jump_model(container)
+ pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T)
+ name = PSY.get_name(component)
+
+ if sos_status == SOSStatusVariable.NO_VARIABLE
+ bin = 1.0
+ @debug "Using Piecewise Linear cost function but no variable/parameter ref for ON status is passed. Default status will be set to online (1.0)" _group =
+ LOG_GROUP_COST_FUNCTIONS
+
+ elseif sos_status == SOSStatusVariable.PARAMETER
+ param = get_default_on_parameter(component)
+ bin = get_parameter(container, param, T).parameter_array[name, period]
+ @debug "Using Piecewise Linear cost function with parameter OnStatusParameter, $T" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ elseif sos_status == SOSStatusVariable.VARIABLE
+ var = get_default_on_variable(component)
+ bin = get_variable(container, var, T)[name, period]
+ @debug "Using Piecewise Linear cost function with variable OnVariable $T" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ else
+ @assert false
+ end
+ P_min = PSY.get_active_power_limits(component).min
+
+ const_container[name, period] = JuMP.@constraint(
+ jump_model,
+ bin * P_min + variables[name, period] ==
+ sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data)
+ )
+
+ const_normalization_container = lazy_container_addition!(
+ container,
+ PieceWiseLinearCostConstraint(),
+ T,
+ axes(variables)...;
+ meta = "normalization",
+ )
+
+ const_normalization_container[name, period] = JuMP.@constraint(
+ jump_model,
+ sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin
+ )
+ return
+end
+
+"""
+Implement the SOS for PWL variables. That is:
+
+```math
+\\{\\delta_{i,t}, ..., \\delta_{k,t}\\} \\in \\text{SOS}_2
+```
+"""
+function _add_pwl_sos_constraint!(
+ container::OptimizationContainer,
+ component::T,
+ ::U,
+ break_points::Vector{Float64},
+ sos_status::SOSStatusVariable,
+ period::Int,
+) where {T <: PSY.Component, U <: VariableType}
+ name = PSY.get_name(component)
+ @warn(
+ "The cost function provided for $(name) is not compatible with a linear PWL cost function.
+ An SOS-2 formulation will be added to the model. This will result in additional binary variables."
+ )
+
+ jump_model = get_jump_model(container)
+ pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T)
+ bp_count = length(break_points)
+ pwl_vars_subset = [pwl_vars[name, i, period] for i in 1:bp_count]
+ JuMP.@constraint(jump_model, pwl_vars_subset in MOI.SOS2(collect(1:bp_count)))
+ return
+end
+
+##################################################
+################ PWL Expressions #################
+##################################################
+
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_data::PSY.PiecewiseLinearData,
+ multiplier::Float64,
+) where {T <: PSY.Component}
+ name = PSY.get_name(component)
+ pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T)
+ gen_cost = JuMP.AffExpr(0.0)
+ cost_data = PSY.get_y_coords(cost_data)
+ for (i, cost) in enumerate(cost_data)
+ JuMP.add_to_expression!(
+ gen_cost,
+ cost * multiplier * pwl_var_container[(name, i, time_period)],
+ )
+ end
+ return gen_cost
+end
+
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_function::PSY.CostCurve{PSY.PiecewisePointCurve},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ cost_data_normalized = get_piecewise_pointcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ multiplier = objective_function_multiplier(U(), V())
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ return _get_pwl_cost_expression(
+ container,
+ component,
+ time_period,
+ cost_data_normalized,
+ multiplier * dt,
+ )
+end
+
+function _get_pwl_cost_expression(
+ container::OptimizationContainer,
+ component::T,
+ time_period::Int,
+ cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve},
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ cost_data_normalized = get_piecewise_pointcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ fuel_cost = PSY.get_fuel_cost(cost_function)
+ fuel_cost_value = _get_fuel_cost_value(
+ container,
+ fuel_cost,
+ time_period,
+ )
+ # Multiplier is not necessary here. There is no negative cost for fuel curves.
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ return _get_pwl_cost_expression(
+ container,
+ component,
+ time_period,
+ cost_data_normalized,
+ dt * fuel_cost_value,
+ )
+end
+
+##################################################
+######## CostCurve: PiecewisePointCurve ##########
+##################################################
+
+"""
+Add PWL cost terms for data coming from a PiecewisePointCurve
+"""
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_function::Union{
+ PSY.CostCurve{PSY.PiecewisePointCurve},
+ PSY.FuelCurve{PSY.PiecewisePointCurve},
+ },
+ ::U,
+ ::V,
+) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
+ # multiplier = objective_function_multiplier(U(), V())
+ name = PSY.get_name(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ power_units = PSY.get_power_units(cost_function)
+
+ # Normalize data
+ data = get_piecewise_pointcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+
+ if all(iszero.((point -> point.y).(PSY.get_points(data)))) # TODO I think this should have been first. before?
+ @debug "All cost terms for component $(name) are 0.0" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ return
+ end
+
+ # Compact PWL data does not exists anymore
+
+ cost_is_convex = PSY.is_convex(data)
+ break_points = PSY.get_x_coords(data)
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ sos_val = _get_sos_value(container, V, component)
+ for t in time_steps
+ _add_pwl_variables!(container, T, name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
+ if !cost_is_convex
+ _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
+ end
+ pwl_cost =
+ _get_pwl_cost_expression(container, component, t, cost_function, U(), V())
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+
+"""
+Add PWL cost terms for data coming from a PiecewisePointCurve for ThermalDispatchNoMin formulation
+"""
+function _add_pwl_term!(
+ container::OptimizationContainer,
+ component::T,
+ cost_function::Union{
+ PSY.CostCurve{PSY.PiecewisePointCurve},
+ PSY.FuelCurve{PSY.PiecewisePointCurve},
+ },
+ ::U,
+ ::V,
+) where {T <: PSY.ThermalGen, U <: VariableType, V <: ThermalDispatchNoMin}
+ name = PSY.get_name(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ power_units = PSY.get_power_units(cost_function)
+
+ # Normalize data
+ data = get_piecewise_pointcurve_per_system_unit(
+ cost_component,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ @debug "PWL cost function detected for device $(name) using $V"
+ slopes = PSY.get_slopes(data)
+ if any(slopes .< 0) || !PSY.is_convex(data)
+ throw(
+ IS.InvalidValue(
+ "The PWL cost data provided for generator $(name) is not compatible with $U.",
+ ),
+ )
+ end
+
+ # Compact PWL data does not exists anymore
+
+ if slopes[1] != 0.0
+ @debug "PWL has no 0.0 intercept for generator $(component_name)"
+ # adds a first intercept a x = 0.0 and y below the intercept of the first tuple to make convex equivalent
+ intercept_point = (x = 0.0, y = first(data).y - COST_EPSILON)
+ data = PSY.PiecewiseLinearData(vcat(intercept_point, get_points(data)))
+ @assert PSY.is_convex(slopes)
+ end
+
+ time_steps = get_time_steps(container)
+ pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
+ break_points = PSY.get_x_coords(data)
+ sos_val = _get_sos_value(container, V, component)
+ for t in time_steps
+ _add_pwl_variables!(container, T, component_name, t, data)
+ _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
+ pwl_cost =
+ _get_pwl_cost_expression(container, component, t, cost_function, U(), V())
+ pwl_cost_expressions[t] = pwl_cost
+ end
+ return pwl_cost_expressions
+end
+
+"""
+Creates piecewise linear cost function using a sum of variables and expression with sign and time step included.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_function::PSY.CostCurve{PSY.PiecewisePointCurve}: container for piecewise linear cost
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::Union{
+ PSY.CostCurve{PSY.PiecewisePointCurve},
+ PSY.FuelCurve{PSY.PiecewisePointCurve},
+ },
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ component_name = PSY.get_name(component)
+ @debug "PWL Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
+ # If array is full of tuples with zeros return 0.0
+ value_curve = PSY.get_value_curve(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ if all(iszero.((point -> point.y).(PSY.get_points(cost_component)))) # TODO I think this should have been first. before?
+ @debug "All cost terms for component $(component_name) are 0.0" _group =
+ LOG_GROUP_COST_FUNCTIONS
+ return
+ end
+ pwl_cost_expressions =
+ _add_pwl_term!(container, component, cost_function, T(), U())
+ for t in get_time_steps(container)
+ add_to_expression!(
+ container,
+ ProductionCostExpression,
+ pwl_cost_expressions[t],
+ component,
+ t,
+ )
+ add_to_objective_invariant_expression!(container, pwl_cost_expressions[t])
+ end
+ return
+end
+
+##################################################
+###### CostCurve: PiecewiseIncrementalCurve ######
+######### and PiecewiseAverageCurve ##############
+##################################################
+
+"""
+Creates piecewise linear cost function using a sum of variables and expression with sign and time step included.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_function::PSY.Union{PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, PSY.CostCurve{PSY.PiecewiseAverageCurve}}: container for piecewise linear cost
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::V,
+ ::U,
+) where {
+ T <: VariableType,
+ V <: Union{
+ PSY.CostCurve{PSY.PiecewiseIncrementalCurve},
+ PSY.CostCurve{PSY.PiecewiseAverageCurve},
+ },
+ U <: AbstractDeviceFormulation,
+}
+ # Create new PiecewisePointCurve
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ pointbased_value_curve = PSY.InputOutputCurve(value_curve)
+ pointbased_cost_function =
+ PSY.CostCurve(; value_curve = pointbased_value_curve, power_units = power_units)
+ # Call method for PiecewisePointCurve
+ _add_variable_cost_to_objective!(
+ container,
+ T(),
+ component,
+ pointbased_cost_function,
+ U(),
+ )
+ return
+end
+
+##################################################
+###### FuelCurve: PiecewiseIncrementalCurve ######
+######### and PiecewiseAverageCurve ##############
+##################################################
+
+"""
+Creates piecewise linear fuel cost function using a sum of variables and expression with sign and time step included.
+
+# Arguments
+
+ - container::OptimizationContainer : the optimization_container model built in PowerSimulations
+ - var_key::VariableKey: The variable name
+ - component_name::String: The component_name of the variable container
+ - cost_function::PSY.Union{PSY.FuelCurve{PSY.PiecewiseIncrementalCurve}, PSY.FuelCurve{PSY.PiecewiseAverageCurve}}: container for piecewise linear cost
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::V,
+ ::U,
+) where {
+ T <: VariableType,
+ V <: Union{
+ PSY.FuelCurve{PSY.PiecewiseIncrementalCurve},
+ PSY.FuelCurve{PSY.PiecewiseAverageCurve},
+ },
+ U <: AbstractDeviceFormulation,
+}
+ # Create new PiecewisePointCurve
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ fuel_cost = PSY.get_fuel_cost(cost_function)
+ pointbased_value_curve = PSY.InputOutputCurve(value_curve)
+ pointbased_cost_function =
+ PSY.FuelCurve(;
+ value_curve = pointbased_value_curve,
+ power_units = power_units,
+ fuel_cost = fuel_cost,
+ )
+ # Call method for PiecewisePointCurve
+ _add_variable_cost_to_objective!(
+ container,
+ T(),
+ component,
+ pointbased_cost_function,
+ U(),
+ )
+ return
+end
diff --git a/src/devices_models/devices/common/objective_function/quadratic_curve.jl b/src/devices_models/devices/common/objective_function/quadratic_curve.jl
new file mode 100644
index 0000000000..4f5a8aea09
--- /dev/null
+++ b/src/devices_models/devices/common/objective_function/quadratic_curve.jl
@@ -0,0 +1,252 @@
+# Add proportional terms to objective function and expression
+function _add_quadraticcurve_variable_term_to_model!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_term_per_unit::Float64,
+ quadratic_term_per_unit::Float64,
+ time_period::Int,
+) where {T <: VariableType}
+ resolution = get_resolution(container)
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
+ if quadratic_term_per_unit >= eps()
+ cost_term = _add_quadratic_term!(
+ container,
+ T(),
+ component,
+ (quadratic_term_per_unit, proportional_term_per_unit),
+ dt,
+ time_period,
+ )
+ else
+ cost_term = _add_proportional_term!(
+ container,
+ T(),
+ component,
+ proportional_term_per_unit * dt,
+ time_period,
+ )
+ end
+ add_to_expression!(
+ container,
+ ProductionCostExpression,
+ cost_term,
+ component,
+ time_period,
+ )
+ return
+end
+
+# Dispatch for vector proportional/quadratic terms
+function _add_quadraticcurve_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_term_per_unit::Vector{Float64},
+ quadratic_term_per_unit::Vector{Float64},
+) where {T <: VariableType}
+ for t in get_time_steps(container)
+ _add_quadraticcurve_variable_term_to_model!(
+ container,
+ T(),
+ component,
+ proportional_term_per_unit[t],
+ quadratic_term_per_unit[t],
+ t,
+ )
+ end
+ return
+end
+
+# Dispatch for scalar proportional/quadratic terms
+function _add_quadraticcurve_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_term_per_unit::Float64,
+ quadratic_term_per_unit::Float64,
+) where {T <: VariableType}
+ for t in get_time_steps(container)
+ _add_quadraticcurve_variable_term_to_model!(
+ container,
+ T(),
+ component,
+ proportional_term_per_unit,
+ quadratic_term_per_unit,
+ t,
+ )
+ end
+ return
+end
+
+@doc raw"""
+Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
+
+# Equation
+
+``` gen_cost = dt*sign*(sum(variable.^2)*cost_data[1] + sum(variable)*cost_data[2]) ```
+
+# LaTeX
+
+`` cost = dt\times sign (sum_{i\in I} c_1 v_i^2 + sum_{i\in I} c_2 v_i ) ``
+
+for quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a
+linear cost term `sum(variable)*cost_data[2]`
+
+# Arguments
+
+* container::OptimizationContainer : the optimization_container model built in PowerSimulations
+* var_key::VariableKey: The variable name
+* component_name::String: The component_name of the variable container
+* cost_component::PSY.CostCurve{PSY.QuadraticCurve} : container for quadratic factors
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.CostCurve{PSY.QuadraticCurve},
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ multiplier = objective_function_multiplier(T(), U())
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ quadratic_term = PSY.get_quadratic_term(cost_component)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ proportional_term_per_unit = get_proportional_cost_per_system_unit(
+ proportional_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ quadratic_term_per_unit = get_quadratic_cost_per_system_unit(
+ quadratic_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ _add_quadraticcurve_variable_cost!(
+ container,
+ T(),
+ component,
+ multiplier * proportional_term_per_unit,
+ multiplier * quadratic_term_per_unit,
+ )
+ return
+end
+
+function _add_variable_cost_to_objective!(
+ ::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.CostCurve{PSY.QuadraticCurve},
+ ::U,
+) where {
+ T <: PowerAboveMinimumVariable,
+ U <: Union{AbstractCompactUnitCommitment, ThermalCompactDispatch},
+}
+ throw(
+ IS.ConflictingInputsError(
+ "Quadratic Cost Curves are not allowed for Compact formulations",
+ ),
+ )
+ return
+end
+
+function _add_fuel_quadratic_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_fuel_curve::Float64,
+ quadratic_fuel_curve::Float64,
+ fuel_cost::Float64,
+) where {T <: VariableType}
+ _add_quadraticcurve_variable_cost!(
+ container,
+ T(),
+ component,
+ proportional_fuel_curve * fuel_cost,
+ quadratic_fuel_curve * fuel_cost,
+ )
+end
+
+function _add_fuel_quadratic_variable_cost!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ proportional_fuel_curve::Float64,
+ quadratic_fuel_curve::Float64,
+ fuel_cost::IS.TimeSeriesKey,
+) where {T <: VariableType}
+ error("Not implemented yet")
+ _add_quadraticcurve_variable_cost!(
+ container,
+ T(),
+ component,
+ proportional_fuel_curve,
+ quadratic_fuel_curve,
+ )
+end
+
+@doc raw"""
+Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
+
+# Equation
+
+``` gen_cost = dt*(sum(variable.^2)*cost_data[1]*fuel_cost + sum(variable)*cost_data[2]*fuel_cost) ```
+
+# LaTeX
+
+`` cost = dt\times (sum_{i\in I} c_f c_1 v_i^2 + sum_{i\in I} c_f c_2 v_i ) ``
+
+for quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a
+linear cost term `sum(variable)*cost_data[2]`
+
+# Arguments
+
+* container::OptimizationContainer : the optimization_container model built in PowerSimulations
+* var_key::VariableKey: The variable name
+* component_name::String: The component_name of the variable container
+* cost_component::PSY.FuelCurve{PSY.QuadraticCurve} : container for quadratic factors
+"""
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Component,
+ cost_function::PSY.FuelCurve{PSY.QuadraticCurve},
+ ::U,
+) where {T <: VariableType, U <: AbstractDeviceFormulation}
+ multiplier = objective_function_multiplier(T(), U())
+ base_power = get_base_power(container)
+ device_base_power = PSY.get_base_power(component)
+ value_curve = PSY.get_value_curve(cost_function)
+ power_units = PSY.get_power_units(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ quadratic_term = PSY.get_quadratic_term(cost_component)
+ proportional_term = PSY.get_proportional_term(cost_component)
+ proportional_term_per_unit = get_proportional_cost_per_system_unit(
+ proportional_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ quadratic_term_per_unit = get_quadratic_cost_per_system_unit(
+ quadratic_term,
+ power_units,
+ base_power,
+ device_base_power,
+ )
+ fuel_cost = PSY.get_fuel_cost(cost_function)
+ # Multiplier is not necessary here. There is no negative cost for fuel curves.
+ _add_fuel_quadratic_variable_cost!(
+ container,
+ T(),
+ component,
+ multiplier * proportional_term_per_unit,
+ multiplier * quadratic_term_per_unit,
+ fuel_cost,
+ )
+ return
+end
diff --git a/src/devices_models/devices/common/objective_functions.jl b/src/devices_models/devices/common/objective_functions.jl
deleted file mode 100644
index 9dd9aade79..0000000000
--- a/src/devices_models/devices/common/objective_functions.jl
+++ /dev/null
@@ -1,921 +0,0 @@
-function add_variable_cost!(
- container::OptimizationContainer,
- ::U,
- devices::IS.FlattenIteratorWrapper{T},
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- for d in devices
- op_cost_data = PSY.get_operation_cost(d)
- _add_variable_cost_to_objective!(container, U(), d, op_cost_data, V())
- end
- return
-end
-
-function add_variable_cost!(
- container::OptimizationContainer,
- ::U,
- service::T,
- ::V,
-) where {T <: PSY.ReserveDemandCurve, U <: VariableType, V <: StepwiseCostReserve}
- _add_variable_cost_to_objective!(container, U(), service, V())
- return
-end
-
-function add_shut_down_cost!(
- container::OptimizationContainer,
- ::U,
- devices::IS.FlattenIteratorWrapper{T},
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- for d in devices
- op_cost_data = PSY.get_operation_cost(d)
- cost_term = shut_down_cost(op_cost_data, d, V())
- iszero(cost_term) && continue
- for t in get_time_steps(container)
- _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
- end
- end
- return
-end
-
-function add_proportional_cost!(
- container::OptimizationContainer,
- ::U,
- devices::IS.FlattenIteratorWrapper{T},
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- for d in devices
- op_cost_data = PSY.get_operation_cost(d)
- cost_term = proportional_cost(op_cost_data, U(), d, V())
- iszero(cost_term) && continue
- for t in get_time_steps(container)
- _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
- end
- end
- return
-end
-
-function add_proportional_cost!(
- container::OptimizationContainer,
- ::U,
- devices::IS.FlattenIteratorWrapper{T},
- ::V,
-) where {T <: PSY.ThermalGen, U <: OnVariable, V <: AbstractCompactUnitCommitment}
- multiplier = objective_function_multiplier(U(), V())
- for d in devices
- op_cost_data = PSY.get_operation_cost(d)
- cost_term = proportional_cost(op_cost_data, U(), d, V())
- iszero(cost_term) && continue
- for t in get_time_steps(container)
- exp = _add_proportional_term!(container, U(), d, cost_term * multiplier, t)
- add_to_expression!(container, ProductionCostExpression, exp, d, t)
- end
- end
- return
-end
-
-function add_proportional_cost!(
- container::OptimizationContainer,
- ::U,
- service::T,
- ::V,
-) where {
- T <: Union{PSY.Reserve, PSY.ReserveNonSpinning},
- U <: ActivePowerReserveVariable,
- V <: AbstractReservesFormulation,
-}
- base_p = get_base_power(container)
- reserve_variable = get_variable(container, U(), T, PSY.get_name(service))
- for index in Iterators.product(axes(reserve_variable)...)
- add_to_objective_invariant_expression!(
- container,
- DEFAULT_RESERVE_COST / base_p * reserve_variable[index...],
- )
- end
- return
-end
-
-function add_proportional_cost!(
- container::OptimizationContainer,
- ::U,
- agcs::IS.FlattenIteratorWrapper{T},
- ::PIDSmoothACE,
-) where {T <: PSY.AGC, U <: LiftVariable}
- lift_variable = get_variable(container, U(), T)
- for index in Iterators.product(axes(lift_variable)...)
- add_to_objective_invariant_expression!(
- container,
- SERVICES_SLACK_COST * lift_variable[index...],
- )
- end
- return
-end
-
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- op_cost::PSY.OperationalCost,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- variable_cost_data = variable_cost(op_cost, T(), component, U())
- _add_variable_cost_to_objective!(container, T(), component, variable_cost_data, U())
- return
-end
-
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- op_cost::PSY.MarketBidCost,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- component_name = PSY.get_name(component)
- @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name
- time_steps = get_time_steps(container)
- initial_time = get_initial_time(container)
- variable_cost_forecast = PSY.get_variable_cost(
- component,
- op_cost;
- start_time = initial_time,
- len = length(time_steps),
- )
- variable_cost_forecast_values = TimeSeries.values(variable_cost_forecast)
- parameter_container = _get_cost_function_parameter_container(
- container,
- CostFunctionParameter(),
- component,
- T(),
- U(),
- eltype(variable_cost_forecast_values),
- )
- pwl_cost_expressions =
- _add_pwl_term!(container, component, variable_cost_forecast_values, T(), U())
- jump_model = get_jump_model(container)
- for t in time_steps
- set_multiplier!(
- parameter_container,
- # Using 1.0 here since we want to reuse the existing code that adds the mulitpler
- # of base power times the time delta.
- 1.0,
- component_name,
- t,
- )
-
- set_parameter!(
- parameter_container,
- jump_model,
- PSY.get_raw_data(variable_cost_forecast_values[t]),
- component_name,
- t,
- )
- add_to_expression!(
- container,
- ProductionCostExpression,
- pwl_cost_expressions[t],
- component,
- t,
- )
- add_to_objective_variant_expression!(container, pwl_cost_expressions[t])
- end
-
- # Service Cost Bid
- ancillary_services = PSY.get_ancillary_services(op_cost)
- for service in ancillary_services
- _add_service_bid_cost!(container, component, service)
- end
- return
-end
-
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Reserve,
- ::U,
-) where {T <: VariableType, U <: StepwiseCostReserve}
- component_name = PSY.get_name(component)
- @debug "PWL Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
- # If array is full of tuples with zeros return 0.0
- time_steps = get_time_steps(container)
- variable_cost_forecast = get_time_series(container, component, "variable_cost")
- variable_cost_forecast_values = TimeSeries.values(variable_cost_forecast)
- parameter_container = _get_cost_function_parameter_container(
- container,
- CostFunctionParameter(),
- component,
- T(),
- U(),
- eltype(variable_cost_forecast_values),
- )
- pwl_cost_expressions =
- _add_pwl_term!(container, component, variable_cost_forecast_values, T(), U())
- jump_model = get_jump_model(container)
- for t in time_steps
- set_multiplier!(
- parameter_container,
- # Using 1.0 here since we want to reuse the existing code that adds the mulitpler
- # of base power times the time delta.
- 1.0,
- component_name,
- t,
- )
- set_parameter!(
- parameter_container,
- jump_model,
- PSY.get_raw_data(variable_cost_forecast_values[t]),
- component_name,
- t,
- )
- add_to_objective_variant_expression!(container, pwl_cost_expressions[t])
- end
- return
-end
-
-function add_start_up_cost!(
- container::OptimizationContainer,
- ::U,
- devices::IS.FlattenIteratorWrapper{T},
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- for d in devices
- op_cost_data = PSY.get_operation_cost(d)
- _add_start_up_cost_to_objective!(container, U(), d, op_cost_data, V())
- end
- return
-end
-
-function _add_start_up_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- op_cost::PSY.OperationalCost,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- cost_term = start_up_cost(op_cost, component, U())
- iszero(cost_term) && return
- multiplier = objective_function_multiplier(T(), U())
- for t in get_time_steps(container)
- _add_proportional_term!(container, T(), component, cost_term * multiplier, t)
- end
- return
-end
-
-const MULTI_START_COST_MAP = Dict{DataType, Int}(
- HotStartVariable => 1,
- WarmStartVariable => 2,
- ColdStartVariable => 3,
-)
-
-function _add_start_up_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- op_cost::Union{PSY.MultiStartCost, PSY.MarketBidCost},
- ::U,
-) where {T <: VariableType, U <: ThermalMultiStartUnitCommitment}
- cost_terms = start_up_cost(op_cost, component, U())
- cost_term = cost_terms[MULTI_START_COST_MAP[T]]
- iszero(cost_term) && return
- multiplier = objective_function_multiplier(T(), U())
- for t in get_time_steps(container)
- _add_proportional_term!(container, T(), component, cost_term * multiplier, t)
- end
- return
-end
-
-function _get_cost_function_parameter_container(
- container::OptimizationContainer,
- ::S,
- component::T,
- ::U,
- ::V,
- cost_type::Type{<:PSY.FunctionData},
-) where {
- S <: ObjectiveFunctionParameter,
- T <: PSY.Component,
- U <: VariableType,
- V <: Union{AbstractDeviceFormulation, AbstractServiceFormulation},
-}
- if has_container_key(container, S, T)
- return get_parameter(container, S(), T)
- else
- container_axes = axes(get_variable(container, U(), T))
- if has_container_key(container, OnStatusParameter, T)
- sos_val = SOSStatusVariable.PARAMETER
- else
- sos_val = sos_status(component, V())
- end
- return add_param_container!(
- container,
- S(),
- T,
- U,
- sos_val,
- uses_compact_power(component, V()),
- PSY.get_raw_data_type(cost_type),
- container_axes...,
- )
- end
-end
-
-function _add_service_bid_cost!(
- container::OptimizationContainer,
- component::PSY.Component,
- service::PSY.Reserve{T},
-) where {T <: PSY.ReserveDirection}
- time_steps = get_time_steps(container)
- initial_time = get_initial_time(container)
- base_power = get_base_power(container)
- forecast_data = PSY.get_services_bid(
- component,
- PSY.get_operation_cost(component),
- service;
- start_time = initial_time,
- len = length(time_steps),
- )
- forecast_data_values = PSY.get_raw_data.(TimeSeries.values(forecast_data)) .* base_power
- reserve_variable = get_variable(container, U(), T, PSY.get_name(service))
- component_name = PSY.get_name(component)
- for t in time_steps
- add_to_objective_invariant_expression!(
- container,
- forecast_data_values[t] * reserve_variable[component_name, t],
- )
- end
-end
-
-function _add_service_bid_cost!(::OptimizationContainer, ::PSY.Component, ::PSY.Service) end
-
-function _add_service_bid_cost!(
- ::OptimizationContainer,
- ::PSY.Component,
- service::PSY.ReserveDemandCurve{T},
-) where {T <: PSY.ReserveDirection}
- error(
- "The Current version doesn't supports cost bid for ReserveDemandCurve services, \\
- please change the forecast data for $(PSY.get_name(service)) \\
- and open a feature request",
- )
- return
-end
-
-"""
-Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
-
-# Arguments
-
- - container::OptimizationContainer : the optimization_container model built in PowerSimulations
- - var_key::VariableKey: The variable name
- - component_name::String: The component_name of the variable container
- - cost_component::PSY.LinearFunctionData : container for cost to be associated with variable
-"""
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- cost_component::PSY.LinearFunctionData,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(T(), U())
- base_power = get_base_power(container)
- cost_data = PSY.get_proportional_term(cost_component)
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- for time_period in get_time_steps(container)
- linear_cost = _add_proportional_term!(
- container,
- T(),
- component,
- cost_data * multiplier * base_power * dt,
- time_period,
- )
- add_to_expression!(
- container,
- ProductionCostExpression,
- linear_cost,
- component,
- time_period,
- )
- end
- return
-end
-
-@doc raw"""
-Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for optimization_container model.
-
-# Equation
-
-``` gen_cost = dt*sign*(sum(variable.^2)*cost_data[1] + sum(variable)*cost_data[2]) ```
-
-# LaTeX
-
-`` cost = dt\times sign (sum_{i\in I} c_1 v_i^2 + sum_{i\in I} c_2 v_i ) ``
-
-for quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a
-linear cost term `sum(variable)*cost_data[2]`
-
-# Arguments
-
-* container::OptimizationContainer : the optimization_container model built in PowerSimulations
-* var_key::VariableKey: The variable name
-* component_name::String: The component_name of the variable container
-* cost_component::PSY.QuadraticFunctionData : container for quadratic factors
-"""
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- cost_component::PSY.QuadraticFunctionData,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(T(), U())
- base_power = get_base_power(container)
- quadratic_term = PSY.get_quadratic_term(cost_component)
- proportional_term = PSY.get_proportional_term(cost_component)
- constant_term = PSY.get_constant_term(cost_component)
- (constant_term == 0) ||
- throw(ArgumentError("Not yet implemented for nonzero constant term"))
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- for time_period in get_time_steps(container)
- if quadratic_term >= eps()
- cost_term = _add_quadratic_term!(
- container,
- T(),
- component,
- (quadratic_term, proportional_term),
- base_power,
- multiplier * dt,
- time_period,
- )
- else
- cost_term = _add_proportional_term!(
- container,
- T(),
- component,
- proportional_term * multiplier * base_power * dt,
- time_period,
- )
- end
- add_to_expression!(
- container,
- ProductionCostExpression,
- cost_term,
- component,
- time_period,
- )
- end
- return
-end
-
-"""
-Creates piecewise linear cost function using a sum of variables and expression with sign and time step included.
-
-# Arguments
-
- - container::OptimizationContainer : the optimization_container model built in PowerSimulations
- - var_key::VariableKey: The variable name
- - component_name::String: The component_name of the variable container
- - cost_component::PSY.PiecewiseLinearPointData: container for piecewise linear cost
-"""
-function _add_variable_cost_to_objective!(
- container::OptimizationContainer,
- ::T,
- component::PSY.Component,
- cost_component::PSY.PiecewiseLinearPointData,
- ::U,
-) where {T <: VariableType, U <: AbstractDeviceFormulation}
- component_name = PSY.get_name(component)
- @debug "PWL Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
- # If array is full of tuples with zeros return 0.0
- if all(iszero.((point -> point.y).(PSY.get_points(cost_component)))) # TODO I think this should have been first. before?
- @debug "All cost terms for component $(component_name) are 0.0" _group =
- LOG_GROUP_COST_FUNCTIONS
- return
- end
- pwl_cost_expressions = _add_pwl_term!(container, component, cost_component, T(), U())
- for t in get_time_steps(container)
- add_to_expression!(
- container,
- ProductionCostExpression,
- pwl_cost_expressions[t],
- component,
- t,
- )
- add_to_objective_invariant_expression!(container, pwl_cost_expressions[t])
- end
- return
-end
-
-function _get_sos_value(
- container::OptimizationContainer,
- ::Type{V},
- component::T,
-) where {T <: PSY.Component, V <: AbstractDeviceFormulation}
- if has_container_key(container, OnStatusParameter, T)
- sos_val = SOSStatusVariable.PARAMETER
- else
- sos_val = sos_status(component, V())
- end
- return sos_val
-end
-
-function _get_sos_value(
- container::OptimizationContainer,
- ::Type{V},
- component::T,
-) where {T <: PSY.Component, V <: AbstractServiceFormulation}
- return SOSStatusVariable.NO_VARIABLE
-end
-
-function _add_pwl_term!(
- container::OptimizationContainer,
- component::T,
- cost_data::AbstractVector{PSY.LinearFunctionData},
- ::U,
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- base_power = get_base_power(container)
- # Re-scale breakpoints by Basepower
- time_steps = get_time_steps(container)
- cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
- for t in time_steps
- proportional_value =
- PSY.get_proportional_term(cost_data[t]) * multiplier * base_power * dt
- cost_expressions[t] =
- _add_proportional_term!(container, U(), component, proportional_value, t)
- end
- return cost_expressions
-end
-
-"""
-Add PWL cost terms for data coming from the MarketBidCost
-"""
-function _add_pwl_term!(
- container::OptimizationContainer,
- component::T,
- cost_data::AbstractVector{PSY.PiecewiseLinearPointData},
- ::U,
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- base_power = get_base_power(container)
- # Re-scale breakpoints by Basepower
- name = PSY.get_name(component)
- time_steps = get_time_steps(container)
- pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
- sos_val = _get_sos_value(container, V, component)
- for t in time_steps
- # Run checks in every time step because each time step has a PWL cost function
- data = cost_data[t]
- compact_status = validate_compact_pwl_data(component, data, base_power)
- if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID
- error(
- "The data provided is not compatible with formulation $V. Use a formulation compatible with Compact Cost Functions",
- )
- # data = _convert_to_full_variable_cost(data, component)
- elseif uses_compact_power(component, V()) &&
- compact_status != COMPACT_PWL_STATUS.VALID
- @warn(
- "The cost data provided is not in compact form. Will attempt to convert. Errors may occur."
- )
- data = _convert_to_compact_variable_cost(data)
- else
- @debug uses_compact_power(component, V()) compact_status name T V
- end
- cost_is_convex = PSY.is_convex(data)
- break_points = PSY.get_x_coords(data) ./ base_power # TODO should this be get_x_lengths/get_breakpoint_upper_bounds?
- _add_pwl_variables!(container, T, name, t, data)
- _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
- if !cost_is_convex
- _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
- end
- pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
- pwl_cost_expressions[t] = pwl_cost
- end
- return pwl_cost_expressions
-end
-
-function _add_pwl_term!(
- container::OptimizationContainer,
- component::T,
- cost_data::AbstractVector{PSY.PiecewiseLinearPointData},
- ::U,
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractServiceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- base_power = get_base_power(container)
- # Re-scale breakpoints by Basepower
- name = PSY.get_name(component)
- time_steps = get_time_steps(container)
- pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
- sos_val = _get_sos_value(container, V, component)
- for t in time_steps
- data = cost_data[t]
- break_points = PSY.get_x_coords(data) ./ base_power
- _add_pwl_variables!(container, T, name, t, data)
- _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
- _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
- pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
- pwl_cost_expressions[t] = pwl_cost
- end
- return pwl_cost_expressions
-end
-
-"""
-Add PWL cost terms for data coming from a constant PWL cost function
-"""
-function _add_pwl_term!(
- container::OptimizationContainer,
- component::T,
- data::PSY.PiecewiseLinearPointData,
- ::U,
- ::V,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- multiplier = objective_function_multiplier(U(), V())
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- base_power = get_base_power(container)
- # Re-scale breakpoints by Basepower
- name = PSY.get_name(component)
-
- compact_status = validate_compact_pwl_data(component, data, base_power)
- if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID
- error(
- "The data provided is not compatible with formulation $V. Use a formulation compatible with Compact Cost Functions",
- )
- # data = _convert_to_full_variable_cost(data, component)
- elseif uses_compact_power(component, V()) && compact_status != COMPACT_PWL_STATUS.VALID
- @warn(
- "The cost data provided is not in compact form. Will attempt to convert. Errors may occur."
- )
- data = _convert_to_compact_variable_cost(data)
- else
- @debug uses_compact_power(component, V()) compact_status name T V
- end
-
- cost_is_convex = PSY.is_convex(data)
- break_points = PSY.get_x_coords(data) ./ base_power
- time_steps = get_time_steps(container)
- pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
- sos_val = _get_sos_value(container, V, component)
- for t in time_steps
- _add_pwl_variables!(container, T, name, t, data)
- _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
- if !cost_is_convex
- _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t)
- end
- pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
- pwl_cost_expressions[t] = pwl_cost
- end
- return pwl_cost_expressions
-end
-
-function _add_pwl_term!(
- container::OptimizationContainer,
- component::T,
- data::PSY.PiecewiseLinearPointData,
- ::U,
- ::V,
-) where {T <: PSY.ThermalGen, U <: VariableType, V <: ThermalDispatchNoMin}
- multiplier = objective_function_multiplier(U(), V())
- resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
- component_name = PSY.get_name(component)
- @debug "PWL cost function detected for device $(component_name) using $V"
- base_power = get_base_power(container)
- slopes = PSY.get_slopes(data)
- if any(slopes .< 0) || !PSY.is_convex(data)
- throw(
- IS.InvalidValue(
- "The PWL cost data provided for generator $(component_name) is not compatible with $U.",
- ),
- )
- end
-
- if validate_compact_pwl_data(component, data, base_power) == COMPACT_PWL_STATUS.VALID
- error("The data provided is not compatible with formulation $V. \\
- Use a formulation compatible with Compact Cost Functions")
- end
-
- if slopes[1] != 0.0
- @debug "PWL has no 0.0 intercept for generator $(component_name)"
- # adds a first intercept a x = 0.0 and y below the intercept of the first tuple to make convex equivalent
- intercept_point = (x = 0.0, y = first(data).y - COST_EPSILON)
- data = PSY.PiecewiseLinearPointData(vcat(intercept_point, get_points(data)))
- @assert PSY.is_convex(slopes)
- end
-
- time_steps = get_time_steps(container)
- pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end])
- break_points = PSY.get_x_coords(data) ./ base_power
- sos_val = _get_sos_value(container, V, component)
- for t in time_steps
- _add_pwl_variables!(container, T, component_name, t, data)
- _add_pwl_constraint!(container, component, U(), break_points, sos_val, t)
- pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt)
- pwl_cost_expressions[t] = pwl_cost
- end
- return pwl_cost_expressions
-end
-
-function _add_pwl_variables!(
- container::OptimizationContainer,
- ::Type{T},
- component_name::String,
- time_period::Int,
- cost_data::PSY.PiecewiseLinearPointData,
-) where {T <: PSY.Component}
- var_container = lazy_container_addition!(container, PieceWiseLinearCostVariable(), T)
- # length(PiecewiseLinearPointData) gets number of segments, here we want number of points
- pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data) + 1)
- for i in 1:(length(cost_data) + 1)
- pwlvars[i] =
- var_container[(component_name, i, time_period)] = JuMP.@variable(
- get_jump_model(container),
- base_name = "PieceWiseLinearCostVariable_$(component_name)_{pwl_$(i), $time_period}",
- lower_bound = 0.0,
- upper_bound = 1.0
- )
- end
- return pwlvars
-end
-
-function _add_pwl_constraint!(
- container::OptimizationContainer,
- component::T,
- ::U,
- break_points::Vector{Float64},
- sos_status::SOSStatusVariable,
- period::Int,
-) where {T <: PSY.Component, U <: VariableType}
- variables = get_variable(container, U(), T)
- const_container = lazy_container_addition!(
- container,
- PieceWiseLinearCostConstraint(),
- T,
- axes(variables)...,
- )
- len_cost_data = length(break_points)
- jump_model = get_jump_model(container)
- pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T)
- name = PSY.get_name(component)
- const_container[name, period] = JuMP.@constraint(
- jump_model,
- variables[name, period] ==
- sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data)
- )
-
- if sos_status == SOSStatusVariable.NO_VARIABLE
- bin = 1.0
- @debug "Using Piecewise Linear cost function but no variable/parameter ref for ON status is passed. Default status will be set to online (1.0)" _group =
- LOG_GROUP_COST_FUNCTIONS
-
- elseif sos_status == SOSStatusVariable.PARAMETER
- param = get_default_on_parameter(component)
- bin = get_parameter(container, param, T).parameter_array[name, period]
- @debug "Using Piecewise Linear cost function with parameter OnStatusParameter, $T" _group =
- LOG_GROUP_COST_FUNCTIONS
- elseif sos_status == SOSStatusVariable.VARIABLE
- var = get_default_on_variable(component)
- bin = get_variable(container, var, T)[name, period]
- @debug "Using Piecewise Linear cost function with variable OnVariable $T" _group =
- LOG_GROUP_COST_FUNCTIONS
- else
- @assert false
- end
-
- JuMP.@constraint(
- jump_model,
- sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin
- )
- return
-end
-
-function _add_pwl_sos_constraint!(
- container::OptimizationContainer,
- component::T,
- ::U,
- break_points::Vector{Float64},
- sos_status::SOSStatusVariable,
- period::Int,
-) where {T <: PSY.Component, U <: VariableType}
- name = PSY.get_name(component)
- @warn(
- "The cost function provided for $(name) is not compatible with a linear PWL cost function.
- An SOS-2 formulation will be added to the model. This will result in additional binary variables."
- )
-
- jump_model = get_jump_model(container)
- pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T)
- bp_count = length(break_points)
- pwl_vars_subset = [pwl_vars[name, i, period] for i in 1:bp_count]
- JuMP.@constraint(jump_model, pwl_vars_subset in MOI.SOS2(collect(1:bp_count)))
- return
-end
-
-function _get_pwl_cost_expression(
- container::OptimizationContainer,
- component::T,
- time_period::Int,
- cost_data::PSY.PiecewiseLinearPointData,
- multiplier::Float64,
-) where {T <: PSY.Component}
- name = PSY.get_name(component)
- pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T)
- gen_cost = JuMP.AffExpr(0.0)
- cost_data = PSY.get_points(cost_data)
- for i in 1:length(cost_data)
- JuMP.add_to_expression!(
- gen_cost,
- cost_data[i].y * multiplier * pwl_var_container[(name, i, time_period)],
- )
- end
- return gen_cost
-end
-
-function _get_no_load_cost(
- component::T,
- ::V,
- ::U,
-) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation}
- return no_load_cost(PSY.get_operation_cost(component), U(), component, V())
-end
-
-function _convert_to_compact_variable_cost(
- var_cost::PSY.PiecewiseLinearPointData,
- p_min::Float64,
- no_load_cost::Float64,
-)
- points = PSY.get_points(var_cost)
- new_points = [(pp - p_min, c - no_load_cost) for (pp, c) in points]
- return PSY.PiecewiseLinearPointData(new_points)
-end
-
-function _convert_to_compact_variable_cost(var_cost::PSY.PiecewiseLinearPointData)
- p_min, no_load_cost = first(PSY.get_points(var_cost))
- return _convert_to_compact_variable_cost(var_cost, p_min, no_load_cost)
-end
-
-function _add_proportional_term!(
- container::OptimizationContainer,
- ::T,
- component::U,
- linear_term::Float64,
- time_period::Int,
-) where {T <: VariableType, U <: PSY.Component}
- component_name = PSY.get_name(component)
- @debug "Linear Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
- variable = get_variable(container, T(), U)[component_name, time_period]
- lin_cost = variable * linear_term
- add_to_objective_invariant_expression!(container, lin_cost)
- return lin_cost
-end
-
-function _add_quadratic_term!(
- container::OptimizationContainer,
- ::T,
- component::U,
- q_terms::NTuple{2, Float64},
- var_multiplier::Float64,
- expression_multiplier::Float64,
- time_period::Int,
-) where {T <: VariableType, U <: PSY.Component}
- component_name = PSY.get_name(component)
- @debug "$component_name Quadratic Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
- var = get_variable(container, T(), U)[component_name, time_period]
- q_cost_ = (var * var_multiplier) .^ 2 * q_terms[1] + var * var_multiplier * q_terms[2]
- q_cost = q_cost_ * expression_multiplier
- add_to_objective_invariant_expression!(container, q_cost)
- return q_cost
-end
-
-function _add_quadratic_term!(
- container::OptimizationContainer,
- ::T,
- component::U,
- q_terms::NTuple{2, Float64},
- var_multiplier::Float64,
- expression_multiplier::Float64,
- time_period::Int,
-) where {T <: PowerAboveMinimumVariable, U <: PSY.ThermalGen}
- component_name = PSY.get_name(component)
- p_min = PSY.get_active_power_limits(component).min
- @debug "$component_name Quadratic Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
- var = get_variable(container, T(), U)[component_name, time_period]
- q_cost_ =
- (var * var_multiplier) .^ 2 * q_terms[1] +
- var * var_multiplier * (q_terms[2] + 2 * q_terms[1] * p_min)
- q_cost = q_cost_ * expression_multiplier
- add_to_objective_invariant_expression!(container, q_cost)
- return q_cost
-end
diff --git a/src/devices_models/devices/common/rateofchange_constraints.jl b/src/devices_models/devices/common/rateofchange_constraints.jl
index ba134bd175..b5558b56b1 100644
--- a/src/devices_models/devices/common/rateofchange_constraints.jl
+++ b/src/devices_models/devices/common/rateofchange_constraints.jl
@@ -62,13 +62,12 @@ function add_linear_ramp_constraints!(
U::Type{S},
devices::IS.FlattenIteratorWrapper{V},
model::DeviceModel{V, W},
- X::Type{<:PM.AbstractPowerModel},
+ ::Type{<:PM.AbstractPowerModel},
) where {
S <: Union{PowerAboveMinimumVariable, ActivePowerVariable},
V <: PSY.Component,
W <: AbstractDeviceFormulation,
}
- parameters = built_for_recurrent_solves(container)
time_steps = get_time_steps(container)
variable = get_variable(container, U(), V)
ramp_devices = _get_ramp_constraint_devices(container, devices)
@@ -91,23 +90,22 @@ function add_linear_ramp_constraints!(
ramp_limits = PSY.get_ramp_limits(get_component(ic))
ic_power = get_value(ic)
@debug "add rate_of_change_constraint" name ic_power
- @assert (parameters && isa(ic_power, JuMP.VariableRef)) || !parameters
con_up[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
expr_up[name, 1] - ic_power <= ramp_limits.up * minutes_per_period
)
con_down[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
ic_power - expr_dn[name, 1] >= -1 * ramp_limits.down * minutes_per_period
)
for t in time_steps[2:end]
con_up[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
expr_up[name, t] - variable[name, t - 1] <=
ramp_limits.up * minutes_per_period
)
con_down[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
variable[name, t - 1] - expr_dn[name, t] >=
-1 * ramp_limits.down * minutes_per_period
)
@@ -147,21 +145,21 @@ function add_linear_ramp_constraints!(
@debug "add rate_of_change_constraint" name ic_power
@assert (parameters && isa(ic_power, JuMP.VariableRef)) || !parameters
con_up[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
variable[name, 1] - ic_power <= ramp_limits.up * minutes_per_period
)
con_down[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
ic_power - variable[name, 1] <= ramp_limits.down * minutes_per_period
)
for t in time_steps[2:end]
con_up[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
variable[name, t] - variable[name, t - 1] <=
ramp_limits.up * minutes_per_period
)
con_down[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
variable[name, t - 1] - variable[name, t] <=
ramp_limits.down * minutes_per_period
)
@@ -234,23 +232,23 @@ function add_semicontinuous_ramp_constraints!(
@debug "add rate_of_change_constraint" name ic_power
con_up[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
expr_up[name, 1] - ic_power <=
ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1]
)
con_down[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
ic_power - expr_dn[name, 1] <=
ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1]
)
for t in time_steps[2:end]
con_up[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
expr_up[name, t] - variable[name, t - 1] <=
ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, t]
)
con_down[name, t] = JuMP.@constraint(
- container.JuMPmodel,
+ get_jump_model(container),
variable[name, t - 1] - expr_dn[name, t] <=
ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, t]
)
@@ -258,64 +256,3 @@ function add_semicontinuous_ramp_constraints!(
end
return
end
-
-function add_semicontinuous_ramp_constraints!(
- container::OptimizationContainer,
- T::Type{<:ConstraintType},
- U::Type{<:VariableType},
- devices::IS.FlattenIteratorWrapper{V},
- model::DeviceModel{V, W},
- X::Type{<:PM.AbstractPowerModel},
-) where {V <: PSY.Component, W <: AbstractDeviceFormulation}
- parameters = built_for_recurrent_solves(container)
- time_steps = get_time_steps(container)
- variable = get_variable(container, U(), V)
- varstart = get_variable(container, StartVariable(), V)
- varstop = get_variable(container, StopVariable(), V)
-
- ramp_devices = _get_ramp_constraint_devices(container, devices)
- minutes_per_period = _get_minutes_per_period(container)
- IC = _get_initial_condition_type(T, V, W)
- initial_conditions_power = get_initial_condition(container, IC(), V)
-
- set_name = [PSY.get_name(r) for r in ramp_devices]
- con_up =
- add_constraints_container!(container, T(), V, set_name, time_steps; meta = "up")
- con_down =
- add_constraints_container!(container, T(), V, set_name, time_steps; meta = "dn")
-
- for ic in initial_conditions_power
- name = get_component_name(ic)
- # This is to filter out devices that dont need a ramping constraint
- name ∉ set_name && continue
- device = get_component(ic)
- ramp_limits = PSY.get_ramp_limits(device)
- power_limits = PSY.get_active_power_limits(device)
- ic_power = get_value(ic)
- @debug "add rate_of_change_constraint" name ic_power
- @assert (parameters && isa(ic_power, JuMP.VariableRef)) || !parameters
- con_up[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
- variable[name, 1] - ic_power <=
- ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, 1]
- )
- con_down[name, 1] = JuMP.@constraint(
- container.JuMPmodel,
- ic_power - variable[name, 1] <=
- ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, 1]
- )
- for t in time_steps[2:end]
- con_up[name, t] = JuMP.@constraint(
- container.JuMPmodel,
- variable[name, t] - variable[name, t - 1] <=
- ramp_limits.up * minutes_per_period + power_limits.min * varstart[name, t]
- )
- con_down[name, t] = JuMP.@constraint(
- container.JuMPmodel,
- variable[name, t - 1] - variable[name, t] <=
- ramp_limits.down * minutes_per_period + power_limits.min * varstop[name, t]
- )
- end
- end
- return
-end
diff --git a/src/devices_models/devices/thermal_generation.jl b/src/devices_models/devices/thermal_generation.jl
index 8a74116584..28b12488da 100644
--- a/src/devices_models/devices/thermal_generation.jl
+++ b/src/devices_models/devices/thermal_generation.jl
@@ -74,29 +74,26 @@ initial_condition_default(::InitialTimeDurationOff, d::PSY.ThermalGen, ::Abstrac
initial_condition_variable(::InitialTimeDurationOff, d::PSY.ThermalGen, ::AbstractThermalFormulation) = OnVariable()
########################Objective Function##################################################
-proportional_cost(cost::PSY.OperationalCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_fixed(cost)
-proportional_cost(cost::PSY.OperationalCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractCompactUnitCommitment) = no_load_cost(cost, S, T, U) + PSY.get_fixed(cost)
-proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_no_load(cost)
-proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractCompactUnitCommitment)=PSY.get_no_load(cost)
-proportional_cost(cost::PSY.MultiStartCost, ::OnVariable, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_fixed(cost) + PSY.get_no_load(cost)
+# TODO: Decide what is the cost for OnVariable, if fixed or constant term in variable
+#proportional_cost(cost::PSY.ThermalGenerationCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation) = no_load_cost(cost, S, T, U)
+proportional_cost(cost::PSY.ThermalGenerationCost, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation) = PSY.get_fixed(cost)
+proportional_cost(cost::PSY.MarketBidCost, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_no_load_cost(cost)
has_multistart_variables(::PSY.ThermalGen, ::AbstractThermalFormulation)=false
has_multistart_variables(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=true
objective_function_multiplier(::VariableType, ::AbstractThermalFormulation)=OBJECTIVE_FUNCTION_POSITIVE
-shut_down_cost(cost::PSY.OperationalCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_shut_down(cost)
-shut_down_cost(cost::PSY.TwoPartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=0.0
+shut_down_cost(cost::PSY.ThermalGenerationCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_shut_down(cost)
+shut_down_cost(cost::PSY.MarketBidCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_shut_down(cost)
sos_status(::PSY.ThermalGen, ::AbstractThermalDispatchFormulation)=SOSStatusVariable.NO_VARIABLE
sos_status(::PSY.ThermalGen, ::AbstractThermalUnitCommitment)=SOSStatusVariable.VARIABLE
sos_status(::PSY.ThermalMultiStart, ::AbstractStandardUnitCommitment)=SOSStatusVariable.VARIABLE
sos_status(::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=SOSStatusVariable.VARIABLE
-start_up_cost(cost::PSY.OperationalCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_start_up(cost)
-start_up_cost(cost::PSY.TwoPartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=0.0
-start_up_cost(cost::PSY.MultiStartCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))
-start_up_cost(cost::PSY.MultiStartCost, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_start_up(cost)
+start_up_cost(cost::PSY.ThermalGenerationCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))
+start_up_cost(cost::PSY.ThermalGenerationCost, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_start_up(cost)
start_up_cost(cost::PSY.MarketBidCost, ::PSY.ThermalGen, ::AbstractThermalFormulation)=maximum(PSY.get_start_up(cost))
start_up_cost(cost::PSY.MarketBidCost, ::PSY.ThermalMultiStart, ::ThermalMultiStartUnitCommitment)=PSY.get_start_up(cost)
# If the formulation used ignores start up costs, the model ignores that data.
@@ -109,23 +106,44 @@ uses_compact_power(::PSY.ThermalGen, ::ThermalCompactDispatch)=true
variable_cost(cost::PSY.OperationalCost, ::ActivePowerVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)
variable_cost(cost::PSY.OperationalCost, ::PowerAboveMinimumVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation)=PSY.get_variable(cost)
-no_load_cost(cost::PSY.MultiStartCost, ::OnVariable, ::PSY.ThermalMultiStart, U::AbstractThermalFormulation) = PSY.get_no_load(cost)
+"""
+Theoretical Cost at power output zero. Mathematically is the intercept with the y-axis
+"""
+function no_load_cost(cost::PSY.ThermalGenerationCost, S::OnVariable, d::PSY.ThermalGen, U::AbstractThermalFormulation)
+ return _no_load_cost(PSY.get_variable(cost), d)
+end
-function no_load_cost(cost::Union{PSY.ThreePartCost, PSY.TwoPartCost}, S::OnVariable, T::PSY.ThermalGen, U::AbstractThermalFormulation)
- return no_load_cost(PSY.get_variable(cost), S, T, U)
+function _no_load_cost(cost_function::PSY.CostCurve{PSY.PiecewisePointCurve}, d::PSY.ThermalGen)
+ value_curve = PSY.get_value_curve(cost_function)
+ cost = PSY.get_function_data(value_curve)
+ return last(first(PSY.get_points(cost)))
end
-# TODO given the old implementations, these functions seem to get the cost at *minimum* load, not *zero* load. Is that correct?
-no_load_cost(cost::PSY.PiecewiseLinearPointData, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = last(first(PSY.get_points(cost)))
-no_load_cost(cost::PSY.LinearFunctionData, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_proportional_term(cost) * PSY.get_active_power_limits(d).min * PSY.get_base_power(d)
+function _no_load_cost(cost_function::Union{PSY.CostCurve{PSY.LinearCurve}, PSY.CostCurve{PSY.QuadraticCurve}}, d::PSY.ThermalGen)
+ value_curve = PSY.get_value_curve(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ # Always in \$/h
+ constant_term = PSY.get_constant_term(cost_component)
+ return constant_term
+end
-function no_load_cost(cost::PSY.QuadraticFunctionData, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation)
- min_power = PSY.get_active_power_limits(d).min
- evaluated = LinearAlgebra.dot(
- [PSY.get_quadratic_term(cost), PSY.get_proportional_term(cost), PSY.get_constant_term(cost)],
- [min_power^2, min_power, 1]
- )
- return evaluated * PSY.get_base_power(d)
+function _no_load_cost(cost_function::PSY.FuelCurve{PSY.PiecewisePointCurve}, d::PSY.ThermalGen)
+ # value_curve = PSY.get_value_curve(cost_function)
+ # cost = PSY.get_function_data(value_curve)
+ return 0.0
+end
+
+function _no_load_cost(cost_function::Union{PSY.FuelCurve{PSY.LinearCurve}, PSY.FuelCurve{PSY.QuadraticCurve}}, d::PSY.ThermalGen)
+ value_curve = PSY.get_value_curve(cost_function)
+ cost_component = PSY.get_function_data(value_curve)
+ # In Unit/h (unit typically in )
+ constant_term = PSY.get_constant_term(cost_component)
+ fuel_cost = PSY.get_fuel_cost(cost_function)
+ if typeof(fuel_cost) <: Float64
+ return constant_term * fuel_cost
+ else
+ error("Time series not implemented yet")
+ end
end
#! format: on
diff --git a/src/feedforward/feedforward_constraints.jl b/src/feedforward/feedforward_constraints.jl
index 25292824ce..2acac1dfb7 100644
--- a/src/feedforward/feedforward_constraints.jl
+++ b/src/feedforward/feedforward_constraints.jl
@@ -43,7 +43,12 @@ function _add_feedforward_constraints!(
::VariableKey{U, V},
devices::IS.FlattenIteratorWrapper{V},
model::DeviceModel,
-) where {T <: ConstraintType, P <: ParameterType, U <: VariableType, V <: PSY.Component}
+) where {
+ T <: ConstraintType,
+ P <: ParameterType,
+ U <: VariableType,
+ V <: PSY.Component,
+}
time_steps = get_time_steps(container)
names = [PSY.get_name(d) for d in devices]
constraint_lb =
@@ -78,7 +83,7 @@ function _add_sc_feedforward_constraints!(
devices::IS.FlattenIteratorWrapper{V},
model::DeviceModel{V, W},
) where {
- T <: FeedforwardSemiContinousConstraint,
+ T <: FeedforwardSemiContinuousConstraint,
P <: OnStatusParameter,
U <: Union{ActivePowerVariable, PowerAboveMinimumVariable},
V <: PSY.Component,
@@ -123,7 +128,7 @@ function _add_sc_feedforward_constraints!(
devices::IS.FlattenIteratorWrapper{V},
model::DeviceModel{V, W},
) where {
- T <: FeedforwardSemiContinousConstraint,
+ T <: FeedforwardSemiContinuousConstraint,
P <: ParameterType,
U <: VariableType,
V <: PSY.Component,
@@ -225,7 +230,7 @@ function add_feedforward_constraints!(
end
_add_sc_feedforward_constraints!(
container,
- FeedforwardSemiContinousConstraint,
+ FeedforwardSemiContinuousConstraint,
parameter_type(),
var,
devices,
@@ -448,7 +453,7 @@ Constructs a equality constraint to a fix a variable in one model using the vari
function add_feedforward_constraints!(
container::OptimizationContainer,
::DeviceModel,
- devices::IS.FlattenIteratorWrapper{T},
+ devices::Union{Vector{T}, IS.FlattenIteratorWrapper{T}},
ff::FixValueFeedforward,
) where {T <: PSY.Component}
time_steps = get_time_steps(container)
@@ -461,9 +466,9 @@ function add_feedforward_constraints!(
variable = get_variable(container, var)
set_name, set_time = JuMP.axes(variable)
IS.@assert_op set_name == [PSY.get_name(d) for d in devices]
- IS.@assert_op set_time == time_steps
+ #IS.@assert_op set_time == time_steps
- for t in time_steps, name in set_name
+ for t in set_time, name in set_name
JuMP.fix(variable[name, t], param[name, t] * multiplier[name, t]; force = true)
end
end
diff --git a/src/feedforward/feedforwards.jl b/src/feedforward/feedforwards.jl
index 5a503a15b0..a69b00a2ef 100644
--- a/src/feedforward/feedforwards.jl
+++ b/src/feedforward/feedforwards.jl
@@ -50,7 +50,22 @@ function get_feedforward_meta(ff::AbstractAffectFeedforward)
end
"""
-Adds an upper bound constraint to a variable.
+ UpperBoundFeedforward(
+ component_type::Type{<:PSY.Component},
+ source::Type{T},
+ affected_values::Vector{DataType},
+ add_slacks::Bool = false,
+ meta = CONTAINER_KEY_EMPTY_META
+ ) where {T}
+
+Constructs a parameterized upper bound constraint to implement feedforward from other models.
+
+# Arguments:
+* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied
+* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward
+* `affected_values::Vector{DataType}` : Specify the variable on which the upper bound will be applied using the source values
+* `add_slacks::Bool = false` : Add slacks variables to relax the upper bound constraint.
+
"""
struct UpperBoundFeedforward <: AbstractAffectFeedforward
optimization_container_key::OptimizationContainerKey
@@ -61,7 +76,7 @@ struct UpperBoundFeedforward <: AbstractAffectFeedforward
source::Type{T},
affected_values::Vector{DataType},
add_slacks::Bool = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
@@ -87,7 +102,22 @@ get_optimization_container_key(ff::UpperBoundFeedforward) = ff.optimization_cont
get_slacks(ff::UpperBoundFeedforward) = ff.add_slacks
"""
-Adds a lower bound constraint to a variable.
+ LowerBoundFeedforward(
+ component_type::Type{<:PSY.Component},
+ source::Type{T},
+ affected_values::Vector{DataType},
+ add_slacks::Bool = false,
+ meta = CONTAINER_KEY_EMPTY_META
+ ) where {T}
+
+Constructs a parameterized lower bound constraint to implement feedforward from other models.
+
+# Arguments:
+* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied
+* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward
+* `affected_values::Vector{DataType}` : Specify the variable on which the lower bound will be applied using the source values
+* `add_slacks::Bool = false` : Add slacks variables to relax the lower bound constraint.
+
"""
struct LowerBoundFeedforward <: AbstractAffectFeedforward
optimization_container_key::OptimizationContainerKey
@@ -98,7 +128,7 @@ struct LowerBoundFeedforward <: AbstractAffectFeedforward
source::Type{T},
affected_values::Vector{DataType},
add_slacks::Bool = false,
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector{VariableKey}(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
@@ -149,7 +179,21 @@ function attach_feedforward!(
end
"""
-Adds a constraint to make the bounds of a variable 0.0. Effectively allows to "turn off" a value.
+ SemiContinuousFeedforward(
+ component_type::Type{<:PSY.Component},
+ source::Type{T},
+ affected_values::Vector{DataType},
+ meta = CONTAINER_KEY_EMPTY_META
+ ) where {T}
+
+It allows to enable/disable bounds to 0.0 for a specified variable. Commonly used to limit the
+`ActivePowerVariable` in an Economic Dispatch problem by the commitment decision taken in
+an another problem (typically a Unit Commitment problem).
+
+# Arguments:
+* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied
+* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward
+* `affected_values::Vector{DataType}` : Specify the variable on which the semicontinuous limit will be applied using the source values
"""
struct SemiContinuousFeedforward <: AbstractAffectFeedforward
optimization_container_key::OptimizationContainerKey
@@ -158,7 +202,7 @@ struct SemiContinuousFeedforward <: AbstractAffectFeedforward
component_type::Type{<:PSY.Component},
source::Type{T},
affected_values::Vector{DataType},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector{VariableKey}(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
@@ -203,8 +247,20 @@ function has_semicontinuous_feedforward(
end
"""
-Fixes a Variable or Parameter Value in the model. Is the only Feed Forward that can be used
+ FixValueFeedforward(
+ component_type::Type{<:PSY.Component},
+ source::Type{T},
+ affected_values::Vector{DataType},
+ meta = CONTAINER_KEY_EMPTY_META
+ ) where {T}
+
+Fixes a Variable or Parameter Value in the model from another problem. Is the only Feed Forward that can be used
with a Parameter or a Variable as the affected value.
+
+# Arguments:
+* `component_type::Type{<:PSY.Component}` : Specify the type of component on which the Feedforward will be applied
+* `source::Type{T}` : Specify the VariableType, ParameterType or AuxVariableType as the source of values for the Feedforward
+* `affected_values::Vector{DataType}` : Specify the variable on which the fix value will be applied using the source values
"""
struct FixValueFeedforward <: AbstractAffectFeedforward
optimization_container_key::OptimizationContainerKey
@@ -213,7 +269,7 @@ struct FixValueFeedforward <: AbstractAffectFeedforward
component_type::Type{<:PSY.Component},
source::Type{T},
affected_values::Vector{DataType},
- meta = CONTAINER_KEY_EMPTY_META,
+ meta = IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {T}
values_vector = Vector(undef, length(affected_values))
for (ix, v) in enumerate(affected_values)
diff --git a/src/initial_conditions/initialization.jl b/src/initial_conditions/initialization.jl
index ec7a3492ad..66535811d8 100644
--- a/src/initial_conditions/initialization.jl
+++ b/src/initial_conditions/initialization.jl
@@ -40,7 +40,7 @@ function get_initial_conditions_template(model::OperationModel)
base_model.use_slacks = service_model.use_slacks
base_model.time_series_names = service_model.time_series_names
base_model.attributes = service_model.attributes
- set_service_model!(ic_template, base_model)
+ set_service_model!(ic_template, get_service_name(service_model), base_model)
end
return ic_template
end
@@ -62,26 +62,36 @@ function _make_init_jump_model(ic_settings::Settings)
end
function build_initial_conditions_model!(model::T) where {T <: OperationModel}
- model.internal.ic_model_container = deepcopy(get_optimization_container(model))
- ic_settings = deepcopy(model.internal.ic_model_container.settings)
+ internal = get_internal(model)
+ IS.Optimization.set_initial_conditions_model_container!(
+ internal,
+ deepcopy(get_optimization_container(model)),
+ )
+ ic_container = IS.Optimization.get_initial_conditions_model_container(internal)
+ ic_settings = deepcopy(get_settings(ic_container))
main_problem_horizon = get_horizon(ic_settings)
# TODO: add an interface to allow user to configure initial_conditions problem
- model.internal.ic_model_container.JuMPmodel = _make_init_jump_model(ic_settings)
+ ic_container.JuMPmodel = _make_init_jump_model(ic_settings)
template = get_initial_conditions_template(model)
- model.internal.ic_model_container.settings = ic_settings
- model.internal.ic_model_container.built_for_recurrent_solves = false
- set_horizon!(ic_settings, min(INITIALIZATION_PROBLEM_HORIZON, main_problem_horizon))
+ ic_container.settings = ic_settings
+ ic_container.built_for_recurrent_solves = false
+ init_horizon = INITIALIZATION_PROBLEM_HORIZON_COUNT * get_resolution(ic_settings)
+ set_horizon!(ic_settings, min(init_horizon, main_problem_horizon))
init_optimization_container!(
- model.internal.ic_model_container,
+ IS.Optimization.get_initial_conditions_model_container(internal),
get_network_model(get_template(model)),
get_system(model),
)
JuMP.set_string_names_on_creation(
- get_jump_model(model.internal.ic_model_container),
+ get_jump_model(IS.Optimization.get_initial_conditions_model_container(internal)),
false,
)
- TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build Initialization $(get_name(model))" begin
- build_impl!(model.internal.ic_model_container, template, get_system(model))
- end
+ TimerOutputs.disable_timer!(BUILD_PROBLEMS_TIMER)
+ build_impl!(
+ model.internal.initial_conditions_model_container,
+ template,
+ get_system(model),
+ )
+ TimerOutputs.enable_timer!(BUILD_PROBLEMS_TIMER)
return
end
diff --git a/src/initial_conditions/update_initial_conditions.jl b/src/initial_conditions/update_initial_conditions.jl
index b0f7aa9568..3a1b6e353e 100644
--- a/src/initial_conditions/update_initial_conditions.jl
+++ b/src/initial_conditions/update_initial_conditions.jl
@@ -1,13 +1,13 @@
function _update_initial_conditions!(
model::OperationModel,
- key::ICKey{T, U},
+ key::InitialConditionKey{T, U},
source, # Store or State are used in simulations by default
) where {T <: InitialConditionType, U <: PSY.Component}
if get_execution_count(model) < 1
return
end
container = get_optimization_container(model)
- model_resolution = get_resolution(model.internal.store_parameters)
+ model_resolution = get_resolution(get_store_params(model))
ini_conditions_vector = get_initial_condition(container, key)
timestamp = get_current_timestamp(model)
previous_values = get_condition.(ini_conditions_vector)
@@ -28,7 +28,7 @@ end
# Note to devs: Implemented this way to avoid ambiguities and future proof custom ic updating
function update_initial_conditions!(
model::DecisionModel,
- key::ICKey{T, U},
+ key::InitialConditionKey{T, U},
source, # Store or State are used in simulations by default
) where {T <: InitialConditionType, U <: PSY.Component}
_update_initial_conditions!(model, key, source)
@@ -37,7 +37,7 @@ end
function update_initial_conditions!(
model::EmulationModel,
- key::ICKey{T, U},
+ key::InitialConditionKey{T, U},
source, # Store or State are used in simulations by default
) where {T <: InitialConditionType, U <: PSY.Component}
_update_initial_conditions!(model, key, source)
diff --git a/src/network_models/area_balance_model.jl b/src/network_models/area_balance_model.jl
index 04d6ffa3e0..eaf9a61978 100644
--- a/src/network_models/area_balance_model.jl
+++ b/src/network_models/area_balance_model.jl
@@ -1,4 +1,30 @@
-function area_balance(
+function add_constraints!(
+ container::OptimizationContainer,
+ ::Type{CopperPlateBalanceConstraint},
+ sys::PSY.System,
+ model::NetworkModel{AreaBalancePowerModel},
+)
+ expressions = get_expression(container, ActivePowerBalance(), PSY.Area)
+ area_names, time_steps = axes(expressions)
+
+ constraints = add_constraints_container!(
+ container,
+ CopperPlateBalanceConstraint(),
+ PSY.Area,
+ area_names,
+ time_steps,
+ )
+
+ for a in area_names, t in time_steps
+ constraints[a, t] =
+ JuMP.@constraint(get_jump_model(container), expressions[a, t] == 0.0)
+ end
+ return
+end
+
+# Unavailable Feature
+#=
+function agc_area_balance(
container::OptimizationContainer,
expression::ExpressionKey,
area_mapping::Dict{String, Array{PSY.ACBus, 1}},
@@ -9,7 +35,7 @@ function area_balance(
constraint = add_constraints_container!(
container,
- AreaDispatchBalanceConstraint(),
+ CopperPlateBalanceConstraint(),
PSY.Area,
keys(area_mapping),
time_steps,
@@ -22,7 +48,7 @@ function area_balance(
JuMP.add_to_expression!(area_net, nodal_net_balance[PSY.get_number(b), t])
end
constraint[k, t] =
- JuMP.@constraint(container.JuMPmodel, area_balance[k, t] == area_net)
+ JuMP.@constraint(get_jump_model(container), area_balance[k, t] == area_net)
end
end
@@ -48,10 +74,11 @@ function area_balance(
for area in keys(area_mapping), t in time_steps
participation_assignment_up[area, t] =
- JuMP.@constraint(container.JuMPmodel, expr_up[area, t] == 0)
+ JuMP.@constraint(get_jump_model(container), expr_up[area, t] == 0)
participation_assignment_dn[area, t] =
- JuMP.@constraint(container.JuMPmodel, expr_dn[area, t] == 0)
+ JuMP.@constraint(get_jump_model(container), expr_dn[area, t] == 0)
end
return
end
+=#
diff --git a/src/network_models/copperplate_model.jl b/src/network_models/copperplate_model.jl
index 1cc0886148..c077d8d45c 100644
--- a/src/network_models/copperplate_model.jl
+++ b/src/network_models/copperplate_model.jl
@@ -19,3 +19,25 @@ function add_constraints!(
return
end
+
+function add_constraints!(
+ container::OptimizationContainer,
+ ::Type{T},
+ sys::U,
+ network_model::NetworkModel{AreaPTDFPowerModel},
+) where {
+ T <: CopperPlateBalanceConstraint,
+ U <: PSY.System,
+}
+ time_steps = get_time_steps(container)
+ expressions = get_expression(container, ActivePowerBalance(), PSY.Area)
+ area_names = PSY.get_name.(get_available_components(network_model, PSY.Area, sys))
+ constraint =
+ add_constraints_container!(container, T(), PSY.Area, area_names, time_steps)
+ jm = get_jump_model(container)
+ for t in time_steps, k in area_names
+ constraint[k, t] = JuMP.@constraint(jm, expressions[k, t] == 0)
+ end
+
+ return
+end
diff --git a/src/network_models/network_constructor.jl b/src/network_models/network_constructor.jl
index 21963fdb09..ad7cc21cbd 100644
--- a/src/network_models/network_constructor.jl
+++ b/src/network_models/network_constructor.jl
@@ -15,7 +15,7 @@ function construct_network!(
sys,
model,
)
- objective_function!(container, PSY.System, model)
+ objective_function!(container, sys, model)
end
add_constraints!(container, CopperPlateBalanceConstraint, sys, model)
@@ -30,22 +30,21 @@ function construct_network!(
model::NetworkModel{AreaBalancePowerModel},
::ProblemTemplate,
)
- area_mapping = PSY.get_aggregation_topology_mapping(PSY.Area, sys)
- branches = get_available_components(model, PSY.Branch, sys)
if get_use_slacks(model)
- throw(
- IS.ConflictingInputsError(
- "Slack Variables are not compatible with AreaBalancePowerModel",
- ),
+ add_variables!(container, SystemBalanceSlackUp, sys, model)
+ add_variables!(container, SystemBalanceSlackDown, sys, model)
+ add_to_expression!(container, ActivePowerBalance, SystemBalanceSlackUp, sys, model)
+ add_to_expression!(
+ container,
+ ActivePowerBalance,
+ SystemBalanceSlackDown,
+ sys,
+ model,
)
+ objective_function!(container, sys, model)
end
- area_balance(
- container,
- ExpressionKey(ActivePowerBalance, PSY.ACBus),
- area_mapping,
- branches,
- )
+ add_constraints!(container, CopperPlateBalanceConstraint, sys, model)
add_constraint_dual!(container, sys, model)
return
end
@@ -53,7 +52,7 @@ end
function construct_network!(
container::OptimizationContainer,
sys::PSY.System,
- model::NetworkModel{PTDFPowerModel},
+ model::NetworkModel{<:AbstractPTDFModel},
::ProblemTemplate,
)
if get_use_slacks(model)
@@ -67,7 +66,7 @@ function construct_network!(
sys,
model,
)
- objective_function!(container, PSY.System, model)
+ objective_function!(container, sys, model)
end
add_constraints!(container, CopperPlateBalanceConstraint, sys, model)
add_constraints!(container, NodalBalanceActiveConstraint, sys, model)
@@ -100,7 +99,7 @@ function construct_network!(
sys,
model,
)
- objective_function!(container, PSY.ACBus, model)
+ objective_function!(container, sys, model)
end
@debug "Building the $T network with instantiate_nip_expr_model method" _group =
@@ -147,7 +146,7 @@ function construct_network!(
sys,
model,
)
- objective_function!(container, PSY.ACBus, model)
+ objective_function!(container, sys, model)
end
@debug "Building the $T network with instantiate_nip_expr_model method" _group =
@@ -207,7 +206,7 @@ function construct_network!(
sys,
model,
)
- objective_function!(container, PSY.ACBus, model)
+ objective_function!(container, sys, model)
end
@debug "Building the $T network with instantiate_bfp_expr_model method" _group =
@@ -272,7 +271,7 @@ function construct_network!(
model,
T,
)
- objective_function!(container, PSY.ACBus, model)
+ objective_function!(container, sys, model)
end
@debug "Building the $T network with instantiate_vip_expr_model method" _group =
diff --git a/src/network_models/network_slack_variables.jl b/src/network_models/network_slack_variables.jl
index 2be536937a..183ba65660 100644
--- a/src/network_models/network_slack_variables.jl
+++ b/src/network_models/network_slack_variables.jl
@@ -1,6 +1,6 @@
#! format: off
-get_variable_multiplier(::SystemBalanceSlackUp, ::Type{<: Union{PSY.ACBus, PSY.System}}, _) = 1.0
-get_variable_multiplier(::SystemBalanceSlackDown, ::Type{<: Union{PSY.ACBus, PSY.System}}, _) = -1.0
+get_variable_multiplier(::SystemBalanceSlackUp, ::Type{<: Union{PSY.ACBus, PSY.Area, PSY.System}}, _) = 1.0
+get_variable_multiplier(::SystemBalanceSlackDown, ::Type{<: Union{PSY.ACBus, PSY.Area, PSY.System}}, _) = -1.0
#! format: on
function add_variables!(
@@ -27,6 +27,31 @@ function add_variables!(
return
end
+function add_variables!(
+ container::OptimizationContainer,
+ ::Type{T},
+ sys::PSY.System,
+ network_model::NetworkModel{U},
+) where {
+ T <: Union{SystemBalanceSlackUp, SystemBalanceSlackDown},
+ U <: Union{AreaBalancePowerModel, AreaPTDFPowerModel},
+}
+ time_steps = get_time_steps(container)
+ areas = get_name.(get_available_components(network_model, PSY.Area, sys))
+ variable =
+ add_variable_container!(container, T(), PSY.Area, areas, time_steps)
+
+ for t in time_steps, area in areas
+ variable[area, t] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "slack_{$(T), $(area), $t}",
+ lower_bound = 0.0
+ )
+ end
+
+ return
+end
+
function add_variables!(
container::OptimizationContainer,
::Type{T},
@@ -95,7 +120,7 @@ end
function objective_function!(
container::OptimizationContainer,
- ::Type{PSY.System},
+ sys::PSY.System,
network_model::NetworkModel{T},
) where {T <: Union{CopperPlatePowerModel, PTDFPowerModel}}
variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.System)
@@ -113,7 +138,25 @@ end
function objective_function!(
container::OptimizationContainer,
- ::Type{PSY.ACBus},
+ sys::PSY.System,
+ network_model::NetworkModel{T},
+) where {T <: Union{AreaBalancePowerModel, AreaPTDFPowerModel}}
+ variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.Area)
+ variable_dn = get_variable(container, SystemBalanceSlackDown(), PSY.Area)
+ areas = PSY.get_name.(get_available_components(network_model, PSY.Area, sys))
+
+ for t in get_time_steps(container), n in areas
+ add_to_objective_invariant_expression!(
+ container,
+ (variable_dn[n, t] + variable_up[n, t]) * BALANCE_SLACK_COST,
+ )
+ end
+ return
+end
+
+function objective_function!(
+ container::OptimizationContainer,
+ sys::PSY.System,
network_model::NetworkModel{T},
) where {T <: PM.AbstractActivePowerModel}
variable_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus)
@@ -131,7 +174,7 @@ end
function objective_function!(
container::OptimizationContainer,
- ::Type{PSY.ACBus},
+ sys::PSY.System,
network_model::NetworkModel{T},
) where {T <: PM.AbstractPowerModel}
variable_p_up = get_variable(container, SystemBalanceSlackUp(), PSY.ACBus, "P")
diff --git a/src/network_models/pm_translator.jl b/src/network_models/pm_translator.jl
index ddb28513d5..58381f732e 100644
--- a/src/network_models/pm_translator.jl
+++ b/src/network_models/pm_translator.jl
@@ -23,11 +23,11 @@ function get_branch_to_pm(
)
PM_branch = Dict{String, Any}(
"br_r" => PSY.get_r(branch),
- "rate_a" => PSY.get_rate(branch),
+ "rate_a" => PSY.get_rating(branch),
"shift" => PSY.get_α(branch),
- "rate_b" => PSY.get_rate(branch),
+ "rate_b" => PSY.get_rating(branch),
"br_x" => PSY.get_x(branch),
- "rate_c" => PSY.get_rate(branch),
+ "rate_c" => PSY.get_rating(branch),
"g_to" => 0.0,
"g_fr" => 0.0,
"b_fr" => PSY.get_primary_shunt(branch) / 2,
@@ -52,11 +52,11 @@ function get_branch_to_pm(
) where {D <: AbstractBranchFormulation}
PM_branch = Dict{String, Any}(
"br_r" => PSY.get_r(branch),
- "rate_a" => PSY.get_rate(branch),
+ "rate_a" => PSY.get_rating(branch),
"shift" => PSY.get_α(branch),
- "rate_b" => PSY.get_rate(branch),
+ "rate_b" => PSY.get_rating(branch),
"br_x" => PSY.get_x(branch),
- "rate_c" => PSY.get_rate(branch),
+ "rate_c" => PSY.get_rating(branch),
"g_to" => 0.0,
"g_fr" => 0.0,
"b_fr" => PSY.get_primary_shunt(branch) / 2,
@@ -107,11 +107,11 @@ function get_branch_to_pm(
)
PM_branch = Dict{String, Any}(
"br_r" => PSY.get_r(branch),
- "rate_a" => PSY.get_rate(branch),
+ "rate_a" => PSY.get_rating(branch),
"shift" => 0.0,
- "rate_b" => PSY.get_rate(branch),
+ "rate_b" => PSY.get_rating(branch),
"br_x" => PSY.get_x(branch),
- "rate_c" => PSY.get_rate(branch),
+ "rate_c" => PSY.get_rating(branch),
"g_to" => 0.0,
"g_fr" => 0.0,
"b_fr" => PSY.get_primary_shunt(branch) / 2,
@@ -162,11 +162,11 @@ function get_branch_to_pm(
)
PM_branch = Dict{String, Any}(
"br_r" => PSY.get_r(branch),
- "rate_a" => PSY.get_rate(branch),
+ "rate_a" => PSY.get_rating(branch),
"shift" => 0.0,
- "rate_b" => PSY.get_rate(branch),
+ "rate_b" => PSY.get_rating(branch),
"br_x" => PSY.get_x(branch),
- "rate_c" => PSY.get_rate(branch),
+ "rate_c" => PSY.get_rating(branch),
"g_to" => 0.0,
"g_fr" => 0.0,
"b_fr" => PSY.get_primary_shunt(branch) / 2,
@@ -217,11 +217,11 @@ function get_branch_to_pm(
)
PM_branch = Dict{String, Any}(
"br_r" => PSY.get_r(branch),
- "rate_a" => PSY.get_rate(branch),
+ "rate_a" => PSY.get_rating(branch),
"shift" => 0.0,
- "rate_b" => PSY.get_rate(branch),
+ "rate_b" => PSY.get_rating(branch),
"br_x" => PSY.get_x(branch),
- "rate_c" => PSY.get_rate(branch),
+ "rate_c" => PSY.get_rating(branch),
"g_to" => 0.0,
"g_fr" => 0.0,
"b_fr" => PSY.get_b(branch).from,
diff --git a/src/operation/abstract_model_store.jl b/src/operation/abstract_model_store.jl
deleted file mode 100644
index ff7c57f684..0000000000
--- a/src/operation/abstract_model_store.jl
+++ /dev/null
@@ -1,91 +0,0 @@
-abstract type AbstractModelStore end
-
-# Required methods for subtypes:
-# = initialize_storage!
-# - write_result!
-# - read_results
-# - write_optimizer_stats!
-# - read_optimizer_stats
-#
-# Each subtype must have a field for each instance of STORE_CONTAINERS.
-
-function Base.empty!(store::AbstractModelStore)
- stype = typeof(store)
- for (name, type) in zip(fieldnames(stype), fieldtypes(stype))
- val = get_data_field(store, name)
- try
- empty!(val)
- catch
- @error "Base.empty! must be customized for type $stype or skipped"
- rethrow()
- end
- end
-end
-
-get_data_field(store::AbstractModelStore, type) = getfield(store, type)
-
-function Base.isempty(store::AbstractModelStore)
- stype = typeof(store)
- for (name, type) in zip(fieldnames(stype), fieldtypes(stype))
- val = get_data_field(store, name)
- try
- !isempty(val) && return false
- catch
- @error "Base.isempty must be customized for type $stype or skipped"
- rethrow()
- end
- end
-
- return true
-end
-
-function list_fields(store::AbstractModelStore, container_type::Symbol)
- return keys(get_data_field(store, container_type))
-end
-
-function write_result!(store::AbstractModelStore, key, index, array)
- field = get_store_container_type(key)
- return write_result!(store, field, key, index, array)
-end
-
-function read_results(store::AbstractModelStore, key; index = nothing)
- field = get_store_container_type(key)
- return read_results(store, field, key; index = index)
-end
-
-function list_keys(store::AbstractModelStore, container_type)
- container = get_data_field(store, container_type)
- return collect(keys(container))
-end
-
-function get_variable_value(
- store::AbstractModelStore,
- ::T,
- ::Type{U},
-) where {T <: VariableType, U <: Union{PSY.Component, PSY.System}}
- return get_data_field(store, :variables)[VariableKey(T, U)]
-end
-
-function get_aux_variable_value(
- store::AbstractModelStore,
- ::T,
- ::Type{U},
-) where {T <: AuxVariableType, U <: Union{PSY.Component, PSY.System}}
- return get_data_field(store, :aux_variables)[AuxVarKey(T, U)]
-end
-
-function get_dual_value(
- store::AbstractModelStore,
- ::T,
- ::Type{U},
-) where {T <: ConstraintType, U <: Union{PSY.Component, PSY.System}}
- return get_data_field(store, :duals)[ConstraintKey(T, U)]
-end
-
-function get_parameter_value(
- store::AbstractModelStore,
- ::T,
- ::Type{U},
-) where {T <: ParameterType, U <: Union{PSY.Component, PSY.System}}
- return get_data_field(store, :parameters)[ParameterKey(T, U)]
-end
diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl
index 443746d09e..b0718de645 100644
--- a/src/operation/decision_model.jl
+++ b/src/operation/decision_model.jl
@@ -13,7 +13,8 @@ mutable struct DecisionModel{M <: DecisionProblem} <: OperationModel
name::Symbol
template::AbstractProblemTemplate
sys::PSY.System
- internal::Union{Nothing, ModelInternal}
+ internal::Union{Nothing, IS.Optimization.ModelInternal}
+ simulation_info::SimulationInfo
store::DecisionModelStore
ext::Dict{String, Any}
end
@@ -36,7 +37,8 @@ Build the optimization problem of type M with the specific system and template.
- `name = nothing`: name of model, string or symbol; defaults to the type of template converted to a symbol.
- `optimizer::Union{Nothing,MOI.OptimizerWithAttributes} = nothing` : The optimizer does
not get serialized. Callers should pass whatever they passed to the original problem.
- - `horizon::Int = UNSET_HORIZON`: Manually specify the length of the forecast Horizon
+ - `horizon::Dates.Period = UNSET_HORIZON`: Manually specify the length of the forecast Horizon
+ - `resolution::Dates.Period = UNSET_RESOLUTION`: Manually specify the model's resolution
- `warm_start::Bool = true`: True will use the current operation point in the system to initialize variable values. False initializes all variables to zero. Default is true
- `system_to_file::Bool = true:`: True to create a copy of the system used in the model.
- `initialize_model::Bool = true`: Option to decide to initialize the model or not.
@@ -72,19 +74,23 @@ function DecisionModel{M}(
elseif name isa String
name = Symbol(name)
end
- internal = ModelInternal(
+ internal = IS.Optimization.ModelInternal(
OptimizationContainer(sys, settings, jump_model, PSY.Deterministic),
)
+
template_ = deepcopy(template)
finalize_template!(template_, sys)
- return DecisionModel{M}(
+ model = DecisionModel{M}(
name,
template_,
sys,
internal,
+ SimulationInfo(),
DecisionModelStore(),
Dict{String, Any}(),
)
+ PSI.validate_time_series!(model)
+ return model
end
function DecisionModel{M}(
@@ -94,6 +100,7 @@ function DecisionModel{M}(
name = nothing,
optimizer = nothing,
horizon = UNSET_HORIZON,
+ resolution = UNSET_RESOLUTION,
warm_start = true,
system_to_file = true,
initialize_model = true,
@@ -114,6 +121,7 @@ function DecisionModel{M}(
settings = Settings(
sys;
horizon = horizon,
+ resolution = resolution,
initial_time = initial_time,
optimizer = optimizer,
time_series_cache_size = time_series_cache_size,
@@ -236,13 +244,12 @@ end
get_problem_type(::DecisionModel{M}) where {M <: DecisionProblem} = M
validate_template(::DecisionModel{<:DecisionProblem}) = nothing
-validate_time_series(::DecisionModel{<:DecisionProblem}) = nothing
# Probably could be more efficient by storing the info in the internal
function get_current_time(model::DecisionModel)
- execution_count = get_internal(model).execution_count
+ execution_count = get_execution_count(model)
initial_time = get_initial_time(model)
- interval = get_interval(model.internal.store_parameters)
+ interval = get_interval(model)
return initial_time + interval * execution_count
end
@@ -251,10 +258,10 @@ function init_model_store_params!(model::DecisionModel)
horizon = get_horizon(model)
system = get_system(model)
interval = PSY.get_forecast_interval(system)
- resolution = PSY.get_time_series_resolution(system)
+ resolution = get_resolution(model)
base_power = PSY.get_base_power(system)
sys_uuid = IS.get_uuid(system)
- model.internal.store_parameters = ModelStoreParams(
+ store_params = ModelStoreParams(
num_executions,
horizon,
iszero(interval) ? resolution : interval,
@@ -263,11 +270,37 @@ function init_model_store_params!(model::DecisionModel)
sys_uuid,
get_metadata(get_optimization_container(model)),
)
+ IS.Optimization.set_store_params!(get_internal(model), store_params)
return
end
-function validate_time_series(model::DecisionModel{<:DefaultDecisionProblem})
+function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem})
sys = get_system(model)
+ settings = get_settings(model)
+ available_resolutions = PSY.get_time_series_resolutions(sys)
+
+ if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1
+ throw(
+ IS.ConflictingInputsError(
+ "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1
+ if get_resolution(settings) ∉ available_resolutions
+ throw(
+ IS.ConflictingInputsError(
+ "Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ end
+ else
+ set_resolution!(settings, first(available_resolutions))
+ end
+
+ if get_horizon(settings) == UNSET_HORIZON
+ set_horizon!(settings, PSY.get_forecast_horizon(sys))
+ end
+
counts = PSY.get_time_series_counts(sys)
if counts.forecast_count < 1
error(
@@ -280,9 +313,9 @@ end
function build_pre_step!(model::DecisionModel{<:DecisionProblem})
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin
validate_template(model)
- validate_time_series(model)
if !isempty(model)
- @info "OptimizationProblem status not BuildStatus.EMPTY. Resetting"
+ @info "OptimizationProblem status not ModelBuildStatus.EMPTY. Resetting"
+
reset!(model)
end
# Initial time are set here because the information is specified in the
@@ -295,7 +328,7 @@ function build_pre_step!(model::DecisionModel{<:DecisionProblem})
)
@info "Initializing ModelStoreParams"
init_model_store_params!(model)
- set_status!(model, BuildStatus.IN_PROGRESS)
+ set_status!(model, ModelBuildStatus.IN_PROGRESS)
end
return
end
@@ -342,17 +375,17 @@ function build!(
file_mode = "w"
add_recorders!(model, recorders)
register_recorders!(model, file_mode)
- logger = configure_logging(model.internal, file_mode)
+ logger = IS.configure_logging(get_internal(model), PROBLEM_LOG_FILENAME, file_mode)
try
Logging.with_logger(logger) do
try
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(model))" begin
build_impl!(model)
end
- set_status!(model, BuildStatus.BUILT)
+ set_status!(model, ModelBuildStatus.BUILT)
@info "\n$(BUILD_PROBLEMS_TIMER)\n"
catch e
- set_status!(model, BuildStatus.FAILED)
+ set_status!(model, ModelBuildStatus.FAILED)
bt = catch_backtrace()
@error "DecisionModel Build Failed" exception = e, bt
end
@@ -378,17 +411,22 @@ function reset!(model::DecisionModel{<:DefaultDecisionProblem})
if was_built_for_recurrent_solves
set_execution_count!(model, 0)
end
- model.internal.container = OptimizationContainer(
- get_system(model),
- get_settings(model),
- nothing,
- PSY.Deterministic,
+ IS.Optimization.set_container!(
+ get_internal(model),
+ OptimizationContainer(
+ get_system(model),
+ get_settings(model),
+ nothing,
+ PSY.Deterministic,
+ ),
)
- model.internal.container.built_for_recurrent_solves = was_built_for_recurrent_solves
- model.internal.ic_model_container = nothing
+ get_optimization_container(model).built_for_recurrent_solves =
+ was_built_for_recurrent_solves
+ internal = get_internal(model)
+ IS.Optimization.set_initial_conditions_model_container!(internal, nothing)
empty_time_series_cache!(model)
empty!(get_store(model))
- set_status!(model, BuildStatus.EMPTY)
+ set_status!(model, ModelBuildStatus.EMPTY)
return
end
@@ -402,7 +440,7 @@ keyword arguments to that function.
# Arguments
- `model::OperationModel = model`: operation model
- - `export_problem_results::Bool = false`: If true, export ProblemResults DataFrames to CSV files. Reduces solution times during simulation.
+ - `export_problem_results::Bool = false`: If true, export OptimizationProblemResults DataFrames to CSV files. Reduces solution times during simulation.
- `console_level = Logging.Error`:
- `file_level = Logging.Info`:
- `disable_timer_outputs = false` : Enable/Disable timing outputs
@@ -437,7 +475,11 @@ function solve!(
disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER)
file_mode = "a"
register_recorders!(model, file_mode)
- logger = configure_logging(model.internal, file_mode)
+ logger = IS.Optimization.configure_logging(
+ get_internal(model),
+ PROBLEM_LOG_FILENAME,
+ file_mode,
+ )
optimizer = get(kwargs, :optimizer, nothing)
try
Logging.with_logger(logger) do
@@ -445,7 +487,7 @@ function solve!(
initialize_storage!(
get_store(model),
get_optimization_container(model),
- model.internal.store_parameters,
+ get_store_params(model),
)
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Solve" begin
_pre_solve_model_checks(model, optimizer)
@@ -466,7 +508,7 @@ function solve!(
end
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Results processing" begin
# TODO: This could be more complicated than it needs to be
- results = ProblemResults(model)
+ results = OptimizationProblemResults(model)
serialize_results(results, get_output_dir(model))
export_problem_results && export_results(results)
end
@@ -509,7 +551,7 @@ function solve!(
# other logic used when solving the models separate from a simulation
solve_impl!(model)
IS.@assert_op get_current_time(model) == start_time
- if get_run_status(model) == RunStatus.SUCCESSFUL
+ if get_run_status(model) == RunStatus.SUCCESSFULLY_FINALIZED
write_results!(store, model, start_time, start_time; exports = exports)
write_optimizer_stats!(store, model, start_time)
advance_execution_count!(model)
@@ -528,7 +570,7 @@ function update_parameters!(
if !is_synchronized(model)
update_objective_function!(get_optimization_container(model))
obj_func = get_objective_expression(get_optimization_container(model))
- set_synchronized_status(obj_func, true)
+ set_synchronized_status!(obj_func, true)
end
return
end
diff --git a/src/operation/decision_model_store.jl b/src/operation/decision_model_store.jl
index 686a5b9586..ca1d1b0f37 100644
--- a/src/operation/decision_model_store.jl
+++ b/src/operation/decision_model_store.jl
@@ -1,12 +1,18 @@
"""
Stores results data for one DecisionModel
"""
-mutable struct DecisionModelStore <: AbstractModelStore
+mutable struct DecisionModelStore <: IS.Optimization.AbstractModelStore
# All DenseAxisArrays have axes (column names, row indexes)
duals::Dict{ConstraintKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}}
- parameters::Dict{ParameterKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}}
+ parameters::Dict{
+ ParameterKey,
+ OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}},
+ }
variables::Dict{VariableKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}}
- aux_variables::Dict{AuxVarKey, OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}}}
+ aux_variables::Dict{
+ AuxVarKey,
+ OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}},
+ }
expressions::Dict{
ExpressionKey,
OrderedDict{Dates.DateTime, DenseAxisArray{Float64, 2}},
@@ -27,7 +33,7 @@ end
function initialize_storage!(
store::DecisionModelStore,
- container::AbstractModelContainer,
+ container::IS.Optimization.AbstractOptimizationContainer,
params::ModelStoreParams,
)
num_of_executions = get_num_executions(params)
@@ -91,8 +97,7 @@ function write_result!(
columns = string.(columns)
end
container = getfield(store, get_store_container_type(key))
- container[key][index] =
- DenseAxisArray(reshape(array.data, 1, length(columns)), ["1"], columns)
+ container[key][index] = DenseAxisArray(to_matrix(array), ["1"], columns)
return
end
@@ -125,7 +130,7 @@ function write_optimizer_stats!(
end
function read_optimizer_stats(store::DecisionModelStore)
- stats = [to_namedtuple(x) for x in values(store.optimizer_stats)]
+ stats = [IS.to_namedtuple(x) for x in values(store.optimizer_stats)]
df = DataFrames.DataFrame(stats)
DataFrames.insertcols!(df, 1, :DateTime => keys(store.optimizer_stats))
return df
diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl
index b47b0ff13f..c635cf35b6 100644
--- a/src/operation/emulation_model.jl
+++ b/src/operation/emulation_model.jl
@@ -54,7 +54,8 @@ mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel
name::Symbol
template::AbstractProblemTemplate
sys::PSY.System
- internal::ModelInternal
+ internal::IS.Optimization.ModelInternal
+ simulation_info::SimulationInfo
store::EmulationModelStore # might be extended to other stores for simulation
ext::Dict{String, Any}
@@ -71,10 +72,18 @@ mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel
name = Symbol(name)
end
finalize_template!(template, sys)
- internal = ModelInternal(
+ internal = IS.Optimization.ModelInternal(
OptimizationContainer(sys, settings, jump_model, PSY.SingleTimeSeries),
)
- new{M}(name, template, sys, internal, EmulationModelStore(), Dict{String, Any}())
+ new{M}(
+ name,
+ template,
+ sys,
+ internal,
+ SimulationInfo(),
+ EmulationModelStore(),
+ Dict{String, Any}(),
+ )
end
end
@@ -82,6 +91,7 @@ function EmulationModel{M}(
template::AbstractProblemTemplate,
sys::PSY.System,
jump_model::Union{Nothing, JuMP.Model} = nothing;
+ resolution = UNSET_RESOLUTION,
name = nothing,
optimizer = nothing,
warm_start = true,
@@ -120,9 +130,12 @@ function EmulationModel{M}(
check_numerical_bounds = check_numerical_bounds,
store_variable_names = store_variable_names,
rebuild_model = rebuild_model,
- horizon = 1,
+ horizon = resolution,
+ resolution = resolution,
)
- return EmulationModel{M}(template, sys, settings, jump_model; name = name)
+ model = EmulationModel{M}(template, sys, settings, jump_model; name = name)
+ validate_time_series!(model)
+ return model
end
"""
@@ -218,50 +231,78 @@ end
get_problem_type(::EmulationModel{M}) where {M <: EmulationProblem} = M
validate_template(::EmulationModel{<:EmulationProblem}) = nothing
-validate_time_series(::EmulationModel{<:EmulationProblem}) = nothing
+
+function validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem})
+ sys = get_system(model)
+ settings = get_settings(model)
+ available_resolutions = PSY.get_time_series_resolutions(sys)
+
+ if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1
+ throw(
+ IS.ConflictingInputsError(
+ "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1
+ if get_resolution(settings) ∉ available_resolutions
+ throw(
+ IS.ConflictingInputsError(
+ "Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)",
+ ),
+ )
+ end
+ else
+ set_resolution!(settings, first(available_resolutions))
+ end
+
+ if get_horizon(settings) == UNSET_HORIZON
+ # Emulation Models Only solve one "step" so Horizon and Resolution must match
+ set_horizon!(settings, get_resolution(settings))
+ end
+
+ counts = PSY.get_time_series_counts(sys)
+ if counts.static_time_series_count < 1
+ error(
+ "The system does not contain Static Time Series data. A EmulationModel can't be built.",
+ )
+ end
+ return
+end
function get_current_time(model::EmulationModel)
- execution_count = get_internal(model).execution_count
+ execution_count = get_execution_count(model)
initial_time = get_initial_time(model)
- resolution = get_resolution(model.internal.store_parameters)
+ resolution = get_resolution(model)
return initial_time + resolution * execution_count
end
function init_model_store_params!(model::EmulationModel)
num_executions = get_executions(model)
system = get_system(model)
- interval = resolution = PSY.get_time_series_resolution(system)
+ settings = get_settings(model)
+ horizon = interval = resolution = get_resolution(settings)
base_power = PSY.get_base_power(system)
sys_uuid = IS.get_uuid(system)
- model.internal.store_parameters = ModelStoreParams(
- num_executions,
- 1,
- interval,
- resolution,
- base_power,
- sys_uuid,
- get_metadata(get_optimization_container(model)),
+ IS.Optimization.set_store_params!(
+ get_internal(model),
+ ModelStoreParams(
+ num_executions,
+ horizon,
+ interval,
+ resolution,
+ base_power,
+ sys_uuid,
+ get_metadata(get_optimization_container(model)),
+ ),
)
return
end
-function validate_time_series(model::EmulationModel{<:DefaultEmulationProblem})
- sys = get_system(model)
- counts = PSY.get_time_series_counts(sys)
- if counts.static_time_series_count < 1
- error(
- "The system does not contain Static TimeSeries data. An Emulation model can't be formulated.",
- )
- end
- return
-end
-
function build_pre_step!(model::EmulationModel)
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin
validate_template(model)
- validate_time_series(model)
if !isempty(model)
- @info "EmulationProblem status not BuildStatus.EMPTY. Resetting"
+ @info "EmulationProblem status not ModelBuildStatus.EMPTY. Resetting"
reset!(model)
end
container = get_optimization_container(model)
@@ -276,7 +317,7 @@ function build_pre_step!(model::EmulationModel)
@info "Initializing ModelStoreParams"
init_model_store_params!(model)
- set_status!(model, BuildStatus.IN_PROGRESS)
+ set_status!(model, ModelBuildStatus.IN_PROGRESS)
end
return
end
@@ -313,7 +354,11 @@ function build!(
file_mode = "w"
add_recorders!(model, recorders)
register_recorders!(model, file_mode)
- logger = configure_logging(model.internal, file_mode)
+ logger = IS.Optimization.configure_logging(
+ get_internal(model),
+ PROBLEM_LOG_FILENAME,
+ file_mode,
+ )
try
Logging.with_logger(logger) do
try
@@ -321,10 +366,10 @@ function build!(
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(model))" begin
build_impl!(model)
end
- set_status!(model, BuildStatus.BUILT)
+ set_status!(model, ModelBuildStatus.BUILT)
@info "\n$(BUILD_PROBLEMS_TIMER)\n"
catch e
- set_status!(model, BuildStatus.FAILED)
+ set_status!(model, ModelBuildStatus.FAILED)
bt = catch_backtrace()
@error "EmulationModel Build Failed" exception = e, bt
end
@@ -350,16 +395,19 @@ function reset!(model::EmulationModel{<:EmulationProblem})
if built_for_recurrent_solves(model)
set_execution_count!(model, 0)
end
- model.internal.container = OptimizationContainer(
- get_system(model),
- get_settings(model),
- nothing,
- PSY.SingleTimeSeries,
+ IS.Optimization.set_container!(
+ get_internal(model),
+ OptimizationContainer(
+ get_system(model),
+ get_settings(model),
+ nothing,
+ PSY.SingleTimeSeries,
+ ),
)
- model.internal.ic_model_container = nothing
+ IS.Optimization.set_initial_conditions_model_container!(get_internal(model), nothing)
empty_time_series_cache!(model)
empty!(get_store(model))
- set_status!(model, BuildStatus.EMPTY)
+ set_status!(model, ModelBuildStatus.EMPTY)
return
end
@@ -376,7 +424,7 @@ function update_parameters!(model::EmulationModel, data::DatasetContainer{InMemo
if !is_synchronized(model)
update_objective_function!(get_optimization_container(model))
obj_func = get_objective_expression(get_optimization_container(model))
- set_synchronized_status(obj_func, true)
+ set_synchronized_status!(obj_func, true)
end
return
end
@@ -419,13 +467,14 @@ function run_impl!(
)
_pre_solve_model_checks(model, optimizer)
internal = get_internal(model)
+ executions = IS.Optimization.get_executions(internal)
# Temporary check. Needs better way to manage re-runs of the same model
if internal.execution_count > 0
error("Call build! again")
end
- prog_bar = ProgressMeter.Progress(internal.executions; enabled = enable_progress_bar)
+ prog_bar = ProgressMeter.Progress(executions; enabled = enable_progress_bar)
initial_time = get_initial_time(model)
- for execution in 1:(internal.executions)
+ for execution in 1:executions
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Run execution" begin
solve_impl!(model)
current_time = initial_time + (execution - 1) * PSI.get_resolution(model)
@@ -455,7 +504,7 @@ keyword arguments to that function.
- `model::EmulationModel = model`: Emulation model
- `optimizer::MOI.OptimizerWithAttributes`: The optimizer that is used to solve the model
- `executions::Int`: Number of executions for the emulator run
- - `export_problem_results::Bool`: If true, export ProblemResults DataFrames to CSV files.
+ - `export_problem_results::Bool`: If true, export OptimizationProblemResults DataFrames to CSV files.
- `output_dir::String`: Required if the model is not already built, otherwise ignored
- `enable_progress_bar::Bool`: Enables/Disable progress bar printing
- `serialize::Bool`: If true, serialize the model to a file to allow re-execution later.
@@ -489,18 +538,22 @@ function run!(
disable_timer_outputs && TimerOutputs.disable_timer!(RUN_OPERATION_MODEL_TIMER)
file_mode = "a"
register_recorders!(model, file_mode)
- logger = configure_logging(model.internal, file_mode)
+ logger = IS.Optimization.configure_logging(
+ get_internal(model),
+ PROBLEM_LOG_FILENAME,
+ file_mode,
+ )
try
Logging.with_logger(logger) do
try
initialize_storage!(
get_store(model),
get_optimization_container(model),
- model.internal.store_parameters,
+ get_store_params(model),
)
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Run" begin
run_impl!(model; kwargs...)
- set_run_status!(model, RunStatus.SUCCESSFUL)
+ set_run_status!(model, RunStatus.SUCCESSFULLY_FINALIZED)
end
if serialize
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Serialize" begin
@@ -510,7 +563,7 @@ function run!(
end
end
TimerOutputs.@timeit RUN_OPERATION_MODEL_TIMER "Results processing" begin
- results = ProblemResults(model)
+ results = OptimizationProblemResults(model)
serialize_results(results, get_output_dir(model))
export_problem_results && export_results(results)
end
@@ -549,7 +602,7 @@ function solve!(
# other logic used when solving the models separate from a simulation
solve_impl!(model)
@assert get_current_time(model) == start_time
- if get_run_status(model) == RunStatus.SUCCESSFUL
+ if get_run_status(model) == RunStatus.SUCCESSFULLY_FINALIZED
advance_execution_count!(model)
write_results!(
store,
diff --git a/src/operation/emulation_model_store.jl b/src/operation/emulation_model_store.jl
index deca8dbca6..f92c947d21 100644
--- a/src/operation/emulation_model_store.jl
+++ b/src/operation/emulation_model_store.jl
@@ -1,7 +1,7 @@
"""
Stores results data for one EmulationModel
"""
-mutable struct EmulationModelStore <: AbstractModelStore
+mutable struct EmulationModelStore <: IS.Optimization.AbstractModelStore
data_container::DatasetContainer{InMemoryDataset}
optimizer_stats::OrderedDict{Int, OptimizerStats}
end
@@ -165,7 +165,9 @@ function write_optimizer_stats!(
end
function read_optimizer_stats(store::EmulationModelStore)
- return DataFrames.DataFrame([to_namedtuple(x) for x in values(store.optimizer_stats)])
+ return DataFrames.DataFrame([
+ IS.to_namedtuple(x) for x in values(store.optimizer_stats)
+ ])
end
function get_last_recorded_row(x::EmulationModelStore, key::OptimizationContainerKey)
diff --git a/src/operation/initial_conditions_update_in_memory_store.jl b/src/operation/initial_conditions_update_in_memory_store.jl
index b17a6aa304..7c7793050b 100644
--- a/src/operation/initial_conditions_update_in_memory_store.jl
+++ b/src/operation/initial_conditions_update_in_memory_store.jl
@@ -9,7 +9,7 @@ function update_initial_conditions!(
T <: InitialCondition{InitialTimeDurationOn, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_aux_variable_value(store, TimeDurationOn(), get_component_type(ic))
+ var_val = get_value(store, TimeDurationOn(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -23,7 +23,7 @@ function update_initial_conditions!(
T <: InitialCondition{InitialTimeDurationOff, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_aux_variable_value(store, TimeDurationOff(), get_component_type(ic))
+ var_val = get_value(store, TimeDurationOff(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -37,7 +37,7 @@ function update_initial_conditions!(
T <: InitialCondition{DevicePower, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_variable_value(store, ActivePowerVariable(), get_component_type(ic))
+ var_val = get_value(store, ActivePowerVariable(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -51,7 +51,7 @@ function update_initial_conditions!(
T <: InitialCondition{DeviceStatus, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_variable_value(store, OnVariable(), get_component_type(ic))
+ var_val = get_value(store, OnVariable(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -66,7 +66,7 @@ function update_initial_conditions!(
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
var_val =
- get_variable_value(store, PowerAboveMinimumVariable(), get_component_type(ic))
+ get_value(store, PowerAboveMinimumVariable(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -80,7 +80,7 @@ function update_initial_conditions!(
T <: InitialCondition{AreaControlError, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_variable_value(store, AreaMismatchVariable(), get_component_type(ic))
+ var_val = get_value(store, AreaMismatchVariable(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
@@ -94,7 +94,7 @@ function update_initial_conditions!(
T <: InitialCondition{InitialEnergyLevel, S},
} where {S <: Union{Float64, JuMP.VariableRef}}
for ic in ics
- var_val = get_variable_value(store, EnergyVariable(), get_component_type(ic))
+ var_val = get_value(store, EnergyVariable(), get_component_type(ic))
set_ic_quantity!(ic, get_last_recorded_value(var_val)[get_component_name(ic)])
end
return
diff --git a/src/operation/model_internal.jl b/src/operation/model_internal.jl
deleted file mode 100644
index 4753e6f648..0000000000
--- a/src/operation/model_internal.jl
+++ /dev/null
@@ -1,76 +0,0 @@
-struct TimeSeriesCacheKey
- component_uuid::Base.UUID
- time_series_type::Type{<:IS.TimeSeriesData}
- name::String
- multiplier_id::Int
-end
-
-# TODO: Marge all structs (ModelInternal, ModelStoreParams and SimulationInfo) to a single Internal Struct
-
-mutable struct SimulationInfo
- number::Int
- sequence_uuid::Base.UUID
-end
-
-mutable struct ModelInternal{T <: AbstractModelContainer}
- container::T
- ic_model_container::Union{Nothing, T}
- status::BuildStatus
- run_status::RunStatus
- base_conversion::Bool
- executions::Int
- execution_count::Int
- output_dir::Union{Nothing, String}
- simulation_info::Union{Nothing, SimulationInfo}
- time_series_cache::Dict{TimeSeriesCacheKey, <:IS.TimeSeriesCache}
- recorders::Vector{Symbol}
- console_level::Base.CoreLogging.LogLevel
- file_level::Base.CoreLogging.LogLevel
- store_parameters::Union{Nothing, ModelStoreParams}
- ext::Dict{String, Any}
-end
-
-function ModelInternal(
- container::T;
- ext = Dict{String, Any}(),
- recorders = [],
-) where {T <: AbstractModelContainer}
- return ModelInternal{T}(
- container,
- nothing,
- BuildStatus.EMPTY,
- RunStatus.READY,
- true,
- 1, #Default executions is 1. The model will be run at least once
- 0,
- nothing,
- nothing,
- Dict{TimeSeriesCacheKey, IS.TimeSeriesCache}(),
- recorders,
- Logging.Warn,
- Logging.Info,
- nothing,
- ext,
- )
-end
-
-function add_recorder!(internal::ModelInternal, recorder::Symbol)
- push!(internal.recorders, recorder)
- return
-end
-
-get_recorders(internal::ModelInternal) = internal.recorders
-
-function configure_logging(internal::ModelInternal, file_mode)
- return IS.configure_logging(;
- console = true,
- console_stream = stderr,
- console_level = internal.console_level,
- file = true,
- filename = joinpath(internal.output_dir, PROBLEM_LOG_FILENAME),
- file_level = internal.file_level,
- file_mode = file_mode,
- tracker = nothing,
- set_global = false,
- )
-end
diff --git a/src/operation/model_store_params.jl b/src/operation/model_store_params.jl
deleted file mode 100644
index 1e60ad5067..0000000000
--- a/src/operation/model_store_params.jl
+++ /dev/null
@@ -1,56 +0,0 @@
-struct SimulationModelStoreRequirements
- duals::Dict{ConstraintKey, Dict{String, Any}}
- parameters::Dict{ParameterKey, Dict{String, Any}}
- variables::Dict{VariableKey, Dict{String, Any}}
- aux_variables::Dict{AuxVarKey, Dict{String, Any}}
- expressions::Dict{ExpressionKey, Dict{String, Any}}
-end
-
-function SimulationModelStoreRequirements()
- return SimulationModelStoreRequirements(
- Dict{ConstraintKey, Dict{String, Any}}(),
- Dict{ParameterKey, Dict{String, Any}}(),
- Dict{VariableKey, Dict{String, Any}}(),
- Dict{AuxVarKey, Dict{String, Any}}(),
- Dict{ExpressionKey, Dict{String, Any}}(),
- )
-end
-
-struct ModelStoreParams
- num_executions::Int
- horizon::Int
- interval::Dates.Millisecond
- resolution::Dates.Millisecond
- base_power::Float64
- system_uuid::Base.UUID
- container_metadata::OptimizationContainerMetadata
-
- function ModelStoreParams(
- num_executions,
- horizon,
- interval,
- resolution,
- base_power,
- system_uuid,
- container_metadata = OptimizationContainerMetadata(),
- )
- new(
- num_executions,
- horizon,
- Dates.Millisecond(interval),
- Dates.Millisecond(resolution),
- base_power,
- system_uuid,
- container_metadata,
- )
- end
-end
-
-get_num_executions(params::ModelStoreParams) = params.num_executions
-get_horizon(params::ModelStoreParams) = params.horizon
-get_interval(params::ModelStoreParams) = params.interval
-get_resolution(params::ModelStoreParams) = params.resolution
-get_base_power(params::ModelStoreParams) = params.base_power
-get_system_uuid(params::ModelStoreParams) = params.system_uuid
-deserialize_key(params::ModelStoreParams, name) =
- deserialize_key(params.container_metadata, name)
diff --git a/src/operation/operation_model_interface.jl b/src/operation/operation_model_interface.jl
index 72b682cf5e..1eb0e8ecc3 100644
--- a/src/operation/operation_model_interface.jl
+++ b/src/operation/operation_model_interface.jl
@@ -1,58 +1,79 @@
# Default implementations of getter/setter functions for OperationModel.
-is_built(model::OperationModel) = model.internal.status == BuildStatus.BUILT
-isempty(model::OperationModel) = model.internal.status == BuildStatus.EMPTY
+is_built(model::OperationModel) =
+ IS.Optimization.get_status(get_internal(model)) == ModelBuildStatus.BUILT
+isempty(model::OperationModel) =
+ IS.Optimization.get_status(get_internal(model)) == ModelBuildStatus.EMPTY
warm_start_enabled(model::OperationModel) =
get_warm_start(get_optimization_container(model).settings)
built_for_recurrent_solves(model::OperationModel) =
get_optimization_container(model).built_for_recurrent_solves
-get_constraints(model::OperationModel) = get_internal(model).container.constraints
-get_execution_count(model::OperationModel) = get_internal(model).execution_count
-get_executions(model::OperationModel) = get_internal(model).executions
+get_constraints(model::OperationModel) =
+ IS.Optimization.get_constraints(get_internal(model))
+get_execution_count(model::OperationModel) =
+ IS.Optimization.get_execution_count(get_internal(model))
+get_executions(model::OperationModel) = IS.Optimization.get_executions(get_internal(model))
get_initial_time(model::OperationModel) = get_initial_time(get_settings(model))
get_internal(model::OperationModel) = model.internal
-get_jump_model(model::OperationModel) = get_jump_model(get_internal(model).container)
+
+function get_jump_model(model::OperationModel)
+ return get_jump_model(IS.Optimization.get_container(get_internal(model)))
+end
+
get_name(model::OperationModel) = model.name
get_store(model::OperationModel) = model.store
is_synchronized(model::OperationModel) = is_synchronized(get_optimization_container(model))
function get_rebuild_model(model::OperationModel)
- sim_info = get_internal(model).simulation_info
+ sim_info = model.simulation_info
if sim_info === nothing
error("Model not part of a simulation")
end
return get_rebuild_model(get_optimization_container(model).settings)
end
-get_optimization_container(model::OperationModel) = get_internal(model).container
+function get_optimization_container(model::OperationModel)
+ return IS.Optimization.get_optimization_container(get_internal(model))
+end
+
function get_resolution(model::OperationModel)
- resolution = PSY.get_time_series_resolution(get_system(model))
- return IS.time_period_conversion(resolution)
+ resolution = get_resolution(get_settings(model))
+ return resolution
end
get_problem_base_power(model::OperationModel) = PSY.get_base_power(model.sys)
get_settings(model::OperationModel) = get_optimization_container(model).settings
get_optimizer_stats(model::OperationModel) =
get_optimizer_stats(get_optimization_container(model))
-get_simulation_info(model::OperationModel) = model.internal.simulation_info
-get_simulation_number(model::OperationModel) = model.internal.simulation_info.number
-get_status(model::OperationModel) = model.internal.status
+get_simulation_info(model::OperationModel) = model.simulation_info
+get_simulation_number(model::OperationModel) = get_number(get_simulation_info(model))
+set_simulation_number!(model::OperationModel, val) =
+ set_number!(get_simulation_info(model), val)
+get_sequence_uuid(model::OperationModel) = get_sequence_uuid(get_simulation_info(model))
+set_sequence_uuid!(model::OperationModel, val) =
+ set_sequence_uuid!(get_simulation_info(model), val)
+get_status(model::OperationModel) = IS.Optimization.get_status(get_internal(model))
get_system(model::OperationModel) = model.sys
get_template(model::OperationModel) = model.template
get_log_file(model::OperationModel) = joinpath(get_output_dir(model), PROBLEM_LOG_FILENAME)
-get_output_dir(model::OperationModel) = model.internal.output_dir
+get_store_params(model::OperationModel) =
+ IS.Optimization.get_store_params(get_internal(model))
+get_output_dir(model::OperationModel) = IS.Optimization.get_output_dir(get_internal(model))
get_initial_conditions_file(model::OperationModel) =
- joinpath(model.internal.output_dir, "initial_conditions.bin")
-get_recorder_dir(model::OperationModel) = joinpath(model.internal.output_dir, "recorder")
+ joinpath(get_output_dir(model), "initial_conditions.bin")
+get_recorder_dir(model::OperationModel) =
+ joinpath(get_output_dir(model), "recorder")
get_variables(model::OperationModel) = get_variables(get_optimization_container(model))
get_parameters(model::OperationModel) = get_parameters(get_optimization_container(model))
get_duals(model::OperationModel) = get_duals(get_optimization_container(model))
get_initial_conditions(model::OperationModel) =
get_initial_conditions(get_optimization_container(model))
-get_interval(model::OperationModel) = model.internal.store_parameters.interval
-get_run_status(model::OperationModel) = model.internal.run_status
-set_run_status!(model::OperationModel, status) = model.internal.run_status = status
-get_time_series_cache(model::OperationModel) = model.internal.time_series_cache
+get_interval(model::OperationModel) = get_store_params(model).interval
+get_run_status(model::OperationModel) = get_run_status(get_simulation_info(model))
+set_run_status!(model::OperationModel, status) =
+ set_run_status!(get_simulation_info(model), status)
+get_time_series_cache(model::OperationModel) =
+ IS.Optimization.get_time_series_cache(get_internal(model))
empty_time_series_cache!(x::OperationModel) = empty!(get_time_series_cache(x))
function get_current_timestamp(model::OperationModel)
@@ -61,10 +82,11 @@ function get_current_timestamp(model::OperationModel)
end
function get_timestamps(model::OperationModel)
- start_time = get_initial_time(get_optimization_container(model))
+ optimization_container = get_optimization_container(model)
+ start_time = get_initial_time(optimization_container)
resolution = get_resolution(model)
- horizon = get_horizon(model)
- return range(start_time; length = horizon, step = resolution)
+ horizon_count = get_time_steps(optimization_container)[end]
+ return range(start_time; length = horizon_count, step = resolution)
end
function write_data(model::OperationModel, output_dir::AbstractString; kwargs...)
@@ -84,12 +106,12 @@ function solve_impl!(model::OperationModel)
container = get_optimization_container(model)
status = solve_impl!(container, get_system(model))
set_run_status!(model, status)
- if status != RunStatus.SUCCESSFUL
+ if status != RunStatus.SUCCESSFULLY_FINALIZED
settings = get_settings(model)
model_name = get_name(model)
ts = get_current_timestamp(model)
output_dir = get_output_dir(model)
- infeasible_opt_path = joinpath(output_dir, "infeasible_$(model_name)_$(ts).json")
+ infeasible_opt_path = joinpath(output_dir, "infeasible_$(model_name).json")
@error("Serializing Infeasible Problem at $(infeasible_opt_path)")
serialize_optimization_model(container, infeasible_opt_path)
if !get_allow_fails(settings)
@@ -101,20 +123,34 @@ function solve_impl!(model::OperationModel)
return
end
-set_console_level!(model::OperationModel, val) = get_internal(model).console_level = val
-set_file_level!(model::OperationModel, val) = get_internal(model).file_level = val
-set_executions!(model::OperationModel, val::Int) = model.internal.executions = val
-set_execution_count!(model::OperationModel, val::Int) =
- get_internal(model).execution_count = val
+set_console_level!(model::OperationModel, val) =
+ IS.Optimization.set_console_level!(get_internal(model), val)
+set_file_level!(model::OperationModel, val) =
+ IS.Optimization.set_file_level!(get_internal(model), val)
+function set_executions!(model::OperationModel, val::Int)
+ IS.Optimization.set_executions!(get_internal(model), val)
+ return
+end
+
+function set_execution_count!(model::OperationModel, val::Int)
+ IS.Optimization.set_execution_count!(get_internal(model), val)
+ return
+end
+
set_initial_time!(model::OperationModel, val::Dates.DateTime) =
set_initial_time!(get_settings(model), val)
-set_simulation_info!(model::OperationModel, info) = model.internal.simulation_info = info
-function set_status!(model::OperationModel, status::BuildStatus)
- model.internal.status = status
+
+get_simulation_info(model::OperationModel, val) = model.simulation_info = val
+
+function set_status!(model::OperationModel, status::ModelBuildStatus)
+ IS.Optimization.set_status!(get_internal(model), status)
+ return
+end
+
+function set_output_dir!(model::OperationModel, path::AbstractString)
+ IS.Optimization.set_output_dir!(get_internal(model), path)
return
end
-set_output_dir!(model::OperationModel, path::AbstractString) =
- get_internal(model).output_dir = path
function advance_execution_count!(model::OperationModel)
internal = get_internal(model)
@@ -123,7 +159,8 @@ function advance_execution_count!(model::OperationModel)
end
function build_initial_conditions!(model::OperationModel)
- @assert model.internal.ic_model_container === nothing
+ @assert IS.Optimization.get_initial_conditions_model_container(get_internal(model)) ===
+ nothing
requires_init = false
for (device_type, device_model) in get_device_models(get_template(model))
requires_init = requires_initialization(get_formulation(device_model)())
@@ -143,7 +180,7 @@ end
function write_initial_conditions_data!(model::OperationModel)
write_initial_conditions_data!(
get_optimization_container(model),
- model.internal.ic_model_container,
+ IS.Optimization.get_initial_conditions_model_container(get_internal(model)),
)
return
end
@@ -185,7 +222,7 @@ function handle_initial_conditions!(model::OperationModel)
if deserialize_initial_conditions && isfile(serialized_initial_conditions_file)
set_initial_conditions_data!(
- model.internal.container,
+ get_optimization_container(model),
Serialization.deserialize(serialized_initial_conditions_file),
)
@info "Deserialized initial_conditions_data"
@@ -194,23 +231,33 @@ function handle_initial_conditions!(model::OperationModel)
build_initial_conditions!(model)
initialize!(model)
end
- model.internal.ic_model_container = nothing
+ IS.Optimization.set_initial_conditions_model_container!(
+ get_internal(model),
+ nothing,
+ )
end
return
end
function initialize!(model::OperationModel)
container = get_optimization_container(model)
- if model.internal.ic_model_container === nothing
+ if IS.Optimization.get_initial_conditions_model_container(get_internal(model)) ===
+ nothing
return
end
@info "Solving Initialization Model for $(get_name(model))"
- status = solve_impl!(model.internal.ic_model_container, get_system(model))
+ status = solve_impl!(
+ IS.Optimization.get_initial_conditions_model_container(get_internal(model)),
+ get_system(model),
+ )
if status == RunStatus.FAILED
error("Model failed to initialize")
end
- write_initial_conditions_data!(container, model.internal.ic_model_container)
+ write_initial_conditions_data!(
+ container,
+ IS.Optimization.get_initial_conditions_model_container(get_internal(model)),
+ )
init_file = get_initial_conditions_file(model)
Serialization.serialize(init_file, get_initial_conditions_data(container))
@info "Serialized initial conditions to $init_file"
@@ -241,9 +288,9 @@ function validate_template(model::OperationModel)
return
end
-function build_if_not_already_built!(model; kwargs...)
+function build_if_not_already_built!(model::OperationModel; kwargs...)
status = get_status(model)
- if status == BuildStatus.EMPTY
+ if status == ModelBuildStatus.EMPTY
if !haskey(kwargs, :output_dir)
error(
"'output_dir' must be provided as a kwarg if the model build status is $status",
@@ -253,7 +300,7 @@ function build_if_not_already_built!(model; kwargs...)
status = build!(model; new_kwargs...)
end
end
- if status != BuildStatus.BUILT
+ if status != ModelBuildStatus.BUILT
error("build! of the $(typeof(model)) $(get_name(model)) failed: $status")
end
return
@@ -306,17 +353,21 @@ function _pre_solve_model_checks(model::OperationModel, optimizer = nothing)
end
optimizer_name = JuMP.solver_name(jump_model)
- @info "Solving $(get_name(model)) with optimizer = $optimizer_name"
+ @info "$(get_name(model)) optimizer set to: $optimizer_name"
settings = get_settings(model)
if get_check_numerical_bounds(settings)
- _check_numerical_bounds(model)
+ @info "Checking Numerical Bounds"
+ TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Numerical Bounds Check" begin
+ _check_numerical_bounds(model)
+ end
end
-
return
end
function _list_names(model::OperationModel, container_type)
- return encode_keys_as_strings(list_keys(get_store(model), container_type))
+ return encode_keys_as_strings(
+ IS.Optimization.list_keys(get_store(model), container_type),
+ )
end
read_dual(model::OperationModel, key::ConstraintKey) = _read_results(model, key)
@@ -353,20 +404,20 @@ read_optimizer_stats(model::OperationModel) = read_optimizer_stats(get_store(mod
function add_recorders!(model::OperationModel, recorders)
internal = get_internal(model)
for name in union(REQUIRED_RECORDERS, recorders)
- add_recorder!(internal, name)
+ IS.Optimization.add_recorder!(internal, name)
end
end
function register_recorders!(model::OperationModel, file_mode)
recorder_dir = get_recorder_dir(model)
mkpath(recorder_dir)
- for name in get_recorders(get_internal(model))
+ for name in IS.Optimization.get_recorders(get_internal(model))
IS.register_recorder!(name; mode = file_mode, directory = recorder_dir)
end
end
function unregister_recorders!(model::OperationModel)
- for name in get_recorders(get_internal(model))
+ for name in IS.Optimization.get_recorders(get_internal(model))
IS.unregister_recorder!(name)
end
end
@@ -399,16 +450,19 @@ function instantiate_network_model(model::OperationModel)
end
list_aux_variable_keys(x::OperationModel) =
- list_keys(get_store(x), STORE_CONTAINER_AUX_VARIABLES)
+ IS.Optimization.list_keys(get_store(x), STORE_CONTAINER_AUX_VARIABLES)
list_aux_variable_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_AUX_VARIABLES)
-list_variable_keys(x::OperationModel) = list_keys(get_store(x), STORE_CONTAINER_VARIABLES)
+list_variable_keys(x::OperationModel) =
+ IS.Optimization.list_keys(get_store(x), STORE_CONTAINER_VARIABLES)
list_variable_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_VARIABLES)
-list_parameter_keys(x::OperationModel) = list_keys(get_store(x), STORE_CONTAINER_PARAMETERS)
+list_parameter_keys(x::OperationModel) =
+ IS.Optimization.list_keys(get_store(x), STORE_CONTAINER_PARAMETERS)
list_parameter_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_PARAMETERS)
-list_dual_keys(x::OperationModel) = list_keys(get_store(x), STORE_CONTAINER_DUALS)
+list_dual_keys(x::OperationModel) =
+ IS.Optimization.list_keys(get_store(x), STORE_CONTAINER_DUALS)
list_dual_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_DUALS)
list_expression_keys(x::OperationModel) =
- list_keys(get_store(x), STORE_CONTAINER_EXPRESSIONS)
+ IS.Optimization.list_keys(get_store(x), STORE_CONTAINER_EXPRESSIONS)
list_expression_names(x::OperationModel) = _list_names(x, STORE_CONTAINER_EXPRESSIONS)
function list_all_keys(x::OperationModel)
diff --git a/src/operation/operation_problem_templates.jl b/src/operation/operation_problem_templates.jl
index 77338b6132..dc91f1a30d 100644
--- a/src/operation/operation_problem_templates.jl
+++ b/src/operation/operation_problem_templates.jl
@@ -7,7 +7,7 @@ function _default_devices_uc()
return [
DeviceModel(PSY.ThermalStandard, ThermalBasicUnitCommitment),
DeviceModel(PSY.RenewableDispatch, RenewableFullDispatch),
- DeviceModel(PSY.RenewableFix, FixedOutput),
+ DeviceModel(PSY.RenewableNonDispatch, FixedOutput),
DeviceModel(PSY.PowerLoad, StaticPowerLoad),
DeviceModel(PSY.InterruptiblePowerLoad, PowerLoadInterruption),
DeviceModel(PSY.Line, StaticBranch),
@@ -93,6 +93,7 @@ function template_economic_dispatch(; kwargs...)
return template
end
+#=
"""
template_agc_reserve_deployment(; kwargs...)
@@ -112,7 +113,7 @@ function template_agc_reserve_deployment(; kwargs...)
set_device_model!(template, PSY.PowerLoad, StaticPowerLoad)
set_device_model!(template, PSY.HydroEnergyReservoir, FixedOutput)
set_device_model!(template, PSY.HydroDispatch, FixedOutput)
- set_device_model!(template, PSY.RenewableFix, FixedOutput)
+ set_device_model!(template, PSY.RenewableNonDispatch, FixedOutput)
set_device_model!(
template,
DeviceModel(PSY.RegulationDevice{PSY.ThermalStandard}, DeviceLimitedRegulation),
@@ -131,3 +132,4 @@ function template_agc_reserve_deployment(; kwargs...)
set_service_model!(template, ServiceModel(PSY.AGC, PIDSmoothACE))
return template
end
+=#
diff --git a/src/operation/optimization_debugging.jl b/src/operation/optimization_debugging.jl
index b67d7b1e96..be497783fe 100644
--- a/src/operation/optimization_debugging.jl
+++ b/src/operation/optimization_debugging.jl
@@ -18,7 +18,7 @@ Each Tuple corresponds to (con_name, internal_index, moi_index)
"""
function get_all_variable_index(model::OperationModel)
var_keys = get_all_variable_keys(model)
- return [(encode_key(v[1]), v[2], v[3]) for v in var_keys]
+ return [(IS.Optimization.encode_key(v[1]), v[2], v[3]) for v in var_keys]
end
function get_all_variable_keys(model::OperationModel)
diff --git a/src/operation/problem_results.jl b/src/operation/problem_results.jl
index cfb1aca219..c0554af322 100644
--- a/src/operation/problem_results.jl
+++ b/src/operation/problem_results.jl
@@ -1,62 +1,10 @@
-# This needs renaming to avoid collision with the DecionModelResults/EmulationModelResults
-mutable struct ProblemResults <: IS.Results
- base_power::Float64
- timestamps::StepRange{Dates.DateTime, Dates.Millisecond}
- system::Union{Nothing, PSY.System}
- system_uuid::Base.UUID
- aux_variable_values::Dict{AuxVarKey, DataFrames.DataFrame}
- variable_values::Dict{VariableKey, DataFrames.DataFrame}
- dual_values::Dict{ConstraintKey, DataFrames.DataFrame}
- parameter_values::Dict{ParameterKey, DataFrames.DataFrame}
- expression_values::Dict{ExpressionKey, DataFrames.DataFrame}
- optimizer_stats::DataFrames.DataFrame
- optimization_container_metadata::OptimizationContainerMetadata
- model_type::String
- output_dir::String
-end
-
-list_aux_variable_keys(res::ProblemResults) = collect(keys(res.aux_variable_values))
-list_aux_variable_names(res::ProblemResults) =
- encode_keys_as_strings(keys(res.aux_variable_values))
-list_variable_keys(res::ProblemResults) = collect(keys(res.variable_values))
-list_variable_names(res::ProblemResults) = encode_keys_as_strings(keys(res.variable_values))
-list_parameter_keys(res::ProblemResults) = collect(keys(res.parameter_values))
-list_parameter_names(res::ProblemResults) =
- encode_keys_as_strings(keys(res.parameter_values))
-list_dual_keys(res::ProblemResults) = collect(keys(res.dual_values))
-list_dual_names(res::ProblemResults) = encode_keys_as_strings(keys(res.dual_values))
-list_expression_keys(res::ProblemResults) = collect(keys(res.expression_values))
-list_expression_names(res::ProblemResults) =
- encode_keys_as_strings(keys(res.expression_values))
-get_timestamps(res::ProblemResults) = res.timestamps
-get_model_base_power(res::ProblemResults) = res.base_power
-get_dual_values(res::ProblemResults) = res.dual_values
-get_expression_values(res::ProblemResults) = res.expression_values
-get_variable_values(res::ProblemResults) = res.variable_values
-get_aux_variable_values(res::ProblemResults) = res.aux_variable_values
-get_total_cost(res::ProblemResults) = get_objective_value(res)
-get_optimizer_stats(res::ProblemResults) = res.optimizer_stats
-get_parameter_values(res::ProblemResults) = res.parameter_values
-get_resolution(res::ProblemResults) = res.timestamps.step
-get_system(res::ProblemResults) = res.system
-get_forecast_horizon(res::ProblemResults) = length(get_timestamps(res))
-
-get_result_values(x::ProblemResults, ::AuxVarKey) = x.aux_variable_values
-get_result_values(x::ProblemResults, ::ConstraintKey) = x.dual_values
-get_result_values(x::ProblemResults, ::ExpressionKey) = x.expression_values
-get_result_values(x::ProblemResults, ::ParameterKey) = x.parameter_values
-get_result_values(x::ProblemResults, ::VariableKey) = x.variable_values
-
-function get_objective_value(res::ProblemResults, execution = 1)
- return res.optimizer_stats[execution, :objective_value]
-end
-
"""
-Construct ProblemResults from a solved DecisionModel.
+Construct OptimizationProblemResults from a solved DecisionModel.
"""
-function ProblemResults(model::DecisionModel)
+function OptimizationProblemResults(model::DecisionModel)
status = get_run_status(model)
- status != RunStatus.SUCCESSFUL && error("problem was not solved successfully: $status")
+ status != RunStatus.SUCCESSFULLY_FINALIZED &&
+ error("problem was not solved successfully: $status")
model_store = get_store(model)
@@ -65,7 +13,7 @@ function ProblemResults(model::DecisionModel)
end
timestamps = get_timestamps(model)
- optimizer_stats = to_dataframe(get_optimizer_stats(model))
+ optimizer_stats = IS.Optimization.to_dataframe(get_optimizer_stats(model))
aux_variable_values =
Dict(x => read_aux_variable(model, x) for x in list_aux_variable_keys(model))
@@ -78,7 +26,7 @@ function ProblemResults(model::DecisionModel)
sys = get_system(model)
- return ProblemResults(
+ return OptimizationProblemResults(
get_problem_base_power(model),
timestamps,
sys,
@@ -91,16 +39,18 @@ function ProblemResults(model::DecisionModel)
optimizer_stats,
get_metadata(get_optimization_container(model)),
IS.strip_module_name(typeof(model)),
+ get_output_dir(model),
mkpath(joinpath(get_output_dir(model), "results")),
)
end
"""
-Construct ProblemResults from a solved EmulationModel.
+Construct OptimizationProblemResults from a solved EmulationModel.
"""
-function ProblemResults(model::EmulationModel)
+function OptimizationProblemResults(model::EmulationModel)
status = get_run_status(model)
- status != RunStatus.SUCCESSFUL && error("problem was not solved successfully: $status")
+ status != RunStatus.SUCCESSFULLY_FINALIZED &&
+ error("problem was not solved successfully: $status")
model_store = get_store(model)
@@ -119,7 +69,7 @@ function ProblemResults(model::EmulationModel)
container = get_optimization_container(model)
sys = get_system(model)
- return ProblemResults(
+ return OptimizationProblemResults(
get_problem_base_power(model),
StepRange(initial_time, get_resolution(model), initial_time),
sys,
@@ -132,619 +82,7 @@ function ProblemResults(model::EmulationModel)
optimizer_stats,
get_metadata(container),
IS.strip_module_name(typeof(model)),
+ get_output_dir(model),
mkpath(joinpath(get_output_dir(model), "results")),
)
end
-
-"""
-Exports all results from the operations problem.
-"""
-function export_results(results::ProblemResults; kwargs...)
- exports = ProblemResultsExport(
- "Problem";
- store_all_duals = true,
- store_all_parameters = true,
- store_all_variables = true,
- store_all_aux_variables = true,
- )
- return export_results(results, exports; kwargs...)
-end
-
-function export_results(
- results::ProblemResults,
- exports::ProblemResultsExport;
- file_type = CSV.File,
-)
- file_type != CSV.File && error("only CSV.File is currently supported")
- export_path = mkpath(joinpath(results.output_dir, "variables"))
- for (key, df) in results.variable_values
- if should_export_variable(exports, key)
- export_result(file_type, export_path, key, df)
- end
- end
-
- export_path = mkpath(joinpath(results.output_dir, "aux_variables"))
- for (key, df) in results.aux_variable_values
- if should_export_aux_variable(exports, key)
- export_result(file_type, export_path, key, df)
- end
- end
-
- export_path = mkpath(joinpath(results.output_dir, "duals"))
- for (key, df) in results.dual_values
- if should_export_dual(exports, key)
- export_result(file_type, export_path, key, df)
- end
- end
-
- export_path = mkpath(joinpath(results.output_dir, "parameters"))
- for (key, df) in results.parameter_values
- if should_export_parameter(exports, key)
- export_result(file_type, export_path, key, df)
- end
- end
-
- export_path = mkpath(joinpath(results.output_dir, "expressions"))
- for (key, df) in results.expression_values
- if should_export_expression(exports, key)
- export_result(file_type, export_path, key, df)
- end
- end
-
- if exports.optimizer_stats
- export_result(
- file_type,
- joinpath(results.output_dir, "optimizer_stats.csv"),
- results.optimizer_stats,
- )
- end
-
- @info "Exported ProblemResults to $(results.output_dir)"
-end
-
-function _deserialize_key(
- ::Type{<:OptimizationContainerKey},
- results::ProblemResults,
- name::AbstractString,
-)
- return deserialize_key(results.optimization_container_metadata, name)
-end
-
-function _deserialize_key(
- ::Type{T},
- ::ProblemResults,
- args...,
-) where {T <: OptimizationContainerKey}
- return make_key(T, args...)
-end
-
-read_optimizer_stats(res::ProblemResults) = res.optimizer_stats
-
-"""
-Set the system in the results instance.
-
-Throws InvalidValue if the system UUID is incorrect.
-"""
-function set_system!(res::ProblemResults, system::PSY.System)
- sys_uuid = IS.get_uuid(system)
- if sys_uuid != res.system_uuid
- throw(
- IS.InvalidValue(
- "System mismatch. $sys_uuid does not match the stored value of $(res.system_uuid)",
- ),
- )
- end
-
- res.system = system
- return
-end
-
-const _PROBLEM_RESULTS_FILENAME = "problem_results.bin"
-
-"""
-Serialize the results to a binary file.
-
-It is recommended that `directory` be the directory that contains a serialized
-OperationModel. That will allow automatic deserialization of the PowerSystems.System.
-The `ProblemResults` instance can be deserialized with `ProblemResults(directory)`.
-"""
-function serialize_results(res::ProblemResults, directory::AbstractString)
- mkpath(directory)
- filename = joinpath(directory, _PROBLEM_RESULTS_FILENAME)
- isfile(filename) && rm(filename)
- Serialization.serialize(filename, _copy_for_serialization(res))
- @info "Serialize ProblemResults to $filename"
-end
-
-"""
-Construct a ProblemResults instance from a serialized directory.
-
-If the directory contains a serialized PowerSystems.System then it will deserialize that
-system and add it to the results. Otherwise, it is up to the caller to call
-[`set_system!`](@ref) on the returned instance to restore it.
-"""
-function ProblemResults(directory::AbstractString)
- filename = joinpath(directory, _PROBLEM_RESULTS_FILENAME)
- if !isfile(filename)
- error("No results file exists in $directory")
- end
-
- results = Serialization.deserialize(filename)
- possible_sys_file = joinpath(directory, make_system_filename(results.system_uuid))
- if isfile(possible_sys_file)
- set_system!(results, PSY.System(possible_sys_file))
- else
- @info "$directory does not contain a serialized System, skipping deserialization."
- end
-
- return results
-end
-
-function _copy_for_serialization(res::ProblemResults)
- return ProblemResults(
- res.base_power,
- res.timestamps,
- nothing,
- res.system_uuid,
- res.aux_variable_values,
- res.variable_values,
- res.dual_values,
- res.parameter_values,
- res.expression_values,
- res.optimizer_stats,
- res.optimization_container_metadata,
- res.model_type,
- res.output_dir,
- )
-end
-
-function _read_results(
- result_values::Dict{<:OptimizationContainerKey, DataFrames.DataFrame},
- container_keys,
- timestamps::Vector{Dates.DateTime},
- time_ids,
- base_power::Float64,
-)
- existing_keys = keys(result_values)
- container_keys = container_keys === nothing ? existing_keys : container_keys
- _validate_keys(existing_keys, container_keys)
- results = Dict{OptimizationContainerKey, DataFrames.DataFrame}()
- for (k, v) in result_values
- if k in container_keys
- num_rows = DataFrames.nrow(v)
- if num_rows == 1 && num_rows < length(time_ids)
- results[k] =
- if convert_result_to_natural_units(k)
- v .* base_power
- else
- v
- end
- else
- results[k] =
- if convert_result_to_natural_units(k)
- v[time_ids, :] .* base_power
- else
- v[time_ids, :]
- end
- DataFrames.insertcols!(results[k], 1, :DateTime => timestamps)
- end
- end
- end
- return results
-end
-
-function _process_timestamps(
- res::ProblemResults,
- start_time::Union{Nothing, Dates.DateTime},
- len::Union{Int, Nothing},
-)
- if start_time === nothing
- start_time = first(get_timestamps(res))
- elseif start_time ∉ get_timestamps(res)
- throw(IS.InvalidValue("start_time not in result timestamps"))
- end
-
- if startswith(res.model_type, "EmulationModel{")
- def_len = DataFrames.nrow(get_optimizer_stats(res))
- requested_range =
- collect(findfirst(x -> x >= start_time, get_timestamps(res)):def_len)
- timestamps = repeat(get_timestamps(res), def_len)
- else
- timestamps = get_timestamps(res)
- requested_range = findall(x -> x >= start_time, timestamps)
- def_len = length(requested_range)
- end
- len = len === nothing ? def_len : len
- if len > def_len
- throw(IS.InvalidValue("requested results have less than $len values"))
- end
- timestamp_ids = requested_range[1:len]
- return timestamp_ids, timestamps[timestamp_ids]
-end
-
-"""
-Return the values for the requested variable key for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `variable::Tuple{Type{<:VariableType}, Type{<:PSY.Component}` : Tuple with variable type and device type for the desired results
- - `start_time::Dates.DateTime` : start time of the requested results
- - `len::Int`: length of results
-"""
-function read_variable(res::ProblemResults, args...; kwargs...)
- key = VariableKey(args...)
- return read_variable(res, key; kwargs...)
-end
-
-function read_variable(res::ProblemResults, key::AbstractString; kwargs...)
- return read_variable(res, _deserialize_key(VariableKey, res, key); kwargs...)
-end
-
-function read_variable(
- res::ProblemResults,
- key::VariableKey;
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- return read_results_with_keys(res, [key]; start_time = start_time, len = len)[key]
-end
-
-"""
-Return the values for the requested variable keys for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `variables::Vector{Tuple{Type{<:VariableType}, Type{<:PSY.Component}}` : Tuple with variable type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_variables(res::ProblemResults, variables; kwargs...)
- return read_variables(res, [VariableKey(x...) for x in variables]; kwargs...)
-end
-
-function read_variables(res::ProblemResults, variables::Vector{<:AbstractString}; kwargs...)
- return read_variables(
- res,
- [_deserialize_key(VariableKey, res, x) for x in variables];
- kwargs...,
- )
-end
-
-function read_variables(
- res::ProblemResults,
- variables::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- result_values =
- read_results_with_keys(res, variables; start_time = start_time, len = len)
- return Dict(encode_key_as_string(k) => v for (k, v) in result_values)
-end
-
-"""
-Return the values for all variables.
-"""
-function read_variables(res::IS.Results)
- return Dict(x => read_variable(res, x) for x in list_variable_names(res))
-end
-
-"""
-Return the values for the requested dual key for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `dual::Tuple{Type{<:ConstraintType}, Type{<:PSY.Component}` : Tuple with dual type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_dual(res::ProblemResults, args...; kwargs...)
- key = ConstraintKey(args...)
- return read_dual(res, key; kwargs...)
-end
-
-function read_dual(res::ProblemResults, key::AbstractString; kwargs...)
- return read_dual(res, _deserialize_key(ConstraintKey, res, key); kwargs...)
-end
-
-function read_dual(
- res::ProblemResults,
- key::ConstraintKey;
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- return read_results_with_keys(res, [key]; start_time = start_time, len = len)[key]
-end
-
-"""
-Return the values for the requested dual keys for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `duals::Vector{Tuple{Type{<:ConstraintType}, Type{<:PSY.Component}}` : Tuple with dual type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_duals(res::ProblemResults, duals; kwargs...)
- return read_duals(res, [ConstraintKey(x...) for x in duals]; kwargs...)
-end
-
-function read_duals(res::ProblemResults, duals::Vector{<:AbstractString}; kwargs...)
- return read_duals(
- res,
- [_deserialize_key(ConstraintKey, res, x) for x in duals];
- kwargs...,
- )
-end
-
-function read_duals(
- res::ProblemResults,
- duals::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- result_values = read_results_with_keys(res, duals; start_time = start_time, len = len)
- return Dict(encode_key_as_string(k) => v for (k, v) in result_values)
-end
-
-"""
-Return the values for all duals.
-"""
-function read_duals(res::IS.Results)
- duals = Dict(x => read_dual(res, x) for x in list_dual_names(res))
-end
-
-"""
-Return the values for the requested parameter key for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `parameter::Tuple{Type{<:ParameterType}, Type{<:PSY.Component}` : Tuple with parameter type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_parameter(res::ProblemResults, args...; kwargs...)
- key = ParameterKey(args...)
- return read_parameter(res, key; kwargs...)
-end
-
-function read_parameter(res::ProblemResults, key::AbstractString; kwargs...)
- return read_parameter(res, _deserialize_key(ParameterKey, res, key); kwargs...)
-end
-
-function read_parameter(
- res::ProblemResults,
- key::ParameterKey;
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- return read_results_with_keys(res, [key]; start_time = start_time, len = len)[key]
-end
-
-"""
-Return the values for the requested parameter keys for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `parameters::Vector{Tuple{Type{<:ParameterType}, Type{<:PSY.Component}}` : Tuple with parameter type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_parameters(res::ProblemResults, parameters; kwargs...)
- return read_parameters(res, [ParameterKey(x...) for x in parameters]; kwargs...)
-end
-
-function read_parameters(
- res::ProblemResults,
- parameters::Vector{<:AbstractString};
- kwargs...,
-)
- return read_parameters(
- res,
- [_deserialize_key(ParameterKey, res, x) for x in parameters];
- kwargs...,
- )
-end
-
-function read_parameters(
- res::ProblemResults,
- parameters::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- result_values =
- read_results_with_keys(res, parameters; start_time = start_time, len = len)
- return Dict(encode_key_as_string(k) => v for (k, v) in result_values)
-end
-
-"""
-Return the values for all parameters.
-"""
-function read_parameters(res::IS.Results)
- parameters = Dict(x => read_parameter(res, x) for x in list_parameter_names(res))
-end
-
-"""
-Return the values for the requested aux_variable key for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `aux_variable::Tuple{Type{<:AuxVariableType}, Type{<:PSY.Component}` : Tuple with aux_variable type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_aux_variable(res::ProblemResults, args...; kwargs...)
- key = AuxVarKey(args...)
- return read_aux_variable(res, key; kwargs...)
-end
-
-function read_aux_variable(res::ProblemResults, key::AbstractString; kwargs...)
- return read_aux_variable(res, _deserialize_key(AuxVarKey, res, key); kwargs...)
-end
-
-function read_aux_variable(
- res::ProblemResults,
- key::AuxVarKey;
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- return read_results_with_keys(res, [key]; start_time = start_time, len = len)[key]
-end
-
-"""
-Return the values for the requested aux_variable keys for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `aux_variables::Vector{Tuple{Type{<:AuxVariableType}, Type{<:PSY.Component}}` : Tuple with aux_variable type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_aux_variables(res::ProblemResults, aux_variables; kwargs...)
- return read_aux_variables(res, [AuxVarKey(x...) for x in aux_variables]; kwargs...)
-end
-
-function read_aux_variables(
- res::ProblemResults,
- aux_variables::Vector{<:AbstractString};
- kwargs...,
-)
- return read_aux_variables(
- res,
- [_deserialize_key(AuxVarKey, res, x) for x in aux_variables];
- kwargs...,
- )
-end
-
-function read_aux_variables(
- res::ProblemResults,
- aux_variables::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- result_values =
- read_results_with_keys(res, aux_variables; start_time = start_time, len = len)
- return Dict(encode_key_as_string(k) => v for (k, v) in result_values)
-end
-
-"""
-Return the values for all auxiliary variables.
-"""
-function read_aux_variables(res::IS.Results)
- return Dict(x => read_aux_variable(res, x) for x in list_aux_variable_names(res))
-end
-
-"""
-Return the values for the requested expression key for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `expression::Tuple{Type{<:ExpressionType}, Type{<:PSY.Component}` : Tuple with expression type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_expression(res::ProblemResults, args...; kwargs...)
- key = ExpressionKey(args...)
- return read_expression(res, key; kwargs...)
-end
-
-function read_expression(res::ProblemResults, key::AbstractString; kwargs...)
- return read_expression(res, _deserialize_key(ExpressionKey, res, key); kwargs...)
-end
-
-function read_expression(
- res::ProblemResults,
- key::ExpressionKey;
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- return read_results_with_keys(res, [key]; start_time = start_time, len = len)[key]
-end
-
-"""
-Return the values for the requested expression keys for a problem.
-Accepts a vector of keys for the return of the values. If the time stamps and keys are
-loaded using the [`load_results!`](@ref) function it will read from memory.
-
-# Arguments
-
- - `expressions::Vector{Tuple{Type{<:ExpressionType}, Type{<:PSY.Component}}` : Tuple with expression type and device type for the desired results
- - `start_time::Dates.DateTime` : initial time of the requested results
- - `len::Int`: length of results
-"""
-function read_expressions(res::ProblemResults; kwargs...)
- return read_expressions(res, collect(keys(res.expression_values)); kwargs...)
-end
-
-function read_expressions(res::ProblemResults, expressions; kwargs...)
- return read_expressions(res, [ExpressionKey(x...) for x in expressions]; kwargs...)
-end
-
-function read_expressions(
- res::ProblemResults,
- expressions::Vector{<:AbstractString};
- kwargs...,
-)
- return read_expressions(
- res,
- [_deserialize_key(ExpressionKey, res, x) for x in expressions];
- kwargs...,
- )
-end
-
-function read_expressions(
- res::ProblemResults,
- expressions::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- result_values =
- read_results_with_keys(res, expressions; start_time = start_time, len = len)
- return Dict(encode_key_as_string(k) => v for (k, v) in result_values)
-end
-
-"""
-Return the values for all expressions.
-"""
-function read_expressions(res::IS.Results)
- return Dict(x => read_expression(res, x) for x in list_expression_names(res))
-end
-
-function read_results_with_keys(
- res::ProblemResults,
- result_keys::Vector{<:OptimizationContainerKey};
- start_time::Union{Nothing, Dates.DateTime} = nothing,
- len::Union{Int, Nothing} = nothing,
-)
- isempty(result_keys) && return Dict{OptimizationContainerKey, DataFrames.DataFrame}()
- (timestamp_ids, timestamps) = _process_timestamps(res, start_time, len)
- return _read_results(
- get_result_values(res, first(result_keys)),
- result_keys,
- timestamps,
- timestamp_ids,
- get_model_base_power(res),
- )
-end
-
-function export_realized_results(res::ProblemResults)
- save_path = mkpath(joinpath(res.output_dir, "export"))
- return export_realized_results(res, save_path)
-end
diff --git a/src/operation/problem_results_export.jl b/src/operation/problem_results_export.jl
deleted file mode 100644
index 3b1e6e5ab9..0000000000
--- a/src/operation/problem_results_export.jl
+++ /dev/null
@@ -1,92 +0,0 @@
-struct ProblemResultsExport
- name::Symbol
- duals::Set{ConstraintKey}
- expressions::Set{ExpressionKey}
- parameters::Set{ParameterKey}
- variables::Set{VariableKey}
- aux_variables::Set{AuxVarKey}
- optimizer_stats::Bool
- store_all_flags::Dict{Symbol, Bool}
-
- function ProblemResultsExport(
- name,
- duals,
- expressions,
- parameters,
- variables,
- aux_variables,
- optimizer_stats,
- store_all_flags,
- )
- duals = _check_fields(duals)
- expressions = _check_fields(expressions)
- parameters = _check_fields(parameters)
- variables = _check_fields(variables)
- aux_variables = _check_fields(aux_variables)
- new(
- name,
- duals,
- expressions,
- parameters,
- variables,
- aux_variables,
- optimizer_stats,
- store_all_flags,
- )
- end
-end
-
-function ProblemResultsExport(
- name;
- duals = Set{ConstraintKey}(),
- expressions = Set{ExpressionKey}(),
- parameters = Set{ParameterKey}(),
- variables = Set{VariableKey}(),
- aux_variables = Set{AuxVarKey}(),
- optimizer_stats = true,
- store_all_duals = false,
- store_all_expressions = false,
- store_all_parameters = false,
- store_all_variables = false,
- store_all_aux_variables = false,
-)
- store_all_flags = Dict(
- :duals => store_all_duals,
- :expressions => store_all_expressions,
- :parameters => store_all_parameters,
- :variables => store_all_variables,
- :aux_variables => store_all_aux_variables,
- )
- return ProblemResultsExport(
- Symbol(name),
- duals,
- expressions,
- parameters,
- variables,
- aux_variables,
- optimizer_stats,
- store_all_flags,
- )
-end
-
-function _check_fields(fields)
- if !(typeof(fields) <: Set)
- fields = Set(fields)
- end
-
- return fields
-end
-
-should_export_dual(x::ProblemResultsExport, key) = _should_export(x, :duals, key)
-should_export_expression(x::ProblemResultsExport, key) =
- _should_export(x, :expressions, key)
-should_export_parameter(x::ProblemResultsExport, key) = _should_export(x, :parameters, key)
-should_export_variable(x::ProblemResultsExport, key) = _should_export(x, :variables, key)
-should_export_aux_variable(x::ProblemResultsExport, key) =
- _should_export(x, :aux_variables, key)
-
-function _should_export(exports::ProblemResultsExport, field_name, key)
- exports.store_all_flags[field_name] && return true
- container = getproperty(exports, field_name)
- return key in container
-end
diff --git a/src/operation/problem_template.jl b/src/operation/problem_template.jl
index b694346bf1..0b0a4b671e 100644
--- a/src/operation/problem_template.jl
+++ b/src/operation/problem_template.jl
@@ -164,9 +164,9 @@ end
function set_service_model!(
template::ProblemTemplate,
service_name::String,
- model::ServiceModel{<:PSY.Service, <:AbstractServiceFormulation},
-)
- _set_model!(template.services, service_name, model)
+ model::ServiceModel{T, <:AbstractServiceFormulation},
+) where {T <: PSY.Service}
+ _set_model!(template.services, (service_name, Symbol(T)), model)
return
end
@@ -264,12 +264,20 @@ function _modify_device_model!(
return
end
+function _modify_device_model!(
+ ::Dict{Symbol, DeviceModel},
+ ::ServiceModel{PSY.TransmissionInterface, VariableMaxInterfaceFlow},
+ ::Vector,
+)
+ return
+end
+
function _add_services_to_device_model!(template::ProblemTemplate)
service_models = get_service_models(template)
devices_template = get_device_models(template)
for (service_key, service_model) in service_models
S = get_component_type(service_model)
- (S <: PSY.AGC || S <: PSY.StaticReserveGroup) && continue
+ (S <: PSY.AGC || S <: PSY.ConstantReserveGroup) && continue
contributing_devices = get_contributing_devices(service_model)
isempty(contributing_devices) && continue
_modify_device_model!(devices_template, service_model, contributing_devices)
@@ -308,7 +316,17 @@ function _populate_aggregated_service_model!(template::ProblemTemplate, sys::PSY
return
end
+function _add_modeled_lines!(template::ProblemTemplate, sys::PSY.System)
+ network_model = get_network_model(template)
+ branch_models = get_branch_models(template)
+ for v in values(branch_models)
+ push!(network_model.modeled_branch_types, get_component_type(v))
+ end
+ return
+end
+
function finalize_template!(template::ProblemTemplate, sys::PSY.System)
+ _add_modeled_lines!(template, sys)
_populate_aggregated_service_model!(template, sys)
_populate_contributing_devices!(template, sys)
_add_services_to_device_model!(template)
diff --git a/src/operation/time_series_interface.jl b/src/operation/time_series_interface.jl
index 1197e6d70c..040f7b2e1d 100644
--- a/src/operation/time_series_interface.jl
+++ b/src/operation/time_series_interface.jl
@@ -1,56 +1,3 @@
-function make_time_series_cache(
- ::Type{T},
- component,
- name,
- initial_time,
- len::Int;
- ignore_scaling_factors = true,
-) where {T <: PSY.StaticTimeSeries}
- return IS.StaticTimeSeriesCache(
- T,
- component,
- name;
- start_time = initial_time,
- ignore_scaling_factors = ignore_scaling_factors,
- )
-end
-
-function make_time_series_cache(
- ::Type{T},
- component,
- name,
- initial_time,
- horizon::Int;
- ignore_scaling_factors = true,
-) where {T <: PSY.AbstractDeterministic}
- return IS.ForecastCache(
- T,
- component,
- name;
- start_time = initial_time,
- horizon = horizon,
- ignore_scaling_factors = ignore_scaling_factors,
- )
-end
-
-function make_time_series_cache(
- ::Type{PSY.Probabilistic},
- component,
- name,
- initial_time,
- horizon::Int;
- ignore_scaling_factors = true,
-)
- return IS.ForecastCache(
- PSY.Probabilistic,
- component,
- name;
- start_time = initial_time,
- horizon = horizon,
- ignore_scaling_factors = ignore_scaling_factors,
- )
-end
-
function get_time_series_values!(
time_series_type::Type{T},
model::DecisionModel,
@@ -73,11 +20,11 @@ function get_time_series_values!(
end
cache = get_time_series_cache(model)
- key = TimeSeriesCacheKey(IS.get_uuid(component), T, name, multiplier_id)
+ key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name)
if haskey(cache, key)
ts_cache = cache[key]
else
- ts_cache = make_time_series_cache(
+ ts_cache = IS.make_time_series_cache(
time_series_type,
component,
name,
@@ -114,11 +61,11 @@ function get_time_series_values!(
end
cache = get_time_series_cache(model)
- key = TimeSeriesCacheKey(IS.get_uuid(component), T, name, multiplier_id)
+ key = IS.TimeSeriesCacheKey(IS.get_uuid(component), T, name)
if haskey(cache, key)
ts_cache = cache[key]
else
- ts_cache = make_time_series_cache(
+ ts_cache = IS.make_time_series_cache(
T,
component,
name,
@@ -132,11 +79,3 @@ function get_time_series_values!(
ts = IS.get_time_series_array!(ts_cache, initial_time)
return TimeSeries.values(ts)
end
-
-function get_time_series_uuid(
- ::Type{T},
- component::U,
- name::AbstractString,
-) where {T <: PSY.TimeSeriesData, U <: PSY.Component}
- return string(IS.get_time_series_uuid(T, component, name))
-end
diff --git a/src/parameters/add_parameters.jl b/src/parameters/add_parameters.jl
index f2f9060532..b581893ed0 100644
--- a/src/parameters/add_parameters.jl
+++ b/src/parameters/add_parameters.jl
@@ -59,7 +59,7 @@ function add_parameters!(
::Type{T},
service::U,
model::ServiceModel{U, V},
-) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractReservesFormulation}
+) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractServiceFormulation}
if get_rebuild_model(get_settings(container)) &&
has_container_key(container, T, U, PSY.get_name(service))
return
@@ -198,7 +198,7 @@ function _add_time_series_parameters!(
initial_values = Dict{String, AbstractArray}()
for device in devices
push!(device_names, PSY.get_name(device))
- ts_uuid = get_time_series_uuid(ts_type, device, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(ts_type, device, ts_name))
if !(ts_uuid in keys(initial_values))
initial_values[ts_uuid] =
get_time_series_initial_values!(container, ts_type, device, ts_name)
@@ -233,7 +233,7 @@ function _add_time_series_parameters!(
add_component_name!(
get_attributes(param_container),
name,
- get_time_series_uuid(ts_type, device, ts_name),
+ string(IS.get_time_series_uuid(ts_type, device, ts_name)),
)
end
return
@@ -258,7 +258,7 @@ function _add_parameters!(
::T,
service::U,
model::ServiceModel{U, V},
-) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractReservesFormulation}
+) where {T <: TimeSeriesParameter, U <: PSY.Service, V <: AbstractServiceFormulation}
ts_type = get_default_time_series_type(container)
if !(ts_type <: Union{PSY.AbstractDeterministic, PSY.StaticTimeSeries})
error("add_parameters! for TimeSeriesParameter is not compatible with $ts_type")
@@ -267,7 +267,7 @@ function _add_parameters!(
time_series_mult_id = _create_time_series_multiplier_index(model, T)
time_steps = get_time_steps(container)
name = PSY.get_name(service)
- ts_uuid = get_time_series_uuid(ts_type, service, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(ts_type, service, ts_name))
@debug "adding" T U _group = LOG_GROUP_OPTIMIZATION_CONTAINER
parameter_container = add_param_container!(
container,
@@ -401,8 +401,7 @@ function _add_parameters!(
D,
key,
names,
- time_steps;
- meta = get_service_name(model),
+ time_steps,
)
jump_model = get_jump_model(container)
diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl
index 9858c0ee00..39e4b8f996 100644
--- a/src/parameters/update_parameters.jl
+++ b/src/parameters/update_parameters.jl
@@ -45,7 +45,7 @@ function _update_parameter_values!(
components = get_available_components(device_model, get_system(model))
ts_uuids = Set{String}()
for component in components
- ts_uuid = get_time_series_uuid(U, component, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(U, component, ts_name))
if !(ts_uuid in ts_uuids)
ts_vector = get_time_series_values!(
U,
@@ -82,7 +82,7 @@ function _update_parameter_values!(
initial_forecast_time = get_current_time(model) # Function not well defined for DecisionModels
horizon = get_time_steps(get_optimization_container(model))[end]
ts_name = get_time_series_name(attributes)
- ts_uuid = get_time_series_uuid(U, service, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(U, service, ts_name))
ts_vector = get_time_series_values!(
U,
model,
@@ -115,7 +115,7 @@ function _update_parameter_values!(
ts_name = get_time_series_name(attributes)
ts_uuids = Set{String}()
for component in components
- ts_uuid = get_time_series_uuid(U, component, ts_name)
+ ts_uuid = string(IS.get_time_series_uuid(U, component, ts_name))
if !(ts_uuid in ts_uuids)
# Note: This interface reads one single value per component at a time.
value = get_time_series_values!(
@@ -331,38 +331,28 @@ end
"""
Update parameter function an OperationModel
"""
-function update_parameter_values!(
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
model::OperationModel,
key::ParameterKey{T, U},
input::DatasetContainer{InMemoryDataset},
) where {T <: ParameterType, U <: PSY.Component}
# Enable again for detailed debugging
# TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin
- optimization_container = get_optimization_container(model)
# Note: Do not instantite a new key here because it might not match the param keys in the container
# if the keys have strings in the meta fields
parameter_array = get_parameter_array(optimization_container, key)
parameter_attributes = get_parameter_attributes(optimization_container, key)
_update_parameter_values!(parameter_array, parameter_attributes, U, model, input)
- IS.@record :execution ParameterUpdateEvent(
- T,
- U,
- parameter_attributes,
- get_current_timestamp(model),
- get_name(model),
- )
- # end
return
end
-function update_parameter_values!(
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
model::OperationModel,
key::ParameterKey{T, U},
input::DatasetContainer{InMemoryDataset},
) where {T <: ObjectiveFunctionParameter, U <: PSY.Component}
- # Enable again for detailed debugging
- # TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin
- optimization_container = get_optimization_container(model)
# Note: Do not instantite a new key here because it might not match the param keys in the container
# if the keys have strings in the meta fields
parameter_array = get_parameter_array(optimization_container, key)
@@ -377,53 +367,68 @@ function update_parameter_values!(
model,
input,
)
- IS.@record :execution ParameterUpdateEvent(
- T,
- U,
+ return
+end
+
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
+ model::OperationModel,
+ key::ParameterKey{T, U},
+ input::DatasetContainer{InMemoryDataset},
+) where {T <: ObjectiveFunctionParameter, U <: PSY.Service}
+ # Note: Do not instantite a new key here because it might not match the param keys in the container
+ # if the keys have strings in the meta fields
+ parameter_array = get_parameter_array(optimization_container, key)
+ # Multiplier is only needed for the objective function since `_update_parameter_values!` also updates the objective function
+ parameter_multiplier = get_parameter_multiplier_array(optimization_container, key)
+ parameter_attributes = get_parameter_attributes(optimization_container, key)
+ _update_parameter_values!(
+ parameter_array,
+ parameter_multiplier,
parameter_attributes,
- get_current_timestamp(model),
- get_name(model),
+ U,
+ model,
+ input,
)
- # end
return
end
-function update_parameter_values!(
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
model::OperationModel,
- key::ParameterKey{FixValueParameter, T},
+ key::ParameterKey{FixValueParameter, U},
input::DatasetContainer{InMemoryDataset},
-) where {T <: PSY.Component}
- # Enable again for detailed debugging
- # TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin
- optimization_container = get_optimization_container(model)
+) where {U <: PSY.Component}
# Note: Do not instantite a new key here because it might not match the param keys in the container
# if the keys have strings in the meta fields
parameter_array = get_parameter_array(optimization_container, key)
parameter_attributes = get_parameter_attributes(optimization_container, key)
_update_parameter_values!(parameter_array, parameter_attributes, T, model, input)
_fix_parameter_value!(optimization_container, parameter_array, parameter_attributes)
- IS.@record :execution ParameterUpdateEvent(
- FixValueParameter,
- T,
- parameter_attributes,
- get_current_timestamp(model),
- get_name(model),
- )
- # end
return
end
-"""
-Update parameter function an OperationModel
-"""
-function update_parameter_values!(
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
+ model::OperationModel,
+ key::ParameterKey{FixValueParameter, U},
+ input::DatasetContainer{InMemoryDataset},
+) where {U <: PSY.Service}
+ # Note: Do not instantite a new key here because it might not match the param keys in the container
+ # if the keys have strings in the meta fields
+ parameter_array = get_parameter_array(optimization_container, key)
+ parameter_attributes = get_parameter_attributes(optimization_container, key)
+ _update_parameter_values!(parameter_array, parameter_attributes, T, model, input)
+ _fix_parameter_value!(optimization_container, parameter_array, parameter_attributes)
+ return
+end
+
+function update_container_parameter_values!(
+ optimization_container::OptimizationContainer,
model::OperationModel,
key::ParameterKey{T, U},
input::DatasetContainer{InMemoryDataset},
) where {T <: ParameterType, U <: PSY.Service}
- # Enable again for detailed debugging
- # TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin
- optimization_container = get_optimization_container(model)
# Note: Do not instantite a new key here because it might not match the param keys in the container
# if the keys have strings in the meta fields
parameter_array = get_parameter_array(optimization_container, key)
@@ -431,32 +436,22 @@ function update_parameter_values!(
service = PSY.get_component(U, get_system(model), key.meta)
@assert service !== nothing
_update_parameter_values!(parameter_array, parameter_attributes, service, model, input)
- IS.@record :execution ParameterUpdateEvent(
- T,
- U,
- parameter_attributes,
- get_current_timestamp(model),
- get_name(model),
- )
- #end
return
end
+"""
+Update parameter function an OperationModel
+"""
function update_parameter_values!(
model::OperationModel,
key::ParameterKey{T, U},
input::DatasetContainer{InMemoryDataset},
-) where {T <: ObjectiveFunctionParameter, U <: PSY.Service}
+) where {T <: ParameterType, U <: PSY.Component}
# Enable again for detailed debugging
# TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin
optimization_container = get_optimization_container(model)
- # Note: Do not instantite a new key here because it might not match the param keys in the container
- # if the keys have strings in the meta fields
- parameter_array = get_parameter_array(optimization_container, key)
+ update_container_parameter_values!(optimization_container, model, key, input)
parameter_attributes = get_parameter_attributes(optimization_container, key)
- service = PSY.get_component(U, get_system(model), key.meta)
- @assert service !== nothing
- _update_parameter_values!(parameter_array, parameter_attributes, service, model, input)
IS.@record :execution ParameterUpdateEvent(
T,
U,
@@ -545,7 +540,7 @@ function _update_parameter_values!(
value, _ = _convert_variable_cost(value)
end
# TODO removed an apparently unused block of code here?
- _set_param_value!(parameter_array, PSY.get_raw_data(value), name, t)
+ _set_param_value!(parameter_array, value, name, t)
update_variable_cost!(
container,
parameter_array,
@@ -570,11 +565,11 @@ function _update_pwl_cost_expression(
::Type{T},
component_name::String,
time_period::Int,
- cost_data::PSY.PiecewiseLinearPointData,
+ cost_data::PSY.PiecewiseLinearData,
) where {T <: PSY.Component}
pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T)
resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
gen_cost = JuMP.AffExpr(0.0)
slopes = PSY.get_slopes(cost_data)
upb = get_breakpoint_upper_bounds(cost_data)
@@ -596,7 +591,7 @@ function update_variable_cost!(
time_period::Int,
) where {T <: PSY.Component}
resolution = get_resolution(container)
- dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
+ dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR
base_power = get_base_power(container)
component_name = PSY.get_name(component)
cost_data = parameter_array[component_name, time_period] # TODO is this a new-style cost?
@@ -631,7 +626,7 @@ function update_variable_cost!(
T,
component_name,
time_period,
- PSY.PiecewiseLinearPointData(cost_data),
+ PSY.PiecewiseLinearData(cost_data),
)
add_to_objective_variant_expression!(container, mult_ * gen_cost)
set_expression!(container, ProductionCostExpression, gen_cost, component, time_period)
diff --git a/src/services_models/agc.jl b/src/services_models/agc.jl
index 435a4e39d8..f8e37598da 100644
--- a/src/services_models/agc.jl
+++ b/src/services_models/agc.jl
@@ -56,7 +56,7 @@ function get_default_attributes(
::Type{PSY.AGC},
::Type{<:AbstractAGCFormulation},
)
- return Dict{String, Any}()
+ return Dict{String, Any}("aggregated_service_model" => false)
end
"""
@@ -77,7 +77,7 @@ end
function _get_variable_initial_value(
d::PSY.Component,
- key::ICKey,
+ key::InitialConditionKey,
::AbstractAGCFormulation,
::Nothing,
)
@@ -174,10 +174,10 @@ function add_constraints!(
::Type{T},
::Type{SteadyStateFrequencyDeviation},
agcs::IS.FlattenIteratorWrapper{U},
- ::ServiceModel{PSY.AGC, V},
+ model::ServiceModel{PSY.AGC, V},
sys::PSY.System,
) where {T <: SACEPIDAreaConstraint, U <: PSY.AGC, V <: PIDSmoothACE}
- services = get_available_components(PSY.AGC, sys)
+ services = get_available_components(model, sys)
time_steps = get_time_steps(container)
agc_names = PSY.get_name.(services)
area_names = [PSY.get_name(PSY.get_area(s)) for s in services]
@@ -196,7 +196,7 @@ function add_constraints!(
kp = PSY.get_K_p(service)
ki = PSY.get_K_i(service)
kd = PSY.get_K_d(service)
- Δt = convert(Dates.Second, container.resolution).value
+ Δt = convert(Dates.Second, get_resolution(container)).value
a = PSY.get_name(service)
for t in time_steps
if t == 1
@@ -294,3 +294,19 @@ function add_feedforward_constraints!(
end
return
end
+
+function add_proportional_cost!(
+ container::OptimizationContainer,
+ ::U,
+ agcs::IS.FlattenIteratorWrapper{T},
+ ::PIDSmoothACE,
+) where {T <: PSY.AGC, U <: LiftVariable}
+ lift_variable = get_variable(container, U(), T)
+ for index in Iterators.product(axes(lift_variable)...)
+ add_to_objective_invariant_expression!(
+ container,
+ SERVICES_SLACK_COST * lift_variable[index...],
+ )
+ end
+ return
+end
diff --git a/src/services_models/reserve_group.jl b/src/services_models/reserve_group.jl
index a893426c51..f02d99af47 100644
--- a/src/services_models/reserve_group.jl
+++ b/src/services_models/reserve_group.jl
@@ -1,11 +1,11 @@
function get_default_time_series_names(
- ::Type{PSY.StaticReserveGroup{T}},
+ ::Type{PSY.ConstantReserveGroup{T}},
::Type{GroupReserve}) where {T <: PSY.ReserveDirection}
return Dict{String, Any}()
end
function get_default_attributes(
- ::Type{PSY.StaticReserveGroup{T}},
+ ::Type{PSY.ConstantReserveGroup{T}},
::Type{GroupReserve}) where {T <: PSY.ReserveDirection}
return Dict{String, Any}()
end
@@ -39,7 +39,7 @@ function add_constraints!(
service::SR,
contributing_services::Vector{<:PSY.Service},
model::ServiceModel{SR, GroupReserve},
-) where {SR <: PSY.StaticReserveGroup}
+) where {SR <: PSY.ConstantReserveGroup}
time_steps = get_time_steps(container)
service_name = PSY.get_name(service)
add_constraints_container!(
diff --git a/src/services_models/reserves.jl b/src/services_models/reserves.jl
index cee5ef7f5b..af42f52310 100644
--- a/src/services_models/reserves.jl
+++ b/src/services_models/reserves.jl
@@ -29,7 +29,7 @@ get_multiplier_value(::RequirementTimeSeriesParameter, d::PSY.ReserveNonSpinning
get_parameter_multiplier(::VariableValueParameter, d::Type{<:PSY.AbstractReserve}, ::AbstractReservesFormulation) = 1.0
get_initial_parameter_value(::VariableValueParameter, d::Type{<:PSY.AbstractReserve}, ::AbstractReservesFormulation) = 0.0
-objective_function_multiplier(::ServiceRequirementVariable, ::StepwiseCostReserve) = 1.0
+objective_function_multiplier(::ServiceRequirementVariable, ::StepwiseCostReserve) = -1.0
sos_status(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=SOSStatusVariable.NO_VARIABLE
uses_compact_power(::PSY.ReserveDemandCurve, ::StepwiseCostReserve)=false
#! format: on
@@ -87,6 +87,40 @@ function get_default_attributes(
return Dict{String, Any}()
end
+"""
+Add variables for ServiceRequirementVariable for StepWiseCostReserve
+"""
+function add_variable!(
+ container::OptimizationContainer,
+ variable_type::T,
+ service::D,
+ formulation,
+) where {
+ T <: ServiceRequirementVariable,
+ D <: PSY.ReserveDemandCurve,
+}
+ time_steps = get_time_steps(container)
+ service_name = PSY.get_name(service)
+ variable = add_variable_container!(
+ container,
+ variable_type,
+ D,
+ [service_name],
+ time_steps;
+ meta = service_name,
+ )
+
+ for t in time_steps
+ variable[service_name, t] = JuMP.@variable(
+ get_jump_model(container),
+ base_name = "$(T)_$(D)_$(service_name)_{$(service_name), $(t)}",
+ lower_bound = 0.0,
+ )
+ end
+
+ return
+end
+
################################## Reserve Requirement Constraint ##########################
function add_constraints!(
container::OptimizationContainer,
@@ -210,7 +244,7 @@ function add_constraints!(
::U,
model::ServiceModel{SR, V},
) where {
- SR <: PSY.StaticReserve,
+ SR <: PSY.ConstantReserve,
V <: AbstractReservesFormulation,
U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}},
} where {D <: PSY.Component}
@@ -276,7 +310,8 @@ function add_constraints!(
)
reserve_variable =
get_variable(container, ActivePowerReserveVariable(), SR, service_name)
- requirement_variable = get_variable(container, ServiceRequirementVariable(), SR)
+ requirement_variable =
+ get_variable(container, ServiceRequirementVariable(), SR, service_name)
jump_model = get_jump_model(container)
for t in time_steps
constraint[service_name, t] = JuMP.@constraint(
@@ -458,3 +493,68 @@ function objective_function!(
add_variable_cost!(container, ServiceRequirementVariable(), service, SR())
return
end
+
+function add_variable_cost!(
+ container::OptimizationContainer,
+ ::U,
+ service::T,
+ ::V,
+) where {T <: PSY.ReserveDemandCurve, U <: VariableType, V <: StepwiseCostReserve}
+ _add_variable_cost_to_objective!(container, U(), service, V())
+ return
+end
+
+function _add_variable_cost_to_objective!(
+ container::OptimizationContainer,
+ ::T,
+ component::PSY.Reserve,
+ ::U,
+) where {T <: VariableType, U <: StepwiseCostReserve}
+ component_name = PSY.get_name(component)
+ @debug "PWL Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name
+ # If array is full of tuples with zeros return 0.0
+ time_steps = get_time_steps(container)
+ variable_cost = PSY.get_variable(component)
+ if variable_cost isa Nothing
+ error("ReserveDemandCurve $(component.name) does not have cost data.")
+ elseif typeof(variable_cost) <: PSY.TimeSeriesKey
+ error(
+ "Timeseries curve for ReserveDemandCurve $(component.name) is not supported yet.",
+ )
+ end
+
+ pwl_cost_expressions =
+ _add_pwl_term!(container, component, variable_cost, T(), U())
+ for t in time_steps
+ add_to_expression!(
+ container,
+ ProductionCostExpression,
+ pwl_cost_expressions[t],
+ component,
+ t,
+ )
+ add_to_objective_invariant_expression!(container, pwl_cost_expressions[t])
+ end
+ return
+end
+
+function add_proportional_cost!(
+ container::OptimizationContainer,
+ ::U,
+ service::T,
+ ::V,
+) where {
+ T <: Union{PSY.Reserve, PSY.ReserveNonSpinning},
+ U <: ActivePowerReserveVariable,
+ V <: AbstractReservesFormulation,
+}
+ base_p = get_base_power(container)
+ reserve_variable = get_variable(container, U(), T, PSY.get_name(service))
+ for index in Iterators.product(axes(reserve_variable)...)
+ add_to_objective_invariant_expression!(
+ container,
+ DEFAULT_RESERVE_COST / base_p * reserve_variable[index...],
+ )
+ end
+ return
+end
diff --git a/src/services_models/services_constructor.jl b/src/services_models/services_constructor.jl
index 6737e3c621..09d9c9d2ec 100644
--- a/src/services_models/services_constructor.jl
+++ b/src/services_models/services_constructor.jl
@@ -18,6 +18,7 @@ function construct_services!(
stage::ArgumentConstructStage,
services_template::ServicesModelContainer,
devices_template::DevicesModelContainer,
+ network_model::NetworkModel{<:PM.AbstractPowerModel},
)
isempty(services_template) && return
incompatible_device_types = get_incompatible_devices(devices_template)
@@ -37,6 +38,7 @@ function construct_services!(
service_model,
devices_template,
incompatible_device_types,
+ network_model,
)
end
groupservice === nothing || construct_service!(
@@ -46,6 +48,7 @@ function construct_services!(
services_template[groupservice],
devices_template,
incompatible_device_types,
+ network_model,
)
return
end
@@ -56,6 +59,7 @@ function construct_services!(
stage::ModelConstructStage,
services_template::ServicesModelContainer,
devices_template::DevicesModelContainer,
+ network_model::NetworkModel{<:PM.AbstractPowerModel},
)
isempty(services_template) && return
incompatible_device_types = get_incompatible_devices(devices_template)
@@ -74,6 +78,7 @@ function construct_services!(
service_model,
devices_template,
incompatible_device_types,
+ network_model,
)
end
groupservice === nothing || construct_service!(
@@ -83,6 +88,7 @@ function construct_services!(
services_template[groupservice],
devices_template,
incompatible_device_types,
+ network_model,
)
return
end
@@ -94,6 +100,7 @@ function construct_service!(
model::ServiceModel{SR, RangeReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -119,6 +126,7 @@ function construct_service!(
model::ServiceModel{SR, RangeReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -148,7 +156,8 @@ function construct_service!(
model::ServiceModel{SR, RangeReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
-) where {SR <: PSY.StaticReserve}
+ ::NetworkModel{<:PM.AbstractPowerModel},
+) where {SR <: PSY.ConstantReserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
contributing_devices = get_contributing_devices(model)
@@ -172,7 +181,8 @@ function construct_service!(
model::ServiceModel{SR, RangeReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
-) where {SR <: PSY.StaticReserve}
+ ::NetworkModel{<:PM.AbstractPowerModel},
+) where {SR <: PSY.ConstantReserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
contributing_devices = get_contributing_devices(model)
@@ -200,11 +210,12 @@ function construct_service!(
model::ServiceModel{SR, StepwiseCostReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
contributing_devices = get_contributing_devices(model)
- add_variable!(container, ServiceRequirementVariable(), [service], StepwiseCostReserve())
+ add_variable!(container, ServiceRequirementVariable(), service, StepwiseCostReserve())
add_variables!(
container,
ActivePowerReserveVariable,
@@ -224,6 +235,7 @@ function construct_service!(
model::ServiceModel{SR, StepwiseCostReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -246,8 +258,9 @@ function construct_service!(
model::ServiceModel{S, T},
devices_template::Dict{Symbol, DeviceModel},
::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {S <: PSY.AGC, T <: AbstractAGCFormulation}
- services = get_available_components(S, sys)
+ services = get_available_components(model, sys)
agc_areas = PSY.get_area.(services)
areas = PSY.get_components(PSY.Area, sys)
if !isempty(setdiff(areas, agc_areas))
@@ -299,9 +312,10 @@ function construct_service!(
model::ServiceModel{S, T},
devices_template::Dict{Symbol, DeviceModel},
::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {S <: PSY.AGC, T <: AbstractAGCFormulation}
areas = PSY.get_components(PSY.Area, sys)
- services = get_available_components(S, sys)
+ services = get_available_components(model, sys)
add_constraints!(container, AbsoluteValueConstraint, LiftVariable, services, model)
add_constraints!(
@@ -331,7 +345,7 @@ function construct_service!(
end
"""
- Constructs a service for StaticReserveGroup.
+ Constructs a service for ConstantReserveGroup.
"""
function construct_service!(
container::OptimizationContainer,
@@ -340,7 +354,8 @@ function construct_service!(
model::ServiceModel{SR, GroupReserve},
::Dict{Symbol, DeviceModel},
::Set{<:DataType},
-) where {SR <: PSY.StaticReserveGroup}
+ ::NetworkModel{<:PM.AbstractPowerModel},
+) where {SR <: PSY.ConstantReserveGroup}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
contributing_services = PSY.get_contributing_services(service)
@@ -357,7 +372,8 @@ function construct_service!(
model::ServiceModel{SR, GroupReserve},
::Dict{Symbol, DeviceModel},
::Set{<:DataType},
-) where {SR <: PSY.StaticReserveGroup}
+ ::NetworkModel{<:PM.AbstractPowerModel},
+) where {SR <: PSY.ConstantReserveGroup}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
contributing_services = PSY.get_contributing_services(service)
@@ -381,6 +397,7 @@ function construct_service!(
model::ServiceModel{SR, RampReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -406,6 +423,7 @@ function construct_service!(
model::ServiceModel{SR, RampReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.Reserve}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -436,6 +454,7 @@ function construct_service!(
model::ServiceModel{SR, NonSpinningReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.ReserveNonSpinning}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -460,6 +479,7 @@ function construct_service!(
model::ServiceModel{SR, NonSpinningReserve},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ ::NetworkModel{<:PM.AbstractPowerModel},
) where {SR <: PSY.ReserveNonSpinning}
name = get_service_name(model)
service = PSY.get_component(SR, sys, name)
@@ -497,8 +517,9 @@ function construct_service!(
model::ServiceModel{T, ConstantMaxInterfaceFlow},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ network_model::NetworkModel{<:PM.AbstractPowerModel},
) where {T <: PSY.TransmissionInterface}
- interfaces = get_available_components(T, sys)
+ interfaces = get_available_components(model, sys)
if get_use_slacks(model)
# Adding the slacks can be done in a cleaner fashion
interface = PSY.get_component(T, sys, get_service_name(model))
@@ -524,6 +545,98 @@ function construct_service!(
model::ServiceModel{T, ConstantMaxInterfaceFlow},
devices_template::Dict{Symbol, DeviceModel},
incompatible_device_types::Set{<:DataType},
+ network_model::NetworkModel{<:PM.AbstractActivePowerModel},
+) where {T <: PSY.TransmissionInterface}
+ name = get_service_name(model)
+ service = PSY.get_component(T, sys, name)
+
+ add_to_expression!(
+ container,
+ InterfaceTotalFlow,
+ FlowActivePowerVariable,
+ service,
+ model,
+ )
+
+ if get_use_slacks(model)
+ add_to_expression!(
+ container,
+ InterfaceTotalFlow,
+ InterfaceFlowSlackUp,
+ service,
+ model,
+ )
+ add_to_expression!(
+ container,
+ InterfaceTotalFlow,
+ InterfaceFlowSlackDown,
+ service,
+ model,
+ )
+ end
+
+ add_constraints!(container, InterfaceFlowLimit, service, model)
+ add_feedforward_constraints!(container, model, service)
+ add_constraint_dual!(container, sys, model)
+ objective_function!(container, service, model)
+ return
+end
+
+function construct_service!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ArgumentConstructStage,
+ model::ServiceModel{T, VariableMaxInterfaceFlow},
+ devices_template::Dict{Symbol, DeviceModel},
+ incompatible_device_types::Set{<:DataType},
+ network_model::NetworkModel{<:PM.AbstractPowerModel},
+) where {T <: PSY.TransmissionInterface}
+ interfaces = get_available_components(model, sys)
+ if get_use_slacks(model)
+ # Adding the slacks can be done in a cleaner fashion
+ interface = PSY.get_component(T, sys, get_service_name(model))
+ @assert PSY.get_available(interface)
+ transmission_interface_slacks!(container, interface)
+ end
+ # Lazy container addition for the expressions.
+ lazy_container_addition!(
+ container,
+ InterfaceTotalFlow(),
+ T,
+ PSY.get_name.(interfaces),
+ get_time_steps(container),
+ )
+ has_ts = PSY.has_time_series.(interfaces)
+ if any(has_ts) && !all(has_ts)
+ error(
+ "Not all TransmissionInterfaces devices have time series. Check data to complete (or remove) time series.",
+ )
+ end
+ if all(has_ts)
+ for device in interfaces
+ name = PSY.get_name(device)
+ num_ts = length(unique(PSY.get_name.(PSY.get_time_series_keys(device))))
+ if num_ts < 2
+ error(
+ "TransmissionInterface $name has less than two time series. It is required to add both min_flow and max_flow time series.",
+ )
+ end
+ add_parameters!(container, MinInterfaceFlowLimitParameter, device, model)
+ add_parameters!(container, MaxInterfaceFlowLimitParameter, device, model)
+ end
+ end
+ #add_feedforward_arguments!(container, model, service)
+ return
+end
+
+function construct_service!(
+ container::OptimizationContainer,
+ sys::PSY.System,
+ ::ModelConstructStage,
+ model::ServiceModel{T, VariableMaxInterfaceFlow},
+ devices_template::Dict{Symbol, DeviceModel},
+ incompatible_device_types::Set{<:DataType},
+ network_model::NetworkModel{<:PM.AbstractActivePowerModel},
) where {T <: PSY.TransmissionInterface}
name = get_service_name(model)
service = PSY.get_component(T, sys, name)
diff --git a/src/services_models/transmission_interface.jl b/src/services_models/transmission_interface.jl
index 62cc9a5edb..797300be0c 100644
--- a/src/services_models/transmission_interface.jl
+++ b/src/services_models/transmission_interface.jl
@@ -6,6 +6,12 @@ get_variable_lower_bound(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface,
get_variable_multiplier(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = 1.0
get_variable_multiplier(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::ConstantMaxInterfaceFlow) = -1.0
+get_variable_multiplier(::InterfaceFlowSlackUp, ::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = 1.0
+get_variable_multiplier(::InterfaceFlowSlackDown, ::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = -1.0
+
+get_multiplier_value(::MinInterfaceFlowLimitParameter, d::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = PSY.get_min_active_power_flow_limit(d)
+get_multiplier_value(::MaxInterfaceFlowLimitParameter, d::PSY.TransmissionInterface, ::VariableMaxInterfaceFlow) = PSY.get_max_active_power_flow_limit(d)
+
#! format: On
function get_default_time_series_names(
::Type{PSY.TransmissionInterface},
@@ -14,12 +20,28 @@ function get_default_time_series_names(
return Dict{Type{<:TimeSeriesParameter}, String}()
end
+function get_default_time_series_names(
+ ::Type{PSY.TransmissionInterface},
+ ::Type{VariableMaxInterfaceFlow},
+)
+ return Dict{Type{<:TimeSeriesParameter}, String}(
+ MinInterfaceFlowLimitParameter => "min_active_power_flow_limit",
+ MaxInterfaceFlowLimitParameter => "max_active_power_flow_limit",
+ )
+end
+
function get_default_attributes(
::Type{<:PSY.TransmissionInterface},
::Type{ConstantMaxInterfaceFlow})
return Dict{String, Any}()
end
+function get_default_attributes(
+ ::Type{<:PSY.TransmissionInterface},
+ ::Type{VariableMaxInterfaceFlow})
+ return Dict{String, Any}()
+end
+
function get_initial_conditions_service_model(
::OperationModel,
::ServiceModel{T, D},
@@ -61,11 +83,62 @@ function add_constraints!(container::OptimizationContainer,
return
end
+function add_constraints!(container::OptimizationContainer,
+ ::Type{InterfaceFlowLimit},
+ interface::T,
+ model::ServiceModel{T, VariableMaxInterfaceFlow},
+) where {T <: PSY.TransmissionInterface}
+ expr = get_expression(container, InterfaceTotalFlow(), T)
+ interfaces, timesteps = axes(expr)
+ constraint_container_ub = lazy_container_addition!(
+ container,
+ InterfaceFlowLimit(),
+ T,
+ interfaces,
+ timesteps;
+ meta = "ub",
+ )
+ constraint_container_lb = lazy_container_addition!(
+ container,
+ InterfaceFlowLimit(),
+ T,
+ interfaces,
+ timesteps;
+ meta = "lb",
+ )
+ int_name = PSY.get_name(interface)
+ param_container_min =
+ get_parameter(container, MinInterfaceFlowLimitParameter(), PSY.TransmissionInterface, int_name)
+ param_multiplier_min = get_parameter_multiplier_array(
+ container,
+ MinInterfaceFlowLimitParameter(),
+ PSY.TransmissionInterface,
+ int_name,
+ )
+ param_container_max =
+ get_parameter(container, MaxInterfaceFlowLimitParameter(), PSY.TransmissionInterface, int_name)
+ param_multiplier_max = get_parameter_multiplier_array(
+ container,
+ MaxInterfaceFlowLimitParameter(),
+ PSY.TransmissionInterface,
+ int_name,
+ )
+ param_min = get_parameter_column_refs(param_container_min, int_name)
+ param_max = get_parameter_column_refs(param_container_max, int_name)
+ for t in timesteps
+ constraint_container_ub[int_name, t] =
+ JuMP.@constraint(get_jump_model(container), expr[int_name, t] <= param_multiplier_max[int_name, t] * param_max[t])
+ constraint_container_lb[int_name, t] =
+ JuMP.@constraint(get_jump_model(container), expr[int_name, t] >= param_multiplier_min[int_name, t] * param_min[t])
+ end
+ return
+end
+
function objective_function!(
container::OptimizationContainer,
service::T,
model::ServiceModel{T, U},
-) where {T <: PSY.TransmissionInterface, U <: ConstantMaxInterfaceFlow}
+) where {T <: PSY.TransmissionInterface, U <: Union{ConstantMaxInterfaceFlow, VariableMaxInterfaceFlow}}
# At the moment the interfaces have no costs associated with them
return
end
diff --git a/src/simulation/decision_model_simulation_results.jl b/src/simulation/decision_model_simulation_results.jl
index 166246c1ff..bf4f94a10e 100644
--- a/src/simulation/decision_model_simulation_results.jl
+++ b/src/simulation/decision_model_simulation_results.jl
@@ -41,7 +41,7 @@ function SimulationProblemResults(
ResultsByKeyAndTime(
list_decision_model_keys(store, name, STORE_CONTAINER_EXPRESSIONS),
),
- get_horizon(problem_params),
+ get_horizon_count(problem_params),
container_key_lookup,
);
kwargs...,
@@ -88,6 +88,27 @@ get_cached_parameters(res::SimulationProblemResults{DecisionModelSimulationResul
get_cached_variables(res::SimulationProblemResults{DecisionModelSimulationResults}) =
res.values.variables.cached_results
+get_cached_results(
+ res::SimulationProblemResults{DecisionModelSimulationResults},
+ ::AuxVarKey,
+) = get_cached_aux_variables(res)
+get_cached_results(
+ res::SimulationProblemResults{DecisionModelSimulationResults},
+ ::ConstraintKey,
+) = get_cached_duals(res)
+get_cached_results(
+ res::SimulationProblemResults{DecisionModelSimulationResults},
+ ::ExpressionKey,
+) = get_cached_expressions(res)
+get_cached_results(
+ res::SimulationProblemResults{DecisionModelSimulationResults},
+ ::ParameterKey,
+) = get_cached_parameters(res)
+get_cached_results(
+ res::SimulationProblemResults{DecisionModelSimulationResults},
+ ::VariableKey,
+) = get_cached_variables(res)
+
function get_forecast_horizon(res::SimulationProblemResults{DecisionModelSimulationResults})
return res.values.forecast_horizon
end
@@ -237,19 +258,20 @@ function _read_results(
res::SimulationProblemResults{DecisionModelSimulationResults},
result_keys,
timestamps::Vector{Dates.DateTime},
- store::Union{Nothing, <:SimulationStore},
+ store::Union{Nothing, <:SimulationStore};
+ cols::Union{Colon, Vector{String}} = (:),
)
vals = _read_results(res, result_keys, timestamps, store)
converted_vals = Dict{OptimizationContainerKey, ResultsByTime{Matrix{Float64}}}()
for (result_key, result_data) in vals
inner_converted = SortedDict(
- (date_key, Matrix{Float64}(permutedims(inner_data.data)))
+ (date_key, Matrix{Float64}(permutedims(inner_data[cols, :].data)))
for (date_key, inner_data) in result_data.data)
converted_vals[result_key] = ResultsByTime{Matrix{Float64}, 1}(
result_data.key,
inner_converted,
result_data.resolution,
- result_data.column_names)
+ (cols isa Vector) ? (cols,) : result_data.column_names)
end
return converted_vals
end
@@ -263,12 +285,9 @@ function _read_results(
isempty(result_keys) &&
return Dict{OptimizationContainerKey, ResultsByTime{DenseAxisArray{Float64, 2}}}()
- if store === nothing && res.store !== nothing
- # In this case we have an InMemorySimulationStore.
- store = res.store
- end
+ _store = try_resolve_store(store, res.store)
existing_keys = list_result_keys(res, first(result_keys))
- _validate_keys(existing_keys, result_keys)
+ IS.Optimization._validate_keys(existing_keys, result_keys)
cached_results = get_cached_results(res, eltype(result_keys))
if _are_results_cached(res, result_keys, timestamps, keys(cached_results))
@debug "reading results from SimulationsResults cache" # NOTE tests match on this
@@ -287,7 +306,7 @@ function _read_results(
return filtered_vals
else
@debug "reading results from data store" # NOTE tests match on this
- vals = _get_store_value(res, result_keys, timestamps, store)
+ vals = _get_store_value(res, result_keys, timestamps, _store)
end
return vals
end
@@ -450,15 +469,32 @@ function get_realized_timestamps(
return requested_range
end
+"""
+High-level function to read a DataFrame of results.
+
+# Arguments
+
+ - `res`: the results to read.
+ - `result_keys::Vector{<:OptimizationContainerKey}`: the keys to read. Output will be a
+ `Dict{OptimizationContainerKey, DataFrame}` with these as the keys
+ - `start_time::Union{Nothing, Dates.DateTime} = nothing`: the time at which the resulting
+ time series should begin; `nothing` indicates the first time in the results
+ - `len::Union{Int, Nothing} = nothing`: the number of steps in the resulting time series;
+ `nothing` indicates up to the end of the results
+ - `cols::Union{Colon, Vector{String}} = (:)`: which columns to fetch; defaults to `:`,
+ i.e., all the columns
+"""
function read_results_with_keys(
res::SimulationProblemResults{DecisionModelSimulationResults},
result_keys::Vector{<:OptimizationContainerKey};
start_time::Union{Nothing, Dates.DateTime} = nothing,
len::Union{Int, Nothing} = nothing,
+ cols::Union{Colon, Vector{String}} = (:),
)
meta = RealizedMeta(res; start_time = start_time, len = len)
timestamps = _process_timestamps(res, meta.start_time, meta.len)
- result_values = _read_results(Matrix{Float64}, res, result_keys, timestamps, nothing)
+ result_values =
+ _read_results(Matrix{Float64}, res, result_keys, timestamps, nothing; cols = cols)
return get_realization(result_values, meta)
end
@@ -508,34 +544,24 @@ function load_results!(
parameters = Vector{Tuple}(),
aux_variables = Vector{Tuple}(),
expressions = Vector{Tuple}(),
+ store::Union{Nothing, <:SimulationStore} = nothing,
)
initial_time = initial_time === nothing ? first(get_timestamps(res)) : initial_time
count = max(count, length(get_results_timestamps(res)))
new_timestamps = _process_timestamps(res, initial_time, count)
- function merge_results(store)
- for (key_type, new_items) in [
- (ConstraintKey, duals),
- (ParameterKey, parameters),
- (VariableKey, variables),
- (AuxVarKey, aux_variables),
- (ExpressionKey, expressions),
- ]
- new_keys = key_type[_deserialize_key(key_type, res, x...) for x in new_items]
- existing_results = get_cached_results(res, key_type)
- total_keys = union(collect(keys(existing_results)), new_keys)
- # _read_results checks the cache to eliminate unnecessary re-reads
- merge!(existing_results, _read_results(res, total_keys, new_timestamps, store))
- end
- end
-
- if res.store isa InMemorySimulationStore
- merge_results(res.store)
- else
- simulation_store_path = joinpath(res.execution_path, "data_store")
- open_store(HdfSimulationStore, simulation_store_path, "r") do store
- merge_results(store)
- end
+ for (key_type, new_items) in [
+ (ConstraintKey, duals),
+ (ParameterKey, parameters),
+ (VariableKey, variables),
+ (AuxVarKey, aux_variables),
+ (ExpressionKey, expressions),
+ ]
+ new_keys = key_type[_deserialize_key(key_type, res, x...) for x in new_items]
+ existing_results = get_cached_results(res, key_type)
+ total_keys = union(collect(keys(existing_results)), new_keys)
+ # _read_results checks the cache to eliminate unnecessary re-reads
+ merge!(existing_results, _read_results(res, total_keys, new_timestamps, store))
end
set_results_timestamps!(res, new_timestamps)
diff --git a/src/simulation/emulation_model_simulation_results.jl b/src/simulation/emulation_model_simulation_results.jl
index ad5a013e71..8eef2be960 100644
--- a/src/simulation/emulation_model_simulation_results.jl
+++ b/src/simulation/emulation_model_simulation_results.jl
@@ -197,13 +197,9 @@ function _read_results(
len = nothing,
)
isempty(result_keys) && return Dict{OptimizationContainerKey, DataFrames.DataFrame}()
- if store === nothing && res.store !== nothing
- # In this case we have an InMemorySimulationStore.
- store = res.store
- end
-
+ _store = try_resolve_store(store, res.store)
existing_keys = list_result_keys(res, first(result_keys))
- _validate_keys(existing_keys, result_keys)
+ IS.Optimization._validate_keys(existing_keys, result_keys)
cached_results = Dict(
k => v for
(k, v) in get_cached_results(res, eltype(result_keys)) if !isempty(v)
@@ -217,7 +213,7 @@ function _read_results(
_get_store_value(
res,
result_keys,
- store;
+ _store;
start_time = start_time,
len = len,
)
diff --git a/src/simulation/hdf_simulation_store.jl b/src/simulation/hdf_simulation_store.jl
index 560a2922a5..ded32ecadb 100644
--- a/src/simulation/hdf_simulation_store.jl
+++ b/src/simulation/hdf_simulation_store.jl
@@ -231,7 +231,7 @@ Return the optimizer stats for a problem as a DataFrame.
function read_optimizer_stats(store::HdfSimulationStore, model_name)
dataset = _get_dataset(OptimizerStats, store, model_name)
data = permutedims(dataset[:, :])
- stats = [to_namedtuple(OptimizerStats(data[i, :])) for i in axes(data)[1]]
+ stats = [IS.to_namedtuple(OptimizerStats(data[i, :])) for i in axes(data)[1]]
return DataFrames.DataFrame(stats)
end
@@ -507,7 +507,7 @@ function _read_result(
#end
columns = get_column_names(key, dataset)
data = permutedims(data)
- @assert_op size(data)[2] == length(columns)
+ @assert_op size(data)[2] == length(columns[1])
@assert_op size(data)[1] == 1
return data, columns
end
@@ -644,41 +644,15 @@ function write_result!(
key::OptimizationContainerKey,
index::EmulationModelIndexType,
simulation_time::Dates.DateTime,
- data::Array{Float64},
+ data::DenseAxisArray,
)
dataset = _get_em_dataset(store, key)
- _write_dataset!(dataset.values, data, index)
+ _write_dataset!(dataset.values, to_matrix(data), index)
set_last_recorded_row!(dataset, index)
set_update_timestamp!(dataset, simulation_time)
return
end
-function write_result!(
- store::HdfSimulationStore,
- model_name::Symbol,
- key::OptimizationContainerKey,
- index::EmulationModelIndexType,
- simulation_time::Dates.DateTime,
- data::DenseAxisArray{Float64, 2},
-)
- data_array = Array{Float64, 3}(undef, size(data)[1], size(data)[2], 1)
- data_array[:, :, 1] = data
- write_result!(store, model_name, key, index, simulation_time, data_array)
- return
-end
-
-function write_result!(
- store::HdfSimulationStore,
- model_name::Symbol,
- key::OptimizationContainerKey,
- index::EmulationModelIndexType,
- simulation_time::Dates.DateTime,
- data::DenseAxisArray{Float64, 1},
-)
- write_result!(store, model_name, key, index, simulation_time, to_matrix(data))
- return
-end
-
function serialize_system!(store::HdfSimulationStore, sys::PSY.System)
root = store.file[HDF_SIMULATION_ROOT_PATH]
systems_group = _get_group_or_create(root, "systems")
@@ -766,11 +740,17 @@ function _deserialize_attributes!(store::HdfSimulationStore)
empty!(get_dm_data(store))
for model in HDF5.read(HDF5.attributes(group)["problem_order"])
problem_group = store.file["simulation/decision_models/$model"]
- horizon = HDF5.read(HDF5.attributes(problem_group)["horizon"])
+ # Fall back on old key for backwards compatibility
+ horizon_count = HDF5.read(
+ if haskey(HDF5.attributes(problem_group), "horizon_count")
+ HDF5.attributes(problem_group)["horizon_count"]
+ else
+ HDF5.attributes(problem_group)["horizon"]
+ end)
model_name = Symbol(model)
store.params.decision_models_params[model_name] = ModelStoreParams(
HDF5.read(HDF5.attributes(problem_group)["num_executions"]),
- horizon,
+ horizon_count,
Dates.Millisecond(HDF5.read(HDF5.attributes(problem_group)["interval_ms"])),
Dates.Millisecond(HDF5.read(HDF5.attributes(problem_group)["resolution_ms"])),
HDF5.read(HDF5.attributes(problem_group)["base_power"]),
@@ -785,7 +765,7 @@ function _deserialize_attributes!(store::HdfSimulationStore)
column_dataset = group[_make_column_name(name)]
resolution =
get_resolution(get_decision_model_params(store, model_name))
- dims = (horizon, size(dataset)[2:end]..., size(dataset)[1])
+ dims = (horizon_count, size(dataset)[2:end]..., size(dataset)[1])
n_dims = max(1, ndims(dataset) - 2)
item = HDF5Dataset{n_dims}(
dataset,
@@ -811,12 +791,18 @@ function _deserialize_attributes!(store::HdfSimulationStore)
end
em_group = _get_emulation_model_path(store)
- horizon = HDF5.read(HDF5.attributes(em_group)["horizon"])
+ # Fall back on old key for backwards compatibility
+ horizon_count = HDF5.read(
+ if haskey(HDF5.attributes(em_group), "horizon_count")
+ HDF5.attributes(em_group)["horizon_count"]
+ else
+ HDF5.attributes(em_group)["horizon"]
+ end)
model_name = Symbol(HDF5.read(HDF5.attributes(em_group)["name"]))
resolution = Dates.Millisecond(HDF5.read(HDF5.attributes(em_group)["resolution_ms"]))
store.params.emulation_model_params[model_name] = ModelStoreParams(
HDF5.read(HDF5.attributes(em_group)["num_executions"]),
- HDF5.read(HDF5.attributes(em_group)["horizon"]),
+ horizon_count,
Dates.Millisecond(HDF5.read(HDF5.attributes(em_group)["interval_ms"])),
resolution,
HDF5.read(HDF5.attributes(em_group)["base_power"]),
@@ -828,7 +814,7 @@ function _deserialize_attributes!(store::HdfSimulationStore)
if !endswith(name, "columns")
dataset = group[name]
column_dataset = group[_make_column_name(name)]
- dims = (horizon, size(dataset)[2:end]..., size(dataset)[1])
+ dims = (horizon_count, size(dataset)[2:end]..., size(dataset)[1])
n_dims = max(1, ndims(dataset) - 1)
item = HDF5Dataset{n_dims}(
dataset,
@@ -862,8 +848,8 @@ function _serialize_attributes(store::HdfSimulationStore)
problem_group = store.file["simulation/decision_models/$problem"]
HDF5.attributes(problem_group)["num_executions"] =
params.decision_models_params[problem].num_executions
- HDF5.attributes(problem_group)["horizon"] =
- params.decision_models_params[problem].horizon
+ HDF5.attributes(problem_group)["horizon_count"] =
+ params.decision_models_params[problem].horizon_count
HDF5.attributes(problem_group)["resolution_ms"] =
Dates.Millisecond(params.decision_models_params[problem].resolution).value
HDF5.attributes(problem_group)["interval_ms"] =
@@ -880,7 +866,7 @@ function _serialize_attributes(store::HdfSimulationStore)
HDF5.attributes(emulation_group)["name"] =
string(first(keys(params.emulation_model_params)))
HDF5.attributes(emulation_group)["num_executions"] = em_params.num_executions
- HDF5.attributes(emulation_group)["horizon"] = em_params.horizon
+ HDF5.attributes(emulation_group)["horizon_count"] = em_params.horizon_count
HDF5.attributes(emulation_group)["resolution_ms"] =
Dates.Millisecond(em_params.resolution).value
HDF5.attributes(emulation_group)["interval_ms"] =
@@ -1041,60 +1027,44 @@ function _read_length(::Type{OptimizerStats}, store::HdfSimulationStore)
return HDF5.read(HDF5.attributes(dataset), "columns")
end
+# Specific data set writing function that writes decision model data. It dispatches on the index type of the dataset as a range
function _write_dataset!(
dataset::HDF5.Dataset,
- array::Matrix{Float64},
- row_range::UnitRange{Int64},
- ::Val{3},
-)
- dataset[:, 1, row_range] = array
- @debug "wrote dataset" dataset row_range
- return
-end
-
-function _write_dataset!(
- dataset::HDF5.Dataset,
- array::Matrix{Float64},
+ array::Array{Float64, 3},
row_range::UnitRange{Int64},
- ::Val{2},
)
- dataset[row_range, :] = array
- @debug "wrote dataset" dataset row_range
+ dataset[:, :, row_range] = array
+ @debug "wrote dm dataset" dataset row_range
return
end
function _write_dataset!(
dataset::HDF5.Dataset,
- array::Array{Float64, 3},
+ array::Array{Float64, 4},
row_range::UnitRange{Int64},
- ::Val{3},
)
- dataset[row_range, :, :] = array
- @debug "wrote dataset" dataset row_range
- return
-end
-
-function _write_dataset!(dataset::HDF5.Dataset, array::Array{Float64}, index::Int)
- _write_dataset!(dataset, array, index:index, Val{ndims(dataset)}())
+ dataset[:, :, :, row_range] = array
+ @debug "wrote dm dataset" dataset row_range
return
end
+# Specific data set writing function that writes emulation model data. It dispatches on the index type of the dataset
function _write_dataset!(
dataset::HDF5.Dataset,
- array::Array{Float64, 3},
- row_range::UnitRange{Int64},
+ array::Array{Float64, 2},
+ index::EmulationModelIndexType,
)
- dataset[:, :, row_range] = array
- @debug "wrote dataset" dataset row_range
+ dataset[index, :] = array
+ @debug "wrote em dataset" dataset index
return
end
function _write_dataset!(
dataset::HDF5.Dataset,
array::Array{Float64, 4},
- row_range::UnitRange{Int64},
+ index::EmulationModelIndexType,
)
- dataset[:, :, :, row_range] = array
- @debug "wrote dataset" dataset row_range
+ dataset[index, :, :] = array
+ @debug "wrote em dataset" dataset index
return
end
diff --git a/src/simulation/in_memory_simulation_store.jl b/src/simulation/in_memory_simulation_store.jl
index 7fbe74d8e8..aedf8ac799 100644
--- a/src/simulation/in_memory_simulation_store.jl
+++ b/src/simulation/in_memory_simulation_store.jl
@@ -73,11 +73,14 @@ function list_decision_model_keys(
model_name::Symbol,
container_type::Symbol,
)
- return list_fields(_get_model_results(store, model_name), container_type)
+ return IS.Optimization.list_fields(
+ _get_model_results(store, model_name),
+ container_type,
+ )
end
function list_emulation_model_keys(store::InMemorySimulationStore, container_type::Symbol)
- return list_fields(store.em_data, container_type)
+ return IS.Optimization.list_fields(store.em_data, container_type)
end
function write_optimizer_stats!(
diff --git a/src/simulation/simulation.jl b/src/simulation/simulation.jl
index cac6d731de..68a2ab172c 100644
--- a/src/simulation/simulation.jl
+++ b/src/simulation/simulation.jl
@@ -74,7 +74,7 @@ mutable struct Simulation
initial_time = nothing,
)
for model in get_decision_models(models)
- if model.internal.simulation_info.sequence_uuid != sequence.uuid
+ if get_sequence_uuid(model) != sequence.uuid
model_name = get_name(model)
throw(
IS.ConflictingInputsError(
@@ -85,7 +85,7 @@ mutable struct Simulation
end
em = get_emulation_model(models)
if em !== nothing
- if em.internal.simulation_info.sequence_uuid != sequence.uuid
+ if get_sequence_uuid(em) != sequence.uuid
model_name = get_name(em)
throw(
IS.ConflictingInputsError(
@@ -159,7 +159,7 @@ get_console_level(sim::Simulation) = sim.internal.console_level
get_file_level(sim::Simulation) = sim.internal.file_level
set_simulation_status!(sim::Simulation, status) = sim.internal.status = status
-set_simulation_build_status!(sim::Simulation, status::BuildStatus) =
+set_simulation_build_status!(sim::Simulation, status::SimulationBuildStatus) =
sim.internal.build_status = status
function set_current_time!(sim::Simulation, val::Dates.DateTime)
@@ -212,7 +212,7 @@ Manually provided initial times have to be compatible with the specified interva
system = get_system(get_models(sim).emulation_model)
ini_time, ts_length =
PSY.check_time_series_consistency(system, PSY.SingleTimeSeries)
- resolution = PSY.get_time_series_resolution(system)
+ resolution = get_resolution(em)
em_available_times = range(ini_time; step = resolution, length = ts_length)
if get_initial_time(sim) ∉ em_available_times
throw(
@@ -266,30 +266,98 @@ function _check_folder(sim::Simulation)
end
end
+# Compare initial conditions for all `InitialConditionType`s with the
+# `requires_reconciliation` trait across `models`, log @info messages for mismatches
+function _initial_conditions_reconciliation!(
+ models::Vector{<:OperationModel})
+ model_names = get_name.(models)
+ has_mismatches = false
+ @info "Reconciling initial conditions across models $(join(model_names, ", "))"
+ # all_ic_keys: all the `ICKey`s that appear in any of the models
+ all_ic_keys = union(keys.(get_initial_conditions.(models))...)
+ # all_ic_values: Dict{ICKey, Dict{model_index, Dict{component_name, ic_value}}}
+ all_ic_values = Dict()
+ for ic_key in all_ic_keys
+ if !requires_reconciliation(get_entry_type(ic_key))
+ @debug "Skipping initial conditions reconciliation for $(get_entry_type(ic_key)) due to false requires_reconciliation"
+ continue
+ end
+ # ic_vals_per_model: Dict{model_index, Dict{component_name, ic_value}}
+ ic_vals_per_model = Dict()
+ for (i, model) in enumerate(models)
+ ics = PSI.get_initial_conditions(model)
+ haskey(ics, ic_key) || continue
+ # ic_vals_per_component: Dict{component_name, ic_value}
+ ic_vals_per_component =
+ Dict(get_name(get_component(ic)) => get_condition(ic) for ic in ics[ic_key])
+ ic_vals_per_model[i] = ic_vals_per_component
+ end
+
+ # Assert that all models have the same components for current ic_key
+ @assert allequal(Set.(keys.(values(ic_vals_per_model)))) "For IC key $ic_key, not all models have the same components"
+
+ # For each component in current ic_key, compare values across models
+ component_names = keys(first(values(ic_vals_per_model)))
+ for component_name in component_names
+ all_values = [result[component_name] for result in values(ic_vals_per_model)]
+ ref_value = first(all_values)
+ if !allequal(isapprox.(all_values, ref_value; atol = ABSOLUTE_TOLERANCE))
+ has_mismatches = true
+ mismatch_msg = "For IC key $ic_key, mismatch on component $component_name:"
+ for (model_i, result) in sort(pairs(ic_vals_per_model); by = first)
+ mismatch_msg *= "\n\t$(model_names[model_i]): $(result[component_name])"
+ end
+ @info mismatch_msg
+ end
+ end
+ all_ic_values[ic_key] = ic_vals_per_model
+ end
+
+ # TODO now that we have found the initial conditions mismatches, we must fix them
+ if has_mismatches
+ @warn "Models have initial condition mismatches; reconciliation is not yet implemented"
+ end
+
+ return all_ic_values
+end
+
+function _build_single_model_for_simulation(
+ model::DecisionModel,
+ sim::Simulation,
+ model_number::Int,
+)
+ initial_time = get_initial_time(sim)
+ set_initial_time!(model, initial_time)
+ output_dir = joinpath(get_models_dir(sim), string(get_name(model)))
+ mkpath(output_dir)
+ set_output_dir!(model, output_dir)
+ try
+ # TODO-PJ: Temporary while are able to switch from PJ to POI
+ container = get_optimization_container(model)
+ container.built_for_recurrent_solves = true
+ build_impl!(model)
+ sim.internal.date_ref[model_number] = initial_time
+ set_status!(model, ModelBuildStatus.BUILT)
+ _pre_solve_model_checks(model)
+ catch
+ set_status!(model, ModelBuildStatus.FAILED)
+ @error "Failed to build $(get_name(model))"
+ rethrow()
+ end
+ return
+end
+
function _build_decision_models!(sim::Simulation)
- for (model_number, model) in enumerate(get_decision_models(get_models(sim)))
- @info("Building problem $(get_name(model))")
- initial_time = get_initial_time(sim)
- set_initial_time!(model, initial_time)
- output_dir = joinpath(get_models_dir(sim), string(get_name(model)))
- mkpath(output_dir)
- set_output_dir!(model, output_dir)
- try
- TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(model))" begin
- # TODO-PJ: Temporary while are able to switch from PJ to POI
- container = get_optimization_container(model)
- container.built_for_recurrent_solves = true
- build_impl!(model)
+ TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build Decision Problems" begin
+ decision_models = get_decision_models(get_models(sim))
+ #TODO: Re-enable Threads.@threads with proper implementation of the timer.
+ for model_n in 1:length(decision_models)
+ TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(decision_models[model_n]))" begin
+ _build_single_model_for_simulation(decision_models[model_n], sim, model_n)
end
- sim.internal.date_ref[model_number] = initial_time
- set_status!(model, BuildStatus.BUILT)
- # TODO: Disable check of variable bounds ?
- _pre_solve_model_checks(model)
- catch
- set_status!(model, BuildStatus.FAILED)
- rethrow()
end
end
+ _initial_conditions_reconciliation!(get_decision_models(get_models(sim)))
return
end
@@ -306,13 +374,13 @@ function _build_emulation_model!(sim::Simulation)
output_dir = joinpath(get_models_dir(sim), string(get_name(model)))
mkpath(output_dir)
set_output_dir!(model, output_dir)
- TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem $(get_name(model))" begin
+ TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Problem Emulation $(get_name(model))" begin
build_impl!(model)
end
sim.internal.date_ref[length(sim.internal.date_ref) + 1] = initial_time
- set_status!(model, BuildStatus.BUILT)
+ set_status!(model, ModelBuildStatus.BUILT)
catch
- set_status!(model, BuildStatus.FAILED)
+ set_status!(model, ModelBuildStatus.FAILED)
rethrow()
end
return
@@ -337,37 +405,39 @@ function _get_model_store_requirements!(
)
model_name = get_name(model)
horizon = get_horizon(model)
+ resolution = get_resolution(model)
+ horizon_count = horizon ÷ resolution
reqs = SimulationModelStoreRequirements()
container = get_optimization_container(model)
for (key, array) in get_duals(container)
!should_write_resulting_value(key) && continue
- reqs.duals[key] = _calc_dimensions(array, key, num_rows, horizon)
+ reqs.duals[key] = _calc_dimensions(array, key, num_rows, horizon_count)
add_rule!(rules, model_name, key, true)
end
for (key, param_container) in get_parameters(container)
!should_write_resulting_value(key) && continue
array = get_multiplier_array(param_container)
- reqs.parameters[key] = _calc_dimensions(array, key, num_rows, horizon)
+ reqs.parameters[key] = _calc_dimensions(array, key, num_rows, horizon_count)
add_rule!(rules, model_name, key, false)
end
for (key, array) in get_variables(container)
!should_write_resulting_value(key) && continue
- reqs.variables[key] = _calc_dimensions(array, key, num_rows, horizon)
+ reqs.variables[key] = _calc_dimensions(array, key, num_rows, horizon_count)
add_rule!(rules, model_name, key, true)
end
for (key, array) in get_aux_variables(container)
!should_write_resulting_value(key) && continue
- reqs.aux_variables[key] = _calc_dimensions(array, key, num_rows, horizon)
+ reqs.aux_variables[key] = _calc_dimensions(array, key, num_rows, horizon_count)
add_rule!(rules, model_name, key, true)
end
for (key, array) in get_expressions(container)
!should_write_resulting_value(key) && continue
- reqs.expressions[key] = _calc_dimensions(array, key, num_rows, horizon)
+ reqs.expressions[key] = _calc_dimensions(array, key, num_rows, horizon_count)
add_rule!(rules, model_name, key, false)
end
@@ -434,7 +504,7 @@ function _initialize_problem_storage!(
)
for model in get_decision_models(models)
model_name = get_name(model)
- decision_model_store_params[model_name] = model.internal.store_parameters
+ decision_model_store_params[model_name] = get_store_params(model)
num_executions = executions_by_model[model_name]
num_rows = num_executions * get_steps(sim)
dm_model_req[model_name] = _get_model_store_requirements!(rules, model, num_rows)
@@ -447,7 +517,7 @@ function _initialize_problem_storage!(
emulation_model_store_params = OrderedDict(
:Emulator => ModelStoreParams(
get_step_resolution(sequence) ÷ resolution, # Num Executions
- 1,
+ resolution, # Horizon
resolution, # Interval
resolution, # Resolution
get_base_power(base_params),
@@ -456,7 +526,7 @@ function _initialize_problem_storage!(
)
else
emulation_model_store_params =
- OrderedDict(Symbol(get_name(em)) => em.internal.store_parameters)
+ OrderedDict(Symbol(get_name(em)) => get_store_params(em))
end
em_model_req = _get_emulation_store_requirements(sim)
@@ -488,7 +558,7 @@ function _build!(
partitions = nothing,
index = nothing,
)
- set_simulation_build_status!(sim, BuildStatus.IN_PROGRESS)
+ set_simulation_build_status!(sim, SimulationBuildStatus.IN_PROGRESS)
problem_initial_times = _get_simulation_initial_times!(sim)
sequence = get_sequence(sim)
step_resolution = get_step_resolution(sequence)
@@ -521,8 +591,7 @@ function _build!(
em = get_emulation_model(simulation_models)
if em !== nothing
- system = get_system(em)
- em_resolution = PSY.get_time_series_resolution(system)
+ em_resolution = get_resolution(em)
set_executions!(em, get_steps(sim) * Int(step_resolution / em_resolution))
end
@@ -530,10 +599,8 @@ function _build!(
_check_steps(sim, problem_initial_times)
end
- TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build Problems" begin
- _build_decision_models!(sim)
- _build_emulation_model!(sim)
- end
+ _build_decision_models!(sim)
+ _build_emulation_model!(sim)
TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Initialize Simulation State" begin
_initialize_simulation_state!(sim)
@@ -652,11 +719,11 @@ function build!(
partitions = partitions,
index = index,
)
- set_simulation_build_status!(sim, BuildStatus.BUILT)
- set_simulation_status!(sim, RunStatus.READY)
+ set_simulation_build_status!(sim, SimulationBuildStatus.BUILT)
+ set_simulation_status!(sim, RunStatus.INITIALIZED)
catch e
@error "Simulation build failed" exception = (e, catch_backtrace())
- set_simulation_build_status!(sim, BuildStatus.FAILED)
+ set_simulation_build_status!(sim, SimulationBuildStatus.FAILED)
set_simulation_status!(sim, RunStatus.NOT_READY)
rethrow(e)
end
@@ -800,9 +867,21 @@ function _write_state_to_store!(store::SimulationStore, sim::Simulation)
if store_update_time < state_update_time
_update_timestamp = max(store_update_time + state_resolution, sim_ini_time)
while _update_timestamp <= state_update_time
- state_values = get_decision_state_value(sim_state, key, _update_timestamp)
- ix = get_last_recorded_row(em_store, key) + 1
- write_result!(store, model_name, key, ix, _update_timestamp, state_values)
+ try
+ state_values =
+ get_decision_state_value(sim_state, key, _update_timestamp)
+ ix = get_last_recorded_row(em_store, key) + 1
+ write_result!(
+ store,
+ model_name,
+ key,
+ ix,
+ _update_timestamp,
+ state_values,
+ )
+ catch
+ @error "could not write result for $(PSI.encode_key_as_string(key))"
+ end
_update_timestamp += state_resolution
end
end
@@ -907,7 +986,7 @@ function _execute!(
start_time = time()
TimerOutputs.@timeit RUN_SIMULATION_TIMER "Execute $(model_name)" begin
if !is_built(model)
- error("$(model_name) status is not BuildStatus.BUILT")
+ error("$(model_name) status is not ModelBuildStatus.BUILT")
end
# Is first run of first problem? Yes -> don't update problem
@@ -921,7 +1000,7 @@ function _execute!(
end # Run problem Timer
TimerOutputs.@timeit RUN_SIMULATION_TIMER "Update State" begin
- if status == RunStatus.SUCCESSFUL
+ if status == RunStatus.SUCCESSFULLY_FINALIZED
# TODO: _update_simulation_state! can use performance improvements
_update_simulation_state!(sim, model)
if model_number == execution_order[end]
@@ -972,7 +1051,7 @@ Solves the simulation model for sequential Simulations.
- `sim::Simulation=sim`: simulation object created by Simulation()
The optional keyword argument `exports` controls exporting of results to CSV files as
-the simulation runs. Refer to [`export_results`](@ref) for a description of this argument.
+the simulation runs.
# Example
```julia
@@ -989,9 +1068,13 @@ function execute!(sim::Simulation; kwargs...)
in_memory = get(kwargs, :in_memory, false)
store_type = in_memory ? InMemorySimulationStore : HdfSimulationStore
- if (get_simulation_build_status(sim) != BuildStatus.BUILT) ||
- (get_simulation_status(sim) != RunStatus.READY)
- error("Simulation status is invalid, you need to rebuild the simulation")
+ sim_build_status = get_simulation_build_status(sim)
+ sim_run_status = get_simulation_status(sim)
+ if (sim_build_status != SimulationBuildStatus.BUILT) ||
+ (sim_run_status != RunStatus.INITIALIZED)
+ error(
+ "Simulation build status $sim_build_status, or Simulation run status $sim_run_status, are invalid, you need to rebuild the simulation",
+ )
end
try
Logging.with_logger(logger) do
@@ -1003,7 +1086,7 @@ function execute!(sim::Simulation; kwargs...)
_execute!(sim; [k => v for (k, v) in kwargs if k != :in_memory]...)
end
@info ("\n$(RUN_SIMULATION_TIMER)\n")
- set_simulation_status!(sim, RunStatus.SUCCESSFUL)
+ set_simulation_status!(sim, RunStatus.SUCCESSFULLY_FINALIZED)
if isnothing(sim.internal.partitions)
# Partitioned simulations serialize the systems once during build.
_serialize_systems_to_store!(store, sim)
@@ -1022,7 +1105,7 @@ function execute!(sim::Simulation; kwargs...)
end
if !in_memory
- compute_file_hash(get_store_dir(sim), HDF_FILENAME)
+ IS.compute_file_hash(get_store_dir(sim), HDF_FILENAME)
end
serialize_status(sim)
diff --git a/src/simulation/simulation_info.jl b/src/simulation/simulation_info.jl
new file mode 100644
index 0000000000..a587a1031f
--- /dev/null
+++ b/src/simulation/simulation_info.jl
@@ -0,0 +1,14 @@
+mutable struct SimulationInfo
+ number::Union{Nothing, Int}
+ sequence_uuid::Union{Nothing, Base.UUID}
+ run_status::RunStatus
+end
+
+SimulationInfo() = SimulationInfo(nothing, nothing, RunStatus.INITIALIZED)
+
+get_number(si::SimulationInfo) = si.number
+set_number!(si::SimulationInfo, val::Int) = si.number = val
+get_sequence_uuid(si::SimulationInfo) = si.sequence_uuid
+set_sequence_uuid!(si::SimulationInfo, val::Base.UUID) = si.sequence_uuid = val
+get_run_status(si::SimulationInfo) = si.run_status
+set_run_status!(si::SimulationInfo, val::RunStatus) = si.run_status = val
diff --git a/src/simulation/simulation_internal.jl b/src/simulation/simulation_internal.jl
index b4378059ae..984d0b8bab 100644
--- a/src/simulation/simulation_internal.jl
+++ b/src/simulation/simulation_internal.jl
@@ -10,7 +10,7 @@ mutable struct SimulationInternal
run_count::OrderedDict{Int, OrderedDict{Int, Int}}
date_ref::OrderedDict{Int, Dates.DateTime}
status::RunStatus
- build_status::BuildStatus
+ build_status::SimulationBuildStatus
simulation_state::SimulationState
store::Union{Nothing, SimulationStore}
recorders::Vector{Symbol}
@@ -73,7 +73,7 @@ function SimulationInternal(
count_dict,
OrderedDict{Int, Dates.DateTime}(),
RunStatus.NOT_READY,
- BuildStatus.EMPTY,
+ SimulationBuildStatus.EMPTY,
SimulationState(),
nothing,
collect(unique_recorders),
diff --git a/src/simulation/simulation_models.jl b/src/simulation/simulation_models.jl
index 39b19f5115..525f792df8 100644
--- a/src/simulation/simulation_models.jl
+++ b/src/simulation/simulation_models.jl
@@ -98,7 +98,7 @@ get_decision_models(models::SimulationModels) = models.decision_models
get_emulation_model(models::SimulationModels) = models.emulation_model
function determine_horizons!(models::SimulationModels)
- horizons = OrderedDict{Symbol, Int}()
+ horizons = OrderedDict{Symbol, Dates.Millisecond}()
for model in models.decision_models
container = get_optimization_container(model)
settings = get_settings(container)
@@ -107,12 +107,15 @@ function determine_horizons!(models::SimulationModels)
sys = get_system(model)
horizon = PSY.get_forecast_horizon(sys)
set_horizon!(settings, horizon)
+ horizons[get_name(model)] = horizon
+ else
+ horizons[get_name(model)] = horizon
end
- horizons[get_name(model)] = horizon
end
em = models.emulation_model
if em !== nothing
- horizons[get_name(em)] = 1
+ resolution = get_resolution(em)
+ horizons[get_name(em)] = resolution
end
return horizons
end
@@ -123,14 +126,16 @@ function determine_intervals(models::SimulationModels)
system = get_system(model)
interval = PSY.get_forecast_interval(system)
if interval == Dates.Millisecond(0)
- throw(IS.InvalidValue("Interval of model $(get_name(model)) not set correctly"))
+ throw(IS.InvalidValue("Model $(get_name(model)) interval not set correctly"))
end
intervals[get_name(model)] = IS.time_period_conversion(interval)
end
em = models.emulation_model
if em !== nothing
- emulator_system = get_system(em)
- emulator_interval = PSY.get_time_series_resolution(emulator_system)
+ emulator_interval = get_resolution(em)
+ if emulator_interval == Dates.Millisecond(0)
+ throw(IS.InvalidValue("Emulator Resolution not set correctly"))
+ end
intervals[get_name(em)] = IS.time_period_conversion(emulator_interval)
end
return intervals
@@ -139,9 +144,8 @@ end
function determine_resolutions(models::SimulationModels)
resolutions = OrderedDict{Symbol, Dates.Millisecond}()
for model in models.decision_models
- system = get_system(model)
- resolution = PSY.get_time_series_resolution(system)
- if resolution == Dates.Millisecond(0)
+ resolution = get_resolution(model)
+ if resolution == UNSET_RESOLUTION
throw(
IS.InvalidValue("Resolution of model $(get_name(model)) not set correctly"),
)
@@ -150,8 +154,7 @@ function determine_resolutions(models::SimulationModels)
end
em = models.emulation_model
if em !== nothing
- emulator_system = get_system(em)
- emulator_resolution = PSY.get_time_series_resolution(emulator_system)
+ emulator_resolution = get_resolution(em)
resolutions[get_name(em)] = IS.time_period_conversion(emulator_resolution)
end
return resolutions
@@ -159,14 +162,14 @@ end
function initialize_simulation_internals!(models::SimulationModels, uuid::Base.UUID)
for (ix, model) in enumerate(get_decision_models(models))
- info = SimulationInfo(ix, uuid)
- set_simulation_info!(model, info)
+ set_simulation_number!(model, ix)
+ set_sequence_uuid!(model, uuid)
end
em = get_emulation_model(models)
if em !== nothing
ix = length(get_decision_models(models)) + 1
- info = SimulationInfo(ix, uuid)
- set_simulation_info!(em, info)
+ set_simulation_number!(em, ix)
+ set_sequence_uuid!(em, uuid)
end
return
end
diff --git a/src/simulation/simulation_partition_results.jl b/src/simulation/simulation_partition_results.jl
index 629e41fd3f..95a817bdad 100644
--- a/src/simulation/simulation_partition_results.jl
+++ b/src/simulation/simulation_partition_results.jl
@@ -59,11 +59,11 @@ _store_subpath() = joinpath("data_store", "simulation_store.h5")
_store_path(x::SimulationPartitionResults) = joinpath(x.path, _store_subpath())
function _check_jobs(results::SimulationPartitionResults)
- overall_status = RunStatus.SUCCESSFUL
+ overall_status = RunStatus.SUCCESSFULLY_FINALIZED
for i in 1:get_num_partitions(results.partitions)
job_results_path = joinpath(_partition_path(results, 1), "results")
status = deserialize_status(job_results_path)
- if status != RunStatus.SUCCESSFUL
+ if status != RunStatus.SUCCESSFULLY_FINALIZED
@warn "partition job index = $i was not successful: $status"
overall_status = status
end
@@ -186,6 +186,6 @@ end
function _complete(results::SimulationPartitionResults, status)
serialize_status(status, joinpath(results.path, "results"))
store_path = _store_path(results)
- compute_file_hash(dirname(store_path), basename(store_path))
+ IS.compute_file_hash(dirname(store_path), basename(store_path))
return
end
diff --git a/src/simulation/simulation_problem_results.jl b/src/simulation/simulation_problem_results.jl
index 94419fdad1..faa327d540 100644
--- a/src/simulation/simulation_problem_results.jl
+++ b/src/simulation/simulation_problem_results.jl
@@ -69,6 +69,7 @@ get_system_uuid(results::PSI.SimulationProblemResults) = results.system_uuid
IS.get_timestamp(result::SimulationProblemResults) = result.results_timestamps
get_interval(res::SimulationProblemResults) = res.timestamps.step
IS.get_base_power(result::SimulationProblemResults) = result.base_power
+get_output_dir(res::SimulationProblemResults) = res.results_output_folder
get_results_timestamps(result::SimulationProblemResults) = result.results_timestamps
function set_results_timestamps!(
@@ -80,8 +81,10 @@ end
list_result_keys(res::SimulationProblemResults, ::AuxVarKey) = list_aux_variable_keys(res)
list_result_keys(res::SimulationProblemResults, ::ConstraintKey) = list_dual_keys(res)
-list_result_keys(res::SimulationProblemResults, ::ExpressionKey) = list_expression_keys(res)
-list_result_keys(res::SimulationProblemResults, ::ParameterKey) = list_parameter_keys(res)
+list_result_keys(res::SimulationProblemResults, ::ExpressionKey) =
+ list_expression_keys(res)
+list_result_keys(res::SimulationProblemResults, ::ParameterKey) =
+ list_parameter_keys(res)
list_result_keys(res::SimulationProblemResults, ::VariableKey) = list_variable_keys(res)
get_cached_results(res::SimulationProblemResults, ::Type{<:AuxVarKey}) =
@@ -149,29 +152,47 @@ If the simulation was configured to serialize all systems to file then the retur
will include all data. If that was not configured then the returned system will include
all data except time series data.
"""
-function get_system!(results::SimulationProblemResults; kwargs...)
- !isnothing(results.system) && return results.system
-
- file = joinpath(
- results.execution_path,
- "problems",
- results.problem,
- make_system_filename(results.system_uuid),
- )
+function get_system!(
+ results::Union{OptimizationProblemResults, SimulationProblemResults};
+ kwargs...,
+)
+ !isnothing(get_system(results)) && return get_system(results)
+ file = locate_system_file(results)
# This flag should remain unpublished because it should never be needed
# by the general audience.
- if !get(kwargs, :use_h5_system, false) && isfile(file)
+ if !get(kwargs, :use_system_fallback, false) && isfile(file)
system = PSY.System(file; time_series_read_only = true)
@info "De-serialized the system from files."
else
- system = _deserialize_system(results, results.store)
+ system = get_system_fallback(results)
end
- results.system = system
- return results.system
+ set_system!(results, system)
+ return get_system(results)
end
+get_system_fallback(results::SimulationProblemResults) =
+ _deserialize_system(results, results.store)
+get_system_fallback(results::OptimizationProblemResults) = error("Could not locate system")
+
+locate_system_file(results::SimulationProblemResults) = joinpath(
+ get_execution_path(results),
+ "problems",
+ get_model_name(results),
+ make_system_filename(results.system_uuid),
+)
+
+locate_system_file(results::OptimizationProblemResults) = joinpath(
+ IS.Optimization.get_results_dir(results),
+ make_system_filename(IS.Optimization.get_source_data_uuid(results)),
+)
+
+get_system(results::OptimizationProblemResults) = IS.Optimization.get_source_data(results)
+
+set_system!(results::OptimizationProblemResults, system) =
+ IS.Optimization.set_source_data!(results, system)
+
function _deserialize_system(results::SimulationProblemResults, ::Nothing)
open_store(
HdfSimulationStore,
@@ -238,20 +259,12 @@ function _deserialize_key(
results::SimulationProblemResults,
args...,
) where {T <: OptimizationContainerKey}
- return make_key(T, args...)
+ return IS.Optimization.make_key(T, args...)
end
get_container_fields(x::SimulationProblemResults) =
(:aux_variables, :duals, :expressions, :parameters, :variables)
-function _validate_keys(existing_keys, result_keys)
- diff = setdiff(result_keys, existing_keys)
- if !isempty(diff)
- throw(IS.InvalidValue("These keys are not stored: $diff"))
- end
- return
-end
-
"""
Return the final values for the requested variables for each time step for a problem.
@@ -298,7 +311,11 @@ function read_realized_variables(
variables::Vector{Tuple{DataType, DataType}};
kwargs...,
)
- return read_realized_variables(res, [VariableKey(x...) for x in variables]; kwargs...)
+ return read_realized_variables(
+ res,
+ [VariableKey(x...) for x in variables];
+ kwargs...,
+ )
end
function read_realized_variables(
@@ -646,7 +663,9 @@ end
function read_realized_expression(res::SimulationProblemResults, expression...; kwargs...)
return first(
- values(read_realized_expressions(res, [ExpressionKey(expression...)]; kwargs...)),
+ values(
+ read_realized_expressions(res, [ExpressionKey(expression...)]; kwargs...),
+ ),
)
end
@@ -672,77 +691,8 @@ function _read_optimizer_stats(res::SimulationProblemResults, ::Nothing)
end
end
-"""
-Save the realized results to CSV files for all variables, paramaters, duals, auxiliary variables,
-expressions, and optimizer statistics.
-
-# Arguments
-
- - `res::Union{ProblemResults, SimulationProblmeResults`: Results
- - `save_path::AbstractString` : path to save results (defaults to simulation path)
-"""
-function export_realized_results(res::SimulationProblemResults)
- save_path = mkpath(joinpath(res.results_output_folder, "export"))
- return export_realized_results(res, save_path)
-end
-
-function export_realized_results(
- res::Union{ProblemResults, SimulationProblemResults},
- save_path::AbstractString,
-)
- if !isdir(save_path)
- throw(IS.ConflictingInputsError("Specified path is not valid."))
- end
- write_data(read_results_with_keys(res, list_variable_keys(res)), save_path)
- !isempty(list_dual_keys(res)) &&
- write_data(
- read_results_with_keys(res, list_dual_keys(res)),
- save_path;
- name = "dual",
- )
- !isempty(list_parameter_keys(res)) && write_data(
- read_results_with_keys(res, list_parameter_keys(res)),
- save_path;
- name = "parameter",
- )
- !isempty(list_aux_variable_keys(res)) && write_data(
- read_results_with_keys(res, list_aux_variable_keys(res)),
- save_path;
- name = "aux_variable",
- )
- !isempty(list_expression_keys(res)) && write_data(
- read_results_with_keys(res, list_expression_keys(res)),
- save_path;
- name = "expression",
- )
- export_optimizer_stats(res, save_path)
- files = readdir(save_path)
- compute_file_hash(save_path, files)
- @info("Files written to $save_path folder.")
- return save_path
-end
-
-"""
-Save the optimizer statistics to CSV or JSON
-
-# Arguments
-
- - `res::Union{ProblemResults, SimulationProblmeResults`: Results
- - `directory::AbstractString` : target directory
- - `format = "CSV"` : can be "csv" or "json
-"""
-function export_optimizer_stats(
- res::Union{ProblemResults, SimulationProblemResults},
- directory::AbstractString;
- format = "csv",
-)
- data = read_optimizer_stats(res)
- isnothing(data) && return
- if uppercase(format) == "CSV"
- CSV.write(joinpath(directory, "optimizer_stats.csv"), data)
- elseif uppercase(format) == "JSON"
- JSON.write(joinpath(directory, "optimizer_stats.json"), JSON.json(to_dict(data)))
- else
- throw(error("writing optimizer stats only supports csv or json formats"))
- end
-end
+# Chooses the user-passed store or results store for reading values. Either could be
+# something or nothing. If both are nothing, we must open the HDF5 store.
+try_resolve_store(user::SimulationStore, results::Union{Nothing, SimulationStore}) = user
+try_resolve_store(user::Nothing, results::SimulationStore) = results
+try_resolve_store(user::Nothing, results::Nothing) = nothing
diff --git a/src/simulation/simulation_results.jl b/src/simulation/simulation_results.jl
index 5a05f73b43..b76ada8c65 100644
--- a/src/simulation/simulation_results.jl
+++ b/src/simulation/simulation_results.jl
@@ -1,6 +1,7 @@
function check_folder_integrity(folder::String)
folder_files = readdir(folder)
alien_files = setdiff(folder_files, KNOWN_SIMULATION_PATHS)
+ alien_files = filter(x -> !any(occursin.(IGNORABLE_FILES, x)), alien_files)
if isempty(alien_files)
return true
else
@@ -334,7 +335,13 @@ function export_results(results::SimulationResults, exports, store::SimulationSt
count = 1,
store = store,
)
- export_result(file_type, export_path, name, timestamp, dfs[timestamp])
+ IS.Optimization.export_result(
+ file_type,
+ export_path,
+ name,
+ timestamp,
+ dfs[timestamp],
+ )
end
end
@@ -348,7 +355,13 @@ function export_results(results::SimulationResults, exports, store::SimulationSt
count = 1,
store = store,
)
- export_result(file_type, export_path, name, timestamp, dfs[timestamp])
+ IS.Optimization.export_result(
+ file_type,
+ export_path,
+ name,
+ timestamp,
+ dfs[timestamp],
+ )
end
end
@@ -362,7 +375,13 @@ function export_results(results::SimulationResults, exports, store::SimulationSt
count = 1,
store = store,
)
- export_result(file_type, export_path, name, timestamp, dfs[timestamp])
+ IS.Optimization.export_result(
+ file_type,
+ export_path,
+ name,
+ timestamp,
+ dfs[timestamp],
+ )
end
end
@@ -376,7 +395,13 @@ function export_results(results::SimulationResults, exports, store::SimulationSt
count = 1,
store = store,
)
- export_result(file_type, export_path, name, timestamp, dfs[timestamp])
+ IS.Optimization.export_result(
+ file_type,
+ export_path,
+ name,
+ timestamp,
+ dfs[timestamp],
+ )
end
end
end
@@ -391,76 +416,27 @@ function export_results(results::SimulationResults, exports, store::SimulationSt
count = 1,
store = store,
)
- export_result(file_type, export_path, name, timestamp, dfs[timestamp])
+ IS.Optimization.export_result(
+ file_type,
+ export_path,
+ name,
+ timestamp,
+ dfs[timestamp],
+ )
end
end
if problem_exports.optimizer_stats
export_path = joinpath(path, problem_results.problem, "optimizer_stats.csv")
df = read_optimizer_stats(problem_results; store = store)
- export_result(file_type, export_path, df)
+ IS.Optimization.export_result(file_type, export_path, df)
end
end
return
end
-function export_result(
- ::Type{CSV.File},
- path,
- key::OptimizationContainerKey,
- timestamp::Dates.DateTime,
- df::DataFrames.DataFrame,
-)
- name = encode_key_as_string(key)
- export_result(CSV.File, path, name, timestamp, df)
- return
-end
-
-function export_result(
- ::Type{CSV.File},
- path,
- name::AbstractString,
- timestamp::Dates.DateTime,
- df::DataFrames.DataFrame,
-)
- filename = joinpath(path, name * "_" * convert_for_path(timestamp) * ".csv")
- export_result(CSV.File, filename, df)
- return
-end
-
-function export_result(
- ::Type{CSV.File},
- path,
- key::OptimizationContainerKey,
- df::DataFrames.DataFrame,
-)
- name = encode_key_as_string(key)
- export_result(CSV.File, path, name, df)
- return
-end
-
-function export_result(
- ::Type{CSV.File},
- path,
- name::AbstractString,
- df::DataFrames.DataFrame,
-)
- filename = joinpath(path, name * ".csv")
- export_result(CSV.File, filename, df)
- return
-end
-
-function export_result(::Type{CSV.File}, filename, df::DataFrames.DataFrame)
- open(filename, "w") do io
- CSV.write(io, df)
- end
-
- @debug "Exported $filename"
- return
-end
-
function _check_status(status::RunStatus, ignore_status)
- status == RunStatus.SUCCESSFUL && return
+ status == RunStatus.SUCCESSFULLY_FINALIZED && return
if ignore_status
@warn "Simulation was not successful: $status. Results may not be valid."
diff --git a/src/simulation/simulation_results_export.jl b/src/simulation/simulation_results_export.jl
index d140aeabf0..6717bd3726 100644
--- a/src/simulation/simulation_results_export.jl
+++ b/src/simulation/simulation_results_export.jl
@@ -2,7 +2,7 @@
const _SUPPORTED_FORMATS = ("csv",)
mutable struct SimulationResultsExport
- models::Dict{Symbol, ProblemResultsExport}
+ models::Dict{Symbol, OptimizationProblemResultsExport}
start_time::Dates.DateTime
end_time::Dates.DateTime
path::Union{Nothing, String}
@@ -10,7 +10,7 @@ mutable struct SimulationResultsExport
end
function SimulationResultsExport(
- models::Vector{ProblemResultsExport},
+ models::Vector{OptimizationProblemResultsExport},
params::SimulationStoreParams;
start_time = nothing,
end_time = nothing,
@@ -55,7 +55,7 @@ function SimulationResultsExport(filename::AbstractString, params::SimulationSto
end
function SimulationResultsExport(data::AbstractDict, params::SimulationStoreParams)
- models = Vector{ProblemResultsExport}()
+ models = Vector{OptimizationProblemResultsExport}()
for model in get(data, "models", [])
if !haskey(model, "name")
throw(IS.InvalidValue("model data does not define 'name'"))
@@ -78,7 +78,7 @@ function SimulationResultsExport(data::AbstractDict, params::SimulationStorePara
deserialize_key(problem_params, x) for
x in get(model, "variables", Set{AuxVarKey}())
)
- problem_export = ProblemResultsExport(
+ problem_export = OptimizationProblemResultsExport(
model["name"];
duals = duals,
parameters = parameters,
@@ -161,5 +161,5 @@ function _should_export(exports::SimulationResultsExport, tstamp, model, field_n
end
problem_exports = get_problem_exports(exports, model)
- return _should_export(problem_exports, field_name, name)
+ return IS.Optimization._should_export(problem_exports, field_name, name)
end
diff --git a/src/simulation/simulation_sequence.jl b/src/simulation/simulation_sequence.jl
index ae7bc188af..9fdb656743 100644
--- a/src/simulation/simulation_sequence.jl
+++ b/src/simulation/simulation_sequence.jl
@@ -1,12 +1,11 @@
function check_simulation_chronology(
- horizons::OrderedDict{Symbol, Int},
+ horizons::OrderedDict{Symbol, Dates.Millisecond},
intervals::OrderedDict{Symbol, Dates.Millisecond},
resolutions::OrderedDict{Symbol, Dates.Millisecond},
)
models = collect(keys(resolutions))
- for (model, horizon) in horizons
- horizon_time = resolutions[model] * horizon
+ for (model, horizon_time) in horizons
if horizon_time < intervals[model]
throw(IS.ConflictingInputsError("horizon ($horizon_time) is
shorter than interval ($interval) for $(model)"))
@@ -16,19 +15,25 @@ function check_simulation_chronology(
for i in 2:length(models)
upper_level_model = models[i - 1]
lower_level_model = models[i]
- if horizons[lower_level_model] * resolutions[lower_level_model] >
- horizons[upper_level_model] * resolutions[upper_level_model]
+ if horizons[lower_level_model] > horizons[upper_level_model]
throw(
IS.ConflictingInputsError(
"The lookahead length $(horizons[upper_level_model]) in model $(upper_level_model) is insufficient to syncronize with $(lower_level_model)",
),
)
end
+ if intervals[lower_level_model] == Dates.Millisecond(0)
+ throw(
+ IS.ConflictingInputsError(
+ "The interval in model $(lower_level_model) is invalid.",
+ ),
+ )
+ end
if (intervals[upper_level_model] % intervals[lower_level_model]) !=
Dates.Millisecond(0)
throw(
IS.ConflictingInputsError(
- "The system's intervals are not compatible for simulation. The interval in model $(upper_level_model) needs to be a mutiple of the interval $(lower_level_model) for a consistent time coordination.",
+ "The intervals are not compatible for simulation. The interval in model $(upper_level_model) needs to be a mutiple of the interval $(lower_level_model) for a consistent time coordination.",
),
)
end
@@ -160,6 +165,7 @@ function _add_feedforward_to_model(
),
)
end
+ @debug "attaching $T to $(PSI.get_component_type(ff)) $(PSI.get_feedforward_meta(ff))"
attach_feedforward!(service_model, ff)
else
service_found = false
@@ -237,7 +243,7 @@ sequence = SimulationSequence(;
```
"""
mutable struct SimulationSequence
- horizons::OrderedDict{Symbol, Int}
+ horizons::OrderedDict{Symbol, Dates.Millisecond}
intervals::OrderedDict{Symbol, Dates.Millisecond}
feedforwards::Dict{Symbol, Vector{<:AbstractAffectFeedforward}}
events::Dict{EventKey, Any}
diff --git a/src/simulation/simulation_state.jl b/src/simulation/simulation_state.jl
index fb70c7845e..6dd7dc0cbf 100644
--- a/src/simulation/simulation_state.jl
+++ b/src/simulation/simulation_state.jl
@@ -44,7 +44,7 @@ function _get_state_params(models::SimulationModels, simulation_step::Dates.Mill
container = get_optimization_container(model)
model_resolution = get_resolution(model)
model_interval = get_interval(model)
- horizon_length = get_horizon(model) * model_resolution
+ horizon_length = get_horizon(model)
# This is the portion of the Horizon that "overflows" into the next step
time_residual = horizon_length - model_interval
@assert_op time_residual >= zero(Dates.Millisecond)
diff --git a/src/simulation/simulation_store_requirements.jl b/src/simulation/simulation_store_requirements.jl
new file mode 100644
index 0000000000..65533baa58
--- /dev/null
+++ b/src/simulation/simulation_store_requirements.jl
@@ -0,0 +1,17 @@
+struct SimulationModelStoreRequirements
+ duals::Dict{ConstraintKey, Dict{String, Any}}
+ parameters::Dict{ParameterKey, Dict{String, Any}}
+ variables::Dict{VariableKey, Dict{String, Any}}
+ aux_variables::Dict{AuxVarKey, Dict{String, Any}}
+ expressions::Dict{ExpressionKey, Dict{String, Any}}
+end
+
+function SimulationModelStoreRequirements()
+ return SimulationModelStoreRequirements(
+ Dict{ConstraintKey, Dict{String, Any}}(),
+ Dict{ParameterKey, Dict{String, Any}}(),
+ Dict{VariableKey, Dict{String, Any}}(),
+ Dict{AuxVarKey, Dict{String, Any}}(),
+ Dict{ExpressionKey, Dict{String, Any}}(),
+ )
+end
diff --git a/src/utils/dataframes_utils.jl b/src/utils/dataframes_utils.jl
index eb9607a909..9567405c15 100644
--- a/src/utils/dataframes_utils.jl
+++ b/src/utils/dataframes_utils.jl
@@ -32,48 +32,3 @@ end
function to_matrix(df_row::DataFrames.DataFrameRow{DataFrames.DataFrame, DataFrames.Index})
return reshape(Vector(df_row), 1, size(df_row)[1])
end
-
-function write_data(
- vars_results::Dict,
- time::DataFrames.DataFrame,
- save_path::AbstractString,
-)
- for (k, v) in vars_results
- var = DataFrames.DataFrame()
- if size(time, 1) == size(v, 1)
- var = hcat(time, v)
- else
- var = v
- end
- file_path = joinpath(save_path, "$(k).csv")
- CSV.write(file_path, var)
- end
-end
-
-function write_data(
- data::DataFrames.DataFrame,
- save_path::AbstractString,
- file_name::String,
-)
- if isfile(save_path)
- save_path = dirname(save_path)
- end
- file_path = joinpath(save_path, "$(file_name).csv")
- CSV.write(file_path, data)
- return
-end
-
-# writing a dictionary of dataframes to files
-function write_data(vars_results::Dict, save_path::String; kwargs...)
- name = get(kwargs, :name, "")
- for (k, v) in vars_results
- keyname = encode_key_as_string(k)
- file_path = joinpath(save_path, "$name$keyname.csv")
- @debug "writing" file_path
- if isempty(vars_results[k])
- @debug "$name$k is empty, not writing $file_path"
- else
- CSV.write(file_path, vars_results[k])
- end
- end
-end
diff --git a/src/utils/file_utils.jl b/src/utils/file_utils.jl
index d4e98ad600..9ea6116bba 100644
--- a/src/utils/file_utils.jl
+++ b/src/utils/file_utils.jl
@@ -16,15 +16,6 @@ function read_dataframe(filename::AbstractString)
end
end
-"""
-Return the SHA 256 hash of a file.
-"""
-function compute_sha256(filename::AbstractString)
- return open(filename) do io
- return bytes2hex(SHA.sha256(io))
- end
-end
-
"""
Return the key for the given value
"""
@@ -35,26 +26,8 @@ function find_key_with_value(d, value)
error("dict does not have value == $value")
end
-function compute_file_hash(path::String, files::Vector{String})
- data = Dict("files" => [])
- for file in files
- file_path = joinpath(path, file)
- # Don't put the path in the file so that we can move results directories.
- file_info = Dict("filename" => file, "hash" => compute_sha256(file_path))
- push!(data["files"], file_info)
- end
-
- open(joinpath(path, HASH_FILENAME), "w") do io
- write(io, JSON.json(data))
- end
-end
-
-function compute_file_hash(path::String, file::String)
- return compute_file_hash(path, [file])
-end
-
function read_file_hashes(path)
- data = open(joinpath(path, HASH_FILENAME), "r") do io
+ data = open(joinpath(path, IS.HASH_FILENAME), "r") do io
JSON.parse(io)
end
@@ -81,7 +54,7 @@ function check_file_integrity(path::String)
filename = file_info["filename"]
@info "checking integrity of $filename"
expected_hash = file_info["hash"]
- actual_hash = compute_sha256(joinpath(path, filename))
+ actual_hash = IS.compute_sha256(joinpath(path, filename))
if expected_hash != actual_hash
@error "hash mismatch for file" filename expected_hash actual_hash
matched = false
@@ -96,7 +69,3 @@ function check_file_integrity(path::String)
)
end
end
-
-to_namedtuple(val) = (; (x => getfield(val, x) for x in fieldnames(typeof(val)))...)
-
-convert_for_path(x::Dates.DateTime) = replace(string(x), ":" => "-")
diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl
index 4554bc2619..0d846a9e05 100644
--- a/src/utils/powersystems_utils.jl
+++ b/src/utils/powersystems_utils.jl
@@ -71,6 +71,7 @@ function get_available_components(
)
end
+#=
function get_available_components(
::Type{PSY.RegulationDevice{T}},
sys::PSY.System,
@@ -81,8 +82,9 @@ function get_available_components(
sys,
)
end
+=#
-make_system_filename(sys::PSY.System) = "system-$(IS.get_uuid(sys)).json"
+make_system_filename(sys::PSY.System) = make_system_filename(IS.get_uuid(sys))
make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json"
function check_hvdc_line_limits_consistency(
@@ -126,37 +128,212 @@ function check_hvdc_line_limits_unidirectional(d::PSY.TwoTerminalHVDCLine)
return
end
-function _validate_compact_pwl_data(
- min::Float64,
- max::Float64,
- data::PSY.PiecewiseLinearPointData,
- base_power::Float64,
+##################################################
+########### Cost Function Utilities ##############
+##################################################
+
+"""
+Obtain proportional (marginal or slope) cost data in system base per unit
+depending on the specified power units
+"""
+function get_proportional_cost_per_system_unit(
+ cost_term::Float64,
+ unit_system::PSY.UnitSystem,
+ system_base_power::Float64,
+ device_base_power::Float64,
)
- data = PSY.get_points(data)
- if isapprox(max - min, last(data).x / base_power) && iszero(first(data).x)
- return COMPACT_PWL_STATUS.VALID
- else
- return COMPACT_PWL_STATUS.INVALID
+ return _get_proportional_cost_per_system_unit(
+ cost_term,
+ Val{unit_system}(),
+ system_base_power,
+ device_base_power,
+ )
+end
+
+function _get_proportional_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.SYSTEM_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term
+end
+
+function _get_proportional_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.DEVICE_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term * (system_base_power / device_base_power)
+end
+
+function _get_proportional_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.NATURAL_UNITS},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term * system_base_power
+end
+
+"""
+Obtain quadratic cost data in system base per unit
+depending on the specified power units
+"""
+function get_quadratic_cost_per_system_unit(
+ cost_term::Float64,
+ unit_system::PSY.UnitSystem,
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return _get_quadratic_cost_per_system_unit(
+ cost_term,
+ Val{unit_system}(),
+ system_base_power,
+ device_base_power,
+ )
+end
+
+function _get_quadratic_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.SYSTEM_BASE}, # SystemBase Unit
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term
+end
+
+function _get_quadratic_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.DEVICE_BASE}, # DeviceBase Unit
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term * (system_base_power / device_base_power)^2
+end
+
+function _get_quadratic_cost_per_system_unit(
+ cost_term::Float64,
+ ::Val{PSY.UnitSystem.NATURAL_UNITS}, # Natural Units
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_term * system_base_power^2
+end
+
+"""
+Obtain the normalized PiecewiseLinear cost data in system base per unit
+depending on the specified power units.
+
+Note that the costs (y-axis) are always in \$/h so
+they do not require transformation
+"""
+function get_piecewise_pointcurve_per_system_unit(
+ cost_component::PSY.PiecewiseLinearData,
+ unit_system::PSY.UnitSystem,
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return _get_piecewise_pointcurve_per_system_unit(
+ cost_component,
+ Val{unit_system}(),
+ system_base_power,
+ device_base_power,
+ )
+end
+
+function _get_piecewise_pointcurve_per_system_unit(
+ cost_component::PSY.PiecewiseLinearData,
+ ::Val{PSY.UnitSystem.SYSTEM_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return cost_component
+end
+
+function _get_piecewise_pointcurve_per_system_unit(
+ cost_component::PSY.PiecewiseLinearData,
+ ::Val{PSY.UnitSystem.DEVICE_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ points = cost_component.points
+ points_normalized = Vector{NamedTuple{(:x, :y)}}(undef, length(points))
+ for (ix, point) in enumerate(points)
+ points_normalized[ix] =
+ (x = point.x * (device_base_power / system_base_power), y = point.y)
end
+ return PSY.PiecewiseLinearData(points_normalized)
end
-function validate_compact_pwl_data(
- d::PSY.ThermalGen,
- data::PSY.PiecewiseLinearPointData,
- base_power::Float64,
+function _get_piecewise_pointcurve_per_system_unit(
+ cost_component::PSY.PiecewiseLinearData,
+ ::Val{PSY.UnitSystem.NATURAL_UNITS},
+ system_base_power::Float64,
+ device_base_power::Float64,
)
- min = PSY.get_active_power_limits(d).min
- max = PSY.get_active_power_limits(d).max
- return _validate_compact_pwl_data(min, max, data, base_power)
+ points = cost_component.points
+ points_normalized = Vector{NamedTuple{(:x, :y)}}(undef, length(points))
+ for (ix, point) in enumerate(points)
+ points_normalized[ix] = (x = point.x / system_base_power, y = point.y)
+ end
+ return PSY.PiecewiseLinearData(points_normalized)
+end
+
+"""
+Obtain the normalized PiecewiseStep cost data in system base per unit
+depending on the specified power units.
+
+Note that the costs (y-axis) are in \$/MWh, \$/(sys pu h) or \$/(device pu h),
+so they also require transformation.
+"""
+function get_piecewise_incrementalcurve_per_system_unit(
+ cost_component::PSY.PiecewiseStepData,
+ unit_system::PSY.UnitSystem,
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ return _get_piecewise_incrementalcurve_per_system_unit(
+ cost_component,
+ Val{unit_system}(),
+ system_base_power,
+ device_base_power,
+ )
end
-function validate_compact_pwl_data(
- d::PSY.Component,
- ::PSY.PiecewiseLinearPointData,
- ::Float64,
+function _get_piecewise_incrementalcurve_per_system_unit(
+ cost_component::PSY.PiecewiseStepData,
+ ::Val{PSY.UnitSystem.SYSTEM_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
)
- @warn "Validation of compact pwl data is not implemented for $(typeof(d))."
- return COMPACT_PWL_STATUS.UNDETERMINED
+ return cost_component
end
-get_breakpoint_upper_bounds = PSY.get_x_lengths
+function _get_piecewise_incrementalcurve_per_system_unit(
+ cost_component::PSY.PiecewiseStepData,
+ ::Val{PSY.UnitSystem.DEVICE_BASE},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ x_coords = PSY.get_x_coords(cost_component)
+ y_coords = PSY.get_y_coords(cost_component)
+ ratio = device_base_power / system_base_power
+ x_coords_normalized = x_coords .* ratio
+ y_coords_normalized = y_coords ./ ratio
+ return PSY.PiecewiseStepData(x_coords_normalized, y_coords_normalized)
+end
+
+function _get_piecewise_incrementalcurve_per_system_unit(
+ cost_component::PSY.PiecewiseStepData,
+ ::Val{PSY.UnitSystem.NATURAL_UNITS},
+ system_base_power::Float64,
+ device_base_power::Float64,
+)
+ x_coords = PSY.get_x_coords(cost_component)
+ y_coords = PSY.get_y_coords(cost_component)
+ x_coords_normalized = x_coords ./ system_base_power
+ y_coords_normalized = y_coords .* system_base_power
+ return PSY.PiecewiseStepData(x_coords_normalized, y_coords_normalized)
+end
diff --git a/src/utils/printing.jl b/src/utils/printing.jl
index 8cdb139e29..ae3f97fb1c 100644
--- a/src/utils/printing.jl
+++ b/src/utils/printing.jl
@@ -350,8 +350,8 @@ function _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwa
table = Matrix{Any}(undef, length(sequence.executions_by_model), length(header))
for (ix, (model, executions)) in enumerate(sequence.executions_by_model)
table[ix, 1] = string(model)
- table[ix, 2] = sequence.horizons[model]
- table[ix, 3] = Dates.Minute(sequence.intervals[model])
+ table[ix, 2] = Dates.canonicalize(sequence.horizons[model])
+ table[ix, 3] = Dates.canonicalize(sequence.intervals[model])
table[ix, 4] = executions
end
@@ -487,7 +487,7 @@ function _show_method(io::IO, results::SimulationResults, backend::Symbol; kwarg
)
end
-ProblemResultsTypes = Union{ProblemResults, SimulationProblemResults}
+ProblemResultsTypes = Union{OptimizationProblemResults, SimulationProblemResults}
function Base.show(io::IO, ::MIME"text/plain", input::ProblemResultsTypes)
_show_method(io, input, :auto)
end
diff --git a/test/Project.toml b/test/Project.toml
index 61512a1a39..f82a281315 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -33,6 +33,5 @@ TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat]
-HiGHS = "=1.1.2"
Ipopt = "=1.4.0"
julia = "^1.6"
diff --git a/test/includes.jl b/test/includes.jl
new file mode 100644
index 0000000000..647e724e94
--- /dev/null
+++ b/test/includes.jl
@@ -0,0 +1,46 @@
+# SIIP Packages
+using PowerSimulations
+using PowerSystems
+using PowerSystemCaseBuilder
+using InfrastructureSystems
+using PowerNetworkMatrices
+using HydroPowerSimulations
+import PowerSystemCaseBuilder: PSITestSystems
+using PowerNetworkMatrices
+using StorageSystemsSimulations
+
+# Test Packages
+using Test
+using Logging
+
+# Dependencies for testing
+using PowerModels
+using DataFrames
+using Dates
+using JuMP
+using TimeSeries
+using CSV
+import JSON3
+using DataFrames
+using DataStructures
+import UUIDs
+using Random
+import Serialization
+
+const PM = PowerModels
+const PSY = PowerSystems
+const PSI = PowerSimulations
+const PSB = PowerSystemCaseBuilder
+const PNM = PowerNetworkMatrices
+
+const IS = InfrastructureSystems
+const BASE_DIR = string(dirname(dirname(pathof(PowerSimulations))))
+const DATA_DIR = joinpath(BASE_DIR, "test/test_data")
+
+include("test_utils/common_operation_model.jl")
+include("test_utils/model_checks.jl")
+include("test_utils/mock_operation_models.jl")
+include("test_utils/solver_definitions.jl")
+include("test_utils/operations_problem_templates.jl")
+
+ENV["RUNNING_PSI_TESTS"] = "true"
diff --git a/test/performance/performance_test.jl b/test/performance/performance_test.jl
index 9d62bc281d..d387796ccf 100644
--- a/test/performance/performance_test.jl
+++ b/test/performance/performance_test.jl
@@ -11,6 +11,8 @@ using HydroPowerSimulations
using HiGHS
using Dates
+@info pkgdir(PowerSimulations)
+
open("precompile_time.txt", "a") do io
write(io, "| $(ARGS[1]) | $(precompile_time.time) |\n")
end
@@ -49,7 +51,7 @@ try
)
template_ed = deepcopy(template_uc)
- set_device_model!(template_ed, ThermalMultiStart, ThermalStandardDispatch)
+ set_device_model!(template_ed, ThermalMultiStart, ThermalBasicDispatch)
set_device_model!(template_ed, ThermalStandard, ThermalBasicDispatch)
set_device_model!(template_ed, HydroDispatch, HydroDispatchRunOfRiver)
set_device_model!(template_ed, HydroEnergyReservoir, HydroDispatchRunOfRiver)
@@ -64,10 +66,11 @@ try
template_uc,
sys_rts_da;
name = "UC",
- optimizer = optimizer_with_attributes(HiGHS.Optimizer),
+ optimizer = optimizer_with_attributes(HiGHS.Optimizer,
+ "mip_rel_gap" => 0.01),
system_to_file = false,
initialize_model = true,
- optimizer_solve_log_print = true,
+ optimizer_solve_log_print = false,
direct_mode_optimizer = true,
check_numerical_bounds = false,
),
@@ -75,7 +78,8 @@ try
template_ed,
sys_rts_rt;
name = "ED",
- optimizer = optimizer_with_attributes(HiGHS.Optimizer),
+ optimizer = optimizer_with_attributes(HiGHS.Optimizer,
+ "mip_rel_gap" => 0.01),
system_to_file = false,
initialize_model = true,
check_numerical_bounds = false,
@@ -123,7 +127,7 @@ try
build_out, time_build, _, _ =
@timed build!(sim; console_level = Logging.Error, serialize = false)
- if build_out == PSI.BuildStatus.BUILT
+ if build_out == PSI.SimulationBuildStatus.BUILT
name = i > 1 ? "Postcompile" : "Precompile"
open("build_time.txt", "a") do io
write(io, "| $(ARGS[1])-Build Time $name | $(time_build) |\n")
@@ -133,6 +137,19 @@ try
write(io, "| $(ARGS[1])- Build Time $name | FAILED TO TEST |\n")
end
end
+
+ solve_out, time_solve, _, _ = @timed execute!(sim; enable_progress_bar = false)
+
+ if solve_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ name = i > 1 ? "Postcompile" : "Precompile"
+ open("solve_time.txt", "a") do io
+ write(io, "| $(ARGS[1])-Solve Time $name | $(time_solve) |\n")
+ end
+ else
+ open("solve_time.txt", "a") do io
+ write(io, "| $(ARGS[1])- Solve Time $name | FAILED TO TEST |\n")
+ end
+ end
end
catch e
rethrow(e)
diff --git a/test/run_partitioned_simulation.jl b/test/run_partitioned_simulation.jl
index 75e2a99d2c..98143761c7 100644
--- a/test/run_partitioned_simulation.jl
+++ b/test/run_partitioned_simulation.jl
@@ -48,9 +48,9 @@ function build_simulation(
error("num_steps and partitions cannot both be set")
end
c_sys5_pjm_da = PSB.build_system(PSISystems, "c_sys5_pjm")
- PSY.transform_single_time_series!(c_sys5_pjm_da, 48, Hour(24))
+ PSY.transform_single_time_series!(c_sys5_pjm_da, Hour(48), Hour(24))
c_sys5_pjm_rt = PSB.build_system(PSISystems, "c_sys5_pjm_rt")
- PSY.transform_single_time_series!(c_sys5_pjm_rt, 12, Hour(1))
+ PSY.transform_single_time_series!(c_sys5_pjm_rt, Hour(1), Hour(1))
for sys in [c_sys5_pjm_da, c_sys5_pjm_rt]
th = get_component(ThermalStandard, sys, "Park City")
@@ -58,8 +58,8 @@ function build_simulation(
set_status!(th, false)
set_active_power!(th, 0.0)
c = get_operation_cost(th)
- c.start_up = 1500
- c.shut_down = 75
+ PSY.set_start_up!(c, 1500.0)
+ PSY.set_shut_down!(c, 75.0)
set_time_at_status!(th, 1)
th = get_component(ThermalStandard, sys, "Alta")
@@ -67,24 +67,24 @@ function build_simulation(
set_active_power_limits!(th, (min = 0.05, max = 0.4))
set_active_power!(th, 0.05)
c = get_operation_cost(th)
- c.start_up = 400
- c.shut_down = 200
+ PSY.set_start_up!(c, 400.0)
+ PSY.set_shut_down!(c, 200.0)
set_time_at_status!(th, 2)
th = get_component(ThermalStandard, sys, "Brighton")
set_active_power_limits!(th, (min = 2.0, max = 6.0))
c = get_operation_cost(th)
set_active_power!(th, 4.88041)
- c.start_up = 5000
- c.shut_down = 3000
+ PSY.set_start_up!(c, 5000.0)
+ PSY.set_shut_down!(c, 3000.0)
th = get_component(ThermalStandard, sys, "Sundance")
set_active_power_limits!(th, (min = 1.0, max = 2.0))
set_time_limits!(th, (up = 5, down = 1))
set_active_power!(th, 2.0)
c = get_operation_cost(th)
- c.start_up = 4000
- c.shut_down = 2000
+ PSY.set_start_up!(c, 4000.0)
+ PSY.set_shut_down!(c, 2000.0)
set_time_at_status!(th, 1)
th = get_component(ThermalStandard, sys, "Solitude")
@@ -92,8 +92,8 @@ function build_simulation(
set_ramp_limits!(th, (up = 0.0052, down = 0.0052))
set_active_power!(th, 2.0)
c = get_operation_cost(th)
- c.start_up = 3000
- c.shut_down = 1500
+ PSY.set_start_up!(c, 3000.0)
+ PSY.set_shut_down!(c, 1500.0)
end
to_json(
@@ -166,7 +166,7 @@ function build_simulation(
status =
build!(sim; partitions = partitions, index = index, serialize = isnothing(index))
- if status != PSI.BuildStatus.BUILT
+ if status != PSI.SimulationBuildStatus.BUILT
error("Failed to build simulation: status=$status")
end
diff --git a/test/runtests.jl b/test/runtests.jl
index 4e6232ec94..f6eaa26fe9 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,32 +1,4 @@
-# SIIP Packages
-using PowerSimulations
-using PowerSystems
-using PowerSystemCaseBuilder
-using InfrastructureSystems
-using PowerNetworkMatrices
-using HydroPowerSimulations
-import PowerSystemCaseBuilder: PSITestSystems
-using PowerNetworkMatrices
-using StorageSystemsSimulations
-
-# Test Packages
-using Test
-using Logging
-
-# Dependencies for testing
-using PowerModels
-using DataFrames
-using Dates
-using JuMP
-using TimeSeries
-using CSV
-import JSON3
-using DataFrames
-using DataStructures
-import UUIDs
-using Random
-import Serialization
-using Base.Filesystem
+include("includes.jl")
# Code Quality Tests
import Aqua
@@ -34,26 +6,8 @@ Aqua.test_unbound_args(PowerSimulations)
Aqua.test_undefined_exports(PowerSimulations)
Aqua.test_ambiguities(PowerSimulations)
-const PM = PowerModels
-const PSY = PowerSystems
-const PSI = PowerSimulations
-const PSB = PowerSystemCaseBuilder
-const PNM = PowerNetworkMatrices
-
-const IS = InfrastructureSystems
-const BASE_DIR = string(dirname(dirname(pathof(PowerSimulations))))
-const DATA_DIR = joinpath(BASE_DIR, "test/test_data")
-
-include("test_utils/common_operation_model.jl")
-include("test_utils/model_checks.jl")
-include("test_utils/mock_operation_models.jl")
-include("test_utils/solver_definitions.jl")
-include("test_utils/operations_problem_templates.jl")
-
const LOG_FILE = "power-simulations-test.log"
-ENV["RUNNING_PSI_TESTS"] = "true"
-
const DISABLED_TEST_FILES = [
# "test_basic_model_structs.jl",
# "test_device_branch_constructors.jl",
@@ -68,7 +22,7 @@ const DISABLED_TEST_FILES = [
# "test_problem_template.jl",
# "test_model_emulation.jl",
# "test_network_constructors.jl",
-"test_services_constructor.jl",
+# "test_services_constructor.jl",
# "test_simulation_models.jl",
# "test_simulation_sequence.jl",
# "test_simulation_build.jl",
diff --git a/test/test_basic_model_structs.jl b/test/test_basic_model_structs.jl
index 031474795f..a5c37eb5c3 100644
--- a/test/test_basic_model_structs.jl
+++ b/test/test_basic_model_structs.jl
@@ -8,6 +8,7 @@ end
@test_throws ArgumentError NetworkModel(PM.AbstractPowerModel)
end
+#=
@testset "ServiceModel Tests" begin
@test_throws ArgumentError ServiceModel(AGC, PSI.AbstractAGCFormulation, "TestName")
@test_throws ArgumentError ServiceModel(
@@ -16,6 +17,7 @@ end
"TestName2",
)
end
+=#
@testset "Feedforward Struct Tests" begin
ffs = [
diff --git a/test/test_device_branch_constructors.jl b/test/test_device_branch_constructors.jl
index 6768d7dc1d..118fda9c60 100644
--- a/test/test_device_branch_constructors.jl
+++ b/test/test_device_branch_constructors.jl
@@ -7,11 +7,11 @@
)
model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)
@test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
@test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine)
@test check_variable_unbounded(model_m, FlowActivePowerVariable, Line)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
FlowActivePowerVariable,
@@ -27,12 +27,13 @@ end
limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, "1"))
template = get_thermal_dispatch_template_network(ACPPowerModel)
model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)
- @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test check_variable_bounded(model_m, FlowActivePowerFromToVariable, MonitoredLine)
@test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, MonitoredLine)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
FlowActivePowerFromToVariable,
@@ -46,7 +47,7 @@ end
@testset "DC Power Flow Models Monitored Line Flow Constraints and Static with inequalities" begin
system = PSB.build_system(PSITestSystems, "c_sys5_ml")
- set_rate!(PSY.get_component(Line, system, "2"), 1.5)
+ set_rating!(PSY.get_component(Line, system, "2"), 1.5)
for model in [DCPPowerModel, PTDFPowerModel]
template = get_thermal_dispatch_template_network(
NetworkModel(model; PTDF_matrix = PTDF(system)),
@@ -55,18 +56,18 @@ end
set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded))
model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)
@test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
@test check_variable_unbounded(model_m, FlowActivePowerVariable, MonitoredLine)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, "2", 1.5)
end
end
@testset "DC Power Flow Models Monitored Line Flow Constraints and Static with Bounds" begin
system = PSB.build_system(PSITestSystems, "c_sys5_ml")
- set_rate!(PSY.get_component(Line, system, "2"), 1.5)
+ set_rating!(PSY.get_component(Line, system, "2"), 1.5)
for model in [DCPPowerModel, PTDFPowerModel]
template = get_thermal_dispatch_template_network(
NetworkModel(model; PTDF_matrix = PTDF(system)),
@@ -75,12 +76,12 @@ end
set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded))
model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)
@test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
@test check_variable_unbounded(model_m, FlowActivePowerVariable, MonitoredLine)
@test check_variable_bounded(model_m, FlowActivePowerVariable, Line)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, "2", 1.5)
end
end
@@ -101,26 +102,28 @@ end
limits_max = min(limits_from.max, limits_to.max)
tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
- rate_limit = PSY.get_rate(tap_transformer)
+ rate_limit = PSY.get_rating(tap_transformer)
transformer = PSY.get_component(Transformer2W, system, "Trans4")
- rate_limit2w = PSY.get_rate(tap_transformer)
+ rate_limit2w = PSY.get_rating(tap_transformer)
for model in [DCPPowerModel, PTDFPowerModel]
template = get_template_dispatch_with_network(
- NetworkModel(model; PTDF_matrix = PTDF(system)),
+ NetworkModel(model),
)
set_device_model!(template, TwoTerminalHVDCLine, HVDCTwoTerminalLossless)
+ set_device_model!(template, DeviceModel(Transformer2W, StaticBranch))
+ set_device_model!(template, DeviceModel(TapTransformer, StaticBranch))
model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)
@test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
@test check_variable_unbounded(model_m, FlowActivePowerVariable, TapTransformer)
@test check_variable_unbounded(model_m, FlowActivePowerVariable, Transformer2W)
psi_constraint_test(model_m, ratelimit_constraint_keys)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
@@ -158,10 +161,10 @@ end
limits_max = min(limits_from.max, limits_to.max)
tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
- rate_limit = PSY.get_rate(tap_transformer)
+ rate_limit = PSY.get_rating(tap_transformer)
transformer = PSY.get_component(Transformer2W, system, "Trans4")
- rate_limit2w = PSY.get_rate(tap_transformer)
+ rate_limit2w = PSY.get_rating(tap_transformer)
for model in [DCPPowerModel, PTDFPowerModel]
template = get_template_dispatch_with_network(
@@ -175,7 +178,7 @@ end
set_device_model!(template, DeviceModel(Transformer2W, StaticBranchBounds))
model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)
@test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
@test check_variable_unbounded(
model_m,
@@ -185,7 +188,7 @@ end
@test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
@test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
@@ -237,10 +240,10 @@ end
add_component!(sys_5, hvdc)
template_uc = ProblemTemplate(
- NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(sys_5)),
+ NetworkModel(PTDFPowerModel),
)
- set_device_model!(template_uc, ThermalStandard, ThermalCompactUnitCommitment)
+ set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, DeviceModel(Line, StaticBranch))
@@ -260,12 +263,15 @@ end
solve!(model)
- ptdf_vars = get_variable_values(ProblemResults(model))
+ ptdf_vars = get_variable_values(OptimizationProblemResults(model))
ptdf_values =
- ptdf_vars[PowerSimulations.VariableKey{FlowActivePowerVariable, TwoTerminalHVDCLine}(
+ ptdf_vars[PowerSimulations.VariableKey{
+ FlowActivePowerVariable,
+ TwoTerminalHVDCLine,
+ }(
"",
)]
- ptdf_objective = model.internal.container.optimizer_stats.objective_value
+ ptdf_objective = PSI.get_optimization_container(model).optimizer_stats.objective_value
set_network_model!(template_uc, NetworkModel(DCPPowerModel))
@@ -278,12 +284,16 @@ end
)
solve!(model; output_dir = mktempdir())
- dcp_vars = get_variable_values(ProblemResults(model))
+ dcp_vars = get_variable_values(OptimizationProblemResults(model))
dcp_values =
- dcp_vars[PowerSimulations.VariableKey{FlowActivePowerVariable, TwoTerminalHVDCLine}(
+ dcp_vars[PowerSimulations.VariableKey{
+ FlowActivePowerVariable,
+ TwoTerminalHVDCLine,
+ }(
"",
)]
- dcp_objective = model.internal.container.optimizer_stats.objective_value
+ dcp_objective =
+ PSI.get_optimization_container(model).optimizer_stats.objective_value
@test isapprox(dcp_objective, ptdf_objective; atol = 0.1)
# Resulting solution is in the 4e5 order of magnitude
@@ -315,10 +325,10 @@ end
@testset "$net_model" begin
PSY.set_loss!(hvdc, (l0 = 0.0, l1 = 0.0))
template_uc = ProblemTemplate(
- NetworkModel(net_model; PTDF_matrix = PTDF(sys_5), use_slacks = true),
+ NetworkModel(net_model; use_slacks = true),
)
- set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
+ set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, DeviceModel(Line, StaticBranchUnbounded))
@@ -331,12 +341,13 @@ end
template_uc,
sys_5;
name = "UC",
- optimizer = HiGHS_optimizer,
+ optimizer = HiGHS.Optimizer,
system_to_file = false,
+ store_variable_names = true,
)
solve!(model_ref; output_dir = mktempdir())
- ref_vars = get_variable_values(ProblemResults(model_ref))
+ ref_vars = get_variable_values(OptimizationProblemResults(model_ref))
ref_values =
ref_vars[PowerSimulations.VariableKey{FlowActivePowerVariable, Line}("")]
hvdc_ref_values = ref_vars[PowerSimulations.VariableKey{
@@ -367,12 +378,12 @@ end
template_uc,
sys_5;
name = "UC",
- optimizer = HiGHS_optimizer,
+ optimizer = HiGHS.Optimizer,
system_to_file = false,
)
solve!(model; output_dir = mktempdir())
- no_loss_vars = get_variable_values(ProblemResults(model))
+ no_loss_vars = get_variable_values(OptimizationProblemResults(model))
no_loss_values =
no_loss_vars[PowerSimulations.VariableKey{FlowActivePowerVariable, Line}(
"",
@@ -389,7 +400,8 @@ end
}(
"",
)]
- no_loss_objective = model.internal.container.optimizer_stats.objective_value
+ no_loss_objective =
+ PSI.get_optimization_container(model).optimizer_stats.objective_value
no_loss_total_gen = sum(
sum.(
eachrow(
@@ -430,7 +442,7 @@ end
)
solve!(model_wl; output_dir = mktempdir())
- dispatch_vars = get_variable_values(ProblemResults(model_wl))
+ dispatch_vars = get_variable_values(OptimizationProblemResults(model_wl))
dispatch_values_ft = dispatch_vars[PowerSimulations.VariableKey{
FlowActivePowerFromToVariable,
TwoTerminalHVDCLine,
@@ -489,17 +501,20 @@ end
limits_max = min(limits_from.max, limits_to.max)
tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
- rate_limit = PSY.get_rate(tap_transformer)
+ rate_limit = PSY.get_rating(tap_transformer)
transformer = PSY.get_component(Transformer2W, system, "Trans4")
- rate_limit2w = PSY.get_rate(tap_transformer)
+ rate_limit2w = PSY.get_rating(tap_transformer)
template = get_template_dispatch_with_network(
- NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(system)),
+ NetworkModel(PTDFPowerModel),
)
+ set_device_model!(template, DeviceModel(TapTransformer, StaticBranch))
+ set_device_model!(template, DeviceModel(Transformer2W, StaticBranch))
set_device_model!(template, DeviceModel(TwoTerminalHVDCLine, HVDCTwoTerminalLossless))
model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)
- @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test !check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
@test !check_variable_bounded(model_m, FlowActivePowerVariable, Transformer2W)
@@ -507,7 +522,7 @@ end
psi_constraint_test(model_m, ratelimit_constraint_keys)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
@@ -548,7 +563,7 @@ end
primary_shunt = 0.0,
tap = 1.0,
α = 0.0,
- rate = get_rate(line),
+ rating = get_rating(line),
arc = get_arc(line),
)
@@ -559,7 +574,8 @@ end
)
set_device_model!(template, DeviceModel(PhaseShiftingTransformer, PhaseAngleControl))
model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer)
- @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test check_variable_unbounded(model_m, FlowActivePowerVariable, Line)
@test check_variable_unbounded(
@@ -568,14 +584,14 @@ end
PhaseShiftingTransformer,
)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
FlowActivePowerVariable,
PhaseShiftingTransformer,
"1",
- get_rate(ps),
+ get_rating(ps),
)
@test check_flow_variable_values(
@@ -607,16 +623,18 @@ end
limits_max = min(limits_from.max, limits_to.max)
tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
- rate_limit = PSY.get_rate(tap_transformer)
+ rate_limit = PSY.get_rating(tap_transformer)
transformer = PSY.get_component(Transformer2W, system, "Trans4")
- rate_limit2w = PSY.get_rate(tap_transformer)
+ rate_limit2w = PSY.get_rating(tap_transformer)
template = get_template_dispatch_with_network(ACPPowerModel)
+ set_device_model!(template, TapTransformer, StaticBranchBounds)
+ set_device_model!(template, Transformer2W, StaticBranchBounds)
set_device_model!(template, DeviceModel(TwoTerminalHVDCLine, HVDCTwoTerminalLossless))
model_m = DecisionModel(template, system; optimizer = ipopt_optimizer)
- @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
-
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test check_variable_bounded(model_m, FlowActivePowerFromToVariable, TapTransformer)
@test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, TapTransformer)
@test check_variable_bounded(model_m, FlowActivePowerToFromVariable, Transformer2W)
@@ -624,7 +642,7 @@ end
psi_constraint_test(model_m, ratelimit_constraint_keys)
- @test solve!(model_m) == RunStatus.SUCCESSFUL
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test check_flow_variable_values(
model_m,
@@ -651,3 +669,62 @@ end
rate_limit2w,
)
end
+
+@testset "Test Line and Monitored Line models with slacks" begin
+ system = PSB.build_system(PSITestSystems, "c_sys5_ml")
+ set_rating!(PSY.get_component(Line, system, "2"), 0.0)
+ for (model, optimizer) in NETWORKS_FOR_TESTING
+ if model ∈ [PM.SDPWRMPowerModel, PM.SparseSDPWRMPowerModel, SOCWRConicPowerModel]
+ # Skip because the data is too in the feasibility margins for these models
+ continue
+ end
+ template = get_thermal_dispatch_template_network(
+ NetworkModel(model; use_slacks = true),
+ )
+ set_device_model!(template, DeviceModel(Line, StaticBranch; use_slacks = true))
+ set_device_model!(
+ template,
+ DeviceModel(MonitoredLine, StaticBranch; use_slacks = true),
+ )
+ model_m = DecisionModel(template, system; optimizer = optimizer)
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(model_m)
+ vars = read_variable(res, "FlowActivePowerSlackUpperBound__Line")
+ # some relaxations will find a solution with 0.0 slack
+ @test sum(vars[!, "2"]) >= -1e-6
+ end
+
+ template = get_thermal_dispatch_template_network(
+ NetworkModel(PTDFPowerModel; use_slacks = true),
+ )
+ set_device_model!(template, DeviceModel(Line, StaticBranchBounds; use_slacks = true))
+ set_device_model!(
+ template,
+ DeviceModel(MonitoredLine, StaticBranchBounds; use_slacks = true),
+ )
+ model_m = DecisionModel(template, system; optimizer = fast_ipopt_optimizer)
+ @test build!(
+ model_m;
+ console_level = Logging.AboveMaxLevel,
+ output_dir = mktempdir(; cleanup = true),
+ ) == PSI.ModelBuildStatus.FAILED
+
+ template = get_thermal_dispatch_template_network(
+ NetworkModel(PTDFPowerModel; use_slacks = true),
+ )
+ set_device_model!(template, DeviceModel(Line, StaticBranch; use_slacks = true))
+ set_device_model!(
+ template,
+ DeviceModel(MonitoredLine, StaticBranch; use_slacks = true),
+ )
+ model_m = DecisionModel(template, system; optimizer = fast_ipopt_optimizer)
+ @test build!(model_m; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(model_m) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(model_m)
+ vars = read_variable(res, "FlowActivePowerSlackUpperBound__Line")
+ # some relaxations will find a solution with 0.0 slack
+ @test sum(vars[!, "2"]) >= -1e-6
+end
diff --git a/test/test_device_hvdc.jl b/test/test_device_hvdc.jl
index cbc2980b4c..2829182c3a 100644
--- a/test/test_device_hvdc.jl
+++ b/test/test_device_hvdc.jl
@@ -7,16 +7,16 @@
#duals=[CopperPlateBalanceConstraint],
))
- set_device_model!(template_uc, ThermalStandard, ThermalCompactUnitCommitment)
+ set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, DeviceModel(Line, StaticBranch))
set_device_model!(template_uc, DeviceModel(InterconnectingConverter, LossLessConverter))
set_device_model!(template_uc, DeviceModel(TModelHVDCLine, LossLessLine))
model = DecisionModel(template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir()) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT
moi_tests(model, 1656, 288, 1248, 528, 888, true)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
template_uc = ProblemTemplate(NetworkModel(
PTDFPowerModel;
@@ -25,14 +25,14 @@
#duals=[CopperPlateBalanceConstraint],
))
- set_device_model!(template_uc, ThermalStandard, ThermalCompactUnitCommitment)
+ set_device_model!(template_uc, ThermalStandard, ThermalStandardUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, DeviceModel(Line, StaticBranch))
set_device_model!(template_uc, DeviceModel(InterconnectingConverter, LossLessConverter))
set_device_model!(template_uc, DeviceModel(TModelHVDCLine, LossLessLine))
model = DecisionModel(template_uc, sys_5; name = "UC", optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir()) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir()) == PSI.ModelBuildStatus.BUILT
moi_tests(model, 1416, 0, 1248, 528, 672, true)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
diff --git a/test/test_device_thermal_generation_constructors.jl b/test/test_device_thermal_generation_constructors.jl
index 4f9f1f931d..c7387981c3 100644
--- a/test/test_device_thermal_generation_constructors.jl
+++ b/test/test_device_thermal_generation_constructors.jl
@@ -1,4 +1,106 @@
test_path = mktempdir()
+
+@testset "Test Thermal Generation Cost Functions " begin
+ test_cases = [
+ ("linear_cost_test", 4664.88, ThermalBasicUnitCommitment),
+ ("linear_fuel_test", 4664.88, ThermalBasicUnitCommitment),
+ ("quadratic_cost_test", 3301.81, ThermalDispatchNoMin),
+ ("quadratic_fuel_test", 3331.12, ThermalDispatchNoMin),
+ ("pwl_io_cost_test", 3421.64, ThermalBasicUnitCommitment),
+ ("pwl_io_fuel_test", 3421.64, ThermalBasicUnitCommitment),
+ ("pwl_incremental_cost_test", 3424.43, ThermalBasicUnitCommitment),
+ ("pwl_incremental_fuel_test", 3424.43, ThermalBasicUnitCommitment),
+ ("non_convex_io_pwl_cost_test", 3047.14, ThermalBasicUnitCommitment),
+ ]
+ for (i, cost_reference, thermal_formulation) in test_cases
+ @testset "$i" begin
+ sys = build_system(PSITestSystems, "c_$(i)")
+ template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))
+ set_device_model!(template, ThermalStandard, thermal_formulation)
+ set_device_model!(template, PowerLoad, StaticPowerLoad)
+ model = DecisionModel(
+ template,
+ sys;
+ name = "UC_$(i)",
+ optimizer = HiGHS_optimizer,
+ system_to_file = false,
+ optimizer_solve_log_print = true,
+ )
+ @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results = OptimizationProblemResults(model)
+ expr = read_expression(results, "ProductionCostExpression__ThermalStandard")
+ var_unit_cost = sum(expr[!, "Test Unit"])
+ @test isapprox(var_unit_cost, cost_reference; atol = 1)
+ @test expr[!, "Test Unit"][end] == 0.0
+ end
+ end
+end
+
+#TODO: This test
+#=
+@testset "Test Thermal Generation Cost Functions Fuel Cost time series" begin
+ test_cases = [
+ "linear_fuel_test_ts",
+ "quadratic_fuel_test_ts",
+ "pwl_io_fuel_test_ts",
+ "pwl_incremental_fuel_test_ts",
+ "market_bid_cost",
+ ]
+ for i in test_cases
+ @testset "$i" begin
+ sys = build_system(PSITestSystems, "c_$(i)")
+ template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))
+ set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)
+ #=
+ model = DecisionModel(
+ template,
+ sys;
+ name = "UC_$(i)",
+ optimizer = HiGHS_optimizer,
+ system_to_file = false,
+ )
+ @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ =#
+ end
+ end
+end
+=#
+
+#=
+#TODO: This test
+@testset "Test Thermal Generation MarketBidCost models" begin
+ test_cases = [
+ ("fixed_market_bid_cost", 20532.76),
+ #"market_bid_cost",
+ ]
+ for (i, cost_reference) in test_cases
+ @testset "$i" begin
+ sys = build_system(PSITestSystems, "c_$(i)")
+ template = ProblemTemplate(NetworkModel(CopperPlatePowerModel))
+ set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)
+ set_device_model!(template, PowerLoad, StaticPowerLoad)
+ model = DecisionModel(
+ template,
+ sys;
+ name = "UC_$(i)",
+ optimizer = HiGHS_optimizer,
+ system_to_file = false,
+ optimizer_solve_log_print = true,
+ )
+ @test build!(model; output_dir = test_path) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results = OptimizationProblemResults(model)
+ expr = read_expression(results, "ProductionCostExpression__ThermalStandard")
+ var_unit_cost = sum(expr[!, "Test Unit1"])
+ @test isapprox(var_unit_cost, cost_reference; atol = 1)
+ @test expr[!, "Test Unit1"][end] == 0.0
+ end
+ end
+end
+=#
+
################################### Unit Commitment tests ##################################
@testset "Thermal UC With DC - PF" begin
bin_variable_keys = [
@@ -407,8 +509,16 @@ end
PSY.ThermalMultiStart,
"hot",
),
- PSI.ConstraintKey(StartupInitialConditionConstraint, PSY.ThermalMultiStart, "lb"),
- PSI.ConstraintKey(StartupInitialConditionConstraint, PSY.ThermalMultiStart, "ub"),
+ PSI.ConstraintKey(
+ StartupInitialConditionConstraint,
+ PSY.ThermalMultiStart,
+ "lb",
+ ),
+ PSI.ConstraintKey(
+ StartupInitialConditionConstraint,
+ PSY.ThermalMultiStart,
+ "ub",
+ ),
]
device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)
no_less_than = Dict(true => 334, false => 282)
@@ -434,8 +544,16 @@ end
PSY.ThermalMultiStart,
"hot",
),
- PSI.ConstraintKey(StartupInitialConditionConstraint, PSY.ThermalMultiStart, "lb"),
- PSI.ConstraintKey(StartupInitialConditionConstraint, PSY.ThermalMultiStart, "ub"),
+ PSI.ConstraintKey(
+ StartupInitialConditionConstraint,
+ PSY.ThermalMultiStart,
+ "lb",
+ ),
+ PSI.ConstraintKey(
+ StartupInitialConditionConstraint,
+ PSY.ThermalMultiStart,
+ "ub",
+ ),
]
device_model = DeviceModel(PSY.ThermalMultiStart, PSI.ThermalMultiStartUnitCommitment)
no_less_than = Dict(true => 382, false => 330)
@@ -571,7 +689,7 @@ end
optimizer = HiGHS_optimizer,
initialize_model = false,
)
- @test build!(ED; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(ED; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT
moi_tests(ED, 10, 0, 15, 15, 5, false)
psi_checksolve_test(ED, [MOI.OPTIMAL], 11191.00)
end
@@ -585,44 +703,17 @@ end
PSB.build_system(PSITestSystems, "c_duration_test");
optimizer = HiGHS_optimizer,
initialize_model = false,
+ store_variable_names = true,
)
- @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ build!(UC; output_dir = mktempdir(; cleanup = true))
+ @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT
moi_tests(UC, 56, 0, 56, 14, 21, true)
psi_checksolve_test(UC, [MOI.OPTIMAL], 8223.50)
end
-## PWL linear Cost implementation test
-@testset "Solving UC with CopperPlate testing Convex PWL" begin
- template = get_thermal_standard_uc_template()
- UC = DecisionModel(
- UnitCommitmentProblem,
- template,
- PSB.build_system(PSITestSystems, "c_linear_pwl_test");
- optimizer = HiGHS_optimizer,
- initialize_model = false,
- )
- @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(UC, 32, 0, 8, 4, 14, true)
- psi_checksolve_test(UC, [MOI.OPTIMAL], 9336.736919354838)
-end
-
-@testset "Solving UC with CopperPlate testing PWL-SOS2 implementation" begin
- template = get_thermal_standard_uc_template()
- UC = DecisionModel(
- UnitCommitmentProblem,
- template,
- PSB.build_system(PSITestSystems, "c_sos_pwl_test");
- optimizer = cbc_optimizer,
- initialize_model = false,
- )
- @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(UC, 32, 0, 8, 4, 14, true)
- # Cbc can have reliability issues with SoS. The objective function target in the this
- # test was calculated with CPLEX do not change if Cbc gets a bad result
- psi_checksolve_test(UC, [MOI.OPTIMAL], 8500.0, 10.0)
-end
-
+#= Test disabled due to inconsistency between the models and the data
@testset "UC with MarketBid Cost in ThermalGenerators" begin
+ sys = PSB.build_system(PSITestSystems, "c_market_bid_cost")
template = get_thermal_standard_uc_template()
set_device_model!(
template,
@@ -631,13 +722,14 @@ end
UC = DecisionModel(
UnitCommitmentProblem,
template,
- PSB.build_system(PSITestSystems, "c_market_bid_cost");
+ sys;
optimizer = cbc_optimizer,
initialize_model = false,
)
- @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.ModelBuildStatus.BUILT
moi_tests(UC, 38, 0, 16, 8, 16, true)
end
+=#
@testset "Solving UC Models with Linear Networks" begin
c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
@@ -645,15 +737,15 @@ end
systems = [c_sys5, c_sys5_dc]
networks = [DCPPowerModel, NFAPowerModel, PTDFPowerModel, CopperPlatePowerModel]
commitment_models = [ThermalStandardUnitCommitment, ThermalCompactUnitCommitment]
- PTDF_ref = IdDict{System, PTDF}(c_sys5 => PTDF(c_sys5), c_sys5_dc => PTDF(c_sys5_dc))
for net in networks, sys in systems, model in commitment_models
template = get_thermal_dispatch_template_network(
- NetworkModel(net; PTDF_matrix = PTDF_ref[sys]),
+ NetworkModel(net),
)
set_device_model!(template, ThermalStandard, model)
UC = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
- @test build!(UC; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(UC; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
psi_checksolve_test(UC, [MOI.OPTIMAL, MOI.LOCALLY_SOLVED], 340000, 100000)
end
end
@@ -794,7 +886,7 @@ end
sys_5 = build_system(PSITestSystems, "c_sys5_uc")
template_uc =
ProblemTemplate(NetworkModel(PTDFPowerModel; PTDF_matrix = PTDF(sys_5)))
- set_device_model!(template_uc, ThermalStandard, ThermalCompactUnitCommitment)
+ set_device_model!(template_uc, ThermalStandard, ThermalBasicUnitCommitment)
set_device_model!(template_uc, RenewableDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, DeviceModel(Line, StaticBranch))
@@ -811,7 +903,7 @@ end
)
solve!(model; output_dir = mktempdir())
- ptdf_vars = get_variable_values(ProblemResults(model))
+ ptdf_vars = get_variable_values(OptimizationProblemResults(model))
on = ptdf_vars[PowerSimulations.VariableKey{OnVariable, ThermalStandard}("")]
on_sundance = on[!, "Sundance"]
@test all(isapprox.(on_sundance, 1.0))
diff --git a/test/test_formulation_combinations.jl b/test/test_formulation_combinations.jl
index 763f19f595..7801c3299a 100644
--- a/test/test_formulation_combinations.jl
+++ b/test/test_formulation_combinations.jl
@@ -13,13 +13,13 @@
end
for item in res["service_formulations"]
- if item["service_type"] == PSY.StaticReserveNonSpinning &&
+ if item["service_type"] == PSY.ConstantReserveNonSpinning &&
item["formulation"] == PSI.NonSpinningReserve
found_valid_service = true
end
- if item["service_type"] == PSY.AGC && item["formulation"] == PSI.NonSpinningReserve
- found_invalid_service = true
- end
+ #if item["service_type"] == PSY.AGC && item["formulation"] == PSI.NonSpinningReserve
+ # found_invalid_service = true
+ #end
end
@test found_valid_device
diff --git a/test/test_ic_reconciliation.jl b/test/test_ic_reconciliation.jl
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/test/test_ic_reconciliation.jl
@@ -0,0 +1 @@
+
diff --git a/test/test_initialization_problem.jl b/test/test_initialization_problem.jl
index dc98503873..3e40dfbf01 100644
--- a/test/test_initialization_problem.jl
+++ b/test/test_initialization_problem.jl
@@ -16,9 +16,10 @@ sys_rts = PSB.build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
sys_rts;
optimizer = HiGHS_optimizer,
initial_time = init_time,
- horizon = 48,
+ horizon = Hour(48),
)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
####### Check initialization problem
check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)
@@ -69,7 +70,7 @@ sys_rts = PSB.build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
meta = "ub",
)
- # @test solve!(model) == RunStatus.SUCCESSFUL
+ # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
end
@@ -89,9 +90,10 @@ end
sys_rts;
optimizer = HiGHS_optimizer,
initial_time = init_time,
- horizon = 48,
+ horizon = Hour(48),
)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
####### Check initialization problem
check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)
@@ -150,7 +152,7 @@ end
meta = "ub",
)
- # @test solve!(model) == RunStatus.SUCCESSFUL
+ # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
end
@@ -170,9 +172,10 @@ end
sys_rts;
optimizer = HiGHS_optimizer,
initial_time = init_time,
- horizon = 48,
+ horizon = Hour(48),
)
PSI.instantiate_network_model(model)
+ PSI.build_pre_step!(model)
setup_ic_model_container!(model)
####### Check initialization problem constraints #####
check_initialization_constraint_count(
@@ -223,7 +226,8 @@ end
meta = "ub",
)
PSI.reset!(model)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
####### Check initialization problem
check_initialization_variable_count(
@@ -293,6 +297,6 @@ end
meta = "ub",
)
- # @test solve!(model) == RunStatus.SUCCESSFUL
+ # @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
end
diff --git a/test/test_model_decision.jl b/test/test_model_decision.jl
index 1b5361c5c1..fbcdc0392c 100644
--- a/test/test_model_decision.jl
+++ b/test/test_model_decision.jl
@@ -6,7 +6,8 @@
@test_throws MethodError DecisionModel(template, c_sys5; bad_kwarg = 10)
model = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
model = DecisionModel(
MockOperationProblem,
@@ -16,13 +17,15 @@
c_sys5_re;
optimizer = GLPK_optimizer,
)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
model = DecisionModel(
get_thermal_dispatch_template_network(),
c_sys5;
optimizer = GLPK_optimizer,
)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
#"Test passing custom JuMP model"
my_model = JuMP.Model()
@@ -34,7 +37,8 @@
my_model;
optimizer = GLPK_optimizer,
)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test haskey(PSI.get_optimization_container(model).JuMPmodel.ext, :PSI_Testing)
end
@@ -47,9 +51,9 @@ end
)
UC = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
output_dir = mktempdir(; cleanup = true)
- @test build!(UC; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(UC; optimizer = GLPK_optimizer) == RunStatus.SUCCESSFUL
- res = ProblemResults(UC)
+ @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(UC; optimizer = GLPK_optimizer) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(UC)
@test isapprox(get_objective_value(res), 340000.0; atol = 100000.0)
vars = res.variable_values
@test PSI.VariableKey(ActivePowerVariable, PSY.ThermalStandard) in keys(vars)
@@ -60,7 +64,7 @@ end
@test length(read_variables(res)) == 4
@test length(read_parameters(res)) == 1
@test length(read_duals(res)) == 0
- @test length(read_expressions(res)) == 1
+ @test length(read_expressions(res)) == 2
@test read_variables(res, ["StartVariable__ThermalStandard"])["StartVariable__ThermalStandard"] ==
read_variable(res, "StartVariable__ThermalStandard")
@test read_variables(res, [(StartVariable, ThermalStandard)])["StartVariable__ThermalStandard"] ==
@@ -95,7 +99,8 @@ end
ServiceModel(VariableReserve{ReserveUp}, RangeReserve, "test"),
)
model = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
container = PSI.get_optimization_container(model)
MOIU.attach_optimizer(container.JuMPmodel)
constraint_indices = get_all_constraint_index(model)
@@ -110,7 +115,7 @@ end
var_index = get_all_variable_index(model)
for (ix, (key, index, moi_index)) in enumerate(var_keys)
index_tuple = var_index[ix]
- @test index_tuple[1] == PSI.encode_key(key)
+ @test index_tuple[1] == IS.Optimization.encode_key(key)
@test index_tuple[2] == index
@test index_tuple[3] == moi_index
val1 = get_variable_index(model, moi_index)
@@ -129,8 +134,8 @@ end
)
model = DecisionModel(template, c_sys5_re; optimizer = ipopt_optimizer)
@test build!(model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
- @test solve!(model) == RunStatus.SUCCESSFUL
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
end
@@ -153,9 +158,9 @@ end
end
model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
@test build!(model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
- @test solve!(model) == RunStatus.SUCCESSFUL
- res = ProblemResults(model)
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(model)
# These tests require results to be working
if network == PTDFPowerModel
@@ -169,16 +174,17 @@ end
@test isapprox(LMPs[1], LMPs[2], atol = 100.0)
end
-@testset "Test ProblemResults interfaces" begin
+@testset "Test OptimizationProblemResults interfaces" begin
sys = PSB.build_system(PSITestSystems, "c_sys5_re")
template = get_template_dispatch_with_network(
NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]),
)
model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- res = ProblemResults(model)
+ res = OptimizationProblemResults(model)
container = PSI.get_optimization_container(model)
constraint_key = PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)
constraints = PSI.get_constraints(container)[constraint_key]
@@ -212,21 +218,23 @@ end
@test all(vals .== param_vals[!, name])
end
- res = ProblemResults(model)
+ res = OptimizationProblemResults(model)
@test length(list_variable_names(res)) == 1
@test length(list_dual_names(res)) == 1
@test get_model_base_power(res) == 100.0
@test isa(get_objective_value(res), Float64)
@test isa(res.variable_values, Dict{PSI.VariableKey, DataFrames.DataFrame})
@test isa(read_variables(res), Dict{String, DataFrames.DataFrame})
- @test isa(PSI.get_total_cost(res), Float64)
+ @test isa(IS.Optimization.get_total_cost(res), Float64)
@test isa(get_optimizer_stats(res), DataFrames.DataFrame)
@test isa(res.dual_values, Dict{PSI.ConstraintKey, DataFrames.DataFrame})
@test isa(read_duals(res), Dict{String, DataFrames.DataFrame})
@test isa(res.parameter_values, Dict{PSI.ParameterKey, DataFrames.DataFrame})
@test isa(read_parameters(res), Dict{String, DataFrames.DataFrame})
@test isa(PSI.get_resolution(res), Dates.TimePeriod)
- @test isa(get_system(res), PSY.System)
+ @test isa(PSI.get_forecast_horizon(res), Int64)
+ @test isa(get_realized_timestamps(res), StepRange{DateTime})
+ @test isa(IS.Optimization.get_source_data(res), PSY.System)
@test length(get_timestamps(res)) == 24
end
@@ -241,7 +249,7 @@ end
output_dir = mktempdir(; cleanup = true)
@test_throws ErrorException solve!(UC)
@test solve!(UC; optimizer = GLPK_optimizer, output_dir = output_dir) ==
- RunStatus.SUCCESSFUL
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Test Serialization, deserialization and write optimizer problem" begin
@@ -251,23 +259,23 @@ end
NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]),
)
model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = fpath) == PSI.BuildStatus.BUILT
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test build!(model; output_dir = fpath) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
file_list = sort!(collect(readdir(fpath)))
model_name = PSI.get_name(model)
@test PSI._JUMP_MODEL_FILENAME in file_list
@test PSI._SERIALIZED_MODEL_FILENAME in file_list
ED2 = DecisionModel(fpath, HiGHS_optimizer)
- @test build!(ED2; output_dir = fpath) == PSI.BuildStatus.BUILT
+ @test build!(ED2; output_dir = fpath) == PSI.ModelBuildStatus.BUILT
psi_checksolve_test(ED2, [MOI.OPTIMAL], 240000.0, 10000)
path2 = mktempdir(; cleanup = true)
model_no_sys =
DecisionModel(template, sys; optimizer = HiGHS_optimizer, system_to_file = false)
- @test build!(model_no_sys; output_dir = path2) == PSI.BuildStatus.BUILT
- @test solve!(model_no_sys) == RunStatus.SUCCESSFUL
+ @test build!(model_no_sys; output_dir = path2) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model_no_sys) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
file_list = sort!(collect(readdir(path2)))
@test .!all(occursin.(r".h5", file_list))
@@ -290,10 +298,11 @@ end
UC = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)
output_dir = mktempdir(; cleanup = true)
- @test build!(UC; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(UC) == RunStatus.SUCCESSFUL
- res = ProblemResults(UC)
- @test isapprox(get_objective_value(res), 247448.0; atol = 10000.0)
+ @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(UC)
+ # This test needs to be reviewed
+ # @test isapprox(get_objective_value(res), 256937.0; atol = 10000.0)
vars = res.variable_values
service_key = PSI.VariableKey(
ActivePowerReserveVariable,
@@ -310,28 +319,29 @@ end
NetworkModel(CopperPlatePowerModel; duals = [CopperPlateBalanceConstraint]),
)
model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = path) == PSI.BuildStatus.BUILT
- @test solve!(model; export_problem_results = true) == RunStatus.SUCCESSFUL
- results1 = ProblemResults(model)
+ @test build!(model; output_dir = path) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model; export_problem_results = true) ==
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results1 = OptimizationProblemResults(model)
var1_a = read_variable(results1, ActivePowerVariable, ThermalStandard)
# Ensure that we can deserialize strings into keys.
var1_b = read_variable(results1, "ActivePowerVariable__ThermalStandard")
# Results were automatically serialized here.
- results2 = ProblemResults(PSI.get_output_dir(model))
+ results2 = OptimizationProblemResults(PSI.get_output_dir(model))
var2 = read_variable(results2, ActivePowerVariable, ThermalStandard)
@test var1_a == var2
# Serialize to a new directory with the exported function.
results_path = joinpath(path, "results")
serialize_results(results1, results_path)
- @test isfile(joinpath(results_path, PSI._PROBLEM_RESULTS_FILENAME))
- results3 = ProblemResults(results_path)
+ @test isfile(joinpath(results_path, IS.Optimization._PROBLEM_RESULTS_FILENAME))
+ results3 = OptimizationProblemResults(results_path)
var3 = read_variable(results3, ActivePowerVariable, ThermalStandard)
@test var1_a == var3
@test get_system(results3) === nothing
set_system!(results3, get_system(results1))
- @test get_system(results3) !== nothing
+ @test get_system(results3) isa PSY.System
exp_file =
joinpath(path, "results", "variables", "ActivePowerVariable__ThermalStandard.csv")
@@ -339,7 +349,7 @@ end
# Manually Multiply by the base power var1_a has natural units and export writes directly from the solver
@test var1_a[:, propertynames(var1_a) .!= :DateTime] == var4 .* 100.0
- @test length(readdir(export_realized_results(results1))) === 6
+ @test length(readdir(IS.Optimization.export_realized_results(results1))) === 7
end
@testset "Test Numerical Stability of Constraints" begin
@@ -348,7 +358,8 @@ end
valid_bounds =
(coefficient = (min = 1.0, max = 1.0), rhs = (min = 0.4, max = 9.930296584))
model = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
bounds = PSI.get_constraint_numerical_bounds(model)
_check_constraint_bounds(bounds, valid_bounds)
@@ -367,7 +378,7 @@ end
for (constraint_key, constraint_bounds) in model_bounds
_check_constraint_bounds(
constraint_bounds,
- valid_model_bounds[PSI.encode_key(constraint_key)],
+ valid_model_bounds[IS.Optimization.encode_key(constraint_key)],
)
end
end
@@ -377,7 +388,8 @@ end
c_sys5 = PSB.build_system(PSITestSystems, "c_sys5_uc")
valid_bounds = (min = 0.0, max = 6.0)
model = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
bounds = PSI.get_variable_numerical_bounds(model)
_check_variable_bounds(bounds, valid_bounds)
@@ -392,7 +404,7 @@ end
for (variable_key, variable_bounds) in model_bounds
_check_variable_bounds(
variable_bounds,
- valid_model_bounds[PSI.encode_key(variable_key)],
+ valid_model_bounds[IS.Optimization.encode_key(variable_key)],
)
end
end
@@ -403,36 +415,39 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib"; force_build = true)
set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)
model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
######## Test with ThermalMultiStartUnitCommitment ########
template = get_thermal_standard_uc_template()
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib"; force_build = true)
set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)
model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
check_duration_on_initial_conditions_values(model, ThermalMultiStart)
check_duration_off_initial_conditions_values(model, ThermalMultiStart)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- ######## Test with ThermalCompactUnitCommitment ########
+ ######## Test with ThermalStandardUnitCommitment ########
template = get_thermal_standard_uc_template()
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_pglib"; force_build = true)
- set_device_model!(template, ThermalMultiStart, ThermalCompactUnitCommitment)
- set_device_model!(template, ThermalStandard, ThermalCompactUnitCommitment)
+ set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)
+ set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)
model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
check_duration_on_initial_conditions_values(model, ThermalMultiStart)
check_duration_off_initial_conditions_values(model, ThermalMultiStart)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Decision Model initial_conditions test for Hydro" begin
@@ -442,7 +457,8 @@ end
set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)
set_device_model!(template, HydroEnergyReservoir, HydroDispatchRunOfRiver)
model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
initial_conditions_data =
PSI.get_initial_conditions_data(PSI.get_optimization_container(model))
@test !PSI.has_initial_condition_value(
@@ -450,7 +466,7 @@ end
ActivePowerVariable(),
HydroEnergyReservoir,
)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
######## Test with HydroCommitmentRunOfRiver ########
template = get_thermal_dispatch_template_network()
@@ -459,7 +475,8 @@ end
set_device_model!(template, HydroEnergyReservoir, HydroCommitmentRunOfRiver)
model = DecisionModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
initial_conditions_data =
PSI.get_initial_conditions_data(PSI.get_optimization_container(model))
@test PSI.has_initial_condition_value(
@@ -467,7 +484,7 @@ end
OnVariable(),
HydroEnergyReservoir,
)
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Test serialization of InitialConditionsData" begin
@@ -483,16 +500,16 @@ end
model = DecisionModel(template, sys; optimizer = optimizer)
output_dir = mktempdir(; cleanup = true)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
ic_file = PSI.get_initial_conditions_file(model)
test_ic_serialization_outputs(model; ic_file_exists = true, message = "make")
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Build again. Initial conditions should be rebuilt.
PSI.reset!(model)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "make")
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Build again, use existing initial conditions.
model = DecisionModel(
@@ -501,9 +518,9 @@ end
optimizer = optimizer,
deserialize_initial_conditions = true,
)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize")
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Construct and build again with custom initial conditions file.
initialization_file = joinpath(output_dir, ic_file * ".old")
@@ -516,16 +533,16 @@ end
initialization_file = initialization_file,
deserialize_initial_conditions = true,
)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize")
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Construct and build again while skipping build of initial conditions.
rm(ic_file)
model = DecisionModel(template, sys; optimizer = optimizer, initialize_model = false)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = false, message = "skip")
- @test solve!(model) == RunStatus.SUCCESSFUL
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Conflicting inputs
model = DecisionModel(
@@ -536,7 +553,7 @@ end
deserialize_initial_conditions = true,
)
@test build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) ==
- PSI.BuildStatus.FAILED
+ PSI.ModelBuildStatus.FAILED
model = DecisionModel(
template,
sys;
@@ -545,7 +562,7 @@ end
initialization_file = "init_file.bin",
)
build!(model; output_dir = output_dir, console_level = Logging.AboveMaxLevel) ==
- PSI.BuildStatus.FAILED
+ PSI.ModelBuildStatus.FAILED
end
@testset "Solve with detailed optimizer stats" begin
@@ -562,8 +579,8 @@ end
detailed_optimizer_stats = true,
)
output_dir = mktempdir(; cleanup = true)
- @test build!(UC; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(UC) == RunStatus.SUCCESSFUL
+ @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# We only test this field because most free solvers don't support detailed stats
@test !ismissing(get_optimizer_stats(UC).objective_bound)
end
@@ -588,8 +605,8 @@ end
detailed_optimizer_stats = true,
)
output_dir = mktempdir(; cleanup = true)
- @test build!(UC; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(UC) == RunStatus.SUCCESSFUL
+ @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# We only test this field because most free solvers don't support detailed stats
p_variable = PSI.get_variable(
PSI.get_optimization_container(UC),
@@ -632,15 +649,15 @@ end
detailed_optimizer_stats = true,
)
output_dir = mktempdir(; cleanup = true)
- @test build!(UC; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(UC) == RunStatus.SUCCESSFUL
+ @test build!(UC; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(UC) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Test for single row result variables" begin
template = get_thermal_dispatch_template_network()
c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat_ems"; force_build = true)
device_model = DeviceModel(
- BatteryEMS,
+ EnergyReservoirStorage,
StorageDispatchWithReserves;
attributes = Dict{String, Any}(
"reservation" => true,
@@ -657,9 +674,9 @@ end
c_sys5_bat;
optimizer = GLPK_optimizer,
)
- @test build!(model; output_dir = output_dir) == PSI.BuildStatus.BUILT
- @test solve!(model) == RunStatus.SUCCESSFUL
- res = ProblemResults(model)
- shortage = read_variable(res, "StorageEnergyShortageVariable__BatteryEMS")
+ @test build!(model; output_dir = output_dir) == PSI.ModelBuildStatus.BUILT
+ @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ res = OptimizationProblemResults(model)
+ shortage = read_variable(res, "StorageEnergyShortageVariable__EnergyReservoirStorage")
@test nrow(shortage) == 1
end
diff --git a/test/test_model_emulation.jl b/test/test_model_emulation.jl
index 06baefcf7f..f824c7ec4b 100644
--- a/test/test_model_emulation.jl
+++ b/test/test_model_emulation.jl
@@ -9,8 +9,8 @@
model = EmulationModel(template, c_sys5; optimizer = GLPK_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
- @test run!(model) == RunStatus.SUCCESSFUL
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
template = get_thermal_standard_uc_template()
c_sys5_uc_re = PSB.build_system(
@@ -23,8 +23,8 @@
model = EmulationModel(template, c_sys5_uc_re; optimizer = GLPK_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
- @test run!(model) == RunStatus.SUCCESSFUL
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@test !isempty(collect(readdir(PSI.get_recorder_dir(model))))
end
@@ -40,10 +40,10 @@ end
set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
model = EmulationModel(template, c_sys5_uc_re; optimizer = HiGHS_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
######## Test with ThermalMultiStartUnitCommitment ########
template = get_thermal_standard_uc_template()
@@ -56,15 +56,15 @@ end
set_device_model!(template, ThermalMultiStart, ThermalMultiStartUnitCommitment)
model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
@test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
check_duration_on_initial_conditions_values(model, ThermalMultiStart)
check_duration_off_initial_conditions_values(model, ThermalMultiStart)
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- ######## Test with ThermalCompactUnitCommitment ########
+ ######## Test with ThermalStandardUnitCommitment ########
template = get_thermal_standard_uc_template()
c_sys5_uc = PSB.build_system(
PSITestSystems,
@@ -72,17 +72,17 @@ end
add_single_time_series = true,
force_build = true,
)
- set_device_model!(template, ThermalMultiStart, ThermalCompactUnitCommitment)
+ set_device_model!(template, ThermalMultiStart, ThermalStandardUnitCommitment)
model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
@test build!(model; executions = 1, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
check_duration_on_initial_conditions_values(model, ThermalStandard)
check_duration_off_initial_conditions_values(model, ThermalStandard)
check_duration_on_initial_conditions_values(model, ThermalMultiStart)
check_duration_off_initial_conditions_values(model, ThermalMultiStart)
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
- ######## Test with ThermalCompactDispatch ########
+ ######## Test with ThermalStandardDispatch ########
template = get_thermal_standard_uc_template()
c_sys5_uc = PSB.build_system(
PSITestSystems,
@@ -90,11 +90,11 @@ end
add_single_time_series = true,
force_build = true,
)
- device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalCompactDispatch)
+ device_model = DeviceModel(PSY.ThermalStandard, PSI.ThermalStandardDispatch)
set_device_model!(template, device_model)
model = EmulationModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
end
@testset "Emulation Model initial_conditions test for Hydro" begin
@@ -110,7 +110,7 @@ end
set_device_model!(template, HydroEnergyReservoir, HydroDispatchRunOfRiver)
model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
initial_conditions_data =
PSI.get_initial_conditions_data(PSI.get_optimization_container(model))
@test !PSI.has_initial_condition_value(
@@ -118,7 +118,7 @@ end
ActivePowerVariable(),
HydroEnergyReservoir,
)
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
######## Test with HydroCommitmentRunOfRiver ########
template = get_thermal_dispatch_template_network()
@@ -133,7 +133,7 @@ end
model = EmulationModel(template, c_sys5_hyd; optimizer = HiGHS_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
initial_conditions_data =
PSI.get_initial_conditions_data(PSI.get_optimization_container(model))
@test PSI.has_initial_condition_value(
@@ -141,7 +141,7 @@ end
OnVariable(),
HydroEnergyReservoir,
)
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Emulation Model Results" begin
@@ -160,9 +160,9 @@ end
executions = executions,
output_dir = mktempdir(; cleanup = true),
) ==
- BuildStatus.BUILT
- @test run!(model) == RunStatus.SUCCESSFUL
- results = ProblemResults(model)
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results = OptimizationProblemResults(model)
@test list_aux_variable_names(results) == []
@test list_aux_variable_keys(results) == []
@test list_variable_names(results) == ["ActivePowerVariable__ThermalStandard"]
@@ -176,7 +176,10 @@ end
@test read_variable(results, "ActivePowerVariable__ThermalStandard") isa DataFrame
@test read_variable(results, ActivePowerVariable, ThermalStandard) isa DataFrame
- @test read_variable(results, PSI.VariableKey(ActivePowerVariable, ThermalStandard)) isa
+ @test read_variable(
+ results,
+ PSI.VariableKey(ActivePowerVariable, ThermalStandard),
+ ) isa
DataFrame
@test read_parameter(results, "ActivePowerTimeSeriesParameter__PowerLoad") isa DataFrame
@@ -221,7 +224,7 @@ end
executions = 10,
output_dir = mktempdir(; cleanup = true),
serialize = serialize,
- ) == RunStatus.SUCCESSFUL
+ ) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
end
@@ -237,25 +240,28 @@ end
model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)
executions = 10
- @test build!(model; executions = executions, output_dir = path) == BuildStatus.BUILT
- @test run!(model; export_problem_results = true) == RunStatus.SUCCESSFUL
- results1 = ProblemResults(model)
+ @test build!(model; executions = executions, output_dir = path) ==
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model; export_problem_results = true) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results1 = OptimizationProblemResults(model)
var1_a = read_variable(results1, ActivePowerVariable, ThermalStandard)
# Ensure that we can deserialize strings into keys.
var1_b = read_variable(results1, "ActivePowerVariable__ThermalStandard")
@test var1_a == var1_b
# Results were automatically serialized here.
- results2 = ProblemResults(joinpath(PSI.get_output_dir(model)))
+ results2 = OptimizationProblemResults(PSI.get_output_dir(model))
var2 = read_variable(results2, ActivePowerVariable, ThermalStandard)
@test var1_a == var2
- @test get_system(results2) !== nothing
+ @test get_system(results2) === nothing
+ get_system!(results2)
+ @test get_system(results2) isa PSY.System
# Serialize to a new directory with the exported function.
results_path = joinpath(path, "results")
serialize_results(results1, results_path)
- @test isfile(joinpath(results_path, PSI._PROBLEM_RESULTS_FILENAME))
- results3 = ProblemResults(results_path)
+ @test isfile(joinpath(results_path, IS.Optimization._PROBLEM_RESULTS_FILENAME))
+ results3 = OptimizationProblemResults(results_path)
var3 = read_variable(results3, ActivePowerVariable, ThermalStandard)
@test var1_a == var3
@test get_system(results3) === nothing
@@ -281,9 +287,10 @@ end
model = EmulationModel(template, c_sys5; optimizer = HiGHS_optimizer)
executions = 10
- @test build!(model; executions = executions, output_dir = path) == BuildStatus.BUILT
- @test run!(model) == RunStatus.SUCCESSFUL
- results = ProblemResults(model)
+ @test build!(model; executions = executions, output_dir = path) ==
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results = OptimizationProblemResults(model)
var1 = read_variable(results, ActivePowerVariable, ThermalStandard)
file_list = sort!(collect(readdir(path)))
@@ -292,8 +299,8 @@ end
path2 = joinpath(path, "tmp")
model2 = EmulationModel(path, HiGHS_optimizer)
build!(model2; output_dir = path2)
- @test run!(model2) == RunStatus.SUCCESSFUL
- results2 = ProblemResults(model2)
+ @test run!(model2) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results2 = OptimizationProblemResults(model2)
var2 = read_variable(results, ActivePowerVariable, ThermalStandard)
@test var1 == var2
@@ -325,16 +332,18 @@ end
model = EmulationModel(template, sys; optimizer = HiGHS_optimizer)
output_dir = mktempdir(; cleanup = true)
- @test build!(model; executions = 1, output_dir = output_dir) == BuildStatus.BUILT
+ @test build!(model; executions = 1, output_dir = output_dir) ==
+ PSI.ModelBuildStatus.BUILT
ic_file = PSI.get_initial_conditions_file(model)
test_ic_serialization_outputs(model; ic_file_exists = true, message = "make")
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Build again, use existing initial conditions.
PSI.reset!(model)
- @test build!(model; executions = 1, output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; executions = 1, output_dir = output_dir) ==
+ PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "make")
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Build again, use existing initial conditions.
model = EmulationModel(
@@ -343,9 +352,10 @@ end
optimizer = optimizer,
deserialize_initial_conditions = true,
)
- @test build!(model; executions = 1, output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; executions = 1, output_dir = output_dir) ==
+ PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize")
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# Construct and build again with custom initial conditions file.
initialization_file = joinpath(output_dir, ic_file * ".old")
@@ -358,13 +368,15 @@ end
initialization_file = initialization_file,
deserialize_initial_conditions = true,
)
- @test build!(model; executions = 1, output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; executions = 1, output_dir = output_dir) ==
+ PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = true, message = "deserialize")
# Construct and build again while skipping build of initial conditions.
model = EmulationModel(template, sys; optimizer = optimizer, initialize_model = false)
rm(ic_file)
- @test build!(model; executions = 1, output_dir = output_dir) == PSI.BuildStatus.BUILT
+ @test build!(model; executions = 1, output_dir = output_dir) ==
+ PSI.ModelBuildStatus.BUILT
test_ic_serialization_outputs(model; ic_file_exists = false, message = "skip")
- @test run!(model) == RunStatus.SUCCESSFUL
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
diff --git a/test/test_network_constructors.jl b/test/test_network_constructors.jl
index b82858a178..bd767adc65 100644
--- a/test/test_network_constructors.jl
+++ b/test/test_network_constructors.jl
@@ -1,25 +1,5 @@
# Note to devs. Use GLPK or Cbc for models with linear constraints and linear cost functions
# Use OSQP for models with quadratic cost function and linear constraints and ipopt otherwise
-const NETWORKS_FOR_TESTING = [
- (PM.ACPPowerModel, fast_ipopt_optimizer),
- (PM.ACRPowerModel, fast_ipopt_optimizer),
- (PM.ACTPowerModel, fast_ipopt_optimizer),
- #(PM.IVRPowerModel, fast_ipopt_optimizer), #instantiate_ivp_expr_model not implemented
- (PM.DCPPowerModel, fast_ipopt_optimizer),
- (PM.DCMPPowerModel, fast_ipopt_optimizer),
- (PM.NFAPowerModel, fast_ipopt_optimizer),
- (PM.DCPLLPowerModel, fast_ipopt_optimizer),
- (PM.LPACCPowerModel, fast_ipopt_optimizer),
- (PM.SOCWRPowerModel, fast_ipopt_optimizer),
- (PM.SOCWRConicPowerModel, scs_solver),
- (PM.QCRMPowerModel, fast_ipopt_optimizer),
- (PM.QCLSPowerModel, fast_ipopt_optimizer),
- #(PM.SOCBFPowerModel, fast_ipopt_optimizer), # not implemented
- (PM.BFAPowerModel, fast_ipopt_optimizer),
- #(PM.SOCBFConicPowerModel, fast_ipopt_optimizer), # not implemented
- (PM.SDPWRMPowerModel, scs_solver),
- (PM.SparseSDPWRMPowerModel, scs_solver),
-]
@testset "All PowerModels models construction" begin
c_sys5 = PSB.build_system(PSITestSystems, "c_sys5")
@@ -29,10 +9,10 @@ const NETWORKS_FOR_TESTING = [
)
ps_model = DecisionModel(template, c_sys5; optimizer = solver)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
- @test ps_model.internal.container.pm !== nothing
+ PSI.ModelBuildStatus.BUILT
+ @test PSI.get_optimization_container(ps_model).pm !== nothing
# TODO: Change test
- # @test :nodal_balance_active in keys(ps_model.internal.container.expressions)
+ # @test :nodal_balance_active in keys(PSI.get_optimization_container(ps_model).expressions)
end
end
@@ -59,7 +39,7 @@ end
ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -82,7 +62,7 @@ end
optimizer = GLPK_optimizer,
)
@test build!(ps_model_re; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_checksolve_test(ps_model_re, [MOI.OPTIMAL], 240000.0, 10000)
end
@@ -121,7 +101,7 @@ end
ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -146,7 +126,7 @@ end
ps_model;
console_level = Logging.AboveMaxLevel, # Ignore expected errors.
output_dir = mktempdir(; cleanup = true),
- ) == PSI.BuildStatus.FAILED
+ ) == PSI.ModelBuildStatus.FAILED
end
@testset "Network DC-PF with VirtualPTDF Model" begin
@@ -184,7 +164,7 @@ end
ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -230,7 +210,7 @@ end
template = get_thermal_dispatch_template_network(DCPPowerModel)
ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -278,7 +258,7 @@ end
template = get_thermal_dispatch_template_network(ACPPowerModel)
ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -320,7 +300,7 @@ end
template = get_thermal_dispatch_template_network(NFAPowerModel)
ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -370,7 +350,7 @@ end
template = get_thermal_dispatch_template_network(network)
ps_model = DecisionModel(template, sys; optimizer = fast_ipopt_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -381,7 +361,7 @@ end
test_results[network][sys][5],
false,
)
- @test ps_model.internal.container.pm !== nothing
+ @test PSI.get_optimization_container(ps_model).pm !== nothing
end
end
@@ -414,7 +394,7 @@ end
template = get_thermal_dispatch_template_network(network)
ps_model = DecisionModel(template, sys; optimizer = ipopt_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
psi_constraint_test(ps_model, constraint_keys)
moi_tests(
ps_model,
@@ -425,7 +405,7 @@ end
test_results[network][sys][5],
false,
)
- @test ps_model.internal.container.pm !== nothing
+ @test PSI.get_optimization_container(ps_model).pm !== nothing
psi_checksolve_test(
ps_model,
[MOI.OPTIMAL, MOI.LOCALLY_SOLVED],
@@ -447,18 +427,18 @@ end
ps_model;
console_level = Logging.AboveMaxLevel, # Ignore expected errors.
output_dir = mktempdir(; cleanup = true),
- ) == PSI.BuildStatus.FAILED
+ ) == PSI.ModelBuildStatus.FAILED
end
end
-@testset "2 Subnetworks DC-PF with CopperPlatePowerModel" begin
+@testset "2 Subnetworks HVDC DC-PF with CopperPlatePowerModel" begin
c_sys5 = PSB.build_system(PSISystems, "2Area 5 Bus System")
# Test passing a VirtualPTDF Model
template = get_thermal_dispatch_template_network(NetworkModel(CopperPlatePowerModel))
ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(ps_model)
moi_tests(ps_model, 264, 0, 288, 240, 48, false)
@@ -470,7 +450,7 @@ end
psi_checksolve_test(ps_model, [MOI.OPTIMAL], 480288, 100)
- results = ProblemResults(ps_model)
+ results = OptimizationProblemResults(ps_model)
hvdc_flow = read_variable(results, "FlowActivePowerVariable__TwoTerminalHVDCLine")
@test all(hvdc_flow[!, "nodeC-nodeC2"] .<= 200)
@test all(hvdc_flow[!, "nodeC-nodeC2"] .>= -200)
@@ -516,14 +496,14 @@ end
template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel))
ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(ps_model)
opt_container = PSI.get_optimization_container(ps_model)
copper_plate_constraints =
PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)
- results = ProblemResults(ps_model)
+ results = OptimizationProblemResults(ps_model)
hvdc_flow = read_variable(results, "FlowActivePowerVariable__TwoTerminalHVDCLine")
@test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0)
@test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0)
@@ -558,7 +538,7 @@ end
ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(ps_model)
moi_tests(ps_model, 552, 0, 576, 528, 336, false)
@@ -570,10 +550,10 @@ end
psi_checksolve_test(ps_model, [MOI.OPTIMAL], 684763, 100)
- results = ProblemResults(ps_model)
+ results = OptimizationProblemResults(ps_model)
hvdc_flow = read_variable(results, "FlowActivePowerVariable__TwoTerminalHVDCLine")
- @test all(hvdc_flow[!, "nodeC-nodeC2"] .<= 200)
- @test all(hvdc_flow[!, "nodeC-nodeC2"] .>= -200)
+ @test all(hvdc_flow[!, "nodeC-nodeC2"] .<= 200 + PSI.ABSOLUTE_TOLERANCE)
+ @test all(hvdc_flow[!, "nodeC-nodeC2"] .>= -200 - PSI.ABSOLUTE_TOLERANCE)
load = read_parameter(results, "ActivePowerTimeSeriesParameter__PowerLoad")
thermal_gen = read_variable(results, "ActivePowerVariable__ThermalStandard")
@@ -616,14 +596,14 @@ end
template = get_thermal_dispatch_template_network(NetworkModel(PTDFPowerModel))
ps_model = DecisionModel(template, c_sys5; optimizer = HiGHS_optimizer)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(ps_model)
opt_container = PSI.get_optimization_container(ps_model)
copper_plate_constraints =
PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.System)
- results = ProblemResults(ps_model)
+ results = OptimizationProblemResults(ps_model)
hvdc_flow = read_variable(results, "FlowActivePowerVariable__TwoTerminalHVDCLine")
@test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0)
@test all(hvdc_flow[!, "nodeC-nodeC2"] .== 0.0)
@@ -673,10 +653,10 @@ end
)
@test build!(uc_model_red; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(uc_model_red)
- res_red = ProblemResults(uc_model_red)
+ res_red = OptimizationProblemResults(uc_model_red)
flow_lines = read_variable(res_red, "FlowActivePowerVariable__Line")
line_names = DataFrames.names(flow_lines)[2:end]
@@ -699,10 +679,10 @@ end
)
@test build!(uc_model_orig; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
solve!(uc_model_orig)
- res_orig = ProblemResults(uc_model_orig)
+ res_orig = OptimizationProblemResults(uc_model_orig)
flow_lines_orig = read_variable(res_orig, "FlowActivePowerVariable__Line")
@@ -726,7 +706,269 @@ end
)
ps_model = DecisionModel(template, new_sys; optimizer = solver)
@test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
- @test ps_model.internal.container.pm !== nothing
+ PSI.ModelBuildStatus.BUILT
+ @test PSI.get_optimization_container(ps_model).pm !== nothing
end
end
+
+@testset "2 Areas AreaBalance PowerModel" begin
+ c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA")
+ transform_single_time_series!(c_sys, Hour(24), Hour(1))
+ template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel))
+ set_device_model!(template, AreaInterchange, StaticBranch)
+ ps_model =
+ DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)
+
+ @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+
+ moi_tests(ps_model, 264, 0, 264, 264, 48, false)
+
+ opt_container = PSI.get_optimization_container(ps_model)
+ copper_plate_constraints =
+ PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)
+ @test size(copper_plate_constraints) == (2, 24)
+
+ psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)
+
+ results = OptimizationProblemResults(ps_model)
+ interarea_flow = read_variable(results, "FlowActivePowerVariable__AreaInterchange")
+ # The values for these tests come from the data
+ @test all(interarea_flow[!, "1_2"] .<= 150)
+ @test all(interarea_flow[!, "1_2"] .>= -150)
+
+ load = read_parameter(results, "ActivePowerTimeSeriesParameter__PowerLoad")
+ thermal_gen = read_variable(results, "ActivePowerVariable__ThermalStandard")
+
+ zone_1_load = sum(eachcol(load[!, ["Bus4_1", "Bus3_1", "Bus2_1"]]))
+ zone_1_gen = sum(
+ eachcol(
+ thermal_gen[
+ !,
+ ["Solitude_1", "Park City_1", "Sundance_1", "Brighton_1", "Alta_1"],
+ ],
+ ),
+ )
+ @test all(
+ isapprox.(
+ sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, "1_2"]; dims = 2),
+ 0.0;
+ atol = 1e-3,
+ ),
+ )
+
+ zone_2_load = sum(eachcol(load[!, ["Bus4_2", "Bus3_2", "Bus2_2"]]))
+ zone_2_gen = sum(
+ eachcol(
+ thermal_gen[
+ !,
+ ["Solitude_2", "Park City_2", "Sundance_2", "Brighton_2", "Alta_2"],
+ ],
+ ),
+ )
+ @test all(
+ isapprox.(
+ sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, "1_2"]; dims = 2),
+ 0.0;
+ atol = 1e-3,
+ ),
+ )
+end
+
+@testset "2 Areas AreaBalance PowerModel with TimeSeries" begin
+ c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA")
+ load = first(get_components(PowerLoad, c_sys))
+ ts_array = get_time_series_array(SingleTimeSeries, load, "max_active_power")
+ tstamp = timestamp(ts_array)
+ area_int = first(get_components(AreaInterchange, c_sys))
+ day_data = [
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ ]
+ weekly_data = repeat(day_data, 7)
+ ts_from_to = SingleTimeSeries(
+ "from_to_flow_limit",
+ TimeArray(tstamp, weekly_data);
+ scaling_factor_multiplier = get_from_to_flow_limit,
+ )
+ ts_to_from = SingleTimeSeries(
+ "to_from_flow_limit",
+ TimeArray(tstamp, weekly_data);
+ scaling_factor_multiplier = get_from_to_flow_limit,
+ )
+ add_time_series!(c_sys, area_int, ts_from_to)
+ add_time_series!(c_sys, area_int, ts_to_from)
+ ## Transform Time Series ##
+ transform_single_time_series!(c_sys, Hour(24), Hour(24))
+ template = get_thermal_dispatch_template_network(NetworkModel(AreaBalancePowerModel))
+ set_device_model!(template, AreaInterchange, StaticBranch)
+ ps_model =
+ DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)
+
+ @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+
+ moi_tests(ps_model, 264, 0, 264, 264, 48, false)
+
+ opt_container = PSI.get_optimization_container(ps_model)
+ copper_plate_constraints =
+ PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)
+ @test size(copper_plate_constraints) == (2, 24)
+
+ psi_checksolve_test(ps_model, [MOI.OPTIMAL], 482055, 1)
+
+ results = OptimizationProblemResults(ps_model)
+ interarea_flow = read_variable(results, "FlowActivePowerVariable__AreaInterchange")
+ # The values for these tests come from the data
+ @test interarea_flow[4, "1_2"] != 0.0
+ @test interarea_flow[5, "1_2"] == 0.0
+ @test interarea_flow[6, "1_2"] == 0.0
+end
+
+@testset "2 Areas AreaPTDFPowerModel" begin
+ c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA")
+ transform_single_time_series!(c_sys, Hour(24), Hour(1))
+ set_flow_limits!(
+ get_component(AreaInterchange, c_sys, "1_2"),
+ (from_to = 1.0, to_from = 1.0),
+ )
+ template = get_thermal_dispatch_template_network(NetworkModel(AreaPTDFPowerModel))
+ set_device_model!(template, AreaInterchange, StaticBranch)
+ set_device_model!(template, MonitoredLine, StaticBranchUnbounded)
+ ps_model =
+ DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)
+
+ @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+
+ moi_tests(ps_model, 576, 0, 576, 576, 360, false)
+
+ opt_container = PSI.get_optimization_container(ps_model)
+ copper_plate_constraints =
+ PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)
+ @test size(copper_plate_constraints) == (2, 24)
+
+ psi_checksolve_test(ps_model, [MOI.OPTIMAL], 666147, 1)
+
+ results = OptimizationProblemResults(ps_model)
+ interarea_flow = read_variable(results, "FlowActivePowerVariable__AreaInterchange")
+ # The values for these tests come from the data
+ @test all(interarea_flow[!, "1_2"] .<= 100.0 + PSI.ABSOLUTE_TOLERANCE)
+ @test all(interarea_flow[!, "1_2"] .>= -100.0 - PSI.ABSOLUTE_TOLERANCE)
+
+ load = read_parameter(results, "ActivePowerTimeSeriesParameter__PowerLoad")
+ thermal_gen = read_variable(results, "ActivePowerVariable__ThermalStandard")
+
+ zone_1_load = sum(eachcol(load[!, ["Bus4_1", "Bus3_1", "Bus2_1"]]))
+ zone_1_gen = sum(
+ eachcol(
+ thermal_gen[
+ !,
+ ["Solitude_1", "Park City_1", "Sundance_1", "Brighton_1", "Alta_1"],
+ ],
+ ),
+ )
+ @test all(
+ isapprox.(
+ sum(zone_1_gen .+ zone_1_load .- interarea_flow[!, "1_2"]; dims = 2),
+ 0.0;
+ atol = 1e-3,
+ ),
+ )
+
+ zone_2_load = sum(eachcol(load[!, ["Bus4_2", "Bus3_2", "Bus2_2"]]))
+ zone_2_gen = sum(
+ eachcol(
+ thermal_gen[
+ !,
+ ["Solitude_2", "Park City_2", "Sundance_2", "Brighton_2", "Alta_2"],
+ ],
+ ),
+ )
+ @test all(
+ isapprox.(
+ sum(zone_2_gen .+ zone_2_load .+ interarea_flow[!, "1_2"]; dims = 2),
+ 0.0;
+ atol = 1e-3,
+ ),
+ )
+end
+
+@testset "2 Areas AreaPTDFPowerModel with Time Series" begin
+ c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA")
+ load = first(get_components(PowerLoad, c_sys))
+ new_line = Line(;
+ name = "C2_D1",
+ available = true,
+ active_power_flow = 0.0,
+ reactive_power_flow = 0.0,
+ arc = Arc(;
+ from = get_component(ACBus, c_sys, "Bus_nodeC_2"),
+ to = get_component(ACBus, c_sys, "Bus_nodeD_1"),
+ ),
+ r = 0.00297,
+ x = 0.0297,
+ b = (from = 0.00337, to = 0.00337),
+ rating = 40.53,
+ angle_limits = (min = -0.7, max = 0.7),
+ )
+ add_component!(c_sys, new_line)
+ ts_array = get_time_series_array(SingleTimeSeries, load, "max_active_power")
+ tstamp = timestamp(ts_array)
+ area_int = first(get_components(AreaInterchange, c_sys))
+ day_data = [
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ 0.9, 0.85, 0.95, 0.2, 0.0, 0.0,
+ ]
+ weekly_data = repeat(day_data, 7)
+ ts_from_to = SingleTimeSeries(
+ "from_to_flow_limit",
+ TimeArray(tstamp, weekly_data);
+ scaling_factor_multiplier = get_from_to_flow_limit,
+ )
+ ts_to_from = SingleTimeSeries(
+ "to_from_flow_limit",
+ TimeArray(tstamp, weekly_data);
+ scaling_factor_multiplier = get_from_to_flow_limit,
+ )
+ add_time_series!(c_sys, area_int, ts_from_to)
+ add_time_series!(c_sys, area_int, ts_to_from)
+ ## Transform Time Series ##
+ transform_single_time_series!(c_sys, Hour(24), Hour(24))
+ set_flow_limits!(
+ get_component(AreaInterchange, c_sys, "1_2"),
+ (from_to = 1.0, to_from = 1.0),
+ )
+ template = get_thermal_dispatch_template_network(NetworkModel(AreaPTDFPowerModel))
+ set_device_model!(template, AreaInterchange, StaticBranch)
+ set_device_model!(template, MonitoredLine, StaticBranchUnbounded)
+ ps_model =
+ DecisionModel(template, c_sys; resolution = Hour(1), optimizer = HiGHS_optimizer)
+
+ @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(ps_model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
+
+ moi_tests(ps_model, 600, 0, 600, 600, 384, false)
+
+ opt_container = PSI.get_optimization_container(ps_model)
+ copper_plate_constraints =
+ PSI.get_constraint(opt_container, CopperPlateBalanceConstraint(), PSY.Area)
+ @test size(copper_plate_constraints) == (2, 24)
+
+ psi_checksolve_test(ps_model, [MOI.OPTIMAL], 662467, 1)
+
+ results = OptimizationProblemResults(ps_model)
+ interarea_flow = read_variable(results, "FlowActivePowerVariable__AreaInterchange")
+ # The values for these tests come from the data
+ @test interarea_flow[1, "1_2"] != 0.0
+ @test interarea_flow[5, "1_2"] == 0.0
+ @test interarea_flow[6, "1_2"] == 0.0
+end
diff --git a/test/test_print.jl b/test/test_print.jl
index ff1e6108ea..2a19c5956c 100644
--- a/test/test_print.jl
+++ b/test/test_print.jl
@@ -24,9 +24,10 @@ end
dm_model = DecisionModel(template, c_sys5; optimizer = GLPK_optimizer)
@test build!(dm_model; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
- @test solve!(dm_model; optimizer = GLPK_optimizer) == RunStatus.SUCCESSFUL
- results = ProblemResults(dm_model)
+ PSI.ModelBuildStatus.BUILT
+ @test solve!(dm_model; optimizer = GLPK_optimizer) ==
+ PSI.RunStatus.SUCCESSFULLY_FINALIZED
+ results = OptimizationProblemResults(dm_model)
variables = read_variables(results)
list = [
diff --git a/test/test_recorder_events.jl b/test/test_recorder_events.jl
index 7c308beac5..3de10be804 100644
--- a/test/test_recorder_events.jl
+++ b/test/test_recorder_events.jl
@@ -10,8 +10,8 @@
model = EmulationModel(template, c_sys5_uc_re; optimizer = GLPK_optimizer)
@test build!(model; executions = 10, output_dir = mktempdir(; cleanup = true)) ==
- BuildStatus.BUILT
- @test run!(model) == RunStatus.SUCCESSFUL
+ PSI.ModelBuildStatus.BUILT
+ @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
recorder_log = joinpath(PSI.get_recorder_dir(model), "execution.log")
events = list_recorder_events(PSI.ParameterUpdateEvent, recorder_log)
diff --git a/test/test_services_constructor.jl b/test/test_services_constructor.jl
index 28bc99ac2e..ce3d8b712c 100644
--- a/test/test_services_constructor.jl
+++ b/test/test_services_constructor.jl
@@ -19,8 +19,9 @@
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 648, 0, 120, 216, 72, false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 624, 0, 216, 216, 48, false)
reserve_variables = [
:ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve1
:ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__ORDC1
@@ -28,8 +29,8 @@
:ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11
]
found_vars = 0
- for (k, var_array) in model.internal.container.variables
- if PSI.encode_key(k) in reserve_variables
+ for (k, var_array) in PSI.get_optimization_container(model).variables
+ if IS.Optimization.encode_key(k) in reserve_variables
for var in var_array
@test JuMP.has_lower_bound(var)
@test JuMP.lower_bound(var) == 0.0
@@ -57,15 +58,16 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
moi_tests(model, 384, 0, 336, 192, 24, false)
reserve_variables = [
:ActivePowerReserveVariable__VariableReserve_ReserveDown_Reserve2,
:ActivePowerReserveVariable__VariableReserve_ReserveUp_Reserve1,
:ActivePowerReserveVariable__VariableReserve_ReserveUp_Reserve11,
]
- for (k, var_array) in model.internal.container.variables
- if PSI.encode_key(k) in reserve_variables
+ for (k, var_array) in PSI.get_optimization_container(model).variables
+ if IS.Optimization.encode_key(k) in reserve_variables
for var in var_array
@test JuMP.has_lower_bound(var)
@test JuMP.lower_bound(var) == 0.0
@@ -95,8 +97,9 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc; optimizer = cbc_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 1008, 0, 480, 216, 192, true)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 984, 0, 576, 216, 168, true)
end
@testset "Test Reserves from Thermal Standard UC with NonSpinningReserve" begin
@@ -112,7 +115,8 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc_non_spin"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
moi_tests(model, 1032, 0, 888, 192, 288, true)
end
@@ -131,8 +135,9 @@ end
c_sys5_re = PSB.build_system(PSITestSystems, "c_sys5_re"; add_reserves = true)
model = DecisionModel(template, c_sys5_re)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 360, 0, 72, 120, 72, false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 336, 0, 168, 120, 48, false)
end
@testset "Test Reserves from Hydro" begin
@@ -154,8 +159,9 @@ end
c_sys5_hyd = PSB.build_system(PSITestSystems, "c_sys5_hyd"; add_reserves = true)
model = DecisionModel(template, c_sys5_hyd)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 240, 0, 48, 96, 72, false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 216, 0, 144, 96, 48, false)
end
@testset "Test Reserves from with slack variables" begin
@@ -192,10 +198,12 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc;)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
moi_tests(model, 504, 0, 120, 192, 24, false)
end
+#=
@testset "Test AGC" begin
c_sys5_reg = PSB.build_system(PSITestSystems, "c_sys5_reg")
@test_throws ArgumentError template_agc_reserve_deployment(; dummy_arg = 0.0)
@@ -204,10 +212,11 @@ end
set_service_model!(template_agc, ServiceModel(PSY.AGC, PIDSmoothACE, "AGC_Area1"))
agc_problem = DecisionModel(AGCReserveDeployment, template_agc, c_sys5_reg)
@test build!(agc_problem; output_dir = mktempdir(; cleanup = true)) ==
- PSI.BuildStatus.BUILT
+ PSI.ModelBuildStatus.BUILT
# These values might change as the AGC model is refined
moi_tests(agc_problem, 696, 0, 480, 0, 384, false)
end
+=#
@testset "Test GroupReserve from Thermal Dispatch" begin
template = get_thermal_dispatch_template_network()
@@ -229,7 +238,7 @@ end
)
set_service_model!(
template,
- ServiceModel(StaticReserveGroup{ReserveDown}, GroupReserve, "init"),
+ ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve, "init"),
)
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
@@ -240,7 +249,7 @@ end
push!(contributing_services, service)
end
end
- groupservice = StaticReserveGroup{ReserveDown}(;
+ groupservice = ConstantReserveGroup{ReserveDown}(;
name = "init",
available = true,
requirement = 0.0,
@@ -249,8 +258,9 @@ end
add_service!(c_sys5_uc, groupservice, contributing_services)
model = DecisionModel(template, c_sys5_uc)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 648, 0, 120, 240, 72, false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 624, 0, 216, 240, 48, false)
end
@testset "Test GroupReserve Errors" begin
@@ -263,7 +273,7 @@ end
)
set_service_model!(
template,
- ServiceModel(StaticReserveGroup{ReserveDown}, GroupReserve),
+ ServiceModel(ConstantReserveGroup{ReserveDown}, GroupReserve),
)
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
@@ -274,7 +284,7 @@ end
push!(contributing_services, service)
end
end
- groupservice = StaticReserveGroup{ReserveDown}(;
+ groupservice = ConstantReserveGroup{ReserveDown}(;
name = "init",
available = true,
requirement = 0.0,
@@ -290,21 +300,22 @@ end
model;
output_dir = mktempdir(; cleanup = true),
console_level = Logging.AboveMaxLevel,
- ) == BuildStatus.FAILED
+ ) == PSI.ModelBuildStatus.FAILED
end
-@testset "Test StaticReserve" begin
+@testset "Test ConstantReserve" begin
template = get_thermal_dispatch_template_network()
set_service_model!(
template,
- ServiceModel(StaticReserve{ReserveUp}, RangeReserve, "Reserve3"),
+ ServiceModel(ConstantReserve{ReserveUp}, RangeReserve, "Reserve3"),
)
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc")
- static_reserve = StaticReserve{ReserveUp}("Reserve3", true, 30, 100)
+ static_reserve = ConstantReserve{ReserveUp}("Reserve3", true, 30, 100)
add_service!(c_sys5_uc, static_reserve, get_components(ThermalGen, c_sys5_uc))
model = DecisionModel(template, c_sys5_uc)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
@test typeof(model) <: DecisionModel{<:PSI.DecisionProblem}
end
@@ -324,8 +335,9 @@ end
c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
model = DecisionModel(template, c_sys5_uc; optimizer = HiGHS_optimizer)
# set manually to test cases for simulation
- model.internal.container.built_for_recurrent_solves = true
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ PSI.get_optimization_container(model).built_for_recurrent_solves = true
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
moi_tests(model, 456, 0, 120, 264, 24, false)
end
@@ -354,8 +366,9 @@ end
)
model = DecisionModel(template, c_sys5_uc)
- @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
- moi_tests(model, 648, 0, 384, 216, 72, false)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 624, 0, 480, 216, 48, false)
reserve_variables = [
:ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve1
:ActivePowerReserveVariable__ReserveDemandCurve__ReserveUp__ORDC1
@@ -363,8 +376,8 @@ end
:ActivePowerReserveVariable__VariableReserve__ReserveUp__Reserve11
]
found_vars = 0
- for (k, var_array) in model.internal.container.variables
- if PSI.encode_key(k) in reserve_variables
+ for (k, var_array) in PSI.get_optimization_container(model).variables
+ if IS.Optimization.encode_key(k) in reserve_variables
for var in var_array
@test JuMP.has_lower_bound(var)
@test JuMP.lower_bound(var) == 0.0
@@ -381,11 +394,131 @@ end
found_constraints = 0
- for (k, _) in model.internal.container.constraints
- if PSI.encode_key(k) in participation_constraints
+ for (k, _) in PSI.get_optimization_container(model).constraints
+ if IS.Optimization.encode_key(k) in participation_constraints
found_constraints += 1
end
end
@test found_constraints == 2
end
+
+@testset "Test Transmission Interface" begin
+ c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
+ interface = TransmissionInterface(;
+ name = "west_east",
+ available = true,
+ active_power_flow_limits = (min = 0.0, max = 400.0),
+ )
+ interface_lines = [
+ get_component(Line, c_sys5_uc, "1"),
+ get_component(Line, c_sys5_uc, "2"),
+ get_component(Line, c_sys5_uc, "6"),
+ ]
+ add_service!(c_sys5_uc, interface, interface_lines)
+
+ template = get_thermal_dispatch_template_network(DCPPowerModel)
+ set_service_model!(
+ template,
+ ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),
+ )
+
+ model = DecisionModel(template, c_sys5_uc)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 432, 144, 288, 288, 288, false)
+
+ template = get_thermal_dispatch_template_network(PTDFPowerModel)
+ set_service_model!(
+ template,
+ ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),
+ )
+ model = DecisionModel(template, c_sys5_uc)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 312, 0, 288, 288, 168, false)
+
+ #= TODO: Fix this test
+ template = get_thermal_dispatch_template_network(ACPPowerModel; use_slacks = true) where
+ set_service_model!(
+ template,
+ ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow; use_slacks = true),
+ )
+ model = DecisionModel(template, c_sys5_uc)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) == PSI.BuildStatus.BUILT
+ moi_tests(model, 312, 0, 288, 288, 168, false)
+ =#
+end
+
+@testset "Test Transmission Interface with TimeSeries" begin
+ c_sys5_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"; add_reserves = true)
+ interface = TransmissionInterface(;
+ name = "west_east",
+ available = true,
+ active_power_flow_limits = (min = 0.0, max = 400.0),
+ )
+ interface_lines = [
+ get_component(Line, c_sys5_uc, "1"),
+ get_component(Line, c_sys5_uc, "2"),
+ get_component(Line, c_sys5_uc, "6"),
+ ]
+ add_service!(c_sys5_uc, interface, interface_lines)
+ # Add TimeSeries Data
+ data_minflow = Dict(
+ DateTime("2024-01-01T00:00:00") => zeros(24),
+ DateTime("2024-01-02T00:00:00") => zeros(24),
+ )
+
+ forecast_minflow = Deterministic(
+ "min_active_power_flow_limit",
+ data_minflow,
+ Hour(1);
+ scaling_factor_multiplier = get_min_active_power_flow_limit,
+ )
+
+ data_maxflow = Dict(
+ DateTime("2024-01-01T00:00:00") => [
+ 0.9, 0.85, 0.95, 0.2, 0.15, 0.2,
+ 0.9, 0.85, 0.95, 0.2, 0.15, 0.2,
+ 0.9, 0.85, 0.95, 0.2, 0.5, 0.5,
+ 0.9, 0.85, 0.95, 0.2, 0.6, 0.6,
+ ],
+ DateTime("2024-01-02T00:00:00") => [
+ 0.9, 0.85, 0.95, 0.2, 0.15, 0.2,
+ 0.9, 0.85, 0.95, 0.2, 0.15, 0.2,
+ 0.9, 0.85, 0.95, 0.2, 0.5, 0.5,
+ 0.9, 0.85, 0.95, 0.2, 0.6, 0.6,
+ ],
+ )
+
+ forecast_maxflow = Deterministic(
+ "max_active_power_flow_limit",
+ data_maxflow,
+ Hour(1);
+ scaling_factor_multiplier = get_max_active_power_flow_limit,
+ )
+
+ add_time_series!(c_sys5_uc, interface, forecast_minflow)
+ add_time_series!(c_sys5_uc, interface, forecast_maxflow)
+
+ template = get_thermal_dispatch_template_network(DCPPowerModel)
+ set_service_model!(
+ template,
+ ServiceModel(TransmissionInterface, VariableMaxInterfaceFlow; use_slacks = true),
+ )
+
+ model = DecisionModel(template, c_sys5_uc)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 432, 144, 288, 288, 288, false)
+
+ template = get_thermal_dispatch_template_network(PTDFPowerModel)
+ set_service_model!(
+ template,
+ ServiceModel(TransmissionInterface, VariableMaxInterfaceFlow; use_slacks = true),
+ )
+ model = DecisionModel(template, c_sys5_uc)
+ @test build!(model; output_dir = mktempdir(; cleanup = true)) ==
+ PSI.ModelBuildStatus.BUILT
+ moi_tests(model, 312, 0, 288, 288, 168, false)
+end
diff --git a/test/test_simulation_build.jl b/test/test_simulation_build.jl
index 1d78fc1a92..0b3f2e4d7c 100644
--- a/test/test_simulation_build.jl
+++ b/test/test_simulation_build.jl
@@ -22,7 +22,7 @@
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
for field in fieldnames(SimulationSequence)
if fieldtype(SimulationSequence, field) == Union{Dates.DateTime, Nothing}
@@ -73,7 +73,7 @@ end
initial_time = second_day,
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
for model in PSI.get_decision_models(PSI.get_models(sim))
@test PSI.get_initial_time(model) == second_day
@@ -181,32 +181,32 @@ end
simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
ac_power_model = PSI.get_simulation_model(PSI.get_models(sim), :ED)
c = PSI.get_constraint(
PSI.get_optimization_container(ac_power_model),
- FeedforwardSemiContinousConstraint(),
+ FeedforwardSemiContinuousConstraint(),
ThermalStandard,
"ActivePowerVariable_ub",
)
@test !isempty(c)
c = PSI.get_constraint(
PSI.get_optimization_container(ac_power_model),
- FeedforwardSemiContinousConstraint(),
+ FeedforwardSemiContinuousConstraint(),
ThermalStandard,
"ActivePowerVariable_lb",
)
@test !isempty(c)
c = PSI.get_constraint(
PSI.get_optimization_container(ac_power_model),
- FeedforwardSemiContinousConstraint(),
+ FeedforwardSemiContinuousConstraint(),
ThermalStandard,
"ReactivePowerVariable_ub",
)
@test !isempty(c)
c = PSI.get_constraint(
PSI.get_optimization_container(ac_power_model),
- FeedforwardSemiContinousConstraint(),
+ FeedforwardSemiContinuousConstraint(),
ThermalStandard,
"ReactivePowerVariable_lb",
)
diff --git a/test/test_simulation_execute.jl b/test/test_simulation_execute.jl
index f50bab178f..6bc4581769 100644
--- a/test/test_simulation_execute.jl
+++ b/test/test_simulation_execute.jl
@@ -23,9 +23,9 @@ function test_single_stage_sequential(in_memory, rebuild)
simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim_single)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
execute_out = execute!(sim_single; in_memory = in_memory)
- @test execute_out == PSI.RunStatus.SUCCESSFUL
+ @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Single stage sequential tests" begin
@@ -59,8 +59,7 @@ function test_2_stage_decision_models_with_feedforwards(in_memory)
template_uc,
c_sys5_hy_uc;
name = "UC",
- optimizer = GLPK_optimizer,
- ),
+ optimizer = HiGHS_optimizer),
DecisionModel(
template_ed,
c_sys5_hy_ed;
@@ -91,10 +90,10 @@ function test_2_stage_decision_models_with_feedforwards(in_memory)
simulation_folder = mktempdir(; cleanup = true),
)
- build_out = build!(sim; console_level = Logging.Error)
- @test build_out == PSI.BuildStatus.BUILT
+ build_out = build!(sim; console_level = Logging.Info)
+ @test build_out == PSI.SimulationBuildStatus.BUILT
execute_out = execute!(sim; in_memory = in_memory)
- @test execute_out == PSI.RunStatus.SUCCESSFUL
+ @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "2-Stage Decision Models with FeedForwards" begin
@@ -129,7 +128,7 @@ end
template_uc,
c_sys5_hy_uc;
name = "UC",
- optimizer = GLPK_optimizer,
+ optimizer = HiGHS_optimizer,
),
DecisionModel(
template_ed,
@@ -162,9 +161,9 @@ end
)
build_out = build!(sim; console_level = Logging.Error)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
execute_out = execute!(sim)
- @test execute_out == PSI.RunStatus.SUCCESSFUL
+ @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
@testset "Verify simulation events" begin
file = joinpath(PSI.get_simulation_dir(sim), "recorder", "simulation_status.log")
@@ -220,7 +219,7 @@ end
# files_path = PSI.serialize_simulation(sim; path = path)
# deserialized_sim = Simulation(files_path, stage_info)
# build_out = build!(deserialized_sim)
- # @test build_out == PSI.BuildStatus.BUILT
+ # @test build_out == PSI.SimulationBuildStatus.BUILT
# for stage in values(PSI.get_stages(deserialized_sim))
# @test PSI.is_stage_built(stage)
# end
@@ -228,6 +227,7 @@ end
end
+#= Re-enable when cost functions are updated
function test_3_stage_simulation_with_feedforwards(in_memory)
sys_rts_da = PSB.build_system(PSISystems, "modified_RTS_GMLC_DA_sys")
sys_rts_rt = PSB.build_system(PSISystems, "modified_RTS_GMLC_RT_sys")
@@ -293,9 +293,9 @@ function test_3_stage_simulation_with_feedforwards(in_memory)
simulation_folder = mktempdir(; cleanup = true),
)
build_out = build!(sim)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
# execute_out = execute!(sim, in_memory = in_memory)
- # @test execute_out == PSI.RunStatus.SUCCESSFUL
+ # @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
end
@testset "Test 3 stage simulation with FeedForwards" begin
@@ -304,6 +304,7 @@ end
end
end
+# TODO: Re-enable once MarketBid Cost is re-implemented
@testset "UC with MarketBid Cost in ThermalGenerators simulations" begin
template = get_thermal_dispatch_template_network(
NetworkModel(CopperPlatePowerModel; use_slacks = true),
@@ -337,7 +338,8 @@ end
simulation_folder = mktempdir(; cleanup = true),
)
- @test build!(sim) == PSI.BuildStatus.BUILT
- @test execute!(sim) == PSI.RunStatus.SUCCESSFUL
+ @test build!(sim) == PSI.SimulationBuildStatus.BUILT
+ @test execute!(sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
# TODO: Add more testing of resulting values
end
+=#
diff --git a/test/test_simulation_models.jl b/test/test_simulation_models.jl
index 70ba311008..2a900dcc8c 100644
--- a/test/test_simulation_models.jl
+++ b/test/test_simulation_models.jl
@@ -3,21 +3,21 @@
[
DecisionModel(
MockOperationProblem;
- horizon = 48,
+ horizon = Hour(48),
interval = Hour(24),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Hour(1),
steps = 2 * 24,
name = "HAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 12,
+ horizon = Hour(12),
interval = Minute(5),
steps = 2 * 24 * 12,
name = "ED",
@@ -33,21 +33,21 @@
[
DecisionModel(
MockOperationProblem;
- horizon = 48,
+ horizon = Hour(48),
interval = Hour(24),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Hour(1),
steps = 2 * 24,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 12,
+ horizon = Hour(12),
interval = Minute(5),
steps = 2 * 24 * 12,
name = "ED",
diff --git a/test/test_simulation_partitions.jl b/test/test_simulation_partitions.jl
index 61d533e7c0..ec65bd7275 100644
--- a/test/test_simulation_partitions.jl
+++ b/test/test_simulation_partitions.jl
@@ -54,7 +54,7 @@ end
initial_time = DateTime("2024-01-02T00:00:00"),
num_steps = 1,
)
- @test execute_simulation(regular_sim) == PSI.RunStatus.SUCCESSFUL
+ @test execute_simulation(regular_sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED
regular_results = SimulationResults(sim_dir, regular_name)
partitioned_results = SimulationResults(sim_dir, partition_name)
@@ -93,7 +93,7 @@ end
skip && continue
r_sum = 0
p_sum = 0
- atol = occursin("ProductionCostExpression", key) ? 11000 : 0
+ atol = occursin("ProductionCostExpression", key) ? 11000 : 1e-6
for i in 2:ncol(rdf)
r_sum += sum(rdf[!, i])
p_sum += sum(pdf[!, i])
diff --git a/test/test_simulation_results.jl b/test/test_simulation_results.jl
index 3f5e035d8e..d3ec744777 100644
--- a/test/test_simulation_results.jl
+++ b/test/test_simulation_results.jl
@@ -109,7 +109,7 @@ NATURAL_UNITS_VALUES = [
]
function compare_results(rpath, epath, model, field, name, timestamp)
- filename = string(name) * "_" * PSI.convert_for_path(timestamp) * ".csv"
+ filename = string(name) * "_" * IS.convert_for_path(timestamp) * ".csv"
rp = joinpath(rpath, model, field, filename)
ep = joinpath(epath, model, field, filename)
df1 = PSI.read_dataframe(rp)
@@ -138,7 +138,7 @@ end
function make_export_all(problems)
return [
- ProblemResultsExport(
+ OptimizationProblemResultsExport(
x;
store_all_duals = true,
store_all_variables = true,
@@ -177,7 +177,7 @@ function run_simulation(
template_uc,
c_sys5_hy_uc;
name = "UC",
- optimizer = GLPK_optimizer,
+ optimizer = HiGHS_optimizer,
system_to_file = system_to_file,
),
DecisionModel(
@@ -212,7 +212,7 @@ function run_simulation(
)
build_out = build!(sim; console_level = Logging.Error)
- @test build_out == PSI.BuildStatus.BUILT
+ @test build_out == PSI.SimulationBuildStatus.BUILT
exports = Dict(
"models" => [
@@ -235,7 +235,7 @@ function run_simulation(
"optimizer_stats" => true,
)
execute_out = execute!(sim; exports = exports, in_memory = in_memory)
- @test execute_out == PSI.RunStatus.SUCCESSFUL
+ @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED
return sim
end
@@ -259,6 +259,13 @@ function test_simulation_results(
)
results = SimulationResults(sim)
test_decision_problem_results(results, c_sys5_hy_ed, c_sys5_hy_uc, in_memory)
+ if !in_memory
+ test_decision_problem_results_kwargs_handling(
+ dirname(results.path),
+ c_sys5_hy_ed,
+ c_sys5_hy_uc,
+ )
+ end
test_emulation_problem_results(results, in_memory)
results_ed = get_decision_problem_results(results, "ED")
@@ -269,12 +276,12 @@ function test_simulation_results(
@test isempty(results)
verify_export_results(results, export_path)
- @test length(readdir(export_realized_results(results_ed))) === 17
+ @test length(readdir(export_realized_results(results_ed))) === 18
# Test that you can't read a failed simulation.
- PSI.set_simulation_status!(sim, RunStatus.FAILED)
+ PSI.set_simulation_status!(sim, PSI.RunStatus.FAILED)
PSI.serialize_status(sim)
- @test PSI.deserialize_status(sim) == RunStatus.FAILED
+ @test PSI.deserialize_status(sim) == PSI.RunStatus.FAILED
@test_throws ErrorException SimulationResults(sim)
@test_logs(
match_mode = :any,
@@ -662,6 +669,40 @@ function test_decision_problem_results_values(
empty!(myres)
@test isempty(PSI.get_cached_variables(myres))
end
+
+ @testset "Test read_results_with_keys" begin
+ myres = deepcopy(results_ed)
+ initial_time = DateTime("2024-01-01T00:00:00")
+ timestamps = PSI._process_timestamps(myres, initial_time, 3)
+ result_keys = [PSI.VariableKey(ActivePowerVariable, ThermalStandard)]
+
+ res1 = PSI.read_results_with_keys(myres, result_keys)
+ @test Set(keys(res1)) == Set(result_keys)
+ res1_df = res1[first(result_keys)]
+ @test size(res1_df) == (576, 6)
+ @test names(res1_df) ==
+ ["DateTime", "Solitude", "Park City", "Alta", "Brighton", "Sundance"]
+ @test first(eltype.(eachcol(res1_df))) === DateTime
+
+ res2 =
+ PSI.read_results_with_keys(myres, result_keys; cols = ["Park City", "Brighton"])
+ @test Set(keys(res2)) == Set(result_keys)
+ res2_df = res2[first(result_keys)]
+ @test size(res2_df) == (576, 3)
+ @test names(res2_df) ==
+ ["DateTime", "Park City", "Brighton"]
+ @test first(eltype.(eachcol(res2_df))) === DateTime
+
+ res3_df =
+ PSI.read_results_with_keys(myres, result_keys; start_time = timestamps[2])[first(
+ result_keys,
+ )]
+ @test res3_df[1, "DateTime"] == timestamps[2]
+
+ res4_df =
+ PSI.read_results_with_keys(myres, result_keys; len = 2)[first(result_keys)]
+ @test size(res4_df) == (2, 6)
+ end
end
function test_decision_problem_results(
@@ -697,7 +738,7 @@ function test_emulation_problem_results(results::SimulationResults, in_memory)
end
expressions_keys = collect(keys(read_realized_expressions(results_em)))
- @test length(expressions_keys) == 3
+ @test length(expressions_keys) == 4
expressions_inputs = (
[
"ProductionCostExpression__HydroEnergyReservoir",
@@ -855,6 +896,26 @@ function test_simulation_results_from_file(path::AbstractString, c_sys5_hy_ed, c
@test get_system(results_uc) === nothing
@test length(read_realized_variables(results_uc)) == length(UC_EXPECTED_VARS)
+ @test_throws IS.InvalidValue set_system!(results_uc, c_sys5_hy_ed)
+ set_system!(results_ed, c_sys5_hy_ed)
+ set_system!(results_uc, c_sys5_hy_uc)
+
+ test_decision_problem_results_values(results_ed, results_uc, c_sys5_hy_ed, c_sys5_hy_uc)
+end
+
+function test_decision_problem_results_kwargs_handling(
+ path::AbstractString,
+ c_sys5_hy_ed,
+ c_sys5_hy_uc,
+)
+ results = SimulationResults(path, "no_cache")
+ @test list_decision_problems(results) == ["ED", "UC"]
+ results_uc = get_decision_problem_results(results, "UC")
+ results_ed = get_decision_problem_results(results, "ED")
+
+ # Verify this works without system.
+ @test get_system(results_uc) === nothing
+
results_ed = get_decision_problem_results(results, "ED")
@test isnothing(get_system(results_ed))
@@ -864,21 +925,6 @@ function test_simulation_results_from_file(path::AbstractString, c_sys5_hy_ed, c
@test_throws IS.InvalidValue set_system!(results_uc, c_sys5_hy_ed)
- current_file = joinpath(
- results_uc.execution_path,
- "problems",
- results_uc.problem,
- PSI.make_system_filename(results_uc.system_uuid),
- )
- mv(current_file, "system-temporary-file-name.json"; force = true)
-
- @test_throws ErrorException get_decision_problem_results(
- results,
- "UC";
- populate_system = true,
- )
- mv("system-temporary-file-name.json", current_file)
-
set_system!(results_ed, c_sys5_hy_ed)
set_system!(results_uc, c_sys5_hy_uc)
diff --git a/test/test_simulation_results_export.jl b/test/test_simulation_results_export.jl
index c7c47c54c2..8ebb725584 100644
--- a/test/test_simulation_results_export.jl
+++ b/test/test_simulation_results_export.jl
@@ -6,7 +6,7 @@ import PowerSimulations:
should_export_dual,
should_export_parameter,
should_export_variable,
- OptimizationContainerMetadata
+ IS.Optimization.OptimizationContainerMetadata
function _make_params()
sim = Dict(
@@ -17,7 +17,7 @@ function _make_params()
problem_defs = OrderedDict(
:ED => Dict(
"execution_count" => 24,
- "horizon" => 12,
+ "horizon" => Dates.Hour(12),
"interval" => Dates.Hour(1),
"resolution" => Dates.Hour(1),
"base_power" => 100.0,
@@ -25,14 +25,14 @@ function _make_params()
),
:UC => Dict(
"execution_count" => 1,
- "horizon" => 24,
+ "horizon" => Dates.Hour(24),
"interval" => Dates.Hour(1),
"resolution" => Dates.Hour(24),
"base_power" => 100.0,
"system_uuid" => Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"),
),
)
- container_metadata = OptimizationContainerMetadata(
+ container_metadata = IS.Optimization.OptimizationContainerMetadata(
Dict(
"ActivePowerVariable__ThermalStandard" =>
PSI.VariableKey(ActivePowerVariable, ThermalStandard),
@@ -46,9 +46,9 @@ function _make_params()
for problem in keys(problem_defs)
problem_params = ModelStoreParams(
problem_defs[problem]["execution_count"],
- problem_defs[problem]["horizon"],
- problem_defs[problem]["interval"],
- problem_defs[problem]["resolution"],
+ IS.time_period_conversion(problem_defs[problem]["horizon"]),
+ IS.time_period_conversion(problem_defs[problem]["interval"]),
+ IS.time_period_conversion(problem_defs[problem]["resolution"]),
problem_defs[problem]["base_power"],
problem_defs[problem]["system_uuid"],
container_metadata,
@@ -104,7 +104,7 @@ end
exports,
valid,
:ED,
- PSI.VariableKey(ActivePowerVariable, RenewableFix),
+ PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch),
)
@test should_export_parameter(
exports,
@@ -116,7 +116,7 @@ end
exports,
valid,
:ED,
- PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableFix),
+ PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch),
)
@test should_export_variable(
@@ -129,7 +129,7 @@ end
exports,
valid,
:UC,
- PSI.VariableKey(ActivePowerVariable, RenewableFix),
+ PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch),
)
@test should_export_parameter(
exports,
@@ -141,7 +141,7 @@ end
exports,
valid,
:UC,
- PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableFix),
+ PSI.ConstraintKey(ActivePowerVariableLimitsConstraint, RenewableNonDispatch),
)
@test exports.path == "export_path"
diff --git a/test/test_simulation_sequence.jl b/test/test_simulation_sequence.jl
index 6639e1fe55..e93cc491ef 100644
--- a/test/test_simulation_sequence.jl
+++ b/test/test_simulation_sequence.jl
@@ -2,21 +2,24 @@
models_array = [
DecisionModel(
MockOperationProblem;
- horizon = 48,
+ horizon = Hour(48),
interval = Hour(24),
+ resolution = Hour(1),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
+ resolution = Minute(5),
interval = Hour(1),
steps = 2 * 24,
name = "HAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 12,
+ horizon = Hour(12),
+ resolution = Minute(5),
interval = Minute(5),
steps = 2 * 24 * 12,
name = "ED",
@@ -29,7 +32,12 @@
)
models = SimulationModels(
models_array,
- EmulationModel(MockEmulationProblem; resolution = Minute(1), name = "AGC"),
+ EmulationModel(
+ MockEmulationProblem;
+ interval = Minute(1),
+ resolution = Minute(1),
+ name = "AGC",
+ ),
)
test_sequence = SimulationSequence(;
@@ -56,14 +64,14 @@
@test length(findall(x -> x == 1, test_sequence.execution_order)) == 1
for model in PSI.get_decision_models(models)
- @test model.internal.simulation_info.sequence_uuid == test_sequence.uuid
+ @test PSI.get_sequence_uuid(model) == test_sequence.uuid
end
# Test single stage sequence
test_sequence = SimulationSequence(;
models = SimulationModels(
# TODO: support passing one model without making a vector
- [DecisionModel(MockOperationProblem; horizon = 48, name = "DAUC")]),
+ [DecisionModel(MockOperationProblem; horizon = Hour(48), name = "DAUC")]),
ini_cond_chronology = InterProblemChronology(),
)
@@ -76,15 +84,17 @@ end
models = SimulationModels([
DecisionModel(
MockOperationProblem;
- horizon = 48,
+ horizon = Hour(48),
interval = Hour(24),
+ resolution = Hour(1),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Hour(5),
+ resolution = Minute(5),
steps = 2 * 24,
name = "HAUC",
),
@@ -95,15 +105,17 @@ end
models = SimulationModels([
DecisionModel(
MockOperationProblem;
- horizon = 2,
+ horizon = Hour(2),
interval = Hour(1),
+ resolution = Hour(1),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Hour(1),
+ resolution = Minute(5),
steps = 2 * 24,
name = "HAUC",
),
@@ -114,15 +126,17 @@ end
models = SimulationModels([
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Hour(1),
+ resolution = Hour(1),
steps = 2,
name = "DAUC",
),
DecisionModel(
MockOperationProblem;
- horizon = 24,
+ horizon = Hour(24),
interval = Minute(22),
+ resolution = Hour(1),
steps = 2 * 24,
name = "HAUC",
),
diff --git a/test/test_simulation_store.jl b/test/test_simulation_store.jl
index bb7a27f4c1..2c3097e230 100644
--- a/test/test_simulation_store.jl
+++ b/test/test_simulation_store.jl
@@ -25,21 +25,24 @@ function _initialize!(store, sim, variables, model_defs, cache_rules)
execution_count = model_defs[model]["execution_count"]
horizon = model_defs[model]["horizon"]
num_rows = execution_count * sim["num_steps"]
+ resolution = model_defs[model]["resolution"]
+ interval = model_defs[model]["interval"]
model_params = ModelStoreParams(
execution_count,
- horizon,
- model_defs[model]["interval"],
- model_defs[model]["resolution"],
+ IS.time_period_conversion(horizon),
+ IS.time_period_conversion(interval),
+ IS.time_period_conversion(resolution),
model_defs[model]["base_power"],
model_defs[model]["system_uuid"],
)
reqs = SimulationModelStoreRequirements()
-
+ horizon_count = horizon ÷ resolution
for (key, array) in model_defs[model]["variables"]
reqs.variables[key] = Dict(
"columns" => model_defs[model]["names"],
- "dims" => (horizon, length(model_defs[model]["names"][1]), num_rows),
+ "dims" =>
+ (horizon_count, length(model_defs[model]["names"][1]), num_rows),
)
keep_in_cache = variables[key]["keep_in_cache"]
add_rule!(cache_rules, model, key, keep_in_cache)
@@ -59,9 +62,9 @@ function _initialize!(store, sim, variables, model_defs, cache_rules)
OrderedDict(
:Emulator => ModelStoreParams(
100, # Num Executions
- 1,
- Minute(5), # Interval
- Minute(5), # Resolution
+ IS.time_period_conversion(Hour(1)),
+ IS.time_period_conversion(Minute(5)), # Interval
+ IS.time_period_conversion(Minute(5)), # Resolution
100.0,
Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"),
),
@@ -165,13 +168,13 @@ end
Dict("keep_in_cache" => true),
PSI.VariableKey(ActivePowerVariable, InterruptiblePowerLoad) =>
Dict("keep_in_cache" => false),
- PSI.VariableKey(ActivePowerVariable, RenewableFix) =>
+ PSI.VariableKey(ActivePowerVariable, RenewableNonDispatch) =>
Dict("keep_in_cache" => false),
)
model_defs = OrderedDict(
:ED => Dict(
"execution_count" => 24,
- "horizon" => 12,
+ "horizon" => Hour(12),
"names" => ([:dev1, :dev2, :dev3, :dev4, :dev5],),
"variables" => Dict(x => ones(12, 5) for x in keys(variables)),
"interval" => Dates.Hour(1),
@@ -181,11 +184,11 @@ end
),
:UC => Dict(
"execution_count" => 1,
- "horizon" => 24,
+ "horizon" => Hour(24),
"names" => ([:dev1, :dev2, :dev3],),
"variables" => Dict(x => ones(24, 3) for x in keys(variables)),
"interval" => Dates.Hour(1),
- "resolution" => Dates.Hour(24),
+ "resolution" => Dates.Hour(1),
"base_power" => 100.0,
"system_uuid" => Base.UUID("4076af6c-e467-56ae-b986-b466b2749572"),
),
diff --git a/test/test_utils.jl b/test/test_utils.jl
index 7e39118246..9adae1d4b0 100644
--- a/test/test_utils.jl
+++ b/test/test_utils.jl
@@ -13,7 +13,7 @@ end
fill!(one, 1.0)
mock_key = PSI.VariableKey(ActivePowerVariable, ThermalStandard)
one_df = PSI.to_dataframe(one, mock_key)
- test_df = DataFrames.DataFrame(PSI.encode_key(mock_key) => [1.0, 1.0])
+ test_df = DataFrames.DataFrame(IS.Optimization.encode_key(mock_key) => [1.0, 1.0])
@test one_df == test_df
two = PSI.DenseAxisArray{Float64}(undef, [:a], 1:2)
diff --git a/test/test_utils/mock_operation_models.jl b/test/test_utils/mock_operation_models.jl
index a681a8ff60..b66d5be01c 100644
--- a/test/test_utils/mock_operation_models.jl
+++ b/test/test_utils/mock_operation_models.jl
@@ -12,6 +12,12 @@ function PSI.DecisionModel(
kwargs...,
) where {T <: PM.AbstractPowerModel}
settings = PSI.Settings(sys; kwargs...)
+ available_resolutions = PSY.get_time_series_resolutions(sys)
+ if length(available_resolutions) == 1
+ PSI.set_resolution!(settings, first(available_resolutions))
+ else
+ error("System has multiple resolutions MockOperationProblem won't work")
+ end
return DecisionModel{MockOperationProblem}(
ProblemTemplate(T),
sys,
@@ -21,12 +27,18 @@ function PSI.DecisionModel(
)
end
-function make_mock_forecast(horizon, resolution, interval, steps)
+function make_mock_forecast(
+ horizon::Dates.TimePeriod,
+ resolution::Dates.TimePeriod,
+ interval::Dates.TimePeriod,
+ steps,
+)
init_time = DateTime("2024-01-01")
timeseries_data = Dict{Dates.DateTime, Vector{Float64}}()
+ horizon_count = horizon ÷ resolution
for i in 1:steps
forecast_timestamps = init_time + interval * i
- timeseries_data[forecast_timestamps] = rand(horizon)
+ timeseries_data[forecast_timestamps] = rand(horizon_count)
end
return Deterministic(;
name = "mock_forecast",
@@ -37,8 +49,9 @@ end
function make_mock_singletimeseries(horizon, resolution)
init_time = DateTime("2024-01-01")
- tstamps = collect(range(init_time; length = horizon, step = resolution))
- timeseries_data = TimeArray(tstamps, rand(horizon))
+ horizon_count = horizon ÷ resolution
+ tstamps = collect(range(init_time; length = horizon_count, step = resolution))
+ timeseries_data = TimeArray(tstamps, rand(horizon_count))
return SingleTimeSeries(; name = "mock_timeseries", data = timeseries_data)
end
@@ -52,14 +65,15 @@ function PSI.DecisionModel(::Type{MockOperationProblem}; name = nothing, kwargs.
add_component!(sys, l)
add_component!(sys, gen)
forecast = make_mock_forecast(
- get(kwargs, :horizon, 24),
+ get(kwargs, :horizon, Hour(24)),
get(kwargs, :resolution, Hour(1)),
get(kwargs, :interval, Hour(1)),
get(kwargs, :steps, 2),
)
add_time_series!(sys, l, forecast)
-
- settings = PSI.Settings(sys; horizon = get(kwargs, :horizon, 24))
+ settings = PSI.Settings(sys;
+ horizon = get(kwargs, :horizon, Hour(24)),
+ resolution = get(kwargs, :resolution, Hour(1)))
return DecisionModel{MockOperationProblem}(
ProblemTemplate(CopperPlatePowerModel),
sys,
@@ -79,12 +93,14 @@ function PSI.EmulationModel(::Type{MockEmulationProblem}; name = nothing, kwargs
add_component!(sys, l)
add_component!(sys, gen)
single_ts = make_mock_singletimeseries(
- get(kwargs, :horizon, 24),
+ get(kwargs, :horizon, Hour(24)),
get(kwargs, :resolution, Hour(1)),
)
add_time_series!(sys, l, single_ts)
- settings = PSI.Settings(sys; horizon = get(kwargs, :horizon, 24))
+ settings = PSI.Settings(sys;
+ horizon = get(kwargs, :resolution, Hour(1)),
+ resolution = get(kwargs, :resolution, Hour(1)))
return EmulationModel{MockEmulationProblem}(
ProblemTemplate(CopperPlatePowerModel),
sys,
@@ -103,6 +119,7 @@ function mock_construct_device!(
set_device_model!(problem.template, model)
template = PSI.get_template(problem)
PSI.finalize_template!(template, PSI.get_system(problem))
+ PSI.validate_time_series!(problem)
PSI.init_optimization_container!(
PSI.get_optimization_container(problem),
PSI.get_network_model(template),
@@ -114,7 +131,7 @@ function mock_construct_device!(
built_for_recurrent_solves
PSI.initialize_system_expressions!(
PSI.get_optimization_container(problem),
- PSI.get_network_formulation(template),
+ PSI.get_network_model(template),
PSI.get_network_model(template).subnetworks,
PSI.get_system(problem),
Dict{Int64, Set{Int64}}(),
diff --git a/test/test_utils/model_checks.jl b/test/test_utils/model_checks.jl
index fc4bda9d82..28f26fd955 100644
--- a/test/test_utils/model_checks.jl
+++ b/test/test_utils/model_checks.jl
@@ -69,7 +69,11 @@ function psi_checkobjfun_test(model::DecisionModel, exp_type)
return
end
-function moi_lbvalue_test(model::DecisionModel, con_key::PSI.ConstraintKey, value::Number)
+function moi_lbvalue_test(
+ model::DecisionModel,
+ con_key::PSI.ConstraintKey,
+ value::Number,
+)
for con in PSI.get_constraints(model)[con_key]
@test JuMP.constraint_object(con).set.lower == value
end
@@ -90,8 +94,9 @@ function psi_checksolve_test(model::DecisionModel, status, expected_result, tol
@test isapprox(obj_value, expected_result, atol = tol)
end
-function psi_ptdf_lmps(res::ProblemResults, ptdf)
- cp_duals = read_dual(res, PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System))
+function psi_ptdf_lmps(res::OptimizationProblemResults, ptdf)
+ cp_duals =
+ read_dual(res, PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System))
λ = Matrix{Float64}(cp_duals[:, propertynames(cp_duals) .!= :DateTime])
flow_duals = read_dual(res, PSI.ConstraintKey(NetworkFlowConstraint, PSY.Line))
@@ -304,9 +309,11 @@ function check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PS
T,
)
for ic in ic_data
+ d = ic.component
name = PSY.get_name(ic.component)
e_value = PSI.jump_value(PSI.get_value(ic))
- @test PSY.get_initial_energy(ic.component) == e_value
+ @test PSY.get_initial_storage_capacity_level(d) * PSY.get_storage_capacity(d) *
+ PSY.get_conversion_factor(d) == e_value
end
end
@@ -395,7 +402,7 @@ function check_initialization_variable_count(
no_component = length(PSY.get_components(PSY.get_available, T, model.sys))
variable = PSI.get_initial_condition_value(initial_conditions_data, S(), T)
rows, cols = size(variable)
- @test rows * cols == no_component * PSI.INITIALIZATION_PROBLEM_HORIZON
+ @test rows * cols == no_component * PSI.INITIALIZATION_PROBLEM_HORIZON_COUNT
end
function check_variable_count(
@@ -414,9 +421,10 @@ function check_initialization_constraint_count(
::S,
::Type{T};
filter_func = PSY.get_available,
- meta = PSI.CONTAINER_KEY_EMPTY_META,
+ meta = PSI.IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {S <: PSI.ConstraintType, T <: PSY.Component}
- container = model.internal.ic_model_container
+ container =
+ IS.Optimization.get_initial_conditions_model_container(PSI.get_internal(model))
no_component = length(PSY.get_components(filter_func, T, model.sys))
time_steps = PSI.get_time_steps(container)[end]
constraint = PSI.get_constraint(container, S(), T, meta)
@@ -428,7 +436,7 @@ function check_constraint_count(
::S,
::Type{T};
filter_func = PSY.get_available,
- meta = PSI.CONTAINER_KEY_EMPTY_META,
+ meta = PSI.IS.Optimization.CONTAINER_KEY_EMPTY_META,
) where {S <: PSI.ConstraintType, T <: PSY.Component}
no_component = length(PSY.get_components(filter_func, T, model.sys))
time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end]
diff --git a/test/test_utils/operations_problem_templates.jl b/test/test_utils/operations_problem_templates.jl
index 3e0a400f29..616d2ee556 100644
--- a/test/test_utils/operations_problem_templates.jl
+++ b/test/test_utils/operations_problem_templates.jl
@@ -1,3 +1,24 @@
+const NETWORKS_FOR_TESTING = [
+ (PM.ACPPowerModel, fast_ipopt_optimizer),
+ (PM.ACRPowerModel, fast_ipopt_optimizer),
+ (PM.ACTPowerModel, fast_ipopt_optimizer),
+ #(PM.IVRPowerModel, fast_ipopt_optimizer), #instantiate_ivp_expr_model not implemented
+ (PM.DCPPowerModel, fast_ipopt_optimizer),
+ (PM.DCMPPowerModel, fast_ipopt_optimizer),
+ (PM.NFAPowerModel, fast_ipopt_optimizer),
+ (PM.DCPLLPowerModel, fast_ipopt_optimizer),
+ (PM.LPACCPowerModel, fast_ipopt_optimizer),
+ (PM.SOCWRPowerModel, fast_ipopt_optimizer),
+ (PM.SOCWRConicPowerModel, scs_solver),
+ (PM.QCRMPowerModel, fast_ipopt_optimizer),
+ (PM.QCLSPowerModel, fast_ipopt_optimizer),
+ #(PM.SOCBFPowerModel, fast_ipopt_optimizer), # not implemented
+ (PM.BFAPowerModel, fast_ipopt_optimizer),
+ #(PM.SOCBFConicPowerModel, fast_ipopt_optimizer), # not implemented
+ (PM.SDPWRMPowerModel, scs_solver),
+ (PM.SparseSDPWRMPowerModel, scs_solver),
+]
+
function get_thermal_standard_uc_template()
template = ProblemTemplate(CopperPlatePowerModel)
set_device_model!(template, PowerLoad, StaticPowerLoad)
@@ -68,8 +89,8 @@ function get_template_dispatch_with_network(network = PTDFPowerModel)
set_device_model!(template, PowerLoad, StaticPowerLoad)
set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
set_device_model!(template, Line, StaticBranch)
- set_device_model!(template, Transformer2W, StaticBranch)
- set_device_model!(template, TapTransformer, StaticBranch)
+ set_device_model!(template, Transformer2W, StaticBranchBounds)
+ set_device_model!(template, TapTransformer, StaticBranchBounds)
set_device_model!(template, TwoTerminalHVDCLine, HVDCTwoTerminalLossless)
return template
end
diff --git a/test/test_utils/solver_definitions.jl b/test/test_utils/solver_definitions.jl
index 64f97f3a36..a7ef5acdb7 100644
--- a/test/test_utils/solver_definitions.jl
+++ b/test/test_utils/solver_definitions.jl
@@ -18,7 +18,7 @@ GLPK_optimizer =
scs_solver = JuMP.optimizer_with_attributes(
SCS.Optimizer,
"max_iters" => 100000,
- "eps" => 1e-4,
+ "eps_infeas" => 1e-4,
"verbose" => 0,
)