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, )